mirror of
https://github.com/element-hq/element-web.git
synced 2025-12-17 02:10:38 +00:00
Compare commits
41 Commits
dbkr/uploa
...
hs/add-hid
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a423a0fd0 | ||
|
|
0512becf8e | ||
|
|
3b27416f92 | ||
|
|
89353db2ba | ||
|
|
9a0857fd1f | ||
|
|
265278dc17 | ||
|
|
c6359e6710 | ||
|
|
c1f0724256 | ||
|
|
e9397096fd | ||
|
|
1b213431a8 | ||
|
|
bc264ff3b7 | ||
|
|
6f92b536be | ||
|
|
e057a2ed7b | ||
|
|
d7a185baea | ||
|
|
93009d4613 | ||
|
|
71257d97e7 | ||
|
|
60eeb8a7de | ||
|
|
571a2e373d | ||
|
|
d0b8564660 | ||
|
|
28ea91566a | ||
|
|
ef32747473 | ||
|
|
7696516e8b | ||
|
|
99274065c9 | ||
|
|
debb6ad626 | ||
|
|
939ff4e37a | ||
|
|
46b1234a1d | ||
|
|
b9c0d63e3e | ||
|
|
cf7e52c6fc | ||
|
|
e87eb127ee | ||
|
|
83e421daf2 | ||
|
|
d6fb24dea7 | ||
|
|
a518c8d662 | ||
|
|
c759e516bd | ||
|
|
c8b55c3dfe | ||
|
|
7197093744 | ||
|
|
4e34adb854 | ||
|
|
72c2a3eb07 | ||
|
|
4d290461c4 | ||
|
|
0cc06450d7 | ||
|
|
9376d71831 | ||
|
|
6d5442a87b |
@@ -28,6 +28,8 @@ const NEW_AVATAR = fs.readFileSync("playwright/sample-files/element.png");
|
||||
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") });
|
||||
};
|
||||
@@ -916,7 +918,26 @@ test.describe("Timeline", () => {
|
||||
await page.getByRole("button", { name: "Hide" }).click();
|
||||
|
||||
// Check that the image is now hidden.
|
||||
await expect(page.getByRole("link", { name: "Show image" })).toBeVisible();
|
||||
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 video is now hidden.
|
||||
await expect(page.getByRole("button", { name: "Show video" })).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
BIN
playwright/sample-files/5secvid.webm
Normal file
BIN
playwright/sample-files/5secvid.webm
Normal file
Binary file not shown.
@@ -228,6 +228,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";
|
||||
|
||||
29
res/css/views/messages/_HiddenMediaPlaceholder.pcss
Normal file
29
res/css/views/messages/_HiddenMediaPlaceholder.pcss
Normal file
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
26
src/components/views/messages/HiddenMediaPlaceholder.tsx
Normal file
26
src/components/views/messages/HiddenMediaPlaceholder.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
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 PropsWithChildren, type MouseEventHandler } from "react";
|
||||
import { VisibilityOnIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
|
||||
interface IProps {
|
||||
onClick: MouseEventHandler<HTMLButtonElement>;
|
||||
}
|
||||
|
||||
const HiddenMediaPlaceholder: React.FunctionComponent<PropsWithChildren<IProps>> = ({ onClick, children }) => {
|
||||
return (
|
||||
<button onClick={onClick} className="mx_HiddenMediaPlaceholder">
|
||||
<div>
|
||||
<VisibilityOnIcon />
|
||||
<span>{children}</span>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default HiddenMediaPlaceholder;
|
||||
@@ -33,6 +33,7 @@ import { presentableTextForFile } from "../../../utils/FileUtils";
|
||||
import { createReconnectedListener } from "../../../utils/connection";
|
||||
import MediaProcessingError from "./shared/MediaProcessingError";
|
||||
import { DecryptError, DownloadError } from "../../../utils/DecryptFile";
|
||||
import HiddenMediaPlaceholder from "./HiddenMediaPlaceholder";
|
||||
import { useMediaVisible } from "../../../hooks/useMediaVisible";
|
||||
|
||||
enum Placeholder {
|
||||
@@ -95,7 +96,7 @@ export class MImageBodyInner extends React.Component<IProps, IState> {
|
||||
if (ev.button === 0 && !ev.metaKey) {
|
||||
ev.preventDefault();
|
||||
if (!this.props.mediaVisible) {
|
||||
this.props.setMediaVisible?.(true);
|
||||
this.props.setMediaVisible(true);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -437,7 +438,11 @@ export class MImageBodyInner extends React.Component<IProps, IState> {
|
||||
if (!this.state.loadedImageDimensions) {
|
||||
let imageElement: JSX.Element;
|
||||
if (!this.props.mediaVisible) {
|
||||
imageElement = <HiddenImagePlaceholder />;
|
||||
imageElement = (
|
||||
<HiddenMediaPlaceholder onClick={this.onClick}>
|
||||
{_t("timeline|m.image|show_image")}
|
||||
</HiddenMediaPlaceholder>
|
||||
);
|
||||
} else {
|
||||
imageElement = (
|
||||
<img
|
||||
@@ -507,7 +512,14 @@ export class MImageBodyInner extends React.Component<IProps, IState> {
|
||||
}
|
||||
|
||||
if (!this.props.mediaVisible) {
|
||||
img = <HiddenImagePlaceholder maxWidth={maxWidth} />;
|
||||
img = (
|
||||
<div style={{ width: maxWidth, height: maxHeight }}>
|
||||
<HiddenMediaPlaceholder onClick={this.onClick}>
|
||||
{_t("timeline|m.image|show_image")}
|
||||
</HiddenMediaPlaceholder>
|
||||
;
|
||||
</div>
|
||||
);
|
||||
showPlaceholder = false; // because we're hiding the image, so don't show the placeholder.
|
||||
}
|
||||
|
||||
@@ -563,7 +575,7 @@ export class MImageBodyInner extends React.Component<IProps, IState> {
|
||||
</div>
|
||||
|
||||
{/* HACK: This div fills out space while the image loads, to prevent scroll jumps */}
|
||||
{!this.props.forExport && !this.state.imgLoaded && (
|
||||
{!this.props.forExport && !this.state.imgLoaded && !placeholder && (
|
||||
<div style={{ height: maxHeight, width: maxWidth }} />
|
||||
)}
|
||||
</div>
|
||||
@@ -596,12 +608,6 @@ export class MImageBodyInner extends React.Component<IProps, IState> {
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
} else if (!this.props.mediaVisible) {
|
||||
return (
|
||||
<div role="button" onClick={this.onClick}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return children;
|
||||
}
|
||||
@@ -680,24 +686,7 @@ export class MImageBodyInner extends React.Component<IProps, IState> {
|
||||
}
|
||||
}
|
||||
|
||||
interface PlaceholderIProps {
|
||||
maxWidth?: number;
|
||||
}
|
||||
|
||||
export class HiddenImagePlaceholder extends React.PureComponent<PlaceholderIProps> {
|
||||
public render(): React.ReactNode {
|
||||
const maxWidth = this.props.maxWidth ? this.props.maxWidth + "px" : null;
|
||||
return (
|
||||
<div className="mx_HiddenImagePlaceholder" style={{ maxWidth: `min(100%, ${maxWidth}px)` }}>
|
||||
<div className="mx_HiddenImagePlaceholder_button">
|
||||
<span className="mx_HiddenImagePlaceholder_eye" />
|
||||
<span>{_t("timeline|m.image|show_image")}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Wrap MImageBody component so we can use a hook here.
|
||||
const MImageBody: React.FC<IBodyProps> = (props) => {
|
||||
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId()!);
|
||||
return <MImageBodyInner mediaVisible={mediaVisible} setMediaVisible={setVisible} {...props} />;
|
||||
|
||||
@@ -21,6 +21,8 @@ import MFileBody from "./MFileBody";
|
||||
import { type ImageSize, suggestedSize as suggestedVideoSize } from "../../../settings/enums/ImageSize";
|
||||
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
|
||||
import MediaProcessingError from "./shared/MediaProcessingError";
|
||||
import HiddenMediaPlaceholder from "./HiddenMediaPlaceholder";
|
||||
import { useMediaVisible } from "../../../hooks/useMediaVisible";
|
||||
|
||||
interface IState {
|
||||
decryptedUrl: string | null;
|
||||
@@ -32,7 +34,19 @@ interface IState {
|
||||
blurhashUrl: string | null;
|
||||
}
|
||||
|
||||
export default class MVideoBody extends React.PureComponent<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;
|
||||
}
|
||||
|
||||
class MVideoBodyInner extends React.PureComponent<IProps, IState> {
|
||||
public static contextType = RoomContext;
|
||||
declare public context: React.ContextType<typeof RoomContext>;
|
||||
|
||||
@@ -49,6 +63,10 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
|
||||
blurhashUrl: null,
|
||||
};
|
||||
|
||||
private onClick = (): void => {
|
||||
this.props.setMediaVisible(true);
|
||||
};
|
||||
|
||||
private getContentUrl(): string | undefined {
|
||||
const content = this.props.mxEvent.getContent<MediaEventContent>();
|
||||
// During export, the content url will point to the MSC, which will later point to a local url
|
||||
@@ -120,11 +138,7 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
|
||||
}
|
||||
}
|
||||
|
||||
public async componentDidMount(): Promise<void> {
|
||||
this.sizeWatcher = SettingsStore.watchSetting("Images.size", null, () => {
|
||||
this.forceUpdate(); // we don't really have a reliable thing to update, so just update the whole thing
|
||||
});
|
||||
|
||||
private async downloadVideo(): Promise<void> {
|
||||
try {
|
||||
this.loadBlurhash();
|
||||
} catch (e) {
|
||||
@@ -174,6 +188,23 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
|
||||
}
|
||||
}
|
||||
|
||||
public async componentDidMount(): Promise<void> {
|
||||
this.sizeWatcher = SettingsStore.watchSetting("Images.size", null, () => {
|
||||
this.forceUpdate(); // we don't really have a reliable thing to update, so just update the whole thing
|
||||
});
|
||||
|
||||
// Do not attempt to load the media if we do not want to show previews here.
|
||||
if (this.props.mediaVisible) {
|
||||
await this.downloadVideo();
|
||||
}
|
||||
}
|
||||
|
||||
public async componentDidUpdate(prevProps: Readonly<IProps>): Promise<void> {
|
||||
if (!prevProps.mediaVisible && this.props.mediaVisible) {
|
||||
await this.downloadVideo();
|
||||
}
|
||||
}
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
SettingsStore.unwatchSetting(this.sizeWatcher);
|
||||
}
|
||||
@@ -244,6 +275,22 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
|
||||
);
|
||||
}
|
||||
|
||||
// Users may not even want to show a poster, so instead show a preview button.
|
||||
if (!this.props.mediaVisible) {
|
||||
return (
|
||||
<span className="mx_MVideoBody">
|
||||
<div
|
||||
className="mx_MVideoBody_container"
|
||||
style={{ width: maxWidth, height: maxHeight, aspectRatio }}
|
||||
>
|
||||
<HiddenMediaPlaceholder onClick={this.onClick}>
|
||||
{_t("timeline|m.video|show_video")}
|
||||
</HiddenMediaPlaceholder>
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// Important: If we aren't autoplaying and we haven't decrypted it yet, show a video with a poster.
|
||||
if (!this.props.forExport && content.file !== undefined && this.state.decryptedUrl === null && autoplay) {
|
||||
// Need to decrypt the attachment
|
||||
@@ -294,3 +341,11 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Wrap MVideoBody component so we can use a hook here.
|
||||
const MVideoBody: React.FC<IBodyProps> = (props) => {
|
||||
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId()!);
|
||||
return <MVideoBodyInner mediaVisible={mediaVisible} setMediaVisible={setVisible} {...props} />;
|
||||
};
|
||||
|
||||
export default MVideoBody;
|
||||
|
||||
@@ -536,9 +536,11 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
|
||||
mediaEventHelperGet={() => this.props.getTile()?.getMediaHelper?.()}
|
||||
key="download"
|
||||
/>,
|
||||
<HideActionButton mxEvent={this.props.mxEvent} key="hide" />,
|
||||
);
|
||||
}
|
||||
if (MediaEventHelper.canHide(this.props.mxEvent)) {
|
||||
toolbarOpts.splice(0, 0, <HideActionButton mxEvent={this.props.mxEvent} key="hide" />);
|
||||
}
|
||||
} else if (
|
||||
// Show thread icon even for deleted messages, but only within main timeline
|
||||
this.context.timelineRenderingType === TimelineRenderingType.Room &&
|
||||
|
||||
@@ -3568,7 +3568,8 @@
|
||||
},
|
||||
"m.sticker": "%(senderDisplayName)s sent a sticker.",
|
||||
"m.video": {
|
||||
"error_decrypting": "Error decrypting video"
|
||||
"error_decrypting": "Error decrypting video",
|
||||
"show_video": "Show video"
|
||||
},
|
||||
"m.widget": {
|
||||
"added": "%(widgetName)s widget added by %(senderName)s",
|
||||
|
||||
@@ -113,4 +113,18 @@ export class MediaEventHelper implements IDestroyable {
|
||||
// Finally, it's probably not media
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public static canHide(event: MatrixEvent): boolean {
|
||||
if (!event) return false;
|
||||
if (event.isRedacted()) return false;
|
||||
const content = event.getContent();
|
||||
const hideTypes: string[] = [MsgType.Video, MsgType.Image];
|
||||
if (hideTypes.includes(content.msgtype!)) return true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { act } from "react";
|
||||
import { fireEvent, render, screen, waitForElementToBeRemoved } from "jest-matrix-react";
|
||||
import { fireEvent, render, screen, waitFor, waitForElementToBeRemoved } from "jest-matrix-react";
|
||||
import { EventType, getHttpUriForMxc, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import encrypt from "matrix-encrypt-attachment";
|
||||
@@ -85,6 +85,10 @@ describe("<MImageBody/>", () => {
|
||||
fetchMock.mockReset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mocked(encrypt.decryptAttachment).mockReset();
|
||||
});
|
||||
|
||||
it("should show a thumbnail while image is being downloaded", async () => {
|
||||
fetchMock.getOnce(url, { status: 200 });
|
||||
|
||||
@@ -166,6 +170,8 @@ describe("<MImageBody/>", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("Show image")).toBeInTheDocument();
|
||||
|
||||
expect(fetchMock).not.toHaveFetched(url);
|
||||
});
|
||||
|
||||
@@ -186,8 +192,12 @@ describe("<MImageBody/>", () => {
|
||||
|
||||
expect(fetchMock).toHaveFetched(url);
|
||||
|
||||
// spinner while downloading image
|
||||
expect(screen.getByRole("progressbar")).toBeInTheDocument();
|
||||
// Show image is asynchronous since it applies through a settings watcher hook, so
|
||||
// be sure to wait here.
|
||||
await waitFor(() => {
|
||||
// spinner while downloading image
|
||||
expect(screen.getByRole("progressbar")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@ 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.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import React, { act } from "react";
|
||||
import { EventType, getHttpUriForMxc, type IContent, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { render, type RenderResult } from "jest-matrix-react";
|
||||
import { fireEvent, render, screen, type RenderResult } from "jest-matrix-react";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
|
||||
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
|
||||
@@ -22,19 +22,22 @@ import {
|
||||
mockClientMethodsUser,
|
||||
} from "../../../../test-utils";
|
||||
import MVideoBody from "../../../../../src/components/views/messages/MVideoBody";
|
||||
import type { IBodyProps } from "../../../../../src/components/views/messages/IBodyProps";
|
||||
import { SettingLevel } from "../../../../../src/settings/SettingLevel";
|
||||
import SettingsStore from "../../../../../src/settings/SettingsStore";
|
||||
|
||||
// Needed so we don't throw an error about failing to decrypt.
|
||||
jest.mock("matrix-encrypt-attachment", () => ({
|
||||
decryptAttachment: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("MVideoBody", () => {
|
||||
it("does not crash when given a portrait image", () => {
|
||||
// Check for an unreliable crash caused by a fractional-sized
|
||||
// image dimension being used for a CanvasImageData.
|
||||
const { asFragment } = makeMVideoBody(720, 1280);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
// If we get here, we did not crash.
|
||||
});
|
||||
const userId = "@user:server";
|
||||
const deviceId = "DEADB33F";
|
||||
|
||||
it("should show poster for encrypted media before downloading it", async () => {
|
||||
const userId = "@user:server";
|
||||
const deviceId = "DEADB33F";
|
||||
const thumbUrl = "https://server/_matrix/media/v3/download/server/encrypted-poster";
|
||||
|
||||
beforeEach(() => {
|
||||
const cli = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(userId),
|
||||
...mockClientMethodsServer(),
|
||||
@@ -49,40 +52,108 @@ describe("MVideoBody", () => {
|
||||
},
|
||||
}),
|
||||
});
|
||||
const thumbUrl = "https://server/_matrix/media/v3/download/server/encrypted-poster";
|
||||
fetchMock.getOnce(thumbUrl, { status: 200 });
|
||||
|
||||
// eslint-disable-next-line no-restricted-properties
|
||||
cli.mxcUrlToHttp.mockImplementation(
|
||||
(mxcUrl: string, width?: number, height?: number, resizeMethod?: string, allowDirectLinks?: boolean) => {
|
||||
return getHttpUriForMxc("https://server", mxcUrl, width, height, resizeMethod, allowDirectLinks);
|
||||
},
|
||||
);
|
||||
const encryptedMediaEvent = new MatrixEvent({
|
||||
room_id: "!room:server",
|
||||
sender: userId,
|
||||
type: EventType.RoomMessage,
|
||||
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",
|
||||
fetchMock.mockReset();
|
||||
});
|
||||
|
||||
const encryptedMediaEvent = new MatrixEvent({
|
||||
room_id: "!room:server",
|
||||
sender: userId,
|
||||
type: EventType.RoomMessage,
|
||||
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",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
it("does not crash when given a portrait image", () => {
|
||||
// Check for an unreliable crash caused by a fractional-sized
|
||||
// image dimension being used for a CanvasImageData.
|
||||
const { asFragment } = makeMVideoBody(720, 1280);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
// If we get here, we did not crash.
|
||||
});
|
||||
|
||||
it("should show poster for encrypted media before downloading it", async () => {
|
||||
fetchMock.getOnce(thumbUrl, { status: 200 });
|
||||
const { asFragment } = render(
|
||||
<MVideoBody mxEvent={encryptedMediaEvent} mediaEventHelper={new MediaEventHelper(encryptedMediaEvent)} />,
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe("with video previews/thumbnails disabled", () => {
|
||||
beforeEach(() => {
|
||||
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 video", async () => {
|
||||
fetchMock.getOnce(thumbUrl, { status: 200 });
|
||||
|
||||
render(
|
||||
<MVideoBody
|
||||
mxEvent={encryptedMediaEvent}
|
||||
mediaEventHelper={new MediaEventHelper(encryptedMediaEvent)}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("Show video")).toBeInTheDocument();
|
||||
|
||||
expect(fetchMock).not.toHaveFetched(thumbUrl);
|
||||
});
|
||||
|
||||
it("should render video poster after user consent", async () => {
|
||||
fetchMock.getOnce(thumbUrl, { status: 200 });
|
||||
|
||||
render(
|
||||
<MVideoBody
|
||||
mxEvent={encryptedMediaEvent}
|
||||
mediaEventHelper={new MediaEventHelper(encryptedMediaEvent)}
|
||||
/>,
|
||||
);
|
||||
|
||||
const placeholderButton = screen.getByRole("button", { name: "Show video" });
|
||||
|
||||
expect(placeholderButton).toBeInTheDocument();
|
||||
fireEvent.click(placeholderButton);
|
||||
|
||||
expect(fetchMock).toHaveFetched(thumbUrl);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function makeMVideoBody(w: number, h: number): RenderResult {
|
||||
@@ -109,7 +180,7 @@ function makeMVideoBody(w: number, h: number): RenderResult {
|
||||
content,
|
||||
});
|
||||
|
||||
const defaultProps: MVideoBody["props"] = {
|
||||
const defaultProps: IBodyProps = {
|
||||
mxEvent: event,
|
||||
highlights: [],
|
||||
highlightLink: "",
|
||||
|
||||
@@ -41,9 +41,6 @@ exports[`<MImageBody/> should generate a thumbnail if one isn't included for ani
|
||||
GIF
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
style="height: 50px; width: 40px;"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
@@ -77,9 +74,6 @@ exports[`<MImageBody/> should show a thumbnail while image is being downloaded 1
|
||||
<div
|
||||
style="max-height: 50px; max-width: 40px;"
|
||||
/>
|
||||
<div
|
||||
style="height: 50px; width: 40px;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user