Compare commits
132 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| ad5d07caf8 | |||
| b2d7abc0a1 | |||
| cc475e6392 | |||
| e4c6717bd5 | |||
| 53f813207e | |||
| 873fde27ac | |||
| 8d9d638953 | |||
| 2f93490054 | |||
| e22efc9dd5 | |||
| e7ac80cf2b | |||
| 4436087777 | |||
| f7bc11361c | |||
| a68b61dafe | |||
| 84c9876b3a | |||
| de864c489a | |||
| 2c277f7d96 | |||
| d0560f594d | |||
| 60b6310494 | |||
| abd27f9b75 | |||
| 3d316959f9 | |||
| 8aa3b79501 | |||
| f35409700a | |||
| b009739b9e | |||
| f007af741e | |||
| 3db4d9488b | |||
| 6b0fa84697 | |||
| 98b0cf2560 | |||
| 372759b6e4 | |||
| ec29b4ffeb | |||
| 95494933fd | |||
| 6fff29c07b | |||
| 6f7ed93b87 | |||
| 8e903c0531 | |||
| b90984a7f6 | |||
| 57006b7366 | |||
| db9ba52873 | |||
| 08b49c733a | |||
| 0f38764709 | |||
| 6040b50ceb | |||
| b88a207bde | |||
| 07bbe358ea | |||
| 39a5765888 | |||
| 9f91995f4e | |||
| 85f2754300 | |||
| 5833654aa6 | |||
| 899ff6cea2 | |||
| 3752429b65 | |||
| d13fbd0e3e | |||
| 5e18c84e53 | |||
| fc1d5c86f9 | |||
| c3ea913ae8 | |||
| f2336aaedf | |||
| 16ab2fd82a | |||
| 295fda027c | |||
| 28e6f00766 | |||
| b1988de753 | |||
| 0302eefd3c | |||
| 8ad5605e49 | |||
| 4b5539fe53 | |||
| b0becbc8d5 | |||
| 4ae353d3d3 | |||
| b4b8b4bfb8 | |||
| 909a8a0648 | |||
| 234c227fd5 | |||
| 78eded3bbd | |||
| f324e4c72f | |||
| 9328a12ccb | |||
| 51380f8116 | |||
| cfd96969fc | |||
| 0034bdf4ad | |||
| 00af1ce7d2 | |||
| 76f84c54db | |||
| bb4766c8c6 | |||
| e287e7591b | |||
| 48f7aca121 | |||
| a14f9e6d1c | |||
| 5fefcd8ce3 | |||
| 76f1d24c7b | |||
| 066dd77aba | |||
| 45a3bf63b2 | |||
| 75f2efffac | |||
| 0584ec3319 | |||
| 38e81ba61a | |||
| 164e4814af | |||
| abf908b14f | |||
| 1deb2e27d8 | |||
| 848ffe8a40 | |||
| 79c10c1b68 | |||
| 7e6eb89524 | |||
| 3d3e52b104 | |||
| 05326984df | |||
| e97e3c673f | |||
| f0ae46afc9 | |||
| aaf4371fae | |||
| 46912431cc | |||
| 6a19e08381 | |||
| 43f392955d |
@@ -1,3 +1,5 @@
|
||||
# Keep this file in sync with .npmignore.
|
||||
|
||||
.jsdoc
|
||||
node_modules
|
||||
.lock-wscript
|
||||
@@ -7,3 +9,7 @@ lib-cov
|
||||
out
|
||||
reports
|
||||
dist/browser-matrix-dev.js
|
||||
|
||||
# version file and tarball created by 'npm pack'
|
||||
/git-revision.txt
|
||||
/matrix-js-sdk-*.tgz
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
# Keep this file in sync with .gitignore.
|
||||
|
||||
.jsdoc
|
||||
node_modules
|
||||
.lock-wscript
|
||||
build/Release
|
||||
coverage
|
||||
lib-cov
|
||||
out
|
||||
reports
|
||||
dist/browser-matrix-dev.js
|
||||
|
||||
# tarball created by 'npm pack'.
|
||||
/matrix-js-sdk-*.tgz
|
||||
|
||||
+83
-3
@@ -1,3 +1,83 @@
|
||||
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
|
||||
[\#98](https://github.com/matrix-org/matrix-js-sdk/pull/98)
|
||||
* Add a delay before we start polling the connectivity check endpoint
|
||||
[\#99](https://github.com/matrix-org/matrix-js-sdk/pull/99)
|
||||
* Clean up a codepath that was only used for crypto messages
|
||||
[\#101](https://github.com/matrix-org/matrix-js-sdk/pull/101)
|
||||
* Add maySendStateEvent method, ported from react-sdk (but fixed).
|
||||
[\#94](https://github.com/matrix-org/matrix-js-sdk/pull/94)
|
||||
* Add Session.logged_out event
|
||||
[\#100](https://github.com/matrix-org/matrix-js-sdk/pull/100)
|
||||
* make presence work when peeking.
|
||||
[\#103](https://github.com/matrix-org/matrix-js-sdk/pull/103)
|
||||
* Add RoomState.mayClientSendStateEvent()
|
||||
[\#104](https://github.com/matrix-org/matrix-js-sdk/pull/104)
|
||||
* Fix displaynames for member join events
|
||||
[\#108](https://github.com/matrix-org/matrix-js-sdk/pull/108)
|
||||
|
||||
Changes in 0.4.1
|
||||
================
|
||||
|
||||
Improvements:
|
||||
* Check that `/sync` filters are correct before reusing them, and recreate
|
||||
them if not (https://github.com/matrix-org/matrix-js-sdk/pull/85).
|
||||
* Fire a `Room.timelineReset` event when a room's timeline is reset by a gappy
|
||||
`/sync` (https://github.com/matrix-org/matrix-js-sdk/pull/87,
|
||||
https://github.com/matrix-org/matrix-js-sdk/pull/93).
|
||||
* Make `TimelineWindow.load()` faster in the simple case of loading the live
|
||||
timeline (https://github.com/matrix-org/matrix-js-sdk/pull/88).
|
||||
* Update room-name calculation code to use the name of the sender of the
|
||||
invite when invited to a room
|
||||
(https://github.com/matrix-org/matrix-js-sdk/pull/89).
|
||||
* Don't reset the timeline when we join a room after peeking into it
|
||||
(https://github.com/matrix-org/matrix-js-sdk/pull/91).
|
||||
* Fire `Room.localEchoUpdated` events as local echoes progress through their
|
||||
transmission process (https://github.com/matrix-org/matrix-js-sdk/pull/95,
|
||||
https://github.com/matrix-org/matrix-js-sdk/pull/97).
|
||||
* 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)
|
||||
* Add `RoomState.maySendStateEvent` which determines if a given user has
|
||||
permission to send a state event
|
||||
(https://github.com/matrix-org/matrix-js-sdk/pull/94)
|
||||
|
||||
Changes in 0.4.0
|
||||
================
|
||||
|
||||
@@ -10,7 +90,7 @@ Changes in 0.4.0
|
||||
* Add a 'bindEmail' option to register()
|
||||
|
||||
Improvements:
|
||||
* Support third prty invites
|
||||
* Support third party invites
|
||||
* More appropriate naming for third party invite rooms
|
||||
* Poll the 'versions' endpoint to re-establish connectivity
|
||||
* Catch exceptions when syncing
|
||||
@@ -25,14 +105,14 @@ Improvements:
|
||||
* Support non-contiguous event timelines
|
||||
* Support new unread counts
|
||||
* Local echo for read-receipts
|
||||
|
||||
|
||||
|
||||
New methods:
|
||||
* Add method to fetch URLs not on the home or identity server
|
||||
* Method to get the last receipt for a user
|
||||
* Method to get all known users
|
||||
* Method to delete an alias
|
||||
|
||||
|
||||
|
||||
Changes in 0.3.0
|
||||
================
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
There is a script `release.sh` which does the following, but if you need to do
|
||||
a release manually, here are the steps:
|
||||
|
||||
- `git checkout -b release-v0.x.x`
|
||||
- Update `CHANGELOG.md`
|
||||
- `npm version 0.x.x`
|
||||
- Merge `release-v0.x.x` onto `master`.
|
||||
- Push `master`.
|
||||
- Push the tag: `git push --tags`
|
||||
- `npm publish`
|
||||
- Generate documentation: `npm run gendoc` (this outputs HTML to `.jsdoc`)
|
||||
- Copy the documentation from `.jsdoc` to the `gh-pages` branch and update `index.html`
|
||||
- Merge `master` onto `develop`.
|
||||
- Push `develop`.
|
||||
Vendored
+15420
File diff suppressed because it is too large
Load Diff
+5
File diff suppressed because one or more lines are too long
Vendored
+15577
File diff suppressed because it is too large
Load Diff
+7
File diff suppressed because one or more lines are too long
Vendored
+15785
File diff suppressed because it is too large
Load Diff
+6
File diff suppressed because one or more lines are too long
Executable
+13
@@ -0,0 +1,13 @@
|
||||
#!/bin/bash -l
|
||||
export NVM_DIR="/home/jenkins/.nvm"
|
||||
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
|
||||
nvm use 0.10
|
||||
npm install
|
||||
npm test
|
||||
jshint --reporter=checkstyle -c .jshint lib spec > jshint.xml || echo "jshint finished with return code $?"
|
||||
gjslint --unix_mode --disable 0131,0211,0200,0222,0212 --max_line_length 90 -r lib/ -r spec/ > gjslint.log || echo "gjslint finished with return code $?"
|
||||
|
||||
# delete the old tarball, if it exists
|
||||
rm -f matrix-js-sdk-*.tgz
|
||||
|
||||
npm pack
|
||||
+163
-68
@@ -143,7 +143,11 @@ function MatrixClient(opts) {
|
||||
if (this.scheduler) {
|
||||
var self = this;
|
||||
this.scheduler.setProcessFunction(function(eventToSend) {
|
||||
eventToSend.status = EventStatus.SENDING;
|
||||
var room = self.getRoom(eventToSend.getRoomId());
|
||||
if (eventToSend.status !== EventStatus.SENDING) {
|
||||
_updatePendingEventStatus(room, eventToSend,
|
||||
EventStatus.SENDING);
|
||||
}
|
||||
return _sendEventHttpRequest(self, eventToSend);
|
||||
});
|
||||
}
|
||||
@@ -161,7 +165,7 @@ function MatrixClient(opts) {
|
||||
this.credentials = {
|
||||
userId: (opts.userId || null)
|
||||
};
|
||||
this._http = new httpApi.MatrixHttpApi(httpOpts);
|
||||
this._http = new httpApi.MatrixHttpApi(this, httpOpts);
|
||||
this.callList = {
|
||||
// callId: MatrixCall
|
||||
};
|
||||
@@ -179,7 +183,7 @@ function MatrixClient(opts) {
|
||||
this._peekSync = null;
|
||||
this._isGuest = false;
|
||||
this._ongoingScrollbacks = {};
|
||||
|
||||
this._txnCtr = 0;
|
||||
this.timelineSupport = Boolean(opts.timelineSupport);
|
||||
}
|
||||
utils.inherits(MatrixClient, EventEmitter);
|
||||
@@ -693,7 +697,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
|
||||
@@ -717,10 +721,32 @@ MatrixClient.prototype.joinRoom = function(roomIdOrAlias, opts, callback) {
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.resendEvent = function(event, room) {
|
||||
event.status = 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
|
||||
@@ -911,7 +937,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
|
||||
@@ -927,11 +953,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) {
|
||||
@@ -1111,6 +1137,7 @@ function _decryptMessage(client, event) {
|
||||
room_id: payload.room_id,
|
||||
user_id: event.getSender(),
|
||||
event_id: event.getId(),
|
||||
unsigned: event.getUnsigned(),
|
||||
type: payload.type,
|
||||
content: payload.content
|
||||
}, "encrypted");
|
||||
@@ -1128,6 +1155,7 @@ function _badEncryptedMessage(event, reason) {
|
||||
room_id: event.getRoomId(),
|
||||
user_id: event.getSender(),
|
||||
event_id: event.getId(),
|
||||
unsigned: event.getUnsigned(),
|
||||
content: {
|
||||
msgtype: "m.bad.encrypted",
|
||||
body: reason,
|
||||
@@ -1137,11 +1165,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
|
||||
@@ -1154,7 +1177,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.
|
||||
event.status = EventStatus.QUEUED;
|
||||
_updatePendingEventStatus(room, event, EventStatus.QUEUED);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1164,56 +1187,28 @@ 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've already received the event via the event stream.
|
||||
// update it and remove the fake event.
|
||||
var matchingEvent =
|
||||
utils.findElement(timeline.getEvents(), function(ev) {
|
||||
return ev.getId() === eventId;
|
||||
}, true);
|
||||
if (event.encryptedType) {
|
||||
// Replace the content and type of the event with the
|
||||
// plaintext that we sent to the server.
|
||||
// TODO: Persist the changes if we storing events somewhere
|
||||
// otherthan in memory.
|
||||
matchingEvent.event.content = event.event.content;
|
||||
matchingEvent.event.type = event.event.type;
|
||||
}
|
||||
room.removeEvents([localEventId]);
|
||||
matchingEvent.status = null; // make sure it's still marked as sent
|
||||
}
|
||||
else {
|
||||
// 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.
|
||||
room.removeEvents([localEventId]);
|
||||
event.event.event_id = res.event_id;
|
||||
event.status = null;
|
||||
room.addEventsToTimeline([event]);
|
||||
}
|
||||
room.updatePendingEvent(event, EventStatus.SENT, res.event_id);
|
||||
}
|
||||
|
||||
_resolve(callback, defer, res);
|
||||
}, function(err) {
|
||||
// the request failed to send.
|
||||
event.status = EventStatus.NOT_SENT;
|
||||
_updatePendingEventStatus(room, event, EventStatus.NOT_SENT);
|
||||
|
||||
_reject(callback, defer, err);
|
||||
});
|
||||
|
||||
return defer.promise;
|
||||
}
|
||||
|
||||
function _updatePendingEventStatus(room, event, newStatus) {
|
||||
if (room) {
|
||||
room.updatePendingEvent(event, newStatus);
|
||||
} else {
|
||||
event.status = newStatus;
|
||||
}
|
||||
}
|
||||
|
||||
function _sendEventHttpRequest(client, event) {
|
||||
var pathParams = {
|
||||
$roomId: event.getRoomId(),
|
||||
@@ -1941,6 +1936,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
|
||||
@@ -2318,10 +2346,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.
|
||||
@@ -2500,6 +2538,27 @@ MatrixClient.prototype.setPushRuleEnabled = function(scope, kind,
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the actions for a push notification rule.
|
||||
* @param {string} scope
|
||||
* @param {string} kind
|
||||
* @param {string} ruleId
|
||||
* @param {array} actions
|
||||
* @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.setPushRuleActions = function(scope, kind,
|
||||
ruleId, actions, callback) {
|
||||
var path = utils.encodeUri("/pushrules/" + scope + "/$kind/$ruleId/actions", {
|
||||
$kind: kind,
|
||||
$ruleId: ruleId
|
||||
});
|
||||
return this._http.authedRequest(
|
||||
callback, "PUT", path, undefined, {"actions": actions}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the room-kind push rule associated with a room.
|
||||
* @param {string} scope "global" or device-specific.
|
||||
@@ -2776,7 +2835,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
|
||||
@@ -2906,11 +2965,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).
|
||||
*/
|
||||
@@ -2927,6 +2989,8 @@ MatrixClient.prototype.startClient = function(opts) {
|
||||
};
|
||||
}
|
||||
|
||||
this._clientOpts = opts;
|
||||
|
||||
if (CRYPTO_ENABLED && this.sessionStore !== null) {
|
||||
this.uploadKeys(5);
|
||||
}
|
||||
@@ -2950,8 +3014,10 @@ MatrixClient.prototype.startClient = function(opts) {
|
||||
MatrixClient.prototype.stopClient = function() {
|
||||
this.clientRunning = false;
|
||||
// TODO: f.e. Room => self.store.storeRoom(room) ?
|
||||
this._syncApi.stop();
|
||||
this._syncApi = null;
|
||||
if (this._syncApi) {
|
||||
this._syncApi.stop();
|
||||
this._syncApi = null;
|
||||
}
|
||||
};
|
||||
|
||||
function setupCallEventHandler(client) {
|
||||
@@ -3237,12 +3303,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
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -3387,6 +3470,18 @@ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED;
|
||||
* });
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fires whenever the login session the JS SDK is using is no
|
||||
* longer valid and the user must log in again.
|
||||
* NB. This only fires when action is required from the user, not
|
||||
* when then login session can be renewed by using a refresh token.
|
||||
* @event module:client~MatrixClient#"Session.logged_out"
|
||||
* @example
|
||||
* matrixClient.on("Session.logged_out", function(call){
|
||||
* // show the login screen
|
||||
* });
|
||||
*/
|
||||
|
||||
// EventEmitter JSDocs
|
||||
|
||||
/**
|
||||
|
||||
+21
-3
@@ -45,6 +45,7 @@ module.exports.PREFIX_IDENTITY_V1 = "/_matrix/identity/api/v1";
|
||||
/**
|
||||
* Construct a MatrixHttpApi.
|
||||
* @constructor
|
||||
* @param {EventEmitter} event_emitter The event emitter to use for emitting events
|
||||
* @param {Object} opts The options to use for this HTTP API.
|
||||
* @param {string} opts.baseUrl Required. The base client-server URL e.g.
|
||||
* 'http://localhost:8008'.
|
||||
@@ -60,9 +61,10 @@ module.exports.PREFIX_IDENTITY_V1 = "/_matrix/identity/api/v1";
|
||||
* @param {Object} opts.extraParams Optional. Extra query parameters to send on
|
||||
* requests.
|
||||
*/
|
||||
module.exports.MatrixHttpApi = function MatrixHttpApi(opts) {
|
||||
module.exports.MatrixHttpApi = function MatrixHttpApi(event_emitter, opts) {
|
||||
utils.checkObjectHasKeys(opts, ["baseUrl", "request", "prefix"]);
|
||||
opts.onlyData = opts.onlyData || false;
|
||||
this.event_emitter = event_emitter;
|
||||
this.opts = opts;
|
||||
this.uploads = [];
|
||||
};
|
||||
@@ -243,7 +245,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);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -268,7 +275,18 @@ module.exports.MatrixHttpApi.prototype = {
|
||||
authedRequest: function(callback, method, path, queryParams, data, localTimeoutMs) {
|
||||
if (!queryParams) { queryParams = {}; }
|
||||
queryParams.access_token = this.opts.accessToken;
|
||||
return this.request(callback, method, path, queryParams, data, localTimeoutMs);
|
||||
var self = this;
|
||||
var request_promise = this.request(
|
||||
callback, method, path, queryParams, data, localTimeoutMs
|
||||
);
|
||||
request_promise.catch(function(err) {
|
||||
if (err.errcode == 'M_UNKNOWN_TOKEN') {
|
||||
self.event_emitter.emit("Session.logged_out");
|
||||
}
|
||||
});
|
||||
// return the original promise, otherwise tests break due to it having to
|
||||
// go around the event loop one more time to process the result of the request
|
||||
return request_promise;
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -211,10 +211,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);
|
||||
@@ -227,7 +225,12 @@ EventTimeline.prototype.addEvent = function(event, atStart, spliceBeforeLocalEch
|
||||
// it again if the prop wasn't previously set. It may also mean that
|
||||
// the sender/target is updated (if the event set was a room member event)
|
||||
// so we want to use the *updated* member (new avatar/name) instead.
|
||||
if (!event.sender || event.getType() === "m.room.member") {
|
||||
//
|
||||
// However, we do NOT want to do this on member events if we're going
|
||||
// back in time, else we'll set the .sender value for BEFORE the given
|
||||
// member event, whereas we want to set the .sender value for the ACTUAL
|
||||
// member event itself.
|
||||
if (!event.sender || (event.getType() === "m.room.member" && !atStart)) {
|
||||
setEventMetadata(event, stateContext, atStart);
|
||||
}
|
||||
}
|
||||
@@ -238,17 +241,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
|
||||
|
||||
+9
-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",
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -143,6 +149,8 @@ module.exports.MatrixEvent.prototype = {
|
||||
/**
|
||||
* Get either 'content' or 'prev_content' depending on if this event is
|
||||
* 'forward-looking' or not. This can be modified via event.forwardLooking.
|
||||
* In practice, this means we get the chronologically earlier content value
|
||||
* for this event (this method should surely be called getEarlierContent)
|
||||
* <strong>This method is experimental and may change.</strong>
|
||||
* @return {Object} event.content if this event is forward-looking, else
|
||||
* event.prev_content.
|
||||
|
||||
@@ -145,6 +145,21 @@ RoomState.prototype.setStateEvents = function(stateEvents) {
|
||||
|
||||
if (event.getType() === "m.room.member") {
|
||||
var userId = event.getStateKey();
|
||||
|
||||
// leave events apparently elide the displayname or avatar_url,
|
||||
// so let's fake one up so that we don't leak user ids
|
||||
// into the timeline
|
||||
if (event.getContent().membership === "leave" ||
|
||||
event.getContent().membership === "ban")
|
||||
{
|
||||
event.getContent().avatar_url =
|
||||
event.getContent().avatar_url ||
|
||||
event.getPrevContent().avatar_url;
|
||||
event.getContent().displayname =
|
||||
event.getContent().displayname ||
|
||||
event.getPrevContent().displayname;
|
||||
}
|
||||
|
||||
var member = self.members[userId];
|
||||
if (!member) {
|
||||
member = new RoomMember(event.getRoomId(), userId);
|
||||
@@ -224,6 +239,65 @@ RoomState.prototype.getUserIdsWithDisplayName = function(displayName) {
|
||||
return this._displayNameToUserIds[displayName] || [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns true if the given MatrixClient has permission to send a state
|
||||
* event of type `stateEventType` into this room.
|
||||
* @param {string} type The type of state events to test
|
||||
* @param {MatrixClient} The client to test permission for
|
||||
* @return {boolean} true if the given client should be permitted to send
|
||||
* the given type of state event into this room,
|
||||
* according to the room's state.
|
||||
*/
|
||||
RoomState.prototype.mayClientSendStateEvent = function(stateEventType, cli) {
|
||||
if (cli.isGuest()) {
|
||||
return false;
|
||||
}
|
||||
return this.maySendStateEvent(stateEventType, cli.credentials.userId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns true if the given user ID has permission to send a state
|
||||
* event of type `stateEventType` into this room.
|
||||
* @param {string} type The type of state events 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 state event into this room,
|
||||
* according to the room's state.
|
||||
*/
|
||||
RoomState.prototype.maySendStateEvent = function(stateEventType, userId) {
|
||||
var member = this.getMember(userId);
|
||||
if (!member || member.membership == 'leave') { return false; }
|
||||
|
||||
var power_levels_event = this.getStateEvents('m.room.power_levels', '');
|
||||
|
||||
var power_levels;
|
||||
var events_levels = {};
|
||||
|
||||
var default_user_level = 0;
|
||||
var user_levels = [];
|
||||
|
||||
var state_default = 0;
|
||||
if (power_levels_event) {
|
||||
power_levels = power_levels_event.getContent();
|
||||
events_levels = power_levels.events || {};
|
||||
|
||||
default_user_level = parseInt(power_levels.users_default || 0);
|
||||
user_levels = power_levels.users || {};
|
||||
|
||||
if (power_levels.state_default !== undefined) {
|
||||
state_default = power_levels.state_default;
|
||||
} else {
|
||||
state_default = 50;
|
||||
}
|
||||
}
|
||||
|
||||
var state_event_level = state_default;
|
||||
if (events_levels[stateEventType] !== undefined) {
|
||||
state_event_level = events_levels[stateEventType];
|
||||
}
|
||||
return member.powerLevel >= state_event_level;
|
||||
};
|
||||
|
||||
/**
|
||||
* The RoomState class.
|
||||
*/
|
||||
|
||||
+399
-127
@@ -68,11 +68,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 +102,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 + "'"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -143,15 +146,39 @@ function Room(roomId, opts) {
|
||||
|
||||
this._notificationCounts = {};
|
||||
|
||||
this._liveTimeline = new EventTimeline(this.roomId);
|
||||
this._fixUpLegacyTimelineFields();
|
||||
|
||||
// just a list - *not* ordered.
|
||||
this._timelines = [];
|
||||
this._timelines = [this._liveTimeline];
|
||||
this._eventIdToTimeline = {};
|
||||
this._timelineSupport = Boolean(opts.timelineSupport);
|
||||
|
||||
this.resetLiveTimeline();
|
||||
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.
|
||||
*
|
||||
@@ -165,8 +192,12 @@ Room.prototype.getLiveTimeline = function() {
|
||||
* Reset the live timeline, and start a new one.
|
||||
*
|
||||
* <p>This is used when /sync returns a 'limited' timeline.
|
||||
*
|
||||
* @param {string=} backPaginationToken token for back-paginating the new timeline
|
||||
*
|
||||
* @fires module:client~MatrixClient#event:"Room.timelineReset"
|
||||
*/
|
||||
Room.prototype.resetLiveTimeline = function() {
|
||||
Room.prototype.resetLiveTimeline = function(backPaginationToken) {
|
||||
var newTimeline;
|
||||
|
||||
if (!this._timelineSupport) {
|
||||
@@ -178,24 +209,34 @@ Room.prototype.resetLiveTimeline = function() {
|
||||
newTimeline = this.addTimeline();
|
||||
}
|
||||
|
||||
if (this._liveTimeline) {
|
||||
// we have an existing timeline. This will always be true, except when
|
||||
// this method is called by our own constructor.
|
||||
|
||||
// initialise the state in the new timeline from our last known state
|
||||
var evMap = this._liveTimeline.getState(EventTimeline.FORWARDS).events;
|
||||
var events = [];
|
||||
for (var evtype in evMap) {
|
||||
if (!evMap.hasOwnProperty(evtype)) { continue; }
|
||||
for (var stateKey in evMap[evtype]) {
|
||||
if (!evMap[evtype].hasOwnProperty(stateKey)) { continue; }
|
||||
events.push(evMap[evtype][stateKey]);
|
||||
}
|
||||
// initialise the state in the new timeline from our last known state
|
||||
var evMap = this._liveTimeline.getState(EventTimeline.FORWARDS).events;
|
||||
var events = [];
|
||||
for (var evtype in evMap) {
|
||||
if (!evMap.hasOwnProperty(evtype)) { continue; }
|
||||
for (var stateKey in evMap[evtype]) {
|
||||
if (!evMap[evtype].hasOwnProperty(stateKey)) { continue; }
|
||||
events.push(evMap[evtype][stateKey]);
|
||||
}
|
||||
newTimeline.initialiseState(events);
|
||||
}
|
||||
this._liveTimeline = newTimeline;
|
||||
newTimeline.initialiseState(events);
|
||||
|
||||
// make sure we set the pagination token before firing timelineReset,
|
||||
// otherwise clients which start back-paginating will fail, and then get
|
||||
// stuck without realising that they *can* back-paginate.
|
||||
newTimeline.setPaginationToken(backPaginationToken, EventTimeline.BACKWARDS);
|
||||
|
||||
this._liveTimeline = newTimeline;
|
||||
this._fixUpLegacyTimelineFields();
|
||||
this.emit("Room.timelineReset", this);
|
||||
};
|
||||
|
||||
/**
|
||||
* Fix up this.timeline, this.oldState and this.currentState
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
Room.prototype._fixUpLegacyTimelineFields = function() {
|
||||
// maintain this.timeline as a reference to the live timeline,
|
||||
// and this.oldState and this.currentState as references to the
|
||||
// state at the start and end of that timeline. These are more
|
||||
@@ -353,9 +394,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);
|
||||
@@ -451,16 +492,26 @@ Room.prototype.addEventsToTimeline = function(events, toStartOfTimeline,
|
||||
// contained the event we knew about (steps 3, 5, 6).
|
||||
//
|
||||
//
|
||||
// Finally, consider what we want to do with the pagination token. In the
|
||||
// case above, we will be given a pagination token which tells us how to
|
||||
// So much for adding events to the timeline. But what do we want to do
|
||||
// with the pagination token?
|
||||
//
|
||||
// In the case above, we will be given a pagination token which tells us how to
|
||||
// get events beyond 'U' - in this case, it makes sense to store this
|
||||
// against timeline4. But what if timeline4 already had 'U' and beyond? in
|
||||
// that case, our best bet is to throw away the pagination token we were
|
||||
// given and stick with whatever token timeline4 had previously. In short,
|
||||
// we want to only store the pagination token if the last event we receive
|
||||
// is one we didn't previously know about.
|
||||
//
|
||||
// We make an exception for this if it turns out that we already knew about
|
||||
// *all* of the events, and we weren't able to join up any timelines. When
|
||||
// that happens, it means our existing pagination token is faulty, since it
|
||||
// is only telling us what we already know. Rather than repeatedly
|
||||
// paginating with the same token, we might as well use the new pagination
|
||||
// token in the hope that we eventually work our way out of the mess.
|
||||
|
||||
var updateToken = false;
|
||||
var didUpdate = false;
|
||||
var lastEventWasNew = false;
|
||||
for (var i = 0; i < events.length; i++) {
|
||||
var event = events[i];
|
||||
var eventId = event.getId();
|
||||
@@ -470,13 +521,12 @@ Room.prototype.addEventsToTimeline = function(events, toStartOfTimeline,
|
||||
if (!existingTimeline) {
|
||||
// we don't know about this event yet. Just add it to the timeline.
|
||||
this._addEventToTimeline(event, timeline, toStartOfTimeline);
|
||||
updateToken = true;
|
||||
lastEventWasNew = true;
|
||||
didUpdate = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// we already know about this event. We don't want to use the pagination
|
||||
// token if this is the last event, as per the text above.
|
||||
updateToken = false;
|
||||
lastEventWasNew = false;
|
||||
|
||||
if (existingTimeline == timeline) {
|
||||
console.log("Event " + eventId + " already in timeline " + timeline);
|
||||
@@ -512,15 +562,19 @@ Room.prototype.addEventsToTimeline = function(events, toStartOfTimeline,
|
||||
timeline.setNeighbouringTimeline(existingTimeline, direction);
|
||||
existingTimeline.setNeighbouringTimeline(timeline, inverseDirection);
|
||||
timeline = existingTimeline;
|
||||
didUpdate = true;
|
||||
}
|
||||
|
||||
if (updateToken) {
|
||||
// see above - if the last event was new to us, or if we didn't find any
|
||||
// new information, we update the pagination token for whatever
|
||||
// timeline we ended up on.
|
||||
if (lastEventWasNew || !didUpdate) {
|
||||
timeline.setPaginationToken(paginationToken, direction);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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.
|
||||
@@ -529,18 +583,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 = {
|
||||
@@ -560,42 +609,7 @@ Room.prototype._addEventToTimeline = function(event, timeline, toStartOfTimeline
|
||||
* @private
|
||||
*/
|
||||
Room.prototype._addLiveEvents = function(events) {
|
||||
var addLocalEchoToEnd = this._opts.pendingEventOrdering === "end";
|
||||
|
||||
for (var i = 0; i < events.length; i++) {
|
||||
var isLocalEcho = (
|
||||
events[i].status === EventStatus.SENDING ||
|
||||
events[i].status === EventStatus.QUEUED
|
||||
);
|
||||
|
||||
// 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];
|
||||
}
|
||||
else if (events[i].getUnsigned().transaction_id) {
|
||||
// remote echo of an event we sent earlier
|
||||
var existingEvent = this._txnToEvent[events[i].getUnsigned().transaction_id];
|
||||
if (existingEvent) {
|
||||
// no longer pending
|
||||
delete this._txnToEvent[events[i].getUnsigned().transaction_id];
|
||||
|
||||
// update the timeline map, because the event id has changed
|
||||
var existingTimeline = this._eventIdToTimeline[existingEvent.getId()];
|
||||
if (existingTimeline) {
|
||||
delete this._eventIdToTimeline[existingEvent.getId()];
|
||||
this._eventIdToTimeline[events[i].getId()] = existingTimeline;
|
||||
}
|
||||
|
||||
// replace the event source
|
||||
existingEvent.event = events[i].event;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (events[i].getType() === "m.room.redaction") {
|
||||
var redactId = events[i].event.redacts;
|
||||
|
||||
@@ -617,21 +631,24 @@ Room.prototype._addLiveEvents = function(events) {
|
||||
// this may be needed to trigger an update.
|
||||
}
|
||||
|
||||
var spliceBeforeLocalEcho = !isLocalEcho && addLocalEchoToEnd;
|
||||
if (events[i].getUnsigned().transaction_id) {
|
||||
var existingEvent = this._txnToEvent[events[i].getUnsigned().transaction_id];
|
||||
if (existingEvent) {
|
||||
// remote echo of an event we sent earlier
|
||||
this._handleRemoteEcho(events[i], existingEvent);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
this._addEventToTimeline(events[i], this._liveTimeline, false);
|
||||
}
|
||||
|
||||
// synthesize and inject implicit read receipts
|
||||
// Done after adding the event because otherwise the app would get a read receipt
|
||||
// pointing to an event that wasn't yet in the timeline
|
||||
//
|
||||
// (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) {
|
||||
if (events[i].sender) {
|
||||
this.addReceipt(synthesizeReceipt(
|
||||
events[i].sender.userId, events[i], "m.read"
|
||||
), true);
|
||||
@@ -639,6 +656,201 @@ Room.prototype._addLiveEvents = function(events) {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 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.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");
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -1120,52 +1332,65 @@ function calculateRoomName(room, userId, ignoreRoomNameEvent) {
|
||||
}
|
||||
|
||||
// get members that are NOT ourselves and are actually in the room.
|
||||
var members = utils.filter(room.currentState.getMembers(), function(m) {
|
||||
var otherMembers = utils.filter(room.currentState.getMembers(), function(m) {
|
||||
return (m.userId !== userId && m.membership !== "leave");
|
||||
});
|
||||
var allMembers = utils.filter(room.currentState.getMembers(), function(m) {
|
||||
return (m.membership !== "leave");
|
||||
});
|
||||
var myMemberEventArray = utils.filter(room.currentState.getMembers(), function(m) {
|
||||
return (m.userId == userId);
|
||||
});
|
||||
var myMemberEvent = (
|
||||
(myMemberEventArray.length && myMemberEventArray[0].events) ?
|
||||
myMemberEventArray[0].events.member.event : undefined
|
||||
);
|
||||
|
||||
// TODO: Localisation
|
||||
if (members.length === 0) {
|
||||
var memberList = utils.filter(room.currentState.getMembers(), function(m) {
|
||||
return (m.membership !== "leave");
|
||||
});
|
||||
if (memberList.length === 1) {
|
||||
if (myMemberEvent && myMemberEvent.content.membership == "invite") {
|
||||
if (room.currentState.getMember(myMemberEvent.sender)) {
|
||||
// extract who invited us to the room
|
||||
return "Invite from " + room.currentState.getMember(
|
||||
myMemberEvent.sender
|
||||
).name;
|
||||
} else if (allMembers[0].events.member) {
|
||||
// use the sender field from the invite event, although this only
|
||||
// gets us the mxid
|
||||
return "Invite from " + myMemberEvent.sender;
|
||||
} else {
|
||||
return "Room Invite";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (otherMembers.length === 0) {
|
||||
if (allMembers.length === 1) {
|
||||
// self-chat, peeked room with 1 participant,
|
||||
// or inbound invite, or outbound 3PID invite.
|
||||
if (memberList[0].membership === "invite") {
|
||||
if (memberList[0].events.member) {
|
||||
// extract who invited us to the room
|
||||
return "Invite from " + memberList[0].events.member.getSender();
|
||||
if (allMembers[0].userId === userId) {
|
||||
var thirdPartyInvites =
|
||||
room.currentState.getStateEvents("m.room.third_party_invite");
|
||||
if (thirdPartyInvites && thirdPartyInvites.length > 0) {
|
||||
var name = "Inviting " +
|
||||
thirdPartyInvites[0].getContent().display_name;
|
||||
if (thirdPartyInvites.length > 1) {
|
||||
if (thirdPartyInvites.length == 2) {
|
||||
name += " and " +
|
||||
thirdPartyInvites[1].getContent().display_name;
|
||||
}
|
||||
else {
|
||||
name += " and " +
|
||||
thirdPartyInvites.length + " others";
|
||||
}
|
||||
}
|
||||
return name;
|
||||
}
|
||||
else {
|
||||
return "Room Invite";
|
||||
return "Empty room";
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (memberList[0].userId === userId) {
|
||||
var thirdPartyInvites =
|
||||
room.currentState.getStateEvents("m.room.third_party_invite");
|
||||
if (thirdPartyInvites && thirdPartyInvites.length > 0) {
|
||||
var name = "Inviting " +
|
||||
thirdPartyInvites[0].getContent().display_name;
|
||||
if (thirdPartyInvites.length > 1) {
|
||||
if (thirdPartyInvites.length == 2) {
|
||||
name += " and " +
|
||||
thirdPartyInvites[1].getContent().display_name;
|
||||
}
|
||||
else {
|
||||
name += " and " +
|
||||
thirdPartyInvites.length + " others";
|
||||
}
|
||||
}
|
||||
return name;
|
||||
}
|
||||
else {
|
||||
return "Empty room";
|
||||
}
|
||||
}
|
||||
else {
|
||||
return memberList[0].name;
|
||||
}
|
||||
return allMembers[0].name;
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -1173,17 +1398,17 @@ function calculateRoomName(room, userId, ignoreRoomNameEvent) {
|
||||
return "Empty room";
|
||||
}
|
||||
}
|
||||
else if (members.length === 1) {
|
||||
return members[0].name;
|
||||
else if (otherMembers.length === 1) {
|
||||
return otherMembers[0].name;
|
||||
}
|
||||
else if (members.length === 2) {
|
||||
else if (otherMembers.length === 2) {
|
||||
return (
|
||||
members[0].name + " and " + members[1].name
|
||||
otherMembers[0].name + " and " + otherMembers[1].name
|
||||
);
|
||||
}
|
||||
else {
|
||||
return (
|
||||
members[0].name + " and " + (members.length - 1) + " others"
|
||||
otherMembers[0].name + " and " + (otherMembers.length - 1) + " others"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1218,6 +1443,18 @@ module.exports = Room;
|
||||
* });
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fires wheneer the live timeline in a room is reset.
|
||||
*
|
||||
* When we get a 'limited' sync (for example, after a network outage), we reset
|
||||
* the live timeline to be empty before adding the recent events to the new
|
||||
* timeline. This event is fired after the timeline is reset, and before the
|
||||
* new events are added.
|
||||
*
|
||||
* @event module:clinet~MatrixClient#"Room.timelineReset"
|
||||
* @param {Room} room The room whose live timeline was reset.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fires when an event we had previously received is redacted.
|
||||
*
|
||||
@@ -1274,3 +1511,38 @@ module.exports = Room;
|
||||
* }
|
||||
* });
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fires when the status of a transmitted event is updated.
|
||||
*
|
||||
* <p>When an event is first transmitted, a temporary copy of the event is
|
||||
* inserted into the timeline, with a temporary event id, and a status of
|
||||
* 'SENDING'.
|
||||
*
|
||||
* <p>Once the echo comes back from the server, the content of the event
|
||||
* (MatrixEvent.event) is replaced by the complete event from the homeserver,
|
||||
* thus updating its event id, as well as server-generated fields such as the
|
||||
* timestamp. Its status is set to null.
|
||||
*
|
||||
* <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
|
||||
* 'SENT'. The server-generated fields are of course not updated yet.
|
||||
*
|
||||
* <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'. 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.
|
||||
*
|
||||
* @event module:client~MatrixClient#"Room.localEchoUpdated"
|
||||
*
|
||||
* @param {MatrixEvent} event The matrix event which has been updated
|
||||
*
|
||||
* @param {Room} room The room containing the redacted event
|
||||
*
|
||||
* @param {string} oldEventId The previous event id (the temporary event id,
|
||||
* except when updating a successfully-sent event when its echo arrives)
|
||||
*
|
||||
* @param {EventStatus} oldStatus The previous event status.
|
||||
*/
|
||||
|
||||
@@ -31,6 +31,8 @@ limitations under the License.
|
||||
* @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 {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.
|
||||
* @prop {MatrixEvent} events.presence The m.presence event for this user.
|
||||
*/
|
||||
@@ -40,6 +42,7 @@ function User(userId) {
|
||||
this.displayName = userId;
|
||||
this.avatarUrl = null;
|
||||
this.lastActiveAgo = 0;
|
||||
this.currentlyActive = false;
|
||||
this.events = {
|
||||
presence: null,
|
||||
profile: null
|
||||
@@ -79,6 +82,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.currentlyActive = event.getContent().currently_active;
|
||||
|
||||
if (eventsToFire.length > 0) {
|
||||
this._updateModifiedTime();
|
||||
|
||||
@@ -160,7 +160,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 +176,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) + '$';
|
||||
}
|
||||
|
||||
+1
-8
@@ -94,15 +94,8 @@ 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);
|
||||
var user = this.users[member.userId] || new User(member.userId);
|
||||
user.setDisplayName(member.name);
|
||||
var rawUrl = (
|
||||
member.events.member ? member.events.member.getContent().avatar_url : null
|
||||
|
||||
+184
-53
@@ -28,6 +28,7 @@ var User = require("./models/user");
|
||||
var Room = require("./models/room");
|
||||
var utils = require("./utils");
|
||||
var Filter = require("./filter");
|
||||
var EventTimeline = require("./models/event-timeline");
|
||||
|
||||
var DEBUG = true;
|
||||
|
||||
@@ -84,7 +85,10 @@ SyncApi.prototype.createRoom = function(roomId) {
|
||||
timelineSupport: client.timelineSupport,
|
||||
});
|
||||
reEmit(client, room, ["Room.name", "Room.timeline", "Room.redaction",
|
||||
"Room.receipt", "Room.tags"]);
|
||||
"Room.receipt", "Room.tags",
|
||||
"Room.timelineReset",
|
||||
"Room.localEchoUpdated",
|
||||
]);
|
||||
this._registerStateListeners(room);
|
||||
return room;
|
||||
};
|
||||
@@ -174,12 +178,14 @@ SyncApi.prototype.syncLeftRooms = function() {
|
||||
var timelineEvents =
|
||||
self._mapSyncEventsFormat(leaveObj.timeline, room);
|
||||
var stateEvents = self._mapSyncEventsFormat(leaveObj.state, room);
|
||||
var paginationToken = (
|
||||
leaveObj.timeline.limited ? leaveObj.timeline.prev_batch : null
|
||||
);
|
||||
self._processRoomEvents(
|
||||
room, stateEvents, timelineEvents, paginationToken
|
||||
);
|
||||
|
||||
// set the back-pagination token. Do this *before* adding any
|
||||
// events so that clients can start back-paginating.
|
||||
room.getLiveTimeline().setPaginationToken(leaveObj.timeline.prev_batch,
|
||||
EventTimeline.BACKWARDS);
|
||||
|
||||
self._processRoomEvents(room, stateEvents, timelineEvents);
|
||||
|
||||
room.recalculate(client.credentials.userId);
|
||||
client.store.storeRoom(room);
|
||||
client.emit("Room", room);
|
||||
@@ -219,6 +225,25 @@ SyncApi.prototype.peek = function(roomId) {
|
||||
response.messages.chunk, client.getEventMapper()
|
||||
);
|
||||
|
||||
// XXX: copypasted from /sync until we kill off this
|
||||
// minging v1 API stuff)
|
||||
// handle presence events (User objects)
|
||||
if (response.presence && utils.isArray(response.presence)) {
|
||||
response.presence.map(client.getEventMapper()).forEach(
|
||||
function(presenceEvent) {
|
||||
var user = client.store.getUser(presenceEvent.getContent().user_id);
|
||||
if (user) {
|
||||
user.setPresenceEvent(presenceEvent);
|
||||
}
|
||||
else {
|
||||
user = createNewUser(client, presenceEvent.getContent().user_id);
|
||||
user.setPresenceEvent(presenceEvent);
|
||||
client.store.storeUser(user);
|
||||
}
|
||||
client.emit("event", presenceEvent);
|
||||
});
|
||||
}
|
||||
|
||||
// set the pagination token before adding the events in case people
|
||||
// fire off pagination requests in response to the Room.timeline
|
||||
// events.
|
||||
@@ -273,6 +298,31 @@ SyncApi.prototype._peekPoll = function(roomId, token) {
|
||||
timeout: 30 * 1000,
|
||||
from: token
|
||||
}, undefined, 50 * 1000).done(function(res) {
|
||||
|
||||
// We have a problem that we get presence both from /events and /sync
|
||||
// however, /sync only returns presence for users in rooms
|
||||
// you're actually joined to.
|
||||
// in order to be sure to get presence for all of the users in the
|
||||
// peeked room, we handle presence explicitly here. This may result
|
||||
// in duplicate presence events firing for some users, which is a
|
||||
// performance drain, but such is life.
|
||||
// XXX: copypasted from /sync until we can kill this minging v1 stuff.
|
||||
|
||||
res.chunk.filter(function(e) {
|
||||
return e.type === "m.presence";
|
||||
}).map(self.client.getEventMapper()).forEach(function(presenceEvent) {
|
||||
var user = self.client.store.getUser(presenceEvent.getContent().user_id);
|
||||
if (user) {
|
||||
user.setPresenceEvent(presenceEvent);
|
||||
}
|
||||
else {
|
||||
user = createNewUser(self.client, presenceEvent.getContent().user_id);
|
||||
user.setPresenceEvent(presenceEvent);
|
||||
self.client.store.storeUser(user);
|
||||
}
|
||||
self.client.emit("event", presenceEvent);
|
||||
});
|
||||
|
||||
// strip out events which aren't for the given room_id (e.g presence)
|
||||
var events = res.chunk.filter(function(e) {
|
||||
return e.room_id === roomId;
|
||||
@@ -338,7 +388,6 @@ SyncApi.prototype.sync = function() {
|
||||
self._getOrCreateFilter(
|
||||
getFilterName(client.credentials.userId), filter
|
||||
).done(function(filterId) {
|
||||
debuglog("Using existing filter ID %s", filterId);
|
||||
self._sync({ filterId: filterId });
|
||||
}, function(err) {
|
||||
self._startKeepAlives().done(function() {
|
||||
@@ -377,7 +426,7 @@ SyncApi.prototype.stop = function() {
|
||||
*/
|
||||
SyncApi.prototype.retryImmediately = function() {
|
||||
if (!this._connectionReturnedDefer) { return false; }
|
||||
this._startKeepAlives();
|
||||
this._startKeepAlives(0);
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -470,8 +519,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
|
||||
@@ -593,28 +643,62 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) {
|
||||
|
||||
joinObj.timeline = joinObj.timeline || {};
|
||||
|
||||
if (joinObj.timeline.limited) {
|
||||
// nuke the timeline so we don't get holes
|
||||
if (joinObj.isBrandNewRoom) {
|
||||
// set the back-pagination token. Do this *before* adding any
|
||||
// events so that clients can start back-paginating.
|
||||
room.getLiveTimeline().setPaginationToken(
|
||||
joinObj.timeline.prev_batch, EventTimeline.BACKWARDS);
|
||||
}
|
||||
else if (joinObj.timeline.limited) {
|
||||
var limited = true;
|
||||
|
||||
// we've got a limited sync, so we *probably* have a gap in the
|
||||
// timeline, so should reset. But we might have been peeking or
|
||||
// paginating and already have some of the events, in which
|
||||
// case we just want to append any subsequent events to the end
|
||||
// of the existing timeline.
|
||||
//
|
||||
// save the old 'next_batch' token as the
|
||||
// forward-pagination token for the previously-active
|
||||
// timeline.
|
||||
room.currentState.paginationToken = syncToken;
|
||||
self._deregisterStateListeners(room);
|
||||
room.resetLiveTimeline();
|
||||
self._registerStateListeners(room);
|
||||
// This is particularly important in the case that we already have
|
||||
// *all* of the events in the timeline - in that case, if we reset
|
||||
// the timeline, we'll end up with an entirely empty timeline,
|
||||
// which we'll try to paginate but not get any new events (which
|
||||
// will stop us linking the empty timeline into the chain).
|
||||
//
|
||||
for (var i = timelineEvents.length - 1; i >= 0; i--) {
|
||||
var eventId = timelineEvents[i].getId();
|
||||
if (room.getTimelineForEvent(eventId)) {
|
||||
debuglog("Already have event " + eventId + " in limited " +
|
||||
"sync - not resetting");
|
||||
limited = false;
|
||||
|
||||
// we might still be missing some of the events before i;
|
||||
// we don't want to be adding them to the end of the
|
||||
// timeline because that would put them out of order.
|
||||
timelineEvents.splice(0, i);
|
||||
|
||||
// XXX: there's a problem here if the skipped part of the
|
||||
// timeline modifies the state set in stateEvents, because
|
||||
// we'll end up using the state from stateEvents rather
|
||||
// than the later state from timelineEvents. We probably
|
||||
// need to wind stateEvents forward over the events we're
|
||||
// skipping.
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (limited) {
|
||||
// save the old 'next_batch' token as the
|
||||
// forward-pagination token for the previously-active
|
||||
// timeline.
|
||||
room.currentState.paginationToken = syncToken;
|
||||
self._deregisterStateListeners(room);
|
||||
room.resetLiveTimeline(joinObj.timeline.prev_batch);
|
||||
self._registerStateListeners(room);
|
||||
}
|
||||
}
|
||||
|
||||
// we want to set a new pagination token if this is the first time
|
||||
// we've made this room or if we're nuking the timeline
|
||||
var paginationToken = null;
|
||||
if (joinObj.isBrandNewRoom || joinObj.timeline.limited) {
|
||||
paginationToken = joinObj.timeline.prev_batch;
|
||||
}
|
||||
|
||||
self._processRoomEvents(
|
||||
room, stateEvents, timelineEvents, paginationToken
|
||||
);
|
||||
self._processRoomEvents(room, stateEvents, timelineEvents);
|
||||
|
||||
// XXX: should we be adding ephemeralEvents to the timeline?
|
||||
// It feels like that for symmetry with room.addAccountData()
|
||||
@@ -635,25 +719,51 @@ 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); });
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Starts polling the connectivity check endpoint
|
||||
* @param {number} delay How long to delay until the first poll.
|
||||
* defaults to a short, randomised interval (to prevent
|
||||
* tightlooping if /versions succeeds but /sync etc. fail).
|
||||
* @return {promise}
|
||||
*/
|
||||
SyncApi.prototype._startKeepAlives = function() {
|
||||
SyncApi.prototype._startKeepAlives = function(delay) {
|
||||
if (delay === undefined) {
|
||||
delay = 5000 + Math.floor(Math.random() * 5000);
|
||||
}
|
||||
|
||||
if (this._keepAliveTimer !== null) {
|
||||
clearTimeout(this._keepAliveTimer);
|
||||
}
|
||||
this._pokeKeepAlive();
|
||||
var self = this;
|
||||
self._keepAliveTimer = setTimeout(
|
||||
self._pokeKeepAlive.bind(self),
|
||||
delay
|
||||
);
|
||||
if (!this._connectionReturnedDefer) {
|
||||
this._connectionReturnedDefer = q.defer();
|
||||
}
|
||||
@@ -702,16 +812,42 @@ SyncApi.prototype._pokeKeepAlive = function() {
|
||||
*/
|
||||
SyncApi.prototype._getOrCreateFilter = function(filterName, filter) {
|
||||
var client = this.client;
|
||||
|
||||
var filterId = client.store.getFilterIdByName(filterName);
|
||||
var promise = q();
|
||||
|
||||
if (filterId) {
|
||||
// super, just use that.
|
||||
return q(filterId);
|
||||
// check that the existing filter matches our expectations
|
||||
promise = client.getFilter(client.credentials.userId,
|
||||
filterId, true
|
||||
).then(function(existingFilter) {
|
||||
var oldStr = JSON.stringify(existingFilter.getDefinition());
|
||||
var newStr = JSON.stringify(filter.getDefinition());
|
||||
|
||||
if (oldStr == newStr) {
|
||||
// super, just use that.
|
||||
debuglog("Using existing filter ID %s: %s", filterId, oldStr);
|
||||
return q(filterId);
|
||||
}
|
||||
debuglog("Existing filter ID %s: %s; new filter: %s",
|
||||
filterId, oldStr, newStr);
|
||||
return;
|
||||
});
|
||||
}
|
||||
|
||||
// create a filter
|
||||
return client.createFilter(filter.getDefinition()).then(function(createdFilter) {
|
||||
client.store.setFilterIdByName(filterName, createdFilter.filterId);
|
||||
return createdFilter.filterId;
|
||||
return promise.then(function(existingId) {
|
||||
if (existingId) {
|
||||
return existingId;
|
||||
}
|
||||
|
||||
// create a new filter
|
||||
return client.createFilter(filter.getDefinition()
|
||||
).then(function(createdFilter) {
|
||||
debuglog("Created new filter ID %s: %s", createdFilter.filterId,
|
||||
JSON.stringify(createdFilter.getDefinition()));
|
||||
client.store.setFilterIdByName(filterName, createdFilter.filterId);
|
||||
return createdFilter.filterId;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -809,11 +945,9 @@ SyncApi.prototype._resolveInvites = function(room) {
|
||||
* at the *START* of the timeline list if it is supplied.
|
||||
* @param {?MatrixEvent[]} timelineEventList A list of timeline events. Lower index
|
||||
* is earlier in time. Higher index is later.
|
||||
* @param {string=} paginationToken pagination token for going backwards in time.
|
||||
* This should only be set if this is a new room/timeline.
|
||||
*/
|
||||
SyncApi.prototype._processRoomEvents = function(room, stateEventList,
|
||||
timelineEventList, paginationToken) {
|
||||
timelineEventList) {
|
||||
timelineEventList = timelineEventList || [];
|
||||
var client = this.client;
|
||||
// "old" and "current" state are the same initially; they
|
||||
@@ -827,15 +961,12 @@ SyncApi.prototype._processRoomEvents = function(room, stateEventList,
|
||||
);
|
||||
var stateEvents = stateEventList;
|
||||
|
||||
// Set the pagination token BEFORE adding events to the timeline: it's not
|
||||
// unreasonable for clients to call scrollback() in response to Room.timeline
|
||||
// events which addEventsToTimeline will emit-- we want to make sure they use
|
||||
// the right token if and when they do.
|
||||
if (paginationToken) {
|
||||
room.oldState.paginationToken = paginationToken;
|
||||
}
|
||||
|
||||
// set the state of the room to as it was before the timeline executes
|
||||
//
|
||||
// XXX: what if we've already seen (some of) the events in the timeline,
|
||||
// and they modify some of the state set in stateEvents? In that case we'll
|
||||
// end up with the state from stateEvents, instead of the more recent state
|
||||
// from the timeline.
|
||||
room.oldState.setStateEvents(oldStateEvents);
|
||||
room.currentState.setStateEvents(stateEvents);
|
||||
|
||||
@@ -888,7 +1019,7 @@ SyncApi.prototype._updateSyncState = function(newState, data) {
|
||||
*/
|
||||
SyncApi.prototype._onOnline = function() {
|
||||
debuglog("Browser thinks we are back online");
|
||||
this._startKeepAlives();
|
||||
this._startKeepAlives(0);
|
||||
};
|
||||
|
||||
function createNewUser(client, userId) {
|
||||
|
||||
+31
-25
@@ -88,39 +88,41 @@ TimelineWindow.prototype.load = function(initialEventId, initialWindowSize) {
|
||||
var self = this;
|
||||
initialWindowSize = initialWindowSize || 20;
|
||||
|
||||
var prom;
|
||||
// given an EventTimeline, and an event index within it, initialise our
|
||||
// fields so that the event in question is in the middle of the window.
|
||||
var initFields = function(timeline, eventIndex) {
|
||||
var endIndex = Math.min(timeline.getEvents().length,
|
||||
eventIndex + Math.ceil(initialWindowSize / 2));
|
||||
var startIndex = Math.max(0, endIndex - initialWindowSize);
|
||||
self._start = new TimelineIndex(timeline, startIndex - timeline.getBaseIndex());
|
||||
self._end = new TimelineIndex(timeline, endIndex - timeline.getBaseIndex());
|
||||
self._eventCount = endIndex - startIndex;
|
||||
};
|
||||
|
||||
// We avoid delaying the resolution of the promise by a reactor tick if
|
||||
// we already have the data we need, which is important to keep room-switching
|
||||
// feeling snappy.
|
||||
//
|
||||
// TODO: ideally we'd spot getEventTimeline returning a resolved promise and
|
||||
// skip straight to the find-event loop.
|
||||
if (initialEventId) {
|
||||
debuglog("TimelineWindow: initialising for event " + initialEventId);
|
||||
prom = this._client.getEventTimeline(this._room, initialEventId)
|
||||
return this._client.getEventTimeline(this._room, initialEventId)
|
||||
.then(function(tl) {
|
||||
// make sure that our window includes the event
|
||||
for (var i = 0; i < tl.getEvents().length; i++) {
|
||||
if (tl.getEvents()[i].getId() == initialEventId) {
|
||||
return {timeline: tl, index: i};
|
||||
initFields(tl, i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw new Error("getEventTimeline result didn't include requested event");
|
||||
});
|
||||
} else {
|
||||
debuglog("TimelineWindow: initialising with live timeline");
|
||||
|
||||
// start with the most recent events
|
||||
var tl = this._room.getLiveTimeline();
|
||||
prom = q({timeline: tl, index: tl.getEvents().length});
|
||||
initFields(tl, tl.getEvents().length);
|
||||
return q();
|
||||
}
|
||||
|
||||
prom = prom.then(function(v) {
|
||||
var tl = v.timeline, eventIndex = v.index;
|
||||
|
||||
var endIndex = Math.min(tl.getEvents().length,
|
||||
eventIndex + Math.ceil(initialWindowSize / 2));
|
||||
var startIndex = Math.max(0, endIndex - initialWindowSize);
|
||||
self._start = new TimelineIndex(tl, startIndex - tl.getBaseIndex());
|
||||
self._end = new TimelineIndex(tl, endIndex - tl.getBaseIndex());
|
||||
self._eventCount = endIndex - startIndex;
|
||||
});
|
||||
|
||||
return prom;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -246,11 +248,15 @@ TimelineWindow.prototype.paginate = function(direction, size, makeRequest) {
|
||||
// end of timeline
|
||||
return false;
|
||||
}
|
||||
// recurse to advance the index into the results, but before we do, set
|
||||
// makeRequest=false. It's not an absolute given that the paginate
|
||||
// request returned events which we can now use, but we certainly don't
|
||||
// want to get stuck in a tight loop here if things start going wrong.
|
||||
return self.paginate(direction, size, false);
|
||||
|
||||
// recurse to advance the index into the results.
|
||||
//
|
||||
// If we don't get any new events, we want to make sure we keep asking
|
||||
// the server for events for as long as we have a valid pagination
|
||||
// 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);
|
||||
});
|
||||
tl.pendingPaginate = prom;
|
||||
return prom;
|
||||
|
||||
+7
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "0.4.0",
|
||||
"version": "0.5.0",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
@@ -10,7 +10,9 @@
|
||||
"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",
|
||||
"lint": "jshint -c .jshint lib spec && gjslint --unix_mode --disable 0131,0211,0200,0222,0212 --max_line_length 90 -r spec/ -r lib/",
|
||||
"release": "npm run build && mkdir dist/$npm_package_version && uglifyjs -c -m -o dist/$npm_package_version/browser-matrix-$npm_package_version.min.js dist/browser-matrix-dev.js && cp dist/browser-matrix-dev.js dist/$npm_package_version/browser-matrix-$npm_package_version.js"
|
||||
"release": "npm run build && mkdir -p dist/$npm_package_version && uglifyjs -c -m -o dist/$npm_package_version/browser-matrix-$npm_package_version.min.js dist/browser-matrix-dev.js && cp dist/browser-matrix-dev.js dist/$npm_package_version/browser-matrix-$npm_package_version.js",
|
||||
"prepublish": "git rev-parse HEAD > git-revision.txt",
|
||||
"version": "npm run release && git add dist/$npm_package_version"
|
||||
},
|
||||
"repository": {
|
||||
"url": "https://github.com/matrix-org/matrix-js-sdk"
|
||||
@@ -31,6 +33,8 @@
|
||||
"watchify": "^3.2.1",
|
||||
"istanbul": "^0.3.13",
|
||||
"jasmine-node": "^1.14.5",
|
||||
"jshint": "^2.8.0"
|
||||
"jshint": "^2.8.0",
|
||||
"jsdoc": "^3.4.0",
|
||||
"uglifyjs": "^2.4.10"
|
||||
}
|
||||
}
|
||||
|
||||
Executable
+120
@@ -0,0 +1,120 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Script to perform a release of matrix-js-sdk. Performs the steps documented
|
||||
# in RELEASING.md
|
||||
#
|
||||
# Requires githib-changelog-generator; to install, do
|
||||
# pip install git+https://github.com/matrix-org/github-changelog-generator.git
|
||||
|
||||
set -e
|
||||
|
||||
USAGE="$0 [-xz] [-c changelog_file] vX.Y.Z"
|
||||
|
||||
help() {
|
||||
cat <<EOF
|
||||
$USAGE
|
||||
|
||||
-c changelog_file: specify name of file containing changelog
|
||||
-x: skip updating the changelog
|
||||
-z: skip generating the jsdoc
|
||||
EOF
|
||||
}
|
||||
|
||||
skip_changelog=
|
||||
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`
|
||||
|
||||
if [ $# -ne 1 ]; then
|
||||
echo "Usage: $USAGE" >&2
|
||||
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}"
|
||||
rel_branch="release-$tag"
|
||||
|
||||
# we might already be on the release branch, in which case, yay
|
||||
if [ $(git symbolic-ref --short HEAD) != "$rel_branch" ]; then
|
||||
echo "Creating release branch"
|
||||
git checkout -b "$rel_branch"
|
||||
fi
|
||||
|
||||
if [ -z "$skip_changelog" ]; then
|
||||
echo "Generating changelog"
|
||||
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_file)" ]; then
|
||||
echo "Committing updated changelog"
|
||||
git commit "$changelog_file" -m "Prepare changelog for $tag"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Bump package.json, build the dist, and tag
|
||||
echo "npm version"
|
||||
npm version "$release"
|
||||
|
||||
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"
|
||||
fi
|
||||
|
||||
# merge release branch to master
|
||||
echo "updating master branch"
|
||||
git checkout master
|
||||
git pull
|
||||
git merge --ff-only "$rel_branch"
|
||||
|
||||
# push everything to github
|
||||
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
|
||||
@@ -279,6 +279,24 @@ describe("MatrixClient events", function() {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit Session.logged_out on M_UNKNOWN_TOKEN", function(done) {
|
||||
httpBackend.when("GET", "/sync").respond(401, { errcode: 'M_UNKNOWN_TOKEN' });
|
||||
|
||||
var sessionLoggedOutCount = 0;
|
||||
client.on("Session.logged_out", function(event, member) {
|
||||
sessionLoggedOutCount++;
|
||||
});
|
||||
|
||||
client.startClient();
|
||||
|
||||
httpBackend.flush().done(function() {
|
||||
expect(sessionLoggedOutCount).toEqual(
|
||||
1, "Session.logged_out fired wrong number of times"
|
||||
);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -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() {
|
||||
|
||||
|
||||
@@ -165,6 +165,7 @@ describe("MatrixClient room timelines", function() {
|
||||
body: "I am a fish", user: userId, room: roomId
|
||||
});
|
||||
ev.event_id = eventId;
|
||||
ev.unsigned = {transaction_id: "txn1"};
|
||||
setNextSyncData([ev]);
|
||||
|
||||
client.on("sync", function(state) {
|
||||
@@ -194,6 +195,7 @@ describe("MatrixClient room timelines", function() {
|
||||
body: "I am a fish", user: userId, room: roomId
|
||||
});
|
||||
ev.event_id = eventId;
|
||||
ev.unsigned = {transaction_id: "txn1"};
|
||||
setNextSyncData([ev]);
|
||||
|
||||
client.on("sync", function(state) {
|
||||
@@ -201,8 +203,7 @@ describe("MatrixClient room timelines", function() {
|
||||
var room = client.getRoom(roomId);
|
||||
var promise = client.sendTextMessage(roomId, "I am a fish", "txn1");
|
||||
httpBackend.flush("/sync", 1).done(function() {
|
||||
// expect 3rd msg, it doesn't know this is the request is just did
|
||||
expect(room.timeline.length).toEqual(3);
|
||||
expect(room.timeline.length).toEqual(2);
|
||||
httpBackend.flush("/txn1", 1);
|
||||
promise.done(function() {
|
||||
expect(room.timeline.length).toEqual(2);
|
||||
@@ -251,7 +252,23 @@ describe("MatrixClient room timelines", function() {
|
||||
});
|
||||
|
||||
it("should set the right event.sender values", function(done) {
|
||||
// make an m.room.member event with prev_content
|
||||
// We're aiming for an eventual timeline of:
|
||||
//
|
||||
// 'Old Alice' joined the room
|
||||
// <Old Alice> I'm old alice
|
||||
// @alice:localhost changed their name from 'Old Alice' to 'Alice'
|
||||
// <Alice> I'm alice
|
||||
// ------^ /messages results above this point, /sync result below
|
||||
// <Bob> hello
|
||||
|
||||
// make an m.room.member event for alice's join
|
||||
var joinMshipEvent = utils.mkMembership({
|
||||
mship: "join", user: userId, room: roomId, name: "Old Alice",
|
||||
url: null
|
||||
});
|
||||
|
||||
// make an m.room.member event with prev_content for alice's nick
|
||||
// change
|
||||
var oldMshipEvent = utils.mkMembership({
|
||||
mship: "join", user: userId, room: roomId, name: userName,
|
||||
url: "mxc://some/url"
|
||||
@@ -262,7 +279,8 @@ describe("MatrixClient room timelines", function() {
|
||||
membership: "join"
|
||||
};
|
||||
|
||||
// set the list of events to return on scrollback
|
||||
// set the list of events to return on scrollback (/messages)
|
||||
// N.B. synapse returns /messages in reverse chronological order
|
||||
sbEvents = [
|
||||
utils.mkMessage({
|
||||
user: userId, room: roomId, msg: "I'm alice"
|
||||
@@ -270,19 +288,23 @@ describe("MatrixClient room timelines", function() {
|
||||
oldMshipEvent,
|
||||
utils.mkMessage({
|
||||
user: userId, room: roomId, msg: "I'm old alice"
|
||||
})
|
||||
}),
|
||||
joinMshipEvent,
|
||||
];
|
||||
|
||||
client.on("sync", function(state) {
|
||||
if (state !== "PREPARED") { return; }
|
||||
var room = client.getRoom(roomId);
|
||||
// sync response
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
|
||||
client.scrollback(room).done(function() {
|
||||
expect(room.timeline.length).toEqual(4);
|
||||
var oldMsg = room.timeline[0];
|
||||
expect(room.timeline.length).toEqual(5);
|
||||
var joinMsg = room.timeline[0];
|
||||
expect(joinMsg.sender.name).toEqual("Old Alice");
|
||||
var oldMsg = room.timeline[1];
|
||||
expect(oldMsg.sender.name).toEqual("Old Alice");
|
||||
var newMsg = room.timeline[2];
|
||||
var newMsg = room.timeline[3];
|
||||
expect(newMsg.sender.name).toEqual(userName);
|
||||
done();
|
||||
});
|
||||
@@ -512,5 +534,31 @@ describe("MatrixClient room timelines", function() {
|
||||
});
|
||||
httpBackend.flush("/sync", 1);
|
||||
});
|
||||
|
||||
it("should emit a 'Room.timelineReset' event", function(done) {
|
||||
var eventData = [
|
||||
utils.mkMessage({user: userId, room: roomId}),
|
||||
];
|
||||
setNextSyncData(eventData);
|
||||
NEXT_SYNC_DATA.rooms.join[roomId].timeline.limited = true;
|
||||
|
||||
client.on("sync", function(state) {
|
||||
if (state !== "PREPARED") { return; }
|
||||
var room = client.getRoom(roomId);
|
||||
|
||||
var emitCount = 0;
|
||||
client.on("Room.timelineReset", function(emitRoom) {
|
||||
expect(emitRoom).toEqual(room);
|
||||
emitCount++;
|
||||
});
|
||||
|
||||
httpBackend.flush("/messages", 1);
|
||||
httpBackend.flush("/sync", 1).done(function() {
|
||||
expect(emitCount).toEqual(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
httpBackend.flush("/sync", 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ var sdk = require("../..");
|
||||
var HttpBackend = require("../mock-request");
|
||||
var utils = require("../test-utils");
|
||||
var MatrixEvent = sdk.MatrixEvent;
|
||||
var EventTimeline = sdk.EventTimeline;
|
||||
|
||||
describe("MatrixClient syncing", function() {
|
||||
var baseUrl = "http://localhost.or.something";
|
||||
@@ -421,6 +422,101 @@ describe("MatrixClient syncing", function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe("timeline", function() {
|
||||
beforeEach(function() {
|
||||
var syncData = {
|
||||
next_batch: "batch_token",
|
||||
rooms: {
|
||||
join: {},
|
||||
},
|
||||
};
|
||||
syncData.rooms.join[roomOne] = {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: roomOne, user: otherUserId, msg: "hello"
|
||||
}),
|
||||
],
|
||||
prev_batch: "pagTok",
|
||||
},
|
||||
};
|
||||
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
client.startClient();
|
||||
httpBackend.flush();
|
||||
});
|
||||
|
||||
it("should set the back-pagination token on new rooms", function(done) {
|
||||
var syncData = {
|
||||
next_batch: "batch_token",
|
||||
rooms: {
|
||||
join: {},
|
||||
},
|
||||
};
|
||||
syncData.rooms.join[roomTwo] = {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: roomTwo, user: otherUserId, msg: "roomtwo"
|
||||
}),
|
||||
],
|
||||
prev_batch: "roomtwotok",
|
||||
},
|
||||
};
|
||||
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
httpBackend.flush().then(function() {
|
||||
var room = client.getRoom(roomTwo);
|
||||
var tok = room.getLiveTimeline()
|
||||
.getPaginationToken(EventTimeline.BACKWARDS);
|
||||
expect(tok).toEqual("roomtwotok");
|
||||
done();
|
||||
}).catch(utils.failTest).done();
|
||||
});
|
||||
|
||||
it("should set the back-pagination token on gappy syncs", function(done) {
|
||||
var syncData = {
|
||||
next_batch: "batch_token",
|
||||
rooms: {
|
||||
join: {},
|
||||
},
|
||||
};
|
||||
syncData.rooms.join[roomOne] = {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: roomOne, user: otherUserId, msg: "world"
|
||||
}),
|
||||
],
|
||||
limited: true,
|
||||
prev_batch: "newerTok",
|
||||
},
|
||||
};
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
var resetCallCount = 0;
|
||||
// the token should be set *before* timelineReset is emitted
|
||||
client.on("Room.timelineReset", function(room) {
|
||||
resetCallCount++;
|
||||
|
||||
var tl = room.getLiveTimeline();
|
||||
expect(tl.getEvents().length).toEqual(0);
|
||||
var tok = tl.getPaginationToken(EventTimeline.BACKWARDS);
|
||||
expect(tok).toEqual("newerTok");
|
||||
});
|
||||
|
||||
httpBackend.flush().then(function() {
|
||||
var room = client.getRoom(roomOne);
|
||||
var tl = room.getLiveTimeline();
|
||||
expect(tl.getEvents().length).toEqual(1);
|
||||
expect(resetCallCount).toEqual(1);
|
||||
done();
|
||||
}).catch(utils.failTest).done();
|
||||
});
|
||||
});
|
||||
|
||||
describe("receipts", function() {
|
||||
var syncData = {
|
||||
rooms: {
|
||||
@@ -512,4 +608,82 @@ describe("MatrixClient syncing", function() {
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
describe("syncLeftRooms", function() {
|
||||
beforeEach(function(done) {
|
||||
client.startClient();
|
||||
|
||||
httpBackend.flush().then(function() {
|
||||
// the /sync call from syncLeftRooms ends up in the request
|
||||
// queue behind the call from the running client; add a response
|
||||
// to flush the client's one out.
|
||||
httpBackend.when("GET", "/sync").respond(200, {});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should create and use an appropriate filter", function(done) {
|
||||
httpBackend.when("POST", "/filter").check(function(req) {
|
||||
expect(req.data).toEqual({
|
||||
room: { timeline: {limit: 1},
|
||||
include_leave: true }});
|
||||
}).respond(200, { filter_id: "another_id" });
|
||||
|
||||
httpBackend.when("GET", "/sync").check(function(req) {
|
||||
expect(req.queryParams.filter).toEqual("another_id");
|
||||
done();
|
||||
}).respond(200, {});
|
||||
|
||||
client.syncLeftRooms();
|
||||
|
||||
// first flush the filter request; this will make syncLeftRooms
|
||||
// make its /sync call
|
||||
httpBackend.flush("/filter").then(function() {
|
||||
// flush the syncs
|
||||
return httpBackend.flush();
|
||||
}).catch(utils.failTest);
|
||||
});
|
||||
|
||||
it("should set the back-pagination token on left rooms", function(done) {
|
||||
var syncData = {
|
||||
next_batch: "batch_token",
|
||||
rooms: {
|
||||
leave: {}
|
||||
},
|
||||
};
|
||||
|
||||
syncData.rooms.leave[roomTwo] = {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: roomTwo, user: otherUserId, msg: "hello"
|
||||
}),
|
||||
],
|
||||
prev_batch: "pagTok",
|
||||
},
|
||||
};
|
||||
|
||||
httpBackend.when("POST", "/filter").respond(200, {
|
||||
filter_id: "another_id"
|
||||
});
|
||||
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
client.syncLeftRooms().then(function() {
|
||||
var room = client.getRoom(roomTwo);
|
||||
var tok = room.getLiveTimeline().getPaginationToken(
|
||||
EventTimeline.BACKWARDS);
|
||||
|
||||
expect(tok).toEqual("pagTok");
|
||||
done();
|
||||
}).catch(utils.failTest).done();
|
||||
|
||||
// first flush the filter request; this will make syncLeftRooms
|
||||
// make its /sync call
|
||||
httpBackend.flush("/filter").then(function() {
|
||||
return httpBackend.flush();
|
||||
}).catch(utils.failTest);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -166,12 +166,15 @@ describe("MatrixClient", function() {
|
||||
});
|
||||
});
|
||||
|
||||
it("should not POST /filter if a filter already exists", function(done) {
|
||||
it("should not POST /filter if a matching filter already exists", function(done) {
|
||||
httpLookups = [];
|
||||
httpLookups.push(PUSH_RULES_RESPONSE);
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
var filterId = "ehfewf";
|
||||
store.getFilterIdByName.andReturn(filterId);
|
||||
var filter = new sdk.Filter(0, filterId);
|
||||
filter.setDefinition({"room": {"timeline": {"limit": 8}}});
|
||||
store.getFilter.andReturn(filter);
|
||||
client.startClient();
|
||||
|
||||
client.on("sync", function syncListener(state) {
|
||||
@@ -220,6 +223,7 @@ describe("MatrixClient", function() {
|
||||
if (state === "ERROR" && httpLookups.length > 0) {
|
||||
expect(httpLookups.length).toEqual(2);
|
||||
expect(client.retryImmediately()).toBe(true);
|
||||
jasmine.Clock.tick(1);
|
||||
} else if (state === "PREPARED" && httpLookups.length === 0) {
|
||||
client.removeListener("sync", syncListener);
|
||||
done();
|
||||
@@ -245,6 +249,7 @@ describe("MatrixClient", function() {
|
||||
expect(client.retryImmediately()).toBe(
|
||||
true, "retryImmediately returned false"
|
||||
);
|
||||
jasmine.Clock.tick(1);
|
||||
} else if (state === "SYNCING" && httpLookups.length === 0) {
|
||||
client.removeListener("sync", syncListener);
|
||||
done();
|
||||
@@ -266,6 +271,7 @@ describe("MatrixClient", function() {
|
||||
if (state === "ERROR" && httpLookups.length > 0) {
|
||||
expect(httpLookups.length).toEqual(3);
|
||||
expect(client.retryImmediately()).toBe(true);
|
||||
jasmine.Clock.tick(1);
|
||||
} else if (state === "PREPARED" && httpLookups.length === 0) {
|
||||
client.removeListener("sync", syncListener);
|
||||
done();
|
||||
@@ -296,8 +302,8 @@ describe("MatrixClient", function() {
|
||||
client.removeListener("sync", syncListener);
|
||||
done();
|
||||
}
|
||||
// standard retry time is 4s
|
||||
jasmine.Clock.tick(4001);
|
||||
// standard retry time is 5 to 10 seconds
|
||||
jasmine.Clock.tick(10000);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -279,4 +279,87 @@ describe("RoomState", function() {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("maySendStateEvent", function() {
|
||||
it("should say non-joined members may not send state",
|
||||
function() {
|
||||
expect(state.maySendStateEvent(
|
||||
'm.room.name', "@nobody:nowhere"
|
||||
)).toEqual(false);
|
||||
});
|
||||
|
||||
it("should say any member may send state with no power level event",
|
||||
function() {
|
||||
expect(state.maySendStateEvent('m.room.name', userA)).toEqual(true);
|
||||
});
|
||||
|
||||
it("should say members with power >=50 may send state with power level event " +
|
||||
"but no state default",
|
||||
function() {
|
||||
var powerLevelEvent = {
|
||||
type: "m.room.power_levels", room: roomId, user: userA, event: true,
|
||||
content: {
|
||||
users_default: 10,
|
||||
// state_default: 50, "intentionally left blank"
|
||||
events_default: 25,
|
||||
users: {
|
||||
}
|
||||
}
|
||||
};
|
||||
powerLevelEvent.content.users[userA] = 50;
|
||||
|
||||
state.setStateEvents([utils.mkEvent(powerLevelEvent)]);
|
||||
|
||||
expect(state.maySendStateEvent('m.room.name', userA)).toEqual(true);
|
||||
expect(state.maySendStateEvent('m.room.name', userB)).toEqual(false);
|
||||
});
|
||||
|
||||
it("should obey state_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] = 30;
|
||||
powerLevelEvent.content.users[userB] = 29;
|
||||
|
||||
state.setStateEvents([utils.mkEvent(powerLevelEvent)]);
|
||||
|
||||
expect(state.maySendStateEvent('m.room.name', userA)).toEqual(true);
|
||||
expect(state.maySendStateEvent('m.room.name', 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": 76
|
||||
},
|
||||
users_default: 10,
|
||||
state_default: 50,
|
||||
events_default: 25,
|
||||
users: {
|
||||
}
|
||||
}
|
||||
};
|
||||
powerLevelEvent.content.users[userA] = 80;
|
||||
powerLevelEvent.content.users[userB] = 50;
|
||||
|
||||
state.setStateEvents([utils.mkEvent(powerLevelEvent)]);
|
||||
|
||||
expect(state.maySendStateEvent('m.room.name', userA)).toEqual(true);
|
||||
expect(state.maySendStateEvent('m.room.name', userB)).toEqual(true);
|
||||
|
||||
expect(state.maySendStateEvent('m.room.other_thing', userA)).toEqual(true);
|
||||
expect(state.maySendStateEvent('m.room.other_thing', userB)).toEqual(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+255
-44
@@ -4,6 +4,7 @@ var Room = sdk.Room;
|
||||
var RoomState = sdk.RoomState;
|
||||
var MatrixEvent = sdk.MatrixEvent;
|
||||
var EventStatus = sdk.EventStatus;
|
||||
var EventTimeline = sdk.EventTimeline;
|
||||
var utils = require("../test-utils");
|
||||
|
||||
describe("Room", function() {
|
||||
@@ -327,8 +328,137 @@ describe("Room", function() {
|
||||
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.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.addEventsToTimeline([remoteEvent]);
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
|
||||
expect(callCount).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
var resetTimelineTests = function(timelineSupport) {
|
||||
var events = [
|
||||
utils.mkMessage({
|
||||
room: roomId, user: userA, msg: "A message", event: true
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: "m.room.name", room: roomId, user: userA, event: true,
|
||||
content: { name: "New Room Name" }
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: "m.room.name", room: roomId, user: userA, event: true,
|
||||
content: { name: "Another New Name" }
|
||||
}),
|
||||
];
|
||||
|
||||
beforeEach(function() {
|
||||
room = new Room(roomId, {timelineSupport: timelineSupport});
|
||||
});
|
||||
|
||||
it("should copy state from previous timeline", function() {
|
||||
room.addEventsToTimeline([events[0], events[1]]);
|
||||
expect(room.getLiveTimeline().getEvents().length).toEqual(2);
|
||||
room.resetLiveTimeline();
|
||||
|
||||
room.addEventsToTimeline([events[2]]);
|
||||
var oldState = room.getLiveTimeline().getState(EventTimeline.BACKWARDS);
|
||||
var newState = room.getLiveTimeline().getState(EventTimeline.FORWARDS);
|
||||
expect(room.getLiveTimeline().getEvents().length).toEqual(1);
|
||||
expect(oldState.getStateEvents("m.room.name", "")).toEqual(events[1]);
|
||||
expect(newState.getStateEvents("m.room.name", "")).toEqual(events[2]);
|
||||
});
|
||||
|
||||
it("should reset the legacy timeline fields", function() {
|
||||
room.addEventsToTimeline([events[0], events[1]]);
|
||||
expect(room.timeline.length).toEqual(2);
|
||||
room.resetLiveTimeline();
|
||||
|
||||
room.addEventsToTimeline([events[2]]);
|
||||
var newLiveTimeline = room.getLiveTimeline();
|
||||
expect(room.timeline).toEqual(newLiveTimeline.getEvents());
|
||||
expect(room.oldState).toEqual(
|
||||
newLiveTimeline.getState(EventTimeline.BACKWARDS));
|
||||
expect(room.currentState).toEqual(
|
||||
newLiveTimeline.getState(EventTimeline.FORWARDS));
|
||||
});
|
||||
|
||||
it("should emit Room.timelineReset event and set the correct " +
|
||||
"pagination token", function() {
|
||||
var callCount = 0;
|
||||
room.on("Room.timelineReset", function(emitRoom) {
|
||||
callCount += 1;
|
||||
expect(emitRoom).toEqual(room);
|
||||
|
||||
// make sure that the pagination token has been set before the
|
||||
// event is emitted.
|
||||
var tok = emitRoom.getLiveTimeline()
|
||||
.getPaginationToken(EventTimeline.BACKWARDS);
|
||||
|
||||
expect(tok).toEqual("pagToken");
|
||||
});
|
||||
room.resetLiveTimeline("pagToken");
|
||||
expect(callCount).toEqual(1);
|
||||
});
|
||||
|
||||
it("should " + (timelineSupport ? "remember" : "forget") +
|
||||
" old timelines", function() {
|
||||
room.addEventsToTimeline([events[0]]);
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
var firstLiveTimeline = room.getLiveTimeline();
|
||||
room.resetLiveTimeline();
|
||||
|
||||
var tl = room.getTimelineForEvent(events[0].getId());
|
||||
expect(tl).toBe(timelineSupport ? firstLiveTimeline : null);
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
describe("resetLiveTimeline with timelinesupport enabled",
|
||||
resetTimelineTests.bind(null, true));
|
||||
describe("resetLiveTimeline with timelinesupport disabled",
|
||||
resetTimelineTests.bind(null, false));
|
||||
|
||||
describe("compareEventOrdering", function() {
|
||||
beforeEach(function() {
|
||||
room = new Room(roomId, {timelineSupport: true});
|
||||
@@ -498,11 +628,15 @@ describe("Room", function() {
|
||||
}, event: true
|
||||
});
|
||||
};
|
||||
var addMember = function(userId, state) {
|
||||
var addMember = function(userId, state, opts) {
|
||||
if (!state) { state = "join"; }
|
||||
stateLookup["m.room.member$" + userId] = utils.mkMembership({
|
||||
room: roomId, mship: state, user: userId, skey: userId, event: true
|
||||
});
|
||||
opts = opts || {};
|
||||
opts.room = roomId;
|
||||
opts.mship = state;
|
||||
opts.user = opts.user || userId;
|
||||
opts.skey = userId;
|
||||
opts.event = true;
|
||||
stateLookup["m.room.member$" + userId] = utils.mkMembership(opts);
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
@@ -528,14 +662,29 @@ describe("Room", function() {
|
||||
var members = [];
|
||||
for (var i = 0; i < memberEvents.length; i++) {
|
||||
members.push({
|
||||
// not interested in user ID vs display name semantics.
|
||||
// That should be tested in RoomMember UTs.
|
||||
name: memberEvents[i].getSender(),
|
||||
userId: memberEvents[i].getSender()
|
||||
name: memberEvents[i].event.content &&
|
||||
memberEvents[i].event.content.displayname ?
|
||||
memberEvents[i].event.content.displayname :
|
||||
memberEvents[i].getStateKey(),
|
||||
userId: memberEvents[i].getStateKey(),
|
||||
events: { member: memberEvents[i] }
|
||||
});
|
||||
}
|
||||
return members;
|
||||
});
|
||||
room.currentState.getMember.andCallFake(function(userId) {
|
||||
var memberEvent = room.currentState.getStateEvents(
|
||||
"m.room.member", userId
|
||||
);
|
||||
return {
|
||||
name: memberEvent.event.content &&
|
||||
memberEvent.event.content.displayname ?
|
||||
memberEvent.event.content.displayname :
|
||||
memberEvent.getStateKey(),
|
||||
userId: memberEvent.getStateKey(),
|
||||
events: { member: memberEvent }
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
describe("Room.recalculate => Stripped State Events", function() {
|
||||
@@ -655,7 +804,7 @@ describe("Room", function() {
|
||||
it("should show the other user's name for private" +
|
||||
" (invite join_rules) rooms if you are invited to it.", function() {
|
||||
setJoinRule("invite");
|
||||
addMember(userA, "invite");
|
||||
addMember(userA, "invite", {user: userB});
|
||||
addMember(userB);
|
||||
room.recalculate(userA);
|
||||
var name = room.name;
|
||||
@@ -728,6 +877,27 @@ describe("Room", function() {
|
||||
expect(name).toEqual("Empty room");
|
||||
});
|
||||
|
||||
it("should return 'Invite from [inviter display name] if state event " +
|
||||
"available",
|
||||
function() {
|
||||
setJoinRule("invite");
|
||||
addMember(userA, 'join', {name: "Alice"});
|
||||
addMember(userB, "invite", {user: userA});
|
||||
room.recalculate(userB);
|
||||
var name = room.name;
|
||||
expect(name).toEqual("Invite from Alice");
|
||||
});
|
||||
|
||||
it("should return inviter mxid if display name not available",
|
||||
function() {
|
||||
setJoinRule("invite");
|
||||
addMember(userA);
|
||||
addMember(userB, "invite", {user: userA});
|
||||
room.recalculate(userB);
|
||||
var name = room.name;
|
||||
expect(name).toEqual("Invite from " + userA);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -973,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
|
||||
@@ -988,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.addEvents([eventA]);
|
||||
room.addPendingEvent(eventB, "TXN1");
|
||||
room.addEvents([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"
|
||||
});
|
||||
@@ -1008,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.addEvents([eventA]);
|
||||
room.addPendingEvent(eventB, "TXN1");
|
||||
room.addEvents([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);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user