Compare commits

..

132 Commits

Author SHA1 Message Date
Richard van der Hoff 9d8973bf1f 0.5.0 2016-03-22 17:56:32 +00:00
Richard van der Hoff 7f95237e02 Prepare changelog for v0.5.0 2016-03-22 17:56:05 +00:00
Matthew Hodgson e1415d9829 Merge pull request #117 from matrix-org/matthew/roomlist
get/setRoomVisibility API
2016-03-22 12:30:18 +00:00
Richard van der Hoff 19a12b3c79 Merge pull request #115 from matrix-org/rav/txnid_clashes
Include a counter in generated transaction IDs
2016-03-22 12:20:02 +00:00
Matthew Hodgson bec41e4f94 incorporate review 2016-03-22 10:20:02 +00:00
Matthew Hodgson 5f177aeec4 get/setRoomVisibility API 2016-03-22 00:55:53 +00:00
Matthew Hodgson b422916452 add to existing users if present, to avoid destroying presence data 2016-03-21 18:15:55 +00:00
Matthew Hodgson 10378c0e7f Merge pull request #116 from matrix-org/matthew/fix-invite-picker-info
update store user metadata based on membership events rather than presence
2016-03-21 16:12:42 +00:00
Matthew Hodgson fba4d5fb0a Merge pull request #114 from matrix-org/matthew/stop-peeking
API to stop peeking
2016-03-21 16:12:29 +00:00
Matthew Hodgson 77101823f5 track kicked rooms correctly 2016-03-19 02:18:37 +00:00
Matthew Hodgson 15bc608368 presence no longer returns profile data, so we have to update our store's users based on membership events instead 2016-03-19 01:45:10 +00:00
Richard van der Hoff dfc4b34d09 Include a counter in generated transaction IDs
Fixes a flaky test which sometimes failed due to sending two events in the same
millisecond.
2016-03-18 21:32:15 +00:00
Richard van der Hoff ad9daecbd4 Pass the right options into SyncApi when peeking
When we peek into a room, we create its Room object. We need to make sure it is
created with the same options as we would if it were created via the /sync
calls.

Save the options passed in when startClient is called, and then pass them into
the SyncApi each time we create it.
2016-03-18 20:59:23 +00:00
Matthew Hodgson d29302716d oops 2016-03-18 19:25:22 +00:00
Matthew Hodgson 6c7d13f8ce API to stop peeking 2016-03-18 19:22:34 +00:00
Richard van der Hoff e15a2d138c Merge pull request #112 from matrix-org/rav/cancel_send
Support for cancelling pending events
2016-03-18 16:17:44 +00:00
Richard van der Hoff 8bc9c19278 Merge pull request #111 from matrix-org/rav/pending_event_list
Implement 'pendingEventList'
2016-03-18 16:17:30 +00:00
David Baker dd86fade11 Merge pull request #113 from matrix-org/dbkr/threepid_lookup
Add a method to the js sdk to look up 3pids on the ID server.
2016-03-18 15:54:43 +00:00
David Baker ba1991aa8f Add more docs :) 2016-03-18 15:54:19 +00:00
David Baker f4fd8d9ba6 Add a method to the js sdk to look up 3pids on the ID server. 2016-03-18 15:15:10 +00:00
Richard van der Hoff 02be0f659a Support for cancelling pending events
Implement client.cancelPendingEvent which will cancel queued or not_sent events
2016-03-17 22:15:46 +00:00
Richard van der Hoff c7be310bdf Fix addPendingEvent invocation in unit test 2016-03-17 22:10:40 +00:00
Richard van der Hoff 55d8f56f98 update docs 2016-03-17 17:53:20 +00:00
Richard van der Hoff ab35fff9e8 Implement 'pendingEventList'
The existing 'pendingEventOrdering'=='end' semantics had been substantially
broken by the introduction of timelines and gappy syncs: after a gappy
sync, pending events would get stuck in the old timeline section. (Part of
https://github.com/vector-im/vector-web/issues/1120).
2016-03-17 17:05:23 +00:00
Richard van der Hoff fdbc7a3112 Merge pull request #110 from matrix-org/rav/refactor_remote_echo
Refactor transmitted-messages code
2016-03-17 16:40:47 +00:00
Richard van der Hoff 3c6bd4774d Refactor transmitted-messages code
This is some preparatory work for fixing up the problems with te timeline
ordering of unsent messages
(https://github.com/vector-im/vector-web/issues/1120). The functional changes
here should be minimal (bar an extra `Room.localEchoUpdated` when the local
echo is first added to the timeline).

Give `MatrixClient.sendEvent` its own entry point `Room.addPendingMessage`
instead of pushing it through `addEventsToTimeline`; this considerably
simplifies the implementation of the latter and also means that we can contain
the `_txnId` ming to MatrixClient.

Move the code which deals with a successful `/send` response from
`MatrixClient` into `Room.updatePendingEvent`, since it involves fiddling with
the innards of the Room.

Also adds a new EventStatus 'SENT' for events which have been successfully sent
but whose remote echo we still haven't received.
2016-03-17 14:26:36 +00:00
Richard van der Hoff a2861c5781 Merge pull request #109 from matrix-org/rav/log_sync_error_stack
Log the stack when we get a sync error
2016-03-17 14:24:07 +00:00
Richard van der Hoff eaf3fe16eb sync error: Don't log the exception twice
If we have e.stack, then it will include the description of the exception.
2016-03-17 12:05:01 +00:00
Richard van der Hoff 963eaf7ec7 Log the stack when we get a sync error
If we have the stack for an exception in the /sync loop, we should log it.
2016-03-17 11:54:43 +00:00
Richard van der Hoff e6e5b9b748 release.sh: fix -z option 2016-03-17 01:33:51 +00:00
Richard van der Hoff 9ad031c857 Make changelog file and jsdoc generation switchable 2016-03-17 01:27:48 +00:00
Richard van der Hoff a0d465cb34 Merge master to develop after release 2016-03-17 01:12:39 +00:00
Richard van der Hoff 2dcf5227f0 Merge remote-tracking branch 'origin/master' into develop 2016-03-17 01:12:18 +00:00
Richard van der Hoff 518e92027c Add missing "Changes in" to changelog 2016-03-17 01:07:34 +00:00
Matthew Hodgson ebc95667b8 workaround for unicode regexp matches - https://github.com/vector-im/vector-web/issues/568 2016-03-17 01:02:50 +00:00
Richard van der Hoff ad5d07caf8 0.4.2 2016-03-17 00:58:33 +00:00
Richard van der Hoff b2d7abc0a1 Prepare changelog for v0.4.2 2016-03-17 00:58:16 +00:00
Richard van der Hoff cc475e6392 add jsdoc as dev dependency 2016-03-17 00:58:16 +00:00
Richard van der Hoff e4c6717bd5 Don't fail release if dist dir already exists 2016-03-17 00:58:16 +00:00
Richard van der Hoff 53f813207e Add option to skip changelog generation 2016-03-17 00:45:39 +00:00
Richard van der Hoff 873fde27ac Don't error if changelog is unchanged 2016-03-17 00:33:01 +00:00
Richard van der Hoff 8d9d638953 release.sh: fix 'read' syntax 2016-03-17 00:22:37 +00:00
Richard van der Hoff 2f93490054 Don't create release branch if we're already there 2016-03-17 00:19:30 +00:00
Richard van der Hoff e22efc9dd5 release.sh: chmod +x 2016-03-16 23:40:44 +00:00
Richard van der Hoff e7ac80cf2b Script to do releases 2016-03-16 23:12:38 +00:00
Richard van der Hoff 4436087777 Use npm version to do release stuff 2016-03-16 23:12:38 +00:00
Matthew Hodgson f7bc11361c trivially add content.currently_active in m.presence events. 2016-03-16 22:35:55 +00:00
Matthew Hodgson a68b61dafe oops, revert accidental merge 2016-03-16 17:33:30 +00:00
Matthew Hodgson 84c9876b3a if synapse handed us profile data in the leave event, then use it. unbreaks overzealous tests 2016-03-16 17:32:47 +00:00
Matthew Hodgson de864c489a make sure we show display names & avatars on parts, and use the right type of content for displaynames for member events in general. fixes https://github.com/vector-im/vector-web/issues/1140 and https://github.com/vector-im/vector-web/issues/873 and a bunch more 2016-03-16 17:32:47 +00:00
Matthew Hodgson 2c277f7d96 Merge pull request #108 from matrix-org/matthew/fix-displaynames
Matthew/fix displaynames
2016-03-16 17:31:41 +00:00
Kegan Dougal d0560f594d Set the right .sender value for m.room.member events 2016-03-16 17:18:33 +00:00
Matthew Hodgson 60b6310494 typo 2016-03-16 16:47:25 +00:00
Matthew Hodgson abd27f9b75 failing test for https://github.com/vector-im/vector-web/issues/1140 2016-03-16 16:45:23 +00:00
Matthew Hodgson 3d316959f9 Revert this as it just doesn't work - our events are always m.room.members at this point 2016-03-16 16:44:22 +00:00
Matthew Hodgson 8aa3b79501 Merge pull request #107 from matrix-org/revert-106-matthew/fix-displaynames
Revert "make sure we show display names & avatars on parts, and use the right…"
2016-03-16 14:49:38 +00:00
Matthew Hodgson f35409700a Revert "make sure we show display names & avatars on parts, and use the right…" 2016-03-16 14:49:29 +00:00
Matthew Hodgson b009739b9e Merge pull request #106 from matrix-org/matthew/fix-displaynames
make sure we show display names & avatars on parts, and use the right…
2016-03-16 14:39:59 +00:00
Matthew Hodgson f007af741e Merge pull request #104 from matrix-org/matthew/may-client-send-state
Add RoomState.mayClientSendStateEvent()
2016-03-16 14:35:27 +00:00
Matthew Hodgson 3db4d9488b oops, normal events should use the chronologically earlier content, but membership changes should use the current content. 2016-03-16 14:31:03 +00:00
Matthew Hodgson 6b0fa84697 if synapse handed us profile data in the leave event, then use it. unbreaks overzealous tests 2016-03-16 14:14:14 +00:00
Matthew Hodgson 98b0cf2560 make sure we show display names & avatars on parts, and use the right type of content for displaynames for member events in general. fixes https://github.com/vector-im/vector-web/issues/1140 and https://github.com/vector-im/vector-web/issues/873 and a bunch more 2016-03-16 13:51:55 +00:00
Matthew Hodgson 372759b6e4 fix lint 2016-03-16 13:43:38 +00:00
Matthew Hodgson ec29b4ffeb Add RoomState.mayClientSendStateEvent() 2016-03-16 13:08:36 +00:00
Matthew Hodgson 95494933fd Merge pull request #103 from matrix-org/matthew/peek-presence
make presence work when peeking.
2016-03-16 11:55:25 +00:00
Matthew Hodgson 6fff29c07b oops, that map should be a forEach 2016-03-16 11:54:56 +00:00
David Baker 6f7ed93b87 Merge pull request #100 from matrix-org/dbkr/session_logged_out
Add Session.logged_out event
2016-03-16 10:44:01 +00:00
David Baker 8e903c0531 Merge pull request #94 from matrix-org/dbkr/may_send_state_event
Add maySendStateEvent method, ported from react-sdk (but fixed).
2016-03-16 10:39:55 +00:00
David Baker b90984a7f6 Use member.powerLevel instead of duplicating the user power level calculation. 2016-03-16 10:38:16 +00:00
David Baker 57006b7366 Check member hasn't left the room 2016-03-16 10:35:29 +00:00
Matthew Hodgson db9ba52873 make presence work when peeking. fixes https://github.com/vector-im/vector-web/issues/780 2016-03-15 21:50:18 +00:00
Richard van der Hoff 08b49c733a Merge pull request #101 from matrix-org/rav/remove_crypto_specialcase
Clean up a codepath that was only used for crypto messages
2016-03-15 17:26:54 +00:00
David Baker 0f38764709 No point throwing the exception if we return the original promise 2016-03-15 16:17:41 +00:00
Richard van der Hoff 6040b50ceb Fix another unit test
We ought to set the transaction_id in this test too
2016-03-15 15:49:21 +00:00
Richard van der Hoff b88a207bde Fix broken unit test
Fix broken unit tests which expected echoes to get matched up when
transaction_ids weren't set
2016-03-15 15:39:29 +00:00
Richard van der Hoff 07bbe358ea Clean up a codepath that was only used for crypto messages
Transmission of encrypted messages was happening somewhat differently to
normal messages. In particular, we weren't copying the 'unsigned' field when we
got the remote-echo, which meant the 'sync' code didn't correctly match up the
echo with the original.

The separate codepath was becoming a thorn in my side, so fix things up to
bring it back in line.
2016-03-15 15:07:26 +00:00
David Baker 39a5765888 Test tghat Session.logged_out is fired 2016-03-15 14:50:37 +00:00
David Baker 9f91995f4e Fix tests by returning the original promise to avoid the extra trip around the event loop. 2016-03-15 14:15:15 +00:00
David Baker 85f2754300 Make the client object be an event emitter rather than a matrixclient to avoid us being tempted to gut wrench stuff directly into the Matrix Client. 2016-03-15 11:05:05 +00:00
David Baker 5833654aa6 Add Session.logged_out event that fires whenever the current session is no longer valid and the user needs to log in again. Also null check _syncApi before trying to stop it. 2016-03-15 10:45:08 +00:00
David Baker 899ff6cea2 Merge pull request #99 from matrix-org/dbkr/sync_dont_tightloop
Add a delay before we start polling the connectivity check endpoint
2016-03-15 10:34:44 +00:00
David Baker 3752429b65 Fix the tests to tick the clock to 'wait' for sync retries. 2016-03-14 17:49:36 +00:00
David Baker d13fbd0e3e fix lint 2016-03-14 17:13:01 +00:00
David Baker 5e18c84e53 Add a delay before we start polling the connectivity check endpoint to avoid tightlooping if the conn check succeeds but /sync etc fails. 2016-03-14 16:50:00 +00:00
Richard van der Hoff fc1d5c86f9 Merge pull request #98 from matrix-org/rav/keep_paginating
Try again if a pagination request gives us no new messages
2016-03-14 15:48:17 +00:00
Richard van der Hoff c3ea913ae8 Try again if a pagination request gives us no new messages
This is basically a workaround for https://matrix.org/jira/browse/SYN-645: if
we knew about all of the events already, we want to try again.

Fixes the second half of https://github.com/vector-im/vector-web/issues/1014
2016-03-14 14:47:29 +00:00
Richard van der Hoff f2336aaedf Merge to master before develop
This avoids picking up things which have been landed on develop since the
release process started.
2016-03-11 12:17:29 +00:00
Richard van der Hoff 16ab2fd82a Add uglifyjs to devDependencies
The release script requires it, so we better install it
2016-03-11 12:15:00 +00:00
Richard van der Hoff 295fda027c Build 0.4.1 2016-03-11 12:00:44 +00:00
Richard van der Hoff 28e6f00766 Prep r0.4.1 2016-03-11 11:50:47 +00:00
Richard van der Hoff b1988de753 Merge branch 'master' into develop 2016-03-11 11:27:52 +00:00
Kegsay 0302eefd3c Update RELEASING.md 2016-03-11 11:05:55 +00:00
Kegsay 8ad5605e49 Update RELEASING.md 2016-03-11 11:03:00 +00:00
Kegsay 4b5539fe53 Create RELEASING.md 2016-03-11 10:58:39 +00:00
Richard van der Hoff b0becbc8d5 Merge pull request #97 from matrix-org/rav/updates_on_send_fail
Raise localEchoUpdated events in more places
2016-03-10 14:19:30 +00:00
Richard van der Hoff 4ae353d3d3 Raise localEchoUpdated events in more places
We need to know about more transitions for local-echo status changes, so raise
localEchoUpdated events for each transition.

Fixes an issue where we weren't turning failed transmissions red, because the
timeline wasn't being updated.
2016-03-10 12:07:27 +00:00
Richard van der Hoff b4b8b4bfb8 Merge pull request #95 from matrix-org/rav/raise_event_on_remote_echo
Emit an event when a local-echo is turned into a proper event
2016-03-08 16:40:03 +00:00
Richard van der Hoff 909a8a0648 Merge pull request #96 from matrix-org/rav/no_pagination_tightloop
Work around confused timelines causing pagination loops
2016-03-08 16:39:46 +00:00
Richard van der Hoff 234c227fd5 Work around confused timelines causing pagination loops
Look out for us getting stuck in a loop of using the same pagination token,
and use something else next time.

Hopefully this will fix https://github.com/vector-im/vector-web/issues/1089.
2016-03-08 15:38:00 +00:00
Richard van der Hoff 78eded3bbd Emit an event when a local-echo is turned into a proper event
We need to trigger an update of the timeline when this happens, so raise an
event for it.
2016-03-08 15:00:19 +00:00
David Baker f324e4c72f lint 2016-03-03 17:48:23 +00:00
David Baker 9328a12ccb Add maySendStateEvent method, ported from react-sdk (but fixed). Plus tests. 2016-03-03 17:44:27 +00:00
Richard van der Hoff 51380f8116 Merge pull request #93 from matrix-org/rav/another_timeline_race
Set the back-pagination token before raising Room.timelineReset
2016-03-02 11:41:20 +00:00
Richard van der Hoff cfd96969fc Add unit tests for setting the pagination token on sync 2016-03-01 13:58:29 +00:00
Richard van der Hoff 0034bdf4ad Set the back-pagination token before raising Room.timelineReset
This fixes another race condition on gappy syncs, wherein we weren't
back-paginating back from the start of the gappy sync.
2016-03-01 13:35:22 +00:00
Richard van der Hoff 00af1ce7d2 Merge pull request #92 from matrix-org/rav/sync_left_rooms_test
Add a unit test for syncLeftRooms
2016-03-01 13:19:12 +00:00
Richard van der Hoff 76f84c54db Add a unit test for syncLeftRooms
We don't have *any* tests for syncLeftRooms right now, so start one.
2016-03-01 12:12:49 +00:00
Mark Haines bb4766c8c6 Merge pull request #90 from matrix-org/markjh/change_push_actions
Add setPushRuleActions method for setting the actions for push notifi…
2016-03-01 10:10:38 +00:00
Richard van der Hoff e287e7591b Merge pull request #91 from matrix-org/rav/fix_stuck_pagination_after_join
Don't reset the timeline when we join a room after peeking
2016-03-01 09:10:21 +00:00
David Baker 48f7aca121 Merge pull request #89 from matrix-org/dbkr/invite_name_from_member_event
Use our inviter's member event to get their display name if it exists.
2016-02-29 18:05:27 +00:00
Richard van der Hoff a14f9e6d1c Don't reset the timeline when we join a room after peeking
If we've already got all the events in a limited sync, there is no need to reset
the timeline.
2016-02-29 17:25:20 +00:00
David Baker 5fefcd8ce3 pep8 2016-02-29 13:53:55 +00:00
David Baker 76f1d24c7b Make room name generation slightly more sane and add unit tests fir invite naming. 2016-02-29 13:51:55 +00:00
Mark Haines 066dd77aba Add setPushRuleActions method for setting the actions for push notification rules 2016-02-26 16:47:22 +00:00
David Baker 45a3bf63b2 Use our inviter's member event to get their display name if it exists. 2016-02-26 14:11:10 +00:00
Richard van der Hoff 75f2efffac Merge pull request #88 from matrix-org/rav/optimise_timeline_load
TimelineWindow.load: make the livetimeline case quicker
2016-02-26 13:27:40 +00:00
Richard van der Hoff 0584ec3319 Merge pull request #87 from matrix-org/rav/reset_timeline
Fire a 'Room.timelineReset' event when we get a gappy sync
2016-02-26 13:27:25 +00:00
Richard van der Hoff 38e81ba61a TimelineWindow.load: make the livetimeline case quicker
Avoid doing a loop round the reactor if we are just loading the live timeline.
2016-02-26 12:45:28 +00:00
Richard van der Hoff 164e4814af Merge remote-tracking branch 'origin/develop' into rav/reset_timeline 2016-02-25 18:35:53 +00:00
Richard van der Hoff abf908b14f Fire a 'Room.timelineReset' event when we get a gappy sync
We need to reset things at the UI level when we get a gappy sync, so give the
clients something to listen for.

Also add a bunch of tests for that bit of code.
2016-02-25 18:26:11 +00:00
Richard van der Hoff 1deb2e27d8 .npmignore which includes git-revision.txt 2016-02-25 17:50:54 +00:00
Richard van der Hoff 848ffe8a40 Merge branch 'master' into develop 2016-02-25 17:29:39 +00:00
Richard van der Hoff 79c10c1b68 Remove old tarball before building new one 2016-02-25 17:23:54 +00:00
Richard van der Hoff 7e6eb89524 Empty commit to kick jenkins 2016-02-25 17:22:53 +00:00
Richard van der Hoff 3d3e52b104 s/version.txt/git-revision.txt/ 2016-02-25 16:48:41 +00:00
Richard van der Hoff 05326984df Add a 'version.txt' file to the tarball
This will enable the vector build to know what it got
2016-02-25 15:21:23 +00:00
Richard van der Hoff e97e3c673f jenkins.sh: Run npm pack after build to build tarball 2016-02-25 13:20:13 +00:00
Richard van der Hoff f0ae46afc9 add jenkins.sh script 2016-02-25 13:17:38 +00:00
Richard van der Hoff aaf4371fae Merge pull request #85 from matrix-org/rav/recreate_filter_on_changes
Check filters before we reuse them
2016-02-24 17:22:57 +00:00
Richard van der Hoff 46912431cc make the tests pass again 2016-02-24 16:15:08 +00:00
Richard van der Hoff 6a19e08381 lint 2016-02-24 15:58:35 +00:00
Richard van der Hoff 43f392955d Check filters before we reuse them
Make sure that we check the content of existing filters before we blindly reuse
them.

Fixes https://github.com/vector-im/vector-web/issues/988
2016-02-24 15:23:42 +00:00
31 changed files with 48602 additions and 364 deletions
+6
View File
@@ -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
View File
@@ -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
View File
@@ -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
================
+14
View File
@@ -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`.
+15420
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+15577
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+15785
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
Executable
+13
View File
@@ -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
View File
@@ -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
View File
@@ -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;
},
/**
+7 -15
View File
@@ -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
View File
@@ -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.
+74
View File
@@ -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
View File
@@ -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.
*/
+4
View File
@@ -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();
+4 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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();
});
});
});
});
+52 -1
View File
@@ -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() {
+56 -8
View File
@@ -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);
});
});
});
+174
View File
@@ -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);
});
});
});
+9 -3
View File
@@ -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);
};
}
+83
View File
@@ -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
View File
@@ -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);
});
});
});