diff --git a/lib/models/room.js b/lib/models/room.js index 402c34dab..17e81895e 100644 --- a/lib/models/room.js +++ b/lib/models/room.js @@ -143,12 +143,14 @@ function Room(roomId, opts) { this._notificationCounts = {}; + this._liveTimeline = new EventTimeline(this.roomId); + this._fixUpLegacyTimelineFields(); + // just a list - *not* ordered. - this._timelines = []; + this._timelines = [this._liveTimeline]; this._eventIdToTimeline = {}; this._timelineSupport = Boolean(opts.timelineSupport); - this.resetLiveTimeline(); } utils.inherits(Room, EventEmitter); @@ -165,6 +167,8 @@ Room.prototype.getLiveTimeline = function() { * Reset the live timeline, and start a new one. * *
This is used when /sync returns a 'limited' timeline. + * + * @fires module:client~MatrixClient#event:"Room.timelineReset" */ Room.prototype.resetLiveTimeline = function() { var newTimeline; @@ -178,24 +182,29 @@ Room.prototype.resetLiveTimeline = function() { newTimeline = this.addTimeline(); } - if (this._liveTimeline) { - // we have an existing timeline. This will always be true, except when - // this method is called by our own constructor. - - // 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]); - } + // 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); } - this._liveTimeline = newTimeline; + newTimeline.initialiseState(events); + this._liveTimeline = newTimeline; + this._fixUpLegacyTimelineFields(); + this.emit("Room.timelineReset", this); +}; + +/** + * Fix up this.timeline, this.oldState and this.currentState + * + * @private + */ +Room.prototype._fixUpLegacyTimelineFields = function() { // 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 @@ -1218,6 +1227,18 @@ module.exports = Room; * }); */ +/** + * Fires wheneer 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:clinet~MatrixClient#"Room.timelineReset" + * @param {Room} room The room whose live timeline was reset. + */ + /** * Fires when an event we had previously received is redacted. * diff --git a/lib/sync.js b/lib/sync.js index 2b4e590c4..de1652f04 100644 --- a/lib/sync.js +++ b/lib/sync.js @@ -84,7 +84,8 @@ SyncApi.prototype.createRoom = function(roomId) { timelineSupport: client.timelineSupport, }); reEmit(client, room, ["Room.name", "Room.timeline", "Room.redaction", - "Room.receipt", "Room.tags"]); + "Room.receipt", "Room.tags", + "Room.timelineReset"]); this._registerStateListeners(room); return room; }; diff --git a/spec/integ/matrix-client-room-timeline.spec.js b/spec/integ/matrix-client-room-timeline.spec.js index 9548fda2d..b479d80ff 100644 --- a/spec/integ/matrix-client-room-timeline.spec.js +++ b/spec/integ/matrix-client-room-timeline.spec.js @@ -512,5 +512,31 @@ describe("MatrixClient room timelines", function() { }); httpBackend.flush("/sync", 1); }); + + it("should emit a 'Room.timelineReset' event", function(done) { + var eventData = [ + utils.mkMessage({user: userId, room: roomId}), + ]; + setNextSyncData(eventData); + NEXT_SYNC_DATA.rooms.join[roomId].timeline.limited = true; + + client.on("sync", function(state) { + if (state !== "PREPARED") { return; } + var room = client.getRoom(roomId); + + var emitCount = 0; + client.on("Room.timelineReset", function(emitRoom) { + expect(emitRoom).toEqual(room); + emitCount++; + }); + + httpBackend.flush("/messages", 1); + httpBackend.flush("/sync", 1).done(function() { + expect(emitCount).toEqual(1); + done(); + }); + }); + httpBackend.flush("/sync", 1); + }); }); }); diff --git a/spec/unit/room.spec.js b/spec/unit/room.spec.js index f82c0c564..79cff7ff6 100644 --- a/spec/unit/room.spec.js +++ b/spec/unit/room.spec.js @@ -4,6 +4,7 @@ var Room = sdk.Room; var RoomState = sdk.RoomState; var MatrixEvent = sdk.MatrixEvent; var EventStatus = sdk.EventStatus; +var EventTimeline = sdk.EventTimeline; var utils = require("../test-utils"); describe("Room", function() { @@ -329,6 +330,80 @@ describe("Room", function() { }); }); + var resetTimelineTests = function(timelineSupport) { + var events = [ + utils.mkMessage({ + room: roomId, user: userA, msg: "A message", event: true + }), + utils.mkEvent({ + type: "m.room.name", room: roomId, user: userA, event: true, + content: { name: "New Room Name" } + }), + utils.mkEvent({ + type: "m.room.name", room: roomId, user: userA, event: true, + content: { name: "Another New Name" } + }), + ]; + + beforeEach(function() { + room = new Room(roomId, {timelineSupport: timelineSupport}); + }); + + it("should copy state from previous timeline", function() { + room.addEventsToTimeline([events[0], events[1]]); + expect(room.getLiveTimeline().getEvents().length).toEqual(2); + room.resetLiveTimeline(); + + room.addEventsToTimeline([events[2]]); + var oldState = room.getLiveTimeline().getState(EventTimeline.BACKWARDS); + var newState = room.getLiveTimeline().getState(EventTimeline.FORWARDS); + expect(room.getLiveTimeline().getEvents().length).toEqual(1); + expect(oldState.getStateEvents("m.room.name", "")).toEqual(events[1]); + expect(newState.getStateEvents("m.room.name", "")).toEqual(events[2]); + }); + + it("should reset the legacy timeline fields", function() { + room.addEventsToTimeline([events[0], events[1]]); + expect(room.timeline.length).toEqual(2); + room.resetLiveTimeline(); + + room.addEventsToTimeline([events[2]]); + var newLiveTimeline = room.getLiveTimeline(); + expect(room.timeline).toEqual(newLiveTimeline.getEvents()); + expect(room.oldState).toEqual( + newLiveTimeline.getState(EventTimeline.BACKWARDS)); + expect(room.currentState).toEqual( + newLiveTimeline.getState(EventTimeline.FORWARDS)); + }); + + it("should emit Room.timelineReset event", function() { + var callCount = 0; + room.on("Room.timelineReset", function(emitRoom) { + callCount += 1; + expect(emitRoom).toEqual(room); + }); + room.resetLiveTimeline(); + expect(callCount).toEqual(1); + }); + + it("should " + (timelineSupport ? "remember" : "forget") + + " old timelines", function() { + room.addEventsToTimeline([events[0]]); + expect(room.timeline.length).toEqual(1); + var firstLiveTimeline = room.getLiveTimeline(); + room.resetLiveTimeline(); + + var tl = room.getTimelineForEvent(events[0].getId()); + expect(tl).toBe(timelineSupport ? firstLiveTimeline : null); + }); + + }; + + describe("resetLiveTimeline with timelinesupport enabled", + resetTimelineTests.bind(null, true)); + describe("resetLiveTimeline with timelinesupport disabled", + resetTimelineTests.bind(null, false)); + describe("compareEventOrdering", function() { beforeEach(function() { room = new Room(roomId, {timelineSupport: true});