Sending URL Preview Bundles (MSC 4095) (#34150)

* Fetches link previews for all links in message (instead of just the first one)

* message url component to display preview for multiple urls

* Revert visible behaviour back to only showing one URL preview

While fetching all link previews in the text message.

* Moved URL preview VM to MessageComposer

* Added com.beeper.linkpreviews to messages sent with the markdown compositor

* MSC 4452 implemented for the older (markdown) composer

* claude told me to use logger instead of console

* Previews generated with snapshot instead of vm

* moved attachPreviews to a separate file

* added attach URL previews to rich text editor

* don't let attachUrlPreviews block clearning the composer

* fixed linter errors

* moved url preview behind labs feature gate

* passed linters

* moved lab feature checking to where attach preview is used

* claude wrote some unit tests for url previewing

* fixed linter errors

* added feature to labs.md

* fixed linter errors

* fixed oxfmt error

* set previews to none if all previews failed

* resolved PR reviews besides the ones that requires a larger code change

* moved url preview VM creation to message composer

* removed delay when clearing composer url preview on message send

* minor changes to stop using globals everywhere

* moved debouncing responsibility to urlpreviewVM

* minor lint fixes

* added comment

* remove composer content from the state of urlpreview, moved it to the vm

* urlpreviewwrapper depends on the vm only

* added comments

* edited the comment

* updated snapshots

* claude fixed tests

* enable feature for tests that requires MSC4095

* updated snapshot
This commit is contained in:
Sirius
2026-07-13 15:31:18 +00:00
committed by GitHub
parent 6aeb5f552b
commit 84653bb416
25 changed files with 588 additions and 210 deletions
@@ -7,11 +7,84 @@ Please see LICENSE files in the repository root for full details.
*/
import { type IContent } from "matrix-js-sdk/src/matrix";
import { type MessageComposerUrlPreviewSnapshot } from "@element-hq/web-shared-components";
import { attachMentions } from "../../../src/utils/messages";
import { attachMentions, attachUrlPreviews } from "../../../src/utils/messages";
import EditorModel from "../../../src/editor/model";
import { mkEvent } from "../../test-utils";
import { createPartCreator } from "../editor/mock";
import { type RoomMessageEventContent } from "../../../@types/url-preview";
import SettingsStore from "../../../src/settings/SettingsStore";
describe("attachUrlPreviews", () => {
beforeEach(() => {
const original = SettingsStore.getValue;
jest.spyOn(SettingsStore, "getValue").mockImplementation(
(setting) => setting === "feature_msc4095_url_preview_bundle" || original(setting),
);
});
afterEach(() => {
jest.restoreAllMocks();
});
const makeContent = (): RoomMessageEventContent =>
({ msgtype: "m.text", body: "hi https://example.com" }) as RoomMessageEventContent;
const snapshot = (image?: object): MessageComposerUrlPreviewSnapshot => ({
previews: [
{
link: "https://example.com",
showTooltipOnLink: false,
title: "Example",
siteName: "example.com",
description: "desc",
...(image ? { image } : {}),
} as any,
],
content: "https://example.com",
});
it("does nothing when there are no previews", () => {
const content = makeContent();
attachUrlPreviews({ previews: [], content: "" }, content);
expect(content["com.beeper.linkpreviews"]).toBeUndefined();
});
it("attaches a preview with no image", () => {
const content = makeContent();
attachUrlPreviews(snapshot(), content);
expect(content["com.beeper.linkpreviews"]).toEqual([
expect.objectContaining({ "og:title": "Example", "og:image": undefined }),
]);
});
it("embeds the mxc url from the preview image", () => {
const content = makeContent();
attachUrlPreviews(
snapshot({
imageThumb: "",
imageFull: "https://example.com/full.png",
mxcImageFull: "mxc://server/img",
imageType: "image/png",
fileSize: 1234,
width: 100,
height: 50,
playable: false,
}),
content,
);
expect(content["com.beeper.linkpreviews"]![0]).toEqual(
expect.objectContaining({
"og:image": "mxc://server/img",
"og:image:width": 100,
"og:image:type": "image/png",
"matrix:image:size": 1234,
}),
);
});
});
describe("attachMentions", () => {
const partsCreator = createPartCreator();