Compare commits

..

56 Commits

Author SHA1 Message Date
Richard van der Hoff 09d08a5092 0.5.3 2016-06-02 13:34:51 +01:00
Richard van der Hoff 8c7d041628 Prepare changelog for v0.5.3 2016-06-02 13:34:36 +01:00
Matthew Hodgson 0421e69f14 revert brand param at reg time 2016-06-02 12:33:09 +01:00
Matthew Hodgson 8b35ddae0a add 'brand' parameter to register() 2016-06-02 11:46:02 +01:00
Matthew Hodgson 7243367a64 only clobber displayname & avatarurl from presence if set. fixes https://github.com/vector-im/vector-web/issues/1039. again. 2016-06-01 03:45:16 +01:00
Matthew Hodgson fb388f5d2d fix typoes 2016-05-17 21:45:27 +01:00
David Baker 0757a1dd1c Merge pull request #133 from matrix-org/dbkr/scalar
Add support for the openid interface
2016-05-06 15:56:33 +01:00
David Baker 06486f7ad0 Fix c+p fails 2016-05-06 14:28:26 +01:00
David Baker a9d8c58ea0 Add support for the openid interface 2016-05-06 13:58:10 +01:00
Paul Evans beec36d484 Merge pull request #129 from matrix-org/paul/bugfix-upload-content
Bugfix for HTTP upload content when running on node
2016-04-25 15:46:09 +01:00
Paul "LeoNerd" Evans 37ea7edaa0 Bugfix for HTTP upload content when running on node 2016-04-21 14:16:20 +01:00
Richard van der Hoff 2c40932080 0.5.2 2016-04-19 13:10:12 +01:00
Richard van der Hoff 3777b3e211 Prepare changelog for v0.5.2 2016-04-19 13:09:56 +01:00
Matthew Hodgson 2f7d7308a1 Merge pull request #128 from matrix-org/matthew/syjs-28
Track the absolute time that presence events are received, so that the relative lastActiveAgo value is meaningful.
2016-04-18 19:15:03 +01:00
Matthew Hodgson 0e606c6fe2 incorporate PR feedback 2016-04-18 14:26:59 +01:00
Matthew Hodgson 3af35c8209 Merge branch 'develop' into matthew/syjs-28 2016-04-18 01:34:11 +01:00
Matthew Hodgson a2aed99f56 track lastPresenceTs 2016-04-18 01:25:34 +01:00
Richard van der Hoff 523a684d3f Merge pull request #127 from matrix-org/rav/refactor_add_events
Refactor the addition of events to rooms
2016-04-17 18:11:02 +01:00
Richard van der Hoff dc386bab46 Fix debug flag name 2016-04-14 17:53:57 +01:00
Richard van der Hoff 69079a2f9a A handy hook script 2016-04-14 17:41:52 +01:00
Richard van der Hoff df33f7aceb Fix lint failures 2016-04-14 17:36:25 +01:00
Richard van der Hoff d87e5471fa Refactor the addition of events to rooms
... and add some sanity checks

Two things here:

1. Clean up the Room API for adding new events to the timeline. Where before
we had addEvents and addEventsToTimeline, whose purposes were unclear, we now
have addLiveEvents which must be used for adding events to the end of the live
timeline, and addEventsToTimeline which should be used for pagination (either
back-pagination of the live timeline, or pagination of an old timeline).

2. Add some sanity checks for the live timeline. Today we have seen problems
where somehow the live timeline had gained a forward pagination token, or the
live timeline had got joined to another timeline, leading to much confusion -
and I would like to notice these sooner.
2016-04-14 17:03:25 +01:00
Richard van der Hoff 90101c0340 Give timelines a name
The debug of "joined timeline [Object] to timeline [Object]" isn't terribly
helpful, so let's at least give them an identifier.
2016-04-14 16:11:48 +01:00
Richard van der Hoff 950fce80c8 Whitespace fix
remove trailing ws
2016-04-14 16:09:51 +01:00
Richard van der Hoff 9135c50b83 Merge pull request #126 from matrix-org/rav/clean_up_testexit
Clean up test shutdown
2016-04-14 15:54:56 +01:00
Richard van der Hoff 83bad6ee0d Fix lint error
semicolons rool
2016-04-14 13:52:34 +01:00
Richard van der Hoff 3404751eb9 Clean up test shutdown
Make sure that the integration tests actually kill off all of their timers, so
that jasmine exits cleanly.

This probably also fixes https://github.com/vector-im/vector-web/issues/1365.
2016-04-14 12:01:23 +01:00
Richard van der Hoff 0282021e09 Add a cachebuster to initial /sync
... in the hope of fending off weird firefox restore issues
2016-04-13 22:17:10 +01:00
Richard van der Hoff 526e1d59e9 Log sync token when starting sync loop
A little bit of debug that might help with
https://github.com/vector-im/vector-web/issues/1354
2016-04-13 14:08:59 +01:00
David Baker dbc3a9a500 Merge pull request #125 from matrix-org/dbkr/get_pushers
Add methods to get (and set) pushers
2016-04-12 13:25:17 +01:00
David Baker cff7c8a59f Add methods to get (and set) pushers 2016-04-12 13:11:00 +01:00
Matthew Hodgson 64b8047f01 Merge pull request #122 from matrix-org/matthew/preview_urls
URL previewing support
2016-04-11 17:35:53 +01:00
Matthew Hodgson 11e4760935 improve commentary 2016-04-08 22:00:07 +01:00
Richard van der Hoff c21d5634bb Merge pull request #124 from matrix-org/rav/limit_pagination
Avoid paginating forever in private rooms
2016-04-08 15:55:37 +01:00
Richard van der Hoff dc56d7f784 Fix typo
s/evnets/events/
2016-04-08 15:54:57 +01:00
Matthew Hodgson 1ff1064295 Merge branch 'develop' into matthew/preview_urls 2016-04-07 17:26:10 +01:00
Richard van der Hoff a493a0ddb3 Fix lint errors 2016-04-07 14:30:35 +01:00
Richard van der Hoff 7573171d05 Avoid paginating forever in private rooms
In TimelineWindow.paginate, keep a count of the number of API requests we have
made, and bail out if it gets too high, to ensure that we don't get stuck in a
loop of paginating right back to the start of the room.
2016-04-07 14:16:02 +01:00
Richard van der Hoff 989e7cf78b Merge pull request #123 from matrix-org/rav/no_recreate_filter
Fix a bug where we recreated sync filters
2016-04-06 18:35:01 +01:00
Richard van der Hoff 384c474800 fix failure of deepCompare to compare arrays
what a difference a character can make
2016-04-06 18:14:34 +01:00
Richard van der Hoff 1d2c705e13 Fix a bug where we recreated sync filters
Fix the object comparison used for client filters (JSON.stringify is
non-deterministic)
2016-04-06 15:56:32 +01:00
Matthew Hodgson c469ff4c8d oops, fix sig 2016-04-03 01:19:34 +01:00
Matthew Hodgson c7575f3f16 cache url preview results 2016-04-03 01:18:01 +01:00
Matthew Hodgson 415251dd70 WIP url previewing 2016-03-31 18:38:34 +01:00
Richard van der Hoff 458cc55dec Merge pull request #121 from matrix-org/rav/realtime_callbacks
Implement HTTP callbacks in realtime
2016-03-31 16:33:25 +01:00
Richard van der Hoff d2adb30ded Implement HTTP callbacks in realtime
Hopefully this will improve our recovery time after a laptop is suspended. The
idea is to treat the timeouts on the http apis as being in realtime, rather
than in elapsed time while the machine is awake.

To do this, we add a layer on top of window.setTimeout. We run a callback every
second, which then checks the wallclock time and runs any pending callbacks.
2016-03-31 13:51:18 +01:00
Richard van der Hoff bbe41aa7b4 0.5.1 2016-03-30 13:18:42 +01:00
Richard van der Hoff ece9391878 Prepare changelog for v0.5.1 2016-03-30 13:17:10 +01:00
David Baker 64640287cf Merge pull request #119 from matrix-org/dbkr/member_count_only_joined
Only count joined members for the member count in notifications.
2016-03-24 14:01:45 +00:00
David Baker 57f88b00ba d'oh, no es6 in js-sdk 2016-03-24 13:57:55 +00:00
David Baker e618ad9589 Only count joined members for the member count in notifications. Fixes https://github.com/vector-im/vector-web/issues/1067 2016-03-24 13:55:30 +00:00
Richard van der Hoff 69c4d7b66e Merge pull request #118 from matrix-org/dbkr/maysendevent
Add maySendEvent to match maySendStateEvent
2016-03-24 11:43:29 +00:00
David Baker d7b3b91eec Failed to remove extra param 2016-03-23 17:08:22 +00:00
David Baker 88cc63e2a2 Add maySendEvent to match maySendStateEvent. Make them use the same function internally. Also add convenience maySendMessage. Also tests. 2016-03-23 15:10:51 +00:00
Richard van der Hoff cdea96fa0a Release script tweaks
- be more helpful if update_changelog is not installed
- behave sanely if v is omitted on tag arg
2016-03-23 14:50:03 +00:00
Matthew Hodgson cfc10fa82d fix invite picker info again... 2016-03-23 12:03:56 +00:00
35 changed files with 49803 additions and 332 deletions
+41 -1
View File
@@ -1,3 +1,43 @@
Changes in [0.5.3](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.5.3) (2016-06-02)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.5.2...v0.5.3)
* Add support for the openid interface
[\#133](https://github.com/matrix-org/matrix-js-sdk/pull/133)
* Bugfix for HTTP upload content when running on node
[\#129](https://github.com/matrix-org/matrix-js-sdk/pull/129)
Changes in [0.5.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.5.2) (2016-04-19)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.5.1...v0.5.2)
* Track the absolute time that presence events are received, so that the
relative lastActiveAgo value is meaningful.
[\#128](https://github.com/matrix-org/matrix-js-sdk/pull/128)
* Refactor the addition of events to rooms
[\#127](https://github.com/matrix-org/matrix-js-sdk/pull/127)
* Clean up test shutdown
[\#126](https://github.com/matrix-org/matrix-js-sdk/pull/126)
* Add methods to get (and set) pushers
[\#125](https://github.com/matrix-org/matrix-js-sdk/pull/125)
* URL previewing support
[\#122](https://github.com/matrix-org/matrix-js-sdk/pull/122)
* Avoid paginating forever in private rooms
[\#124](https://github.com/matrix-org/matrix-js-sdk/pull/124)
* Fix a bug where we recreated sync filters
[\#123](https://github.com/matrix-org/matrix-js-sdk/pull/123)
* Implement HTTP timeouts in realtime
[\#121](https://github.com/matrix-org/matrix-js-sdk/pull/121)
Changes in [0.5.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.5.1) (2016-03-30)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.5.0...v0.5.1)
* Only count joined members for the member count in notifications.
[\#119](https://github.com/matrix-org/matrix-js-sdk/pull/119)
* Add maySendEvent to match maySendStateEvent
[\#118](https://github.com/matrix-org/matrix-js-sdk/pull/118)
Changes in [0.5.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.5.0) (2016-03-22)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.4.2...v0.5.0)
@@ -70,7 +110,7 @@ Improvements:
* Avoid getting stuck in a pagination loop when the server sends us only
messages we've already seen
(https://github.com/matrix-org/matrix-js-sdk/pull/96).
New methods:
* Add `MatrixClient.setPushRuleActions` to set the actions for a push
notification rule (https://github.com/matrix-org/matrix-js-sdk/pull/90)
+15832
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+16305
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+16327
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+24
View File
@@ -0,0 +1,24 @@
#!/bin/sh
#
# pre-commit: script to run checks on a working copy before commit
#
# To use, symlink it into .git/hooks:
# ln -s ../../git-hooks/pre-commit .git/hooks
#
set -e
# create a temp dir
tmpdir=`mktemp -d`
trap 'rm -rf "$tmpdir"' EXIT
# get a copy of the index
git checkout-index --prefix="$tmpdir/" -a
# keep node_modules/.bin on the path
rootdir=`git rev-parse --show-toplevel`
export PATH="$rootdir/node_modules/.bin:$PATH"
# now run our checks
cd "$tmpdir"
npm run lint
+88 -3
View File
@@ -185,6 +185,7 @@ function MatrixClient(opts) {
this._ongoingScrollbacks = {};
this._txnCtr = 0;
this.timelineSupport = Boolean(opts.timelineSupport);
this.urlPreviewCache = {};
}
utils.inherits(MatrixClient, EventEmitter);
@@ -907,7 +908,7 @@ MatrixClient.prototype.getStateEvent = function(roomId, eventType, stateKey, cal
* @return {module:client.Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.sendStateEvent = function(roomId, eventType, content, stateKey,
MatrixClient.prototype.sendStateEvent = function(roomId, eventType, content, stateKey,
callback) {
var pathParams = {
$roomId: roomId,
@@ -1448,6 +1449,41 @@ MatrixClient.prototype.getCurrentUploads = function() {
return this._http.getCurrentUploads();
};
/**
* Get a preview of the given URL as of (roughly) the given point in time,
* described as an object with OpenGraph keys and associated values.
* Attributes may be synthesized where actual OG metadata is lacking.
* Caches results to prevent hammering the server.
* @param {string} url The URL to get preview data for
* @param {Number} ts The preferred point in time that the preview should
* describe (ms since epoch). The preview returned will either be the most
* recent one preceding this timestamp if available, or failing that the next
* most recent available preview.
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: Object of OG metadata.
* @return {module:http-api.MatrixError} Rejects: with an error response.
* May return synthesized attributes if the URL lacked OG meta.
*/
MatrixClient.prototype.getUrlPreview = function(url, ts, callback) {
var key = ts + "_" + url;
var og = this.urlPreviewCache[key];
if (og) {
return q(og);
}
var self = this;
return this._http.authedRequestWithPrefix(
callback, "GET", "/preview_url", {
url: url,
ts: ts,
}, undefined, httpApi.PREFIX_MEDIA_R0
).then(function(response) {
// TODO: expire cache occasionally
self.urlPreviewCache[key] = response;
return response;
});
};
/**
* @param {string} roomId
* @param {boolean} isTyping
@@ -1693,7 +1729,7 @@ MatrixClient.prototype.kick = function(roomId, userId, reason, callback) {
* @return {module:client.Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
function _setMembershipState(client, roomId, userId, membershipValue, reason,
function _setMembershipState(client, roomId, userId, membershipValue, reason,
callback) {
if (utils.isFunction(reason)) { callback = reason; reason = undefined; }
@@ -1904,6 +1940,38 @@ MatrixClient.prototype.setPresence = function(presence, callback) {
);
};
// Pushers
// =======
/**
* Gets all pushers registered for the logged-in user
*
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: Array of objects representing pushers
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.getPushers = function(callback) {
var path = "/pushers";
return this._http.authedRequest(
callback, "GET", path, undefined, undefined
);
};
/**
* Adds a new pusher or updates an existing pusher
*
* @param {Object} pusher Object representing a pusher
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: Empty json object on success
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.setPusher = function(pusher, callback) {
var path = "/pushers/set";
return this._http.authedRequest(
callback, "POST", path, null, pusher
);
};
// Public (non-authed) operations
// ==============================
@@ -2063,7 +2131,7 @@ MatrixClient.prototype.scrollback = function(room, limit, callback) {
return self._http.authedRequest(callback, "GET", path, params);
}).done(function(res) {
var matrixEvents = utils.map(res.chunk, _PojoToMatrixEventMapper(self));
room.addEventsToTimeline(matrixEvents, true);
room.addEventsToTimeline(matrixEvents, true, room.getLiveTimeline());
room.oldState.paginationToken = res.end;
if (res.chunk.length === 0) {
room.oldState.paginationToken = null;
@@ -2908,6 +2976,23 @@ MatrixClient.prototype.getFilter = function(userId, filterId, allowCached) {
});
};
/**
* Gets a bearer token from the Home Server that the user can
* present to a third party in order to prove their ownership
* of the Matrix account they are logged into.
* @return {module:client.Promise} Resolves: Token object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.getOpenIdToken = function() {
var path = utils.encodeUri("/user/$userId/openid/request_token", {
$userId: this.credentials.userId,
});
return this._http.authedRequest(
undefined, "POST", path, undefined, {}
);
};
// VoIP operations
// ===============
+24 -9
View File
@@ -21,6 +21,11 @@ limitations under the License.
var q = require("q");
var utils = require("./utils");
// we use our own implementation of setTimeout, so that if we get suspended in
// the middle of a /sync, we cancel the sync as soon as we awake, rather than
// waiting for the delay to elapse.
var callbacks = require("./realtime-callbacks");
/*
TODO:
- CS: complete register function (doing stages)
@@ -42,6 +47,11 @@ module.exports.PREFIX_UNSTABLE = "/_matrix/client/unstable";
*/
module.exports.PREFIX_IDENTITY_V1 = "/_matrix/identity/api/v1";
/**
* URI path for the media repo API
*/
module.exports.PREFIX_MEDIA_R0 = "/_matrix/media/r0";
/**
* Construct a MatrixHttpApi.
* @constructor
@@ -125,12 +135,12 @@ module.exports.MatrixHttpApi.prototype = {
cb(new Error('Timeout'));
};
xhr.timeout_timer = setTimeout(timeout_fn, 30000);
xhr.timeout_timer = callbacks.setTimeout(timeout_fn, 30000);
xhr.onreadystatechange = function() {
switch (xhr.readyState) {
case global.XMLHttpRequest.DONE:
clearTimeout(xhr.timeout_timer);
callbacks.clearTimeout(xhr.timeout_timer);
var err;
if (!xhr.responseText) {
err = new Error('No response body.');
@@ -152,10 +162,10 @@ module.exports.MatrixHttpApi.prototype = {
}
};
xhr.upload.addEventListener("progress", function(ev) {
clearTimeout(xhr.timeout_timer);
callbacks.clearTimeout(xhr.timeout_timer);
upload.loaded = ev.loaded;
upload.total = ev.total;
xhr.timeout_timer = setTimeout(timeout_fn, 30000);
xhr.timeout_timer = callbacks.setTimeout(timeout_fn, 30000);
defer.notify(ev);
});
url += "?access_token=" + encodeURIComponent(this.opts.accessToken);
@@ -178,9 +188,10 @@ module.exports.MatrixHttpApi.prototype = {
upload.request = this.opts.request({
uri: url,
qs: queryParams,
method: "POST"
method: "POST",
headers: {"Content-Type": file.type},
body: file.stream
}, requestCallback(defer, callback, this.opts.onlyData));
file.stream.pipe(this.opts.request);
}
this.uploads.push(upload);
@@ -450,9 +461,13 @@ module.exports.MatrixHttpApi.prototype = {
var timeoutId;
var timedOut = false;
var req;
if (localTimeoutMs) {
timeoutId = setTimeout(function() {
timeoutId = callbacks.setTimeout(function() {
timedOut = true;
if (req && req.abort) {
req.abort();
}
defer.reject(new module.exports.MatrixError({
error: "Locally timed out waiting for a response",
errcode: "ORG.MATRIX.JSSDK_TIMEOUT",
@@ -464,7 +479,7 @@ module.exports.MatrixHttpApi.prototype = {
var reqPromise = defer.promise;
try {
var req = this.opts.request(
req = this.opts.request(
{
uri: uri,
method: method,
@@ -477,7 +492,7 @@ module.exports.MatrixHttpApi.prototype = {
},
function(err, response, body) {
if (localTimeoutMs) {
clearTimeout(timeoutId);
callbacks.clearTimeout(timeoutId);
if (timedOut) {
return; // already rejected promise
}
+11
View File
@@ -42,6 +42,8 @@ function EventTimeline(roomId) {
// this is used by client.js
this._paginationRequests = {'b': null, 'f': null};
this._name = roomId + ":" + new Date().toISOString();
}
/**
@@ -290,6 +292,15 @@ EventTimeline.prototype.removeEvent = function(eventId) {
return null;
};
/**
* Return a string to identify this timeline, for debugging
*
* @return {string} name for this timeline
*/
EventTimeline.prototype.toString = function() {
return this._name;
};
/**
* The EventTimeline class
+48 -4
View File
@@ -239,6 +239,30 @@ RoomState.prototype.getUserIdsWithDisplayName = function(displayName) {
return this._displayNameToUserIds[displayName] || [];
};
/**
* Short-form for maySendEvent('m.room.message', userId)
* @param {string} userId The user ID of the user to test permission for
* @return {boolean} true if the given user ID should be permitted to send
* message events into the given room.
*/
RoomState.prototype.maySendMessage = function(userId) {
return this._maySendEventOfType('m.room.message', userId, false);
};
/**
* Returns true if the given user ID has permission to send a normal
* event of type `eventType` into this room.
* @param {string} type The type of event to test
* @param {string} userId The user ID of the user to test permission for
* @return {boolean} true if the given user ID should be permitted to send
* the given type of event into this room,
* according to the room's state.
*/
RoomState.prototype.maySendEvent = function(eventType, userId) {
return this._maySendEventOfType(eventType, userId, false);
};
/**
* Returns true if the given MatrixClient has permission to send a state
* event of type `stateEventType` into this room.
@@ -265,6 +289,22 @@ RoomState.prototype.mayClientSendStateEvent = function(stateEventType, cli) {
* according to the room's state.
*/
RoomState.prototype.maySendStateEvent = function(stateEventType, userId) {
return this._maySendEventOfType(stateEventType, userId, true);
};
/**
* Returns true if the given user ID has permission to send a normal or state
* event of type `eventType` into this room.
* @param {string} type The type of event to test
* @param {string} userId The user ID of the user to test permission for
* @param {boolean} state If true, tests if the user may send a state
event of this type. Otherwise tests whether
they may send a regular event.
* @return {boolean} true if the given user ID should be permitted to send
* the given type of event into this room,
* according to the room's state.
*/
RoomState.prototype._maySendEventOfType = function(eventType, userId, state) {
var member = this.getMember(userId);
if (!member || member.membership == 'leave') { return false; }
@@ -277,6 +317,7 @@ RoomState.prototype.maySendStateEvent = function(stateEventType, userId) {
var user_levels = [];
var state_default = 0;
var events_default = 0;
if (power_levels_event) {
power_levels = power_levels_event.getContent();
events_levels = power_levels.events || {};
@@ -289,13 +330,16 @@ RoomState.prototype.maySendStateEvent = function(stateEventType, userId) {
} else {
state_default = 50;
}
if (power_levels.events_default !== undefined) {
events_default = power_levels.events_default;
}
}
var state_event_level = state_default;
if (events_levels[stateEventType] !== undefined) {
state_event_level = events_levels[stateEventType];
var required_level = state ? state_default : events_default;
if (events_levels[eventType] !== undefined) {
required_level = events_levels[eventType];
}
return member.powerLevel >= state_event_level;
return member.powerLevel >= required_level;
};
/**
+123 -90
View File
@@ -26,6 +26,18 @@ var utils = require("../utils");
var ContentRepo = require("../content-repo");
var EventTimeline = require("./event-timeline");
// var DEBUG = false;
var DEBUG = true;
if (DEBUG) {
// using bind means that we get to keep useful line numbers in the console
var debuglog = console.log.bind(console);
} else {
var debuglog = function() {};
}
function synthesizeReceipt(userId, event, receiptType) {
// console.log("synthesizing receipt for "+event.getId());
// This is really ugly because JS has no way to express an object literal
@@ -416,8 +428,8 @@ Room.prototype.addTimeline = function() {
* (oldest) instead of the end (newest) of the timeline. If true, the oldest
* event will be the <b>last</b> element of 'events'.
*
* @param {module:models/event-timeline~EventTimeline=} timeline timeline to
* add events to. If not given, events will be added to the live timeline
* @param {module:models/event-timeline~EventTimeline} timeline timeline to
* add events to.
*
* @param {string=} paginationToken token for the next batch of events
*
@@ -427,13 +439,16 @@ Room.prototype.addTimeline = function() {
Room.prototype.addEventsToTimeline = function(events, toStartOfTimeline,
timeline, paginationToken) {
if (!timeline) {
timeline = this._liveTimeline;
throw new Error(
"'timeline' not specified for Room.addEventsToTimeline"
);
}
if (!toStartOfTimeline && timeline == this._liveTimeline) {
// special treatment for live events
this._addLiveEvents(events);
return;
throw new Error(
"Room.addEventsToTimeline cannot be used for adding events to " +
"the live timeline - use Room.addLiveEvents instead"
);
}
var direction = toStartOfTimeline ? EventTimeline.BACKWARDS :
@@ -529,7 +544,7 @@ Room.prototype.addEventsToTimeline = function(events, toStartOfTimeline,
lastEventWasNew = false;
if (existingTimeline == timeline) {
console.log("Event " + eventId + " already in timeline " + timeline);
debuglog("Event " + eventId + " already in timeline " + timeline);
continue;
}
@@ -545,10 +560,10 @@ Room.prototype.addEventsToTimeline = function(events, toStartOfTimeline,
// that would happen, so I'm going to ignore it for now.
//
if (existingTimeline == neighbour) {
console.log("Event " + eventId + " in neighbouring timeline - " +
debuglog("Event " + eventId + " in neighbouring timeline - " +
"switching to " + existingTimeline);
} else {
console.log("Event " + eventId + " already in a different " +
debuglog("Event " + eventId + " already in a different " +
"timeline " + existingTimeline);
}
timeline = existingTimeline;
@@ -601,58 +616,92 @@ Room.prototype._addEventToTimeline = function(event, timeline, toStartOfTimeline
/**
* Add some events to the end of this room's live timeline. Will fire
* "Room.timeline" for each event added.
* Add an event to the end of this room's live timeline. Will fire
* "Room.timeline"..
*
* @param {MatrixEvent[]} events A list of events to add.
* @param {MatrixEvent} event Event to be added
* @param {string?} duplicateStrategy 'ignore' or 'replace'
* @fires module:client~MatrixClient#event:"Room.timeline"
* @private
*/
Room.prototype._addLiveEvents = function(events) {
for (var i = 0; i < events.length; i++) {
if (events[i].getType() === "m.room.redaction") {
var redactId = events[i].event.redacts;
Room.prototype._addLiveEvent = function(event, duplicateStrategy) {
if (event.getType() === "m.room.redaction") {
var redactId = event.event.redacts;
// if we know about this event, redact its contents now.
var redactedEvent = this.findEventById(redactId);
if (redactedEvent) {
redactedEvent.makeRedacted(events[i]);
this.emit("Room.redaction", events[i], this);
// if we know about this event, redact its contents now.
var redactedEvent = this.findEventById(redactId);
if (redactedEvent) {
redactedEvent.makeRedacted(event);
this.emit("Room.redaction", event, this);
// TODO: we stash user displaynames (among other things) in
// RoomMember objects which are then attached to other events
// (in the sender and target fields). We should get those
// RoomMember objects to update themselves when the events that
// they are based on are changed.
// TODO: we stash user displaynames (among other things) in
// RoomMember objects which are then attached to other events
// (in the sender and target fields). We should get those
// RoomMember objects to update themselves when the events that
// they are based on are changed.
}
// NB: We continue to add the redaction event to the timeline so
// clients can say "so and so redacted an event" if they wish to. Also
// this may be needed to trigger an update.
}
if (event.getUnsigned().transaction_id) {
var existingEvent = this._txnToEvent[event.getUnsigned().transaction_id];
if (existingEvent) {
// remote echo of an event we sent earlier
this._handleRemoteEcho(event, existingEvent);
return;
}
}
var timeline = this._eventIdToTimeline[event.getId()];
if (timeline) {
if (duplicateStrategy === "replace") {
debuglog("Room._addLiveEvent: replacing duplicate event " +
event.getId());
var tlEvents = timeline.getEvents();
for (var j = 0; j < tlEvents.length; j++) {
if (tlEvents[j].getId() === event.getId()) {
// still need to set the right metadata on this event
setEventMetadata(
event,
timeline.getState(EventTimeline.FORWARDS),
false
);
if (!tlEvents[j].encryptedType) {
tlEvents[j] = event;
}
// XXX: we need to fire an event when this happens.
break;
}
}
// NB: We continue to add the redaction event to the timeline so
// clients can say "so and so redacted an event" if they wish to. Also
// this may be needed to trigger an update.
} else {
debuglog("Room._addLiveEvent: ignoring duplicate event " +
event.getId());
}
return;
}
if (events[i].getUnsigned().transaction_id) {
var existingEvent = this._txnToEvent[events[i].getUnsigned().transaction_id];
if (existingEvent) {
// remote echo of an event we sent earlier
this._handleRemoteEcho(events[i], existingEvent);
continue;
}
}
// TODO: pass through filter to see if this should be added to the timeline.
this._addEventToTimeline(event, this._liveTimeline, false);
if (!this._eventIdToTimeline[events[i].getId()]) {
// TODO: pass through filter to see if this should be added to the timeline.
this._addEventToTimeline(events[i], this._liveTimeline, false);
}
// synthesize and inject implicit read receipts
// Done after adding the event because otherwise the app would get a read receipt
// pointing to an event that wasn't yet in the timeline
if (event.sender) {
this.addReceipt(synthesizeReceipt(
event.sender.userId, event, "m.read"
), true);
// synthesize and inject implicit read receipts
// Done after adding the event because otherwise the app would get a read receipt
// pointing to an event that wasn't yet in the timeline
if (events[i].sender) {
this.addReceipt(synthesizeReceipt(
events[i].sender.userId, events[i], "m.read"
), true);
}
// Any live events from a user could be taken as implicit
// presence information: evidence that they are currently active.
// ...except in a world where we use 'user.currentlyActive' to reduce
// presence spam, this isn't very useful - we'll get a transition when
// they are no longer currently active anyway. So don't bother to
// reset the lastActiveAgo and lastPresenceTs from the RoomState's user.
}
};
@@ -855,20 +904,36 @@ Room.prototype.updatePendingEvent = function(event, newStatus, newEventId) {
* Add some events to this room. This can include state events, message
* events and typing notifications. These events are treated as "live" so
* they will go to the end of the timeline.
*
* @param {MatrixEvent[]} events A list of events to add.
*
* @param {string} duplicateStrategy Optional. Applies to events in the
* timeline only. If this is not specified, no duplicate suppression is
* performed (this improves performance). If this is 'replace' then if a
* duplicate is encountered, the event passed to this function will replace the
* existing event in the timeline. If this is 'ignore', then the event passed to
* timeline only. If this is 'replace' then if a duplicate is encountered, the
* event passed to this function will replace the existing event in the
* timeline. If this is not specified, or is 'ignore', then the event passed to
* this function will be ignored entirely, preserving the existing event in the
* timeline. Events are identical based on their event ID <b>only</b>.
*
* @throws If <code>duplicateStrategy</code> is not falsey, 'replace' or 'ignore'.
*/
Room.prototype.addEvents = function(events, duplicateStrategy) {
Room.prototype.addLiveEvents = function(events, duplicateStrategy) {
if (duplicateStrategy && ["replace", "ignore"].indexOf(duplicateStrategy) === -1) {
throw new Error("duplicateStrategy MUST be either 'replace' or 'ignore'");
}
// sanity check that the live timeline is still live
if (this._liveTimeline.getPaginationToken(EventTimeline.FORWARDS)) {
throw new Error(
"live timeline is no longer live - it has a pagination token (" +
this._liveTimeline.getPaginationToken(EventTimeline.FORWARDS) + ")"
);
}
if (this._liveTimeline.getNeighbouringTimeline(EventTimeline.FORWARDS)) {
throw new Error(
"live timeline is no longer live - it has a neighbouring timeline"
);
}
for (var i = 0; i < events.length; i++) {
if (events[i].getType() === "m.typing") {
this.currentState.setTypingEvent(events[i]);
@@ -879,41 +944,9 @@ Room.prototype.addEvents = function(events, duplicateStrategy) {
// N.B. account_data is added directly by /sync to avoid
// having to maintain an event.isAccountData() here
else {
var timeline = this._eventIdToTimeline[events[i].getId()];
if (timeline && duplicateStrategy) {
// is there a duplicate?
var shouldIgnore = false;
var tlEvents = timeline.getEvents();
for (var j = 0; j < tlEvents.length; j++) {
if (tlEvents[j].getId() === events[i].getId()) {
if (duplicateStrategy === "replace") {
// still need to set the right metadata on this event
setEventMetadata(
events[i],
timeline.getState(EventTimeline.FORWARDS),
false
);
if (!tlEvents[j].encryptedType) {
tlEvents[j] = events[i];
}
// skip the insert so we don't add this event twice.
// Don't break in case we replace multiple events.
shouldIgnore = true;
}
else if (duplicateStrategy === "ignore") {
shouldIgnore = true;
break; // stop searching, we're skipping the insert
}
}
}
if (shouldIgnore) {
continue; // skip the insertion of this event.
}
}
// TODO: We should have a filter to say "only add state event
// types X Y Z to the timeline".
this._addLiveEvents([events[i]]);
this._addLiveEvent(events[i], duplicateStrategy);
}
}
};
@@ -1444,14 +1477,14 @@ module.exports = Room;
*/
/**
* Fires wheneer the live timeline in a room is reset.
* Fires whenever 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"
* @event module:client~MatrixClient#"Room.timelineReset"
* @param {Room} room The room whose live timeline was reset.
*/
+23 -3
View File
@@ -30,7 +30,12 @@ limitations under the License.
* @prop {string} displayName The 'displayname' of the user if known.
* @prop {string} avatarUrl The 'avatar_url' of the user if known.
* @prop {string} presence The presence enum if known.
* @prop {Number} lastActiveAgo The last time the user performed some action in ms.
* @prop {Number} lastActiveAgo The time elapsed in ms since the user interacted
* proactively with the server, or we saw a message from the user
* @prop {Number} lastPresenceTs Timestamp (ms since the epoch) for when we last
* received presence data for this user. We can subtract
* lastActiveAgo from this to approximate an absolute value for
* when a user was last active.
* @prop {Boolean} currentlyActive Whether we should consider lastActiveAgo to be
* an approximation and that the user should be seen as active 'now'
* @prop {Object} events The events describing this user.
@@ -42,6 +47,7 @@ function User(userId) {
this.displayName = userId;
this.avatarUrl = null;
this.lastActiveAgo = 0;
this.lastPresenceTs = 0;
this.currentlyActive = false;
this.events = {
presence: null,
@@ -79,9 +85,14 @@ User.prototype.setPresenceEvent = function(event) {
}
this.presence = event.getContent().presence;
this.displayName = event.getContent().displayname;
this.avatarUrl = event.getContent().avatar_url;
if (event.getContent().displayname) {
this.displayName = event.getContent().displayname;
}
if (event.getContent().avatarUrl) {
this.avatarUrl = event.getContent().avatar_url;
}
this.lastActiveAgo = event.getContent().last_active_ago;
this.lastPresenceTs = Date.now();
this.currentlyActive = event.getContent().currently_active;
if (eventsToFire.length > 0) {
@@ -136,6 +147,15 @@ User.prototype.getLastModifiedTime = function() {
return this._modified;
};
/**
* Get the absolute timestamp when this User was last known active on the server.
* It is *NOT* accurate if this.currentlyActive is true.
* @return {number} The timestamp
*/
User.prototype.getLastActiveTs = function() {
return this.lastPresenceTs - this.lastActiveAgo;
};
/**
* The User class.
*/
+3 -1
View File
@@ -125,7 +125,9 @@ function PushProcessor(client) {
var room = client.getRoom(ev.room_id);
if (!room || !room.currentState || !room.currentState.members) { return false; }
var memberCount = Object.keys(room.currentState.members).length;
var memberCount = Object.keys(room.currentState.members).filter(function(m) {
return room.currentState.members[m].membership == 'join';
}).length;
var m = cond.is.match(/^([=<>]*)([0-9]*)$/);
if (!m) { return false; }
+203
View File
@@ -0,0 +1,203 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/* A re-implementation of the javascript callback functions (setTimeout,
* clearTimeout; setInterval and clearInterval are not yet implemented) which
* try to improve handling of large clock jumps (as seen when
* suspending/resuming the system).
*
* In particular, if a timeout would have fired while the system was suspended,
* it will instead fire as soon as possible after resume.
*/
"use strict";
// we schedule a callback at least this often, to check if we've missed out on
// some wall-clock time due to being suspended.
var TIMER_CHECK_PERIOD_MS = 1000;
// counter, for making up ids to return from setTimeout
var _count = 0;
// the key for our callback with the real global.setTimeout
var _realCallbackKey;
// a sorted list of the callbacks to be run.
// each is an object with keys [runAt, func, params, key].
var _callbackList = [];
// var debuglog = console.log.bind(console);
var debuglog = function() {};
/**
* Replace the function used by this module to get the current time.
*
* Intended for use by the unit tests.
*
* @param {function} f function which should return a millisecond counter
*
* @internal
*/
module.exports.setNow = function(f) {
_now = f || Date.now;
};
var _now = Date.now;
/**
* reimplementation of window.setTimeout, which will call the callback if
* the wallclock time goes past the deadline.
*
* @param {function} func callback to be called after a delay
* @param {Number} delayMs number of milliseconds to delay by
*
* @return {Number} an identifier for this callback, which may be passed into
* clearTimeout later.
*/
module.exports.setTimeout = function(func, delayMs) {
delayMs = delayMs || 0;
if (delayMs < 0) {
delayMs = 0;
}
var params = Array.prototype.slice.call(arguments, 2);
var runAt = _now() + delayMs;
var key = _count++;
debuglog("setTimeout: scheduling cb", key, "at", runAt,
"(delay", delayMs, ")");
var data = {
runAt: runAt,
func: func,
params: params,
key: key,
};
// figure out where it goes in the list
var idx = binarySearch(
_callbackList, function(el) {
return el.runAt - runAt;
}
);
_callbackList.splice(idx, 0, data);
_scheduleRealCallback();
return key;
};
/**
* reimplementation of window.clearTimeout, which mirrors setTimeout
*
* @param {Number} key result from an earlier setTimeout call
*/
module.exports.clearTimeout = function(key) {
if (_callbackList.length === 0) {
return;
}
// remove the element from the list
var i;
for (i = 0; i < _callbackList.length; i++) {
var cb = _callbackList[i];
if (cb.key == key) {
_callbackList.splice(i, 1);
break;
}
}
// iff it was the first one in the list, reschedule our callback.
if (i === 0) {
_scheduleRealCallback();
}
};
// use the real global.setTimeout to schedule a callback to _runCallbacks.
function _scheduleRealCallback() {
if (_realCallbackKey) {
global.clearTimeout(_realCallbackKey);
}
var first = _callbackList[0];
if (!first) {
debuglog("_scheduleRealCallback: no more callbacks, not rescheduling");
return;
}
var now = _now();
var delayMs = Math.min(first.runAt - now, TIMER_CHECK_PERIOD_MS);
debuglog("_scheduleRealCallback: now:", now, "delay:", delayMs);
_realCallbackKey = global.setTimeout(_runCallbacks, delayMs);
}
function _runCallbacks() {
var cb;
var now = _now();
debuglog("_runCallbacks: now:", now);
// get the list of things to call
var callbacksToRun = [];
while (true) {
var first = _callbackList[0];
if (!first || first.runAt > now) {
break;
}
cb = _callbackList.shift();
debuglog("_runCallbacks: popping", cb.key);
callbacksToRun.push(cb);
}
// reschedule the real callback before running our functions, to
// keep the codepaths the same whether or not our functions
// register their own setTimeouts.
_scheduleRealCallback();
for (var i = 0; i < callbacksToRun.length; i++) {
cb = callbacksToRun[i];
try {
cb.func.apply(null, cb.params);
} catch (e) {
console.error("Uncaught exception in callback function",
e.stack || e);
}
}
}
/* search in a sorted array.
*
* returns the index of the last element for which func returns
* greater than zero, or array.length if no such element exists.
*/
function binarySearch(array, func) {
// min is inclusive, max exclusive.
var min = 0,
max = array.length;
while (min < max) {
var mid = (min + max) >> 1;
var res = func(array[mid]);
if (res > 0) {
// the element at 'mid' is too big; set it as the new max.
max = mid;
} else {
// the element at 'mid' is too small. 'min' is inclusive, so +1.
min = mid + 1;
}
}
// presumably, min==max now.
return min;
}
+6 -5
View File
@@ -96,11 +96,12 @@ module.exports.MatrixInMemoryStore.prototype = {
}
var user = this.users[member.userId] || new User(member.userId);
user.setDisplayName(member.name);
var rawUrl = (
member.events.member ? member.events.member.getContent().avatar_url : null
);
user.setAvatarUrl(rawUrl);
if (member.name) {
user.setDisplayName(member.name);
}
if (member.events.member && member.events.member.getContent().avatar_url) {
user.setAvatarUrl(member.events.member.getContent().avatar_url);
}
this.users[user.userId] = user;
},
+2 -2
View File
@@ -373,7 +373,7 @@ WebStorageStore.prototype.scrollback = function(room, limit) {
);
room.addEventsToTimeline(utils.map(scrollback, function(e) {
return new MatrixEvent(e);
}), true);
}), true, room.getLiveTimeline());
this._tokens[room.storageToken] = {
earliestIndex: earliestIndex
@@ -594,7 +594,7 @@ function loadRoom(store, roomId, numEvents, tokenArray) {
index--;
}
// add events backwards to diverge old state correctly.
room.addEventsToTimeline(recentEvents.reverse(), true);
room.addEventsToTimeline(recentEvents.reverse(), true, room.getLiveTimeline());
room.oldState.paginationToken = currentStateMap.pagination_token;
// set the token data to let us know which index this room instance is at
// for scrollback.
+27 -11
View File
@@ -262,7 +262,8 @@ SyncApi.prototype.peek = function(roomId) {
// will overwrite the pagination token, so make sure it overwrites
// it with the right thing.
peekRoom.addEventsToTimeline(messages.reverse(), true,
undefined, response.messages.start);
peekRoom.getLiveTimeline(),
response.messages.start);
client.store.storeRoom(peekRoom);
client.emit("Room", peekRoom);
@@ -328,7 +329,7 @@ SyncApi.prototype._peekPoll = function(roomId, token) {
return e.room_id === roomId;
}).map(self.client.getEventMapper());
var room = self.client.getRoom(roomId);
room.addEvents(events);
room.addLiveEvents(events);
self._peekPoll(roomId, res.end);
}, function(err) {
console.error("[%s] Peek poll failed: %s", roomId, err);
@@ -351,7 +352,9 @@ SyncApi.prototype.getSyncState = function() {
* Main entry point
*/
SyncApi.prototype.sync = function() {
debuglog("SyncApi.sync");
debuglog("SyncApi.sync: starting with sync token " +
this.client.store.getSyncToken());
var client = this.client;
var self = this;
@@ -417,6 +420,10 @@ SyncApi.prototype.stop = function() {
}
this._running = false;
if (this._currentSyncRequest) { this._currentSyncRequest.abort(); }
if (this._keepAliveTimer) {
clearTimeout(this._keepAliveTimer);
this._keepAliveTimer = null;
}
};
/**
@@ -459,9 +466,17 @@ SyncApi.prototype._sync = function(syncOptions) {
var qps = {
filter: filterId,
timeout: this.opts.pollTimeout,
since: syncToken || undefined // do not send 'null'
};
if (syncToken) {
qps.since = syncToken;
} else {
// use a cachebuster for initialsyncs, to make sure that
// we don't get a stale sync
// (https://github.com/vector-im/vector-web/issues/1354)
qps._cacheBuster = Date.now();
}
if (self._syncConnectionLost) {
// we think the connection is dead. If it comes back up, we won't know
// about it till /sync returns. If the timeout= is high, this could
@@ -703,7 +718,7 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) {
// XXX: should we be adding ephemeralEvents to the timeline?
// It feels like that for symmetry with room.addAccountData()
// there should be a room.addEphemeralEvents() or similar.
room.addEvents(ephemeralEvents);
room.addLiveEvents(ephemeralEvents);
// we deliberately don't add accountData to the timeline
room.addAccountData(accountDataEvents);
@@ -821,16 +836,17 @@ SyncApi.prototype._getOrCreateFilter = function(filterName, filter) {
promise = client.getFilter(client.credentials.userId,
filterId, true
).then(function(existingFilter) {
var oldStr = JSON.stringify(existingFilter.getDefinition());
var newStr = JSON.stringify(filter.getDefinition());
var oldDef = existingFilter.getDefinition();
var newDef = filter.getDefinition();
if (oldStr == newStr) {
if (utils.deepCompare(oldDef, newDef)) {
// super, just use that.
debuglog("Using existing filter ID %s: %s", filterId, oldStr);
debuglog("Using existing filter ID %s: %s", filterId,
JSON.stringify(oldDef));
return q(filterId);
}
debuglog("Existing filter ID %s: %s; new filter: %s",
filterId, oldStr, newStr);
filterId, JSON.stringify(oldDef), JSON.stringify(newDef));
return;
});
}
@@ -978,7 +994,7 @@ SyncApi.prototype._processRoomEvents = function(room, stateEventList,
// execute the timeline events, this will begin to diverge the current state
// if the timeline has any state events in it.
room.addEventsToTimeline(timelineEventList);
room.addLiveEvents(timelineEventList);
};
/**
+25 -3
View File
@@ -30,6 +30,13 @@ var DEBUG = false;
*/
var debuglog = DEBUG ? console.log.bind(console) : function() {};
/**
* the number of times we ask the server for more events before giving up
*
* @private
*/
var DEFAULT_PAGINATE_LOOP_LIMIT = 5;
/**
* Construct a TimelineWindow.
*
@@ -179,10 +186,14 @@ TimelineWindow.prototype.canPaginate = function(direction) {
* even if there are fewer than 'size' of them, as we will just return those
* we already know about.)
*
* @param {number} [requestLimit = 5] limit for the number of API requests we
* should make.
*
* @return {module:client.Promise} Resolves to a boolean which is true if more events
* were successfully retrieved.
*/
TimelineWindow.prototype.paginate = function(direction, size, makeRequest) {
TimelineWindow.prototype.paginate = function(direction, size, makeRequest,
requestLimit) {
// Either wind back the message cap (if there are enough events in the
// timeline to do so), or fire off a pagination request.
@@ -190,6 +201,10 @@ TimelineWindow.prototype.paginate = function(direction, size, makeRequest) {
makeRequest = true;
}
if (requestLimit === undefined) {
requestLimit = DEFAULT_PAGINATE_LOOP_LIMIT;
}
var tl;
if (direction == EventTimeline.BACKWARDS) {
tl = this._start;
@@ -224,7 +239,9 @@ TimelineWindow.prototype.paginate = function(direction, size, makeRequest) {
return q(true);
}
if (!makeRequest) {
if (!makeRequest || requestLimit === 0) {
// todo: should we return something different to indicate that there
// might be more events out there, but we haven't found them yet?
return q(false);
}
@@ -256,7 +273,12 @@ TimelineWindow.prototype.paginate = function(direction, size, makeRequest) {
// token. In particular, we want to know if we've actually hit the
// start of the timeline, or if we just happened to know about all of
// the events thanks to https://matrix.org/jira/browse/SYN-645.
return self.paginate(direction, size, true);
//
// On the other hand, we necessarily want to wait forever for the
// server to make its mind up about whether there are other events,
// because it gives a bad user experience
// (https://github.com/vector-im/vector-web/issues/1204).
return self.paginate(direction, size, true, requestLimit - 1);
});
tl.pendingPaginate = prom;
return prom;
+86
View File
@@ -246,6 +246,92 @@ module.exports.deepCopy = function(obj) {
return JSON.parse(JSON.stringify(obj));
};
/**
* Compare two objects for equality. The objects MUST NOT have circular references.
*
* @param {Object} x The first object to compare.
* @param {Object} y The second object to compare.
*
* @return {boolean} true if the two objects are equal
*/
var deepCompare = module.exports.deepCompare = function(x, y) {
// Inspired by
// http://stackoverflow.com/questions/1068834/object-comparison-in-javascript#1144249
// Compare primitives and functions.
// Also check if both arguments link to the same object.
if (x === y) {
return true;
}
if (typeof x !== typeof y) {
return false;
}
// special-case NaN (since NaN !== NaN)
if (typeof x === 'number' && isNaN(x) && isNaN(y)) {
return true;
}
// special-case null (since typeof null == 'object', but null.constructor
// throws)
if (x === null || y === null) {
return x === y;
}
// everything else is either an unequal primitive, or an object
if (!(x instanceof Object)) {
return false;
}
// check they are the same type of object
if (x.constructor !== y.constructor || x.prototype !== y.prototype) {
return false;
}
// special-casing for some special types of object
if (x instanceof RegExp || x instanceof Date) {
return x.toString() === y.toString();
}
// the object algorithm works for Array, but it's sub-optimal.
if (x instanceof Array) {
if (x.length !== y.length) {
return false;
}
for (var i = 0; i < x.length; i++) {
if (!deepCompare(x[i], y[i])) {
return false;
}
}
} else {
// disable jshint "The body of a for in should be wrapped in an if
// statement"
/* jshint -W089 */
// check that all of y's direct keys are in x
var p;
for (p in y) {
if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) {
return false;
}
}
// finally, compare each of x's keys with y
for (p in y) {
if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) {
return false;
}
if (!deepCompare(x[p], y[p])) {
return false;
}
}
}
/* jshint +W089 */
return true;
};
/**
* Run polyfills to add Array.map and Array.filter if they are missing.
+3 -3
View File
@@ -1,11 +1,11 @@
{
"name": "matrix-js-sdk",
"version": "0.5.0",
"version": "0.5.3",
"description": "Matrix Client-Server SDK for Javascript",
"main": "index.js",
"scripts": {
"test": "istanbul cover --report cobertura --config .istanbul.yml -i \"lib/**/*.js\" jasmine-node -- spec --verbose --junitreport --forceexit --captureExceptions",
"check": "jasmine-node spec --verbose --junitreport --forceexit --captureExceptions",
"test": "istanbul cover --report cobertura --config .istanbul.yml -i \"lib/**/*.js\" jasmine-node -- spec --verbose --junitreport --captureExceptions",
"check": "jasmine-node spec --verbose --junitreport --captureExceptions",
"gendoc": "jsdoc -r lib -P package.json -R README.md -d .jsdoc",
"build": "jshint -c .jshint lib/ && browserify browser-index.js -o dist/browser-matrix-dev.js --ignore-missing",
"watch": "watchify browser-index.js -o dist/browser-matrix-dev.js -v",
+14 -14
View File
@@ -3,7 +3,7 @@
# Script to perform a release of matrix-js-sdk. Performs the steps documented
# in RELEASING.md
#
# Requires githib-changelog-generator; to install, do
# Requires github-changelog-generator; to install, do
# pip install git+https://github.com/matrix-org/github-changelog-generator.git
set -e
@@ -47,21 +47,19 @@ if [ $# -ne 1 ]; then
exit 1
fi
tag="$1"
case "$tag" in
v*) ;;
*)
echo 2>&1 "Tag $tag must start with v"
exit 1
;;
esac
# strip leading 'v' to get release
release="${tag#v}"
# ignore leading v on release
release="${1#v}"
tag="v${release}"
rel_branch="release-$tag"
if [ -z "$skip_changelog" ]; then
if ! command -v update_changelog >/dev/null 2>&1; then
echo "release.sh requires github-changelog-generator. Try:" >&2
echo " pip install git+https://github.com/matrix-org/github-changelog-generator.git" >&2
exit 1
fi
fi
# we might already be on the release branch, in which case, yay
if [ $(git symbolic-ref --short HEAD) != "$rel_branch" ]; then
echo "Creating release branch"
@@ -79,6 +77,8 @@ if [ -z "$skip_changelog" ]; then
fi
fi
set -x
# Bump package.json, build the dist, and tag
echo "npm version"
npm version "$release"
@@ -24,6 +24,7 @@ describe("MatrixClient events", function() {
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
client.stopClient();
});
describe("emissions", function() {
@@ -97,6 +97,7 @@ function startClient(httpBackend, client) {
describe("getEventTimeline support", function() {
var httpBackend;
var client;
beforeEach(function() {
utils.beforeEach(this);
@@ -104,8 +105,14 @@ describe("getEventTimeline support", function() {
sdk.request(httpBackend.requestFn);
});
afterEach(function() {
if (client) {
client.stopClient();
}
});
it("timeline support must be enabled to work", function(done) {
var client = sdk.createClient({
client = sdk.createClient({
baseUrl: baseUrl,
userId: userId,
accessToken: accessToken,
@@ -120,7 +127,7 @@ describe("getEventTimeline support", function() {
});
it("timeline support works when enabled", function(done) {
var client = sdk.createClient({
client = sdk.createClient({
baseUrl: baseUrl,
userId: userId,
accessToken: accessToken,
@@ -141,7 +148,7 @@ describe("getEventTimeline support", function() {
it("scrollback should be able to scroll back to before a gappy /sync",
function(done) {
// need a client with timelineSupport disabled to make this work
var client = sdk.createClient({
client = sdk.createClient({
baseUrl: baseUrl,
userId: userId,
accessToken: accessToken,
@@ -229,6 +236,7 @@ describe("MatrixClient event timelines", function() {
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
client.stopClient();
});
describe("getEventTimeline", function() {
+1 -1
View File
@@ -34,7 +34,7 @@ describe("MatrixClient", function() {
it("should no-op if you've already joined a room", function() {
var roomId = "!foo:bar";
var room = new Room(roomId);
room.addEvents([
room.addLiveEvents([
utils.mkMembership({
user: userId, room: roomId, mship: "join", event: true
})
+4
View File
@@ -73,6 +73,10 @@ describe("MatrixClient opts", function() {
});
});
afterEach(function() {
client.stopClient();
});
it("should be able to send messages", function(done) {
var eventId = "$flibble:wibble";
httpBackend.when("PUT", "/txn1").respond(200, {
+10 -11
View File
@@ -126,6 +126,7 @@ describe("MatrixClient room timelines", function() {
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
client.stopClient();
});
describe("local echo events", function() {
@@ -391,16 +392,16 @@ describe("MatrixClient room timelines", function() {
});
httpBackend.flush("/messages", 1);
httpBackend.flush("/sync", 1).done(function() {
httpBackend.flush("/sync", 1).then(function() {
expect(index).toEqual(2);
expect(room.timeline[room.timeline.length - 1].event).toEqual(
expect(room.timeline.length).toEqual(3);
expect(room.timeline[2].event).toEqual(
eventData[1]
);
expect(room.timeline[room.timeline.length - 2].event).toEqual(
expect(room.timeline[1].event).toEqual(
eventData[0]
);
done();
});
}).catch(utils.failTest).done(done);
});
httpBackend.flush("/sync", 1);
});
@@ -419,13 +420,12 @@ describe("MatrixClient room timelines", function() {
client.on("sync", function(state) {
if (state !== "PREPARED") { return; }
var room = client.getRoom(roomId);
httpBackend.flush("/sync", 1).done(function() {
httpBackend.flush("/sync", 1).then(function() {
var preNameEvent = room.timeline[room.timeline.length - 3];
var postNameEvent = room.timeline[room.timeline.length - 1];
expect(preNameEvent.sender.name).toEqual(userName);
expect(postNameEvent.sender.name).toEqual("New Name");
done();
});
}).catch(utils.failTest).done(done);
});
httpBackend.flush("/sync", 1);
});
@@ -487,7 +487,7 @@ describe("MatrixClient room timelines", function() {
client.on("sync", function(state) {
if (state !== "PREPARED") { return; }
var room = client.getRoom(roomId);
httpBackend.flush("/sync", 1).done(function() {
httpBackend.flush("/sync", 1).then(function() {
expect(room.currentState.getMembers().length).toEqual(4);
expect(room.currentState.getMember(userC).name).toEqual("C");
expect(room.currentState.getMember(userC).membership).toEqual(
@@ -497,8 +497,7 @@ describe("MatrixClient room timelines", function() {
expect(room.currentState.getMember(userD).membership).toEqual(
"invite"
);
done();
});
}).catch(utils.failTest).done(done);
});
httpBackend.flush("/sync", 1);
});
+1
View File
@@ -32,6 +32,7 @@ describe("MatrixClient syncing", function() {
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
client.stopClient();
});
describe("startClient", function() {
+14
View File
@@ -15,6 +15,20 @@ function HttpBackend() {
realReq.callback = callback;
console.log("HTTP backend received request: %s %s", opts.method, opts.uri);
self.requests.push(realReq);
var abort = function() {
var idx = self.requests.indexOf(realReq);
if (idx >= 0) {
console.log("Aborting HTTP request: %s %s", opts.method,
opts.uri);
self.requests.splice(idx, 1);
realReq.callback("aborted");
}
};
return {
abort: abort
};
};
}
HttpBackend.prototype = {
+179
View File
@@ -0,0 +1,179 @@
"use strict";
var callbacks = require("../../lib/realtime-callbacks");
var test_utils = require("../test-utils.js");
describe("realtime-callbacks", function() {
var clock = jasmine.Clock;
var fakeDate;
function tick(millis) {
// make sure we tick the fakedate first, otherwise nothing will happen!
fakeDate += millis;
clock.tick(millis);
}
beforeEach(function() {
test_utils.beforeEach(this);
clock.useMock();
fakeDate = Date.now();
callbacks.setNow(function() { return fakeDate; });
});
afterEach(function() {
callbacks.setNow();
});
describe("setTimeout", function() {
it("should call the callback after the timeout", function() {
var callback = jasmine.createSpy();
callbacks.setTimeout(callback, 100);
expect(callback).not.toHaveBeenCalled();
tick(100);
expect(callback).toHaveBeenCalled();
});
it("should default to a zero timeout", function() {
var callback = jasmine.createSpy();
callbacks.setTimeout(callback);
expect(callback).not.toHaveBeenCalled();
tick(0);
expect(callback).toHaveBeenCalled();
});
it("should pass any parameters to the callback", function() {
var callback = jasmine.createSpy();
callbacks.setTimeout(callback, 0, "a", "b", "c");
tick(0);
expect(callback).toHaveBeenCalledWith("a", "b", "c");
});
it("should set 'this' to the global object", function() {
var callback = jasmine.createSpy();
callback.andCallFake(function() {
expect(this).toBe(global);
expect(this.console).toBeDefined();
});
callbacks.setTimeout(callback);
tick(0);
expect(callback).toHaveBeenCalled();
});
it("should handle timeouts of several seconds", function() {
var callback = jasmine.createSpy();
callbacks.setTimeout(callback, 2000);
expect(callback).not.toHaveBeenCalled();
for (var i = 0; i < 4; i++) {
tick(500);
}
expect(callback).toHaveBeenCalled();
});
it("should call multiple callbacks in the right order", function() {
var callback1 = jasmine.createSpy("callback1");
var callback2 = jasmine.createSpy("callback2");
var callback3 = jasmine.createSpy("callback3");
callbacks.setTimeout(callback2, 200);
callbacks.setTimeout(callback1, 100);
callbacks.setTimeout(callback3, 300);
expect(callback1).not.toHaveBeenCalled();
expect(callback2).not.toHaveBeenCalled();
expect(callback3).not.toHaveBeenCalled();
tick(100);
expect(callback1).toHaveBeenCalled();
expect(callback2).not.toHaveBeenCalled();
expect(callback3).not.toHaveBeenCalled();
tick(100);
expect(callback1).toHaveBeenCalled();
expect(callback2).toHaveBeenCalled();
expect(callback3).not.toHaveBeenCalled();
tick(100);
expect(callback1).toHaveBeenCalled();
expect(callback2).toHaveBeenCalled();
expect(callback3).toHaveBeenCalled();
});
it("should treat -ve timeouts the same as a zero timeout", function() {
var callback1 = jasmine.createSpy("callback1");
var callback2 = jasmine.createSpy("callback2");
// check that cb1 is called before cb2
callback1.andCallFake(function() {
expect(callback2).not.toHaveBeenCalled();
});
callbacks.setTimeout(callback1);
callbacks.setTimeout(callback2, -100);
expect(callback1).not.toHaveBeenCalled();
expect(callback2).not.toHaveBeenCalled();
tick(0);
expect(callback1).toHaveBeenCalled();
expect(callback2).toHaveBeenCalled();
});
it("should not get confused by chained calls", function() {
var callback2 = jasmine.createSpy("callback2");
var callback1 = jasmine.createSpy("callback1");
callback1.andCallFake(function() {
callbacks.setTimeout(callback2, 0);
expect(callback2).not.toHaveBeenCalled();
});
callbacks.setTimeout(callback1);
expect(callback1).not.toHaveBeenCalled();
expect(callback2).not.toHaveBeenCalled();
tick(0);
expect(callback1).toHaveBeenCalled();
expect(callback2).toHaveBeenCalled();
});
it("should be immune to exceptions", function() {
var callback1 = jasmine.createSpy("callback1");
callback1.andCallFake(function() {
throw new Error("prepare to die");
});
var callback2 = jasmine.createSpy("callback2");
callbacks.setTimeout(callback1, 0);
callbacks.setTimeout(callback2, 0);
expect(callback1).not.toHaveBeenCalled();
expect(callback2).not.toHaveBeenCalled();
tick(0);
expect(callback1).toHaveBeenCalled();
expect(callback2).toHaveBeenCalled();
});
});
describe("cancelTimeout", function() {
it("should cancel a pending timeout", function() {
var callback = jasmine.createSpy();
var k = callbacks.setTimeout(callback);
callbacks.clearTimeout(k);
tick(0);
expect(callback).not.toHaveBeenCalled();
});
it("should not affect sooner timeouts", function() {
var callback1 = jasmine.createSpy("callback1");
var callback2 = jasmine.createSpy("callback2");
callbacks.setTimeout(callback1, 100);
var k = callbacks.setTimeout(callback2, 200);
callbacks.clearTimeout(k);
tick(100);
expect(callback1).toHaveBeenCalled();
expect(callback2).not.toHaveBeenCalled();
tick(150);
expect(callback2).not.toHaveBeenCalled();
});
});
});
+70
View File
@@ -362,4 +362,74 @@ describe("RoomState", function() {
expect(state.maySendStateEvent('m.room.other_thing', userB)).toEqual(false);
});
});
describe("maySendEvent", function() {
it("should say non-joined members may not send events",
function() {
expect(state.maySendEvent(
'm.room.message', "@nobody:nowhere"
)).toEqual(false);
expect(state.maySendMessage("@nobody:nowhere")).toEqual(false);
});
it("should say any member may send events with no power level event",
function() {
expect(state.maySendEvent('m.room.message', userA)).toEqual(true);
expect(state.maySendMessage(userA)).toEqual(true);
});
it("should obey events_default",
function() {
var powerLevelEvent = {
type: "m.room.power_levels", room: roomId, user: userA, event: true,
content: {
users_default: 10,
state_default: 30,
events_default: 25,
users: {
}
}
};
powerLevelEvent.content.users[userA] = 26;
powerLevelEvent.content.users[userB] = 24;
state.setStateEvents([utils.mkEvent(powerLevelEvent)]);
expect(state.maySendEvent('m.room.message', userA)).toEqual(true);
expect(state.maySendEvent('m.room.message', userB)).toEqual(false);
expect(state.maySendMessage(userA)).toEqual(true);
expect(state.maySendMessage(userB)).toEqual(false);
});
it("should honour explicit event power levels in the power_levels event",
function() {
var powerLevelEvent = {
type: "m.room.power_levels", room: roomId, user: userA, event: true,
content: {
events: {
"m.room.other_thing": 33
},
users_default: 10,
state_default: 50,
events_default: 25,
users: {
}
}
};
powerLevelEvent.content.users[userA] = 40;
powerLevelEvent.content.users[userB] = 30;
state.setStateEvents([utils.mkEvent(powerLevelEvent)]);
expect(state.maySendEvent('m.room.message', userA)).toEqual(true);
expect(state.maySendEvent('m.room.message', userB)).toEqual(true);
expect(state.maySendMessage(userA)).toEqual(true);
expect(state.maySendMessage(userB)).toEqual(true);
expect(state.maySendEvent('m.room.other_thing', userA)).toEqual(true);
expect(state.maySendEvent('m.room.other_thing', userB)).toEqual(false);
});
});
});
+168 -168
View File
@@ -82,7 +82,7 @@ describe("Room", function() {
});
});
describe("addEvents", function() {
describe("addLiveEvents", function() {
var events = [
utils.mkMessage({
room: roomId, user: userA, msg: "changing room name", event: true
@@ -100,12 +100,12 @@ describe("Room", function() {
user_ids: [userA]
}
});
room.addEvents([typing]);
room.addLiveEvents([typing]);
expect(room.currentState.setTypingEvent).toHaveBeenCalledWith(typing);
});
it("should throw if duplicateStrategy isn't 'replace' or 'ignore'", function() {
expect(function() { room.addEvents(events, "foo"); }).toThrow();
expect(function() { room.addLiveEvents(events, "foo"); }).toThrow();
});
it("should replace a timeline event if dupe strategy is 'replace'", function() {
@@ -114,9 +114,9 @@ describe("Room", function() {
room: roomId, user: userA, msg: "dupe", event: true
});
dupe.event.event_id = events[0].getId();
room.addEvents(events);
room.addLiveEvents(events);
expect(room.timeline[0]).toEqual(events[0]);
room.addEvents([dupe], "replace");
room.addLiveEvents([dupe], "replace");
expect(room.timeline[0]).toEqual(dupe);
});
@@ -126,39 +126,13 @@ describe("Room", function() {
room: roomId, user: userA, msg: "dupe", event: true
});
dupe.event.event_id = events[0].getId();
room.addEvents(events);
room.addLiveEvents(events);
expect(room.timeline[0]).toEqual(events[0]);
room.addEvents([dupe], "ignore");
room.addLiveEvents([dupe], "ignore");
expect(room.timeline[0]).toEqual(events[0]);
});
});
describe("addEventsToTimeline", function() {
var events = [
utils.mkMessage({
room: roomId, user: userA, msg: "changing room name", event: true
}),
utils.mkEvent({
type: "m.room.name", room: roomId, user: userA, event: true,
content: { name: "New Room Name" }
})
];
it("should be able to add events to the end", function() {
room.addEventsToTimeline(events);
expect(room.timeline.length).toEqual(2);
expect(room.timeline[0]).toEqual(events[0]);
expect(room.timeline[1]).toEqual(events[1]);
});
it("should be able to add events to the start", function() {
room.addEventsToTimeline(events, true);
expect(room.timeline.length).toEqual(2);
expect(room.timeline[0]).toEqual(events[1]);
expect(room.timeline[1]).toEqual(events[0]);
});
it("should emit 'Room.timeline' events when added to the end",
it("should emit 'Room.timeline' events",
function() {
var callCount = 0;
room.on("Room.timeline", function(event, emitRoom, toStart) {
@@ -168,99 +142,10 @@ describe("Room", function() {
expect(emitRoom).toEqual(room);
expect(toStart).toBeFalsy();
});
room.addEventsToTimeline(events);
room.addLiveEvents(events);
expect(callCount).toEqual(2);
});
it("should emit 'Room.timeline' events when added to the start",
function() {
var callCount = 0;
room.on("Room.timeline", function(event, emitRoom, toStart) {
callCount += 1;
expect(room.timeline.length).toEqual(callCount);
expect(event).toEqual(events[callCount - 1]);
expect(emitRoom).toEqual(room);
expect(toStart).toBe(true);
});
room.addEventsToTimeline(events, true);
expect(callCount).toEqual(2);
});
it("should set event.sender for new and old events", function() {
var sentinel = {
userId: userA,
membership: "join",
name: "Alice"
};
var oldSentinel = {
userId: userA,
membership: "join",
name: "Old Alice"
};
room.currentState.getSentinelMember.andCallFake(function(uid) {
if (uid === userA) {
return sentinel;
}
return null;
});
room.oldState.getSentinelMember.andCallFake(function(uid) {
if (uid === userA) {
return oldSentinel;
}
return null;
});
var newEv = utils.mkEvent({
type: "m.room.name", room: roomId, user: userA, event: true,
content: { name: "New Room Name" }
});
var oldEv = utils.mkEvent({
type: "m.room.name", room: roomId, user: userA, event: true,
content: { name: "Old Room Name" }
});
room.addEventsToTimeline([newEv]);
expect(newEv.sender).toEqual(sentinel);
room.addEventsToTimeline([oldEv], true);
expect(oldEv.sender).toEqual(oldSentinel);
});
it("should set event.target for new and old m.room.member events",
function() {
var sentinel = {
userId: userA,
membership: "join",
name: "Alice"
};
var oldSentinel = {
userId: userA,
membership: "join",
name: "Old Alice"
};
room.currentState.getSentinelMember.andCallFake(function(uid) {
if (uid === userA) {
return sentinel;
}
return null;
});
room.oldState.getSentinelMember.andCallFake(function(uid) {
if (uid === userA) {
return oldSentinel;
}
return null;
});
var newEv = utils.mkMembership({
room: roomId, mship: "invite", user: userB, skey: userA, event: true
});
var oldEv = utils.mkMembership({
room: roomId, mship: "ban", user: userB, skey: userA, event: true
});
room.addEventsToTimeline([newEv]);
expect(newEv.target).toEqual(sentinel);
room.addEventsToTimeline([oldEv], true);
expect(oldEv.target).toEqual(oldSentinel);
});
it("should call setStateEvents on the right RoomState with the right " +
"forwardLooking value for new events", function() {
var events = [
@@ -274,7 +159,7 @@ describe("Room", function() {
}
})
];
room.addEventsToTimeline(events);
room.addLiveEvents(events);
expect(room.currentState.setStateEvents).toHaveBeenCalledWith(
[events[0]]
);
@@ -286,33 +171,6 @@ describe("Room", function() {
expect(room.oldState.setStateEvents).not.toHaveBeenCalled();
});
it("should call setStateEvents on the right RoomState with the right " +
"forwardLooking value for old events", function() {
var events = [
utils.mkMembership({
room: roomId, mship: "invite", user: userB, skey: userA, event: true
}),
utils.mkEvent({
type: "m.room.name", room: roomId, user: userB, event: true,
content: {
name: "New room"
}
})
];
room.addEventsToTimeline(events, true);
expect(room.oldState.setStateEvents).toHaveBeenCalledWith(
[events[0]]
);
expect(room.oldState.setStateEvents).toHaveBeenCalledWith(
[events[1]]
);
expect(events[0].forwardLooking).toBe(false);
expect(events[1].forwardLooking).toBe(false);
expect(room.currentState.setStateEvents).not.toHaveBeenCalled();
});
it("should synthesize read receipts for the senders of events", function() {
var sentinel = {
userId: userA,
@@ -325,7 +183,7 @@ describe("Room", function() {
}
return null;
});
room.addEventsToTimeline(events);
room.addLiveEvents(events);
expect(room.getEventReadUpTo(userA)).toEqual(events[1].getId());
});
@@ -370,13 +228,155 @@ describe("Room", function() {
expect(room.timeline.length).toEqual(1);
// then the remoteEvent
room.addEventsToTimeline([remoteEvent]);
room.addLiveEvents([remoteEvent]);
expect(room.timeline.length).toEqual(1);
expect(callCount).toEqual(2);
});
});
describe("addEventsToTimeline", function() {
var events = [
utils.mkMessage({
room: roomId, user: userA, msg: "changing room name", event: true
}),
utils.mkEvent({
type: "m.room.name", room: roomId, user: userA, event: true,
content: { name: "New Room Name" }
})
];
it("should not be able to add events to the end", function() {
expect(function() {
room.addEventsToTimeline(events, false, room.getLiveTimeline());
}).toThrow();
});
it("should be able to add events to the start", function() {
room.addEventsToTimeline(events, true, room.getLiveTimeline());
expect(room.timeline.length).toEqual(2);
expect(room.timeline[0]).toEqual(events[1]);
expect(room.timeline[1]).toEqual(events[0]);
});
it("should emit 'Room.timeline' events when added to the start",
function() {
var callCount = 0;
room.on("Room.timeline", function(event, emitRoom, toStart) {
callCount += 1;
expect(room.timeline.length).toEqual(callCount);
expect(event).toEqual(events[callCount - 1]);
expect(emitRoom).toEqual(room);
expect(toStart).toBe(true);
});
room.addEventsToTimeline(events, true, room.getLiveTimeline());
expect(callCount).toEqual(2);
});
});
describe("event metadata handling", function() {
it("should set event.sender for new and old events", function() {
var sentinel = {
userId: userA,
membership: "join",
name: "Alice"
};
var oldSentinel = {
userId: userA,
membership: "join",
name: "Old Alice"
};
room.currentState.getSentinelMember.andCallFake(function(uid) {
if (uid === userA) {
return sentinel;
}
return null;
});
room.oldState.getSentinelMember.andCallFake(function(uid) {
if (uid === userA) {
return oldSentinel;
}
return null;
});
var newEv = utils.mkEvent({
type: "m.room.name", room: roomId, user: userA, event: true,
content: { name: "New Room Name" }
});
var oldEv = utils.mkEvent({
type: "m.room.name", room: roomId, user: userA, event: true,
content: { name: "Old Room Name" }
});
room.addLiveEvents([newEv]);
expect(newEv.sender).toEqual(sentinel);
room.addEventsToTimeline([oldEv], true, room.getLiveTimeline());
expect(oldEv.sender).toEqual(oldSentinel);
});
it("should set event.target for new and old m.room.member events",
function() {
var sentinel = {
userId: userA,
membership: "join",
name: "Alice"
};
var oldSentinel = {
userId: userA,
membership: "join",
name: "Old Alice"
};
room.currentState.getSentinelMember.andCallFake(function(uid) {
if (uid === userA) {
return sentinel;
}
return null;
});
room.oldState.getSentinelMember.andCallFake(function(uid) {
if (uid === userA) {
return oldSentinel;
}
return null;
});
var newEv = utils.mkMembership({
room: roomId, mship: "invite", user: userB, skey: userA, event: true
});
var oldEv = utils.mkMembership({
room: roomId, mship: "ban", user: userB, skey: userA, event: true
});
room.addLiveEvents([newEv]);
expect(newEv.target).toEqual(sentinel);
room.addEventsToTimeline([oldEv], true, room.getLiveTimeline());
expect(oldEv.target).toEqual(oldSentinel);
});
it("should call setStateEvents on the right RoomState with the right " +
"forwardLooking value for old events", function() {
var events = [
utils.mkMembership({
room: roomId, mship: "invite", user: userB, skey: userA, event: true
}),
utils.mkEvent({
type: "m.room.name", room: roomId, user: userB, event: true,
content: {
name: "New room"
}
})
];
room.addEventsToTimeline(events, true, room.getLiveTimeline());
expect(room.oldState.setStateEvents).toHaveBeenCalledWith(
[events[0]]
);
expect(room.oldState.setStateEvents).toHaveBeenCalledWith(
[events[1]]
);
expect(events[0].forwardLooking).toBe(false);
expect(events[1].forwardLooking).toBe(false);
expect(room.currentState.setStateEvents).not.toHaveBeenCalled();
});
});
var resetTimelineTests = function(timelineSupport) {
var events = [
utils.mkMessage({
@@ -397,11 +397,11 @@ describe("Room", function() {
});
it("should copy state from previous timeline", function() {
room.addEventsToTimeline([events[0], events[1]]);
room.addLiveEvents([events[0], events[1]]);
expect(room.getLiveTimeline().getEvents().length).toEqual(2);
room.resetLiveTimeline();
room.addEventsToTimeline([events[2]]);
room.addLiveEvents([events[2]]);
var oldState = room.getLiveTimeline().getState(EventTimeline.BACKWARDS);
var newState = room.getLiveTimeline().getState(EventTimeline.FORWARDS);
expect(room.getLiveTimeline().getEvents().length).toEqual(1);
@@ -410,11 +410,11 @@ describe("Room", function() {
});
it("should reset the legacy timeline fields", function() {
room.addEventsToTimeline([events[0], events[1]]);
room.addLiveEvents([events[0], events[1]]);
expect(room.timeline.length).toEqual(2);
room.resetLiveTimeline();
room.addEventsToTimeline([events[2]]);
room.addLiveEvents([events[2]]);
var newLiveTimeline = room.getLiveTimeline();
expect(room.timeline).toEqual(newLiveTimeline.getEvents());
expect(room.oldState).toEqual(
@@ -443,7 +443,7 @@ describe("Room", function() {
it("should " + (timelineSupport ? "remember" : "forget") +
" old timelines", function() {
room.addEventsToTimeline([events[0]]);
room.addLiveEvents([events[0]]);
expect(room.timeline.length).toEqual(1);
var firstLiveTimeline = room.getLiveTimeline();
room.resetLiveTimeline();
@@ -477,7 +477,7 @@ describe("Room", function() {
];
it("should handle events in the same timeline", function() {
room.addEventsToTimeline(events);
room.addLiveEvents(events);
expect(room.compareEventOrdering(events[0].getId(),
events[1].getId()))
@@ -496,7 +496,7 @@ describe("Room", function() {
room.getLiveTimeline().setNeighbouringTimeline(oldTimeline, 'b');
room.addEventsToTimeline([events[0]], false, oldTimeline);
room.addEventsToTimeline([events[1]]);
room.addLiveEvents([events[1]]);
expect(room.compareEventOrdering(events[0].getId(),
events[1].getId()))
@@ -510,7 +510,7 @@ describe("Room", function() {
var oldTimeline = room.addTimeline();
room.addEventsToTimeline([events[0]], false, oldTimeline);
room.addEventsToTimeline([events[1]]);
room.addLiveEvents([events[1]]);
expect(room.compareEventOrdering(events[0].getId(),
events[1].getId()))
@@ -521,7 +521,7 @@ describe("Room", function() {
});
it("should return null for unknown events", function() {
room.addEventsToTimeline(events);
room.addLiveEvents(events);
expect(room.compareEventOrdering(events[0].getId(), "xxx"))
.toBe(null);
@@ -1068,7 +1068,7 @@ describe("Room", function() {
}),
];
room.addEventsToTimeline(events);
room.addLiveEvents(events);
var ts = 13787898424;
// check it initialises correctly
@@ -1159,9 +1159,9 @@ describe("Room", function() {
var eventC = utils.mkMessage({
room: roomId, user: userA, msg: "remote 2", event: true
});
room.addEvents([eventA]);
room.addLiveEvents([eventA]);
room.addPendingEvent(eventB, "TXN1");
room.addEvents([eventC]);
room.addLiveEvents([eventC]);
expect(room.timeline).toEqual(
[eventA, eventC]
);
@@ -1185,9 +1185,9 @@ describe("Room", function() {
var eventC = utils.mkMessage({
room: roomId, user: userA, msg: "remote 2", event: true
});
room.addEvents([eventA]);
room.addLiveEvents([eventA]);
room.addPendingEvent(eventB, "TXN1");
room.addEvents([eventC]);
room.addLiveEvents([eventC]);
expect(room.timeline).toEqual(
[eventA, eventB, eventC]
);
+38
View File
@@ -418,5 +418,43 @@ describe("TimelineWindow", function() {
}).catch(utils.failTest).done(done);
});
it("should limit the number of unsuccessful pagination requests",
function(done) {
var timeline = createTimeline();
timeline.setPaginationToken("toktok", EventTimeline.FORWARDS);
var timelineWindow = createWindow(timeline, {windowLimit: 5});
var eventId = timeline.getEvents()[1].getId();
var paginateCount = 0;
client.paginateEventTimeline = function(timeline0, opts) {
expect(timeline0).toBe(timeline);
expect(opts.backwards).toBe(false);
expect(opts.limit).toEqual(2);
paginateCount += 1;
return q(true);
};
timelineWindow.load(eventId, 3).then(function() {
var expectedEvents = timeline.getEvents();
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
.toBe(false);
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
.toBe(true);
return timelineWindow.paginate(EventTimeline.FORWARDS, 2, true, 3);
}).then(function(success) {
expect(success).toBe(false);
expect(paginateCount).toEqual(3);
var expectedEvents = timeline.getEvents().slice(0, 3);
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
.toBe(false);
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
.toBe(true);
}).catch(utils.failTest).done(done);
});
});
});
+68
View File
@@ -132,4 +132,72 @@ describe("utils", function() {
}, ["foo"]); }).not.toThrow();
});
});
describe("deepCompare", function() {
var assert = {
isTrue: function(x) { expect(x).toBe(true); },
isFalse: function(x) { expect(x).toBe(false); },
};
it("should handle primitives", function() {
assert.isTrue(utils.deepCompare(null, null));
assert.isFalse(utils.deepCompare(null, undefined));
assert.isTrue(utils.deepCompare("hi", "hi"));
assert.isTrue(utils.deepCompare(5, 5));
assert.isFalse(utils.deepCompare(5, 10));
});
it("should handle regexps", function() {
assert.isTrue(utils.deepCompare(/abc/, /abc/));
assert.isFalse(utils.deepCompare(/abc/, /123/));
var r = /abc/;
assert.isTrue(utils.deepCompare(r, r));
});
it("should handle dates", function() {
assert.isTrue(utils.deepCompare(new Date("2011-03-31"),
new Date("2011-03-31")));
assert.isFalse(utils.deepCompare(new Date("2011-03-31"),
new Date("1970-01-01")));
});
it("should handle arrays", function() {
assert.isTrue(utils.deepCompare([], []));
assert.isTrue(utils.deepCompare([1, 2], [1, 2]));
assert.isFalse(utils.deepCompare([1, 2], [2, 1]));
assert.isFalse(utils.deepCompare([1, 2], [1, 2, 3]));
});
it("should handle simple objects", function() {
assert.isTrue(utils.deepCompare({}, {}));
assert.isTrue(utils.deepCompare({a: 1, b: 2}, {a: 1, b: 2}));
assert.isTrue(utils.deepCompare({a: 1, b: 2}, {b: 2, a: 1}));
assert.isFalse(utils.deepCompare({a: 1, b: 2}, {a: 1, b: 3}));
assert.isTrue(utils.deepCompare({1: {name: "mhc", age: 28},
2: {name: "arb", age: 26}},
{1: {name: "mhc", age: 28},
2: {name: "arb", age: 26}}));
assert.isFalse(utils.deepCompare({1: {name: "mhc", age: 28},
2: {name: "arb", age: 26}},
{1: {name: "mhc", age: 28},
2: {name: "arb", age: 27}}));
assert.isFalse(utils.deepCompare({}, null));
assert.isFalse(utils.deepCompare({}, undefined));
});
it("should handle functions", function() {
// no two different function is equal really, they capture their
// context variables so even if they have same toString(), they
// won't have same functionality
var func = function(x) { return true; };
var func2 = function(x) { return true; };
assert.isTrue(utils.deepCompare(func, func));
assert.isFalse(utils.deepCompare(func, func2));
assert.isTrue(utils.deepCompare({ a: { b: func } }, { a: { b: func } }));
assert.isFalse(utils.deepCompare({ a: { b: func } }, { a: { b: func2 } }));
});
});
});