diff --git a/src/models/event-timeline-set.js b/src/models/event-timeline-set.js index a64201da8..150adfed4 100644 --- a/src/models/event-timeline-set.js +++ b/src/models/event-timeline-set.js @@ -744,6 +744,7 @@ EventTimelineSet.prototype._aggregateRelations = function(event) { relationsWithEventType = relationsWithRelType[eventType] = new Relations( relationType, eventType, + this.room, ); } diff --git a/src/models/relations.js b/src/models/relations.js index 16f02cb26..c19a245d4 100644 --- a/src/models/relations.js +++ b/src/models/relations.js @@ -14,6 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +import EventEmitter from 'events'; + /** * A container for relation events that supports easy access to common ways of * aggregating such events. Each instance holds events that of a single relation @@ -22,21 +24,29 @@ limitations under the License. * The typical way to get one of these containers is via * EventTimelineSet#getRelationsForEvent. */ -export default class Relations { +export default class Relations extends EventEmitter { /** * @param {String} relationType * The type of relation involved, such as "m.annotation", "m.reference", * "m.replace", etc. * @param {String} eventType * The relation event's type, such as "m.reaction", etc. + * @param {?Room} room + * Room for this container. May be null for non-room cases, such as the + * notification timeline. */ - constructor(relationType, eventType) { + constructor(relationType, eventType, room) { + super(); this.relationType = relationType; this.eventType = eventType; - this._events = []; + this._relations = new Set(); this._annotationsByKey = {}; this._annotationsBySender = {}; this._sortedAnnotationsByKey = []; + + if (room) { + room.on("Room.beforeRedaction", this._onBeforeRedaction); + } } /** @@ -66,11 +76,11 @@ export default class Relations { this._aggregateAnnotation(key, event); } - this._events.push(event); + this._relations.add(event); } /** - * Get all events in this collection. + * Get all relation events in this collection. * * These are currently in the order of insertion to this collection, which * won't match timeline order in the case of scrollback. @@ -79,8 +89,8 @@ export default class Relations { * @return {Array} * Relation events in insertion order. */ - getEvents() { - return this._events; + getRelations() { + return [...this._relations]; } _aggregateAnnotation(key, event) { @@ -90,16 +100,16 @@ export default class Relations { let eventsForKey = this._annotationsByKey[key]; if (!eventsForKey) { - eventsForKey = this._annotationsByKey[key] = []; + eventsForKey = this._annotationsByKey[key] = new Set(); this._sortedAnnotationsByKey.push([key, eventsForKey]); } - // Add the new event to the list for this key - eventsForKey.push(event); + // Add the new event to the set for this key + eventsForKey.add(event); // Re-sort the [key, events] pairs in descending order of event count this._sortedAnnotationsByKey.sort((a, b) => { const aEvents = a[1]; const bEvents = b[1]; - return bEvents.length - aEvents.length; + return bEvents.size - aEvents.size; }); const sender = event.getSender(); @@ -111,6 +121,49 @@ export default class Relations { eventsFromSender.push(event); } + /** + * For relations that are about to be redacted, remove them from aggregation + * data sets and emit an update event. + * + * @param {MatrixEvent} redactedEvent + * The original relation event that is about to be redacted. + */ + _onBeforeRedaction = (redactedEvent) => { + if (!this._relations.has(redactedEvent)) { + return; + } + + if (this.relationType === "m.annotation") { + // Remove the redacted annotation from aggregation by key + const content = redactedEvent.getContent(); + const relation = content && content["m.relates_to"]; + if (!relation) { + return; + } + + const key = relation.key; + const eventsForKey = this._annotationsByKey[key]; + if (!eventsForKey) { + return; + } + eventsForKey.delete(redactedEvent); + + // Re-sort the [key, events] pairs in descending order of event count + this._sortedAnnotationsByKey.sort((a, b) => { + const aEvents = a[1]; + const bEvents = b[1]; + return bEvents.size - aEvents.size; + }); + } + + // Dispatch a redaction event on this collection. `setTimeout` is used + // to wait until the next event loop iteration by which time the event + // has actually been marked as redacted. + setTimeout(() => { + this.emit("Relations.redaction"); + }, 0); + } + /** * Get all events in this collection grouped by key and sorted by descending * event count in each group. @@ -119,6 +172,7 @@ export default class Relations { * * @return {Array} * An array of [key, events] pairs sorted by descending event count. + * The events are stored in a Set (which preserves insertion order). */ getSortedAnnotationsByKey() { if (this.relationType !== "m.annotation") { diff --git a/src/models/room.js b/src/models/room.js index d026c673d..e50edf07b 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -1011,6 +1011,7 @@ Room.prototype._addLiveEvent = function(event, duplicateStrategy) { // if we know about this event, redact its contents now. const redactedEvent = this.getUnfilteredTimelineSet().findEventById(redactId); if (redactedEvent) { + this.emit("Room.beforeRedaction", redactedEvent, event, this); redactedEvent.makeRedacted(event); this.emit("Room.redaction", event, this);