mirror of
https://github.com/element-hq/element-web.git
synced 2025-12-11 01:40:42 +00:00
Compare commits
25 Commits
toger5/aut
...
hs/add-hid
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7a185baea | ||
|
|
93009d4613 | ||
|
|
71257d97e7 | ||
|
|
60eeb8a7de | ||
|
|
571a2e373d | ||
|
|
d0b8564660 | ||
|
|
28ea91566a | ||
|
|
ef32747473 | ||
|
|
7696516e8b | ||
|
|
46b1234a1d | ||
|
|
b9c0d63e3e | ||
|
|
cf7e52c6fc | ||
|
|
e87eb127ee | ||
|
|
83e421daf2 | ||
|
|
d6fb24dea7 | ||
|
|
a518c8d662 | ||
|
|
c759e516bd | ||
|
|
c8b55c3dfe | ||
|
|
7197093744 | ||
|
|
4e34adb854 | ||
|
|
72c2a3eb07 | ||
|
|
4d290461c4 | ||
|
|
0cc06450d7 | ||
|
|
9376d71831 | ||
|
|
6d5442a87b |
@@ -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.
|
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
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
@@ -905,6 +905,19 @@ test.describe("Timeline", () => {
|
|||||||
mask: [page.locator(".mx_MessageTimestamp")],
|
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("link", { name: "Show image" })).toBeVisible();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe("message sending", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
test.describe("message sending", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||||
|
|||||||
44
src/components/views/messages/HideActionButton.tsx
Normal file
44
src/components/views/messages/HideActionButton.tsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
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 from "react";
|
||||||
|
import { VisibilityOffIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||||
|
|
||||||
|
import { RovingAccessibleButton } from "../../../accessibility/RovingTabIndex";
|
||||||
|
import { _t } from "../../../languageHandler";
|
||||||
|
import { useMediaVisible } from "../../../hooks/useMediaVisible";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
/**
|
||||||
|
* Matrix event that this action applies to.
|
||||||
|
*/
|
||||||
|
mxEvent: MatrixEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Quick action button for marking a media event as hidden.
|
||||||
|
*/
|
||||||
|
export const HideActionButton: React.FC<IProps> = ({ mxEvent }) => {
|
||||||
|
const [mediaIsVisible, setVisible] = useMediaVisible(mxEvent.getId()!);
|
||||||
|
|
||||||
|
if (!mediaIsVisible) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RovingAccessibleButton
|
||||||
|
className="mx_MessageActionBar_iconButton "
|
||||||
|
title={_t("action|hide")}
|
||||||
|
onClick={() => setVisible(false)}
|
||||||
|
placement="left"
|
||||||
|
>
|
||||||
|
<VisibilityOffIcon />
|
||||||
|
</RovingAccessibleButton>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -93,7 +93,7 @@ export function computedStyle(element: HTMLElement | null): string {
|
|||||||
|
|
||||||
interface IProps extends IBodyProps {
|
interface IProps extends IBodyProps {
|
||||||
/* whether or not to show the default placeholder for the file. Defaults to true. */
|
/* whether or not to show the default placeholder for the file. Defaults to true. */
|
||||||
showGenericPlaceholder: boolean;
|
showGenericPlaceholder?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
@@ -105,11 +105,6 @@ export default class MFileBody extends React.Component<IProps, IState> {
|
|||||||
declare public context: React.ContextType<typeof RoomContext>;
|
declare public context: React.ContextType<typeof RoomContext>;
|
||||||
|
|
||||||
public state: IState = {};
|
public state: IState = {};
|
||||||
|
|
||||||
public static defaultProps = {
|
|
||||||
showGenericPlaceholder: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
private iframe: React.RefObject<HTMLIFrameElement> = createRef();
|
private iframe: React.RefObject<HTMLIFrameElement> = createRef();
|
||||||
private dummyLink: React.RefObject<HTMLAnchorElement> = createRef();
|
private dummyLink: React.RefObject<HTMLAnchorElement> = createRef();
|
||||||
private userDidClick = false;
|
private userDidClick = false;
|
||||||
@@ -191,15 +186,17 @@ export default class MFileBody extends React.Component<IProps, IState> {
|
|||||||
const contentUrl = this.getContentUrl();
|
const contentUrl = this.getContentUrl();
|
||||||
const contentFileSize = this.content.info ? this.content.info.size : null;
|
const contentFileSize = this.content.info ? this.content.info.size : null;
|
||||||
const fileType = this.content.info?.mimetype ?? "application/octet-stream";
|
const fileType = this.content.info?.mimetype ?? "application/octet-stream";
|
||||||
|
// defaultProps breaks types on IBodyProps, so instead define the default here.
|
||||||
|
const showGenericPlaceholder = this.props.showGenericPlaceholder ?? true;
|
||||||
|
|
||||||
let showDownloadLink =
|
let showDownloadLink =
|
||||||
!this.props.showGenericPlaceholder ||
|
!showGenericPlaceholder ||
|
||||||
(this.context.timelineRenderingType !== TimelineRenderingType.Room &&
|
(this.context.timelineRenderingType !== TimelineRenderingType.Room &&
|
||||||
this.context.timelineRenderingType !== TimelineRenderingType.Search &&
|
this.context.timelineRenderingType !== TimelineRenderingType.Search &&
|
||||||
this.context.timelineRenderingType !== TimelineRenderingType.Pinned);
|
this.context.timelineRenderingType !== TimelineRenderingType.Pinned);
|
||||||
|
|
||||||
let placeholder: React.ReactNode = null;
|
let placeholder: React.ReactNode = null;
|
||||||
if (this.props.showGenericPlaceholder) {
|
if (showGenericPlaceholder) {
|
||||||
placeholder = (
|
placeholder = (
|
||||||
<AccessibleButton className="mx_MediaBody mx_MFileBody_info" onClick={this.onPlaceholderClick}>
|
<AccessibleButton className="mx_MediaBody mx_MFileBody_info" onClick={this.onPlaceholderClick}>
|
||||||
<span className="mx_MFileBody_info_icon" />
|
<span className="mx_MFileBody_info_icon" />
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import { presentableTextForFile } from "../../../utils/FileUtils";
|
|||||||
import { createReconnectedListener } from "../../../utils/connection";
|
import { createReconnectedListener } from "../../../utils/connection";
|
||||||
import MediaProcessingError from "./shared/MediaProcessingError";
|
import MediaProcessingError from "./shared/MediaProcessingError";
|
||||||
import { DecryptError, DownloadError } from "../../../utils/DecryptFile";
|
import { DecryptError, DownloadError } from "../../../utils/DecryptFile";
|
||||||
|
import { useMediaVisible } from "../../../hooks/useMediaVisible";
|
||||||
|
|
||||||
enum Placeholder {
|
enum Placeholder {
|
||||||
NoImage,
|
NoImage,
|
||||||
@@ -52,11 +53,25 @@ interface IState {
|
|||||||
};
|
};
|
||||||
hover: boolean;
|
hover: boolean;
|
||||||
focus: boolean;
|
focus: boolean;
|
||||||
showImage: boolean;
|
|
||||||
placeholder: Placeholder;
|
placeholder: Placeholder;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class MImageBody extends React.Component<IBodyProps, IState> {
|
interface IProps extends IBodyProps {
|
||||||
|
/**
|
||||||
|
* Should the media be behind a preview.
|
||||||
|
*/
|
||||||
|
mediaVisible: boolean;
|
||||||
|
/**
|
||||||
|
* Set the visibility of the media event.
|
||||||
|
* @param visible Should the event be visible.
|
||||||
|
*/
|
||||||
|
setMediaVisible: (visible: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private Only use for inheritance. Use the default export for presentation.
|
||||||
|
*/
|
||||||
|
export class MImageBodyInner extends React.Component<IProps, IState> {
|
||||||
public static contextType = RoomContext;
|
public static contextType = RoomContext;
|
||||||
declare public context: React.ContextType<typeof RoomContext>;
|
declare public context: React.ContextType<typeof RoomContext>;
|
||||||
|
|
||||||
@@ -73,21 +88,14 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
|
|||||||
imgLoaded: false,
|
imgLoaded: false,
|
||||||
hover: false,
|
hover: false,
|
||||||
focus: false,
|
focus: false,
|
||||||
showImage: SettingsStore.getValue("showImages"),
|
|
||||||
placeholder: Placeholder.NoImage,
|
placeholder: Placeholder.NoImage,
|
||||||
};
|
};
|
||||||
|
|
||||||
protected showImage(): void {
|
|
||||||
localStorage.setItem("mx_ShowImage_" + this.props.mxEvent.getId(), "true");
|
|
||||||
this.setState({ showImage: true });
|
|
||||||
this.downloadImage();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected onClick = (ev: React.MouseEvent): void => {
|
protected onClick = (ev: React.MouseEvent): void => {
|
||||||
if (ev.button === 0 && !ev.metaKey) {
|
if (ev.button === 0 && !ev.metaKey) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
if (!this.state.showImage) {
|
if (!this.props.mediaVisible) {
|
||||||
this.showImage();
|
this.props.setMediaVisible?.(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,7 +133,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
|
|||||||
private get shouldAutoplay(): boolean {
|
private get shouldAutoplay(): boolean {
|
||||||
return !(
|
return !(
|
||||||
!this.state.contentUrl ||
|
!this.state.contentUrl ||
|
||||||
!this.state.showImage ||
|
!this.props.mediaVisible ||
|
||||||
!this.state.isAnimated ||
|
!this.state.isAnimated ||
|
||||||
SettingsStore.getValue("autoplayGifs")
|
SettingsStore.getValue("autoplayGifs")
|
||||||
);
|
);
|
||||||
@@ -346,14 +354,10 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
|
|||||||
public componentDidMount(): void {
|
public componentDidMount(): void {
|
||||||
this.unmounted = false;
|
this.unmounted = false;
|
||||||
|
|
||||||
const showImage =
|
if (this.props.mediaVisible) {
|
||||||
this.state.showImage || localStorage.getItem("mx_ShowImage_" + this.props.mxEvent.getId()) === "true";
|
|
||||||
|
|
||||||
if (showImage) {
|
|
||||||
// noinspection JSIgnoredPromiseFromCall
|
// noinspection JSIgnoredPromiseFromCall
|
||||||
this.downloadImage();
|
this.downloadImage();
|
||||||
this.setState({ showImage: true });
|
}
|
||||||
} // else don't download anything because we don't want to display anything.
|
|
||||||
|
|
||||||
// Add a 150ms timer for blurhash to first appear.
|
// Add a 150ms timer for blurhash to first appear.
|
||||||
if (this.props.mxEvent.getContent().info?.[BLURHASH_FIELD]) {
|
if (this.props.mxEvent.getContent().info?.[BLURHASH_FIELD]) {
|
||||||
@@ -372,6 +376,13 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public componentDidUpdate(prevProps: Readonly<IProps>): void {
|
||||||
|
if (!prevProps.mediaVisible && this.props.mediaVisible) {
|
||||||
|
// noinspection JSIgnoredPromiseFromCall
|
||||||
|
this.downloadImage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public componentWillUnmount(): void {
|
public componentWillUnmount(): void {
|
||||||
this.unmounted = true;
|
this.unmounted = true;
|
||||||
MatrixClientPeg.get()?.off(ClientEvent.Sync, this.reconnectedListener);
|
MatrixClientPeg.get()?.off(ClientEvent.Sync, this.reconnectedListener);
|
||||||
@@ -425,7 +436,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
|
|||||||
// by the same width and height logic below.
|
// by the same width and height logic below.
|
||||||
if (!this.state.loadedImageDimensions) {
|
if (!this.state.loadedImageDimensions) {
|
||||||
let imageElement: JSX.Element;
|
let imageElement: JSX.Element;
|
||||||
if (!this.state.showImage) {
|
if (!this.props.mediaVisible) {
|
||||||
imageElement = <HiddenImagePlaceholder />;
|
imageElement = <HiddenImagePlaceholder />;
|
||||||
} else {
|
} else {
|
||||||
imageElement = (
|
imageElement = (
|
||||||
@@ -495,7 +506,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.state.showImage) {
|
if (!this.props.mediaVisible) {
|
||||||
img = <HiddenImagePlaceholder maxWidth={maxWidth} />;
|
img = <HiddenImagePlaceholder maxWidth={maxWidth} />;
|
||||||
showPlaceholder = false; // because we're hiding the image, so don't show the placeholder.
|
showPlaceholder = false; // because we're hiding the image, so don't show the placeholder.
|
||||||
}
|
}
|
||||||
@@ -506,7 +517,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let banner: ReactNode | undefined;
|
let banner: ReactNode | undefined;
|
||||||
if (this.state.showImage && hoverOrFocus) {
|
if (this.props.mediaVisible && hoverOrFocus) {
|
||||||
banner = this.getBanner(content);
|
banner = this.getBanner(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -585,7 +596,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
|
|||||||
{children}
|
{children}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
} else if (!this.state.showImage) {
|
} else if (!this.props.mediaVisible) {
|
||||||
return (
|
return (
|
||||||
<div role="button" onClick={this.onClick}>
|
<div role="button" onClick={this.onClick}>
|
||||||
{children}
|
{children}
|
||||||
@@ -686,3 +697,10 @@ export class HiddenImagePlaceholder extends React.PureComponent<PlaceholderIProp
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MImageBody: React.FC<IBodyProps> = (props) => {
|
||||||
|
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId()!);
|
||||||
|
return <MImageBodyInner mediaVisible={mediaVisible} setMediaVisible={setVisible} {...props} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MImageBody;
|
||||||
|
|||||||
@@ -9,11 +9,13 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { type ImageContent } from "matrix-js-sdk/src/types";
|
import { type ImageContent } from "matrix-js-sdk/src/types";
|
||||||
|
|
||||||
import MImageBody from "./MImageBody";
|
import { MImageBodyInner } from "./MImageBody";
|
||||||
|
import { type IBodyProps } from "./IBodyProps";
|
||||||
|
import { useMediaVisible } from "../../../hooks/useMediaVisible";
|
||||||
|
|
||||||
const FORCED_IMAGE_HEIGHT = 44;
|
const FORCED_IMAGE_HEIGHT = 44;
|
||||||
|
|
||||||
export default class MImageReplyBody extends MImageBody {
|
class MImageReplyBodyInner extends MImageBodyInner {
|
||||||
public onClick = (ev: React.MouseEvent): void => {
|
public onClick = (ev: React.MouseEvent): void => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
};
|
};
|
||||||
@@ -35,3 +37,9 @@ export default class MImageReplyBody extends MImageBody {
|
|||||||
return <div className="mx_MImageReplyBody">{thumbnail}</div>;
|
return <div className="mx_MImageReplyBody">{thumbnail}</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const MImageReplyBody: React.FC<IBodyProps> = (props) => {
|
||||||
|
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId()!);
|
||||||
|
return <MImageReplyBodyInner mediaVisible={mediaVisible} setMediaVisible={setVisible} {...props} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MImageReplyBody;
|
||||||
|
|||||||
@@ -9,16 +9,18 @@ import React, { type ComponentProps, type ReactNode } from "react";
|
|||||||
import { type Tooltip } from "@vector-im/compound-web";
|
import { type Tooltip } from "@vector-im/compound-web";
|
||||||
import { type MediaEventContent } from "matrix-js-sdk/src/types";
|
import { type MediaEventContent } from "matrix-js-sdk/src/types";
|
||||||
|
|
||||||
import MImageBody from "./MImageBody";
|
import { MImageBodyInner } from "./MImageBody";
|
||||||
import { BLURHASH_FIELD } from "../../../utils/image-media";
|
import { BLURHASH_FIELD } from "../../../utils/image-media";
|
||||||
import IconsShowStickersSvg from "../../../../res/img/icons-show-stickers.svg";
|
import IconsShowStickersSvg from "../../../../res/img/icons-show-stickers.svg";
|
||||||
|
import { type IBodyProps } from "./IBodyProps";
|
||||||
|
import { useMediaVisible } from "../../../hooks/useMediaVisible";
|
||||||
|
|
||||||
export default class MStickerBody extends MImageBody {
|
class MStickerBodyInner extends MImageBodyInner {
|
||||||
// Mostly empty to prevent default behaviour of MImageBody
|
// Mostly empty to prevent default behaviour of MImageBody
|
||||||
protected onClick = (ev: React.MouseEvent): void => {
|
protected onClick = (ev: React.MouseEvent): void => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
if (!this.state.showImage) {
|
if (!this.props.mediaVisible) {
|
||||||
this.showImage();
|
this.props.setMediaVisible?.(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -26,7 +28,7 @@ export default class MStickerBody extends MImageBody {
|
|||||||
// which is added by mx_MStickerBody_wrapper
|
// which is added by mx_MStickerBody_wrapper
|
||||||
protected wrapImage(contentUrl: string, children: React.ReactNode): JSX.Element {
|
protected wrapImage(contentUrl: string, children: React.ReactNode): JSX.Element {
|
||||||
let onClick: React.MouseEventHandler | undefined;
|
let onClick: React.MouseEventHandler | undefined;
|
||||||
if (!this.state.showImage) {
|
if (!this.props.mediaVisible) {
|
||||||
onClick = this.onClick;
|
onClick = this.onClick;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
@@ -75,3 +77,10 @@ export default class MStickerBody extends MImageBody {
|
|||||||
return null; // we don't need a banner, we have a tooltip
|
return null; // we don't need a banner, we have a tooltip
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MStickerBody: React.FC<IBodyProps> = (props) => {
|
||||||
|
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId()!);
|
||||||
|
return <MStickerBodyInner mediaVisible={mediaVisible} setMediaVisible={setVisible} {...props} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MStickerBody;
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ import { type GetRelationsForEvent, type IEventTileType } from "../rooms/EventTi
|
|||||||
import { type ButtonEvent } from "../elements/AccessibleButton";
|
import { type ButtonEvent } from "../elements/AccessibleButton";
|
||||||
import PinningUtils from "../../../utils/PinningUtils";
|
import PinningUtils from "../../../utils/PinningUtils";
|
||||||
import PosthogTrackers from "../../../PosthogTrackers.ts";
|
import PosthogTrackers from "../../../PosthogTrackers.ts";
|
||||||
|
import { HideActionButton } from "./HideActionButton.tsx";
|
||||||
|
|
||||||
interface IOptionsButtonProps {
|
interface IOptionsButtonProps {
|
||||||
mxEvent: MatrixEvent;
|
mxEvent: MatrixEvent;
|
||||||
@@ -535,6 +536,7 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
|
|||||||
mediaEventHelperGet={() => this.props.getTile()?.getMediaHelper?.()}
|
mediaEventHelperGet={() => this.props.getTile()?.getMediaHelper?.()}
|
||||||
key="download"
|
key="download"
|
||||||
/>,
|
/>,
|
||||||
|
<HideActionButton mxEvent={this.props.mxEvent} key="hide" />,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (
|
} else if (
|
||||||
|
|||||||
@@ -45,8 +45,8 @@ import { type GetRelationsForEvent, type IEventTileOps } from "../rooms/EventTil
|
|||||||
// onMessageAllowed is handled internally
|
// onMessageAllowed is handled internally
|
||||||
interface IProps extends Omit<IBodyProps, "onMessageAllowed" | "mediaEventHelper"> {
|
interface IProps extends Omit<IBodyProps, "onMessageAllowed" | "mediaEventHelper"> {
|
||||||
/* overrides for the msgtype-specific components, used by ReplyTile to override file rendering */
|
/* overrides for the msgtype-specific components, used by ReplyTile to override file rendering */
|
||||||
overrideBodyTypes?: Record<string, typeof React.Component>;
|
overrideBodyTypes?: Record<string, React.ComponentType<IBodyProps>>;
|
||||||
overrideEventTypes?: Record<string, typeof React.Component>;
|
overrideEventTypes?: Record<string, React.ComponentType<IBodyProps>>;
|
||||||
|
|
||||||
// helper function to access relations for this event
|
// helper function to access relations for this event
|
||||||
getRelationsForEvent?: GetRelationsForEvent;
|
getRelationsForEvent?: GetRelationsForEvent;
|
||||||
@@ -58,7 +58,7 @@ export interface IOperableEventTile {
|
|||||||
getEventTileOps(): IEventTileOps | null;
|
getEventTileOps(): IEventTileOps | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseBodyTypes = new Map<string, typeof React.Component>([
|
const baseBodyTypes = new Map<string, React.ComponentType<IBodyProps>>([
|
||||||
[MsgType.Text, TextualBody],
|
[MsgType.Text, TextualBody],
|
||||||
[MsgType.Notice, TextualBody],
|
[MsgType.Notice, TextualBody],
|
||||||
[MsgType.Emote, TextualBody],
|
[MsgType.Emote, TextualBody],
|
||||||
@@ -80,7 +80,7 @@ const baseEvTypes = new Map<string, React.ComponentType<IBodyProps>>([
|
|||||||
export default class MessageEvent extends React.Component<IProps> implements IMediaBody, IOperableEventTile {
|
export default class MessageEvent extends React.Component<IProps> implements IMediaBody, IOperableEventTile {
|
||||||
private body: React.RefObject<React.Component | IOperableEventTile> = createRef();
|
private body: React.RefObject<React.Component | IOperableEventTile> = createRef();
|
||||||
private mediaHelper?: MediaEventHelper;
|
private mediaHelper?: MediaEventHelper;
|
||||||
private bodyTypes = new Map<string, typeof React.Component>(baseBodyTypes.entries());
|
private bodyTypes = new Map<string, React.ComponentType<IBodyProps>>(baseBodyTypes.entries());
|
||||||
private evTypes = new Map<string, React.ComponentType<IBodyProps>>(baseEvTypes.entries());
|
private evTypes = new Map<string, React.ComponentType<IBodyProps>>(baseEvTypes.entries());
|
||||||
|
|
||||||
public static contextType = MatrixClientContext;
|
public static contextType = MatrixClientContext;
|
||||||
@@ -115,7 +115,7 @@ export default class MessageEvent extends React.Component<IProps> implements IMe
|
|||||||
}
|
}
|
||||||
|
|
||||||
private updateComponentMaps(): void {
|
private updateComponentMaps(): void {
|
||||||
this.bodyTypes = new Map<string, typeof React.Component>(baseBodyTypes.entries());
|
this.bodyTypes = new Map<string, React.ComponentType<IBodyProps>>(baseBodyTypes.entries());
|
||||||
for (const [bodyType, bodyComponent] of Object.entries(this.props.overrideBodyTypes ?? {})) {
|
for (const [bodyType, bodyComponent] of Object.entries(this.props.overrideBodyTypes ?? {})) {
|
||||||
this.bodyTypes.set(bodyType, bodyComponent);
|
this.bodyTypes.set(bodyType, bodyComponent);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { type ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPaylo
|
|||||||
import { renderReplyTile } from "../../../events/EventTileFactory";
|
import { renderReplyTile } from "../../../events/EventTileFactory";
|
||||||
import { type GetRelationsForEvent } from "../rooms/EventTile";
|
import { type GetRelationsForEvent } from "../rooms/EventTile";
|
||||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
|
import { type IBodyProps } from "../messages/IBodyProps";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
mxEvent: MatrixEvent;
|
mxEvent: MatrixEvent;
|
||||||
@@ -139,13 +140,13 @@ export default class ReplyTile extends React.PureComponent<IProps> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const msgtypeOverrides: Record<string, typeof React.Component> = {
|
const msgtypeOverrides: Record<string, React.ComponentType<IBodyProps>> = {
|
||||||
[MsgType.Image]: MImageReplyBody,
|
[MsgType.Image]: MImageReplyBody,
|
||||||
// Override audio and video body with file body. We also hide the download/decrypt button using CSS
|
// Override audio and video body with file body. We also hide the download/decrypt button using CSS
|
||||||
[MsgType.Audio]: isVoiceMessage(mxEvent) ? MVoiceMessageBody : MFileBody,
|
[MsgType.Audio]: isVoiceMessage(mxEvent) ? MVoiceMessageBody : MFileBody,
|
||||||
[MsgType.Video]: MFileBody,
|
[MsgType.Video]: MFileBody,
|
||||||
};
|
};
|
||||||
const evOverrides: Record<string, typeof React.Component> = {
|
const evOverrides: Record<string, React.ComponentType<IBodyProps>> = {
|
||||||
// Use MImageReplyBody so that the sticker isn't taking up a lot of space
|
// Use MImageReplyBody so that the sticker isn't taking up a lot of space
|
||||||
[EventType.Sticker]: MImageReplyBody,
|
[EventType.Sticker]: MImageReplyBody,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import HiddenBody from "../components/views/messages/HiddenBody";
|
|||||||
import ViewSourceEvent from "../components/views/messages/ViewSourceEvent";
|
import ViewSourceEvent from "../components/views/messages/ViewSourceEvent";
|
||||||
import { shouldDisplayAsBeaconTile } from "../utils/beacon/timeline";
|
import { shouldDisplayAsBeaconTile } from "../utils/beacon/timeline";
|
||||||
import { ElementCall } from "../models/Call";
|
import { ElementCall } from "../models/Call";
|
||||||
|
import { type IBodyProps } from "../components/views/messages/IBodyProps";
|
||||||
|
|
||||||
// Subset of EventTile's IProps plus some mixins
|
// Subset of EventTile's IProps plus some mixins
|
||||||
export interface EventTileTypeProps
|
export interface EventTileTypeProps
|
||||||
@@ -64,8 +65,8 @@ export interface EventTileTypeProps
|
|||||||
ref?: React.RefObject<any>; // `any` because it's effectively impossible to convince TS of a reasonable type
|
ref?: React.RefObject<any>; // `any` because it's effectively impossible to convince TS of a reasonable type
|
||||||
timestamp?: JSX.Element;
|
timestamp?: JSX.Element;
|
||||||
maxImageHeight?: number; // pixels
|
maxImageHeight?: number; // pixels
|
||||||
overrideBodyTypes?: Record<string, typeof React.Component>;
|
overrideBodyTypes?: Record<string, React.ComponentType<IBodyProps>>;
|
||||||
overrideEventTypes?: Record<string, typeof React.Component>;
|
overrideEventTypes?: Record<string, React.ComponentType<IBodyProps>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type FactoryProps = Omit<EventTileTypeProps, "ref">;
|
type FactoryProps = Omit<EventTileTypeProps, "ref">;
|
||||||
|
|||||||
35
src/hooks/useMediaVisible.ts
Normal file
35
src/hooks/useMediaVisible.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
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 { useCallback } from "react";
|
||||||
|
|
||||||
|
import { SettingLevel } from "../settings/SettingLevel";
|
||||||
|
import { useSettingValue } from "./useSettings";
|
||||||
|
import SettingsStore from "../settings/SettingsStore";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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): [boolean, (visible: boolean) => void] {
|
||||||
|
const defaultShowImages = useSettingValue("showImages", SettingLevel.DEVICE);
|
||||||
|
const eventVisibility = useSettingValue("showMediaEventIds", SettingLevel.DEVICE);
|
||||||
|
const setMediaVisible = useCallback(
|
||||||
|
(visible: boolean) => {
|
||||||
|
SettingsStore.setValue("showMediaEventIds", null, SettingLevel.DEVICE, {
|
||||||
|
...eventVisibility,
|
||||||
|
[eventId]: visible,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[eventId, eventVisibility],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Always prefer the explicit per-event user preference here.
|
||||||
|
const imgIsVisible = eventVisibility[eventId] ?? defaultShowImages;
|
||||||
|
return [imgIsVisible, setMediaVisible];
|
||||||
|
}
|
||||||
@@ -64,6 +64,7 @@
|
|||||||
"go": "Go",
|
"go": "Go",
|
||||||
"go_back": "Go back",
|
"go_back": "Go back",
|
||||||
"got_it": "Got it",
|
"got_it": "Got it",
|
||||||
|
"hide": "Hide",
|
||||||
"hide_advanced": "Hide advanced",
|
"hide_advanced": "Hide advanced",
|
||||||
"hold": "Hold",
|
"hold": "Hold",
|
||||||
"ignore": "Ignore",
|
"ignore": "Ignore",
|
||||||
|
|||||||
@@ -273,6 +273,7 @@ export interface Settings {
|
|||||||
"language": IBaseSetting<string>;
|
"language": IBaseSetting<string>;
|
||||||
"breadcrumb_rooms": IBaseSetting<string[]>;
|
"breadcrumb_rooms": IBaseSetting<string[]>;
|
||||||
"recent_emoji": IBaseSetting<RecentEmojiData>;
|
"recent_emoji": IBaseSetting<RecentEmojiData>;
|
||||||
|
"showMediaEventIds": IBaseSetting<{ [eventId: string]: boolean }>;
|
||||||
"SpotlightSearch.recentSearches": IBaseSetting<string[]>;
|
"SpotlightSearch.recentSearches": IBaseSetting<string[]>;
|
||||||
"SpotlightSearch.showNsfwPublicRooms": IBaseSetting<boolean>;
|
"SpotlightSearch.showNsfwPublicRooms": IBaseSetting<boolean>;
|
||||||
"room_directory_servers": IBaseSetting<string[]>;
|
"room_directory_servers": IBaseSetting<string[]>;
|
||||||
@@ -969,6 +970,11 @@ export const SETTINGS: Settings = {
|
|||||||
supportedLevels: [SettingLevel.ACCOUNT],
|
supportedLevels: [SettingLevel.ACCOUNT],
|
||||||
default: [], // list of room IDs, most recent first
|
default: [], // list of room IDs, most recent first
|
||||||
},
|
},
|
||||||
|
"showMediaEventIds": {
|
||||||
|
// not really a setting
|
||||||
|
supportedLevels: [SettingLevel.DEVICE],
|
||||||
|
default: {}, // List of events => is visible
|
||||||
|
},
|
||||||
"SpotlightSearch.showNsfwPublicRooms": {
|
"SpotlightSearch.showNsfwPublicRooms": {
|
||||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||||
displayName: _td("settings|show_nsfw_content"),
|
displayName: _td("settings|show_nsfw_content"),
|
||||||
|
|||||||
@@ -697,6 +697,24 @@ export default class SettingsStore {
|
|||||||
client.on(ClientEvent.Sync, onSync);
|
client.on(ClientEvent.Sync, onSync);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrate the setting for visible images to a setting.
|
||||||
|
*/
|
||||||
|
private static migrateShowImagesToSettings(): void {
|
||||||
|
const MIGRATION_DONE_FLAG = "mx_show_images_migration_done";
|
||||||
|
if (localStorage.getItem(MIGRATION_DONE_FLAG)) return;
|
||||||
|
|
||||||
|
logger.info("Performing one-time settings migration of shown images to settings store");
|
||||||
|
const newValue = Object.fromEntries(
|
||||||
|
Object.keys(localStorage)
|
||||||
|
.filter((k) => k.startsWith("mx_ShowImage_"))
|
||||||
|
.map((k) => [k.slice("mx_ShowImage_".length), true]),
|
||||||
|
);
|
||||||
|
this.setValue("showMediaEventIds", null, SettingLevel.DEVICE, newValue);
|
||||||
|
|
||||||
|
localStorage.setItem(MIGRATION_DONE_FLAG, "true");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runs or queues any setting migrations needed.
|
* Runs or queues any setting migrations needed.
|
||||||
*/
|
*/
|
||||||
@@ -708,6 +726,12 @@ export default class SettingsStore {
|
|||||||
// be disabled in E2EE rooms.
|
// be disabled in E2EE rooms.
|
||||||
SettingsStore.migrateURLPreviewsE2EE(isFreshLogin);
|
SettingsStore.migrateURLPreviewsE2EE(isFreshLogin);
|
||||||
|
|
||||||
|
// This can be removed once enough users have run a version of Element with
|
||||||
|
// this migration.
|
||||||
|
// The consequences of missing the migration are that previously shown images
|
||||||
|
// will now be hidden again, so this fails safely.
|
||||||
|
SettingsStore.migrateShowImagesToSettings();
|
||||||
|
|
||||||
// Dev notes: to add your migration, just add a new `migrateMyFeature` function, call it, and
|
// Dev notes: to add your migration, just add a new `migrateMyFeature` function, call it, and
|
||||||
// add a comment to note when it can be removed.
|
// add a comment to note when it can be removed.
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2024,2025 New Vector Ltd.
|
||||||
|
Copyright 2024 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 React from "react";
|
||||||
|
import { fireEvent, render, screen } from "jest-matrix-react";
|
||||||
|
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
|
import { HideActionButton } from "../../../../../src/components/views/messages/HideActionButton";
|
||||||
|
import SettingsStore from "../../../../../src/settings/SettingsStore";
|
||||||
|
import { SettingLevel } from "../../../../../src/settings/SettingLevel";
|
||||||
|
import type { Settings } from "../../../../../src/settings/Settings";
|
||||||
|
|
||||||
|
function mockSetting(
|
||||||
|
showImages: Settings["showImages"]["default"],
|
||||||
|
showMediaEventIds: Settings["showMediaEventIds"]["default"],
|
||||||
|
) {
|
||||||
|
jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName) => {
|
||||||
|
if (settingName === "showImages") {
|
||||||
|
return showImages;
|
||||||
|
} else if (settingName === "showMediaEventIds") {
|
||||||
|
return showMediaEventIds;
|
||||||
|
}
|
||||||
|
throw Error(`Unexpected setting ${settingName}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const event = new MatrixEvent({
|
||||||
|
event_id: "$foo:bar",
|
||||||
|
room_id: "!room:id",
|
||||||
|
sender: "@user:id",
|
||||||
|
type: "m.room.message",
|
||||||
|
content: {
|
||||||
|
body: "test",
|
||||||
|
msgtype: "m.image",
|
||||||
|
url: "mxc://matrix.org/1234",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("HideActionButton", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
it("should show button when event is visible by showMediaEventIds setting", async () => {
|
||||||
|
mockSetting(false, { "$foo:bar": true });
|
||||||
|
render(<HideActionButton mxEvent={event} />);
|
||||||
|
expect(screen.getByRole("button")).toBeVisible();
|
||||||
|
});
|
||||||
|
it("should show button when event is visible by showImages setting", async () => {
|
||||||
|
mockSetting(true, {});
|
||||||
|
render(<HideActionButton mxEvent={event} />);
|
||||||
|
expect(screen.getByRole("button")).toBeVisible();
|
||||||
|
});
|
||||||
|
it("should hide button when event is hidden by showMediaEventIds setting", async () => {
|
||||||
|
jest.spyOn(SettingsStore, "getValue").mockReturnValue({ "$foo:bar": false });
|
||||||
|
render(<HideActionButton mxEvent={event} />);
|
||||||
|
expect(screen.queryByRole("button")).toBeNull();
|
||||||
|
});
|
||||||
|
it("should hide button when event is hidden by showImages setting", async () => {
|
||||||
|
mockSetting(false, {});
|
||||||
|
render(<HideActionButton mxEvent={event} />);
|
||||||
|
expect(screen.queryByRole("button")).toBeNull();
|
||||||
|
});
|
||||||
|
it("should store event as hidden when clicked", async () => {
|
||||||
|
const spy = jest.spyOn(SettingsStore, "setValue");
|
||||||
|
render(<HideActionButton mxEvent={event} />);
|
||||||
|
fireEvent.click(screen.getByRole("button"));
|
||||||
|
expect(spy).toHaveBeenCalledWith("showMediaEventIds", null, SettingLevel.DEVICE, { "$foo:bar": false });
|
||||||
|
// Button should be hidden after the setting is set.
|
||||||
|
expect(screen.queryByRole("button")).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React, { act } from "react";
|
||||||
import { fireEvent, render, screen, waitForElementToBeRemoved } from "jest-matrix-react";
|
import { fireEvent, render, screen, waitForElementToBeRemoved } from "jest-matrix-react";
|
||||||
import { EventType, getHttpUriForMxc, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
import { EventType, getHttpUriForMxc, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
||||||
import fetchMock from "fetch-mock-jest";
|
import fetchMock from "fetch-mock-jest";
|
||||||
@@ -27,6 +27,7 @@ import {
|
|||||||
} from "../../../../test-utils";
|
} from "../../../../test-utils";
|
||||||
import { MediaEventHelper } from "../../../../../src/utils/MediaEventHelper";
|
import { MediaEventHelper } from "../../../../../src/utils/MediaEventHelper";
|
||||||
import SettingsStore from "../../../../../src/settings/SettingsStore";
|
import SettingsStore from "../../../../../src/settings/SettingsStore";
|
||||||
|
import { SettingLevel } from "../../../../../src/settings/SettingLevel";
|
||||||
|
|
||||||
jest.mock("matrix-encrypt-attachment", () => ({
|
jest.mock("matrix-encrypt-attachment", () => ({
|
||||||
decryptAttachment: jest.fn(),
|
decryptAttachment: jest.fn(),
|
||||||
@@ -57,6 +58,7 @@ describe("<MImageBody/>", () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
const encryptedMediaEvent = new MatrixEvent({
|
const encryptedMediaEvent = new MatrixEvent({
|
||||||
|
event_id: "$foo:bar",
|
||||||
room_id: "!room:server",
|
room_id: "!room:server",
|
||||||
sender: userId,
|
sender: userId,
|
||||||
type: EventType.RoomMessage,
|
type: EventType.RoomMessage,
|
||||||
@@ -131,7 +133,26 @@ describe("<MImageBody/>", () => {
|
|||||||
|
|
||||||
describe("with image previews/thumbnails disabled", () => {
|
describe("with image previews/thumbnails disabled", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
|
act(() => {
|
||||||
|
SettingsStore.setValue("showImages", null, SettingLevel.DEVICE, false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
act(() => {
|
||||||
|
SettingsStore.setValue(
|
||||||
|
"showImages",
|
||||||
|
null,
|
||||||
|
SettingLevel.DEVICE,
|
||||||
|
SettingsStore.getDefaultValue("showImages"),
|
||||||
|
);
|
||||||
|
SettingsStore.setValue(
|
||||||
|
"showMediaEventIds",
|
||||||
|
null,
|
||||||
|
SettingLevel.DEVICE,
|
||||||
|
SettingsStore.getDefaultValue("showMediaEventIds"),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not download image", async () => {
|
it("should not download image", async () => {
|
||||||
@@ -163,7 +184,6 @@ describe("<MImageBody/>", () => {
|
|||||||
|
|
||||||
fireEvent.click(screen.getByRole("button"));
|
fireEvent.click(screen.getByRole("button"));
|
||||||
|
|
||||||
// image fetched after clicking show image
|
|
||||||
expect(fetchMock).toHaveFetched(url);
|
expect(fetchMock).toHaveFetched(url);
|
||||||
|
|
||||||
// spinner while downloading image
|
// spinner while downloading image
|
||||||
|
|||||||
71
test/unit-tests/hooks/useMediaVisible-test.tsx
Normal file
71
test/unit-tests/hooks/useMediaVisible-test.tsx
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
/*
|
||||||
|
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 { act, renderHook, waitFor } from "jest-matrix-react";
|
||||||
|
|
||||||
|
import { useMediaVisible } from "../../../src/hooks/useMediaVisible";
|
||||||
|
import SettingsStore from "../../../src/settings/SettingsStore";
|
||||||
|
import { SettingLevel } from "../../../src/settings/SettingLevel";
|
||||||
|
|
||||||
|
const EVENT_ID = "$fibble:example.org";
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
return renderHook(() => useMediaVisible(EVENT_ID));
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("useMediaVisible", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
// Using act here as otherwise React warns about state updates not being wrapped.
|
||||||
|
act(() => {
|
||||||
|
SettingsStore.setValue(
|
||||||
|
"showMediaEventIds",
|
||||||
|
null,
|
||||||
|
SettingLevel.DEVICE,
|
||||||
|
SettingsStore.getDefaultValue("showMediaEventIds"),
|
||||||
|
);
|
||||||
|
SettingsStore.setValue(
|
||||||
|
"showImages",
|
||||||
|
null,
|
||||||
|
SettingLevel.DEVICE,
|
||||||
|
SettingsStore.getDefaultValue("showImages"),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should display images by default", async () => {
|
||||||
|
const { result } = render();
|
||||||
|
expect(result.current[0]).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should hide images when the default is changed", async () => {
|
||||||
|
SettingsStore.setValue("showImages", null, SettingLevel.DEVICE, false);
|
||||||
|
const { result } = render();
|
||||||
|
expect(result.current[0]).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should hide images after function is called", async () => {
|
||||||
|
const { result } = render();
|
||||||
|
expect(result.current[0]).toEqual(true);
|
||||||
|
act(() => {
|
||||||
|
result.current[1](false);
|
||||||
|
});
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current[0]).toEqual(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it("should show images after function is called", async () => {
|
||||||
|
SettingsStore.setValue("showImages", null, SettingLevel.DEVICE, false);
|
||||||
|
const { result } = render();
|
||||||
|
expect(result.current[0]).toEqual(false);
|
||||||
|
act(() => {
|
||||||
|
result.current[1](true);
|
||||||
|
});
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current[0]).toEqual(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user