Compare commits
80 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2c40932080 | |||
| 3777b3e211 | |||
| 2f7d7308a1 | |||
| 0e606c6fe2 | |||
| 3af35c8209 | |||
| a2aed99f56 | |||
| 523a684d3f | |||
| dc386bab46 | |||
| 69079a2f9a | |||
| df33f7aceb | |||
| d87e5471fa | |||
| 90101c0340 | |||
| 950fce80c8 | |||
| 9135c50b83 | |||
| 83bad6ee0d | |||
| 3404751eb9 | |||
| 0282021e09 | |||
| 526e1d59e9 | |||
| dbc3a9a500 | |||
| cff7c8a59f | |||
| 64b8047f01 | |||
| 11e4760935 | |||
| c21d5634bb | |||
| dc56d7f784 | |||
| 1ff1064295 | |||
| a493a0ddb3 | |||
| 7573171d05 | |||
| 989e7cf78b | |||
| 384c474800 | |||
| 1d2c705e13 | |||
| c469ff4c8d | |||
| c7575f3f16 | |||
| 415251dd70 | |||
| 458cc55dec | |||
| d2adb30ded | |||
| bbe41aa7b4 | |||
| ece9391878 | |||
| 64640287cf | |||
| 57f88b00ba | |||
| e618ad9589 | |||
| 69c4d7b66e | |||
| d7b3b91eec | |||
| 88cc63e2a2 | |||
| cdea96fa0a | |||
| cfc10fa82d | |||
| 9d8973bf1f | |||
| 7f95237e02 | |||
| e1415d9829 | |||
| 19a12b3c79 | |||
| bec41e4f94 | |||
| 5f177aeec4 | |||
| b422916452 | |||
| 10378c0e7f | |||
| fba4d5fb0a | |||
| 77101823f5 | |||
| 15bc608368 | |||
| dfc4b34d09 | |||
| ad9daecbd4 | |||
| d29302716d | |||
| 6c7d13f8ce | |||
| e15a2d138c | |||
| 8bc9c19278 | |||
| dd86fade11 | |||
| ba1991aa8f | |||
| f4fd8d9ba6 | |||
| 02be0f659a | |||
| c7be310bdf | |||
| 55d8f56f98 | |||
| ab35fff9e8 | |||
| fdbc7a3112 | |||
| 3c6bd4774d | |||
| a2861c5781 | |||
| eaf3fe16eb | |||
| 963eaf7ec7 | |||
| e6e5b9b748 | |||
| 9ad031c857 | |||
| a0d465cb34 | |||
| 2dcf5227f0 | |||
| 518e92027c | |||
| ebc95667b8 |
+63
-3
@@ -1,5 +1,65 @@
|
||||
[0.4.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.4.2) (2016-03-17)
|
||||
=====================================================================================
|
||||
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)
|
||||
|
||||
**BREAKING CHANGES**:
|
||||
* `opts.pendingEventOrdering`==`end` is no longer supported in the arguments to
|
||||
`MatrixClient.startClient()`. Instead we provide a `detached` option, which
|
||||
puts pending events into a completely separate list in the Room, accessible
|
||||
via `Room.getPendingEvents()`.
|
||||
[\#111](https://github.com/matrix-org/matrix-js-sdk/pull/111)
|
||||
|
||||
Other improvements:
|
||||
* Log the stack when we get a sync error
|
||||
[\#109](https://github.com/matrix-org/matrix-js-sdk/pull/109)
|
||||
* Refactor transmitted-messages code
|
||||
[\#110](https://github.com/matrix-org/matrix-js-sdk/pull/110)
|
||||
* Add a method to the js sdk to look up 3pids on the ID server.
|
||||
[\#113](https://github.com/matrix-org/matrix-js-sdk/pull/113)
|
||||
* Support for cancelling pending events
|
||||
[\#112](https://github.com/matrix-org/matrix-js-sdk/pull/112)
|
||||
* API to stop peeking
|
||||
[\#114](https://github.com/matrix-org/matrix-js-sdk/pull/114)
|
||||
* update store user metadata based on membership events rather than presence
|
||||
[\#116](https://github.com/matrix-org/matrix-js-sdk/pull/116)
|
||||
* Include a counter in generated transaction IDs
|
||||
[\#115](https://github.com/matrix-org/matrix-js-sdk/pull/115)
|
||||
* get/setRoomVisibility API
|
||||
[\#117](https://github.com/matrix-org/matrix-js-sdk/pull/117)
|
||||
|
||||
Changes in [0.4.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.4.2) (2016-03-17)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.4.1...v0.4.2)
|
||||
|
||||
* Try again if a pagination request gives us no new messages
|
||||
@@ -41,7 +101,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)
|
||||
|
||||
Vendored
+15785
File diff suppressed because it is too large
Load Diff
+6
File diff suppressed because one or more lines are too long
Vendored
+15832
File diff suppressed because it is too large
Load Diff
+7
File diff suppressed because one or more lines are too long
Vendored
+16305
File diff suppressed because it is too large
Load Diff
+8
File diff suppressed because one or more lines are too long
Executable
+24
@@ -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
|
||||
+186
-62
@@ -144,7 +144,10 @@ function MatrixClient(opts) {
|
||||
var self = this;
|
||||
this.scheduler.setProcessFunction(function(eventToSend) {
|
||||
var room = self.getRoom(eventToSend.getRoomId());
|
||||
_updateLocalEchoStatus(room, eventToSend, EventStatus.SENDING);
|
||||
if (eventToSend.status !== EventStatus.SENDING) {
|
||||
_updatePendingEventStatus(room, eventToSend,
|
||||
EventStatus.SENDING);
|
||||
}
|
||||
return _sendEventHttpRequest(self, eventToSend);
|
||||
});
|
||||
}
|
||||
@@ -180,8 +183,9 @@ function MatrixClient(opts) {
|
||||
this._peekSync = null;
|
||||
this._isGuest = false;
|
||||
this._ongoingScrollbacks = {};
|
||||
|
||||
this._txnCtr = 0;
|
||||
this.timelineSupport = Boolean(opts.timelineSupport);
|
||||
this.urlPreviewCache = {};
|
||||
}
|
||||
utils.inherits(MatrixClient, EventEmitter);
|
||||
|
||||
@@ -694,7 +698,7 @@ MatrixClient.prototype.joinRoom = function(roomIdOrAlias, opts, callback) {
|
||||
return self._http.authedRequest(undefined, "POST", path, undefined, data);
|
||||
}).then(function(res) {
|
||||
var roomId = res.room_id;
|
||||
var syncApi = new SyncApi(self);
|
||||
var syncApi = new SyncApi(self, self._clientOpts);
|
||||
var room = syncApi.createRoom(roomId);
|
||||
if (opts.syncRoom) {
|
||||
// v2 will do this for us
|
||||
@@ -718,10 +722,32 @@ MatrixClient.prototype.joinRoom = function(roomIdOrAlias, opts, callback) {
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.resendEvent = function(event, room) {
|
||||
_updateLocalEchoStatus(room, event, EventStatus.SENDING);
|
||||
_updatePendingEventStatus(room, event, EventStatus.SENDING);
|
||||
return _sendEvent(this, room, event);
|
||||
};
|
||||
|
||||
/**
|
||||
* Cancel a queued or unsent event.
|
||||
*
|
||||
* @param {MatrixEvent} event Event to cancel
|
||||
* @throws Error if the event is not in QUEUED or NOT_SENT state
|
||||
*/
|
||||
MatrixClient.prototype.cancelPendingEvent = function(event) {
|
||||
if ([EventStatus.QUEUED, EventStatus.NOT_SENT].indexOf(event.status) < 0) {
|
||||
throw new Error("cannot cancel an event with status " + event.status);
|
||||
}
|
||||
|
||||
// first tell the scheduler to forget about it, if it's queued
|
||||
if (this.scheduler) {
|
||||
this.scheduler.removeEventFromQueue(event);
|
||||
}
|
||||
|
||||
// then tell the room about the change of state, which will remove it
|
||||
// from the room's list of pending events.
|
||||
var room = this.getRoom(event.getRoomId());
|
||||
_updatePendingEventStatus(room, event, EventStatus.CANCELLED);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} roomId
|
||||
* @param {string} name
|
||||
@@ -882,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,
|
||||
@@ -912,7 +938,7 @@ MatrixClient.prototype.sendEvent = function(roomId, eventType, content, txnId,
|
||||
if (utils.isFunction(txnId)) { callback = txnId; txnId = undefined; }
|
||||
|
||||
if (!txnId) {
|
||||
txnId = "m" + new Date().getTime();
|
||||
txnId = "m" + new Date().getTime() + "." + (this._txnCtr++);
|
||||
}
|
||||
|
||||
// we always construct a MatrixEvent when sending because the store and
|
||||
@@ -928,11 +954,11 @@ MatrixClient.prototype.sendEvent = function(roomId, eventType, content, txnId,
|
||||
content: content
|
||||
});
|
||||
localEvent._txnId = txnId;
|
||||
localEvent.status = EventStatus.SENDING;
|
||||
|
||||
// add this event immediately to the local store as 'sending'.
|
||||
if (room) {
|
||||
localEvent.status = EventStatus.SENDING;
|
||||
room.addEventsToTimeline([localEvent]);
|
||||
room.addPendingEvent(localEvent, txnId);
|
||||
}
|
||||
|
||||
if (eventType === "m.room.message" && this.sessionStore && CRYPTO_ENABLED) {
|
||||
@@ -1140,11 +1166,6 @@ function _badEncryptedMessage(event, reason) {
|
||||
}
|
||||
|
||||
function _sendEvent(client, room, event, callback) {
|
||||
// cache the local event ID here because if /sync returns before /send then
|
||||
// event.getId() will return a REAL event ID which we will then incorrectly
|
||||
// remove!
|
||||
var localEventId = event.getId();
|
||||
|
||||
var defer = q.defer();
|
||||
var promise;
|
||||
// this event may be queued
|
||||
@@ -1157,7 +1178,7 @@ function _sendEvent(client, room, event, callback) {
|
||||
if (promise && client.scheduler.getQueueForEvent(event).length > 1) {
|
||||
// event is processed FIFO so if the length is 2 or more we know
|
||||
// this event is stuck behind an earlier event.
|
||||
_updateLocalEchoStatus(room, event, EventStatus.QUEUED);
|
||||
_updatePendingEventStatus(room, event, EventStatus.QUEUED);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1167,42 +1188,13 @@ function _sendEvent(client, room, event, callback) {
|
||||
|
||||
promise.done(function(res) { // the request was sent OK
|
||||
if (room) {
|
||||
var eventId = res.event_id;
|
||||
|
||||
// FIXME: This manipulation of the room should probably be done
|
||||
// inside the room class, not by the client.
|
||||
var timeline = room.getTimelineForEvent(eventId);
|
||||
if (!timeline) {
|
||||
// we haven't yet received the event from the stream; we
|
||||
// need to update the fake event with the right event id.
|
||||
//
|
||||
// best way to make sure the room timeline structures are updated
|
||||
// correctly is to remove the event and add it again with the right
|
||||
// ID.
|
||||
//
|
||||
// This will also make us synthesize our own read receipt for the
|
||||
// sent message.
|
||||
var oldStatus = event.status;
|
||||
room.removeEvents([localEventId]);
|
||||
event.event.event_id = res.event_id;
|
||||
// TODO: at this point, we're still expecting the remote echo
|
||||
// to come back and update the server-generated fields for
|
||||
// us. We should probably set the status to some distinct value
|
||||
// so that the client app can figure out what is going on.
|
||||
event.status = null;
|
||||
room.addEventsToTimeline([event]);
|
||||
|
||||
// FIXME: doing this here is a horrible fudge, but this all
|
||||
// needs unpicking, which will touch the crypto code.
|
||||
room.emit("Room.localEchoUpdated", event, room, localEventId,
|
||||
oldStatus);
|
||||
}
|
||||
room.updatePendingEvent(event, EventStatus.SENT, res.event_id);
|
||||
}
|
||||
|
||||
_resolve(callback, defer, res);
|
||||
}, function(err) {
|
||||
// the request failed to send.
|
||||
_updateLocalEchoStatus(room, event, EventStatus.NOT_SENT);
|
||||
_updatePendingEventStatus(room, event, EventStatus.NOT_SENT);
|
||||
|
||||
_reject(callback, defer, err);
|
||||
});
|
||||
@@ -1210,9 +1202,9 @@ function _sendEvent(client, room, event, callback) {
|
||||
return defer.promise;
|
||||
}
|
||||
|
||||
function _updateLocalEchoStatus(room, event, newStatus) {
|
||||
function _updatePendingEventStatus(room, event, newStatus) {
|
||||
if (room) {
|
||||
room.updateLocalEchoStatus(event, newStatus);
|
||||
room.updatePendingEvent(event, newStatus);
|
||||
} else {
|
||||
event.status = newStatus;
|
||||
}
|
||||
@@ -1457,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
|
||||
@@ -1702,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; }
|
||||
|
||||
@@ -1913,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
|
||||
// ==============================
|
||||
|
||||
@@ -1945,6 +2004,39 @@ MatrixClient.prototype.resolveRoomAlias = function(roomAlias, callback) {
|
||||
return this._http.request(callback, "GET", path);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the visibility of a room in the current HS's room directory
|
||||
* @param {string} roomId
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.getRoomDirectoryVisibility =
|
||||
function(roomId, callback) {
|
||||
var path = utils.encodeUri("/directory/list/room/$roomId", {
|
||||
$roomId: roomId
|
||||
});
|
||||
return this._http.authedRequest(callback, "GET", path);
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the visbility of a room in the current HS's room directory
|
||||
* @param {string} roomId
|
||||
* @param {string} visibility
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: result object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.setRoomDirectoryVisibility =
|
||||
function(roomId, visibility, callback) {
|
||||
var path = utils.encodeUri("/directory/list/room/$roomId", {
|
||||
$roomId: roomId
|
||||
});
|
||||
return this._http.authedRequest(
|
||||
callback, "PUT", path, undefined, { "visibility": visibility }
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} roomId
|
||||
* @param {Number} limit
|
||||
@@ -2039,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;
|
||||
@@ -2322,10 +2414,20 @@ MatrixClient.prototype.peekInRoom = function(roomId) {
|
||||
if (this._peekSync) {
|
||||
this._peekSync.stopPeeking();
|
||||
}
|
||||
this._peekSync = new SyncApi(this);
|
||||
this._peekSync = new SyncApi(this, this._clientOpts);
|
||||
return this._peekSync.peek(roomId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Stop any ongoing room peeking.
|
||||
*/
|
||||
MatrixClient.prototype.stopPeeking = function() {
|
||||
if (this._peekSync) {
|
||||
this._peekSync.stopPeeking();
|
||||
this._peekSync = null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Set r/w flags for guest access in a room.
|
||||
* @param {string} roomId The room to configure guest access in.
|
||||
@@ -2801,7 +2903,7 @@ MatrixClient.prototype.syncLeftRooms = function() {
|
||||
return this._syncLeftRoomsPromise; // return the ongoing request
|
||||
}
|
||||
var self = this;
|
||||
var syncApi = new SyncApi(this);
|
||||
var syncApi = new SyncApi(this, this._clientOpts);
|
||||
this._syncLeftRoomsPromise = syncApi.syncLeftRooms();
|
||||
|
||||
// cleanup locks
|
||||
@@ -2931,11 +3033,14 @@ MatrixClient.prototype.isLoggedIn = function() {
|
||||
* @param {Boolean=} opts.resolveInvitesToProfiles True to do /profile requests
|
||||
* on every invite event if the displayname/avatar_url is not known for this user ID.
|
||||
* Default: false.
|
||||
* @param {String=} opts.pendingEventOrdering Controls where pending messages appear
|
||||
* in a room's timeline. If "<b>chronological</b>", messages will appear in the timeline
|
||||
* when the call to <code>sendEvent</code> was made. If "<b>end</b>", pending messages
|
||||
* will always appear at the end of the timeline (multiple pending messages will be sorted
|
||||
* chronologically). Default: "chronological".
|
||||
*
|
||||
* @param {String=} opts.pendingEventOrdering Controls where pending messages
|
||||
* appear in a room's timeline. If "<b>chronological</b>", messages will appear
|
||||
* in the timeline when the call to <code>sendEvent</code> was made. If
|
||||
* "<b>detached</b>", pending messages will appear in a separate list,
|
||||
* accessbile via {@link module:models/room~Room#getPendingEvents}. Default:
|
||||
* "chronological".
|
||||
*
|
||||
* @param {Number=} opts.pollTimeout The number of milliseconds to wait on /events.
|
||||
* Default: 30000 (30 seconds).
|
||||
*/
|
||||
@@ -2952,6 +3057,8 @@ MatrixClient.prototype.startClient = function(opts) {
|
||||
};
|
||||
}
|
||||
|
||||
this._clientOpts = opts;
|
||||
|
||||
if (CRYPTO_ENABLED && this.sessionStore !== null) {
|
||||
this.uploadKeys(5);
|
||||
}
|
||||
@@ -3264,12 +3371,29 @@ MatrixClient.prototype.requestEmailToken = function(email, clientSecret,
|
||||
return this._http.idServerRequest(
|
||||
callback, "POST", "/validate/email/requestToken",
|
||||
params, httpApi.PREFIX_IDENTITY_V1
|
||||
).then(function(res) {
|
||||
if (typeof res === "string") {
|
||||
return JSON.parse(res);
|
||||
}
|
||||
return res;
|
||||
});
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Looks up the public Matrix ID mapping for a given 3rd party
|
||||
* identifier from the Identity Server
|
||||
* @param {string} medium The medium of the threepid, eg. 'email'
|
||||
* @param {string} address The textual address of the threepid
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: A threepid mapping
|
||||
* object or the empty object if no mapping
|
||||
* exists
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.lookupThreePid = function(medium, address, callback) {
|
||||
var params = {
|
||||
medium: medium,
|
||||
address: address,
|
||||
};
|
||||
return this._http.idServerRequest(
|
||||
callback, "GET", "/lookup",
|
||||
params, httpApi.PREFIX_IDENTITY_V1
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
+27
-8
@@ -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);
|
||||
@@ -245,7 +255,12 @@ module.exports.MatrixHttpApi.prototype = {
|
||||
opts,
|
||||
requestCallback(defer, callback, this.opts.onlyData)
|
||||
);
|
||||
return defer.promise;
|
||||
// ID server does not always take JSON, so we can't use requests' 'json'
|
||||
// option as we do with the home server, but it does return JSON, so
|
||||
// parse it manually
|
||||
return defer.promise.then(function(response) {
|
||||
return JSON.parse(response);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -445,9 +460,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",
|
||||
@@ -459,7 +478,7 @@ module.exports.MatrixHttpApi.prototype = {
|
||||
var reqPromise = defer.promise;
|
||||
|
||||
try {
|
||||
var req = this.opts.request(
|
||||
req = this.opts.request(
|
||||
{
|
||||
uri: uri,
|
||||
method: method,
|
||||
@@ -472,7 +491,7 @@ module.exports.MatrixHttpApi.prototype = {
|
||||
},
|
||||
function(err, response, body) {
|
||||
if (localTimeoutMs) {
|
||||
clearTimeout(timeoutId);
|
||||
callbacks.clearTimeout(timeoutId);
|
||||
if (timedOut) {
|
||||
return; // already rejected promise
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -211,10 +213,8 @@ EventTimeline.prototype.setNeighbouringTimeline = function(neighbour, direction)
|
||||
*
|
||||
* @param {MatrixEvent} event new event
|
||||
* @param {boolean} atStart true to insert new event at the start
|
||||
* @param {boolean} [spliceBeforeLocalEcho = false] insert this event before any
|
||||
* localecho events at the end of the timeline. Ignored if atStart == true
|
||||
*/
|
||||
EventTimeline.prototype.addEvent = function(event, atStart, spliceBeforeLocalEcho) {
|
||||
EventTimeline.prototype.addEvent = function(event, atStart) {
|
||||
var stateContext = atStart ? this._startState : this._endState;
|
||||
|
||||
setEventMetadata(event, stateContext, atStart);
|
||||
@@ -243,17 +243,6 @@ EventTimeline.prototype.addEvent = function(event, atStart, spliceBeforeLocalEch
|
||||
insertIndex = 0;
|
||||
} else {
|
||||
insertIndex = this._events.length;
|
||||
|
||||
// if this is a real event, we might need to splice it in before any pending
|
||||
// local echo events.
|
||||
if (spliceBeforeLocalEcho) {
|
||||
for (var j = this._events.length - 1; j >= 0; j--) {
|
||||
if (!this._events[j].status) { // real events don't have a status
|
||||
insertIndex = j + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._events.splice(insertIndex, 0, event); // insert element
|
||||
@@ -303,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
|
||||
|
||||
+7
-1
@@ -32,7 +32,13 @@ module.exports.EventStatus = {
|
||||
/** The event is in the process of being sent. */
|
||||
SENDING: "sending",
|
||||
/** The event is in a queue waiting to be sent. */
|
||||
QUEUED: "queued"
|
||||
QUEUED: "queued",
|
||||
/** The event has been sent to the server, but we have not yet received the
|
||||
* echo. */
|
||||
SENT: "sent",
|
||||
|
||||
/** The event was cancelled before it was successfully sent. */
|
||||
CANCELLED: "cancelled",
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
+336
-159
@@ -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
|
||||
@@ -68,11 +80,14 @@ function synthesizeReceipt(userId, event, receiptType) {
|
||||
* @param {*} opts.storageToken Optional. The token which a data store can use
|
||||
* to remember the state of the room. What this means is dependent on the store
|
||||
* implementation.
|
||||
* @param {String=} opts.pendingEventOrdering Controls where pending messages appear
|
||||
* in a room's timeline. If "<b>chronological</b>", messages will appear in the timeline
|
||||
* when the call to <code>sendEvent</code> was made. If "<b>end</b>", pending messages
|
||||
* will always appear at the end of the timeline (multiple pending messages will be sorted
|
||||
* chronologically). Default: "chronological".
|
||||
*
|
||||
* @param {String=} opts.pendingEventOrdering Controls where pending messages
|
||||
* appear in a room's timeline. If "<b>chronological</b>", messages will appear
|
||||
* in the timeline when the call to <code>sendEvent</code> was made. If
|
||||
* "<b>detached</b>", pending messages will appear in a separate list,
|
||||
* accessbile via {@link module:models/room~Room#getPendingEvents}. Default:
|
||||
* "chronological".
|
||||
*
|
||||
* @param {boolean} [opts.timelineSupport = false] Set to true to enable improved
|
||||
* timeline support.
|
||||
*
|
||||
@@ -99,10 +114,10 @@ function Room(roomId, opts) {
|
||||
opts = opts || {};
|
||||
opts.pendingEventOrdering = opts.pendingEventOrdering || "chronological";
|
||||
|
||||
if (["chronological", "end"].indexOf(opts.pendingEventOrdering) === -1) {
|
||||
if (["chronological", "detached"].indexOf(opts.pendingEventOrdering) === -1) {
|
||||
throw new Error(
|
||||
"opts.pendingEventOrdering MUST be either 'chronological' or " +
|
||||
"'end'. Got: '" + opts.pendingEventOrdering + "'"
|
||||
"'detached'. Got: '" + opts.pendingEventOrdering + "'"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -151,9 +166,31 @@ function Room(roomId, opts) {
|
||||
this._eventIdToTimeline = {};
|
||||
this._timelineSupport = Boolean(opts.timelineSupport);
|
||||
|
||||
if (this._opts.pendingEventOrdering == "detached") {
|
||||
this._pendingEventList = [];
|
||||
}
|
||||
}
|
||||
utils.inherits(Room, EventEmitter);
|
||||
|
||||
/**
|
||||
* Get the list of pending sent events for this room
|
||||
*
|
||||
* @return {module:models/event.MatrixEvent[]} A list of the sent events
|
||||
* waiting for remote echo.
|
||||
*
|
||||
* @throws If <code>opts.pendingEventOrdering</code> was not 'detached'
|
||||
*/
|
||||
Room.prototype.getPendingEvents = function() {
|
||||
if (this._opts.pendingEventOrdering !== "detached") {
|
||||
throw new Error(
|
||||
"Cannot call getPendingEventList with pendingEventOrdering == " +
|
||||
this._opts.pendingEventOrdering);
|
||||
}
|
||||
|
||||
return this._pendingEventList;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Get the live timeline for this room.
|
||||
*
|
||||
@@ -369,9 +406,9 @@ Room.prototype.getAvatarUrl = function(baseUrl, width, height, resizeMethod,
|
||||
*/
|
||||
Room.prototype.addTimeline = function() {
|
||||
if (!this._timelineSupport) {
|
||||
throw Error("timeline support is disabled. Set the 'timelineSupport'" +
|
||||
" parameter to true when creating MatrixClient to enable" +
|
||||
" it.");
|
||||
throw new Error("timeline support is disabled. Set the 'timelineSupport'" +
|
||||
" parameter to true when creating MatrixClient to enable" +
|
||||
" it.");
|
||||
}
|
||||
|
||||
var timeline = new EventTimeline(this.roomId);
|
||||
@@ -391,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
|
||||
*
|
||||
@@ -402,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 :
|
||||
@@ -504,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;
|
||||
}
|
||||
|
||||
@@ -520,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;
|
||||
@@ -549,7 +589,7 @@ Room.prototype.addEventsToTimeline = function(events, toStartOfTimeline,
|
||||
};
|
||||
|
||||
/**
|
||||
* Check for redactions, and otherwise add event to the given timeline. Assumes
|
||||
* Add event to the given timeline, and emit Room.timeline. Assumes
|
||||
* we have already checked we don't know about this event.
|
||||
*
|
||||
* Will fire "Room.timeline" for each event added.
|
||||
@@ -558,18 +598,13 @@ Room.prototype.addEventsToTimeline = function(events, toStartOfTimeline,
|
||||
* @param {EventTimeline} timeline
|
||||
* @param {boolean} toStartOfTimeline
|
||||
*
|
||||
* @param {boolean} spliceBeforeLocalEcho if true, insert this event before
|
||||
* any localecho events at the end of the timeline. Ignored if
|
||||
* toStartOfTimeline == true.
|
||||
*
|
||||
* @fires module:client~MatrixClient#event:"Room.timeline"
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
Room.prototype._addEventToTimeline = function(event, timeline, toStartOfTimeline,
|
||||
spliceBeforeLocalEcho) {
|
||||
Room.prototype._addEventToTimeline = function(event, timeline, toStartOfTimeline) {
|
||||
var eventId = event.getId();
|
||||
timeline.addEvent(event, toStartOfTimeline, spliceBeforeLocalEcho);
|
||||
timeline.addEvent(event, toStartOfTimeline);
|
||||
this._eventIdToTimeline[eventId] = timeline;
|
||||
|
||||
var data = {
|
||||
@@ -581,150 +616,324 @@ 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) {
|
||||
var addLocalEchoToEnd = this._opts.pendingEventOrdering === "end";
|
||||
Room.prototype._addLiveEvent = function(event, duplicateStrategy) {
|
||||
if (event.getType() === "m.room.redaction") {
|
||||
var redactId = event.event.redacts;
|
||||
|
||||
for (var i = 0; i < events.length; i++) {
|
||||
var isLocalEcho = (
|
||||
events[i].status === EventStatus.SENDING ||
|
||||
events[i].status === EventStatus.QUEUED
|
||||
);
|
||||
// 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);
|
||||
|
||||
// FIXME: HORRIBLE ASSUMPTION THAT THIS PROP EXISTS
|
||||
// Exists due to client.js:815 (MatrixClient.sendEvent)
|
||||
// We should make txnId a first class citizen.
|
||||
if (events[i]._txnId) {
|
||||
// this is the outgoing copy of the event (ie, the local echo).
|
||||
this._txnToEvent[events[i]._txnId] = events[i];
|
||||
// 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.
|
||||
}
|
||||
else if (events[i].getUnsigned().transaction_id) {
|
||||
|
||||
// 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
|
||||
var existingEvent = this._txnToEvent[events[i].getUnsigned().transaction_id];
|
||||
if (existingEvent) {
|
||||
var oldEventId = existingEvent.getId();
|
||||
var oldStatus = existingEvent.status;
|
||||
this._handleRemoteEcho(event, existingEvent);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// no longer pending
|
||||
delete this._txnToEvent[events[i].getUnsigned().transaction_id];
|
||||
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
|
||||
);
|
||||
|
||||
// update the timeline map, because the event id has changed
|
||||
var existingTimeline = this._eventIdToTimeline[oldEventId];
|
||||
if (existingTimeline) {
|
||||
delete this._eventIdToTimeline[oldEventId];
|
||||
this._eventIdToTimeline[events[i].getId()] = existingTimeline;
|
||||
if (!tlEvents[j].encryptedType) {
|
||||
tlEvents[j] = event;
|
||||
}
|
||||
|
||||
// XXX: we need to fire an event when this happens.
|
||||
break;
|
||||
}
|
||||
|
||||
// replace the event source, but preserve the original content
|
||||
// and type in case it was encrypted (we won't be able to
|
||||
// decrypt it, even though we sent it.)
|
||||
var existingSource = existingEvent.event;
|
||||
existingEvent.event = events[i].event;
|
||||
existingEvent.event.content = existingSource.content;
|
||||
existingEvent.event.type = existingSource.type;
|
||||
|
||||
// successfully sent.
|
||||
existingEvent.status = null;
|
||||
|
||||
this.emit("Room.localEchoUpdated", existingEvent, this, oldEventId,
|
||||
oldStatus);
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
debuglog("Room._addLiveEvent: ignoring duplicate event " +
|
||||
event.getId());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: pass through filter to see if this should be added to the timeline.
|
||||
this._addEventToTimeline(event, this._liveTimeline, false);
|
||||
|
||||
if (events[i].getType() === "m.room.redaction") {
|
||||
var redactId = events[i].event.redacts;
|
||||
// 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);
|
||||
|
||||
// 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);
|
||||
|
||||
// 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.
|
||||
}
|
||||
|
||||
var spliceBeforeLocalEcho = !isLocalEcho && addLocalEchoToEnd;
|
||||
|
||||
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,
|
||||
spliceBeforeLocalEcho);
|
||||
}
|
||||
|
||||
// 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
|
||||
//
|
||||
// (we don't do this for local echoes, as they have temporary event
|
||||
// ids, which don't make much sense as RRs).
|
||||
if (events[i].sender && !isLocalEcho) {
|
||||
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.
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Update the status field on a local echo, to reflect its transmission
|
||||
* Add a pending outgoing event to this room.
|
||||
*
|
||||
* <p>The event is added to either the pendingEventList, or the live timeline,
|
||||
* depending on the setting of opts.pendingEventOrdering.
|
||||
*
|
||||
* <p>This is an internal method, intended for use by MatrixClient.
|
||||
*
|
||||
* @param {module:models/event.MatrixEvent} event The event to add.
|
||||
*
|
||||
* @param {string} txnId Transaction id for this outgoing event
|
||||
*
|
||||
* @fires module:client~MatrixClient#event:"Room.localEchoUpdated"
|
||||
*
|
||||
* @throws if the event doesn't have status SENDING, or we aren't given a
|
||||
* unique transaction id.
|
||||
*/
|
||||
Room.prototype.addPendingEvent = function(event, txnId) {
|
||||
if (event.status !== EventStatus.SENDING) {
|
||||
throw new Error("addPendingEvent called on an event with status " +
|
||||
event.status);
|
||||
}
|
||||
|
||||
if (this._txnToEvent[txnId]) {
|
||||
throw new Error("addPendingEvent called on an event with known txnId " +
|
||||
txnId);
|
||||
}
|
||||
|
||||
// call setEventMetadata to set up event.sender etc
|
||||
setEventMetadata(
|
||||
event,
|
||||
this._liveTimeline.getState(EventTimeline.FORWARDS),
|
||||
false
|
||||
);
|
||||
|
||||
this._txnToEvent[txnId] = event;
|
||||
|
||||
if (this._opts.pendingEventOrdering == "detached") {
|
||||
this._pendingEventList.push(event);
|
||||
} else {
|
||||
this._addEventToTimeline(event, this._liveTimeline, false);
|
||||
}
|
||||
|
||||
this.emit("Room.localEchoUpdated", event, this, null, null);
|
||||
};
|
||||
|
||||
/**
|
||||
* Deal with the echo of a message we sent.
|
||||
*
|
||||
* <p>We move the event to the live timeline if it isn't there already, and
|
||||
* update it.
|
||||
*
|
||||
* @param {module:models/event~MatrixEvent} remoteEvent The event received from
|
||||
* /sync
|
||||
* @param {module:models/event~MatrixEvent} localEvent The local echo, which
|
||||
* should be either in the _pendingEventList or the timeline.
|
||||
*
|
||||
* @fires module:client~MatrixClient#event:"Room.localEchoUpdated"
|
||||
* @private
|
||||
*/
|
||||
Room.prototype._handleRemoteEcho = function(remoteEvent, localEvent) {
|
||||
var oldEventId = localEvent.getId();
|
||||
var newEventId = remoteEvent.getId();
|
||||
var oldStatus = localEvent.status;
|
||||
|
||||
// no longer pending
|
||||
delete this._txnToEvent[remoteEvent.transaction_id];
|
||||
|
||||
// if it's in the pending list, remove it
|
||||
if (this._pendingEventList) {
|
||||
utils.removeElement(
|
||||
this._pendingEventList,
|
||||
function(ev) { return ev.getId() == oldEventId; },
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
// replace the event source, but preserve the original content
|
||||
// and type in case it was encrypted (we won't be able to
|
||||
// decrypt it, even though we sent it.)
|
||||
var existingSource = localEvent.event;
|
||||
localEvent.event = remoteEvent.event;
|
||||
localEvent.event.content = existingSource.content;
|
||||
localEvent.event.type = existingSource.type;
|
||||
|
||||
// successfully sent.
|
||||
localEvent.status = null;
|
||||
|
||||
// if it's already in the timeline, update the timeline map. If it's not, add it.
|
||||
var existingTimeline = this._eventIdToTimeline[oldEventId];
|
||||
if (existingTimeline) {
|
||||
delete this._eventIdToTimeline[oldEventId];
|
||||
this._eventIdToTimeline[newEventId] = existingTimeline;
|
||||
} else {
|
||||
this._addEventToTimeline(localEvent, this._liveTimeline, false);
|
||||
}
|
||||
|
||||
this.emit("Room.localEchoUpdated", localEvent, this,
|
||||
oldEventId, oldStatus);
|
||||
};
|
||||
|
||||
/* a map from current event status to a list of allowed next statuses
|
||||
*/
|
||||
var ALLOWED_TRANSITIONS = {};
|
||||
|
||||
ALLOWED_TRANSITIONS[EventStatus.SENDING] =
|
||||
[EventStatus.QUEUED, EventStatus.NOT_SENT, EventStatus.SENT];
|
||||
|
||||
ALLOWED_TRANSITIONS[EventStatus.QUEUED] =
|
||||
[EventStatus.SENDING, EventStatus.CANCELLED];
|
||||
|
||||
ALLOWED_TRANSITIONS[EventStatus.SENT] =
|
||||
[];
|
||||
|
||||
ALLOWED_TRANSITIONS[EventStatus.NOT_SENT] =
|
||||
[EventStatus.SENDING, EventStatus.QUEUED, EventStatus.CANCELLED];
|
||||
|
||||
ALLOWED_TRANSITIONS[EventStatus.CANCELLED] =
|
||||
[];
|
||||
|
||||
/**
|
||||
* Update the status / event id on a pending event, to reflect its transmission
|
||||
* progress.
|
||||
*
|
||||
* <p>This is an internal method.
|
||||
*
|
||||
* @param {MatrixEvent} event local echo event
|
||||
* @param {EventStatus} newStatus status to assign
|
||||
* @param {string} newEventId new event id to assign. Ignored unless
|
||||
* newStatus == EventStatus.SENT.
|
||||
* @fires module:client~MatrixClient#event:"Room.localEchoUpdated"
|
||||
*/
|
||||
Room.prototype.updateLocalEchoStatus = function(event, newStatus) {
|
||||
if (!event.status) {
|
||||
throw new Error("updateLocalEchoStatus called on an event which is " +
|
||||
"not a local echo.");
|
||||
Room.prototype.updatePendingEvent = function(event, newStatus, newEventId) {
|
||||
// if the message was sent, we expect an event id
|
||||
if (newStatus == EventStatus.SENT && !newEventId) {
|
||||
throw new Error("updatePendingEvent called with status=SENT, " +
|
||||
"but no new event id");
|
||||
}
|
||||
if (!this.getTimelineForEvent(event.getId())) {
|
||||
throw new Error("updateLocalEchoStatus called on an unknown event.");
|
||||
|
||||
// SENT races against /sync, so we have to special-case it.
|
||||
if (newStatus == EventStatus.SENT) {
|
||||
var timeline = this._eventIdToTimeline[newEventId];
|
||||
if (timeline) {
|
||||
// we've already received the event via the event stream.
|
||||
// nothing more to do here.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var oldStatus = event.status;
|
||||
var oldEventId = event.getId();
|
||||
|
||||
if (!oldStatus) {
|
||||
throw new Error("updatePendingEventStatus called on an event which is " +
|
||||
"not a local echo.");
|
||||
}
|
||||
|
||||
var allowed = ALLOWED_TRANSITIONS[oldStatus];
|
||||
if (!allowed || allowed.indexOf(newStatus) < 0) {
|
||||
throw new Error("Invalid EventStatus transition " + oldStatus + "->" +
|
||||
newStatus);
|
||||
}
|
||||
|
||||
event.status = newStatus;
|
||||
|
||||
if (newStatus == EventStatus.SENT) {
|
||||
// update the event id
|
||||
event.event.event_id = newEventId;
|
||||
|
||||
// if the event was already in the timeline (which will be the case if
|
||||
// opts.pendingEventOrdering==chronological), we need to update the
|
||||
// timeline map.
|
||||
var existingTimeline = this._eventIdToTimeline[oldEventId];
|
||||
if (existingTimeline) {
|
||||
delete this._eventIdToTimeline[oldEventId];
|
||||
this._eventIdToTimeline[newEventId] = existingTimeline;
|
||||
}
|
||||
}
|
||||
else if (newStatus == EventStatus.CANCELLED) {
|
||||
// remove it from the pending event list, or the timeline.
|
||||
if (this._pendingEventList) {
|
||||
utils.removeElement(
|
||||
this._pendingEventList,
|
||||
function(ev) { return ev.getId() == oldEventId; },
|
||||
false
|
||||
);
|
||||
}
|
||||
this.removeEvent(oldEventId);
|
||||
}
|
||||
|
||||
this.emit("Room.localEchoUpdated", event, this, event.getId(), oldStatus);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 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]);
|
||||
@@ -735,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);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1382,14 +1559,14 @@ module.exports = Room;
|
||||
*
|
||||
* <p>Once the /send request completes, if the remote echo has not already
|
||||
* arrived, the event is updated with a new event id and the status is set to
|
||||
* null. The server-generated fields are of course not updated yet.
|
||||
* 'SENT'. The server-generated fields are of course not updated yet.
|
||||
*
|
||||
* <p>Finally, the /send might fail. In this case, the event's status is set to
|
||||
* <p>If the /send fails, In this case, the event's status is set to
|
||||
* 'NOT_SENT'. If it is later resent, the process starts again, setting the
|
||||
* status to 'SENDING'.
|
||||
* status to 'SENDING'. Alternatively, the message may be cancelled, which
|
||||
* removes the event from the room, and sets the status to 'CANCELLED'.
|
||||
*
|
||||
* <p>This event is raised to reflect each of the transitions above (except the
|
||||
* first send attempt).
|
||||
* <p>This event is raised to reflect each of the transitions above.
|
||||
*
|
||||
* @event module:client~MatrixClient#"Room.localEchoUpdated"
|
||||
*
|
||||
|
||||
+17
-1
@@ -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,
|
||||
@@ -82,6 +88,7 @@ User.prototype.setPresenceEvent = function(event) {
|
||||
this.displayName = event.getContent().displayname;
|
||||
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 +143,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.
|
||||
*/
|
||||
|
||||
@@ -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; }
|
||||
@@ -160,7 +162,9 @@ function PushProcessor(client) {
|
||||
|
||||
var displayName = room.currentState.getMember(client.credentials.userId).name;
|
||||
|
||||
var pat = new RegExp("\\b" + escapeRegExp(displayName) + "\\b", 'i');
|
||||
// N.B. we can't use \b as it chokes on unicode. however \W seems to be okay
|
||||
// as shorthand for [^0-9A-Za-z_].
|
||||
var pat = new RegExp("(^|\\W)" + escapeRegExp(displayName) + "(\\W|$)", 'i');
|
||||
return ev.content.body.search(pat) > -1;
|
||||
};
|
||||
|
||||
@@ -174,7 +178,7 @@ function PushProcessor(client) {
|
||||
|
||||
var pat;
|
||||
if (cond.key == 'content.body') {
|
||||
pat = '\\b' + globToRegexp(cond.pattern) + '\\b';
|
||||
pat = '(^|\\W)' + globToRegexp(cond.pattern) + '(\\W|$)';
|
||||
} else {
|
||||
pat = '^' + globToRegexp(cond.pattern) + '$';
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
+7
-13
@@ -94,20 +94,14 @@ module.exports.MatrixInMemoryStore.prototype = {
|
||||
// which would then show up in these lists (!)
|
||||
return;
|
||||
}
|
||||
// We don't clobber any existing entry in the user map which has presence
|
||||
// so user entries with presence info are preferred. This does mean we will
|
||||
// clobber room member entries constantly, which is desirable to keep things
|
||||
// like display names and avatar URLs up-to-date.
|
||||
if (this.users[member.userId] && this.users[member.userId].events.presence) {
|
||||
return;
|
||||
}
|
||||
|
||||
var user = new User(member.userId);
|
||||
user.setDisplayName(member.name);
|
||||
var rawUrl = (
|
||||
member.events.member ? member.events.member.getContent().avatar_url : null
|
||||
);
|
||||
user.setAvatarUrl(rawUrl);
|
||||
var user = this.users[member.userId] || new User(member.userId);
|
||||
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;
|
||||
},
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
+47
-16
@@ -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
|
||||
@@ -519,8 +534,9 @@ SyncApi.prototype._sync = function(syncOptions) {
|
||||
self._processSyncResponse(syncToken, data);
|
||||
}
|
||||
catch (e) {
|
||||
console.error("Caught /sync error:");
|
||||
console.error(e);
|
||||
// log the exception with stack if we have it, else fall back
|
||||
// to the plain description
|
||||
console.error("Caught /sync error", e.stack || e);
|
||||
}
|
||||
|
||||
// emit synced events
|
||||
@@ -702,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);
|
||||
@@ -718,14 +734,28 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) {
|
||||
accountDataEvents.forEach(function(e) { client.emit("event", e); });
|
||||
});
|
||||
|
||||
// Handle leaves
|
||||
// Handle leaves (e.g. kicked rooms)
|
||||
leaveRooms.forEach(function(leaveObj) {
|
||||
// Do the bear minimum to register rejected invites / you leaving rooms
|
||||
var room = leaveObj.room;
|
||||
var stateEvents =
|
||||
self._mapSyncEventsFormat(leaveObj.state, room);
|
||||
var timelineEvents =
|
||||
self._mapSyncEventsFormat(leaveObj.timeline, room);
|
||||
room.addEvents(timelineEvents);
|
||||
var accountDataEvents =
|
||||
self._mapSyncEventsFormat(leaveObj.account_data);
|
||||
|
||||
self._processRoomEvents(room, stateEvents, timelineEvents);
|
||||
room.addAccountData(accountDataEvents);
|
||||
|
||||
room.recalculate(client.credentials.userId);
|
||||
if (leaveObj.isBrandNewRoom) {
|
||||
client.store.storeRoom(room);
|
||||
client.emit("Room", room);
|
||||
}
|
||||
|
||||
stateEvents.forEach(function(e) { client.emit("event", e); });
|
||||
timelineEvents.forEach(function(e) { client.emit("event", e); });
|
||||
accountDataEvents.forEach(function(e) { client.emit("event", e); });
|
||||
});
|
||||
};
|
||||
|
||||
@@ -806,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;
|
||||
});
|
||||
}
|
||||
@@ -963,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
@@ -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;
|
||||
|
||||
@@ -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
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "0.4.2",
|
||||
"version": "0.5.2",
|
||||
"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",
|
||||
|
||||
+53
-35
@@ -3,31 +3,41 @@
|
||||
# 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
|
||||
|
||||
USAGE="$0 [-x] vX.Y.Z"
|
||||
USAGE="$0 [-xz] [-c changelog_file] vX.Y.Z"
|
||||
|
||||
help() {
|
||||
cat <<EOF
|
||||
$USAGE
|
||||
|
||||
-x: skip updating the changelog
|
||||
-c changelog_file: specify name of file containing changelog
|
||||
-x: skip updating the changelog
|
||||
-z: skip generating the jsdoc
|
||||
EOF
|
||||
}
|
||||
|
||||
skip_changelog=
|
||||
while getopts hx f; do
|
||||
skip_jsdoc=
|
||||
changelog_file="CHANGELOG.md"
|
||||
while getopts hc:xz f; do
|
||||
case $f in
|
||||
h)
|
||||
help
|
||||
exit 0
|
||||
;;
|
||||
c)
|
||||
changelog_file="$OPTARG"
|
||||
;;
|
||||
x)
|
||||
skip_changelog=1
|
||||
;;
|
||||
z)
|
||||
skip_jsdoc=1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
shift `expr $OPTIND - 1`
|
||||
@@ -37,22 +47,18 @@ 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"
|
||||
|
||||
cd `dirname $0`
|
||||
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
|
||||
@@ -62,32 +68,35 @@ fi
|
||||
|
||||
if [ -z "$skip_changelog" ]; then
|
||||
echo "Generating changelog"
|
||||
update_changelog "$release"
|
||||
read -p "Edit CHANGELOG.md manually, or press enter to continue " REPLY
|
||||
update_changelog -f "$changelog_file" "$release"
|
||||
read -p "Edit $changelog_file manually, or press enter to continue " REPLY
|
||||
|
||||
if [ -n "$(git ls-files --modified CHANGELOG.md)" ]; then
|
||||
if [ -n "$(git ls-files --modified $changelog_file)" ]; then
|
||||
echo "Committing updated changelog"
|
||||
git commit "CHANGELOG.md" -m "Prepare changelog for $tag"
|
||||
git commit "$changelog_file" -m "Prepare changelog for $tag"
|
||||
fi
|
||||
fi
|
||||
|
||||
set -x
|
||||
|
||||
# Bump package.json, build the dist, and tag
|
||||
echo "npm version"
|
||||
npm version "$release"
|
||||
|
||||
# generate the docs
|
||||
echo "generating jsdocs"
|
||||
npm run gendoc
|
||||
if [ -z "$skip_jsdoc" ]; then
|
||||
echo "generating jsdocs"
|
||||
npm run gendoc
|
||||
|
||||
echo "copying jsdocs to gh-pages branch"
|
||||
git checkout gh-pages
|
||||
git pull
|
||||
cp -ar ".jsdoc/matrix-js-sdk/$release" .
|
||||
perl -i -pe 'BEGIN {$rel=shift} $_ =~ /^<\/ul>/ && print
|
||||
"<li><a href=\"${rel}/index.html\">Version ${rel}</a></li>\n"' \
|
||||
$release index.html
|
||||
git add "$release"
|
||||
git commit --no-verify -m "Add jsdoc for $release" index.html "$release"
|
||||
echo "copying jsdocs to gh-pages branch"
|
||||
git checkout gh-pages
|
||||
git pull
|
||||
cp -ar ".jsdoc/matrix-js-sdk/$release" .
|
||||
perl -i -pe 'BEGIN {$rel=shift} $_ =~ /^<\/ul>/ && print
|
||||
"<li><a href=\"${rel}/index.html\">Version ${rel}</a></li>\n"' \
|
||||
$release index.html
|
||||
git add "$release"
|
||||
git commit --no-verify -m "Add jsdoc for $release" index.html "$release"
|
||||
fi
|
||||
|
||||
# merge release branch to master
|
||||
echo "updating master branch"
|
||||
@@ -96,7 +105,16 @@ git pull
|
||||
git merge --ff-only "$rel_branch"
|
||||
|
||||
# push everything to github
|
||||
git push origin master "$rel_branch" "$tag" "gh-pages"
|
||||
git push origin master "$rel_branch" "$tag"
|
||||
if [ -z "$skip_jsdoc" ]; then
|
||||
git push origin gh-pages
|
||||
fi
|
||||
|
||||
# publish to npmjs
|
||||
npm publish
|
||||
|
||||
# finally, merge master back onto develop
|
||||
git checkout develop
|
||||
git pull
|
||||
git merge master
|
||||
git push origin develop
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -2,22 +2,30 @@
|
||||
var sdk = require("../..");
|
||||
var HttpBackend = require("../mock-request");
|
||||
var utils = require("../test-utils");
|
||||
var EventStatus = sdk.EventStatus;
|
||||
|
||||
describe("MatrixClient retrying", function() {
|
||||
var baseUrl = "http://localhost.or.something";
|
||||
var client, httpBackend;
|
||||
var scheduler;
|
||||
var userId = "@alice:localhost";
|
||||
var accessToken = "aseukfgwef";
|
||||
var roomId = "!room:here";
|
||||
var room;
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this);
|
||||
httpBackend = new HttpBackend();
|
||||
sdk.request(httpBackend.requestFn);
|
||||
scheduler = new sdk.MatrixScheduler();
|
||||
client = sdk.createClient({
|
||||
baseUrl: baseUrl,
|
||||
userId: userId,
|
||||
accessToken: accessToken
|
||||
accessToken: accessToken,
|
||||
scheduler: scheduler,
|
||||
});
|
||||
room = new sdk.Room(roomId);
|
||||
client.store.storeRoom(room);
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
@@ -40,6 +48,49 @@ describe("MatrixClient retrying", function() {
|
||||
|
||||
});
|
||||
|
||||
it("should mark events as EventStatus.CANCELLED when cancelled", function(done) {
|
||||
|
||||
// send a couple of events; the second will be queued
|
||||
var ev1, ev2;
|
||||
client.sendMessage(roomId, "m1").then(function(ev) {
|
||||
expect(ev).toEqual(ev1);
|
||||
});
|
||||
client.sendMessage(roomId, "m2").then(function(ev) {
|
||||
expect(ev).toEqual(ev2);
|
||||
});
|
||||
|
||||
// both events should be in the timeline at this point
|
||||
var tl = room.getLiveTimeline().getEvents();
|
||||
expect(tl.length).toEqual(2);
|
||||
ev1 = tl[0];
|
||||
ev2 = tl[1];
|
||||
|
||||
expect(ev1.status).toEqual(EventStatus.SENDING);
|
||||
expect(ev2.status).toEqual(EventStatus.QUEUED);
|
||||
|
||||
// now we can cancel the second and check everything looks sane
|
||||
client.cancelPendingEvent(ev2);
|
||||
expect(ev2.status).toEqual(EventStatus.CANCELLED);
|
||||
expect(tl.length).toEqual(1);
|
||||
|
||||
// shouldn't be able to cancel the first message yet
|
||||
expect(function() { client.cancelPendingEvent(ev1); })
|
||||
.toThrow();
|
||||
|
||||
// fail the first send
|
||||
httpBackend.when("PUT", "/send/m.room.message/")
|
||||
.respond(400);
|
||||
httpBackend.flush().then(function() {
|
||||
expect(ev1.status).toEqual(EventStatus.NOT_SENT);
|
||||
expect(tl.length).toEqual(1);
|
||||
|
||||
// cancel the first message
|
||||
client.cancelPendingEvent(ev1);
|
||||
expect(ev1.status).toEqual(EventStatus.CANCELLED);
|
||||
expect(tl.length).toEqual(0);
|
||||
}).catch(utils.failTest).done(done);
|
||||
});
|
||||
|
||||
describe("resending", function() {
|
||||
xit("should be able to resend a NOT_SENT event", function() {
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -32,6 +32,7 @@ describe("MatrixClient syncing", function() {
|
||||
|
||||
afterEach(function() {
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
client.stopClient();
|
||||
});
|
||||
|
||||
describe("startClient", function() {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+208
-153
@@ -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,11 +126,113 @@ 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]);
|
||||
});
|
||||
|
||||
it("should emit 'Room.timeline' events",
|
||||
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).toBeFalsy();
|
||||
});
|
||||
room.addLiveEvents(events);
|
||||
expect(callCount).toEqual(2);
|
||||
});
|
||||
|
||||
it("should call setStateEvents on the right RoomState with the right " +
|
||||
"forwardLooking value for new 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.addLiveEvents(events);
|
||||
expect(room.currentState.setStateEvents).toHaveBeenCalledWith(
|
||||
[events[0]]
|
||||
);
|
||||
expect(room.currentState.setStateEvents).toHaveBeenCalledWith(
|
||||
[events[1]]
|
||||
);
|
||||
expect(events[0].forwardLooking).toBe(true);
|
||||
expect(events[1].forwardLooking).toBe(true);
|
||||
expect(room.oldState.setStateEvents).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should synthesize read receipts for the senders of events", function() {
|
||||
var sentinel = {
|
||||
userId: userA,
|
||||
membership: "join",
|
||||
name: "Alice"
|
||||
};
|
||||
room.currentState.getSentinelMember.andCallFake(function(uid) {
|
||||
if (uid === userA) {
|
||||
return sentinel;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
room.addLiveEvents(events);
|
||||
expect(room.getEventReadUpTo(userA)).toEqual(events[1].getId());
|
||||
});
|
||||
|
||||
it("should emit Room.localEchoUpdated when a local echo is updated", function() {
|
||||
var localEvent = utils.mkMessage({
|
||||
room: roomId, user: userA, event: true,
|
||||
});
|
||||
localEvent.status = EventStatus.SENDING;
|
||||
var localEventId = localEvent.getId();
|
||||
|
||||
var remoteEvent = utils.mkMessage({
|
||||
room: roomId, user: userA, event: true,
|
||||
});
|
||||
remoteEvent.event.unsigned = {transaction_id: "TXN_ID"};
|
||||
var remoteEventId = remoteEvent.getId();
|
||||
|
||||
var callCount = 0;
|
||||
room.on("Room.localEchoUpdated",
|
||||
function(event, emitRoom, oldEventId, oldStatus) {
|
||||
switch (callCount) {
|
||||
case 0:
|
||||
expect(event.getId()).toEqual(localEventId);
|
||||
expect(event.status).toEqual(EventStatus.SENDING);
|
||||
expect(emitRoom).toEqual(room);
|
||||
expect(oldEventId).toBe(null);
|
||||
expect(oldStatus).toBe(null);
|
||||
break;
|
||||
case 1:
|
||||
expect(event.getId()).toEqual(remoteEventId);
|
||||
expect(event.status).toBe(null);
|
||||
expect(emitRoom).toEqual(room);
|
||||
expect(oldEventId).toEqual(localEventId);
|
||||
expect(oldStatus).toBe(EventStatus.SENDING);
|
||||
break;
|
||||
}
|
||||
callCount += 1;
|
||||
}
|
||||
);
|
||||
|
||||
// first add the local echo
|
||||
room.addPendingEvent(localEvent, "TXN_ID");
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
|
||||
// then the remoteEvent
|
||||
room.addLiveEvents([remoteEvent]);
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
|
||||
expect(callCount).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("addEventsToTimeline", function() {
|
||||
@@ -144,34 +246,19 @@ describe("Room", function() {
|
||||
})
|
||||
];
|
||||
|
||||
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 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.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 end",
|
||||
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).toBeFalsy();
|
||||
});
|
||||
room.addEventsToTimeline(events);
|
||||
expect(callCount).toEqual(2);
|
||||
});
|
||||
|
||||
it("should emit 'Room.timeline' events when added to the start",
|
||||
function() {
|
||||
var callCount = 0;
|
||||
@@ -182,10 +269,12 @@ describe("Room", function() {
|
||||
expect(emitRoom).toEqual(room);
|
||||
expect(toStart).toBe(true);
|
||||
});
|
||||
room.addEventsToTimeline(events, 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,
|
||||
@@ -218,9 +307,9 @@ describe("Room", function() {
|
||||
type: "m.room.name", room: roomId, user: userA, event: true,
|
||||
content: { name: "Old Room Name" }
|
||||
});
|
||||
room.addEventsToTimeline([newEv]);
|
||||
room.addLiveEvents([newEv]);
|
||||
expect(newEv.sender).toEqual(sentinel);
|
||||
room.addEventsToTimeline([oldEv], true);
|
||||
room.addEventsToTimeline([oldEv], true, room.getLiveTimeline());
|
||||
expect(oldEv.sender).toEqual(oldSentinel);
|
||||
});
|
||||
|
||||
@@ -255,38 +344,12 @@ describe("Room", function() {
|
||||
var oldEv = utils.mkMembership({
|
||||
room: roomId, mship: "ban", user: userB, skey: userA, event: true
|
||||
});
|
||||
room.addEventsToTimeline([newEv]);
|
||||
room.addLiveEvents([newEv]);
|
||||
expect(newEv.target).toEqual(sentinel);
|
||||
room.addEventsToTimeline([oldEv], true);
|
||||
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 new 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);
|
||||
expect(room.currentState.setStateEvents).toHaveBeenCalledWith(
|
||||
[events[0]]
|
||||
);
|
||||
expect(room.currentState.setStateEvents).toHaveBeenCalledWith(
|
||||
[events[1]]
|
||||
);
|
||||
expect(events[0].forwardLooking).toBe(true);
|
||||
expect(events[1].forwardLooking).toBe(true);
|
||||
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 = [
|
||||
@@ -301,7 +364,7 @@ describe("Room", function() {
|
||||
})
|
||||
];
|
||||
|
||||
room.addEventsToTimeline(events, true);
|
||||
room.addEventsToTimeline(events, true, room.getLiveTimeline());
|
||||
expect(room.oldState.setStateEvents).toHaveBeenCalledWith(
|
||||
[events[0]]
|
||||
);
|
||||
@@ -312,55 +375,6 @@ describe("Room", function() {
|
||||
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,
|
||||
membership: "join",
|
||||
name: "Alice"
|
||||
};
|
||||
room.currentState.getSentinelMember.andCallFake(function(uid) {
|
||||
if (uid === userA) {
|
||||
return sentinel;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
room.addEventsToTimeline(events);
|
||||
expect(room.getEventReadUpTo(userA)).toEqual(events[1].getId());
|
||||
});
|
||||
|
||||
it("should emit Room.localEchoUpdated when a local echo is updated", function() {
|
||||
var localEvent = utils.mkMessage({
|
||||
room: roomId, user: userA, event: true,
|
||||
});
|
||||
localEvent._txnId = "TXN_ID";
|
||||
localEvent.status = EventStatus.SENDING;
|
||||
var localEventId = localEvent.getId();
|
||||
|
||||
var remoteEvent = utils.mkMessage({
|
||||
room: roomId, user: userA, event: true,
|
||||
});
|
||||
remoteEvent.event.unsigned = {transaction_id: "TXN_ID"};
|
||||
var remoteEventId = remoteEvent.getId();
|
||||
|
||||
var callCount = 0;
|
||||
room.on("Room.localEchoUpdated", function(event, emitRoom, oldEventId) {
|
||||
callCount += 1;
|
||||
expect(event.getId()).toEqual(remoteEventId);
|
||||
expect(emitRoom).toEqual(room);
|
||||
expect(oldEventId).toEqual(localEventId);
|
||||
});
|
||||
|
||||
// first add the local echo to the timeline
|
||||
room.addEventsToTimeline([localEvent]);
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
|
||||
// then the remoteEvent
|
||||
room.addEventsToTimeline([remoteEvent]);
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
|
||||
expect(callCount).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
var resetTimelineTests = function(timelineSupport) {
|
||||
@@ -383,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);
|
||||
@@ -396,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(
|
||||
@@ -429,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();
|
||||
@@ -463,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()))
|
||||
@@ -482,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()))
|
||||
@@ -496,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()))
|
||||
@@ -507,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);
|
||||
@@ -1054,7 +1068,7 @@ describe("Room", function() {
|
||||
}),
|
||||
];
|
||||
|
||||
room.addEventsToTimeline(events);
|
||||
room.addLiveEvents(events);
|
||||
var ts = 13787898424;
|
||||
|
||||
// check it initialises correctly
|
||||
@@ -1129,10 +1143,11 @@ describe("Room", function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe("pendingEventOrdering", function() {
|
||||
it("should sort pending events to the end of the timeline if 'end'", function() {
|
||||
describe("addPendingEvent", function() {
|
||||
it("should add pending events to the pendingEventList if " +
|
||||
"pendingEventOrdering == 'detached'", function() {
|
||||
var room = new Room(roomId, {
|
||||
pendingEventOrdering: "end"
|
||||
pendingEventOrdering: "detached"
|
||||
});
|
||||
var eventA = utils.mkMessage({
|
||||
room: roomId, user: userA, msg: "remote 1", event: true
|
||||
@@ -1144,13 +1159,19 @@ describe("Room", function() {
|
||||
var eventC = utils.mkMessage({
|
||||
room: roomId, user: userA, msg: "remote 2", event: true
|
||||
});
|
||||
room.addEvents([eventA, eventB, eventC]);
|
||||
room.addLiveEvents([eventA]);
|
||||
room.addPendingEvent(eventB, "TXN1");
|
||||
room.addLiveEvents([eventC]);
|
||||
expect(room.timeline).toEqual(
|
||||
[eventA, eventC, eventB]
|
||||
[eventA, eventC]
|
||||
);
|
||||
expect(room.getPendingEvents()).toEqual(
|
||||
[eventB]
|
||||
);
|
||||
});
|
||||
|
||||
it("should sort pending events chronologically if 'chronological'", function() {
|
||||
it("should add pending events to the timeline if " +
|
||||
"pendingEventOrdering == 'chronological'", function() {
|
||||
room = new Room(roomId, {
|
||||
pendingEventOrdering: "chronological"
|
||||
});
|
||||
@@ -1164,50 +1185,84 @@ describe("Room", function() {
|
||||
var eventC = utils.mkMessage({
|
||||
room: roomId, user: userA, msg: "remote 2", event: true
|
||||
});
|
||||
room.addEvents([eventA, eventB, eventC]);
|
||||
room.addLiveEvents([eventA]);
|
||||
room.addPendingEvent(eventB, "TXN1");
|
||||
room.addLiveEvents([eventC]);
|
||||
expect(room.timeline).toEqual(
|
||||
[eventA, eventB, eventC]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("should treat NOT_SENT events as local echo", function() {
|
||||
describe("updatePendingEvent", function() {
|
||||
it("should remove cancelled events from the pending list", function() {
|
||||
var room = new Room(roomId, {
|
||||
pendingEventOrdering: "end"
|
||||
pendingEventOrdering: "detached"
|
||||
});
|
||||
var eventA = utils.mkMessage({
|
||||
room: roomId, user: userA, msg: "remote 1", event: true
|
||||
room: roomId, user: userA, event: true
|
||||
});
|
||||
var eventB = utils.mkMessage({
|
||||
room: roomId, user: userA, msg: "local 1", event: true
|
||||
});
|
||||
eventB.status = EventStatus.NOT_SENT;
|
||||
var eventC = utils.mkMessage({
|
||||
room: roomId, user: userA, msg: "remote 2", event: true
|
||||
});
|
||||
room.addEvents([eventA, eventB, eventC]);
|
||||
expect(room.timeline).toEqual(
|
||||
[eventA, eventC, eventB]
|
||||
eventA.status = EventStatus.SENDING;
|
||||
var eventId = eventA.getId();
|
||||
|
||||
room.addPendingEvent(eventA, "TXN1");
|
||||
expect(room.getPendingEvents()).toEqual(
|
||||
[eventA]
|
||||
);
|
||||
|
||||
// the event has to have been failed or queued before it can be
|
||||
// cancelled
|
||||
room.updatePendingEvent(eventA, EventStatus.NOT_SENT);
|
||||
|
||||
var callCount = 0;
|
||||
room.on("Room.localEchoUpdated",
|
||||
function(event, emitRoom, oldEventId, oldStatus) {
|
||||
expect(event).toEqual(eventA);
|
||||
expect(event.status).toEqual(EventStatus.CANCELLED);
|
||||
expect(emitRoom).toEqual(room);
|
||||
expect(oldEventId).toEqual(eventId);
|
||||
expect(oldStatus).toEqual(EventStatus.NOT_SENT);
|
||||
callCount++;
|
||||
});
|
||||
|
||||
room.updatePendingEvent(eventA, EventStatus.CANCELLED);
|
||||
expect(room.getPendingEvents()).toEqual([]);
|
||||
expect(callCount).toEqual(1);
|
||||
});
|
||||
|
||||
it("should treat QUEUED events as local echo", function() {
|
||||
var room = new Room(roomId, {
|
||||
pendingEventOrdering: "end"
|
||||
});
|
||||
|
||||
it("should remove cancelled events from the timeline", function() {
|
||||
var room = new Room(roomId);
|
||||
var eventA = utils.mkMessage({
|
||||
room: roomId, user: userA, msg: "remote 1", event: true
|
||||
room: roomId, user: userA, event: true
|
||||
});
|
||||
var eventB = utils.mkMessage({
|
||||
room: roomId, user: userA, msg: "local 1", event: true
|
||||
});
|
||||
eventB.status = EventStatus.QUEUED;
|
||||
var eventC = utils.mkMessage({
|
||||
room: roomId, user: userA, msg: "remote 2", event: true
|
||||
});
|
||||
room.addEvents([eventA, eventB, eventC]);
|
||||
expect(room.timeline).toEqual(
|
||||
[eventA, eventC, eventB]
|
||||
eventA.status = EventStatus.SENDING;
|
||||
var eventId = eventA.getId();
|
||||
|
||||
room.addPendingEvent(eventA, "TXN1");
|
||||
expect(room.getLiveTimeline().getEvents()).toEqual(
|
||||
[eventA]
|
||||
);
|
||||
|
||||
// the event has to have been failed or queued before it can be
|
||||
// cancelled
|
||||
room.updatePendingEvent(eventA, EventStatus.NOT_SENT);
|
||||
|
||||
var callCount = 0;
|
||||
room.on("Room.localEchoUpdated",
|
||||
function(event, emitRoom, oldEventId, oldStatus) {
|
||||
expect(event).toEqual(eventA);
|
||||
expect(event.status).toEqual(EventStatus.CANCELLED);
|
||||
expect(emitRoom).toEqual(room);
|
||||
expect(oldEventId).toEqual(eventId);
|
||||
expect(oldStatus).toEqual(EventStatus.NOT_SENT);
|
||||
callCount++;
|
||||
});
|
||||
|
||||
room.updatePendingEvent(eventA, EventStatus.CANCELLED);
|
||||
expect(room.getLiveTimeline().getEvents()).toEqual([]);
|
||||
expect(callCount).toEqual(1);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 } }));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user