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(