Skip to content

Add support for hiding videos #29496

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 44 commits into from
Mar 24, 2025
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
6d5442a
start hide
Half-Shot Mar 11, 2025
9376d71
Move useSettingsValueWithSetter to useSettings
Half-Shot Mar 11, 2025
0cc0645
Add new setting showMediaEventIds
Half-Shot Mar 11, 2025
4d29046
Add a migration path
Half-Shot Mar 11, 2025
72c2a3e
Add an action button to hide settings.
Half-Shot Mar 11, 2025
4e34adb
Tweaks to MImageBody to support new setting.
Half-Shot Mar 11, 2025
7197093
Fixup and add tests
Half-Shot Mar 11, 2025
c8b55c3
add description for migration
Half-Shot Mar 11, 2025
c759e51
docs fixes
Half-Shot Mar 11, 2025
a518c8d
add type
Half-Shot Mar 11, 2025
d6fb24d
i18n
Half-Shot Mar 11, 2025
83e421d
appese prettier
Half-Shot Mar 11, 2025
e87eb12
Add tests for HideActionButton
Half-Shot Mar 11, 2025
cf7e52c
lint
Half-Shot Mar 11, 2025
b9c0d63
lint
Half-Shot Mar 11, 2025
46b1234
Merge branch 'develop' into hs/add-hide-image-button
Half-Shot Mar 13, 2025
939ff4e
First pass at support for previewing/hiding images.
Half-Shot Mar 14, 2025
debb6ad
Add a test for video files.
Half-Shot Mar 14, 2025
9927406
First pass at supporting hiding video files.
Half-Shot Mar 14, 2025
7696516
Use a hook for media visibility.
Half-Shot Mar 17, 2025
ef32747
Drop setting hook usage.
Half-Shot Mar 17, 2025
28ea915
Merge branch 'develop' into hs/add-hide-image-button
Half-Shot Mar 17, 2025
d0b8564
Fixup MImageBody test
Half-Shot Mar 17, 2025
571a2e3
Fixup tests
Half-Shot Mar 17, 2025
60eeb8a
Support functional components for message body rendering.
Half-Shot Mar 17, 2025
71257d9
Merge remote-tracking branch 'origin/develop' into hs/add-hide-image-…
Half-Shot Mar 17, 2025
93009d4
Add a comment
Half-Shot Mar 17, 2025
d7a185b
Move props into IProps
Half-Shot Mar 17, 2025
e057a2e
Merge branch 'hs/add-hide-image-button' into hs/add-hide-image-button…
Half-Shot Mar 17, 2025
6f92b53
Use new wrapping logic
Half-Shot Mar 17, 2025
bc264ff
lint
Half-Shot Mar 17, 2025
1b21343
fixup
Half-Shot Mar 17, 2025
e939709
allow for a delay for the image to render
Half-Shot Mar 17, 2025
c1f0724
remove .only
Half-Shot Mar 17, 2025
c6359e6
lint
Half-Shot Mar 17, 2025
265278d
Merge remote-tracking branch 'origin/develop' into hs/add-hide-image-…
Half-Shot Mar 19, 2025
9a0857f
Fix jest test
Half-Shot Mar 19, 2025
89353db
Fixup tests.
Half-Shot Mar 20, 2025
3b27416
make tests happy
Half-Shot Mar 20, 2025
0512bec
Improve comments
Half-Shot Mar 20, 2025
7a423a0
Merge branch 'develop' into hs/add-hide-image-button+video-preview
Half-Shot Mar 20, 2025
ab40dc2
Merge remote-tracking branch 'origin/develop' into hs/add-hide-image-…
Half-Shot Mar 24, 2025
75a2c2f
review fixes
Half-Shot Mar 24, 2025
026f40a
unbreak test
Half-Shot Mar 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 35 additions & 1 deletion playwright/e2e/timeline/timeline.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2024, 2025 New Vector Ltd.
Copyright 2022, 2023 The Matrix.org Foundation C.I.C.

SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Expand Down Expand Up @@ -28,6 +28,8 @@
const OLD_NAME = "Alan";
const NEW_NAME = "Alan (away)";

const VIDEO_FILE = fs.readFileSync("playwright/sample-files/5secvid.webm");

const getEventTilesWithBodies = (page: Page): Locator => {
return page.locator(".mx_EventTile").filter({ has: page.locator(".mx_EventTile_body") });
};
Expand Down Expand Up @@ -403,7 +405,7 @@
await expect(locator).toHaveCSS("min-width", "46px");
}
// Record alignment of collapsed GELS and messages on messagePanel
await expect(page.locator(".mx_MainSplit")).toMatchScreenshot(

Check failure on line 408 in playwright/e2e/timeline/timeline.spec.ts

View workflow job for this annotation

GitHub Actions / Run Tests [Chrome] 5/6

[Chrome] › playwright/e2e/timeline/timeline.spec.ts:345:13 › Timeline › message displaying › should align generic event list summary with messages and emote on IRC layout @screenshot

1) [Chrome] › playwright/e2e/timeline/timeline.spec.ts:345:13 › Timeline › message displaying › should align generic event list summary with messages and emote on IRC layout @screenshot Error: expect(locator).toHaveScreenshot(expected) 252 pixels (ratio 0.01 of all image pixels) are different. Expected: /home/runner/work/element-web/element-web/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-and-messages-irc-layout-linux.png Received: /home/runner/work/element-web/element-web/playwright/test-results/timeline-timeline-Timeline-5dcf4-ges-and-emote-on-IRC-layout-Chrome/collapsed-gels-and-messages-irc-layout-actual.png Diff: /home/runner/work/element-web/element-web/playwright/test-results/timeline-timeline-Timeline-5dcf4-ges-and-emote-on-IRC-layout-Chrome/collapsed-gels-and-messages-irc-layout-diff.png Call log: - expect.toHaveScreenshot(collapsed-gels-and-messages-irc-layout.png) with timeout 5000ms - verifying given screenshot expectation - waiting for locator('.mx_MainSplit') - locator resolved to <div class="mx_MainSplit">…</div> - taking element screenshot - disabled all CSS animations - waiting for fonts to load... - fonts loaded - attempting scroll into view action - waiting for element to be stable - 40 pixels (ratio 0.01 of all image pixels) are different. - waiting 100ms before taking screenshot - waiting for locator('.mx_MainSplit') - locator resolved to <div class="mx_MainSplit">…</div> - taking element screenshot - disabled all CSS animations - waiting for fonts to load... - fonts loaded - attempting scroll into view action - waiting for element to be stable - 212 pixels (ratio 0.01 of all image pixels) are different. - waiting 250ms before taking screenshot - waiting for locator('.mx_MainSplit') - locator resolved to <div class="mx_MainSplit">…</div> - taking element screenshot - disabled all CSS animations - waiting for fonts to load... - fonts loaded - attempting scroll into view action - waiting for element to be stable - captured a stable screenshot - 252 pixels (ratio 0.01 of all image pixels) are different. 406 | } 407 | // Record alignment of collapsed GELS and messages on messagePanel > 408 | await expect(page.locator(".mx_MainSplit")).toMatchScreenshot( | ^ 409 | "collapsed-gels-and-messages-irc-layout.png", 410 | { 411 | // Exclude timestamp from snapshot of mx_MainSplit at /home/runner/work/element-web/element-web/playwright/e2e/timeline/timeline.spec.ts:408:61
"collapsed-gels-and-messages-irc-layout.png",
{
// Exclude timestamp from snapshot of mx_MainSplit
Expand Down Expand Up @@ -909,6 +911,38 @@
mask: [page.locator(".mx_MessageTimestamp")],
});
});

test("should be able to hide an image", { tag: "@screenshot" }, async ({ page, app, room, context }) => {
await app.viewRoomById(room.roomId);
await sendImage(app.client, 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();
});

test("should be able to hide a video", { tag: "@screenshot" }, async ({ page, app, 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, {
msgtype: "m.video" as MsgType,
body: "bbb.webm",
url: upload.content_uri,
});

await app.timeline.scrollToBottom();
const imgTile = page.locator(".mx_MVideoBody").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 video" })).toBeVisible();
});
});

test.describe("message sending", { tag: ["@no-firefox", "@no-webkit"] }, () => {
Expand Down
Binary file added playwright/sample-files/5secvid.webm
Binary file not shown.
1 change: 1 addition & 0 deletions res/css/_components.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@
@import "./views/messages/_DisambiguatedProfile.pcss";
@import "./views/messages/_EventTileBubble.pcss";
@import "./views/messages/_HiddenBody.pcss";
@import "./views/messages/_HiddenMediaPlaceholder.pcss";
@import "./views/messages/_JumpToDatePicker.pcss";
@import "./views/messages/_LegacyCallEvent.pcss";
@import "./views/messages/_MEmoteBody.pcss";
Expand Down
29 changes: 29 additions & 0 deletions res/css/views/messages/_HiddenMediaPlaceholder.pcss
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
.mx_HiddenMediaPlaceholder {
border: none;
width: 100%;
height: 100%;
inset: 0;

/* To center the text in the middle of the frame */
display: flex;
align-items: center;
justify-content: center;
text-align: center;

cursor: pointer;
background-color: $header-panel-bg-color;

> div {
color: $accent;
/* Icon alignment */
display: flex;
> svg {
margin-top: auto;
margin-bottom: auto;
}
}
}

.mx_EventTile:hover .mx_HiddenMediaPlaceholder {
background-color: $background;
}
36 changes: 0 additions & 36 deletions res/css/views/messages/_MImageBody.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -79,39 +79,3 @@ Please see LICENSE files in the repository root for full details.
color: $imagebody-giflabel-color;
pointer-events: none;
}

.mx_HiddenImagePlaceholder {
position: absolute;
inset: 0;

/* To center the text in the middle of the frame */
display: flex;
align-items: center;
justify-content: center;
text-align: center;

cursor: pointer;
background-color: $header-panel-bg-color;

.mx_HiddenImagePlaceholder_button {
color: $accent;

span.mx_HiddenImagePlaceholder_eye {
margin-right: 8px;

background-color: $accent;
mask-image: url("$(res)/img/element-icons/eye.svg");
display: inline-block;
width: 18px;
height: 14px;
}

span:not(.mx_HiddenImagePlaceholder_eye) {
vertical-align: text-bottom;
}
}
}

.mx_EventTile:hover .mx_HiddenImagePlaceholder {
background-color: $background;
}
55 changes: 2 additions & 53 deletions src/components/views/directory/NetworkDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/

import { without } from "lodash";
import React, { useCallback, useEffect, useState } from "react";
import React, { useCallback } from "react";
import { MatrixError } from "matrix-js-sdk/src/matrix";

import { MenuItemRadio } from "../../../accessibility/context_menu/MenuItemRadio";
Expand All @@ -16,7 +16,6 @@ import { MatrixClientPeg } from "../../../MatrixClientPeg";
import Modal from "../../../Modal";
import SdkConfig from "../../../SdkConfig";
import { SettingLevel } from "../../../settings/SettingLevel";
import SettingsStore from "../../../settings/SettingsStore";
import { type Protocols } from "../../../utils/DirectoryUtils";
import {
type AdditionalOptionsProps,
Expand All @@ -26,7 +25,7 @@ import {
import TextInputDialog from "../dialogs/TextInputDialog";
import AccessibleButton from "../elements/AccessibleButton";
import withValidation from "../elements/Validation";
import { type SettingKey, type Settings } from "../../../settings/Settings.tsx";
import { useSettingsValueWithSetter } from "../../../hooks/useSettings.ts";

const SETTING_NAME = "room_directory_servers";

Expand Down Expand Up @@ -68,56 +67,6 @@ const validServer = withValidation<undefined, { error?: unknown }>({
memoize: true,
});

function useSettingsValueWithSetter<S extends SettingKey>(
settingName: S,
level: SettingLevel,
roomId: string | null,
excludeDefault: true,
): [Settings[S]["default"] | undefined, (value: Settings[S]["default"]) => Promise<void>];
function useSettingsValueWithSetter<S extends SettingKey>(
settingName: S,
level: SettingLevel,
roomId?: string | null,
excludeDefault?: false,
): [Settings[S]["default"], (value: Settings[S]["default"]) => Promise<void>];
function useSettingsValueWithSetter<S extends SettingKey>(
settingName: S,
level: SettingLevel,
roomId: string | null = null,
excludeDefault = false,
): [Settings[S]["default"] | undefined, (value: Settings[S]["default"]) => Promise<void>] {
const [value, setValue] = useState(
// XXX: This seems naff but is needed to convince TypeScript that the overload is fine
excludeDefault
? SettingsStore.getValue(settingName, roomId, excludeDefault)
: SettingsStore.getValue(settingName, roomId, excludeDefault),
);
const setter = useCallback(
async (value: Settings[S]["default"]): Promise<void> => {
setValue(value);
SettingsStore.setValue(settingName, roomId, level, value);
},
[level, roomId, settingName],
);

useEffect(() => {
const ref = SettingsStore.watchSetting(settingName, roomId, () => {
setValue(
// XXX: This seems naff but is needed to convince TypeScript that the overload is fine
excludeDefault
? SettingsStore.getValue(settingName, roomId, excludeDefault)
: SettingsStore.getValue(settingName, roomId, excludeDefault),
);
});
// clean-up
return () => {
SettingsStore.unwatchSetting(ref);
};
}, [settingName, roomId, excludeDefault]);

return [value, setter];
}

interface ServerList {
allServers: string[];
homeServer: string;
Expand Down
31 changes: 31 additions & 0 deletions src/components/views/messages/HiddenMediaPlaceholder.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
Copyright 2025 New Vector Ltd.

SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/

import React, { type MouseEventHandler } from "react";
import { VisibilityOnIcon } from "@vector-im/compound-design-tokens/assets/web/icons";

import { _t } from "../../../languageHandler";

interface IProps {
kind: "m.image" | "m.video";
onClick: MouseEventHandler<HTMLButtonElement>;
}

const HiddenMediaPlaceholder: React.FunctionComponent<IProps> = (props) => {
return (
<button onClick={props.onClick} className="mx_HiddenMediaPlaceholder">
<div>
<VisibilityOnIcon />
<span>
{props.kind === "m.image" ? _t("timeline|m.image|show_image") : _t("timeline|m.video|show_video")}
</span>
</div>
</button>
);
};

export default HiddenMediaPlaceholder;
65 changes: 65 additions & 0 deletions src/components/views/messages/HideActionButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2021 The Matrix.org Foundation C.I.C.

SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/

import { type MatrixEvent } from "matrix-js-sdk/src/matrix";
import React, { useCallback } from "react";
import classNames from "classnames";
import { VisibilityOffIcon } from "@vector-im/compound-design-tokens/assets/web/icons";

import { RovingAccessibleButton } from "../../../accessibility/RovingTabIndex";
import { _t } from "../../../languageHandler";
import { SettingLevel } from "../../../settings/SettingLevel";
import { useSettingsValueWithSetter, useSettingValue } from "../../../hooks/useSettings";

interface IProps {
mxEvent: MatrixEvent;
}

const HideActionButton: React.FC<IProps> = ({ mxEvent }) => {
const eventId = mxEvent.getId()!;
let spinner: JSX.Element | undefined;
const defaultShowImages = useSettingValue("showImages", SettingLevel.DEVICE);
const [eventVisibility, setEventIds] = useSettingsValueWithSetter("showMediaEventIds", SettingLevel.DEVICE);
const onClick = useCallback(() => {
if (!eventId) {
return;
}
setEventIds({
...eventVisibility,
[eventId]: false,
});
}, [setEventIds, eventId, eventVisibility]);

const classes = classNames({
mx_MessageActionBar_iconButton: true,
mx_MessageActionBar_downloadButton: true,
mx_MessageActionBar_downloadSpinnerButton: !!spinner,
});

const imgIsVisible =
eventVisibility[eventId] === true || (defaultShowImages && eventVisibility[eventId] === undefined);

if (!imgIsVisible) {
return;
}

return (
<RovingAccessibleButton
className={classes}
title={_t("action|hide")}
onClick={onClick}
disabled={!!spinner}
placement="left"
>
<VisibilityOffIcon />
{spinner}
</RovingAccessibleButton>
);
};

export default HideActionButton;
Loading
Loading