diff --git a/src/components/views/elements/EventListSummary.tsx b/src/components/views/elements/EventListSummary.tsx index c052a83f661..79d89aed0d0 100644 --- a/src/components/views/elements/EventListSummary.tsx +++ b/src/components/views/elements/EventListSummary.tsx @@ -9,8 +9,9 @@ Please see LICENSE files in the repository root for full details. */ import React, { ComponentProps, ReactNode } from "react"; -import { MatrixEvent, RoomMember, EventType } from "matrix-js-sdk/src/matrix"; +import { EventType, MatrixEvent, MatrixEventEvent, RoomMember } from "matrix-js-sdk/src/matrix"; import { KnownMembership } from "matrix-js-sdk/src/types"; +import { throttle } from "lodash"; import { _t } from "../../../languageHandler"; import { formatList } from "../../../utils/FormattingUtils"; @@ -22,6 +23,8 @@ import { Layout } from "../../../settings/enums/Layout"; import RightPanelStore from "../../../stores/right-panel/RightPanelStore"; import AccessibleButton from "./AccessibleButton"; import RoomContext from "../../../contexts/RoomContext"; +import { arrayHasDiff } from "../../../utils/arrays.ts"; +import { objectHasDiff } from "../../../utils/objects.ts"; const onPinnedMessagesClick = (): void => { RightPanelStore.instance.setCard({ phase: RightPanelPhases.PinnedMessages }, false); @@ -69,9 +72,14 @@ enum TransitionType { const SEP = ","; -export default class EventListSummary extends React.Component< - IProps & Required> -> { +type Props = IProps & Required>; + +interface State { + userEvents: Record; + summaryMembers: RoomMember[]; +} + +export default class EventListSummary extends React.Component { public static contextType = RoomContext; declare public context: React.ContextType; @@ -82,15 +90,122 @@ export default class EventListSummary extends React.Component< layout: Layout.Group, }; - public shouldComponentUpdate(nextProps: IProps): boolean { + public constructor(props: Props) { + super(props); + + this.state = this.generateState(); + } + + private generateState(): State { + const eventsToRender = this.props.events; + + // Map user IDs to latest Avatar Member. ES6 Maps are ordered by when the key was created, + // so this works perfectly for us to match event order whilst storing the latest Avatar Member + const latestUserAvatarMember = new Map(); + + // Object mapping user IDs to an array of IUserEvents + const userEvents: Record = {}; + eventsToRender.forEach((e, index) => { + const type = e.getType(); + + let userKey = e.getSender()!; + if (e.isState() && type === EventType.RoomThirdPartyInvite) { + userKey = e.getContent().display_name; + } else if (e.isState() && type === EventType.RoomMember) { + userKey = e.getStateKey()!; + } else if (e.isRedacted() && e.getUnsigned()?.redacted_because) { + userKey = e.getUnsigned().redacted_because!.sender; + } + + // Initialise a user's events + if (!userEvents[userKey]) { + userEvents[userKey] = []; + } + + let displayName = userKey; + if (e.isRedacted()) { + const sender = this.context?.room?.getMember(userKey); + if (sender) { + displayName = sender.name; + latestUserAvatarMember.set(userKey, sender); + } + } else if (e.target && TARGET_AS_DISPLAY_NAME_EVENTS.includes(type as EventType)) { + displayName = e.target.name; + latestUserAvatarMember.set(userKey, e.target); + } else if (e.sender && type !== EventType.RoomThirdPartyInvite) { + displayName = e.sender.name; + latestUserAvatarMember.set(userKey, e.sender); + } + + userEvents[userKey].push({ + mxEvent: e, + displayName, + index: index, + }); + }); + + return { + userEvents, + summaryMembers: Array.from(latestUserAvatarMember.values()), + }; + } + + public componentDidMount(): void { + this.bindSentinelListeners(this.props.events); + } + + public componentDidUpdate(prevProps: Readonly): void { + if (prevProps.events !== this.props.events) { + this.unbindSentinelListeners(prevProps.events); + this.bindSentinelListeners(this.props.events); + this.setState(this.generateState()); + } + } + + public componentWillUnmount(): void { + this.unbindSentinelListeners(this.props.events); + } + + private bindSentinelListeners(events: MatrixEvent[]): void { + for (const event of events) { + event.on(MatrixEventEvent.SentinelUpdated, this.onEventSentinelUpdated); + } + } + + private unbindSentinelListeners(events: MatrixEvent[]): void { + for (const event of events) { + event.on(MatrixEventEvent.SentinelUpdated, this.onEventSentinelUpdated); + } + } + + private onEventSentinelUpdated = throttle( + (): void => { + console.log("@@ SENTINEL UPDATED"); + this.setState(this.generateState()); + }, + 500, + { leading: true, trailing: true }, + ); + + public shouldComponentUpdate(nextProps: Props, nextState: State): boolean { // Update if // - The number of summarised events has changed // - or if the summary is about to toggle to become collapsed // - or if there are fewEvents, meaning the child eventTiles are shown as-is + // - or if the summary members have changed + // - or if the one of IUserEvents within userEvents have changed return ( nextProps.events.length !== this.props.events.length || nextProps.events.length < this.props.threshold || - nextProps.layout !== this.props.layout + nextProps.layout !== this.props.layout || + arrayHasDiff(nextState.summaryMembers, this.state.summaryMembers) || + arrayHasDiff(Object.values(nextState.userEvents), Object.values(this.state.userEvents)) || + Object.keys(nextState.userEvents).length !== Object.keys(this.state.userEvents).length || + Object.keys(nextState.userEvents).some((userId) => + nextState.userEvents[userId].some((event, i) => + objectHasDiff(event, this.state.userEvents[userId]?.[i] ?? {}), + ), + ) ); } @@ -492,54 +607,7 @@ export default class EventListSummary extends React.Component< } public render(): React.ReactNode { - const eventsToRender = this.props.events; - - // Map user IDs to latest Avatar Member. ES6 Maps are ordered by when the key was created, - // so this works perfectly for us to match event order whilst storing the latest Avatar Member - const latestUserAvatarMember = new Map(); - - // Object mapping user IDs to an array of IUserEvents - const userEvents: Record = {}; - eventsToRender.forEach((e, index) => { - const type = e.getType(); - - let userKey = e.getSender()!; - if (e.isState() && type === EventType.RoomThirdPartyInvite) { - userKey = e.getContent().display_name; - } else if (e.isState() && type === EventType.RoomMember) { - userKey = e.getStateKey()!; - } else if (e.isRedacted() && e.getUnsigned()?.redacted_because) { - userKey = e.getUnsigned().redacted_because!.sender; - } - - // Initialise a user's events - if (!userEvents[userKey]) { - userEvents[userKey] = []; - } - - let displayName = userKey; - if (e.isRedacted()) { - const sender = this.context?.room?.getMember(userKey); - if (sender) { - displayName = sender.name; - latestUserAvatarMember.set(userKey, sender); - } - } else if (e.target && TARGET_AS_DISPLAY_NAME_EVENTS.includes(type as EventType)) { - displayName = e.target.name; - latestUserAvatarMember.set(userKey, e.target); - } else if (e.sender && type !== EventType.RoomThirdPartyInvite) { - displayName = e.sender.name; - latestUserAvatarMember.set(userKey, e.sender); - } - - userEvents[userKey].push({ - mxEvent: e, - displayName, - index: index, - }); - }); - - const aggregate = this.getAggregate(userEvents); + const aggregate = this.getAggregate(this.state.userEvents); // Sort types by order of lowest event index within sequence const orderedTransitionSequences = Object.keys(aggregate.names).sort( @@ -554,7 +622,7 @@ export default class EventListSummary extends React.Component< onToggle={this.props.onToggle} startExpanded={this.props.startExpanded} children={this.props.children} - summaryMembers={[...latestUserAvatarMember.values()]} + summaryMembers={this.state.summaryMembers} layout={this.props.layout} summaryText={this.generateSummary(aggregate.names, orderedTransitionSequences)} /> diff --git a/src/components/views/messages/TextualEvent.tsx b/src/components/views/messages/TextualEvent.tsx index 8549fc5cab5..41a8fdf1154 100644 --- a/src/components/views/messages/TextualEvent.tsx +++ b/src/components/views/messages/TextualEvent.tsx @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import React from "react"; -import { MatrixEvent } from "matrix-js-sdk/src/matrix"; +import { MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/matrix"; import RoomContext from "../../../contexts/RoomContext"; import * as TextForEvent from "../../../TextForEvent"; @@ -21,6 +21,19 @@ export default class TextualEvent extends React.Component { public static contextType = RoomContext; declare public context: React.ContextType; + public componentDidMount(): void { + this.props.mxEvent.on(MatrixEventEvent.SentinelUpdated, this.onEventSentinelUpdated); + } + public componentWillUnmount(): void { + this.props.mxEvent.off(MatrixEventEvent.SentinelUpdated, this.onEventSentinelUpdated); + } + + private onEventSentinelUpdated = (): void => { + // XXX: this is crap, but we don't have a better way to force a re-render + // Many TextForEvent handlers render parts of `event.sender` and `event.target` so ensure they are updated + this.forceUpdate(); + }; + public render(): React.ReactNode { const text = TextForEvent.textForEvent( this.props.mxEvent, diff --git a/src/hooks/usePinnedEvents.ts b/src/hooks/usePinnedEvents.ts index bdda4a77013..b065edc83f7 100644 --- a/src/hooks/usePinnedEvents.ts +++ b/src/hooks/usePinnedEvents.ts @@ -154,7 +154,7 @@ async function fetchPinnedEvent(room: Room, pinnedEventId: string, cli: MatrixCl const senderUserId = event.getSender(); if (senderUserId && PinningUtils.isUnpinnable(event)) { // Inject sender information - event.sender = room.getMember(senderUserId); + event.setMetadata(room.currentState, false); // Also inject any edits we've found if (edit) event.makeReplaced(edit); diff --git a/src/utils/exportUtils/Exporter.ts b/src/utils/exportUtils/Exporter.ts index 3852443d1e6..0c549038b47 100644 --- a/src/utils/exportUtils/Exporter.ts +++ b/src/utils/exportUtils/Exporter.ts @@ -110,12 +110,7 @@ export default abstract class Exporter { } protected setEventMetadata(event: MatrixEvent): MatrixEvent { - const roomState = this.room.currentState; - const sender = event.getSender(); - event.sender = (!!sender && roomState?.getSentinelMember(sender)) || null; - if (event.getType() === "m.room.member") { - event.target = roomState?.getSentinelMember(event.getStateKey()!) ?? null; - } + event.setMetadata(this.room.currentState, false); return event; } diff --git a/test/unit-tests/components/views/right_panel/__snapshots__/PinnedMessagesCard-test.tsx.snap b/test/unit-tests/components/views/right_panel/__snapshots__/PinnedMessagesCard-test.tsx.snap index 1ffef77d9c0..4a4ac6d6d63 100644 --- a/test/unit-tests/components/views/right_panel/__snapshots__/PinnedMessagesCard-test.tsx.snap +++ b/test/unit-tests/components/views/right_panel/__snapshots__/PinnedMessagesCard-test.tsx.snap @@ -145,6 +145,7 @@ exports[` should show two pinned messages 1`] = ` data-type="round" role="presentation" style="--cpd-avatar-size: 32px;" + title="@alice:example.org" > a @@ -222,6 +223,7 @@ exports[` should show two pinned messages 1`] = ` data-type="round" role="presentation" style="--cpd-avatar-size: 32px;" + title="@alice:example.org" > a @@ -364,6 +366,7 @@ exports[` unpin all should not allow to unpinall 1`] = ` data-type="round" role="presentation" style="--cpd-avatar-size: 32px;" + title="@alice:example.org" > a @@ -441,6 +444,7 @@ exports[` unpin all should not allow to unpinall 1`] = ` data-type="round" role="presentation" style="--cpd-avatar-size: 32px;" + title="@alice:example.org" > a