Skip to content

Commit

Permalink
Implement receipt handling and expose new Room functions
Browse files Browse the repository at this point in the history
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
kegsay committed Oct 16, 2015
1 parent 43fc200 commit 9048efe
Show file tree
Hide file tree
Showing 4 changed files with 313 additions and 0 deletions.
3 changes: 3 additions & 0 deletions 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();
98 changes: 98 additions & 0 deletions lib/models/room.js
Expand Up @@ -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);

Expand Down Expand Up @@ -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?
Expand Down Expand Up @@ -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(
Expand Down
137 changes: 137 additions & 0 deletions lib/utils.js
Expand Up @@ -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.
Expand Down
75 changes: 75 additions & 0 deletions spec/unit/room.spec.js
Expand Up @@ -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() {
Expand Down Expand Up @@ -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.