diff --git a/lib/models/room.js b/lib/models/room.js index a1cf29629..f448cceb1 100644 --- a/lib/models/room.js +++ b/lib/models/room.js @@ -10,6 +10,23 @@ var MatrixEvent = require("./event").MatrixEvent; var utils = require("../utils"); var ContentRepo = require("../content-repo"); +function synthesizeReceipt(userId, event, receiptType) { + // This is really ugly because JS has no way to express an object literal + // where the name of a key comes from an expression + var fakeReceipt = { + content: {}, + type: "m.receipt", + room_id: event.getRoomId() + }; + fakeReceipt.content[event.getId()] = {}; + fakeReceipt.content[event.getId()][receiptType] = {}; + fakeReceipt.content[event.getId()][receiptType][userId] = { + ts: event + }; + return new MatrixEvent(fakeReceipt); +} + + /** * Construct a new Room. * @constructor @@ -207,16 +224,10 @@ 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 (events[i].sender) { - var fakeReceipt = {content: {}}; - fakeReceipt.content[events[i].getId()] = { - 'm.read': { - } - }; - fakeReceipt.content[events[i].getId()]['m.read'][events[i].sender.userId] = { - ts: events[i].getTs() - }; - this.addReceipt(new MatrixEvent(fakeReceipt)); + if (!toStartOfTimeline && 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); @@ -356,6 +367,45 @@ Room.prototype.recalculate = function(userId) { if (oldName !== this.name) { this.emit("Room.name", this); } + + + + // recalculate read receipts, adding implicit ones where necessary + // NB. This is a duplication of logic for injecting implicit receipts, + // it would be technically possible to only ever generate these + // receipts in addEventsToTimeline but doing so means correctly + // choosing whether to keep or replace the existing receipt which + // is complex and slow. This is faster and more understandable. + + var usersFound = {}; + for (var i = this.timeline.length - 1; i >= 0; --i) { + // loop through the timeline backwards looking for either an + // event sent by each user or a real receipt from them. + // Replace the read receipt for that user with whichever + // occurs later in the timeline (ie. first because we're going + // backwards). + var e = this.timeline[i]; + + var readReceiptsForEvent = this.getReceiptsForEvent(e); + + for (var receiptIt = 0; receiptIt < readReceiptsForEvent.length; ++receiptIt) { + var receipt = readReceiptsForEvent[receiptIt]; + if (receipt.type !== "m.read") { continue; } + + if (usersFound[receipt.userId]) { continue; } + + // Then this is the receipt we keep for this user + usersFound[receipt.userId] = 1; + } + + if (e.sender && usersFound[e.sender.userId] === undefined) { + // no receipt yet for this sender, so we synthesize one. + + this.addReceipt(synthesizeReceipt(e.sender.userId, e, "m.read")); + + usersFound[e.sender.userId] = 1; + } + } };