Files
element-web/src/components/views/rooms/LinkPreviewWidget.tsx
Will Hunt 75d9898dff Global configuration flag for media previews (#29582)
* Modify useMediaVisible to take a room.

* Add initial support for a account data level key.

* Update controls.

* Update settings

* Lint and fixes

* make some tests go happy

* lint

* i18n

* update preferences

* prettier

* Update settings tab.

* update screenshot

* Update docs

* Rewrite controller

* Rewrite tons of tests

* Rewrite RoomAvatar to be a functional component

This is so we can use hooks to determine the setting state.

* lint

* lint

* Tidy up comments

* Apply media visible hook to inline images.

* Move conditionals.

* copyright all the things

* Review changes

* Update html utils to properly discard media.

* Types fix

* Fixing tests that break settings getValue expectations

* Fix logic around media preview calculation

* Fix room header tests

* Fixup tests for timelinePanel

* Clear settings in matrixchat

* Update tests to use SettingsStore where possible.

* fix bug

* revert changes to client.ts

* copyright years

* Add header

* Add a test for MediaPreviewAccountSettingsTab

* Mark initMatrixClient as optional

* Improve on types

* Ensure we do not set the account data twice.

* lint

* Review changes

* Ensure we include the client on rendered messages.

* Fix test

* update labels

* clean designs

* update settings tab

* update snapshot

* copyright

* prevent mutation
2025-04-22 09:37:47 +00:00

140 lines
5.3 KiB
TypeScript

/*
Copyright 2024, 2025 New Vector Ltd.
Copyright 2016-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 React, { type JSX, type ComponentProps, createRef, type ReactNode } from "react";
import { decode } from "html-entities";
import { type MatrixEvent, type IPreviewUrlResponse } from "matrix-js-sdk/src/matrix";
import { Linkify } from "../../../HtmlUtils";
import Modal from "../../../Modal";
import * as ImageUtils from "../../../ImageUtils";
import { mediaFromMxc } from "../../../customisations/Media";
import ImageView from "../elements/ImageView";
import LinkWithTooltip from "../elements/LinkWithTooltip";
import PlatformPeg from "../../../PlatformPeg";
interface IProps {
link: string;
preview: IPreviewUrlResponse;
mxEvent: MatrixEvent; // the Event associated with the preview
children?: ReactNode;
mediaVisible: boolean;
}
export default class LinkPreviewWidget extends React.Component<IProps> {
private image = createRef<HTMLImageElement>();
private onImageClick = (ev: React.MouseEvent): void => {
const p = this.props.preview;
if (ev.button != 0 || ev.metaKey) return;
ev.preventDefault();
let src: string | null | undefined = p["og:image"];
if (src?.startsWith("mxc://")) {
src = mediaFromMxc(src).srcHttp;
}
if (!src) return;
const params: Omit<ComponentProps<typeof ImageView>, "onFinished"> = {
src: src,
width: p["og:image:width"],
height: p["og:image:height"],
name: p["og:title"] || p["og:description"] || this.props.link,
fileSize: p["matrix:image:size"],
link: this.props.link,
};
if (this.image.current) {
const clientRect = this.image.current.getBoundingClientRect();
params.thumbnailInfo = {
width: clientRect.width,
height: clientRect.height,
positionX: clientRect.x,
positionY: clientRect.y,
};
}
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", undefined, true);
};
public render(): React.ReactNode {
const p = this.props.preview;
// FIXME: do we want to factor out all image displaying between this and MImageBody - especially for lightboxing?
let image: string | null = p["og:image"] ?? null;
if (!this.props.mediaVisible) {
image = null; // Don't render a button to show the image, just hide it outright
}
const imageMaxWidth = 100;
const imageMaxHeight = 100;
if (image && image.startsWith("mxc://")) {
// We deliberately don't want a square here, so use the source HTTP thumbnail function
image = mediaFromMxc(image).getThumbnailOfSourceHttp(imageMaxWidth, imageMaxHeight, "scale");
}
const thumbHeight =
ImageUtils.thumbHeight(p["og:image:width"], p["og:image:height"], imageMaxWidth, imageMaxHeight) ??
imageMaxHeight;
let img: JSX.Element | undefined;
if (image) {
img = (
<div className="mx_LinkPreviewWidget_image" style={{ height: thumbHeight }}>
<img
ref={this.image}
style={{ maxWidth: imageMaxWidth, maxHeight: imageMaxHeight }}
src={image}
onClick={this.onImageClick}
alt=""
/>
</div>
);
}
// The description includes &-encoded HTML entities, we decode those as React treats the thing as an
// opaque string. This does not allow any HTML to be injected into the DOM.
const description = decode(p["og:description"] || "");
const title = p["og:title"]?.trim() ?? "";
const anchor = (
<a href={this.props.link} target="_blank" rel="noreferrer noopener">
{title}
</a>
);
const needsTooltip = PlatformPeg.get()?.needsUrlTooltips() && this.props.link !== title;
return (
<div className="mx_LinkPreviewWidget">
<div className="mx_LinkPreviewWidget_wrapImageCaption">
{img}
<div className="mx_LinkPreviewWidget_caption">
<div className="mx_LinkPreviewWidget_title">
{needsTooltip ? (
<LinkWithTooltip tooltip={new URL(this.props.link, window.location.href).toString()}>
{anchor}
</LinkWithTooltip>
) : (
anchor
)}
{p["og:site_name"] && (
<span className="mx_LinkPreviewWidget_siteName">{" - " + p["og:site_name"]}</span>
)}
</div>
<div className="mx_LinkPreviewWidget_description">
<Linkify>{description}</Linkify>
</div>
</div>
</div>
{this.props.children}
</div>
);
}
}