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:
@@ -35,6 +35,7 @@ import { addTextToComposer } from "../../../../test-utils/composer";
|
||||
import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx";
|
||||
import { SDKContextClass } from "../../../../../src/contexts/SDKContextClass";
|
||||
import { RoomUploadContextProvider } from "../../../../../src/viewmodels/room/RoomUploadViewModel.tsx";
|
||||
import { MessageComposerUrlPreviewViewModel } from "../../../../../src/viewmodels/composer/MessageComposerUrlPreviewViewModel.ts";
|
||||
import { SDKContext } from "../../../../../src/contexts/SDKContext.ts";
|
||||
|
||||
jest.mock("../../../../../src/utils/local-room", () => ({
|
||||
@@ -186,6 +187,12 @@ describe("<SendMessageComposer/>", () => {
|
||||
spyDispatcher.mockReset();
|
||||
});
|
||||
|
||||
const urlPreviewVm = new MessageComposerUrlPreviewViewModel({
|
||||
client: mockClient,
|
||||
visible: false,
|
||||
showTooltips: false,
|
||||
urlPreviewBundle: false,
|
||||
});
|
||||
const defaultProps = {
|
||||
room: mockRoom,
|
||||
toggleStickerPickerOpen: jest.fn(),
|
||||
@@ -194,7 +201,7 @@ describe("<SendMessageComposer/>", () => {
|
||||
<MatrixClientContext.Provider value={client}>
|
||||
<ScopedRoomContextProvider room={mockRoom} {...roomContext}>
|
||||
<RoomUploadContextProvider>
|
||||
<SendMessageComposer {...defaultProps} {...props} />
|
||||
<SendMessageComposer {...defaultProps} {...props} urlPreviewVm={urlPreviewVm} />
|
||||
</RoomUploadContextProvider>
|
||||
</ScopedRoomContextProvider>
|
||||
</MatrixClientContext.Provider>
|
||||
@@ -439,6 +446,13 @@ describe("<SendMessageComposer/>", () => {
|
||||
|
||||
it("should call prepareToEncrypt when the user is typing", async () => {
|
||||
const cli = stubClient();
|
||||
const urlPreviewVm = new MessageComposerUrlPreviewViewModel({
|
||||
client: cli,
|
||||
visible: false,
|
||||
showTooltips: false,
|
||||
urlPreviewBundle: false,
|
||||
});
|
||||
|
||||
cli.isRoomEncrypted = jest.fn().mockReturnValue(true);
|
||||
const room = mkStubRoom("!roomId:server", "Room", cli);
|
||||
|
||||
@@ -448,7 +462,11 @@ describe("<SendMessageComposer/>", () => {
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<ScopedRoomContextProvider {...({ room } as unknown as RoomContextType)}>
|
||||
<RoomUploadContextProvider>
|
||||
<SendMessageComposer room={room} toggleStickerPickerOpen={jest.fn()} />
|
||||
<SendMessageComposer
|
||||
room={room}
|
||||
toggleStickerPickerOpen={jest.fn()}
|
||||
urlPreviewVm={urlPreviewVm}
|
||||
/>
|
||||
</RoomUploadContextProvider>
|
||||
</ScopedRoomContextProvider>
|
||||
</MatrixClientContext.Provider>,
|
||||
|
||||
+27
-2
@@ -65,7 +65,11 @@ describe("message", () => {
|
||||
describe("sendMessage", () => {
|
||||
it("Should not send empty html message", async () => {
|
||||
// When
|
||||
await sendMessage("", true, { roomContext: defaultRoomContext, mxClient: mockClient });
|
||||
await sendMessage("", true, {
|
||||
roomContext: defaultRoomContext,
|
||||
mxClient: mockClient,
|
||||
urlPreviewSnapshot: { previews: [], content: "" },
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(mockClient.sendMessage).toHaveBeenCalledTimes(0);
|
||||
@@ -80,6 +84,7 @@ describe("message", () => {
|
||||
await sendMessage(message, true, {
|
||||
roomContext: mockRoomContextWithoutId,
|
||||
mxClient: mockClient,
|
||||
urlPreviewSnapshot: { previews: [], content: "" },
|
||||
});
|
||||
|
||||
// Then
|
||||
@@ -93,6 +98,7 @@ describe("message", () => {
|
||||
await sendMessage(message, true, {
|
||||
roomContext: defaultRoomContext,
|
||||
mxClient: mockClient,
|
||||
urlPreviewSnapshot: { previews: [], content: "" },
|
||||
});
|
||||
|
||||
// Then
|
||||
@@ -104,6 +110,7 @@ describe("message", () => {
|
||||
roomContext: defaultRoomContext,
|
||||
mxClient: mockClient,
|
||||
relation: {},
|
||||
urlPreviewSnapshot: { previews: [], content: "" },
|
||||
});
|
||||
|
||||
// Then
|
||||
@@ -118,6 +125,7 @@ describe("message", () => {
|
||||
event_id: "valid_id",
|
||||
rel_type: "m.does_not_match",
|
||||
},
|
||||
urlPreviewSnapshot: { previews: [], content: "" },
|
||||
});
|
||||
|
||||
// Then
|
||||
@@ -133,6 +141,7 @@ describe("message", () => {
|
||||
event_id: "valid_id",
|
||||
rel_type: "m.thread",
|
||||
},
|
||||
urlPreviewSnapshot: { previews: [], content: "" },
|
||||
});
|
||||
|
||||
// Then
|
||||
@@ -145,6 +154,7 @@ describe("message", () => {
|
||||
await sendMessage(message, true, {
|
||||
roomContext: defaultRoomContext,
|
||||
mxClient: mockClient,
|
||||
urlPreviewSnapshot: { previews: [], content: "" },
|
||||
});
|
||||
|
||||
// Then
|
||||
@@ -172,6 +182,7 @@ describe("message", () => {
|
||||
roomContext: defaultRoomContext,
|
||||
mxClient: mockClient,
|
||||
replyToEvent: mockReplyEvent,
|
||||
urlPreviewSnapshot: { previews: [], content: "" },
|
||||
});
|
||||
|
||||
// Then
|
||||
@@ -201,6 +212,7 @@ describe("message", () => {
|
||||
await sendMessage(message, true, {
|
||||
roomContext: defaultRoomContext,
|
||||
mxClient: mockClient,
|
||||
urlPreviewSnapshot: { previews: [], content: "" },
|
||||
});
|
||||
|
||||
// Then
|
||||
@@ -212,7 +224,11 @@ describe("message", () => {
|
||||
|
||||
it("Should handle emojis", async () => {
|
||||
// When
|
||||
await sendMessage("🎉", false, { roomContext: defaultRoomContext, mxClient: mockClient });
|
||||
await sendMessage("🎉", false, {
|
||||
roomContext: defaultRoomContext,
|
||||
mxClient: mockClient,
|
||||
urlPreviewSnapshot: { previews: [], content: "" },
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(spyDispatcher).toHaveBeenCalledWith({ action: "effects.confetti" });
|
||||
@@ -227,6 +243,7 @@ describe("message", () => {
|
||||
await sendMessage(validCommand, true, {
|
||||
roomContext: defaultRoomContext,
|
||||
mxClient: mockClient,
|
||||
urlPreviewSnapshot: { previews: [], content: "" },
|
||||
});
|
||||
|
||||
// Then
|
||||
@@ -239,6 +256,7 @@ describe("message", () => {
|
||||
await sendMessage(invalidPrefixCommand, true, {
|
||||
roomContext: defaultRoomContext,
|
||||
mxClient: mockClient,
|
||||
urlPreviewSnapshot: { previews: [], content: "" },
|
||||
});
|
||||
|
||||
// Then
|
||||
@@ -256,6 +274,7 @@ describe("message", () => {
|
||||
const result = await sendMessage(validCommand, true, {
|
||||
roomContext: defaultRoomContext,
|
||||
mxClient: mockClient,
|
||||
urlPreviewSnapshot: { previews: [], content: "" },
|
||||
});
|
||||
|
||||
// Then
|
||||
@@ -270,6 +289,7 @@ describe("message", () => {
|
||||
await sendMessage(inputText, true, {
|
||||
roomContext: defaultRoomContext,
|
||||
mxClient: mockClient,
|
||||
urlPreviewSnapshot: { previews: [], content: "" },
|
||||
});
|
||||
expect(mockClient.sendMessage).toHaveBeenCalledWith(
|
||||
"myfakeroom",
|
||||
@@ -289,6 +309,7 @@ describe("message", () => {
|
||||
roomContext: defaultRoomContext,
|
||||
mxClient: mockClient,
|
||||
relation: mockRelation,
|
||||
urlPreviewSnapshot: { previews: [], content: "" },
|
||||
});
|
||||
|
||||
expect(mockClient.sendMessage).toHaveBeenCalledWith(
|
||||
@@ -305,6 +326,7 @@ describe("message", () => {
|
||||
roomContext: defaultRoomContext,
|
||||
mxClient: mockClient,
|
||||
replyToEvent: mockEvent,
|
||||
urlPreviewSnapshot: { previews: [], content: "" },
|
||||
});
|
||||
|
||||
expect(addReplySpy).toHaveBeenCalledTimes(1);
|
||||
@@ -319,6 +341,7 @@ describe("message", () => {
|
||||
roomContext: defaultRoomContext,
|
||||
mxClient: mockClient,
|
||||
replyToEvent: mockEvent,
|
||||
urlPreviewSnapshot: { previews: [], content: "" },
|
||||
});
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
@@ -333,6 +356,7 @@ describe("message", () => {
|
||||
await sendMessage(invalidCommandInput, true, {
|
||||
roomContext: defaultRoomContext,
|
||||
mxClient: mockClient,
|
||||
urlPreviewSnapshot: { previews: [], content: "" },
|
||||
});
|
||||
|
||||
// we expect the message to have been sent
|
||||
@@ -353,6 +377,7 @@ describe("message", () => {
|
||||
const result = await sendMessage(invalidCommandInput, true, {
|
||||
roomContext: defaultRoomContext,
|
||||
mxClient: mockClient,
|
||||
urlPreviewSnapshot: { previews: [], content: "" },
|
||||
});
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user