/* 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 Please see LICENSE files in the repository root for full details. */ import React, { useContext, useEffect } from "react"; import { MatrixEvent, MatrixError, IPreviewUrlResponse, MatrixClient } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; import CloseIcon from "@vector-im/compound-design-tokens/assets/web/icons/close"; import { useStateToggle } from "../../../hooks/useStateToggle"; import LinkPreviewWidget from "./LinkPreviewWidget"; import AccessibleButton from "../elements/AccessibleButton"; import { _t } from "../../../languageHandler"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import { useAsyncMemo } from "../../../hooks/useAsyncMemo"; const INITIAL_NUM_PREVIEWS = 2; interface IProps { links: string[]; // the URLs to be previewed mxEvent: MatrixEvent; // the Event associated with the preview onCancelClick(): void; // called when the preview's cancel ('hide') button is clicked onHeightChanged?(): void; // called when the preview's contents has loaded } const LinkPreviewGroup: React.FC = ({ links, mxEvent, onCancelClick, onHeightChanged }) => { const cli = useContext(MatrixClientContext); const [expanded, toggleExpanded] = useStateToggle(); const ts = mxEvent.getTs(); const previews = useAsyncMemo<[string, IPreviewUrlResponse][]>( async () => { return fetchPreviews(cli, links, ts); }, [links, ts], [], ); useEffect(() => { onHeightChanged?.(); }, [onHeightChanged, expanded, previews]); const showPreviews = expanded ? previews : previews.slice(0, INITIAL_NUM_PREVIEWS); let toggleButton: JSX.Element | undefined; if (previews.length > INITIAL_NUM_PREVIEWS) { toggleButton = ( {expanded ? _t("action|collapse") : _t("timeline|url_preview|show_n_more", { count: previews.length - showPreviews.length })} ); } return (
{showPreviews.map(([link, preview], i) => ( {i === 0 ? ( ) : undefined} ))} {toggleButton}
); }; const fetchPreviews = (cli: MatrixClient, links: string[], ts: number): Promise<[string, IPreviewUrlResponse][]> => { return Promise.all<[string, IPreviewUrlResponse] | void>( links.map(async (link): Promise<[string, IPreviewUrlResponse] | undefined> => { try { const preview = await cli.getUrlPreview(link, ts); // Ensure at least one of the rendered fields is truthy if ( preview?.["og:image"]?.startsWith("mxc://") || !!preview?.["og:description"] || !!preview?.["og:title"] ) { return [link, preview]; } } catch (error) { if (error instanceof MatrixError && error.httpStatus === 404) { // Quieten 404 Not found errors, not all URLs can have a preview generated logger.debug("Failed to get URL preview: ", error); } else { logger.error("Failed to get URL preview: ", error); } } }), ).then((a) => a.filter(Boolean)) as Promise<[string, IPreviewUrlResponse][]>; }; export default LinkPreviewGroup;