You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-11-28 05:03:59 +03:00
Add basic read path for relations
This adds a read path for relations (gated behind an unstable option). A few basic client-side grouping and sorting operations are supported. Consumers are expected to ask the `EventTimelineSet` for a relation container when desired.
This commit is contained in:
@@ -148,6 +148,11 @@ function keyFromRecoverySession(session, decryptionKey) {
|
||||
* maintain support for back-paginating the live timeline after a '/sync'
|
||||
* result with a gap.
|
||||
*
|
||||
* @param {boolean} [opts.unstableClientRelationAggregation = false]
|
||||
* Optional. Set to true to enable client-side aggregation of event relations
|
||||
* via `EventTimelineSet#getRelationsForEvent`.
|
||||
* This feature is currently unstable and the API may change without notice.
|
||||
*
|
||||
* @param {Array} [opts.verificationMethods] Optional. The verification method
|
||||
* that the application can handle. Each element should be an item from {@link
|
||||
* module:crypto~verificationMethods verificationMethods}, or a class that
|
||||
@@ -213,6 +218,7 @@ function MatrixClient(opts) {
|
||||
this.timelineSupport = Boolean(opts.timelineSupport);
|
||||
this.urlPreviewCache = {};
|
||||
this._notifTimelineSet = null;
|
||||
this.unstableClientRelationAggregation = !!opts.unstableClientRelationAggregation;
|
||||
|
||||
this._crypto = null;
|
||||
this._cryptoStore = opts.cryptoStore;
|
||||
|
||||
@@ -20,6 +20,7 @@ limitations under the License.
|
||||
const EventEmitter = require("events").EventEmitter;
|
||||
const utils = require("../utils");
|
||||
const EventTimeline = require("./event-timeline");
|
||||
import Relations from './relations';
|
||||
|
||||
// var DEBUG = false;
|
||||
const DEBUG = true;
|
||||
@@ -54,22 +55,38 @@ if (DEBUG) {
|
||||
* map from event_id to timeline and index.
|
||||
*
|
||||
* @constructor
|
||||
* @param {?Room} room the optional room for this timelineSet
|
||||
* @param {Object} opts hash of options inherited from Room.
|
||||
* opts.timelineSupport gives whether timeline support is enabled
|
||||
* opts.filter is the filter object, if any, for this timelineSet.
|
||||
* @param {?Room} room
|
||||
* Room for this timelineSet. May be null for non-room cases, such as the
|
||||
* notification timeline.
|
||||
* @param {Object} opts Options inherited from Room.
|
||||
*
|
||||
* @param {boolean} [opts.timelineSupport = false]
|
||||
* Set to true to enable improved timeline support.
|
||||
* @param {Object} [opts.filter = null]
|
||||
* The filter object, if any, for this timelineSet.
|
||||
* @param {boolean} [opts.unstableClientRelationAggregation = false]
|
||||
* Optional. Set to true to enable client-side aggregation of event relations
|
||||
* via `getRelationsForEvent`.
|
||||
* This feature is currently unstable and the API may change without notice.
|
||||
*/
|
||||
function EventTimelineSet(room, opts) {
|
||||
this.room = room;
|
||||
|
||||
this._timelineSupport = Boolean(opts.timelineSupport);
|
||||
this._liveTimeline = new EventTimeline(this);
|
||||
this._unstableClientRelationAggregation = !!opts.unstableClientRelationAggregation;
|
||||
|
||||
// just a list - *not* ordered.
|
||||
this._timelines = [this._liveTimeline];
|
||||
this._eventIdToTimeline = {};
|
||||
|
||||
this._filter = opts.filter || null;
|
||||
|
||||
if (this._unstableClientRelationAggregation) {
|
||||
// A tree of objects to access a set of relations for an event, as in:
|
||||
// this._relations[relatesToEventId][relationType][relationEventType]
|
||||
this._relations = {};
|
||||
}
|
||||
}
|
||||
utils.inherits(EventTimelineSet, EventEmitter);
|
||||
|
||||
@@ -523,6 +540,8 @@ EventTimelineSet.prototype.addEventToTimeline = function(event, timeline,
|
||||
timeline.addEvent(event, toStartOfTimeline);
|
||||
this._eventIdToTimeline[eventId] = timeline;
|
||||
|
||||
this._aggregateRelations(event);
|
||||
|
||||
const data = {
|
||||
timeline: timeline,
|
||||
liveEvent: !toStartOfTimeline && timeline == this._liveTimeline,
|
||||
@@ -657,6 +676,80 @@ EventTimelineSet.prototype.compareEventOrdering = function(eventId1, eventId2) {
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a collection of relations to a given event in this timeline set.
|
||||
*
|
||||
* @param {String} eventId
|
||||
* The ID of the event that you'd like to access relation events for.
|
||||
* For example, with annotations, this would be the ID of the event being annotated.
|
||||
* @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.
|
||||
*
|
||||
* @returns {Relations}
|
||||
* A container for relation events.
|
||||
*/
|
||||
EventTimelineSet.prototype.getRelationsForEvent = function(
|
||||
eventId, relationType, eventType,
|
||||
) {
|
||||
if (!this._unstableClientRelationAggregation) {
|
||||
throw new Error("Client-side relation aggregation is disabled");
|
||||
}
|
||||
|
||||
if (!eventId || !relationType || !eventType) {
|
||||
throw new Error("Invalid arguments for `getRelationsForEvent`");
|
||||
}
|
||||
|
||||
// debuglog("Getting relations for: ", eventId, relationType, eventType);
|
||||
|
||||
const relationsForEvent = this._relations[eventId] || {};
|
||||
const relationsWithRelType = relationsForEvent[relationType] || {};
|
||||
return relationsWithRelType[eventType];
|
||||
};
|
||||
|
||||
/**
|
||||
* Add relation events to the relevant relation collection.
|
||||
*
|
||||
* @param {MatrixEvent} event
|
||||
* The new relation event to be aggregated.
|
||||
*/
|
||||
EventTimelineSet.prototype._aggregateRelations = function(event) {
|
||||
if (!this._unstableClientRelationAggregation) {
|
||||
return;
|
||||
}
|
||||
|
||||
const content = event.getContent();
|
||||
const relation = content && content["m.relates_to"];
|
||||
if (!relation || !relation.rel_type || !relation.event_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const relatesToEventId = relation.event_id;
|
||||
const relationType = relation.rel_type;
|
||||
const eventType = event.getType();
|
||||
|
||||
// debuglog("Aggregating relation: ", event.getId(), eventType, relation);
|
||||
|
||||
let relationsForEvent = this._relations[relatesToEventId];
|
||||
if (!relationsForEvent) {
|
||||
relationsForEvent = this._relations[relatesToEventId] = {};
|
||||
}
|
||||
let relationsWithRelType = relationsForEvent[relationType];
|
||||
if (!relationsWithRelType) {
|
||||
relationsWithRelType = relationsForEvent[relationType] = {};
|
||||
}
|
||||
let relationsWithEventType = relationsWithRelType[eventType];
|
||||
if (!relationsWithEventType) {
|
||||
relationsWithEventType = relationsWithRelType[eventType] = new Relations(
|
||||
relationType,
|
||||
eventType,
|
||||
);
|
||||
}
|
||||
|
||||
relationsWithEventType.addEvent(event);
|
||||
};
|
||||
|
||||
/**
|
||||
* The EventTimelineSet class.
|
||||
*/
|
||||
|
||||
149
src/models/relations.js
Normal file
149
src/models/relations.js
Normal file
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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
|
||||
* type and event type. All of the events also relate to the same original event.
|
||||
*
|
||||
* The typical way to get one of these containers is via
|
||||
* EventTimelineSet#getRelationsForEvent.
|
||||
*/
|
||||
export default class Relations {
|
||||
/**
|
||||
* @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.
|
||||
*/
|
||||
constructor(relationType, eventType) {
|
||||
this.relationType = relationType;
|
||||
this.eventType = eventType;
|
||||
this._events = [];
|
||||
this._annotationsByKey = {};
|
||||
this._annotationsBySender = {};
|
||||
this._sortedAnnotationsByKey = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add relation events to this collection.
|
||||
*
|
||||
* @param {MatrixEvent} event
|
||||
* The new relation event to be aggregated.
|
||||
*/
|
||||
addEvent(event) {
|
||||
const content = event.getContent();
|
||||
const relation = content && content["m.relates_to"];
|
||||
if (!relation || !relation.rel_type || !relation.event_id) {
|
||||
console.error("Event must have relation info");
|
||||
return;
|
||||
}
|
||||
|
||||
const relationType = relation.rel_type;
|
||||
const eventType = event.getType();
|
||||
|
||||
if (this.relationType !== relationType || this.eventType !== eventType) {
|
||||
console.error("Event relation info doesn't match this container");
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.relationType === "m.annotation") {
|
||||
const key = relation.key;
|
||||
this._aggregateAnnotation(key, event);
|
||||
}
|
||||
|
||||
this._events.push(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all 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.
|
||||
* TODO: Tweak `addEvent` to insert correctly for scrollback.
|
||||
*
|
||||
* @return {Array}
|
||||
* Relation events in insertion order.
|
||||
*/
|
||||
getEvents() {
|
||||
return this._events;
|
||||
}
|
||||
|
||||
_aggregateAnnotation(key, event) {
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
|
||||
let eventsForKey = this._annotationsByKey[key];
|
||||
if (!eventsForKey) {
|
||||
eventsForKey = this._annotationsByKey[key] = [];
|
||||
this._sortedAnnotationsByKey.push([key, eventsForKey]);
|
||||
}
|
||||
// Add the new event to the list for this key
|
||||
eventsForKey.push(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;
|
||||
});
|
||||
|
||||
const sender = event.getSender();
|
||||
let eventsFromSender = this._annotationsBySender[sender];
|
||||
if (!eventsFromSender) {
|
||||
eventsFromSender = this._annotationsBySender[sender] = [];
|
||||
}
|
||||
// Add the new event to the list for this sender
|
||||
eventsFromSender.push(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all events in this collection grouped by key and sorted by descending
|
||||
* event count in each group.
|
||||
*
|
||||
* This is currently only supported for the annotation relation type.
|
||||
*
|
||||
* @return {Array}
|
||||
* An array of [key, events] pairs sorted by descending event count.
|
||||
*/
|
||||
getSortedAnnotationsByKey() {
|
||||
if (this.relationType !== "m.annotation") {
|
||||
// Other relation types are not grouped currently.
|
||||
return null;
|
||||
}
|
||||
|
||||
return this._sortedAnnotationsByKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all events in this collection grouped by sender.
|
||||
*
|
||||
* This is currently only supported for the annotation relation type.
|
||||
*
|
||||
* @return {Object}
|
||||
* An object with each relation sender as a key and the matching list of
|
||||
* events for that sender as a value.
|
||||
*/
|
||||
getAnnotationsBySender() {
|
||||
if (this.relationType !== "m.annotation") {
|
||||
// Other relation types are not grouped currently.
|
||||
return null;
|
||||
}
|
||||
|
||||
return this._annotationsBySender;
|
||||
}
|
||||
}
|
||||
@@ -92,9 +92,12 @@ function synthesizeReceipt(userId, event, receiptType) {
|
||||
* "<b>detached</b>", pending messages will appear in a separate list,
|
||||
* accessbile via {@link module:models/room#getPendingEvents}. Default:
|
||||
* "chronological".
|
||||
*
|
||||
* @param {boolean} [opts.timelineSupport = false] Set to true to enable improved
|
||||
* timeline support.
|
||||
* @param {boolean} [opts.unstableClientRelationAggregation = false]
|
||||
* Optional. Set to true to enable client-side aggregation of event relations
|
||||
* via `EventTimelineSet#getRelationsForEvent`.
|
||||
* This feature is currently unstable and the API may change without notice.
|
||||
*
|
||||
* @prop {string} roomId The ID of this room.
|
||||
* @prop {string} name The human-readable display name for this room.
|
||||
|
||||
@@ -116,10 +116,15 @@ function SyncApi(client, opts) {
|
||||
*/
|
||||
SyncApi.prototype.createRoom = function(roomId) {
|
||||
const client = this.client;
|
||||
const {
|
||||
timelineSupport,
|
||||
unstableClientRelationAggregation,
|
||||
} = client;
|
||||
const room = new Room(roomId, client, client.getUserId(), {
|
||||
lazyLoadMembers: this.opts.lazyLoadMembers,
|
||||
pendingEventOrdering: this.opts.pendingEventOrdering,
|
||||
timelineSupport: client.timelineSupport,
|
||||
timelineSupport,
|
||||
unstableClientRelationAggregation,
|
||||
});
|
||||
client.reEmitter.reEmit(room, ["Room.name", "Room.timeline", "Room.redaction",
|
||||
"Room.receipt", "Room.tags",
|
||||
|
||||
Reference in New Issue
Block a user