From be91d19e318180f1e1578b60f30c1e183e9766fa Mon Sep 17 00:00:00 2001 From: Will Hunt <2072976+Half-Shot@users.noreply.github.com> Date: Tue, 30 Jun 2026 17:58:03 +0100 Subject: [PATCH] Add URL preview above message composer (#33964) * Modify LinkPreview to export shared atomics * Implement MessageComposerUrlPreviewView * Create UrlPreviewFetcher utility function * Modify view models * Implement in composer * Support running tests in dom-less vitest environment * Add a playwright test * hide another one * fmt * cleanup * test rte too * fixup * Add back docstring * cleanup * off by one * remove description check * Cleanup hacks * Remove another hack * cleanup * one more * fixup window here too * whoops type * Rename to be cleaeer * Trim URLs first * fix bug --- .../e2e/links/composer-url-preview.spec.ts | 77 ++++ .../views/messages/TextualBodyFactory.tsx | 16 +- .../views/rooms/MessageComposer.tsx | 12 + .../views/rooms/MessageComposerUrlPreview.tsx | 45 ++ .../hooks/spotlight/useDebouncedCallback.ts | 9 +- apps/web/src/utils/UrlPreviewFetcher.test.ts | 223 ++++++++++ apps/web/src/utils/UrlPreviewFetcher.ts | 204 ++++++++++ .../UrlPreviewFetcher.test.ts.snap | 95 +++++ ...MessageComposerUrlPreviewViewModel.test.ts | 134 ++++++ .../MessageComposerUrlPreviewViewModel.ts | 95 +++++ ...geComposerUrlPreviewViewModel.test.ts.snap | 39 ++ .../message-body/UrlPreviewGroupViewModel.ts | 383 ++++-------------- .../UrlPreviewGroupViewModel-test.ts | 158 +------- .../UrlPreviewGroupViewModel-test.ts.snap | 101 ----- apps/web/tsconfig.json | 2 +- .../default-auto.png | Bin 0 -> 21869 bytes .../with-image-and-loads-of-text-auto.png | Bin 0 -> 28571 bytes .../with-image-and-site-icon-auto.png | Bin 0 -> 25410 bytes .../with-image-auto.png | Bin 0 -> 24853 bytes packages/shared-components/src/index.ts | 1 + .../MessageComposerUrlPreview.module.css | 50 +++ .../MessageComposerUrlPreview.stories.tsx | 110 +++++ .../MessageComposerUrlPreview.test.tsx | 26 ++ .../MessageComposerUrlPreview.tsx | 57 +++ .../MessageComposerUrlPreview.test.tsx.snap | 80 ++++ .../MessageComposerUrlPreview/index.ts | 8 + .../LinkPreview/LinkPreview.module.css | 50 +-- .../LinkPreview/LinkPreview.tsx | 4 +- .../UrlPreviewGroupView.tsx | 7 +- 29 files changed, 1386 insertions(+), 600 deletions(-) create mode 100644 apps/web/playwright/e2e/links/composer-url-preview.spec.ts create mode 100644 apps/web/src/components/views/rooms/MessageComposerUrlPreview.tsx create mode 100644 apps/web/src/utils/UrlPreviewFetcher.test.ts create mode 100644 apps/web/src/utils/UrlPreviewFetcher.ts create mode 100644 apps/web/src/utils/__snapshots__/UrlPreviewFetcher.test.ts.snap create mode 100644 apps/web/src/viewmodels/composer/MessageComposerUrlPreviewViewModel.test.ts create mode 100644 apps/web/src/viewmodels/composer/MessageComposerUrlPreviewViewModel.ts create mode 100644 apps/web/src/viewmodels/composer/__snapshots__/MessageComposerUrlPreviewViewModel.test.ts.snap create mode 100644 packages/shared-components/__vis__/linux/__baselines__/room/composer/MessageComposerUrlPreview/MessageComposerUrlPreview.stories.tsx/default-auto.png create mode 100644 packages/shared-components/__vis__/linux/__baselines__/room/composer/MessageComposerUrlPreview/MessageComposerUrlPreview.stories.tsx/with-image-and-loads-of-text-auto.png create mode 100644 packages/shared-components/__vis__/linux/__baselines__/room/composer/MessageComposerUrlPreview/MessageComposerUrlPreview.stories.tsx/with-image-and-site-icon-auto.png create mode 100644 packages/shared-components/__vis__/linux/__baselines__/room/composer/MessageComposerUrlPreview/MessageComposerUrlPreview.stories.tsx/with-image-auto.png create mode 100644 packages/shared-components/src/room/composer/MessageComposerUrlPreview/MessageComposerUrlPreview.module.css create mode 100644 packages/shared-components/src/room/composer/MessageComposerUrlPreview/MessageComposerUrlPreview.stories.tsx create mode 100644 packages/shared-components/src/room/composer/MessageComposerUrlPreview/MessageComposerUrlPreview.test.tsx create mode 100644 packages/shared-components/src/room/composer/MessageComposerUrlPreview/MessageComposerUrlPreview.tsx create mode 100644 packages/shared-components/src/room/composer/MessageComposerUrlPreview/__snapshots__/MessageComposerUrlPreview.test.tsx.snap create mode 100644 packages/shared-components/src/room/composer/MessageComposerUrlPreview/index.ts diff --git a/apps/web/playwright/e2e/links/composer-url-preview.spec.ts b/apps/web/playwright/e2e/links/composer-url-preview.spec.ts new file mode 100644 index 0000000000..b3828aeb9c --- /dev/null +++ b/apps/web/playwright/e2e/links/composer-url-preview.spec.ts @@ -0,0 +1,77 @@ +/* + * 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 { test, expect } from "../../element-web-test"; + +const PREVIEW_URL_PATTERN = /.*\/_matrix\/(client\/v1\/media|media\/v3)\/preview_url.*/; + +test.describe("Composer URL preview", () => { + test.use({ + displayName: "Alice", + room: async ({ user, app }, use) => { + const roomId = await app.client.createRoom({ name: "Test room" }); + await use({ roomId }); + }, + }); + + for (const editor of ["cider", "rich text"]) { + test.describe(`in ${editor}`, () => { + test.use({ + labsFlags: editor === "rich_text" ? ["feature_wysiwyg_composer"] : [], + }); + + test("shows a preview when a URL is typed into the composer", async ({ page, app, room }) => { + await page.route(PREVIEW_URL_PATTERN, (route) => + route.fulfill({ + json: { + "og:title": "Example Site", + "og:description": "A great description", + "og:site_name": "example.org", + }, + }), + ); + + await page.goto(`#/room/${room.roomId}`); + const composer = page.getByRole("textbox", { name: "Send an unencrypted message…" }); + await composer.pressSequentially("https://example.org/"); + + await expect(page.getByRole("link", { name: "Example Site" })).toBeVisible(); + }); + + test("does not show a preview when the server returns a 404", async ({ page, app, room }) => { + await page.route(PREVIEW_URL_PATTERN, (route) => route.fulfill({ status: 404 })); + + await page.goto(`#/room/${room.roomId}`); + const composer = page.getByRole("textbox", { name: "Send an unencrypted message…" }); + await composer.pressSequentially("https://example.org/"); + + await expect(page.getByRole("button", { name: "Hide preview" })).not.toBeVisible(); + }); + + test("shows the second URL's preview if the first has no valid preview", async ({ page, app, room }) => { + await page.route(PREVIEW_URL_PATTERN, (route, request) => { + const url = new URL(request.url()).searchParams.get("url"); + if (url === "https://example.org/") { + return route.fulfill({ status: 404 }); + } + return route.fulfill({ + json: { + "og:title": "Fallback Site", + "og:site_name": "fallback.example.org", + }, + }); + }); + + await page.goto(`#/room/${room.roomId}`); + const composer = page.getByRole("textbox", { name: "Send an unencrypted message…" }); + await composer.pressSequentially("https://example.org/ https://fallback.example.org/"); + + await expect(page.getByRole("link", { name: "Fallback Site" })).toBeVisible(); + }); + }); + } +}); diff --git a/apps/web/src/components/views/messages/TextualBodyFactory.tsx b/apps/web/src/components/views/messages/TextualBodyFactory.tsx index b51eebe78b..252d2f4e0e 100644 --- a/apps/web/src/components/views/messages/TextualBodyFactory.tsx +++ b/apps/web/src/components/views/messages/TextualBodyFactory.tsx @@ -24,7 +24,6 @@ import { useMatrixClientContext } from "../../../contexts/MatrixClientContext"; import { useMediaVisible } from "../../../hooks/useMediaVisible"; import { TextualBodyViewModel } from "../../../viewmodels/room/timeline/event-tile/body/TextualBodyViewModel"; import { EventContentBodyViewModel } from "../../../viewmodels/message-body/EventContentBodyViewModel"; -import { UrlPreviewGroupViewModel } from "../../../viewmodels/message-body/UrlPreviewGroupViewModel"; import { getParentEventId } from "../../../utils/Reply"; import Modal from "../../../Modal"; import SettingsStore from "../../../settings/SettingsStore"; @@ -32,6 +31,8 @@ import PosthogTrackers from "../../../PosthogTrackers"; import ImageView from "../elements/ImageView"; import EditMessageComposer from "../rooms/EditMessageComposer"; import { EditWysiwygComposer } from "../rooms/wysiwyg_composer"; +import { UrlPreviewGroupViewModel } from "../../../viewmodels/message-body/UrlPreviewGroupViewModel"; +import PlatformPeg from "../../../PlatformPeg"; const logger = rootLogger.getChild("TextualBodyFactory"); @@ -119,6 +120,7 @@ export function TextualBodyFactory(props: Readonly): JSX.Element { ); }, visible: props.showUrlPreview ?? false, + showTooltips: PlatformPeg.get()?.needsUrlTooltips() ?? true, }), ); @@ -182,10 +184,16 @@ export function TextualBodyFactory(props: Readonly): JSX.Element { ]); useEffect(() => { - void urlPreviewVm.updateHidden(props.showUrlPreview ?? false, mediaVisible).catch((error) => { - logger.warn("UrlPreviewViewModel failed to updateHidden", error); + void urlPreviewVm.updateUrlPreviewVisible(props.showUrlPreview ?? false).catch((error) => { + logger.warn("UrlPreviewViewModel failed to updateUrlPreviewVisible", error); }); - }, [props.showUrlPreview, mediaVisible, urlPreviewVm]); + }, [props.showUrlPreview, urlPreviewVm]); + + useEffect(() => { + void urlPreviewVm.updateMediaVisible(mediaVisible).catch((error) => { + logger.warn("UrlPreviewViewModel failed to updateMediaVisible", error); + }); + }, [mediaVisible, urlPreviewVm]); useEffect(() => { if (previews.length === 0) { diff --git a/apps/web/src/components/views/rooms/MessageComposer.tsx b/apps/web/src/components/views/rooms/MessageComposer.tsx index 6329326efe..6c26d128a3 100644 --- a/apps/web/src/components/views/rooms/MessageComposer.tsx +++ b/apps/web/src/components/views/rooms/MessageComposer.tsx @@ -54,6 +54,8 @@ import { type MatrixClientProps, withMatrixClientHOC } from "../../../contexts/M import { UIFeature } from "../../../settings/UIFeature"; import { formatTimeLeft } from "../../../DateUtils"; import RoomReplacedSvg from "../../../../res/img/room_replaced.svg"; +import { MessageComposerUrlPreviewWrapper } from "./MessageComposerUrlPreview"; +import { Type } from "../../../editor/parts"; // The prefix used when persisting editor drafts to localstorage. export const WYSIWYG_EDITOR_STATE_STORAGE_PREFIX = "mx_wysiwyg_state_"; @@ -101,6 +103,8 @@ interface IState { isWysiwygLabEnabled: boolean; isRichTextEnabled: boolean; initialComposerContent: string; + // Specifically for generating previews only. + urlPreviewComposerContent: string; } type WysiwygComposerState = { @@ -142,6 +146,7 @@ export class MessageComposer extends React.Component { this.state = { isComposerEmpty: initialComposerContent?.length === 0, composerContent: initialComposerContent, + urlPreviewComposerContent: initialComposerContent, haveRecording: false, recordingTimeLeftSeconds: undefined, // when set to a number, shows a toast isMenuOpen: false, @@ -418,6 +423,11 @@ export class MessageComposer extends React.Component { private onChange = (model: EditorModel): void => { this.setState({ + urlPreviewComposerContent: model + .serializeParts() + .filter((part) => part.type === Type.Plain) + .map((part) => part.text) + .join(" "), isComposerEmpty: model.isEmpty, }); }; @@ -425,6 +435,7 @@ export class MessageComposer extends React.Component { private onWysiwygChange = (content: string): void => { this.setState({ composerContent: content, + urlPreviewComposerContent: content, isComposerEmpty: content?.length === 0, }); }; @@ -674,6 +685,7 @@ export class MessageComposer extends React.Component { return (
+ + new MessageComposerUrlPreviewViewModel({ + client: MatrixClientPeg.safeGet(), + visible: showUrlPreview, + showTooltips: PlatformPeg.get()?.needsUrlTooltips() ?? true, + }), + ); + + useDebouncedCallback<[MessageComposerUrlPreviewViewModel, string]>( + true, + (vm, content) => { + void vm.updateWithText(content); + }, + [vm, content], + DEBOUNCE_REQUEST_TIMEOUT_MS, + ); + + useEffect(() => { + void vm.updateUrlPreviewVisible(showUrlPreview); + }, [vm, showUrlPreview]); + + return ; +} diff --git a/apps/web/src/hooks/spotlight/useDebouncedCallback.ts b/apps/web/src/hooks/spotlight/useDebouncedCallback.ts index 26edb3c68c..8972dca1df 100644 --- a/apps/web/src/hooks/spotlight/useDebouncedCallback.ts +++ b/apps/web/src/hooks/spotlight/useDebouncedCallback.ts @@ -8,26 +8,27 @@ Please see LICENSE files in the repository root for full details. import { useEffect } from "react"; -const DEBOUNCE_TIMEOUT = 100; +const DEFAULT_DEBOUNCE_TIMEOUT = 100; export function useDebouncedCallback( enabled: boolean, callback: (...params: T) => unknown, params: T, + timeout = DEFAULT_DEBOUNCE_TIMEOUT, ): void { useEffect(() => { - let handle: number | null = null; + let handle: ReturnType | null = null; const doSearch = (): void => { handle = null; callback(...params); }; if (enabled !== false) { - handle = window.setTimeout(doSearch, DEBOUNCE_TIMEOUT); + handle = globalThis.setTimeout(doSearch, timeout); return () => { if (handle) { clearTimeout(handle); } }; } - }, [enabled, callback, params]); + }, [enabled, callback, params, timeout]); } diff --git a/apps/web/src/utils/UrlPreviewFetcher.test.ts b/apps/web/src/utils/UrlPreviewFetcher.test.ts new file mode 100644 index 0000000000..c4f0ebb59b --- /dev/null +++ b/apps/web/src/utils/UrlPreviewFetcher.test.ts @@ -0,0 +1,223 @@ +/* + * 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 { vi, describe, it, expect, beforeAll, afterAll, type Mock } from "vitest"; + +import type { IPreviewUrlResponse, MatrixClient } from "matrix-js-sdk/src/matrix"; +import { UrlPreviewFetcher } from "./UrlPreviewFetcher"; + +const IMAGE_MXC = "mxc://example.org/abc"; +const BASIC_PREVIEW_OGDATA = { + "og:title": "This is an example!", + "og:description": "This is a description", + "og:type": "document", + "og:url": "https://example.org", + "og:site_name": "Example.org", +}; + +function getFetcher(): { + fetcher: UrlPreviewFetcher; + client: { getUrlPreview: Mock; mxcUrlToHttp: Mock }; +} { + const client = { + getUrlPreview: vi.fn(), + mxcUrlToHttp: vi.fn(), + } as unknown as MatrixClient; + return { + fetcher: new UrlPreviewFetcher(client, 0, false), + client: client as unknown as { getUrlPreview: Mock; mxcUrlToHttp: Mock }, + }; +} + +describe("UrlPreviewFetcher", () => { + let originalDevicePixelRatio: Window["devicePixelRatio"]; + beforeAll(() => { + originalDevicePixelRatio = window.devicePixelRatio; + window.devicePixelRatio = 1; + }); + afterAll(() => { + window.devicePixelRatio = originalDevicePixelRatio; + }); + it("should return null when the fetch fails", async () => { + const { fetcher, client } = getFetcher(); + client.getUrlPreview.mockRejectedValue(new Error("Forced test failure")); + expect(await fetcher.fetchPreview("https://example.org", true)).toBeNull(); + }); + + it("should return null when title equals the URL and there is no image", async () => { + const { fetcher, client } = getFetcher(); + client.getUrlPreview.mockResolvedValueOnce({ + "og:title": "https://example.org", + "og:type": "document", + "og:url": "https://example.org", + }); + expect(await fetcher.fetchPreview("https://example.org", true)).toBeNull(); + }); + + it("should cache results and not re-fetch for the same URL", async () => { + const { fetcher, client } = getFetcher(); + client.getUrlPreview.mockResolvedValue(BASIC_PREVIEW_OGDATA); + await fetcher.fetchPreview("https://example.org", true); + await fetcher.fetchPreview("https://example.org", true); + expect(client.getUrlPreview).toHaveBeenCalledTimes(1); + }); + + it("should re-fetch after clearCache is called", async () => { + const { fetcher, client } = getFetcher(); + client.getUrlPreview.mockResolvedValue(BASIC_PREVIEW_OGDATA); + await fetcher.fetchPreview("https://example.org", true); + fetcher.clearCache(); + await fetcher.fetchPreview("https://example.org", true); + expect(client.getUrlPreview).toHaveBeenCalledTimes(2); + }); + + it("should not process media when loadMedia is false", async () => { + const { fetcher, client } = getFetcher(); + client.getUrlPreview.mockResolvedValueOnce({ + "og:title": "This is an example!", + "og:type": "document", + "og:url": "https://example.org", + "og:image": IMAGE_MXC, + "og:image:height": 128, + "og:image:width": 128, + "matrix:image:size": 10000, + }); + const preview = await fetcher.fetchPreview("https://example.org", false); + expect(preview?.image).toBeUndefined(); + // eslint-disable-next-line no-restricted-properties + expect(client.mxcUrlToHttp).not.toHaveBeenCalled(); + }); + + it("should preview a URL with media", async () => { + const { fetcher, client } = getFetcher(); + client.getUrlPreview.mockResolvedValueOnce({ + "og:title": "This is an example!", + "og:type": "document", + "og:url": "https://example.org", + "og:image": IMAGE_MXC, + "og:image:height": 128, + "og:image:width": 128, + "matrix:image:size": 10000, + }); + // eslint-disable-next-line no-restricted-properties + client.mxcUrlToHttp.mockImplementation((url, width) => { + expect(url).toEqual(IMAGE_MXC); + if (width) return "https://example.org/image/thumb"; + return "https://example.org/image/src"; + }); + const preview = await fetcher.fetchPreview("https://example.org", true); + expect(preview).toMatchSnapshot(); + }); + + it.each>([ + { "matrix:image:size": 8191 }, + { "og:image:width": 95 }, + { "og:image:height": 95 }, + ])("should use a site icon for small images %s", async (extraResp) => { + const { fetcher, client } = getFetcher(); + client.getUrlPreview.mockResolvedValueOnce({ + "og:title": "This is an example!", + "og:type": "document", + "og:url": "https://example.org", + "og:image": IMAGE_MXC, + "og:image:height": 128, + "og:image:width": 128, + "matrix:image:size": 8193, + ...extraResp, + }); + // eslint-disable-next-line no-restricted-properties + client.mxcUrlToHttp.mockImplementation((url) => { + expect(url).toEqual(IMAGE_MXC); + return "https://example.org/image/src"; + }); + const preview = await fetcher.fetchPreview("https://example.org", true); + expect(preview?.siteIcon).toBeTruthy(); + expect(preview?.image).toBeUndefined(); + }); + + it.each(["og:video", "og:video:type", "og:audio"])("detects playable links via %s", async (property) => { + const { fetcher, client } = getFetcher(); + // eslint-disable-next-line no-restricted-properties + client.mxcUrlToHttp.mockImplementation((url, width) => { + if (width) return "https://example.org/image/thumb"; + return "https://example.org/image/src"; + }); + client.getUrlPreview.mockResolvedValueOnce({ + ...BASIC_PREVIEW_OGDATA, + "og:image": IMAGE_MXC, + "og:image:height": 128, + "og:image:width": 128, + "matrix:image:size": 10000, + [property]: "anything", + }); + const preview = await fetcher.fetchPreview("https://example.org", true); + expect(preview?.image?.playable).toBe(true); + }); + + describe("calculates author", () => { + it("should use the profile:username if provided", async () => { + const { fetcher, client } = getFetcher(); + client.getUrlPreview.mockResolvedValueOnce({ ...BASIC_PREVIEW_OGDATA, "profile:username": "my username" }); + const preview = await fetcher.fetchPreview("https://example.org", true); + expect(preview?.author).toEqual("my username"); + }); + + it("should use author if the og:type is an article", async () => { + const { fetcher, client } = getFetcher(); + client.getUrlPreview.mockResolvedValueOnce({ + ...BASIC_PREVIEW_OGDATA, + "og:type": "article", + "article:author": "my name", + }); + const preview = await fetcher.fetchPreview("https://example.org", true); + expect(preview?.author).toEqual("my name"); + }); + + it("should NOT use author if the author is a URL", async () => { + const { fetcher, client } = getFetcher(); + client.getUrlPreview.mockResolvedValueOnce({ + ...BASIC_PREVIEW_OGDATA, + "og:type": "article", + "article:author": "https://junk.example.org/foo", + }); + const preview = await fetcher.fetchPreview("https://example.org", true); + expect(preview?.author).toBeUndefined(); + }); + }); + + // og:url and og:type are not surfaced in the preview. + const baseOg = { + "og:url": "https://example.org", + "og:type": "document", + }; + + it.each([ + { ...baseOg, "og:title": "Basic title" }, + { ...baseOg, "og:site_name": "Site name", "og:title": "" }, + { ...baseOg, "og:description": "A description", "og:title": "" }, + { ...baseOg, "og:title": "Cool blog", "og:site_name": "Cool site" }, + { + ...baseOg, + "og:title": "Media test", + // API *may* return a string, so check we parse correctly. + "og:image:height": "500" as unknown as number, + "og:image:width": 500, + "matrix:image:size": 10000, + "og:image": IMAGE_MXC, + }, + ])("handles different kinds of opengraph responses %s", async (og) => { + const { fetcher, client } = getFetcher(); + // eslint-disable-next-line no-restricted-properties + client.mxcUrlToHttp.mockImplementation((url, width) => { + if (width) return "https://example.org/image/thumb"; + return "https://example.org/image/src"; + }); + client.getUrlPreview.mockResolvedValueOnce(og); + const preview = await fetcher.fetchPreview("https://example.org", true); + expect(preview).toMatchSnapshot(); + }); +}); diff --git a/apps/web/src/utils/UrlPreviewFetcher.ts b/apps/web/src/utils/UrlPreviewFetcher.ts new file mode 100644 index 0000000000..3b6a4a2181 --- /dev/null +++ b/apps/web/src/utils/UrlPreviewFetcher.ts @@ -0,0 +1,204 @@ +/* + * 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 { logger as rootLogger } from "matrix-js-sdk/src/logger"; +import { type IPreviewUrlResponse, type MatrixClient, MatrixError } from "matrix-js-sdk/src/matrix"; +import { decode } from "html-entities"; + +import type { UrlPreview } from "@element-hq/web-shared-components"; +import { mediaFromMxc } from "../customisations/Media"; +import { thumbHeight } from "../ImageUtils"; + +const logger = rootLogger.getChild("UrlPreviewFetcher"); + +export const PREVIEW_WIDTH_PX = 478; +export const PREVIEW_HEIGHT_PX = 200; +export const MIN_PREVIEW_PX = 96; +export const MIN_IMAGE_SIZE_BYTES = 8192; + +/** + * Handles fetching and parsing URL previews. + * Maintains a cache of previously fetched previews; call `clearCache` when + * media visibility changes so images are re-fetched with the correct visibility. + */ +export class UrlPreviewFetcher { + private readonly cache = new Map(); + + public constructor( + private readonly client: MatrixClient, + private readonly previewRequestTs: number, + private readonly showTooltips: boolean, + ) {} + + public clearCache(): void { + this.cache.clear(); + } + + /** + * Parse a numeric value from OpenGraph. The OpenGraph spec defines all values as strings + * although Synapse may return these values as numbers. To be compatible, test strings + * and numbers. + * @param value The numeric value + * @returns A number if the value parsed correctly, or undefined otherwise. + */ + private static getNumberFromOpenGraph(value: number | string | undefined): number | undefined { + if (typeof value === "number") { + return value; + } else if (typeof value === "string" && value) { + const i = Number.parseInt(value, 10); + if (!Number.isNaN(i)) return i; + } + return undefined; + } + + /** + * Calculate the best possible title from an opengraph response. + * @param response The opengraph response + * @param link The link being used to preview. + * @returns The title value. + */ + private static getBaseMetadataFromResponse( + response: IPreviewUrlResponse, + link: string, + ): Pick { + let title = + typeof response["og:title"] === "string" && response["og:title"].trim() + ? response["og:title"].trim() + : undefined; + let description = + typeof response["og:description"] === "string" && response["og:description"].trim() + ? response["og:description"].trim() + : undefined; + const siteName = + typeof response["og:site_name"] === "string" && response["og:site_name"].trim() + ? response["og:site_name"].trim() + : new URL(link).hostname; + + if (!title && description) { + title = description; + description = undefined; + } else if (!title && siteName) { + title = siteName; + } else if (!title) { + title = link; + } + + if (description && description.toLowerCase() === siteName.toLowerCase()) { + description = undefined; + } + + return { title, description: description && decode(description), siteName }; + } + + /** + * Calculate the best possible author from an opengraph response. + * @param response The opengraph response + * @returns The author value, or undefined if no valid author could be found. + */ + private static getAuthorFromResponse(response: IPreviewUrlResponse): UrlPreview["author"] { + let calculatedAuthor: string | undefined; + if (response["og:type"] === "article") { + if (typeof response["article:author"] === "string" && response["article:author"]) { + calculatedAuthor = response["article:author"]; + } + } + if (typeof response["profile:username"] === "string" && response["profile:username"]) { + calculatedAuthor = response["profile:username"]; + } + if (calculatedAuthor && URL.canParse(calculatedAuthor)) { + return undefined; + } + return calculatedAuthor; + } + + /** + * Calculate whether the provided image from the preview response is an full size preview or + * a site icon. + * @returns `true` if the image should be used as a preview, otherwise `false` + */ + private static isImagePreview(width?: number, height?: number, bytes?: number): boolean { + if (width && width < MIN_PREVIEW_PX) return false; + if (height && height < MIN_PREVIEW_PX) return false; + if (bytes && bytes < MIN_IMAGE_SIZE_BYTES) return false; + return true; + } + + /** + * Fetch a preview for a single URL, returning a cached result if available. + * @param link The URL to preview. + * @param loadMedia Whether to include the preview image. Pass false when media is hidden. + */ + public async fetchPreview(link: string, loadMedia: boolean): Promise { + const cached = this.cache.get(link); + if (cached) return cached; + + let response: IPreviewUrlResponse; + try { + response = await this.client.getUrlPreview(link, this.previewRequestTs); + } catch (error) { + if (error instanceof MatrixError && error.httpStatus === 404) { + logger.debug("Failed to get URL preview: ", error); + } else { + logger.error("Failed to get URL preview: ", error); + } + return null; + } + + const { title, description, siteName } = UrlPreviewFetcher.getBaseMetadataFromResponse(response, link); + const author = UrlPreviewFetcher.getAuthorFromResponse(response); + const hasImage = response["og:image"] && typeof response["og:image"] === "string"; + + if (title === link && !hasImage) { + return null; + } + + let image: UrlPreview["image"]; + let siteIcon: string | undefined; + + if (typeof response["og:image"] === "string" && loadMedia) { + const media = mediaFromMxc(response["og:image"], this.client); + const declaredHeight = UrlPreviewFetcher.getNumberFromOpenGraph(response["og:image:height"]); + const declaredWidth = UrlPreviewFetcher.getNumberFromOpenGraph(response["og:image:width"]); + const imageSize = UrlPreviewFetcher.getNumberFromOpenGraph(response["matrix:image:size"]); + const alt = typeof response["og:image:alt"] === "string" ? response["og:image:alt"] : undefined; + + if (UrlPreviewFetcher.isImagePreview(declaredWidth, declaredHeight, imageSize)) { + const width = Math.min(declaredWidth ?? PREVIEW_WIDTH_PX, PREVIEW_WIDTH_PX); + const height = + thumbHeight(width, declaredHeight, PREVIEW_WIDTH_PX, PREVIEW_WIDTH_PX) ?? PREVIEW_WIDTH_PX; + const thumb = media.getThumbnailOfSourceHttp(PREVIEW_WIDTH_PX, PREVIEW_HEIGHT_PX, "scale"); + const playable = !!response["og:video"] || !!response["og:video:type"] || !!response["og:audio"]; + if (thumb) { + image = { + imageThumb: thumb, + imageFull: media.srcHttp ?? thumb, + width, + height, + fileSize: UrlPreviewFetcher.getNumberFromOpenGraph(response["matrix:image:size"]), + alt, + playable, + }; + } + } else if (media.srcHttp) { + siteIcon = media.srcHttp; + } + } + + const result = { + link, + title, + author, + description, + siteName, + siteIcon, + showTooltipOnLink: !!(link !== title && this.showTooltips), + image, + } satisfies UrlPreview; + this.cache.set(link, result); + return result; + } +} diff --git a/apps/web/src/utils/__snapshots__/UrlPreviewFetcher.test.ts.snap b/apps/web/src/utils/__snapshots__/UrlPreviewFetcher.test.ts.snap new file mode 100644 index 0000000000..c164f7642f --- /dev/null +++ b/apps/web/src/utils/__snapshots__/UrlPreviewFetcher.test.ts.snap @@ -0,0 +1,95 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`UrlPreviewFetcher > handles different kinds of opengraph responses { 'og:url': 'https://example.org', 'og:type': 'document', 'og:description': 'A description', 'og:title': '' } 1`] = ` +{ + "author": undefined, + "description": undefined, + "image": undefined, + "link": "https://example.org", + "showTooltipOnLink": false, + "siteIcon": undefined, + "siteName": "example.org", + "title": "A description", +} +`; + +exports[`UrlPreviewFetcher > handles different kinds of opengraph responses { 'og:url': 'https://example.org', 'og:type': 'document', 'og:site_name': 'Site name', 'og:title': '' } 1`] = ` +{ + "author": undefined, + "description": undefined, + "image": undefined, + "link": "https://example.org", + "showTooltipOnLink": false, + "siteIcon": undefined, + "siteName": "Site name", + "title": "Site name", +} +`; + +exports[`UrlPreviewFetcher > handles different kinds of opengraph responses { 'og:url': 'https://example.org', 'og:type': 'document', 'og:title': 'Basic title' } 1`] = ` +{ + "author": undefined, + "description": undefined, + "image": undefined, + "link": "https://example.org", + "showTooltipOnLink": false, + "siteIcon": undefined, + "siteName": "example.org", + "title": "Basic title", +} +`; + +exports[`UrlPreviewFetcher > handles different kinds of opengraph responses { 'og:url': 'https://example.org', 'og:type': 'document', 'og:title': 'Cool blog', 'og:site_name': 'Cool site' } 1`] = ` +{ + "author": undefined, + "description": undefined, + "image": undefined, + "link": "https://example.org", + "showTooltipOnLink": false, + "siteIcon": undefined, + "siteName": "Cool site", + "title": "Cool blog", +} +`; + +exports[`UrlPreviewFetcher > handles different kinds of opengraph responses { 'og:url': 'https://example.org', 'og:type': 'document', 'og:title': 'Media test', 'og:image:height': '500', 'og:image:width': 500, 'matrix:image:size': 10000, 'og:image': 'mxc://example.org/abc' } 1`] = ` +{ + "author": undefined, + "description": undefined, + "image": { + "alt": undefined, + "fileSize": 10000, + "height": 478, + "imageFull": "https://example.org/image/src", + "imageThumb": "https://example.org/image/thumb", + "playable": false, + "width": 478, + }, + "link": "https://example.org", + "showTooltipOnLink": false, + "siteIcon": undefined, + "siteName": "example.org", + "title": "Media test", +} +`; + +exports[`UrlPreviewFetcher > should preview a URL with media 1`] = ` +{ + "author": undefined, + "description": undefined, + "image": { + "alt": undefined, + "fileSize": 10000, + "height": 128, + "imageFull": "https://example.org/image/src", + "imageThumb": "https://example.org/image/thumb", + "playable": false, + "width": 128, + }, + "link": "https://example.org", + "showTooltipOnLink": false, + "siteIcon": undefined, + "siteName": "example.org", + "title": "This is an example!", +} +`; diff --git a/apps/web/src/viewmodels/composer/MessageComposerUrlPreviewViewModel.test.ts b/apps/web/src/viewmodels/composer/MessageComposerUrlPreviewViewModel.test.ts new file mode 100644 index 0000000000..693ccf47d5 --- /dev/null +++ b/apps/web/src/viewmodels/composer/MessageComposerUrlPreviewViewModel.test.ts @@ -0,0 +1,134 @@ +/* + * 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 { vi, describe, it, expect, type Mock, beforeAll, afterAll } from "vitest"; + +import type { MatrixClient } from "matrix-js-sdk/src/matrix"; +import { MessageComposerUrlPreviewViewModel } from "./MessageComposerUrlPreviewViewModel"; + +const IMAGE_MXC = "mxc://example.org/abc"; +const BASIC_PREVIEW_OGDATA = { + "og:title": "This is an example!", + "og:description": "This is a description", + "og:type": "document", + "og:url": "https://example.org", + "og:site_name": "Example.org", +}; + +function getViewModel({ visible } = { visible: true }): { + vm: MessageComposerUrlPreviewViewModel; + client: { getUrlPreview: Mock; mxcUrlToHttp: Mock }; +} { + const client = { + getUrlPreview: vi.fn(), + mxcUrlToHttp: vi.fn(), + } as unknown as MatrixClient; + const vm = new MessageComposerUrlPreviewViewModel({ client, visible, showTooltips: false }); + return { vm, client: client as unknown as { getUrlPreview: Mock; mxcUrlToHttp: Mock } }; +} +describe("MessageComposerUrlPreviewViewModel", () => { + let originalDevicePixelRatio: Window["devicePixelRatio"]; + beforeAll(() => { + originalDevicePixelRatio = window.devicePixelRatio; + window.devicePixelRatio = 1; + }); + afterAll(() => { + window.devicePixelRatio = originalDevicePixelRatio; + }); + + it("should return no preview by default", () => { + expect(getViewModel().vm.getSnapshot()).toMatchInlineSnapshot(` +{ + "preview": null, +} +`); + }); + + it("should preview a valid URL in text", async () => { + const { vm, client } = getViewModel(); + client.getUrlPreview.mockResolvedValueOnce(BASIC_PREVIEW_OGDATA); + await vm.updateWithText("Check out https://example.org today"); + expect(vm.getSnapshot()).toMatchSnapshot(); + }); + + it("should return null when preview is not visible", async () => { + const { vm, client } = getViewModel({ visible: false }); + await vm.updateWithText("https://example.org"); + expect(vm.getSnapshot().preview).toBeNull(); + expect(client.getUrlPreview).not.toHaveBeenCalled(); + }); + + it("should return null when all URL fetches fail", async () => { + const { vm, client } = getViewModel(); + client.getUrlPreview.mockRejectedValue(new Error("Forced test failure")); + await vm.updateWithText("https://example.org"); + expect(vm.getSnapshot().preview).toBeNull(); + }); + + it("should use the first URL with a valid preview when multiple are given", async () => { + const { vm, client } = getViewModel(); + client.getUrlPreview + .mockRejectedValueOnce(new Error("First URL failed")) + .mockResolvedValueOnce(BASIC_PREVIEW_OGDATA); + await vm.updateWithText("https://example.org/one https://example.org/two"); + expect(vm.getSnapshot().preview?.link).toEqual("https://example.org/two"); + }); + + it("should not re-fetch when text changes but the URL set does not", async () => { + const { vm, client } = getViewModel(); + client.getUrlPreview.mockResolvedValue(BASIC_PREVIEW_OGDATA); + await vm.updateWithText("https://example.org"); + await vm.updateWithText("https://example.org some extra words"); + expect(client.getUrlPreview).toHaveBeenCalledTimes(1); + }); + + it("should deduplicate repeated URLs", async () => { + const { vm, client } = getViewModel(); + client.getUrlPreview.mockResolvedValue(BASIC_PREVIEW_OGDATA); + await vm.updateWithText("https://example.org https://example.org https://example.org"); + expect(client.getUrlPreview).toHaveBeenCalledTimes(1); + }); + + it("should hide preview when made invisible", async () => { + const { vm, client } = getViewModel(); + client.getUrlPreview.mockResolvedValue(BASIC_PREVIEW_OGDATA); + await vm.updateWithText("https://example.org"); + expect(vm.getSnapshot().preview).not.toBeNull(); + await vm.updateUrlPreviewVisible(false); + expect(vm.getSnapshot().preview).toBeNull(); + }); + + it("should restore preview when made visible again", async () => { + const { vm, client } = getViewModel({ visible: false }); + client.getUrlPreview.mockResolvedValue(BASIC_PREVIEW_OGDATA); + await vm.updateWithText("https://example.org"); + expect(vm.getSnapshot().preview).toBeNull(); + await vm.updateUrlPreviewVisible(true); + expect(vm.getSnapshot().preview).not.toBeNull(); + }); + + it("should preview a URL with media", async () => { + const { vm, client } = getViewModel(); + client.getUrlPreview.mockResolvedValueOnce({ + "og:title": "Media example", + "og:type": "document", + "og:url": "https://example.org", + "og:image": IMAGE_MXC, + "og:image:height": 128, + "og:image:width": 128, + "matrix:image:size": 10000, + }); + // eslint-disable-next-line no-restricted-properties + client.mxcUrlToHttp.mockImplementation((url, width) => { + expect(url).toEqual(IMAGE_MXC); + if (width) return "https://example.org/image/thumb"; + return "https://example.org/image/src"; + }); + await vm.updateWithText("https://example.org"); + expect(vm.getSnapshot()).toMatchSnapshot(); + }); +}); diff --git a/apps/web/src/viewmodels/composer/MessageComposerUrlPreviewViewModel.ts b/apps/web/src/viewmodels/composer/MessageComposerUrlPreviewViewModel.ts new file mode 100644 index 0000000000..9329c46fa7 --- /dev/null +++ b/apps/web/src/viewmodels/composer/MessageComposerUrlPreviewViewModel.ts @@ -0,0 +1,95 @@ +/* + * 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 { logger as rootLogger } from "matrix-js-sdk/src/logger"; +import { type MatrixClient } from "matrix-js-sdk/src/matrix"; +import { BaseViewModel, type MessageComposerUrlPreviewSnapshot } from "@element-hq/web-shared-components"; + +import { UrlPreviewFetcher } from "../../utils/UrlPreviewFetcher"; + +const logger = rootLogger.getChild("MessageComposerUrlPreviewViewModel"); + +export interface MessageComposerUrlPreviewViewModelProps { + client: MatrixClient; + visible: boolean; + showTooltips: boolean; +} + +export class MessageComposerUrlPreviewViewModel extends BaseViewModel< + MessageComposerUrlPreviewSnapshot, + MessageComposerUrlPreviewViewModelProps +> { + private readonly fetcher: UrlPreviewFetcher; + + /** + * Calculated set of links from the message text. + */ + private links: Set = new Set(); + + /** + * Should the URL preview render according to the application. + */ + private urlPreviewVisible: boolean; + + public constructor(props: MessageComposerUrlPreviewViewModelProps) { + super(props, { preview: null }); + this.urlPreviewVisible = props.visible; + this.fetcher = new UrlPreviewFetcher(props.client, Date.now(), props.showTooltips); + } + + private async computeSnapshot(): Promise { + if (!this.urlPreviewVisible) { + this.snapshot.set({ preview: null }); + return; + } + // We always select the *first* viable preview out of the message. + // Subsequent links are ignored. + for (const link of this.links) { + try { + const preview = await this.fetcher.fetchPreview(link, true); + if (preview) { + this.snapshot.set({ preview }); + return; + } + } catch (ex) { + logger.warn("Fetching preview failed", ex); + } + } + this.snapshot.set({ preview: null }); + } + + /** + * Trigger a recalculation of the links in the provided text. + * @param content Plaintext from the message composer. + */ + public async updateWithText(content: string): Promise { + const newLinks = new Set( + content + .split(" ") + .map((w) => w.trim()) + .filter((word) => URL.canParse(word)), + ); + if (this.links.symmetricDifference(newLinks).size === 0) { + // Skip if the URL set hasn't changed + return; + } + this.links = newLinks; + return this.computeSnapshot(); + } + + /** + * Update the view model about visible state of previews. + * @param urlPreviewVisible Whether URL previews are hidden for this room. + * + * @returns A promise that completes when the snapshot has been recomputed. + */ + public readonly updateUrlPreviewVisible = (urlPreviewVisible: boolean): Promise => { + this.urlPreviewVisible = urlPreviewVisible; + this.fetcher.clearCache(); + return this.computeSnapshot(); + }; +} diff --git a/apps/web/src/viewmodels/composer/__snapshots__/MessageComposerUrlPreviewViewModel.test.ts.snap b/apps/web/src/viewmodels/composer/__snapshots__/MessageComposerUrlPreviewViewModel.test.ts.snap new file mode 100644 index 0000000000..7a41ca8b03 --- /dev/null +++ b/apps/web/src/viewmodels/composer/__snapshots__/MessageComposerUrlPreviewViewModel.test.ts.snap @@ -0,0 +1,39 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`MessageComposerUrlPreviewViewModel > should preview a URL with media 1`] = ` +{ + "preview": { + "author": undefined, + "description": undefined, + "image": { + "alt": undefined, + "fileSize": 10000, + "height": 128, + "imageFull": "https://example.org/image/src", + "imageThumb": "https://example.org/image/thumb", + "playable": false, + "width": 128, + }, + "link": "https://example.org", + "showTooltipOnLink": false, + "siteIcon": undefined, + "siteName": "example.org", + "title": "Media example", + }, +} +`; + +exports[`MessageComposerUrlPreviewViewModel > should preview a valid URL in text 1`] = ` +{ + "preview": { + "author": undefined, + "description": "This is a description", + "image": undefined, + "link": "https://example.org", + "showTooltipOnLink": false, + "siteIcon": undefined, + "siteName": "Example.org", + "title": "This is an example!", + }, +} +`; diff --git a/apps/web/src/viewmodels/message-body/UrlPreviewGroupViewModel.ts b/apps/web/src/viewmodels/message-body/UrlPreviewGroupViewModel.ts index 3ff1c40d3b..5b421d6455 100644 --- a/apps/web/src/viewmodels/message-body/UrlPreviewGroupViewModel.ts +++ b/apps/web/src/viewmodels/message-body/UrlPreviewGroupViewModel.ts @@ -5,175 +5,48 @@ * Please see LICENSE files in the repository root for full details. */ +import { type MatrixClient, type MatrixEvent } from "matrix-js-sdk/src/matrix"; import { BaseViewModel, - type UrlPreviewGroupViewSnapshot, - type UrlPreviewGroupViewActions, type UrlPreview, + type UrlPreviewGroupViewActions, + type UrlPreviewGroupViewSnapshot, } from "@element-hq/web-shared-components"; -import { logger as rootLogger } from "matrix-js-sdk/src/logger"; -import { type IPreviewUrlResponse, type MatrixClient, MatrixError, type MatrixEvent } from "matrix-js-sdk/src/matrix"; -import { decode } from "html-entities"; import { type UrlPreviewVisibilityChanged } from "@matrix-org/analytics-events/types/typescript/UrlPreviewVisibilityChanged"; -import { isPermalinkHost } from "../../utils/permalinks/Permalinks"; -import { mediaFromMxc } from "../../customisations/Media"; -import PlatformPeg from "../../PlatformPeg"; -import { thumbHeight } from "../../ImageUtils"; import { PosthogAnalytics } from "../../PosthogAnalytics"; +import { isPermalinkHost } from "../../utils/permalinks/Permalinks"; +import { UrlPreviewFetcher } from "../../utils/UrlPreviewFetcher"; -const logger = rootLogger.getChild("UrlPreviewGroupViewModel"); +// From https://github.com/matrix-org/matrix-spec-proposals/pull/4095 +export const BUNDLED_LINK_PREVIEWS = "com.beeper.linkpreviews"; + +export const MAX_PREVIEWS_WHEN_LIMITED = 2; + +export enum PreviewVisibility { + /** Preview is entirely hidden and cannot be changed. */ + Hidden, + /** Preview is hidden by the user and may be shown again. */ + UserHidden, + /** Preview is visible but media should not be rendered. */ + MediaHidden, + /** Preview is fully visible including media. */ + Visible, +} export interface UrlPreviewGroupViewModelProps { client: MatrixClient; mxEvent: MatrixEvent; - mediaVisible: boolean; visible: boolean; + mediaVisible: boolean; + showTooltips: boolean; onImageClicked: (preview: UrlPreview) => void; } -export const MAX_PREVIEWS_WHEN_LIMITED = 2; -export const PREVIEW_WIDTH_PX = 478; -export const PREVIEW_HEIGHT_PX = 200; -export const MIN_PREVIEW_PX = 96; -export const MIN_IMAGE_SIZE_BYTES = 8192; -// From https://github.com/matrix-org/matrix-spec-proposals/pull/4095 -export const BUNDLED_LINK_PREVIEWS = "com.beeper.linkpreviews"; - -export enum PreviewVisibility { - /** - * Preview is entirely hidden from view and can not be changed. - */ - Hidden, - /** - * Preview is entirely hidden from view but the user may change this. - */ - UserHidden, - /** - * Preview is visible but media should not be rendered. - */ - MediaHidden, - /** - * Preview is visible and media should be rendered. - */ - Visible, -} - -/** - * ViewModel for fetching and rendering URL previews for an individual event. - */ export class UrlPreviewGroupViewModel extends BaseViewModel implements UrlPreviewGroupViewActions { - /** - * Parse a numeric value from OpenGraph. The OpenGraph spec defines all values as strings - * although Synapse may return these values as numbers. To be compatible, test strings - * and numbers. - * @param value The numeric value - * @returns A number if the value parsed correctly, or undefined otherwise. - */ - private static getNumberFromOpenGraph(value: number | string | undefined): number | undefined { - if (typeof value === "number") { - return value; - } else if (typeof value === "string" && value) { - const i = parseInt(value, 10); - if (!isNaN(i)) { - return i; - } - } - return undefined; - } - - /** - * Calculate the best possible title from an opengraph response. - * @param response The opengraph response - * @param link The link being used to preview. - * @returns The title value. - */ - private static getBaseMetadataFromResponse( - response: IPreviewUrlResponse, - link: string, - ): Pick { - let title = - typeof response["og:title"] === "string" && response["og:title"].trim() - ? response["og:title"].trim() - : undefined; - let description = - typeof response["og:description"] === "string" && response["og:description"].trim() - ? response["og:description"].trim() - : undefined; - const siteName = - typeof response["og:site_name"] === "string" && response["og:site_name"].trim() - ? response["og:site_name"].trim() - : new URL(link).hostname; - - // If there is no title, use the description as the title. - if (!title && description) { - title = description; - description = undefined; - } else if (!title && siteName) { - title = siteName; - } else if (!title) { - title = link; - } - - // If the description matches the site name, don't bother with a description. - if (description && description.toLowerCase() === siteName.toLowerCase()) { - description = undefined; - } - - return { - title, - description: description && decode(description), - siteName, - }; - } - - /** - * Calculate the best possible author from an opengraph response. - * @param response The opengraph response - * @returns The author value, or undefined if no valid author could be found. - */ - private static getAuthorFromResponse(response: IPreviewUrlResponse): UrlPreview["author"] { - let calculatedAuthor: string | undefined; - if (response["og:type"] === "article") { - if (typeof response["article:author"] === "string" && response["article:author"]) { - calculatedAuthor = response["article:author"]; - } - // Otherwise fall through to check the profile. - } - if (typeof response["profile:username"] === "string" && response["profile:username"]) { - calculatedAuthor = response["profile:username"]; - } - if (calculatedAuthor && URL.canParse(calculatedAuthor)) { - // Some sites return URLs as authors which doesn't look good in Element, so discard it. - return; - } - return calculatedAuthor; - } - - /** - * Calculate whether the provided image from the preview response is an full size preview or - * a site icon. - * @returns `true` if the image should be used as a preview, otherwise `false` - */ - private static isImagePreview(width?: number, height?: number, bytes?: number): boolean { - // We can't currently distinguish from a preview image and a favicon. Neither OpenGraph nor Matrix - // have a clear distinction, so we're using a heuristic here to check the dimensions & size of the file and - // deciding whether to render it as a full preview or icon. - if (width && width < MIN_PREVIEW_PX) { - return false; - } - if (height && height < MIN_PREVIEW_PX) { - return false; - } - if (bytes && bytes < MIN_IMAGE_SIZE_BYTES) { - return false; - } - return true; - } - /** * Determine if an anchor element can be rendered into a preview. * If it can, return the value of `href` @@ -181,36 +54,15 @@ export class UrlPreviewGroupViewModel * @returns The value of the `href` of the node, or null if this node cannot be previewed. */ private static getAnchorLink(node: HTMLAnchorElement): string | null { - // don't try to preview relative links const href = node.getAttribute("href"); - if (!href || !URL.canParse(href)) { - return null; - } + if (!href || !URL.canParse(href)) return null; const url = new URL(href); - if (!["http:", "https:"].includes(url.protocol)) { - return null; - } - // never preview permalinks (if anything we should give a smart - // preview of the room/user they point to: nobody needs to be reminded - // what the matrix.to site looks like). - if (isPermalinkHost(url.host)) { - return null; - } + if (!["http:", "https:"].includes(url.protocol)) return null; + if (isPermalinkHost(url.host)) return null; - // as a random heuristic to avoid highlighting things like "foo.pl" - // we require the linked text to either include a / (either from http:// - // or from a full foo.bar/baz style schemeless URL) - or be a markdown-style - // link, in which case we check the target text differs from the link value. - if (node.textContent?.includes("/")) { - return href; - } - - if (node.textContent?.toLowerCase().trim().startsWith(url.host.toLowerCase())) { - // it's a "foo.pl" style link - return null; - } - // it's a [foo bar](http://foo.com) style link + if (node.textContent?.includes("/")) return href; + if (node.textContent?.toLowerCase().trim().startsWith(url.host.toLowerCase())) return null; return href; } @@ -221,43 +73,26 @@ export class UrlPreviewGroupViewModel */ private static findLinks(nodes: Iterable): string[] { let links = new Set(); - for (const node of nodes) { if (node.tagName === "A") { const href = this.getAnchorLink(node as HTMLAnchorElement); - if (href) { - links.add(href); - } + if (href) links.add(href); } else if (node.tagName === "PRE" || node.tagName === "CODE" || node.tagName === "BLOCKQUOTE") { continue; - } else if (node.children && node.children.length) { + } else if (node.children?.length) { links = new Set([...links, ...this.findLinks(node.children)]); } } return [...links]; } - private readonly client: MatrixClient; private readonly storageKey: string; - private readonly eventSendTime: number; - - /** - * Should the URL preview render according to the application. - */ - private urlPreviewVisible: boolean; - /** - * Should media be rendered in the preview. - */ - private mediaVisible: boolean; - /** - * Has the user opted to render this individual preview, or hide it. - */ - private urlPreviewEnabledByUser: boolean; + private readonly fetcher: UrlPreviewFetcher; /** * Calculated set of links from the provided DOM element. */ - private links: Array = []; + private links: string[] = []; /** * Should the preview limit how many links are rendered. If `false`, all @@ -266,9 +101,19 @@ export class UrlPreviewGroupViewModel private limitPreviews = true; /** - * A cache containing all previously calculated previews. + * Should the URL preview render according to the application. */ - private readonly previewCache = new Map(); + private urlPreviewVisible: boolean; + + /** + * Should media be rendered in the preview. + */ + private mediaVisible: boolean; + + /** + * Has the user opted to render this individual preview, or hide it. + */ + private urlPreviewEnabledByUser: boolean; /** * Called when the user clicks on the preview thumbnail. @@ -276,111 +121,31 @@ export class UrlPreviewGroupViewModel public readonly onImageClick: (preview: UrlPreview) => void; public constructor(props: UrlPreviewGroupViewModelProps) { - const storageKey = `hide_preview_${props.mxEvent.getId()}`; super(props, { previews: [], totalPreviewCount: 0, previewsLimited: true, overPreviewLimit: false, }); - this.urlPreviewEnabledByUser = globalThis.localStorage.getItem(storageKey) !== "1"; + this.onImageClick = props.onImageClicked; + this.storageKey = `hide_preview_${props.mxEvent.getId()}`; this.urlPreviewVisible = props.visible; this.mediaVisible = props.mediaVisible; - this.storageKey = storageKey; - this.client = props.client; - this.eventSendTime = props.mxEvent.getTs(); - this.onImageClick = props.onImageClicked; + this.urlPreviewEnabledByUser = globalThis.localStorage.getItem(this.storageKey) !== "1"; + this.fetcher = new UrlPreviewFetcher(props.client, props.mxEvent.getTs(), props.showTooltips); } /** - * Fetch a complete preview of a given URL. - * Will always return a cached response if it was previously calculated. - * @param link A URL to be previewed. - * @returns A Promise that returns the snapshot needed to render the preview, or null - * if the resource could not be previewed. + * `true` only when the user has chosen to hide previews. */ - private async fetchPreview(link: string): Promise { - const cached = this.previewCache.get(link); - if (cached) { - return cached; - } - let preview: IPreviewUrlResponse; - - try { - preview = await this.client.getUrlPreview(link, this.eventSendTime); - } catch (error) { - if (error instanceof MatrixError && error.httpStatus === 404) { - // Quieten 404 Not found errors, not all URLs can have a preview generated - logger.debug("Failed to get URL preview: ", error); - } else { - logger.error("Failed to get URL preview: ", error); - } - return null; - } - - const { title, description, siteName } = UrlPreviewGroupViewModel.getBaseMetadataFromResponse(preview, link); - const author = UrlPreviewGroupViewModel.getAuthorFromResponse(preview); - const hasImage = preview["og:image"] && typeof preview?.["og:image"] === "string"; - // Ensure we have something relevant to render. - // The title must not just be the link, or we must have an image. - if (title === link && !hasImage) { - return null; - } - let image: UrlPreview["image"]; - let siteIcon: string | undefined; - if (typeof preview["og:image"] === "string" && this.visibility > PreviewVisibility.MediaHidden) { - const media = mediaFromMxc(preview["og:image"], this.client); - const declaredHeight = UrlPreviewGroupViewModel.getNumberFromOpenGraph(preview["og:image:height"]); - const declaredWidth = UrlPreviewGroupViewModel.getNumberFromOpenGraph(preview["og:image:width"]); - const imageSize = UrlPreviewGroupViewModel.getNumberFromOpenGraph(preview["matrix:image:size"]); - const alt = typeof preview["og:image:alt"] === "string" ? preview["og:image:alt"] : undefined; - - const isImagePreview = UrlPreviewGroupViewModel.isImagePreview(declaredWidth, declaredHeight, imageSize); - if (isImagePreview) { - const width = Math.min(declaredWidth ?? PREVIEW_WIDTH_PX, PREVIEW_WIDTH_PX); - const height = - thumbHeight(width, declaredHeight, PREVIEW_WIDTH_PX, PREVIEW_WIDTH_PX) ?? PREVIEW_WIDTH_PX; - const thumb = media.getThumbnailOfSourceHttp(PREVIEW_WIDTH_PX, PREVIEW_HEIGHT_PX, "scale"); - const playable = !!preview["og:video"] || !!preview["og:video:type"] || !!preview["og:audio"]; - // No thumb, no preview. - if (thumb) { - image = { - imageThumb: thumb, - imageFull: media.srcHttp ?? thumb, - width, - height, - fileSize: UrlPreviewGroupViewModel.getNumberFromOpenGraph(preview["matrix:image:size"]), - alt, - playable, - }; - } - } else if (media.srcHttp) { - siteIcon = media.srcHttp; - } - } - - const result = { - link, - title, - author, - description, - siteName, - siteIcon, - showTooltipOnLink: !!(link !== title && PlatformPeg.get()?.needsUrlTooltips()), - image, - } satisfies UrlPreview; - this.previewCache.set(link, result); - return result; + public get isPreviewHiddenByUser(): boolean { + return this.visibility === PreviewVisibility.UserHidden; } private get visibility(): PreviewVisibility { - if (!this.urlPreviewVisible) { - return PreviewVisibility.Hidden; - } else if (!this.urlPreviewEnabledByUser) { - return PreviewVisibility.UserHidden; - } else if (!this.mediaVisible) { - return PreviewVisibility.MediaHidden; - } + if (!this.urlPreviewVisible) return PreviewVisibility.Hidden; + if (!this.urlPreviewEnabledByUser) return PreviewVisibility.UserHidden; + if (!this.mediaVisible) return PreviewVisibility.MediaHidden; return PreviewVisibility.Visible; } @@ -389,28 +154,29 @@ export class UrlPreviewGroupViewModel * for the previously-calculated links. */ private async computeSnapshot(): Promise { - // This uses MSC4095. If the sender has sent us an empty URL previews bundle - // then they do not want to have URL previews be visible. + // MSC4095: an empty bundled previews array means the sender opted out of previews. const bundledLinkPreviews = this.props.mxEvent.getContent()[BUNDLED_LINK_PREVIEWS]; if (Array.isArray(bundledLinkPreviews) && bundledLinkPreviews.length === 0) { - return this.snapshot.merge({ + this.snapshot.merge({ previews: [], totalPreviewCount: 0, previewsLimited: false, overPreviewLimit: false, }); - } // otherwise, we do not support bundled previews yet so will fallback to old behaviour. + return; + } + const loadMedia = this.visibility === PreviewVisibility.Visible; const previews = this.visibility <= PreviewVisibility.UserHidden ? [] : await Promise.all( this.links .slice(0, this.limitPreviews ? MAX_PREVIEWS_WHEN_LIMITED : undefined) - .map((link) => this.fetchPreview(link)), + .map((link) => this.fetcher.fetchPreview(link, loadMedia)), ); this.snapshot.merge({ - previews: previews.filter((m) => !!m), + previews: previews.filter((p) => !!p), totalPreviewCount: this.links.length, previewsLimited: this.limitPreviews, overPreviewLimit: this.links.length > MAX_PREVIEWS_WHEN_LIMITED, @@ -423,7 +189,6 @@ export class UrlPreviewGroupViewModel */ public async updateEventElement(eventElement: HTMLDivElement | HTMLSpanElement): Promise { const newLinks = UrlPreviewGroupViewModel.findLinks([eventElement]); - // Only recalculate if the set of links has changed. if (newLinks.some((x) => !this.links.includes(x)) || this.links.some((x) => !newLinks.includes(x))) { this.links = newLinks; return this.computeSnapshot(); @@ -431,19 +196,26 @@ export class UrlPreviewGroupViewModel } /** - * Update the view model about the status of whether the event should be - * viewable. + * Update the view model about visible state of previews. * @param urlPreviewVisible Whether URL previews are hidden for this room. - * @param mediaVisible Whether media is hidden for this room or event. * * @returns A promise that completes when the snapshot has been recomputed. */ - public readonly updateHidden = (urlPreviewVisible: boolean, mediaVisible: boolean): Promise => { + public readonly updateUrlPreviewVisible = (urlPreviewVisible: boolean): Promise => { this.urlPreviewVisible = urlPreviewVisible; + this.fetcher.clearCache(); + return this.computeSnapshot(); + }; + + /** + * Update the view model about visible state of media. + * @param urlPreviewVisible Whether media is hidden for this room or event. + * + * @returns A promise that completes when the snapshot has been recomputed. + */ + public readonly updateMediaVisible = (mediaVisible: boolean): Promise => { this.mediaVisible = mediaVisible; - // Changing the visibility here means we need to clear cache as we may need to load - // the media again. - this.previewCache.clear(); + this.fetcher.clearCache(); return this.computeSnapshot(); }; @@ -489,11 +261,4 @@ export class UrlPreviewGroupViewModel this.limitPreviews = !this.limitPreviews; return this.computeSnapshot(); }; - - /** - * `true` only when the user has chosen to hide previews. - */ - public get isPreviewHiddenByUser(): boolean { - return this.visibility === PreviewVisibility.UserHidden; - } } diff --git a/apps/web/test/viewmodels/message-body/UrlPreviewGroupViewModel-test.ts b/apps/web/test/viewmodels/message-body/UrlPreviewGroupViewModel-test.ts index e3e85a29b3..c508410dc2 100644 --- a/apps/web/test/viewmodels/message-body/UrlPreviewGroupViewModel-test.ts +++ b/apps/web/test/viewmodels/message-body/UrlPreviewGroupViewModel-test.ts @@ -8,7 +8,7 @@ import { expect } from "@jest/globals"; import type { MockedObject } from "jest-mock-vitest-adapter"; -import type { MatrixClient, IPreviewUrlResponse } from "matrix-js-sdk/src/matrix"; +import type { MatrixClient } from "matrix-js-sdk/src/matrix"; import { BUNDLED_LINK_PREVIEWS, UrlPreviewGroupViewModel, @@ -42,6 +42,7 @@ function getViewModel( mediaVisible, visible, onImageClicked, + showTooltips: false, mxEvent: mkEvent({ event: true, user: "@foo:bar", @@ -89,15 +90,9 @@ describe("UrlPreviewGroupViewModel", () => { const { previews } = vm.getSnapshot(); expect(previews).toHaveLength(3); expect(previews).toMatchObject([ - { - link: "https://example.org/1", - }, - { - link: "https://example.org/2", - }, - { - link: "https://example.org/3", - }, + { link: "https://example.org/1" }, + { link: "https://example.org/2" }, + { link: "https://example.org/3" }, ]); }); it("should hide preview when invisible", async () => { @@ -108,56 +103,6 @@ describe("UrlPreviewGroupViewModel", () => { expect(vm.getSnapshot()).toMatchSnapshot(); expect(client.getUrlPreview).not.toHaveBeenCalled(); }); - it("should preview a URL with media", async () => { - const { vm, client } = getViewModel(); - client.getUrlPreview.mockResolvedValueOnce({ - "og:title": "This is an example!", - "og:type": "document", - "og:url": "https://example.org", - "og:image": IMAGE_MXC, - "og:image:height": 128, - "og:image:width": 128, - "matrix:image:size": 10000, - }); - // eslint-disable-next-line no-restricted-properties - client.mxcUrlToHttp.mockImplementation((url, width) => { - expect(url).toEqual(IMAGE_MXC); - if (width) { - return "https://example.org/image/thumb"; - } - return "https://example.org/image/src"; - }); - const msg = document.createElement("div"); - msg.innerHTML = 'Test'; - await vm.updateEventElement(msg); - expect(vm.getSnapshot()).toMatchSnapshot(); - }); - it.each>([ - { "matrix:image:size": 8191 }, - { "og:image:width": 95 }, - { "og:image:height": 95 }, - ])("should preview a URL with a site icon", async (extraResp) => { - const { vm, client } = getViewModel(); - client.getUrlPreview.mockResolvedValueOnce({ - "og:title": "This is an example!", - "og:type": "document", - "og:url": "https://example.org", - "og:image": IMAGE_MXC, - "og:image:height": 128, - "og:image:width": 128, - "matrix:image:size": 8193, - ...extraResp, - }); - // eslint-disable-next-line no-restricted-properties - client.mxcUrlToHttp.mockImplementation((url) => { - expect(url).toEqual(IMAGE_MXC); - return "https://example.org/image/src"; - }); - const msg = document.createElement("div"); - msg.innerHTML = 'Test'; - await vm.updateEventElement(msg); - expect(vm.getSnapshot().previews[0].siteIcon).toBeTruthy(); - }); it("should ignore media when mediaVisible is false", async () => { const { vm, client } = getViewModel({ mediaVisible: false, visible: true, showPreview: true }); client.getUrlPreview.mockResolvedValueOnce({ @@ -248,41 +193,6 @@ describe("UrlPreviewGroupViewModel", () => { `); }); - describe("calculates author", () => { - it("should use the profile:username if provided", async () => { - const { vm, client } = getViewModel(); - client.getUrlPreview.mockResolvedValueOnce({ ...BASIC_PREVIEW_OGDATA, "profile:username": "my username" }); - const msg = document.createElement("div"); - msg.innerHTML = 'Test'; - await vm.updateEventElement(msg); - expect(vm.getSnapshot().previews[0].author).toEqual("my username"); - }); - it("should use author if the og:type is an article", async () => { - const { vm, client } = getViewModel(); - client.getUrlPreview.mockResolvedValueOnce({ - ...BASIC_PREVIEW_OGDATA, - "og:type": "article", - "article:author": "my name", - }); - const msg = document.createElement("div"); - msg.innerHTML = 'Test'; - await vm.updateEventElement(msg); - expect(vm.getSnapshot().previews[0].author).toEqual("my name"); - }); - it("should NOT use author if the author is a URL", async () => { - const { vm, client } = getViewModel(); - client.getUrlPreview.mockResolvedValueOnce({ - ...BASIC_PREVIEW_OGDATA, - "og:type": "article", - "article:author": "https://junk.example.org/foo", - }); - const msg = document.createElement("div"); - msg.innerHTML = 'Test'; - await vm.updateEventElement(msg); - expect(vm.getSnapshot().previews[0].author).toBeUndefined(); - }); - }); - it.each([ { text: "", href: "", hasPreview: false }, { text: "test", href: "noprotocol.example.org", hasPreview: false }, @@ -297,62 +207,4 @@ describe("UrlPreviewGroupViewModel", () => { await vm.updateEventElement(msg); expect(vm.getSnapshot().previews).toHaveLength(item.hasPreview ? 1 : 0); }); - - // og:url, og:type are ignored. - const baseOg = { - "og:url": "https://example.org", - "og:type": "document", - }; - - it.each([ - { ...baseOg, "og:title": "Basic title" }, - { ...baseOg, "og:site_name": "Site name", "og:title": "" }, - { ...baseOg, "og:description": "A description", "og:title": "" }, - { ...baseOg, "og:title": "Cool blog", "og:site_name": "Cool site" }, - { - ...baseOg, - "og:title": "Media test", - // API *may* return a string, so check we parse correctly. - "og:image:height": "500" as unknown as number, - "og:image:width": 500, - "matrix:image:size": 10000, - "og:image": IMAGE_MXC, - }, - ])("handles different kinds of opengraph responses %s", async (og) => { - const { vm, client } = getViewModel(); - // eslint-disable-next-line no-restricted-properties - client.mxcUrlToHttp.mockImplementation((url, width) => { - expect(url).toEqual(IMAGE_MXC); - if (width) { - return "https://example.org/image/thumb"; - } - return "https://example.org/image/src"; - }); - client.getUrlPreview.mockResolvedValueOnce(og); - const msg = document.createElement("div"); - msg.innerHTML = `test`; - await vm.updateEventElement(msg); - expect(vm.getSnapshot().previews[0]).toMatchSnapshot(); - }); - - it.each(["og:video", "og:video:type", "og:audio"])("detects playable links via %s", async (property) => { - const { vm, client } = getViewModel(); - // eslint-disable-next-line no-restricted-properties - client.mxcUrlToHttp.mockImplementation((url, width) => { - expect(url).toEqual(IMAGE_MXC); - if (width) { - return "https://example.org/image/thumb"; - } - return "https://example.org/image/src"; - }); - client.getUrlPreview.mockResolvedValueOnce({ - ...BASIC_PREVIEW_OGDATA, - "og:image": IMAGE_MXC, - [property]: "anything", - }); - const msg = document.createElement("div"); - msg.innerHTML = `test`; - await vm.updateEventElement(msg); - expect(vm.getSnapshot().previews[0].image?.playable).toEqual(true); - }); }); diff --git a/apps/web/test/viewmodels/message-body/__snapshots__/UrlPreviewGroupViewModel-test.ts.snap b/apps/web/test/viewmodels/message-body/__snapshots__/UrlPreviewGroupViewModel-test.ts.snap index 0412afaba9..f925798734 100644 --- a/apps/web/test/viewmodels/message-body/__snapshots__/UrlPreviewGroupViewModel-test.ts.snap +++ b/apps/web/test/viewmodels/message-body/__snapshots__/UrlPreviewGroupViewModel-test.ts.snap @@ -1,78 +1,5 @@ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing -exports[`UrlPreviewGroupViewModel handles different kinds of opengraph responses {\\n 'og:url': 'https://example.org',\\n 'og:type': 'document',\\n 'og:description': 'A description',\\n 'og:title': ''\\n} 1`] = ` -{ - "author": undefined, - "description": undefined, - "image": undefined, - "link": "https://example.org", - "showTooltipOnLink": false, - "siteIcon": undefined, - "siteName": "example.org", - "title": "A description", -} -`; - -exports[`UrlPreviewGroupViewModel handles different kinds of opengraph responses {\\n 'og:url': 'https://example.org',\\n 'og:type': 'document',\\n 'og:site_name': 'Site name',\\n 'og:title': ''\\n} 1`] = ` -{ - "author": undefined, - "description": undefined, - "image": undefined, - "link": "https://example.org", - "showTooltipOnLink": false, - "siteIcon": undefined, - "siteName": "Site name", - "title": "Site name", -} -`; - -exports[`UrlPreviewGroupViewModel handles different kinds of opengraph responses {\\n 'og:url': 'https://example.org',\\n 'og:type': 'document',\\n 'og:title': 'Basic title'\\n} 1`] = ` -{ - "author": undefined, - "description": undefined, - "image": undefined, - "link": "https://example.org", - "showTooltipOnLink": false, - "siteIcon": undefined, - "siteName": "example.org", - "title": "Basic title", -} -`; - -exports[`UrlPreviewGroupViewModel handles different kinds of opengraph responses {\\n 'og:url': 'https://example.org',\\n 'og:type': 'document',\\n 'og:title': 'Cool blog',\\n 'og:site_name': 'Cool site'\\n} 1`] = ` -{ - "author": undefined, - "description": undefined, - "image": undefined, - "link": "https://example.org", - "showTooltipOnLink": false, - "siteIcon": undefined, - "siteName": "Cool site", - "title": "Cool blog", -} -`; - -exports[`UrlPreviewGroupViewModel handles different kinds of opengraph responses {\\n 'og:url': 'https://example.org',\\n 'og:type': 'document',\\n 'og:title': 'Media test',\\n 'og:image:height': '500',\\n 'og:image:width': 500,\\n 'matrix:image:size': 10000,\\n 'og:image': 'mxc://example.org/abc'\\n} 1`] = ` -{ - "author": undefined, - "description": undefined, - "image": { - "alt": undefined, - "fileSize": 10000, - "height": 478, - "imageFull": "https://example.org/image/src", - "imageThumb": "https://example.org/image/thumb", - "playable": false, - "width": 478, - }, - "link": "https://example.org", - "showTooltipOnLink": false, - "siteIcon": undefined, - "siteName": "example.org", - "title": "Media test", -} -`; - exports[`UrlPreviewGroupViewModel should deduplicate multiple versions of the same URL 1`] = ` { "overPreviewLimit": false, @@ -160,34 +87,6 @@ exports[`UrlPreviewGroupViewModel should ignore media when mediaVisible is false } `; -exports[`UrlPreviewGroupViewModel should preview a URL with media 1`] = ` -{ - "overPreviewLimit": false, - "previews": [ - { - "author": undefined, - "description": undefined, - "image": { - "alt": undefined, - "fileSize": 10000, - "height": 128, - "imageFull": "https://example.org/image/src", - "imageThumb": "https://example.org/image/thumb", - "playable": false, - "width": 128, - }, - "link": "https://example.org", - "showTooltipOnLink": false, - "siteIcon": undefined, - "siteName": "example.org", - "title": "This is an example!", - }, - ], - "previewsLimited": true, - "totalPreviewCount": 1, -} -`; - exports[`UrlPreviewGroupViewModel should preview a single valid URL 1`] = ` { "overPreviewLimit": false, diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 54db4d3be9..a567efa76d 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -14,7 +14,7 @@ "outDir": "./lib", "declaration": true, "jsx": "react", - "lib": ["es2022", "es2024.promise", "dom", "dom.iterable"], + "lib": ["es2022", "es2024.promise", "dom", "dom.iterable", "ESNext.Collection"], "strict": true, "types": ["node", "modernizr"], "paths": { diff --git a/packages/shared-components/__vis__/linux/__baselines__/room/composer/MessageComposerUrlPreview/MessageComposerUrlPreview.stories.tsx/default-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room/composer/MessageComposerUrlPreview/MessageComposerUrlPreview.stories.tsx/default-auto.png new file mode 100644 index 0000000000000000000000000000000000000000..c6ca271a4be9f091a2c94815e9a3bcc29df28913 GIT binary patch literal 21869 zcmZvEc|276|NqQ1%2L@vB}>VcEG;7IR9BQRH`)+NyKEt{&eW%cER~c9)4dgyrLtyk z!ELcrB3Yv%yX*$@d!2LU%#82j_voMLJ?FgMukHO>p6}NPrCORU5?n4wAP^SqF#Bsa zfiSO>Kp@rd&xaL9-(^_@f(T*9Ut4Vah`r6{nq0k#77Pxo^Rme`3!`pTTw}6h-;ZOD zYxewFsVMr7*vbu_+tyg`zNEI{&W59luiyK=DR$4+cLq1tHx<tUi#^oxf~640tSM+ zpZN7M&Jl$%Mp_OGQk3Kg#|35CQURH6HO2-K*+EQ%=6HVa0*}CYf?c7d?@6S=_mU2x zhzig>VK)a-{<4+wgs8T`OZXc4W%nvASGgU0Z2|o+bc)Zl7sryUH3f8+?Xtmo&*OkPS4mKRX%o ztg%0!?4WP63{%d-X}VfRILtkyv&L`YyJ@>`33FR8tE!+^GPgC@bapE1bJdm4R>m7b z45xo5RU5_EKda1f3|qTjIHA!$zOUn^wN+HF-`UUD~nfm(Vj;weVw!zD=IT zR|Ji$d-hW@zCtoSMCoj+*Fda$vCf-)C4V05|NUC+jZwgWsFmT2_ub&~J402z8q1!f z#~MQv6aCuXem!)#uj7Zc z<$Jk?$tM|;2Mn5R13p`93OAKKw|cSOMB}r{O?G9Swmx_LKD`~PFrHN{`0(!`(lugS}LFW&@Uw4il{%9f<`g*b9>e<=B z*F!(7t;)6u&!{H`jX5@?Daa1&4;6BFb7tV~kIGw}D`r->oOlZT5IZ3=BZ+q_xH zD$$@lQ1TUPx|4ahaqrWECtmV7v)p^dD`Y~p23IcJ@IfSL=0mcAodWZi-vi-{rS1Ve zhu5*Dhs!FA^8JG&lu3)kYc$yY3Cb@Q@|K|~Tf}W#({g`D^eX0Pr?h*aD(l!p`&G?| zlI&Lly_uDgnmM!X-{O@jwWjnMd=5OVn$@Z+94^)f&~G2=`Zub~-m6l3^s9@*aCcp9 zQPi)2<4bdnjj0c>&HQjft~&8pQqWlM1pVa`PP0>9K)uVg2dzSL5^@$)Xvmy^dkS(kM5pY>?ymyI>L z#utU3p2@DWR|qM3b@^7*aP^F5Z+3;7XVrn^io%TeKTkg8wgheqHVSz6YJ?=TR^+z5 zcd-3^tV7nRq#L?al?4}Z-Q zV{QJXRFcajMQ9Z{o^vAk7cpvI9t8e814|BlBneXXkd+>1CKIL`OJ)_vil>`G$Ci4| z=9CyO)j#Mxb;Y^4aI!&e;L4MMqO^EB=cuGanG*lDYK8up_u^6IL%H_)?Xd`tILpAA z96gaVn43|sF|zRcpS5~jm+JI8a&j~$a%*4P^t24_tbQ@@|=v5l0>N|PPA4V%k68Kxja$V z-Ba;QhjrTKPKY7Peuc*uzGqWIm){icyDL>)G#F?wG9vETGZXS5qjl(?&+Q*tTbA@B zO%+YNv$70jCi$(+Fek%_3T))%5o7uOG-kHXg81C4(MRnwmJ-)8M@>7y{{y+I(O`Y z=87@zKN)UUw5=xGR(MyMTp!izwefLyd~;5QeJBWVEwe5E(<}F@*{wr~c_US=<;F=& z-(Ad-*CD~@nkEOgdA94>y}6tDVR`b*FRAN6(#-ghmfMa~@e2m7W(EhC2aY>Sm;cN1 zQ>ruk`QKNcvZ#s>jjR)=y;`CkI(YrbaKCb4pQC1ue{0y#oefc;osNDhjdez{s@|HG zAI?b7i9E>6^nB@B5+?3e~JN&~ZH(aN%{fp68Vq=?PO5*2d{Iw$T~2k$kbJmPhTr5ZIpgxS`;|Jff8 zJ^F|8;?Jt9xv%;nUe{MT9=|t!sZJ;7Kx|Y0Jh{+CEt9j;6NAZXYmb#gPA5)Eh6@*+ zeCF5j#6euGlY^UcX$4+TaN%7NgS^WY>&Q3v-<>_5_)x?PIJeCt|L zHQ+zxSiOC$BICLHx#4+f$+^|TmtyZp#FiAdS2!m{1+J>IJ9EdcqUV=O!nw&32vwf7 zjvo_GURu`P(3u!8`RcYqk?-uq!x6bfYL4!=C5)C|{a|P{YcXz~JcGoJZ=}bZn6z{c zs4wu9W`^`o&xdFwi!dL%m%|fw88Xk_f@|s-P0YCe2npvD{AeI-|>8?d$`iVwpA;{uvMAq`=&wXm9^%HqFC35cbq@I zaFl9Te!wDa+hmA&_4vcQekx7v5Q*!swLiT~K!by))jUd8MCXS(U0 zuab>!*Ke|^-9WcA({lP6CEo@g6hc7&vEz|o%2h0cEH!ny~4_cl&9r^B)YH}>@Q2BnhmnQ9dGAoVh6lLT>>U*TFtX=3E z_^&h;v*|?suQyO);hHt$1cTWz@gzg17^|}O;qYmlBJtLrf_T@XPz_j3ZsB&U>#HNY4+DK8kzZN zpM!WKR!Tm_ia7Bw-Yv4kXe3wn*Vm2?N7mH73dtuK`8!kdUR~BJc2AW~4Crt5?DMIQ z^2zV(@NEwAeUfqcTXFGip+@bAkDwYQ%aaG}$9=sk<(C zoN4^VAz(n7*;^1|F!t4X=yQ^Ttexvj+1G$1_cyJqS&!i6zT8$(IKjd8fo~rSB@D<) z9-p1)_3Is~820*XWH{J%!E@^O@n7``{@<2e|FW)6yMdXU=)b1QsIzIJ|DNfa&7Fz2 zW#XnLpm`AOOkzbBj{RHAx*!^0IQb)b@*|uh19)HcWhOdMVbKH3%%b`BM0PWUg!P1WjD-(a+q~hEj@3Io6G2I z-@uAEIq6`>vrZ>UqAiWI`-_>0z0}$N>@&v?1Q}0!f2L6NQ>wOWhZ?V?mRd1-2zP@z z0{YKMy$MMh`9AGgcc3%;MRi@Ho1F3FTa#y9tK38N`wP{C?>Cg~JLqFE-7ckbT(iSe zIYErs=SwLZPB1T*@oid`)ExL;_h;>A=QM5O#1dALyX;NJ+sz?uTQ2`4xgCo4S>2iN z4bIUP#?qR_6NiI?A^Vl}r04!UJ@frC(f~zV)hJ#)*5uN9@3TU1aR0F573V1Zl`Vsp z>N^8kMs`~?nAimd_e@MK()pFm8qE+c^Y3wA!I~W}85Sw&?sj>T1RC1!{n4cTzO`k< z+8@Wq(FU@4O#jX4%*y2luCXTKTq+F5TjCeAxy$@%*1a!r*pd0|MW(s4?FHA8i3fGx zSpU7AnlBxsCp-letkv4An)B=G?K47;-a6o)&%87iA2immN~r!x$mz8I&U>Fd^eZ1} zq3(pfgE$$!|tcQj19hy`fF_B$2XP?(qjj*0!-hmH!slCk%Dl$eK`?2#h$Z)N#Ct{QitDu9eU#Uwx{KIMsr7i z>5RO?c>cL|3wK+UUx_a16Q$1w{uTwN(fI`uuEC5<2^wiLG}%e;ru&`NIZ_T!*u5F&TkJ#}t&RHy@w%x^>|A+mI@Q z;k83e!_}P`naR0DqiO4U%sY?8yXN>+2eioews`J4d#bww0$7AvMxkC^qf^K9(1VQL zrJ99riT{da2Ui;Qoibcm<*hxPB*rnw)GdpEw!R>7|8wOYD}kVYP5Z2hrQ zcPckjyA0R=YpDL>&CQ}rulMS?r+ea?8lT^GHj?!}F*RfKw3qqE@wROKNjeyy>uBMW zc1C1~5wo{;+yOd_c|>)Nhh`y}JUCT?q**kQoaQcmJ2C(58M*zNf(^3u>CLbjyCFZ-2wTCJvg>P1>zFu^9Di&JB!wFVtGRb$nU)5H~ z4ZH|CB_flr#mxS$oaBG1%XM~mNcmx4v|;Cux6gjuFyB>VY}a!;w`pllPVs|E4TC>} z1LbKtEQ8k0r>~>JXM}aN0RPyKc%r%0^}oRG-?v@PO@}HEk3KId_Yco;9IG5z;q}3& zPCZf_QrBKm9MTRVEoGJ(NK-YdVl(FccS+Wa)Ue~v%HRaw)}Zk){ei;F(WR3X(8(Xo zW&PPV>=;!wo7U^!lO`@~Fz8e;8sXn;q0_t~w`uDAbW_myj3NRk?Ek*)edoaC#JzSt zKl9qo3k^FSsERxq;^dN?(HB>=Vrc)_hVhDw;L)Cm*RIyZCyRR03x?ezGq2uq4T_#( zwz5{T#sO39jXHX?b@lPqRjq*CP0Dq?D_-Z||9iaLbhhX9WNt}=&RaSA{&}8nL`{E| zBu;y)rMgf3uIzaJzI;~UP{X%vQR2bgpK9Lg{=RSh$0u{%hH3+^BdmVC0K@5ZS*_n8 z)wK>>HR*&efM>`YXME&^Q zbL5_P_Lse(UySmPr9S)h?b+=7_&3YtSY3nWf9hF7kN1_$GW*1nm`zfDM$b0*jPDQ1 z-ZSi#;a8^C9+WZzb>>qnWJe!>0w=cC^G%O!bzb)Mg?sv;tETC_@z zWf=NibeiBtC)9W{);@`nOWbUz|765kO}Y(Jjd*7 z|70G#Uc0yGmv%;IK>yUXvpZ(LP6^qSD@r9Ku~t5QAL4&#ZT-}-O~&ggKQDI)T&q#i z`_II?+c{THH@_BUj@4>sy5Gvk>uvvUmQeRV%CSc9UDDfuM9=bN&I3hft65I3(b;}Bn zJoo<9vpQRs@UxHqRBSpG^u(wyw?y5^kVMX3l41{0Th689U*0a8)F z0%3ejS1@mJE|T&;O4&RQ#c&B;Dnyb1KYyvf!h1bpO&rJ)CFUx`U+QZ8LQ9)C%HbS2 z;o`8RxO%;qnJGn)n+o~8;Y!7q|JNB3dAA6}dVFVk6(6~+su&>rw}FZ-x$#(J}VL z74S~A_^uONHiCurC&9ZWK*mJHmV3KGVUbgJ(I{Ks-e$|STX;lA7?dN` zt)||k5$x|ih~aGosHwCsf)XM$p`TexIrNbaOBAUidL)mJ|I~5XYs4({)qHCtY0(>P zmdttDhC(5vmtK2#tBY^Wy0<#$(W0~bIbSc=3xJ9P^i>WcP38;(qCQVAR`W01PAM=N z1w>S~mGWFB`Adn=0(7!SKzmBf$%kW(tw3h$QuVEfyUd6^#i4qJ3g}T~M-OUgdOKe# z+ZUFARi&<+RK?zBm=RqtucDC24Y%fLcjHr2_FWpCcoG#B_!owJ00dA?r zw|$A@`b)l)hcb~A1eaY*vdaOSJ$N)<)I*p)3s-zInc3@kHKQl);o}I~7XPtDui`j1 z`6kgd?nCk@xtM0fJp>@$4*u1$v}mo}WL_%#wV-#)b5~Bywb3h3%PSRin=INWhc0}e z-UoT^Kg{M|qU@x|!=xZ!wLS6;?nQ3*+)4dd|^~9~Z6vgsF?7OiIUYfo8z%m_07IH7Nn4bR^l* zi{YpSDd!MzK}-K4RUtLXLNMyWqXDQ<(eIYpl`bQZcY@Z<&*v<{9|v9iE1BG!oBPES zAQ9uOUl+s5QpZw`FVS~{O?St-qOR^Npo`xjtANrq%)c3-1f2YdrkKI$V&Ge1&s5dcQ%#<5yT?_BEoSKwVm0z6zWljpCXng9N%#8n3(jy$<-OE7=0#hml-01nlIzZ*~35(ju`Qy&`qGsMEN0Fdfo zP7#0M+BOLub1Q;_DPCxdB+z%2^0@mXxclU~^`rtBksg5}S5F7D(m zi=@QUf^WdFh+cmAFwZh_FH)-~O0$l?;?KKGVQ7*R7qk_RIQ>1x{pGdpf$S^)rc3P) zn{!;|!)3p4c=0*uo><=X3rj%CNA3ek0$uFPhGq;RBBd;?PhjIV3VMIXRuwq9aPa&! zpg-=F7{7?wR)}ba7lt9hC5X=(kI7~xuzXak8>NVh5MUddnZfdjr8}`yH!48OQF@H)lVxJq3wxi{)lkm-c3#D9x^*!Lcz1fu)0>#+pTww`cc(-W7$H7V$g^ zY%c@0*Z;dSgDS!MIAbS-TZ&2(WHmHL@9$j7`ABF3-TusD!108cSAb(5`QO5 zKxAnaZ3cHZ@)}i8?3vSH;J&48d>kc^x{EF(k0i-+?$Wa`)!rg3m}jMOuY!d3i$pp)h~8J3+yeOv@G! zNeP8Lqqm&m$v7e8)SV0?*fZ$ZBXJ%DhDknkNCP=-Bdj;@>Pp3u;ie`~>Jy(5jPHql z?PNn=JZ~_?MVDRYveOBA{`8k!cj2f5d5iHJHau#&FZes8Ivyj(AZ}u1)pu0zE6;g= zzKp+ALXPoA;HH8TPn7ur{{6&wn+#-gcVZ96hiV~urpdU)bK-QD3F2b45WdrYJ7u#=AVhL~qOWL52AH9>p_``wq+9aYJGuae zbsb-z2(+`tVxo*!02AjywaLjG9}TIL`J5ZHQnl2KH%~lB>w}SVh&X4*Cly&S$5nZxS9d z2s5<~!1&A+ZQQ3~_m@h*1}Dx#J^Me^p6hrZa55lv!txbZzP7~$jr%BbrCp>*u@jN7 zeD^>P&%6QsBwwLB1n;I4PnS8tTT^IbUksrsE41e}P8)I0MG~<}1Ra|#jlAlUrv^Y3 zI&lS=PvSmI{|16*%eZz%yx^WP_}T{q?=d)7HmdCuOG(hmc(r*eiiL@tNTh=;) z;H+>TGa9?|z;@UOuI=_FRvXG;`ZK<}cyqlG3KO??_eaqx8EHJ&PebT-t9hD@h3@V6 zaA%w%jB2{Hu**VxxRYKE4j&=?;u3yP(c#XTzrYtlt?M_iT@oAaWZVN+*tQ_U6&C~@ z?xc8uC;X~%=;SI1hC4g9!{LpPE%}AhMeKKj@;N|)7C$RcGbd=rtrP*e^V2MFy2!2@ z4>B5vdA~o`&v|tDY7pYY*UvfKhHTbQnd6YXKthey%%kh92fstgy5LAcK)d0e=h zfwqq);%LFBjFe2!!?9||ag;1$Gf59Az+rF?^uXdLutRd3Ji`>Baz%s|B*-y*w*~__ z=0AXLRU5m$MDpiBz7Z2z=`|>YSAT~tpTqE|c#SCi5cEh|3pIHZEKgM?7KKAqW$Mp6 z(F<-kDiWJ6MD)`WTfo^9`M`)IRSK5++-1y-Ck%BETm7NG{e2Y;s zd_0=&f-)g_w;u#_gpd*dN$49+u%UtdYup${Bt7gve1_imGtCbx3n9#1pvfXKLT5_! z^A~4#r$FhkI>R3*g|C5u-*@fu9JSIP3#P=w0Rv{99a({QTL@47q%}%f5KashMv$1AlC#gGR7_ZN3O0C|b7$6u=t33@9xL8HqUmh{>FuRXlf zOc?b&4}urI%d=p!m$lMY^#ezS*1 zyJl7t6S5vut@@>BRB^5mSJ*8^H9rnV<+wxo+!(x(te3u(&>#C@(HuQeP3Zo7P=~^7 zAP;$YY8d1}L9fm6UoRq5IbX5o2Orv!9&>>PDB|$>!IUtX9xg4s6${}u(d@r-B=RRI zD$+w^gwEEje~x1T^i3f2rfc?)Rp~eRYVpQW9)driDKQ#N0P@h_;nl@ANE(d~&tSyN zEGUK;O~9{69?G_C3=6t%ssOrO7Zvavkq#sC@KO3)aBj84^Sj8bfECD)c|DjfqVhQ3jwm{t2(*kEYbVH z9Q{pAXnA%)^eHYG11ZE@^mI^%r*m=R5onRd|LJtV-sj-PbXf@avczUfR1{Da8#k^g z06iAmli)DO#*LQ%8L3w%L~%B7+}H#TmdOqn>%mlo#0dm9W>A0|$}V%1iBC@+6g3rj98a` z(B_<_Fstku}z3ieiNb3<5wDyd)Si3#_}daJb=0hF!?RyD87=defS z_tt1zObUYh$-Lrnl2>EitJ=QJLV-6knytsN67nHSxn}$cKk@cIDha&n$S0)(yxYK*Q-})x%fkFZ!P_p)yJmZ?bT*?DUh7W+92U?B)<9K!H zeiGdGQxO2+R#p(#TSQ_VN<~SEpk2kojdQ@K*L;N<010T+H@fl258=v$F>;)k=Y2++ zV{61KOXbO*1WPZ7JmsyFplX5Q+497PoxFhr=@8M2ku%3hb8-j;zZjtukkWa8Ge#?D z3%wXAeM3rT8yRGvLQ7&#dae#1wGwGw95#P~^gneQQCHtC<$?Dg)Hry2d3FRUeQ6e{ zg{{q_ApOx(jqD)kcc4@a^&2V_0QT$i`fz34p`l&uO*p2v-QCbS5?G;p<{PnsQ%Z+D z=N-!Bq)I5v;rCu}CkPn!+eU6`xeS#j`#_CzI^0zaO&s(^(`W2%;G}n7C={(%O(Q-tJ} zDsDm@cKe;{2GTYXclIANn**}vlsd3T$ijpM?!6 z)_fzlP)lQ>BGU4gMzjo7KmK#3g%DU|?-v)L5rGm-9>F@L`B1S4`w?>3vg1EFTUrQ2 zCYGh(d$E54A}>HB>V~dUIgjg!QB2ZPpvX(Wa?FktZWBn6;T!o&ekCm5JEX!>Y0&)z zx|Eu95%m86YUJ;fboJ_%luiZh> z#Fp2=@-w;$JMpDlrfHy08^UtYofKSnst<7#3@E8;Y0{VK530IQauuNzr4jq zf)PVY@YU{;KLpF)zpF%WHAp`tkg~}Ymis1CID#O8?h!RBwj}CKimD2OY$#i{2X9ZX<@*}&w9(2@(UkMF)*&atUON*!&2Y4ChXp z<7n}26-37S%8XT*`Ej%uL*}nRONg48ixy-5T~2z821hPhjQp2Kx*+)!K{$BOV$2EQ zQZoUN+sD8PjuxYYAj|dcn2Q!;;qjfsQ@gonF-AzmobiV_7cEA0%aEbBkUaGD^X3E} zGXH9*-o~}axpQ(WTqlv+x<9PR-uZ>sI`o~8Mq`%WKKvQ)MKGcE0Pyl;)4EYHvn>>! zUTlpDK;j-Jb}CWWtq8_p%0m#c^2H=CZ(@VbPigK8SVrrfv@f3UJoAl7kLA5!hR<+jk(FP8!Cb?2?rZu8rjt_ zrq~$h4Al&c%5Ez=@%}9AG5Ghsp!40z)Jrr&bT?G9wLm)krARhJjS#+6yibv*3d3QQ z^{PegFs2lp4q|>xqqu^NzlHBsKomW zyvq3Zqw&348IVo)hkDpUP=2pz3r6$}IXC_1er>epLN=%=qUnQwLknSF|F)ak{!w&D z{VS=?z+w68FUv8NqLxb#$jfhma8lIyWKR8%rAB4;0OL{nFQ6WQSqI{kK)4(&Q_X;@ z4un&1M{=9zrLMWrq!wa#diC0?{crqvX{5vecTYS|J=7(t@w8!7WZuF}{EIMs6*(0{ zr;tq;mw>xPmmkA?i@&%B0vTc&Gvu4RMc#H~AYDOIRKj1O8evQ%*EI0=|M(EV+qF*QG-I2NC!2gdwQBhmx$bL=^n>2_7A7+#JfB;rwBaCl&^szE^o{0#D{y_an=w*HOQpe-yLow<2sLTVbWB%fU@5yl*j1Zg=`Y{l>@@_#oOcJHjs&MKNAhNW+e*}okGoKQNh!o;k=9!~*_KCQ;Q7--Cr^w8UBw67SH;~cC6zLD)fOBL$e zB@57DBJ4nPt1_T}dI-E(s}|y}h8$2g($q>V*ibh8JMT3Cxm$q|eG=lrNlM||D6Gf7 ztMvlGpePiLi5PWI9d#RhJ73%KDl2myi-1NUk2NXTEYkRl+zoO}upDX<9sg;IQw|y2 zMY!Jp!KaidtV)p@-b1mHz!ZTU0*H&ap8p^4w(BL*QjAK}EW>G+nuFlxBIqzQ+GMaJ zpC6Qe!40q2EhP8%EDAtHbVUmuTSRNnW>O>;ipa#hZ}7isK^f)qMg{#B4(z`S-M{d7 zNhhWzs@4Gy`G%}lFqeq8+ zgLv&Tk48XhK67~;v0oV?vVK)VcccO|c$^K{A;;PCnulmG4HIbuM40qnTIAorH4lP2%;BM>yke|5T%e)99)D$x_c zO%nywL02VwsoeC=;0H4NCJ4d+S#>X`Vp4%BOoRW^dPGMx!bBH56@)>}eKj*n40RNh zy``rx%Tn%vdhgwpWE&w&?CE0c32;*l+giM)rMK{1oC4x60*jw~hpmp?01pJTVd#FZc@c(k*;WPYUTa!5pAPw2%Du5 zD>&^*X)&0ya}ndIHDK(R`gXPvIEe8KG-*ur(@2|%aE~L#bTLq)*fm#9$G}F6RZPK3 z6%863*~utkO#cLre@b!XBw(O~AjT?y1!-6}Vn5qpr#(Q7pWFapZ@hHnhEn9aJCsRb zVqTyZ{_77mM+EW>!P0qMt+%*)0;@G5fQ}_>dBVl)(IW{>J2FF`z@InglAAL_-zSjA z0p-8}xHI(P0=hh|&YhumI!!!|SqO~BJ3}9?)Q0~OWX=pd{C$}g_Sv30L%&pP5Q|7R zp~m8wp{J1NAEhrHv>+9yBA5*o_n}lR#B10Hj20Uz#?Eo2XbpC?z=4Vp??9)r zWD!krp<;Adp6X6)2BYyp#V8Yubkfu+${eT|bq2}|+54}#P%)}nz#qC4=R(DZgvd;b zS%%OYs2EepF=u*G)LihkRh)gv4iO^G>zMYq4)a+{0)_ z97h3{LWWO}n~*twnu;RN2S?|kY6{@wdO}?s)LC8>malJ7K}+`~HAtJjrnc?^fk6I+ z{Ov&02cY25O*oK(Vdk767kO1!e&nfwfpdUQD5J`g*M{Zn&=edqXV19EZ-M2P_f+uF zA*w6!3ltkrBNBBhU!-u3C*v`xTZtln0G1m*s&IiQ$|%`pm;6y!{`otF!#y14H3BKx zp0GSbRGouEeGxG$*-6x$S@viyW-chw9mcAbW{`ui&J6t*Q7Y!^FhR{bL;qNwu^ka0g$75FzETyRX1JC^Lk7@cNSc_p+-oRbqmKbo z)!861w}_M?U7X*hG3JZRIPj=S#RQT>WW!G0HTi2#k?UuV$9v*V&7loF#NDw#n|%^Q zA3Rb5scUfWb$9y85;OTAq6Bvx5Ojk5Mb$^w=Z>;a!PE0rUD&Mze0*9$MFA-=5kn6| z_c``pH^LpyB+CO3eP@LEGn7Ah8Db1-POt}m>7Mvle~dlOAd3V9L^a)win!?W)tr2R zAT*wY^j}RjocaTsW1)h3=RN<|8b^G<{r>Rvnt^k`RCjXnSsq0pV=QHmH^YRflf0@@ zF=b2@005hGZ!wQjL7YYK$4fkB)M_9<&oxclKU69cuKn$CJYaky~gP6qPBpEM(+x7XJ0g8OYg?hlcue}jyVqS6QO7K-3%>T%V%pU?#K{s8Eh#N+JBvr rK%SK&5Lm2-VlJu#0%1N9@#YbTbsbGXy#WVcg|NfK@~`x*PUrt0<~?9E literal 0 HcmV?d00001 diff --git a/packages/shared-components/__vis__/linux/__baselines__/room/composer/MessageComposerUrlPreview/MessageComposerUrlPreview.stories.tsx/with-image-and-loads-of-text-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room/composer/MessageComposerUrlPreview/MessageComposerUrlPreview.stories.tsx/with-image-and-loads-of-text-auto.png new file mode 100644 index 0000000000000000000000000000000000000000..0a8aeb3816cc3a8eef5e646c44d1df002be41a68 GIT binary patch literal 28571 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 bC+@~IzZKlz*=_vic)}&dr^FqMF|Ya`69p*w literal 0 HcmV?d00001 diff --git a/packages/shared-components/__vis__/linux/__baselines__/room/composer/MessageComposerUrlPreview/MessageComposerUrlPreview.stories.tsx/with-image-and-site-icon-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room/composer/MessageComposerUrlPreview/MessageComposerUrlPreview.stories.tsx/with-image-and-site-icon-auto.png new file mode 100644 index 0000000000000000000000000000000000000000..8fc269dfbb8601f8d8a3471bdf1506c78b62034f GIT binary patch literal 25410 zcmZuacRbYp|96KX4UthYN=C_QAoEI+WHc%3$|sa0WUssWw9HDS%xe-M$zJzKNQI2- z6|zV6IOF$x-*Uc>-+y}E`~7@9_j*0=x^HBlvu@3nH7FEno$k@YCs8OC5(D=go0C z!zC@{PsAV7@k~J-3k??4etqjj-Gs(ulh<92PtSW*62;fnBtmbfi~8V!D4? zt(o)fo+yTHkUt2O9H%8vZHj)L^MmeQ^$OClCaXZW-}aSwVXH6unueZi-4qy1?2 zJJVu?rs2&Ifu`!d@xJt&x_CkY zQmjgfh1{BXm0I0TdH=mN67ud;U3qIogp!4F zh2d=Z#nkI(&6Ii`)?Xf*8%>&w4hzyPnc8vTfLChsDH|Id@Aid}0GB#*NyS#%SMHsH zmLhgZS=M}JB7XDt1@>AWkGwuVV%72G{%AYPhN79#&Q8thw=zoKA7ywIblQ(TYO#n6 zGI#o?bbN!~b)5ImK%2jxnVb2%A@_r@tv++J^_>RyZmplUxSHK3G5SW%S9Wn>r0HUD z;N}K=U4Hkk(WmOa~TVwafqvGbPfxw(&dKDLX$j4frX%c6w@ zuYEXpvhLArn8oAVXG(4sudN=1HMG4OcpueFu;9iwHN^-Q;1+N0cpx`hIeer2wJuM! zoG|MyDctvEquhSS04E;$yjek_IL+msoVmJYakrb61yg(tAzHB|b%T*yawhh-l!sE9 zJ}UI3OoR{W2N}Axxfs6BY0Y-*t=JG8;@zb++G`bZY4432uEiZi0jYHfo{dXjlD?L6 z-ajlqh*q^J8I;5pju$GLr#wrGks{Y@6m)fhID7qiIkPQ92` z?&#QlHm9T_dc1Ll{Da&XV%Qu7M)bDsABQX6jbJ+;i4nf_zj9g^@&%M!uPPSKICfqP zC{qe?8(fHfrtW3ndNnSVQ+CEwZK~HQrOtFZaG_u!Xr^F9m@D;B&VOy1y>}gdZV2;M zt|hdQ4d&bLYL;pk4J?SvU!VVbcl8k)?hj!xBSe-0L1Rm2`>T_K_8l(+WbgN=*$n)- z>uv~l;R7bSR57@QEs`q#i{9~4EaQ_w6(gx-faG&jZL}Ru7yvV40?3#bgU^< z(w-Bm?0a_Os%M!|h+tBIb>Hs^S%*2*+&)4^!BFC8P_mymIjYT4$ko<+bUY@g+Tegf z`2q4o{lb&vqj&!)5cHnAENP>e?>%us=S#Wj$YUYHm{qL>!!F6C#cxU!P5c%zJ3aMSS*tBv(4FZ^rOS)L$w zzhOfQf2xRqaERunz0vNwqrJyQ-JRw_6?}?A6|9OUx&|gP$G%B-X6T3r>4>=8*y>Vq z!m;L|PSKk%GnKgc-v!=9;SpaF>RNZ_;4B>6yr)~gcfQuS7UWWF5U3kvmD<@?olx7j zuPQ~sU$d{J)!fbJ;MC=cSI?}UglSA4sMtJKK0>;ku}H2R3?83euPmi8oh4VSqcQ)& z#=E_iWMWC-7(2^4c#o27cS{>NqYN~A`=Y%@{J+Z7XCOC$7+yo*H2f+1o=EF(b1fJr#$r_=8LA>jC1osNJ3}6pM{WN z>uj>Sdt=LJd*O!STOn$jHfYRUS9`2xFjuHM`n?*BV>mzMR$%VB zdVj9!g`5oQf`JH&kHlova?g%(<(YiD&P8oK-yH`Q)Z2M7+N9OZ{YzTC3e?M$hc=A% z8;@+XT5U79;Ity|SLvx3(oOPr8mk7#VheIFWf~hBFTVDJ{9mjs=Wb~>`FZKS z;k8`nt9ve_)w|C>p81mMS|0qSr%K_(OU=Gm?O4t2c@1wPp4u9xAEK3tCRzAY2z3I- zM0F#yge;0uc0^8Tdwr5Lj$P_6uhQW@MV=$SN-mHqcAF?sIVEw##`9yM*j!V|VN=h8 zre2?do^wsh`9D+4khK00$*bOydU5{W(Q^-m9rRePneINHD>aaik>Axh`pTwYSCy4mOy!g1uw*+cy-H)VkO^5OB2h$k?8&b4|iSx<7}G3+r|S z-Bw>H6>3WF)f-+sVrqKW)N8^mFerbxsIu+A&h%&Ua?yFi{f^_Gv?~jUN{vm$J37VY z$c-K03CZ`PqlbG(4Ec_ZIgBaIGZnND4WF6D!9zIVJvIhEjI6*G0!Ml?BJ%^c`HuikD92;_ljCOkGzSJSpK;oDtGO`Svrjc6P205TQOPm4t{t=(OqIsz zlejYxNe57|ZWnWMaxM-xXQgD?If8QK`D7wRI3Fc*^2zxpX$M?9IM#bUSGh4%HPgy7 z$Eq+mD`j@3NWq~oF{QRc=OWof<>JuP0R`9jnR?u~{l#yYqq+}d2UH)8fNXWjjx^}bR zd!_w~^|%H1t8??u_S!kVPnl9tNOrzDarn{o(0|n(7U!R7I#?Xuf)AZOr}gO6!|0In zi)zk;{7c?WgG(8y7f*1P$9TB*ik*@)2p+0!bMFn^zcp_=2PsTH$JxX<(x++N!#k}v z{yqy1uD^Us-^#n^+01;5!TBwo)hO@J`o?dx)RyaH2sJofj2JuVvh0u^=DOb}m%#fw zd1Ak8IdwMVm!$sv`#ciGGow5g=fB)faZwZC5hmMA8H!F$PL^akT8XKsT+E1y792=Q zwNO*=IXFL%uG5j->eBIXv%NxUj#8B&et5tkVD+${Xbxwy$U@4F@pSFo9vffHI{#X5 zD{-!C5N(}^9O~?fNZ#xeuhU)r?NGOA@WQ#s8`X~#LMmc-l+?T}6&Hsj=l7K7PR69v zjTYQ{l|=S38z<#G6eiEH#g@!>1-q+A`NXtdZXM5zDbcVI;SZ^Z2o<*I9&I1ooYH1# zaDO^#Y#?*=Avx@TTig4-(aCYckb>sfy@&zcAX9lx0}l(N!6x zcCj+jB-QP#n#a;mN?eYdM39J}y+idXOZqHe$UbGEHbQ(hL#)^^dW({|vrY4jUoxZb z-wl-Th0a+eWNLP6PT`>E*jwdiIP)~(ngPbu;p9JCyu}(H$(6Pped*?UO4&opjQ+K5AMt1Vp2njYNWt`ZWI=xiF1uIQHn%uamKOG4VH(!5 zks>1tg~^>hs`<+6y@ymgLTy9@-85!jk%Y9Amqr>kEO%Y3w7ERpX^@RHU3KZ+bV6<+^JOD(b@{}ZnsGd zZ&Ybi+wkI)iC5V#tKrDo3Yu9-d4AWLmqw(mx}Vq-r}-^7H+%IIIu^b03&|PI+@F*; z_pMhysRHBh(ZZ5DyLv<6vHX87C?7aI-Zn99Tdf-BCBAK}sG@PtaKr)is+K%`~MvmU}I=}EJwWrnB>S|`=76qUGax-m;J4fdy6@z&k zvOP;nH>$;wkFrd3rF4GGDV3|YO`RE<3=6_Zggrh=I?Lp}CI&cGFvO}2+5H3O1wuV- zN?tEQu4v+2XyJU7YU== zSx9{=xA$$LOk!`J7K=mEBTV&!=;%IMyAa2ysJ`!mm6ff+oU45Bzb4qmJ0BiM-M+54 z&uXDGp(NmuQS>_D|EfcE9~k(>9*C0mQB!d<;QOH?|3r`d@nMqUvN+UE5xY^5zOrZB zjE$i|mT!E-)%>f8zhx-&Rfd(*#16}*lH|?my7NLx>0*tcM)eJq!aT*UN4y?cmmD`y z=bLsZewve~QEZ=nDzNR!bY`MWko!xUi$A=&rh3igRl6&~H~QqYD7YTC3EW)0aY*;` zR`#b4e`IuR+`n`HyszWOw9l@&4fFO>nFE}f_u4F`f8@NjY4geUKXjjSI9ohtuv#pIeuA&~Eqp9ilCKjeG$mvH#$!cpPo?jX@6%NH6&EpGI=S)A++YXijQ;1Qdrq^^pNGV<}fCFQ@%`**gL!(ed99hu zS-nGl>w|-j8on=Ry_53aQ`IjKkGQh`xXRtz?95l336;|PGX;g6eA+wQ9(s9ZUo0Oy zr(oZ>@Nhx3(7dHG^j}gdxa7!uy7rEN!)1O|qR)Nj#v)=;3#;rh&U>{yQ@MQ1%wwrV zFXWBfu=iL>{e+ICf7n(>oY~S}pNDr!C){O1CQe0c$eDZTUgWUeKhIEYw{O9;&eJUK z4E^+PzW;a*+$snUnyt00xL4jKJKBCQ=h`QavrS|}b*I0--6x+`1hB|yDEWHMdgz6O z4D|+D=1sDsw7h8h(d{)|dbe(3XPutp*iGmi#JN7O(OO-SKkE@LJlU%9=B$F>?+z#j zD^;yeI!(J>9CJCQ=JmbIYIs7jBfZbCI8`zz#Qg2?`!bUH_x8!RT_>9>T==GvGRqfH zq@J-PT^tm%$?6emtJNdblJQs_2dBDEn(sb}Wm%~?cKmDW;P72oC`w4-ZR%bX6_rPS zoA@q23*NF6sFxHon^<`9_@0t>y`_|jj81V~0U_@})#L_g+4--&jwHJ_VPoHU!Ia^y zUmdpv9X9fN_v$r#=xdwTp4#uDGFdRCi|JGB`##fNE*GTLI^R0z;$`X;IolQ!JW}90 zGBa(n;YX68q4ytN2Y-jb#igpShC08&RKKk=*=}FX#LQS<{aD=CFiMEM(Q=u*pd9Sg zqm!z|GLU~^V5lQsZfQsU%txhv#@E+8@x|39#;D4#KUSN&{T(^Y)IP$z=RVSg?Gnc! zr9aZN979AY=9%sFQnJyi|4ziErLGg7+`i0Nbr-*1eJx~QBvaKOf0m$pq0vgg;p&%h z#d^`!yw3Hb=|buE2jtZE&gJkIu}rrJT9pI`c$Btu-sbGqo{;_4eZ0fo%%jXD<$7hu zQOj7DSk1-e>N>$Zb)QPr&uvTkJ@5XcOx8?xo+*v0-KSjV)Y6_ZWN$zEQ9ouO@Ta=_ zkN!@{FW;4#R8$&uYVQ1Z>Qb_5s7CI-b91g@Gv{|T5ZJ$_@XV=5bffSvNsns#} z>g>}m{m32jcc1x>{4&Sx!l$&fqWYsZhhAnigs4nUOch0DIlt26$SvJdTDLe@S=;tB zFle{bTL@VBZ)#)IrL)LQbGuH{#?f@5a8EEp30qT|@e`rX916zE1)6f?-+uXQl4I|C zW4F5+pSYVnzwGe+;T>77lT$~y&0k4+_$&n;7>zmL{4ya=PO;g&I`emWQ^)8IuIZ_X z!=?_#6PliOyPqu_-fb}axBQucn03W-LHkmn%HXM~-oe3p!R1rC=Z}oVYI@uV-W;RW zLw4{qR%+vo52ke8)LA&R7^Lg0jd%CjK3A}}uWYT{e5x*z`;@VW2+thfVPmJd3?+^n ztCAaEW)z|n6giUmTAKHg)y{^- z6mGH=>CU`#CrCtdn?#_H;9ikvzJQ*JAO#_zl-mAKt7hmFW`+WPN$3afe|x-KBR4)F zN6yoeZ1rD9l7Qy61BxCeUESP$omyTNjb9vErxf%HDpbXW`G%D8>;dgLulM#t2%{T? zN?+~ux#4>FwdSstYF-{mgQFH?J$k@J2JNRww@8248t-aO^gXnCgr93~$tVe}wvqUq zE(eoNex8zK;f1`s%3ooDb|UMDr|~axHld-%#l)jC86lqj-SDs zRTbbJkyCXR(zWNU%1oRme|1EA4VQj)H1#{|rK0bYaiH-L|AKvGgMzGt!FhEU{0DX6 z3zqv|5EB>s6>i()%QyD84?a^Ey(I!}t~vBWc?4=mk1#VnzqzKijvD8R`vJ+Bdt|fb z{b$B4D?(D2ZWu&5FIqkt5Bu`Q*h0uJzxUD1>bq>f_eZm`IUQqN^+lr@$c*XR8=k}BxVE;xi;@~t@r4uzYrT`6&vMtf5^37Bf4N=eTuWw z)siUJz@hXu*($51kxZ+>XTo~IP2$NJ^-kaGt()wv25cNQMmVH9Ud=D+HdIf)C*;<+ z)3tGDlta(e=o?9d5(CvCfhLEgc^J&O*tNVGT_G>5C|S3YTj9N@^ahO?Bg#z%zok=O z{OSz~9s84gPMNv?F4BtJ-0&hHDPJOsWwG#~xms_$kLF~Sd+&?8tA#E%e}A|oa3Z^S z=*tf##HV$MdB-C3r#)*MdX88+SZ?*3*X?Tx7ZF+fyV|O$$Klkti*z=wIPke6dC2X3 z@5G0YrSUt07k_`gT_~&>PAJ?z^TVcqt0d;{r}68_g8KGJ;>-6|ct~UM{P+qLNtl@# zmi#d`+gu%MX!%g@N0h44;!KHVNyUbROOdXPoApZGEcrS*7&`1e{<&LjgX(sez?*hf z$IN=eEUVdTNt=qSyvYp zWxCi`Yw8ft8LYa^cWT`7%cU^$l=;3lBh@*1l2P8r6o=;qma-s>;E^rBPitYdw0@I9;0NfDpHlrGyZQEsigJ(1%j znO#){C3X3WiGwB%A1hj8*ZR85B=5a-BY)qZLC#42)#EC4LZxZ^mfn{mJ^T9_Qd(n^ z^xD?Gcl(kY61X=s`bXq}z3P!!Z=K(|ymg6m`gR#cnSq1N@zIfz%mVzzx{DX@klWY& zOx5Aq;8X{H(Ra(Ks@?AEZl~H-zH~2)mMMg>vSic#qU9s;QWoDw7R1Fm_@U|&8muu4gZABl)G&R)LiV7yLI=U~%EZ?o~P(96RaT&LR@L?{%xowEAe?>srmF*<2HcJn;fQ z_4@X9t@S|;-UCY)`)l5fj_k-S=qqyw(h>I9k>2R^-Nb7s?P6+~Vvv2hhediZ$)zuX zFj;Hv)}l;~bha9CTY&mISDBM^cd^5GG&Dw-8*Ww3&JTSK=*Sx(<6bZE-SCVmS;H1d zzpkT>A%+r^E#s?gVtfp8JBJ(s($mx3N4#2jS9c^Xe#g^?D|Li6iY&B=qm|9w z;C>)LWU-wz(`=&|m2cktaIxYhOMZ^u{4}{$aJA4(i+i8Yn2T9D9uSt> zRixMEkUM(hp+*%;-_{Uuuj9!g?mwc{RiF3#b}zHD;7zqIE1#OoV_E<0&?Oxm9lpSf zw&B?nPwpGl2dYX`+h&8Gsd+ff?cpdJxU^I)id=49`8(g^kTcPHEhkTTIG^15X5_Bt z!&RgI4Wwmclpxo!YbUlx?HxMkX+P&#w!iM)KHJz4b(m=%B5h+_i(TOOw88E)$Er64 zg({Apztne~6ST?c>$MFD%NrDT>?gL3lo_Z^-f(y78ND-*9PB23@Nd{%A>q{v?Ing$ zn;XI=lDgZatBq7HSH1r4?nS3Djl5UhWA{t6=ahQ}t2^Nm!@~Obob~5_M-}`Q=O->r z9WC_PamSdaTUd;XE#&pUbZU2=>_SW5eICi(KAJTFFKYGtM7*kA-`85_76{dxH1G9Cw4zSl zjPQ-MJ+(qZX{wcM-w-nTR`XkgM*iQdisH>S(PX{fV5fM>Vf0=9(>EXKe-}VU|ABcKGkcVZSJ|CD&_A{R%n6ea%c+PFMxg=te_Ji0} zmM~Q>W}rS9D8L>a8|B+wmF3nnr0v*bpHbrXR$uSP5!d-~%Xz1%98!$y1G9N z{Bo5STAC+<-95|9-P-adV(xfCs;(t<*jQyx0uK+#BlfhhGIcXHnZJfw6QK4qi1RVRP(sZ_rc${<-89D3M$FU=6E+4ni}{y>|f`c?6qLpVGexw9pj>N|YrU)_pUDALiK`#o44+xGXZdM9sAfrRgLj#US* zoL|G<;=K7ka%Irv%6R3gN60Pg>q$&++#?vac+_={oGmi~GrpPKh`DD@2_Ke*+$)pGHhQpcHO53O9=jIffsp$l(C z>YwOmKZ>2ud0Z`$Wf0Z+w^-dOr^g|p?778Qzwyw9wy(f@AjxLS1IPCxWI=iXfaCNjkU)-VJG#Cbhdhs3$`z`H2v{N%AJ3N zT!)%;Uk88mo>pxO)>^-K;`P!$RZqe!x2leOvQq4xFNhSpeBD6vg?nS3+dm~$u{VPE z8j!W-_IP>Eor(z@nQzPZSl3tm)b#4cf~A&+qrZ{|`NPa^{8u&GI{#Mbi(|>6q7Zjy zsQhAp)`t61`@FHXfuj@y4MP5tQnUDIbE!ZIMWTzUT#xud7^LCm65gtI7XY?_(( z@7adcxP{4iP38|j>0z=ZdKkqox zn}%nR)rqZrA!{{VY)@NOiFma--W@WYA)I6hjV(xsSUjcQxymQ5mX!6MrLn2~vB8hu z%eBhuYHRuW?w)$%mb%(9bg=?I!4hln_on&wb#u2W-e`_REt*Wk*9v);kp`k%o41i| z0#>zd=ouh+Ue+(%?68(reB-brV^D73VJu4c{`sJ~+_5KeA&|@86eo;ndVl_x1G%D< zSh^kmfEFo`Rj6%P+`kyiU-sWuvIft14GPQoR&v^9z2>s2M$JDr95KCjo>!ON5*hKK z$)fU3XtaBmYtjy;k_+q1_Fi!pz)yS2N-)7u1Z$2!S0S|fr<+O4B^ruwh<%e9ivP38 zL#DEtOY{K?8m+uqU%-(=mKi;YaDkJZRQPs}yaT46`AQ5c1FK^)=Zm?&hTxAewUKcr zV9{*jkLwuLF;U_wddx>-(g00bL>4kQO62{ZLT_3~_h5Z&V@ej~GWxtYj=nrWLblBw ze#@e^%lj;$G^ZRwl7ihrgeE7czja$TV7Bmj9c>wp+4ZlmFy~dIhNTmlW1b9>~hWk@_ii^c(QRA2oz~?-} zgQ##(BN22e!U(nn;Mt+!beqTGK4B1B#EJp_BXQxOwC7<8@|Dd@7mvLK@KyhXKV`sC zgoo^zcd(rRFK!5bz6=L@;t*&tf(6JIxe_bQ`}dlCo{^|#+Y95!1|;JZtSqaOFy8sX zYj!3*v2#fg@TkZhiFi8g)HnJiL{W7?Rfi|}Ut2B!VH`0{&OAL`0(qzu5(r7Y2xBuHu^F#*r7Op_3CAgfw=qE)E}UsC!PUtLRTV1FK0G=pa>71 zz71f`Q5k&-9r5D03@Yw40NX7j(v)>to1}plKk*NMzncA6I?WYh%*>h#Fx~-T092*!>Nt}ANubmx?)(Jb^(@83g4Cu$mlrw4A0kO zprx%fjwxpnUUI}K)d5FIO|O)hg|bOUbOctoJw_hPF_0sF5M?pz){|V`RqlAMXx>Jj z1u&#Q`2$x2)m0pa@gBmd6(2kh1TK^kr7X9(GG$NTf&;kdzjOZE4xAj*3tgbD@^e~` zwTN3WuFEzr>IdX5dcNEa+{zMt=|mIEKo(h716~4^MrPYFK#jbkZQ zC?D?Z48v-$o?bR_qB|JMsOQ^@aEc7hhpq|2Au#dVS0i46E`O?^rRgT_4<^o^=G>38 zA%wH+CN#5?`T)q=yLVEnlzqmeD-`Jv>j8NRIo6YkmUs?ROUcWh!GLZ%yjVx4Zi^fa zxn_QR5M-srKSb&J5YmYcG2Iea;ts%~yXCUrvc$KD;m%_IY~;d!uhvs#3gE<6Ot0ma`0tlAN`KDU{?Vnzmq{kDqI8t7h2`%9XJzKy8jzQh98`5x+o?4e-D-&+ zfRGEOb@QJg3(|pzs{=dQ!_kcqJ8?2}0RoOSpf&zn!Z%5Jqe4?eKF~KW+=zxsoll-& z0N9;iv?o7RN+5=#jNVI^_&2B{!`_5WQB)ShB~P(+u+HsZorlXl@lp;nvP7p+oQ2>7 z4*sV znwF!w{2sLNMyX^Vy|uM*Gn2hR9cOt3XsH=WJHW&|`&vulpcq^GN4NUL6%rTaG0{eq z7rQT`rda50aD3z1Lp>{=CGG?AE}uWQ6!h%jvb@olOW?@gYxKR|(c2T6WS;|hAMZ4- zC(N-wTdq1`5t_AqY_cX8D)dDB03$jbgGxW_W1^xqsPG|YCVjwXbQZXIAC)GDa3gRZ zZ$~AGzztd!zm}9pm2)F&Ahi|$!w0mqSmw~Ww>#3ft%!6UTY$iQ0_^FH5p5!5#0JRM zn5XN|MMZeRp$wIlHjwDQuThaf9Q#_Cb}2$XxcINWv?!)>^FuCv`U&ta{p(r;bGRl| zW6pSqql^l;%iaSF8F|7u;d(&c%5g#!IQ<)Y`yoMRxF%kH z{@ZR4R94Q^*#LaXGL-$X8L;Sa%RYqkmo;!%okqUE;=Fpg7HkuXx2w?+Va0*PW1ku% zfJJIcLJdu4z>5>AE|V#_?*;2tnjCjQ6f=%%NT=qEr@ViuQ-afaFnTy!Fhr}V1-Sk` zL3FhyrT{|77&5)8&J(5sxtau4ziegk;Ag|6NeP8^8aZu1D`es7QYCfaL0& zRhe~&6^HC}EWPC|2Upxjd>(^y2;0VK<5N&~6RxbO-%mZ%=mKc!b6JK9a#mb8et}|U zgezd^zlIB1C@+Hv)>CTU(_qj^^BM5HO^OK+*I=moh#N5keR?i0ShK0#}bcYrar!h+WP%nHztzcbDuzA1c_WriDF0?xm+B-ggmEeR1R!5=v1 zU0U^FA)dXS7H28J30MT0zsce5E}zK_EW)J)sO=Kp2PK98pdI?ly}AN<&d30rLwAea z5KzNTx>NJ+Hwm036%h#sZusF&9ra&W57t4-poV=n*t6W~4v55*Pl#(VQxrlfU`P;i zxr3c9L2L@!X{am++d%8v&TVDXhN4EN(JnlqA0SD%YwroBn*lJ~PO<@#ooe>PC5r$5dQqLmGt zjXZp21^n+PJZk>aZR0=O9|Y;rLJrt12CNqTUPt)IUQa2w!!6W zOX30nb4jg-C`|l{xx#Qj0jRTt8;d9t2&uRDczPg}28kIT?PR6!A{CDJq<#Qlmz^YC z?MPeIkqX8iqkKTx1-5twb%5HSLV0}?zXkaK_Y01V4+^Au9x(Pb1K$tAk>3pxHaWhE zQK@o$O~7YE-beydH{03CB;l_ycz^a9sN(DJm!#+lM1(fBgZQ)&j&01Pr4*Xn2;S^9 zX17shd!KVMp#i?{Vg9TkX%FJV=}b~yAnt?+cIsI~>b)~79>)~{>HhYSJD=jOFu(BO zB#{0_&463VAFsmnfeF-4xx2Q(FzIUne$Da+Yqdbas~&Abn5VQJG+EpMTisAmDupQZ zYb-vVet02-&b#`TL@BI(jlg?SKOme3R?mM5q~)$LF(NnB%w9qA7JK%a&b%6X9Ci8+ zg@K*Yazet|vAu^m0W>GjEQd)LWO4}lVo#ifm?T`r|6x1f1fxNJRi6zR0+HR_%Uo4Jxqje!>vQO7RUp*rxsEuQXO#LS<)cD<6zKW#(yM~0q3%P zjQ4U)%LnVj+OH9SGIKl0 z!<-QCSYbAQUi`S!#96XAeHIYMJfH<<{TJ}vQbc8f+KeILi zj{%%FCV*p~9!Z5s%Yo3!3MUewQnby*^V01Z2EWjsB(c2Q36P&h(av;nvDfGTJml3m zf`Y6k(3z8THDc<{ZHH#`V!poaB24~=mt7d$OA^k08fgP5HpfPudo&efVaNv7Q^BuFxCv>MSr3I zj*fc^`3){c%oz3UPXT}{BvAxTc*v1SyR{JgDG%@qpCqWBvOHY-bc*dkT2x5<*!&UkUG=0)ccnkine<7WbhG~;#Y$}8{Yh6?7r3yv(lu0x3 zKKM0HiVQRwbStM!nw5-!oTQ9j%%Z{K?qjQC*xKKQn$lYbs`H)S_8=M6yasKz(kqav zdTZJ_>Tr*aANoghrenHwl0M0n+2Hp<>3dBoxs+qAF^PEuw?zi{KVLu3%KrX5iRto0 z9|nyP_^ZfwF5e#~G1@yqH}GHPn7#(#2I*61ToEq=pVe_P)Vl8}|6khASlGUWq4{xp zJDUF+a=KF`5srv`bF&j&?641t2q+wSZ>6?o+HD|;5YJhUbjjzT5zhFxA z2Cz7QkF{nqz^w67xC++uVCxxNZB@%)_S1T#Yo^iwu5IgQ0&~UrW4}-DL}lc%tywlX z5IWqjeNsdzS>0v2Orhuks;5ZNPyNX*Ob!N~?*ZJ8zfb)19>1bwwT%bvPhShzNN-lC z>Lyr2fm6npA-v*eK-fQFK}G0nafqhrcxnlbNcYnb00-rqffJ3+pvm+UKpivZ-9`u@ zdSYCUQ!Ri#W~R`GeZo3OX_D9(V`l18r?p73loyxB%oNhF4Qzu*#$XyVQ(t&&Oe%*Q z57D17W~RQdnQid?$}uzT1(5jvjG3t~oMUy$*sx;EOnqVBAz~IfbE7JK%0Uq~=t|%Y zWTLgNJ=ZC$9Ls-#vBtG1pI>LwEkQ_(aQ6wZ^PL4n31YZdW-!!<*IZV=H)J#~ln+H$ zgW}eQ6Ir3em%%OTZWg&_s9b#pku9Zvp?dFE$fA)EXQe2W`Zr0O4)p|VGa?SBk{R!@eKcEa&I8s(JdzovmA2BxHvlrg*LB?llEg?9XUtLkeFj5e2% z7#%Z;fPtXRX8VX6>UwJLLPhSs4W^W@4C5iUVlCKe=s8L%5NfGsXLnMTFvcW_a0P<+ z7x5?{f0+G-CTB@>8o~~Wa1*L%HoIp|RDD6`5{GPou=+!9z@KchixIERQ!X;4UIX&4 z={xD099W_Y<}3_);{f>y)!1vx=99?vL4tsRNXd3Bx{7WLK#6S<{+B;wE&d$8ct#J1 zs5q5%O+hfqY=*B9Ie$4MtkuEYK)WkTGhHE5-BT1s>n}NEnFI*zbbzdi`p&fOIE6iF$l`2J6cwh$|@%A4`QqnlekcA?vi%@PA4Mrd> z>d#&10lL+#0WI^VyR1RjA#`(^&?cTz9|3vS@H^UQ{S@guRT1rg{6E1sZ<<~XV9ruT z>m4AgqLCLu6t#c^w#eX+d#lHXz?QWI?I~KJ5a)=o28r(mEay}%Q*QpKlyBLFGr)*L zgQ8b++?2_|iTN07X{5kU@@-tv%)lZ{HY~zarPbV77B?J^lOi-?{u=F7W(rP-PS$|L z>wfOgV$vqqRVEPP2*2I*Es<6vQ|;;yuygxbOh<$yPMk@d{<0veyj%T}ue(S&SsW!H>6m;-?&F zWQcaAL|wuouyJv_xFOnklN;qZ5fgiC2%@z#9Ab>t&!g+W*@=&Vqn%AxmhUCS_^*q@ zvE_r`N%`eX=Z2;As8E-C2Q%sRRo=?e#lal-zK`8A-Wc_B@eiFx+5#>9zEka$M@ zsue;MEda;SWq*zBd>n<1g6qGNY?hDiN4$ ztZk%hmDI?#uc2~9`~g^==h!(=Et|3WQVQYvky8B>)vO4=IRq%t=?_%p-`lKI>C>+F z$4DN4=rovk(%}eu`Wzn%gO74xlouG?6_swdEI};}U=%caSJLI_MCsXyC*EdsFr{$^JyeFSj~<;(m~ zRbsABdR{A8!8sDYOVgY7(LyY3-{lNY9q-zj&)_hAC$5fl*KU|SXU?V@Fr)-O**0Zr z55zStWOyxe#~LqYG#j;s^kj(-wH!qu817;}3=`?zRLbwxAt3}Y2;vy#iRF{mo9MHE z^`s{w8Y?E0r;mbIj8emz;xkqd{}_mW=h`$_jj>oRr$mkR13_1WGAuFg@XK>Z!WszH zyjQbjc;d403yd6o8tLGz+lIndF1>sNSzR)9=!E0qUgFhhkDq}-SVFUp7bUP?;i#uh zb>lLDj8O+v6E%IE+4zBKp`s|VFygk9&|*nij|fu~?nro8{TOonwi}1|iV1W?a*ls# zwGbkcNQ^6t*^$BC&fAnSla2`XUD($G9joo!nXQ#D#$I3Q1oJ^B(N=~zA%Ot9nKe3V zHG1Z`L@@GCy3p}h4mSAqk6yi|>2mynj|mq*OojjM`cK|;#nW%VB93Jg9E-?6R%hv$ z;vsq)FQ}3h6Sm>Pmci`*q6Q-X7t^m&!M~uCH{eBnio?hPt;$%DbU%hg`L6j)3DHuo z@al9$MgxyA>3^y9(_?5qXtCoG822URTzU~s+jYU>JU~<)8%OApE#m*cf)FYIu|ybC zw2e+QT=B*s30Mo#kyef}eb5FHe};*dz~XL$2-jR@{+owu-6e`jeq5qUb6VPx z3mTIE6^5~R7yWurbP2NyiRXk1Ku#fc5A_-Z&JeA@Tmh}a6(ei=?*p4D34n5jXbdKP zI~E5U27f=iTT9Oc$Q#HOvk^3@W&ux`uPeEbE#@nbnCw{h(nBQGvd2V?yw+i^4_PNM z8!uGMYaZ`H@&B|eU7?SkJ0MW#{?s=q=3W7ZVJg~PT-ktE7n{iZ0sQ0>D%VNGhGUZ! zgx+Ocy@z3xh4@1>Y_6EY(3A1O9jHn+=#(O1pW27eG*}3v-{rVO$(_r)F0d8}Hd&al zhnn`f^xztjf(ok-_MvaKiWY?E%Pw4TXCQQ)`Qc78ZR`NsX^IrYAh+n?yQ#x?L=$J% zQ1%GL;FeLRyL%Vi6=1h}BkY=D#K6;Zj`ti|hO>st;M8G(0N}5B&NIQ;_ZgCoy#n%e zIC`C!;as+NNWFCccd%tzBM17W@YokD*U{cjjxL8DaK8h9+qvrA#>c-IWKyz0+(F<8 z{r#sJN8IwYKRllX#Y~#VW_?i+OgxiniPoS&>plEX4PHSHFoHUA*^g4=-d~59Hl-n< z9WL+!;D?mM5I2Th&-c1?IA~VQN!6GVIbmbegtlQU05>RVyEOP|n9I07tf!C(+XpDM z^9PaYkb;4>_QDl+3ygL3;Zrn4!vOarsTrgY2c5Q=h)ya4&Se{jKL?W`h>~VX$9R~+ zijf0)d?RdYqs$ETspTr=L}0`hV;!Fga2um)rsshXb=H2(yC^fiQ+roN{`G8Yc=67? zu{&01ineE_{V(;NjGhp1o5261{+cx&N7Ki96!XK~0C?}bnLd%$n9#_f?d$2o`LEc1 z7&5d=6b5Tm-&6*rC83z(8Dlq=NR4v(TF8^j+b;-PnClC>%MMd8^h@Iusnima0B(Lj zpVo8K=#kVY^uS^e=6arX8NCvPtBuWwn-I|eXED;Jt5j5e3mUl}fiXVjx|Lbka-_%o zgGt(sC0+n{p{@RM&c(y6O)-xxR7A~3&6n>405@(1b3_rs;x+jXeJdh8P8ky>u&C+6f1#; zNwlbfXa}B8-A}z?iDL|yi7!AfS|0keDvvQ>{%H%oR{0tEJ_f~R=mTckn;BYX-R zBPr1Mg|C6Iws)U1rCt<4qe2_){X7kPzH8>A@wtlCo0@maAUa3LTvMmN&Ppk3Y$16! zV{Yit9X_725W{5~1mG=h33M>+r>1S619*=oCp}z2K#Il-@z}lr@G&2|Wib5K8`{er z8rbHJRVMVGuBPbw7nkjVEL(f+G?s0db@02Ntc0hl+9d`nR;Ey_C}?@_ou_K&Xy5&z z1GcL;H}JhE5Q?X15Q!U2*|?zD*gkWRxk?t>3a2CTg5soW5_R#| zCv1cCuTx-&+5iq{m(c?|Sl*05;yPa2KLM9g$I@5M{iKy#_jN{&WbrVW%yc$;>e&P} z@F0)gn_D2Er{eP%>8I;3Ol@_=9wWitX-z%-&RkRzr0DDUf|A>%BJc||DP)05IR9E- zMENJuv2ajMfRzGNWJYit?Y2$o7G52B0=piJVymLIE}e*roN38l^C!##|T9c?nnJ)D3+amPp782kZks!ape+`ZS7Yvt>$hK{oV}g(jei zK3k^l$86HUzM+u1JX@yx6IvF^CIr)LnTmFn&j<-n{t)D7vt=4u%5uz#*)oMZ>>}&w z!z*XYRDmJ8F)L@wR5l-MAatDB#yndd71r#5<+Nnv{}G_Eypg?TKPm+0gDg%^Wk&gf za(z2#rAYaKV8%zZs}%n;;VZUe9?-5*%ulc~T?F81q6aaX?oP|1>9m6 z)%CR@ItJ7#Owv5hgUg5ok;Wz?K#r?^Os`376Y;+w^nsSqna_MqF*aMU39Rq+@YoAL z`R8;4`l1bB2$#cYW3(Y-1T?FsF@^P?P}K24{*rZPd9qxp0Sn#ioBVjunKSC2C~1Wu zU4c8KILu%9ZY4(hMl;nz&`x^@!uS8EWNor&$f(_bT&PL@+zz%Ug<}7`@zX`6~ zWv`8#rUdXZz7O;pBBV9U}X;C&QxS^mfX1f{EOa5(7@?&kjixt=O+ literal 0 HcmV?d00001 diff --git a/packages/shared-components/__vis__/linux/__baselines__/room/composer/MessageComposerUrlPreview/MessageComposerUrlPreview.stories.tsx/with-image-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room/composer/MessageComposerUrlPreview/MessageComposerUrlPreview.stories.tsx/with-image-auto.png new file mode 100644 index 0000000000000000000000000000000000000000..64d38d1a49a2ae02c844baad307c3c3d1db4e16b GIT binary patch literal 24853 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 literal 0 HcmV?d00001 diff --git a/packages/shared-components/src/index.ts b/packages/shared-components/src/index.ts index 9c4106bf14..e45a914f36 100644 --- a/packages/shared-components/src/index.ts +++ b/packages/shared-components/src/index.ts @@ -14,6 +14,7 @@ export * from "./core/AvatarWithDetails"; export * from "./core/roving"; export * from "./room/composer/Banner"; export * from "./room/composer/UploadButton"; +export * from "./room/composer/MessageComposerUrlPreview/index.ts"; export * from "./crypto/SasEmoji"; export * from "./menus/UserMenu"; export * from "./notifications/NotificationBadgeView"; diff --git a/packages/shared-components/src/room/composer/MessageComposerUrlPreview/MessageComposerUrlPreview.module.css b/packages/shared-components/src/room/composer/MessageComposerUrlPreview/MessageComposerUrlPreview.module.css new file mode 100644 index 0000000000..4db55d85e0 --- /dev/null +++ b/packages/shared-components/src/room/composer/MessageComposerUrlPreview/MessageComposerUrlPreview.module.css @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2025 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 { + container-type: inline-size; + container-name: banner; + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--cpd-space-4x); + padding: var(--cpd-space-3x) var(--cpd-space-4x); + border-top: 1px solid var(--cpd-color-gray-400); + + > div { + display: flex; + gap: var(--cpd-space-4x); + align-items: center; + } + + .text { + display: flex; + flex-direction: column; + > a { + color: var(--cpd-color-text-action-accent); + font-weight: 400; + font-size: 13px; + } + > .description { + margin: 0; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 1; + line-clamp: 1; + overflow: hidden; + /* Cap the max width of a line so it doesn't go on forever. */ + max-width: 40em; + } + } +} + +.image { + width: 40px; + height: 40px; + border-radius: 6px; + object-fit: scale-down; +} diff --git a/packages/shared-components/src/room/composer/MessageComposerUrlPreview/MessageComposerUrlPreview.stories.tsx b/packages/shared-components/src/room/composer/MessageComposerUrlPreview/MessageComposerUrlPreview.stories.tsx new file mode 100644 index 0000000000..557df63edb --- /dev/null +++ b/packages/shared-components/src/room/composer/MessageComposerUrlPreview/MessageComposerUrlPreview.stories.tsx @@ -0,0 +1,110 @@ +/* + * 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 } from "react"; + +import type { Meta, StoryFn } from "@storybook/react-vite"; +import siteIconFile from "../../../../static/element.png"; +import imagePreviewFile from "../../../../static/wideImage.png"; +import { MessageComposerUrlPreviewView, type MessageComposerUrlPreviewSnapshot } from "./MessageComposerUrlPreview"; +import { useMockedViewModel } from "../../../core/viewmodel"; +import { LinkedTextContext } from "../../../core/utils/LinkedText"; +import { withViewDocs } from "../../../../.storybook/withViewDocs"; + +type MessageComposerUrlPreviewWrapperProps = MessageComposerUrlPreviewSnapshot; + +const MessageComposerUrlPreviewViewWrapperImpl = ({ + ...rest +}: MessageComposerUrlPreviewWrapperProps): JSX.Element | null => { + const vm = useMockedViewModel(rest, {}); + return ( + + + + ); +}; + +const MessageComposerUrlPreviewViewWrapper = withViewDocs( + MessageComposerUrlPreviewViewWrapperImpl, + MessageComposerUrlPreviewView, +); + +export default { + title: "Composer/MessageComposerUrlPreview", + component: MessageComposerUrlPreviewViewWrapper, + tags: ["autodocs"], + args: {}, + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/sI9A2kV2K4xeiyqJsL7Ey3/Link-Previews?node-id=243-8476&t=NNrkR2fxEeVenjbU-0", + }, + }, +} satisfies Meta; + +const Template: StoryFn = (args) => ( + +); + +export const Default = Template.bind({}); +Default.args = { + preview: { + title: "A simple title", + description: "A simple description", + link: "https://matrix.org", + siteName: "matrix.org", + showTooltipOnLink: false, + }, +}; + +export const WithImage = Template.bind({}); +WithImage.args = { + preview: { + ...Default.args.preview!, + image: { + imageThumb: imagePreviewFile, + imageFull: imagePreviewFile, + alt: "The element logo", + playable: false, + }, + }, +}; +export const WithImageAndSiteIcon = Template.bind({}); +WithImageAndSiteIcon.args = { + preview: { + ...Default.args.preview!, + siteIcon: siteIconFile, + image: { + imageThumb: imagePreviewFile, + imageFull: imagePreviewFile, + alt: "The element logo", + playable: false, + }, + }, +}; + +export const WithImageAndLoadsOfText = Template.bind({}); +WithImageAndLoadsOfText.args = { + preview: { + ...Default.args.preview!, + description: `Molestiae aliquam quos possimus molestiae id sit nulla rerum. Sunt cumque illum alias. Illo ipsa ut iure quia nulla magnam repellat. + + Esse velit corporis sapiente temporibus quia ipsam. Pariatur est rem veritatis. Inventore sit consequatur odio ipsa error non assumenda. Est eum ex dignissimos voluptatibus voluptatem delectus modi. Nisi quia eius ea quibusdam. Aut eveniet maxime non. + + Impedit qui minus soluta cupiditate. Illo blanditiis sint et dolores rem consequuntur rerum ut. Delectus tempore dolorem veritatis odit enim ut dolores. Sit quae rerum explicabo consequatur tenetur. Labore in doloremque libero. + + Incidunt ut ea quae nobis. Reiciendis inventore quas qui eum voluptatem ex et qui. Adipisci quibusdam dolores hic inventore et suscipit cupiditate consequuntur. + + Temporibus similique sint quo. Omnis tempora quidem explicabo in quidem magnam quia. Aut sunt accusantium ut et ut laborum debitis in. Enim nihil sit consectetur facilis quidem voluptatem. Quod impedit odit veritatis est laudantium tempore sit labore. Atque minima aliquam nostrum et.`, + image: { + imageThumb: imagePreviewFile, + imageFull: imagePreviewFile, + alt: "The element logo", + playable: false, + }, + }, +}; diff --git a/packages/shared-components/src/room/composer/MessageComposerUrlPreview/MessageComposerUrlPreview.test.tsx b/packages/shared-components/src/room/composer/MessageComposerUrlPreview/MessageComposerUrlPreview.test.tsx new file mode 100644 index 0000000000..fc4022b2f8 --- /dev/null +++ b/packages/shared-components/src/room/composer/MessageComposerUrlPreview/MessageComposerUrlPreview.test.tsx @@ -0,0 +1,26 @@ +/* + * 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 "./MessageComposerUrlPreview.stories.tsx"; + +const { Default, WithImage } = composeStories(stories); + +describe("MessageComposerUrlPreview", () => { + it("renders a preview", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + it("renders a preview with an image", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/packages/shared-components/src/room/composer/MessageComposerUrlPreview/MessageComposerUrlPreview.tsx b/packages/shared-components/src/room/composer/MessageComposerUrlPreview/MessageComposerUrlPreview.tsx new file mode 100644 index 0000000000..4e34927fd0 --- /dev/null +++ b/packages/shared-components/src/room/composer/MessageComposerUrlPreview/MessageComposerUrlPreview.tsx @@ -0,0 +1,57 @@ +/* + * 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 } from "react"; +import { Text } from "@vector-im/compound-web"; +import classNames from "classnames"; + +import { type UrlPreview } from "../../timeline/event-tile/UrlPreviewGroupView"; +import styles from "./MessageComposerUrlPreview.module.css"; +import { LinkSiteName, LinkTitle } from "../../timeline/event-tile/UrlPreviewGroupView/LinkPreview/LinkPreview"; +import { useViewModel, type ViewModel } from "../../../core/viewmodel"; + +/** Snapshot data for rendering a URL preview attached to the composer. */ +export interface MessageComposerUrlPreviewSnapshot { + /** URL preview to render. */ + preview: UrlPreview | null; +} + +/** Props for MessageComposerUrlPreviewView. */ +export interface MessageComposerUrlPreviewProps { + /** + * The view model for the component. + */ + vm: ViewModel; + /** + * Extra CSS classes to apply to the component. + */ + className?: string; +} + +/** + * MessageComposerUrlPreviewView renders a preview of a single URL above the messasge composer. + */ +export function MessageComposerUrlPreviewView({ vm, className }: MessageComposerUrlPreviewProps): JSX.Element | null { + const { preview } = useViewModel(vm); + if (!preview) { + return null; + } + return ( +
+
+ {preview?.image?.imageThumb && ( + {preview.image.alt} + )} +
+ + + {preview?.description} +
+
+
+ ); +} diff --git a/packages/shared-components/src/room/composer/MessageComposerUrlPreview/__snapshots__/MessageComposerUrlPreview.test.tsx.snap b/packages/shared-components/src/room/composer/MessageComposerUrlPreview/__snapshots__/MessageComposerUrlPreview.test.tsx.snap new file mode 100644 index 0000000000..3872de585c --- /dev/null +++ b/packages/shared-components/src/room/composer/MessageComposerUrlPreview/__snapshots__/MessageComposerUrlPreview.test.tsx.snap @@ -0,0 +1,80 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`MessageComposerUrlPreview > renders a preview 1`] = ` +
+
+
+
+
+ + matrix.org + +
+ + A simple title + +

+ A simple description +

+
+
+
+
+`; + +exports[`MessageComposerUrlPreview > renders a preview with an image 1`] = ` +
+
+
+ The element logo +
+
+ + matrix.org + +
+ + A simple title + +

+ A simple description +

+
+
+
+
+`; diff --git a/packages/shared-components/src/room/composer/MessageComposerUrlPreview/index.ts b/packages/shared-components/src/room/composer/MessageComposerUrlPreview/index.ts new file mode 100644 index 0000000000..b1d28063ff --- /dev/null +++ b/packages/shared-components/src/room/composer/MessageComposerUrlPreview/index.ts @@ -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 * from "./MessageComposerUrlPreview"; diff --git a/packages/shared-components/src/room/timeline/event-tile/UrlPreviewGroupView/LinkPreview/LinkPreview.module.css b/packages/shared-components/src/room/timeline/event-tile/UrlPreviewGroupView/LinkPreview/LinkPreview.module.css index 4027e8281a..ee2b667143 100644 --- a/packages/shared-components/src/room/timeline/event-tile/UrlPreviewGroupView/LinkPreview/LinkPreview.module.css +++ b/packages/shared-components/src/room/timeline/event-tile/UrlPreviewGroupView/LinkPreview/LinkPreview.module.css @@ -87,31 +87,31 @@ button.preview { flex: 1; overflow: hidden; /* cause it to wrap rather than clip */ } +} - .title, - .description { - display: -webkit-box; - -webkit-box-orient: vertical; - overflow: hidden; - white-space: normal; - line-clamp: 2; - -webkit-line-clamp: 2; - margin: 0; - } - - .title { - color: var(--cpd-color-text-primary); - text-decoration-line: none; - } - - .siteName { - margin-top: var(--cpd-space-1x); - vertical-align: middle; - display: flex; - gap: var(--cpd-space-1-5x); - > * { - /* Center everything */ - margin: auto 0; - } +.siteName { + margin-top: var(--cpd-space-1x); + vertical-align: middle; + display: flex; + gap: var(--cpd-space-1-5x); + > * { + /* Center everything */ + margin: auto 0; } } + +.title, +.description { + display: -webkit-box; + -webkit-box-orient: vertical; + overflow: hidden; + white-space: normal; + line-clamp: 2; + -webkit-line-clamp: 2; + margin: 0; +} + +.title { + color: var(--cpd-color-text-primary); + text-decoration-line: none; +} diff --git a/packages/shared-components/src/room/timeline/event-tile/UrlPreviewGroupView/LinkPreview/LinkPreview.tsx b/packages/shared-components/src/room/timeline/event-tile/UrlPreviewGroupView/LinkPreview/LinkPreview.tsx index 4dc9406316..d8cfc6cec2 100644 --- a/packages/shared-components/src/room/timeline/event-tile/UrlPreviewGroupView/LinkPreview/LinkPreview.tsx +++ b/packages/shared-components/src/room/timeline/event-tile/UrlPreviewGroupView/LinkPreview/LinkPreview.tsx @@ -21,7 +21,7 @@ export interface LinkPreviewActions { export type LinkPreviewProps = UrlPreview & LinkPreviewActions; -function LinkTitle({ +export function LinkTitle({ title, showTooltipOnLink, link, @@ -44,7 +44,7 @@ function LinkTitle({ return showTooltipOnLink ? {anchor} : anchor; } -function LinkSiteName({ siteIcon, siteName }: { siteIcon?: string; siteName: string }): JSX.Element { +export function LinkSiteName({ siteIcon, siteName }: { siteIcon?: string; siteName: string }): JSX.Element { return (
{siteIcon && } diff --git a/packages/shared-components/src/room/timeline/event-tile/UrlPreviewGroupView/UrlPreviewGroupView.tsx b/packages/shared-components/src/room/timeline/event-tile/UrlPreviewGroupView/UrlPreviewGroupView.tsx index 2a627fa20d..15699d1b06 100644 --- a/packages/shared-components/src/room/timeline/event-tile/UrlPreviewGroupView/UrlPreviewGroupView.tsx +++ b/packages/shared-components/src/room/timeline/event-tile/UrlPreviewGroupView/UrlPreviewGroupView.tsx @@ -100,7 +100,12 @@ export function UrlPreviewGroupView({ vm, className }: UrlPreviewGroupViewProps)
{previews.map((preview) => ( - vm.onImageClick(preview)} {...preview} /> + vm.onImageClick(preview)} + {...preview} + image={preview.image} + /> ))} {toggleButton}