Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 482d756

Browse files
authored
Ensure EventListSummary key does not change during backpagination (#7915)
1 parent 115e17b commit 482d756

File tree

2 files changed

+105
-19
lines changed

2 files changed

+105
-19
lines changed

src/components/structures/MessagePanel.tsx

+29-19
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,9 @@ export default class MessagePanel extends React.Component<IProps, IState> {
254254
private readonly showTypingNotificationsWatcherRef: string;
255255
private eventTiles: Record<string, EventTile> = {};
256256

257+
// A map to allow groupers to maintain consistent keys even if their first event is uprooted due to back-pagination.
258+
public grouperKeyMap = new WeakMap<MatrixEvent, string>();
259+
257260
constructor(props, context) {
258261
super(props, context);
259262

@@ -684,14 +687,13 @@ export default class MessagePanel extends React.Component<IProps, IState> {
684687
break; // break on first grouper
685688
}
686689
}
690+
687691
if (!grouper) {
688-
const wantTile = this.shouldShowEvent(mxEv);
689-
const isGrouped = false;
690-
if (wantTile) {
692+
if (this.shouldShowEvent(mxEv)) {
691693
// make sure we unpack the array returned by getTilesForEvent,
692-
// otherwise react will auto-generate keys and we will end up
693-
// replacing all of the DOM elements every time we paginate.
694-
ret.push(...this.getTilesForEvent(prevEvent, mxEv, last, isGrouped, nextEvent, nextTile));
694+
// otherwise React will auto-generate keys, and we will end up
695+
// replacing all the DOM elements every time we paginate.
696+
ret.push(...this.getTilesForEvent(prevEvent, mxEv, last, false, nextEvent, nextTile));
695697
prevEvent = mxEv;
696698
}
697699

@@ -1127,15 +1129,15 @@ class CreationGrouper extends BaseGrouper {
11271129
if (!this.events || !this.events.length) return [];
11281130

11291131
const panel = this.panel;
1130-
const ret = [];
1132+
const ret: ReactNode[] = [];
11311133
const isGrouped = true;
11321134
const createEvent = this.event;
11331135
const lastShownEvent = this.lastShownEvent;
11341136

11351137
if (panel.wantsDateSeparator(this.prevEvent, createEvent.getDate())) {
11361138
const ts = createEvent.getTs();
11371139
ret.push(
1138-
<li key={ts+'~'}><DateSeparator key={ts+'~'} roomId={createEvent.getRoomId()} ts={ts} /></li>,
1140+
<li key={ts+'~'}><DateSeparator roomId={createEvent.getRoomId()} ts={ts} /></li>,
11391141
);
11401142
}
11411143

@@ -1262,6 +1264,10 @@ class MainGrouper extends BaseGrouper {
12621264
this.events.push(ev);
12631265
}
12641266

1267+
private generateKey(): string {
1268+
return "eventlistsummary-" + this.events[0].getId();
1269+
}
1270+
12651271
public getTiles(): ReactNode[] {
12661272
// If we don't have any events to group, don't even try to group them. The logic
12671273
// below assumes that we have a group of events to deal with, but we might not if
@@ -1271,24 +1277,28 @@ class MainGrouper extends BaseGrouper {
12711277
const isGrouped = true;
12721278
const panel = this.panel;
12731279
const lastShownEvent = this.lastShownEvent;
1274-
const ret = [];
1280+
const ret: ReactNode[] = [];
12751281

12761282
if (panel.wantsDateSeparator(this.prevEvent, this.events[0].getDate())) {
12771283
const ts = this.events[0].getTs();
12781284
ret.push(
1279-
<li key={ts+'~'}><DateSeparator key={ts+'~'} roomId={this.events[0].getRoomId()} ts={ts} /></li>,
1285+
<li key={ts+'~'}><DateSeparator roomId={this.events[0].getRoomId()} ts={ts} /></li>,
12801286
);
12811287
}
12821288

1283-
// Ensure that the key of the EventListSummary does not change with new events.
1284-
// This will prevent it from being re-created unnecessarily, and
1285-
// instead will allow new props to be provided. In turn, the shouldComponentUpdate
1286-
// method on ELS can be used to prevent unnecessary renderings.
1287-
//
1288-
// Whilst back-paginating with an ELS at the top of the panel, prevEvent will be null,
1289-
// so use the key "eventlistsummary-initial". Otherwise, use the ID of the first
1290-
// membership event, which will not change during forward pagination.
1291-
const key = "eventlistsummary-" + (this.prevEvent ? this.events[0].getId() : "initial");
1289+
// Ensure that the key of the EventListSummary does not change with new events in either direction.
1290+
// This will prevent it from being re-created unnecessarily, and instead will allow new props to be provided.
1291+
// In turn, the shouldComponentUpdate method on ELS can be used to prevent unnecessary renderings.
1292+
const keyEvent = this.events.find(e => this.panel.grouperKeyMap.get(e));
1293+
const key = keyEvent ? this.panel.grouperKeyMap.get(keyEvent) : this.generateKey();
1294+
if (!keyEvent) {
1295+
// Populate the weak map with the key that we are using for all related events.
1296+
this.events.forEach(e => {
1297+
if (!this.panel.grouperKeyMap.has(e)) {
1298+
this.panel.grouperKeyMap.set(e, key);
1299+
}
1300+
});
1301+
}
12921302

12931303
let highlightInSummary = false;
12941304
let eventTiles = this.events.map((e, i) => {

test/components/structures/MessagePanel-test.js

+76
Original file line numberDiff line numberDiff line change
@@ -496,4 +496,80 @@ describe('MessagePanel', function() {
496496

497497
expect(Dates.length).toEqual(1);
498498
});
499+
500+
it('appends events into summaries during forward pagination without changing key', () => {
501+
const events = mkMelsEvents().slice(1, 11);
502+
503+
const res = mount(<WrappedMessagePanel events={events} />);
504+
let els = res.find("EventListSummary");
505+
expect(els.length).toEqual(1);
506+
expect(els.key()).toEqual("eventlistsummary-" + events[0].getId());
507+
expect(els.prop("events").length).toEqual(10);
508+
509+
res.setProps({
510+
events: [
511+
...events,
512+
TestUtilsMatrix.mkMembership({
513+
event: true,
514+
room: "!room:id",
515+
user: "@user:id",
516+
target: {
517+
userId: "@user:id",
518+
name: "Bob",
519+
getAvatarUrl: () => {
520+
return "avatar.jpeg";
521+
},
522+
getMxcAvatarUrl: () => 'mxc://avatar.url/image.png',
523+
},
524+
ts: Date.now(),
525+
mship: 'join',
526+
prevMship: 'join',
527+
name: 'A user',
528+
}),
529+
],
530+
});
531+
532+
els = res.find("EventListSummary");
533+
expect(els.length).toEqual(1);
534+
expect(els.key()).toEqual("eventlistsummary-" + events[0].getId());
535+
expect(els.prop("events").length).toEqual(11);
536+
});
537+
538+
it('prepends events into summaries during backward pagination without changing key', () => {
539+
const events = mkMelsEvents().slice(1, 11);
540+
541+
const res = mount(<WrappedMessagePanel events={events} />);
542+
let els = res.find("EventListSummary");
543+
expect(els.length).toEqual(1);
544+
expect(els.key()).toEqual("eventlistsummary-" + events[0].getId());
545+
expect(els.prop("events").length).toEqual(10);
546+
547+
res.setProps({
548+
events: [
549+
TestUtilsMatrix.mkMembership({
550+
event: true,
551+
room: "!room:id",
552+
user: "@user:id",
553+
target: {
554+
userId: "@user:id",
555+
name: "Bob",
556+
getAvatarUrl: () => {
557+
return "avatar.jpeg";
558+
},
559+
getMxcAvatarUrl: () => 'mxc://avatar.url/image.png',
560+
},
561+
ts: Date.now(),
562+
mship: 'join',
563+
prevMship: 'join',
564+
name: 'A user',
565+
}),
566+
...events,
567+
],
568+
});
569+
570+
els = res.find("EventListSummary");
571+
expect(els.length).toEqual(1);
572+
expect(els.key()).toEqual("eventlistsummary-" + events[0].getId());
573+
expect(els.prop("events").length).toEqual(11);
574+
});
499575
});

0 commit comments

Comments
 (0)