/* Copyright 2019-2024 New Vector Ltd. Copyright 2017 Vector Creations Ltd Copyright 2016 OpenMarket 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 sanitizeHtml from "sanitize-html"; import classnames from "classnames"; import { logger } from "matrix-js-sdk/src/logger"; import { _t, TranslationKey } from "../../languageHandler"; import dis from "../../dispatcher/dispatcher"; import { MatrixClientPeg } from "../../MatrixClientPeg"; import MatrixClientContext from "../../contexts/MatrixClientContext"; import AutoHideScrollbar from "./AutoHideScrollbar"; import { ActionPayload } from "../../dispatcher/payloads"; interface IProps { // URL to request embedded page content from url?: string; // Class name prefix to apply for a given instance className?: string; // Whether to wrap the page in a scrollbar scrollbar?: boolean; // Map of keys to replace with values, e.g {$placeholder: "value"} replaceMap?: Record; } interface IState { page: string; } export default class EmbeddedPage extends React.PureComponent { public static contextType = MatrixClientContext; declare public context: React.ContextType; private unmounted = false; private dispatcherRef?: string; public constructor(props: IProps, context: React.ContextType) { super(props, context); this.state = { page: "", }; } private translate(s: TranslationKey): string { return sanitizeHtml(_t(s)); } private async fetchEmbed(): Promise { let res: Response; try { res = await fetch(this.props.url!, { method: "GET" }); } catch (err) { if (this.unmounted) return; logger.warn(`Error loading page: ${err}`); this.setState({ page: _t("cant_load_page") }); return; } if (this.unmounted) return; if (!res.ok) { logger.warn(`Error loading page: ${res.status}`); this.setState({ page: _t("cant_load_page") }); return; } let body = (await res.text()).replace(/_t\(['"]([\s\S]*?)['"]\)/gm, (match, g1) => this.translate(g1)); if (this.props.replaceMap) { Object.keys(this.props.replaceMap).forEach((key) => { body = body.split(key).join(this.props.replaceMap![key]); }); } this.setState({ page: body }); } public componentDidMount(): void { this.unmounted = false; if (!this.props.url) { return; } // We use fetch to inline the page into the react component // so that it can inherit CSS and theming easily rather than mess around // with iframes and trying to synchronise document.stylesheets. this.fetchEmbed(); this.dispatcherRef = dis.register(this.onAction); } public componentWillUnmount(): void { this.unmounted = true; dis.unregister(this.dispatcherRef); } private onAction = (payload: ActionPayload): void => { // HACK: Workaround for the context's MatrixClient not being set up at render time. if (payload.action === "client_started") { this.forceUpdate(); } }; public render(): React.ReactNode { // HACK: Workaround for the context's MatrixClient not updating. const client = this.context || MatrixClientPeg.get(); const isGuest = client ? client.isGuest() : true; const className = this.props.className; const classes = classnames(className, { [`${className}_guest`]: isGuest, [`${className}_loggedIn`]: !!client, }); const content =
; if (this.props.scrollbar) { return {content}; } else { return
{content}
; } } }