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];