Skip to content

Commit

Permalink
Update relation collections after redaction
Browse files Browse the repository at this point in the history
This watches for redactions of relations and updates the relations collection
to match, including various aggregations. In addition, a redaction event is
emitted on the redaction collection to notify consumers of the change.

Part of element-hq/element-web#9574
Part of element-hq/element-web#9485
  • Loading branch information
jryans committed May 10, 2019
1 parent 00e4236 commit 4c8321d
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 11 deletions.
1 change: 1 addition & 0 deletions src/models/event-timeline-set.js
Original file line number Diff line number Diff line change
Expand Up @@ -744,6 +744,7 @@ EventTimelineSet.prototype._aggregateRelations = function(event) {
relationsWithEventType = relationsWithRelType[eventType] = new Relations(
relationType,
eventType,
this.room,
);
}

Expand Down
76 changes: 65 additions & 11 deletions src/models/relations.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
}
}

/**
Expand Down Expand Up @@ -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.
Expand All @@ -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) {
Expand All @@ -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();
Expand All @@ -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.
Expand All @@ -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") {
Expand Down
1 change: 1 addition & 0 deletions src/models/room.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down

0 comments on commit 4c8321d

Please sign in to comment.