diff --git a/apps/web/src/settings/Settings.tsx b/apps/web/src/settings/Settings.tsx index a04da85104..8c47861bf8 100644 --- a/apps/web/src/settings/Settings.tsx +++ b/apps/web/src/settings/Settings.tsx @@ -1140,7 +1140,7 @@ export const SETTINGS: Settings = { "urlPreviewsEnabled": { // Enabled by default and client configurable as this setting only allows unencrypted // messages to be previewed. - supportedLevels: [SettingLevel.DEVICE, SettingLevel.ACCOUNT, SettingLevel.CONFIG], + supportedLevels: [SettingLevel.ROOM_DEVICE, SettingLevel.DEVICE, SettingLevel.ACCOUNT, SettingLevel.CONFIG], supportedLevelsAreOrdered: true, displayName: _td("settings|inline_url_previews_default"), default: true, diff --git a/apps/web/src/viewmodels/message-body/UrlPreviewGroupViewModel.ts b/apps/web/src/viewmodels/message-body/UrlPreviewGroupViewModel.ts index 025bc4e337..3ff1c40d3b 100644 --- a/apps/web/src/viewmodels/message-body/UrlPreviewGroupViewModel.ts +++ b/apps/web/src/viewmodels/message-body/UrlPreviewGroupViewModel.ts @@ -37,6 +37,8 @@ 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 { /** @@ -387,6 +389,18 @@ 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. + const bundledLinkPreviews = this.props.mxEvent.getContent()[BUNDLED_LINK_PREVIEWS]; + if (Array.isArray(bundledLinkPreviews) && bundledLinkPreviews.length === 0) { + return this.snapshot.merge({ + previews: [], + totalPreviewCount: 0, + previewsLimited: false, + overPreviewLimit: false, + }); + } // otherwise, we do not support bundled previews yet so will fallback to old behaviour. + const previews = 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 9541e999a8..d48c5711ff 100644 --- a/apps/web/test/viewmodels/message-body/UrlPreviewGroupViewModel-test.ts +++ b/apps/web/test/viewmodels/message-body/UrlPreviewGroupViewModel-test.ts @@ -9,7 +9,10 @@ import { expect } from "@jest/globals"; import type { MockedObject } from "jest-mock"; import type { MatrixClient, IPreviewUrlResponse } from "matrix-js-sdk/src/matrix"; -import { UrlPreviewGroupViewModel } from "../../../src/viewmodels/message-body/UrlPreviewGroupViewModel"; +import { + BUNDLED_LINK_PREVIEWS, + UrlPreviewGroupViewModel, +} from "../../../src/viewmodels/message-body/UrlPreviewGroupViewModel"; import type { UrlPreview } from "@element-hq/web-shared-components"; import { getMockClientWithEventEmitter, mkEvent } from "../../test-utils"; @@ -22,7 +25,9 @@ const BASIC_PREVIEW_OGDATA = { "og:site_name": "Example.org", }; -function getViewModel({ mediaVisible, visible } = { mediaVisible: true, visible: true }): { +function getViewModel( + { mediaVisible, visible, showPreview } = { mediaVisible: true, visible: true, showPreview: true }, +): { vm: UrlPreviewGroupViewModel; client: MockedObject; onImageClicked: jest.Mock; @@ -41,7 +46,9 @@ function getViewModel({ mediaVisible, visible } = { mediaVisible: true, visible: event: true, user: "@foo:bar", type: "m.room.message", - content: {}, + content: { + ...(showPreview ? undefined : { [BUNDLED_LINK_PREVIEWS]: [] }), + }, id: "$id", }), }); @@ -94,7 +101,7 @@ describe("UrlPreviewGroupViewModel", () => { ]); }); it("should hide preview when invisible", async () => { - const { vm, client } = getViewModel({ visible: false, mediaVisible: true }); + const { vm, client } = getViewModel({ visible: false, mediaVisible: true, showPreview: true }); const msg = document.createElement("div"); msg.innerHTML = 'Test'; await vm.updateEventElement(msg); @@ -152,7 +159,7 @@ describe("UrlPreviewGroupViewModel", () => { expect(vm.getSnapshot().previews[0].siteIcon).toBeTruthy(); }); it("should ignore media when mediaVisible is false", async () => { - const { vm, client } = getViewModel({ mediaVisible: false, visible: true }); + const { vm, client } = getViewModel({ mediaVisible: false, visible: true, showPreview: true }); client.getUrlPreview.mockResolvedValueOnce({ "og:title": "This is an example!", "og:type": "document", @@ -225,6 +232,21 @@ describe("UrlPreviewGroupViewModel", () => { await vm.onShowClick(); 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 }); + client.getUrlPreview.mockResolvedValueOnce(BASIC_PREVIEW_OGDATA); + const msg = document.createElement("div"); + msg.innerHTML = 'Test'; + await vm.updateEventElement(msg); + expect(vm.getSnapshot()).toMatchInlineSnapshot(` +{ + "overPreviewLimit": false, + "previews": [], + "previewsLimited": false, + "totalPreviewCount": 0, +} +`); + }); describe("calculates author", () => { it("should use the profile:username if provided", async () => {