Update design of files list in right panel (#144)
* Update design of files list in right panel Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Make i18n script happier Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Improve coverage Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Improve coverage Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Improve coverage Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Discard changes to src/components/structures/MessagePanel.tsx * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update tests Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Fix flaky screenshot test Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update screenshot Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
|
@ -84,7 +84,7 @@ test.describe("FilePanel", () => {
|
||||||
await expect(filePanelMessageList.locator(".mx_EventTile")).toHaveCount(3);
|
await expect(filePanelMessageList.locator(".mx_EventTile")).toHaveCount(3);
|
||||||
|
|
||||||
// Assert that the download links are rendered
|
// Assert that the download links are rendered
|
||||||
await expect(filePanelMessageList.locator(".mx_MFileBody_download")).toHaveCount(3);
|
await expect(filePanelMessageList.locator(".mx_MFileBody_download,.mx_MFileBody_info")).toHaveCount(3);
|
||||||
|
|
||||||
// Assert that the sender of the files is rendered on all of the tiles
|
// Assert that the sender of the files is rendered on all of the tiles
|
||||||
await expect(filePanelMessageList.getByText(NAME)).toHaveCount(3);
|
await expect(filePanelMessageList.getByText(NAME)).toHaveCount(3);
|
||||||
|
@ -176,8 +176,7 @@ test.describe("FilePanel", () => {
|
||||||
// Assert that the file size is displayed in kibibytes, not kilobytes (1000 bytes)
|
// Assert that the file size is displayed in kibibytes, not kilobytes (1000 bytes)
|
||||||
// See: https://github.com/vector-im/element-web/issues/24866
|
// See: https://github.com/vector-im/element-web/issues/24866
|
||||||
await expect(tile.locator(".mx_MFileBody_info_filename", { hasText: size })).toBeVisible();
|
await expect(tile.locator(".mx_MFileBody_info_filename", { hasText: size })).toBeVisible();
|
||||||
await expect(tile.locator(".mx_MFileBody_download a", { hasText: size })).toBeVisible();
|
await expect(tile.locator(".mx_MFileBody_info", { hasText: size })).toBeVisible();
|
||||||
await expect(tile.locator(".mx_MFileBody_download .mx_MImageBody_size", { hasText: size })).toBeVisible();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Before Width: | Height: | Size: 8.9 KiB After Width: | Height: | Size: 8.9 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 9.9 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 32 KiB |
|
@ -20,70 +20,51 @@ Please see LICENSE files in the repository root for full details.
|
||||||
|
|
||||||
.mx_RoomView_MessageList {
|
.mx_RoomView_MessageList {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
gap: var(--cpd-space-6x);
|
||||||
h2 {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* FIXME: rather than having EventTile's default CSS be for MessagePanel,
|
/* FIXME: rather than having EventTile's default CSS be for MessagePanel,
|
||||||
we should make EventTile a base CSS class and customise it specifically
|
we should make EventTile a base CSS class and customise it specifically
|
||||||
for usage in {Message,File,Notification}Panel. */
|
for usage in {Message,File,Notification}Panel. */
|
||||||
|
|
||||||
.mx_EventTile_avatar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Overrides for the attachment body tiles */
|
/* Overrides for the attachment body tiles */
|
||||||
.mx_EventTile {
|
.mx_EventTile {
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
margin-top: 10px;
|
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
|
|
||||||
|
& + .mx_EventTile {
|
||||||
|
border-top: 1px solid var(--cpd-color-gray-400);
|
||||||
|
padding-top: var(--cpd-space-6x);
|
||||||
|
}
|
||||||
|
|
||||||
.mx_EventTile_line {
|
.mx_EventTile_line {
|
||||||
padding-inline-start: 0;
|
padding-inline-start: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_MFileBody {
|
|
||||||
line-height: 2.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_MFileBody_download {
|
.mx_MFileBody_download {
|
||||||
padding-top: $spacing-8;
|
margin-top: var(--cpd-space-4x);
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
font: var(--cpd-font-body-md-regular);
|
|
||||||
color: $event-timestamp-color;
|
|
||||||
|
|
||||||
.mx_MImageBody_size {
|
|
||||||
font: var(--cpd-font-body-md-regular);
|
|
||||||
text-align: right;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_MFileBody_downloadLink {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
color: $light-fg-color;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* anchor link as wrapper */
|
/* anchor link as wrapper */
|
||||||
.mx_EventTile_senderDetailsLink {
|
.mx_EventTile_senderDetailsLink {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
margin-bottom: var(--cpd-space-1x);
|
||||||
|
display: block;
|
||||||
|
|
||||||
.mx_EventTile_senderDetails {
|
.mx_EventTile_senderDetails {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
|
||||||
margin-top: -2px;
|
margin-top: -2px;
|
||||||
|
gap: var(--cpd-space-2x);
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
.mx_DisambiguatedProfile {
|
.mx_DisambiguatedProfile {
|
||||||
color: $event-timestamp-color; /* for ellipsis. Color of displayName and mxid is inherited */
|
color: $event-timestamp-color; /* for ellipsis. Color of displayName and mxid is inherited */
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_MessageTimestamp {
|
.mx_MessageTimestamp {
|
||||||
text-align: right;
|
margin-left: auto;
|
||||||
color: $secondary-content;
|
font: var(--cpd-font-body-xs-regular);
|
||||||
font: var(--cpd-font-body-sm-regular);
|
color: var(--cpd-color-text-secondary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,25 +8,7 @@ Please see LICENSE files in the repository root for full details.
|
||||||
|
|
||||||
.mx_MFileBody_download {
|
.mx_MFileBody_download {
|
||||||
color: $accent;
|
color: $accent;
|
||||||
|
height: var(--cpd-space-9x);
|
||||||
.mx_MFileBody_download_icon {
|
|
||||||
/* 12px instead of 14px to better match surrounding font size */
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
mask-size: 12px;
|
|
||||||
|
|
||||||
mask-position: center;
|
|
||||||
mask-repeat: no-repeat;
|
|
||||||
mask-image: url("$(res)/img/download.svg");
|
|
||||||
background-color: $accent;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_MFileBody_download a {
|
|
||||||
color: $accent;
|
|
||||||
text-decoration: none;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_MFileBody_download object {
|
.mx_MFileBody_download object {
|
||||||
|
@ -43,12 +25,6 @@ Please see LICENSE files in the repository root for full details.
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
border: none;
|
border: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
/* Set the height of the iframe to be 1 line of text.
|
|
||||||
* Iframes don't automatically size themselves to fit their content.
|
|
||||||
* So either we have to fix the height of the iframe using CSS or
|
|
||||||
* use javascript's cross-origin postMessage API to communicate how
|
|
||||||
* big the content of the iframe is. */
|
|
||||||
height: 1.5em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_MFileBody_info {
|
.mx_MFileBody_info {
|
||||||
|
@ -81,6 +57,8 @@ Please see LICENSE files in the repository root for full details.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_MFileBody_info_filename {
|
.mx_MFileBody_info_filename {
|
||||||
|
font: var(--cpd-font-body-md-regular);
|
||||||
|
color: var(--cpd-color-text-primary);
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
|
@ -477,6 +477,8 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public readMarkerForEvent(eventId: string, isLastEvent: boolean): ReactNode {
|
public readMarkerForEvent(eventId: string, isLastEvent: boolean): ReactNode {
|
||||||
|
if (this.context.timelineRenderingType === TimelineRenderingType.File) return null;
|
||||||
|
|
||||||
const visible = !isLastEvent && this.props.readMarkerVisible;
|
const visible = !isLastEvent && this.props.readMarkerVisible;
|
||||||
|
|
||||||
if (this.props.readMarkerEventId === eventId) {
|
if (this.props.readMarkerEventId === eventId) {
|
||||||
|
|
|
@ -9,13 +9,15 @@ Please see LICENSE files in the repository root for full details.
|
||||||
import React, { AllHTMLAttributes, createRef } from "react";
|
import React, { AllHTMLAttributes, createRef } from "react";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
import { MediaEventContent } from "matrix-js-sdk/src/types";
|
import { MediaEventContent } from "matrix-js-sdk/src/types";
|
||||||
|
import { Button } from "@vector-im/compound-web";
|
||||||
|
import { DownloadIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||||
|
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import Modal from "../../../Modal";
|
import Modal from "../../../Modal";
|
||||||
import AccessibleButton from "../elements/AccessibleButton";
|
import AccessibleButton from "../elements/AccessibleButton";
|
||||||
import { mediaFromContent } from "../../../customisations/Media";
|
import { mediaFromContent } from "../../../customisations/Media";
|
||||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||||
import { fileSize, presentableTextForFile } from "../../../utils/FileUtils";
|
import { downloadLabelForFile, presentableTextForFile } from "../../../utils/FileUtils";
|
||||||
import { IBodyProps } from "./IBodyProps";
|
import { IBodyProps } from "./IBodyProps";
|
||||||
import { FileDownloader } from "../../../utils/FileDownloader";
|
import { FileDownloader } from "../../../utils/FileDownloader";
|
||||||
import TextWithTooltip from "../elements/TextWithTooltip";
|
import TextWithTooltip from "../elements/TextWithTooltip";
|
||||||
|
@ -26,7 +28,9 @@ export let DOWNLOAD_ICON_URL: string; // cached copy of the download.svg asset f
|
||||||
async function cacheDownloadIcon(): Promise<void> {
|
async function cacheDownloadIcon(): Promise<void> {
|
||||||
if (DOWNLOAD_ICON_URL) return; // cached already
|
if (DOWNLOAD_ICON_URL) return; // cached already
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const svg = await fetch(require("../../../../res/img/download.svg").default).then((r) => r.text());
|
const svg = await fetch(require("@vector-im/compound-design-tokens/icons/download.svg").default).then((r) =>
|
||||||
|
r.text(),
|
||||||
|
);
|
||||||
DOWNLOAD_ICON_URL = "data:image/svg+xml;base64," + window.btoa(svg);
|
DOWNLOAD_ICON_URL = "data:image/svg+xml;base64," + window.btoa(svg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -125,7 +129,7 @@ export default class MFileBody extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private get linkText(): string {
|
private get linkText(): string {
|
||||||
return presentableTextForFile(this.content);
|
return downloadLabelForFile(this.content, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private downloadFile(fileName: string, text: string): void {
|
private downloadFile(fileName: string, text: string): void {
|
||||||
|
@ -138,7 +142,7 @@ export default class MFileBody extends React.Component<IProps, IState> {
|
||||||
imgSrc: DOWNLOAD_ICON_URL,
|
imgSrc: DOWNLOAD_ICON_URL,
|
||||||
imgStyle: null,
|
imgStyle: null,
|
||||||
style: computedStyle(this.dummyLink.current),
|
style: computedStyle(this.dummyLink.current),
|
||||||
textContent: _t("timeline|m.file|download_label", { text }),
|
textContent: text,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -188,6 +192,12 @@ export default class MFileBody extends React.Component<IProps, IState> {
|
||||||
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";
|
||||||
|
|
||||||
|
let showDownloadLink =
|
||||||
|
!this.props.showGenericPlaceholder ||
|
||||||
|
(this.context.timelineRenderingType !== TimelineRenderingType.Room &&
|
||||||
|
this.context.timelineRenderingType !== TimelineRenderingType.Search &&
|
||||||
|
this.context.timelineRenderingType !== TimelineRenderingType.Pinned);
|
||||||
|
|
||||||
let placeholder: React.ReactNode = null;
|
let placeholder: React.ReactNode = null;
|
||||||
if (this.props.showGenericPlaceholder) {
|
if (this.props.showGenericPlaceholder) {
|
||||||
placeholder = (
|
placeholder = (
|
||||||
|
@ -200,6 +210,7 @@ export default class MFileBody extends React.Component<IProps, IState> {
|
||||||
</TextWithTooltip>
|
</TextWithTooltip>
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
);
|
);
|
||||||
|
showDownloadLink = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.forExport) {
|
if (this.props.forExport) {
|
||||||
|
@ -212,12 +223,6 @@ export default class MFileBody extends React.Component<IProps, IState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let showDownloadLink =
|
|
||||||
!this.props.showGenericPlaceholder ||
|
|
||||||
(this.context.timelineRenderingType !== TimelineRenderingType.Room &&
|
|
||||||
this.context.timelineRenderingType !== TimelineRenderingType.Search &&
|
|
||||||
this.context.timelineRenderingType !== TimelineRenderingType.Pinned);
|
|
||||||
|
|
||||||
if (this.context.timelineRenderingType === TimelineRenderingType.Thread) {
|
if (this.context.timelineRenderingType === TimelineRenderingType.Thread) {
|
||||||
showDownloadLink = false;
|
showDownloadLink = false;
|
||||||
}
|
}
|
||||||
|
@ -235,9 +240,9 @@ export default class MFileBody extends React.Component<IProps, IState> {
|
||||||
{placeholder}
|
{placeholder}
|
||||||
{showDownloadLink && (
|
{showDownloadLink && (
|
||||||
<div className="mx_MFileBody_download">
|
<div className="mx_MFileBody_download">
|
||||||
<AccessibleButton onClick={this.decryptFile}>
|
<Button size="sm" kind="secondary" Icon={DownloadIcon} onClick={this.decryptFile}>
|
||||||
{_t("timeline|m.file|decrypt_label", { text: this.linkText })}
|
{this.linkText}
|
||||||
</AccessibleButton>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
|
@ -254,14 +259,13 @@ export default class MFileBody extends React.Component<IProps, IState> {
|
||||||
<div className="mx_MFileBody_download">
|
<div className="mx_MFileBody_download">
|
||||||
<div aria-hidden style={{ display: "none" }}>
|
<div aria-hidden style={{ display: "none" }}>
|
||||||
{/*
|
{/*
|
||||||
* Add dummy copy of the "a" tag
|
* Add dummy copy of the button
|
||||||
* We'll use it to learn how the download link
|
* We'll use it to learn how the download button
|
||||||
* would have been styled if it was rendered inline.
|
* would have been styled if it was rendered inline.
|
||||||
*/}
|
*/}
|
||||||
{/* this violates multiple eslint rules
|
{/* this violates multiple eslint rules
|
||||||
so ignore it completely */}
|
so ignore it completely */}
|
||||||
{/* eslint-disable-next-line */}
|
<Button size="sm" kind="secondary" Icon={DownloadIcon} as="a" ref={this.dummyLink} />
|
||||||
<a ref={this.dummyLink} />
|
|
||||||
</div>
|
</div>
|
||||||
{/*
|
{/*
|
||||||
TODO: Move iframe (and dummy link) into FileDownloader.
|
TODO: Move iframe (and dummy link) into FileDownloader.
|
||||||
|
@ -283,7 +287,10 @@ export default class MFileBody extends React.Component<IProps, IState> {
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
} else if (contentUrl) {
|
} else if (contentUrl) {
|
||||||
const downloadProps: AllHTMLAttributes<HTMLAnchorElement> = {
|
const downloadProps: Pick<
|
||||||
|
AllHTMLAttributes<HTMLAnchorElement>,
|
||||||
|
"target" | "rel" | "href" | "onClick" | "download"
|
||||||
|
> = {
|
||||||
target: "_blank",
|
target: "_blank",
|
||||||
rel: "noreferrer noopener",
|
rel: "noreferrer noopener",
|
||||||
|
|
||||||
|
@ -332,25 +339,18 @@ export default class MFileBody extends React.Component<IProps, IState> {
|
||||||
{placeholder}
|
{placeholder}
|
||||||
{showDownloadLink && (
|
{showDownloadLink && (
|
||||||
<div className="mx_MFileBody_download">
|
<div className="mx_MFileBody_download">
|
||||||
<a {...downloadProps}>
|
<Button size="sm" kind="secondary" Icon={DownloadIcon} as="a" {...downloadProps}>
|
||||||
<span className="mx_MFileBody_download_icon" />
|
{this.linkText}
|
||||||
{_t("timeline|m.file|download_label", { text: this.linkText })}
|
</Button>
|
||||||
</a>
|
|
||||||
{this.context.timelineRenderingType === TimelineRenderingType.File && (
|
|
||||||
<div className="mx_MImageBody_size">
|
|
||||||
{this.content.info?.size ? fileSize(this.content.info.size) : ""}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const extra = this.linkText ? ": " + this.linkText : "";
|
|
||||||
return (
|
return (
|
||||||
<span className="mx_MFileBody">
|
<span className="mx_MFileBody">
|
||||||
{placeholder}
|
{placeholder}
|
||||||
{_t("timeline|m.file|error_invalid", { extra: extra })}
|
{_t("timeline|m.file|error_invalid")}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1024,6 +1024,9 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||||
// no avatar or sender profile for continuation messages and call tiles
|
// no avatar or sender profile for continuation messages and call tiles
|
||||||
avatarSize = null;
|
avatarSize = null;
|
||||||
needsSenderProfile = false;
|
needsSenderProfile = false;
|
||||||
|
} else if (this.context.timelineRenderingType === TimelineRenderingType.File) {
|
||||||
|
avatarSize = "20px";
|
||||||
|
needsSenderProfile = true;
|
||||||
} else {
|
} else {
|
||||||
avatarSize = "30px";
|
avatarSize = "30px";
|
||||||
needsSenderProfile = true;
|
needsSenderProfile = true;
|
||||||
|
@ -1351,6 +1354,18 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||||
"data-scroll-tokens": scrollToken,
|
"data-scroll-tokens": scrollToken,
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
|
<a
|
||||||
|
className="mx_EventTile_senderDetailsLink"
|
||||||
|
key="mx_EventTile_senderDetailsLink"
|
||||||
|
href={permalink}
|
||||||
|
onClick={this.onPermalinkClicked}
|
||||||
|
>
|
||||||
|
<div className="mx_EventTile_senderDetails" onContextMenu={this.onTimestampContextMenu}>
|
||||||
|
{avatar}
|
||||||
|
{sender}
|
||||||
|
{timestamp}
|
||||||
|
</div>
|
||||||
|
</a>,
|
||||||
<div className={lineClasses} key="mx_EventTile_line" onContextMenu={this.onContextMenu}>
|
<div className={lineClasses} key="mx_EventTile_line" onContextMenu={this.onContextMenu}>
|
||||||
{this.renderContextMenu()}
|
{this.renderContextMenu()}
|
||||||
{renderTile(
|
{renderTile(
|
||||||
|
@ -1371,17 +1386,6 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||||
this.context.showHiddenEvents,
|
this.context.showHiddenEvents,
|
||||||
)}
|
)}
|
||||||
</div>,
|
</div>,
|
||||||
<a
|
|
||||||
className="mx_EventTile_senderDetailsLink"
|
|
||||||
key="mx_EventTile_senderDetailsLink"
|
|
||||||
href={permalink}
|
|
||||||
onClick={this.onPermalinkClicked}
|
|
||||||
>
|
|
||||||
<div className="mx_EventTile_senderDetails" onContextMenu={this.onTimestampContextMenu}>
|
|
||||||
{sender}
|
|
||||||
{timestamp}
|
|
||||||
</div>
|
|
||||||
</a>,
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3328,10 +3328,8 @@
|
||||||
"voice_call_unsupported": "%(senderName)s placed a voice call. (not supported by this browser)"
|
"voice_call_unsupported": "%(senderName)s placed a voice call. (not supported by this browser)"
|
||||||
},
|
},
|
||||||
"m.file": {
|
"m.file": {
|
||||||
"decrypt_label": "Decrypt %(text)s",
|
|
||||||
"download_label": "Download %(text)s",
|
|
||||||
"error_decrypting": "Error decrypting attachment",
|
"error_decrypting": "Error decrypting attachment",
|
||||||
"error_invalid": "Invalid file%(extra)s"
|
"error_invalid": "Invalid file"
|
||||||
},
|
},
|
||||||
"m.image": {
|
"m.image": {
|
||||||
"error": "Unable to show image due to error",
|
"error": "Unable to show image due to error",
|
||||||
|
|
|
@ -36,9 +36,9 @@ function remoteRender(event: MessageEvent): void {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
img.style = data.imgStyle;
|
img.style = data.imgStyle;
|
||||||
} else {
|
} else {
|
||||||
img.style.width = "12px";
|
img.style.width = "20px";
|
||||||
img.style.height = "12px";
|
img.style.height = "20px";
|
||||||
img.style.webkitMaskSize = "12px";
|
img.style.webkitMaskSize = "20px";
|
||||||
img.style.webkitMaskPosition = "center";
|
img.style.webkitMaskPosition = "center";
|
||||||
img.style.webkitMaskRepeat = "no-repeat";
|
img.style.webkitMaskRepeat = "no-repeat";
|
||||||
img.style.display = "inline-block";
|
img.style.display = "inline-block";
|
||||||
|
|
|
@ -21,6 +21,17 @@ import { MediaEventContent } from "matrix-js-sdk/src/types";
|
||||||
|
|
||||||
import { _t } from "../languageHandler";
|
import { _t } from "../languageHandler";
|
||||||
|
|
||||||
|
export function downloadLabelForFile(content: MediaEventContent, withSize = true): string {
|
||||||
|
let text = _t("action|download");
|
||||||
|
|
||||||
|
if (content.info?.size && withSize) {
|
||||||
|
// If we know the size of the file then add it as human-readable string to the end of the link text
|
||||||
|
// so that the user knows how big a file they are downloading.
|
||||||
|
text += " (" + <string>fileSize(content.info.size, { base: 2, standard: "jedec" }) + ")";
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extracts a human-readable label for the file attachment to use as
|
* Extracts a human-readable label for the file attachment to use as
|
||||||
* link text.
|
* link text.
|
||||||
|
|
88
test/components/views/messages/MFileBody-test.tsx
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
/*
|
||||||
|
Copyright 2024 New Vector Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||||
|
Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { render } from "jest-matrix-react";
|
||||||
|
import { EventType, getHttpUriForMxc, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
|
import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalinks";
|
||||||
|
import {
|
||||||
|
getMockClientWithEventEmitter,
|
||||||
|
mockClientMethodsCrypto,
|
||||||
|
mockClientMethodsDevice,
|
||||||
|
mockClientMethodsServer,
|
||||||
|
mockClientMethodsUser,
|
||||||
|
} from "../../../test-utils";
|
||||||
|
import { MediaEventHelper } from "../../../../src/utils/MediaEventHelper";
|
||||||
|
import SettingsStore from "../../../../src/settings/SettingsStore";
|
||||||
|
import MFileBody from "../../../../src/components/views/messages/MFileBody.tsx";
|
||||||
|
import RoomContext, { TimelineRenderingType } from "../../../../src/contexts/RoomContext.ts";
|
||||||
|
|
||||||
|
jest.mock("matrix-encrypt-attachment", () => ({
|
||||||
|
decryptAttachment: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("<MFileBody/>", () => {
|
||||||
|
const userId = "@user:server";
|
||||||
|
const deviceId = "DEADB33F";
|
||||||
|
const cli = getMockClientWithEventEmitter({
|
||||||
|
...mockClientMethodsUser(userId),
|
||||||
|
...mockClientMethodsServer(),
|
||||||
|
...mockClientMethodsDevice(deviceId),
|
||||||
|
...mockClientMethodsCrypto(),
|
||||||
|
getRooms: jest.fn().mockReturnValue([]),
|
||||||
|
getIgnoredUsers: jest.fn(),
|
||||||
|
getVersions: jest.fn().mockResolvedValue({
|
||||||
|
unstable_features: {
|
||||||
|
"org.matrix.msc3882": true,
|
||||||
|
"org.matrix.msc3886": true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
// 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 mediaEvent = new MatrixEvent({
|
||||||
|
room_id: "!room:server",
|
||||||
|
sender: userId,
|
||||||
|
type: EventType.RoomMessage,
|
||||||
|
content: {
|
||||||
|
body: "alt for a image",
|
||||||
|
msgtype: "m.image",
|
||||||
|
url: "mxc://server/image",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const props = {
|
||||||
|
onHeightChanged: jest.fn(),
|
||||||
|
onMessageAllowed: jest.fn(),
|
||||||
|
permalinkCreator: new RoomPermalinkCreator(new Room(mediaEvent.getRoomId()!, cli, cli.getUserId()!)),
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.spyOn(SettingsStore, "getValue").mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show a download button in file rendering type", async () => {
|
||||||
|
const { container, getByRole } = render(
|
||||||
|
<RoomContext.Provider value={{ timelineRenderingType: TimelineRenderingType.File } as any}>
|
||||||
|
<MFileBody
|
||||||
|
{...props}
|
||||||
|
mxEvent={mediaEvent}
|
||||||
|
mediaEventHelper={new MediaEventHelper(mediaEvent)}
|
||||||
|
showGenericPlaceholder={false}
|
||||||
|
/>
|
||||||
|
</RoomContext.Provider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(getByRole("link", { name: "Download" })).toBeInTheDocument();
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,39 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`<MFileBody/> should show a download button in file rendering type 1`] = `
|
||||||
|
<div>
|
||||||
|
<span
|
||||||
|
class="mx_MFileBody"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_MFileBody_download"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="_button_i91xf_17 _has-icon_i91xf_66"
|
||||||
|
data-kind="secondary"
|
||||||
|
data-size="sm"
|
||||||
|
download="alt for a image"
|
||||||
|
href="https://server/_matrix/media/v3/download/server/image"
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
role="link"
|
||||||
|
tabindex="0"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="currentColor"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="20"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M12 15.575c-.133 0-.258-.02-.375-.063a.877.877 0 0 1-.325-.212l-3.6-3.6a.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7c.183-.183.42-.28.712-.288.292-.008.53.08.713.263L11 12.15V5c0-.283.096-.52.287-.713A.968.968 0 0 1 12 4c.283 0 .52.096.713.287.191.192.287.43.287.713v7.15l1.875-1.875c.183-.183.42-.27.713-.263.291.009.529.105.712.288a.948.948 0 0 1 .275.7.948.948 0 0 1-.275.7l-3.6 3.6c-.1.1-.208.17-.325.212a1.106 1.106 0 0 1-.375.063ZM6 20c-.55 0-1.02-.196-1.412-.587A1.926 1.926 0 0 1 4 18v-2c0-.283.096-.52.287-.713A.967.967 0 0 1 5 15c.283 0 .52.096.713.287.191.192.287.43.287.713v2h12v-2a.97.97 0 0 1 .288-.713A.968.968 0 0 1 19 15a.97.97 0 0 1 .712.287c.192.192.288.43.288.713v2c0 .55-.196 1.02-.587 1.413A1.926 1.926 0 0 1 18 20H6Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Download
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
`;
|
|
@ -169,6 +169,15 @@ describe("EventTile", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("EventTile renderingType: File", () => {
|
||||||
|
it("should not display the pinned message badge", async () => {
|
||||||
|
jest.spyOn(PinningUtils, "isPinned").mockReturnValue(true);
|
||||||
|
getComponent({}, TimelineRenderingType.File);
|
||||||
|
|
||||||
|
expect(screen.queryByText("Pinned message")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("EventTile renderingType: default", () => {
|
describe("EventTile renderingType: default", () => {
|
||||||
it.each([[Layout.Group], [Layout.Bubble], [Layout.IRC]])(
|
it.each([[Layout.Group], [Layout.Bubble], [Layout.IRC]])(
|
||||||
"should display the pinned message badge",
|
"should display the pinned message badge",
|
||||||
|
|
60
test/utils/FileUtils-test.ts
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
/*
|
||||||
|
Copyright 2024 New Vector Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||||
|
Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { MediaEventContent } from "matrix-js-sdk/src/types";
|
||||||
|
|
||||||
|
import { downloadLabelForFile } from "../../src/utils/FileUtils.ts";
|
||||||
|
|
||||||
|
describe("FileUtils", () => {
|
||||||
|
describe("downloadLabelForFile", () => {
|
||||||
|
it.each([
|
||||||
|
[
|
||||||
|
"File with size",
|
||||||
|
{
|
||||||
|
input: {
|
||||||
|
msgtype: "m.file",
|
||||||
|
body: "Test",
|
||||||
|
info: {
|
||||||
|
size: 102434566,
|
||||||
|
},
|
||||||
|
} as MediaEventContent,
|
||||||
|
output: "Download (97.69 MB)",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Image",
|
||||||
|
{
|
||||||
|
input: {
|
||||||
|
msgtype: "m.image",
|
||||||
|
body: "Test",
|
||||||
|
} as MediaEventContent,
|
||||||
|
output: "Download",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Video",
|
||||||
|
{
|
||||||
|
input: {
|
||||||
|
msgtype: "m.video",
|
||||||
|
body: "Test",
|
||||||
|
} as MediaEventContent,
|
||||||
|
output: "Download",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Audio",
|
||||||
|
{
|
||||||
|
input: {
|
||||||
|
msgtype: "m.audio",
|
||||||
|
body: "Test",
|
||||||
|
} as MediaEventContent,
|
||||||
|
output: "Download",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])("should correctly label %s", (_d, { input, output }) => expect(downloadLabelForFile(input)).toBe(output));
|
||||||
|
});
|
||||||
|
});
|