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
This commit is contained in:
Will Hunt
2026-06-30 16:58:03 +00:00
committed by GitHub
parent bbbd050f50
commit be91d19e31
29 changed files with 1386 additions and 600 deletions
@@ -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();
});
});
}
});