diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js
index a13278cf68..4f4136ed66 100644
--- a/src/components/structures/MessagePanel.js
+++ b/src/components/structures/MessagePanel.js
@@ -403,8 +403,6 @@ export default class MessagePanel extends React.Component {
_getEventTiles() {
const DateSeparator = sdk.getComponent('messages.DateSeparator');
- const EventListSummary = sdk.getComponent('views.elements.EventListSummary');
- const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary');
this.eventNodes = {};
@@ -447,199 +445,48 @@ export default class MessagePanel extends React.Component {
this._readReceiptsByEvent = this._getReadReceiptsByShownEvent();
}
+ let grouper = null;
+
for (i = 0; i < this.props.events.length; i++) {
const mxEv = this.props.events[i];
const eventId = mxEv.getId();
const last = (mxEv === lastShownEvent);
- // Wrap initial room creation events into an EventListSummary
- // Grouping only events sent by the same user that sent the `m.room.create` and only until
- // the first non-state event or membership event which is not regarding the sender of the `m.room.create` event
- const shouldGroup = (ev) => {
- if (ev.getType() === "m.room.member"
- && (ev.getStateKey() !== mxEv.getSender() || ev.getContent()["membership"] !== "join")) {
- return false;
- }
- if (ev.isState() && ev.getSender() === mxEv.getSender()) {
- return true;
- }
- return false;
- };
- // events that we include in the group but then eject out and place
- // above the group.
- const shouldEject = (ev) => {
- if (ev.getType() === "m.room.encryption") return true;
- return false;
- };
- if (mxEv.getType() === "m.room.create") {
- let summaryReadMarker = null;
- const ts1 = mxEv.getTs();
-
- if (this._wantsDateSeparator(prevEvent, mxEv.getDate())) {
- const dateSeparator =
;
- ret.push(dateSeparator);
+ if (grouper) {
+ if (grouper.shouldGroup(mxEv)) {
+ grouper.add(mxEv);
+ continue;
+ } else {
+ // not part of group, so get the group tiles, close the
+ // group, and continue like a normal event
+ ret.push(...grouper.getTiles());
+ prevEvent = grouper.getNewPrevEvent();
+ grouper = null;
}
-
- // If RM event is the first in the summary, append the RM after the summary
- summaryReadMarker = summaryReadMarker || this._readMarkerForEvent(mxEv.getId());
-
- // If this m.room.create event should be shown (room upgrade) then show it before the summary
- if (this._shouldShowEvent(mxEv)) {
- // pass in the mxEv as prevEvent as well so no extra DateSeparator is rendered
- ret.push(...this._getTilesForEvent(mxEv, mxEv, false));
- }
-
- const summarisedEvents = []; // Don't add m.room.create here as we don't want it inside the summary
- const ejectedEvents = [];
- for (;i + 1 < this.props.events.length; i++) {
- const collapsedMxEv = this.props.events[i + 1];
-
- // Ignore redacted/hidden member events
- if (!this._shouldShowEvent(collapsedMxEv)) {
- // If this hidden event is the RM and in or at end of a summary put RM after the summary.
- summaryReadMarker = summaryReadMarker || this._readMarkerForEvent(collapsedMxEv.getId());
- continue;
- }
-
- if (!shouldGroup(collapsedMxEv) || this._wantsDateSeparator(mxEv, collapsedMxEv.getDate())) {
- break;
- }
-
- // If RM event is in the summary, mark it as such and the RM will be appended after the summary.
- summaryReadMarker = summaryReadMarker || this._readMarkerForEvent(collapsedMxEv.getId());
-
- if (shouldEject(collapsedMxEv)) {
- ejectedEvents.push(collapsedMxEv);
- } else {
- summarisedEvents.push(collapsedMxEv);
- }
- }
-
- // At this point, i = the index of the last event in the summary sequence
- const eventTiles = summarisedEvents.map((e) => {
- // In order to prevent DateSeparators from appearing in the expanded form
- // of EventListSummary, render each member event as if the previous
- // one was itself. This way, the timestamp of the previous event === the
- // timestamp of the current event, and no DateSeparator is inserted.
- return this._getTilesForEvent(e, e, e === lastShownEvent);
- }).reduce((a, b) => a.concat(b), []);
-
- for (const ejected of ejectedEvents) {
- ret.push(...this._getTilesForEvent(mxEv, ejected, last));
- }
-
- // Get sender profile from the latest event in the summary as the m.room.create doesn't contain one
- const ev = this.props.events[i];
- ret.push(
- { eventTiles }
- );
-
- if (summaryReadMarker) {
- ret.push(summaryReadMarker);
- }
-
- prevEvent = mxEv;
- continue;
}
- const wantTile = this._shouldShowEvent(mxEv);
-
- // Wrap consecutive member events in a ListSummary, ignore if redacted
- if (isMembershipChange(mxEv) && wantTile) {
- let summaryReadMarker = null;
- const ts1 = mxEv.getTs();
- // Ensure that the key of the MemberEventListSummary does not change with new
- // member events. This will prevent it from being re-created unnecessarily, and
- // instead will allow new props to be provided. In turn, the shouldComponentUpdate
- // method on MELS can be used to prevent unnecessary renderings.
- //
- // Whilst back-paginating with a MELS at the top of the panel, prevEvent will be null,
- // so use the key "membereventlistsummary-initial". Otherwise, use the ID of the first
- // membership event, which will not change during forward pagination.
- const key = "membereventlistsummary-" + (prevEvent ? mxEv.getId() : "initial");
-
- if (this._wantsDateSeparator(prevEvent, mxEv.getDate())) {
- const dateSeparator = ;
- ret.push(dateSeparator);
+ for (const Grouper of groupers) {
+ if (Grouper.canStartGroup(this, mxEv)) {
+ grouper = new Grouper(this, mxEv, prevEvent, lastShownEvent);
}
-
- // If RM event is the first in the MELS, append the RM after MELS
- summaryReadMarker = summaryReadMarker || this._readMarkerForEvent(mxEv.getId());
-
- const summarisedEvents = [mxEv];
- for (;i + 1 < this.props.events.length; i++) {
- const collapsedMxEv = this.props.events[i + 1];
-
- // Ignore redacted/hidden member events
- if (!this._shouldShowEvent(collapsedMxEv)) {
- // If this hidden event is the RM and in or at end of a MELS put RM after MELS.
- summaryReadMarker = summaryReadMarker || this._readMarkerForEvent(collapsedMxEv.getId());
- continue;
- }
-
- if (!isMembershipChange(collapsedMxEv) ||
- this._wantsDateSeparator(mxEv, collapsedMxEv.getDate())) {
- break;
- }
-
- // If RM event is in MELS mark it as such and the RM will be appended after MELS.
- summaryReadMarker = summaryReadMarker || this._readMarkerForEvent(collapsedMxEv.getId());
-
- summarisedEvents.push(collapsedMxEv);
- }
-
- let highlightInMels = false;
-
- // At this point, i = the index of the last event in the summary sequence
- let eventTiles = summarisedEvents.map((e) => {
- if (e.getId() === this.props.highlightedEventId) {
- highlightInMels = true;
- }
- // In order to prevent DateSeparators from appearing in the expanded form
- // of MemberEventListSummary, render each member event as if the previous
- // one was itself. This way, the timestamp of the previous event === the
- // timestamp of the current event, and no DateSeparator is inserted.
- return this._getTilesForEvent(e, e, e === lastShownEvent);
- }).reduce((a, b) => a.concat(b), []);
-
- if (eventTiles.length === 0) {
- eventTiles = null;
- }
-
- ret.push(
- { eventTiles }
- );
-
- if (summaryReadMarker) {
- ret.push(summaryReadMarker);
- }
-
- prevEvent = mxEv;
- continue;
}
+ if (!grouper) {
+ const wantTile = this._shouldShowEvent(mxEv);
+ if (wantTile) {
+ // make sure we unpack the array returned by _getTilesForEvent,
+ // otherwise react will auto-generate keys and we will end up
+ // replacing all of the DOM elements every time we paginate.
+ ret.push(...this._getTilesForEvent(prevEvent, mxEv, last));
+ prevEvent = mxEv;
+ }
- if (wantTile) {
- // make sure we unpack the array returned by _getTilesForEvent,
- // otherwise react will auto-generate keys and we will end up
- // replacing all of the DOM elements every time we paginate.
- ret.push(...this._getTilesForEvent(prevEvent, mxEv, last));
- prevEvent = mxEv;
+ const readMarker = this._readMarkerForEvent(eventId, i >= lastShownNonLocalEchoIndex);
+ if (readMarker) ret.push(readMarker);
}
+ }
- const readMarker = this._readMarkerForEvent(eventId, i >= lastShownNonLocalEchoIndex);
- if (readMarker) ret.push(readMarker);
+ if (grouper) {
+ ret.push(...grouper.getTiles());
}
return ret;
@@ -950,3 +797,222 @@ export default class MessagePanel extends React.Component {
);
}
}
+
+/* Grouper classes determine when events can be grouped together in a summary.
+ * Groupers should have the following methods:
+ * - canStartGroup (static): determines if a new group should be started with the
+ * given event
+ * - shouldGroup: determines if the given event should be added to an existing group
+ * - add: adds an event to an existing group (should only be called if shouldGroup
+ * return true)
+ * - getTiles: returns the tiles that represent the group
+ * - getNewPrevEvent: returns the event that should be used as the new prevEvent
+ * when determining things such as whether a date separator is necessary
+ */
+
+// Wrap initial room creation events into an EventListSummary
+// Grouping only events sent by the same user that sent the `m.room.create` and only until
+// the first non-state event or membership event which is not regarding the sender of the `m.room.create` event
+class CreationGrouper {
+ static canStartGroup = function (panel, ev) {
+ return ev.getType() === "m.room.create";
+ };
+
+ constructor(panel, createEvent, prevEvent, lastShownEvent) {
+ this.panel = panel;
+ this.createEvent = createEvent;
+ this.prevEvent = prevEvent;
+ this.lastShownEvent = lastShownEvent;
+ this.events = [];
+ // events that we include in the group but then eject out and place
+ // above the group.
+ this.ejectedEvents = [];
+ this.readMarker = panel._readMarkerForEvent(createEvent.getId());
+ }
+
+ shouldGroup(ev) {
+ const panel = this.panel;
+ const createEvent = this.createEvent;
+ if (!panel._shouldShowEvent(ev)) {
+ this.readMarker = this.readMarker || panel._readMarkerForEvent(ev.getId());
+ return true;
+ }
+ if (panel._wantsDateSeparator(this.createEvent, ev.getDate())) {
+ return false;
+ }
+ if (ev.getType() === "m.room.member"
+ && (ev.getStateKey() !== createEvent.getSender() || ev.getContent()["membership"] !== "join")) {
+ return false;
+ }
+ if (ev.isState() && ev.getSender() === createEvent.getSender()) {
+ return true;
+ }
+ return false;
+ }
+
+ add(ev) {
+ const panel = this.panel;
+ this.readMarker = this.readMarker || panel._readMarkerForEvent(ev.getId());
+ if (!panel._shouldShowEvent(ev)) {
+ return;
+ }
+ if (ev.getType() === "m.room.encryption") {
+ this.ejectedEvents.push(ev);
+ } else {
+ this.events.push(ev);
+ }
+ }
+
+ getTiles() {
+ const DateSeparator = sdk.getComponent('messages.DateSeparator');
+ const EventListSummary = sdk.getComponent('views.elements.EventListSummary');
+
+ const panel = this.panel;
+ const ret = [];
+ const createEvent = this.createEvent;
+ const lastShownEvent = this.lastShownEvent;
+
+ if (panel._wantsDateSeparator(this.prevEvent, createEvent.getDate())) {
+ const ts = createEvent.getTs();
+ ret.push(
+
+ )
+ }
+
+ // If this m.room.create event should be shown (room upgrade) then show it before the summary
+ if (panel._shouldShowEvent(createEvent)) {
+ // pass in the createEvent as prevEvent as well so no extra DateSeparator is rendered
+ ret.push(...panel._getTilesForEvent(createEvent, createEvent, false));
+ }
+
+ for (const ejected of this.ejectedEvents) {
+ ret.push(...panel._getTilesForEvent(
+ createEvent, ejected, createEvent === lastShownEvent
+ ));
+ }
+
+ const eventTiles = this.events.map((e) => {
+ // In order to prevent DateSeparators from appearing in the expanded form
+ // of EventListSummary, render each member event as if the previous
+ // one was itself. This way, the timestamp of the previous event === the
+ // timestamp of the current event, and no DateSeparator is inserted.
+ return panel._getTilesForEvent(e, e, e === lastShownEvent);
+ }).reduce((a, b) => a.concat(b), []);
+ // Get sender profile from the latest event in the summary as the m.room.create doesn't contain one
+ const ev = this.events[this.events.length - 1];
+ ret.push(
+
+ { eventTiles }
+
+ );
+
+ if (this.readMarker) {
+ ret.push(readMarker);
+ }
+
+ return ret;
+ }
+
+ getNewPrevEvent() {
+ return this.createEvent;
+ }
+}
+
+// Wrap consecutive member events in a ListSummary, ignore if redacted
+class MemberGrouper {
+ static canStartGroup = function (panel, ev) {
+ return panel._shouldShowEvent(ev) && isMembershipChange(ev);
+ }
+
+ constructor(panel, ev, prevEvent, lastShownEvent) {
+ this.panel = panel;
+ this.readMarker = panel._readMarkerForEvent(ev.getId());
+ this.events = [ev];
+ this.prevEvent = prevEvent;
+ this.lastShownEvent = lastShownEvent;
+ }
+
+ shouldGroup(ev) {
+ return isMembershipChange(ev);
+ }
+
+ add(ev) {
+ this.readMarker = this.readMarker || this.panel._readMarkerForEvent(ev.getId());
+ this.events.push(ev);
+ }
+
+ getTiles() {
+ const DateSeparator = sdk.getComponent('messages.DateSeparator');
+ const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary');
+
+ const panel = this.panel;
+ const lastShownEvent = lastShownEvent;
+ const ret = [];
+
+ if (panel._wantsDateSeparator(this.prevEvent, this.events[0].getDate())) {
+ const ts = this.events[0].getTs();
+ ret.push(
+
+ );
+ }
+
+ // Ensure that the key of the MemberEventListSummary does not change with new
+ // member events. This will prevent it from being re-created unnecessarily, and
+ // instead will allow new props to be provided. In turn, the shouldComponentUpdate
+ // method on MELS can be used to prevent unnecessary renderings.
+ //
+ // Whilst back-paginating with a MELS at the top of the panel, prevEvent will be null,
+ // so use the key "membereventlistsummary-initial". Otherwise, use the ID of the first
+ // membership event, which will not change during forward pagination.
+ const key = "membereventlistsummary-" + (
+ this.prevEvent ? this.events[0].getId() : "initial"
+ );
+
+ let highlightInMels;
+ let eventTiles = this.events.map((e) => {
+ if (e.getId() === panel.props.highlightedEventId) {
+ highlightInMels = true;
+ }
+ // In order to prevent DateSeparators from appearing in the expanded form
+ // of MemberEventListSummary, render each member event as if the previous
+ // one was itself. This way, the timestamp of the previous event === the
+ // timestamp of the current event, and no DateSeparator is inserted.
+ return panel._getTilesForEvent(e, e, e === lastShownEvent);
+ }).reduce((a, b) => a.concat(b), []);
+
+ if (eventTiles.length === 0) {
+ eventTiles = null;
+ }
+
+ ret.push(
+
+ { eventTiles }
+
+ );
+
+ if (this.readMarker) {
+ ret.push(this.readMarker);
+ }
+
+ return ret;
+ }
+
+ getNewPrevEvent() {
+ return this.events[0];
+ }
+}
+
+// all the grouper classes that we use
+const groupers = [CreationGrouper, MemberGrouper];