You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-11-29 16:43:09 +03:00
Merge branch 'develop' into markjh/megolm
Conflicts: lib/crypto/algorithms/megolm.js
This commit is contained in:
253
lib/client.js
253
lib/client.js
@@ -146,6 +146,7 @@ function MatrixClient(opts) {
|
|||||||
this._ongoingScrollbacks = {};
|
this._ongoingScrollbacks = {};
|
||||||
this.timelineSupport = Boolean(opts.timelineSupport);
|
this.timelineSupport = Boolean(opts.timelineSupport);
|
||||||
this.urlPreviewCache = {};
|
this.urlPreviewCache = {};
|
||||||
|
this._notifTimelineSet = null;
|
||||||
|
|
||||||
this._crypto = null;
|
this._crypto = null;
|
||||||
if (CRYPTO_ENABLED && opts.sessionStore !== null &&
|
if (CRYPTO_ENABLED && opts.sessionStore !== null &&
|
||||||
@@ -249,6 +250,24 @@ MatrixClient.prototype.retryImmediately = function() {
|
|||||||
return this._syncApi.retryImmediately();
|
return this._syncApi.retryImmediately();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the global notification EventTimelineSet, if any
|
||||||
|
*
|
||||||
|
* @return {EventTimelineSet} the globl notification EventTimelineSet
|
||||||
|
*/
|
||||||
|
MatrixClient.prototype.getNotifTimelineSet = function() {
|
||||||
|
return this._notifTimelineSet;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the global notification EventTimelineSet
|
||||||
|
*
|
||||||
|
* @param {EventTimelineSet} notifTimelineSet
|
||||||
|
*/
|
||||||
|
MatrixClient.prototype.setNotifTimelineSet = function(notifTimelineSet) {
|
||||||
|
this._notifTimelineSet = notifTimelineSet;
|
||||||
|
};
|
||||||
|
|
||||||
// Crypto bits
|
// Crypto bits
|
||||||
// ===========
|
// ===========
|
||||||
|
|
||||||
@@ -1330,16 +1349,16 @@ function _membershipChange(client, roomId, userId, membership, reason, callback)
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Obtain a dict of actions which should be performed for this event according
|
* Obtain a dict of actions which should be performed for this event according
|
||||||
* to the push rules for this user.
|
* to the push rules for this user. Caches the dict on the event.
|
||||||
* @param {MatrixEvent} event The event to get push actions for.
|
* @param {MatrixEvent} event The event to get push actions for.
|
||||||
* @return {module:pushprocessor~PushAction} A dict of actions to perform.
|
* @return {module:pushprocessor~PushAction} A dict of actions to perform.
|
||||||
*/
|
*/
|
||||||
MatrixClient.prototype.getPushActionsForEvent = function(event) {
|
MatrixClient.prototype.getPushActionsForEvent = function(event) {
|
||||||
if (event._pushActions === undefined) {
|
if (!event.getPushActions()) {
|
||||||
var pushProcessor = new PushProcessor(this);
|
var pushProcessor = new PushProcessor(this);
|
||||||
event._pushActions = pushProcessor.actionsForEvent(event);
|
event.setPushActions(pushProcessor.actionsForEvent(event));
|
||||||
}
|
}
|
||||||
return event._pushActions;
|
return event.getPushActions();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Profile operations
|
// Profile operations
|
||||||
@@ -1584,18 +1603,18 @@ MatrixClient.prototype.paginateEventContext = function(eventContext, opts) {
|
|||||||
/**
|
/**
|
||||||
* Get an EventTimeline for the given event
|
* Get an EventTimeline for the given event
|
||||||
*
|
*
|
||||||
* <p>If the room object already has the given event in its store, the
|
* <p>If the EventTimelineSet object already has the given event in its store, the
|
||||||
* corresponding timeline will be returned. Otherwise, a /context request is
|
* corresponding timeline will be returned. Otherwise, a /context request is
|
||||||
* made, and used to construct an EventTimeline.
|
* made, and used to construct an EventTimeline.
|
||||||
*
|
*
|
||||||
* @param {Room} room The room to look for the event in
|
* @param {EventTimelineSet} timelineSet The timelineSet to look for the event in
|
||||||
* @param {string} eventId The ID of the event to look for
|
* @param {string} eventId The ID of the event to look for
|
||||||
*
|
*
|
||||||
* @return {module:client.Promise} Resolves:
|
* @return {module:client.Promise} Resolves:
|
||||||
* {@link module:models/event-timeline~EventTimeline} including the given
|
* {@link module:models/event-timeline~EventTimeline} including the given
|
||||||
* event
|
* event
|
||||||
*/
|
*/
|
||||||
MatrixClient.prototype.getEventTimeline = function(room, eventId) {
|
MatrixClient.prototype.getEventTimeline = function(timelineSet, eventId) {
|
||||||
// don't allow any timeline support unless it's been enabled.
|
// don't allow any timeline support unless it's been enabled.
|
||||||
if (!this.timelineSupport) {
|
if (!this.timelineSupport) {
|
||||||
throw new Error("timeline support is disabled. Set the 'timelineSupport'" +
|
throw new Error("timeline support is disabled. Set the 'timelineSupport'" +
|
||||||
@@ -1603,13 +1622,13 @@ MatrixClient.prototype.getEventTimeline = function(room, eventId) {
|
|||||||
" it.");
|
" it.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (room.getTimelineForEvent(eventId)) {
|
if (timelineSet.getTimelineForEvent(eventId)) {
|
||||||
return q(room.getTimelineForEvent(eventId));
|
return q(timelineSet.getTimelineForEvent(eventId));
|
||||||
}
|
}
|
||||||
|
|
||||||
var path = utils.encodeUri(
|
var path = utils.encodeUri(
|
||||||
"/rooms/$roomId/context/$eventId", {
|
"/rooms/$roomId/context/$eventId", {
|
||||||
$roomId: room.roomId,
|
$roomId: timelineSet.room.roomId,
|
||||||
$eventId: eventId,
|
$eventId: eventId,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -1626,8 +1645,8 @@ MatrixClient.prototype.getEventTimeline = function(room, eventId) {
|
|||||||
|
|
||||||
// by the time the request completes, the event might have ended up in
|
// by the time the request completes, the event might have ended up in
|
||||||
// the timeline.
|
// the timeline.
|
||||||
if (room.getTimelineForEvent(eventId)) {
|
if (timelineSet.getTimelineForEvent(eventId)) {
|
||||||
return room.getTimelineForEvent(eventId);
|
return timelineSet.getTimelineForEvent(eventId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// we start with the last event, since that's the point at which we
|
// we start with the last event, since that's the point at which we
|
||||||
@@ -1639,21 +1658,22 @@ MatrixClient.prototype.getEventTimeline = function(room, eventId) {
|
|||||||
.concat(res.events_before);
|
.concat(res.events_before);
|
||||||
var matrixEvents = utils.map(events, self.getEventMapper());
|
var matrixEvents = utils.map(events, self.getEventMapper());
|
||||||
|
|
||||||
var timeline = room.getTimelineForEvent(matrixEvents[0].getId());
|
var timeline = timelineSet.getTimelineForEvent(matrixEvents[0].getId());
|
||||||
if (!timeline) {
|
if (!timeline) {
|
||||||
timeline = room.addTimeline();
|
timeline = timelineSet.addTimeline();
|
||||||
timeline.initialiseState(utils.map(res.state,
|
timeline.initialiseState(utils.map(res.state,
|
||||||
self.getEventMapper()));
|
self.getEventMapper()));
|
||||||
timeline.getState(EventTimeline.FORWARDS).paginationToken = res.end;
|
timeline.getState(EventTimeline.FORWARDS).paginationToken = res.end;
|
||||||
}
|
}
|
||||||
room.addEventsToTimeline(matrixEvents, true, timeline, res.start);
|
timelineSet.addEventsToTimeline(matrixEvents, true, timeline, res.start);
|
||||||
|
|
||||||
// there is no guarantee that the event ended up in "timeline" (we
|
// there is no guarantee that the event ended up in "timeline" (we
|
||||||
// might have switched to a neighbouring timeline) - so check the
|
// might have switched to a neighbouring timeline) - so check the
|
||||||
// room's index again. On the other hand, there's no guarantee the
|
// room's index again. On the other hand, there's no guarantee the
|
||||||
// event ended up anywhere, if it was later redacted, so we just
|
// event ended up anywhere, if it was later redacted, so we just
|
||||||
// return the timeline we first thought of.
|
// return the timeline we first thought of.
|
||||||
return room.getTimelineForEvent(eventId) || timeline;
|
var tl = timelineSet.getTimelineForEvent(eventId) || timeline;
|
||||||
|
return tl;
|
||||||
});
|
});
|
||||||
return promise;
|
return promise;
|
||||||
};
|
};
|
||||||
@@ -1665,7 +1685,7 @@ MatrixClient.prototype.getEventTimeline = function(room, eventId) {
|
|||||||
* @param {module:models/event-timeline~EventTimeline} eventTimeline timeline
|
* @param {module:models/event-timeline~EventTimeline} eventTimeline timeline
|
||||||
* object to be updated
|
* object to be updated
|
||||||
* @param {Object} [opts]
|
* @param {Object} [opts]
|
||||||
* @param {boolean} [opts.backwards = false] true to fill backwards,
|
* @param {bool} [opts.backwards = false] true to fill backwards,
|
||||||
* false to go forwards
|
* false to go forwards
|
||||||
* @param {number} [opts.limit = 30] number of events to request
|
* @param {number} [opts.limit = 30] number of events to request
|
||||||
*
|
*
|
||||||
@@ -1673,14 +1693,17 @@ MatrixClient.prototype.getEventTimeline = function(room, eventId) {
|
|||||||
* events and we reached either end of the timeline; else true.
|
* events and we reached either end of the timeline; else true.
|
||||||
*/
|
*/
|
||||||
MatrixClient.prototype.paginateEventTimeline = function(eventTimeline, opts) {
|
MatrixClient.prototype.paginateEventTimeline = function(eventTimeline, opts) {
|
||||||
|
var isNotifTimeline = (eventTimeline.getTimelineSet() === this._notifTimelineSet);
|
||||||
|
|
||||||
// TODO: we should implement a backoff (as per scrollback()) to deal more
|
// TODO: we should implement a backoff (as per scrollback()) to deal more
|
||||||
// nicely with HTTP errors.
|
// nicely with HTTP errors.
|
||||||
opts = opts || {};
|
opts = opts || {};
|
||||||
var backwards = opts.backwards || false;
|
var backwards = opts.backwards || false;
|
||||||
|
|
||||||
var room = this.getRoom(eventTimeline.getRoomId());
|
if (isNotifTimeline) {
|
||||||
if (!room) {
|
if (!backwards) {
|
||||||
throw new Error("Unknown room " + eventTimeline.getRoomId());
|
throw new Error("paginateNotifTimeline can only paginate backwards");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
|
var dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
|
||||||
@@ -1698,39 +1721,131 @@ MatrixClient.prototype.paginateEventTimeline = function(eventTimeline, opts) {
|
|||||||
return pendingRequest;
|
return pendingRequest;
|
||||||
}
|
}
|
||||||
|
|
||||||
var path = utils.encodeUri(
|
var path, params, promise;
|
||||||
"/rooms/$roomId/messages", {$roomId: eventTimeline.getRoomId()}
|
|
||||||
);
|
|
||||||
var params = {
|
|
||||||
from: token,
|
|
||||||
limit: ('limit' in opts) ? opts.limit : 30,
|
|
||||||
dir: dir
|
|
||||||
};
|
|
||||||
|
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
var promise =
|
if (isNotifTimeline) {
|
||||||
this._http.authedRequest(undefined, "GET", path, params
|
path = "/notifications";
|
||||||
).then(function(res) {
|
params = {
|
||||||
var token = res.end;
|
limit: ('limit' in opts) ? opts.limit : 30,
|
||||||
var matrixEvents = utils.map(res.chunk, self.getEventMapper());
|
only: 'highlight',
|
||||||
room.addEventsToTimeline(matrixEvents, backwards, eventTimeline, token);
|
};
|
||||||
|
|
||||||
// if we've hit the end of the timeline, we need to stop trying to
|
if (token && token !== "end") {
|
||||||
// paginate. We need to keep the 'forwards' token though, to make sure
|
params.from = token;
|
||||||
// we can recover from gappy syncs.
|
|
||||||
if (backwards && res.end == res.start) {
|
|
||||||
eventTimeline.setPaginationToken(null, dir);
|
|
||||||
}
|
}
|
||||||
return res.end != res.start;
|
|
||||||
}).finally(function() {
|
promise =
|
||||||
eventTimeline._paginationRequests[dir] = null;
|
this._http.authedRequestWithPrefix(undefined, "GET", path, params,
|
||||||
});
|
undefined, httpApi.PREFIX_UNSTABLE
|
||||||
eventTimeline._paginationRequests[dir] = promise;
|
).then(function(res) {
|
||||||
|
var token = res.next_token;
|
||||||
|
var matrixEvents = [];
|
||||||
|
|
||||||
|
for (var i = 0; i < res.notifications.length; i++) {
|
||||||
|
var notification = res.notifications[i];
|
||||||
|
var event = self.getEventMapper()(notification.event);
|
||||||
|
event.setPushActions(
|
||||||
|
PushProcessor.actionListToActionsObject(notification.actions)
|
||||||
|
);
|
||||||
|
event.event.room_id = notification.room_id; // XXX: gutwrenching
|
||||||
|
matrixEvents[i] = event;
|
||||||
|
}
|
||||||
|
|
||||||
|
eventTimeline.getTimelineSet()
|
||||||
|
.addEventsToTimeline(matrixEvents, backwards, eventTimeline, token);
|
||||||
|
|
||||||
|
// if we've hit the end of the timeline, we need to stop trying to
|
||||||
|
// paginate. We need to keep the 'forwards' token though, to make sure
|
||||||
|
// we can recover from gappy syncs.
|
||||||
|
if (backwards && !res.next_token) {
|
||||||
|
eventTimeline.setPaginationToken(null, dir);
|
||||||
|
}
|
||||||
|
return res.next_token ? true : false;
|
||||||
|
}).finally(function() {
|
||||||
|
eventTimeline._paginationRequests[dir] = null;
|
||||||
|
});
|
||||||
|
eventTimeline._paginationRequests[dir] = promise;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
var room = this.getRoom(eventTimeline.getRoomId());
|
||||||
|
if (!room) {
|
||||||
|
throw new Error("Unknown room " + eventTimeline.getRoomId());
|
||||||
|
}
|
||||||
|
|
||||||
|
path = utils.encodeUri(
|
||||||
|
"/rooms/$roomId/messages", {$roomId: eventTimeline.getRoomId()}
|
||||||
|
);
|
||||||
|
params = {
|
||||||
|
from: token,
|
||||||
|
limit: ('limit' in opts) ? opts.limit : 30,
|
||||||
|
dir: dir
|
||||||
|
};
|
||||||
|
|
||||||
|
var filter = eventTimeline.getFilter();
|
||||||
|
if (filter) {
|
||||||
|
// XXX: it's horrific that /messages' filter parameter doesn't match
|
||||||
|
// /sync's one - see https://matrix.org/jira/browse/SPEC-451
|
||||||
|
params.filter = JSON.stringify(filter.getRoomTimelineFilterComponent());
|
||||||
|
}
|
||||||
|
|
||||||
|
promise =
|
||||||
|
this._http.authedRequest(undefined, "GET", path, params
|
||||||
|
).then(function(res) {
|
||||||
|
var token = res.end;
|
||||||
|
var matrixEvents = utils.map(res.chunk, self.getEventMapper());
|
||||||
|
eventTimeline.getTimelineSet()
|
||||||
|
.addEventsToTimeline(matrixEvents, backwards, eventTimeline, token);
|
||||||
|
|
||||||
|
// if we've hit the end of the timeline, we need to stop trying to
|
||||||
|
// paginate. We need to keep the 'forwards' token though, to make sure
|
||||||
|
// we can recover from gappy syncs.
|
||||||
|
if (backwards && res.end == res.start) {
|
||||||
|
eventTimeline.setPaginationToken(null, dir);
|
||||||
|
}
|
||||||
|
return res.end != res.start;
|
||||||
|
}).finally(function() {
|
||||||
|
eventTimeline._paginationRequests[dir] = null;
|
||||||
|
});
|
||||||
|
eventTimeline._paginationRequests[dir] = promise;
|
||||||
|
}
|
||||||
|
|
||||||
return promise;
|
return promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the notifTimelineSet entirely, paginating in some historical notifs as
|
||||||
|
* a starting point for subsequent pagination.
|
||||||
|
*/
|
||||||
|
MatrixClient.prototype.resetNotifTimelineSet = function() {
|
||||||
|
if (!this._notifTimelineSet) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: This thing is a total hack, and results in duplicate events being
|
||||||
|
// added to the timeline both from /sync and /notifications, and lots of
|
||||||
|
// slow and wasteful processing and pagination. The correct solution is to
|
||||||
|
// extend /messages or /search or something to filter on notifications.
|
||||||
|
|
||||||
|
// use the fictitious token 'end'. in practice we would ideally give it
|
||||||
|
// the oldest backwards pagination token from /sync, but /sync doesn't
|
||||||
|
// know about /notifications, so we have no choice but to start paginating
|
||||||
|
// from the current point in time. This may well overlap with historical
|
||||||
|
// notifs which are then inserted into the timeline by /sync responses.
|
||||||
|
this._notifTimelineSet.resetLiveTimeline('end', true);
|
||||||
|
|
||||||
|
// we could try to paginate a single event at this point in order to get
|
||||||
|
// a more valid pagination token, but it just ends up with an out of order
|
||||||
|
// timeline. given what a mess this is and given we're going to have duplicate
|
||||||
|
// events anyway, just leave it with the dummy token for now.
|
||||||
|
/*
|
||||||
|
this.paginateNotifTimeline(this._notifTimelineSet.getLiveTimeline(), {
|
||||||
|
backwards: true,
|
||||||
|
limit: 1
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Peek into a room and receive updates about the room. This only works if the
|
* Peek into a room and receive updates about the room. This only works if the
|
||||||
* history visibility for the room is world_readable.
|
* history visibility for the room is world_readable.
|
||||||
@@ -2242,6 +2357,54 @@ MatrixClient.prototype.getFilter = function(userId, filterId, allowCached) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} filterName
|
||||||
|
* @param {Filter} filter
|
||||||
|
* @return {Promise<String>} Filter ID
|
||||||
|
*/
|
||||||
|
MatrixClient.prototype.getOrCreateFilter = function(filterName, filter) {
|
||||||
|
|
||||||
|
var filterId = this.store.getFilterIdByName(filterName);
|
||||||
|
var promise = q();
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
if (filterId) {
|
||||||
|
// check that the existing filter matches our expectations
|
||||||
|
promise = self.getFilter(self.credentials.userId,
|
||||||
|
filterId, true
|
||||||
|
).then(function(existingFilter) {
|
||||||
|
var oldDef = existingFilter.getDefinition();
|
||||||
|
var newDef = filter.getDefinition();
|
||||||
|
|
||||||
|
if (utils.deepCompare(oldDef, newDef)) {
|
||||||
|
// super, just use that.
|
||||||
|
// debuglog("Using existing filter ID %s: %s", filterId,
|
||||||
|
// JSON.stringify(oldDef));
|
||||||
|
return q(filterId);
|
||||||
|
}
|
||||||
|
// debuglog("Existing filter ID %s: %s; new filter: %s",
|
||||||
|
// filterId, JSON.stringify(oldDef), JSON.stringify(newDef));
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return promise.then(function(existingId) {
|
||||||
|
if (existingId) {
|
||||||
|
return existingId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a new filter
|
||||||
|
return self.createFilter(filter.getDefinition()
|
||||||
|
).then(function(createdFilter) {
|
||||||
|
// debuglog("Created new filter ID %s: %s", createdFilter.filterId,
|
||||||
|
// JSON.stringify(createdFilter.getDefinition()));
|
||||||
|
self.store.setFilterIdByName(filterName, createdFilter.filterId);
|
||||||
|
return createdFilter.filterId;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a bearer token from the Home Server that the user can
|
* Gets a bearer token from the Home Server that the user can
|
||||||
* present to a third party in order to prove their ownership
|
* present to a third party in order to prove their ownership
|
||||||
|
|||||||
@@ -522,21 +522,23 @@ OlmDevice.prototype.getOutboundGroupSessionKey = function(sessionId) {
|
|||||||
* store an InboundGroupSession in the session store
|
* store an InboundGroupSession in the session store
|
||||||
*
|
*
|
||||||
* @param {string} roomId
|
* @param {string} roomId
|
||||||
* @param {string} senderKey
|
* @param {string} senderCurve25519Key
|
||||||
* @param {string} sessionId
|
* @param {string} sessionId
|
||||||
* @param {Olm.InboundGroupSession} session
|
* @param {Olm.InboundGroupSession} session
|
||||||
|
* @param {object} keysClaimed Other keys the sender claims.
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
OlmDevice.prototype._saveInboundGroupSession = function(
|
OlmDevice.prototype._saveInboundGroupSession = function(
|
||||||
roomId, senderKey, sessionId, session
|
roomId, senderCurve25519Key, sessionId, session, keysClaimed
|
||||||
) {
|
) {
|
||||||
var r = {
|
var r = {
|
||||||
room_id: roomId,
|
room_id: roomId,
|
||||||
session: session.pickle(this._pickleKey),
|
session: session.pickle(this._pickleKey),
|
||||||
|
keysClaimed: keysClaimed,
|
||||||
};
|
};
|
||||||
|
|
||||||
this._sessionStore.storeEndToEndInboundGroupSession(
|
this._sessionStore.storeEndToEndInboundGroupSession(
|
||||||
senderKey, sessionId, JSON.stringify(r)
|
senderCurve25519Key, sessionId, JSON.stringify(r)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -547,7 +549,9 @@ OlmDevice.prototype._saveInboundGroupSession = function(
|
|||||||
* @param {string} senderKey
|
* @param {string} senderKey
|
||||||
* @param {string} sessionId
|
* @param {string} sessionId
|
||||||
* @param {function} func
|
* @param {function} func
|
||||||
* @return {object} result of func
|
* @return {object} Object with two keys "result": result of func, "exists"
|
||||||
|
* whether the session exists. if the session doesn't exist then the function
|
||||||
|
* isn't called and the "result" is undefined.
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
OlmDevice.prototype._getInboundGroupSession = function(
|
OlmDevice.prototype._getInboundGroupSession = function(
|
||||||
@@ -558,7 +562,7 @@ OlmDevice.prototype._getInboundGroupSession = function(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (r === null) {
|
if (r === null) {
|
||||||
throw new Error("Unknown inbound group session id");
|
return {sessionExists: false};
|
||||||
}
|
}
|
||||||
|
|
||||||
r = JSON.parse(r);
|
r = JSON.parse(r);
|
||||||
@@ -575,7 +579,12 @@ OlmDevice.prototype._getInboundGroupSession = function(
|
|||||||
var session = new Olm.InboundGroupSession();
|
var session = new Olm.InboundGroupSession();
|
||||||
try {
|
try {
|
||||||
session.unpickle(this._pickleKey, r.session);
|
session.unpickle(this._pickleKey, r.session);
|
||||||
return func(session);
|
return {
|
||||||
|
sessionExists: true,
|
||||||
|
result: func(session),
|
||||||
|
keysProved: {curve25519: senderKey},
|
||||||
|
keysClaimed: r.keysClaimed || {},
|
||||||
|
};
|
||||||
} finally {
|
} finally {
|
||||||
session.free();
|
session.free();
|
||||||
}
|
}
|
||||||
@@ -616,7 +625,7 @@ OlmDevice.prototype.addInboundGroupSession = function(
|
|||||||
* @param {string} sessionId session identifier
|
* @param {string} sessionId session identifier
|
||||||
* @param {string} body base64-encoded body of the encrypted message
|
* @param {string} body base64-encoded body of the encrypted message
|
||||||
*
|
*
|
||||||
* @return {string} plaintext
|
* @return {object} {result: "plaintext"|undefined, sessionExists: Boolean}
|
||||||
*/
|
*/
|
||||||
OlmDevice.prototype.decryptGroupMessage = function(
|
OlmDevice.prototype.decryptGroupMessage = function(
|
||||||
roomId, senderKey, sessionId, body
|
roomId, senderKey, sessionId, body
|
||||||
|
|||||||
@@ -272,6 +272,9 @@ MegolmEncryption.prototype.encryptMessage = function(room, eventType, content) {
|
|||||||
sender_key: self._olmDevice.deviceCurve25519Key,
|
sender_key: self._olmDevice.deviceCurve25519Key,
|
||||||
ciphertext: ciphertext,
|
ciphertext: ciphertext,
|
||||||
session_id: session_id,
|
session_id: session_id,
|
||||||
|
// Include our device ID so that recipients can send us a
|
||||||
|
// m.new_device message if they don't have our session key.
|
||||||
|
device_id: self._deviceId,
|
||||||
};
|
};
|
||||||
|
|
||||||
return encryptedContent;
|
return encryptedContent;
|
||||||
@@ -356,7 +359,8 @@ utils.inherits(MegolmDecryption, base.DecryptionAlgorithm);
|
|||||||
*
|
*
|
||||||
* @param {object} event raw event
|
* @param {object} event raw event
|
||||||
*
|
*
|
||||||
* @return {object} decrypted payload (with properties 'type', 'content')
|
* @return {object} object with 'result' key with decrypted payload (with
|
||||||
|
* properties 'type', 'content') and a 'sessionExists' key.
|
||||||
*
|
*
|
||||||
* @throws {module:crypto/algorithms/base.DecryptionError} if there is a
|
* @throws {module:crypto/algorithms/base.DecryptionError} if there is a
|
||||||
* problem decrypting the event
|
* problem decrypting the event
|
||||||
@@ -377,7 +381,12 @@ MegolmDecryption.prototype.decryptEvent = function(event) {
|
|||||||
var res = this._olmDevice.decryptGroupMessage(
|
var res = this._olmDevice.decryptGroupMessage(
|
||||||
event.room_id, content.sender_key, content.session_id, content.ciphertext
|
event.room_id, content.sender_key, content.session_id, content.ciphertext
|
||||||
);
|
);
|
||||||
return JSON.parse(res);
|
if (res.sessionExists) {
|
||||||
|
res.result = JSON.parse(res.result);
|
||||||
|
return res;
|
||||||
|
} else {
|
||||||
|
return {sessionExists: false};
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new base.DecryptionError(e);
|
throw new base.DecryptionError(e);
|
||||||
}
|
}
|
||||||
@@ -402,7 +411,7 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
|
|||||||
|
|
||||||
this._olmDevice.addInboundGroupSession(
|
this._olmDevice.addInboundGroupSession(
|
||||||
content.room_id, event.getSenderKey(), content.session_id,
|
content.room_id, event.getSenderKey(), content.session_id,
|
||||||
content.session_key
|
content.session_key, event.getKeysClaimed()
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -119,6 +119,17 @@ OlmEncryption.prototype.encryptMessage = function(room, eventType, content) {
|
|||||||
room_id: room.roomId,
|
room_id: room.roomId,
|
||||||
type: eventType,
|
type: eventType,
|
||||||
content: content,
|
content: content,
|
||||||
|
// Include the ED25519 key so that the recipient knows what
|
||||||
|
// device this message came from.
|
||||||
|
// We don't need to include the curve25519 key since the
|
||||||
|
// recipient will already know this from the olm headers.
|
||||||
|
// When combined with the device keys retrieved from the
|
||||||
|
// homeserver signed by the ed25519 key this proves that
|
||||||
|
// the curve25519 key and the ed25519 key are owned by
|
||||||
|
// the same device.
|
||||||
|
keys: {
|
||||||
|
"ed25519": self._olmDevice.deviceEd25519Key
|
||||||
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -142,7 +153,9 @@ utils.inherits(OlmDecryption, base.DecryptionAlgorithm);
|
|||||||
*
|
*
|
||||||
* @param {object} event raw event
|
* @param {object} event raw event
|
||||||
*
|
*
|
||||||
* @return {object} decrypted payload (with properties 'type', 'content')
|
* @return {object} result object with result property with the decrypted
|
||||||
|
* payload (with properties 'type', 'content'), and a "sessionExists" key
|
||||||
|
* always set to true.
|
||||||
*
|
*
|
||||||
* @throws {module:crypto/algorithms/base.DecryptionError} if there is a
|
* @throws {module:crypto/algorithms/base.DecryptionError} if there is a
|
||||||
* problem decrypting the event
|
* problem decrypting the event
|
||||||
@@ -198,7 +211,13 @@ OlmDecryption.prototype.decryptEvent = function(event) {
|
|||||||
// TODO: Check the sender user id matches the sender key.
|
// TODO: Check the sender user id matches the sender key.
|
||||||
// TODO: check the room_id and fingerprint
|
// TODO: check the room_id and fingerprint
|
||||||
if (payloadString !== null) {
|
if (payloadString !== null) {
|
||||||
return JSON.parse(payloadString);
|
var payload = JSON.parse(payloadString);
|
||||||
|
return {
|
||||||
|
result: payload,
|
||||||
|
sessionExists: true,
|
||||||
|
keysProved: {curve25519: deviceKey},
|
||||||
|
keysClaimed: payload.keys || {}
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
throw new base.DecryptionError("Bad Encrypted Message");
|
throw new base.DecryptionError("Bad Encrypted Message");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,36 +85,53 @@ function Crypto(baseApis, eventEmitter, sessionStore, userId, deviceId) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
_registerEventHandlers(this, eventEmitter);
|
_registerEventHandlers(this, eventEmitter);
|
||||||
|
|
||||||
|
this._lastNewDeviceMessageTsByUserDeviceRoom = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
function _registerEventHandlers(crypto, eventEmitter) {
|
function _registerEventHandlers(crypto, eventEmitter) {
|
||||||
eventEmitter.on("sync", function(syncState, oldState, data) {
|
eventEmitter.on("sync", function(syncState, oldState, data) {
|
||||||
if (syncState == "PREPARED") {
|
try {
|
||||||
// XXX ugh. we're assuming the eventEmitter is a MatrixClient.
|
if (syncState == "PREPARED") {
|
||||||
// how can we avoid doing so?
|
// XXX ugh. we're assuming the eventEmitter is a MatrixClient.
|
||||||
var rooms = eventEmitter.getRooms();
|
// how can we avoid doing so?
|
||||||
crypto._onInitialSyncCompleted(rooms);
|
var rooms = eventEmitter.getRooms();
|
||||||
|
crypto._onInitialSyncCompleted(rooms);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error handling sync", e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
eventEmitter.on(
|
eventEmitter.on("RoomMember.membership", function(event, member, oldMembership) {
|
||||||
"RoomMember.membership",
|
try {
|
||||||
crypto._onRoomMembership.bind(crypto)
|
crypto._onRoomMembership(event, member, oldMembership);
|
||||||
);
|
} catch (e) {
|
||||||
|
console.error("Error handling membership change:", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
eventEmitter.on("toDeviceEvent", function(event) {
|
eventEmitter.on("toDeviceEvent", function(event) {
|
||||||
if (event.getType() == "m.room_key") {
|
try {
|
||||||
crypto._onRoomKeyEvent(event);
|
if (event.getType() == "m.room_key") {
|
||||||
} else if (event.getType() == "m.new_device") {
|
crypto._onRoomKeyEvent(event);
|
||||||
crypto._onNewDeviceEvent(event);
|
} else if (event.getType() == "m.new_device") {
|
||||||
|
crypto._onNewDeviceEvent(event);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error handling toDeviceEvent:", e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
eventEmitter.on("event", function(event) {
|
eventEmitter.on("event", function(event) {
|
||||||
if (!event.isState() || event.getType() != "m.room.encryption") {
|
try {
|
||||||
return;
|
if (!event.isState() || event.getType() != "m.room.encryption") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
crypto._onCryptoEvent(event);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error handling crypto event:", e);
|
||||||
}
|
}
|
||||||
crypto._onCryptoEvent(event);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -812,7 +829,66 @@ Crypto.prototype.decryptEvent = function(event) {
|
|||||||
var alg = new AlgClass({
|
var alg = new AlgClass({
|
||||||
olmDevice: this._olmDevice,
|
olmDevice: this._olmDevice,
|
||||||
});
|
});
|
||||||
return alg.decryptEvent(event);
|
var r = alg.decryptEvent(event);
|
||||||
|
var payload = r.result;
|
||||||
|
payload.keysClaimed = r.keysClaimed;
|
||||||
|
payload.keysProved = r.keysProved;
|
||||||
|
if (r.sessionExists) {
|
||||||
|
return payload;
|
||||||
|
} else {
|
||||||
|
// We've got a message for a session we don't have.
|
||||||
|
// Maybe the sender forgot to tell us about the session.
|
||||||
|
// Remind the sender that we exist so that they might
|
||||||
|
// tell us about the sender.
|
||||||
|
if (event.getRoomId !== undefined && event.getSender !== undefined) {
|
||||||
|
this._sendPingToDevice(
|
||||||
|
event.getSender(), event.content.device, event.getRoomId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new algorithms.DecryptionError("Unknown inbound session id");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a "m.new_device" message to remind it that we exist and are a member
|
||||||
|
* of a room.
|
||||||
|
*
|
||||||
|
* This is rate limited to send a message at most once an hour per desination.
|
||||||
|
*
|
||||||
|
* @param {string} userId The ID of the user to ping.
|
||||||
|
* @param {string} deviceId The ID of the device to ping.
|
||||||
|
* @param {string} roomId The ID of the room we want to remind them about.
|
||||||
|
*/
|
||||||
|
Crypto.prototype._sendPingToDevice = function(userId, deviceId, roomId) {
|
||||||
|
if (deviceId === undefined) {
|
||||||
|
deviceId = "*";
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastMessageTsMap = this._lastNewDeviceMessageTsByUserDeviceRoom;
|
||||||
|
var lastTsByDevice = lastMessageTsMap[userId] || {};
|
||||||
|
var lastTsByRoom = lastTsByDevice[deviceId] || {};
|
||||||
|
var lastTs = lastTsByRoom[roomId];
|
||||||
|
var timeNowMs = Date.now();
|
||||||
|
var oneHourMs = 1000 * 60 * 60;
|
||||||
|
|
||||||
|
if (lastTs === undefined || lastTs + oneHourMs < timeNowMs) {
|
||||||
|
var content = {
|
||||||
|
userId: {
|
||||||
|
deviceId: {
|
||||||
|
device_id: this._deviceId,
|
||||||
|
rooms: [roomId],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
lastTsByRoom[roomId] = timeNowMs;
|
||||||
|
|
||||||
|
this._baseApis.sendToDevice(
|
||||||
|
"m.new_device", // OH HAI!
|
||||||
|
content
|
||||||
|
).done(function() {});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
141
lib/filter-component.js
Normal file
141
lib/filter-component.js
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2016 OpenMarket 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.
|
||||||
|
*/
|
||||||
|
"use strict";
|
||||||
|
/**
|
||||||
|
* @module filter-component
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a value matches a given field value, which may be a * terminated
|
||||||
|
* wildcard pattern.
|
||||||
|
* @param {String} actual_value The value to be compared
|
||||||
|
* @param {String} filter_value The filter pattern to be compared
|
||||||
|
* @return {bool} true if the actual_value matches the filter_value
|
||||||
|
*/
|
||||||
|
function _matches_wildcard(actual_value, filter_value) {
|
||||||
|
if (filter_value.endsWith("*")) {
|
||||||
|
var type_prefix = filter_value.slice(0, -1);
|
||||||
|
return actual_value.substr(0, type_prefix.length) === type_prefix;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return actual_value === filter_value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FilterComponent is a section of a Filter definition which defines the
|
||||||
|
* types, rooms, senders filters etc to be applied to a particular type of resource.
|
||||||
|
* This is all ported over from synapse's Filter object.
|
||||||
|
*
|
||||||
|
* N.B. that synapse refers to these as 'Filters', and what js-sdk refers to as
|
||||||
|
* 'Filters' are referred to as 'FilterCollections'.
|
||||||
|
*
|
||||||
|
* @constructor
|
||||||
|
* @param {Object} the definition of this filter JSON, e.g. { 'contains_url': true }
|
||||||
|
*/
|
||||||
|
function FilterComponent(filter_json) {
|
||||||
|
this.filter_json = filter_json;
|
||||||
|
|
||||||
|
this.types = filter_json.types || null;
|
||||||
|
this.not_types = filter_json.not_types || [];
|
||||||
|
|
||||||
|
this.rooms = filter_json.rooms || null;
|
||||||
|
this.not_rooms = filter_json.not_rooms || [];
|
||||||
|
|
||||||
|
this.senders = filter_json.senders || null;
|
||||||
|
this.not_senders = filter_json.not_senders || [];
|
||||||
|
|
||||||
|
this.contains_url = filter_json.contains_url || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks with the filter component matches the given event
|
||||||
|
* @param {MatrixEvent} event event to be checked against the filter
|
||||||
|
* @return {bool} true if the event matches the filter
|
||||||
|
*/
|
||||||
|
FilterComponent.prototype.check = function(event) {
|
||||||
|
return this._checkFields(
|
||||||
|
event.getRoomId(),
|
||||||
|
event.getSender(),
|
||||||
|
event.getType(),
|
||||||
|
event.getContent() ? event.getContent().url !== undefined : false
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the filter component matches the given event fields.
|
||||||
|
* @param {String} room_id the room_id for the event being checked
|
||||||
|
* @param {String} sender the sender of the event being checked
|
||||||
|
* @param {String} event_type the type of the event being checked
|
||||||
|
* @param {String} contains_url whether the event contains a content.url field
|
||||||
|
* @return {bool} true if the event fields match the filter
|
||||||
|
*/
|
||||||
|
FilterComponent.prototype._checkFields =
|
||||||
|
function(room_id, sender, event_type, contains_url)
|
||||||
|
{
|
||||||
|
var literal_keys = {
|
||||||
|
"rooms": function(v) { return room_id === v; },
|
||||||
|
"senders": function(v) { return sender === v; },
|
||||||
|
"types": function(v) { return _matches_wildcard(event_type, v); },
|
||||||
|
};
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
Object.keys(literal_keys).forEach(function(name) {
|
||||||
|
var match_func = literal_keys[name];
|
||||||
|
var not_name = "not_" + name;
|
||||||
|
var disallowed_values = self[not_name];
|
||||||
|
if (disallowed_values.map(match_func)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var allowed_values = self[name];
|
||||||
|
if (allowed_values) {
|
||||||
|
if (!allowed_values.map(match_func)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var contains_url_filter = this.filter_json.contains_url;
|
||||||
|
if (contains_url_filter !== undefined) {
|
||||||
|
if (contains_url_filter !== contains_url) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters a list of events down to those which match this filter component
|
||||||
|
* @param {MatrixEvent[]} events Events to be checked againt the filter component
|
||||||
|
* @return {MatrixEvent[]} events which matched the filter component
|
||||||
|
*/
|
||||||
|
FilterComponent.prototype.filter = function(events) {
|
||||||
|
return events.filter(this.check, this);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the limit field for a given filter component, providing a default of
|
||||||
|
* 10 if none is otherwise specified. Cargo-culted from Synapse.
|
||||||
|
* @return {Number} the limit for this filter component.
|
||||||
|
*/
|
||||||
|
FilterComponent.prototype.limit = function() {
|
||||||
|
return this.filter_json.limit !== undefined ? this.filter_json.limit : 10;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** The FilterComponent class */
|
||||||
|
module.exports = FilterComponent;
|
||||||
@@ -18,6 +18,8 @@ limitations under the License.
|
|||||||
* @module filter
|
* @module filter
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
var FilterComponent = require("./filter-component");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} obj
|
* @param {Object} obj
|
||||||
* @param {string} keyNesting
|
* @param {string} keyNesting
|
||||||
@@ -49,6 +51,14 @@ function Filter(userId, filterId) {
|
|||||||
this.definition = {};
|
this.definition = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the ID of this filter on your homeserver (if known)
|
||||||
|
* @return {?Number} The filter ID
|
||||||
|
*/
|
||||||
|
Filter.prototype.getFilterId = function() {
|
||||||
|
return this.filterId;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the JSON body of the filter.
|
* Get the JSON body of the filter.
|
||||||
* @return {Object} The filter definition
|
* @return {Object} The filter definition
|
||||||
@@ -63,6 +73,88 @@ Filter.prototype.getDefinition = function() {
|
|||||||
*/
|
*/
|
||||||
Filter.prototype.setDefinition = function(definition) {
|
Filter.prototype.setDefinition = function(definition) {
|
||||||
this.definition = definition;
|
this.definition = definition;
|
||||||
|
|
||||||
|
// This is all ported from synapse's FilterCollection()
|
||||||
|
|
||||||
|
// definitions look something like:
|
||||||
|
// {
|
||||||
|
// "room": {
|
||||||
|
// "rooms": ["!abcde:example.com"],
|
||||||
|
// "not_rooms": ["!123456:example.com"],
|
||||||
|
// "state": {
|
||||||
|
// "types": ["m.room.*"],
|
||||||
|
// "not_rooms": ["!726s6s6q:example.com"],
|
||||||
|
// },
|
||||||
|
// "timeline": {
|
||||||
|
// "limit": 10,
|
||||||
|
// "types": ["m.room.message"],
|
||||||
|
// "not_rooms": ["!726s6s6q:example.com"],
|
||||||
|
// "not_senders": ["@spam:example.com"]
|
||||||
|
// "contains_url": true
|
||||||
|
// },
|
||||||
|
// "ephemeral": {
|
||||||
|
// "types": ["m.receipt", "m.typing"],
|
||||||
|
// "not_rooms": ["!726s6s6q:example.com"],
|
||||||
|
// "not_senders": ["@spam:example.com"]
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// "presence": {
|
||||||
|
// "types": ["m.presence"],
|
||||||
|
// "not_senders": ["@alice:example.com"]
|
||||||
|
// },
|
||||||
|
// "event_format": "client",
|
||||||
|
// "event_fields": ["type", "content", "sender"]
|
||||||
|
// }
|
||||||
|
|
||||||
|
var room_filter_json = definition.room;
|
||||||
|
|
||||||
|
// consider the top level rooms/not_rooms filter
|
||||||
|
var room_filter_fields = {};
|
||||||
|
if (room_filter_json) {
|
||||||
|
if (room_filter_json.rooms) {
|
||||||
|
room_filter_fields.rooms = room_filter_json.rooms;
|
||||||
|
}
|
||||||
|
if (room_filter_json.rooms) {
|
||||||
|
room_filter_fields.not_rooms = room_filter_json.not_rooms;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._include_leave = room_filter_json.include_leave || false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._room_filter = new FilterComponent(room_filter_fields);
|
||||||
|
this._room_timeline_filter = new FilterComponent(
|
||||||
|
room_filter_json ? (room_filter_json.timeline || {}) : {}
|
||||||
|
);
|
||||||
|
|
||||||
|
// don't bother porting this from synapse yet:
|
||||||
|
// this._room_state_filter =
|
||||||
|
// new FilterComponent(room_filter_json.state || {});
|
||||||
|
// this._room_ephemeral_filter =
|
||||||
|
// new FilterComponent(room_filter_json.ephemeral || {});
|
||||||
|
// this._room_account_data_filter =
|
||||||
|
// new FilterComponent(room_filter_json.account_data || {});
|
||||||
|
// this._presence_filter =
|
||||||
|
// new FilterComponent(definition.presence || {});
|
||||||
|
// this._account_data_filter =
|
||||||
|
// new FilterComponent(definition.account_data || {});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the room.timeline filter component of the filter
|
||||||
|
* @return {FilterComponent} room timeline filter component
|
||||||
|
*/
|
||||||
|
Filter.prototype.getRoomTimelineFilterComponent = function() {
|
||||||
|
return this._room_timeline_filter;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter the list of events based on whether they are allowed in a timeline
|
||||||
|
* based on this filter
|
||||||
|
* @param {MatrixEvent[]} events the list of events being filtered
|
||||||
|
* @return {MatrixEvent[]} the list of events which match the filter
|
||||||
|
*/
|
||||||
|
Filter.prototype.filterRoomTimeline = function(events) {
|
||||||
|
return this._room_timeline_filter.filter(this._room_filter.filter(events));
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ module.exports.MatrixClient = require("./client").MatrixClient;
|
|||||||
module.exports.Room = require("./models/room");
|
module.exports.Room = require("./models/room");
|
||||||
/** The {@link module:models/event-timeline~EventTimeline} class. */
|
/** The {@link module:models/event-timeline~EventTimeline} class. */
|
||||||
module.exports.EventTimeline = require("./models/event-timeline");
|
module.exports.EventTimeline = require("./models/event-timeline");
|
||||||
|
/** The {@link module:models/event-timeline-set~EventTimelineSet} class. */
|
||||||
|
module.exports.EventTimelineSet = require("./models/event-timeline-set");
|
||||||
/** The {@link module:models/room-member|RoomMember} class. */
|
/** The {@link module:models/room-member|RoomMember} class. */
|
||||||
module.exports.RoomMember = require("./models/room-member");
|
module.exports.RoomMember = require("./models/room-member");
|
||||||
/** The {@link module:models/room-state~RoomState|RoomState} class. */
|
/** The {@link module:models/room-state~RoomState|RoomState} class. */
|
||||||
|
|||||||
654
lib/models/event-timeline-set.js
Normal file
654
lib/models/event-timeline-set.js
Normal file
@@ -0,0 +1,654 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2016 OpenMarket 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.
|
||||||
|
*/
|
||||||
|
"use strict";
|
||||||
|
/**
|
||||||
|
* @module models/event-timeline-set
|
||||||
|
*/
|
||||||
|
var EventEmitter = require("events").EventEmitter;
|
||||||
|
var utils = require("../utils");
|
||||||
|
var EventTimeline = require("./event-timeline");
|
||||||
|
|
||||||
|
// var DEBUG = false;
|
||||||
|
var DEBUG = true;
|
||||||
|
|
||||||
|
if (DEBUG) {
|
||||||
|
// using bind means that we get to keep useful line numbers in the console
|
||||||
|
var debuglog = console.log.bind(console);
|
||||||
|
} else {
|
||||||
|
var debuglog = function() {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a set of EventTimeline objects, typically on behalf of a given
|
||||||
|
* room. A room may have multiple EventTimelineSets for different levels
|
||||||
|
* of filtering. The global notification list is also an EventTimelineSet, but
|
||||||
|
* lacks a room.
|
||||||
|
*
|
||||||
|
* <p>This is an ordered sequence of timelines, which may or may not
|
||||||
|
* be continuous. Each timeline lists a series of events, as well as tracking
|
||||||
|
* the room state at the start and the end of the timeline (if appropriate).
|
||||||
|
* It also tracks forward and backward pagination tokens, as well as containing
|
||||||
|
* links to the next timeline in the sequence.
|
||||||
|
*
|
||||||
|
* <p>There is one special timeline - the 'live' timeline, which represents the
|
||||||
|
* timeline to which events are being added in real-time as they are received
|
||||||
|
* from the /sync API. Note that you should not retain references to this
|
||||||
|
* timeline - even if it is the current timeline right now, it may not remain
|
||||||
|
* so if the server gives us a timeline gap in /sync.
|
||||||
|
*
|
||||||
|
* <p>In order that we can find events from their ids later, we also maintain a
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
function EventTimelineSet(room, opts) {
|
||||||
|
this.room = room;
|
||||||
|
|
||||||
|
this._timelineSupport = Boolean(opts.timelineSupport);
|
||||||
|
this._liveTimeline = new EventTimeline(this);
|
||||||
|
|
||||||
|
// just a list - *not* ordered.
|
||||||
|
this._timelines = [this._liveTimeline];
|
||||||
|
this._eventIdToTimeline = {};
|
||||||
|
|
||||||
|
this._filter = opts.filter || null;
|
||||||
|
}
|
||||||
|
utils.inherits(EventTimelineSet, EventEmitter);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the filter object this timeline set is filtered on, if any
|
||||||
|
* @return {?Filter} the optional filter for this timelineSet
|
||||||
|
*/
|
||||||
|
EventTimelineSet.prototype.getFilter = function() {
|
||||||
|
return this._filter;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the filter object this timeline set is filtered on
|
||||||
|
* (passed to the server when paginating via /messages).
|
||||||
|
* @param {Filter} filter the filter for this timelineSet
|
||||||
|
*/
|
||||||
|
EventTimelineSet.prototype.setFilter = function(filter) {
|
||||||
|
this._filter = filter;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the list of pending sent events for this timelineSet's room, filtered
|
||||||
|
* by the timelineSet's filter if appropriate.
|
||||||
|
*
|
||||||
|
* @return {module:models/event.MatrixEvent[]} A list of the sent events
|
||||||
|
* waiting for remote echo.
|
||||||
|
*
|
||||||
|
* @throws If <code>opts.pendingEventOrdering</code> was not 'detached'
|
||||||
|
*/
|
||||||
|
EventTimelineSet.prototype.getPendingEvents = function() {
|
||||||
|
if (!this.room) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._filter) {
|
||||||
|
return this._filter.filterRoomTimeline(this.room.getPendingEvents());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return this.room.getPendingEvents();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the live timeline for this room.
|
||||||
|
*
|
||||||
|
* @return {module:models/event-timeline~EventTimeline} live timeline
|
||||||
|
*/
|
||||||
|
EventTimelineSet.prototype.getLiveTimeline = function() {
|
||||||
|
return this._liveTimeline;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the timeline (if any) this event is in.
|
||||||
|
* @param {String} eventId the eventId being sought
|
||||||
|
* @return {module:models/event-timeline~EventTimeline} timeline
|
||||||
|
*/
|
||||||
|
EventTimelineSet.prototype.eventIdToTimeline = function(eventId) {
|
||||||
|
return this._eventIdToTimeline[eventId];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Track a new event as if it were in the same timeline as an old event,
|
||||||
|
* replacing it.
|
||||||
|
* @param {String} oldEventId event ID of the original event
|
||||||
|
* @param {String} newEventId event ID of the replacement event
|
||||||
|
*/
|
||||||
|
EventTimelineSet.prototype.replaceEventId = function(oldEventId, newEventId) {
|
||||||
|
var existingTimeline = this._eventIdToTimeline[oldEventId];
|
||||||
|
if (existingTimeline) {
|
||||||
|
delete this._eventIdToTimeline[oldEventId];
|
||||||
|
this._eventIdToTimeline[newEventId] = existingTimeline;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the live timeline, and start a new one.
|
||||||
|
*
|
||||||
|
* <p>This is used when /sync returns a 'limited' timeline.
|
||||||
|
*
|
||||||
|
* @param {string=} backPaginationToken token for back-paginating the new timeline
|
||||||
|
* @param {?bool} flush Whether to flush the non-live timelines too.
|
||||||
|
*
|
||||||
|
* @fires module:client~MatrixClient#event:"Room.timelineReset"
|
||||||
|
*/
|
||||||
|
EventTimelineSet.prototype.resetLiveTimeline = function(backPaginationToken, flush) {
|
||||||
|
var newTimeline;
|
||||||
|
|
||||||
|
if (!this._timelineSupport || flush) {
|
||||||
|
// if timeline support is disabled, forget about the old timelines
|
||||||
|
newTimeline = new EventTimeline(this);
|
||||||
|
this._timelines = [newTimeline];
|
||||||
|
this._eventIdToTimeline = {};
|
||||||
|
} else {
|
||||||
|
newTimeline = this.addTimeline();
|
||||||
|
}
|
||||||
|
|
||||||
|
// initialise the state in the new timeline from our last known state
|
||||||
|
var evMap = this._liveTimeline.getState(EventTimeline.FORWARDS).events;
|
||||||
|
var events = [];
|
||||||
|
for (var evtype in evMap) {
|
||||||
|
if (!evMap.hasOwnProperty(evtype)) { continue; }
|
||||||
|
for (var stateKey in evMap[evtype]) {
|
||||||
|
if (!evMap[evtype].hasOwnProperty(stateKey)) { continue; }
|
||||||
|
events.push(evMap[evtype][stateKey]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newTimeline.initialiseState(events);
|
||||||
|
|
||||||
|
// make sure we set the pagination token before firing timelineReset,
|
||||||
|
// otherwise clients which start back-paginating will fail, and then get
|
||||||
|
// stuck without realising that they *can* back-paginate.
|
||||||
|
newTimeline.setPaginationToken(backPaginationToken, EventTimeline.BACKWARDS);
|
||||||
|
|
||||||
|
this._liveTimeline = newTimeline;
|
||||||
|
this.emit("Room.timelineReset", this.room, this);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the timeline which contains the given event, if any
|
||||||
|
*
|
||||||
|
* @param {string} eventId event ID to look for
|
||||||
|
* @return {?module:models/event-timeline~EventTimeline} timeline containing
|
||||||
|
* the given event, or null if unknown
|
||||||
|
*/
|
||||||
|
EventTimelineSet.prototype.getTimelineForEvent = function(eventId) {
|
||||||
|
var res = this._eventIdToTimeline[eventId];
|
||||||
|
return (res === undefined) ? null : res;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an event which is stored in our timelines
|
||||||
|
*
|
||||||
|
* @param {string} eventId event ID to look for
|
||||||
|
* @return {?module:models/event~MatrixEvent} the given event, or undefined if unknown
|
||||||
|
*/
|
||||||
|
EventTimelineSet.prototype.findEventById = function(eventId) {
|
||||||
|
var tl = this.getTimelineForEvent(eventId);
|
||||||
|
if (!tl) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return utils.findElement(tl.getEvents(),
|
||||||
|
function(ev) { return ev.getId() == eventId; });
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new timeline to this timeline list
|
||||||
|
*
|
||||||
|
* @return {module:models/event-timeline~EventTimeline} newly-created timeline
|
||||||
|
*/
|
||||||
|
EventTimelineSet.prototype.addTimeline = function() {
|
||||||
|
if (!this._timelineSupport) {
|
||||||
|
throw new Error("timeline support is disabled. Set the 'timelineSupport'" +
|
||||||
|
" parameter to true when creating MatrixClient to enable" +
|
||||||
|
" it.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var timeline = new EventTimeline(this);
|
||||||
|
this._timelines.push(timeline);
|
||||||
|
return timeline;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add events to a timeline
|
||||||
|
*
|
||||||
|
* <p>Will fire "Room.timeline" for each event added.
|
||||||
|
*
|
||||||
|
* @param {MatrixEvent[]} events A list of events to add.
|
||||||
|
*
|
||||||
|
* @param {boolean} toStartOfTimeline True to add these events to the start
|
||||||
|
* (oldest) instead of the end (newest) of the timeline. If true, the oldest
|
||||||
|
* event will be the <b>last</b> element of 'events'.
|
||||||
|
*
|
||||||
|
* @param {module:models/event-timeline~EventTimeline} timeline timeline to
|
||||||
|
* add events to.
|
||||||
|
*
|
||||||
|
* @param {string=} paginationToken token for the next batch of events
|
||||||
|
*
|
||||||
|
* @fires module:client~MatrixClient#event:"Room.timeline"
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
EventTimelineSet.prototype.addEventsToTimeline = function(events, toStartOfTimeline,
|
||||||
|
timeline, paginationToken) {
|
||||||
|
if (!timeline) {
|
||||||
|
throw new Error(
|
||||||
|
"'timeline' not specified for EventTimelineSet.addEventsToTimeline"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!toStartOfTimeline && timeline == this._liveTimeline) {
|
||||||
|
throw new Error(
|
||||||
|
"EventTimelineSet.addEventsToTimeline cannot be used for adding events to " +
|
||||||
|
"the live timeline - use Room.addLiveEvents instead"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._filter) {
|
||||||
|
events = this._filter.filterRoomTimeline(events);
|
||||||
|
if (!events.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var direction = toStartOfTimeline ? EventTimeline.BACKWARDS :
|
||||||
|
EventTimeline.FORWARDS;
|
||||||
|
var inverseDirection = toStartOfTimeline ? EventTimeline.FORWARDS :
|
||||||
|
EventTimeline.BACKWARDS;
|
||||||
|
|
||||||
|
// Adding events to timelines can be quite complicated. The following
|
||||||
|
// illustrates some of the corner-cases.
|
||||||
|
//
|
||||||
|
// Let's say we start by knowing about four timelines. timeline3 and
|
||||||
|
// timeline4 are neighbours:
|
||||||
|
//
|
||||||
|
// timeline1 timeline2 timeline3 timeline4
|
||||||
|
// [M] [P] [S] <------> [T]
|
||||||
|
//
|
||||||
|
// Now we paginate timeline1, and get the following events from the server:
|
||||||
|
// [M, N, P, R, S, T, U].
|
||||||
|
//
|
||||||
|
// 1. First, we ignore event M, since we already know about it.
|
||||||
|
//
|
||||||
|
// 2. Next, we append N to timeline 1.
|
||||||
|
//
|
||||||
|
// 3. Next, we don't add event P, since we already know about it,
|
||||||
|
// but we do link together the timelines. We now have:
|
||||||
|
//
|
||||||
|
// timeline1 timeline2 timeline3 timeline4
|
||||||
|
// [M, N] <---> [P] [S] <------> [T]
|
||||||
|
//
|
||||||
|
// 4. Now we add event R to timeline2:
|
||||||
|
//
|
||||||
|
// timeline1 timeline2 timeline3 timeline4
|
||||||
|
// [M, N] <---> [P, R] [S] <------> [T]
|
||||||
|
//
|
||||||
|
// Note that we have switched the timeline we are working on from
|
||||||
|
// timeline1 to timeline2.
|
||||||
|
//
|
||||||
|
// 5. We ignore event S, but again join the timelines:
|
||||||
|
//
|
||||||
|
// timeline1 timeline2 timeline3 timeline4
|
||||||
|
// [M, N] <---> [P, R] <---> [S] <------> [T]
|
||||||
|
//
|
||||||
|
// 6. We ignore event T, and the timelines are already joined, so there
|
||||||
|
// is nothing to do.
|
||||||
|
//
|
||||||
|
// 7. Finally, we add event U to timeline4:
|
||||||
|
//
|
||||||
|
// timeline1 timeline2 timeline3 timeline4
|
||||||
|
// [M, N] <---> [P, R] <---> [S] <------> [T, U]
|
||||||
|
//
|
||||||
|
// The important thing to note in the above is what happened when we
|
||||||
|
// already knew about a given event:
|
||||||
|
//
|
||||||
|
// - if it was appropriate, we joined up the timelines (steps 3, 5).
|
||||||
|
// - in any case, we started adding further events to the timeline which
|
||||||
|
// contained the event we knew about (steps 3, 5, 6).
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// So much for adding events to the timeline. But what do we want to do
|
||||||
|
// with the pagination token?
|
||||||
|
//
|
||||||
|
// In the case above, we will be given a pagination token which tells us how to
|
||||||
|
// get events beyond 'U' - in this case, it makes sense to store this
|
||||||
|
// against timeline4. But what if timeline4 already had 'U' and beyond? in
|
||||||
|
// that case, our best bet is to throw away the pagination token we were
|
||||||
|
// given and stick with whatever token timeline4 had previously. In short,
|
||||||
|
// we want to only store the pagination token if the last event we receive
|
||||||
|
// is one we didn't previously know about.
|
||||||
|
//
|
||||||
|
// We make an exception for this if it turns out that we already knew about
|
||||||
|
// *all* of the events, and we weren't able to join up any timelines. When
|
||||||
|
// that happens, it means our existing pagination token is faulty, since it
|
||||||
|
// is only telling us what we already know. Rather than repeatedly
|
||||||
|
// paginating with the same token, we might as well use the new pagination
|
||||||
|
// token in the hope that we eventually work our way out of the mess.
|
||||||
|
|
||||||
|
var didUpdate = false;
|
||||||
|
var lastEventWasNew = false;
|
||||||
|
for (var i = 0; i < events.length; i++) {
|
||||||
|
var event = events[i];
|
||||||
|
var eventId = event.getId();
|
||||||
|
|
||||||
|
var existingTimeline = this._eventIdToTimeline[eventId];
|
||||||
|
|
||||||
|
if (!existingTimeline) {
|
||||||
|
// we don't know about this event yet. Just add it to the timeline.
|
||||||
|
this.addEventToTimeline(event, timeline, toStartOfTimeline);
|
||||||
|
lastEventWasNew = true;
|
||||||
|
didUpdate = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastEventWasNew = false;
|
||||||
|
|
||||||
|
if (existingTimeline == timeline) {
|
||||||
|
debuglog("Event " + eventId + " already in timeline " + timeline);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var neighbour = timeline.getNeighbouringTimeline(direction);
|
||||||
|
if (neighbour) {
|
||||||
|
// this timeline already has a neighbour in the relevant direction;
|
||||||
|
// let's assume the timelines are already correctly linked up, and
|
||||||
|
// skip over to it.
|
||||||
|
//
|
||||||
|
// there's probably some edge-case here where we end up with an
|
||||||
|
// event which is in a timeline a way down the chain, and there is
|
||||||
|
// a break in the chain somewhere. But I can't really imagine how
|
||||||
|
// that would happen, so I'm going to ignore it for now.
|
||||||
|
//
|
||||||
|
if (existingTimeline == neighbour) {
|
||||||
|
debuglog("Event " + eventId + " in neighbouring timeline - " +
|
||||||
|
"switching to " + existingTimeline);
|
||||||
|
} else {
|
||||||
|
debuglog("Event " + eventId + " already in a different " +
|
||||||
|
"timeline " + existingTimeline);
|
||||||
|
}
|
||||||
|
timeline = existingTimeline;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// time to join the timelines.
|
||||||
|
console.info("Already have timeline for " + eventId +
|
||||||
|
" - joining timeline " + timeline + " to " +
|
||||||
|
existingTimeline);
|
||||||
|
timeline.setNeighbouringTimeline(existingTimeline, direction);
|
||||||
|
existingTimeline.setNeighbouringTimeline(timeline, inverseDirection);
|
||||||
|
timeline = existingTimeline;
|
||||||
|
didUpdate = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// see above - if the last event was new to us, or if we didn't find any
|
||||||
|
// new information, we update the pagination token for whatever
|
||||||
|
// timeline we ended up on.
|
||||||
|
if (lastEventWasNew || !didUpdate) {
|
||||||
|
timeline.setPaginationToken(paginationToken, direction);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an event to the end of this live timeline.
|
||||||
|
*
|
||||||
|
* @param {MatrixEvent} event Event to be added
|
||||||
|
* @param {string?} duplicateStrategy 'ignore' or 'replace'
|
||||||
|
*/
|
||||||
|
EventTimelineSet.prototype.addLiveEvent = function(event, duplicateStrategy) {
|
||||||
|
if (this._filter) {
|
||||||
|
var events = this._filter.filterRoomTimeline([event]);
|
||||||
|
if (!events.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var timeline = this._eventIdToTimeline[event.getId()];
|
||||||
|
if (timeline) {
|
||||||
|
if (duplicateStrategy === "replace") {
|
||||||
|
debuglog("EventTimelineSet.addLiveEvent: replacing duplicate event " +
|
||||||
|
event.getId());
|
||||||
|
var tlEvents = timeline.getEvents();
|
||||||
|
for (var j = 0; j < tlEvents.length; j++) {
|
||||||
|
if (tlEvents[j].getId() === event.getId()) {
|
||||||
|
// still need to set the right metadata on this event
|
||||||
|
EventTimeline.setEventMetadata(
|
||||||
|
event,
|
||||||
|
timeline.getState(EventTimeline.FORWARDS),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!tlEvents[j].encryptedType) {
|
||||||
|
tlEvents[j] = event;
|
||||||
|
}
|
||||||
|
|
||||||
|
// XXX: we need to fire an event when this happens.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debuglog("EventTimelineSet.addLiveEvent: ignoring duplicate event " +
|
||||||
|
event.getId());
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.addEventToTimeline(event, this._liveTimeline, false);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add event to the given timeline, and emit Room.timeline. Assumes
|
||||||
|
* we have already checked we don't know about this event.
|
||||||
|
*
|
||||||
|
* Will fire "Room.timeline" for each event added.
|
||||||
|
*
|
||||||
|
* @param {MatrixEvent} event
|
||||||
|
* @param {EventTimeline} timeline
|
||||||
|
* @param {boolean} toStartOfTimeline
|
||||||
|
*
|
||||||
|
* @fires module:client~MatrixClient#event:"Room.timeline"
|
||||||
|
*/
|
||||||
|
EventTimelineSet.prototype.addEventToTimeline = function(event, timeline,
|
||||||
|
toStartOfTimeline) {
|
||||||
|
var eventId = event.getId();
|
||||||
|
timeline.addEvent(event, toStartOfTimeline);
|
||||||
|
this._eventIdToTimeline[eventId] = timeline;
|
||||||
|
|
||||||
|
var data = {
|
||||||
|
timeline: timeline,
|
||||||
|
liveEvent: !toStartOfTimeline && timeline == this._liveTimeline,
|
||||||
|
};
|
||||||
|
this.emit("Room.timeline", event, this.room,
|
||||||
|
Boolean(toStartOfTimeline), false, data);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces event with ID oldEventId with one with newEventId, if oldEventId is
|
||||||
|
* recognised. Otherwise, add to the live timeline. Used to handle remote echos.
|
||||||
|
*
|
||||||
|
* @param {MatrixEvent} localEvent the new event to be added to the timeline
|
||||||
|
* @param {String} oldEventId the ID of the original event
|
||||||
|
* @param {boolean} newEventId the ID of the replacement event
|
||||||
|
*
|
||||||
|
* @fires module:client~MatrixClient#event:"Room.timeline"
|
||||||
|
*/
|
||||||
|
EventTimelineSet.prototype.handleRemoteEcho = function(localEvent, oldEventId,
|
||||||
|
newEventId) {
|
||||||
|
// XXX: why don't we infer newEventId from localEvent?
|
||||||
|
var existingTimeline = this._eventIdToTimeline[oldEventId];
|
||||||
|
if (existingTimeline) {
|
||||||
|
delete this._eventIdToTimeline[oldEventId];
|
||||||
|
this._eventIdToTimeline[newEventId] = existingTimeline;
|
||||||
|
} else {
|
||||||
|
if (this._filter) {
|
||||||
|
if (this._filter.filterRoomTimeline([localEvent]).length) {
|
||||||
|
this.addEventToTimeline(localEvent, this._liveTimeline, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.addEventToTimeline(localEvent, this._liveTimeline, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a single event from this room.
|
||||||
|
*
|
||||||
|
* @param {String} eventId The id of the event to remove
|
||||||
|
*
|
||||||
|
* @return {?MatrixEvent} the removed event, or null if the event was not found
|
||||||
|
* in this room.
|
||||||
|
*/
|
||||||
|
EventTimelineSet.prototype.removeEvent = function(eventId) {
|
||||||
|
var timeline = this._eventIdToTimeline[eventId];
|
||||||
|
if (!timeline) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var removed = timeline.removeEvent(eventId);
|
||||||
|
if (removed) {
|
||||||
|
delete this._eventIdToTimeline[eventId];
|
||||||
|
var data = {
|
||||||
|
timeline: timeline,
|
||||||
|
};
|
||||||
|
this.emit("Room.timeline", removed, this.room, undefined, true, data);
|
||||||
|
}
|
||||||
|
return removed;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine where two events appear in the timeline relative to one another
|
||||||
|
*
|
||||||
|
* @param {string} eventId1 The id of the first event
|
||||||
|
* @param {string} eventId2 The id of the second event
|
||||||
|
|
||||||
|
* @return {?number} a number less than zero if eventId1 precedes eventId2, and
|
||||||
|
* greater than zero if eventId1 succeeds eventId2. zero if they are the
|
||||||
|
* same event; null if we can't tell (either because we don't know about one
|
||||||
|
* of the events, or because they are in separate timelines which don't join
|
||||||
|
* up).
|
||||||
|
*/
|
||||||
|
EventTimelineSet.prototype.compareEventOrdering = function(eventId1, eventId2) {
|
||||||
|
if (eventId1 == eventId2) {
|
||||||
|
// optimise this case
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var timeline1 = this._eventIdToTimeline[eventId1];
|
||||||
|
var timeline2 = this._eventIdToTimeline[eventId2];
|
||||||
|
|
||||||
|
if (timeline1 === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (timeline2 === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeline1 === timeline2) {
|
||||||
|
// both events are in the same timeline - figure out their
|
||||||
|
// relative indices
|
||||||
|
var idx1, idx2;
|
||||||
|
var events = timeline1.getEvents();
|
||||||
|
for (var idx = 0; idx < events.length &&
|
||||||
|
(idx1 === undefined || idx2 === undefined); idx++) {
|
||||||
|
var evId = events[idx].getId();
|
||||||
|
if (evId == eventId1) {
|
||||||
|
idx1 = idx;
|
||||||
|
}
|
||||||
|
if (evId == eventId2) {
|
||||||
|
idx2 = idx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return idx1 - idx2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// the events are in different timelines. Iterate through the
|
||||||
|
// linkedlist to see which comes first.
|
||||||
|
|
||||||
|
// first work forwards from timeline1
|
||||||
|
var tl = timeline1;
|
||||||
|
while (tl) {
|
||||||
|
if (tl === timeline2) {
|
||||||
|
// timeline1 is before timeline2
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
tl = tl.getNeighbouringTimeline(EventTimeline.FORWARDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
// now try backwards from timeline1
|
||||||
|
tl = timeline1;
|
||||||
|
while (tl) {
|
||||||
|
if (tl === timeline2) {
|
||||||
|
// timeline2 is before timeline1
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
tl = tl.getNeighbouringTimeline(EventTimeline.BACKWARDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
// the timelines are not contiguous.
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The EventTimelineSet class.
|
||||||
|
*/
|
||||||
|
module.exports = EventTimelineSet;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires whenever the timeline in a room is updated.
|
||||||
|
* @event module:client~MatrixClient#"Room.timeline"
|
||||||
|
* @param {MatrixEvent} event The matrix event which caused this event to fire.
|
||||||
|
* @param {?Room} room The room, if any, whose timeline was updated.
|
||||||
|
* @param {boolean} toStartOfTimeline True if this event was added to the start
|
||||||
|
* @param {boolean} removed True if this event has just been removed from the timeline
|
||||||
|
* (beginning; oldest) of the timeline e.g. due to pagination.
|
||||||
|
*
|
||||||
|
* @param {object} data more data about the event
|
||||||
|
*
|
||||||
|
* @param {module:event-timeline.EventTimeline} data.timeline the timeline the
|
||||||
|
* event was added to/removed from
|
||||||
|
*
|
||||||
|
* @param {boolean} data.liveEvent true if the event was a real-time event
|
||||||
|
* added to the end of the live timeline
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* matrixClient.on("Room.timeline",
|
||||||
|
* function(event, room, toStartOfTimeline, removed, data) {
|
||||||
|
* if (!toStartOfTimeline && data.liveEvent) {
|
||||||
|
* var messageToAppend = room.timeline.[room.timeline.length - 1];
|
||||||
|
* }
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires whenever the live timeline in a room is reset.
|
||||||
|
*
|
||||||
|
* When we get a 'limited' sync (for example, after a network outage), we reset
|
||||||
|
* the live timeline to be empty before adding the recent events to the new
|
||||||
|
* timeline. This event is fired after the timeline is reset, and before the
|
||||||
|
* new events are added.
|
||||||
|
*
|
||||||
|
* @event module:client~MatrixClient#"Room.timelineReset"
|
||||||
|
* @param {Room} room The room whose live timeline was reset, if any
|
||||||
|
* @param {EventTimelineSet} timelineSet timelineSet room whose live timeline was reset
|
||||||
|
*/
|
||||||
@@ -25,16 +25,17 @@ var MatrixEvent = require("./event").MatrixEvent;
|
|||||||
* <p>Once a timeline joins up with its neighbour, they are linked together into a
|
* <p>Once a timeline joins up with its neighbour, they are linked together into a
|
||||||
* doubly-linked list.
|
* doubly-linked list.
|
||||||
*
|
*
|
||||||
* @param {string} roomId the ID of the room where this timeline came from
|
* @param {EventTimelineSet} eventTimelineSet the set of timelines this is part of
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
function EventTimeline(roomId) {
|
function EventTimeline(eventTimelineSet) {
|
||||||
this._roomId = roomId;
|
this._eventTimelineSet = eventTimelineSet;
|
||||||
|
this._roomId = eventTimelineSet.room ? eventTimelineSet.room.roomId : null;
|
||||||
this._events = [];
|
this._events = [];
|
||||||
this._baseIndex = 0;
|
this._baseIndex = 0;
|
||||||
this._startState = new RoomState(roomId);
|
this._startState = new RoomState(this._roomId);
|
||||||
this._startState.paginationToken = null;
|
this._startState.paginationToken = null;
|
||||||
this._endState = new RoomState(roomId);
|
this._endState = new RoomState(this._roomId);
|
||||||
this._endState.paginationToken = null;
|
this._endState.paginationToken = null;
|
||||||
|
|
||||||
this._prevTimeline = null;
|
this._prevTimeline = null;
|
||||||
@@ -43,7 +44,7 @@ function EventTimeline(roomId) {
|
|||||||
// this is used by client.js
|
// this is used by client.js
|
||||||
this._paginationRequests = {'b': null, 'f': null};
|
this._paginationRequests = {'b': null, 'f': null};
|
||||||
|
|
||||||
this._name = roomId + ":" + new Date().toISOString();
|
this._name = this._roomId + ":" + new Date().toISOString();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -91,6 +92,22 @@ EventTimeline.prototype.getRoomId = function() {
|
|||||||
return this._roomId;
|
return this._roomId;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the filter for this timeline's timelineSet (if any)
|
||||||
|
* @return {Filter} filter
|
||||||
|
*/
|
||||||
|
EventTimeline.prototype.getFilter = function() {
|
||||||
|
return this._eventTimelineSet.getFilter();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the timelineSet for this timeline
|
||||||
|
* @return {EventTimelineSet} timelineSet
|
||||||
|
*/
|
||||||
|
EventTimeline.prototype.getTimelineSet = function() {
|
||||||
|
return this._eventTimelineSet;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the base index.
|
* Get the base index.
|
||||||
*
|
*
|
||||||
@@ -217,23 +234,29 @@ EventTimeline.prototype.setNeighbouringTimeline = function(neighbour, direction)
|
|||||||
EventTimeline.prototype.addEvent = function(event, atStart) {
|
EventTimeline.prototype.addEvent = function(event, atStart) {
|
||||||
var stateContext = atStart ? this._startState : this._endState;
|
var stateContext = atStart ? this._startState : this._endState;
|
||||||
|
|
||||||
setEventMetadata(event, stateContext, atStart);
|
// only call setEventMetadata on the unfiltered timelineSets
|
||||||
|
var timelineSet = this.getTimelineSet();
|
||||||
|
if (timelineSet.room &&
|
||||||
|
timelineSet.room.getUnfilteredTimelineSet() === timelineSet)
|
||||||
|
{
|
||||||
|
EventTimeline.setEventMetadata(event, stateContext, atStart);
|
||||||
|
|
||||||
// modify state
|
// modify state
|
||||||
if (event.isState()) {
|
if (event.isState()) {
|
||||||
stateContext.setStateEvents([event]);
|
stateContext.setStateEvents([event]);
|
||||||
// it is possible that the act of setting the state event means we
|
// it is possible that the act of setting the state event means we
|
||||||
// can set more metadata (specifically sender/target props), so try
|
// can set more metadata (specifically sender/target props), so try
|
||||||
// it again if the prop wasn't previously set. It may also mean that
|
// it again if the prop wasn't previously set. It may also mean that
|
||||||
// the sender/target is updated (if the event set was a room member event)
|
// the sender/target is updated (if the event set was a room member event)
|
||||||
// so we want to use the *updated* member (new avatar/name) instead.
|
// so we want to use the *updated* member (new avatar/name) instead.
|
||||||
//
|
//
|
||||||
// However, we do NOT want to do this on member events if we're going
|
// However, we do NOT want to do this on member events if we're going
|
||||||
// back in time, else we'll set the .sender value for BEFORE the given
|
// back in time, else we'll set the .sender value for BEFORE the given
|
||||||
// member event, whereas we want to set the .sender value for the ACTUAL
|
// member event, whereas we want to set the .sender value for the ACTUAL
|
||||||
// member event itself.
|
// member event itself.
|
||||||
if (!event.sender || (event.getType() === "m.room.member" && !atStart)) {
|
if (!event.sender || (event.getType() === "m.room.member" && !atStart)) {
|
||||||
setEventMetadata(event, stateContext, atStart);
|
EventTimeline.setEventMetadata(event, stateContext, atStart);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,7 +274,14 @@ EventTimeline.prototype.addEvent = function(event, atStart) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function setEventMetadata(event, stateContext, toStartOfTimeline) {
|
/**
|
||||||
|
* Static helper method to set sender and target properties
|
||||||
|
*
|
||||||
|
* @param {MatrixEvent} event the event whose metadata is to be set
|
||||||
|
* @param {RoomState} stateContext the room state to be queried
|
||||||
|
* @param {bool} toStartOfTimeline if true the event's forwardLooking flag is set false
|
||||||
|
*/
|
||||||
|
EventTimeline.setEventMetadata = function(event, stateContext, toStartOfTimeline) {
|
||||||
// set sender and target properties
|
// set sender and target properties
|
||||||
event.sender = stateContext.getSentinelMember(
|
event.sender = stateContext.getSentinelMember(
|
||||||
event.getSender()
|
event.getSender()
|
||||||
@@ -270,7 +300,7 @@ function setEventMetadata(event, stateContext, toStartOfTimeline) {
|
|||||||
event.forwardLooking = false;
|
event.forwardLooking = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove an event from the timeline
|
* Remove an event from the timeline
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ module.exports.MatrixEvent = function MatrixEvent(event, clearEvent) {
|
|||||||
this.forwardLooking = true;
|
this.forwardLooking = true;
|
||||||
|
|
||||||
this._clearEvent = clearEvent || {};
|
this._clearEvent = clearEvent || {};
|
||||||
|
this._pushActions = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports.MatrixEvent.prototype = {
|
module.exports.MatrixEvent.prototype = {
|
||||||
@@ -232,12 +233,28 @@ module.exports.MatrixEvent.prototype = {
|
|||||||
return Boolean(this._clearEvent.type);
|
return Boolean(this._clearEvent.type);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The curve25519 key that sent this event
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
getSenderKey: function() {
|
getSenderKey: function() {
|
||||||
if (!this.isEncrypted()) {
|
return this.getKeysProved().curve25519 || null;
|
||||||
return null;
|
},
|
||||||
}
|
|
||||||
var c = this.getWireContent();
|
/**
|
||||||
return c.sender_key;
|
* The keys that must have been owned by the sender of this encrypted event.
|
||||||
|
* @return {object}
|
||||||
|
*/
|
||||||
|
getKeysProved: function() {
|
||||||
|
return this._clearEvent.keysProved || {};
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The additional keys the sender of this encrypted event claims to possess
|
||||||
|
* @return {object}
|
||||||
|
*/
|
||||||
|
getKeysClaimed: function() {
|
||||||
|
return this._clearEvent.keysClaimed || {};
|
||||||
},
|
},
|
||||||
|
|
||||||
getUnsigned: function() {
|
getUnsigned: function() {
|
||||||
@@ -294,6 +311,24 @@ module.exports.MatrixEvent.prototype = {
|
|||||||
isRedacted: function() {
|
isRedacted: function() {
|
||||||
return Boolean(this.getUnsigned().redacted_because);
|
return Boolean(this.getUnsigned().redacted_because);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the push actions, if known, for this event
|
||||||
|
*
|
||||||
|
* @return {?Object} push actions
|
||||||
|
*/
|
||||||
|
getPushActions: function() {
|
||||||
|
return this._pushActions;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the push actions for this event.
|
||||||
|
*
|
||||||
|
* @param {Object} pushActions push actions
|
||||||
|
*/
|
||||||
|
setPushActions: function(pushActions) {
|
||||||
|
this._pushActions = pushActions;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ var RoomMember = require("./room-member");
|
|||||||
/**
|
/**
|
||||||
* Construct room state.
|
* Construct room state.
|
||||||
* @constructor
|
* @constructor
|
||||||
* @param {string} roomId Required. The ID of the room which has this state.
|
* @param {?string} roomId Optional. The ID of the room which has this state.
|
||||||
|
* If none is specified it just tracks paginationTokens, useful for notifTimelineSet
|
||||||
* @prop {Object.<string, RoomMember>} members The room member dictionary, keyed
|
* @prop {Object.<string, RoomMember>} members The room member dictionary, keyed
|
||||||
* on the user's ID.
|
* on the user's ID.
|
||||||
* @prop {Object.<string, Object.<string, MatrixEvent>>} events The state
|
* @prop {Object.<string, Object.<string, MatrixEvent>>} events The state
|
||||||
|
|||||||
@@ -25,17 +25,7 @@ var MatrixEvent = require("./event").MatrixEvent;
|
|||||||
var utils = require("../utils");
|
var utils = require("../utils");
|
||||||
var ContentRepo = require("../content-repo");
|
var ContentRepo = require("../content-repo");
|
||||||
var EventTimeline = require("./event-timeline");
|
var EventTimeline = require("./event-timeline");
|
||||||
|
var EventTimelineSet = require("./event-timeline-set");
|
||||||
|
|
||||||
// var DEBUG = false;
|
|
||||||
var DEBUG = true;
|
|
||||||
|
|
||||||
if (DEBUG) {
|
|
||||||
// using bind means that we get to keep useful line numbers in the console
|
|
||||||
var debuglog = console.log.bind(console);
|
|
||||||
} else {
|
|
||||||
var debuglog = function() {};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function synthesizeReceipt(userId, event, receiptType) {
|
function synthesizeReceipt(userId, event, receiptType) {
|
||||||
@@ -159,13 +149,18 @@ function Room(roomId, opts) {
|
|||||||
|
|
||||||
this._notificationCounts = {};
|
this._notificationCounts = {};
|
||||||
|
|
||||||
this._liveTimeline = new EventTimeline(this.roomId);
|
// all our per-room timeline sets. the first one is the unfiltered ones;
|
||||||
|
// the subsequent ones are the filtered ones in no particular order.
|
||||||
|
this._timelineSets = [new EventTimelineSet(this, opts)];
|
||||||
|
reEmit(this, this.getUnfilteredTimelineSet(),
|
||||||
|
["Room.timeline", "Room.timelineReset"]);
|
||||||
|
|
||||||
this._fixUpLegacyTimelineFields();
|
this._fixUpLegacyTimelineFields();
|
||||||
|
|
||||||
// just a list - *not* ordered.
|
// any filtered timeline sets we're maintaining for this room
|
||||||
this._timelines = [this._liveTimeline];
|
this._filteredTimelineSets = {
|
||||||
this._eventIdToTimeline = {};
|
// filter_id: timelineSet
|
||||||
this._timelineSupport = Boolean(opts.timelineSupport);
|
};
|
||||||
|
|
||||||
if (this._opts.pendingEventOrdering == "detached") {
|
if (this._opts.pendingEventOrdering == "detached") {
|
||||||
this._pendingEventList = [];
|
this._pendingEventList = [];
|
||||||
@@ -191,57 +186,29 @@ Room.prototype.getPendingEvents = function() {
|
|||||||
return this._pendingEventList;
|
return this._pendingEventList;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the live timeline for this room.
|
* Get the live unfiltered timeline for this room.
|
||||||
*
|
*
|
||||||
* @return {module:models/event-timeline~EventTimeline} live timeline
|
* @return {module:models/event-timeline~EventTimeline} live timeline
|
||||||
*/
|
*/
|
||||||
Room.prototype.getLiveTimeline = function() {
|
Room.prototype.getLiveTimeline = function() {
|
||||||
return this._liveTimeline;
|
return this.getUnfilteredTimelineSet().getLiveTimeline();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset the live timeline, and start a new one.
|
* Reset the live timeline of all timelineSets, and start new ones.
|
||||||
*
|
*
|
||||||
* <p>This is used when /sync returns a 'limited' timeline.
|
* <p>This is used when /sync returns a 'limited' timeline.
|
||||||
*
|
*
|
||||||
* @param {string=} backPaginationToken token for back-paginating the new timeline
|
* @param {string=} backPaginationToken token for back-paginating the new timeline
|
||||||
*
|
|
||||||
* @fires module:client~MatrixClient#event:"Room.timelineReset"
|
|
||||||
*/
|
*/
|
||||||
Room.prototype.resetLiveTimeline = function(backPaginationToken) {
|
Room.prototype.resetLiveTimeline = function(backPaginationToken) {
|
||||||
var newTimeline;
|
for (var i = 0; i < this._timelineSets.length; i++) {
|
||||||
|
this._timelineSets[i].resetLiveTimeline(backPaginationToken);
|
||||||
if (!this._timelineSupport) {
|
|
||||||
// if timeline support is disabled, forget about the old timelines
|
|
||||||
newTimeline = new EventTimeline(this.roomId);
|
|
||||||
this._timelines = [newTimeline];
|
|
||||||
this._eventIdToTimeline = {};
|
|
||||||
} else {
|
|
||||||
newTimeline = this.addTimeline();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// initialise the state in the new timeline from our last known state
|
|
||||||
var evMap = this._liveTimeline.getState(EventTimeline.FORWARDS).events;
|
|
||||||
var events = [];
|
|
||||||
for (var evtype in evMap) {
|
|
||||||
if (!evMap.hasOwnProperty(evtype)) { continue; }
|
|
||||||
for (var stateKey in evMap[evtype]) {
|
|
||||||
if (!evMap[evtype].hasOwnProperty(stateKey)) { continue; }
|
|
||||||
events.push(evMap[evtype][stateKey]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
newTimeline.initialiseState(events);
|
|
||||||
|
|
||||||
// make sure we set the pagination token before firing timelineReset,
|
|
||||||
// otherwise clients which start back-paginating will fail, and then get
|
|
||||||
// stuck without realising that they *can* back-paginate.
|
|
||||||
newTimeline.setPaginationToken(backPaginationToken, EventTimeline.BACKWARDS);
|
|
||||||
|
|
||||||
this._liveTimeline = newTimeline;
|
|
||||||
this._fixUpLegacyTimelineFields();
|
this._fixUpLegacyTimelineFields();
|
||||||
this.emit("Room.timelineReset", this);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -254,39 +221,59 @@ Room.prototype._fixUpLegacyTimelineFields = function() {
|
|||||||
// and this.oldState and this.currentState as references to the
|
// and this.oldState and this.currentState as references to the
|
||||||
// state at the start and end of that timeline. These are more
|
// state at the start and end of that timeline. These are more
|
||||||
// for backwards-compatibility than anything else.
|
// for backwards-compatibility than anything else.
|
||||||
this.timeline = this._liveTimeline.getEvents();
|
this.timeline = this.getLiveTimeline().getEvents();
|
||||||
this.oldState = this._liveTimeline.getState(EventTimeline.BACKWARDS);
|
this.oldState = this.getLiveTimeline()
|
||||||
this.currentState = this._liveTimeline.getState(EventTimeline.FORWARDS);
|
.getState(EventTimeline.BACKWARDS);
|
||||||
|
this.currentState = this.getLiveTimeline()
|
||||||
|
.getState(EventTimeline.FORWARDS);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the timeline which contains the given event, if any
|
* Return the timeline sets for this room.
|
||||||
|
* @return {EventTimelineSet[]} array of timeline sets for this room
|
||||||
|
*/
|
||||||
|
Room.prototype.getTimelineSets = function() {
|
||||||
|
return this._timelineSets;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to return the main unfiltered timeline set for this room
|
||||||
|
* @return {EventTimelineSet} room's unfiltered timeline set
|
||||||
|
*/
|
||||||
|
Room.prototype.getUnfilteredTimelineSet = function() {
|
||||||
|
return this._timelineSets[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the timeline which contains the given event from the unfiltered set, if any
|
||||||
*
|
*
|
||||||
* @param {string} eventId event ID to look for
|
* @param {string} eventId event ID to look for
|
||||||
* @return {?module:models/event-timeline~EventTimeline} timeline containing
|
* @return {?module:models/event-timeline~EventTimeline} timeline containing
|
||||||
* the given event, or null if unknown
|
* the given event, or null if unknown
|
||||||
*/
|
*/
|
||||||
Room.prototype.getTimelineForEvent = function(eventId) {
|
Room.prototype.getTimelineForEvent = function(eventId) {
|
||||||
var res = this._eventIdToTimeline[eventId];
|
return this.getUnfilteredTimelineSet().getTimelineForEvent(eventId);
|
||||||
return (res === undefined) ? null : res;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get an event which is stored in our timelines
|
* Add a new timeline to this room's unfiltered timeline set
|
||||||
|
*
|
||||||
|
* @return {module:models/event-timeline~EventTimeline} newly-created timeline
|
||||||
|
*/
|
||||||
|
Room.prototype.addTimeline = function() {
|
||||||
|
return this.getUnfilteredTimelineSet().addTimeline();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an event which is stored in our unfiltered timeline set
|
||||||
*
|
*
|
||||||
* @param {string} eventId event ID to look for
|
* @param {string} eventId event ID to look for
|
||||||
* @return {?module:models/event.MatrixEvent} the given event, or undefined if unknown
|
* @return {?module:models/event.MatrixEvent} the given event, or undefined if unknown
|
||||||
*/
|
*/
|
||||||
Room.prototype.findEventById = function(eventId) {
|
Room.prototype.findEventById = function(eventId) {
|
||||||
var tl = this.getTimelineForEvent(eventId);
|
return this.getUnfilteredTimelineSet().findEventById(eventId);
|
||||||
if (!tl) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return utils.findElement(tl.getEvents(),
|
|
||||||
function(ev) { return ev.getId() == eventId; });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get one of the notification counts for this room
|
* Get one of the notification counts for this room
|
||||||
* @param {String} type The type of notification count to get. default: 'total'
|
* @param {String} type The type of notification count to get. default: 'total'
|
||||||
@@ -379,6 +366,33 @@ Room.prototype.getCanonicalAlias = function() {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add events to a timeline
|
||||||
|
*
|
||||||
|
* <p>Will fire "Room.timeline" for each event added.
|
||||||
|
*
|
||||||
|
* @param {MatrixEvent[]} events A list of events to add.
|
||||||
|
*
|
||||||
|
* @param {boolean} toStartOfTimeline True to add these events to the start
|
||||||
|
* (oldest) instead of the end (newest) of the timeline. If true, the oldest
|
||||||
|
* event will be the <b>last</b> element of 'events'.
|
||||||
|
*
|
||||||
|
* @param {module:models/event-timeline~EventTimeline} timeline timeline to
|
||||||
|
* add events to.
|
||||||
|
*
|
||||||
|
* @param {string=} paginationToken token for the next batch of events
|
||||||
|
*
|
||||||
|
* @fires module:client~MatrixClient#event:"Room.timeline"
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
Room.prototype.addEventsToTimeline = function(events, toStartOfTimeline,
|
||||||
|
timeline, paginationToken) {
|
||||||
|
timeline.getTimelineSet().addEventsToTimeline(
|
||||||
|
events, toStartOfTimeline,
|
||||||
|
timeline, paginationToken
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a member from the current room state.
|
* Get a member from the current room state.
|
||||||
* @param {string} userId The user ID of the member.
|
* @param {string} userId The user ID of the member.
|
||||||
@@ -438,224 +452,72 @@ Room.prototype.getCanonicalAlias = function() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a new timeline to this room
|
* Add a timelineSet for this room with the given filter
|
||||||
*
|
* @param {Filter} filter The filter to be applied to this timelineSet
|
||||||
* @return {module:models/event-timeline~EventTimeline} newly-created timeline
|
* @return {EventTimelineSet} The timelineSet
|
||||||
*/
|
*/
|
||||||
Room.prototype.addTimeline = function() {
|
Room.prototype.getOrCreateFilteredTimelineSet = function(filter) {
|
||||||
if (!this._timelineSupport) {
|
if (this._filteredTimelineSets[filter.filterId]) {
|
||||||
throw new Error("timeline support is disabled. Set the 'timelineSupport'" +
|
return this._filteredTimelineSets[filter.filterId];
|
||||||
" parameter to true when creating MatrixClient to enable" +
|
}
|
||||||
" it.");
|
var opts = Object.assign({ filter: filter }, this._opts);
|
||||||
|
var timelineSet = new EventTimelineSet(this, opts);
|
||||||
|
reEmit(this, timelineSet, ["Room.timeline", "Room.timelineReset"]);
|
||||||
|
this._filteredTimelineSets[filter.filterId] = timelineSet;
|
||||||
|
this._timelineSets.push(timelineSet);
|
||||||
|
|
||||||
|
// populate up the new timelineSet with filtered events from our live
|
||||||
|
// unfiltered timeline.
|
||||||
|
//
|
||||||
|
// XXX: This is risky as our timeline
|
||||||
|
// may have grown huge and so take a long time to filter.
|
||||||
|
// see https://github.com/vector-im/vector-web/issues/2109
|
||||||
|
|
||||||
|
var unfilteredLiveTimeline = this.getLiveTimeline();
|
||||||
|
|
||||||
|
unfilteredLiveTimeline.getEvents().forEach(function(event) {
|
||||||
|
timelineSet.addLiveEvent(event);
|
||||||
|
});
|
||||||
|
|
||||||
|
// find the earliest unfiltered timeline
|
||||||
|
var timeline = unfilteredLiveTimeline;
|
||||||
|
while (timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS)) {
|
||||||
|
timeline = timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
var timeline = new EventTimeline(this.roomId);
|
timelineSet.getLiveTimeline().setPaginationToken(
|
||||||
this._timelines.push(timeline);
|
timeline.getPaginationToken(EventTimeline.BACKWARDS),
|
||||||
return timeline;
|
EventTimeline.BACKWARDS
|
||||||
|
);
|
||||||
|
|
||||||
|
// alternatively, we could try to do something like this to try and re-paginate
|
||||||
|
// in the filtered events from nothing, but Mark says it's an abuse of the API
|
||||||
|
// to do so:
|
||||||
|
//
|
||||||
|
// timelineSet.resetLiveTimeline(
|
||||||
|
// unfilteredLiveTimeline.getPaginationToken(EventTimeline.FORWARDS)
|
||||||
|
// );
|
||||||
|
|
||||||
|
return timelineSet;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add events to a timeline
|
* Forget the timelineSet for this room with the given filter
|
||||||
*
|
|
||||||
* <p>Will fire "Room.timeline" for each event added.
|
|
||||||
*
|
|
||||||
* @param {MatrixEvent[]} events A list of events to add.
|
|
||||||
*
|
|
||||||
* @param {boolean} toStartOfTimeline True to add these events to the start
|
|
||||||
* (oldest) instead of the end (newest) of the timeline. If true, the oldest
|
|
||||||
* event will be the <b>last</b> element of 'events'.
|
|
||||||
*
|
|
||||||
* @param {module:models/event-timeline~EventTimeline} timeline timeline to
|
|
||||||
* add events to.
|
|
||||||
*
|
|
||||||
* @param {string=} paginationToken token for the next batch of events
|
|
||||||
*
|
|
||||||
* @fires module:client~MatrixClient#event:"Room.timeline"
|
|
||||||
*
|
*
|
||||||
|
* @param {Filter} filter the filter whose timelineSet is to be forgotten
|
||||||
*/
|
*/
|
||||||
Room.prototype.addEventsToTimeline = function(events, toStartOfTimeline,
|
Room.prototype.removeFilteredTimelineSet = function(filter) {
|
||||||
timeline, paginationToken) {
|
var timelineSet = this._filteredTimelineSets[filter.filterId];
|
||||||
if (!timeline) {
|
delete this._filteredTimelineSets[filter.filterId];
|
||||||
throw new Error(
|
var i = this._timelineSets.indexOf(timelineSet);
|
||||||
"'timeline' not specified for Room.addEventsToTimeline"
|
if (i > -1) {
|
||||||
);
|
this._timelineSets.splice(i, 1);
|
||||||
}
|
|
||||||
|
|
||||||
if (!toStartOfTimeline && timeline == this._liveTimeline) {
|
|
||||||
throw new Error(
|
|
||||||
"Room.addEventsToTimeline cannot be used for adding events to " +
|
|
||||||
"the live timeline - use Room.addLiveEvents instead"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
var direction = toStartOfTimeline ? EventTimeline.BACKWARDS :
|
|
||||||
EventTimeline.FORWARDS;
|
|
||||||
var inverseDirection = toStartOfTimeline ? EventTimeline.FORWARDS :
|
|
||||||
EventTimeline.BACKWARDS;
|
|
||||||
|
|
||||||
// Adding events to timelines can be quite complicated. The following
|
|
||||||
// illustrates some of the corner-cases.
|
|
||||||
//
|
|
||||||
// Let's say we start by knowing about four timelines. timeline3 and
|
|
||||||
// timeline4 are neighbours:
|
|
||||||
//
|
|
||||||
// timeline1 timeline2 timeline3 timeline4
|
|
||||||
// [M] [P] [S] <------> [T]
|
|
||||||
//
|
|
||||||
// Now we paginate timeline1, and get the following events from the server:
|
|
||||||
// [M, N, P, R, S, T, U].
|
|
||||||
//
|
|
||||||
// 1. First, we ignore event M, since we already know about it.
|
|
||||||
//
|
|
||||||
// 2. Next, we append N to timeline 1.
|
|
||||||
//
|
|
||||||
// 3. Next, we don't add event P, since we already know about it,
|
|
||||||
// but we do link together the timelines. We now have:
|
|
||||||
//
|
|
||||||
// timeline1 timeline2 timeline3 timeline4
|
|
||||||
// [M, N] <---> [P] [S] <------> [T]
|
|
||||||
//
|
|
||||||
// 4. Now we add event R to timeline2:
|
|
||||||
//
|
|
||||||
// timeline1 timeline2 timeline3 timeline4
|
|
||||||
// [M, N] <---> [P, R] [S] <------> [T]
|
|
||||||
//
|
|
||||||
// Note that we have switched the timeline we are working on from
|
|
||||||
// timeline1 to timeline2.
|
|
||||||
//
|
|
||||||
// 5. We ignore event S, but again join the timelines:
|
|
||||||
//
|
|
||||||
// timeline1 timeline2 timeline3 timeline4
|
|
||||||
// [M, N] <---> [P, R] <---> [S] <------> [T]
|
|
||||||
//
|
|
||||||
// 6. We ignore event T, and the timelines are already joined, so there
|
|
||||||
// is nothing to do.
|
|
||||||
//
|
|
||||||
// 7. Finally, we add event U to timeline4:
|
|
||||||
//
|
|
||||||
// timeline1 timeline2 timeline3 timeline4
|
|
||||||
// [M, N] <---> [P, R] <---> [S] <------> [T, U]
|
|
||||||
//
|
|
||||||
// The important thing to note in the above is what happened when we
|
|
||||||
// already knew about a given event:
|
|
||||||
//
|
|
||||||
// - if it was appropriate, we joined up the timelines (steps 3, 5).
|
|
||||||
// - in any case, we started adding further events to the timeline which
|
|
||||||
// contained the event we knew about (steps 3, 5, 6).
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// So much for adding events to the timeline. But what do we want to do
|
|
||||||
// with the pagination token?
|
|
||||||
//
|
|
||||||
// In the case above, we will be given a pagination token which tells us how to
|
|
||||||
// get events beyond 'U' - in this case, it makes sense to store this
|
|
||||||
// against timeline4. But what if timeline4 already had 'U' and beyond? in
|
|
||||||
// that case, our best bet is to throw away the pagination token we were
|
|
||||||
// given and stick with whatever token timeline4 had previously. In short,
|
|
||||||
// we want to only store the pagination token if the last event we receive
|
|
||||||
// is one we didn't previously know about.
|
|
||||||
//
|
|
||||||
// We make an exception for this if it turns out that we already knew about
|
|
||||||
// *all* of the events, and we weren't able to join up any timelines. When
|
|
||||||
// that happens, it means our existing pagination token is faulty, since it
|
|
||||||
// is only telling us what we already know. Rather than repeatedly
|
|
||||||
// paginating with the same token, we might as well use the new pagination
|
|
||||||
// token in the hope that we eventually work our way out of the mess.
|
|
||||||
|
|
||||||
var didUpdate = false;
|
|
||||||
var lastEventWasNew = false;
|
|
||||||
for (var i = 0; i < events.length; i++) {
|
|
||||||
var event = events[i];
|
|
||||||
var eventId = event.getId();
|
|
||||||
|
|
||||||
var existingTimeline = this._eventIdToTimeline[eventId];
|
|
||||||
|
|
||||||
if (!existingTimeline) {
|
|
||||||
// we don't know about this event yet. Just add it to the timeline.
|
|
||||||
this._addEventToTimeline(event, timeline, toStartOfTimeline);
|
|
||||||
lastEventWasNew = true;
|
|
||||||
didUpdate = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
lastEventWasNew = false;
|
|
||||||
|
|
||||||
if (existingTimeline == timeline) {
|
|
||||||
debuglog("Event " + eventId + " already in timeline " + timeline);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var neighbour = timeline.getNeighbouringTimeline(direction);
|
|
||||||
if (neighbour) {
|
|
||||||
// this timeline already has a neighbour in the relevant direction;
|
|
||||||
// let's assume the timelines are already correctly linked up, and
|
|
||||||
// skip over to it.
|
|
||||||
//
|
|
||||||
// there's probably some edge-case here where we end up with an
|
|
||||||
// event which is in a timeline a way down the chain, and there is
|
|
||||||
// a break in the chain somewhere. But I can't really imagine how
|
|
||||||
// that would happen, so I'm going to ignore it for now.
|
|
||||||
//
|
|
||||||
if (existingTimeline == neighbour) {
|
|
||||||
debuglog("Event " + eventId + " in neighbouring timeline - " +
|
|
||||||
"switching to " + existingTimeline);
|
|
||||||
} else {
|
|
||||||
debuglog("Event " + eventId + " already in a different " +
|
|
||||||
"timeline " + existingTimeline);
|
|
||||||
}
|
|
||||||
timeline = existingTimeline;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// time to join the timelines.
|
|
||||||
console.info("Already have timeline for " + eventId +
|
|
||||||
" - joining timeline " + timeline + " to " +
|
|
||||||
existingTimeline);
|
|
||||||
timeline.setNeighbouringTimeline(existingTimeline, direction);
|
|
||||||
existingTimeline.setNeighbouringTimeline(timeline, inverseDirection);
|
|
||||||
timeline = existingTimeline;
|
|
||||||
didUpdate = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// see above - if the last event was new to us, or if we didn't find any
|
|
||||||
// new information, we update the pagination token for whatever
|
|
||||||
// timeline we ended up on.
|
|
||||||
if (lastEventWasNew || !didUpdate) {
|
|
||||||
timeline.setPaginationToken(paginationToken, direction);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add event to the given timeline, and emit Room.timeline. Assumes
|
* Add an event to the end of this room's live timelines. Will fire
|
||||||
* we have already checked we don't know about this event.
|
* "Room.timeline".
|
||||||
*
|
|
||||||
* Will fire "Room.timeline" for each event added.
|
|
||||||
*
|
|
||||||
* @param {MatrixEvent} event
|
|
||||||
* @param {EventTimeline} timeline
|
|
||||||
* @param {boolean} toStartOfTimeline
|
|
||||||
*
|
|
||||||
* @fires module:client~MatrixClient#event:"Room.timeline"
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
Room.prototype._addEventToTimeline = function(event, timeline, toStartOfTimeline) {
|
|
||||||
var eventId = event.getId();
|
|
||||||
timeline.addEvent(event, toStartOfTimeline);
|
|
||||||
this._eventIdToTimeline[eventId] = timeline;
|
|
||||||
|
|
||||||
var data = {
|
|
||||||
timeline: timeline,
|
|
||||||
liveEvent: !toStartOfTimeline && timeline == this._liveTimeline,
|
|
||||||
};
|
|
||||||
this.emit("Room.timeline", event, this, Boolean(toStartOfTimeline), false, data);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add an event to the end of this room's live timeline. Will fire
|
|
||||||
* "Room.timeline"..
|
|
||||||
*
|
*
|
||||||
* @param {MatrixEvent} event Event to be added
|
* @param {MatrixEvent} event Event to be added
|
||||||
* @param {string?} duplicateStrategy 'ignore' or 'replace'
|
* @param {string?} duplicateStrategy 'ignore' or 'replace'
|
||||||
@@ -663,11 +525,12 @@ Room.prototype._addEventToTimeline = function(event, timeline, toStartOfTimeline
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
Room.prototype._addLiveEvent = function(event, duplicateStrategy) {
|
Room.prototype._addLiveEvent = function(event, duplicateStrategy) {
|
||||||
|
var i;
|
||||||
if (event.getType() === "m.room.redaction") {
|
if (event.getType() === "m.room.redaction") {
|
||||||
var redactId = event.event.redacts;
|
var redactId = event.event.redacts;
|
||||||
|
|
||||||
// if we know about this event, redact its contents now.
|
// if we know about this event, redact its contents now.
|
||||||
var redactedEvent = this.findEventById(redactId);
|
var redactedEvent = this.getUnfilteredTimelineSet().findEventById(redactId);
|
||||||
if (redactedEvent) {
|
if (redactedEvent) {
|
||||||
redactedEvent.makeRedacted(event);
|
redactedEvent.makeRedacted(event);
|
||||||
this.emit("Room.redaction", event, this);
|
this.emit("Room.redaction", event, this);
|
||||||
@@ -679,6 +542,8 @@ Room.prototype._addLiveEvent = function(event, duplicateStrategy) {
|
|||||||
// they are based on are changed.
|
// they are based on are changed.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIXME: apply redactions to notification list
|
||||||
|
|
||||||
// NB: We continue to add the redaction event to the timeline so
|
// NB: We continue to add the redaction event to the timeline so
|
||||||
// clients can say "so and so redacted an event" if they wish to. Also
|
// clients can say "so and so redacted an event" if they wish to. Also
|
||||||
// this may be needed to trigger an update.
|
// this may be needed to trigger an update.
|
||||||
@@ -693,39 +558,11 @@ Room.prototype._addLiveEvent = function(event, duplicateStrategy) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var timeline = this._eventIdToTimeline[event.getId()];
|
// add to our timeline sets
|
||||||
if (timeline) {
|
for (i = 0; i < this._timelineSets.length; i++) {
|
||||||
if (duplicateStrategy === "replace") {
|
this._timelineSets[i].addLiveEvent(event, duplicateStrategy);
|
||||||
debuglog("Room._addLiveEvent: replacing duplicate event " +
|
|
||||||
event.getId());
|
|
||||||
var tlEvents = timeline.getEvents();
|
|
||||||
for (var j = 0; j < tlEvents.length; j++) {
|
|
||||||
if (tlEvents[j].getId() === event.getId()) {
|
|
||||||
// still need to set the right metadata on this event
|
|
||||||
setEventMetadata(
|
|
||||||
event,
|
|
||||||
timeline.getState(EventTimeline.FORWARDS),
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!tlEvents[j].encryptedType) {
|
|
||||||
tlEvents[j] = event;
|
|
||||||
}
|
|
||||||
|
|
||||||
// XXX: we need to fire an event when this happens.
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
debuglog("Room._addLiveEvent: ignoring duplicate event " +
|
|
||||||
event.getId());
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: pass through filter to see if this should be added to the timeline.
|
|
||||||
this._addEventToTimeline(event, this._liveTimeline, false);
|
|
||||||
|
|
||||||
// synthesize and inject implicit read receipts
|
// synthesize and inject implicit read receipts
|
||||||
// Done after adding the event because otherwise the app would get a read receipt
|
// Done after adding the event because otherwise the app would get a read receipt
|
||||||
// pointing to an event that wasn't yet in the timeline
|
// pointing to an event that wasn't yet in the timeline
|
||||||
@@ -773,9 +610,11 @@ Room.prototype.addPendingEvent = function(event, txnId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// call setEventMetadata to set up event.sender etc
|
// call setEventMetadata to set up event.sender etc
|
||||||
setEventMetadata(
|
// as event is shared over all timelineSets, we set up its metadata based
|
||||||
|
// on the unfiltered timelineSet.
|
||||||
|
EventTimeline.setEventMetadata(
|
||||||
event,
|
event,
|
||||||
this._liveTimeline.getState(EventTimeline.FORWARDS),
|
this.getLiveTimeline().getState(EventTimeline.FORWARDS),
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -784,7 +623,19 @@ Room.prototype.addPendingEvent = function(event, txnId) {
|
|||||||
if (this._opts.pendingEventOrdering == "detached") {
|
if (this._opts.pendingEventOrdering == "detached") {
|
||||||
this._pendingEventList.push(event);
|
this._pendingEventList.push(event);
|
||||||
} else {
|
} else {
|
||||||
this._addEventToTimeline(event, this._liveTimeline, false);
|
for (var i = 0; i < this._timelineSets.length; i++) {
|
||||||
|
var timelineSet = this._timelineSets[i];
|
||||||
|
if (timelineSet.getFilter()) {
|
||||||
|
if (this._filter.filterRoomTimeline([event]).length) {
|
||||||
|
timelineSet.addEventToTimeline(event,
|
||||||
|
timelineSet.getLiveTimeline(), false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
timelineSet.addEventToTimeline(event,
|
||||||
|
timelineSet.getLiveTimeline(), false);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.emit("Room.localEchoUpdated", event, this, null, null);
|
this.emit("Room.localEchoUpdated", event, this, null, null);
|
||||||
@@ -828,13 +679,11 @@ Room.prototype._handleRemoteEcho = function(remoteEvent, localEvent) {
|
|||||||
// successfully sent.
|
// successfully sent.
|
||||||
localEvent.status = null;
|
localEvent.status = null;
|
||||||
|
|
||||||
// if it's already in the timeline, update the timeline map. If it's not, add it.
|
for (var i = 0; i < this._timelineSets.length; i++) {
|
||||||
var existingTimeline = this._eventIdToTimeline[oldEventId];
|
var timelineSet = this._timelineSets[i];
|
||||||
if (existingTimeline) {
|
|
||||||
delete this._eventIdToTimeline[oldEventId];
|
// if it's already in the timeline, update the timeline map. If it's not, add it.
|
||||||
this._eventIdToTimeline[newEventId] = existingTimeline;
|
timelineSet.handleRemoteEcho(localEvent, oldEventId, newEventId);
|
||||||
} else {
|
|
||||||
this._addEventToTimeline(localEvent, this._liveTimeline, false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.emit("Room.localEchoUpdated", localEvent, this,
|
this.emit("Room.localEchoUpdated", localEvent, this,
|
||||||
@@ -890,7 +739,7 @@ Room.prototype.updatePendingEvent = function(event, newStatus, newEventId) {
|
|||||||
|
|
||||||
// SENT races against /sync, so we have to special-case it.
|
// SENT races against /sync, so we have to special-case it.
|
||||||
if (newStatus == EventStatus.SENT) {
|
if (newStatus == EventStatus.SENT) {
|
||||||
var timeline = this._eventIdToTimeline[newEventId];
|
var timeline = this.getUnfilteredTimelineSet().eventIdToTimeline(newEventId);
|
||||||
if (timeline) {
|
if (timeline) {
|
||||||
// we've already received the event via the event stream.
|
// we've already received the event via the event stream.
|
||||||
// nothing more to do here.
|
// nothing more to do here.
|
||||||
@@ -921,10 +770,8 @@ Room.prototype.updatePendingEvent = function(event, newStatus, newEventId) {
|
|||||||
// if the event was already in the timeline (which will be the case if
|
// if the event was already in the timeline (which will be the case if
|
||||||
// opts.pendingEventOrdering==chronological), we need to update the
|
// opts.pendingEventOrdering==chronological), we need to update the
|
||||||
// timeline map.
|
// timeline map.
|
||||||
var existingTimeline = this._eventIdToTimeline[oldEventId];
|
for (var i = 0; i < this._timelineSets.length; i++) {
|
||||||
if (existingTimeline) {
|
this._timelineSets[i].replaceEventId(oldEventId, newEventId);
|
||||||
delete this._eventIdToTimeline[oldEventId];
|
|
||||||
this._eventIdToTimeline[newEventId] = existingTimeline;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (newStatus == EventStatus.CANCELLED) {
|
else if (newStatus == EventStatus.CANCELLED) {
|
||||||
@@ -960,24 +807,29 @@ Room.prototype.updatePendingEvent = function(event, newStatus, newEventId) {
|
|||||||
* @throws If <code>duplicateStrategy</code> is not falsey, 'replace' or 'ignore'.
|
* @throws If <code>duplicateStrategy</code> is not falsey, 'replace' or 'ignore'.
|
||||||
*/
|
*/
|
||||||
Room.prototype.addLiveEvents = function(events, duplicateStrategy) {
|
Room.prototype.addLiveEvents = function(events, duplicateStrategy) {
|
||||||
|
var i;
|
||||||
if (duplicateStrategy && ["replace", "ignore"].indexOf(duplicateStrategy) === -1) {
|
if (duplicateStrategy && ["replace", "ignore"].indexOf(duplicateStrategy) === -1) {
|
||||||
throw new Error("duplicateStrategy MUST be either 'replace' or 'ignore'");
|
throw new Error("duplicateStrategy MUST be either 'replace' or 'ignore'");
|
||||||
}
|
}
|
||||||
|
|
||||||
// sanity check that the live timeline is still live
|
// sanity check that the live timeline is still live
|
||||||
if (this._liveTimeline.getPaginationToken(EventTimeline.FORWARDS)) {
|
for (i = 0; i < this._timelineSets.length; i++) {
|
||||||
throw new Error(
|
var liveTimeline = this._timelineSets[i].getLiveTimeline();
|
||||||
"live timeline is no longer live - it has a pagination token (" +
|
if (liveTimeline.getPaginationToken(EventTimeline.FORWARDS)) {
|
||||||
this._liveTimeline.getPaginationToken(EventTimeline.FORWARDS) + ")"
|
throw new Error(
|
||||||
);
|
"live timeline " + i + " is no longer live - it has a pagination token " +
|
||||||
}
|
"(" + liveTimeline.getPaginationToken(EventTimeline.FORWARDS) + ")"
|
||||||
if (this._liveTimeline.getNeighbouringTimeline(EventTimeline.FORWARDS)) {
|
);
|
||||||
throw new Error(
|
}
|
||||||
"live timeline is no longer live - it has a neighbouring timeline"
|
if (liveTimeline.getNeighbouringTimeline(EventTimeline.FORWARDS)) {
|
||||||
);
|
throw new Error(
|
||||||
|
"live timeline " + i + " is no longer live - " +
|
||||||
|
"it has a neighbouring timeline"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var i = 0; i < events.length; i++) {
|
for (i = 0; i < events.length; i++) {
|
||||||
if (events[i].getType() === "m.typing") {
|
if (events[i].getType() === "m.typing") {
|
||||||
this.currentState.setTypingEvent(events[i]);
|
this.currentState.setTypingEvent(events[i]);
|
||||||
}
|
}
|
||||||
@@ -1009,98 +861,19 @@ Room.prototype.removeEvents = function(event_ids) {
|
|||||||
*
|
*
|
||||||
* @param {String} eventId The id of the event to remove
|
* @param {String} eventId The id of the event to remove
|
||||||
*
|
*
|
||||||
* @return {?MatrixEvent} the removed event, or null if the event was not found
|
* @return {bool} true if the event was removed from any of the room's timeline sets
|
||||||
* in this room.
|
|
||||||
*/
|
*/
|
||||||
Room.prototype.removeEvent = function(eventId) {
|
Room.prototype.removeEvent = function(eventId) {
|
||||||
var timeline = this._eventIdToTimeline[eventId];
|
var removedAny = false;
|
||||||
if (!timeline) {
|
for (var i = 0; i < this._timelineSets.length; i++) {
|
||||||
return null;
|
var removed = this._timelineSets[i].removeEvent(eventId);
|
||||||
|
if (removed) {
|
||||||
|
removedAny = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return removedAny;
|
||||||
var removed = timeline.removeEvent(eventId);
|
|
||||||
if (removed) {
|
|
||||||
delete this._eventIdToTimeline[eventId];
|
|
||||||
var data = {
|
|
||||||
timeline: timeline,
|
|
||||||
};
|
|
||||||
this.emit("Room.timeline", removed, this, undefined, true, data);
|
|
||||||
}
|
|
||||||
return removed;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine where two events appear in the timeline relative to one another
|
|
||||||
*
|
|
||||||
* @param {string} eventId1 The id of the first event
|
|
||||||
* @param {string} eventId2 The id of the second event
|
|
||||||
|
|
||||||
* @return {?number} a number less than zero if eventId1 precedes eventId2, and
|
|
||||||
* greater than zero if eventId1 succeeds eventId2. zero if they are the
|
|
||||||
* same event; null if we can't tell (either because we don't know about one
|
|
||||||
* of the events, or because they are in separate timelines which don't join
|
|
||||||
* up).
|
|
||||||
*/
|
|
||||||
Room.prototype.compareEventOrdering = function(eventId1, eventId2) {
|
|
||||||
if (eventId1 == eventId2) {
|
|
||||||
// optimise this case
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
var timeline1 = this._eventIdToTimeline[eventId1];
|
|
||||||
var timeline2 = this._eventIdToTimeline[eventId2];
|
|
||||||
|
|
||||||
if (timeline1 === undefined) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (timeline2 === undefined) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (timeline1 === timeline2) {
|
|
||||||
// both events are in the same timeline - figure out their
|
|
||||||
// relative indices
|
|
||||||
var idx1, idx2;
|
|
||||||
var events = timeline1.getEvents();
|
|
||||||
for (var idx = 0; idx < events.length &&
|
|
||||||
(idx1 === undefined || idx2 === undefined); idx++) {
|
|
||||||
var evId = events[idx].getId();
|
|
||||||
if (evId == eventId1) {
|
|
||||||
idx1 = idx;
|
|
||||||
}
|
|
||||||
if (evId == eventId2) {
|
|
||||||
idx2 = idx;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return idx1 - idx2;
|
|
||||||
}
|
|
||||||
|
|
||||||
// the events are in different timelines. Iterate through the
|
|
||||||
// linkedlist to see which comes first.
|
|
||||||
|
|
||||||
// first work forwards from timeline1
|
|
||||||
var tl = timeline1;
|
|
||||||
while (tl) {
|
|
||||||
if (tl === timeline2) {
|
|
||||||
// timeline1 is before timeline2
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
tl = tl.getNeighbouringTimeline(EventTimeline.FORWARDS);
|
|
||||||
}
|
|
||||||
|
|
||||||
// now try backwards from timeline1
|
|
||||||
tl = timeline1;
|
|
||||||
while (tl) {
|
|
||||||
if (tl === timeline2) {
|
|
||||||
// timeline2 is before timeline1
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
tl = tl.getNeighbouringTimeline(EventTimeline.BACKWARDS);
|
|
||||||
}
|
|
||||||
|
|
||||||
// the timelines are not contiguous.
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recalculate various aspects of the room, including the room name and
|
* Recalculate various aspects of the room, including the room name and
|
||||||
@@ -1221,7 +994,7 @@ Room.prototype.addReceipt = function(event, fake) {
|
|||||||
// as there's nothing that would read it.
|
// as there's nothing that would read it.
|
||||||
}
|
}
|
||||||
this._addReceiptsToStructure(event, this._receipts);
|
this._addReceiptsToStructure(event, this._receipts);
|
||||||
this._receiptCacheByEventId = this._buildReciptCache(this._receipts);
|
this._receiptCacheByEventId = this._buildReceiptCache(this._receipts);
|
||||||
|
|
||||||
// send events after we've regenerated the cache, otherwise things that
|
// send events after we've regenerated the cache, otherwise things that
|
||||||
// listened for the event would read from a stale cache
|
// listened for the event would read from a stale cache
|
||||||
@@ -1254,7 +1027,7 @@ Room.prototype._addReceiptsToStructure = function(event, receipts) {
|
|||||||
// than the one we already have. (This is managed
|
// than the one we already have. (This is managed
|
||||||
// server-side, but because we synthesize RRs locally we
|
// server-side, but because we synthesize RRs locally we
|
||||||
// have to do it here too.)
|
// have to do it here too.)
|
||||||
var ordering = self.compareEventOrdering(
|
var ordering = self.getUnfilteredTimelineSet().compareEventOrdering(
|
||||||
existingReceipt.eventId, eventId);
|
existingReceipt.eventId, eventId);
|
||||||
if (ordering !== null && ordering >= 0) {
|
if (ordering !== null && ordering >= 0) {
|
||||||
return;
|
return;
|
||||||
@@ -1275,7 +1048,7 @@ Room.prototype._addReceiptsToStructure = function(event, receipts) {
|
|||||||
* @param {Object} receipts A map of receipts
|
* @param {Object} receipts A map of receipts
|
||||||
* @return {Object} Map of receipts by event ID
|
* @return {Object} Map of receipts by event ID
|
||||||
*/
|
*/
|
||||||
Room.prototype._buildReciptCache = function(receipts) {
|
Room.prototype._buildReceiptCache = function(receipts) {
|
||||||
var receiptCacheByEventId = {};
|
var receiptCacheByEventId = {};
|
||||||
utils.keys(receipts).forEach(function(receiptType) {
|
utils.keys(receipts).forEach(function(receiptType) {
|
||||||
utils.keys(receipts[receiptType]).forEach(function(userId) {
|
utils.keys(receipts[receiptType]).forEach(function(userId) {
|
||||||
@@ -1350,27 +1123,6 @@ Room.prototype.getAccountData = function(type) {
|
|||||||
return this.accountData[type];
|
return this.accountData[type];
|
||||||
};
|
};
|
||||||
|
|
||||||
function setEventMetadata(event, stateContext, toStartOfTimeline) {
|
|
||||||
// set sender and target properties
|
|
||||||
event.sender = stateContext.getSentinelMember(
|
|
||||||
event.getSender()
|
|
||||||
);
|
|
||||||
if (event.getType() === "m.room.member") {
|
|
||||||
event.target = stateContext.getSentinelMember(
|
|
||||||
event.getStateKey()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (event.isState()) {
|
|
||||||
// room state has no concept of 'old' or 'current', but we want the
|
|
||||||
// room state to regress back to previous values if toStartOfTimeline
|
|
||||||
// is set, which means inspecting prev_content if it exists. This
|
|
||||||
// is done by toggling the forwardLooking flag.
|
|
||||||
if (toStartOfTimeline) {
|
|
||||||
event.forwardLooking = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is an internal method. Calculates the name of the room from the current
|
* This is an internal method. Calculates the name of the room from the current
|
||||||
* room state.
|
* room state.
|
||||||
@@ -1486,49 +1238,30 @@ function calculateRoomName(room, userId, ignoreRoomNameEvent) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIXME: copypasted from sync.js
|
||||||
|
function reEmit(reEmitEntity, emittableEntity, eventNames) {
|
||||||
|
utils.forEach(eventNames, function(eventName) {
|
||||||
|
// setup a listener on the entity (the Room, User, etc) for this event
|
||||||
|
emittableEntity.on(eventName, function() {
|
||||||
|
// take the args from the listener and reuse them, adding the
|
||||||
|
// event name to the arg list so it works with .emit()
|
||||||
|
// Transformation Example:
|
||||||
|
// listener on "foo" => function(a,b) { ... }
|
||||||
|
// Re-emit on "thing" => thing.emit("foo", a, b)
|
||||||
|
var newArgs = [eventName];
|
||||||
|
for (var i = 0; i < arguments.length; i++) {
|
||||||
|
newArgs.push(arguments[i]);
|
||||||
|
}
|
||||||
|
reEmitEntity.emit.apply(reEmitEntity, newArgs);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Room class.
|
* The Room class.
|
||||||
*/
|
*/
|
||||||
module.exports = Room;
|
module.exports = Room;
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires whenever the timeline in a room is updated.
|
|
||||||
* @event module:client~MatrixClient#"Room.timeline"
|
|
||||||
* @param {MatrixEvent} event The matrix event which caused this event to fire.
|
|
||||||
* @param {Room} room The room whose Room.timeline was updated.
|
|
||||||
* @param {boolean} toStartOfTimeline True if this event was added to the start
|
|
||||||
* @param {boolean} removed True if this event has just been removed from the timeline
|
|
||||||
* (beginning; oldest) of the timeline e.g. due to pagination.
|
|
||||||
*
|
|
||||||
* @param {object} data more data about the event
|
|
||||||
*
|
|
||||||
* @param {module:event-timeline.EventTimeline} data.timeline the timeline the
|
|
||||||
* event was added to/removed from
|
|
||||||
*
|
|
||||||
* @param {boolean} data.liveEvent true if the event was a real-time event
|
|
||||||
* added to the end of the live timeline
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* matrixClient.on("Room.timeline",
|
|
||||||
* function(event, room, toStartOfTimeline, removed, data) {
|
|
||||||
* if (!toStartOfTimeline && data.liveEvent) {
|
|
||||||
* var messageToAppend = room.timeline.[room.timeline.length - 1];
|
|
||||||
* }
|
|
||||||
* });
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires whenever the live timeline in a room is reset.
|
|
||||||
*
|
|
||||||
* When we get a 'limited' sync (for example, after a network outage), we reset
|
|
||||||
* the live timeline to be empty before adding the recent events to the new
|
|
||||||
* timeline. This event is fired after the timeline is reset, and before the
|
|
||||||
* new events are added.
|
|
||||||
*
|
|
||||||
* @event module:client~MatrixClient#"Room.timelineReset"
|
|
||||||
* @param {Room} room The room whose live timeline was reset.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fires when an event we had previously received is redacted.
|
* Fires when an event we had previously received is redacted.
|
||||||
*
|
*
|
||||||
|
|||||||
97
lib/sync.js
97
lib/sync.js
@@ -72,6 +72,12 @@ function SyncApi(client, opts) {
|
|||||||
this._running = false;
|
this._running = false;
|
||||||
this._keepAliveTimer = null;
|
this._keepAliveTimer = null;
|
||||||
this._connectionReturnedDefer = null;
|
this._connectionReturnedDefer = null;
|
||||||
|
this._notifEvents = []; // accumulator of sync events in the current sync response
|
||||||
|
|
||||||
|
if (client.getNotifTimelineSet()) {
|
||||||
|
reEmit(client, client.getNotifTimelineSet(),
|
||||||
|
["Room.timeline", "Room.timelineReset"]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -148,7 +154,7 @@ SyncApi.prototype.syncLeftRooms = function() {
|
|||||||
timeout: 0 // don't want to block since this is a single isolated req
|
timeout: 0 // don't want to block since this is a single isolated req
|
||||||
};
|
};
|
||||||
|
|
||||||
return this._getOrCreateFilter(
|
return client.getOrCreateFilter(
|
||||||
getFilterName(client.credentials.userId, "LEFT_ROOMS"), filter
|
getFilterName(client.credentials.userId, "LEFT_ROOMS"), filter
|
||||||
).then(function(filterId) {
|
).then(function(filterId) {
|
||||||
qps.filter = filterId;
|
qps.filter = filterId;
|
||||||
@@ -389,9 +395,15 @@ SyncApi.prototype.sync = function() {
|
|||||||
var filter = new Filter(client.credentials.userId);
|
var filter = new Filter(client.credentials.userId);
|
||||||
filter.setTimelineLimit(self.opts.initialSyncLimit);
|
filter.setTimelineLimit(self.opts.initialSyncLimit);
|
||||||
|
|
||||||
self._getOrCreateFilter(
|
client.getOrCreateFilter(
|
||||||
getFilterName(client.credentials.userId), filter
|
getFilterName(client.credentials.userId), filter
|
||||||
).done(function(filterId) {
|
).done(function(filterId) {
|
||||||
|
// reset the notifications timeline to prepare it to paginate from
|
||||||
|
// the current point in time.
|
||||||
|
// The right solution would be to tie /sync pagination tokens into
|
||||||
|
// /notifications API somehow.
|
||||||
|
client.resetNotifTimelineSet();
|
||||||
|
|
||||||
self._sync({ filterId: filterId });
|
self._sync({ filterId: filterId });
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
self._startKeepAlives().done(function() {
|
self._startKeepAlives().done(function() {
|
||||||
@@ -666,6 +678,8 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._notifEvents = [];
|
||||||
|
|
||||||
// Handle invites
|
// Handle invites
|
||||||
inviteRooms.forEach(function(inviteObj) {
|
inviteRooms.forEach(function(inviteObj) {
|
||||||
var room = inviteObj.room;
|
var room = inviteObj.room;
|
||||||
@@ -751,6 +765,12 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) {
|
|||||||
room.currentState.paginationToken = syncToken;
|
room.currentState.paginationToken = syncToken;
|
||||||
self._deregisterStateListeners(room);
|
self._deregisterStateListeners(room);
|
||||||
room.resetLiveTimeline(joinObj.timeline.prev_batch);
|
room.resetLiveTimeline(joinObj.timeline.prev_batch);
|
||||||
|
|
||||||
|
// We have to assume any gap in any timeline is
|
||||||
|
// reason to stop incrementally tracking notifications and
|
||||||
|
// reset the timeline.
|
||||||
|
client.resetNotifTimelineSet();
|
||||||
|
|
||||||
self._registerStateListeners(room);
|
self._registerStateListeners(room);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -799,6 +819,20 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) {
|
|||||||
timelineEvents.forEach(function(e) { client.emit("event", e); });
|
timelineEvents.forEach(function(e) { client.emit("event", e); });
|
||||||
accountDataEvents.forEach(function(e) { client.emit("event", e); });
|
accountDataEvents.forEach(function(e) { client.emit("event", e); });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// update the notification timeline, if appropriate.
|
||||||
|
// we only do this for live events, as otherwise we can't order them sanely
|
||||||
|
// in the timeline relative to ones paginated in by /notifications.
|
||||||
|
// XXX: we could fix this by making EventTimeline support chronological
|
||||||
|
// ordering... but it doesn't, right now.
|
||||||
|
if (syncToken && this._notifEvents.length) {
|
||||||
|
this._notifEvents.sort(function(a, b) {
|
||||||
|
return a.getTs() - b.getTs();
|
||||||
|
});
|
||||||
|
this._notifEvents.forEach(function(event) {
|
||||||
|
client.getNotifTimelineSet().addLiveEvent(event);
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -862,53 +896,6 @@ SyncApi.prototype._pokeKeepAlive = function() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} filterName
|
|
||||||
* @param {Filter} filter
|
|
||||||
* @return {Promise<String>} Filter ID
|
|
||||||
*/
|
|
||||||
SyncApi.prototype._getOrCreateFilter = function(filterName, filter) {
|
|
||||||
var client = this.client;
|
|
||||||
|
|
||||||
var filterId = client.store.getFilterIdByName(filterName);
|
|
||||||
var promise = q();
|
|
||||||
|
|
||||||
if (filterId) {
|
|
||||||
// check that the existing filter matches our expectations
|
|
||||||
promise = client.getFilter(client.credentials.userId,
|
|
||||||
filterId, true
|
|
||||||
).then(function(existingFilter) {
|
|
||||||
var oldDef = existingFilter.getDefinition();
|
|
||||||
var newDef = filter.getDefinition();
|
|
||||||
|
|
||||||
if (utils.deepCompare(oldDef, newDef)) {
|
|
||||||
// super, just use that.
|
|
||||||
debuglog("Using existing filter ID %s: %s", filterId,
|
|
||||||
JSON.stringify(oldDef));
|
|
||||||
return q(filterId);
|
|
||||||
}
|
|
||||||
debuglog("Existing filter ID %s: %s; new filter: %s",
|
|
||||||
filterId, JSON.stringify(oldDef), JSON.stringify(newDef));
|
|
||||||
return;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return promise.then(function(existingId) {
|
|
||||||
if (existingId) {
|
|
||||||
return existingId;
|
|
||||||
}
|
|
||||||
|
|
||||||
// create a new filter
|
|
||||||
return client.createFilter(filter.getDefinition()
|
|
||||||
).then(function(createdFilter) {
|
|
||||||
debuglog("Created new filter ID %s: %s", createdFilter.filterId,
|
|
||||||
JSON.stringify(createdFilter.getDefinition()));
|
|
||||||
client.store.setFilterIdByName(filterName, createdFilter.filterId);
|
|
||||||
return createdFilter.filterId;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} obj
|
* @param {Object} obj
|
||||||
* @return {Object[]}
|
* @return {Object[]}
|
||||||
@@ -1034,6 +1021,18 @@ SyncApi.prototype._processRoomEvents = function(room, stateEventList,
|
|||||||
// may make notifications appear which should have the right name.
|
// may make notifications appear which should have the right name.
|
||||||
room.recalculate(this.client.credentials.userId);
|
room.recalculate(this.client.credentials.userId);
|
||||||
|
|
||||||
|
// gather our notifications into this._notifEvents
|
||||||
|
if (client.getNotifTimelineSet()) {
|
||||||
|
for (var i = 0; i < timelineEventList.length; i++) {
|
||||||
|
var pushActions = client.getPushActionsForEvent(timelineEventList[i]);
|
||||||
|
if (pushActions && pushActions.notify &&
|
||||||
|
pushActions.tweaks && pushActions.tweaks.highlight)
|
||||||
|
{
|
||||||
|
this._notifEvents.push(timelineEventList[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// execute the timeline events, this will begin to diverge the current state
|
// execute the timeline events, this will begin to diverge the current state
|
||||||
// if the timeline has any state events in it.
|
// if the timeline has any state events in it.
|
||||||
room.addLiveEvents(timelineEventList);
|
room.addLiveEvents(timelineEventList);
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ var DEFAULT_PAGINATE_LOOP_LIMIT = 5;
|
|||||||
* @param {MatrixClient} client MatrixClient to be used for context/pagination
|
* @param {MatrixClient} client MatrixClient to be used for context/pagination
|
||||||
* requests.
|
* requests.
|
||||||
*
|
*
|
||||||
* @param {Room} room The room to track
|
* @param {EventTimelineSet} timelineSet The timelineSet to track
|
||||||
*
|
*
|
||||||
* @param {Object} [opts] Configuration options for this window
|
* @param {Object} [opts] Configuration options for this window
|
||||||
*
|
*
|
||||||
@@ -66,10 +66,10 @@ var DEFAULT_PAGINATE_LOOP_LIMIT = 5;
|
|||||||
*
|
*
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
function TimelineWindow(client, room, opts) {
|
function TimelineWindow(client, timelineSet, opts) {
|
||||||
opts = opts || {};
|
opts = opts || {};
|
||||||
this._client = client;
|
this._client = client;
|
||||||
this._room = room;
|
this._timelineSet = timelineSet;
|
||||||
|
|
||||||
// these will be TimelineIndex objects; they delineate the 'start' and
|
// these will be TimelineIndex objects; they delineate the 'start' and
|
||||||
// 'end' of the window.
|
// 'end' of the window.
|
||||||
@@ -113,7 +113,7 @@ TimelineWindow.prototype.load = function(initialEventId, initialWindowSize) {
|
|||||||
// TODO: ideally we'd spot getEventTimeline returning a resolved promise and
|
// TODO: ideally we'd spot getEventTimeline returning a resolved promise and
|
||||||
// skip straight to the find-event loop.
|
// skip straight to the find-event loop.
|
||||||
if (initialEventId) {
|
if (initialEventId) {
|
||||||
return this._client.getEventTimeline(this._room, initialEventId)
|
return this._client.getEventTimeline(this._timelineSet, initialEventId)
|
||||||
.then(function(tl) {
|
.then(function(tl) {
|
||||||
// make sure that our window includes the event
|
// make sure that our window includes the event
|
||||||
for (var i = 0; i < tl.getEvents().length; i++) {
|
for (var i = 0; i < tl.getEvents().length; i++) {
|
||||||
@@ -126,7 +126,7 @@ TimelineWindow.prototype.load = function(initialEventId, initialWindowSize) {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// start with the most recent events
|
// start with the most recent events
|
||||||
var tl = this._room.getLiveTimeline();
|
var tl = this._timelineSet.getLiveTimeline();
|
||||||
initFields(tl, tl.getEvents().length);
|
initFields(tl, tl.getEvents().length);
|
||||||
return q();
|
return q();
|
||||||
}
|
}
|
||||||
@@ -254,6 +254,7 @@ TimelineWindow.prototype.paginate = function(direction, size, makeRequest,
|
|||||||
|
|
||||||
debuglog("TimelineWindow: starting request");
|
debuglog("TimelineWindow: starting request");
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
var prom = this._client.paginateEventTimeline(tl.timeline, {
|
var prom = this._client.paginateEventTimeline(tl.timeline, {
|
||||||
backwards: direction == EventTimeline.BACKWARDS,
|
backwards: direction == EventTimeline.BACKWARDS,
|
||||||
limit: size
|
limit: size
|
||||||
|
|||||||
@@ -121,7 +121,8 @@ describe("getEventTimeline support", function() {
|
|||||||
startClient(httpBackend, client
|
startClient(httpBackend, client
|
||||||
).then(function() {
|
).then(function() {
|
||||||
var room = client.getRoom(roomId);
|
var room = client.getRoom(roomId);
|
||||||
expect(function() { client.getEventTimeline(room, "event"); })
|
var timelineSet = room.getTimelineSets()[0];
|
||||||
|
expect(function() { client.getEventTimeline(timelineSet, "event"); })
|
||||||
.toThrow();
|
.toThrow();
|
||||||
}).catch(utils.failTest).done(done);
|
}).catch(utils.failTest).done(done);
|
||||||
});
|
});
|
||||||
@@ -137,7 +138,8 @@ describe("getEventTimeline support", function() {
|
|||||||
startClient(httpBackend, client
|
startClient(httpBackend, client
|
||||||
).then(function() {
|
).then(function() {
|
||||||
var room = client.getRoom(roomId);
|
var room = client.getRoom(roomId);
|
||||||
expect(function() { client.getEventTimeline(room, "event"); })
|
var timelineSet = room.getTimelineSets()[0];
|
||||||
|
expect(function() { client.getEventTimeline(timelineSet, "event"); })
|
||||||
.not.toThrow();
|
.not.toThrow();
|
||||||
}).catch(utils.failTest).done(done);
|
}).catch(utils.failTest).done(done);
|
||||||
|
|
||||||
@@ -242,6 +244,7 @@ describe("MatrixClient event timelines", function() {
|
|||||||
describe("getEventTimeline", function() {
|
describe("getEventTimeline", function() {
|
||||||
it("should create a new timeline for new events", function(done) {
|
it("should create a new timeline for new events", function(done) {
|
||||||
var room = client.getRoom(roomId);
|
var room = client.getRoom(roomId);
|
||||||
|
var timelineSet = room.getTimelineSets()[0];
|
||||||
httpBackend.when("GET", "/rooms/!foo%3Abar/context/event1%3Abar")
|
httpBackend.when("GET", "/rooms/!foo%3Abar/context/event1%3Abar")
|
||||||
.respond(200, function() {
|
.respond(200, function() {
|
||||||
return {
|
return {
|
||||||
@@ -257,7 +260,7 @@ describe("MatrixClient event timelines", function() {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
client.getEventTimeline(room, "event1:bar").then(function(tl) {
|
client.getEventTimeline(timelineSet, "event1:bar").then(function(tl) {
|
||||||
expect(tl.getEvents().length).toEqual(4);
|
expect(tl.getEvents().length).toEqual(4);
|
||||||
for (var i = 0; i < 4; i++) {
|
for (var i = 0; i < 4; i++) {
|
||||||
expect(tl.getEvents()[i].event).toEqual(EVENTS[i]);
|
expect(tl.getEvents()[i].event).toEqual(EVENTS[i]);
|
||||||
@@ -274,6 +277,7 @@ describe("MatrixClient event timelines", function() {
|
|||||||
|
|
||||||
it("should return existing timeline for known events", function(done) {
|
it("should return existing timeline for known events", function(done) {
|
||||||
var room = client.getRoom(roomId);
|
var room = client.getRoom(roomId);
|
||||||
|
var timelineSet = room.getTimelineSets()[0];
|
||||||
httpBackend.when("GET", "/sync").respond(200, {
|
httpBackend.when("GET", "/sync").respond(200, {
|
||||||
next_batch: "s_5_4",
|
next_batch: "s_5_4",
|
||||||
rooms: {
|
rooms: {
|
||||||
@@ -291,7 +295,7 @@ describe("MatrixClient event timelines", function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
httpBackend.flush("/sync").then(function() {
|
httpBackend.flush("/sync").then(function() {
|
||||||
return client.getEventTimeline(room, EVENTS[0].event_id);
|
return client.getEventTimeline(timelineSet, EVENTS[0].event_id);
|
||||||
}).then(function(tl) {
|
}).then(function(tl) {
|
||||||
expect(tl.getEvents().length).toEqual(2);
|
expect(tl.getEvents().length).toEqual(2);
|
||||||
expect(tl.getEvents()[1].event).toEqual(EVENTS[0]);
|
expect(tl.getEvents()[1].event).toEqual(EVENTS[0]);
|
||||||
@@ -305,6 +309,7 @@ describe("MatrixClient event timelines", function() {
|
|||||||
|
|
||||||
it("should update timelines where they overlap a previous /sync", function(done) {
|
it("should update timelines where they overlap a previous /sync", function(done) {
|
||||||
var room = client.getRoom(roomId);
|
var room = client.getRoom(roomId);
|
||||||
|
var timelineSet = room.getTimelineSets()[0];
|
||||||
httpBackend.when("GET", "/sync").respond(200, {
|
httpBackend.when("GET", "/sync").respond(200, {
|
||||||
next_batch: "s_5_4",
|
next_batch: "s_5_4",
|
||||||
rooms: {
|
rooms: {
|
||||||
@@ -335,7 +340,7 @@ describe("MatrixClient event timelines", function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
client.on("sync", function() {
|
client.on("sync", function() {
|
||||||
client.getEventTimeline(room, EVENTS[2].event_id
|
client.getEventTimeline(timelineSet, EVENTS[2].event_id
|
||||||
).then(function(tl) {
|
).then(function(tl) {
|
||||||
expect(tl.getEvents().length).toEqual(4);
|
expect(tl.getEvents().length).toEqual(4);
|
||||||
expect(tl.getEvents()[0].event).toEqual(EVENTS[1]);
|
expect(tl.getEvents()[0].event).toEqual(EVENTS[1]);
|
||||||
@@ -354,6 +359,7 @@ describe("MatrixClient event timelines", function() {
|
|||||||
it("should join timelines where they overlap a previous /context",
|
it("should join timelines where they overlap a previous /context",
|
||||||
function(done) {
|
function(done) {
|
||||||
var room = client.getRoom(roomId);
|
var room = client.getRoom(roomId);
|
||||||
|
var timelineSet = room.getTimelineSets()[0];
|
||||||
|
|
||||||
// we fetch event 0, then 2, then 3, and finally 1. 1 is returned
|
// we fetch event 0, then 2, then 3, and finally 1. 1 is returned
|
||||||
// with context which joins them all up.
|
// with context which joins them all up.
|
||||||
@@ -410,19 +416,19 @@ describe("MatrixClient event timelines", function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
var tl0, tl2, tl3;
|
var tl0, tl2, tl3;
|
||||||
client.getEventTimeline(room, EVENTS[0].event_id
|
client.getEventTimeline(timelineSet, EVENTS[0].event_id
|
||||||
).then(function(tl) {
|
).then(function(tl) {
|
||||||
expect(tl.getEvents().length).toEqual(1);
|
expect(tl.getEvents().length).toEqual(1);
|
||||||
tl0 = tl;
|
tl0 = tl;
|
||||||
return client.getEventTimeline(room, EVENTS[2].event_id);
|
return client.getEventTimeline(timelineSet, EVENTS[2].event_id);
|
||||||
}).then(function(tl) {
|
}).then(function(tl) {
|
||||||
expect(tl.getEvents().length).toEqual(1);
|
expect(tl.getEvents().length).toEqual(1);
|
||||||
tl2 = tl;
|
tl2 = tl;
|
||||||
return client.getEventTimeline(room, EVENTS[3].event_id);
|
return client.getEventTimeline(timelineSet, EVENTS[3].event_id);
|
||||||
}).then(function(tl) {
|
}).then(function(tl) {
|
||||||
expect(tl.getEvents().length).toEqual(1);
|
expect(tl.getEvents().length).toEqual(1);
|
||||||
tl3 = tl;
|
tl3 = tl;
|
||||||
return client.getEventTimeline(room, EVENTS[1].event_id);
|
return client.getEventTimeline(timelineSet, EVENTS[1].event_id);
|
||||||
}).then(function(tl) {
|
}).then(function(tl) {
|
||||||
// we expect it to get merged in with event 2
|
// we expect it to get merged in with event 2
|
||||||
expect(tl.getEvents().length).toEqual(2);
|
expect(tl.getEvents().length).toEqual(2);
|
||||||
@@ -447,6 +453,7 @@ describe("MatrixClient event timelines", function() {
|
|||||||
|
|
||||||
it("should fail gracefully if there is no event field", function(done) {
|
it("should fail gracefully if there is no event field", function(done) {
|
||||||
var room = client.getRoom(roomId);
|
var room = client.getRoom(roomId);
|
||||||
|
var timelineSet = room.getTimelineSets()[0];
|
||||||
// we fetch event 0, then 2, then 3, and finally 1. 1 is returned
|
// we fetch event 0, then 2, then 3, and finally 1. 1 is returned
|
||||||
// with context which joins them all up.
|
// with context which joins them all up.
|
||||||
httpBackend.when("GET", "/rooms/!foo%3Abar/context/event1")
|
httpBackend.when("GET", "/rooms/!foo%3Abar/context/event1")
|
||||||
@@ -460,7 +467,7 @@ describe("MatrixClient event timelines", function() {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
client.getEventTimeline(room, "event1"
|
client.getEventTimeline(timelineSet, "event1"
|
||||||
).then(function(tl) {
|
).then(function(tl) {
|
||||||
// could do with a fail()
|
// could do with a fail()
|
||||||
expect(true).toBeFalsy();
|
expect(true).toBeFalsy();
|
||||||
@@ -475,6 +482,7 @@ describe("MatrixClient event timelines", function() {
|
|||||||
describe("paginateEventTimeline", function() {
|
describe("paginateEventTimeline", function() {
|
||||||
it("should allow you to paginate backwards", function(done) {
|
it("should allow you to paginate backwards", function(done) {
|
||||||
var room = client.getRoom(roomId);
|
var room = client.getRoom(roomId);
|
||||||
|
var timelineSet = room.getTimelineSets()[0];
|
||||||
|
|
||||||
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
|
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
|
||||||
encodeURIComponent(EVENTS[0].event_id))
|
encodeURIComponent(EVENTS[0].event_id))
|
||||||
@@ -503,7 +511,7 @@ describe("MatrixClient event timelines", function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
var tl;
|
var tl;
|
||||||
client.getEventTimeline(room, EVENTS[0].event_id
|
client.getEventTimeline(timelineSet, EVENTS[0].event_id
|
||||||
).then(function(tl0) {
|
).then(function(tl0) {
|
||||||
tl = tl0;
|
tl = tl0;
|
||||||
return client.paginateEventTimeline(tl, {backwards: true});
|
return client.paginateEventTimeline(tl, {backwards: true});
|
||||||
@@ -525,6 +533,7 @@ describe("MatrixClient event timelines", function() {
|
|||||||
|
|
||||||
it("should allow you to paginate forwards", function(done) {
|
it("should allow you to paginate forwards", function(done) {
|
||||||
var room = client.getRoom(roomId);
|
var room = client.getRoom(roomId);
|
||||||
|
var timelineSet = room.getTimelineSets()[0];
|
||||||
|
|
||||||
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
|
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
|
||||||
encodeURIComponent(EVENTS[0].event_id))
|
encodeURIComponent(EVENTS[0].event_id))
|
||||||
@@ -553,7 +562,7 @@ describe("MatrixClient event timelines", function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
var tl;
|
var tl;
|
||||||
client.getEventTimeline(room, EVENTS[0].event_id
|
client.getEventTimeline(timelineSet, EVENTS[0].event_id
|
||||||
).then(function(tl0) {
|
).then(function(tl0) {
|
||||||
tl = tl0;
|
tl = tl0;
|
||||||
return client.paginateEventTimeline(
|
return client.paginateEventTimeline(
|
||||||
@@ -607,10 +616,11 @@ describe("MatrixClient event timelines", function() {
|
|||||||
|
|
||||||
it("should work when /send returns before /sync", function(done) {
|
it("should work when /send returns before /sync", function(done) {
|
||||||
var room = client.getRoom(roomId);
|
var room = client.getRoom(roomId);
|
||||||
|
var timelineSet = room.getTimelineSets()[0];
|
||||||
|
|
||||||
client.sendTextMessage(roomId, "a body", TXN_ID).then(function(res) {
|
client.sendTextMessage(roomId, "a body", TXN_ID).then(function(res) {
|
||||||
expect(res.event_id).toEqual(event.event_id);
|
expect(res.event_id).toEqual(event.event_id);
|
||||||
return client.getEventTimeline(room, event.event_id);
|
return client.getEventTimeline(timelineSet, event.event_id);
|
||||||
}).then(function(tl) {
|
}).then(function(tl) {
|
||||||
// 2 because the initial sync contained an event
|
// 2 because the initial sync contained an event
|
||||||
expect(tl.getEvents().length).toEqual(2);
|
expect(tl.getEvents().length).toEqual(2);
|
||||||
@@ -619,7 +629,7 @@ describe("MatrixClient event timelines", function() {
|
|||||||
// now let the sync complete, and check it again
|
// now let the sync complete, and check it again
|
||||||
return httpBackend.flush("/sync", 1);
|
return httpBackend.flush("/sync", 1);
|
||||||
}).then(function() {
|
}).then(function() {
|
||||||
return client.getEventTimeline(room, event.event_id);
|
return client.getEventTimeline(timelineSet, event.event_id);
|
||||||
}).then(function(tl) {
|
}).then(function(tl) {
|
||||||
expect(tl.getEvents().length).toEqual(2);
|
expect(tl.getEvents().length).toEqual(2);
|
||||||
expect(tl.getEvents()[1].event).toEqual(event);
|
expect(tl.getEvents()[1].event).toEqual(event);
|
||||||
@@ -630,13 +640,14 @@ describe("MatrixClient event timelines", function() {
|
|||||||
|
|
||||||
it("should work when /send returns after /sync", function(done) {
|
it("should work when /send returns after /sync", function(done) {
|
||||||
var room = client.getRoom(roomId);
|
var room = client.getRoom(roomId);
|
||||||
|
var timelineSet = room.getTimelineSets()[0];
|
||||||
|
|
||||||
// initiate the send, and set up checks to be done when it completes
|
// initiate the send, and set up checks to be done when it completes
|
||||||
// - but note that it won't complete until after the /sync does, below.
|
// - but note that it won't complete until after the /sync does, below.
|
||||||
client.sendTextMessage(roomId, "a body", TXN_ID).then(function(res) {
|
client.sendTextMessage(roomId, "a body", TXN_ID).then(function(res) {
|
||||||
console.log("sendTextMessage completed");
|
console.log("sendTextMessage completed");
|
||||||
expect(res.event_id).toEqual(event.event_id);
|
expect(res.event_id).toEqual(event.event_id);
|
||||||
return client.getEventTimeline(room, event.event_id);
|
return client.getEventTimeline(timelineSet, event.event_id);
|
||||||
}).then(function(tl) {
|
}).then(function(tl) {
|
||||||
console.log("getEventTimeline completed (2)");
|
console.log("getEventTimeline completed (2)");
|
||||||
expect(tl.getEvents().length).toEqual(2);
|
expect(tl.getEvents().length).toEqual(2);
|
||||||
@@ -644,7 +655,7 @@ describe("MatrixClient event timelines", function() {
|
|||||||
}).catch(utils.failTest).done(done);
|
}).catch(utils.failTest).done(done);
|
||||||
|
|
||||||
httpBackend.flush("/sync", 1).then(function() {
|
httpBackend.flush("/sync", 1).then(function() {
|
||||||
return client.getEventTimeline(room, event.event_id);
|
return client.getEventTimeline(timelineSet, event.event_id);
|
||||||
}).then(function(tl) {
|
}).then(function(tl) {
|
||||||
console.log("getEventTimeline completed (1)");
|
console.log("getEventTimeline completed (1)");
|
||||||
expect(tl.getEvents().length).toEqual(2);
|
expect(tl.getEvents().length).toEqual(2);
|
||||||
|
|||||||
@@ -16,7 +16,12 @@ describe("EventTimeline", function() {
|
|||||||
|
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
utils.beforeEach(this);
|
utils.beforeEach(this);
|
||||||
timeline = new EventTimeline(roomId);
|
|
||||||
|
// XXX: this is a horrid hack; should use sinon or something instead to mock
|
||||||
|
var timelineSet = { room: { roomId: roomId }};
|
||||||
|
timelineSet.room.getUnfilteredTimelineSet = function() { return timelineSet; };
|
||||||
|
|
||||||
|
timeline = new EventTimeline(timelineSet);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("construction", function() {
|
describe("construction", function() {
|
||||||
|
|||||||
@@ -479,14 +479,14 @@ describe("Room", function() {
|
|||||||
it("should handle events in the same timeline", function() {
|
it("should handle events in the same timeline", function() {
|
||||||
room.addLiveEvents(events);
|
room.addLiveEvents(events);
|
||||||
|
|
||||||
expect(room.compareEventOrdering(events[0].getId(),
|
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId(),
|
||||||
events[1].getId()))
|
events[1].getId()))
|
||||||
.toBeLessThan(0);
|
.toBeLessThan(0);
|
||||||
expect(room.compareEventOrdering(events[2].getId(),
|
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[2].getId(),
|
||||||
events[1].getId()))
|
events[1].getId()))
|
||||||
.toBeGreaterThan(0);
|
.toBeGreaterThan(0);
|
||||||
expect(room.compareEventOrdering(events[1].getId(),
|
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[1].getId(),
|
||||||
events[1].getId()))
|
events[1].getId()))
|
||||||
.toEqual(0);
|
.toEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -498,11 +498,11 @@ describe("Room", function() {
|
|||||||
room.addEventsToTimeline([events[0]], false, oldTimeline);
|
room.addEventsToTimeline([events[0]], false, oldTimeline);
|
||||||
room.addLiveEvents([events[1]]);
|
room.addLiveEvents([events[1]]);
|
||||||
|
|
||||||
expect(room.compareEventOrdering(events[0].getId(),
|
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId(),
|
||||||
events[1].getId()))
|
events[1].getId()))
|
||||||
.toBeLessThan(0);
|
.toBeLessThan(0);
|
||||||
expect(room.compareEventOrdering(events[1].getId(),
|
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[1].getId(),
|
||||||
events[0].getId()))
|
events[0].getId()))
|
||||||
.toBeGreaterThan(0);
|
.toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -512,24 +512,26 @@ describe("Room", function() {
|
|||||||
room.addEventsToTimeline([events[0]], false, oldTimeline);
|
room.addEventsToTimeline([events[0]], false, oldTimeline);
|
||||||
room.addLiveEvents([events[1]]);
|
room.addLiveEvents([events[1]]);
|
||||||
|
|
||||||
expect(room.compareEventOrdering(events[0].getId(),
|
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId(),
|
||||||
events[1].getId()))
|
events[1].getId()))
|
||||||
.toBe(null);
|
.toBe(null);
|
||||||
expect(room.compareEventOrdering(events[1].getId(),
|
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[1].getId(),
|
||||||
events[0].getId()))
|
events[0].getId()))
|
||||||
.toBe(null);
|
.toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return null for unknown events", function() {
|
it("should return null for unknown events", function() {
|
||||||
room.addLiveEvents(events);
|
room.addLiveEvents(events);
|
||||||
|
|
||||||
expect(room.compareEventOrdering(events[0].getId(), "xxx"))
|
expect(room.getUnfilteredTimelineSet()
|
||||||
.toBe(null);
|
.compareEventOrdering(events[0].getId(), "xxx"))
|
||||||
expect(room.compareEventOrdering("xxx", events[0].getId()))
|
.toBe(null);
|
||||||
.toBe(null);
|
expect(room.getUnfilteredTimelineSet()
|
||||||
expect(room.compareEventOrdering(events[0].getId(),
|
.compareEventOrdering("xxx", events[0].getId()))
|
||||||
events[0].getId()))
|
.toBe(null);
|
||||||
.toBe(0);
|
expect(room.getUnfilteredTimelineSet()
|
||||||
|
.compareEventOrdering(events[0].getId(), events[0].getId()))
|
||||||
|
.toBe(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,11 @@ function createTimeline(numEvents, baseIndex) {
|
|||||||
if (numEvents === undefined) { numEvents = 3; }
|
if (numEvents === undefined) { numEvents = 3; }
|
||||||
if (baseIndex === undefined) { baseIndex = 1; }
|
if (baseIndex === undefined) { baseIndex = 1; }
|
||||||
|
|
||||||
var timeline = new EventTimeline(ROOM_ID);
|
// XXX: this is a horrid hack
|
||||||
|
var timelineSet = { room: { roomId: ROOM_ID }};
|
||||||
|
timelineSet.room.getUnfilteredTimelineSet = function() { return timelineSet; };
|
||||||
|
|
||||||
|
var timeline = new EventTimeline(timelineSet);
|
||||||
|
|
||||||
// add the events after the baseIndex first
|
// add the events after the baseIndex first
|
||||||
addEventsToTimeline(timeline, numEvents - baseIndex, false);
|
addEventsToTimeline(timeline, numEvents - baseIndex, false);
|
||||||
@@ -133,19 +137,19 @@ describe("TimelineIndex", function() {
|
|||||||
|
|
||||||
describe("TimelineWindow", function() {
|
describe("TimelineWindow", function() {
|
||||||
/**
|
/**
|
||||||
* create a dummy room and client, and a TimelineWindow
|
* create a dummy eventTimelineSet and client, and a TimelineWindow
|
||||||
* attached to them.
|
* attached to them.
|
||||||
*/
|
*/
|
||||||
var room, client;
|
var timelineSet, client;
|
||||||
function createWindow(timeline, opts) {
|
function createWindow(timeline, opts) {
|
||||||
room = {};
|
timelineSet = {};
|
||||||
client = {};
|
client = {};
|
||||||
client.getEventTimeline = function(room0, eventId0) {
|
client.getEventTimeline = function(timelineSet0, eventId0) {
|
||||||
expect(room0).toBe(room);
|
expect(timelineSet0).toBe(timelineSet);
|
||||||
return q(timeline);
|
return q(timeline);
|
||||||
};
|
};
|
||||||
|
|
||||||
return new TimelineWindow(client, room, opts);
|
return new TimelineWindow(client, timelineSet, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
@@ -169,15 +173,15 @@ describe("TimelineWindow", function() {
|
|||||||
var timeline = createTimeline();
|
var timeline = createTimeline();
|
||||||
var eventId = timeline.getEvents()[1].getId();
|
var eventId = timeline.getEvents()[1].getId();
|
||||||
|
|
||||||
var room = {};
|
var timelineSet = {};
|
||||||
var client = {};
|
var client = {};
|
||||||
client.getEventTimeline = function(room0, eventId0) {
|
client.getEventTimeline = function(timelineSet0, eventId0) {
|
||||||
expect(room0).toBe(room);
|
expect(timelineSet0).toBe(timelineSet);
|
||||||
expect(eventId0).toEqual(eventId);
|
expect(eventId0).toEqual(eventId);
|
||||||
return q(timeline);
|
return q(timeline);
|
||||||
};
|
};
|
||||||
|
|
||||||
var timelineWindow = new TimelineWindow(client, room);
|
var timelineWindow = new TimelineWindow(client, timelineSet);
|
||||||
timelineWindow.load(eventId, 3).then(function() {
|
timelineWindow.load(eventId, 3).then(function() {
|
||||||
var expectedEvents = timeline.getEvents();
|
var expectedEvents = timeline.getEvents();
|
||||||
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
|
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
|
||||||
@@ -192,12 +196,12 @@ describe("TimelineWindow", function() {
|
|||||||
|
|
||||||
var eventId = timeline.getEvents()[1].getId();
|
var eventId = timeline.getEvents()[1].getId();
|
||||||
|
|
||||||
var room = {};
|
var timelineSet = {};
|
||||||
var client = {};
|
var client = {};
|
||||||
|
|
||||||
var timelineWindow = new TimelineWindow(client, room);
|
var timelineWindow = new TimelineWindow(client, timelineSet);
|
||||||
|
|
||||||
client.getEventTimeline = function(room0, eventId0) {
|
client.getEventTimeline = function(timelineSet0, eventId0) {
|
||||||
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
|
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
|
||||||
.toBe(false);
|
.toBe(false);
|
||||||
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
|
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
|
||||||
|
|||||||
Reference in New Issue
Block a user