You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-11-26 17:03:12 +03:00
Support for non-contiguous event timelines
This provides optional support for fetching old events via the /context API, and paginating backwards and forwards from them, eventually merging into the live timeline. To support it, events are now stored in an EventTimeline, rather than directly in an array in the Room; the old names are maintained as references for compatibility. The feature has to be enabled explicitly, otherwise it would be impossible for existing clients to back-paginate to the old events after a gappy /sync. Still TODO here: * An object which provides a window into the timelines to make them possible to use. This will be a separate PR. * Rewrite the 'EventContext' used by the searchRoomEvents API in terms of an EventTimeline - it is essentially a subset.
This commit is contained in:
145
lib/client.js
145
lib/client.js
@@ -81,6 +81,10 @@ var OLM_ALGORITHM = "m.olm.v1.curve25519-aes-sha2";
|
||||
* to all requests with this client. Useful for application services which require
|
||||
* <code>?user_id=</code>.
|
||||
*
|
||||
* @param {boolean} [opts.timelineSupport = false] Set to true to enable improved
|
||||
* timeline support ((@link getEventTimeline}). It is disabled by default for
|
||||
* compatibility with older clients - in particular to maintain support for
|
||||
* back-paginating the live timeline after a '/sync' result with a gap.
|
||||
*/
|
||||
function MatrixClient(opts) {
|
||||
utils.checkObjectHasKeys(opts, ["baseUrl", "request"]);
|
||||
@@ -172,6 +176,8 @@ function MatrixClient(opts) {
|
||||
this._peekSync = null;
|
||||
this._isGuest = false;
|
||||
this._ongoingScrollbacks = {};
|
||||
|
||||
this.timelineSupport = Boolean(opts.timelineSupport);
|
||||
}
|
||||
utils.inherits(MatrixClient, EventEmitter);
|
||||
|
||||
@@ -2060,6 +2066,145 @@ MatrixClient.prototype.paginateEventContext = function(eventContext, opts) {
|
||||
return promise;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get an EventTimeline for the given event
|
||||
*
|
||||
* <p>If the room object already has the given event in its store, the
|
||||
* corresponding timeline will be returned. Otherwise, we make a /context
|
||||
* request, and use it to construct an EventTimeline.
|
||||
*
|
||||
* @param {Room} room The room to look for the event in
|
||||
* @param {string} eventId The ID of the event to look for
|
||||
*
|
||||
* @return {module:client.Promise} Resolves:
|
||||
* {@link module:models/event-timeline~EventTimeline} including the given
|
||||
* event
|
||||
*/
|
||||
MatrixClient.prototype.getEventTimeline = function(room, eventId) {
|
||||
// don't allow any timeline support unless it's been enabled.
|
||||
if (!this.timelineSupport) {
|
||||
throw Error("timeline support is disabled. Set the 'timelineSupport'" +
|
||||
" parameter to true when creating MatrixClient to enable" +
|
||||
" it.");
|
||||
}
|
||||
|
||||
if (room.getTimelineForEvent(eventId)) {
|
||||
return q(room.getTimelineForEvent(eventId));
|
||||
}
|
||||
|
||||
var path = utils.encodeUri(
|
||||
"/rooms/$roomId/context/$eventId", {
|
||||
$roomId: room.roomId,
|
||||
$eventId: eventId,
|
||||
}
|
||||
);
|
||||
|
||||
// TODO: we should implement a backoff (as per scrollback()) to deal more
|
||||
// nicely with HTTP errors.
|
||||
var self = this;
|
||||
var promise =
|
||||
self._http.authedRequest(undefined, "GET", path
|
||||
).then(function(res) {
|
||||
if (!res.event) {
|
||||
throw Error("'event' not in '/context' result - homeserver too old?");
|
||||
}
|
||||
|
||||
// by the time the request completes, the event might have ended up in
|
||||
// the timeline.
|
||||
if (room.getTimelineForEvent(eventId)) {
|
||||
return room.getTimelineForEvent(eventId);
|
||||
}
|
||||
|
||||
// we start with the last event, since that's the point at which we
|
||||
// have known state.
|
||||
var events = res.events_before
|
||||
.concat([res.event])
|
||||
.concat(res.events_after);
|
||||
events.reverse();
|
||||
var matrixEvents = utils.map(events, self.getEventMapper());
|
||||
|
||||
var timeline = room.getTimelineForEvent(matrixEvents[0].getId());
|
||||
if (!timeline) {
|
||||
timeline = room.createTimeline();
|
||||
timeline.initialiseState(utils.map(res.state,
|
||||
self.getEventMapper()));
|
||||
timeline.getState(false).paginationToken = res.end;
|
||||
}
|
||||
room.addEventsToTimeline(matrixEvents, true, timeline, res.start);
|
||||
|
||||
// there is no guarantee that the event ended up in "timeline" (we
|
||||
// might have switched to a neighbouring timeline) - so check 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
|
||||
// return the timeline we first thought of.
|
||||
return room.getTimelineForEvent(eventId) || timeline;
|
||||
});
|
||||
return promise;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Take an EventTimeline, and back/forward-fill results.
|
||||
*
|
||||
* @param {module:models/event-timeline~EventTimeline} eventTimeline timeline
|
||||
* object to be updated
|
||||
* @param {Object =} opts
|
||||
* @param {boolean} [opts.backwards = false] true to fill backwards,
|
||||
* false to go forwards
|
||||
* @param {number} [opts.limit = 30] number of events to request
|
||||
*
|
||||
* @return {module:client.Promise} Resolves: false if there are no events and
|
||||
* we reached the end of the timeline; else true. Rejects: Error with an error response
|
||||
*/
|
||||
MatrixClient.prototype.paginateEventTimeline = function(eventTimeline, opts) {
|
||||
// TODO: we should implement a backoff (as per scrollback()) to deal more
|
||||
// nicely with HTTP errors.
|
||||
opts = opts || {};
|
||||
var backwards = opts.backwards || false;
|
||||
|
||||
var token = eventTimeline.getPaginationToken(backwards);
|
||||
if (!token) {
|
||||
// no more results.
|
||||
return q.reject(new Error("No paginate token"));
|
||||
}
|
||||
|
||||
var dir = backwards ? 'b' : 'f';
|
||||
var pendingRequest = eventTimeline._paginationRequests[dir];
|
||||
|
||||
if (pendingRequest) {
|
||||
// already a request in progress - return the existing promise
|
||||
return pendingRequest;
|
||||
}
|
||||
|
||||
var path = utils.encodeUri(
|
||||
"/rooms/$roomId/messages", {$roomId: eventTimeline.getRoomId()}
|
||||
);
|
||||
var params = {
|
||||
from: token,
|
||||
limit: ('limit' in opts) ? opts.limit : 30,
|
||||
dir: dir
|
||||
};
|
||||
|
||||
var self = this;
|
||||
|
||||
var promise =
|
||||
this._http.authedRequest(undefined, "GET", path, params
|
||||
).then(function(res) {
|
||||
var token = res.end;
|
||||
console.log("client: completed paginate; backwards=" + backwards +
|
||||
"; token=" + token);
|
||||
var room = self.getRoom(eventTimeline.getRoomId());
|
||||
var matrixEvents = utils.map(res.chunk, self.getEventMapper());
|
||||
room.addEventsToTimeline(matrixEvents, backwards, eventTimeline, token);
|
||||
return res.end != res.start;
|
||||
}).finally(function() {
|
||||
eventTimeline._paginationRequests[dir] = null;
|
||||
});
|
||||
eventTimeline._paginationRequests[dir] = promise;
|
||||
|
||||
return promise;
|
||||
};
|
||||
|
||||
// Registration/Login operations
|
||||
// =============================
|
||||
|
||||
|
||||
@@ -32,6 +32,8 @@ module.exports.MatrixError = require("./http-api").MatrixError;
|
||||
module.exports.MatrixClient = require("./client").MatrixClient;
|
||||
/** The {@link module:models/room~Room|Room} class. */
|
||||
module.exports.Room = require("./models/room");
|
||||
/** The {@link module:models/event-timeline~EventTimeline} class. */
|
||||
module.exports.EventTimeline = require("./models/event-timeline");
|
||||
/** The {@link module:models/room-member~RoomMember|RoomMember} class. */
|
||||
module.exports.RoomMember = require("./models/room-member");
|
||||
/** The {@link module:models/room-state~RoomState|RoomState} class. */
|
||||
|
||||
252
lib/models/event-timeline.js
Normal file
252
lib/models/event-timeline.js
Normal file
@@ -0,0 +1,252 @@
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* @module models/event-timeline
|
||||
*/
|
||||
|
||||
var RoomState = require("./room-state");
|
||||
|
||||
/**
|
||||
* Construct a new EventTimeline
|
||||
*
|
||||
* <p>An EventTimeline represents a contiguous sequence of events in a room.
|
||||
*
|
||||
* <p>As well as keeping track of the events themselves, it stores the state of
|
||||
* the room at the beginning and end of the timeline, and pagination tokens for
|
||||
* going backwards and forwards in the timeline.
|
||||
*
|
||||
* <p>In order that clients can meaningfully maintain an index into a timeline, we
|
||||
* track a 'baseIndex'. This starts at zero, but is incremented when events are
|
||||
* prepended to the timeline. The index of an event relative to baseIndex
|
||||
* therefore remains constant.
|
||||
*
|
||||
* <p>Once a timeline joins up with its neighbour, we link them together into a
|
||||
* doubly-linked list.
|
||||
*
|
||||
* @param {string} roomId the ID of the room where this timeline came from
|
||||
* @constructor
|
||||
*/
|
||||
function EventTimeline(roomId) {
|
||||
this._roomId = roomId;
|
||||
this._events = [];
|
||||
this._baseIndex = -1;
|
||||
this._startState = new RoomState(roomId);
|
||||
this._endState = new RoomState(roomId);
|
||||
|
||||
this._prevTimeline = null;
|
||||
this._nextTimeline = null;
|
||||
|
||||
// this is used by client.js
|
||||
this._paginationRequests = {'b': null, 'f': null};
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise the start and end state with the given events
|
||||
*
|
||||
* <p>This can only be called before any events are added.
|
||||
*
|
||||
* @param {MatrixEvent[]} stateEvents list of state events to intialise the
|
||||
* state with.
|
||||
*/
|
||||
EventTimeline.prototype.initialiseState = function(stateEvents) {
|
||||
if (this._events.length > 0) {
|
||||
throw new Error("Cannot initialise state after events are added");
|
||||
}
|
||||
|
||||
// do we need to copy here? sync thinks we do but I can't see why
|
||||
this._startState.setStateEvents(stateEvents);
|
||||
this._endState.setStateEvents(stateEvents);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the ID of the room for this timeline
|
||||
* @return {string} room ID
|
||||
*/
|
||||
EventTimeline.prototype.getRoomId = function() {
|
||||
return this._roomId;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the base index
|
||||
*
|
||||
* @return {number}
|
||||
*/
|
||||
EventTimeline.prototype.getBaseIndex = function() {
|
||||
return this._baseIndex;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the list of events in this context
|
||||
*
|
||||
* @return {MatrixEvent[]} An array of MatrixEvents
|
||||
*/
|
||||
EventTimeline.prototype.getEvents = function() {
|
||||
return this._events;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the room state at the start/end of the timeline
|
||||
*
|
||||
* @param {boolean} start true to get the state at the start of the timeline;
|
||||
* false to get the state at the end of the timeline.
|
||||
* @return {RoomState} state at the start/end of the timeline
|
||||
*/
|
||||
EventTimeline.prototype.getState = function(start) {
|
||||
return start ? this._startState : this._endState;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a pagination token
|
||||
*
|
||||
* @param {boolean} backwards true to get the pagination token for going
|
||||
* backwards in time
|
||||
* @return {?string} pagination token
|
||||
*/
|
||||
EventTimeline.prototype.getPaginationToken = function(backwards) {
|
||||
return this.getState(backwards).paginationToken;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set a pagination token
|
||||
*
|
||||
* @param {?string} token pagination token
|
||||
* @param {boolean} backwards true to set the pagination token for going
|
||||
* backwards in time
|
||||
*/
|
||||
EventTimeline.prototype.setPaginationToken = function(token, backwards) {
|
||||
this.getState(backwards).paginationToken = token;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the next timeline in the series
|
||||
*
|
||||
* @param {boolean} before true to get the previous timeline; false to get the
|
||||
* following one
|
||||
*
|
||||
* @return {?EventTimeline} previous or following timeline, if they have been
|
||||
* joined up.
|
||||
*/
|
||||
EventTimeline.prototype.getNeighbouringTimeline = function(before) {
|
||||
return before ? this._prevTimeline : this._nextTimeline;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the next timeline in the series
|
||||
*
|
||||
* @param {EventTimeline} neighbour previous/following timeline
|
||||
*
|
||||
* @param {boolean} before true to set the previous timeline; false to set
|
||||
* following one.
|
||||
*/
|
||||
EventTimeline.prototype.setNeighbouringTimeline = function(neighbour, before) {
|
||||
if (this.getNeighbouringTimeline(before)) {
|
||||
throw new Error("timeline already has a neighbouring timeline - " +
|
||||
"cannot reset neighbour");
|
||||
}
|
||||
if (before) {
|
||||
this._prevTimeline = neighbour;
|
||||
} else {
|
||||
this._nextTimeline = neighbour;
|
||||
}
|
||||
|
||||
// make sure we don't try to paginate this timeline
|
||||
this.setPaginationToken(null, before);
|
||||
};
|
||||
|
||||
/**
|
||||
* Add a new event to the timeline, and update the state
|
||||
*
|
||||
* @param {MatrixEvent} event new event
|
||||
* @param {boolean} atStart true to insert new event at the start
|
||||
* @param {boolean} [spliceBeforeLocalEcho = false] insert this event before any
|
||||
* localecho events at the end of the timeline. Ignored if atStart == true
|
||||
*/
|
||||
EventTimeline.prototype.addEvent = function(event, atStart, spliceBeforeLocalEcho) {
|
||||
var stateContext = atStart ? this._startState : this._endState;
|
||||
|
||||
setEventMetadata(event, stateContext, atStart);
|
||||
|
||||
// modify state
|
||||
if (event.isState()) {
|
||||
stateContext.setStateEvents([event]);
|
||||
// it is possible that the act of setting the state event means we
|
||||
// can set more metadata (specifically sender/target props), so try
|
||||
// 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)
|
||||
// so we want to use the *updated* member (new avatar/name) instead.
|
||||
if (!event.sender || event.getType() === "m.room.member") {
|
||||
setEventMetadata(event, stateContext, atStart);
|
||||
}
|
||||
}
|
||||
|
||||
var insertIndex;
|
||||
|
||||
if (atStart) {
|
||||
insertIndex = 0;
|
||||
} else {
|
||||
insertIndex = this._events.length;
|
||||
|
||||
// if this is a real event, we might need to splice it in before any pending
|
||||
// local echo events.
|
||||
if (spliceBeforeLocalEcho) {
|
||||
for (var j = this._events.length - 1; j >= 0; j--) {
|
||||
if (!this._events[j].status) { // real events don't have a status
|
||||
insertIndex = j + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._events.splice(insertIndex, 0, event); // insert element
|
||||
if (insertIndex <= this._baseIndex || this._baseIndex == -1) {
|
||||
this._baseIndex++;
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an event from the timeline
|
||||
*
|
||||
* @param {string} eventId ID of event to be removed
|
||||
* @return {?MatrixEvent} removed event, or null if not found
|
||||
*/
|
||||
EventTimeline.prototype.removeEvent = function(eventId) {
|
||||
for (var i = this._events.length - 1; i >= 0; i--) {
|
||||
var ev = this._events[i];
|
||||
if (ev.getId() == eventId) {
|
||||
this._events.splice(i, 1);
|
||||
if (i < this._baseIndex) {
|
||||
this._baseIndex--;
|
||||
}
|
||||
return ev;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* The EventTimeline class
|
||||
*/
|
||||
module.exports = EventTimeline;
|
||||
@@ -20,11 +20,11 @@ limitations under the License.
|
||||
var EventEmitter = require("events").EventEmitter;
|
||||
|
||||
var EventStatus = require("./event").EventStatus;
|
||||
var RoomState = require("./room-state");
|
||||
var RoomSummary = require("./room-summary");
|
||||
var MatrixEvent = require("./event").MatrixEvent;
|
||||
var utils = require("../utils");
|
||||
var ContentRepo = require("../content-repo");
|
||||
var EventTimeline = require("./event-timeline");
|
||||
|
||||
function synthesizeReceipt(userId, event, receiptType) {
|
||||
// This is really ugly because JS has no way to express an object literal
|
||||
@@ -45,6 +45,22 @@ function synthesizeReceipt(userId, event, receiptType) {
|
||||
|
||||
/**
|
||||
* Construct a new Room.
|
||||
*
|
||||
* <p>For a room, we store 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. 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 {string} roomId Required. The ID of this room.
|
||||
* @param {Object=} opts Configuration options
|
||||
@@ -56,18 +72,24 @@ function synthesizeReceipt(userId, event, receiptType) {
|
||||
* when the call to <code>sendEvent</code> was made. If "<b>end</b>", pending messages
|
||||
* will always appear at the end of the timeline (multiple pending messages will be sorted
|
||||
* chronologically). Default: "chronological".
|
||||
* @param {boolean} [opts.timelineSupport = false] Set to true to enable improved
|
||||
* timeline support.
|
||||
*
|
||||
* @prop {string} roomId The ID of this room.
|
||||
* @prop {string} name The human-readable display name for this room.
|
||||
* @prop {Array<MatrixEvent>} timeline The ordered list of message events for
|
||||
* this room, with the oldest event at index 0.
|
||||
* @prop {Array<MatrixEvent>} timeline The live event timeline for this room,
|
||||
* with the oldest event at index 0. Present for backwards compatibility -
|
||||
* prefer getLiveTimeline().getEvents().
|
||||
* @prop {object} tags Dict of room tags; the keys are the tag name and the values
|
||||
* are any metadata associated with the tag - e.g. { "fav" : { order: 1 } }
|
||||
* @prop {object} accountData Dict of per-room account_data events; the keys are the
|
||||
* event type and the values are the events.
|
||||
* @prop {RoomState} oldState The state of the room at the time of the oldest
|
||||
* event in the timeline.
|
||||
* event in the live timeline. Present for backwards compatibility -
|
||||
* prefer getLiveTimeline().getState(true).
|
||||
* @prop {RoomState} currentState The state of the room at the time of the
|
||||
* newest event in the timeline.
|
||||
* newest event in the timeline. Present for backwards compatibility -
|
||||
* prefer getLiveTimeline().getState(false).
|
||||
* @prop {RoomSummary} summary The room summary.
|
||||
* @prop {*} storageToken A token which a data store can use to remember
|
||||
* the state of the room.
|
||||
@@ -85,7 +107,6 @@ function Room(roomId, opts) {
|
||||
|
||||
this.roomId = roomId;
|
||||
this.name = roomId;
|
||||
this.timeline = [];
|
||||
this.tags = {
|
||||
// $tagName: { $metadata: $value },
|
||||
// $tagName: { $metadata: $value },
|
||||
@@ -93,8 +114,6 @@ function Room(roomId, opts) {
|
||||
this.accountData = {
|
||||
// $eventType: $event
|
||||
};
|
||||
this.oldState = new RoomState(roomId);
|
||||
this.currentState = new RoomState(roomId);
|
||||
this.summary = null;
|
||||
this.storageToken = opts.storageToken;
|
||||
this._opts = opts;
|
||||
@@ -119,9 +138,68 @@ function Room(roomId, opts) {
|
||||
// data: <receipt data>
|
||||
// }]
|
||||
};
|
||||
|
||||
// just a list - *not* ordered.
|
||||
this._timelines = [];
|
||||
this._eventIdToTimeline = {};
|
||||
this._timelineSupport = Boolean(opts.timelineSupport);
|
||||
|
||||
this.resetLiveTimeline();
|
||||
}
|
||||
utils.inherits(Room, EventEmitter);
|
||||
|
||||
/**
|
||||
* Get the live timeline for this room.
|
||||
*
|
||||
* @return {module:models/event-timeline~EventTimeline} live timeline
|
||||
*/
|
||||
Room.prototype.getLiveTimeline = function() {
|
||||
return this._liveTimeline;
|
||||
};
|
||||
|
||||
/**
|
||||
* Reset the live timeline, and start a new one.
|
||||
*
|
||||
* <p>This is used when /sync returns a 'limited' timeline.
|
||||
*/
|
||||
Room.prototype.resetLiveTimeline = function() {
|
||||
var newTimeline;
|
||||
|
||||
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.createTimeline();
|
||||
}
|
||||
|
||||
if (this._liveTimeline) {
|
||||
// initialise the state in the new timeline from our last known state
|
||||
newTimeline.initialiseState(this._liveTimeline.getState(false).events);
|
||||
}
|
||||
this._liveTimeline = newTimeline;
|
||||
|
||||
// maintain this.timeline as a reference to the live timeline,
|
||||
// and this.oldState and this.currentState as references to the
|
||||
// state at the start and end of that timeline. These are more
|
||||
// for backwards-compatibility than anything else.
|
||||
this.timeline = this._liveTimeline.getEvents();
|
||||
this.oldState = this._liveTimeline.getState(true);
|
||||
this.currentState = this._liveTimeline.getState(false);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 undefined if unknown
|
||||
*/
|
||||
Room.prototype.getTimelineForEvent = function(eventId) {
|
||||
return this._eventIdToTimeline[eventId];
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the avatar URL for a room if one was set.
|
||||
* @param {String} baseUrl The homeserver base URL. See
|
||||
@@ -214,43 +292,195 @@ Room.prototype.getAvatarUrl = function(baseUrl, width, height, resizeMethod,
|
||||
};
|
||||
|
||||
/**
|
||||
* Add some events to this room's timeline. Will fire "Room.timeline" for
|
||||
* each event added.
|
||||
* Create a new timeline for this room
|
||||
*
|
||||
* @param {Array<MatrixEvent>} stateBeforeEvent state of the room before this event
|
||||
* @return {module:models/event-timeline~EventTimeline} newly-created timeline
|
||||
*/
|
||||
Room.prototype.createTimeline = function() {
|
||||
if (!this._timelineSupport) {
|
||||
throw Error("timeline support is disabled. Set the 'timelineSupport'" +
|
||||
" parameter to true when creating MatrixClient to enable" +
|
||||
" it.");
|
||||
}
|
||||
|
||||
var timeline = new EventTimeline(this.roomId);
|
||||
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. If not given, events will be added to the live timeline
|
||||
*
|
||||
* @param {string=} paginationToken token for the next batch of events
|
||||
*
|
||||
* @fires module:client~MatrixClient#event:"Room.timeline"
|
||||
*
|
||||
*/
|
||||
Room.prototype.addEventsToTimeline = function(events, toStartOfTimeline) {
|
||||
var stateContext = toStartOfTimeline ? this.oldState : this.currentState;
|
||||
|
||||
function checkForRedaction(redactEvent) {
|
||||
return function(e) {
|
||||
return e.getId() === redactEvent.event.redacts;
|
||||
};
|
||||
Room.prototype.addEventsToTimeline = function(events, toStartOfTimeline,
|
||||
timeline, paginationToken) {
|
||||
if (!timeline) {
|
||||
timeline = this._liveTimeline;
|
||||
}
|
||||
|
||||
if (!toStartOfTimeline && timeline == this._liveTimeline) {
|
||||
// special treatment for live events
|
||||
this._addLiveEvents(events);
|
||||
return;
|
||||
}
|
||||
|
||||
var updateToken = false;
|
||||
for (var i = 0; i < events.length; i++) {
|
||||
var existingTimeline = this._checkExistingTimeline(events[i], timeline,
|
||||
toStartOfTimeline);
|
||||
if (existingTimeline) {
|
||||
// switch to the other timeline
|
||||
timeline = existingTimeline;
|
||||
updateToken = false;
|
||||
} else {
|
||||
this._addEventToTimeline(events[i], timeline, toStartOfTimeline);
|
||||
updateToken = true;
|
||||
}
|
||||
}
|
||||
if (updateToken) {
|
||||
timeline.setPaginationToken(paginationToken, toStartOfTimeline);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if this event is already in a timeline, and join up the timelines if
|
||||
* necessary
|
||||
*
|
||||
* @param {MatrixEvent} event event to add
|
||||
* @param {EventTimeline} timeline timeline we think we should add to
|
||||
* @param {boolean} toStartOfTimeline true if we're adding to the start of
|
||||
* the timeline
|
||||
* @return {?EventTimeline} the timeline with the event already in, or null if
|
||||
* none
|
||||
* @private
|
||||
*/
|
||||
Room.prototype._checkExistingTimeline = function(event, timeline,
|
||||
toStartOfTimeline) {
|
||||
var eventId = event.getId();
|
||||
|
||||
var existingTimeline = this._eventIdToTimeline[eventId];
|
||||
if (!existingTimeline) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// we already know about this event. Hopefully it's in this timeline, or
|
||||
// its neighbour
|
||||
if (existingTimeline == timeline) {
|
||||
console.log("Event " + eventId + " already in timeline " + timeline);
|
||||
return timeline;
|
||||
}
|
||||
|
||||
var neighbour = timeline.getNeighbouringTimeline(toStartOfTimeline);
|
||||
if (neighbour) {
|
||||
if (existingTimeline == neighbour) {
|
||||
console.log("Event " + eventId + " in neighbouring timeline - " +
|
||||
"switching to " + existingTimeline);
|
||||
} else {
|
||||
console.warn("Event " + eventId + " already in a different " +
|
||||
"timeline " + existingTimeline);
|
||||
}
|
||||
return existingTimeline;
|
||||
}
|
||||
|
||||
// time to join the timelines.
|
||||
console.info("Already have timeline for " + eventId +
|
||||
" - joining timeline " + timeline + " to " +
|
||||
existingTimeline);
|
||||
timeline.setNeighbouringTimeline(existingTimeline, toStartOfTimeline);
|
||||
existingTimeline.setNeighbouringTimeline(timeline, !toStartOfTimeline);
|
||||
return existingTimeline;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check for redactions, and otherwise add event to the given timeline. Assumes
|
||||
* we have already checked we don't lnow about this event.
|
||||
*
|
||||
* Will fire "Room.timeline" for each event added.
|
||||
*
|
||||
* @param {MatrixEvent} event
|
||||
* @param {EventTimeline} timeline
|
||||
* @param {boolean} toStartOfTimeline
|
||||
* @param {boolean} spliceBeforeLocalEcho
|
||||
* @fires module:client~MatrixClient#event:"Room.timeline"
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
Room.prototype._addEventToTimeline = function(event, timeline, toStartOfTimeline,
|
||||
spliceBeforeLocalEcho) {
|
||||
var eventId = event.getId();
|
||||
|
||||
if (this._redactions.indexOf(eventId) >= 0) {
|
||||
return; // do not add the redacted event.
|
||||
}
|
||||
|
||||
if (event.getType() === "m.room.redaction") {
|
||||
var redactId = event.event.redacts;
|
||||
|
||||
// try to remove the element
|
||||
var removed = this.removeEvent(redactId);
|
||||
if (!removed) {
|
||||
// redactions will trickle in BEFORE the event redacted so make
|
||||
// a note of the redacted event; we'll check it later.
|
||||
this._redactions.push(event.event.redacts);
|
||||
}
|
||||
// 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.
|
||||
}
|
||||
|
||||
if (this._redactions.indexOf(eventId) < 0) {
|
||||
timeline.addEvent(event, toStartOfTimeline, spliceBeforeLocalEcho);
|
||||
this._eventIdToTimeline[eventId] = timeline;
|
||||
}
|
||||
|
||||
var data = {
|
||||
timeline: timeline,
|
||||
liveEvent: !toStartOfTimeline && timeline == this._liveTimeline,
|
||||
};
|
||||
this.emit("Room.timeline", event, this, Boolean(toStartOfTimeline), false, data);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Add some events to the end of this room's live timeline. Will fire
|
||||
* "Room.timeline" for each event added.
|
||||
*
|
||||
* @param {MatrixEvent[]} events A list of events to add.
|
||||
* @fires module:client~MatrixClient#event:"Room.timeline"
|
||||
* @private
|
||||
*/
|
||||
Room.prototype._addLiveEvents = function(events) {
|
||||
var addLocalEchoToEnd = this._opts.pendingEventOrdering === "end";
|
||||
|
||||
for (var i = 0; i < events.length; i++) {
|
||||
if (toStartOfTimeline && this._redactions.indexOf(events[i].getId()) >= 0) {
|
||||
continue; // do not add the redacted event.
|
||||
}
|
||||
var isLocalEcho = (
|
||||
!Boolean(toStartOfTimeline) && (
|
||||
events[i].status === EventStatus.SENDING ||
|
||||
events[i].status === EventStatus.QUEUED
|
||||
)
|
||||
);
|
||||
|
||||
// FIXME: HORRIBLE ASSUMPTION THAT THIS PROP EXISTS
|
||||
// Exists due to client.js:815 (MatrixClient.sendEvent)
|
||||
// We should make txnId a first class citizen.
|
||||
if (!toStartOfTimeline && events[i]._txnId) {
|
||||
if (events[i]._txnId) {
|
||||
this._txnToEvent[events[i]._txnId] = events[i];
|
||||
}
|
||||
else if (!toStartOfTimeline && events[i].getUnsigned().transaction_id) {
|
||||
else if (events[i].getUnsigned().transaction_id) {
|
||||
var existingEvent = this._txnToEvent[events[i].getUnsigned().transaction_id];
|
||||
if (existingEvent) {
|
||||
// no longer pending
|
||||
@@ -261,54 +491,12 @@ Room.prototype.addEventsToTimeline = function(events, toStartOfTimeline) {
|
||||
}
|
||||
}
|
||||
|
||||
setEventMetadata(events[i], stateContext, toStartOfTimeline);
|
||||
// modify state
|
||||
if (events[i].isState()) {
|
||||
stateContext.setStateEvents([events[i]]);
|
||||
// it is possible that the act of setting the state event means we
|
||||
// can set more metadata (specifically sender/target props), so try
|
||||
// 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)
|
||||
// so we want to use the *updated* member (new avatar/name) instead.
|
||||
if (!events[i].sender || events[i].getType() === "m.room.member") {
|
||||
setEventMetadata(events[i], stateContext, toStartOfTimeline);
|
||||
}
|
||||
}
|
||||
if (events[i].getType() === "m.room.redaction") {
|
||||
// try to remove the element
|
||||
var removed = utils.removeElement(
|
||||
this.timeline, checkForRedaction(events[i])
|
||||
);
|
||||
if (!removed && toStartOfTimeline) {
|
||||
// redactions will trickle in BEFORE the event redacted so make
|
||||
// a note of the redacted event; we'll check it later.
|
||||
this._redactions.push(events[i].event.redacts);
|
||||
}
|
||||
// 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.
|
||||
}
|
||||
var spliceBeforeLocalEcho = !isLocalEcho && addLocalEchoToEnd;
|
||||
|
||||
if (!this._eventIdToTimeline[events[i].getId()]) {
|
||||
// TODO: pass through filter to see if this should be added to the timeline.
|
||||
if (toStartOfTimeline) {
|
||||
this.timeline.unshift(events[i]);
|
||||
}
|
||||
else {
|
||||
// everything gets added to the end by default. What we actually want to
|
||||
// do in this scenario is *NOT* add REAL events to the end if there are
|
||||
// existing local echo events at the end.
|
||||
if (addLocalEchoToEnd && !isLocalEcho) {
|
||||
var insertIndex = this.timeline.length;
|
||||
for (var j = this.timeline.length - 1; j >= 0; j--) {
|
||||
if (!this.timeline[j].status) { // real events don't have a status
|
||||
insertIndex = j + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.timeline.splice(insertIndex, 0, events[i]); // insert element
|
||||
}
|
||||
else {
|
||||
this.timeline.push(events[i]);
|
||||
}
|
||||
this._addEventToTimeline(events[i], this._liveTimeline, false,
|
||||
spliceBeforeLocalEcho);
|
||||
}
|
||||
|
||||
// synthesize and inject implicit read receipts
|
||||
@@ -317,13 +505,11 @@ Room.prototype.addEventsToTimeline = function(events, toStartOfTimeline) {
|
||||
|
||||
// This is really ugly because JS has no way to express an object literal
|
||||
// where the name of a key comes from an expression
|
||||
if (!toStartOfTimeline && events[i].sender) {
|
||||
if (events[i].sender) {
|
||||
this.addReceipt(new MatrixEvent(synthesizeReceipt(
|
||||
events[i].sender.userId, events[i], "m.read"
|
||||
)));
|
||||
}
|
||||
|
||||
this.emit("Room.timeline", events[i], this, Boolean(toStartOfTimeline), false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -358,17 +544,21 @@ Room.prototype.addEvents = function(events, duplicateStrategy) {
|
||||
if (duplicateStrategy) {
|
||||
// is there a duplicate?
|
||||
var shouldIgnore = false;
|
||||
for (var j = 0; j < this.timeline.length; j++) {
|
||||
if (this.timeline[j].getId() === events[i].getId()) {
|
||||
var timeline = this._eventIdToTimeline[events[i].getId()];
|
||||
if (timeline) {
|
||||
var tlEvents = timeline.getEvents();
|
||||
for (var j = 0; j < tlEvents.length; j++) {
|
||||
if (tlEvents[j].getId() === events[i].getId()) {
|
||||
if (duplicateStrategy === "replace") {
|
||||
// still need to set the right metadata on this event
|
||||
setEventMetadata(
|
||||
events[i],
|
||||
this.currentState,
|
||||
timeline.getState(false),
|
||||
false
|
||||
);
|
||||
if (!this.timeline[j].encryptedType) {
|
||||
this.timeline[j] = events[i];
|
||||
|
||||
if (!tlEvents[j].encryptedType) {
|
||||
tlEvents[j] = events[i];
|
||||
}
|
||||
// skip the insert so we don't add this event twice.
|
||||
// Don't break in case we replace multiple events.
|
||||
@@ -380,38 +570,48 @@ Room.prototype.addEvents = function(events, duplicateStrategy) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (shouldIgnore) {
|
||||
continue; // skip the insertion of this event.
|
||||
}
|
||||
}
|
||||
// TODO: We should have a filter to say "only add state event
|
||||
// types X Y Z to the timeline".
|
||||
this.addEventsToTimeline([events[i]]);
|
||||
this._addLiveEvents([events[i]]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes events from this room.
|
||||
* @param {String} event_ids A list of event_ids to remove.
|
||||
* @param {String[]} event_ids A list of event_ids to remove.
|
||||
*/
|
||||
Room.prototype.removeEvents = function(event_ids) {
|
||||
// avoids defining a function in the loop, which is a lint error
|
||||
function removeEventWithId(timeline, id) {
|
||||
// NB. we supply reverse to search from the end,
|
||||
// on the assumption that recents events are much
|
||||
// more likley to be removed than older ones.
|
||||
return utils.removeElement(timeline, function(e) {
|
||||
return e.getId() == id;
|
||||
}, true);
|
||||
for (var i = 0; i < event_ids.length; ++i) {
|
||||
this.removeEvent(event_ids[i]);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 none
|
||||
*/
|
||||
Room.prototype.removeEvent = function(eventId) {
|
||||
var timeline = this._eventIdToTimeline[eventId];
|
||||
if (!timeline) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var i = 0; i < event_ids.length; ++i) {
|
||||
var removed = removeEventWithId(this.timeline, event_ids[i]);
|
||||
var removed = timeline.removeEvent(eventId);
|
||||
if (removed) {
|
||||
this.emit("Room.timeline", removed, this, undefined, true);
|
||||
}
|
||||
delete this._eventIdToTimeline[eventId];
|
||||
var data = {
|
||||
timeline: timeline,
|
||||
};
|
||||
this.emit("Room.timeline", removed, this, undefined, true, data);
|
||||
}
|
||||
return removed;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -756,10 +956,19 @@ module.exports = Room;
|
||||
* @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){
|
||||
* if (toStartOfTimeline) {
|
||||
* var messageToAppend = room.timeline[room.timeline.length - 1];
|
||||
* matrixClient.on("Room.timeline", function(event, room, toStartOfTimeline, data){
|
||||
* if (!toStartOfTimeline && data.liveEvent) {
|
||||
* var messageToAppend = room.timeline.[room.timeline.length - 1];
|
||||
* }
|
||||
* });
|
||||
*/
|
||||
|
||||
29
lib/sync.js
29
lib/sync.js
@@ -76,10 +76,20 @@ function SyncApi(client, opts) {
|
||||
SyncApi.prototype.createRoom = function(roomId) {
|
||||
var client = this.client;
|
||||
var room = new Room(roomId, {
|
||||
pendingEventOrdering: this.opts.pendingEventOrdering
|
||||
pendingEventOrdering: this.opts.pendingEventOrdering,
|
||||
timelineSupport: client.timelineSupport,
|
||||
});
|
||||
reEmit(client, room, ["Room.name", "Room.timeline", "Room.receipt", "Room.tags"]);
|
||||
this._registerStateListeners(room);
|
||||
return room;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Room} room
|
||||
* @private
|
||||
*/
|
||||
SyncApi.prototype._registerStateListeners = function(room) {
|
||||
var client = this.client;
|
||||
// we need to also re-emit room state and room member events, so hook it up
|
||||
// to the client now. We need to add a listener for RoomState.members in
|
||||
// order to hook them correctly. (TODO: find a better way?)
|
||||
@@ -96,9 +106,20 @@ SyncApi.prototype.createRoom = function(roomId) {
|
||||
]
|
||||
);
|
||||
});
|
||||
return room;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Room} room
|
||||
* @private
|
||||
*/
|
||||
SyncApi.prototype._deregisterStateListeners = function(room) {
|
||||
// could do with a better way of achieving this.
|
||||
room.currentState.removeAllListeners("RoomState.events");
|
||||
room.currentState.removeAllListeners("RoomState.members");
|
||||
room.currentState.removeAllListeners("RoomState.newMember");
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sync rooms the user has left.
|
||||
* @return {Promise} Resolved when they've been added to the store.
|
||||
@@ -472,7 +493,9 @@ SyncApi.prototype._sync = function(syncOptions, attempt) {
|
||||
|
||||
if (joinObj.timeline.limited) {
|
||||
// nuke the timeline so we don't get holes
|
||||
room.timeline = [];
|
||||
self._deregisterStateListeners(room);
|
||||
room.resetLiveTimeline();
|
||||
self._registerStateListeners(room);
|
||||
}
|
||||
|
||||
// we want to set a new pagination token if this is the first time
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"gendoc": "jsdoc -r lib -P package.json -R README.md -d .jsdoc",
|
||||
"build": "jshint -c .jshint lib/ && browserify browser-index.js -o dist/browser-matrix-dev.js --ignore-missing",
|
||||
"watch": "watchify browser-index.js -o dist/browser-matrix-dev.js -v",
|
||||
"lint": "jshint -c .jshint lib spec && gjslint --unix_mode --disable 0131,0211,0200,0222 --max_line_length 90 -r spec/ -r lib/",
|
||||
"lint": "jshint -c .jshint lib spec && gjslint --unix_mode --disable 0131,0211,0200,0222,0212 --max_line_length 90 -r spec/ -r lib/",
|
||||
"release": "npm run build && mkdir dist/$npm_package_version && uglifyjs -c -m -o dist/$npm_package_version/browser-matrix-$npm_package_version.min.js dist/browser-matrix-dev.js && cp dist/browser-matrix-dev.js dist/$npm_package_version/browser-matrix-$npm_package_version.js"
|
||||
},
|
||||
"repository": {
|
||||
|
||||
553
spec/integ/matrix-client-event-timeline.spec.js
Normal file
553
spec/integ/matrix-client-event-timeline.spec.js
Normal file
@@ -0,0 +1,553 @@
|
||||
"use strict";
|
||||
var sdk = require("../..");
|
||||
var HttpBackend = require("../mock-request");
|
||||
var utils = require("../test-utils");
|
||||
|
||||
var baseUrl = "http://localhost.or.something";
|
||||
var userId = "@alice:localhost";
|
||||
var userName = "Alice";
|
||||
var accessToken = "aseukfgwef";
|
||||
var roomId = "!foo:bar";
|
||||
var otherUserId = "@bob:localhost";
|
||||
|
||||
var USER_MEMBERSHIP_EVENT = utils.mkMembership({
|
||||
room: roomId, mship: "join", user: userId, name: userName
|
||||
});
|
||||
|
||||
var ROOM_NAME_EVENT = utils.mkEvent({
|
||||
type: "m.room.name", room: roomId, user: otherUserId,
|
||||
content: {
|
||||
name: "Old room name"
|
||||
}
|
||||
});
|
||||
|
||||
var INITIAL_SYNC_DATA = {
|
||||
next_batch: "s_5_3",
|
||||
rooms: {
|
||||
join: {
|
||||
"!foo:bar": { // roomId
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: roomId, user: otherUserId, msg: "hello"
|
||||
})
|
||||
],
|
||||
prev_batch: "f_1_1"
|
||||
},
|
||||
state: {
|
||||
events: [
|
||||
ROOM_NAME_EVENT,
|
||||
utils.mkMembership({
|
||||
room: roomId, mship: "join",
|
||||
user: otherUserId, name: "Bob"
|
||||
}),
|
||||
USER_MEMBERSHIP_EVENT,
|
||||
utils.mkEvent({
|
||||
type: "m.room.create", room: roomId, user: userId,
|
||||
content: {
|
||||
creator: userId
|
||||
}
|
||||
})
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var EVENTS = [
|
||||
utils.mkMessage({
|
||||
room: roomId, user: userId, msg: "we",
|
||||
}),
|
||||
utils.mkMessage({
|
||||
room: roomId, user: userId, msg: "could",
|
||||
}),
|
||||
utils.mkMessage({
|
||||
room: roomId, user: userId, msg: "be",
|
||||
}),
|
||||
utils.mkMessage({
|
||||
room: roomId, user: userId, msg: "heroes",
|
||||
}),
|
||||
];
|
||||
|
||||
// start the client, and wait for it to initialise
|
||||
function startClient(httpBackend, client) {
|
||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
||||
httpBackend.when("GET", "/sync").respond(200, INITIAL_SYNC_DATA);
|
||||
|
||||
client.startClient();
|
||||
|
||||
var syncstate;
|
||||
client.on("sync", function(state) {
|
||||
syncstate = state;
|
||||
});
|
||||
|
||||
return httpBackend.flush().then(function() {
|
||||
expect(syncstate).toEqual("SYNCING");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
describe("getEventTimeline support", function() {
|
||||
var httpBackend;
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this);
|
||||
httpBackend = new HttpBackend();
|
||||
sdk.request(httpBackend.requestFn);
|
||||
});
|
||||
|
||||
it("timeline support must be enabled to work", function(done) {
|
||||
var client = sdk.createClient({
|
||||
baseUrl: baseUrl,
|
||||
userId: userId,
|
||||
accessToken: accessToken,
|
||||
});
|
||||
|
||||
startClient(httpBackend, client
|
||||
).then(function() {
|
||||
var room = client.getRoom(roomId);
|
||||
expect(function() { client.getEventTimeline(room, "event"); })
|
||||
.toThrow();
|
||||
}).catch(exceptFail).done(done);
|
||||
});
|
||||
|
||||
it("timeline support works when enabled", function(done) {
|
||||
var client = sdk.createClient({
|
||||
baseUrl: baseUrl,
|
||||
userId: userId,
|
||||
accessToken: accessToken,
|
||||
timelineSupport: true,
|
||||
});
|
||||
|
||||
startClient(httpBackend, client
|
||||
).then(function() {
|
||||
var room = client.getRoom(roomId);
|
||||
expect(function() { client.getEventTimeline(room, "event"); })
|
||||
.not.toThrow();
|
||||
}).catch(exceptFail).done(done);
|
||||
|
||||
httpBackend.flush().catch(exceptFail);
|
||||
});
|
||||
|
||||
|
||||
it("scrollback should be able to scroll back to before a gappy /sync",
|
||||
function(done) {
|
||||
// need a client with timelineSupport disabled to make this work
|
||||
var client = sdk.createClient({
|
||||
baseUrl: baseUrl,
|
||||
userId: userId,
|
||||
accessToken: accessToken,
|
||||
});
|
||||
var room;
|
||||
|
||||
startClient(httpBackend, client
|
||||
).then(function() {
|
||||
room = client.getRoom(roomId);
|
||||
|
||||
httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: "s_5_4",
|
||||
rooms: {
|
||||
join: {
|
||||
"!foo:bar": {
|
||||
timeline: {
|
||||
events: [
|
||||
EVENTS[0],
|
||||
],
|
||||
prev_batch: "f_1_1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: "s_5_5",
|
||||
rooms: {
|
||||
join: {
|
||||
"!foo:bar": {
|
||||
timeline: {
|
||||
events: [
|
||||
EVENTS[1],
|
||||
],
|
||||
limited: true,
|
||||
prev_batch: "f_1_2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
httpBackend.when("GET", "/messages").respond(200, {
|
||||
chunk: [EVENTS[0]],
|
||||
start: "pagin_start",
|
||||
end: "pagin_end",
|
||||
});
|
||||
|
||||
|
||||
return httpBackend.flush("/sync", 2);
|
||||
}).then(function() {
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
expect(room.timeline[0].event).toEqual(EVENTS[1]);
|
||||
|
||||
httpBackend.flush("/messages", 1);
|
||||
return client.scrollback(room);
|
||||
}).then(function() {
|
||||
expect(room.timeline.length).toEqual(2);
|
||||
expect(room.timeline[0].event).toEqual(EVENTS[0]);
|
||||
expect(room.timeline[1].event).toEqual(EVENTS[1]);
|
||||
expect(room.oldState.paginationToken).toEqual("pagin_end");
|
||||
}).catch(exceptFail).done(done);
|
||||
});
|
||||
});
|
||||
|
||||
describe("MatrixClient event timelines", function() {
|
||||
var client, httpBackend;
|
||||
|
||||
beforeEach(function(done) {
|
||||
utils.beforeEach(this);
|
||||
httpBackend = new HttpBackend();
|
||||
sdk.request(httpBackend.requestFn);
|
||||
|
||||
client = sdk.createClient({
|
||||
baseUrl: baseUrl,
|
||||
userId: userId,
|
||||
accessToken: accessToken,
|
||||
timelineSupport: true,
|
||||
});
|
||||
|
||||
startClient(httpBackend, client)
|
||||
.catch(exceptFail).done(done);
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
});
|
||||
|
||||
describe("getEventTimeline", function() {
|
||||
it("should create a new timeline for new events", function(done) {
|
||||
var room = client.getRoom(roomId);
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/context/event1%3Abar")
|
||||
.respond(200, function() {
|
||||
return {
|
||||
start: "start_token",
|
||||
events_before: [EVENTS[0]],
|
||||
event: EVENTS[1],
|
||||
events_after: [EVENTS[2]],
|
||||
state: [
|
||||
ROOM_NAME_EVENT,
|
||||
USER_MEMBERSHIP_EVENT,
|
||||
],
|
||||
end: "end_token",
|
||||
};
|
||||
});
|
||||
|
||||
client.getEventTimeline(room, "event1:bar").then(function(tl) {
|
||||
expect(tl.getEvents().length).toEqual(3);
|
||||
for (var i = 0; i < 3; i++) {
|
||||
expect(tl.getEvents()[i].event).toEqual(EVENTS[i]);
|
||||
expect(tl.getEvents()[i].sender.name).toEqual(userName);
|
||||
}
|
||||
expect(tl.getPaginationToken(true)).toEqual("start_token");
|
||||
expect(tl.getPaginationToken(false)).toEqual("end_token");
|
||||
}).catch(exceptFail).done(done);
|
||||
|
||||
httpBackend.flush().catch(exceptFail);
|
||||
});
|
||||
|
||||
it("should return existing timeline for known events", function(done) {
|
||||
var room = client.getRoom(roomId);
|
||||
httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: "s_5_4",
|
||||
rooms: {
|
||||
join: {
|
||||
"!foo:bar": {
|
||||
timeline: {
|
||||
events: [
|
||||
EVENTS[0],
|
||||
],
|
||||
prev_batch: "f_1_2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
httpBackend.flush("/sync").then(function() {
|
||||
return client.getEventTimeline(room, EVENTS[0].event_id);
|
||||
}).then(function(tl) {
|
||||
expect(tl.getEvents().length).toEqual(2);
|
||||
expect(tl.getEvents()[1].event).toEqual(EVENTS[0]);
|
||||
expect(tl.getEvents()[1].sender.name).toEqual(userName);
|
||||
expect(tl.getPaginationToken(true)).toEqual("f_1_1");
|
||||
// expect(tl.getPaginationToken(false)).toEqual("s_5_4");
|
||||
}).catch(exceptFail).done(done);
|
||||
|
||||
httpBackend.flush().catch(exceptFail);
|
||||
});
|
||||
|
||||
it("should update timelines where they overlap a previous /sync", function(done) {
|
||||
var room = client.getRoom(roomId);
|
||||
httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: "s_5_4",
|
||||
rooms: {
|
||||
join: {
|
||||
"!foo:bar": {
|
||||
timeline: {
|
||||
events: [
|
||||
EVENTS[3],
|
||||
],
|
||||
prev_batch: "f_1_2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
|
||||
encodeURIComponent(EVENTS[2].event_id))
|
||||
.respond(200, function() {
|
||||
return {
|
||||
start: "start_token",
|
||||
events_before: [EVENTS[1]],
|
||||
event: EVENTS[2],
|
||||
events_after: [EVENTS[3]],
|
||||
end: "end_token",
|
||||
state: [],
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
httpBackend.flush("/sync").then(function() {
|
||||
return client.getEventTimeline(room, EVENTS[2].event_id);
|
||||
}).then(function(tl) {
|
||||
expect(tl.getEvents().length).toEqual(4);
|
||||
expect(tl.getEvents()[0].event).toEqual(EVENTS[1]);
|
||||
expect(tl.getEvents()[1].event).toEqual(EVENTS[2]);
|
||||
expect(tl.getEvents()[3].event).toEqual(EVENTS[3]);
|
||||
expect(tl.getPaginationToken(true)).toEqual("start_token");
|
||||
// expect(tl.getPaginationToken(false)).toEqual("s_5_4");
|
||||
}).catch(exceptFail).done(done);
|
||||
|
||||
httpBackend.flush().catch(exceptFail);
|
||||
});
|
||||
|
||||
it("should join timelines where they overlap a previous /context",
|
||||
function(done) {
|
||||
var room = client.getRoom(roomId);
|
||||
|
||||
// we fetch event 0, then 2, then 3, and finally 1. 1 is returned
|
||||
// with context which joins them all up.
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
|
||||
encodeURIComponent(EVENTS[0].event_id))
|
||||
.respond(200, function() {
|
||||
return {
|
||||
start: "start_token0",
|
||||
events_before: [],
|
||||
event: EVENTS[0],
|
||||
events_after: [],
|
||||
end: "end_token0",
|
||||
state: [],
|
||||
};
|
||||
});
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
|
||||
encodeURIComponent(EVENTS[2].event_id))
|
||||
.respond(200, function() {
|
||||
return {
|
||||
start: "start_token2",
|
||||
events_before: [],
|
||||
event: EVENTS[2],
|
||||
events_after: [],
|
||||
end: "end_token2",
|
||||
state: [],
|
||||
};
|
||||
});
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
|
||||
encodeURIComponent(EVENTS[3].event_id))
|
||||
.respond(200, function() {
|
||||
return {
|
||||
start: "start_token3",
|
||||
events_before: [],
|
||||
event: EVENTS[3],
|
||||
events_after: [],
|
||||
end: "end_token3",
|
||||
state: [],
|
||||
};
|
||||
});
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
|
||||
encodeURIComponent(EVENTS[1].event_id))
|
||||
.respond(200, function() {
|
||||
return {
|
||||
start: "start_token4",
|
||||
events_before: [EVENTS[0]],
|
||||
event: EVENTS[1],
|
||||
events_after: [EVENTS[2], EVENTS[3]],
|
||||
end: "end_token4",
|
||||
state: [],
|
||||
};
|
||||
});
|
||||
|
||||
var tl0, tl2, tl3;
|
||||
client.getEventTimeline(room, EVENTS[0].event_id
|
||||
).then(function(tl) {
|
||||
expect(tl.getEvents().length).toEqual(1);
|
||||
tl0 = tl;
|
||||
return client.getEventTimeline(room, EVENTS[2].event_id);
|
||||
}).then(function(tl) {
|
||||
expect(tl.getEvents().length).toEqual(1);
|
||||
tl2 = tl;
|
||||
return client.getEventTimeline(room, EVENTS[3].event_id);
|
||||
}).then(function(tl) {
|
||||
expect(tl.getEvents().length).toEqual(1);
|
||||
tl3 = tl;
|
||||
return client.getEventTimeline(room, EVENTS[1].event_id);
|
||||
}).then(function(tl) {
|
||||
// we expect it to get merged in with event 2
|
||||
expect(tl.getEvents().length).toEqual(2);
|
||||
expect(tl.getEvents()[0].event).toEqual(EVENTS[1]);
|
||||
expect(tl.getEvents()[1].event).toEqual(EVENTS[2]);
|
||||
expect(tl.getNeighbouringTimeline(true)).toBe(tl0);
|
||||
expect(tl.getNeighbouringTimeline(false)).toBe(tl3);
|
||||
expect(tl0.getPaginationToken(true)).toEqual("start_token0");
|
||||
expect(tl0.getPaginationToken(false)).toBe(null);
|
||||
expect(tl3.getPaginationToken(true)).toBe(null);
|
||||
expect(tl3.getPaginationToken(false)).toEqual("end_token3");
|
||||
}).catch(exceptFail).done(done);
|
||||
|
||||
httpBackend.flush().catch(exceptFail);
|
||||
});
|
||||
|
||||
it("should fail gracefully if there is no event field", function(done) {
|
||||
var room = client.getRoom(roomId);
|
||||
// we fetch event 0, then 2, then 3, and finally 1. 1 is returned
|
||||
// with context which joins them all up.
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/context/event1")
|
||||
.respond(200, function() {
|
||||
return {
|
||||
start: "start_token",
|
||||
events_before: [],
|
||||
events_after: [],
|
||||
end: "end_token",
|
||||
state: [],
|
||||
};
|
||||
});
|
||||
|
||||
client.getEventTimeline(room, "event1"
|
||||
).then(function(tl) {
|
||||
// could do with a fail()
|
||||
expect(true).toBeFalsy();
|
||||
}).catch(function(e) {
|
||||
expect(String(e)).toMatch(/'event'/);
|
||||
}).catch(exceptFail).done(done);
|
||||
|
||||
httpBackend.flush().catch(exceptFail);
|
||||
});
|
||||
});
|
||||
|
||||
describe("paginateEventTimeline", function() {
|
||||
it("should allow you to paginate backwards", function(done) {
|
||||
var room = client.getRoom(roomId);
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
|
||||
encodeURIComponent(EVENTS[0].event_id))
|
||||
.respond(200, function() {
|
||||
return {
|
||||
start: "start_token0",
|
||||
events_before: [],
|
||||
event: EVENTS[0],
|
||||
events_after: [],
|
||||
end: "end_token0",
|
||||
state: [],
|
||||
};
|
||||
});
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/messages")
|
||||
.check(function(req) {
|
||||
var params = req.queryParams;
|
||||
expect(params.dir).toEqual("b");
|
||||
expect(params.from).toEqual("start_token0");
|
||||
expect(params.limit).toEqual(30);
|
||||
}).respond(200, function() {
|
||||
return {
|
||||
chunk: [EVENTS[1], EVENTS[2]],
|
||||
end: "start_token1",
|
||||
};
|
||||
});
|
||||
|
||||
var tl;
|
||||
client.getEventTimeline(room, EVENTS[0].event_id
|
||||
).then(function(tl0) {
|
||||
tl = tl0;
|
||||
return client.paginateEventTimeline(tl, {backwards: true});
|
||||
}).then(function(success) {
|
||||
expect(success).toBeTruthy();
|
||||
expect(tl.getEvents().length).toEqual(3);
|
||||
expect(tl.getEvents()[0].event).toEqual(EVENTS[2]);
|
||||
expect(tl.getEvents()[1].event).toEqual(EVENTS[1]);
|
||||
expect(tl.getEvents()[2].event).toEqual(EVENTS[0]);
|
||||
expect(tl.getPaginationToken(true)).toEqual("start_token1");
|
||||
expect(tl.getPaginationToken(false)).toEqual("end_token0");
|
||||
}).catch(exceptFail).done(done);
|
||||
|
||||
httpBackend.flush().catch(exceptFail);
|
||||
});
|
||||
|
||||
|
||||
it("should allow you to paginate forwards", function(done) {
|
||||
var room = client.getRoom(roomId);
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
|
||||
encodeURIComponent(EVENTS[0].event_id))
|
||||
.respond(200, function() {
|
||||
return {
|
||||
start: "start_token0",
|
||||
events_before: [],
|
||||
event: EVENTS[0],
|
||||
events_after: [],
|
||||
end: "end_token0",
|
||||
state: [],
|
||||
};
|
||||
});
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/messages")
|
||||
.check(function(req) {
|
||||
var params = req.queryParams;
|
||||
expect(params.dir).toEqual("f");
|
||||
expect(params.from).toEqual("end_token0");
|
||||
expect(params.limit).toEqual(20);
|
||||
}).respond(200, function() {
|
||||
return {
|
||||
chunk: [EVENTS[1], EVENTS[2]],
|
||||
end: "end_token1",
|
||||
};
|
||||
});
|
||||
|
||||
var tl;
|
||||
client.getEventTimeline(room, EVENTS[0].event_id
|
||||
).then(function(tl0) {
|
||||
tl = tl0;
|
||||
return client.paginateEventTimeline(tl, {backwards: false, limit: 20});
|
||||
}).then(function(success) {
|
||||
expect(success).toBeTruthy();
|
||||
expect(tl.getEvents().length).toEqual(3);
|
||||
expect(tl.getEvents()[0].event).toEqual(EVENTS[0]);
|
||||
expect(tl.getEvents()[1].event).toEqual(EVENTS[1]);
|
||||
expect(tl.getEvents()[2].event).toEqual(EVENTS[2]);
|
||||
expect(tl.getPaginationToken(true)).toEqual("start_token0");
|
||||
expect(tl.getPaginationToken(false)).toEqual("end_token1");
|
||||
}).catch(exceptFail).done(done);
|
||||
|
||||
httpBackend.flush().catch(exceptFail);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
// make the test fail, with the given exception
|
||||
function exceptFail(error) {
|
||||
expect(error.stack).toBe(null);
|
||||
}
|
||||
@@ -107,7 +107,9 @@ describe("MatrixClient room timelines", function() {
|
||||
client = sdk.createClient({
|
||||
baseUrl: baseUrl,
|
||||
userId: userId,
|
||||
accessToken: accessToken
|
||||
accessToken: accessToken,
|
||||
// these tests should work with or without timelineSupport
|
||||
timelineSupport: true,
|
||||
});
|
||||
setNextSyncData();
|
||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
|
||||
331
spec/unit/event-timeline.spec.js
Normal file
331
spec/unit/event-timeline.spec.js
Normal file
@@ -0,0 +1,331 @@
|
||||
"use strict";
|
||||
var sdk = require("../..");
|
||||
var EventTimeline = sdk.EventTimeline;
|
||||
var utils = require("../test-utils");
|
||||
|
||||
describe("EventTimeline", function() {
|
||||
var roomId = "!foo:bar";
|
||||
var userA = "@alice:bar";
|
||||
var userB = "@bertha:bar";
|
||||
var timeline;
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this);
|
||||
timeline = new EventTimeline(roomId);
|
||||
|
||||
// mock RoomStates
|
||||
timeline._startState = utils.mock(sdk.RoomState, "startState");
|
||||
timeline._endState = utils.mock(sdk.RoomState, "endState");
|
||||
});
|
||||
|
||||
describe("construction", function() {
|
||||
it("getRoomId should get room id", function() {
|
||||
var v = timeline.getRoomId();
|
||||
expect(v).toEqual(roomId);
|
||||
});
|
||||
});
|
||||
|
||||
describe("initialiseState", function() {
|
||||
it("should copy state events to start and end state", function() {
|
||||
var events = [
|
||||
utils.mkMembership({
|
||||
room: roomId, mship: "invite", user: userB, skey: userA,
|
||||
event: true,
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: "m.room.name", room: roomId, user: userB,
|
||||
event: true,
|
||||
content: { name: "New room" },
|
||||
})
|
||||
];
|
||||
timeline.initialiseState(events);
|
||||
expect(timeline._startState.setStateEvents).toHaveBeenCalledWith(
|
||||
events
|
||||
);
|
||||
expect(timeline._endState.setStateEvents).toHaveBeenCalledWith(
|
||||
events
|
||||
);
|
||||
});
|
||||
|
||||
it("should raise an exception if called after events are added", function() {
|
||||
var event =
|
||||
utils.mkMessage({
|
||||
room: roomId, user: userA, msg: "Adam stole the plushies",
|
||||
event: true,
|
||||
});
|
||||
|
||||
var state =
|
||||
utils.mkMembership({
|
||||
room: roomId, mship: "invite", user: userB, skey: userA,
|
||||
event: true,
|
||||
});
|
||||
|
||||
expect(function() { timeline.initialiseState(state); }).not.toThrow();
|
||||
timeline.addEvent(event, false);
|
||||
expect(function() { timeline.initialiseState(state); }).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("paginationTokens", function() {
|
||||
it("pagination tokens should start undefined", function() {
|
||||
expect(timeline.getPaginationToken(true)).toBe(undefined);
|
||||
expect(timeline.getPaginationToken(false)).toBe(undefined);
|
||||
});
|
||||
|
||||
it("setPaginationToken should set token", function() {
|
||||
timeline.setPaginationToken("back", true);
|
||||
timeline.setPaginationToken("fwd", false);
|
||||
expect(timeline.getPaginationToken(true)).toEqual("back");
|
||||
expect(timeline.getPaginationToken(false)).toEqual("fwd");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("neighbouringTimelines", function() {
|
||||
it("neighbouring timelines should start null", function() {
|
||||
expect(timeline.getNeighbouringTimeline(true)).toBe(null);
|
||||
expect(timeline.getNeighbouringTimeline(false)).toBe(null);
|
||||
});
|
||||
|
||||
it("setNeighbouringTimeline should set neighbour", function() {
|
||||
var prev = {a: "a"};
|
||||
var next = {b: "b"};
|
||||
timeline.setNeighbouringTimeline(prev, true);
|
||||
timeline.setNeighbouringTimeline(next, false);
|
||||
expect(timeline.getNeighbouringTimeline(true)).toBe(prev);
|
||||
expect(timeline.getNeighbouringTimeline(false)).toBe(next);
|
||||
});
|
||||
|
||||
it("setNeighbouringTimeline should throw if called twice", function() {
|
||||
var prev = {a: "a"};
|
||||
var next = {b: "b"};
|
||||
expect(function() {timeline.setNeighbouringTimeline(prev, true);}).
|
||||
not.toThrow();
|
||||
expect(timeline.getNeighbouringTimeline(true)).toBe(prev);
|
||||
expect(function() {timeline.setNeighbouringTimeline(prev, true);}).
|
||||
toThrow();
|
||||
|
||||
expect(function() {timeline.setNeighbouringTimeline(next, false);}).
|
||||
not.toThrow();
|
||||
expect(timeline.getNeighbouringTimeline(false)).toBe(next);
|
||||
expect(function() {timeline.setNeighbouringTimeline(next, false);}).
|
||||
toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("addEvent", function() {
|
||||
var events = [
|
||||
utils.mkMessage({
|
||||
room: roomId, user: userA, msg: "hungry hungry hungry",
|
||||
event: true,
|
||||
}),
|
||||
utils.mkMessage({
|
||||
room: roomId, user: userB, msg: "nom nom nom",
|
||||
event: true,
|
||||
}),
|
||||
];
|
||||
|
||||
it("should be able to add events to the end", function() {
|
||||
timeline.addEvent(events[0], false);
|
||||
expect(timeline.getBaseIndex()).toEqual(0);
|
||||
timeline.addEvent(events[1], false);
|
||||
expect(timeline.getBaseIndex()).toEqual(0);
|
||||
expect(timeline.getEvents().length).toEqual(2);
|
||||
expect(timeline.getEvents()[0]).toEqual(events[0]);
|
||||
expect(timeline.getEvents()[1]).toEqual(events[1]);
|
||||
});
|
||||
|
||||
it("should be able to add events to the start", function() {
|
||||
timeline.addEvent(events[0], true);
|
||||
expect(timeline.getBaseIndex()).toEqual(0);
|
||||
timeline.addEvent(events[1], true);
|
||||
expect(timeline.getBaseIndex()).toEqual(1);
|
||||
expect(timeline.getEvents().length).toEqual(2);
|
||||
expect(timeline.getEvents()[0]).toEqual(events[1]);
|
||||
expect(timeline.getEvents()[1]).toEqual(events[0]);
|
||||
});
|
||||
|
||||
it("should set event.sender for new and old events", function() {
|
||||
var sentinel = {
|
||||
userId: userA,
|
||||
membership: "join",
|
||||
name: "Alice"
|
||||
};
|
||||
var oldSentinel = {
|
||||
userId: userA,
|
||||
membership: "join",
|
||||
name: "Old Alice"
|
||||
};
|
||||
timeline.getState(false).getSentinelMember.andCallFake(function(uid) {
|
||||
if (uid === userA) {
|
||||
return sentinel;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
timeline.getState(true).getSentinelMember.andCallFake(function(uid) {
|
||||
if (uid === userA) {
|
||||
return oldSentinel;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
var newEv = utils.mkEvent({
|
||||
type: "m.room.name", room: roomId, user: userA, event: true,
|
||||
content: { name: "New Room Name" }
|
||||
});
|
||||
var oldEv = utils.mkEvent({
|
||||
type: "m.room.name", room: roomId, user: userA, event: true,
|
||||
content: { name: "Old Room Name" }
|
||||
});
|
||||
|
||||
timeline.addEvent(newEv, false);
|
||||
expect(newEv.sender).toEqual(sentinel);
|
||||
timeline.addEvent(oldEv, true);
|
||||
expect(oldEv.sender).toEqual(oldSentinel);
|
||||
});
|
||||
|
||||
it("should set event.target for new and old m.room.member events",
|
||||
function() {
|
||||
var sentinel = {
|
||||
userId: userA,
|
||||
membership: "join",
|
||||
name: "Alice"
|
||||
};
|
||||
var oldSentinel = {
|
||||
userId: userA,
|
||||
membership: "join",
|
||||
name: "Old Alice"
|
||||
};
|
||||
timeline.getState(false).getSentinelMember.andCallFake(function(uid) {
|
||||
if (uid === userA) {
|
||||
return sentinel;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
timeline.getState(true).getSentinelMember.andCallFake(function(uid) {
|
||||
if (uid === userA) {
|
||||
return oldSentinel;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
var newEv = utils.mkMembership({
|
||||
room: roomId, mship: "invite", user: userB, skey: userA, event: true
|
||||
});
|
||||
var oldEv = utils.mkMembership({
|
||||
room: roomId, mship: "ban", user: userB, skey: userA, event: true
|
||||
});
|
||||
timeline.addEvent(newEv, false);
|
||||
expect(newEv.target).toEqual(sentinel);
|
||||
timeline.addEvent(oldEv, true);
|
||||
expect(oldEv.target).toEqual(oldSentinel);
|
||||
});
|
||||
|
||||
it("should call setStateEvents on the right RoomState with the right " +
|
||||
"forwardLooking value for new events", function() {
|
||||
var events = [
|
||||
utils.mkMembership({
|
||||
room: roomId, mship: "invite", user: userB, skey: userA, event: true
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: "m.room.name", room: roomId, user: userB, event: true,
|
||||
content: {
|
||||
name: "New room"
|
||||
}
|
||||
})
|
||||
];
|
||||
|
||||
timeline.addEvent(events[0], false);
|
||||
timeline.addEvent(events[1], false);
|
||||
|
||||
expect(timeline.getState(false).setStateEvents).
|
||||
toHaveBeenCalledWith([events[0]]);
|
||||
expect(timeline.getState(false).setStateEvents).
|
||||
toHaveBeenCalledWith([events[1]]);
|
||||
|
||||
expect(events[0].forwardLooking).toBe(true);
|
||||
expect(events[1].forwardLooking).toBe(true);
|
||||
|
||||
expect(timeline.getState(true).setStateEvents).
|
||||
not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
it("should call setStateEvents on the right RoomState with the right " +
|
||||
"forwardLooking value for old events", function() {
|
||||
var events = [
|
||||
utils.mkMembership({
|
||||
room: roomId, mship: "invite", user: userB, skey: userA, event: true
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: "m.room.name", room: roomId, user: userB, event: true,
|
||||
content: {
|
||||
name: "New room"
|
||||
}
|
||||
})
|
||||
];
|
||||
|
||||
timeline.addEvent(events[0], true);
|
||||
timeline.addEvent(events[1], true);
|
||||
|
||||
expect(timeline.getState(true).setStateEvents).
|
||||
toHaveBeenCalledWith([events[0]]);
|
||||
expect(timeline.getState(true).setStateEvents).
|
||||
toHaveBeenCalledWith([events[1]]);
|
||||
|
||||
expect(events[0].forwardLooking).toBe(false);
|
||||
expect(events[1].forwardLooking).toBe(false);
|
||||
|
||||
expect(timeline.getState(false).setStateEvents).
|
||||
not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("removeEvent", function() {
|
||||
var events = [
|
||||
utils.mkMessage({
|
||||
room: roomId, user: userA, msg: "hungry hungry hungry",
|
||||
event: true,
|
||||
}),
|
||||
utils.mkMessage({
|
||||
room: roomId, user: userB, msg: "nom nom nom",
|
||||
event: true,
|
||||
}),
|
||||
utils.mkMessage({
|
||||
room: roomId, user: userB, msg: "piiie",
|
||||
event: true,
|
||||
}),
|
||||
];
|
||||
|
||||
it("should remove events", function() {
|
||||
timeline.addEvent(events[0], false);
|
||||
timeline.addEvent(events[1], false);
|
||||
expect(timeline.getEvents().length).toEqual(2);
|
||||
|
||||
var ev = timeline.removeEvent(events[0].getId());
|
||||
expect(ev).toBe(events[0]);
|
||||
expect(timeline.getEvents().length).toEqual(1);
|
||||
|
||||
ev = timeline.removeEvent(events[1].getId());
|
||||
expect(ev).toBe(events[1]);
|
||||
expect(timeline.getEvents().length).toEqual(0);
|
||||
});
|
||||
|
||||
it("should update baseIndex", function() {
|
||||
timeline.addEvent(events[0], false);
|
||||
timeline.addEvent(events[1], true);
|
||||
timeline.addEvent(events[2], false);
|
||||
expect(timeline.getEvents().length).toEqual(3);
|
||||
expect(timeline.getBaseIndex()).toEqual(1);
|
||||
|
||||
timeline.removeEvent(events[2].getId());
|
||||
expect(timeline.getEvents().length).toEqual(2);
|
||||
expect(timeline.getBaseIndex()).toEqual(1);
|
||||
|
||||
timeline.removeEvent(events[1].getId());
|
||||
expect(timeline.getEvents().length).toEqual(1);
|
||||
expect(timeline.getBaseIndex()).toEqual(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,8 +18,10 @@ describe("Room", function() {
|
||||
utils.beforeEach(this);
|
||||
room = new Room(roomId);
|
||||
// mock RoomStates
|
||||
room.oldState = utils.mock(sdk.RoomState, "oldState");
|
||||
room.currentState = utils.mock(sdk.RoomState, "currentState");
|
||||
room.oldState = room.getLiveTimeline()._startState =
|
||||
utils.mock(sdk.RoomState, "oldState");
|
||||
room.currentState = room.getLiveTimeline()._endState =
|
||||
utils.mock(sdk.RoomState, "currentState");
|
||||
});
|
||||
|
||||
describe("getAvatarUrl", function() {
|
||||
@@ -259,10 +261,7 @@ describe("Room", function() {
|
||||
});
|
||||
|
||||
it("should call setStateEvents on the right RoomState with the right " +
|
||||
"forwardLooking value", function() {
|
||||
room.oldState = utils.mock(RoomState);
|
||||
room.currentState = utils.mock(RoomState);
|
||||
|
||||
"forwardLooking value for new events", function() {
|
||||
var events = [
|
||||
utils.mkMembership({
|
||||
room: roomId, mship: "invite", user: userB, skey: userA, event: true
|
||||
@@ -284,8 +283,23 @@ describe("Room", function() {
|
||||
expect(events[0].forwardLooking).toBe(true);
|
||||
expect(events[1].forwardLooking).toBe(true);
|
||||
expect(room.oldState.setStateEvents).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
it("should call setStateEvents on the right RoomState with the right " +
|
||||
"forwardLooking value for old events", function() {
|
||||
var events = [
|
||||
utils.mkMembership({
|
||||
room: roomId, mship: "invite", user: userB, skey: userA, event: true
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: "m.room.name", room: roomId, user: userB, event: true,
|
||||
content: {
|
||||
name: "New room"
|
||||
}
|
||||
})
|
||||
];
|
||||
|
||||
// test old
|
||||
room.addEventsToTimeline(events, true);
|
||||
expect(room.oldState.setStateEvents).toHaveBeenCalledWith(
|
||||
[events[0]]
|
||||
@@ -295,6 +309,7 @@ describe("Room", function() {
|
||||
);
|
||||
expect(events[0].forwardLooking).toBe(false);
|
||||
expect(events[1].forwardLooking).toBe(false);
|
||||
expect(room.currentState.setStateEvents).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user