Skip to content
Merged
42 changes: 28 additions & 14 deletions playwright/e2e/timeline/timeline.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -908,23 +908,37 @@ test.describe("Timeline", () => {
});
});

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

// Check that the image is now hidden.
await expect(page.getByRole("button", { name: "Show image" })).toBeVisible();
});
const bot = new Bot(page, homeserver, {});
await bot.prepareClient();
await app.client.inviteUser(room.roomId, bot.credentials.userId);

test("should be able to hide a video", async ({ page, app, room, context }) => {
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();
},
);

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,
Expand Down
2 changes: 1 addition & 1 deletion src/components/views/messages/EventContentBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ interface Props extends ReplacerOptions {
const EventContentBody = memo(
({ as, mxEvent, stripReply, content, linkify, highlights, includeDir = true, ref, ...options }: Props) => {
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(
Expand Down
4 changes: 2 additions & 2 deletions src/components/views/messages/HideActionButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ interface IProps {
* Quick action button for marking a media event as hidden.
*/
export const HideActionButton: React.FC<IProps> = ({ mxEvent }) => {
const [mediaIsVisible, setVisible] = useMediaVisible(mxEvent.getId(), mxEvent.getRoomId());
const [mediaIsVisible, setVisible] = useMediaVisible(mxEvent);

if (!mediaIsVisible) {
if (!mediaIsVisible || !setVisible) {
return;
}

Expand Down
6 changes: 3 additions & 3 deletions src/components/views/messages/MImageBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was always optional an this is just fixing the type? As it seems slightly weird for it to be optional. Could probably do with some doc?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, at the time this was an attempt to make it so that if it was your own media then you wouldn't have a button to hide media. However this has led to a lot of extra code, and I think there might be a valid use case for potentially hiding images you don't want visible on your client.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh right, okay - it just not having the method in that case seems like a slightly weird interface, but not going to block on it

}

/**
Expand Down Expand Up @@ -95,7 +95,7 @@ export class MImageBodyInner extends React.Component<IProps, IState> {
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;
}
Expand Down Expand Up @@ -686,7 +686,7 @@ export class MImageBodyInner extends React.Component<IProps, IState> {

// Wrap MImageBody component so we can use a hook here.
const MImageBody: React.FC<IBodyProps> = (props) => {
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId(), props.mxEvent.getRoomId());
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent);
return <MImageBodyInner mediaVisible={mediaVisible} setMediaVisible={setVisible} {...props} />;
};

Expand Down
2 changes: 1 addition & 1 deletion src/components/views/messages/MImageReplyBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class MImageReplyBodyInner extends MImageBodyInner {
}
}
const MImageReplyBody: React.FC<IBodyProps> = (props) => {
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId(), props.mxEvent.getRoomId());
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent);
return <MImageReplyBodyInner mediaVisible={mediaVisible} setMediaVisible={setVisible} {...props} />;
};

Expand Down
2 changes: 1 addition & 1 deletion src/components/views/messages/MStickerBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ class MStickerBodyInner extends MImageBodyInner {
}

const MStickerBody: React.FC<IBodyProps> = (props) => {
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId(), props.mxEvent.getRoomId());
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent);
return <MStickerBodyInner mediaVisible={mediaVisible} setMediaVisible={setVisible} {...props} />;
};

Expand Down
6 changes: 3 additions & 3 deletions src/components/views/messages/MVideoBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<IProps, IState> {
Expand All @@ -64,7 +64,7 @@ class MVideoBodyInner extends React.PureComponent<IProps, IState> {
};

private onClick = (): void => {
this.props.setMediaVisible(true);
this.props.setMediaVisible?.(true);
};

private getContentUrl(): string | undefined {
Expand Down Expand Up @@ -342,7 +342,7 @@ class MVideoBodyInner extends React.PureComponent<IProps, IState> {

// Wrap MVideoBody component so we can use a hook here.
const MVideoBody: React.FC<IBodyProps> = (props) => {
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId(), props.mxEvent.getRoomId());
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent);
return <MVideoBodyInner mediaVisible={mediaVisible} setMediaVisible={setVisible} {...props} />;
};

Expand Down
2 changes: 1 addition & 1 deletion src/components/views/messages/MessageActionBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -538,7 +538,7 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
/>,
);
}
if (MediaEventHelper.canHide(this.props.mxEvent)) {
if (MediaEventHelper.canHide(this.props.mxEvent, MatrixClientPeg.safeGet().getSafeUserId())) {
toolbarOpts.splice(0, 0, <HideActionButton mxEvent={this.props.mxEvent} key="hide" />);
}
} else if (
Expand Down
2 changes: 1 addition & 1 deletion src/components/views/rooms/LinkPreviewGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ interface IProps {
const LinkPreviewGroup: React.FC<IProps> = ({ 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][]>(
Expand Down
28 changes: 22 additions & 6 deletions src/hooks/useMediaVisible.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, type MatrixEvent } from "matrix-js-sdk/src/matrix";

import { SettingLevel } from "../settings/SettingLevel";
import { useSettingValue } from "./useSettings";
Expand All @@ -19,14 +19,26 @@ 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.
*
* 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.
*
* @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).
*
*/
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, {
Expand All @@ -37,6 +49,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;
Expand Down
5 changes: 3 additions & 2 deletions src/utils/MediaEventHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ describe("HideActionButton", () => {
beforeEach(() => {
cli = getMockClientWithEventEmitter({
getRoom: jest.fn(),
getUserId: jest.fn(),
});
});
afterEach(() => {
Expand All @@ -73,6 +74,13 @@ describe("HideActionButton", () => {
render(<HideActionButton mxEvent={event} />, 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(<HideActionButton mxEvent={event} />, withClientContextRenderOptions(cli));
expect(screen.queryByRole("button")).toBeNull();
});
it("should store event as hidden when clicked", async () => {
const spy = jest.spyOn(SettingsStore, "setValue");
render(<HideActionButton mxEvent={event} />, withClientContextRenderOptions(cli));
Expand Down
Loading
Loading