From 6679e93afc77b740c938e5489dabcef642f5be7f Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 16 Oct 2015 09:12:50 +0100 Subject: [PATCH 1/8] Add untested read receipt sending method --- lib/client.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/lib/client.js b/lib/client.js index fa3e67846..58f7687e4 100644 --- a/lib/client.js +++ b/lib/client.js @@ -1124,6 +1124,25 @@ MatrixClient.prototype.sendHtmlNotice = function(roomId, body, htmlBody, callbac return this.sendMessage(roomId, content, callback); }; +/** + * @param {Event} event + * @param {string} receiptType + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixClient.prototype.sendReceipt = function(event, receiptType, callback) { + var path = utils.encodeUri("/rooms/$roomId/receipt/$receiptType/$eventId", { + $roomId: event.getRoomId(), + $receiptType: receiptType, + $eventId: event.getId() + }); + return this._http.authedRequest( + callback, "POST", path, undefined, {} + ); +}; + + /** * Upload a file to the media repository on the home server. * @param {File} file object From 43fc200daeac4524163ede4c4a0f7f6525c42176 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 16 Oct 2015 09:36:13 +0100 Subject: [PATCH 2/8] Read receipt HTTP API tweaks --- lib/client.js | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/lib/client.js b/lib/client.js index 58f7687e4..ea7aee9cb 100644 --- a/lib/client.js +++ b/lib/client.js @@ -1125,8 +1125,9 @@ MatrixClient.prototype.sendHtmlNotice = function(roomId, body, htmlBody, callbac }; /** - * @param {Event} event - * @param {string} receiptType + * Send a receipt. + * @param {Event} event The event being acknowledged + * @param {string} receiptType The kind of receipt e.g. "m.read" * @param {module:client.callback} callback Optional. * @return {module:client.Promise} Resolves: TODO * @return {module:http-api.MatrixError} Rejects: with an error response. @@ -1137,11 +1138,22 @@ MatrixClient.prototype.sendReceipt = function(event, receiptType, callback) { $receiptType: receiptType, $eventId: event.getId() }); - return this._http.authedRequest( - callback, "POST", path, undefined, {} + return this._http.authedRequestWithPrefix( + callback, "POST", path, undefined, {}, httpApi.PREFIX_V2_ALPHA ); }; +/** + * Send a read receipt. + * @param {Event} event The event that has been read. + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixClient.prototype.sendReadReceipt = function(event, callback) { + return this.sendReceipt(event, "m.read", callback); +}; + /** * Upload a file to the media repository on the home server. From 9048efeb658e96f1613e083e43d3dc5834f946ef Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 16 Oct 2015 11:32:27 +0100 Subject: [PATCH 3/8] Implement receipt handling and expose new Room functions Add polyfills for Array.map/filter according to MDN because it looks much better than the utils format. Add stub tests for edge cases and implement test for the common case. --- index.js | 3 + lib/models/room.js | 98 +++++++++++++++++++++++++++++ lib/utils.js | 137 +++++++++++++++++++++++++++++++++++++++++ spec/unit/room.spec.js | 75 ++++++++++++++++++++++ 4 files changed, 313 insertions(+) diff --git a/index.js b/index.js index f05f83f04..35845b1f7 100644 --- a/index.js +++ b/index.js @@ -1,3 +1,6 @@ var matrixcs = require("./lib/matrix"); matrixcs.request(require("request")); module.exports = matrixcs; + +var utils = require("./lib/utils"); +utils.runPolyfills(); diff --git a/lib/models/room.js b/lib/models/room.js index 3e398c50f..c9c8b4794 100644 --- a/lib/models/room.js +++ b/lib/models/room.js @@ -36,6 +36,25 @@ function Room(roomId, storageToken) { this.summary = null; this.storageToken = storageToken; this._redactions = []; + // receipts should clobber based on receipt_type and user_id pairs hence + // the form of this structure. This is sub-optimal for the exposed APIs + // which pass in an event ID and get back some receipts, so we also store + // a pre-cached list for this purpose. + this._receipts = { + // receipt_type: { + // user_id: { + // eventId: , + // data: + // } + // } + }; + this._receiptCacheByEventId = { + // $event_id: [{ + // type: $type, + // userId: $user_id, + // data: + // }] + }; } utils.inherits(Room, EventEmitter); @@ -164,6 +183,9 @@ Room.prototype.addEvents = function(events, duplicateStrategy) { if (events[i].getType() === "m.typing") { this.currentState.setTypingEvent(events[i]); } + else if (events[i].getType() === "m.receipt") { + addReceipt(this, events[i]); + } else { if (duplicateStrategy) { // is there a duplicate? @@ -220,6 +242,82 @@ Room.prototype.recalculate = function(userId) { } }; + +/** + * Get a list of user IDs who have read up to the given event. + * @param {MatrixEvent} event the event to get read receipts for. + * @return {String[]} A list of user IDs. + */ +Room.prototype.getUsersReadUpTo = function(event) { + return this.getReceiptsForEvent(event).filter(function(receipt) { + return receipt.type === "m.read"; + }).map(function(receipt) { + return receipt.userId; + }); +}; + +/** + * Get a list of receipts for the given event. + * @param {MatrixEvent} event the event to get receipts for + * @return {Object[]} A list of receipts with a userId, type and data keys or + * an empty list. + */ +Room.prototype.getReceiptsForEvent = function(event) { + return this._receiptCacheByEventId[event.getId()] || []; +}; + +/** + * Add a receipt event to the room. + * @param {MatrixEvent} event The m.receipt event. + */ +Room.prototype.addReceipt = function(event) { + // event content looks like: + // content: { + // $event_id: { + // $receipt_type: { + // $user_id: { + // ts: $timestamp + // } + // } + // } + // } + var self = this; + utils.keys(event.getContent()).forEach(function(eventId) { + utils.keys(event.getContent()[eventId]).forEach(function(receiptType) { + utils.keys(event.getContent()[eventId][receiptType]).forEach(function(userId) { + var receipt = event.getContent()[eventId][receiptType][userId]; + if (!self._receipts[receiptType]) { + self._receipts[receiptType] = {}; + } + if (!self._receipts[receiptType][userId]) { + self._receipts[receiptType][userId] = {}; + } + var oldEventId = self._receipts[receiptType][userId].eventId; + self._receipts[receiptType][userId] = { + eventId: eventId, + data: receipt + }; + }); + }); + }); + + // pre-cache receipts by event + self._receiptCacheByEventId = {}; + utils.keys(self._receipts).forEach(function(receiptType) { + utils.keys(self._receipts[receiptType]).forEach(function(userId) { + var receipt = self._receipts[receiptType][userId]; + if (!self._receiptCacheByEventId[receipt.eventId]) { + self._receiptCacheByEventId[receipt.eventId] = []; + } + self._receiptCacheByEventId[receipt.eventId].push({ + userId: userId, + type: receiptType, + data: receipt.data + }); + }); + }); +}; + function setEventMetadata(event, stateContext, toStartOfTimeline) { // set sender and target properties event.sender = stateContext.getSentinelMember( diff --git a/lib/utils.js b/lib/utils.js index 913d5815a..464bebc7d 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -228,6 +228,143 @@ module.exports.deepCopy = function(obj) { return JSON.parse(JSON.stringify(obj)); }; + +/** + * Run polyfills to add Array.map and Array.filter if they are missing. + */ +module.exports.runPolyfills = function() { + // Array.prototype.filter + // ======================================================== + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter + if (!Array.prototype.filter) { + Array.prototype.filter = function(fun/*, thisArg*/) { + 'use strict'; + + if (this === void 0 || this === null) { + throw new TypeError(); + } + + var t = Object(this); + var len = t.length >>> 0; + if (typeof fun !== 'function') { + throw new TypeError(); + } + + var res = []; + var thisArg = arguments.length >= 2 ? arguments[1] : void 0; + for (var i = 0; i < len; i++) { + if (i in t) { + var val = t[i]; + + // NOTE: Technically this should Object.defineProperty at + // the next index, as push can be affected by + // properties on Object.prototype and Array.prototype. + // But that method's new, and collisions should be + // rare, so use the more-compatible alternative. + if (fun.call(thisArg, val, i, t)) { + res.push(val); + } + } + } + + return res; + }; + } + + // Array.prototype.map + // ======================================================== + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map + // Production steps of ECMA-262, Edition 5, 15.4.4.19 + // Reference: http://es5.github.io/#x15.4.4.19 + if (!Array.prototype.map) { + + Array.prototype.map = function(callback, thisArg) { + + var T, A, k; + + if (this == null) { + throw new TypeError(' this is null or not defined'); + } + + // 1. Let O be the result of calling ToObject passing the |this| + // value as the argument. + var O = Object(this); + + // 2. Let lenValue be the result of calling the Get internal + // method of O with the argument "length". + // 3. Let len be ToUint32(lenValue). + var len = O.length >>> 0; + + // 4. If IsCallable(callback) is false, throw a TypeError exception. + // See: http://es5.github.com/#x9.11 + if (typeof callback !== 'function') { + throw new TypeError(callback + ' is not a function'); + } + + // 5. If thisArg was supplied, let T be thisArg; else let T be undefined. + if (arguments.length > 1) { + T = thisArg; + } + + // 6. Let A be a new array created as if by the expression new Array(len) + // where Array is the standard built-in constructor with that name and + // len is the value of len. + A = new Array(len); + + // 7. Let k be 0 + k = 0; + + // 8. Repeat, while k < len + while (k < len) { + + var kValue, mappedValue; + + // a. Let Pk be ToString(k). + // This is implicit for LHS operands of the in operator + // b. Let kPresent be the result of calling the HasProperty internal + // method of O with argument Pk. + // This step can be combined with c + // c. If kPresent is true, then + if (k in O) { + + // i. Let kValue be the result of calling the Get internal + // method of O with argument Pk. + kValue = O[k]; + + // ii. Let mappedValue be the result of calling the Call internal + // method of callback with T as the this value and argument + // list containing kValue, k, and O. + mappedValue = callback.call(T, kValue, k, O); + + // iii. Call the DefineOwnProperty internal method of A with arguments + // Pk, Property Descriptor + // { Value: mappedValue, + // Writable: true, + // Enumerable: true, + // Configurable: true }, + // and false. + + // In browsers that support Object.defineProperty, use the following: + // Object.defineProperty(A, k, { + // value: mappedValue, + // writable: true, + // enumerable: true, + // configurable: true + // }); + + // For best browser support, use the following: + A[k] = mappedValue; + } + // d. Increase k by 1. + k++; + } + + // 9. return A + return A; + }; + } +} + /** * Inherit the prototype methods from one constructor into another. This is a * port of the Node.js implementation with an Object.create polyfill. diff --git a/spec/unit/room.spec.js b/spec/unit/room.spec.js index e0277a88f..d37560599 100644 --- a/spec/unit/room.spec.js +++ b/spec/unit/room.spec.js @@ -2,6 +2,7 @@ var sdk = require("../.."); var Room = sdk.Room; var RoomState = sdk.RoomState; +var MatrixEvent = sdk.MatrixEvent; var utils = require("../test-utils"); describe("Room", function() { @@ -549,4 +550,78 @@ describe("Room", function() { expect(name).toEqual("?"); }); }); + + describe("addReceipt", function() { + + var eventToAck = utils.mkMessage({ + room: roomId, user: userA, msg: "PLEASE ACKNOWLEDGE MY EXISTENCE", + event: true + }); + + function mkReceipt(roomId, records) { + var content = {}; + records.forEach(function(r) { + if (!content[r.eventId]) { content[r.eventId] = {}; } + if (!content[r.eventId][r.type]) { content[r.eventId][r.type] = {}; } + content[r.eventId][r.type][r.userId] = { + ts: r.ts + }; + }); + return new MatrixEvent({ + content: content, + room_id: roomId, + type: "m.receipt" + }); + } + + function mkRecord(eventId, type, userId, ts) { + ts = ts || Date.now(); + return { + eventId: eventId, + type: type, + userId: userId, + ts: ts + }; + } + + it("should store the receipt so it can be obtained via getReceiptsForEvent", + function() { + var ts = 13787898424; + room.addReceipt(mkReceipt(roomId, [ + mkRecord(eventToAck.getId(), "m.read", userB, ts) + ])); + expect(room.getReceiptsForEvent(eventToAck)).toEqual([{ + type: "m.read", + userId: userB, + data: { + ts: ts + } + }]); + }); + + it("should clobber receipts based on type and user ID", function() { + + }); + + it("should persist multiple receipts for a single event ID", function() { + + }); + + it("should persist multiple receipts for a single receipt type", function() { + + }); + + it("should persist multiple receipts for a single user ID", function() { + + }); + + }); + + describe("getUsersReadUpTo", function() { + + it("should return user IDs read up to the given event", function() { + + }); + + }) }); From 7ec8421d19541d188710b5a3f37fc13ad75106d8 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 16 Oct 2015 11:38:49 +0100 Subject: [PATCH 4/8] Fix linting errors --- .jshint | 2 +- lib/models/room.js | 6 +++--- lib/utils.js | 21 ++++++++++----------- spec/unit/room.spec.js | 2 +- 4 files changed, 15 insertions(+), 16 deletions(-) diff --git a/.jshint b/.jshint index 41484cf8f..871364f40 100644 --- a/.jshint +++ b/.jshint @@ -5,7 +5,7 @@ "nonew": true, "curly": true, "forin": true, - "freeze": true, + "freeze": false, "undef": true, "unused": "vars" } diff --git a/lib/models/room.js b/lib/models/room.js index c9c8b4794..51554645a 100644 --- a/lib/models/room.js +++ b/lib/models/room.js @@ -184,7 +184,7 @@ Room.prototype.addEvents = function(events, duplicateStrategy) { this.currentState.setTypingEvent(events[i]); } else if (events[i].getType() === "m.receipt") { - addReceipt(this, events[i]); + this.addReceipt(events[i]); } else { if (duplicateStrategy) { @@ -284,7 +284,8 @@ Room.prototype.addReceipt = function(event) { var self = this; utils.keys(event.getContent()).forEach(function(eventId) { utils.keys(event.getContent()[eventId]).forEach(function(receiptType) { - utils.keys(event.getContent()[eventId][receiptType]).forEach(function(userId) { + utils.keys(event.getContent()[eventId][receiptType]).forEach( + function(userId) { var receipt = event.getContent()[eventId][receiptType][userId]; if (!self._receipts[receiptType]) { self._receipts[receiptType] = {}; @@ -292,7 +293,6 @@ Room.prototype.addReceipt = function(event) { if (!self._receipts[receiptType][userId]) { self._receipts[receiptType][userId] = {}; } - var oldEventId = self._receipts[receiptType][userId].eventId; self._receipts[receiptType][userId] = { eventId: eventId, data: receipt diff --git a/lib/utils.js b/lib/utils.js index 464bebc7d..2460a02fa 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -238,7 +238,6 @@ module.exports.runPolyfills = function() { // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter if (!Array.prototype.filter) { Array.prototype.filter = function(fun/*, thisArg*/) { - 'use strict'; if (this === void 0 || this === null) { throw new TypeError(); @@ -282,15 +281,15 @@ module.exports.runPolyfills = function() { var T, A, k; - if (this == null) { + if (this === null || this === undefined) { throw new TypeError(' this is null or not defined'); } - // 1. Let O be the result of calling ToObject passing the |this| + // 1. Let O be the result of calling ToObject passing the |this| // value as the argument. var O = Object(this); - // 2. Let lenValue be the result of calling the Get internal + // 2. Let lenValue be the result of calling the Get internal // method of O with the argument "length". // 3. Let len be ToUint32(lenValue). var len = O.length >>> 0; @@ -306,8 +305,8 @@ module.exports.runPolyfills = function() { T = thisArg; } - // 6. Let A be a new array created as if by the expression new Array(len) - // where Array is the standard built-in constructor with that name and + // 6. Let A be a new array created as if by the expression new Array(len) + // where Array is the standard built-in constructor with that name and // len is the value of len. A = new Array(len); @@ -321,18 +320,18 @@ module.exports.runPolyfills = function() { // a. Let Pk be ToString(k). // This is implicit for LHS operands of the in operator - // b. Let kPresent be the result of calling the HasProperty internal + // b. Let kPresent be the result of calling the HasProperty internal // method of O with argument Pk. // This step can be combined with c // c. If kPresent is true, then if (k in O) { - // i. Let kValue be the result of calling the Get internal + // i. Let kValue be the result of calling the Get internal // method of O with argument Pk. kValue = O[k]; - // ii. Let mappedValue be the result of calling the Call internal - // method of callback with T as the this value and argument + // ii. Let mappedValue be the result of calling the Call internal + // method of callback with T as the this value and argument // list containing kValue, k, and O. mappedValue = callback.call(T, kValue, k, O); @@ -363,7 +362,7 @@ module.exports.runPolyfills = function() { return A; }; } -} +}; /** * Inherit the prototype methods from one constructor into another. This is a diff --git a/spec/unit/room.spec.js b/spec/unit/room.spec.js index d37560599..423db86c0 100644 --- a/spec/unit/room.spec.js +++ b/spec/unit/room.spec.js @@ -623,5 +623,5 @@ describe("Room", function() { }); - }) + }); }); From 40d113a423283f71515d84808fc817ed6cc467f6 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 16 Oct 2015 11:54:47 +0100 Subject: [PATCH 5/8] Pass in receipts from initialSync --- lib/client.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/lib/client.js b/lib/client.js index ea7aee9cb..87476d6c8 100644 --- a/lib/client.js +++ b/lib/client.js @@ -1903,6 +1903,19 @@ function doInitialSync(client, historyLen) { user.setPresenceEvent(e); client.store.storeUser(user); }); + + // group receipts by room ID. + var receiptsByRoom = {}; + data.receipts = data.receipts || []; + utils.forEach(data.receipts.map(_PojoToMatrixEventMapper(client)), + function(receiptEvent) { + if (!receiptsByRoom[receiptEvent.getRoomId()]) { + receiptsByRoom[receiptEvent.getRoomId()] = []; + } + receiptsByRoom[receiptEvent.getRoomId()].push(receiptEvent); + } + ); + for (i = 0; i < data.rooms.length; i++) { var room = createNewRoom(client, data.rooms[i].room_id); if (!data.rooms[i].state) { @@ -1926,6 +1939,11 @@ function doInitialSync(client, historyLen) { client, room, data.rooms[i].state, data.rooms[i].messages ); + var receipts = receiptsByRoom[room.roomId] || []; + for (j = 0; j < receipts.length; j++) { + room.addReceipt(receipts[j]); + } + // cache the name/summary/etc prior to storage since we don't // know how the store will serialise the Room. room.recalculate(client.credentials.userId); From a52f92830a87e60b501590c858a92cf5d2f8cd41 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 16 Oct 2015 13:37:53 +0100 Subject: [PATCH 6/8] Implement unit tests for read receipts. --- spec/unit/room.spec.js | 142 ++++++++++++++++++++++++++++++++--------- 1 file changed, 111 insertions(+), 31 deletions(-) diff --git a/spec/unit/room.spec.js b/spec/unit/room.spec.js index 423db86c0..a4ea0f896 100644 --- a/spec/unit/room.spec.js +++ b/spec/unit/room.spec.js @@ -551,7 +551,7 @@ describe("Room", function() { }); }); - describe("addReceipt", function() { + describe("receipts", function() { var eventToAck = utils.mkMessage({ room: roomId, user: userA, msg: "PLEASE ACKNOWLEDGE MY EXISTENCE", @@ -584,44 +584,124 @@ describe("Room", function() { }; } - it("should store the receipt so it can be obtained via getReceiptsForEvent", - function() { - var ts = 13787898424; - room.addReceipt(mkReceipt(roomId, [ - mkRecord(eventToAck.getId(), "m.read", userB, ts) - ])); - expect(room.getReceiptsForEvent(eventToAck)).toEqual([{ - type: "m.read", - userId: userB, - data: { - ts: ts + describe("addReceipt", function() { + + it("should store the receipt so it can be obtained via getReceiptsForEvent", + function() { + var ts = 13787898424; + room.addReceipt(mkReceipt(roomId, [ + mkRecord(eventToAck.getId(), "m.read", userB, ts) + ])); + expect(room.getReceiptsForEvent(eventToAck)).toEqual([{ + type: "m.read", + userId: userB, + data: { + ts: ts + } + }]); + }); + + it("should clobber receipts based on type and user ID", function() { + var nextEventToAck = utils.mkMessage({ + room: roomId, user: userA, msg: "I AM HERE YOU KNOW", + event: true + }); + var ts = 13787898424; + room.addReceipt(mkReceipt(roomId, [ + mkRecord(eventToAck.getId(), "m.read", userB, ts) + ])); + var ts2 = 13787899999; + room.addReceipt(mkReceipt(roomId, [ + mkRecord(nextEventToAck.getId(), "m.read", userB, ts2) + ])); + expect(room.getReceiptsForEvent(eventToAck)).toEqual([]); + expect(room.getReceiptsForEvent(nextEventToAck)).toEqual([{ + type: "m.read", + userId: userB, + data: { + ts: ts2 + } + }]); + }); + + it("should persist multiple receipts for a single event ID", function() { + var ts = 13787898424; + room.addReceipt(mkReceipt(roomId, [ + mkRecord(eventToAck.getId(), "m.read", userB, ts), + mkRecord(eventToAck.getId(), "m.read", userC, ts), + mkRecord(eventToAck.getId(), "m.read", userD, ts) + ])); + expect(room.getUsersReadUpTo(eventToAck)).toEqual( + [userB, userC, userD] + ); + }); + + it("should persist multiple receipts for a single receipt type", function() { + var eventTwo = utils.mkMessage({ + room: roomId, user: userA, msg: "2222", + event: true + }); + var eventThree = utils.mkMessage({ + room: roomId, user: userA, msg: "3333", + event: true + }); + var ts = 13787898424; + room.addReceipt(mkReceipt(roomId, [ + mkRecord(eventToAck.getId(), "m.read", userB, ts), + mkRecord(eventTwo.getId(), "m.read", userC, ts), + mkRecord(eventThree.getId(), "m.read", userD, ts) + ])); + expect(room.getUsersReadUpTo(eventToAck)).toEqual([userB]); + expect(room.getUsersReadUpTo(eventTwo)).toEqual([userC]); + expect(room.getUsersReadUpTo(eventThree)).toEqual([userD]); + }); + + it("should persist multiple receipts for a single user ID", function() { + room.addReceipt(mkReceipt(roomId, [ + mkRecord(eventToAck.getId(), "m.delivered", userB, 13787898424), + mkRecord(eventToAck.getId(), "m.read", userB, 22222222), + mkRecord(eventToAck.getId(), "m.seen", userB, 33333333), + ])); + expect(room.getReceiptsForEvent(eventToAck)).toEqual([ + { + type: "m.delivered", + userId: userB, + data: { + ts: 13787898424 + } + }, + { + type: "m.read", + userId: userB, + data: { + ts: 22222222 + } + }, + { + type: "m.seen", + userId: userB, + data: { + ts: 33333333 + } } - }]); - }); - - it("should clobber receipts based on type and user ID", function() { + ]); + }); }); - it("should persist multiple receipts for a single event ID", function() { + describe("getUsersReadUpTo", function() { - }); - - it("should persist multiple receipts for a single receipt type", function() { - - }); - - it("should persist multiple receipts for a single user ID", function() { + it("should return user IDs read up to the given event", function() { + var ts = 13787898424; + room.addReceipt(mkReceipt(roomId, [ + mkRecord(eventToAck.getId(), "m.read", userB, ts) + ])); + expect(room.getUsersReadUpTo(eventToAck)).toEqual([userB]); + }); }); }); - describe("getUsersReadUpTo", function() { - - it("should return user IDs read up to the given event", function() { - - }); - - }); + }); From a101857cb66da60ca5b4ac1d6138bf1211432ce0 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 16 Oct 2015 13:51:44 +0100 Subject: [PATCH 7/8] Add integration tests for read receipts --- spec/integ/matrix-client-syncing.spec.js | 117 +++++++++++++++++++++++ spec/unit/room.spec.js | 1 - 2 files changed, 117 insertions(+), 1 deletion(-) diff --git a/spec/integ/matrix-client-syncing.spec.js b/spec/integ/matrix-client-syncing.spec.js index 0382d4d86..6e85f0c36 100644 --- a/spec/integ/matrix-client-syncing.spec.js +++ b/spec/integ/matrix-client-syncing.spec.js @@ -2,6 +2,7 @@ var sdk = require("../.."); var HttpBackend = require("../mock-request"); var utils = require("../test-utils"); +var MatrixEvent = sdk.MatrixEvent; describe("MatrixClient syncing", function() { var baseUrl = "http://localhost.or.something"; @@ -270,6 +271,122 @@ describe("MatrixClient syncing", function() { }); }); + describe("receipts", function() { + var roomOne = "!foo:localhost"; + var initialSync = { + end: "s_5_3", + presence: [], + receipts: [], + rooms: [{ + membership: "join", + room_id: roomOne, + messages: { + start: "f_1_1", + end: "f_2_2", + chunk: [ + utils.mkMessage({ + room: roomOne, user: otherUserId, msg: "hello" + }) + ] + }, + state: [ + utils.mkEvent({ + type: "m.room.name", room: roomOne, user: otherUserId, + content: { + name: "Old room name" + } + }), + utils.mkMembership({ + room: roomOne, mship: "join", user: otherUserId + }), + utils.mkMembership({ + room: roomOne, mship: "join", user: selfUserId + }), + utils.mkEvent({ + type: "m.room.create", room: roomOne, user: selfUserId, + content: { + creator: selfUserId + } + }) + ] + }] + }; + var eventData = { + start: "s_5_3", + end: "e_6_7", + chunk: [] + }; + + beforeEach(function() { + eventData.chunk = []; + initialSync.receipts = []; + }); + + it("should sync receipts from /initialSync.", function(done) { + var ackEvent = initialSync.rooms[0].messages.chunk[0]; + var receipt = {}; + receipt[ackEvent.event_id] = { + "m.read": {} + }; + receipt[ackEvent.event_id]["m.read"][otherUserId] = { + ts: 176592842636 + }; + initialSync.receipts = [{ + content: receipt, + room_id: roomOne, + type: "m.receipt" + }]; + httpBackend.when("GET", "/initialSync").respond(200, initialSync); + httpBackend.when("GET", "/events").respond(200, eventData); + + client.startClient(); + + httpBackend.flush().done(function() { + var room = client.getRoom(roomOne); + expect(room.getReceiptsForEvent(new MatrixEvent(ackEvent))).toEqual([{ + type: "m.read", + userId: otherUserId, + data: { + ts: 176592842636 + } + }]); + done(); + }); + }); + + it("should sync receipts from /events.", function(done) { + var ackEvent = initialSync.rooms[0].messages.chunk[0]; + var receipt = {}; + receipt[ackEvent.event_id] = { + "m.read": {} + }; + receipt[ackEvent.event_id]["m.read"][otherUserId] = { + ts: 176592842636 + }; + eventData.chunk = [{ + content: receipt, + room_id: roomOne, + type: "m.receipt" + }]; + httpBackend.when("GET", "/initialSync").respond(200, initialSync); + httpBackend.when("GET", "/events").respond(200, eventData); + + client.startClient(); + + httpBackend.flush().done(function() { + var room = client.getRoom(roomOne); + expect(room.getReceiptsForEvent(new MatrixEvent(ackEvent))).toEqual([{ + type: "m.read", + userId: otherUserId, + data: { + ts: 176592842636 + } + }]); + done(); + }); + }); + }); + describe("of a room", function() { xit("should sync when a join event (which changes state) for the user" + " arrives down the event stream (e.g. join from another device)", function() { diff --git a/spec/unit/room.spec.js b/spec/unit/room.spec.js index a4ea0f896..0c5b22f8f 100644 --- a/spec/unit/room.spec.js +++ b/spec/unit/room.spec.js @@ -703,5 +703,4 @@ describe("Room", function() { }); - }); From a9c43451590f32584623ce6c7e1f0995103e11ce Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Mon, 19 Oct 2015 15:29:57 +0100 Subject: [PATCH 8/8] Clarify the link is the source of the code --- lib/utils.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/utils.js b/lib/utils.js index 2460a02fa..a18a1dc21 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -235,6 +235,7 @@ module.exports.deepCopy = function(obj) { module.exports.runPolyfills = function() { // Array.prototype.filter // ======================================================== + // SOURCE: // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter if (!Array.prototype.filter) { Array.prototype.filter = function(fun/*, thisArg*/) { @@ -272,6 +273,7 @@ module.exports.runPolyfills = function() { // Array.prototype.map // ======================================================== + // SOURCE: // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map // Production steps of ECMA-262, Edition 5, 15.4.4.19 // Reference: http://es5.github.io/#x15.4.4.19