Permalink
Browse files

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.
  • Loading branch information...
1 parent 43fc200 commit 9048efeb658e96f1613e083e43d3dc5834f946ef @Kegsay Kegsay committed Oct 16, 2015
Showing with 313 additions and 0 deletions.
  1. +3 −0 index.js
  2. +98 −0 lib/models/room.js
  3. +137 −0 lib/utils.js
  4. +75 −0 spec/unit/room.spec.js
View
@@ -1,3 +1,6 @@
var matrixcs = require("./lib/matrix");
matrixcs.request(require("request"));
module.exports = matrixcs;
+
+var utils = require("./lib/utils");
+utils.runPolyfills();
View
@@ -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: <event_id>,
+ // data: <receipt_data>
+ // }
+ // }
+ };
+ this._receiptCacheByEventId = {
+ // $event_id: [{
+ // type: $type,
+ // userId: $user_id,
+ // data: <receipt 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 <b>read up to</b> 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(
View
@@ -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.
@@ -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() {
+
+ });
+
+ })
});

0 comments on commit 9048efe

Please sign in to comment.