Skip to content

Commit f99d7ce

Browse files
authored
React to MatrixEvent sender/target being updated for rendering state events (#28947)
* React to MatrixEvent sender/target sentinels being updated for rendering state events Signed-off-by: Michael Telatynski <[email protected]> * Iterate Signed-off-by: Michael Telatynski <[email protected]> * Iterate Signed-off-by: Michael Telatynski <[email protected]> * React to sentinel changes in EventListSummary Signed-off-by: Michael Telatynski <[email protected]> * Iterate Signed-off-by: Michael Telatynski <[email protected]> * Iterate Signed-off-by: Michael Telatynski <[email protected]> --------- Signed-off-by: Michael Telatynski <[email protected]>
1 parent 585aa75 commit f99d7ce

File tree

5 files changed

+143
-63
lines changed

5 files changed

+143
-63
lines changed

src/components/views/elements/EventListSummary.tsx

Lines changed: 123 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ Please see LICENSE files in the repository root for full details.
99
*/
1010

1111
import React, { ComponentProps, ReactNode } from "react";
12-
import { MatrixEvent, RoomMember, EventType } from "matrix-js-sdk/src/matrix";
12+
import { EventType, MatrixEvent, MatrixEventEvent, RoomMember } from "matrix-js-sdk/src/matrix";
1313
import { KnownMembership } from "matrix-js-sdk/src/types";
14+
import { throttle } from "lodash";
1415

1516
import { _t } from "../../../languageHandler";
1617
import { formatList } from "../../../utils/FormattingUtils";
@@ -22,6 +23,8 @@ import { Layout } from "../../../settings/enums/Layout";
2223
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
2324
import AccessibleButton from "./AccessibleButton";
2425
import RoomContext from "../../../contexts/RoomContext";
26+
import { arrayHasDiff } from "../../../utils/arrays.ts";
27+
import { objectHasDiff } from "../../../utils/objects.ts";
2528

2629
const onPinnedMessagesClick = (): void => {
2730
RightPanelStore.instance.setCard({ phase: RightPanelPhases.PinnedMessages }, false);
@@ -69,9 +72,14 @@ enum TransitionType {
6972

7073
const SEP = ",";
7174

72-
export default class EventListSummary extends React.Component<
73-
IProps & Required<Pick<IProps, "summaryLength" | "threshold" | "avatarsMaxLength" | "layout">>
74-
> {
75+
type Props = IProps & Required<Pick<IProps, "summaryLength" | "threshold" | "avatarsMaxLength" | "layout">>;
76+
77+
interface State {
78+
userEvents: Record<string, IUserEvents[]>;
79+
summaryMembers: RoomMember[];
80+
}
81+
82+
export default class EventListSummary extends React.Component<Props, State> {
7583
public static contextType = RoomContext;
7684
declare public context: React.ContextType<typeof RoomContext>;
7785

@@ -82,15 +90,122 @@ export default class EventListSummary extends React.Component<
8290
layout: Layout.Group,
8391
};
8492

85-
public shouldComponentUpdate(nextProps: IProps): boolean {
93+
public constructor(props: Props) {
94+
super(props);
95+
96+
this.state = this.generateState();
97+
}
98+
99+
private generateState(): State {
100+
const eventsToRender = this.props.events;
101+
102+
// Map user IDs to latest Avatar Member. ES6 Maps are ordered by when the key was created,
103+
// so this works perfectly for us to match event order whilst storing the latest Avatar Member
104+
const latestUserAvatarMember = new Map<string, RoomMember>();
105+
106+
// Object mapping user IDs to an array of IUserEvents
107+
const userEvents: Record<string, IUserEvents[]> = {};
108+
eventsToRender.forEach((e, index) => {
109+
const type = e.getType();
110+
111+
let userKey = e.getSender()!;
112+
if (e.isState() && type === EventType.RoomThirdPartyInvite) {
113+
userKey = e.getContent().display_name;
114+
} else if (e.isState() && type === EventType.RoomMember) {
115+
userKey = e.getStateKey()!;
116+
} else if (e.isRedacted() && e.getUnsigned()?.redacted_because) {
117+
userKey = e.getUnsigned().redacted_because!.sender;
118+
}
119+
120+
// Initialise a user's events
121+
if (!userEvents[userKey]) {
122+
userEvents[userKey] = [];
123+
}
124+
125+
let displayName = userKey;
126+
if (e.isRedacted()) {
127+
const sender = this.context?.room?.getMember(userKey);
128+
if (sender) {
129+
displayName = sender.name;
130+
latestUserAvatarMember.set(userKey, sender);
131+
}
132+
} else if (e.target && TARGET_AS_DISPLAY_NAME_EVENTS.includes(type as EventType)) {
133+
displayName = e.target.name;
134+
latestUserAvatarMember.set(userKey, e.target);
135+
} else if (e.sender && type !== EventType.RoomThirdPartyInvite) {
136+
displayName = e.sender.name;
137+
latestUserAvatarMember.set(userKey, e.sender);
138+
}
139+
140+
userEvents[userKey].push({
141+
mxEvent: e,
142+
displayName,
143+
index: index,
144+
});
145+
});
146+
147+
return {
148+
userEvents,
149+
summaryMembers: Array.from(latestUserAvatarMember.values()),
150+
};
151+
}
152+
153+
public componentDidMount(): void {
154+
this.bindSentinelListeners(this.props.events);
155+
}
156+
157+
public componentDidUpdate(prevProps: Readonly<Props>): void {
158+
if (prevProps.events !== this.props.events) {
159+
this.unbindSentinelListeners(prevProps.events);
160+
this.bindSentinelListeners(this.props.events);
161+
this.setState(this.generateState());
162+
}
163+
}
164+
165+
public componentWillUnmount(): void {
166+
this.unbindSentinelListeners(this.props.events);
167+
}
168+
169+
private bindSentinelListeners(events: MatrixEvent[]): void {
170+
for (const event of events) {
171+
event.on(MatrixEventEvent.SentinelUpdated, this.onEventSentinelUpdated);
172+
}
173+
}
174+
175+
private unbindSentinelListeners(events: MatrixEvent[]): void {
176+
for (const event of events) {
177+
event.on(MatrixEventEvent.SentinelUpdated, this.onEventSentinelUpdated);
178+
}
179+
}
180+
181+
private onEventSentinelUpdated = throttle(
182+
(): void => {
183+
console.log("@@ SENTINEL UPDATED");
184+
this.setState(this.generateState());
185+
},
186+
500,
187+
{ leading: true, trailing: true },
188+
);
189+
190+
public shouldComponentUpdate(nextProps: Props, nextState: State): boolean {
86191
// Update if
87192
// - The number of summarised events has changed
88193
// - or if the summary is about to toggle to become collapsed
89194
// - or if there are fewEvents, meaning the child eventTiles are shown as-is
195+
// - or if the summary members have changed
196+
// - or if the one of IUserEvents within userEvents have changed
90197
return (
91198
nextProps.events.length !== this.props.events.length ||
92199
nextProps.events.length < this.props.threshold ||
93-
nextProps.layout !== this.props.layout
200+
nextProps.layout !== this.props.layout ||
201+
arrayHasDiff(nextState.summaryMembers, this.state.summaryMembers) ||
202+
arrayHasDiff(Object.values(nextState.userEvents), Object.values(this.state.userEvents)) ||
203+
Object.keys(nextState.userEvents).length !== Object.keys(this.state.userEvents).length ||
204+
Object.keys(nextState.userEvents).some((userId) =>
205+
nextState.userEvents[userId].some((event, i) =>
206+
objectHasDiff(event, this.state.userEvents[userId]?.[i] ?? {}),
207+
),
208+
)
94209
);
95210
}
96211

@@ -492,54 +607,7 @@ export default class EventListSummary extends React.Component<
492607
}
493608

494609
public render(): React.ReactNode {
495-
const eventsToRender = this.props.events;
496-
497-
// Map user IDs to latest Avatar Member. ES6 Maps are ordered by when the key was created,
498-
// so this works perfectly for us to match event order whilst storing the latest Avatar Member
499-
const latestUserAvatarMember = new Map<string, RoomMember>();
500-
501-
// Object mapping user IDs to an array of IUserEvents
502-
const userEvents: Record<string, IUserEvents[]> = {};
503-
eventsToRender.forEach((e, index) => {
504-
const type = e.getType();
505-
506-
let userKey = e.getSender()!;
507-
if (e.isState() && type === EventType.RoomThirdPartyInvite) {
508-
userKey = e.getContent().display_name;
509-
} else if (e.isState() && type === EventType.RoomMember) {
510-
userKey = e.getStateKey()!;
511-
} else if (e.isRedacted() && e.getUnsigned()?.redacted_because) {
512-
userKey = e.getUnsigned().redacted_because!.sender;
513-
}
514-
515-
// Initialise a user's events
516-
if (!userEvents[userKey]) {
517-
userEvents[userKey] = [];
518-
}
519-
520-
let displayName = userKey;
521-
if (e.isRedacted()) {
522-
const sender = this.context?.room?.getMember(userKey);
523-
if (sender) {
524-
displayName = sender.name;
525-
latestUserAvatarMember.set(userKey, sender);
526-
}
527-
} else if (e.target && TARGET_AS_DISPLAY_NAME_EVENTS.includes(type as EventType)) {
528-
displayName = e.target.name;
529-
latestUserAvatarMember.set(userKey, e.target);
530-
} else if (e.sender && type !== EventType.RoomThirdPartyInvite) {
531-
displayName = e.sender.name;
532-
latestUserAvatarMember.set(userKey, e.sender);
533-
}
534-
535-
userEvents[userKey].push({
536-
mxEvent: e,
537-
displayName,
538-
index: index,
539-
});
540-
});
541-
542-
const aggregate = this.getAggregate(userEvents);
610+
const aggregate = this.getAggregate(this.state.userEvents);
543611

544612
// Sort types by order of lowest event index within sequence
545613
const orderedTransitionSequences = Object.keys(aggregate.names).sort(
@@ -554,7 +622,7 @@ export default class EventListSummary extends React.Component<
554622
onToggle={this.props.onToggle}
555623
startExpanded={this.props.startExpanded}
556624
children={this.props.children}
557-
summaryMembers={[...latestUserAvatarMember.values()]}
625+
summaryMembers={this.state.summaryMembers}
558626
layout={this.props.layout}
559627
summaryText={this.generateSummary(aggregate.names, orderedTransitionSequences)}
560628
/>

src/components/views/messages/TextualEvent.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
77
*/
88

99
import React from "react";
10-
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
10+
import { MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/matrix";
1111

1212
import RoomContext from "../../../contexts/RoomContext";
1313
import * as TextForEvent from "../../../TextForEvent";
@@ -21,6 +21,19 @@ export default class TextualEvent extends React.Component<IProps> {
2121
public static contextType = RoomContext;
2222
declare public context: React.ContextType<typeof RoomContext>;
2323

24+
public componentDidMount(): void {
25+
this.props.mxEvent.on(MatrixEventEvent.SentinelUpdated, this.onEventSentinelUpdated);
26+
}
27+
public componentWillUnmount(): void {
28+
this.props.mxEvent.off(MatrixEventEvent.SentinelUpdated, this.onEventSentinelUpdated);
29+
}
30+
31+
private onEventSentinelUpdated = (): void => {
32+
// XXX: this is crap, but we don't have a better way to force a re-render
33+
// Many TextForEvent handlers render parts of `event.sender` and `event.target` so ensure they are updated
34+
this.forceUpdate();
35+
};
36+
2437
public render(): React.ReactNode {
2538
const text = TextForEvent.textForEvent(
2639
this.props.mxEvent,

src/hooks/usePinnedEvents.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ async function fetchPinnedEvent(room: Room, pinnedEventId: string, cli: MatrixCl
154154
const senderUserId = event.getSender();
155155
if (senderUserId && PinningUtils.isUnpinnable(event)) {
156156
// Inject sender information
157-
event.sender = room.getMember(senderUserId);
157+
event.setMetadata(room.currentState, false);
158158
// Also inject any edits we've found
159159
if (edit) event.makeReplaced(edit);
160160

src/utils/exportUtils/Exporter.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -110,12 +110,7 @@ export default abstract class Exporter {
110110
}
111111

112112
protected setEventMetadata(event: MatrixEvent): MatrixEvent {
113-
const roomState = this.room.currentState;
114-
const sender = event.getSender();
115-
event.sender = (!!sender && roomState?.getSentinelMember(sender)) || null;
116-
if (event.getType() === "m.room.member") {
117-
event.target = roomState?.getSentinelMember(event.getStateKey()!) ?? null;
118-
}
113+
event.setMetadata(this.room.currentState, false);
119114
return event;
120115
}
121116

test/unit-tests/components/views/right_panel/__snapshots__/PinnedMessagesCard-test.tsx.snap

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ exports[`<PinnedMessagesCard /> should show two pinned messages 1`] = `
145145
data-type="round"
146146
role="presentation"
147147
style="--cpd-avatar-size: 32px;"
148+
title="@alice:example.org"
148149
>
149150
a
150151
</span>
@@ -222,6 +223,7 @@ exports[`<PinnedMessagesCard /> should show two pinned messages 1`] = `
222223
data-type="round"
223224
role="presentation"
224225
style="--cpd-avatar-size: 32px;"
226+
title="@alice:example.org"
225227
>
226228
a
227229
</span>
@@ -364,6 +366,7 @@ exports[`<PinnedMessagesCard /> unpin all should not allow to unpinall 1`] = `
364366
data-type="round"
365367
role="presentation"
366368
style="--cpd-avatar-size: 32px;"
369+
title="@alice:example.org"
367370
>
368371
a
369372
</span>
@@ -441,6 +444,7 @@ exports[`<PinnedMessagesCard /> unpin all should not allow to unpinall 1`] = `
441444
data-type="round"
442445
role="presentation"
443446
style="--cpd-avatar-size: 32px;"
447+
title="@alice:example.org"
444448
>
445449
a
446450
</span>

0 commit comments

Comments
 (0)