/* Copyright 2024 New Vector Ltd. Copyright 2020, 2021 Tulir Asokan 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, { createRef } from "react"; import classNames from "classnames"; import { type MatrixEvent, MatrixEventEvent, EventType, MsgType } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; import { _t } from "../../../languageHandler"; import dis from "../../../dispatcher/dispatcher"; import { Action } from "../../../dispatcher/actions"; import { type RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"; import SenderProfile from "../messages/SenderProfile"; import MImageReplyBody from "../messages/MImageReplyBody"; import { isVoiceMessage } from "../../../utils/EventUtils"; import { getEventDisplayInfo } from "../../../utils/EventRenderingUtils"; import MFileBody from "../messages/MFileBody"; import MemberAvatar from "../avatars/MemberAvatar"; import MVoiceMessageBody from "../messages/MVoiceMessageBody"; import { type ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; import { renderReplyTile } from "../../../events/EventTileFactory"; import { type GetRelationsForEvent } from "../rooms/EventTile"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { type IBodyProps } from "../messages/IBodyProps"; interface IProps { mxEvent: MatrixEvent; permalinkCreator?: RoomPermalinkCreator; highlights?: string[]; highlightLink?: string; toggleExpandedQuote?: () => void; getRelationsForEvent?: GetRelationsForEvent; } export default class ReplyTile extends React.PureComponent { private anchorElement = createRef(); public componentDidMount(): void { this.props.mxEvent.on(MatrixEventEvent.Decrypted, this.onDecrypted); this.props.mxEvent.on(MatrixEventEvent.BeforeRedaction, this.onEventRequiresUpdate); this.props.mxEvent.on(MatrixEventEvent.Replaced, this.onEventRequiresUpdate); } public componentWillUnmount(): void { this.props.mxEvent.removeListener(MatrixEventEvent.Decrypted, this.onDecrypted); this.props.mxEvent.removeListener(MatrixEventEvent.BeforeRedaction, this.onEventRequiresUpdate); this.props.mxEvent.removeListener(MatrixEventEvent.Replaced, this.onEventRequiresUpdate); } private onDecrypted = (): void => { this.forceUpdate(); }; private onEventRequiresUpdate = (): void => { // Force update when necessary - redactions and edits this.forceUpdate(); }; private onClick = (e: React.MouseEvent): void => { const clickTarget = e.target as HTMLElement; // Following a link within a reply should not dispatch the `view_room` action // so that the browser can direct the user to the correct location // The exception being the link wrapping the reply if ( clickTarget.tagName.toLowerCase() !== "a" || clickTarget.closest("a") === null || clickTarget === this.anchorElement.current ) { // This allows the permalink to be opened in a new tab/window or copied as // matrix.to, but also for it to enable routing within Riot when clicked. e.preventDefault(); // Expand thread on shift key if (this.props.toggleExpandedQuote && e.shiftKey) { this.props.toggleExpandedQuote(); } else { dis.dispatch({ action: Action.ViewRoom, event_id: this.props.mxEvent.getId(), highlighted: true, room_id: this.props.mxEvent.getRoomId(), metricsTrigger: undefined, // room doesn't change }); } } }; public render(): React.ReactNode { const mxEvent = this.props.mxEvent; const msgType = mxEvent.getContent().msgtype; const evType = mxEvent.getType(); const { hasRenderer, isInfoMessage, isSeeingThroughMessageHiddenForModeration } = getEventDisplayInfo( MatrixClientPeg.safeGet(), mxEvent, false /* Replies are never hidden, so this should be fine */, ); // This shouldn't happen: the caller should check we support this type // before trying to instantiate us if (!hasRenderer) { const { mxEvent } = this.props; logger.warn(`Event type not supported: type:${mxEvent.getType()} isState:${mxEvent.isState()}`); return (
{_t("timeline|error_no_renderer")}
); } const classes = classNames("mx_ReplyTile", { mx_ReplyTile_inline: msgType === MsgType.Emote, mx_ReplyTile_info: isInfoMessage && !mxEvent.isRedacted(), mx_ReplyTile_audio: msgType === MsgType.Audio, mx_ReplyTile_video: msgType === MsgType.Video, }); let permalink = "#"; if (this.props.permalinkCreator) { permalink = this.props.permalinkCreator.forEvent(mxEvent.getId()!); } let sender; const hasOwnSender = isInfoMessage || evType === EventType.RoomCreate; if (!hasOwnSender) { sender = (
); } const msgtypeOverrides: Record> = { [MsgType.Image]: MImageReplyBody, // Override audio and video body with file body. We also hide the download/decrypt button using CSS [MsgType.Audio]: isVoiceMessage(mxEvent) ? MVoiceMessageBody : MFileBody, [MsgType.Video]: MFileBody, }; const evOverrides: Record> = { // Use MImageReplyBody so that the sticker isn't taking up a lot of space [EventType.Sticker]: MImageReplyBody, }; return ( ); } }