Use url preview bundle for URL preview in timeline (MSC4095) (#34170)

* use url preview bundle preview content in timeline

* fixed linting errors

* claude wrote a test!

* moved reading settings from the view model into the component

* claude wrote more tests

* applied reviews
This commit is contained in:
Sirius
2026-07-17 10:18:10 +00:00
committed by GitHub
parent adbe90ceb7
commit 63e65fc60a
5 changed files with 384 additions and 16 deletions
@@ -8,9 +8,10 @@
import { expect } from "@jest/globals";
import type { MockedObject } from "jest-mock-vitest-adapter";
import type { MatrixClient } from "matrix-js-sdk/src/matrix";
import { MsgType, type MatrixClient } from "matrix-js-sdk/src/matrix";
import {
BUNDLED_LINK_PREVIEWS,
MAX_PREVIEWS_WHEN_LIMITED,
UrlPreviewGroupViewModel,
} from "../../../src/viewmodels/message-body/UrlPreviewGroupViewModel";
import type { UrlPreview } from "@element-hq/web-shared-components";
@@ -25,9 +26,46 @@ const BASIC_PREVIEW_OGDATA = {
"og:site_name": "Example.org",
};
function getViewModel(
{ mediaVisible, visible, showPreview } = { mediaVisible: true, visible: true, showPreview: true },
): {
const BUNDLE_PREVIEW_ONE = {
"matched_url": "https://example.org/1",
"og:title": "Bundled one",
"og:description": "First bundled preview",
"og:url": "https://example.org/1",
};
const BUNDLE_PREVIEW_TWO = {
"matched_url": "https://example.org/2",
"og:title": "Bundled two",
"og:description": "Second bundled preview",
"og:url": "https://example.org/2",
};
const BUNDLE_PREVIEW_THREE = {
"matched_url": "https://example.org/3",
"og:title": "Bundled three",
"og:description": "Third bundled preview",
"og:url": "https://example.org/3",
};
const BUNDLE_PREVIEW_WITH_IMAGE = {
"matched_url": "https://example.org/image",
"og:title": "Bundled with image",
"og:image": IMAGE_MXC,
"og:image:type": "image/png",
"og:image:width": 128,
"og:image:height": 128,
};
function getViewModel({
mediaVisible = true,
visible = true,
showPreview = true,
urlPreviewBundleEnabled = true,
content,
}: {
mediaVisible?: boolean;
visible?: boolean;
showPreview?: boolean;
urlPreviewBundleEnabled?: boolean;
content?: object;
} = {}): {
vm: UrlPreviewGroupViewModel;
client: MockedObject<MatrixClient>;
onImageClicked: jest.Mock<void, [UrlPreview]>;
@@ -49,9 +87,11 @@ function getViewModel(
type: "m.room.message",
content: {
...(showPreview ? undefined : { [BUNDLED_LINK_PREVIEWS]: [] }),
...content,
},
id: "$id",
}),
urlPreviewBundleEnabled,
});
return { vm, client, onImageClicked };
}
@@ -96,7 +136,12 @@ describe("UrlPreviewGroupViewModel", () => {
]);
});
it("should hide preview when invisible", async () => {
const { vm, client } = getViewModel({ visible: false, mediaVisible: true, showPreview: true });
const { vm, client } = getViewModel({
visible: false,
mediaVisible: true,
showPreview: true,
urlPreviewBundleEnabled: false,
});
const msg = document.createElement("div");
msg.innerHTML = '<a href="https://example.org">Test</a>';
await vm.updateEventElement(msg);
@@ -104,7 +149,12 @@ describe("UrlPreviewGroupViewModel", () => {
expect(client.getUrlPreview).not.toHaveBeenCalled();
});
it("should ignore media when mediaVisible is false", async () => {
const { vm, client } = getViewModel({ mediaVisible: false, visible: true, showPreview: true });
const { vm, client } = getViewModel({
mediaVisible: false,
visible: true,
showPreview: true,
urlPreviewBundleEnabled: false,
});
client.getUrlPreview.mockResolvedValueOnce({
"og:title": "This is an example!",
"og:type": "document",
@@ -178,7 +228,12 @@ describe("UrlPreviewGroupViewModel", () => {
expect(vm.getSnapshot()).toMatchSnapshot();
});
it("should hide a preview if the message requests it", async () => {
const { vm, client } = getViewModel({ showPreview: false, mediaVisible: true, visible: true });
const { vm, client } = getViewModel({
showPreview: false,
mediaVisible: true,
visible: true,
urlPreviewBundleEnabled: false,
});
client.getUrlPreview.mockResolvedValueOnce(BASIC_PREVIEW_OGDATA);
const msg = document.createElement("div");
msg.innerHTML = '<a href="https://example.org">Test</a>';
@@ -207,4 +262,122 @@ describe("UrlPreviewGroupViewModel", () => {
await vm.updateEventElement(msg);
expect(vm.getSnapshot().previews).toHaveLength(item.hasPreview ? 1 : 0);
});
describe("bundled link previews (MSC4095)", () => {
it("should render bundled previews when the message is text and the bundle is enabled", async () => {
const { vm, client } = getViewModel({
urlPreviewBundleEnabled: true,
content: {
msgtype: MsgType.Text,
[BUNDLED_LINK_PREVIEWS]: [BUNDLE_PREVIEW_ONE, BUNDLE_PREVIEW_TWO],
},
});
const msg = document.createElement("div");
msg.innerHTML = '<a href="https://example.org/1">Test1</a><a href="https://example.org/2">Test2</a>';
await vm.updateEventElement(msg);
const { previews } = vm.getSnapshot();
expect(previews).toMatchObject([
{
link: "https://example.org/1",
title: "Bundled one",
description: "First bundled preview",
siteName: "example.org",
ogUrl: "https://example.org/1",
},
{
link: "https://example.org/2",
title: "Bundled two",
description: "Second bundled preview",
siteName: "example.org",
ogUrl: "https://example.org/2",
},
]);
// Bundled previews are provided inline and must not trigger network fetches.
expect(client.getUrlPreview).not.toHaveBeenCalled();
});
it("should render an image for a bundled preview", async () => {
const { vm, client } = getViewModel({
urlPreviewBundleEnabled: true,
content: {
msgtype: MsgType.Text,
[BUNDLED_LINK_PREVIEWS]: [BUNDLE_PREVIEW_WITH_IMAGE],
},
});
// eslint-disable-next-line no-restricted-properties
client.mxcUrlToHttp.mockReturnValue("https://example.org/image/src");
const msg = document.createElement("div");
msg.innerHTML = '<a href="https://example.org/image">Test</a>';
await vm.updateEventElement(msg);
const { previews } = vm.getSnapshot();
expect(previews).toHaveLength(1);
expect(previews[0].image).toMatchObject({
mxcImageFull: IMAGE_MXC,
imageType: "image/png",
width: 128,
height: 128,
});
expect(client.getUrlPreview).not.toHaveBeenCalled();
});
it("should limit bundled previews and reveal the rest when the limit is toggled", async () => {
const { vm, client } = getViewModel({
urlPreviewBundleEnabled: true,
content: {
msgtype: MsgType.Text,
[BUNDLED_LINK_PREVIEWS]: [BUNDLE_PREVIEW_ONE, BUNDLE_PREVIEW_TWO, BUNDLE_PREVIEW_THREE],
},
});
const msg = document.createElement("div");
msg.innerHTML =
'<a href="https://example.org/1">Test1</a><a href="https://example.org/2">Test2</a><a href="https://example.org/3">Test3</a>';
await vm.updateEventElement(msg);
let snapshot = vm.getSnapshot();
expect(snapshot.previews).toHaveLength(MAX_PREVIEWS_WHEN_LIMITED);
expect(snapshot.previewsLimited).toBe(true);
expect(snapshot.overPreviewLimit).toBe(true);
await vm.onTogglePreviewLimit();
snapshot = vm.getSnapshot();
expect(snapshot.previews).toHaveLength(3);
expect(snapshot.previewsLimited).toBe(false);
expect(client.getUrlPreview).not.toHaveBeenCalled();
});
it("should fetch previews instead of using the bundle when the bundle setting is disabled", async () => {
const { vm, client } = getViewModel({
urlPreviewBundleEnabled: false,
content: {
msgtype: MsgType.Text,
[BUNDLED_LINK_PREVIEWS]: [BUNDLE_PREVIEW_ONE],
},
});
client.getUrlPreview.mockResolvedValueOnce(BASIC_PREVIEW_OGDATA);
const msg = document.createElement("div");
msg.innerHTML = '<a href="https://example.org/1">Test1</a>';
await vm.updateEventElement(msg);
const { previews } = vm.getSnapshot();
expect(client.getUrlPreview).toHaveBeenCalledWith("https://example.org/1", expect.anything());
// The fetched preview wins over the ignored bundle entry.
expect(previews).toMatchObject([{ title: "This is an example!" }]);
});
it("should fetch previews instead of using the bundle when the message is not a text message", async () => {
const { vm, client } = getViewModel({
urlPreviewBundleEnabled: true,
content: {
msgtype: MsgType.Notice,
[BUNDLED_LINK_PREVIEWS]: [BUNDLE_PREVIEW_ONE],
},
});
client.getUrlPreview.mockResolvedValueOnce(BASIC_PREVIEW_OGDATA);
const msg = document.createElement("div");
msg.innerHTML = '<a href="https://example.org/1">Test1</a>';
await vm.updateEventElement(msg);
const { previews } = vm.getSnapshot();
expect(client.getUrlPreview).toHaveBeenCalledWith("https://example.org/1", expect.anything());
expect(previews).toMatchObject([{ title: "This is an example!" }]);
});
});
});