Move EventTileBubble to shared components (#31911)

* Move EventTileBubble to shared components as is

* Added documentation and updated stories and unit tests

* Move 'global' element web css to _common.pcss

* Adding playwright snapshots

* Updated comments

* Added legacy mx_MessageTimestamp class and updated snapshots

* Regenerate snapshots with correct hash

* Changes to css and removed timestamp from properties after review.

* Update screenshot for room-list and fix flaky CI playwright test.

* Blur the play button before matching screenshots

* Changed to button focused instead of blur for consistancy

* Stabilize play button appearance in CI (disabled due to decoding)

* Force play button appearance in CI (disabled due to decoding)

* Add comments on playwright test changes.
Change from React.RefObject<any> to Ref<HTMLDivElement> in EncryptionEvent.tsx

* Update playwright/e2e/composer/CIDER.spec.ts

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>

* Update playwright/e2e/composer/CIDER.spec.ts

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>

* Update playwright/e2e/crypto/toasts.spec.ts

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
rbondesson 2026-02-03 15:37:57 +01:00 committed by GitHub
parent eb909f1090
commit a1be203683
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
76 changed files with 463 additions and 180 deletions

View File

@ -56,7 +56,6 @@ module.exports = {
{ from: "res/css/views/rooms/_EditMessageComposer.pcss", type: "css" },
{ from: "res/css/views/right_panel/_BaseCard.pcss", type: "css" },
{ from: "res/css/views/messages/_MessageTimestamp.pcss", type: "css" },
{ from: "res/css/views/messages/_EventTileBubble.pcss", type: "css" },
{ from: "res/css/views/messages/_MessageActionBar.pcss", type: "css" },
{ from: "res/css/views/voip/LegacyCallView/_LegacyCallViewButtons.pcss", type: "css" },
{ from: "res/css/views/elements/_ToggleSwitch.pcss", type: "css" },

View File

@ -0,0 +1,51 @@
/*
* Copyright 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
.container {
background-color: var(--cpd-color-bg-subtle-secondary);
padding: var(--cpd-space-3x);
border-radius: 8px;
/* Reserve space for external timestamps, but also cap the width */
/* Legacy variable: --MessageTimestamp-width: 46px; /* 8 + 30 (avatar) + 8 */
/* max-width: min(calc(100% - 2 * var(--MessageTimestamp-width)), 600px); */
max-width: min(calc(100% - 2 * 46px), 600px);
box-sizing: border-box;
display: grid;
grid-template-columns: 24px minmax(0, 1fr) min-content min-content;
svg {
position: relative;
grid-column: 1;
grid-row: 1 / 3;
width: 16px;
height: 16px;
content: "";
inset: 0;
mask-repeat: no-repeat;
mask-position: center;
mask-size: contain;
margin-top: var(--cpd-space-1x);
}
.title,
.subtitle {
grid-column: 2;
overflow-wrap: break-word;
min-inline-size: 50px;
}
.title {
font-weight: var(--cpd-font-weight-semibold);
font-size: var(--cpd-font-size-body-md);
grid-row: 1;
}
.subtitle {
font-size: var(--cpd-font-size-body-sm);
grid-row: 2;
}
}

View File

@ -0,0 +1,49 @@
/*
* Copyright 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { LockSolidIcon, ErrorSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import type { Meta, StoryFn } from "@storybook/react-vite";
import { EventTileBubble } from "./EventTileBubble";
export default {
title: "Event/EventTileBubble",
component: EventTileBubble,
tags: ["autodocs"],
args: {
icon: <ErrorSolidIcon />,
title: "Title goes here",
subtitle: "Subtitle goes here",
className: "custom-class",
},
} as Meta<typeof EventTileBubble>;
const Template: StoryFn<typeof EventTileBubble> = (args) => <EventTileBubble {...args} />;
export const Default = Template.bind({});
export const HasLockSolidIcon = Template.bind({});
HasLockSolidIcon.args = {
className: undefined,
icon: <LockSolidIcon />,
children: undefined,
};
export const HasChildren = Template.bind({});
HasChildren.args = {
className: undefined,
children: <div>children</div>,
};
export const IsCryptoEventBubble = Template.bind({});
IsCryptoEventBubble.args = {
className: undefined,
icon: <LockSolidIcon />,
title: "Encryption enabled",
subtitle: "Messages here are end-to-end encrypted. Verify XYZ in their profile - tap on their profile picture.",
};

View File

@ -0,0 +1,37 @@
/*
* Copyright 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
import { render } from "@test-utils";
import { composeStories } from "@storybook/react-vite";
import { describe, it, expect } from "vitest";
import React from "react";
import * as stories from "./EventTileBubble.stories.tsx";
const { Default, HasLockSolidIcon, HasChildren, IsCryptoEventBubble } = composeStories(stories);
describe("EventTileBubble", () => {
it("renders the event tile bubble", () => {
const { container } = render(<Default />);
expect(container).toMatchSnapshot();
});
it("renders the event tile bubble with icon", () => {
const { container } = render(<HasLockSolidIcon />);
expect(container).toMatchSnapshot();
});
it("renders the event tile bubble with children", () => {
const { container } = render(<HasChildren />);
expect(container).toMatchSnapshot();
});
it("renders the event tile bubble as crypto event bubble", () => {
const { container } = render(<IsCryptoEventBubble />);
expect(container).toMatchSnapshot();
});
});

View File

@ -0,0 +1,64 @@
/*
* Copyright 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
import React, { type JSX, type ReactNode } from "react";
import classNames from "classnames";
import styles from "./EventTileBubble.module.css";
export interface EventTileBubbleProps {
/**
* Icon rendered at the start of the bubble.
*/
icon: JSX.Element;
/**
* Main title text for the bubble.
*/
title: string;
/**
* Optional subtitle rendered beneath the title.
*/
subtitle?: ReactNode;
/**
* Optional extra class name for the container.
*/
className?: string;
/**
* Optional children rendered between subtitle and timestamp.
*/
children?: JSX.Element;
/**
* Forwarded ref for the container element.
*/
ref?: React.RefObject<HTMLDivElement>;
}
/**
* EventTileBubble renders a compact event tile with an icon, title, and optional subtitle/content.
*
* @example
* ```tsx
* <EventTileBubble icon={<Icon />} title="Room created" />
* ```
*/
export function EventTileBubble({
icon,
title,
subtitle,
className,
children,
ref,
}: EventTileBubbleProps): JSX.Element {
return (
<div className={classNames(styles.container, className)} ref={ref}>
{icon}
<div className={styles.title}>{title}</div>
{subtitle && <div className={styles.subtitle}>{subtitle}</div>}
{children}
</div>
);
}

View File

@ -0,0 +1,124 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`EventTileBubble > renders the event tile bubble 1`] = `
<div>
<div
class="container custom-class"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
/>
</svg>
<div
class="title"
>
Title goes here
</div>
<div
class="subtitle"
>
Subtitle goes here
</div>
</div>
</div>
`;
exports[`EventTileBubble > renders the event tile bubble as crypto event bubble 1`] = `
<div>
<div
class="container"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6 22q-.824 0-1.412-.587A1.93 1.93 0 0 1 4 20V10q0-.825.588-1.412A1.93 1.93 0 0 1 6 8h1V6q0-2.075 1.463-3.537Q9.926 1 12 1q2.075 0 3.537 1.463Q17 3.925 17 6v2h1q.824 0 1.413.588Q20 9.175 20 10v10q0 .824-.587 1.413A1.93 1.93 0 0 1 18 22zM9 8h6V6q0-1.25-.875-2.125A2.9 2.9 0 0 0 12 3q-1.25 0-2.125.875A2.9 2.9 0 0 0 9 6z"
/>
</svg>
<div
class="title"
>
Encryption enabled
</div>
<div
class="subtitle"
>
Messages here are end-to-end encrypted. Verify XYZ in their profile - tap on their profile picture.
</div>
</div>
</div>
`;
exports[`EventTileBubble > renders the event tile bubble with children 1`] = `
<div>
<div
class="container"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
/>
</svg>
<div
class="title"
>
Title goes here
</div>
<div
class="subtitle"
>
Subtitle goes here
</div>
<div>
children
</div>
</div>
</div>
`;
exports[`EventTileBubble > renders the event tile bubble with icon 1`] = `
<div>
<div
class="container"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6 22q-.824 0-1.412-.587A1.93 1.93 0 0 1 4 20V10q0-.825.588-1.412A1.93 1.93 0 0 1 6 8h1V6q0-2.075 1.463-3.537Q9.926 1 12 1q2.075 0 3.537 1.463Q17 3.925 17 6v2h1q.824 0 1.413.588Q20 9.175 20 10v10q0 .824-.587 1.413A1.93 1.93 0 0 1 18 22zM9 8h6V6q0-1.25-.875-2.125A2.9 2.9 0 0 0 12 3q-1.25 0-2.125.875A2.9 2.9 0 0 0 9 6z"
/>
</svg>
<div
class="title"
>
Title goes here
</div>
<div
class="subtitle"
>
Subtitle goes here
</div>
</div>
</div>
`;

View File

@ -0,0 +1,8 @@
/*
* Copyright 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
export { EventTileBubble, type EventTileBubbleProps } from "./EventTileBubble";

View File

@ -13,6 +13,7 @@ export * from "./audio/SeekBar";
export * from "./avatar/AvatarWithDetails";
export * from "./composer/Banner";
export * from "./crypto/SasEmoji";
export * from "./event-tiles/EventTileBubble";
export * from "./event-tiles/TextualEventView";
export * from "./message-body/MediaBody";
export * from "./message-body/DecryptionFailureBodyView";

View File

@ -23,6 +23,7 @@ const clickButtonReply = async (tile: Locator) => {
};
test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
test.slow();
test.use({
displayName: "Hanako",
});
@ -100,35 +101,39 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
.mx_MessageActionBar {
display: none !important;
}
/* Stabilize play button appearance in CI (disabled due to decoding) */
button[aria-label="Play"] {
opacity: 1 !important;
}
button[aria-label="Play"] svg,
button[aria-label="Play"] path {
fill: magenta !important;
stroke: magenta !important;
}
`,
mask: [page.getByTestId("audio-player-seek")],
clip: undefined,
};
// Take a snapshot of mx_EventTile_last on IRC layout
await expect(page.locator(".mx_EventTile_last")).toMatchScreenshot(
`${detail.replaceAll(" ", "-")}-irc-layout.png`,
screenshotOptions,
);
screenshotOptions.clip = await page.locator(".mx_EventTile_last").boundingBox();
await expect(page).toMatchScreenshot(`${detail.replaceAll(" ", "-")}-irc-layout.png`, screenshotOptions);
// Take a snapshot on modern/group layout
await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Group);
const groupTile = page.locator(".mx_EventTile_last[data-layout='group']");
await groupTile.locator(".mx_MessageTimestamp").click();
await checkPlayerVisibility(groupTile);
await expect(page.locator(".mx_EventTile_last")).toMatchScreenshot(
`${detail.replaceAll(" ", "-")}-group-layout.png`,
screenshotOptions,
);
screenshotOptions.clip = await page.locator(".mx_EventTile_last").boundingBox();
await expect(page).toMatchScreenshot(`${detail.replaceAll(" ", "-")}-group-layout.png`, screenshotOptions);
// Take a snapshot on bubble layout
await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Bubble);
const bubbleTile = page.locator(".mx_EventTile_last[data-layout='bubble']");
await bubbleTile.locator(".mx_MessageTimestamp").click();
await checkPlayerVisibility(bubbleTile);
await expect(page.locator(".mx_EventTile_last")).toMatchScreenshot(
`${detail.replaceAll(" ", "-")}-bubble-layout.png`,
screenshotOptions,
);
screenshotOptions.clip = await page.locator(".mx_EventTile_last").boundingBox();
await expect(page).toMatchScreenshot(`${detail.replaceAll(" ", "-")}-bubble-layout.png`, screenshotOptions);
};
test.beforeEach(async ({ page, app, user }) => {

View File

@ -80,7 +80,15 @@ test.describe("Composer", () => {
test.use({ viewport: { width: 1280, height: 720 } });
test("render emoji picker", { tag: "@screenshot" }, async ({ page, app }) => {
await app.getComposer(false).getByRole("button", { name: "Emoji" }).click();
await expect(page.getByTestId("mx_EmojiPicker")).toMatchScreenshot("emoji-picker.png");
// Mask the background of the screenshot to avoid failing the test just because some
// other component have changed its rendering.
await expect(page.getByTestId("mx_EmojiPicker")).toMatchScreenshot("emoji-picker.png", {
css: `
.mx_ContextualMenu_background {
background-color: magenta !important;
}
`,
});
});
});
@ -88,7 +96,15 @@ test.describe("Composer", () => {
test.use({ viewport: { width: 1280, height: 360 } });
test("render emoji picker", { tag: "@screenshot" }, async ({ page, app }) => {
await app.getComposer(false).getByRole("button", { name: "Emoji" }).click();
await expect(page.getByTestId("mx_EmojiPicker")).toMatchScreenshot("emoji-picker-small.png");
// Mask the background of the screenshot to avoid failing the test just because some
// other component have changed its rendering.
await expect(page.getByTestId("mx_EmojiPicker")).toMatchScreenshot("emoji-picker-small.png", {
css: `
.mx_ContextualMenu_background {
background-color: magenta !important;
}
`,
});
});
});

View File

@ -37,7 +37,15 @@ test.describe("Key storage out of sync toast", () => {
// playwright only evaluates the 'first()' call initially, not subsequent times it checks, so
// it would always be checking the same toast, even if another one is now the first.
await expect(page.getByRole("alert")).toHaveCount(2);
await expect(page.getByRole("alert").first()).toMatchScreenshot("key-storage-out-of-sync-toast.png");
// Mask the background of the screenshot to avoid failing the test just because some
// other component have changed its rendering.
await expect(page.getByRole("alert").first()).toMatchScreenshot("key-storage-out-of-sync-toast.png", {
css: `
.mx_ToastContainer {
background-color: magenta !important;
}
`,
});
await page.getByRole("button", { name: "Enter recovery key" }).click();

View File

@ -37,6 +37,8 @@ test.describe("Room list", () => {
});
test.describe("Room list", () => {
test.slow();
test.beforeEach(async ({ page, app, user }) => {
for (let i = 0; i < 30; i++) {
await app.client.createRoom({ name: `room${i}` });

View File

@ -949,6 +949,10 @@ test.describe("Timeline", () => {
await page.getByRole("textbox", { name: "Edit message" }).press("Enter");
const newTile = page.locator(".mx_EventTile");
const codeBlock = newTile.locator(".mx_EventTile_pre_container");
await expect(codeBlock).toBeVisible();
await codeBlock.hover();
await expect(newTile.locator(".mx_EventTile_copyButton")).toBeVisible();
await expect(newTile).toMatchScreenshot("edited-code-block.png", {
css: `
.mx_MessageTimestamp {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

After

Width:  |  Height:  |  Size: 166 KiB

View File

@ -22,6 +22,7 @@ Please see LICENSE files in the repository root for full details.
--buttons-dialog-gap-row: $spacing-8;
--buttons-dialog-gap-column: $spacing-8;
--MBody-border-radius: 8px;
--EventTileBubble_margin-block: 10px;
/* Expected z-indexes for dialogs:
4000 - Default wrapper index

View File

@ -221,7 +221,6 @@
@import "./views/messages/_CreateEvent.pcss";
@import "./views/messages/_DateSeparator.pcss";
@import "./views/messages/_DisambiguatedProfile.pcss";
@import "./views/messages/_EventTileBubble.pcss";
@import "./views/messages/_HiddenBody.pcss";
@import "./views/messages/_HiddenMediaPlaceholder.pcss";
@import "./views/messages/_JumpToDatePicker.pcss";

View File

@ -1,59 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
.mx_EventTileBubble {
--EventTileBubble_margin-block: 10px;
background-color: $dark-panel-bg-color;
padding: 10px;
border-radius: 8px;
/* Reserve space for external timestamps, but also cap the width */
max-width: min(calc(100% - 2 * var(--MessageTimestamp-width)), 600px);
box-sizing: border-box;
display: grid;
grid-template-columns: 24px minmax(0, 1fr) min-content min-content;
svg {
position: relative;
grid-column: 1;
grid-row: 1 / 3;
width: 16px;
height: 16px;
content: "";
inset: 0;
mask-repeat: no-repeat;
mask-position: center;
mask-size: contain;
margin-top: $spacing-4;
}
.mx_EventTileBubble_title,
.mx_EventTileBubble_subtitle {
grid-column: 2;
overflow-wrap: break-word;
min-inline-size: 50px;
}
.mx_EventTileBubble_title {
font-weight: var(--cpd-font-weight-semibold);
font-size: $font-15px;
grid-row: 1;
}
.mx_EventTileBubble_subtitle {
font-size: $font-12px;
grid-row: 2;
}
.mx_MessageTimestamp {
grid-column: 4;
grid-row: 1 / 3;
align-self: center;
margin-left: $spacing-16;
}
}

View File

@ -9,12 +9,12 @@ Please see LICENSE files in the repository root for full details.
import React, { type RefObject } from "react";
import { type MatrixEvent } from "matrix-js-sdk/src/matrix";
import { LockSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { EventTileBubble } from "@element-hq/web-shared-components";
import type ResizeNotifier from "../../utils/ResizeNotifier";
import ErrorBoundary from "../views/elements/ErrorBoundary";
import RoomHeader from "../views/rooms/RoomHeader/RoomHeader.tsx";
import ScrollPanel from "./ScrollPanel";
import EventTileBubble from "../views/messages/EventTileBubble";
import NewRoomIntro from "../views/rooms/NewRoomIntro";
import { UnwrappedEventTile } from "../views/rooms/EventTile";
import { _t } from "../../languageHandler";
@ -45,7 +45,7 @@ export const WaitingForThirdPartyRoomView: React.FC<Props> = ({ roomView, resize
<ScrollPanel className="mx_RoomView_messagePanel">
<EventTileBubble
icon={<LockSolidIcon />}
className="mx_cryptoEvent mx_cryptoEvent_icon"
className="mx_EventTileBubble mx_cryptoEvent mx_cryptoEvent_icon"
title={_t("room|waiting_for_join_title", { brand })}
subtitle={_t("room|waiting_for_join_subtitle", { brand })}
/>

View File

@ -6,13 +6,13 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import React, { type JSX, type Ref, type ReactNode } from "react";
import React, { type JSX, type ReactNode } from "react";
import { type MatrixEvent } from "matrix-js-sdk/src/matrix";
import { ErrorSolidIcon, LockSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { EventTileBubble } from "@element-hq/web-shared-components";
import type { RoomEncryptionEventContent } from "matrix-js-sdk/src/types";
import { _t } from "../../../languageHandler";
import EventTileBubble from "./EventTileBubble";
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";
import DMRoomMap from "../../../utils/DMRoomMap";
import { objectHasDiff } from "../../../utils/objects";
@ -23,7 +23,7 @@ import { useIsEncrypted } from "../../../hooks/useIsEncrypted.ts";
interface IProps {
mxEvent: MatrixEvent;
timestamp?: JSX.Element;
ref?: Ref<HTMLDivElement>;
ref?: React.RefObject<HTMLDivElement>;
}
const EncryptionEvent = ({ mxEvent, timestamp, ref }: IProps): ReactNode => {
@ -60,11 +60,12 @@ const EncryptionEvent = ({ mxEvent, timestamp, ref }: IProps): ReactNode => {
return (
<EventTileBubble
icon={<LockSolidIcon />}
className="mx_cryptoEvent mx_cryptoEvent_icon"
className="mx_EventTileBubble mx_cryptoEvent mx_cryptoEvent_icon"
title={stateEncrypted ? _t("common|state_encryption_enabled") : _t("common|encryption_enabled")}
subtitle={subtitle}
timestamp={timestamp}
/>
>
{timestamp}
</EventTileBubble>
);
}
@ -72,23 +73,25 @@ const EncryptionEvent = ({ mxEvent, timestamp, ref }: IProps): ReactNode => {
return (
<EventTileBubble
icon={<LockSolidIcon />}
className="mx_cryptoEvent mx_cryptoEvent_icon"
className="mx_EventTileBubble mx_cryptoEvent mx_cryptoEvent_icon"
title={_t("common|encryption_enabled")}
subtitle={_t("timeline|m.room.encryption|disable_attempt")}
timestamp={timestamp}
/>
>
{timestamp}
</EventTileBubble>
);
}
return (
<EventTileBubble
icon={<ErrorSolidIcon color="var(--cpd-color-icon-critical-primary)" />}
className="mx_cryptoEvent"
className="mx_EventTileBubble mx_cryptoEvent"
title={_t("timeline|m.room.encryption|disabled")}
subtitle={_t("timeline|m.room.encryption|unsupported")}
ref={ref}
timestamp={timestamp}
/>
>
{timestamp}
</EventTileBubble>
);
};

View File

@ -1,34 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2020 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import React, { type JSX, type ReactNode, type Ref } from "react";
import classNames from "classnames";
interface IProps {
className: string;
icon: JSX.Element;
title: string;
timestamp?: JSX.Element;
subtitle?: ReactNode;
children?: JSX.Element;
ref?: Ref<HTMLDivElement>;
}
const EventTileBubble = ({ className, icon, title, timestamp, subtitle, children, ref }: IProps): JSX.Element => {
return (
<div className={classNames("mx_EventTileBubble", className)} ref={ref}>
{icon}
<div className="mx_EventTileBubble_title">{title}</div>
{subtitle && <div className="mx_EventTileBubble_subtitle">{subtitle}</div>}
{children}
{timestamp}
</div>
);
};
export default EventTileBubble;

View File

@ -9,10 +9,10 @@ Please see LICENSE files in the repository root for full details.
import React, { type JSX } from "react";
import { type MatrixEvent } from "matrix-js-sdk/src/matrix";
import { VideoCallSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { EventTileBubble } from "@element-hq/web-shared-components";
import { _t } from "../../../languageHandler";
import WidgetStore from "../../../stores/WidgetStore";
import EventTileBubble from "./EventTileBubble";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
@ -43,32 +43,35 @@ export default class MJitsiWidgetEvent extends React.PureComponent<IProps> {
return (
<EventTileBubble
icon={<VideoCallSolidIcon />}
className="mx_MJitsiWidgetEvent"
className="mx_EventTileBubble mx_MJitsiWidgetEvent"
title={_t("timeline|m.widget|jitsi_ended", { senderName })}
timestamp={this.props.timestamp}
/>
>
{this.props.timestamp}
</EventTileBubble>
);
} else if (prevUrl) {
// modified
return (
<EventTileBubble
icon={<VideoCallSolidIcon />}
className="mx_MJitsiWidgetEvent"
className="mx_EventTileBubble mx_MJitsiWidgetEvent"
title={_t("timeline|m.widget|jitsi_updated", { senderName })}
subtitle={joinCopy}
timestamp={this.props.timestamp}
/>
>
{this.props.timestamp}
</EventTileBubble>
);
} else {
// assume added
return (
<EventTileBubble
icon={<VideoCallSolidIcon />}
className="mx_MJitsiWidgetEvent"
className="mx_EventTileBubble mx_MJitsiWidgetEvent"
title={_t("timeline|m.widget|jitsi_started", { senderName })}
subtitle={joinCopy}
timestamp={this.props.timestamp}
/>
>
{this.props.timestamp}
</EventTileBubble>
);
}
}

View File

@ -9,10 +9,10 @@ Please see LICENSE files in the repository root for full details.
import React, { type JSX } from "react";
import { type MatrixEvent } from "matrix-js-sdk/src/matrix";
import { LockSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { EventTileBubble } from "@element-hq/web-shared-components";
import { _t } from "../../../languageHandler";
import { getNameForEventRoom, userLabelForEventRoom } from "../../../utils/KeyVerificationStateObserver";
import EventTileBubble from "./EventTileBubble";
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";
interface Props {
@ -75,12 +75,11 @@ const MKeyVerificationRequest: React.FC<Props> = ({ mxEvent, timestamp }) => {
return (
<EventTileBubble
icon={<LockSolidIcon />}
className="mx_cryptoEvent mx_cryptoEvent_icon"
className="mx_EventTileBubble mx_cryptoEvent mx_cryptoEvent_icon"
title={title}
subtitle={subtitle}
timestamp={timestamp}
>
<></>
{timestamp}
</EventTileBubble>
);
};

View File

@ -11,13 +11,13 @@ import React, { type JSX, useCallback } from "react";
import { logger } from "matrix-js-sdk/src/logger";
import { type MatrixEvent, type Room, type RoomState } from "matrix-js-sdk/src/matrix";
import { ChatSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { EventTileBubble } from "@element-hq/web-shared-components";
import dis from "../../../dispatcher/dispatcher";
import { Action } from "../../../dispatcher/actions";
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
import { _t } from "../../../languageHandler";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import EventTileBubble from "./EventTileBubble";
import { type ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { useRoomState } from "../../../hooks/useRoomState";
import SettingsStore from "../../../settings/SettingsStore";
@ -91,26 +91,28 @@ export const RoomPredecessorTile: React.FC<IProps> = ({ mxEvent, timestamp }) =>
return (
<EventTileBubble
icon={<ChatSolidIcon />}
className="mx_CreateEvent"
className="mx_EventTileBubble mx_CreateEvent"
title={_t("timeline|m.room.create|continuation")}
timestamp={timestamp}
>
<div className="mx_EventTile_body">
<span className="mx_EventTile_tileError">
{!!guessedLink ? (
<>
{_t("timeline|m.room.create|unknown_predecessor_guess_server", {
<>
<div className="mx_EventTile_body">
<span className="mx_EventTile_tileError">
{!!guessedLink ? (
<>
{_t("timeline|m.room.create|unknown_predecessor_guess_server", {
roomId: predecessor.roomId,
})}
<a href={guessedLink}>{guessedLink}</a>
</>
) : (
_t("timeline|m.room.create|unknown_predecessor", {
roomId: predecessor.roomId,
})}
<a href={guessedLink}>{guessedLink}</a>
</>
) : (
_t("timeline|m.room.create|unknown_predecessor", {
roomId: predecessor.roomId,
})
)}
</span>
</div>
})
)}
</span>
</div>
{timestamp}
</>
</EventTileBubble>
);
}
@ -131,11 +133,12 @@ export const RoomPredecessorTile: React.FC<IProps> = ({ mxEvent, timestamp }) =>
return (
<EventTileBubble
icon={<ChatSolidIcon />}
className="mx_CreateEvent"
className="mx_EventTileBubble mx_CreateEvent"
title={_t("timeline|m.room.create|continuation")}
subtitle={link}
timestamp={timestamp}
/>
>
{timestamp}
</EventTileBubble>
);
function createLinkWithRoom(room: Room, roomId: string, eventId?: string): string {

View File

@ -117,7 +117,7 @@ export interface IEventTileOps {
unhideWidget(): void;
}
export interface IEventTileType extends React.Component {
export interface IEventTileType extends React.Component<HTMLDivElement> {
getEventTileOps?(): IEventTileOps;
getMediaHelper(): MediaEventHelper | undefined;
}

View File

@ -9,8 +9,8 @@ Please see LICENSE files in the repository root for full details.
import React from "react";
import { EventTimeline } from "matrix-js-sdk/src/matrix";
import { VisibilityOffIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { EventTileBubble } from "@element-hq/web-shared-components";
import EventTileBubble from "../messages/EventTileBubble";
import { _t } from "../../../languageHandler";
import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx";
@ -30,7 +30,7 @@ const HistoryTile: React.FC = () => {
return (
<EventTileBubble
icon={<VisibilityOffIcon />}
className="mx_HistoryTile"
className="mx_EventTileBubble mx_HistoryTile"
title={_t("timeline|historical_messages_unavailable")}
subtitle={subtitle}
/>

View File

@ -10,6 +10,7 @@ import React, { type JSX, useContext } from "react";
import { EventType, type Room, type User, type MatrixClient } from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types";
import { ErrorSolidIcon, UserAddIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { EventTileBubble } from "@element-hq/web-shared-components";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import DMRoomMap from "../../../utils/DMRoomMap";
@ -22,7 +23,6 @@ import { type ViewUserPayload } from "../../../dispatcher/payloads/ViewUserPaylo
import { Action } from "../../../dispatcher/actions";
import SpaceStore from "../../../stores/spaces/SpaceStore";
import { showSpaceInvite } from "../../../utils/space";
import EventTileBubble from "../messages/EventTileBubble";
import { RoomSettingsTab } from "../dialogs/RoomSettingsDialog";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
@ -296,7 +296,7 @@ const NewRoomIntro: React.FC = () => {
{!hasExpectedEncryptionSettings(cli, room) && (
<EventTileBubble
icon={<ErrorSolidIcon color="var(--cpd-color-icon-critical-primary)" />}
className="mx_cryptoEvent"
className="mx_EventTileBubble mx_cryptoEvent"
title={_t("room|intro|unencrypted_warning")}
subtitle={subtitle}
/>

View File

@ -21,7 +21,7 @@ import { TextualEventView } from "@element-hq/web-shared-components";
import SettingsStore from "../settings/SettingsStore";
import type LegacyCallEventGrouper from "../components/structures/LegacyCallEventGrouper";
import { type EventTileProps } from "../components/views/rooms/EventTile";
import { type IEventTileType, type EventTileProps } from "../components/views/rooms/EventTile";
import { TimelineRenderingType } from "../contexts/RoomContext";
import MessageEvent from "../components/views/messages/MessageEvent";
import LegacyCallEvent from "../components/views/messages/LegacyCallEvent";
@ -61,7 +61,7 @@ export interface EventTileTypeProps extends Pick<
| "isSeeingThroughMessageHiddenForModeration"
| "inhibitInteraction"
> {
ref?: React.RefObject<any>; // `any` because it's effectively impossible to convince TS of a reasonable type
ref?: React.RefObject<IEventTileType | null>;
maxImageHeight?: number; // pixels
overrideBodyTypes?: Record<string, React.ComponentType<IBodyProps>>;
overrideEventTypes?: Record<string, React.ComponentType<IBodyProps>>;

View File

@ -61,7 +61,7 @@ exports[`MessagePanel should handle lots of membership events quickly 1`] = `
</div>
</li>
<div
class="mx_EventTileBubble mx_HistoryTile"
class="_container_sq5fu_8 mx_EventTileBubble mx_HistoryTile"
>
<svg
fill="currentColor"
@ -75,7 +75,7 @@ exports[`MessagePanel should handle lots of membership events quickly 1`] = `
/>
</svg>
<div
class="mx_EventTileBubble_title"
class="_title_sq5fu_34"
>
You can't see earlier messages
</div>

View File

@ -152,7 +152,7 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
class="mx_NewRoomIntro"
>
<div
class="mx_EventTileBubble mx_cryptoEvent"
class="_container_sq5fu_8 mx_EventTileBubble mx_cryptoEvent"
>
<svg
color="var(--cpd-color-icon-critical-primary)"
@ -167,12 +167,12 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
/>
</svg>
<div
class="mx_EventTileBubble_title"
class="_title_sq5fu_34"
>
End-to-end encryption isn't enabled
</div>
<div
class="mx_EventTileBubble_subtitle"
class="_subtitle_sq5fu_35"
>
<span>
@ -342,7 +342,7 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
class="mx_NewRoomIntro"
>
<div
class="mx_EventTileBubble mx_cryptoEvent"
class="_container_sq5fu_8 mx_EventTileBubble mx_cryptoEvent"
>
<svg
color="var(--cpd-color-icon-critical-primary)"
@ -357,12 +357,12 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
/>
</svg>
<div
class="mx_EventTileBubble_title"
class="_title_sq5fu_34"
>
End-to-end encryption isn't enabled
</div>
<div
class="mx_EventTileBubble_subtitle"
class="_subtitle_sq5fu_35"
>
<span>
@ -717,7 +717,7 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
style="height: 400px;"
>
<div
class="mx_EventTileBubble mx_cryptoEvent mx_cryptoEvent_icon"
class="_container_sq5fu_8 mx_EventTileBubble mx_cryptoEvent mx_cryptoEvent_icon"
>
<svg
fill="currentColor"
@ -731,12 +731,12 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
/>
</svg>
<div
class="mx_EventTileBubble_title"
class="_title_sq5fu_34"
>
Encryption enabled
</div>
<div
class="mx_EventTileBubble_subtitle"
class="_subtitle_sq5fu_35"
>
Messages in this chat will be end-to-end encrypted.
</div>

View File

@ -3,7 +3,7 @@
exports[`<RoomPredecessorTile /> Renders as expected 1`] = `
<DocumentFragment>
<div
class="mx_EventTileBubble mx_CreateEvent"
class="_container_sq5fu_8 mx_EventTileBubble mx_CreateEvent"
>
<svg
fill="currentColor"
@ -17,12 +17,12 @@ exports[`<RoomPredecessorTile /> Renders as expected 1`] = `
/>
</svg>
<div
class="mx_EventTileBubble_title"
class="_title_sq5fu_34"
>
This room is a continuation of another conversation.
</div>
<div
class="mx_EventTileBubble_subtitle"
class="_subtitle_sq5fu_35"
>
<a
href="https://matrix.to/#/old_room_id/$tombstone_event_id"