diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 5a11ce8ab9..698768067a 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -54,6 +54,10 @@ module.exports = React.createClass({ // ID of an event to highlight. If undefined, no event will be highlighted. highlightedEventId: PropTypes.string, + // The room these events are all in together, if any. + // (The notification panel won't have a room here, for example.) + room: PropTypes.object, + // Should we show URL Previews showUrlPreview: PropTypes.bool, @@ -117,10 +121,48 @@ module.exports = React.createClass({ // to manage its animations this._readReceiptMap = {}; + // Track read receipts by event ID. For each _shown_ event ID, we store + // the list of read receipts to display: + // [ + // { + // userId: string, + // member: RoomMember, + // ts: number, + // }, + // ] + // This is recomputed on each render. It's only stored on the component + // for ease of passing the data around since it's computed in one pass + // over all events. + this._readReceiptsByEvent = {}; + + // Track read receipts by user ID. For each user ID we've ever shown a + // a read receipt for, we store an object: + // { + // lastShownEventId: string, + // receipt: { + // userId: string, + // member: RoomMember, + // ts: number, + // }, + // } + // so that we can always keep receipts displayed by reverting back to + // the last shown event for that user ID when needed. This may feel like + // it duplicates the receipt storage in the room, but at this layer, we + // are tracking _shown_ event IDs, which the JS SDK knows nothing about. + // This is recomputed on each render, using the data from the previous + // render as our fallback for any user IDs we can't match a receipt to a + // displayed event in the current render cycle. + this._readReceiptsByUserId = {}; + // Remember the read marker ghost node so we can do the cleanup that // Velocity requires this._readMarkerGhostNode = null; + // Cache hidden events setting on mount since Settings is expensive to + // query, and we check this in a hot code path. + this._showHiddenEventsInTimeline = + SettingsStore.getValue("showHiddenEventsInTimeline"); + this._isMounted = true; }, @@ -261,7 +303,7 @@ module.exports = React.createClass({ return false; // ignored = no show (only happens if the ignore happens after an event was received) } - if (SettingsStore.getValue("showHiddenEventsInTimeline")) { + if (this._showHiddenEventsInTimeline) { return true; } @@ -327,6 +369,11 @@ module.exports = React.createClass({ this.currentGhostEventId = null; } + this._readReceiptsByEvent = {}; + if (this.props.showReadReceipts) { + this._readReceiptsByEvent = this._getReadReceiptsByShownEvent(); + } + const isMembershipChange = (e) => e.getType() === 'm.room.member'; for (i = 0; i < this.props.events.length; i++) { @@ -527,10 +574,8 @@ module.exports = React.createClass({ // Local echos have a send "status". const scrollToken = mxEv.status ? undefined : eventId; - let readReceipts; - if (this.props.showReadReceipts) { - readReceipts = this._getReadReceiptsForEvent(mxEv); - } + const readReceipts = this._readReceiptsByEvent[eventId]; + ret.push(
  • { - return r2.ts - r1.ts; - }); + // Get an object that maps from event ID to a list of read receipts that + // should be shown next to that event. If a hidden event has read receipts, + // they are folded into the receipts of the last shown event. + _getReadReceiptsByShownEvent: function() { + const receiptsByEvent = {}; + const receiptsByUserId = {}; + + let lastShownEventId; + for (const event of this.props.events) { + if (this._shouldShowEvent(event)) { + lastShownEventId = event.getId(); + } + if (!lastShownEventId) { + continue; + } + + const existingReceipts = receiptsByEvent[lastShownEventId] || []; + const newReceipts = this._getReadReceiptsForEvent(event); + receiptsByEvent[lastShownEventId] = existingReceipts.concat(newReceipts); + + // Record these receipts along with their last shown event ID for + // each associated user ID. + for (const receipt of newReceipts) { + receiptsByUserId[receipt.userId] = { + lastShownEventId, + receipt, + }; + } + } + + // It's possible in some cases (for example, when a read receipt + // advances before we have paginated in the new event that it's marking + // received) that we can temporarily not have a matching event for + // someone which had one in the last. By looking through our previous + // mapping of receipts by user ID, we can cover recover any receipts + // that would have been lost by using the same event ID from last time. + for (const userId in this._readReceiptsByUserId) { + if (receiptsByUserId[userId]) { + continue; + } + const { lastShownEventId, receipt } = this._readReceiptsByUserId[userId]; + const existingReceipts = receiptsByEvent[lastShownEventId] || []; + receiptsByEvent[lastShownEventId] = existingReceipts.concat(receipt); + receiptsByUserId[userId] = { lastShownEventId, receipt }; + } + this._readReceiptsByUserId = receiptsByUserId; + + // After grouping receipts by shown events, do another pass to sort each + // receipt list. + for (const eventId in receiptsByEvent) { + receiptsByEvent[eventId].sort((r1, r2) => { + return r2.ts - r1.ts; + }); + } + + return receiptsByEvent; }, _getReadMarkerTile: function(visible) {