From 1c5e719638353db4bc54e86ff2b9df9548d9e8be Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Tue, 22 Apr 2025 11:14:41 +0100 Subject: [PATCH 01/12] Always show media from your own user --- playwright/e2e/timeline/timeline.spec.ts | 20 ++++++-- src/hooks/useMediaVisible.ts | 15 ++++-- .../unit-tests/hooks/useMediaVisible-test.tsx | 48 ++++++++++++------- 3 files changed, 56 insertions(+), 27 deletions(-) diff --git a/playwright/e2e/timeline/timeline.spec.ts b/playwright/e2e/timeline/timeline.spec.ts index e5bfa0c71bf..2f432ca1373 100644 --- a/playwright/e2e/timeline/timeline.spec.ts +++ b/playwright/e2e/timeline/timeline.spec.ts @@ -908,9 +908,14 @@ test.describe("Timeline", () => { }); }); - test("should be able to hide an image", { tag: "@screenshot" }, async ({ page, app, room, context }) => { + test("should be able to hide an image", { tag: "@screenshot" }, async ({ page, app, homeserver, room, context }) => { await app.viewRoomById(room.roomId); - await sendImage(app.client, room.roomId, NEW_AVATAR); + + const bot = new Bot(page, homeserver, {}); + await bot.prepareClient(); + await app.client.inviteUser(room.roomId, bot.credentials.userId); + + await sendImage(bot, room.roomId, NEW_AVATAR); await app.timeline.scrollToBottom(); const imgTile = page.locator(".mx_MImageBody").first(); await expect(imgTile).toBeVisible(); @@ -921,10 +926,15 @@ test.describe("Timeline", () => { await expect(page.getByRole("button", { name: "Show image" })).toBeVisible(); }); - test("should be able to hide a video", async ({ page, app, room, context }) => { + test("should be able to hide a video", async ({ page, app, homeserver, room, context }) => { await app.viewRoomById(room.roomId); - const upload = await app.client.uploadContent(VIDEO_FILE, { name: "bbb.webm", type: "video/webm" }); - await app.client.sendEvent(room.roomId, null, "m.room.message" as EventType, { + + const bot = new Bot(page, homeserver, {}); + await bot.prepareClient(); + await app.client.inviteUser(room.roomId, bot.credentials.userId); + + const upload = await bot.uploadContent(VIDEO_FILE, { name: "bbb.webm", type: "video/webm" }); + await bot.sendEvent(room.roomId, null, "m.room.message" as EventType, { msgtype: "m.video" as MsgType, body: "bbb.webm", url: upload.content_uri, diff --git a/src/hooks/useMediaVisible.ts b/src/hooks/useMediaVisible.ts index f367e87c4f0..60acc7dcab9 100644 --- a/src/hooks/useMediaVisible.ts +++ b/src/hooks/useMediaVisible.ts @@ -6,7 +6,7 @@ Please see LICENSE files in the repository root for full details. */ import { useCallback } from "react"; -import { JoinRule } from "matrix-js-sdk/src/matrix"; +import { JoinRule, MatrixEvent } from "matrix-js-sdk/src/matrix"; import { SettingLevel } from "../settings/SettingLevel"; import { useSettingValue } from "./useSettings"; @@ -19,14 +19,15 @@ const PRIVATE_JOIN_RULES: JoinRule[] = [JoinRule.Invite, JoinRule.Knock, JoinRul /** * Should the media event be visible in the client, or hidden. - * @param eventId The eventId of the media event. * @returns A boolean describing the hidden status, and a function to set the visiblity. */ -export function useMediaVisible(eventId?: string, roomId?: string): [boolean, (visible: boolean) => void] { - const mediaPreviewSetting = useSettingValue("mediaPreviewConfig", roomId); +export function useMediaVisible(mxEvent?: MatrixEvent): [boolean, (visible: boolean) => void]|[true] { + const eventId = mxEvent?.getId(); + const mediaPreviewSetting = useSettingValue("mediaPreviewConfig", mxEvent?.getRoomId()); const client = useMatrixClientContext(); const eventVisibility = useSettingValue("showMediaEventIds"); - const joinRule = useRoomState(client.getRoom(roomId) ?? undefined, (state) => state.getJoinRule()); + const room = client.getRoom(mxEvent?.getRoomId()) ?? undefined; + const joinRule = useRoomState(room, (state) => state.getJoinRule()); const setMediaVisible = useCallback( (visible: boolean) => { SettingsStore.setValue("showMediaEventIds", null, SettingLevel.DEVICE, { @@ -37,6 +38,10 @@ export function useMediaVisible(eventId?: string, roomId?: string): [boolean, (v [eventId, eventVisibility], ); + if (mxEvent?.getSender() === client.getUserId()) { + return [true]; + } + const roomIsPrivate = joinRule ? PRIVATE_JOIN_RULES.includes(joinRule) : false; const explicitEventVisiblity = eventId ? eventVisibility[eventId] : undefined; diff --git a/test/unit-tests/hooks/useMediaVisible-test.tsx b/test/unit-tests/hooks/useMediaVisible-test.tsx index d8f71b764a8..64dda02888c 100644 --- a/test/unit-tests/hooks/useMediaVisible-test.tsx +++ b/test/unit-tests/hooks/useMediaVisible-test.tsx @@ -6,7 +6,7 @@ Please see LICENSE files in the repository root for full details. */ import { act, renderHook, waitFor } from "jest-matrix-react"; -import { JoinRule, type MatrixClient, type Room } from "matrix-js-sdk/src/matrix"; +import { JoinRule, MatrixEvent, type MatrixClient, type Room } from "matrix-js-sdk/src/matrix"; import { useMediaVisible } from "../../../src/hooks/useMediaVisible"; import { createTestClient, mkStubRoom, withClientContextRenderOptions } from "../../test-utils"; @@ -22,8 +22,12 @@ describe("useMediaVisible", () => { let room: Room; const mediaPreviewConfig: MediaPreviewConfig = MediaPreviewConfigController.default; - function render() { - return renderHook(() => useMediaVisible(EVENT_ID, ROOM_ID), withClientContextRenderOptions(matrixClient)); + function render({sender}: {sender?: string} = {}) { + return renderHook(() => useMediaVisible(new MatrixEvent({ + event_id: EVENT_ID, + room_id: ROOM_ID, + sender, + })), withClientContextRenderOptions(matrixClient)); } beforeEach(() => { matrixClient = createTestClient(); @@ -42,53 +46,63 @@ describe("useMediaVisible", () => { jest.restoreAllMocks(); }); - it("should display media by default", async () => { - const { result } = render(); - expect(result.current[0]).toEqual(true); + it("should display media by default", () => { + const [ visible ] = render().result.current; + expect(visible).toEqual(true); }); - it("should hide media when media previews are Off", async () => { + it("should hide media when media previews are Off", () => { mediaPreviewConfig.media_previews = MediaPreviewValue.Off; - const { result } = render(); - expect(result.current[0]).toEqual(false); + const [ visible ] = render().result.current; + expect(visible).toEqual(false); + }); + + it("should always show media sent by us", () => { + mediaPreviewConfig.media_previews = MediaPreviewValue.Off; + const [ visible, setVisible ] = render({ sender: matrixClient.getUserId()! }).result.current; + expect(visible).toEqual(true); + expect(setVisible).toBeUndefined(); }); it.each([[JoinRule.Invite], [JoinRule.Knock], [JoinRule.Restricted]])( "should display media when media previews are Private and the join rule is %s", - async (rule) => { + (rule) => { mediaPreviewConfig.media_previews = MediaPreviewValue.Private; room.currentState.getJoinRule = jest.fn().mockReturnValue(rule); - const { result } = render(); - expect(result.current[0]).toEqual(true); + const [ visible ] = render().result.current; + expect(visible).toEqual(true); }, ); it.each([[JoinRule.Public], ["anything_else"]])( "should hide media when media previews are Private and the join rule is %s", - async (rule) => { + (rule) => { mediaPreviewConfig.media_previews = MediaPreviewValue.Private; room.currentState.getJoinRule = jest.fn().mockReturnValue(rule); - const { result } = render(); - expect(result.current[0]).toEqual(false); + const [ visible ] = render().result.current; + expect(visible).toEqual(false); }, ); it("should hide media after function is called", async () => { const { result } = render(); expect(result.current[0]).toEqual(true); + expect(result.current[1]).toBeDefined(); act(() => { - result.current[1](false); + result.current[1]!(false); }); await waitFor(() => { expect(result.current[0]).toEqual(false); }); }); + it("should show media after function is called", async () => { mediaPreviewConfig.media_previews = MediaPreviewValue.Off; const { result } = render(); expect(result.current[0]).toEqual(false); + expect(result.current[1]).toBeDefined(); act(() => { - result.current[1](true); + result.current[1]!(true); }); await waitFor(() => { expect(result.current[0]).toEqual(true); From 644ee82950a337dd2835b42d5271619096db2811 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Tue, 22 Apr 2025 11:14:58 +0100 Subject: [PATCH 02/12] Update usages of useMediaVisible --- src/components/views/messages/EventContentBody.tsx | 2 +- src/components/views/messages/HideActionButton.tsx | 4 ++-- src/components/views/messages/MImageBody.tsx | 6 +++--- src/components/views/messages/MImageReplyBody.tsx | 2 +- src/components/views/messages/MStickerBody.tsx | 2 +- src/components/views/messages/MVideoBody.tsx | 6 +++--- src/components/views/rooms/LinkPreviewGroup.tsx | 2 +- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/components/views/messages/EventContentBody.tsx b/src/components/views/messages/EventContentBody.tsx index 66ff7915eaf..101a7cbfb54 100644 --- a/src/components/views/messages/EventContentBody.tsx +++ b/src/components/views/messages/EventContentBody.tsx @@ -151,7 +151,7 @@ const EventContentBody = memo( forwardRef( ({ as, mxEvent, stripReply, content, linkify, highlights, includeDir = true, ...options }, ref) => { const enableBigEmoji = useSettingValue("TextualBody.enableBigEmoji"); - const [mediaIsVisible] = useMediaVisible(mxEvent?.getId(), mxEvent?.getRoomId()); + const [mediaIsVisible] = useMediaVisible(mxEvent); const replacer = useReplacer(content, mxEvent, options); const linkifyOptions = useMemo( diff --git a/src/components/views/messages/HideActionButton.tsx b/src/components/views/messages/HideActionButton.tsx index 0c9817b2a6f..79f6bc5c38c 100644 --- a/src/components/views/messages/HideActionButton.tsx +++ b/src/components/views/messages/HideActionButton.tsx @@ -25,9 +25,9 @@ interface IProps { * Quick action button for marking a media event as hidden. */ export const HideActionButton: React.FC = ({ mxEvent }) => { - const [mediaIsVisible, setVisible] = useMediaVisible(mxEvent.getId(), mxEvent.getRoomId()); + const [mediaIsVisible, setVisible] = useMediaVisible(mxEvent); - if (!mediaIsVisible) { + if (!mediaIsVisible || !setVisible) { return; } diff --git a/src/components/views/messages/MImageBody.tsx b/src/components/views/messages/MImageBody.tsx index 79f840ce39c..0a26b250d39 100644 --- a/src/components/views/messages/MImageBody.tsx +++ b/src/components/views/messages/MImageBody.tsx @@ -66,7 +66,7 @@ interface IProps extends IBodyProps { * Set the visibility of the media event. * @param visible Should the event be visible. */ - setMediaVisible: (visible: boolean) => void; + setMediaVisible?: (visible: boolean) => void; } /** @@ -95,7 +95,7 @@ export class MImageBodyInner extends React.Component { protected onClick = (ev: React.MouseEvent): void => { if (ev.button === 0 && !ev.metaKey) { ev.preventDefault(); - if (!this.props.mediaVisible) { + if (!this.props.mediaVisible && this.props.setMediaVisible) { this.props.setMediaVisible(true); return; } @@ -686,7 +686,7 @@ export class MImageBodyInner extends React.Component { // Wrap MImageBody component so we can use a hook here. const MImageBody: React.FC = (props) => { - const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId(), props.mxEvent.getRoomId()); + const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent); return ; }; diff --git a/src/components/views/messages/MImageReplyBody.tsx b/src/components/views/messages/MImageReplyBody.tsx index b73f8f77c34..5f04df724da 100644 --- a/src/components/views/messages/MImageReplyBody.tsx +++ b/src/components/views/messages/MImageReplyBody.tsx @@ -38,7 +38,7 @@ class MImageReplyBodyInner extends MImageBodyInner { } } const MImageReplyBody: React.FC = (props) => { - const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId(), props.mxEvent.getRoomId()); + const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent); return ; }; diff --git a/src/components/views/messages/MStickerBody.tsx b/src/components/views/messages/MStickerBody.tsx index 3a922d35aa3..0e5ae8cba92 100644 --- a/src/components/views/messages/MStickerBody.tsx +++ b/src/components/views/messages/MStickerBody.tsx @@ -79,7 +79,7 @@ class MStickerBodyInner extends MImageBodyInner { } const MStickerBody: React.FC = (props) => { - const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId(), props.mxEvent.getRoomId()); + const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent); return ; }; diff --git a/src/components/views/messages/MVideoBody.tsx b/src/components/views/messages/MVideoBody.tsx index 6a36dae6a8e..e83dfb98abf 100644 --- a/src/components/views/messages/MVideoBody.tsx +++ b/src/components/views/messages/MVideoBody.tsx @@ -43,7 +43,7 @@ interface IProps extends IBodyProps { * Set the visibility of the media event. * @param visible Should the event be visible. */ - setMediaVisible: (visible: boolean) => void; + setMediaVisible?: (visible: boolean) => void; } class MVideoBodyInner extends React.PureComponent { @@ -64,7 +64,7 @@ class MVideoBodyInner extends React.PureComponent { }; private onClick = (): void => { - this.props.setMediaVisible(true); + this.props.setMediaVisible?.(true); }; private getContentUrl(): string | undefined { @@ -342,7 +342,7 @@ class MVideoBodyInner extends React.PureComponent { // Wrap MVideoBody component so we can use a hook here. const MVideoBody: React.FC = (props) => { - const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId(), props.mxEvent.getRoomId()); + const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent); return ; }; diff --git a/src/components/views/rooms/LinkPreviewGroup.tsx b/src/components/views/rooms/LinkPreviewGroup.tsx index 69c98cb6c9a..471220bfeb7 100644 --- a/src/components/views/rooms/LinkPreviewGroup.tsx +++ b/src/components/views/rooms/LinkPreviewGroup.tsx @@ -30,7 +30,7 @@ interface IProps { const LinkPreviewGroup: React.FC = ({ links, mxEvent, onCancelClick }) => { const cli = useContext(MatrixClientContext); const [expanded, toggleExpanded] = useStateToggle(); - const [mediaVisible] = useMediaVisible(mxEvent.getId(), mxEvent.getRoomId()); + const [mediaVisible] = useMediaVisible(mxEvent); const ts = mxEvent.getTs(); const previews = useAsyncMemo<[string, IPreviewUrlResponse][]>( From 8e7a231388bfeb80d0689a0b4687cd34975dc502 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Tue, 22 Apr 2025 11:19:28 +0100 Subject: [PATCH 03/12] lint --- playwright/e2e/timeline/timeline.spec.ts | 32 +++++++++++-------- .../views/messages/MessageActionBar.tsx | 2 +- src/hooks/useMediaVisible.ts | 4 +-- src/utils/MediaEventHelper.ts | 5 +-- .../unit-tests/hooks/useMediaVisible-test.tsx | 28 +++++++++------- 5 files changed, 41 insertions(+), 30 deletions(-) diff --git a/playwright/e2e/timeline/timeline.spec.ts b/playwright/e2e/timeline/timeline.spec.ts index 2f432ca1373..4da622d42d9 100644 --- a/playwright/e2e/timeline/timeline.spec.ts +++ b/playwright/e2e/timeline/timeline.spec.ts @@ -908,23 +908,27 @@ test.describe("Timeline", () => { }); }); - test("should be able to hide an image", { tag: "@screenshot" }, async ({ page, app, homeserver, room, context }) => { - await app.viewRoomById(room.roomId); + test( + "should be able to hide an image", + { tag: "@screenshot" }, + async ({ page, app, homeserver, room, context }) => { + await app.viewRoomById(room.roomId); - const bot = new Bot(page, homeserver, {}); - await bot.prepareClient(); - await app.client.inviteUser(room.roomId, bot.credentials.userId); + const bot = new Bot(page, homeserver, {}); + await bot.prepareClient(); + await app.client.inviteUser(room.roomId, bot.credentials.userId); - await sendImage(bot, room.roomId, NEW_AVATAR); - await app.timeline.scrollToBottom(); - const imgTile = page.locator(".mx_MImageBody").first(); - await expect(imgTile).toBeVisible(); - await imgTile.hover(); - await page.getByRole("button", { name: "Hide" }).click(); + await sendImage(bot, room.roomId, NEW_AVATAR); + await app.timeline.scrollToBottom(); + const imgTile = page.locator(".mx_MImageBody").first(); + await expect(imgTile).toBeVisible(); + await imgTile.hover(); + await page.getByRole("button", { name: "Hide" }).click(); - // Check that the image is now hidden. - await expect(page.getByRole("button", { name: "Show image" })).toBeVisible(); - }); + // Check that the image is now hidden. + await expect(page.getByRole("button", { name: "Show image" })).toBeVisible(); + }, + ); test("should be able to hide a video", async ({ page, app, homeserver, room, context }) => { await app.viewRoomById(room.roomId); diff --git a/src/components/views/messages/MessageActionBar.tsx b/src/components/views/messages/MessageActionBar.tsx index eb109028d95..fbb899af726 100644 --- a/src/components/views/messages/MessageActionBar.tsx +++ b/src/components/views/messages/MessageActionBar.tsx @@ -538,7 +538,7 @@ export default class MessageActionBar extends React.PureComponent, ); } - if (MediaEventHelper.canHide(this.props.mxEvent)) { + if (MediaEventHelper.canHide(this.props.mxEvent, MatrixClientPeg.safeGet().getSafeUserId())) { toolbarOpts.splice(0, 0, ); } } else if ( diff --git a/src/hooks/useMediaVisible.ts b/src/hooks/useMediaVisible.ts index 60acc7dcab9..e750592f36c 100644 --- a/src/hooks/useMediaVisible.ts +++ b/src/hooks/useMediaVisible.ts @@ -6,7 +6,7 @@ Please see LICENSE files in the repository root for full details. */ import { useCallback } from "react"; -import { JoinRule, MatrixEvent } from "matrix-js-sdk/src/matrix"; +import { JoinRule, type MatrixEvent } from "matrix-js-sdk/src/matrix"; import { SettingLevel } from "../settings/SettingLevel"; import { useSettingValue } from "./useSettings"; @@ -21,7 +21,7 @@ const PRIVATE_JOIN_RULES: JoinRule[] = [JoinRule.Invite, JoinRule.Knock, JoinRul * Should the media event be visible in the client, or hidden. * @returns A boolean describing the hidden status, and a function to set the visiblity. */ -export function useMediaVisible(mxEvent?: MatrixEvent): [boolean, (visible: boolean) => void]|[true] { +export function useMediaVisible(mxEvent?: MatrixEvent): [boolean, (visible: boolean) => void] | [true] { const eventId = mxEvent?.getId(); const mediaPreviewSetting = useSettingValue("mediaPreviewConfig", mxEvent?.getRoomId()); const client = useMatrixClientContext(); diff --git a/src/utils/MediaEventHelper.ts b/src/utils/MediaEventHelper.ts index 98cbe4da583..49e60b5dce6 100644 --- a/src/utils/MediaEventHelper.ts +++ b/src/utils/MediaEventHelper.ts @@ -117,11 +117,12 @@ export class MediaEventHelper implements IDestroyable { /** * Determine if the media event in question supports being hidden in the timeline. * @param event Any matrix event. - * @returns `true` if the media can be hidden, otherwise false. + * @returns `true` if the media can be hidden, otherwise `false`. */ - public static canHide(event: MatrixEvent): boolean { + public static canHide(event: MatrixEvent, myUserId: string): boolean { if (!event) return false; if (event.isRedacted()) return false; + if (event.getSender() === myUserId) return false; const content = event.getContent(); const hideTypes: string[] = [MsgType.Video, MsgType.Image]; if (hideTypes.includes(content.msgtype!)) return true; diff --git a/test/unit-tests/hooks/useMediaVisible-test.tsx b/test/unit-tests/hooks/useMediaVisible-test.tsx index 64dda02888c..ab70461cb41 100644 --- a/test/unit-tests/hooks/useMediaVisible-test.tsx +++ b/test/unit-tests/hooks/useMediaVisible-test.tsx @@ -22,12 +22,18 @@ describe("useMediaVisible", () => { let room: Room; const mediaPreviewConfig: MediaPreviewConfig = MediaPreviewConfigController.default; - function render({sender}: {sender?: string} = {}) { - return renderHook(() => useMediaVisible(new MatrixEvent({ - event_id: EVENT_ID, - room_id: ROOM_ID, - sender, - })), withClientContextRenderOptions(matrixClient)); + function render({ sender }: { sender?: string } = {}) { + return renderHook( + () => + useMediaVisible( + new MatrixEvent({ + event_id: EVENT_ID, + room_id: ROOM_ID, + sender, + }), + ), + withClientContextRenderOptions(matrixClient), + ); } beforeEach(() => { matrixClient = createTestClient(); @@ -47,19 +53,19 @@ describe("useMediaVisible", () => { }); it("should display media by default", () => { - const [ visible ] = render().result.current; + const [visible] = render().result.current; expect(visible).toEqual(true); }); it("should hide media when media previews are Off", () => { mediaPreviewConfig.media_previews = MediaPreviewValue.Off; - const [ visible ] = render().result.current; + const [visible] = render().result.current; expect(visible).toEqual(false); }); it("should always show media sent by us", () => { mediaPreviewConfig.media_previews = MediaPreviewValue.Off; - const [ visible, setVisible ] = render({ sender: matrixClient.getUserId()! }).result.current; + const [visible, setVisible] = render({ sender: matrixClient.getUserId()! }).result.current; expect(visible).toEqual(true); expect(setVisible).toBeUndefined(); }); @@ -69,7 +75,7 @@ describe("useMediaVisible", () => { (rule) => { mediaPreviewConfig.media_previews = MediaPreviewValue.Private; room.currentState.getJoinRule = jest.fn().mockReturnValue(rule); - const [ visible ] = render().result.current; + const [visible] = render().result.current; expect(visible).toEqual(true); }, ); @@ -79,7 +85,7 @@ describe("useMediaVisible", () => { (rule) => { mediaPreviewConfig.media_previews = MediaPreviewValue.Private; room.currentState.getJoinRule = jest.fn().mockReturnValue(rule); - const [ visible ] = render().result.current; + const [visible] = render().result.current; expect(visible).toEqual(false); }, ); From 85757c6552b3f667df8400cb8a9db756a758c532 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Tue, 22 Apr 2025 11:44:33 +0100 Subject: [PATCH 04/12] Add a test for HideActionButton --- .../components/views/messages/HideActionButton-test.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/unit-tests/components/views/messages/HideActionButton-test.tsx b/test/unit-tests/components/views/messages/HideActionButton-test.tsx index 650afe69e63..59d58bb8603 100644 --- a/test/unit-tests/components/views/messages/HideActionButton-test.tsx +++ b/test/unit-tests/components/views/messages/HideActionButton-test.tsx @@ -48,6 +48,7 @@ describe("HideActionButton", () => { beforeEach(() => { cli = getMockClientWithEventEmitter({ getRoom: jest.fn(), + getUserId: jest.fn(), }); }); afterEach(() => { @@ -73,6 +74,13 @@ describe("HideActionButton", () => { render(, withClientContextRenderOptions(cli)); expect(screen.queryByRole("button")).toBeNull(); }); + it("should hide button when event is not hideable", async () => { + mockSetting(MediaPreviewValue.Off, {}); + // Make it so that the event comes from us, and therefore is always visible and never hideable. + cli.getUserId.mockReturnValue(event.getSender()!); + render(, withClientContextRenderOptions(cli)); + expect(screen.queryByRole("button")).toBeNull(); + }); it("should store event as hidden when clicked", async () => { const spy = jest.spyOn(SettingsStore, "setValue"); render(, withClientContextRenderOptions(cli)); From b90e84524226bb2401e9ce2bab894d16caed2040 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Tue, 22 Apr 2025 11:50:13 +0100 Subject: [PATCH 05/12] Improve docs --- src/hooks/useMediaVisible.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/hooks/useMediaVisible.ts b/src/hooks/useMediaVisible.ts index e750592f36c..703c582363e 100644 --- a/src/hooks/useMediaVisible.ts +++ b/src/hooks/useMediaVisible.ts @@ -19,7 +19,16 @@ const PRIVATE_JOIN_RULES: JoinRule[] = [JoinRule.Invite, JoinRule.Knock, JoinRul /** * Should the media event be visible in the client, or hidden. - * @returns A boolean describing the hidden status, and a function to set the visiblity. + * + * This function uses the `mediaPreviewConfig` setting to determine the rules for the room + * along with the `showMediaEventIds` setting for specific events. + * + * A function may be provided to alter the visible state. + * + * @returns Returns a tuple of: + * A boolean describing the hidden status. This is always true if the event was sent by us. + * A function to show or hide the event. This is `undefined` if the event was sent by us (visiblity cannot be changed). + * */ export function useMediaVisible(mxEvent?: MatrixEvent): [boolean, (visible: boolean) => void] | [true] { const eventId = mxEvent?.getId(); From e1e8eb53bb7c886ce4aabe6499f822a0464919d3 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Tue, 22 Apr 2025 11:51:00 +0100 Subject: [PATCH 06/12] Document the event --- src/hooks/useMediaVisible.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/hooks/useMediaVisible.ts b/src/hooks/useMediaVisible.ts index 703c582363e..850da5b8d94 100644 --- a/src/hooks/useMediaVisible.ts +++ b/src/hooks/useMediaVisible.ts @@ -25,6 +25,8 @@ const PRIVATE_JOIN_RULES: JoinRule[] = [JoinRule.Invite, JoinRule.Knock, JoinRul * * A function may be provided to alter the visible state. * + * @param The event that contains the media. If not provided, the global rule is used. + * * @returns Returns a tuple of: * A boolean describing the hidden status. This is always true if the event was sent by us. * A function to show or hide the event. This is `undefined` if the event was sent by us (visiblity cannot be changed). From 5108c1751360f29d4d171d69325c2b5f438a353f Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Mon, 1 Sep 2025 09:29:21 +0100 Subject: [PATCH 07/12] fixup test --- .../views/messages/MVideoBody-test.tsx | 127 +++++++++++------- .../__snapshots__/MVideoBody-test.tsx.snap | 27 +++- 2 files changed, 103 insertions(+), 51 deletions(-) diff --git a/test/unit-tests/components/views/messages/MVideoBody-test.tsx b/test/unit-tests/components/views/messages/MVideoBody-test.tsx index 1d058a7b0c9..a4fe79df96f 100644 --- a/test/unit-tests/components/views/messages/MVideoBody-test.tsx +++ b/test/unit-tests/components/views/messages/MVideoBody-test.tsx @@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details. import React from "react"; import { EventType, getHttpUriForMxc, type IContent, type MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix"; -import { fireEvent, render, screen, type RenderResult } from "jest-matrix-react"; +import { fireEvent, render, screen } from "jest-matrix-react"; import fetchMock from "fetch-mock-jest"; import { type MockedObject } from "jest-mock"; @@ -34,7 +34,8 @@ jest.mock("matrix-encrypt-attachment", () => ({ })); describe("MVideoBody", () => { - const userId = "@user:server"; + const ourUserId = "@user:server"; + const senderUserId = "@other_use:server"; const deviceId = "DEADB33F"; const thumbUrl = "https://server/_matrix/media/v3/download/server/encrypted-poster"; @@ -42,7 +43,7 @@ describe("MVideoBody", () => { beforeEach(() => { cli = getMockClientWithEventEmitter({ - ...mockClientMethodsUser(userId), + ...mockClientMethodsUser(ourUserId), ...mockClientMethodsServer(), ...mockClientMethodsDevice(deviceId), ...mockClientMethodsCrypto(), @@ -67,7 +68,7 @@ describe("MVideoBody", () => { const encryptedMediaEvent = new MatrixEvent({ room_id: "!room:server", - sender: userId, + sender: senderUserId, type: EventType.RoomMessage, event_id: "$foo:bar", content: { @@ -86,10 +87,47 @@ describe("MVideoBody", () => { }, }); - it("does not crash when given a portrait image", () => { + it("does not crash when given portrait dimensions", () => { // Check for an unreliable crash caused by a fractional-sized // image dimension being used for a CanvasImageData. - const { asFragment } = makeMVideoBody(720, 1280); + const content: IContent = { + info: { + "w": 720, + "h": 1280, + "mimetype": "video/mp4", + "size": 2495675, + "thumbnail_file": { + url: "", + key: { alg: "", key_ops: [], kty: "", k: "", ext: true }, + iv: "", + hashes: {}, + v: "", + }, + "thumbnail_info": { mimetype: "" }, + "xyz.amorgan.blurhash": "TrGl6bofof~paxWC?bj[oL%2fPj]", + }, + url: "http://example.com", + }; + + const event = new MatrixEvent({ + content, + }); + + const defaultProps: IBodyProps = { + mxEvent: event, + highlights: [], + highlightLink: "", + onMessageAllowed: jest.fn(), + permalinkCreator: {} as RoomPermalinkCreator, + mediaEventHelper: { media: { isEncrypted: false } } as MediaEventHelper, + }; + + const { asFragment } = render( + + + , + withClientContextRenderOptions(cli), + ); expect(asFragment()).toMatchSnapshot(); // If we get here, we did not crash. }); @@ -153,50 +191,39 @@ describe("MVideoBody", () => { expect(fetchMock).toHaveFetched(thumbUrl); }); - }); -}); -function makeMVideoBody(w: number, h: number): RenderResult { - const content: IContent = { - info: { - "w": w, - "h": h, - "mimetype": "video/mp4", - "size": 2495675, - "thumbnail_file": { - url: "", - key: { alg: "", key_ops: [], kty: "", k: "", ext: true }, - iv: "", - hashes: {}, - v: "", - }, - "thumbnail_info": { mimetype: "" }, - "xyz.amorgan.blurhash": "TrGl6bofof~paxWC?bj[oL%2fPj]", - }, - url: "http://example.com", - }; - - const event = new MatrixEvent({ - content, - }); + it("should download video if we were the sender", async () => { + fetchMock.getOnce(thumbUrl, { status: 200 }); + const ourEncryptedMediaEvent = new MatrixEvent({ + room_id: "!room:server", + sender: ourUserId, + type: EventType.RoomMessage, + event_id: "$foo:bar", + content: { + body: "alt for a test video", + info: { + duration: 420, + w: 40, + h: 50, + thumbnail_file: { + url: "mxc://server/encrypted-poster", + }, + }, + file: { + url: "mxc://server/encrypted-image", + }, + }, + }); + const { asFragment } = render( + , + withClientContextRenderOptions(cli), + ); - const defaultProps: IBodyProps = { - mxEvent: event, - highlights: [], - highlightLink: "", - onMessageAllowed: jest.fn(), - permalinkCreator: {} as RoomPermalinkCreator, - mediaEventHelper: { media: { isEncrypted: false } } as MediaEventHelper, - }; - - const mockClient = getMockClientWithEventEmitter({ - mxcUrlToHttp: jest.fn(), - getRoom: jest.fn(), + expect(fetchMock).toHaveFetched(thumbUrl); + expect(asFragment()).toMatchSnapshot(); + }); }); - - return render( - - - , - ); -} +}); diff --git a/test/unit-tests/components/views/messages/__snapshots__/MVideoBody-test.tsx.snap b/test/unit-tests/components/views/messages/__snapshots__/MVideoBody-test.tsx.snap index 086941082cd..06ca3bfc821 100644 --- a/test/unit-tests/components/views/messages/__snapshots__/MVideoBody-test.tsx.snap +++ b/test/unit-tests/components/views/messages/__snapshots__/MVideoBody-test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`MVideoBody does not crash when given a portrait image 1`] = ` +exports[`MVideoBody does not crash when given portrait dimensions 1`] = ` `; + +exports[`MVideoBody with video previews/thumbnails disabled should download video if we were the sender 1`] = ` + + +
+