Compare commits

...

488 Commits

Author SHA1 Message Date
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
David Baker 7728009ef3 Changelog 2016-02-24 16:20:46 +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
David Baker 41d2076bd4 Merge branch 'develop' 2016-02-24 13:52:10 +00:00
David Baker 670d230f2e Bump to 0.4.0 2016-02-24 13:49:58 +00:00
David Baker 7970f3f5a5 Merge pull request #84 from matrix-org/dbkr/keypair_3pid_invites
Add support for new keypair style 3pid invites
2016-02-23 16:37:48 +00:00
David Baker 567716c4f7 Use more normal promise structure 2016-02-23 11:21:29 +00:00
David Baker 518e41c078 add docs 2016-02-23 11:08:07 +00:00
David Baker bd600f65fb Add support for new keypair style 3pid invites (add an option to joinRoom for specifying the signing url) 2016-02-23 10:11:04 +00:00
Matthew Hodgson 363b08c4d8 don't NPE on 50x's - as per BOTS-170 2016-02-22 10:34:43 +00:00
Matthew Hodgson 2150bdc444 fix tests 2016-02-19 17:59:26 +00:00
Matthew Hodgson 5886b3358d f1x l1nt 2016-02-19 17:56:55 +00:00
Matthew Hodgson 8b887d8559 name 3PID invite rooms better 2016-02-19 17:45:57 +00:00
Richard van der Hoff 7278c38fa6 Merge pull request #83 from matrix-org/rav/fix_context_event_ordering
Interpret the response from /context correctly
2016-02-19 17:12:48 +00:00
Richard van der Hoff 24ae4a8d1a Interpret the response from /context correctly
events_before are backwards

Fixes https://github.com/vector-im/vector-web/issues/963
2016-02-19 17:03:47 +00:00
David Baker 923b9cad39 Merge pull request #82 from matrix-org/real_receipts
Add a param to getEventReadUpTo to have it ignore implicit read receipts
2016-02-19 16:20:04 +00:00
David Baker e9f6e41550 Local echos are fake too. 2016-02-19 16:18:29 +00:00
David Baker 2950417f70 Add docs to appease jslint 2016-02-19 15:35:36 +00:00
David Baker 39f641a851 Address PR comments 2016-02-19 15:22:38 +00:00
David Baker 95fff38dbb Add a param to getEventReadUpTo to have it ignore implicit read receipts. Store real receipts separately to make this work. 2016-02-19 14:42:07 +00:00
Richard van der Hoff 785326376a Merge pull request #80 from matrix-org/rav/keep_redactions
Keep redacted events in the timeline
2016-02-17 21:41:42 +00:00
Richard van der Hoff 1baf14861c Merge pull request #81 from matrix-org/rav/fix_timeline_after_join
EventTimeline: Fix baseIndex after removing the last event
2016-02-17 12:23:29 +00:00
Richard van der Hoff 8e47fe2968 Fix lint 2016-02-16 22:32:50 +00:00
Richard van der Hoff 88827fab84 EventTimeline: Fix baseIndex after removing the last event
Removing the last event in an EventTimeline (as we might, for instance, if it
was a local echo in an empty timeline) got us into a state where the baseIndex
would increment when adding events to the end of the timeline, causing much
confusion.
2016-02-16 22:22:26 +00:00
Richard van der Hoff ab0a06eea7 More delinting 2016-02-16 16:19:32 +00:00
Richard van der Hoff 6a6db36088 delintificate 2016-02-16 16:14:45 +00:00
Richard van der Hoff 6f3bdcfbb6 Remember to propagate Room.redaction 2016-02-16 16:03:40 +00:00
Richard van der Hoff 4c6d0a5128 Redactions: only remove the keys that are specced for removal 2016-02-16 16:03:18 +00:00
Richard van der Hoff 5eff278454 Keep redacted events in the timeline
Everything gets confused when we remove events from timelines, so keep
redacted events in there, and mark them as redacted instead.
2016-02-16 12:05:13 +00:00
David Baker c8d1e210a3 Merge pull request #79 from matrix-org/dbkr/new_reconnection_logic
Use only /versions requests for connection recovery
2016-02-10 11:17:29 +00:00
David Baker 8614632e54 No ES6 in the JS SDK and other lint warnings. 2016-02-10 10:48:03 +00:00
David Baker c3796c61cd Use a promise for when the connection comes back 2016-02-10 10:40:42 +00:00
David Baker 6224aff882 improve comment as per PR review 2016-02-10 10:09:39 +00:00
David Baker e506a1b2de Oops: set keepAliveTimer! 2016-02-09 17:39:30 +00:00
David Baker b40a6d1481 remember bound function as it returns a new reference each time 2016-02-09 17:06:20 +00:00
David Baker 5c8f73019e lint 2016-02-09 16:22:12 +00:00
David Baker 3cfc4f8ba5 Add short delay to treating a 400 as a success to avoid hammering in a loop. 2016-02-09 16:17:52 +00:00
David Baker 4d46251b15 Just use the keepalive logic to recover from lost internet connections.
* This should fix the problem where we could end up with two concurrent syncs
   due to both retrying /sync and hitting /versions. There is now one and only
   one mechanism for connection recovery.
 * Hit /versions a little less often as I think every 2 seconds is a little
   over-aggressive. Also introduce randomness to minimize possibility of
   thundering herds.
 * Add a listener for the 'online' event in case any browser is nice enough
   to ever fire it.
 * Treat a 400 response from /versions as successful since older synapses
   will not support it.
2016-02-09 16:08:14 +00:00
David Baker 977e33f1bd Merge pull request #76 from matrix-org/dbkr/sync_gutwrench_less
Change the sync state tracking slightly to gut-wrench client a bit less
2016-02-08 14:18:59 +00:00
David Baker daa0e6291e Add docs for STOPPED state and correct js doc 2016-02-08 14:18:15 +00:00
Daniel Wagner-Hall 620417ed1b Merge pull request #78 from matrix-org/daniel/r0
Use /r0 or /unstable for all requests
2016-02-08 11:57:13 +00:00
Daniel Wagner-Hall 02196416e4 Use /r0 or /unstable for all requests 2016-02-08 11:15:30 +00:00
Richard van der Hoff 5ca4bc6b85 Merge pull request #77 from matrix-org/rav/check_old_timelines_for_echoes
Check old timelines when looking for echoes of sent messages
2016-02-05 19:41:37 +00:00
Richard van der Hoff 76c79ec299 Check old timelines when looking for echoes of sent messages
After /send completes, we check the room to see if the echo has already come
back via /sync. It's a bit of an edge-case, but we ought to check all
timelines, not just the live one.

Furthermore, we now have a map from eventId to timeline, so we can handle the
case where the echo has *not* yet come back more efficiently than searching
through the whole timeline.
2016-02-05 17:31:46 +00:00
David Baker fc730d4637 remove unintentional log line 2016-02-05 14:13:14 +00:00
David Baker 41d5917bb6 Change the sync state tracking slightly to gut-wrench client a bit less
Should be functionally identical outside of client and sync.
2016-02-05 14:09:58 +00:00
Richard van der Hoff 122867e8ee Merge pull request #75 from matrix-org/rav/read_receipt_ordering
Give precedence to later Read Receipts
2016-02-04 16:50:59 +01:00
Richard van der Hoff f3e5e03009 Give precedence to later Read Receipts
In order to resolve the conflict between local and remote read-receipts, try to
give precedence to the read-receipt which refers to the most recent event.

Also fix the read-receipt synthesis in _addLiveEvents so that it actually works
(drop the spurious MatrixEvent wrapper), and remove the synthesis in
recalculate() (which appears to be redundant).
2016-02-04 15:35:27 +00:00
Richard van der Hoff 1b43e5ed98 Merge pull request #74 from matrix-org/rav/fix_localecho_timeline_bug
Fix a bug which made the timelines get confused about local messages
2016-02-03 15:41:52 +01:00
Richard van der Hoff 9e65f12ddd Fix lint warnings 2016-02-03 14:29:39 +00:00
Richard van der Hoff be297ddbcd Merge pull request #73 from matrix-org/rav/fix_timeline_tightloop
Make sure we don't end up calling /messages in a loop if things go weird
2016-02-03 15:14:41 +01:00
Richard van der Hoff 8ee1d17ff7 Fix a bug which made the timelines get confused about local messages
Make sure that the timeline index is kept consistent when the id of an event
changes when we receive the remote echo of a message we sent.
2016-02-03 11:56:09 +00:00
Richard van der Hoff a2185fefc1 Make sure we don't end up calling /messages in a loop if things go weird
If we somehow end up in a situation where calling /messages returns a load
of messages, but none of them are new, then currently we start calling
/messages again and again in a tight loop. This is bad, so fix it.
2016-02-03 11:47:04 +00:00
David Baker f172272dd3 Merge pull request #72 from matrix-org/dbkr/document_getroom
Add docs to getRoom noting how you shouldn't assume it will return so…
2016-02-02 17:16:30 +00:00
David Baker 8a77b29d17 Add docs to getRoom noting how you shouldn't assume it will return something because you've got a Room-like event 2016-02-02 16:30:28 +00:00
Richard van der Hoff 0a7efe3e8b Fix undefined variable accesses
fix a merge error in the last commit
2016-01-30 00:44:40 +00:00
Richard van der Hoff 2af947fc1a Stash the old next_batch token for pagination
When we reset the live timeline due to a limited sync, stash next_batch as the
pagination token so that we can work forward to the new live timeline.

(Note that this requires matrix-org/synapse#535)
2016-01-30 00:12:31 +00:00
Richard van der Hoff 1499087098 TimelineWindow: fix canPaginate during load
We should return false rather than throw an exception if someone calls
canPaginate before the timeline finishes loading.
2016-01-30 00:09:12 +00:00
David Baker 672e96b90e Merge pull request #71 from matrix-org/dbkr/separate_sync_processing
Separate the actual processing of the sync response from the loop doing the requests.
2016-01-29 13:05:27 +00:00
David Baker 3d5e2937e2 docstring 2016-01-29 11:29:14 +00:00
David Baker bbf3b2637a Re-apply: 5297855ad3
Separate the actual processing of the sync response from the loop doing the requests.
2016-01-29 11:25:01 +00:00
David Baker 04a1c4f1a2 Revert "Separate the actual processing of the sync response from the loop doing the requests."
This reverts commit 5297855ad3.

Accidentally committed to wrong branch
2016-01-29 11:17:12 +00:00
David Baker 5297855ad3 Separate the actual processing of the sync response from the loop doing the requests. 2016-01-29 11:05:32 +00:00
Richard van der Hoff a221674680 Merge pull request #64 from matrix-org/rav/timeline_window
TimelineWindow object
2016-01-28 16:46:03 +00:00
Richard van der Hoff 8db95f42fb Add some unit tests for TimelineWindow. 2016-01-28 16:38:45 +00:00
Richard van der Hoff b42e5d5fcf Address review comments
Rearrange a couple of things for clarity, and add some comments.
2016-01-28 12:36:12 +00:00
Richard van der Hoff b1e2090eef TimelineWindow object
A handy thing for tracking a window into a room timeline

Could really do with some unit tests... sorry.
2016-01-27 09:53:15 +00:00
Richard van der Hoff 8716185f4a Merge pull request #63 from matrix-org/rav/context
Support for non-contiguous event timelines
2016-01-27 09:50:57 +00:00
Richard van der Hoff 3c2fad7c8d Merge remote-tracking branch 'origin/develop' into rav/context 2016-01-27 09:49:00 +00:00
Richard van der Hoff 60a243f160 Rename 'exceptFail' to 'failTest', and move it out to test-utils.js 2016-01-27 09:48:28 +00:00
Richard van der Hoff a87cefa035 Replace the boolean args on EventTimeline methods with constants 2016-01-26 22:38:26 +00:00
Richard van der Hoff 101d3952d3 Test that the pagination tokens actually start at null 2016-01-26 21:25:10 +00:00
Richard van der Hoff a01501b42c Address a number of review comments.
Make sure that room state is copied correctly when resetting the live
timeline.

Also comments and bits.
2016-01-26 18:09:15 +00:00
David Baker d37369b463 Merge pull request #70 from matrix-org/dbkr/receipt_local_echo
Add local echo for read receipts.
2016-01-26 14:11:49 +00:00
David Baker 0c3abcccf2 camelCase 2016-01-26 14:11:36 +00:00
Richard van der Hoff 48d1bc3158 Fix incompatibility with peeking.
The peek code needs to make sure it sets the pagination token /after/ adding
the events to the timeline, otherwise it will get reset when the events
are added.
2016-01-26 11:21:49 +00:00
David Baker 15e8784daf Add local echo for read receipts. Fixes https://github.com/vector-im/vector-web/issues/623 2016-01-25 17:49:41 +00:00
Richard van der Hoff 840b8f0bc0 Merge branch 'develop' into rav/context
Conflicts:
	lib/models/room.js
2016-01-25 10:45:22 +00:00
Kegsay 7a4cc62280 Merge pull request #69 from matrix-org/kegan/sync-keep-alive
Implement a keep-alive timer for /sync requests
2016-01-22 16:23:43 +00:00
Kegan Dougal f8ec35691f Interrupt /sync backoff when keep-alive succeeds in order to immediately retry if we were waiting 2016-01-21 18:02:26 +00:00
Kegan Dougal c5e7df8975 Hit /versions instead of / since it is actually a known endpoint 2016-01-21 17:52:52 +00:00
Kegan Dougal 7bdab05785 Unbreak tests 2016-01-21 17:34:12 +00:00
Kegan Dougal 197144dcda Implement a keep-alive timer for /sync requests
When a /sync request fails, we spin up a keep-alive poll to /_matrix/client/r0
which 400s. We treat any HTTP response code as a success for the purposes of
polling the server. When a successful poll is done, we shoot the current /sync
request in the head immediately (via a hacky abort() on the promise) and retry
the /sync.
2016-01-21 17:17:27 +00:00
Kegan Dougal ff990914b2 Remove low client-side timeouts hack 2016-01-21 16:12:07 +00:00
David Baker 4bc2869522 Merge pull request #68 from matrix-org/dbkr/new_unread_count_format
Update for new unread count format
2016-01-21 09:54:51 +00:00
David Baker 1f1d743678 Merge remote-tracking branch 'origin/develop' into dbkr/new_unread_count_format 2016-01-21 09:50:20 +00:00
Matthew Hodgson 787c0ebabc fix another test 2016-01-21 00:14:29 +00:00
Matthew Hodgson d559ad794a STUPID LINE LENGTH LIMITS 2016-01-21 00:10:15 +00:00
Matthew Hodgson 24655ac60e missing semicolon 2016-01-21 00:02:24 +00:00
Matthew Hodgson 5fd0ea2f6f fix test 2016-01-20 23:56:16 +00:00
Matthew Hodgson eaf7b03bb1 if we are the only person in a room, call it an 'Empty room' too, given this is how humans see a room if they're the only person in it... 2016-01-20 23:55:09 +00:00
David Baker d375804cd6 Merge branch 'develop' into dbkr/new_unread_count_format 2016-01-20 18:53:14 +00:00
David Baker a24a9d35c4 Fix PR comments: typos and redundant line 2016-01-20 18:52:32 +00:00
Kegan Dougal a0d81fccdb Fix test 2016-01-20 17:29:27 +00:00
David Baker b4e4aaff00 Merge branch 'develop' into dbkr/new_unread_count_format 2016-01-20 17:25:54 +00:00
Matthew Hodgson 3a73b54e4a .name defaults to mxid 2016-01-20 17:22:16 +00:00
David Baker 5ec0fce2a4 style 2016-01-20 17:19:26 +00:00
Matthew Hodgson 8b7497374f name self-chats by displayname if possible rather than mxid, and name empty-chats as 'Empty room' rather than the fugly '?' 2016-01-20 17:19:13 +00:00
David Baker 8cb180525e Add getter/setter for unread notif counts. 2016-01-20 17:16:20 +00:00
Kegan Dougal 6df9d08dc1 Fix tests 2016-01-20 15:59:34 +00:00
David Baker b3c06dd723 Update for new unread count format 2016-01-20 15:59:06 +00:00
Kegan Dougal 2a88b8db4e Improve performance of hasMembershipState to not be stupid 2016-01-20 15:09:35 +00:00
Kegsay 31c29b7e5e Merge pull request #67 from matrix-org/kegan/address-book
Make getUsers() return users for *EEEEEVERYOOOOONE*
2016-01-19 12:51:16 +00:00
Kegan Dougal 865db906e3 Make getUsers() return users for *EEEEEVERYOOOOONE* regardless of presence events 2016-01-19 11:40:08 +00:00
manuroe 0b52a7e7c9 Merge branch 'push-rules-settings' into develop
# Conflicts:
#	lib/sync.js
2016-01-18 17:35:51 +01:00
manuroe 80f7220a7b Merge branch 'develop' into push-rules-settings
# Conflicts:
#	lib/sync.js
2016-01-18 17:33:13 +01:00
manuroe 2e664adb32 Updated push rules methods after review 2016-01-18 17:22:35 +01:00
Kegan Dougal 14ef9348be Add getUsers() 2016-01-18 12:05:26 +00:00
Kegsay 3c170bf063 Merge pull request #66 from matrix-org/matthew/roomsettings2
deleteAlias() support
2016-01-18 10:27:58 +00:00
Kegan Dougal 3e67406a30 Tweak docs 2016-01-18 10:27:43 +00:00
Matthew Hodgson d6075bb5bd add an XXX 2016-01-17 23:32:00 +00:00
Matthew Hodgson d8e56dad1b oops 2016-01-16 01:01:11 +00:00
Matthew Hodgson 67872206ff deleteAlias() support 2016-01-16 00:58:44 +00:00
Richard van der Hoff 43e7173c30 fix some racy tests 2016-01-16 00:18:51 +00:00
Richard van der Hoff e0ddd65922 Address review comments
Improve comments and naming.
2016-01-15 23:50:09 +00:00
Richard van der Hoff dfb2fa821d minor jsdoc fixes 2016-01-15 17:11:00 +00:00
Richard van der Hoff fce7248ed5 Flag the top of the timeline when we hit it 2016-01-15 17:10:01 +00:00
Richard van der Hoff e68ab7d54a Tweak duplicateStrategy code to reduce diff 2016-01-15 13:23:51 +00:00
Richard van der Hoff 706966ffe9 Support for non-contiguous event timelines
This provides optional support for fetching old events via the /context API,
and paginating backwards and forwards from them, eventually merging into the
live timeline.

To support it, events are now stored in an EventTimeline, rather than directly
in an array in the Room; the old names are maintained as references for
compatibility.

The feature has to be enabled explicitly, otherwise it would be impossible for
existing clients to back-paginate to the old events after a gappy /sync.

Still TODO here:

* An object which provides a window into the timelines to make them possible to
  use. This will be a separate PR.

* Rewrite the 'EventContext' used by the searchRoomEvents API in terms of an
  EventTimeline - it is essentially a subset.
2016-01-15 13:19:11 +00:00
manuroe e3b4cb03e1 Fixed Jenkins tests complaints 2016-01-14 19:17:08 +01:00
manuroe 8011aab561 Added MatrixClient getRoomPushRule and setRoomMutePushRule methods 2016-01-14 18:58:15 +01:00
Matthew Hodgson a8d24798e6 oooooooops... let's pretend nobody saw this 2016-01-14 17:23:04 +00:00
Kegan Dougal e4c38ac78c Linting 2016-01-13 15:31:27 +00:00
Kegan Dougal 5fa6f0037f Cache the third_party_invite token to allow constant time lookups 2016-01-13 15:31:27 +00:00
Matthew Hodgson a0df2a70cd s/getImplicitRoomName/getDefaultRoomName/ # as kegan doesn't like the word 'implicit' 2016-01-13 14:02:26 +00:00
Kegan Dougal 0bab00c47c Add debug logging to sync polling. Add speculative fix for vector-im/vector-web#544 2016-01-13 13:17:21 +00:00
Matthew Hodgson f48fb34818 Merge pull request #62 from matrix-org/matthew/roomsettings2
Room.getImplicitRoomName
2016-01-13 12:58:52 +00:00
Matthew Hodgson 8810ff2256 merge and add null check 2016-01-13 12:58:46 +00:00
Matthew Hodgson 17efc5163f Merge branch 'develop' into matthew/roomsettings2 2016-01-13 12:55:30 +00:00
Matthew Hodgson 3ce07a020d ooops - fix getDomain expression 2016-01-13 12:52:22 +00:00
Matthew Hodgson 71abef0117 fix merge conflict 2016-01-13 12:46:47 +00:00
Matthew Hodgson a79270b8f8 Merge pull request #61 from matrix-org/matthew/accountdata
implement account data
2016-01-13 12:43:53 +00:00
Matthew Hodgson 87db054e22 fix jsdoc 2016-01-13 12:43:42 +00:00
Kegan Dougal ae06dd2ab8 Fix thinko where idServerRequests return strings 2016-01-12 17:23:50 +00:00
Matthew Hodgson 88c7293838 based on PR review, rewrite account_data support to avoid tracking the section that events came from, and instead having /sync results piped into the right bit of the room directly 2016-01-11 19:25:44 +00:00
Matthew Hodgson 051e83582b add a cli.getDomain() method rather than react-sdk maintaining its own (multiple) implementations 2016-01-11 18:23:07 +00:00
Matthew Hodgson 57072bc4f4 s/implicit/ignoreRoomNameEvent/ on calculateRoomName 2016-01-11 18:20:26 +00:00
Kegan Dougal e8f77256de Set the updated .sender and .target props on the event when the event itself updates these props. 2016-01-11 17:35:46 +00:00
Kegan Dougal 51fe73bc27 Return v2 prev_content when calling getPrevContent() 2016-01-11 17:15:52 +00:00
Kegan Dougal 97003f7382 Use timeout=0 rather than timeout=1 because of SYN-588
Fixes https://github.com/vector-im/vector-web/issues/606
2016-01-11 16:33:43 +00:00
manuroe c10218a1fa Fixed Jenkins tests complaints 2016-01-11 17:12:28 +01:00
manuroe 5c59a2ea3e Fixed MatrixClient pushRules method and ivar clash.
Added MatrixClient.setPushRuleEnabled method.
2016-01-11 17:00:05 +01:00
Kegsay 0b79ac1386 Merge pull request #60 from matrix-org/kegan/guest-access
Add constructs for guest access
2016-01-11 15:19:14 +00:00
Kegan Dougal 4fe95f18b9 More commenting on the creation of guest filters 2016-01-11 14:52:06 +00:00
Kegan Dougal db5ca49ee2 Linting 2016-01-11 09:31:00 +00:00
Matthew Hodgson d7158b575f fix trailing space 2016-01-10 20:05:58 +00:00
Matthew Hodgson 678d70528e add a Room.getImplicitRoomName so clients can know what a room would be called if it didn't have an explicit m.room.name state event 2016-01-10 20:02:35 +00:00
David Baker 02b33766ee Document the order of the room timeline because I can never remember which way round it is. 2016-01-08 20:26:07 +00:00
Matthew Hodgson 9bd45cf7c7 more lint and thinkos 2016-01-08 03:45:05 +00:00
Matthew Hodgson c64aebdb17 lint and thinkos 2016-01-08 03:41:05 +00:00
Matthew Hodgson 387ad09c5f implement account data 2016-01-08 03:22:08 +00:00
Kegan Dougal ea3bd1450e Add functions to support upgrading guest accounts 2016-01-07 17:23:56 +00:00
Kegan Dougal 8f4bd9c693 NOP typing/receipts when a guest 2016-01-07 15:03:37 +00:00
Kegan Dougal cdb4bc5107 Implement peek syncing.
This involves hitting room initial sync then /events?room_id=!thing:here
It even works.
2016-01-07 14:58:28 +00:00
Matthew Hodgson 446faed9b5 copyrights please... 2016-01-07 04:15:38 +00:00
Kegan Dougal d36c928d95 Fix tests 2016-01-06 17:35:56 +00:00
Kegan Dougal 3a3f25c1bc Remove guest rooms array; replace with a peeking SyncApi
After much discussion, the HS will now behave the same for guests/non-guests
wrt joining a room (you get the entire room state on join). This leave "peeking"
which never triggers a join. This can be implemented for guests by doing a
room initial sync followed by a specific /events poll with a specific room_id.
This means there are 2 sync streams: /sync and the peek /events. Architected
so you can only have 1 peek stream in progress at a time (if this were arbitrary
we'd quickly run into concurrent in-flight browser request limits (5).
2016-01-06 17:29:14 +00:00
Kegan Dougal 73e65bc18b Add setGuestAccess to allow easy room guest access configuration. 2016-01-05 17:09:10 +00:00
Kegan Dougal 445491c4ad Fix guest rooms UT to reflect reality 2016-01-05 16:57:59 +00:00
Kegan Dougal f12499c6bf Support guest room filters 2016-01-05 13:24:51 +00:00
Kegan Dougal 8c6c65ab6c Don't do requests we know are going to fail as a guest 2016-01-05 11:50:24 +00:00
Richard van der Hoff b85e267fdb Merge pull request #59 from matrix-org/rav/event_context
Enhancements to search results, and event context implementation

This change adds support to the JDK for processing the results of a room
search, as well as back-paginating the results.

It treats each search result as a 'context' object, which can itself be
backwards or forward-paginated.
2016-01-04 12:50:10 +00:00
Richard van der Hoff c669d21af7 Enhancements to search results, and event context implementation
This change adds support to the JDK for processing the results of a room
search, as well as back-paginating the results.

It treats each search result as a 'context' object, which can itself be
backwards or forward-paginated.
2016-01-04 12:50:07 +00:00
Kegsay 3e4cef89fd Merge pull request #58 from matrix-org/notif_sync
Propagate the unread count from sync onto rooms
2015-12-23 10:07:13 +00:00
David Baker a06d1f62d7 Merge remote-tracking branch 'origin/develop' into notif_sync 2015-12-22 14:47:39 +00:00
Kegan Dougal 2802092231 Add 60s to client-side timeout to account for slow HSes 2015-12-21 13:49:14 +00:00
Richard van der Hoff a419e241a6 Merge pull request #57 from matrix-org/rav/search_backfill
Allow passing a next_batch token into /search for backfill
2015-12-21 09:15:53 +00:00
David Baker 7aa4bd7f46 Propagate unread notif count from sync to the room object 2015-12-18 17:49:42 +00:00
Kegsay 2eec76bc1d Merge pull request #56 from matrix-org/kegan/archived-rooms
Add syncLeftRooms()
2015-12-18 16:57:17 +00:00
Richard van der Hoff 13fcff9688 Allow passing a next_batch token into /search for backfill 2015-12-18 16:23:48 +00:00
Kegan Dougal 1174147d64 Don't lie in comments; remove spurious flag setting 2015-12-18 15:29:28 +00:00
Kegan Dougal de53b292a2 Remove debug logging 2015-12-18 15:21:28 +00:00
Kegan Dougal b50d61428c Impl syncing of left rooms. Factor out getting or creating filters. 2015-12-18 11:57:46 +00:00
Kegan Dougal 4bc5343b67 Merge branch 'develop' into kegan/archived-rooms 2015-12-18 09:47:43 +00:00
Kegan Dougal da560ffeff Fix SYJS-38 - Events stuck in 'sending' state
v1 used to clobber events after sending so `.status` would be `null`. v2 is
smarter and just clobbers the `.event` data so references to the local echo
event would reflect the new event. However, the `.status` in this case would
still have the old value (SENDING), so make sure to reset it after the 200 OK
from sending the event.
2015-12-18 09:14:48 +00:00
Kegan Dougal 59965b1c59 Add public api for syncing left rooms. Not implemented yet. 2015-12-17 17:17:53 +00:00
Kegsay 431f4a4797 Merge pull request #55 from matrix-org/kegan/sync-exp-backoff
Reset /sync backoff if setTimeout takes a long time to fire
2015-12-17 16:42:22 +00:00
Kegan Dougal 40c3fe558c Add note stating when timing delays can occur. 2015-12-17 16:31:00 +00:00
Kegan Dougal a12cd8d4a0 Fix JSDoc 2015-12-17 16:18:28 +00:00
Kegan Dougal ac7a469582 Add setIncludeLeaveRooms 2015-12-17 15:53:56 +00:00
Kegan Dougal 2e9376614f BREAKING: Add bindEmail option to register() 2015-12-17 14:13:17 +00:00
Kegan Dougal cd9d1daf17 Reset /sync backoff if setTimeout takes a long time to fire
This fixes vector-web/vector-im#536

The bug here is that we were assuming `setTimeout` would fire after the
requested time. This is not true when the machine is asleep. We now timestamp
before and after `setTimeout` and reset the attempt count if we have waited
more than twice what we originally requested. This allows for some jitter which
is to be expected.
2015-12-17 11:27:21 +00:00
Kegan Dougal bfa8dd0007 Cache the local event ID else we can remove a real event. Fixes vector-im/vector-web#530 2015-12-16 17:51:12 +00:00
Kegan Dougal 7e35ef258f Delete the room when it is forgotten and fire a new experimental event 2015-12-16 16:25:09 +00:00
Kegan Dougal 8bd43a8d53 Add /forget 2015-12-16 15:57:29 +00:00
Kegsay 8b87c0045d Merge pull request #54 from matrix-org/kegan/stale-call-notifications
Do not erroneously emit Call.incoming events on startup
2015-12-15 17:03:03 +00:00
Kegan Dougal 77356f0007 Fix vector-im/vector-web#494 2015-12-15 16:54:26 +00:00
Kegan Dougal 4cd6f615b3 Do bear minimum leave room handling so rejecting invites / leaving rooms are displayed correctly. 2015-12-15 16:21:36 +00:00
Kegan Dougal 65ef1dfd75 Lint and tests 2015-12-15 15:57:24 +00:00
Kegan Dougal 5719d513b7 Strip protocol part when doing 3PID invites for. Partially fixes vector-im/vector-web#419 2015-12-15 15:52:52 +00:00
Kegsay b90697264c Follow the spec 2015-12-15 15:33:56 +00:00
Kegan Dougal 406a2bb001 Do not erroneously emit Call.incoming events on startup
Check to see if the call was answered or hung up in addition to having a valid
lifetime before emitting the event. Fixes vector-im/vector-web#344
2015-12-15 15:13:41 +00:00
Kegsay 3115043b94 Merge pull request #53 from matrix-org/kegan/v2-sync
Use /sync instead of /initialSync and /events
2015-12-15 14:24:26 +00:00
Kegan Dougal bfda04daea Move local timeout logic to the HTTP API class. Fixes /sync bug
The ability to set a local timeout is applicable to any request, so move it
to http-api.js - We only use this on /sync requests currently. This simplifies
the SyncApi since it doesn't need to worry about it anymore -- the request
promise just gets rejected if the timer expires.

Whilst testing I noticed a weird anomaly which I cannot explain. Playing with
Chrome's network debugger, once you recover from a black hole (0kbps, 90s RTT)
the subsequent requests take 20s to return *even though there is no throttling*.
This was causing issues when using a local timer of timeout= and BUFFER_PERIOD_MS
when timeout=1 due to attempts>1 - they were being knifed before the response
could return. The 20s latency was entirely artifical (checked synapse logs and
they were sub 1s), but I cannot find anywhere in the JS-SDK or browser-requests
where this would be the cause. This persisted even when BUFFER_PERIOD_MS was
changed from 20s to 10s.
2015-12-15 11:59:41 +00:00
Kegan Dougal 46504b8b9f Fix tests; need more paranoia 2015-12-14 14:16:47 +00:00
Kegan Dougal f48c9175e5 Linting 2015-12-14 14:12:49 +00:00
Kegan Dougal bd4d8433ab Fix catchup bugs caused by using a stale pagination token 2015-12-14 14:11:25 +00:00
Kegan Dougal a00e318d73 Fix pagination - set prev_batch at the right time 2015-12-14 12:28:04 +00:00
Kegan Dougal fcf1abb185 Use v2 transaction IDs to suppress dupes without linear scans of the timeline! 2015-12-14 11:35:50 +00:00
Kegan Dougal 13cab79e04 Revert prev commit - emit SYNCING>SYNCING and now comment why we do this 2015-12-14 10:31:27 +00:00
Kegan Dougal fc6ce20e14 Check unsigned.age for getAge() for v2. Don't spam SYNCING emissions. 2015-12-14 10:27:53 +00:00
Kegan Dougal 9c49d26525 Linting 2015-12-14 09:24:33 +00:00
Kegan Dougal d6299b634c Scope filter keys in localStorage on user_id 2015-12-14 09:22:02 +00:00
Kegan Dougal a6f64b5f03 v2 filter test 2015-12-11 15:27:40 +00:00
Kegan Dougal 465635444f s/user_id/sender/g in tests 2015-12-11 15:07:40 +00:00
Kegan Dougal eedff29acb Add filter stub to crypto test 2015-12-11 13:35:46 +00:00
Kegan Dougal 7c43d15ea5 More linting; crypto test fix 2015-12-11 13:31:26 +00:00
Kegan Dougal de32ac0c44 Fix linting 2015-12-11 13:23:46 +00:00
Kegan Dougal 3d9d31d6b1 Fix remaining integration tests 2015-12-11 13:22:27 +00:00
Kegan Dougal b219836b3e Fix a bunch of integration tests 2015-12-11 12:53:26 +00:00
Kegan Dougal 26d9fed537 Fix MatrixClient unit tests 2015-12-11 11:07:31 +00:00
Kegan Dougal d6ba39f292 More linting 2015-12-10 15:01:39 +00:00
Kegan Dougal 8576ebce8f Linting 2015-12-10 14:57:13 +00:00
Kegan Dougal f08152a1d8 Handle ephemeral and account_data events 2015-12-10 14:27:21 +00:00
Kegan Dougal 6af2197183 Process join rooms and add local timeouts to /sync
This actually works now, though there's a number of teething
issues which may be app-specific. That, and all the tests are
broken.
2015-12-10 14:14:56 +00:00
Kegan Dougal 4c7e6807d2 Parse invites from /sync 2015-12-10 13:26:50 +00:00
Kegan Dougal 11f0513c62 Merge branch 'develop' into kegan/v2-sync 2015-12-10 11:50:48 +00:00
Kegan Dougal 3d57b4ce6a Be paranoid on /sync processing 2015-12-09 16:41:36 +00:00
Kegan Dougal 243bdd78f4 Handle presence key in /sync 2015-12-09 16:09:47 +00:00
Kegan Dougal b622960b32 Do all prep for /sync calls
This includes managing filters in localStorage. The /sync response
is not yet parsed.
2015-12-09 15:25:09 +00:00
Kegan Dougal 06f927aa22 Minor cleanup and reshuffle 2015-12-09 11:27:03 +00:00
Kegan Dougal f7ffed4b98 More linting 2015-12-09 11:19:20 +00:00
Kegan Dougal 0576e4ca0c Linting 2015-12-09 11:16:46 +00:00
Kegan Dougal 529fb23555 Linting 2015-12-09 10:43:21 +00:00
Kegan Dougal 3543abf7bd Make things work again 2015-12-09 10:42:38 +00:00
Kegsay a5847485b9 Merge pull request #52 from matrix-org/kegan/v2-filters
Support v2 filters
2015-12-09 09:10:59 +00:00
Kegan Dougal 2b659656cc Move alllll the sync code to sync.js - still more to do (in FIXME XXX) 2015-12-08 17:41:09 +00:00
Kegan Dougal ac3aa5538f Linting 2015-12-08 16:10:52 +00:00
Kegan Dougal c65f32f6a6 Add filter integration tests; more bug fixes. 2015-12-08 16:08:04 +00:00
Kegan Dougal 86a162c818 Add filter UTs and fix bugs 2015-12-08 15:39:55 +00:00
Kegan Dougal 1987726a95 Add initial v2 filter impl 2015-12-08 15:23:09 +00:00
Kegsay d2537cd00c Merge pull request #51 from matrix-org/kegan/unsent-timeline
Add config option to control how pending events are ordered
2015-12-07 17:00:43 +00:00
Kegan Dougal 61db191835 Add UTs 2015-12-07 15:45:13 +00:00
Kegan Dougal b7ac6a2e33 Add config option to sort pending events to the end of the timeline 2015-12-07 15:36:32 +00:00
Kegan Dougal c0178c3e80 Add getScheduler. Fix JSDoc 2015-12-07 11:29:02 +00:00
Kegsay e58fb29722 Merge pull request #50 from matrix-org/kegan/scrollback-requests
Scrollback improvements
2015-12-04 17:39:22 +00:00
Kegan Dougal a1300ec095 Wait for the last request (/messages, not /events) 2015-12-04 17:34:56 +00:00
Kegan Dougal 73e0216f78 Scrollback improvements
Add a 3s delay between scrollback requests if the previous scrollback request
failed.

Return the same promise if scrollback() is called multiple times whilst a
scrollback request is ongoing.
2015-12-04 17:27:16 +00:00
Kegan Dougal d16dfdaee3 Also emit a 'RoomState.members' event for m.room.power_levels 2015-12-04 16:11:13 +00:00
Kegan Dougal 02a605f368 Guest room ID fixes / initialSync support 2015-12-04 15:31:07 +00:00
Kegan Dougal 71d5756223 Fix linting 2015-12-04 09:36:18 +00:00
Kegan Dougal 2866743ce6 Fix broken test 2015-12-04 09:33:53 +00:00
David Baker e91a5e3793 Merge pull request #49 from matrix-org/upload_cancel
Add ability to cancel file uploads
2015-12-03 10:57:37 +00:00
David Baker 7f5ad041cc Pass the http status out with the error so upper level can can see what went wrong. 2015-12-03 10:51:09 +00:00
David Baker 92ea275275 Add ability to cancel file uploads 2015-12-02 18:14:13 +00:00
Kegan Dougal 0c114a2ab3 Only send up room_ids query param if isGuest is set 2015-12-02 12:34:25 +00:00
Matthew Hodgson e3757880ee add sendHtmlEmote 2015-11-29 01:18:22 +00:00
Kegan Dougal 88d680ef77 s/private_user_data/account_data/g 2015-11-20 17:32:25 +00:00
David Baker e1b3cf027c Merge pull request #47 from matrix-org/ignore-non-mxc
Don't return non-mxc URLs by default.
2015-11-12 15:51:20 +00:00
David Baker d0a725d1cc moar backticks 2015-11-12 15:50:57 +00:00
David Baker 07dbd26ba4 Typo 2015-11-12 15:40:59 +00:00
David Baker c93f56e4ec oops, 0.3.0 was already released 2015-11-12 15:40:32 +00:00
David Baker c0866b9787 Add changelog entry for breaking change 2015-11-12 15:39:12 +00:00
David Baker 14a9f6c444 lint & quote style 2015-11-12 12:14:13 +00:00
David Baker 588870b479 lint 2015-11-12 12:08:20 +00:00
David Baker f74bb3c145 Update UTs 2015-11-12 12:05:06 +00:00
David Baker 7095753410 Don't return non-mxc URLs by default. 2015-11-12 11:57:53 +00:00
Kegan Dougal 4f851dc431 Add isGuest/setGuest 2015-11-11 13:40:06 +00:00
Kegsay e89cc336b1 Merge pull request #46 from matrix-org/kegan/anon
Guest access endpoints
2015-11-10 17:06:04 +00:00
Kegan Dougal 959c588658 Guest rooms UTs 2015-11-10 16:49:50 +00:00
Kegan Dougal 7c887c1a5d Linting 2015-11-10 16:38:15 +00:00
Kegan Dougal 56bcf9796a Add MatrixClient.setGuestRooms
This is used in /events to grab events for the rooms the Guest is interested
in.
2015-11-10 16:36:44 +00:00
Kegan Dougal ee270314f8 Expose setting the HTTP body for extensibility 2015-11-10 16:30:41 +00:00
Kegan Dougal bda76afe4b Add a registerGuest endpoint 2015-11-10 16:25:15 +00:00
David Baker 4d426a3f31 The ts, not the event 2015-11-10 11:58:59 +00:00
Kegsay 9d33248c6e Merge pull request #45 from matrix-org/3pid-invites
Invite by 3PID endpoint
2015-11-09 17:09:35 +00:00
Kegan Dougal 46329ceb94 Remove the ability to set display_name in line with new spec 2015-11-09 16:58:52 +00:00
Kegan Dougal 2160f0bc08 Don't allow the IS URL to be configured on a per-request basis. 2015-11-09 16:51:12 +00:00
Kegan Dougal b231f19ec6 Make the display_name check for contains rather than equality. Add UT. 2015-11-09 16:50:10 +00:00
Kegan Dougal 6eb896e7a3 Use a small timeout when trying to poll when having previously failed. This makes re-connects propagate more quickly. 2015-11-09 15:55:09 +00:00
David Baker d7874315c3 Merge pull request #44 from matrix-org/implicit_read_receipts_2
Synthesize implicit read receipts in recalculateRoom
2015-11-09 15:08:04 +00:00
David Baker c95b27683f Add higher level keys to fake receipts 2015-11-09 15:05:46 +00:00
Kegan Dougal b0655d0431 Add UTs 2015-11-09 14:45:17 +00:00
David Baker ad24596d3f Revert c13b180 as it fails lint (creating functions in a loop) 2015-11-09 13:48:05 +00:00
Kegan Dougal 80a6cf34e2 Add 3pid invite endpoints 2015-11-09 11:58:45 +00:00
David Baker c13b1800b9 forEach probably nicer here 2015-11-09 10:23:37 +00:00
Kegsay b4bb0f011d Merge pull request #39 from matrix-org/matthew/room-tags
Room tag support
2015-11-09 09:34:57 +00:00
Matthew Hodgson b9ace61ccb split long lines 2015-11-07 20:26:16 +00:00
Matthew Hodgson 21273582a4 room tagging unit tests 2015-11-07 20:23:21 +00:00
Matthew Hodgson 53bbabea4f pass event in the Room.tags event 2015-11-07 20:23:09 +00:00
Matthew Hodgson 170a78a420 convert PUD POJOs to events with ugly utils.map rather than iterating in the for loop 2015-11-07 20:20:50 +00:00
Matthew Hodgson 8771ced8e4 fix jsdoc thinko 2015-11-07 17:28:37 +00:00
Matthew Hodgson dc7d2698b7 Merge branch 'develop' into matthew/room-tags 2015-11-07 17:25:53 +00:00
Matthew Hodgson 3d4694a92f fix casing of tagName 2015-11-07 17:22:45 +00:00
David Baker 8b2f94a6b2 Merge branch 'develop' into implicit_read_receipts_2 2015-11-06 15:39:16 +00:00
David Baker 6736164d98 Make linter happy (space at end of line) 2015-11-06 15:38:46 +00:00
David Baker 77266fe221 Fix lint errors and make thing that didn't need to be a member function not a member function 2015-11-06 15:26:35 +00:00
David Baker 14a48c1182 Synthesize implicit read receipts in recalculateRoom to make them correct when the room is first loaded. 2015-11-06 15:13:30 +00:00
Kegsay 5f6e52f367 Merge pull request #42 from stevenhammerton/sh-token-login
SH - CAS / Login token login
2015-11-06 14:34:48 +00:00
Steven Hammerton e71a87c62c Update javadoc 2015-11-06 12:14:24 +00:00
Steven Hammerton b963f177cc Update CAS login to return url rather than update location as the JS SDK may not be run within a browser env 2015-11-06 12:11:50 +00:00
Steven Hammerton c3097979f2 Change login with CAS to redirect to HS for CAS login 2015-11-06 11:19:32 +00:00
Kegan Dougal 21e56d2f53 Tweak RETRY_BACKOFF_RATELIMIT to take browser-request's CORS failures into account. 2015-11-05 15:48:48 +00:00
Kegsay 455ce26741 Merge pull request #40 from matrix-org/kegan/syncing
Syncing bugs/fixes
2015-11-05 14:53:47 +00:00
Steven Hammerton d241f5b3eb Add login with token method 2015-11-05 14:51:23 +00:00
David Baker d34f8eda1a Merge pull request #41 from matrix-org/implicit_read_receipts
Implicit read receipts
2015-11-05 14:42:48 +00:00
David Baker 483095c3da Fix PR comments 2015-11-05 14:41:35 +00:00
David Baker 856c34016d Fix event removal 2015-11-05 14:13:52 +00:00
David Baker ad80d4f059 fix lint errors 2015-11-05 13:57:21 +00:00
David Baker 0da547a239 Implicit read receipts
* Inject implicit read receipts into the timeline
 * Twiddle local echo a bit to make the implicit receipts match the various different stages of local echo.
2015-11-05 13:39:03 +00:00
Kegan Dougal 16278892d8 Modify how detection of the end of pagination is done
Synapse may filter down the events resulting in < 'limit' events being
returned *but it still has more events*. Change the check to see if the
request returned an empty array instead. This may add an extra HTTP hit.
2015-11-05 13:26:16 +00:00
Kegan Dougal 8500f404a9 Finish implementing UTs 2015-11-05 13:12:37 +00:00
Kegan Dougal 5d782a317c Add some sync emission tests. Emit after starting timers.
We want to emit AFTER starting the timers so tests can speed
up time. We also want to do this because clients may want to
retryImmediately() on sync errors (which would be lost unless
the timer had already been started)
2015-11-04 16:09:30 +00:00
Kegan Dougal af435204a0 More helpful logging 2015-11-04 15:40:42 +00:00
Kegan Dougal b4c353e65f Linting 2015-11-04 15:37:10 +00:00
Kegan Dougal e42f6c0cad Add http fixings to allow MatrixClient UTs 2015-11-04 15:35:31 +00:00
David Baker bc512a6e4c Check m.room.name event actually has a name in the content before using it. This should fix the recent disasters with #android being shown as 'undefined' (or crashing vector). 2015-11-04 15:20:25 +00:00
Kegsay 9cf7edc48d Merge pull request #38 from matrix-org/receipt_events
Emit events for read receipts
2015-11-04 12:04:19 +00:00
David Baker 904539df58 Fix c+p fail & add unit test 2015-11-04 12:02:02 +00:00
Kegan Dougal c9df9c33a8 Linting 2015-11-04 11:53:10 +00:00
Kegan Dougal 5c3bfa6a83 Add stub unit tests for syncing 2015-11-04 11:50:32 +00:00
Matthew Hodgson 149ed04a4f fix some review feedback; add initial api for setting & deleting tags; still a WIP 2015-11-04 02:24:36 +00:00
Kegan Dougal e98eaaee6e Add MatrixClient.retryImmediately() to stop backing off and sync RIGHT NOW 2015-11-03 17:13:50 +00:00
Kegan Dougal 4b93d801ae Implement the new sync state API
Also have retry schemes for the rest of the syncing ops (/events, /pushrules)
2015-11-03 16:44:19 +00:00
Matthew Hodgson 5a1cc4c2e7 store the tags in the right place 2015-11-03 16:19:52 +00:00
Matthew Hodgson 8016a70bc4 remember to check initialSync for m.tag events 2015-11-03 16:18:12 +00:00
Matthew Hodgson 70536d5676 add support for tracking room tags 2015-11-03 16:05:48 +00:00
Kegan Dougal 27ce0970c5 BREAKING: Introduce a formal API for syncing state
BREAKING CHANGE:
  This replaces syncComplete and syncError.
2015-11-03 14:35:49 +00:00
Kegan Dougal 49f6634d73 Retry /initialSync if it fails (exp backoff up to 2.1min). 2015-11-03 14:01:17 +00:00
David Baker 142ee81e66 Emit events for read receipts 2015-11-03 11:43:52 +00:00
Kegan Dougal 3b21998d96 Expose timeout= on /events to clients 2015-11-03 10:18:56 +00:00
Kegan Dougal 0fb307d09b Use the history length specified in startClient() for room initial syncs. 2015-11-03 10:15:30 +00:00
Kegsay c1160d3419 Merge pull request #36 from matrix-org/kegan/event-stream-js-errors
Wrap /events response processing in a try/catch
2015-11-02 17:13:17 +00:00
Kegsay 48253f0ff0 Merge pull request #37 from matrix-org/kegan/supports-voip
Add MatrixClient.supportsVoip()
2015-11-02 17:10:46 +00:00
Kegan Dougal c3c7ee5453 Add MatrixClient.supportsVoip()
This allows developers to gracefully degrade their UIs if VoIP is not supported.
2015-11-02 17:03:19 +00:00
David Baker c6aac8cbd9 Merge pull request #35 from matrix-org/event_read_up_to
Add event to get last read receipt for a user.
2015-11-02 16:04:16 +00:00
David Baker 1b43bc78d0 Remove unnecessary null check & s/"/'/ 2015-11-02 16:02:48 +00:00
Kegan Dougal 93a091c7e8 Wrap /events response processing in a try/catch 2015-11-02 16:02:06 +00:00
David Baker 083dde3557 Fix doc 2015-11-02 16:00:40 +00:00
David Baker 4adc5f2c85 Also need to check if the event is null 2015-11-02 15:19:29 +00:00
Daniel Wagner-Hall ced14819e4 Merge pull request #34 from matrix-org/daniel/naming
Simplify logic and layout
2015-11-02 15:07:58 +00:00
Daniel Wagner-Hall 0b42d85c5b Use double-quotes for consistency 2015-11-02 15:07:47 +00:00
David Baker c4a35020f1 Add event to get last read receipt for a user. 2015-11-02 14:39:10 +00:00
Daniel Wagner-Hall 11f052bcc6 Simplify logic and layout 2015-10-30 14:58:59 +00:00
Kegan Dougal 0ea11ea806 Add 0.3.0 browser dist 2015-10-28 16:54:07 +00:00
Kegan Dougal 7cad5a0479 Merge branch 'develop' 2015-10-28 16:49:11 +00:00
Kegan Dougal 83c53f6a79 Fix doc typo 2015-10-28 16:48:08 +00:00
Kegan Dougal ae13ed7ded Add disclaimer to screensharing 2015-10-28 16:45:07 +00:00
Kegan Dougal b17385120a Bump to 0.3.0 and add CHANGELOG 2015-10-28 16:42:44 +00:00
Kegsay cc0d8da416 Merge pull request #32 from matrix-org/member-info-for-invites
Retrieving profile info for invites
2015-10-26 16:42:21 +00:00
Kegsay c796702eba Merge pull request #31 from matrix-org/search-api
Add search functions and tests
2015-10-26 16:36:15 +00:00
Kegan Dougal 2675442ced Line lengths 2015-10-26 16:31:10 +00:00
Kegan Dougal aa3e6514c6 Add test for firing (pew pew) of events 2015-10-26 16:30:15 +00:00
Kegan Dougal be6d64fbfd Add integration tests; fix bugs. 2015-10-26 16:12:06 +00:00
Kegan Dougal 4cbab72369 Resolve invites to profile info
This is so inviters/invitees have a display name and avatar_url if they have
set one. This info isn't contained in the m.room.member event so we get it
direct from /profile.

This is gated behind `resolveInvitesToProfiles` on `startClient(opts)`.
2015-10-26 15:27:44 +00:00
Kegan Dougal 0227b1c68d Add search functions and tests 2015-10-26 13:27:45 +00:00
Matthew Hodgson 4c051202af s/getMembersWithMemership/getMembersWithMembership/g 2015-10-24 01:45:02 +01:00
Matthew Hodgson 981b9e0595 Merge branch 'screen-sharing' into develop 2015-10-23 12:58:55 +01:00
Matthew Hodgson 9e719ba31e drop res back to 640x360 as 1024x576 gave us the wrong aspect ratio 2015-10-23 12:57:46 +01:00
Kegan Dougal c65f576f8d More logging 2015-10-21 17:15:26 +01:00
Kegan Dougal 2c805bbece More paranoia when handling responses 2015-10-21 16:00:31 +01:00
Kegan Dougal 02b836698c More clarity on cache updating 2015-10-21 14:07:14 +01:00
Kegsay 25112ede58 Merge pull request #30 from matrix-org/set-state-events-perf
Change calculating display names from O(n^2) to O(n)
2015-10-21 13:47:54 +01:00
Kegan Dougal 5888c8a56c Commenting on splice 2015-10-21 13:47:23 +01:00
Kegan Dougal 1cee7bf397 JSDoc 2015-10-21 13:30:32 +01:00
Kegan Dougal cab7a71a94 Change calculating display names from O(n^2) to O(n)
Reduces initial sync times from ~30s to ~1s on accounts with heavily
populated rooms.

The problem was that f.e. RoomMember it would try to calculate the
display name, which involved looping each RoomMember to get their
display name to check for disambiguation. We now cache display names
to user IDs so we don't need to loop every member when disambiguating.
2015-10-21 13:25:23 +01:00
Matthew Hodgson d7c63e3487 Merge pull request #29 from matrix-org/screen-sharing
Basic screen-sharing support; adds a screensharing stream into the call and adds support for playback of both an audio-only stream (i.e. the voice-over) alongside an AV stream like the screenshare.
2015-10-21 01:45:38 +01:00
Matthew Hodgson bff749fd50 fix linter 2015-10-21 01:44:54 +01:00
Matthew Hodgson 5c286352cb improve constraints a bit; fix comments; try to stop sharing more aggressively 2015-10-21 01:40:20 +01:00
Matthew Hodgson 9ec3504c72 dial down logging 2015-10-21 01:40:01 +01:00
Matthew Hodgson 26b3e32ca2 add the concept of a dedicated remote audio element used for playing back audio-only streams (i.e. voice calls, and the voice stream that accompanies a screenshare). Correctly tidy up screen capture calls. 2015-10-21 01:18:55 +01:00
Kegan Dougal 4e2c83cc08 Debug logging 2015-10-20 17:21:25 +01:00
Kegan Dougal 17def14eba Get screen-sharing with audio working 2015-10-20 16:43:51 +01:00
Kegan Dougal f260de573b Add right constraints to get screen-sharing working
Requires --enable-usermedia-screen-capturing flag on chrome enabled.
2015-10-20 15:11:17 +01:00
David Baker 4fd45ab278 Merge pull request #28 from matrix-org/voip-mute
VoIP local muting
2015-10-20 10:33:24 +01:00
Kegsay 4a2e9eb927 Merge pull request #27 from matrix-org/room-avatars
Room avatars
2015-10-19 16:51:30 +01:00
Kegan Dougal dd8adef9ed Remove unused args 2015-10-19 16:50:16 +01:00
Kegan Dougal 9164debf03 Add the same for video 2015-10-19 16:48:47 +01:00
Kegan Dougal 534bef8632 Add MatrixCall.isMicrophoneMuted() 2015-10-19 16:28:01 +01:00
Kegan Dougal d8c43d02ba Add MatrixCall.setMicrophoneMuted 2015-10-19 16:21:13 +01:00
Kegsay ae3738f822 Formatting 2015-10-19 15:39:23 +01:00
Kegan Dougal be621e1aa7 Add breaking changes to CHANGELOG 2015-10-19 15:38:39 +01:00
Kegan Dougal 343d63a28a Merge branch 'develop' into room-avatars 2015-10-19 15:33:42 +01:00
Kegsay 0a28d6e950 Merge pull request #26 from matrix-org/invite-room-state
Invite room state
2015-10-19 15:31:45 +01:00
Kegsay b493a62afa Merge pull request #25 from matrix-org/initial-sync-improvements
Add support for archived=true in initial sync
2015-10-19 15:31:32 +01:00
Kegsay 37a8c9bd72 Merge pull request #23 from matrix-org/read_receipts
Receipts
2015-10-19 15:30:23 +01:00
Kegan Dougal a9c4345159 Clarify the link is the source of the code 2015-10-19 15:29:57 +01:00
Kegsay 5f1153b43f Merge pull request #24 from matrix-org/canonical-alias
Look for a canonical alias when determining the room name
2015-10-19 15:28:33 +01:00
Kegan Dougal 2c213f88d9 Units! Tests! Linting! 2015-10-19 15:24:24 +01:00
Kegan Dougal a236219111 ContentRepo unit tests 2015-10-19 15:00:06 +01:00
Kegan Dougal 2f9958cca9 JSDoc linkify 2015-10-19 14:37:17 +01:00
Kegan Dougal f26154d0ac Add support for m.room.avatar: refactor avatar URLs
BREAKING CHANGE.

Scope each "getAvatarUrl" to be instance methods on the entity it
relates to (Room and RoomMember respectively). By doing this, we
can actually pull out specific state such as the `m.room.avatar`
event more easily rather than keeping it in the global cesspit
of `MatrixClient`.

This was complicated by `getHttpUriForMxc` and `getIdenticonUri`
which were attached to the HTTP API to pull out the `baseUrl` when
crafting the URL. Pull out this dependency out and explicitly pass
it in when crafting the URL. This is trivial to get from
`MatrixClient.getHomeserverUrl()`.
2015-10-19 14:14:34 +01:00
Kegan Dougal 5ae87b7c95 Bug fixes and unit tests 2015-10-16 17:27:05 +01:00
Kegan Dougal 219103a4e2 Yank out invite event from initialSync. Set stripped state events when recalculating invited rooms. 2015-10-16 17:07:04 +01:00
Kegan Dougal 4ec7b9bb3f Add support for archived=true in initial sync
Make MatrixClient.startClient take 'opts' instead of 'historyLen' in
a backwards compatible way. Add 'includeArchivedRooms' as an option.
2015-10-16 15:00:26 +01:00
Kegan Dougal bad8b7fb76 Look for a canonical alias when determining the room name 2015-10-16 14:30:21 +01:00
Kegan Dougal a101857cb6 Add integration tests for read receipts 2015-10-16 13:51:44 +01:00
Kegan Dougal a52f92830a Implement unit tests for read receipts. 2015-10-16 13:37:53 +01:00
Kegan Dougal 40d113a423 Pass in receipts from initialSync 2015-10-16 11:54:47 +01:00
Kegan Dougal 7ec8421d19 Fix linting errors 2015-10-16 11:38:49 +01:00
Kegan Dougal 9048efeb65 Implement receipt handling and expose new Room functions
Add polyfills for Array.map/filter according to MDN because it looks much
better than the utils format.

Add stub tests for edge cases and implement test for the common case.
2015-10-16 11:32:27 +01:00
Kegan Dougal 43fc200dae Read receipt HTTP API tweaks 2015-10-16 09:36:13 +01:00
David Baker 6679e93afc Add untested read receipt sending method 2015-10-16 09:12:50 +01:00
56 changed files with 52478 additions and 1326 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
+1 -1
View File
@@ -5,7 +5,7 @@
"nonew": true,
"curly": true,
"forin": true,
"freeze": true,
"freeze": false,
"undef": true,
"unused": "vars"
}
+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
+146 -7
View File
@@ -1,3 +1,142 @@
[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
================
**BREAKING CHANGES**:
* `RoomMember.getAvatarUrl()` and `MatrixClient.mxcUrlToHttp()` now return the
empty string when given anything other than an mxc:// URL. This ensures that
clients never inadvertantly reference content directly, leaking information
to third party servers. The `allowDirectLinks` option is provided if the client
wants to allow such links.
* Add a 'bindEmail' option to register()
Improvements:
* Support third party invites
* More appropriate naming for third party invite rooms
* Poll the 'versions' endpoint to re-establish connectivity
* Catch exceptions when syncing
* Room tag support
* Generate implicit read receipts
* Support CAS login
* Guest access support
* Never return non-mxc URLs by default
* Ability to cancel file uploads
* Use the Matrix C/S API v2 with r0 prefix
* Account data support
* 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
================
* `MatrixClient.getAvatarUrlForMember` has been removed and replaced with
`RoomMember.getAvatarUrl`. Arguments remain the same except the homeserver
URL must now be supplied from `MatrixClient.getHomeserverUrl()`.
```javascript
// before
var url = client.getAvatarUrlForMember(member, width, height, resize, allowDefault)
// after
var url = member.getAvatarUrl(client.getHomeserverUrl(), width, height, resize, allowDefault)
```
* `MatrixClient.getAvatarUrlForRoom` has been removed and replaced with
`Room.getAvatarUrl`. Arguments remain the same except the homeserver
URL must now be supplied from `MatrixClient.getHomeserverUrl()`.
```javascript
// before
var url = client.getAvatarUrlForRoom(room, width, height, resize, allowDefault)
// after
var url = room.getAvatarUrl(client.getHomeserverUrl(), width, height, resize, allowDefault)
```
* `s/Room.getMembersWithMemership/Room.getMembersWithMem`b`ership/g`
New methods:
* Added support for sending receipts via
`MatrixClient.sendReceipt(event, receiptType, callback)` and
`MatrixClient.sendReadReceipt(event, callback)`.
* Added support for receiving receipts via
`Room.getReceiptsForEvent(event)` and `Room.getUsersReadUpTo(event)`. Receipts
can be directly added to a `Room` using `Room.addReceipt(event)` though the
`MatrixClient` does this for you.
* Added support for muting local video and audio via the new methods
`MatrixCall.setMicrophoneMuted()`, `MatrixCall.isMicrophoneMuted(muted)`,
`MatrixCall.isLocalVideoMuted()` and `Matrix.setLocalVideoMuted(muted)`.
* Added **experimental** support for screen-sharing in Chrome via
`MatrixCall.placeScreenSharingCall(remoteVideoElement, localVideoElement)`.
* Added ability to perform server-side searches using
`MatrixClient.searchMessageText(opts)` and `MatrixClient.search(opts)`.
Improvements:
* Improve the performance of initial sync processing from `O(n^2)` to `O(n)`.
* `Room.name` will now take into account `m.room.canonical_alias` events.
* `MatrixClient.startClient` now takes an Object `opts` rather than a Number in
a backwards-compatible way. This `opts` allows syncing configuration options
to be specified including `includeArchivedRooms` and `resolveInvitesToProfiles`.
* `Room` objects which represent room invitations will now have state populated
from `invite_room_state` if it is included in the `m.room.member` event.
* `Room.getAvatarUrl` will now take into account `m.room.avatar` events.
Changes in 0.2.2
================
@@ -22,6 +161,10 @@ New methods:
Changes in 0.2.1
================
**BREAKING CHANGES**
* `MatrixClient.joinRoom` has changed from `(roomIdOrAlias, callback)` to
`(roomIdOrAlias, opts, callback)`.
Bug fixes:
* The `Content-Type` of file uploads is now explicitly set, without relying
on the browser to do it for us.
@@ -33,10 +176,6 @@ Improvements:
* There is now a try/catch block around the `request` function which will
reject/errback appropriately if an exception is thrown synchronously in it.
Breaking changes:
* `MatrixClient.joinRoom` has changed from `(roomIdOrAlias, callback)` to
`(roomIdOrAlias, opts, callback)`.
New methods:
* `MatrixClient.createAlias(alias, roomId)`
* `MatrixClient.getRoomIdForAlias(alias)`
@@ -57,7 +196,7 @@ Modified methods:
Changes in 0.2.0
================
Breaking changes:
**BREAKING CHANGES**:
* `MatrixClient.setPowerLevel` now expects a `MatrixEvent` and not an `Object`
for the `event` parameter.
@@ -88,7 +227,7 @@ New methods:
* `MatrixClient.mxcUrlToHttp(url, w, h, method)`
* `MatrixClient.getAvatarUrlForRoom(room, w, h, method)`
* `MatrixClient.uploadContent(file, callback)`
* `Room.getMembersWithMemership(membership)`
* `Room.getMembersWithMembership(membership)`
* `MatrixScheduler.getQueueForEvent(event)`
* `MatrixScheduler.removeEventFromQueue(event)`
* `$DATA_STORE.setSyncToken(token)`
@@ -123,7 +262,7 @@ Bug fixes:
Changes in 0.1.1
================
Breaking changes:
**BREAKING CHANGES**:
* `Room.calculateRoomName` is now private. Use `Room.recalculate` instead, and
access the calculated name via `Room.name`.
* `new MatrixClient(...)` no longer creates a `MatrixInMemoryStore` if
+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`.
+10864
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+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
+3
View File
@@ -1,3 +1,6 @@
var matrixcs = require("./lib/matrix");
matrixcs.request(require("request"));
module.exports = matrixcs;
var utils = require("./lib/utils");
utils.runPolyfills();
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
+1392 -471
View File
File diff suppressed because it is too large Load Diff
+105
View File
@@ -0,0 +1,105 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* @module content-repo
*/
var utils = require("./utils");
/** Content Repo utility functions */
module.exports = {
/**
* Get the HTTP URL for an MXC URI.
* @param {string} baseUrl The base homeserver url which has a content repo.
* @param {string} mxc The mxc:// URI.
* @param {Number} width The desired width of the thumbnail.
* @param {Number} height The desired height of the thumbnail.
* @param {string} resizeMethod The thumbnail resize method to use, either
* "crop" or "scale".
* @param {Boolean} allowDirectLinks If true, return any non-mxc URLs
* directly. Fetching such URLs will leak information about the user to
* anyone they share a room with. If false, will return the emptry string
* for such URLs.
* @return {string} The complete URL to the content.
*/
getHttpUriForMxc: function(baseUrl, mxc, width, height,
resizeMethod, allowDirectLinks) {
if (typeof mxc !== "string" || !mxc) {
return '';
}
if (mxc.indexOf("mxc://") !== 0) {
if (allowDirectLinks) {
return mxc;
} else {
return '';
}
}
var serverAndMediaId = mxc.slice(6); // strips mxc://
var prefix = "/_matrix/media/v1/download/";
var params = {};
if (width) {
params.width = width;
}
if (height) {
params.height = height;
}
if (resizeMethod) {
params.method = resizeMethod;
}
if (utils.keys(params).length > 0) {
// these are thumbnailing params so they probably want the
// thumbnailing API...
prefix = "/_matrix/media/v1/thumbnail/";
}
var fragmentOffset = serverAndMediaId.indexOf("#"),
fragment = "";
if (fragmentOffset >= 0) {
fragment = serverAndMediaId.substr(fragmentOffset);
serverAndMediaId = serverAndMediaId.substr(0, fragmentOffset);
}
return baseUrl + prefix + serverAndMediaId +
(utils.keys(params).length === 0 ? "" :
("?" + utils.encodeParams(params))) + fragment;
},
/**
* Get an identicon URL from an arbitrary string.
* @param {string} baseUrl The base homeserver url which has a content repo.
* @param {string} identiconString The string to create an identicon for.
* @param {Number} width The desired width of the image in pixels. Default: 96.
* @param {Number} height The desired height of the image in pixels. Default: 96.
* @return {string} The complete URL to the identicon.
*/
getIdenticonUri: function(baseUrl, identiconString, width, height) {
if (!identiconString) {
return null;
}
if (!width) { width = 96; }
if (!height) { height = 96; }
var params = {
width: width,
height: height
};
var path = utils.encodeUri("/_matrix/media/v1/identicon/$ident", {
$ident: identiconString
});
return baseUrl + path +
(utils.keys(params).length === 0 ? "" :
("?" + utils.encodeParams(params)));
}
};
+100
View File
@@ -0,0 +1,100 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* @module filter
*/
/**
* @param {Object} obj
* @param {string} keyNesting
* @param {*} val
*/
function setProp(obj, keyNesting, val) {
var nestedKeys = keyNesting.split(".");
var currentObj = obj;
for (var i = 0; i < (nestedKeys.length - 1); i++) {
if (!currentObj[nestedKeys[i]]) {
currentObj[nestedKeys[i]] = {};
}
currentObj = currentObj[nestedKeys[i]];
}
currentObj[nestedKeys[nestedKeys.length - 1]] = val;
}
/**
* Construct a new Filter.
* @constructor
* @param {string} userId The user ID for this filter.
* @param {string=} filterId The filter ID if known.
* @prop {string} userId The user ID of the filter
* @prop {?string} filterId The filter ID
*/
function Filter(userId, filterId) {
this.userId = userId;
this.filterId = filterId;
this.definition = {};
}
/**
* Get the JSON body of the filter.
* @return {Object} The filter definition
*/
Filter.prototype.getDefinition = function() {
return this.definition;
};
/**
* Set the JSON body of the filter
* @param {Object} definition The filter definition
*/
Filter.prototype.setDefinition = function(definition) {
this.definition = definition;
};
/**
* Set the max number of events to return for each room's timeline.
* @param {Number} limit The max number of events to return for each room.
*/
Filter.prototype.setTimelineLimit = function(limit) {
setProp(this.definition, "room.timeline.limit", limit);
};
/**
* Control whether left rooms should be included in responses.
* @param {boolean} includeLeave True to make rooms the user has left appear
* in responses.
*/
Filter.prototype.setIncludeLeaveRooms = function(includeLeave) {
setProp(this.definition, "room.include_leave", includeLeave);
};
/**
* Create a filter from existing data.
* @static
* @param {string} userId
* @param {string} filterId
* @param {Object} jsonObj
* @return {Filter}
*/
Filter.fromJson = function(userId, filterId, jsonObj) {
var filter = new Filter(userId, filterId);
filter.setDefinition(jsonObj);
return filter;
};
/** The Filter class */
module.exports = Filter;
+195 -103
View File
@@ -1,3 +1,18 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* This is an internal module. See {@link MatrixHttpApi} for the public class.
@@ -13,15 +28,14 @@ TODO:
*/
/**
* A constant representing the URI path for version 1 of the Client-Server HTTP API.
* A constant representing the URI path for release 0 of the Client-Server HTTP API.
*/
module.exports.PREFIX_V1 = "/_matrix/client/api/v1";
module.exports.PREFIX_R0 = "/_matrix/client/r0";
/**
* A constant representing the URI path for version 2 alpha of the Client-Server
* HTTP API.
* A constant representing the URI path for as-yet unspecified Client-Server HTTP APIs.
*/
module.exports.PREFIX_V2_ALPHA = "/_matrix/client/v2_alpha";
module.exports.PREFIX_UNSTABLE = "/_matrix/client/unstable";
/**
* URI path for the identity API
@@ -31,13 +45,14 @@ 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'.
* @param {Function} opts.request Required. The function to call for HTTP
* requests. This function must look like function(opts, callback){ ... }.
* @param {string} opts.prefix Required. The matrix client prefix to use, e.g.
* '/_matrix/client/api/v1'. See PREFIX_V1 and PREFIX_V2_ALPHA for constants.
* '/_matrix/client/r0'. See PREFIX_R0 and PREFIX_UNSTABLE for constants.
* @param {bool} opts.onlyData True to return only the 'data' component of the
* response (e.g. the parsed HTTP body). If false, requests will return status
* codes and headers in addition to data. Default: false.
@@ -46,89 +61,16 @@ 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 = [];
};
module.exports.MatrixHttpApi.prototype = {
// URI functions
// =============
/**
* Get the HTTP URL for an MXC URI.
* @param {string} mxc The mxc:// URI.
* @param {Number} width The desired width of the thumbnail.
* @param {Number} height The desired height of the thumbnail.
* @param {string} resizeMethod The thumbnail resize method to use, either
* "crop" or "scale".
* @return {string} The complete URL to the content.
*/
getHttpUriForMxc: function(mxc, width, height, resizeMethod) {
if (typeof mxc !== "string" || !mxc) {
return mxc;
}
if (mxc.indexOf("mxc://") !== 0) {
return mxc;
}
var serverAndMediaId = mxc.slice(6); // strips mxc://
var prefix = "/_matrix/media/v1/download/";
var params = {};
if (width) {
params.width = width;
}
if (height) {
params.height = height;
}
if (resizeMethod) {
params.method = resizeMethod;
}
if (utils.keys(params).length > 0) {
// these are thumbnailing params so they probably want the
// thumbnailing API...
prefix = "/_matrix/media/v1/thumbnail/";
}
var fragmentOffset = serverAndMediaId.indexOf("#"),
fragment = "";
if (fragmentOffset >= 0) {
fragment = serverAndMediaId.substr(fragmentOffset);
serverAndMediaId = serverAndMediaId.substr(0, fragmentOffset);
}
return this.opts.baseUrl + prefix + serverAndMediaId +
(utils.keys(params).length === 0 ? "" :
("?" + utils.encodeParams(params))) + fragment;
},
/**
* Get an identicon URL from an arbitrary string.
* @param {string} identiconString The string to create an identicon for.
* @param {Number} width The desired width of the image in pixels.
* @param {Number} height The desired height of the image in pixels.
* @return {string} The complete URL to the identicon.
*/
getIdenticonUri: function(identiconString, width, height) {
if (!identiconString) {
return;
}
if (!width) { width = 96; }
if (!height) { height = 96; }
var params = {
width: width,
height: height
};
var path = utils.encodeUri("/_matrix/media/v1/identicon/$ident", {
$ident: identiconString
});
return this.opts.baseUrl + path +
(utils.keys(params).length === 0 ? "" :
("?" + utils.encodeParams(params)));
},
/**
* Get the content repository url with query parameters.
* @return {Object} An object with a 'base', 'path' and 'params' for base URL,
@@ -170,8 +112,12 @@ module.exports.MatrixHttpApi.prototype = {
// use XMLHttpRequest directly.
// (browser-request doesn't support progress either, which is also kind
// of important here)
var upload = { loaded: 0, total: 0 };
if (global.XMLHttpRequest) {
var xhr = new global.XMLHttpRequest();
upload.xhr = xhr;
var cb = requestCallback(defer, callback, this.opts.onlyData);
var timeout_fn = function() {
@@ -185,10 +131,19 @@ module.exports.MatrixHttpApi.prototype = {
switch (xhr.readyState) {
case global.XMLHttpRequest.DONE:
clearTimeout(xhr.timeout_timer);
var err;
if (!xhr.responseText) {
err = new Error('No response body.');
err.http_status = xhr.status;
cb(err);
return;
}
var resp = JSON.parse(xhr.responseText);
if (resp.content_uri === undefined) {
cb(new Error('Bad response'));
err = Error('Bad response');
err.http_status = xhr.status;
cb(err);
return;
}
@@ -198,6 +153,8 @@ module.exports.MatrixHttpApi.prototype = {
};
xhr.upload.addEventListener("progress", function(ev) {
clearTimeout(xhr.timeout_timer);
upload.loaded = ev.loaded;
upload.total = ev.total;
xhr.timeout_timer = setTimeout(timeout_fn, 30000);
defer.notify(ev);
});
@@ -218,16 +175,47 @@ module.exports.MatrixHttpApi.prototype = {
filename: file.name,
access_token: this.opts.accessToken
};
file.stream.pipe(
this.opts.request({
uri: url,
qs: queryParams,
method: "POST"
}, requestCallback(defer, callback, this.opts.onlyData))
);
upload.request = this.opts.request({
uri: url,
qs: queryParams,
method: "POST"
}, requestCallback(defer, callback, this.opts.onlyData));
file.stream.pipe(this.opts.request);
}
return defer.promise;
this.uploads.push(upload);
var self = this;
upload.promise = defer.promise.finally(function() {
var uploadsKeys = Object.keys(self.uploads);
for (var i = 0; i < uploadsKeys.length; ++i) {
if (self.uploads[uploadsKeys[i]].promise === defer.promise) {
self.uploads.splice(uploadsKeys[i], 1);
}
}
});
return upload.promise;
},
cancelUpload: function(promise) {
var uploadsKeys = Object.keys(this.uploads);
for (var i = 0; i < uploadsKeys.length; ++i) {
var upload = this.uploads[uploadsKeys[i]];
if (upload.promise === promise) {
if (upload.xhr !== undefined) {
upload.xhr.abort();
return true;
} else if (upload.request !== undefined) {
upload.request.abort();
return true;
}
}
}
return false;
},
getCurrentUploads: function() {
return this.uploads;
},
idServerRequest: function(callback, method, path, params, prefix) {
@@ -270,6 +258,8 @@ module.exports.MatrixHttpApi.prototype = {
* @param {Object} queryParams A dict of query params (these will NOT be
* urlencoded).
* @param {Object} data The HTTP JSON body.
* @param {Number=} localTimeoutMs The maximum amount of time to wait before
* timing out the request. If not specified, there is no timeout.
* @return {module:client.Promise} Resolves to <code>{data: {Object},
* headers: {Object}, code: {Number}}</code>.
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
@@ -277,10 +267,21 @@ module.exports.MatrixHttpApi.prototype = {
* @return {module:http-api.MatrixError} Rejects with an error if a problem
* occurred. This includes network problems and Matrix-specific error JSON.
*/
authedRequest: function(callback, method, path, queryParams, data) {
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);
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;
},
/**
@@ -293,6 +294,8 @@ module.exports.MatrixHttpApi.prototype = {
* @param {Object} queryParams A dict of query params (these will NOT be
* urlencoded).
* @param {Object} data The HTTP JSON body.
* @param {Number=} localTimeoutMs The maximum amount of time to wait before
* timing out the request. If not specified, there is no timeout.
* @return {module:client.Promise} Resolves to <code>{data: {Object},
* headers: {Object}, code: {Number}}</code>.
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
@@ -300,9 +303,9 @@ module.exports.MatrixHttpApi.prototype = {
* @return {module:http-api.MatrixError} Rejects with an error if a problem
* occurred. This includes network problems and Matrix-specific error JSON.
*/
request: function(callback, method, path, queryParams, data) {
request: function(callback, method, path, queryParams, data, localTimeoutMs) {
return this.requestWithPrefix(
callback, method, path, queryParams, data, this.opts.prefix
callback, method, path, queryParams, data, this.opts.prefix, localTimeoutMs
);
},
@@ -320,6 +323,8 @@ module.exports.MatrixHttpApi.prototype = {
* @param {Object} data The HTTP JSON body.
* @param {string} prefix The full prefix to use e.g.
* "/_matrix/client/v2_alpha".
* @param {Number=} localTimeoutMs The maximum amount of time to wait before
* timing out the request. If not specified, there is no timeout.
* @return {module:client.Promise} Resolves to <code>{data: {Object},
* headers: {Object}, code: {Number}}</code>.
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
@@ -328,13 +333,15 @@ module.exports.MatrixHttpApi.prototype = {
* occurred. This includes network problems and Matrix-specific error JSON.
*/
authedRequestWithPrefix: function(callback, method, path, queryParams, data,
prefix) {
prefix, localTimeoutMs) {
var fullUri = this.opts.baseUrl + prefix + path;
if (!queryParams) {
queryParams = {};
}
queryParams.access_token = this.opts.accessToken;
return this._request(callback, method, fullUri, queryParams, data);
return this._request(
callback, method, fullUri, queryParams, data, localTimeoutMs
);
},
/**
@@ -351,6 +358,8 @@ module.exports.MatrixHttpApi.prototype = {
* @param {Object} data The HTTP JSON body.
* @param {string} prefix The full prefix to use e.g.
* "/_matrix/client/v2_alpha".
* @param {Number=} localTimeoutMs The maximum amount of time to wait before
* timing out the request. If not specified, there is no timeout.
* @return {module:client.Promise} Resolves to <code>{data: {Object},
* headers: {Object}, code: {Number}}</code>.
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
@@ -358,20 +367,71 @@ module.exports.MatrixHttpApi.prototype = {
* @return {module:http-api.MatrixError} Rejects with an error if a problem
* occurred. This includes network problems and Matrix-specific error JSON.
*/
requestWithPrefix: function(callback, method, path, queryParams, data, prefix) {
requestWithPrefix: function(callback, method, path, queryParams, data, prefix,
localTimeoutMs) {
var fullUri = this.opts.baseUrl + prefix + path;
if (!queryParams) {
queryParams = {};
}
return this._request(callback, method, fullUri, queryParams, data);
return this._request(
callback, method, fullUri, queryParams, data, localTimeoutMs
);
},
_request: function(callback, method, uri, queryParams, data) {
/**
* Perform a request to an arbitrary URL.
* @param {Function} callback Optional. The callback to invoke on
* success/failure. See the promise return values for more information.
* @param {string} method The HTTP method e.g. "GET".
* @param {string} uri The HTTP URI
* @param {Object} queryParams A dict of query params (these will NOT be
* urlencoded).
* @param {Object} data The HTTP JSON body.
* @param {Number=} localTimeoutMs The maximum amount of time to wait before
* timing out the request. If not specified, there is no timeout.
* @return {module:client.Promise} Resolves to <code>{data: {Object},
* headers: {Object}, code: {Number}}</code>.
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
* object only.
* @return {module:http-api.MatrixError} Rejects with an error if a problem
* occurred. This includes network problems and Matrix-specific error JSON.
*/
requestOtherUrl: function(callback, method, uri, queryParams, data,
localTimeoutMs) {
if (!queryParams) {
queryParams = {};
}
return this._request(
callback, method, uri, queryParams, data, localTimeoutMs
);
},
/**
* Form and return a homeserver request URL based on the given path
* params and prefix.
* @param {string} path The HTTP path <b>after</b> the supplied prefix e.g.
* "/createRoom".
* @param {Object} queryParams A dict of query params (these will NOT be
* urlencoded).
* @param {string} prefix The full prefix to use e.g.
* "/_matrix/client/v2_alpha".
* @return {string} URL
*/
getUrl: function(path, queryParams, prefix) {
var queryString = "";
if (queryParams) {
queryString = "?" + utils.encodeParams(queryParams);
}
return this.opts.baseUrl + prefix + path + queryString;
},
_request: function(callback, method, uri, queryParams, data, localTimeoutMs) {
if (callback !== undefined && !utils.isFunction(callback)) {
throw Error(
"Expected callback to be a function but got " + typeof callback
);
}
var self = this;
if (!queryParams) {
queryParams = {};
}
@@ -382,8 +442,24 @@ module.exports.MatrixHttpApi.prototype = {
}
}
var defer = q.defer();
var timeoutId;
var timedOut = false;
if (localTimeoutMs) {
timeoutId = setTimeout(function() {
timedOut = true;
defer.reject(new module.exports.MatrixError({
error: "Locally timed out waiting for a response",
errcode: "ORG.MATRIX.JSSDK_TIMEOUT",
timeout: localTimeoutMs
}));
}, localTimeoutMs);
}
var reqPromise = defer.promise;
try {
this.opts.request(
var req = this.opts.request(
{
uri: uri,
method: method,
@@ -391,10 +467,25 @@ module.exports.MatrixHttpApi.prototype = {
qs: queryParams,
body: data,
json: true,
timeout: localTimeoutMs,
_matrix_opts: this.opts
},
requestCallback(defer, callback, this.opts.onlyData)
function(err, response, body) {
if (localTimeoutMs) {
clearTimeout(timeoutId);
if (timedOut) {
return; // already rejected promise
}
}
var handlerFn = requestCallback(defer, callback, self.opts.onlyData);
handlerFn(err, response, body);
}
);
if (req && req.abort) {
// FIXME: This is EVIL, but I can't think of a better way to expose
// abort() operations on underlying HTTP requests :(
reqPromise.abort = req.abort.bind(req);
}
}
catch (ex) {
defer.reject(ex);
@@ -402,7 +493,7 @@ module.exports.MatrixHttpApi.prototype = {
callback(ex);
}
}
return defer.promise;
return reqPromise;
}
};
@@ -451,6 +542,7 @@ var requestCallback = function(defer, userDefinedCallback, onlyData) {
* @prop {integer} httpStatus The numeric HTTP status code given
*/
module.exports.MatrixError = function MatrixError(errorJson) {
errorJson = errorJson || {};
this.errcode = errorJson.errcode;
this.name = errorJson.errcode || "Unknown error code";
this.message = errorJson.error || "Unknown message";
+26 -1
View File
@@ -1,3 +1,18 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/** The {@link module:models/event.MatrixEvent|MatrixEvent} class. */
@@ -17,6 +32,8 @@ module.exports.MatrixError = require("./http-api").MatrixError;
module.exports.MatrixClient = require("./client").MatrixClient;
/** The {@link module:models/room~Room|Room} class. */
module.exports.Room = require("./models/room");
/** The {@link module:models/event-timeline~EventTimeline} class. */
module.exports.EventTimeline = require("./models/event-timeline");
/** The {@link module:models/room-member~RoomMember|RoomMember} class. */
module.exports.RoomMember = require("./models/room-member");
/** The {@link module:models/room-state~RoomState|RoomState} class. */
@@ -30,6 +47,12 @@ module.exports.MatrixScheduler = require("./scheduler");
module.exports.WebStorageSessionStore = require("./store/session/webstorage");
/** True if crypto libraries are being used on this client. */
module.exports.CRYPTO_ENABLED = require("./client").CRYPTO_ENABLED;
/** {@link module:content-repo|ContentRepo} utility functions. */
module.exports.ContentRepo = require("./content-repo");
/** The {@link module:filter~Filter|Filter} class. */
module.exports.Filter = require("./filter");
/** The {@link module:timeline-window~TimelineWindow} class. */
module.exports.TimelineWindow = require("./timeline-window").TimelineWindow;
/**
* Create a new Matrix Call.
@@ -77,7 +100,9 @@ module.exports.createClient = function(opts) {
};
}
opts.request = opts.request || request;
opts.store = opts.store || new module.exports.MatrixInMemoryStore();
opts.store = opts.store || new module.exports.MatrixInMemoryStore({
localStorage: global.localStorage
});
opts.scheduler = opts.scheduler || new module.exports.MatrixScheduler();
return new module.exports.MatrixClient(opts);
};
+119
View File
@@ -0,0 +1,119 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* @module models/event-context
*/
/**
* Construct a new EventContext
*
* An eventcontext is used for circumstances such as search results, when we
* have a particular event of interest, and a bunch of events before and after
* it.
*
* It also stores pagination tokens for going backwards and forwards in the
* timeline.
*
* @param {MatrixEvent} ourEvent the event at the centre of this context
*
* @constructor
*/
function EventContext(ourEvent) {
this._timeline = [ourEvent];
this._ourEventIndex = 0;
this._paginateTokens = {b: null, f: null};
// this is used by MatrixClient to keep track of active requests
this._paginateRequests = {b: null, f: null};
}
/**
* Get the main event of interest
*
* This is a convenience function for getTimeline()[getOurEventIndex()].
*
* @return {MatrixEvent} The event at the centre of this context.
*/
EventContext.prototype.getEvent = function() {
return this._timeline[this._ourEventIndex];
};
/**
* Get the list of events in this context
*
* @return {Array} An array of MatrixEvents
*/
EventContext.prototype.getTimeline = function() {
return this._timeline;
};
/**
* Get the index in the timeline of our event
*
* @return {Number}
*/
EventContext.prototype.getOurEventIndex = function() {
return this._ourEventIndex;
};
/**
* Get a pagination token.
*
* @param {boolean} backwards true to get the pagination token for going
* backwards in time
* @return {string}
*/
EventContext.prototype.getPaginateToken = function(backwards) {
return this._paginateTokens[backwards ? 'b' : 'f'];
};
/**
* Set a pagination token.
*
* Generally this will be used only by the matrix js sdk.
*
* @param {string} token pagination token
* @param {boolean} backwards true to set the pagination token for going
* backwards in time
*/
EventContext.prototype.setPaginateToken = function(token, backwards) {
this._paginateTokens[backwards ? 'b' : 'f'] = token;
};
/**
* Add more events to the timeline
*
* @param {Array} events new events, in timeline order
* @param {boolean} atStart true to insert new events at the start
*/
EventContext.prototype.addEvents = function(events, atStart) {
// TODO: should we share logic with Room.addEventsToTimeline?
// Should Room even use EventContext?
if (atStart) {
this._timeline = events.concat(this._timeline);
this._ourEventIndex += events.length;
} else {
this._timeline = this._timeline.concat(events);
}
};
/**
* The EventContext class
*/
module.exports = EventContext;
+310
View File
@@ -0,0 +1,310 @@
"use strict";
/**
* @module models/event-timeline
*/
var RoomState = require("./room-state");
var utils = require("../utils");
var MatrixEvent = require("./event").MatrixEvent;
/**
* Construct a new EventTimeline
*
* <p>An EventTimeline represents a contiguous sequence of events in a room.
*
* <p>As well as keeping track of the events themselves, it stores the state of
* the room at the beginning and end of the timeline, and pagination tokens for
* going backwards and forwards in the timeline.
*
* <p>In order that clients can meaningfully maintain an index into a timeline,
* the EventTimeline object tracks a 'baseIndex'. This starts at zero, but is
* incremented when events are prepended to the timeline. The index of an event
* relative to baseIndex therefore remains constant.
*
* <p>Once a timeline joins up with its neighbour, they are linked together into a
* doubly-linked list.
*
* @param {string} roomId the ID of the room where this timeline came from
* @constructor
*/
function EventTimeline(roomId) {
this._roomId = roomId;
this._events = [];
this._baseIndex = 0;
this._startState = new RoomState(roomId);
this._startState.paginationToken = null;
this._endState = new RoomState(roomId);
this._endState.paginationToken = null;
this._prevTimeline = null;
this._nextTimeline = null;
// this is used by client.js
this._paginationRequests = {'b': null, 'f': null};
}
/**
* Symbolic constant for methods which take a 'direction' argument:
* refers to the start of the timeline, or backwards in time.
*/
EventTimeline.BACKWARDS = "b";
/**
* Symbolic constant for methods which take a 'direction' argument:
* refers to the end of the timeline, or forwards in time.
*/
EventTimeline.FORWARDS = "f";
/**
* Initialise the start and end state with the given events
*
* <p>This can only be called before any events are added.
*
* @param {MatrixEvent[]} stateEvents list of state events to initialise the
* state with.
* @throws {Error} if an attempt is made to call this after addEvent is called.
*/
EventTimeline.prototype.initialiseState = function(stateEvents) {
if (this._events.length > 0) {
throw new Error("Cannot initialise state after events are added");
}
// we deep-copy the events here, in case they get changed later - we don't
// want changes to the start state leaking through to the end state.
var oldStateEvents = utils.map(
utils.deepCopy(
stateEvents.map(function(mxEvent) { return mxEvent.event; })
), function(ev) { return new MatrixEvent(ev); });
this._startState.setStateEvents(oldStateEvents);
this._endState.setStateEvents(stateEvents);
};
/**
* Get the ID of the room for this timeline
* @return {string} room ID
*/
EventTimeline.prototype.getRoomId = function() {
return this._roomId;
};
/**
* Get the base index.
*
* <p>This is an index which is incremented when events are prepended to the
* timeline. An individual event therefore stays at the same index in the array
* relative to the base index (although note that a given event's index may
* well be less than the base index, thus giving that event a negative relative
* index).
*
* @return {number}
*/
EventTimeline.prototype.getBaseIndex = function() {
return this._baseIndex;
};
/**
* Get the list of events in this context
*
* @return {MatrixEvent[]} An array of MatrixEvents
*/
EventTimeline.prototype.getEvents = function() {
return this._events;
};
/**
* Get the room state at the start/end of the timeline
*
* @param {string} direction EventTimeline.BACKWARDS to get the state at the
* start of the timeline; EventTimeline.FORWARDS to get the state at the end
* of the timeline.
*
* @return {RoomState} state at the start/end of the timeline
*/
EventTimeline.prototype.getState = function(direction) {
if (direction == EventTimeline.BACKWARDS) {
return this._startState;
} else if (direction == EventTimeline.FORWARDS) {
return this._endState;
} else {
throw new Error("Invalid direction '" + direction + "'");
}
};
/**
* Get a pagination token
*
* @param {string} direction EventTimeline.BACKWARDS to get the pagination
* token for going backwards in time; EventTimeline.FORWARDS to get the
* pagination token for going forwards in time.
*
* @return {?string} pagination token
*/
EventTimeline.prototype.getPaginationToken = function(direction) {
return this.getState(direction).paginationToken;
};
/**
* Set a pagination token
*
* @param {?string} token pagination token
*
* @param {string} direction EventTimeline.BACKWARDS to set the pagination
* token for going backwards in time; EventTimeline.FORWARDS to set the
* pagination token for going forwards in time.
*/
EventTimeline.prototype.setPaginationToken = function(token, direction) {
this.getState(direction).paginationToken = token;
};
/**
* Get the next timeline in the series
*
* @param {string} direction EventTimeline.BACKWARDS to get the previous
* timeline; EventTimeline.FORWARDS to get the next timeline.
*
* @return {?EventTimeline} previous or following timeline, if they have been
* joined up.
*/
EventTimeline.prototype.getNeighbouringTimeline = function(direction) {
if (direction == EventTimeline.BACKWARDS) {
return this._prevTimeline;
} else if (direction == EventTimeline.FORWARDS) {
return this._nextTimeline;
} else {
throw new Error("Invalid direction '" + direction + "'");
}
};
/**
* Set the next timeline in the series
*
* @param {EventTimeline} neighbour previous/following timeline
*
* @param {string} direction EventTimeline.BACKWARDS to set the previous
* timeline; EventTimeline.FORWARDS to set the next timeline.
*
* @throws {Error} if an attempt is made to set the neighbouring timeline when
* it is already set.
*/
EventTimeline.prototype.setNeighbouringTimeline = function(neighbour, direction) {
if (this.getNeighbouringTimeline(direction)) {
throw new Error("timeline already has a neighbouring timeline - " +
"cannot reset neighbour");
}
if (direction == EventTimeline.BACKWARDS) {
this._prevTimeline = neighbour;
} else if (direction == EventTimeline.FORWARDS) {
this._nextTimeline = neighbour;
} else {
throw new Error("Invalid direction '" + direction + "'");
}
// make sure we don't try to paginate this timeline
this.setPaginationToken(null, direction);
};
/**
* Add a new event to the timeline, and update the state
*
* @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) {
var stateContext = atStart ? this._startState : this._endState;
setEventMetadata(event, stateContext, atStart);
// modify state
if (event.isState()) {
stateContext.setStateEvents([event]);
// it is possible that the act of setting the state event means we
// can set more metadata (specifically sender/target props), so try
// 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.
//
// 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);
}
}
var insertIndex;
if (atStart) {
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
if (atStart) {
this._baseIndex++;
}
};
function setEventMetadata(event, stateContext, toStartOfTimeline) {
// set sender and target properties
event.sender = stateContext.getSentinelMember(
event.getSender()
);
if (event.getType() === "m.room.member") {
event.target = stateContext.getSentinelMember(
event.getStateKey()
);
}
if (event.isState()) {
// room state has no concept of 'old' or 'current', but we want the
// room state to regress back to previous values if toStartOfTimeline
// is set, which means inspecting prev_content if it exists. This
// is done by toggling the forwardLooking flag.
if (toStartOfTimeline) {
event.forwardLooking = false;
}
}
}
/**
* Remove an event from the timeline
*
* @param {string} eventId ID of event to be removed
* @return {?MatrixEvent} removed event, or null if not found
*/
EventTimeline.prototype.removeEvent = function(eventId) {
for (var i = this._events.length - 1; i >= 0; i--) {
var ev = this._events[i];
if (ev.getId() == eventId) {
this._events.splice(i, 1);
if (i < this._baseIndex) {
this._baseIndex--;
}
return ev;
}
}
return null;
};
/**
* The EventTimeline class
*/
module.exports = EventTimeline;
+104 -5
View File
@@ -1,3 +1,18 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
@@ -6,7 +21,6 @@
* @module models/event
*/
/**
* Enum for event statuses.
* @readonly
@@ -63,7 +77,7 @@ module.exports.MatrixEvent.prototype = {
* @return {string} The user ID, e.g. <code>@alice:matrix.org</code>
*/
getSender: function() {
return this.event.user_id;
return this.event.sender || this.event.user_id; // v2 / v1
},
/**
@@ -122,12 +136,15 @@ module.exports.MatrixEvent.prototype = {
* @return {Object} The previous event content JSON, or an empty object.
*/
getPrevContent: function() {
return this.event.prev_content || {};
// v2 then v1 then default
return this.getUnsigned().prev_content || this.event.prev_content || {};
},
/**
* 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.
@@ -143,7 +160,7 @@ module.exports.MatrixEvent.prototype = {
* @return {Number} The age of this event in milliseconds.
*/
getAge: function() {
return this.event.age;
return this.getUnsigned().age || this.event.age; // v2 / v1
},
/**
@@ -169,5 +186,87 @@ module.exports.MatrixEvent.prototype = {
*/
isEncrypted: function() {
return this.encrypted;
}
},
getUnsigned: function() {
return this.event.unsigned || {};
},
/**
* Update the content of an event in the same way it would be by the server
* if it were redacted before it was sent to us
*
* @param {Object} the raw event causing the redaction
*/
makeRedacted: function(redaction_event) {
if (!this.event.unsigned) {
this.event.unsigned = {};
}
this.event.unsigned.redacted_because = redaction_event;
var key;
for (key in this.event) {
if (!this.event.hasOwnProperty(key)) { continue; }
if (!_REDACT_KEEP_KEY_MAP[key]) {
delete this.event[key];
}
}
var keeps = _REDACT_KEEP_CONTENT_MAP[this.getType()] || {};
for (key in this.event.content) {
if (!this.event.content.hasOwnProperty(key)) { continue; }
if (!keeps[key]) {
delete this.event.content[key];
}
}
},
/**
* Check if this event has been redacted
*
* @return {boolean} True if this event has been redacted
*/
isRedacted: function() {
return Boolean(this.getUnsigned().redacted_because);
},
};
/* http://matrix.org/docs/spec/r0.0.1/client_server.html#redactions says:
*
* the server should strip off any keys not in the following list:
* event_id
* type
* room_id
* user_id
* state_key
* prev_state
* content
* [we keep 'unsigned' as well, since that is created by the local server]
*
* The content object should also be stripped of all keys, unless it is one of
* one of the following event types:
* m.room.member allows key membership
* m.room.create allows key creator
* m.room.join_rules allows key join_rule
* m.room.power_levels allows keys ban, events, events_default, kick,
* redact, state_default, users, users_default.
* m.room.aliases allows key aliases
*/
// a map giving the keys we keep when an event is redacted
var _REDACT_KEEP_KEY_MAP = [
'event_id', 'type', 'room_id', 'user_id', 'state_key', 'prev_state',
'content', 'unsigned',
].reduce(function(ret, val) { ret[val] = 1; return ret; }, {});
// a map from event type to the .content keys we keep when an event is redacted
var _REDACT_KEEP_CONTENT_MAP = {
'm.room.member': {'membership': 1},
'm.room.create': {'creator': 1},
'm.room.join_rules': {'join_rule': 1},
'm.room.power_levels': {'ban': 1, 'events': 1, 'events_default': 1,
'kick': 1, 'redact': 1, 'state_default': 1,
'users': 1, 'users_default': 1,
},
'm.room.aliases': {'aliases': 1},
};
+60 -10
View File
@@ -1,8 +1,24 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* @module models/room-member
*/
var EventEmitter = require("events").EventEmitter;
var ContentRepo = require("../content-repo");
var utils = require("../utils");
@@ -147,6 +163,45 @@ RoomMember.prototype.getLastModifiedTime = function() {
return this._modified;
};
/**
* Get the avatar URL for a room member.
* @param {string} baseUrl The base homeserver URL See
* {@link module:client~MatrixClient#getHomeserverUrl}.
* @param {Number} width The desired width of the thumbnail.
* @param {Number} height The desired height of the thumbnail.
* @param {string} resizeMethod The thumbnail resize method to use, either
* "crop" or "scale".
* @param {Boolean} allowDefault (optional) Passing false causes this method to
* return null if the user has no avatar image. Otherwise, a default image URL
* will be returned. Default: true.
* @param {Boolean} allowDirectLinks (optional) If true, the avatar URL will be
* returned even if it is a direct hyperlink rather than a matrix content URL.
* If false, any non-matrix content URLs will be ignored. Setting this option to
* true will expose URLs that, if fetched, will leak information about the user
* to anyone who they share a room with.
* @return {?string} the avatar URL or null.
*/
RoomMember.prototype.getAvatarUrl =
function(baseUrl, width, height, resizeMethod, allowDefault, allowDirectLinks) {
if (allowDefault === undefined) { allowDefault = true; }
if (!this.events.member && !allowDefault) {
return null;
}
var rawUrl = this.events.member ? this.events.member.getContent().avatar_url : null;
var httpUrl = ContentRepo.getHttpUriForMxc(
baseUrl, rawUrl, width, height, resizeMethod, allowDirectLinks
);
if (httpUrl) {
return httpUrl;
}
else if (allowDefault) {
return ContentRepo.getIdenticonUri(
baseUrl, this.userId, width, height
);
}
return null;
};
function calculateDisplayName(member, event, roomState) {
var displayName = event.getDirectionalContent().displayname;
var selfUserId = member.userId;
@@ -174,18 +229,13 @@ function calculateDisplayName(member, event, roomState) {
return displayName;
}
var stateEvents = utils.filter(
roomState.getStateEvents("m.room.member"),
function(e) {
return e.getContent().displayname === displayName &&
e.getSender() !== selfUserId;
}
);
if (stateEvents.length > 0) {
// need to disambiguate
var userIds = roomState.getUserIdsWithDisplayName(displayName);
var otherUsers = userIds.filter(function(u) {
return u !== selfUserId;
});
if (otherUsers.length > 0) {
return displayName + " (" + selfUserId + ")";
}
return displayName;
}
+162
View File
@@ -1,3 +1,18 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* @module models/room-state
@@ -31,6 +46,9 @@ function RoomState(roomId) {
// userId: RoomMember
};
this._updateModifiedTime();
this._displayNameToUserIds = {};
this._userIdsToDisplayNames = {};
this._tokenToInvite = {}; // 3pid invite state_key to m.room.member invite
}
utils.inherits(RoomState, EventEmitter);
@@ -108,6 +126,12 @@ RoomState.prototype.setStateEvents = function(stateEvents) {
self.events[event.getType()] = {};
}
self.events[event.getType()][event.getStateKey()] = event;
if (event.getType() === "m.room.member") {
_updateDisplayNameCache(
self, event.getStateKey(), event.getContent().displayname
);
_updateThirdPartyTokenCache(self, event);
}
self.emit("RoomState.events", event, self);
});
@@ -121,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);
@@ -149,6 +188,7 @@ RoomState.prototype.setStateEvents = function(stateEvents) {
var members = utils.values(self.members);
utils.forEach(members, function(member) {
member.setPowerLevelEvent(event);
self.emit("RoomState.members", event, self, member);
});
}
});
@@ -164,6 +204,16 @@ RoomState.prototype.setTypingEvent = function(event) {
});
};
/**
* Get the m.room.member event which has the given third party invite token.
*
* @param {string} token The token
* @return {?MatrixEvent} The m.room.member event or null
*/
RoomState.prototype.getInviteForThreePidToken = function(token) {
return this._tokenToInvite[token] || null;
};
/**
* Update the last modified time to the current time.
*/
@@ -180,11 +230,123 @@ RoomState.prototype.getLastModifiedTime = function() {
return this._modified;
};
/**
* Get user IDs with the specified display name.
* @param {string} displayName The display name to get user IDs from.
* @return {string[]} An array of user IDs or an empty array.
*/
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.
*/
module.exports = RoomState;
function _updateThirdPartyTokenCache(roomState, memberEvent) {
if (!memberEvent.getContent().third_party_invite) {
return;
}
var token = (memberEvent.getContent().third_party_invite.signed || {}).token;
if (!token) {
return;
}
var threePidInvite = roomState.getStateEvents(
"m.room.third_party_invite", token
);
if (!threePidInvite) {
return;
}
roomState._tokenToInvite[token] = memberEvent;
}
function _updateDisplayNameCache(roomState, userId, displayName) {
var oldName = roomState._userIdsToDisplayNames[userId];
delete roomState._userIdsToDisplayNames[userId];
if (oldName) {
// Remove the old name from the cache.
// We clobber the user_id > name lookup but the name -> [user_id] lookup
// means we need to remove that user ID from that array rather than nuking
// the lot.
var existingUserIds = roomState._displayNameToUserIds[oldName] || [];
for (var i = 0; i < existingUserIds.length; i++) {
if (existingUserIds[i] === userId) {
// remove this user ID from this array
existingUserIds.splice(i, 1);
i--;
}
}
roomState._displayNameToUserIds[oldName] = existingUserIds;
}
roomState._userIdsToDisplayNames[userId] = displayName;
if (!roomState._displayNameToUserIds[displayName]) {
roomState._displayNameToUserIds[displayName] = [];
}
roomState._displayNameToUserIds[displayName].push(userId);
}
/**
* Fires whenever the event dictionary in room state is updated.
* @event module:client~MatrixClient#"RoomState.events"
+15
View File
@@ -1,3 +1,18 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* @module models/room-summary
+1171 -109
View File
File diff suppressed because it is too large Load Diff
+66
View File
@@ -0,0 +1,66 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* @module models/search-result
*/
var EventContext = require("./event-context");
var utils = require("../utils");
/**
* Construct a new SearchResult
*
* @param {number} rank where this SearchResult ranks in the results
* @param {event-context.EventContext} eventContext the matching event and its
* context
*
* @constructor
*/
function SearchResult(rank, eventContext) {
this.rank = rank;
this.context = eventContext;
}
/**
* Create a SearchResponse from the response to /search
* @static
* @param {Object} jsonObj
* @param {function} eventMapper
* @return {SearchResult}
*/
SearchResult.fromJson = function(jsonObj, eventMapper) {
var jsonContext = jsonObj.context || {};
var events_before = jsonContext.events_before || [];
var events_after = jsonContext.events_after || [];
var context = new EventContext(eventMapper(jsonObj.result));
context.setPaginateToken(jsonContext.start, true);
context.addEvents(utils.map(events_before, eventMapper), true);
context.addEvents(utils.map(events_after, eventMapper), false);
context.setPaginateToken(jsonContext.end, false);
return new SearchResult(jsonObj.rank, context);
};
/**
* The SearchResult class
*/
module.exports = SearchResult;
+45
View File
@@ -1,3 +1,18 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* @module models/user
@@ -16,6 +31,8 @@
* @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.
*/
@@ -25,6 +42,7 @@ function User(userId) {
this.displayName = userId;
this.avatarUrl = null;
this.lastActiveAgo = 0;
this.currentlyActive = false;
this.events = {
presence: null,
profile: null
@@ -64,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();
@@ -74,6 +93,32 @@ User.prototype.setPresenceEvent = function(event) {
}
};
/**
* Manually set this user's display name. No event is emitted in response to this
* as there is no underlying MatrixEvent to emit with.
* @param {string} name The new display name.
*/
User.prototype.setDisplayName = function(name) {
var oldName = this.displayName;
this.displayName = name;
if (name !== oldName) {
this._updateModifiedTime();
}
};
/**
* Manually set this user's avatar URL. No event is emitted in response to this
* as there is no underlying MatrixEvent to emit with.
* @param {string} url The new avatar URL.
*/
User.prototype.setAvatarUrl = function(url) {
var oldUrl = this.avatarUrl;
this.avatarUrl = url;
if (url !== oldUrl) {
this._updateModifiedTime();
}
};
/**
* Update the last modified time to the current time.
*/
+16 -1
View File
@@ -1,3 +1,18 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* @module pushprocessor
*/
@@ -195,7 +210,7 @@ function PushProcessor(client) {
};
var matchingRuleForEventWithRulesets = function(ev, rulesets) {
if (!rulesets) { return null; }
if (!rulesets || !rulesets.device) { return null; }
if (ev.user_id == client.credentials.userId) { return null; }
var allDevNames = Object.keys(rulesets.device);
+21
View File
@@ -1,3 +1,18 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* This is an internal module which manages queuing, scheduling and retrying
@@ -133,6 +148,12 @@ MatrixScheduler.RETRY_BACKOFF_RATELIMIT = function(event, attempts, err) {
// client error; no amount of retrying with save you now.
return -1;
}
// we ship with browser-request which returns { cors: rejected } when trying
// with no connection, so if we match that, give up since they have no conn.
if (err.cors === "rejected") {
return -1;
}
if (err.name === "M_LIMIT_EXCEEDED") {
var waitTime = err.data.retry_after_ms;
if (waitTime) {
+142 -1
View File
@@ -1,15 +1,36 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* This is an internal module. See {@link MatrixInMemoryStore} for the public class.
* @module store/memory
*/
var utils = require("../utils");
var User = require("../models/user");
/**
* Construct a new in-memory data store for the Matrix Client.
* @constructor
* @param {Object=} opts Config options
* @param {LocalStorage} opts.localStorage The local storage instance to persist
* some forms of data such as tokens. Rooms will NOT be stored. See
* {@link WebStorageStore} to persist rooms.
*/
module.exports.MatrixInMemoryStore = function MatrixInMemoryStore() {
module.exports.MatrixInMemoryStore = function MatrixInMemoryStore(opts) {
opts = opts || {};
this.rooms = {
// roomId: Room
};
@@ -17,6 +38,12 @@ module.exports.MatrixInMemoryStore = function MatrixInMemoryStore() {
// userId: User
};
this.syncToken = null;
this.filters = {
// userId: {
// filterId: Filter
// }
};
this.localStorage = opts.localStorage;
};
module.exports.MatrixInMemoryStore.prototype = {
@@ -29,6 +56,7 @@ module.exports.MatrixInMemoryStore.prototype = {
return this.syncToken;
},
/**
* Set the token to stream from.
* @param {string} token The token to stream from.
@@ -43,6 +71,44 @@ module.exports.MatrixInMemoryStore.prototype = {
*/
storeRoom: function(room) {
this.rooms[room.roomId] = room;
// add listeners for room member changes so we can keep the room member
// map up-to-date.
room.currentState.on("RoomState.members", this._onRoomMember.bind(this));
// add existing members
var self = this;
room.currentState.getMembers().forEach(function(m) {
self._onRoomMember(null, room.currentState, m);
});
},
/**
* Called when a room member in a room being tracked by this store has been
* updated.
* @param {MatrixEvent} event
* @param {RoomState} state
* @param {RoomMember} member
*/
_onRoomMember: function(event, state, member) {
if (member.membership === "invite") {
// We do NOT add invited members because people love to typo user IDs
// which would then show up in these lists (!)
return;
}
// We don't clobber any existing entry in the user map which has presence
// so user entries with presence info are preferred. This does mean we will
// clobber room member entries constantly, which is desirable to keep things
// like display names and avatar URLs up-to-date.
if (this.users[member.userId] && this.users[member.userId].events.presence) {
return;
}
var user = new User(member.userId);
user.setDisplayName(member.name);
var rawUrl = (
member.events.member ? member.events.member.getContent().avatar_url : null
);
user.setAvatarUrl(rawUrl);
this.users[user.userId] = user;
},
/**
@@ -62,6 +128,17 @@ module.exports.MatrixInMemoryStore.prototype = {
return utils.values(this.rooms);
},
/**
* Permanently delete a room.
* @param {string} roomId
*/
removeRoom: function(roomId) {
if (this.rooms[roomId]) {
this.rooms[roomId].removeListener("RoomState.members", this._onRoomMember);
}
delete this.rooms[roomId];
},
/**
* Retrieve a summary of all the rooms.
* @return {RoomSummary[]} A summary of each room.
@@ -89,6 +166,14 @@ module.exports.MatrixInMemoryStore.prototype = {
return this.users[userId] || null;
},
/**
* Retrieve all known users.
* @return {User[]} A list of users, which may be empty.
*/
getUsers: function() {
return utils.values(this.users);
},
/**
* Retrieve scrollback for this room.
* @param {Room} room The matrix room
@@ -109,6 +194,62 @@ module.exports.MatrixInMemoryStore.prototype = {
*/
storeEvents: function(room, events, token, toStart) {
// no-op because they've already been added to the room instance.
},
/**
* Store a filter.
* @param {Filter} filter
*/
storeFilter: function(filter) {
if (!filter) { return; }
if (!this.filters[filter.userId]) {
this.filters[filter.userId] = {};
}
this.filters[filter.userId][filter.filterId] = filter;
},
/**
* Retrieve a filter.
* @param {string} userId
* @param {string} filterId
* @return {?Filter} A filter or null.
*/
getFilter: function(userId, filterId) {
if (!this.filters[userId] || !this.filters[userId][filterId]) {
return null;
}
return this.filters[userId][filterId];
},
/**
* Retrieve a filter ID with the given name.
* @param {string} filterName The filter name.
* @return {?string} The filter ID or null.
*/
getFilterIdByName: function(filterName) {
if (!this.localStorage) {
return null;
}
try {
return this.localStorage.getItem("mxjssdk_memory_filter_" + filterName);
}
catch (e) {}
return null;
},
/**
* Set a filter name to ID mapping.
* @param {string} filterName
* @param {string} filterId
*/
setFilterIdByName: function(filterName, filterId) {
if (!this.localStorage) {
return;
}
try {
this.localStorage.setItem("mxjssdk_memory_filter_" + filterName, filterId);
}
catch (e) {}
}
// TODO
+15
View File
@@ -1,3 +1,18 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
+66
View File
@@ -1,3 +1,18 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* This is an internal module.
@@ -54,6 +69,14 @@ StubStore.prototype = {
return [];
},
/**
* Permanently delete a room.
* @param {string} roomId
*/
removeRoom: function(roomId) {
return;
},
/**
* No-op.
* @return {Array} An empty array.
@@ -78,6 +101,14 @@ StubStore.prototype = {
return null;
},
/**
* No-op.
* @return {User[]}
*/
getUsers: function() {
return [];
},
/**
* No-op.
* @param {Room} room
@@ -96,6 +127,41 @@ StubStore.prototype = {
* @param {boolean} toStart True if these are paginated results.
*/
storeEvents: function(room, events, token, toStart) {
},
/**
* Store a filter.
* @param {Filter} filter
*/
storeFilter: function(filter) {
},
/**
* Retrieve a filter.
* @param {string} userId
* @param {string} filterId
* @return {?Filter} A filter or null.
*/
getFilter: function(userId, filterId) {
return null;
},
/**
* Retrieve a filter ID with the given name.
* @param {string} filterName The filter name.
* @return {?string} The filter ID or null.
*/
getFilterIdByName: function(filterName) {
return null;
},
/**
* Set a filter name to ID mapping.
* @param {string} filterName
* @param {string} filterId
*/
setFilterIdByName: function(filterName, filterId) {
}
// TODO
+36 -1
View File
@@ -1,3 +1,18 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* This is an internal module. Implementation details:
@@ -458,6 +473,24 @@ WebStorageStore.prototype._syncTimeline = function(roomId, timelineIndices) {
setItem(this.store, keyName(roomId, "timeline", "live"), []);
};
/**
* Store a filter.
* @param {Filter} filter
*/
WebStorageStore.prototype.storeFilter = function(filter) {
};
/**
* Retrieve a filter.
* @param {string} userId
* @param {string} filterId
* @return {?Filter} A filter or null.
*/
WebStorageStore.prototype.getFilter = function(userId, filterId) {
return null;
};
function SerialisedRoom(roomId) {
this.state = {
events: {}
@@ -514,7 +547,9 @@ SerialisedRoom.fromRoom = function(room, batchSize) {
};
function loadRoom(store, roomId, numEvents, tokenArray) {
var room = new Room(roomId, tokenArray.length);
var room = new Room(roomId, {
storageToken: tokenArray.length
});
// populate state (flatten nested struct to event array)
var currentStateMap = getItem(store, keyName(roomId, "state"));
+1035
View File
File diff suppressed because it is too large Load Diff
+462
View File
@@ -0,0 +1,462 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/** @module timeline-window */
var q = require("q");
var EventTimeline = require("./models/event-timeline");
/**
* @private
*/
var DEBUG = false;
/**
* @private
*/
var debuglog = DEBUG ? console.log.bind(console) : function() {};
/**
* Construct a TimelineWindow.
*
* <p>This abstracts the separate timelines in a Matrix {@link
* module:models/room~Room|Room} into a single iterable thing. It keeps track of
* the start and endpoints of the window, which can be advanced with the help
* of pagination requests.
*
* <p>Before the window is useful, it must be initialised by calling {@link
* module:timeline-window~TimelineWindow#load|load}.
*
* <p>Note that the window will not automatically extend itself when new events
* are received from /sync; you should arrange to call {@link
* module:timeline-window~TimelineWindow#paginate|paginate} on {@link
* module:client~MatrixClient.event:"Room.timeline"|Room.timeline} events.
*
* @param {MatrixClient} client MatrixClient to be used for context/pagination
* requests.
*
* @param {Room} room The room to track
*
* @param {Object} [opts] Configuration options for this window
*
* @param {number} [opts.windowLimit = 1000] maximum number of events to keep
* in the window. If more events are retrieved via pagination requests,
* excess events will be dropped from the other end of the window.
*
* @constructor
*/
function TimelineWindow(client, room, opts) {
opts = opts || {};
this._client = client;
this._room = room;
// these will be TimelineIndex objects; they delineate the 'start' and
// 'end' of the window.
//
// _start.index is inclusive; _end.index is exclusive.
this._start = null;
this._end = null;
this._eventCount = 0;
this._windowLimit = opts.windowLimit || 1000;
}
/**
* Initialise the window to point at a given event, or the live timeline
*
* @param {string} [initialEventId] If given, the window will contain the
* given event
* @param {number} [initialWindowSize = 20] Size of the initial window
*
* @return {module:client.Promise}
*/
TimelineWindow.prototype.load = function(initialEventId, initialWindowSize) {
var self = this;
initialWindowSize = initialWindowSize || 20;
// 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) {
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) {
initFields(tl, i);
return;
}
}
throw new Error("getEventTimeline result didn't include requested event");
});
} else {
// start with the most recent events
var tl = this._room.getLiveTimeline();
initFields(tl, tl.getEvents().length);
return q();
}
};
/**
* Check if this window can be extended
*
* <p>This returns true if we either have more events, or if we have a
* pagination token which means we can paginate in that direction. It does not
* necessarily mean that there are more events available in that direction at
* this time.
*
* @param {string} direction EventTimeline.BACKWARDS to check if we can
* paginate backwards; EventTimeline.FORWARDS to check if we can go forwards
*
* @return {boolean} true if we can paginate in the given direction
*/
TimelineWindow.prototype.canPaginate = function(direction) {
var tl;
if (direction == EventTimeline.BACKWARDS) {
tl = this._start;
} else if (direction == EventTimeline.FORWARDS) {
tl = this._end;
} else {
throw new Error("Invalid direction '" + direction + "'");
}
if (!tl) {
debuglog("TimelineWindow: no timeline yet");
return false;
}
if (direction == EventTimeline.BACKWARDS) {
if (tl.index > tl.minIndex()) { return true; }
} else {
if (tl.index < tl.maxIndex()) { return true; }
}
return Boolean(tl.timeline.getNeighbouringTimeline(direction) ||
tl.timeline.getPaginationToken(direction));
};
/**
* Attempt to extend the window
*
* @param {string} direction EventTimeline.BACKWARDS to extend the window
* backwards (towards older events); EventTimeline.FORWARDS to go forwards.
*
* @param {number} size number of events to try to extend by. If fewer than this
* number are immediately available, then we return immediately rather than
* making an API call.
*
* @param {boolean} [makeRequest = true] whether we should make API calls to
* fetch further events if we don't have any at all. (This has no effect if
* the room already knows about additional events in the relevant direction,
* even if there are fewer than 'size' of them, as we will just return those
* we already know about.)
*
* @return {module:client.Promise} Resolves to a boolean which is true if more events
* were successfully retrieved.
*/
TimelineWindow.prototype.paginate = function(direction, size, makeRequest) {
// Either wind back the message cap (if there are enough events in the
// timeline to do so), or fire off a pagination request.
if (makeRequest === undefined) {
makeRequest = true;
}
var tl;
if (direction == EventTimeline.BACKWARDS) {
tl = this._start;
} else if (direction == EventTimeline.FORWARDS) {
tl = this._end;
} else {
throw new Error("Invalid direction '" + direction + "'");
}
if (!tl) {
debuglog("TimelineWindow: no timeline yet");
return q(false);
}
if (tl.pendingPaginate) {
return tl.pendingPaginate;
}
// try moving the cap
var count = (direction == EventTimeline.BACKWARDS) ?
tl.retreat(size) : tl.advance(size);
if (count) {
this._eventCount += count;
debuglog("TimelineWindow: increased cap by " + count +
" (now " + this._eventCount + ")");
// remove some events from the other end, if necessary
var excess = this._eventCount - this._windowLimit;
if (excess > 0) {
this._unpaginate(excess, direction != EventTimeline.BACKWARDS);
}
return q(true);
}
if (!makeRequest) {
return q(false);
}
// try making a pagination request
var token = tl.timeline.getPaginationToken(direction);
if (!token) {
debuglog("TimelineWindow: no token");
return q(false);
}
debuglog("TimelineWindow: starting request");
var self = this;
var prom = this._client.paginateEventTimeline(tl.timeline, {
backwards: direction == EventTimeline.BACKWARDS,
limit: size
}).finally(function() {
tl.pendingPaginate = null;
}).then(function(r) {
debuglog("TimelineWindow: request completed with result " + r);
if (!r) {
// end of timeline
return 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;
};
/**
* Trim the window to the windowlimit
*
* @param {number} delta number of events to remove from the timeline
* @param {boolean} startOfTimeline if events should be removed from the start
* of the timeline.
*
* @private
*/
TimelineWindow.prototype._unpaginate = function(delta, startOfTimeline) {
var tl = startOfTimeline ? this._start : this._end;
// sanity-check the delta
if (delta > this._eventCount || delta < 0) {
throw new Error("Attemting to unpaginate " + delta + " events, but " +
"only have " + this._eventCount + " in the timeline");
}
while (delta > 0) {
var count = startOfTimeline ? tl.advance(delta) : tl.retreat(delta);
if (count <= 0) {
// sadness. This shouldn't be possible.
throw new Error(
"Unable to unpaginate any further, but still have " +
this._eventCount + " events");
}
delta -= count;
this._eventCount -= count;
debuglog("TimelineWindow.unpaginate: dropped " + count +
" (now " + this._eventCount + ")");
}
};
/**
* Get a list of the events currently in the window
*
* @return {MatrixEvent[]} the events in the window
*/
TimelineWindow.prototype.getEvents = function() {
if (!this._start) {
// not yet loaded
return [];
}
var result = [];
// iterate through each timeline between this._start and this._end
// (inclusive).
var timeline = this._start.timeline;
while (true) {
var events = timeline.getEvents();
// For the first timeline in the chain, we want to start at
// this._start.index. For the last timeline in the chain, we want to
// stop before this._end.index. Otherwise, we want to copy all of the
// events in the timeline.
//
// (Note that both this._start.index and this._end.index are relative
// to their respective timelines' BaseIndex).
//
var startIndex = 0, endIndex = events.length;
if (timeline === this._start.timeline) {
startIndex = this._start.index + timeline.getBaseIndex();
}
if (timeline === this._end.timeline) {
endIndex = this._end.index + timeline.getBaseIndex();
}
for (var i = startIndex; i < endIndex; i++) {
result.push(events[i]);
}
// if we're not done, iterate to the next timeline.
if (timeline === this._end.timeline) {
break;
} else {
timeline = timeline.getNeighbouringTimeline(EventTimeline.FORWARDS);
}
}
return result;
};
/**
* a thing which contains a timeline reference, and an index into it.
*
* @constructor
* @param {EventTimeline} timeline
* @param {number} index
* @private
*/
function TimelineIndex(timeline, index) {
this.timeline = timeline;
// the indexes are relative to BaseIndex, so could well be negative.
this.index = index;
}
/**
* @return {number} the minimum possible value for the index in the current
* timeline
*/
TimelineIndex.prototype.minIndex = function() {
return this.timeline.getBaseIndex() * -1;
};
/**
* @return {number} the maximum possible value for the index in the current
* timeline (exclusive - ie, it actually returns one more than the index
* of the last element).
*/
TimelineIndex.prototype.maxIndex = function() {
return this.timeline.getEvents().length - this.timeline.getBaseIndex();
};
/**
* Try move the index forward, or into the neighbouring timeline
*
* @param {number} delta number of events to advance by
* @return {number} number of events successfully advanced by
*/
TimelineIndex.prototype.advance = function(delta) {
if (!delta) {
return 0;
}
// first try moving the index in the current timeline. See if there is room
// to do so.
var cappedDelta;
if (delta < 0) {
// we want to wind the index backwards.
//
// (this.minIndex() - this.index) is a negative number whose magnitude
// is the amount of room we have to wind back the index in the current
// timeline. We cap delta to this quantity.
cappedDelta = Math.max(delta, this.minIndex() - this.index);
if (cappedDelta < 0) {
this.index += cappedDelta;
return cappedDelta;
}
} else {
// we want to wind the index forwards.
//
// (this.maxIndex() - this.index) is a (positive) number whose magnitude
// is the amount of room we have to wind forward the index in the current
// timeline. We cap delta to this quantity.
cappedDelta = Math.min(delta, this.maxIndex() - this.index);
if (cappedDelta > 0) {
this.index += cappedDelta;
return cappedDelta;
}
}
// the index is already at the start/end of the current timeline.
//
// next see if there is a neighbouring timeline to switch to.
var neighbour = this.timeline.getNeighbouringTimeline(
delta < 0 ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS);
if (neighbour) {
this.timeline = neighbour;
if (delta < 0) {
this.index = this.maxIndex();
} else {
this.index = this.minIndex();
}
debuglog("paginate: switched to new neighbour");
// recurse, using the next timeline
return this.advance(delta);
}
return 0;
};
/**
* Try move the index backwards, or into the neighbouring timeline
*
* @param {number} delta number of events to retreat by
* @return {number} number of events successfully retreated by
*/
TimelineIndex.prototype.retreat = function(delta) {
return this.advance(delta * -1) * -1;
};
/**
* The TimelineWindow class.
*/
module.exports.TimelineWindow = TimelineWindow;
/**
* The TimelineIndex class. exported here for unit testing.
*/
module.exports.TimelineIndex = TimelineIndex;
+226 -2
View File
@@ -1,3 +1,18 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* This is an internal module.
@@ -152,19 +167,22 @@ module.exports.findElement = function(array, fn, reverse) {
*/
module.exports.removeElement = function(array, fn, reverse) {
var i;
var removed;
if (reverse) {
for (i = array.length - 1; i >= 0; i--) {
if (fn(array[i], i, array)) {
removed = array[i];
array.splice(i, 1);
return true;
return removed;
}
}
}
else {
for (i = 0; i < array.length; i++) {
if (fn(array[i], i, array)) {
removed = array[i];
array.splice(i, 1);
return true;
return removed;
}
}
}
@@ -228,6 +246,212 @@ module.exports.deepCopy = function(obj) {
return JSON.parse(JSON.stringify(obj));
};
/**
* Run polyfills to add Array.map and Array.filter if they are missing.
*/
module.exports.runPolyfills = function() {
// Array.prototype.filter
// ========================================================
// SOURCE:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter
if (!Array.prototype.filter) {
Array.prototype.filter = function(fun/*, thisArg*/) {
if (this === void 0 || this === null) {
throw new TypeError();
}
var t = Object(this);
var len = t.length >>> 0;
if (typeof fun !== 'function') {
throw new TypeError();
}
var res = [];
var thisArg = arguments.length >= 2 ? arguments[1] : void 0;
for (var i = 0; i < len; i++) {
if (i in t) {
var val = t[i];
// NOTE: Technically this should Object.defineProperty at
// the next index, as push can be affected by
// properties on Object.prototype and Array.prototype.
// But that method's new, and collisions should be
// rare, so use the more-compatible alternative.
if (fun.call(thisArg, val, i, t)) {
res.push(val);
}
}
}
return res;
};
}
// Array.prototype.map
// ========================================================
// SOURCE:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map
// Production steps of ECMA-262, Edition 5, 15.4.4.19
// Reference: http://es5.github.io/#x15.4.4.19
if (!Array.prototype.map) {
Array.prototype.map = function(callback, thisArg) {
var T, A, k;
if (this === null || this === undefined) {
throw new TypeError(' this is null or not defined');
}
// 1. Let O be the result of calling ToObject passing the |this|
// value as the argument.
var O = Object(this);
// 2. Let lenValue be the result of calling the Get internal
// method of O with the argument "length".
// 3. Let len be ToUint32(lenValue).
var len = O.length >>> 0;
// 4. If IsCallable(callback) is false, throw a TypeError exception.
// See: http://es5.github.com/#x9.11
if (typeof callback !== 'function') {
throw new TypeError(callback + ' is not a function');
}
// 5. If thisArg was supplied, let T be thisArg; else let T be undefined.
if (arguments.length > 1) {
T = thisArg;
}
// 6. Let A be a new array created as if by the expression new Array(len)
// where Array is the standard built-in constructor with that name and
// len is the value of len.
A = new Array(len);
// 7. Let k be 0
k = 0;
// 8. Repeat, while k < len
while (k < len) {
var kValue, mappedValue;
// a. Let Pk be ToString(k).
// This is implicit for LHS operands of the in operator
// b. Let kPresent be the result of calling the HasProperty internal
// method of O with argument Pk.
// This step can be combined with c
// c. If kPresent is true, then
if (k in O) {
// i. Let kValue be the result of calling the Get internal
// method of O with argument Pk.
kValue = O[k];
// ii. Let mappedValue be the result of calling the Call internal
// method of callback with T as the this value and argument
// list containing kValue, k, and O.
mappedValue = callback.call(T, kValue, k, O);
// iii. Call the DefineOwnProperty internal method of A with arguments
// Pk, Property Descriptor
// { Value: mappedValue,
// Writable: true,
// Enumerable: true,
// Configurable: true },
// and false.
// In browsers that support Object.defineProperty, use the following:
// Object.defineProperty(A, k, {
// value: mappedValue,
// writable: true,
// enumerable: true,
// configurable: true
// });
// For best browser support, use the following:
A[k] = mappedValue;
}
// d. Increase k by 1.
k++;
}
// 9. return A
return A;
};
}
// Array.prototype.forEach
// ========================================================
// SOURCE:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach
// Production steps of ECMA-262, Edition 5, 15.4.4.18
// Reference: http://es5.github.io/#x15.4.4.18
if (!Array.prototype.forEach) {
Array.prototype.forEach = function(callback, thisArg) {
var T, k;
if (this === null || this === undefined) {
throw new TypeError(' this is null or not defined');
}
// 1. Let O be the result of calling ToObject passing the |this| value as the
// argument.
var O = Object(this);
// 2. Let lenValue be the result of calling the Get internal method of O with the
// argument "length".
// 3. Let len be ToUint32(lenValue).
var len = O.length >>> 0;
// 4. If IsCallable(callback) is false, throw a TypeError exception.
// See: http://es5.github.com/#x9.11
if (typeof callback !== "function") {
throw new TypeError(callback + ' is not a function');
}
// 5. If thisArg was supplied, let T be thisArg; else let T be undefined.
if (arguments.length > 1) {
T = thisArg;
}
// 6. Let k be 0
k = 0;
// 7. Repeat, while k < len
while (k < len) {
var kValue;
// a. Let Pk be ToString(k).
// This is implicit for LHS operands of the in operator
// b. Let kPresent be the result of calling the HasProperty internal
// method of O with
// argument Pk.
// This step can be combined with c
// c. If kPresent is true, then
if (k in O) {
// i. Let kValue be the result of calling the Get internal method of O with
// argument Pk
kValue = O[k];
// ii. Call the Call internal method of callback with T as the this value and
// argument list containing kValue, k, and O.
callback.call(T, kValue, k, O);
}
// d. Increase k by 1.
k++;
}
// 8. return undefined
};
}
};
/**
* Inherit the prototype methods from one constructor into another. This is a
* port of the Node.js implementation with an Object.create polyfill.
+258 -22
View File
@@ -1,3 +1,18 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* This is an internal module. See {@link createNewMatrixCall} for the public API.
@@ -44,6 +59,8 @@ function MatrixCall(opts) {
// possible
this.candidateSendQueue = [];
this.candidateSendTries = 0;
this.screenSharingStream = null;
}
/** The length of time a call can be ringing for. */
MatrixCall.CALL_TIMEOUT_MS = 60000;
@@ -64,6 +81,7 @@ utils.inherits(MatrixCall, EventEmitter);
* @throws If you have not specified a listener for 'error' events.
*/
MatrixCall.prototype.placeVoiceCall = function() {
debuglog("placeVoiceCall");
checkForErrorListener(this);
_placeCallWithConstraints(this, _getUserMediaVideoContraints('voice'));
this.type = 'voice';
@@ -78,6 +96,7 @@ MatrixCall.prototype.placeVoiceCall = function() {
* @throws If you have not specified a listener for 'error' events.
*/
MatrixCall.prototype.placeVideoCall = function(remoteVideoElement, localVideoElement) {
debuglog("placeVideoCall");
checkForErrorListener(this);
this.localVideoElement = localVideoElement;
this.remoteVideoElement = remoteVideoElement;
@@ -86,6 +105,45 @@ MatrixCall.prototype.placeVideoCall = function(remoteVideoElement, localVideoEle
_tryPlayRemoteStream(this);
};
/**
* Place a screen-sharing call to this room. This includes audio.
* <b>This method is EXPERIMENTAL and subject to change without warning. It
* only works in Google Chrome.</b>
* @param {Element} remoteVideoElement a <code>&lt;video&gt;</code> DOM element
* to render video to.
* @param {Element} localVideoElement a <code>&lt;video&gt;</code> DOM element
* to render the local camera preview.
* @throws If you have not specified a listener for 'error' events.
*/
MatrixCall.prototype.placeScreenSharingCall =
function(remoteVideoElement, localVideoElement)
{
debuglog("placeScreenSharingCall");
checkForErrorListener(this);
var screenConstraints = _getChromeScreenSharingConstraints(this);
if (!screenConstraints) {
return;
}
this.localVideoElement = localVideoElement;
this.remoteVideoElement = remoteVideoElement;
var self = this;
this.webRtc.getUserMedia(screenConstraints, function(stream) {
self.screenSharingStream = stream;
debuglog("Got screen stream, requesting audio stream...");
var audioConstraints = _getUserMediaVideoContraints('voice');
_placeCallWithConstraints(self, audioConstraints);
}, function(err) {
self.emit("error",
callError(
MatrixCall.ERR_NO_USER_MEDIA,
"Failed to get screen-sharing stream: " + err
)
);
});
this.type = 'video';
_tryPlayRemoteStream(this);
};
/**
* Retrieve the local <code>&lt;video&gt;</code> DOM element.
* @return {Element} The dom element
@@ -95,13 +153,23 @@ MatrixCall.prototype.getLocalVideoElement = function() {
};
/**
* Retrieve the remote <code>&lt;video&gt;</code> DOM element.
* Retrieve the remote <code>&lt;video&gt;</code> DOM element
* used for playing back video capable streams.
* @return {Element} The dom element
*/
MatrixCall.prototype.getRemoteVideoElement = function() {
return this.remoteVideoElement;
};
/**
* Retrieve the remote <code>&lt;audio&gt;</code> DOM element
* used for playing back audio only streams.
* @return {Element} The dom element
*/
MatrixCall.prototype.getRemoteAudioElement = function() {
return this.remoteAudioElement;
};
/**
* Set the local <code>&lt;video&gt;</code> DOM element. If this call is active,
* video will be rendered to it immediately.
@@ -126,7 +194,7 @@ MatrixCall.prototype.setLocalVideoElement = function(element) {
/**
* Set the remote <code>&lt;video&gt;</code> DOM element. If this call is active,
* video will be rendered to it immediately.
* the first received video-capable stream will be rendered to it immediately.
* @param {Element} element The <code>&lt;video&gt;</code> DOM element.
*/
MatrixCall.prototype.setRemoteVideoElement = function(element) {
@@ -134,6 +202,16 @@ MatrixCall.prototype.setRemoteVideoElement = function(element) {
_tryPlayRemoteStream(this);
};
/**
* Set the remote <code>&lt;audio&gt;</code> DOM element. If this call is active,
* the first received audio-only stream will be rendered to it immediately.
* @param {Element} element The <code>&lt;video&gt;</code> DOM element.
*/
MatrixCall.prototype.setRemoteAudioElement = function(element) {
this.remoteAudioElement = element;
_tryPlayRemoteAudioStream(this);
};
/**
* Configure this call from an invite event. Used by MatrixClient.
* @protected
@@ -170,6 +248,7 @@ MatrixCall.prototype._initWithInvite = function(event) {
if (event.getAge()) {
setTimeout(function() {
if (self.state == 'ringing') {
debuglog("Call invite has expired. Hanging up.");
self.hangupParty = 'remote'; // effectively
setState(self, 'ended');
stopAllMedia(self);
@@ -238,6 +317,7 @@ MatrixCall.prototype._replacedBy = function(newCall) {
}
newCall.localVideoElement = this.localVideoElement;
newCall.remoteVideoElement = this.remoteVideoElement;
newCall.remoteAudioElement = this.remoteAudioElement;
this.successor = newCall;
this.emit("replaced", newCall);
this.hangup(true);
@@ -259,6 +339,60 @@ MatrixCall.prototype.hangup = function(reason, suppressEvent) {
sendEvent(this, 'm.call.hangup', content);
};
/**
* Set whether the local video preview should be muted or not.
* @param {boolean} muted True to mute the local video.
*/
MatrixCall.prototype.setLocalVideoMuted = function(muted) {
if (!this.localAVStream) {
return;
}
setTracksEnabled(this.localAVStream.getVideoTracks(), !muted);
};
/**
* Check if local video is muted.
*
* If there are multiple video tracks, <i>all</i> of the tracks need to be muted
* for this to return true. This means if there are no video tracks, this will
* return true.
* @return {Boolean} True if the local preview video is muted, else false
* (including if the call is not set up yet).
*/
MatrixCall.prototype.isLocalVideoMuted = function() {
if (!this.localAVStream) {
return false;
}
return !isTracksEnabled(this.localAVStream.getVideoTracks());
};
/**
* Set whether the microphone should be muted or not.
* @param {boolean} muted True to mute the mic.
*/
MatrixCall.prototype.setMicrophoneMuted = function(muted) {
if (!this.localAVStream) {
return;
}
setTracksEnabled(this.localAVStream.getAudioTracks(), !muted);
};
/**
* Check if the microphone is muted.
*
* If there are multiple audio tracks, <i>all</i> of the tracks need to be muted
* for this to return true. This means if there are no audio tracks, this will
* return true.
* @return {Boolean} True if the mic is muted, else false (including if the call
* is not set up yet).
*/
MatrixCall.prototype.isMicrophoneMuted = function() {
if (!this.localAVStream) {
return false;
}
return !isTracksEnabled(this.localAVStream.getAudioTracks());
};
/**
* Internal
* @private
@@ -272,12 +406,19 @@ MatrixCall.prototype._gotUserMediaForInvite = function(stream) {
if (this.state == 'ended') {
return;
}
debuglog("_gotUserMediaForInvite -> " + this.type);
var self = this;
var videoEl = this.getLocalVideoElement();
if (videoEl && this.type == 'video') {
videoEl.autoplay = true;
videoEl.src = this.URL.createObjectURL(stream);
if (this.screenSharingStream) {
debuglog("Setting screen sharing stream to the local video element");
videoEl.src = this.URL.createObjectURL(this.screenSharingStream);
}
else {
videoEl.src = this.URL.createObjectURL(stream);
}
videoEl.muted = true;
setTimeout(function() {
var vel = self.getLocalVideoElement();
@@ -288,12 +429,16 @@ MatrixCall.prototype._gotUserMediaForInvite = function(stream) {
}
this.localAVStream = stream;
var audioTracks = stream.getAudioTracks();
for (var i = 0; i < audioTracks.length; i++) {
audioTracks[i].enabled = true;
}
// why do we enable audio (and only audio) tracks here? -- matthew
setTracksEnabled(stream.getAudioTracks(), true);
this.peerConn = _createPeerConnection(this);
this.peerConn.addStream(stream);
if (this.screenSharingStream) {
console.log("Adding screen-sharing stream to peer connection");
this.peerConn.addStream(this.screenSharingStream);
// let's use this for the local preview...
this.localAVStream = this.screenSharingStream;
}
this.peerConn.createOffer(
hookCallback(self, self._gotLocalOffer),
hookCallback(self, self._getLocalOfferFailed)
@@ -326,10 +471,7 @@ MatrixCall.prototype._gotUserMediaForAnswer = function(stream) {
}
self.localAVStream = stream;
var audioTracks = stream.getAudioTracks();
for (var i = 0; i < audioTracks.length; i++) {
audioTracks[i].enabled = true;
}
setTracksEnabled(stream.getAudioTracks(), true);
self.peerConn.addStream(stream);
var constraints = {
@@ -481,8 +623,9 @@ MatrixCall.prototype._getLocalOfferFailed = function(error) {
/**
* Internal
* @private
* @param {Object} error
*/
MatrixCall.prototype._getUserMediaFailed = function() {
MatrixCall.prototype._getUserMediaFailed = function(error) {
this.emit(
"error",
callError(
@@ -550,22 +693,21 @@ MatrixCall.prototype._onSetRemoteDescriptionError = function(e) {
* @param {Object} event
*/
MatrixCall.prototype._onAddStream = function(event) {
debuglog("Stream added" + event);
debuglog("Stream id " + event.stream.id + " added");
var s = event.stream;
this.remoteAVStream = s;
if (this.direction == 'inbound') {
if (s.getVideoTracks().length > 0) {
this.type = 'video';
} else {
this.type = 'voice';
}
if (s.getVideoTracks().length > 0) {
this.type = 'video';
this.remoteAVStream = s;
} else {
this.type = 'voice';
this.remoteAStream = s;
}
var self = this;
forAllTracksOnStream(s, function(t) {
debuglog("Track id " + t.id + " added");
// not currently implemented in chrome
t.onstarted = hookCallback(self, self._onRemoteStreamTrackStarted);
});
@@ -574,7 +716,12 @@ MatrixCall.prototype._onAddStream = function(event) {
// not currently implemented in chrome
event.stream.onstarted = hookCallback(self, self._onRemoteStreamStarted);
_tryPlayRemoteStream(this);
if (this.type === 'video') {
_tryPlayRemoteStream(this);
}
else {
_tryPlayRemoteAudioStream(this);
}
};
/**
@@ -631,6 +778,21 @@ MatrixCall.prototype._onAnsweredElsewhere = function(msg) {
terminate(this, "remote", "answered_elsewhere", true);
};
var setTracksEnabled = function(tracks, enabled) {
for (var i = 0; i < tracks.length; i++) {
tracks[i].enabled = enabled;
}
};
var isTracksEnabled = function(tracks) {
for (var i = 0; i < tracks.length; i++) {
if (tracks[i].enabled) {
return true; // at least one track is enabled
}
}
return false;
};
var setState = function(self, state) {
var oldState = self.state;
self.state = state;
@@ -666,6 +828,12 @@ var terminate = function(self, hangupParty, hangupReason, shouldEmit) {
}
self.getRemoteVideoElement().src = "";
}
if (self.getRemoteAudioElement()) {
if (self.getRemoteAudioElement().pause) {
self.getRemoteAudioElement().pause();
}
self.getRemoteAudioElement().src = "";
}
if (self.getLocalVideoElement()) {
if (self.getLocalVideoElement().pause) {
self.getLocalVideoElement().pause();
@@ -685,6 +853,7 @@ var terminate = function(self, hangupParty, hangupReason, shouldEmit) {
};
var stopAllMedia = function(self) {
debuglog("stopAllMedia (stream=%s)", self.localAVStream);
if (self.localAVStream) {
forAllTracksOnStream(self.localAVStream, function(t) {
if (t.stop) {
@@ -697,6 +866,16 @@ var stopAllMedia = function(self) {
self.localAVStream.stop();
}
}
if (self.screenSharingStream) {
forAllTracksOnStream(self.screenSharingStream, function(t) {
if (t.stop) {
t.stop();
}
});
if (self.screenSharingStream.stop) {
self.screenSharingStream.stop();
}
}
if (self.remoteAVStream) {
forAllTracksOnStream(self.remoteAVStream, function(t) {
if (t.stop) {
@@ -704,6 +883,13 @@ var stopAllMedia = function(self) {
}
});
}
if (self.remoteAStream) {
forAllTracksOnStream(self.remoteAStream, function(t) {
if (t.stop) {
t.stop();
}
});
}
};
var _tryPlayRemoteStream = function(self) {
@@ -724,6 +910,24 @@ var _tryPlayRemoteStream = function(self) {
}
};
var _tryPlayRemoteAudioStream = function(self) {
if (self.getRemoteAudioElement() && self.remoteAStream) {
var player = self.getRemoteAudioElement();
player.autoplay = true;
player.src = self.URL.createObjectURL(self.remoteAStream);
setTimeout(function() {
var ael = self.getRemoteAudioElement();
if (ael.play) {
ael.play();
}
// OpenWebRTC does not support oniceconnectionstatechange yet
if (self.webRtc.isOpenWebRTC()) {
setState(self, 'connected');
}
}, 0);
}
};
var checkForErrorListener = function(self) {
if (self.listeners("error").length === 0) {
throw new Error(
@@ -822,6 +1026,38 @@ var _createPeerConnection = function(self) {
return pc;
};
var _getChromeScreenSharingConstraints = function(call) {
var screen = global.screen;
if (!screen) {
call.emit("error", callError(
MatrixCall.ERR_NO_USER_MEDIA,
"Couldn't determine screen sharing constaints."
));
return;
}
// it won't work at all if you're not on HTTPS so whine whine whine
if (!global.window || global.window.location.protocol !== "https:") {
call.emit("error", callError(
MatrixCall.ERR_NO_USER_MEDIA,
"You need to be using HTTPS to place a screen-sharing call."
));
return;
}
return {
video: {
mandatory: {
chromeMediaSource: "screen",
chromeMediaSourceId: "" + Date.now(),
maxWidth: screen.width,
maxHeight: screen.height,
minFrameRate: 1,
maxFrameRate: 10
}
}
};
};
var _getUserMediaVideoContraints = function(callType) {
switch (callType) {
case 'voice':
+8 -4
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "0.2.2",
"version": "0.4.2",
"description": "Matrix Client-Server SDK for Javascript",
"main": "index.js",
"scripts": {
@@ -9,8 +9,10 @@
"gendoc": "jsdoc -r lib -P package.json -R README.md -d .jsdoc",
"build": "jshint -c .jshint lib/ && browserify browser-index.js -o dist/browser-matrix-dev.js --ignore-missing",
"watch": "watchify browser-index.js -o dist/browser-matrix-dev.js -v",
"lint": "jshint -c .jshint lib spec && gjslint --unix_mode --disable 0131,0211,0200,0222 --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"
"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 -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
+102
View File
@@ -0,0 +1,102 @@
#!/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 [-x] vX.Y.Z"
help() {
cat <<EOF
$USAGE
-x: skip updating the changelog
EOF
}
skip_changelog=
while getopts hx f; do
case $f in
h)
help
exit 0
;;
x)
skip_changelog=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"
cd `dirname $0`
# 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 "$release"
read -p "Edit CHANGELOG.md manually, or press enter to continue " REPLY
if [ -n "$(git ls-files --modified CHANGELOG.md)" ]; then
echo "Committing updated changelog"
git commit "CHANGELOG.md" -m "Prepare changelog for $tag"
fi
fi
# Bump package.json, build the dist, and tag
echo "npm version"
npm version "$release"
# generate the docs
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"
# 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" "gh-pages"
# publish to npmjs
npm publish
+19 -14
View File
@@ -67,6 +67,7 @@ describe("MatrixClient crypto", function() {
});
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
});
describe("Ali account setup", function() {
@@ -200,22 +201,26 @@ describe("MatrixClient crypto", function() {
});
function bobRecvMessage(done) {
var initialSync = {
end: "alpha",
presence: [],
rooms: []
var syncData = {
next_batch: "x",
rooms: {
join: {
}
}
};
var events = {
start: "alpha",
end: "beta",
chunk: [utils.mkEvent({
type: "m.room.encrypted",
room: roomId,
content: aliMessage
})]
syncData.rooms.join[roomId] = {
timeline: {
events: [
utils.mkEvent({
type: "m.room.encrypted",
room: roomId,
content: aliMessage
})
]
}
};
httpBackend.when("GET", "initialSync").respond(200, initialSync);
httpBackend.when("GET", "events").respond(200, events);
httpBackend.when("GET", "/sync").respond(200, syncData);
bobClient.on("event", function(event) {
expect(event.getType()).toEqual("m.room.message");
expect(event.getContent()).toEqual({
+114 -89
View File
@@ -19,6 +19,7 @@ describe("MatrixClient events", function() {
accessToken: selfAccessToken
});
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
});
afterEach(function() {
@@ -26,108 +27,114 @@ describe("MatrixClient events", function() {
});
describe("emissions", function() {
var initialSync = {
end: "s_5_3",
presence: [{
event_id: "$wefiuewh:bar",
type: "m.presence",
content: {
user_id: "@foo:bar",
displayname: "Foo Bar",
presence: "online"
}
}],
rooms: [{
room_id: "!erufh:bar",
membership: "join",
messages: {
start: "s",
end: "t",
chunk: [
utils.mkMessage({
room: "!erufh:bar", user: "@foo:bar", msg: "hmmm"
})
]
},
state: [
utils.mkMembership({
room: "!erufh:bar", mship: "join", user: "@foo:bar"
}),
utils.mkEvent({
type: "m.room.create", room: "!erufh:bar", user: "@foo:bar",
content: {
creator: "@foo:bar"
}
var SYNC_DATA = {
next_batch: "s_5_3",
presence: {
events: [
utils.mkPresence({
user: "@foo:bar", name: "Foo Bar", presence: "online"
})
]
}]
};
var eventData = {
start: "s_5_3",
end: "e_6_7",
chunk: [
utils.mkMessage({
room: "!erufh:bar", user: "@foo:bar", msg: "ello ello"
}),
utils.mkMessage({
room: "!erufh:bar", user: "@foo:bar", msg: ":D"
}),
utils.mkEvent({
type: "m.typing", room: "!erufh:bar", content: {
user_ids: ["@foo:bar"]
},
rooms: {
join: {
"!erufh:bar": {
timeline: {
events: [
utils.mkMessage({
room: "!erufh:bar", user: "@foo:bar", msg: "hmmm"
})
],
prev_batch: "s"
},
state: {
events: [
utils.mkMembership({
room: "!erufh:bar", mship: "join", user: "@foo:bar"
}),
utils.mkEvent({
type: "m.room.create", room: "!erufh:bar",
user: "@foo:bar",
content: {
creator: "@foo:bar"
}
})
]
}
}
})
]
}
}
};
var NEXT_SYNC_DATA = {
next_batch: "e_6_7",
rooms: {
join: {
"!erufh:bar": {
timeline: {
events: [
utils.mkMessage({
room: "!erufh:bar", user: "@foo:bar", msg: "ello ello"
}),
utils.mkMessage({
room: "!erufh:bar", user: "@foo:bar", msg: ":D"
}),
]
},
ephemeral: {
events: [
utils.mkEvent({
type: "m.typing", room: "!erufh:bar", content: {
user_ids: ["@foo:bar"]
}
})
]
}
}
}
}
};
it("should emit events from both /initialSync and /events", function(done) {
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
httpBackend.when("GET", "/events").respond(200, eventData);
it("should emit events from both the first and subsequent /sync calls",
function(done) {
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
var expectedEvents = [];
expectedEvents = expectedEvents.concat(
SYNC_DATA.presence.events,
SYNC_DATA.rooms.join["!erufh:bar"].timeline.events,
SYNC_DATA.rooms.join["!erufh:bar"].state.events,
NEXT_SYNC_DATA.rooms.join["!erufh:bar"].timeline.events,
NEXT_SYNC_DATA.rooms.join["!erufh:bar"].ephemeral.events
);
// initial sync events are unordered, so make an array of the types
// that should be emitted and we'll just pick them off one by one,
// so long as this is emptied we're good.
var initialSyncEventTypes = [
"m.presence", "m.room.member", "m.room.message", "m.room.create"
];
var chunkIndex = 0;
client.on("event", function(event) {
if (initialSyncEventTypes.length === 0) {
if (chunkIndex + 1 >= eventData.chunk.length) {
return;
var found = false;
for (var i = 0; i < expectedEvents.length; i++) {
if (expectedEvents[i].event_id === event.getId()) {
expectedEvents.splice(i, 1);
found = true;
break;
}
// this should be /events now
expect(eventData.chunk[chunkIndex].event_id).toEqual(
event.getId()
);
chunkIndex++;
return;
}
var index = initialSyncEventTypes.indexOf(event.getType());
expect(index).not.toEqual(
-1, "Unexpected event type: " + event.getType()
expect(found).toBe(
true, "Unexpected 'event' emitted: " + event.getType()
);
if (index >= 0) {
initialSyncEventTypes.splice(index, 1);
}
});
client.startClient();
httpBackend.flush().done(function() {
expect(initialSyncEventTypes.length).toEqual(
0, "Failed to see all events from /initialSync"
);
expect(chunkIndex + 1).toEqual(
eventData.chunk.length, "Failed to see all events from /events"
expect(expectedEvents.length).toEqual(
0, "Failed to see all events from /sync calls"
);
done();
});
});
it("should emit User events", function(done) {
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
httpBackend.when("GET", "/events").respond(200, eventData);
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
var fired = false;
client.on("User.presence", function(event, user) {
fired = true;
@@ -135,9 +142,9 @@ describe("MatrixClient events", function() {
expect(event).toBeDefined();
if (!user || !event) { return; }
expect(event.event).toEqual(initialSync.presence[0]);
expect(event.event).toEqual(SYNC_DATA.presence.events[0]);
expect(user.presence).toEqual(
initialSync.presence[0].content.presence
SYNC_DATA.presence.events[0].content.presence
);
});
client.startClient();
@@ -149,8 +156,8 @@ describe("MatrixClient events", function() {
});
it("should emit Room events", function(done) {
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
httpBackend.when("GET", "/events").respond(200, eventData);
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
var roomInvokeCount = 0;
var roomNameInvokeCount = 0;
var timelineFireCount = 0;
@@ -183,8 +190,8 @@ describe("MatrixClient events", function() {
});
it("should emit RoomState events", function(done) {
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
httpBackend.when("GET", "/events").respond(200, eventData);
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
var roomStateEventTypes = [
"m.room.member", "m.room.create"
@@ -232,8 +239,8 @@ describe("MatrixClient events", function() {
});
it("should emit RoomMember events", function(done) {
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
httpBackend.when("GET", "/events").respond(200, eventData);
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
var typingInvokeCount = 0;
var powerLevelInvokeCount = 0;
@@ -272,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();
});
});
});
});
@@ -0,0 +1,650 @@
"use strict";
var q = require("q");
var sdk = require("../..");
var HttpBackend = require("../mock-request");
var utils = require("../test-utils");
var EventTimeline = sdk.EventTimeline;
var baseUrl = "http://localhost.or.something";
var userId = "@alice:localhost";
var userName = "Alice";
var accessToken = "aseukfgwef";
var roomId = "!foo:bar";
var otherUserId = "@bob:localhost";
var USER_MEMBERSHIP_EVENT = utils.mkMembership({
room: roomId, mship: "join", user: userId, name: userName
});
var ROOM_NAME_EVENT = utils.mkEvent({
type: "m.room.name", room: roomId, user: otherUserId,
content: {
name: "Old room name"
}
});
var INITIAL_SYNC_DATA = {
next_batch: "s_5_3",
rooms: {
join: {
"!foo:bar": { // roomId
timeline: {
events: [
utils.mkMessage({
room: roomId, user: otherUserId, msg: "hello"
})
],
prev_batch: "f_1_1"
},
state: {
events: [
ROOM_NAME_EVENT,
utils.mkMembership({
room: roomId, mship: "join",
user: otherUserId, name: "Bob"
}),
USER_MEMBERSHIP_EVENT,
utils.mkEvent({
type: "m.room.create", room: roomId, user: userId,
content: {
creator: userId
}
})
]
}
}
}
}
};
var EVENTS = [
utils.mkMessage({
room: roomId, user: userId, msg: "we",
}),
utils.mkMessage({
room: roomId, user: userId, msg: "could",
}),
utils.mkMessage({
room: roomId, user: userId, msg: "be",
}),
utils.mkMessage({
room: roomId, user: userId, msg: "heroes",
}),
];
// start the client, and wait for it to initialise
function startClient(httpBackend, client) {
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
httpBackend.when("GET", "/sync").respond(200, INITIAL_SYNC_DATA);
client.startClient();
// set up a promise which will resolve once the client is initialised
var deferred = q.defer();
client.on("sync", function(state) {
console.log("sync", state);
if (state != "SYNCING") {
return;
}
deferred.resolve();
});
httpBackend.flush();
return deferred.promise;
}
describe("getEventTimeline support", function() {
var httpBackend;
beforeEach(function() {
utils.beforeEach(this);
httpBackend = new HttpBackend();
sdk.request(httpBackend.requestFn);
});
it("timeline support must be enabled to work", function(done) {
var client = sdk.createClient({
baseUrl: baseUrl,
userId: userId,
accessToken: accessToken,
});
startClient(httpBackend, client
).then(function() {
var room = client.getRoom(roomId);
expect(function() { client.getEventTimeline(room, "event"); })
.toThrow();
}).catch(utils.failTest).done(done);
});
it("timeline support works when enabled", function(done) {
var client = sdk.createClient({
baseUrl: baseUrl,
userId: userId,
accessToken: accessToken,
timelineSupport: true,
});
startClient(httpBackend, client
).then(function() {
var room = client.getRoom(roomId);
expect(function() { client.getEventTimeline(room, "event"); })
.not.toThrow();
}).catch(utils.failTest).done(done);
httpBackend.flush().catch(utils.failTest);
});
it("scrollback should be able to scroll back to before a gappy /sync",
function(done) {
// need a client with timelineSupport disabled to make this work
var client = sdk.createClient({
baseUrl: baseUrl,
userId: userId,
accessToken: accessToken,
});
var room;
startClient(httpBackend, client
).then(function() {
room = client.getRoom(roomId);
httpBackend.when("GET", "/sync").respond(200, {
next_batch: "s_5_4",
rooms: {
join: {
"!foo:bar": {
timeline: {
events: [
EVENTS[0],
],
prev_batch: "f_1_1",
},
},
},
},
});
httpBackend.when("GET", "/sync").respond(200, {
next_batch: "s_5_5",
rooms: {
join: {
"!foo:bar": {
timeline: {
events: [
EVENTS[1],
],
limited: true,
prev_batch: "f_1_2",
},
},
},
},
});
httpBackend.when("GET", "/messages").respond(200, {
chunk: [EVENTS[0]],
start: "pagin_start",
end: "pagin_end",
});
return httpBackend.flush("/sync", 2);
}).then(function() {
expect(room.timeline.length).toEqual(1);
expect(room.timeline[0].event).toEqual(EVENTS[1]);
httpBackend.flush("/messages", 1);
return client.scrollback(room);
}).then(function() {
expect(room.timeline.length).toEqual(2);
expect(room.timeline[0].event).toEqual(EVENTS[0]);
expect(room.timeline[1].event).toEqual(EVENTS[1]);
expect(room.oldState.paginationToken).toEqual("pagin_end");
}).catch(utils.failTest).done(done);
});
});
describe("MatrixClient event timelines", function() {
var client, httpBackend;
beforeEach(function(done) {
utils.beforeEach(this);
httpBackend = new HttpBackend();
sdk.request(httpBackend.requestFn);
client = sdk.createClient({
baseUrl: baseUrl,
userId: userId,
accessToken: accessToken,
timelineSupport: true,
});
startClient(httpBackend, client)
.catch(utils.failTest).done(done);
});
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
});
describe("getEventTimeline", function() {
it("should create a new timeline for new events", function(done) {
var room = client.getRoom(roomId);
httpBackend.when("GET", "/rooms/!foo%3Abar/context/event1%3Abar")
.respond(200, function() {
return {
start: "start_token",
events_before: [EVENTS[1], EVENTS[0]],
event: EVENTS[2],
events_after: [EVENTS[3]],
state: [
ROOM_NAME_EVENT,
USER_MEMBERSHIP_EVENT,
],
end: "end_token",
};
});
client.getEventTimeline(room, "event1:bar").then(function(tl) {
expect(tl.getEvents().length).toEqual(4);
for (var i = 0; i < 4; i++) {
expect(tl.getEvents()[i].event).toEqual(EVENTS[i]);
expect(tl.getEvents()[i].sender.name).toEqual(userName);
}
expect(tl.getPaginationToken(EventTimeline.BACKWARDS))
.toEqual("start_token");
expect(tl.getPaginationToken(EventTimeline.FORWARDS))
.toEqual("end_token");
}).catch(utils.failTest).done(done);
httpBackend.flush().catch(utils.failTest);
});
it("should return existing timeline for known events", function(done) {
var room = client.getRoom(roomId);
httpBackend.when("GET", "/sync").respond(200, {
next_batch: "s_5_4",
rooms: {
join: {
"!foo:bar": {
timeline: {
events: [
EVENTS[0],
],
prev_batch: "f_1_2",
},
},
},
},
});
httpBackend.flush("/sync").then(function() {
return client.getEventTimeline(room, EVENTS[0].event_id);
}).then(function(tl) {
expect(tl.getEvents().length).toEqual(2);
expect(tl.getEvents()[1].event).toEqual(EVENTS[0]);
expect(tl.getEvents()[1].sender.name).toEqual(userName);
expect(tl.getPaginationToken(EventTimeline.BACKWARDS)).toEqual("f_1_1");
// expect(tl.getPaginationToken(EventTimeline.FORWARDS)).toEqual("s_5_4");
}).catch(utils.failTest).done(done);
httpBackend.flush().catch(utils.failTest);
});
it("should update timelines where they overlap a previous /sync", function(done) {
var room = client.getRoom(roomId);
httpBackend.when("GET", "/sync").respond(200, {
next_batch: "s_5_4",
rooms: {
join: {
"!foo:bar": {
timeline: {
events: [
EVENTS[3],
],
prev_batch: "f_1_2",
},
},
},
},
});
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
encodeURIComponent(EVENTS[2].event_id))
.respond(200, function() {
return {
start: "start_token",
events_before: [EVENTS[1]],
event: EVENTS[2],
events_after: [EVENTS[3]],
end: "end_token",
state: [],
};
});
client.on("sync", function() {
client.getEventTimeline(room, EVENTS[2].event_id
).then(function(tl) {
expect(tl.getEvents().length).toEqual(4);
expect(tl.getEvents()[0].event).toEqual(EVENTS[1]);
expect(tl.getEvents()[1].event).toEqual(EVENTS[2]);
expect(tl.getEvents()[3].event).toEqual(EVENTS[3]);
expect(tl.getPaginationToken(EventTimeline.BACKWARDS))
.toEqual("start_token");
// expect(tl.getPaginationToken(EventTimeline.FORWARDS))
// .toEqual("s_5_4");
}).catch(utils.failTest).done(done);
});
httpBackend.flush().catch(utils.failTest);
});
it("should join timelines where they overlap a previous /context",
function(done) {
var room = client.getRoom(roomId);
// we fetch event 0, then 2, then 3, and finally 1. 1 is returned
// with context which joins them all up.
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
encodeURIComponent(EVENTS[0].event_id))
.respond(200, function() {
return {
start: "start_token0",
events_before: [],
event: EVENTS[0],
events_after: [],
end: "end_token0",
state: [],
};
});
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
encodeURIComponent(EVENTS[2].event_id))
.respond(200, function() {
return {
start: "start_token2",
events_before: [],
event: EVENTS[2],
events_after: [],
end: "end_token2",
state: [],
};
});
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
encodeURIComponent(EVENTS[3].event_id))
.respond(200, function() {
return {
start: "start_token3",
events_before: [],
event: EVENTS[3],
events_after: [],
end: "end_token3",
state: [],
};
});
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
encodeURIComponent(EVENTS[1].event_id))
.respond(200, function() {
return {
start: "start_token4",
events_before: [EVENTS[0]],
event: EVENTS[1],
events_after: [EVENTS[2], EVENTS[3]],
end: "end_token4",
state: [],
};
});
var tl0, tl2, tl3;
client.getEventTimeline(room, EVENTS[0].event_id
).then(function(tl) {
expect(tl.getEvents().length).toEqual(1);
tl0 = tl;
return client.getEventTimeline(room, EVENTS[2].event_id);
}).then(function(tl) {
expect(tl.getEvents().length).toEqual(1);
tl2 = tl;
return client.getEventTimeline(room, EVENTS[3].event_id);
}).then(function(tl) {
expect(tl.getEvents().length).toEqual(1);
tl3 = tl;
return client.getEventTimeline(room, EVENTS[1].event_id);
}).then(function(tl) {
// we expect it to get merged in with event 2
expect(tl.getEvents().length).toEqual(2);
expect(tl.getEvents()[0].event).toEqual(EVENTS[1]);
expect(tl.getEvents()[1].event).toEqual(EVENTS[2]);
expect(tl.getNeighbouringTimeline(EventTimeline.BACKWARDS))
.toBe(tl0);
expect(tl.getNeighbouringTimeline(EventTimeline.FORWARDS))
.toBe(tl3);
expect(tl0.getPaginationToken(EventTimeline.BACKWARDS))
.toEqual("start_token0");
expect(tl0.getPaginationToken(EventTimeline.FORWARDS))
.toBe(null);
expect(tl3.getPaginationToken(EventTimeline.BACKWARDS))
.toBe(null);
expect(tl3.getPaginationToken(EventTimeline.FORWARDS))
.toEqual("end_token3");
}).catch(utils.failTest).done(done);
httpBackend.flush().catch(utils.failTest);
});
it("should fail gracefully if there is no event field", function(done) {
var room = client.getRoom(roomId);
// we fetch event 0, then 2, then 3, and finally 1. 1 is returned
// with context which joins them all up.
httpBackend.when("GET", "/rooms/!foo%3Abar/context/event1")
.respond(200, function() {
return {
start: "start_token",
events_before: [],
events_after: [],
end: "end_token",
state: [],
};
});
client.getEventTimeline(room, "event1"
).then(function(tl) {
// could do with a fail()
expect(true).toBeFalsy();
}).catch(function(e) {
expect(String(e)).toMatch(/'event'/);
}).catch(utils.failTest).done(done);
httpBackend.flush().catch(utils.failTest);
});
});
describe("paginateEventTimeline", function() {
it("should allow you to paginate backwards", function(done) {
var room = client.getRoom(roomId);
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
encodeURIComponent(EVENTS[0].event_id))
.respond(200, function() {
return {
start: "start_token0",
events_before: [],
event: EVENTS[0],
events_after: [],
end: "end_token0",
state: [],
};
});
httpBackend.when("GET", "/rooms/!foo%3Abar/messages")
.check(function(req) {
var params = req.queryParams;
expect(params.dir).toEqual("b");
expect(params.from).toEqual("start_token0");
expect(params.limit).toEqual(30);
}).respond(200, function() {
return {
chunk: [EVENTS[1], EVENTS[2]],
end: "start_token1",
};
});
var tl;
client.getEventTimeline(room, EVENTS[0].event_id
).then(function(tl0) {
tl = tl0;
return client.paginateEventTimeline(tl, {backwards: true});
}).then(function(success) {
expect(success).toBeTruthy();
expect(tl.getEvents().length).toEqual(3);
expect(tl.getEvents()[0].event).toEqual(EVENTS[2]);
expect(tl.getEvents()[1].event).toEqual(EVENTS[1]);
expect(tl.getEvents()[2].event).toEqual(EVENTS[0]);
expect(tl.getPaginationToken(EventTimeline.BACKWARDS))
.toEqual("start_token1");
expect(tl.getPaginationToken(EventTimeline.FORWARDS))
.toEqual("end_token0");
}).catch(utils.failTest).done(done);
httpBackend.flush().catch(utils.failTest);
});
it("should allow you to paginate forwards", function(done) {
var room = client.getRoom(roomId);
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
encodeURIComponent(EVENTS[0].event_id))
.respond(200, function() {
return {
start: "start_token0",
events_before: [],
event: EVENTS[0],
events_after: [],
end: "end_token0",
state: [],
};
});
httpBackend.when("GET", "/rooms/!foo%3Abar/messages")
.check(function(req) {
var params = req.queryParams;
expect(params.dir).toEqual("f");
expect(params.from).toEqual("end_token0");
expect(params.limit).toEqual(20);
}).respond(200, function() {
return {
chunk: [EVENTS[1], EVENTS[2]],
end: "end_token1",
};
});
var tl;
client.getEventTimeline(room, EVENTS[0].event_id
).then(function(tl0) {
tl = tl0;
return client.paginateEventTimeline(
tl, {backwards: false, limit: 20});
}).then(function(success) {
expect(success).toBeTruthy();
expect(tl.getEvents().length).toEqual(3);
expect(tl.getEvents()[0].event).toEqual(EVENTS[0]);
expect(tl.getEvents()[1].event).toEqual(EVENTS[1]);
expect(tl.getEvents()[2].event).toEqual(EVENTS[2]);
expect(tl.getPaginationToken(EventTimeline.BACKWARDS))
.toEqual("start_token0");
expect(tl.getPaginationToken(EventTimeline.FORWARDS))
.toEqual("end_token1");
}).catch(utils.failTest).done(done);
httpBackend.flush().catch(utils.failTest);
});
});
describe("event timeline for sent events", function() {
var TXN_ID = "txn1";
var event = utils.mkMessage({
room: roomId, user: userId, msg: "a body",
});
event.unsigned = {transaction_id: TXN_ID};
beforeEach(function() {
// set up handlers for both the message send, and the
// /sync
httpBackend.when("PUT", "/send/m.room.message/" + TXN_ID)
.respond(200, {
event_id: event.event_id,
});
httpBackend.when("GET", "/sync").respond(200, {
next_batch: "s_5_4",
rooms: {
join: {
"!foo:bar": {
timeline: {
events: [
event
],
prev_batch: "f_1_1",
},
},
},
},
});
});
it("should work when /send returns before /sync", function(done) {
var room = client.getRoom(roomId);
client.sendTextMessage(roomId, "a body", TXN_ID).then(function(res) {
expect(res.event_id).toEqual(event.event_id);
return client.getEventTimeline(room, event.event_id);
}).then(function(tl) {
// 2 because the initial sync contained an event
expect(tl.getEvents().length).toEqual(2);
expect(tl.getEvents()[1].getContent().body).toEqual("a body");
// now let the sync complete, and check it again
return httpBackend.flush("/sync", 1);
}).then(function() {
return client.getEventTimeline(room, event.event_id);
}).then(function(tl) {
expect(tl.getEvents().length).toEqual(2);
expect(tl.getEvents()[1].event).toEqual(event);
}).catch(utils.failTest).done(done);
httpBackend.flush("/send/m.room.message/" + TXN_ID, 1).catch(utils.failTest);
});
it("should work when /send returns after /sync", function(done) {
var room = client.getRoom(roomId);
// initiate the send, and set up checks to be done when it completes
// - but note that it won't complete until after the /sync does, below.
client.sendTextMessage(roomId, "a body", TXN_ID).then(function(res) {
console.log("sendTextMessage completed");
expect(res.event_id).toEqual(event.event_id);
return client.getEventTimeline(room, event.event_id);
}).then(function(tl) {
console.log("getEventTimeline completed (2)");
expect(tl.getEvents().length).toEqual(2);
expect(tl.getEvents()[1].getContent().body).toEqual("a body");
}).catch(utils.failTest).done(done);
httpBackend.flush("/sync", 1).then(function() {
return client.getEventTimeline(room, event.event_id);
}).then(function(tl) {
console.log("getEventTimeline completed (1)");
expect(tl.getEvents().length).toEqual(2);
expect(tl.getEvents()[1].event).toEqual(event);
// now let the send complete.
return httpBackend.flush("/send/m.room.message/" + TXN_ID, 1);
}).catch(utils.failTest);
});
});
});
+130
View File
@@ -4,6 +4,7 @@ var HttpBackend = require("../mock-request");
var publicGlobals = require("../../lib/matrix");
var Room = publicGlobals.Room;
var MatrixInMemoryStore = publicGlobals.MatrixInMemoryStore;
var Filter = publicGlobals.Filter;
var utils = require("../test-utils");
describe("MatrixClient", function() {
@@ -43,4 +44,133 @@ describe("MatrixClient", function() {
httpBackend.verifyNoOutstandingRequests();
});
});
describe("getFilter", function() {
var filterId = "f1lt3r1d";
it("should return a filter from the store if allowCached", function(done) {
var filter = Filter.fromJson(userId, filterId, {
event_format: "client"
});
store.storeFilter(filter);
client.getFilter(userId, filterId, true).done(function(gotFilter) {
expect(gotFilter).toEqual(filter);
done();
});
httpBackend.verifyNoOutstandingRequests();
});
it("should do an HTTP request if !allowCached even if one exists",
function(done) {
var httpFilterDefinition = {
event_format: "federation"
};
httpBackend.when(
"GET", "/user/" + encodeURIComponent(userId) + "/filter/" + filterId
).respond(200, httpFilterDefinition);
var storeFilter = Filter.fromJson(userId, filterId, {
event_format: "client"
});
store.storeFilter(storeFilter);
client.getFilter(userId, filterId, false).done(function(gotFilter) {
expect(gotFilter.getDefinition()).toEqual(httpFilterDefinition);
done();
});
httpBackend.flush();
});
it("should do an HTTP request if nothing is in the cache and then store it",
function(done) {
var httpFilterDefinition = {
event_format: "federation"
};
expect(store.getFilter(userId, filterId)).toBeNull();
httpBackend.when(
"GET", "/user/" + encodeURIComponent(userId) + "/filter/" + filterId
).respond(200, httpFilterDefinition);
client.getFilter(userId, filterId, true).done(function(gotFilter) {
expect(gotFilter.getDefinition()).toEqual(httpFilterDefinition);
expect(store.getFilter(userId, filterId)).toBeDefined();
done();
});
httpBackend.flush();
});
});
describe("createFilter", function() {
var filterId = "f1llllllerid";
it("should do an HTTP request and then store the filter", function(done) {
expect(store.getFilter(userId, filterId)).toBeNull();
var filterDefinition = {
event_format: "client"
};
httpBackend.when(
"POST", "/user/" + encodeURIComponent(userId) + "/filter"
).check(function(req) {
expect(req.data).toEqual(filterDefinition);
}).respond(200, {
filter_id: filterId
});
client.createFilter(filterDefinition).done(function(gotFilter) {
expect(gotFilter.getDefinition()).toEqual(filterDefinition);
expect(store.getFilter(userId, filterId)).toEqual(gotFilter);
done();
});
httpBackend.flush();
});
});
describe("searching", function() {
var response = {
search_categories: {
room_events: {
count: 24,
results: {
"$flibble:localhost": {
rank: 0.1,
result: {
type: "m.room.message",
user_id: "@alice:localhost",
room_id: "!feuiwhf:localhost",
content: {
body: "a result",
msgtype: "m.text"
}
}
}
}
}
}
};
it("searchMessageText should perform a /search for room_events", function(done) {
client.searchMessageText({
query: "monkeys"
});
httpBackend.when("POST", "/search").check(function(req) {
expect(req.data).toEqual({
search_categories: {
room_events: {
search_term: "monkeys"
}
}
});
}).respond(200, response);
httpBackend.flush().done(function() {
done();
});
});
});
});
+42 -44
View File
@@ -11,47 +11,45 @@ describe("MatrixClient opts", function() {
var userB = "@bob:localhost";
var accessToken = "aseukfgwef";
var roomId = "!foo:bar";
var eventData = {
chunk: [],
start: "s",
end: "e"
};
var initialSync = {
end: "s_5_3",
presence: [],
rooms: [{
membership: "join",
room_id: roomId,
messages: {
start: "f_1_1",
end: "f_2_2",
chunk: [
utils.mkMessage({
room: roomId, user: userB, msg: "hello"
})
]
},
state: [
utils.mkEvent({
type: "m.room.name", room: roomId, user: userB,
content: {
name: "Old room name"
var syncData = {
next_batch: "s_5_3",
presence: {},
rooms: {
join: {
"!foo:bar": { // roomId
timeline: {
events: [
utils.mkMessage({
room: roomId, user: userB, msg: "hello"
})
],
prev_batch: "f_1_1"
},
state: {
events: [
utils.mkEvent({
type: "m.room.name", room: roomId, user: userB,
content: {
name: "Old room name"
}
}),
utils.mkMembership({
room: roomId, mship: "join", user: userB, name: "Bob"
}),
utils.mkMembership({
room: roomId, mship: "join", user: userId, name: "Alice"
}),
utils.mkEvent({
type: "m.room.create", room: roomId, user: userId,
content: {
creator: userId
}
})
]
}
}),
utils.mkMembership({
room: roomId, mship: "join", user: userB, name: "Bob"
}),
utils.mkMembership({
room: roomId, mship: "join", user: userId, name: "Alice"
}),
utils.mkEvent({
type: "m.room.create", room: roomId, user: userId,
content: {
creator: userId
}
})
]
}]
}
}
}
};
beforeEach(function() {
@@ -101,13 +99,13 @@ describe("MatrixClient opts", function() {
);
});
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
httpBackend.when("GET", "/events").respond(200, eventData);
httpBackend.when("POST", "/filter").respond(200, { filter_id: "foo" });
httpBackend.when("GET", "/sync").respond(200, syncData);
client.startClient();
httpBackend.flush("/pushrules", 1).then(function() {
return httpBackend.flush("/initialSync", 1);
return httpBackend.flush("/filter", 1);
}).then(function() {
return httpBackend.flush("/events", 1);
return httpBackend.flush("/sync", 1);
}).done(function() {
expect(expectedEventTypes.length).toEqual(
0, "Expected to see event types: " + expectedEventTypes
+275 -124
View File
@@ -12,45 +12,94 @@ describe("MatrixClient room timelines", function() {
var accessToken = "aseukfgwef";
var roomId = "!foo:bar";
var otherUserId = "@bob:localhost";
var eventData;
var initialSync = {
end: "s_5_3",
presence: [],
rooms: [{
membership: "join",
room_id: roomId,
messages: {
start: "f_1_1",
end: "f_2_2",
chunk: [
utils.mkMessage({
room: roomId, user: otherUserId, msg: "hello"
})
]
},
state: [
utils.mkEvent({
type: "m.room.name", room: roomId, user: otherUserId,
content: {
name: "Old room name"
var USER_MEMBERSHIP_EVENT = utils.mkMembership({
room: roomId, mship: "join", user: userId, name: userName
});
var ROOM_NAME_EVENT = utils.mkEvent({
type: "m.room.name", room: roomId, user: otherUserId,
content: {
name: "Old room name"
}
});
var NEXT_SYNC_DATA;
var SYNC_DATA = {
next_batch: "s_5_3",
rooms: {
join: {
"!foo:bar": { // roomId
timeline: {
events: [
utils.mkMessage({
room: roomId, user: otherUserId, msg: "hello"
})
],
prev_batch: "f_1_1"
},
state: {
events: [
ROOM_NAME_EVENT,
utils.mkMembership({
room: roomId, mship: "join",
user: otherUserId, name: "Bob"
}),
USER_MEMBERSHIP_EVENT,
utils.mkEvent({
type: "m.room.create", room: roomId, user: userId,
content: {
creator: userId
}
})
]
}
}),
utils.mkMembership({
room: roomId, mship: "join", user: otherUserId, name: "Bob"
}),
utils.mkMembership({
room: roomId, mship: "join", user: userId, name: userName
}),
utils.mkEvent({
type: "m.room.create", room: roomId, user: userId,
content: {
creator: userId
}
})
]
}]
}
}
}
};
function setNextSyncData(events) {
events = events || [];
NEXT_SYNC_DATA = {
next_batch: "n",
presence: { events: [] },
rooms: {
invite: {},
join: {
"!foo:bar": {
timeline: { events: [] },
state: { events: [] },
ephemeral: { events: [] }
}
},
leave: {}
}
};
events.forEach(function(e) {
if (e.room_id !== roomId) {
throw new Error("setNextSyncData only works with one room id");
}
if (e.state_key) {
if (e.__prev_event === undefined) {
throw new Error(
"setNextSyncData needs the prev state set to '__prev_event' " +
"for " + e.type
);
}
if (e.__prev_event !== null) {
// push the previous state for this event type
NEXT_SYNC_DATA.rooms.join[roomId].state.events.push(e.__prev_event);
}
// push the current
NEXT_SYNC_DATA.rooms.join[roomId].timeline.events.push(e);
}
else if (["m.typing", "m.receipt"].indexOf(e.type) !== -1) {
NEXT_SYNC_DATA.rooms.join[roomId].ephemeral.events.push(e);
}
else {
NEXT_SYNC_DATA.rooms.join[roomId].timeline.events.push(e);
}
});
}
beforeEach(function(done) {
utils.beforeEach(this);
httpBackend = new HttpBackend();
@@ -58,20 +107,21 @@ describe("MatrixClient room timelines", function() {
client = sdk.createClient({
baseUrl: baseUrl,
userId: userId,
accessToken: accessToken
accessToken: accessToken,
// these tests should work with or without timelineSupport
timelineSupport: true,
});
eventData = {
chunk: [],
end: "end_",
start: "start_"
};
setNextSyncData();
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
httpBackend.when("GET", "/events").respond(200, function() {
return eventData;
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend.when("GET", "/sync").respond(200, function() {
return NEXT_SYNC_DATA;
});
client.startClient();
httpBackend.flush("/pushrules").done(done);
httpBackend.flush("/pushrules").then(function() {
return httpBackend.flush("/filter");
}).done(done);
});
afterEach(function() {
@@ -82,7 +132,8 @@ describe("MatrixClient room timelines", function() {
it("should be added immediately after calling MatrixClient.sendEvent " +
"with EventStatus.SENDING and the right event.sender", function(done) {
client.on("syncComplete", function() {
client.on("sync", function(state) {
if (state !== "PREPARED") { return; }
var room = client.getRoom(roomId);
expect(room.timeline.length).toEqual(1);
@@ -96,11 +147,11 @@ describe("MatrixClient room timelines", function() {
expect(member.userId).toEqual(userId);
expect(member.name).toEqual(userName);
httpBackend.flush("/events", 1).done(function() {
httpBackend.flush("/sync", 1).done(function() {
done();
});
});
httpBackend.flush("/initialSync", 1);
httpBackend.flush("/sync", 1);
});
it("should be updated correctly when the send request finishes " +
@@ -109,26 +160,28 @@ describe("MatrixClient room timelines", function() {
httpBackend.when("PUT", "/txn1").respond(200, {
event_id: eventId
});
eventData.chunk = [
utils.mkMessage({
body: "I am a fish", user: userId, room: roomId
})
];
eventData.chunk[0].event_id = eventId;
client.on("syncComplete", function() {
var ev = utils.mkMessage({
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) {
if (state !== "PREPARED") { return; }
var room = client.getRoom(roomId);
client.sendTextMessage(roomId, "I am a fish", "txn1").done(
function() {
expect(room.timeline[1].getId()).toEqual(eventId);
httpBackend.flush("/events", 1).done(function() {
httpBackend.flush("/sync", 1).done(function() {
expect(room.timeline[1].getId()).toEqual(eventId);
done();
});
});
httpBackend.flush("/txn1", 1);
});
httpBackend.flush("/initialSync", 1);
httpBackend.flush("/sync", 1);
});
it("should be updated correctly when the send request finishes " +
@@ -137,19 +190,20 @@ describe("MatrixClient room timelines", function() {
httpBackend.when("PUT", "/txn1").respond(200, {
event_id: eventId
});
eventData.chunk = [
utils.mkMessage({
body: "I am a fish", user: userId, room: roomId
})
];
eventData.chunk[0].event_id = eventId;
client.on("syncComplete", function() {
var ev = utils.mkMessage({
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) {
if (state !== "PREPARED") { return; }
var room = client.getRoom(roomId);
var promise = client.sendTextMessage(roomId, "I am a fish", "txn1");
httpBackend.flush("/events", 1).done(function() {
// expect 3rd msg, it doesn't know this is the request is just did
expect(room.timeline.length).toEqual(3);
httpBackend.flush("/sync", 1).done(function() {
expect(room.timeline.length).toEqual(2);
httpBackend.flush("/txn1", 1);
promise.done(function() {
expect(room.timeline.length).toEqual(2);
@@ -159,7 +213,7 @@ describe("MatrixClient room timelines", function() {
});
});
httpBackend.flush("/initialSync", 1);
httpBackend.flush("/sync", 1);
});
});
@@ -180,7 +234,8 @@ describe("MatrixClient room timelines", function() {
it("should set Room.oldState.paginationToken to null at the start" +
" of the timeline.", function(done) {
client.on("syncComplete", function() {
client.on("sync", function(state) {
if (state !== "PREPARED") { return; }
var room = client.getRoom(roomId);
expect(room.timeline.length).toEqual(1);
@@ -191,13 +246,29 @@ describe("MatrixClient room timelines", function() {
});
httpBackend.flush("/messages", 1);
httpBackend.flush("/events", 1);
httpBackend.flush("/sync", 1);
});
httpBackend.flush("/initialSync", 1);
httpBackend.flush("/sync", 1);
});
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"
@@ -208,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"
@@ -216,26 +288,31 @@ describe("MatrixClient room timelines", function() {
oldMshipEvent,
utils.mkMessage({
user: userId, room: roomId, msg: "I'm old alice"
})
}),
joinMshipEvent,
];
client.on("syncComplete", function() {
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();
});
httpBackend.flush("/messages", 1);
httpBackend.flush("/events", 1);
httpBackend.flush("/sync", 1);
});
httpBackend.flush("/initialSync", 1);
httpBackend.flush("/sync", 1);
});
it("should add it them to the right place in the timeline", function(done) {
@@ -249,7 +326,8 @@ describe("MatrixClient room timelines", function() {
})
];
client.on("syncComplete", function() {
client.on("sync", function(state) {
if (state !== "PREPARED") { return; }
var room = client.getRoom(roomId);
expect(room.timeline.length).toEqual(1);
@@ -261,9 +339,9 @@ describe("MatrixClient room timelines", function() {
});
httpBackend.flush("/messages", 1);
httpBackend.flush("/events", 1);
httpBackend.flush("/sync", 1);
});
httpBackend.flush("/initialSync", 1);
httpBackend.flush("/sync", 1);
});
it("should use 'end' as the next pagination token", function(done) {
@@ -274,7 +352,8 @@ describe("MatrixClient room timelines", function() {
})
];
client.on("syncComplete", function() {
client.on("sync", function(state) {
if (state !== "PREPARED") { return; }
var room = client.getRoom(roomId);
expect(room.oldState.paginationToken).toBeDefined();
@@ -282,58 +361,65 @@ describe("MatrixClient room timelines", function() {
expect(room.oldState.paginationToken).toEqual(sbEndTok);
});
httpBackend.flush("/messages", 1);
httpBackend.flush("/events", 1).done(function() {
done();
});
httpBackend.flush("/sync", 1);
httpBackend.flush("/messages", 1).done(function() {
done();
});
});
httpBackend.flush("/initialSync", 1);
httpBackend.flush("/sync", 1);
});
});
describe("new events", function() {
it("should be added to the right place in the timeline", function(done) {
eventData.chunk = [
var eventData = [
utils.mkMessage({user: userId, room: roomId}),
utils.mkMessage({user: userId, room: roomId})
];
client.on("syncComplete", function() {
setNextSyncData(eventData);
client.on("sync", function(state) {
if (state !== "PREPARED") { return; }
var room = client.getRoom(roomId);
var index = 0;
client.on("Room.timeline", function(event, rm, toStart) {
expect(toStart).toBe(false);
expect(rm).toEqual(room);
expect(event.event).toEqual(eventData.chunk[index]);
expect(event.event).toEqual(eventData[index]);
index += 1;
});
httpBackend.flush("/messages", 1);
httpBackend.flush("/events", 1).done(function() {
httpBackend.flush("/sync", 1).done(function() {
expect(index).toEqual(2);
expect(room.timeline[room.timeline.length - 1].event).toEqual(
eventData.chunk[1]
eventData[1]
);
expect(room.timeline[room.timeline.length - 2].event).toEqual(
eventData.chunk[0]
eventData[0]
);
done();
});
});
httpBackend.flush("/initialSync", 1);
httpBackend.flush("/sync", 1);
});
it("should set the right event.sender values", function(done) {
eventData.chunk = [
var eventData = [
utils.mkMessage({user: userId, room: roomId}),
utils.mkMembership({
user: userId, room: roomId, mship: "join", name: "New Name"
}),
utils.mkMessage({user: userId, room: roomId})
];
client.on("syncComplete", function() {
eventData[1].__prev_event = USER_MEMBERSHIP_EVENT;
setNextSyncData(eventData);
client.on("sync", function(state) {
if (state !== "PREPARED") { return; }
var room = client.getRoom(roomId);
httpBackend.flush("/events", 1).done(function() {
httpBackend.flush("/sync", 1).done(function() {
var preNameEvent = room.timeline[room.timeline.length - 3];
var postNameEvent = room.timeline[room.timeline.length - 1];
expect(preNameEvent.sender.name).toEqual(userName);
@@ -341,50 +427,52 @@ describe("MatrixClient room timelines", function() {
done();
});
});
httpBackend.flush("/initialSync", 1);
httpBackend.flush("/sync", 1);
});
it("should set the right room.name", function(done) {
eventData.chunk = [
utils.mkEvent({
user: userId, room: roomId, type: "m.room.name", content: {
name: "Room 2"
}
})
];
client.on("syncComplete", function() {
var secondRoomNameEvent = utils.mkEvent({
user: userId, room: roomId, type: "m.room.name", content: {
name: "Room 2"
}
});
secondRoomNameEvent.__prev_event = ROOM_NAME_EVENT;
setNextSyncData([secondRoomNameEvent]);
client.on("sync", function(state) {
if (state !== "PREPARED") { return; }
var room = client.getRoom(roomId);
var nameEmitCount = 0;
client.on("Room.name", function(rm) {
nameEmitCount += 1;
});
httpBackend.flush("/events", 1).done(function() {
httpBackend.flush("/sync", 1).done(function() {
expect(nameEmitCount).toEqual(1);
expect(room.name).toEqual("Room 2");
// do another round
eventData.chunk = [
utils.mkEvent({
user: userId, room: roomId, type: "m.room.name", content: {
name: "Room 3"
}
})
];
httpBackend.when("GET", "/events").respond(200, eventData);
httpBackend.flush("/events", 1).done(function() {
var thirdRoomNameEvent = utils.mkEvent({
user: userId, room: roomId, type: "m.room.name", content: {
name: "Room 3"
}
});
thirdRoomNameEvent.__prev_event = secondRoomNameEvent;
setNextSyncData([thirdRoomNameEvent]);
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
httpBackend.flush("/sync", 1).done(function() {
expect(nameEmitCount).toEqual(2);
expect(room.name).toEqual("Room 3");
done();
});
});
});
httpBackend.flush("/initialSync", 1);
httpBackend.flush("/sync", 1);
});
it("should set the right room members", function(done) {
var userC = "@cee:bar";
var userD = "@dee:bar";
eventData.chunk = [
var eventData = [
utils.mkMembership({
user: userC, room: roomId, mship: "join", name: "C"
}),
@@ -392,9 +480,14 @@ describe("MatrixClient room timelines", function() {
user: userC, room: roomId, mship: "invite", skey: userD
})
];
client.on("syncComplete", function() {
eventData[0].__prev_event = null;
eventData[1].__prev_event = null;
setNextSyncData(eventData);
client.on("sync", function(state) {
if (state !== "PREPARED") { return; }
var room = client.getRoom(roomId);
httpBackend.flush("/events", 1).done(function() {
httpBackend.flush("/sync", 1).done(function() {
expect(room.currentState.getMembers().length).toEqual(4);
expect(room.currentState.getMember(userC).name).toEqual("C");
expect(room.currentState.getMember(userC).membership).toEqual(
@@ -407,7 +500,65 @@ describe("MatrixClient room timelines", function() {
done();
});
});
httpBackend.flush("/initialSync", 1);
httpBackend.flush("/sync", 1);
});
});
describe("gappy sync", function() {
it("should copy the last known state to the new timeline", 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);
httpBackend.flush("/messages", 1);
httpBackend.flush("/sync", 1).done(function() {
expect(room.timeline.length).toEqual(1);
expect(room.timeline[0].event).toEqual(eventData[0]);
expect(room.currentState.getMembers().length).toEqual(2);
expect(room.currentState.getMember(userId).name).toEqual(userName);
expect(room.currentState.getMember(userId).membership).toEqual(
"join"
);
expect(room.currentState.getMember(otherUserId).name).toEqual("Bob");
expect(room.currentState.getMember(otherUserId).membership).toEqual(
"join"
);
done();
});
});
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);
});
});
});
+551 -145
View File
@@ -2,6 +2,8 @@
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";
@@ -9,6 +11,11 @@ describe("MatrixClient syncing", function() {
var selfUserId = "@alice:localhost";
var selfAccessToken = "aseukfgwef";
var otherUserId = "@bob:localhost";
var userA = "@alice:bar";
var userB = "@bob:bar";
var userC = "@claire:bar";
var roomOne = "!foo:localhost";
var roomTwo = "!bar:localhost";
beforeEach(function() {
utils.beforeEach(this);
@@ -20,6 +27,7 @@ describe("MatrixClient syncing", function() {
accessToken: selfAccessToken
});
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
});
afterEach(function() {
@@ -27,20 +35,14 @@ describe("MatrixClient syncing", function() {
});
describe("startClient", function() {
var initialSync = {
end: "s_5_3",
presence: [],
rooms: []
};
var eventData = {
start: "s_5_3",
end: "e_6_7",
chunk: []
var syncData = {
next_batch: "batch_token",
rooms: {},
presence: {}
};
it("should start with /initialSync then move onto /events.", function(done) {
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
httpBackend.when("GET", "/events").respond(200, eventData);
it("should /sync after /pushrules and /filter.", function(done) {
httpBackend.when("GET", "/sync").respond(200, syncData);
client.startClient();
@@ -49,12 +51,12 @@ describe("MatrixClient syncing", function() {
});
});
it("should pass the 'end' token from /initialSync to the from= param " +
" of /events", function(done) {
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
httpBackend.when("GET", "/events").check(function(req) {
expect(req.queryParams.from).toEqual(initialSync.end);
}).respond(200, eventData);
it("should pass the 'next_batch' token from /sync to the since= param " +
" of the next /sync", function(done) {
httpBackend.when("GET", "/sync").respond(200, syncData);
httpBackend.when("GET", "/sync").check(function(req) {
expect(req.queryParams.since).toEqual(syncData.next_batch);
}).respond(200, syncData);
client.startClient();
@@ -64,81 +66,32 @@ describe("MatrixClient syncing", function() {
});
});
describe("users", function() {
var userA = "@alice:bar";
var userB = "@bob:bar";
var userC = "@claire:bar";
var initialSync = {
end: "s_5_3",
presence: [
utils.mkPresence({
user: userA, presence: "online"
}),
utils.mkPresence({
user: userB, presence: "unavailable"
})
],
rooms: []
};
var eventData = {
start: "s_5_3",
end: "e_6_7",
chunk: [
// existing user change
utils.mkPresence({
user: userA, presence: "offline"
}),
// new user C
utils.mkPresence({
user: userC, presence: "online"
})
]
describe("resolving invites to profile info", function() {
var syncData = {
next_batch: "s_5_3",
presence: {
events: []
},
rooms: {
join: {
}
}
};
it("should create users for presence events from /initialSync and /events",
function(done) {
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
httpBackend.when("GET", "/events").respond(200, eventData);
client.startClient();
httpBackend.flush().done(function() {
expect(client.getUser(userA).presence).toEqual("offline");
expect(client.getUser(userB).presence).toEqual("unavailable");
expect(client.getUser(userC).presence).toEqual("online");
done();
});
});
});
describe("room state", function() {
var roomOne = "!foo:localhost";
var roomTwo = "!bar:localhost";
var msgText = "some text here";
var otherDisplayName = "Bob Smith";
var initialSync = {
end: "s_5_3",
presence: [],
rooms: [
{
membership: "join",
room_id: roomOne,
messages: {
start: "f_1_1",
end: "f_2_2",
chunk: [
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "hello"
})
]
},
state: [
utils.mkEvent({
type: "m.room.name", room: roomOne, user: otherUserId,
content: {
name: "Old room name"
}
}),
beforeEach(function() {
syncData.presence.events = [];
syncData.rooms.join[roomOne] = {
timeline: {
events: [
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "hello"
})
]
},
state: {
events: [
utils.mkMembership({
room: roomOne, mship: "join", user: otherUserId
}),
@@ -152,72 +105,271 @@ describe("MatrixClient syncing", function() {
}
})
]
},
{
membership: "join",
room_id: roomTwo,
messages: {
start: "f_1_1",
end: "f_2_2",
chunk: [
utils.mkMessage({
room: roomTwo, user: otherUserId, msg: "hiii"
})
]
},
state: [
utils.mkMembership({
room: roomTwo, mship: "join", user: otherUserId,
name: otherDisplayName
}),
utils.mkMembership({
room: roomTwo, mship: "join", user: selfUserId
}),
utils.mkEvent({
type: "m.room.create", room: roomTwo, user: selfUserId,
content: {
creator: selfUserId
}
})
]
}
]
};
var eventData = {
start: "s_5_3",
end: "e_6_7",
chunk: [
utils.mkEvent({
type: "m.room.name", room: roomOne, user: selfUserId,
content: { name: "A new room name" }
}),
utils.mkMessage({
room: roomTwo, user: otherUserId, msg: msgText
}),
utils.mkEvent({
type: "m.typing", room: roomTwo,
content: { user_ids: [otherUserId] }
};
});
it("should resolve incoming invites from /sync", function(done) {
syncData.rooms.join[roomOne].state.events.push(
utils.mkMembership({
room: roomOne, mship: "invite", user: userC
})
]
);
httpBackend.when("GET", "/sync").respond(200, syncData);
httpBackend.when("GET", "/profile/" + encodeURIComponent(userC)).respond(
200, {
avatar_url: "mxc://flibble/wibble",
displayname: "The Boss"
}
);
client.startClient({
resolveInvitesToProfiles: true
});
httpBackend.flush().done(function() {
var member = client.getRoom(roomOne).getMember(userC);
expect(member.name).toEqual("The Boss");
expect(
member.getAvatarUrl("home.server.url", null, null, null, false)
).toBeDefined();
done();
});
});
it("should use cached values from m.presence wherever possible", function(done) {
syncData.presence.events = [
utils.mkPresence({
user: userC, presence: "online", name: "The Ghost"
}),
];
syncData.rooms.join[roomOne].state.events.push(
utils.mkMembership({
room: roomOne, mship: "invite", user: userC
})
);
httpBackend.when("GET", "/sync").respond(200, syncData);
client.startClient({
resolveInvitesToProfiles: true
});
httpBackend.flush().done(function() {
var member = client.getRoom(roomOne).getMember(userC);
expect(member.name).toEqual("The Ghost");
done();
});
});
it("should result in events on the room member firing", function(done) {
syncData.presence.events = [
utils.mkPresence({
user: userC, presence: "online", name: "The Ghost"
})
];
syncData.rooms.join[roomOne].state.events.push(
utils.mkMembership({
room: roomOne, mship: "invite", user: userC
})
);
httpBackend.when("GET", "/sync").respond(200, syncData);
var latestFiredName = null;
client.on("RoomMember.name", function(event, m) {
if (m.userId === userC && m.roomId === roomOne) {
latestFiredName = m.name;
}
});
client.startClient({
resolveInvitesToProfiles: true
});
httpBackend.flush().done(function() {
expect(latestFiredName).toEqual("The Ghost");
done();
});
});
it("should no-op if resolveInvitesToProfiles is not set", function(done) {
syncData.rooms.join[roomOne].state.events.push(
utils.mkMembership({
room: roomOne, mship: "invite", user: userC
})
);
httpBackend.when("GET", "/sync").respond(200, syncData);
client.startClient();
httpBackend.flush().done(function() {
var member = client.getRoom(roomOne).getMember(userC);
expect(member.name).toEqual(userC);
expect(
member.getAvatarUrl("home.server.url", null, null, null, false)
).toBeNull();
done();
});
});
});
describe("users", function() {
var syncData = {
next_batch: "nb",
presence: {
events: [
utils.mkPresence({
user: userA, presence: "online"
}),
utils.mkPresence({
user: userB, presence: "unavailable"
})
]
}
};
it("should create users for presence events from /sync",
function(done) {
httpBackend.when("GET", "/sync").respond(200, syncData);
client.startClient();
httpBackend.flush().done(function() {
expect(client.getUser(userA).presence).toEqual("online");
expect(client.getUser(userB).presence).toEqual("unavailable");
done();
});
});
});
describe("room state", function() {
var msgText = "some text here";
var otherDisplayName = "Bob Smith";
var syncData = {
rooms: {
join: {
}
}
};
syncData.rooms.join[roomOne] = {
timeline: {
events: [
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "hello"
})
]
},
state: {
events: [
utils.mkEvent({
type: "m.room.name", room: roomOne, user: otherUserId,
content: {
name: "Old room name"
}
}),
utils.mkMembership({
room: roomOne, mship: "join", user: otherUserId
}),
utils.mkMembership({
room: roomOne, mship: "join", user: selfUserId
}),
utils.mkEvent({
type: "m.room.create", room: roomOne, user: selfUserId,
content: {
creator: selfUserId
}
})
]
}
};
syncData.rooms.join[roomTwo] = {
timeline: {
events: [
utils.mkMessage({
room: roomTwo, user: otherUserId, msg: "hiii"
})
]
},
state: {
events: [
utils.mkMembership({
room: roomTwo, mship: "join", user: otherUserId,
name: otherDisplayName
}),
utils.mkMembership({
room: roomTwo, mship: "join", user: selfUserId
}),
utils.mkEvent({
type: "m.room.create", room: roomTwo, user: selfUserId,
content: {
creator: selfUserId
}
})
]
}
};
var nextSyncData = {
rooms: {
join: {
}
}
};
nextSyncData.rooms.join[roomOne] = {
state: {
events: [
utils.mkEvent({
type: "m.room.name", room: roomOne, user: selfUserId,
content: { name: "A new room name" }
})
]
}
};
nextSyncData.rooms.join[roomTwo] = {
timeline: {
events: [
utils.mkMessage({
room: roomTwo, user: otherUserId, msg: msgText
})
]
},
ephemeral: {
events: [
utils.mkEvent({
type: "m.typing", room: roomTwo,
content: { user_ids: [otherUserId] }
})
]
}
};
it("should continually recalculate the right room name.", function(done) {
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
httpBackend.when("GET", "/events").respond(200, eventData);
httpBackend.when("GET", "/sync").respond(200, syncData);
httpBackend.when("GET", "/sync").respond(200, nextSyncData);
client.startClient();
httpBackend.flush().done(function() {
var room = client.getRoom(roomOne);
// should have clobbered the name to the one from /events
expect(room.name).toEqual(eventData.chunk[0].content.name);
expect(room.name).toEqual(
nextSyncData.rooms.join[roomOne].state.events[0].content.name
);
done();
});
});
it("should store the right events in the timeline.", function(done) {
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
httpBackend.when("GET", "/events").respond(200, eventData);
httpBackend.when("GET", "/sync").respond(200, syncData);
httpBackend.when("GET", "/sync").respond(200, nextSyncData);
client.startClient();
@@ -231,8 +383,8 @@ describe("MatrixClient syncing", function() {
});
it("should set the right room name.", function(done) {
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
httpBackend.when("GET", "/events").respond(200, eventData);
httpBackend.when("GET", "/sync").respond(200, syncData);
httpBackend.when("GET", "/sync").respond(200, nextSyncData);
client.startClient();
httpBackend.flush().done(function() {
@@ -244,8 +396,8 @@ describe("MatrixClient syncing", function() {
});
it("should set the right user's typing flag.", function(done) {
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
httpBackend.when("GET", "/events").respond(200, eventData);
httpBackend.when("GET", "/sync").respond(200, syncData);
httpBackend.when("GET", "/sync").respond(200, nextSyncData);
client.startClient();
@@ -270,6 +422,182 @@ 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: {
join: {
}
}
};
syncData.rooms.join[roomOne] = {
timeline: {
events: [
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "hello"
}),
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "world"
})
]
},
state: {
events: [
utils.mkEvent({
type: "m.room.name", room: roomOne, user: otherUserId,
content: {
name: "Old room name"
}
}),
utils.mkMembership({
room: roomOne, mship: "join", user: otherUserId
}),
utils.mkMembership({
room: roomOne, mship: "join", user: selfUserId
}),
utils.mkEvent({
type: "m.room.create", room: roomOne, user: selfUserId,
content: {
creator: selfUserId
}
})
]
}
};
beforeEach(function() {
syncData.rooms.join[roomOne].ephemeral = {
events: []
};
});
it("should sync receipts from /sync.", function(done) {
var ackEvent = syncData.rooms.join[roomOne].timeline.events[0];
var receipt = {};
receipt[ackEvent.event_id] = {
"m.read": {}
};
receipt[ackEvent.event_id]["m.read"][userC] = {
ts: 176592842636
};
syncData.rooms.join[roomOne].ephemeral.events = [{
content: receipt,
room_id: roomOne,
type: "m.receipt"
}];
httpBackend.when("GET", "/sync").respond(200, syncData);
client.startClient();
httpBackend.flush().done(function() {
var room = client.getRoom(roomOne);
expect(room.getReceiptsForEvent(new MatrixEvent(ackEvent))).toEqual([{
type: "m.read",
userId: userC,
data: {
ts: 176592842636
}
}]);
done();
});
});
});
describe("of a room", function() {
xit("should sync when a join event (which changes state) for the user" +
" arrives down the event stream (e.g. join from another device)", function() {
@@ -280,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);
});
});
});
+8
View File
@@ -13,6 +13,7 @@ function HttpBackend() {
this.requestFn = function(opts, callback) {
var realReq = new Request(opts.method, opts.uri, opts.body, opts.qs);
realReq.callback = callback;
console.log("HTTP backend received request: %s %s", opts.method, opts.uri);
self.requests.push(realReq);
};
}
@@ -27,6 +28,7 @@ HttpBackend.prototype = {
var defer = q.defer();
var self = this;
var flushed = 0;
var triedWaiting = false;
console.log(
"HTTP backend flushing... (path=%s numToFlush=%s)", path, numToFlush
);
@@ -48,6 +50,12 @@ HttpBackend.prototype = {
setTimeout(tryFlush, 0);
}
}
else if (flushed === 0 && !triedWaiting) {
// we may not have made the request yet, wait a generous amount of
// time before giving up.
setTimeout(tryFlush, 5);
triedWaiting = true;
}
else {
console.log(" no more flushes. [%s]", path);
defer.resolve();
+22 -2
View File
@@ -61,7 +61,7 @@ module.exports.mkEvent = function(opts) {
var event = {
type: opts.type,
room_id: opts.room,
user_id: opts.user,
sender: opts.user,
content: opts.content,
event_id: "$" + Math.random() + "-" + Math.random()
};
@@ -88,8 +88,8 @@ module.exports.mkPresence = function(opts) {
var event = {
event_id: "$" + Math.random() + "-" + Math.random(),
type: "m.presence",
sender: opts.user,
content: {
user_id: opts.user,
avatar_url: opts.url,
displayname: opts.name,
last_active_ago: opts.ago,
@@ -151,3 +151,23 @@ module.exports.mkMessage = function(opts) {
};
return module.exports.mkEvent(opts);
};
/**
* make the test fail, with the given exception
*
* <p>This is useful for use with integration tests which use asyncronous
* methods: it can be added as a 'catch' handler in a promise chain.
*
* @param {Error} error exception to be reported
*
* @example
* it("should not throw", function(done) {
* asynchronousMethod().then(function() {
* // some tests
* }).catch(utils.failTest).done(done);
* });
*/
module.exports.failTest = function(error) {
expect(error.stack).toBe(null);
};
+92
View File
@@ -0,0 +1,92 @@
"use strict";
var ContentRepo = require("../../lib/content-repo");
var testUtils = require("../test-utils");
describe("ContentRepo", function() {
var baseUrl = "https://my.home.server";
beforeEach(function() {
testUtils.beforeEach(this);
});
describe("getHttpUriForMxc", function() {
it("should do nothing to HTTP URLs when allowing direct links", function() {
var httpUrl = "http://example.com/image.jpeg";
expect(
ContentRepo.getHttpUriForMxc(
baseUrl, httpUrl, undefined, undefined, undefined, true
)
).toEqual(httpUrl);
});
it("should return the empty string HTTP URLs by default", function() {
var httpUrl = "http://example.com/image.jpeg";
expect(ContentRepo.getHttpUriForMxc(baseUrl, httpUrl)).toEqual("");
});
it("should return a download URL if no width/height/resize are specified",
function() {
var mxcUri = "mxc://server.name/resourceid";
expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
baseUrl + "/_matrix/media/v1/download/server.name/resourceid"
);
});
it("should return the empty string for null input", function() {
expect(ContentRepo.getHttpUriForMxc(null)).toEqual("");
});
it("should return a thumbnail URL if a width/height/resize is specified",
function() {
var mxcUri = "mxc://server.name/resourceid";
expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri, 32, 64, "crop")).toEqual(
baseUrl + "/_matrix/media/v1/thumbnail/server.name/resourceid" +
"?width=32&height=64&method=crop"
);
});
it("should put fragments from mxc:// URIs after any query parameters",
function() {
var mxcUri = "mxc://server.name/resourceid#automade";
expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri, 32)).toEqual(
baseUrl + "/_matrix/media/v1/thumbnail/server.name/resourceid" +
"?width=32#automade"
);
});
it("should put fragments from mxc:// URIs at the end of the HTTP URI",
function() {
var mxcUri = "mxc://server.name/resourceid#automade";
expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
baseUrl + "/_matrix/media/v1/download/server.name/resourceid#automade"
);
});
});
describe("getIdenticonUri", function() {
it("should do nothing for null input", function() {
expect(ContentRepo.getIdenticonUri(null)).toEqual(null);
});
it("should set w/h by default to 96", function() {
expect(ContentRepo.getIdenticonUri(baseUrl, "foobar")).toEqual(
baseUrl + "/_matrix/media/v1/identicon/foobar" +
"?width=96&height=96"
);
});
it("should be able to set custom w/h", function() {
expect(ContentRepo.getIdenticonUri(baseUrl, "foobar", 32, 64)).toEqual(
baseUrl + "/_matrix/media/v1/identicon/foobar" +
"?width=32&height=64"
);
});
it("should URL encode the identicon string", function() {
expect(ContentRepo.getIdenticonUri(baseUrl, "foo#bar", 32, 64)).toEqual(
baseUrl + "/_matrix/media/v1/identicon/foo%23bar" +
"?width=32&height=64"
);
});
});
});
+365
View File
@@ -0,0 +1,365 @@
"use strict";
var sdk = require("../..");
var EventTimeline = sdk.EventTimeline;
var utils = require("../test-utils");
function mockRoomStates(timeline) {
timeline._startState = utils.mock(sdk.RoomState, "startState");
timeline._endState = utils.mock(sdk.RoomState, "endState");
}
describe("EventTimeline", function() {
var roomId = "!foo:bar";
var userA = "@alice:bar";
var userB = "@bertha:bar";
var timeline;
beforeEach(function() {
utils.beforeEach(this);
timeline = new EventTimeline(roomId);
});
describe("construction", function() {
it("getRoomId should get room id", function() {
var v = timeline.getRoomId();
expect(v).toEqual(roomId);
});
});
describe("initialiseState", function() {
beforeEach(function() {
mockRoomStates(timeline);
});
it("should copy state events to start and end state", function() {
var events = [
utils.mkMembership({
room: roomId, mship: "invite", user: userB, skey: userA,
event: true,
}),
utils.mkEvent({
type: "m.room.name", room: roomId, user: userB,
event: true,
content: { name: "New room" },
})
];
timeline.initialiseState(events);
expect(timeline._startState.setStateEvents).toHaveBeenCalledWith(
events
);
expect(timeline._endState.setStateEvents).toHaveBeenCalledWith(
events
);
});
it("should raise an exception if called after events are added", function() {
var event =
utils.mkMessage({
room: roomId, user: userA, msg: "Adam stole the plushies",
event: true,
});
var state = [
utils.mkMembership({
room: roomId, mship: "invite", user: userB, skey: userA,
event: true,
})
];
expect(function() { timeline.initialiseState(state); }).not.toThrow();
timeline.addEvent(event, false);
expect(function() { timeline.initialiseState(state); }).toThrow();
});
});
describe("paginationTokens", function() {
it("pagination tokens should start null", function() {
expect(timeline.getPaginationToken(EventTimeline.BACKWARDS)).toBe(null);
expect(timeline.getPaginationToken(EventTimeline.FORWARDS)).toBe(null);
});
it("setPaginationToken should set token", function() {
timeline.setPaginationToken("back", EventTimeline.BACKWARDS);
timeline.setPaginationToken("fwd", EventTimeline.FORWARDS);
expect(timeline.getPaginationToken(EventTimeline.BACKWARDS)).toEqual("back");
expect(timeline.getPaginationToken(EventTimeline.FORWARDS)).toEqual("fwd");
});
});
describe("neighbouringTimelines", function() {
it("neighbouring timelines should start null", function() {
expect(timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS)).toBe(null);
expect(timeline.getNeighbouringTimeline(EventTimeline.FORWARDS)).toBe(null);
});
it("setNeighbouringTimeline should set neighbour", function() {
var prev = {a: "a"};
var next = {b: "b"};
timeline.setNeighbouringTimeline(prev, EventTimeline.BACKWARDS);
timeline.setNeighbouringTimeline(next, EventTimeline.FORWARDS);
expect(timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS)).toBe(prev);
expect(timeline.getNeighbouringTimeline(EventTimeline.FORWARDS)).toBe(next);
});
it("setNeighbouringTimeline should throw if called twice", function() {
var prev = {a: "a"};
var next = {b: "b"};
expect(function() {
timeline.setNeighbouringTimeline(prev, EventTimeline.BACKWARDS);
}).not.toThrow();
expect(timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS))
.toBe(prev);
expect(function() {
timeline.setNeighbouringTimeline(prev, EventTimeline.BACKWARDS);
}).toThrow();
expect(function() {
timeline.setNeighbouringTimeline(next, EventTimeline.FORWARDS);
}).not.toThrow();
expect(timeline.getNeighbouringTimeline(EventTimeline.FORWARDS))
.toBe(next);
expect(function() {
timeline.setNeighbouringTimeline(next, EventTimeline.FORWARDS);
}).toThrow();
});
});
describe("addEvent", function() {
beforeEach(function() {
mockRoomStates(timeline);
});
var events = [
utils.mkMessage({
room: roomId, user: userA, msg: "hungry hungry hungry",
event: true,
}),
utils.mkMessage({
room: roomId, user: userB, msg: "nom nom nom",
event: true,
}),
];
it("should be able to add events to the end", function() {
timeline.addEvent(events[0], false);
var initialIndex = timeline.getBaseIndex();
timeline.addEvent(events[1], false);
expect(timeline.getBaseIndex()).toEqual(initialIndex);
expect(timeline.getEvents().length).toEqual(2);
expect(timeline.getEvents()[0]).toEqual(events[0]);
expect(timeline.getEvents()[1]).toEqual(events[1]);
});
it("should be able to add events to the start", function() {
timeline.addEvent(events[0], true);
var initialIndex = timeline.getBaseIndex();
timeline.addEvent(events[1], true);
expect(timeline.getBaseIndex()).toEqual(initialIndex + 1);
expect(timeline.getEvents().length).toEqual(2);
expect(timeline.getEvents()[0]).toEqual(events[1]);
expect(timeline.getEvents()[1]).toEqual(events[0]);
});
it("should set event.sender for new and old events", function() {
var sentinel = {
userId: userA,
membership: "join",
name: "Alice"
};
var oldSentinel = {
userId: userA,
membership: "join",
name: "Old Alice"
};
timeline.getState(EventTimeline.FORWARDS).getSentinelMember
.andCallFake(function(uid) {
if (uid === userA) {
return sentinel;
}
return null;
});
timeline.getState(EventTimeline.BACKWARDS).getSentinelMember
.andCallFake(function(uid) {
if (uid === userA) {
return oldSentinel;
}
return null;
});
var newEv = utils.mkEvent({
type: "m.room.name", room: roomId, user: userA, event: true,
content: { name: "New Room Name" }
});
var oldEv = utils.mkEvent({
type: "m.room.name", room: roomId, user: userA, event: true,
content: { name: "Old Room Name" }
});
timeline.addEvent(newEv, false);
expect(newEv.sender).toEqual(sentinel);
timeline.addEvent(oldEv, true);
expect(oldEv.sender).toEqual(oldSentinel);
});
it("should set event.target for new and old m.room.member events",
function() {
var sentinel = {
userId: userA,
membership: "join",
name: "Alice"
};
var oldSentinel = {
userId: userA,
membership: "join",
name: "Old Alice"
};
timeline.getState(EventTimeline.FORWARDS).getSentinelMember
.andCallFake(function(uid) {
if (uid === userA) {
return sentinel;
}
return null;
});
timeline.getState(EventTimeline.BACKWARDS).getSentinelMember
.andCallFake(function(uid) {
if (uid === userA) {
return oldSentinel;
}
return null;
});
var newEv = utils.mkMembership({
room: roomId, mship: "invite", user: userB, skey: userA, event: true
});
var oldEv = utils.mkMembership({
room: roomId, mship: "ban", user: userB, skey: userA, event: true
});
timeline.addEvent(newEv, false);
expect(newEv.target).toEqual(sentinel);
timeline.addEvent(oldEv, true);
expect(oldEv.target).toEqual(oldSentinel);
});
it("should call setStateEvents on the right RoomState with the right " +
"forwardLooking value for new events", function() {
var events = [
utils.mkMembership({
room: roomId, mship: "invite", user: userB, skey: userA, event: true
}),
utils.mkEvent({
type: "m.room.name", room: roomId, user: userB, event: true,
content: {
name: "New room"
}
})
];
timeline.addEvent(events[0], false);
timeline.addEvent(events[1], false);
expect(timeline.getState(EventTimeline.FORWARDS).setStateEvents).
toHaveBeenCalledWith([events[0]]);
expect(timeline.getState(EventTimeline.FORWARDS).setStateEvents).
toHaveBeenCalledWith([events[1]]);
expect(events[0].forwardLooking).toBe(true);
expect(events[1].forwardLooking).toBe(true);
expect(timeline.getState(EventTimeline.BACKWARDS).setStateEvents).
not.toHaveBeenCalled();
});
it("should call setStateEvents on the right RoomState with the right " +
"forwardLooking value for old events", function() {
var events = [
utils.mkMembership({
room: roomId, mship: "invite", user: userB, skey: userA, event: true
}),
utils.mkEvent({
type: "m.room.name", room: roomId, user: userB, event: true,
content: {
name: "New room"
}
})
];
timeline.addEvent(events[0], true);
timeline.addEvent(events[1], true);
expect(timeline.getState(EventTimeline.BACKWARDS).setStateEvents).
toHaveBeenCalledWith([events[0]]);
expect(timeline.getState(EventTimeline.BACKWARDS).setStateEvents).
toHaveBeenCalledWith([events[1]]);
expect(events[0].forwardLooking).toBe(false);
expect(events[1].forwardLooking).toBe(false);
expect(timeline.getState(EventTimeline.FORWARDS).setStateEvents).
not.toHaveBeenCalled();
});
});
describe("removeEvent", function() {
var events = [
utils.mkMessage({
room: roomId, user: userA, msg: "hungry hungry hungry",
event: true,
}),
utils.mkMessage({
room: roomId, user: userB, msg: "nom nom nom",
event: true,
}),
utils.mkMessage({
room: roomId, user: userB, msg: "piiie",
event: true,
}),
];
it("should remove events", function() {
timeline.addEvent(events[0], false);
timeline.addEvent(events[1], false);
expect(timeline.getEvents().length).toEqual(2);
var ev = timeline.removeEvent(events[0].getId());
expect(ev).toBe(events[0]);
expect(timeline.getEvents().length).toEqual(1);
ev = timeline.removeEvent(events[1].getId());
expect(ev).toBe(events[1]);
expect(timeline.getEvents().length).toEqual(0);
});
it("should update baseIndex", function() {
timeline.addEvent(events[0], false);
timeline.addEvent(events[1], true);
timeline.addEvent(events[2], false);
expect(timeline.getEvents().length).toEqual(3);
expect(timeline.getBaseIndex()).toEqual(1);
timeline.removeEvent(events[2].getId());
expect(timeline.getEvents().length).toEqual(2);
expect(timeline.getBaseIndex()).toEqual(1);
timeline.removeEvent(events[1].getId());
expect(timeline.getEvents().length).toEqual(1);
expect(timeline.getBaseIndex()).toEqual(0);
});
// this is basically https://github.com/vector-im/vector-web/issues/937
// - removing the last event got baseIndex into such a state that
// further addEvent(ev, false) calls made the index increase.
it("should not make baseIndex assplode when removing the last event",
function() {
timeline.addEvent(events[0], true);
timeline.removeEvent(events[0].getId());
var initialIndex = timeline.getBaseIndex();
timeline.addEvent(events[1], false);
timeline.addEvent(events[2], false);
expect(timeline.getBaseIndex()).toEqual(initialIndex);
expect(timeline.getEvents().length).toEqual(2);
});
});
});
+50
View File
@@ -0,0 +1,50 @@
"use strict";
var sdk = require("../..");
var Filter = sdk.Filter;
var utils = require("../test-utils");
describe("Filter", function() {
var filterId = "f1lt3ring15g00d4ursoul";
var userId = "@sir_arthur_david:humming.tiger";
var filter;
beforeEach(function() {
utils.beforeEach(this);
filter = new Filter(userId);
});
describe("fromJson", function() {
it("create a new Filter from the provided values", function() {
var definition = {
event_fields: ["type", "content"]
};
var f = Filter.fromJson(userId, filterId, definition);
expect(f.getDefinition()).toEqual(definition);
expect(f.userId).toEqual(userId);
expect(f.filterId).toEqual(filterId);
});
});
describe("setTimelineLimit", function() {
it("should set room.timeline.limit of the filter definition", function() {
filter.setTimelineLimit(10);
expect(filter.getDefinition()).toEqual({
room: {
timeline: {
limit: 10
}
}
});
});
});
describe("setDefinition/getDefinition", function() {
it("should set and get the filter body", function() {
var definition = {
event_format: "client"
};
filter.setDefinition(definition);
expect(filter.getDefinition()).toEqual(definition);
});
});
});
+454
View File
@@ -0,0 +1,454 @@
"use strict";
var q = require("q");
var sdk = require("../..");
var MatrixClient = sdk.MatrixClient;
var utils = require("../test-utils");
describe("MatrixClient", function() {
var userId = "@alice:bar";
var identityServerUrl = "https://identity.server";
var identityServerDomain = "identity.server";
var client, store, scheduler;
var KEEP_ALIVE_PATH = "/_matrix/client/versions";
var PUSH_RULES_RESPONSE = {
method: "GET",
path: "/pushrules/",
data: {}
};
var FILTER_PATH = "/user/" + encodeURIComponent(userId) + "/filter";
var FILTER_RESPONSE = {
method: "POST",
path: FILTER_PATH,
data: { filter_id: "f1lt3r" }
};
var SYNC_DATA = {
next_batch: "s_5_3",
presence: { events: [] },
rooms: {}
};
var SYNC_RESPONSE = {
method: "GET",
path: "/sync",
data: SYNC_DATA
};
var httpLookups = [
// items are objects which look like:
// {
// method: "GET",
// path: "/initialSync",
// data: {},
// error: { errcode: M_FORBIDDEN } // if present will reject promise,
// expectBody: {} // additional expects on the body
// expectQueryParams: {} // additional expects on query params
// thenCall: function(){} // function to call *AFTER* returning response.
// }
// items are popped off when processed and block if no items left.
];
var pendingLookup = null;
function httpReq(cb, method, path, qp, data, prefix) {
if (path === KEEP_ALIVE_PATH) {
return q();
}
var next = httpLookups.shift();
var logLine = (
"MatrixClient[UT] RECV " + method + " " + path + " " +
"EXPECT " + (next ? next.method : next) + " " + (next ? next.path : next)
);
console.log(logLine);
if (!next) { // no more things to return
if (pendingLookup) {
if (pendingLookup.method === method && pendingLookup.path === path) {
return pendingLookup.promise;
}
// >1 pending thing, and they are different, whine.
expect(false).toBe(
true, ">1 pending request. You should probably handle them. " +
"PENDING: " + JSON.stringify(pendingLookup) + " JUST GOT: " +
method + " " + path
);
}
pendingLookup = {
promise: q.defer().promise,
method: method,
path: path
};
return pendingLookup.promise;
}
if (next.path === path && next.method === method) {
console.log(
"MatrixClient[UT] Matched. Returning " +
(next.error ? "BAD" : "GOOD") + " response"
);
if (next.expectBody) {
expect(next.expectBody).toEqual(data);
}
if (next.expectQueryParams) {
Object.keys(next.expectQueryParams).forEach(function(k) {
expect(qp[k]).toEqual(next.expectQueryParams[k]);
});
}
if (next.thenCall) {
process.nextTick(next.thenCall, 0); // next tick so we return first.
}
if (next.error) {
return q.reject({
errcode: next.error.errcode,
name: next.error.errcode,
message: "Expected testing error",
data: next.error
});
}
return q(next.data);
}
expect(true).toBe(false, "Expected different request. " + logLine);
return q.defer().promise;
}
beforeEach(function() {
utils.beforeEach(this);
jasmine.Clock.useMock();
scheduler = jasmine.createSpyObj("scheduler", [
"getQueueForEvent", "queueEvent", "removeEventFromQueue",
"setProcessFunction"
]);
store = jasmine.createSpyObj("store", [
"getRoom", "getRooms", "getUser", "getSyncToken", "scrollback",
"setSyncToken", "storeEvents", "storeRoom", "storeUser",
"getFilterIdByName", "setFilterIdByName", "getFilter", "storeFilter"
]);
client = new MatrixClient({
baseUrl: "https://my.home.server",
idBaseUrl: identityServerUrl,
accessToken: "my.access.token",
request: function() {}, // NOP
store: store,
scheduler: scheduler,
userId: userId
});
// FIXME: We shouldn't be yanking _http like this.
client._http = jasmine.createSpyObj("httpApi", [
"authedRequest", "authedRequestWithPrefix", "getContentUri",
"request", "requestWithPrefix", "uploadContent"
]);
client._http.authedRequest.andCallFake(httpReq);
client._http.authedRequestWithPrefix.andCallFake(httpReq);
client._http.requestWithPrefix.andCallFake(httpReq);
// set reasonable working defaults
pendingLookup = null;
httpLookups = [];
httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push(FILTER_RESPONSE);
httpLookups.push(SYNC_RESPONSE);
});
afterEach(function() {
// need to re-stub the requests with NOPs because there are no guarantees
// clients from previous tests will be GC'd before the next test. This
// means they may call /events and then fail an expect() which will fail
// a DIFFERENT test (pollution between tests!) - we return unresolved
// promises to stop the client from continuing to run.
client._http.authedRequest.andCallFake(function() {
return q.defer().promise;
});
client._http.authedRequestWithPrefix.andCallFake(function() {
return q.defer().promise;
});
});
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) {
if (state === "SYNCING") {
expect(httpLookups.length).toEqual(0);
client.removeListener("sync", syncListener);
done();
}
});
});
describe("getSyncState", function() {
it("should return null if the client isn't started", function() {
expect(client.getSyncState()).toBeNull();
});
it("should return the same sync state as emitted sync events", function(done) {
client.on("sync", function syncListener(state) {
expect(state).toEqual(client.getSyncState());
if (state === "SYNCING") {
client.removeListener("sync", syncListener);
done();
}
});
client.startClient();
});
});
describe("retryImmediately", function() {
it("should return false if there is no request waiting", function() {
client.startClient();
expect(client.retryImmediately()).toBe(false);
});
it("should work on /filter", function(done) {
httpLookups = [];
httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push({
method: "POST", path: FILTER_PATH, error: { errcode: "NOPE_NOPE_NOPE" }
});
httpLookups.push(FILTER_RESPONSE);
httpLookups.push(SYNC_RESPONSE);
client.on("sync", function syncListener(state) {
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();
} else {
// unexpected state transition!
expect(state).toEqual(null);
}
});
client.startClient();
});
it("should work on /sync", function(done) {
httpLookups.push({
method: "GET", path: "/sync", error: { errcode: "NOPE_NOPE_NOPE" }
});
httpLookups.push({
method: "GET", path: "/sync", data: SYNC_DATA
});
client.on("sync", function syncListener(state) {
if (state === "ERROR" && httpLookups.length > 0) {
expect(httpLookups.length).toEqual(1);
expect(client.retryImmediately()).toBe(
true, "retryImmediately returned false"
);
jasmine.Clock.tick(1);
} else if (state === "SYNCING" && httpLookups.length === 0) {
client.removeListener("sync", syncListener);
done();
}
});
client.startClient();
});
it("should work on /pushrules", function(done) {
httpLookups = [];
httpLookups.push({
method: "GET", path: "/pushrules/", error: { errcode: "NOPE_NOPE_NOPE" }
});
httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push(FILTER_RESPONSE);
httpLookups.push(SYNC_RESPONSE);
client.on("sync", function syncListener(state) {
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();
} else {
// unexpected state transition!
expect(state).toEqual(null);
}
});
client.startClient();
});
});
describe("emitted sync events", function() {
function syncChecker(expectedStates, done) {
return function syncListener(state, old) {
var expected = expectedStates.shift();
console.log(
"'sync' curr=%s old=%s EXPECT=%s", state, old, expected
);
if (!expected) {
done();
return;
}
expect(state).toEqual(expected[0]);
expect(old).toEqual(expected[1]);
if (expectedStates.length === 0) {
client.removeListener("sync", syncListener);
done();
}
// standard retry time is 5 to 10 seconds
jasmine.Clock.tick(10000);
};
}
it("should transition null -> PREPARED after the first /sync", function(done) {
var expectedStates = [];
expectedStates.push(["PREPARED", null]);
client.on("sync", syncChecker(expectedStates, done));
client.startClient();
});
it("should transition null -> ERROR after a failed /filter", function(done) {
var expectedStates = [];
httpLookups = [];
httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push({
method: "POST", path: FILTER_PATH, error: { errcode: "NOPE_NOPE_NOPE" }
});
expectedStates.push(["ERROR", null]);
client.on("sync", syncChecker(expectedStates, done));
client.startClient();
});
it("should transition ERROR -> PREPARED after /sync if prev failed",
function(done) {
var expectedStates = [];
httpLookups = [];
httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push(FILTER_RESPONSE);
httpLookups.push({
method: "GET", path: "/sync", error: { errcode: "NOPE_NOPE_NOPE" }
});
httpLookups.push({
method: "GET", path: "/sync", data: SYNC_DATA
});
expectedStates.push(["ERROR", null]);
expectedStates.push(["PREPARED", "ERROR"]);
client.on("sync", syncChecker(expectedStates, done));
client.startClient();
});
it("should transition PREPARED -> SYNCING after /sync", function(done) {
var expectedStates = [];
expectedStates.push(["PREPARED", null]);
expectedStates.push(["SYNCING", "PREPARED"]);
client.on("sync", syncChecker(expectedStates, done));
client.startClient();
});
it("should transition SYNCING -> ERROR after a failed /sync", function(done) {
var expectedStates = [];
httpLookups.push({
method: "GET", path: "/sync", error: { errcode: "NONONONONO" }
});
expectedStates.push(["PREPARED", null]);
expectedStates.push(["SYNCING", "PREPARED"]);
expectedStates.push(["ERROR", "SYNCING"]);
client.on("sync", syncChecker(expectedStates, done));
client.startClient();
});
xit("should transition ERROR -> SYNCING after /sync if prev failed",
function(done) {
var expectedStates = [];
httpLookups.push({
method: "GET", path: "/sync", error: { errcode: "NONONONONO" }
});
httpLookups.push(SYNC_RESPONSE);
expectedStates.push(["PREPARED", null]);
expectedStates.push(["SYNCING", "PREPARED"]);
expectedStates.push(["ERROR", "SYNCING"]);
client.on("sync", syncChecker(expectedStates, done));
client.startClient();
});
it("should transition SYNCING -> SYNCING on subsequent /sync successes",
function(done) {
var expectedStates = [];
httpLookups.push(SYNC_RESPONSE);
httpLookups.push(SYNC_RESPONSE);
expectedStates.push(["PREPARED", null]);
expectedStates.push(["SYNCING", "PREPARED"]);
expectedStates.push(["SYNCING", "SYNCING"]);
client.on("sync", syncChecker(expectedStates, done));
client.startClient();
});
it("should transition ERROR -> ERROR if multiple /sync fails", function(done) {
var expectedStates = [];
httpLookups.push({
method: "GET", path: "/sync", error: { errcode: "NONONONONO" }
});
httpLookups.push({
method: "GET", path: "/sync", error: { errcode: "NONONONONO" }
});
expectedStates.push(["PREPARED", null]);
expectedStates.push(["SYNCING", "PREPARED"]);
expectedStates.push(["ERROR", "SYNCING"]);
expectedStates.push(["ERROR", "ERROR"]);
client.on("sync", syncChecker(expectedStates, done));
client.startClient();
});
});
describe("inviteByEmail", function() {
var roomId = "!foo:bar";
it("should send an invite HTTP POST", function() {
httpLookups = [{
method: "POST",
path: "/rooms/!foo%3Abar/invite",
data: {},
expectBody: {
id_server: identityServerDomain,
medium: "email",
address: "alice@gmail.com"
}
}];
client.inviteByEmail(roomId, "alice@gmail.com");
expect(httpLookups.length).toEqual(0);
});
});
describe("guest rooms", function() {
it("should only do /sync calls (without filter/pushrules)", function(done) {
httpLookups = []; // no /pushrules or /filter
httpLookups.push({
method: "GET",
path: "/sync",
data: SYNC_DATA,
thenCall: function() {
done();
}
});
client.setGuest(true);
client.startClient();
});
xit("should be able to peek into a room using peekInRoom", function(done) {
});
});
});
+37
View File
@@ -15,6 +15,40 @@ describe("RoomMember", function() {
member = new RoomMember(roomId, userA);
});
describe("getAvatarUrl", function() {
var hsUrl = "https://my.home.server";
it("should return the URL from m.room.member preferentially", function() {
member.events.member = utils.mkEvent({
event: true,
type: "m.room.member",
skey: userA,
room: roomId,
user: userA,
content: {
membership: "join",
avatar_url: "mxc://flibble/wibble"
}
});
var url = member.getAvatarUrl(hsUrl);
// we don't care about how the mxc->http conversion is done, other
// than it contains the mxc body.
expect(url.indexOf("flibble/wibble")).not.toEqual(-1);
});
it("should return an identicon HTTP URL if allowDefault was set and there " +
"was no m.room.member event", function() {
var url = member.getAvatarUrl(hsUrl, 64, 64, "crop", true);
expect(url.indexOf("http")).toEqual(0); // don't care about form
});
it("should return nothing if there is no m.room.member and allowDefault=false",
function() {
var url = member.getAvatarUrl(hsUrl, 64, 64, "crop", false);
expect(url).toEqual(null);
});
});
describe("setPowerLevelEvent", function() {
it("should set 'powerLevel' and 'powerLevelNorm'.", function() {
var event = utils.mkEvent({
@@ -167,6 +201,9 @@ describe("RoomMember", function() {
}),
joinEvent
];
},
getUserIdsWithDisplayName: function(displayName) {
return [userA, userC];
}
};
expect(member.name).toEqual(userA); // default = user_id
+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);
});
});
});
+831 -170
View File
File diff suppressed because it is too large Load Diff
+422
View File
@@ -0,0 +1,422 @@
"use strict";
var q = require("q");
var sdk = require("../..");
var EventTimeline = sdk.EventTimeline;
var TimelineWindow = sdk.TimelineWindow;
var TimelineIndex = require("../../lib/timeline-window").TimelineIndex;
var utils = require("../test-utils");
var ROOM_ID = "roomId";
var USER_ID = "userId";
/*
* create a timeline with a bunch (default 3) events.
* baseIndex is 1 by default.
*/
function createTimeline(numEvents, baseIndex) {
if (numEvents === undefined) { numEvents = 3; }
if (baseIndex === undefined) { baseIndex = 1; }
var timeline = new EventTimeline(ROOM_ID);
// add the events after the baseIndex first
addEventsToTimeline(timeline, numEvents - baseIndex, false);
// then add those before the baseIndex
addEventsToTimeline(timeline, baseIndex, true);
expect(timeline.getBaseIndex()).toEqual(baseIndex);
return timeline;
}
function addEventsToTimeline(timeline, numEvents, atStart) {
for (var i = 0; i < numEvents; i++) {
timeline.addEvent(
utils.mkMessage({
room: ROOM_ID, user: USER_ID,
event: true,
}), atStart
);
}
}
/*
* create a pair of linked timelines
*/
function createLinkedTimelines() {
var tl1 = createTimeline();
var tl2 = createTimeline();
tl1.setNeighbouringTimeline(tl2, EventTimeline.FORWARDS);
tl2.setNeighbouringTimeline(tl1, EventTimeline.BACKWARDS);
return [tl1, tl2];
}
describe("TimelineIndex", function() {
beforeEach(function() {
utils.beforeEach(this);
});
describe("minIndex", function() {
it("should return the min index relative to BaseIndex", function() {
var timelineIndex = new TimelineIndex(createTimeline(), 0);
expect(timelineIndex.minIndex()).toEqual(-1);
});
});
describe("maxIndex", function() {
it("should return the max index relative to BaseIndex", function() {
var timelineIndex = new TimelineIndex(createTimeline(), 0);
expect(timelineIndex.maxIndex()).toEqual(2);
});
});
describe("advance", function() {
it("should advance up to the end of the timeline", function() {
var timelineIndex = new TimelineIndex(createTimeline(), 0);
var result = timelineIndex.advance(3);
expect(result).toEqual(2);
expect(timelineIndex.index).toEqual(2);
});
it("should retreat back to the start of the timeline", function() {
var timelineIndex = new TimelineIndex(createTimeline(), 0);
var result = timelineIndex.advance(-2);
expect(result).toEqual(-1);
expect(timelineIndex.index).toEqual(-1);
});
it("should advance into the next timeline", function() {
var timelines = createLinkedTimelines();
var tl1 = timelines[0], tl2 = timelines[1];
// initialise the index pointing at the end of the first timeline
var timelineIndex = new TimelineIndex(tl1, 2);
var result = timelineIndex.advance(1);
expect(result).toEqual(1);
expect(timelineIndex.timeline).toBe(tl2);
// we expect the index to be the zero (ie, the same as the
// BaseIndex), because the BaseIndex points at the second event,
// and we've advanced past the first.
expect(timelineIndex.index).toEqual(0);
});
it("should retreat into the previous timeline", function() {
var timelines = createLinkedTimelines();
var tl1 = timelines[0], tl2 = timelines[1];
// initialise the index pointing at the start of the second
// timeline
var timelineIndex = new TimelineIndex(tl2, -1);
var result = timelineIndex.advance(-1);
expect(result).toEqual(-1);
expect(timelineIndex.timeline).toBe(tl1);
expect(timelineIndex.index).toEqual(1);
});
});
describe("retreat", function() {
it("should retreat up to the start of the timeline", function() {
var timelineIndex = new TimelineIndex(createTimeline(), 0);
var result = timelineIndex.retreat(2);
expect(result).toEqual(1);
expect(timelineIndex.index).toEqual(-1);
});
});
});
describe("TimelineWindow", function() {
/**
* create a dummy room and client, and a TimelineWindow
* attached to them.
*/
var room, client;
function createWindow(timeline, opts) {
room = {};
client = {};
client.getEventTimeline = function(room0, eventId0) {
expect(room0).toBe(room);
return q(timeline);
};
return new TimelineWindow(client, room, opts);
}
beforeEach(function() {
utils.beforeEach(this);
});
describe("load", function() {
it("should initialise from the live timeline", function(done) {
var liveTimeline = createTimeline();
var room = {};
room.getLiveTimeline = function() { return liveTimeline; };
var timelineWindow = new TimelineWindow(undefined, room);
timelineWindow.load(undefined, 2).then(function() {
var expectedEvents = liveTimeline.getEvents().slice(1);
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
}).catch(utils.failTest).done(done);
});
it("should initialise from a specific event", function(done) {
var timeline = createTimeline();
var eventId = timeline.getEvents()[1].getId();
var room = {};
var client = {};
client.getEventTimeline = function(room0, eventId0) {
expect(room0).toBe(room);
expect(eventId0).toEqual(eventId);
return q(timeline);
};
var timelineWindow = new TimelineWindow(client, room);
timelineWindow.load(eventId, 3).then(function() {
var expectedEvents = timeline.getEvents();
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
}).catch(utils.failTest).done(done);
});
it("canPaginate should return false until load has returned",
function(done) {
var timeline = createTimeline();
timeline.setPaginationToken("toktok1", EventTimeline.BACKWARDS);
timeline.setPaginationToken("toktok2", EventTimeline.FORWARDS);
var eventId = timeline.getEvents()[1].getId();
var room = {};
var client = {};
var timelineWindow = new TimelineWindow(client, room);
client.getEventTimeline = function(room0, eventId0) {
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
.toBe(false);
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
.toBe(false);
return q(timeline);
};
timelineWindow.load(eventId, 3).then(function() {
var expectedEvents = timeline.getEvents();
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
.toBe(true);
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
.toBe(true);
}).catch(utils.failTest).done(done);
});
});
describe("pagination", function() {
it("should be able to advance across the initial timeline",
function(done) {
var timeline = createTimeline();
var eventId = timeline.getEvents()[1].getId();
var timelineWindow = createWindow(timeline);
timelineWindow.load(eventId, 1).then(function() {
var expectedEvents = [timeline.getEvents()[1]];
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
.toBe(true);
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
.toBe(true);
return timelineWindow.paginate(EventTimeline.FORWARDS, 2);
}).then(function(success) {
expect(success).toBe(true);
var expectedEvents = timeline.getEvents().slice(1);
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
.toBe(true);
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
.toBe(false);
return timelineWindow.paginate(EventTimeline.FORWARDS, 2);
}).then(function(success) {
expect(success).toBe(false);
return timelineWindow.paginate(EventTimeline.BACKWARDS, 2);
}).then(function(success) {
expect(success).toBe(true);
var expectedEvents = timeline.getEvents();
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
.toBe(false);
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
.toBe(false);
return timelineWindow.paginate(EventTimeline.BACKWARDS, 2);
}).then(function(success) {
expect(success).toBe(false);
}).catch(utils.failTest).done(done);
});
it("should advance into next timeline", function(done) {
var tls = createLinkedTimelines();
var eventId = tls[0].getEvents()[1].getId();
var timelineWindow = createWindow(tls[0], {windowLimit: 5});
timelineWindow.load(eventId, 3).then(function() {
var expectedEvents = tls[0].getEvents();
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
.toBe(false);
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
.toBe(true);
return timelineWindow.paginate(EventTimeline.FORWARDS, 2);
}).then(function(success) {
expect(success).toBe(true);
var expectedEvents = tls[0].getEvents()
.concat(tls[1].getEvents().slice(0, 2));
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
.toBe(false);
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
.toBe(true);
return timelineWindow.paginate(EventTimeline.FORWARDS, 2);
}).then(function(success) {
expect(success).toBe(true);
// the windowLimit should have made us drop an event from
// tls[0]
var expectedEvents = tls[0].getEvents().slice(1)
.concat(tls[1].getEvents());
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
.toBe(true);
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
.toBe(false);
return timelineWindow.paginate(EventTimeline.FORWARDS, 2);
}).then(function(success) {
expect(success).toBe(false);
}).catch(utils.failTest).done(done);
});
it("should retreat into previous timeline", function(done) {
var tls = createLinkedTimelines();
var eventId = tls[1].getEvents()[1].getId();
var timelineWindow = createWindow(tls[1], {windowLimit: 5});
timelineWindow.load(eventId, 3).then(function() {
var expectedEvents = tls[1].getEvents();
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
.toBe(true);
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
.toBe(false);
return timelineWindow.paginate(EventTimeline.BACKWARDS, 2);
}).then(function(success) {
expect(success).toBe(true);
var expectedEvents = tls[0].getEvents().slice(1, 3)
.concat(tls[1].getEvents());
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
.toBe(true);
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
.toBe(false);
return timelineWindow.paginate(EventTimeline.BACKWARDS, 2);
}).then(function(success) {
expect(success).toBe(true);
// the windowLimit should have made us drop an event from
// tls[1]
var expectedEvents = tls[0].getEvents()
.concat(tls[1].getEvents().slice(0, 2));
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
.toBe(false);
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
.toBe(true);
return timelineWindow.paginate(EventTimeline.BACKWARDS, 2);
}).then(function(success) {
expect(success).toBe(false);
}).catch(utils.failTest).done(done);
});
it("should make forward pagination requests", function(done) {
var timeline = createTimeline();
timeline.setPaginationToken("toktok", EventTimeline.FORWARDS);
var timelineWindow = createWindow(timeline, {windowLimit: 5});
var eventId = timeline.getEvents()[1].getId();
client.paginateEventTimeline = function(timeline0, opts) {
expect(timeline0).toBe(timeline);
expect(opts.backwards).toBe(false);
expect(opts.limit).toEqual(2);
addEventsToTimeline(timeline, 3, false);
return q(true);
};
timelineWindow.load(eventId, 3).then(function() {
var expectedEvents = timeline.getEvents();
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
.toBe(false);
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
.toBe(true);
return timelineWindow.paginate(EventTimeline.FORWARDS, 2);
}).then(function(success) {
expect(success).toBe(true);
var expectedEvents = timeline.getEvents().slice(0, 5);
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
}).catch(utils.failTest).done(done);
});
it("should make backward pagination requests", function(done) {
var timeline = createTimeline();
timeline.setPaginationToken("toktok", EventTimeline.BACKWARDS);
var timelineWindow = createWindow(timeline, {windowLimit: 5});
var eventId = timeline.getEvents()[1].getId();
client.paginateEventTimeline = function(timeline0, opts) {
expect(timeline0).toBe(timeline);
expect(opts.backwards).toBe(true);
expect(opts.limit).toEqual(2);
addEventsToTimeline(timeline, 3, true);
return q(true);
};
timelineWindow.load(eventId, 3).then(function() {
var expectedEvents = timeline.getEvents();
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
.toBe(true);
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
.toBe(false);
return timelineWindow.paginate(EventTimeline.BACKWARDS, 2);
}).then(function(success) {
expect(success).toBe(true);
var expectedEvents = timeline.getEvents().slice(1, 6);
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
}).catch(utils.failTest).done(done);
});
});
});