/* Copyright 2024 New Vector Ltd. Copyright 2019 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, { type JSX } from "react"; import { MatrixEvent, EventType, RelationType, MatrixClient, MatrixError } from "matrix-js-sdk/src/matrix"; import { defer } from "matrix-js-sdk/src/utils"; import { logger } from "matrix-js-sdk/src/logger"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { _t } from "../../../languageHandler"; import { wantsDateSeparator } from "../../../DateUtils"; import SettingsStore from "../../../settings/SettingsStore"; import BaseDialog from "./BaseDialog"; import ScrollPanel from "../../structures/ScrollPanel"; import Spinner from "../elements/Spinner"; import EditHistoryMessage from "../messages/EditHistoryMessage"; import DateSeparator from "../messages/DateSeparator"; interface IProps { mxEvent: MatrixEvent; onFinished(): void; } interface IState { originalEvent: MatrixEvent | null; error: MatrixError | null; events: MatrixEvent[]; nextBatch: string | null; isLoading: boolean; isTwelveHour: boolean; } export default class MessageEditHistoryDialog extends React.PureComponent { public constructor(props: IProps) { super(props); this.state = { originalEvent: null, error: null, events: [], nextBatch: null, isLoading: true, isTwelveHour: SettingsStore.getValue("showTwelveHourTimestamps"), }; } private loadMoreEdits = async (backwards?: boolean): Promise => { if (backwards || (!this.state.nextBatch && !this.state.isLoading)) { // bail out on backwards as we only paginate in one direction return false; } const opts = { from: this.state.nextBatch ?? undefined }; const roomId = this.props.mxEvent.getRoomId()!; const eventId = this.props.mxEvent.getId()!; const client = MatrixClientPeg.safeGet(); const { resolve, reject, promise } = defer(); let result: Awaited>; try { result = await client.relations(roomId, eventId, RelationType.Replace, EventType.RoomMessage, opts); } catch (error) { // log if the server returned an error if (error instanceof MatrixError && error.errcode) { logger.error("fetching /relations failed with error", error); } this.setState({ error: error as MatrixError }, () => reject(error)); return promise; } const newEvents = result.events; this.locallyRedactEventsIfNeeded(newEvents); this.setState( { originalEvent: this.state.originalEvent ?? result.originalEvent ?? null, events: this.state.events.concat(newEvents), nextBatch: result.nextBatch ?? null, isLoading: false, }, () => { const hasMoreResults = !!this.state.nextBatch; resolve(hasMoreResults); }, ); return promise; }; private locallyRedactEventsIfNeeded(newEvents: MatrixEvent[]): void { const roomId = this.props.mxEvent.getRoomId(); const client = MatrixClientPeg.safeGet(); const room = client.getRoom(roomId); if (!room) return; const pendingEvents = room.getPendingEvents(); for (const e of newEvents) { const pendingRedaction = pendingEvents.find((pe) => { return pe.getType() === EventType.RoomRedaction && pe.getAssociatedId() === e.getId(); }); if (pendingRedaction) { e.markLocallyRedacted(pendingRedaction); } } } public componentDidMount(): void { this.loadMoreEdits(); } private renderEdits(): JSX.Element[] { const nodes: JSX.Element[] = []; let lastEvent: MatrixEvent; let allEvents = this.state.events; // append original event when we've done last pagination if (this.state.originalEvent && !this.state.nextBatch) { allEvents = allEvents.concat(this.state.originalEvent); } const baseEventId = this.props.mxEvent.getId(); allEvents.forEach((e, i) => { if (!lastEvent || wantsDateSeparator(lastEvent.getDate() || undefined, e.getDate() || undefined)) { nodes.push(
  • , ); } const isBaseEvent = e.getId() === baseEventId; nodes.push( , ); lastEvent = e; }); return nodes; } public render(): React.ReactNode { let content; if (this.state.error) { const { error } = this.state; if (error.errcode === "M_UNRECOGNIZED") { content =

    {_t("error|edit_history_unsupported")}

    ; } else if (error.errcode) { // some kind of error from the homeserver content =

    {_t("error|something_went_wrong")}

    ; } else { content = (

    {_t("cannot_reach_homeserver")}
    {_t("cannot_reach_homeserver_detail")}

    ); } } else if (this.state.isLoading) { content = ; } else { content = (
      {this.renderEdits()}
    ); } return ( {content} ); } }