Compare commits
488 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ad5d07caf8 | |||
| b2d7abc0a1 | |||
| cc475e6392 | |||
| e4c6717bd5 | |||
| 53f813207e | |||
| 873fde27ac | |||
| 8d9d638953 | |||
| 2f93490054 | |||
| e22efc9dd5 | |||
| e7ac80cf2b | |||
| 4436087777 | |||
| f7bc11361c | |||
| a68b61dafe | |||
| 84c9876b3a | |||
| de864c489a | |||
| 2c277f7d96 | |||
| d0560f594d | |||
| 60b6310494 | |||
| abd27f9b75 | |||
| 3d316959f9 | |||
| 8aa3b79501 | |||
| f35409700a | |||
| b009739b9e | |||
| f007af741e | |||
| 3db4d9488b | |||
| 6b0fa84697 | |||
| 98b0cf2560 | |||
| 372759b6e4 | |||
| ec29b4ffeb | |||
| 95494933fd | |||
| 6fff29c07b | |||
| 6f7ed93b87 | |||
| 8e903c0531 | |||
| b90984a7f6 | |||
| 57006b7366 | |||
| db9ba52873 | |||
| 08b49c733a | |||
| 0f38764709 | |||
| 6040b50ceb | |||
| b88a207bde | |||
| 07bbe358ea | |||
| 39a5765888 | |||
| 9f91995f4e | |||
| 85f2754300 | |||
| 5833654aa6 | |||
| 899ff6cea2 | |||
| 3752429b65 | |||
| d13fbd0e3e | |||
| 5e18c84e53 | |||
| fc1d5c86f9 | |||
| c3ea913ae8 | |||
| f2336aaedf | |||
| 16ab2fd82a | |||
| 295fda027c | |||
| 28e6f00766 | |||
| b1988de753 | |||
| 0302eefd3c | |||
| 8ad5605e49 | |||
| 4b5539fe53 | |||
| b0becbc8d5 | |||
| 4ae353d3d3 | |||
| b4b8b4bfb8 | |||
| 909a8a0648 | |||
| 234c227fd5 | |||
| 78eded3bbd | |||
| f324e4c72f | |||
| 9328a12ccb | |||
| 51380f8116 | |||
| cfd96969fc | |||
| 0034bdf4ad | |||
| 00af1ce7d2 | |||
| 76f84c54db | |||
| bb4766c8c6 | |||
| e287e7591b | |||
| 48f7aca121 | |||
| a14f9e6d1c | |||
| 5fefcd8ce3 | |||
| 76f1d24c7b | |||
| 066dd77aba | |||
| 45a3bf63b2 | |||
| 75f2efffac | |||
| 0584ec3319 | |||
| 38e81ba61a | |||
| 164e4814af | |||
| abf908b14f | |||
| 1deb2e27d8 | |||
| 848ffe8a40 | |||
| 79c10c1b68 | |||
| 7e6eb89524 | |||
| 3d3e52b104 | |||
| 05326984df | |||
| e97e3c673f | |||
| f0ae46afc9 | |||
| aaf4371fae | |||
| 7728009ef3 | |||
| 46912431cc | |||
| 6a19e08381 | |||
| 43f392955d | |||
| 41d2076bd4 | |||
| 670d230f2e | |||
| 7970f3f5a5 | |||
| 567716c4f7 | |||
| 518e41c078 | |||
| bd600f65fb | |||
| 363b08c4d8 | |||
| 2150bdc444 | |||
| 5886b3358d | |||
| 8b887d8559 | |||
| 7278c38fa6 | |||
| 24ae4a8d1a | |||
| 923b9cad39 | |||
| e9f6e41550 | |||
| 2950417f70 | |||
| 39f641a851 | |||
| 95fff38dbb | |||
| 785326376a | |||
| 1baf14861c | |||
| 8e47fe2968 | |||
| 88827fab84 | |||
| ab0a06eea7 | |||
| 6a6db36088 | |||
| 6f3bdcfbb6 | |||
| 4c6d0a5128 | |||
| 5eff278454 | |||
| c8d1e210a3 | |||
| 8614632e54 | |||
| c3796c61cd | |||
| 6224aff882 | |||
| e506a1b2de | |||
| b40a6d1481 | |||
| 5c8f73019e | |||
| 3cfc4f8ba5 | |||
| 4d46251b15 | |||
| 977e33f1bd | |||
| daa0e6291e | |||
| 620417ed1b | |||
| 02196416e4 | |||
| 5ca4bc6b85 | |||
| 76c79ec299 | |||
| fc730d4637 | |||
| 41d5917bb6 | |||
| 122867e8ee | |||
| f3e5e03009 | |||
| 1b43e5ed98 | |||
| 9e65f12ddd | |||
| be297ddbcd | |||
| 8ee1d17ff7 | |||
| a2185fefc1 | |||
| f172272dd3 | |||
| 8a77b29d17 | |||
| 0a7efe3e8b | |||
| 2af947fc1a | |||
| 1499087098 | |||
| 672e96b90e | |||
| 3d5e2937e2 | |||
| bbf3b2637a | |||
| 04a1c4f1a2 | |||
| 5297855ad3 | |||
| a221674680 | |||
| 8db95f42fb | |||
| b42e5d5fcf | |||
| b1e2090eef | |||
| 8716185f4a | |||
| 3c2fad7c8d | |||
| 60a243f160 | |||
| a87cefa035 | |||
| 101d3952d3 | |||
| a01501b42c | |||
| d37369b463 | |||
| 0c3abcccf2 | |||
| 48d1bc3158 | |||
| 15e8784daf | |||
| 840b8f0bc0 | |||
| 7a4cc62280 | |||
| f8ec35691f | |||
| c5e7df8975 | |||
| 7bdab05785 | |||
| 197144dcda | |||
| ff990914b2 | |||
| 4bc2869522 | |||
| 1f1d743678 | |||
| 787c0ebabc | |||
| d559ad794a | |||
| 24655ac60e | |||
| 5fd0ea2f6f | |||
| eaf7b03bb1 | |||
| d375804cd6 | |||
| a24a9d35c4 | |||
| a0d81fccdb | |||
| b4e4aaff00 | |||
| 3a73b54e4a | |||
| 5ec0fce2a4 | |||
| 8b7497374f | |||
| 8cb180525e | |||
| 6df9d08dc1 | |||
| b3c06dd723 | |||
| 2a88b8db4e | |||
| 31c29b7e5e | |||
| 865db906e3 | |||
| 0b52a7e7c9 | |||
| 80f7220a7b | |||
| 2e664adb32 | |||
| 14ef9348be | |||
| 3c170bf063 | |||
| 3e67406a30 | |||
| d6075bb5bd | |||
| d8e56dad1b | |||
| 67872206ff | |||
| 43e7173c30 | |||
| e0ddd65922 | |||
| dfb2fa821d | |||
| fce7248ed5 | |||
| e68ab7d54a | |||
| 706966ffe9 | |||
| e3b4cb03e1 | |||
| 8011aab561 | |||
| a8d24798e6 | |||
| e4c38ac78c | |||
| 5fa6f0037f | |||
| a0df2a70cd | |||
| 0bab00c47c | |||
| f48fb34818 | |||
| 8810ff2256 | |||
| 17efc5163f | |||
| 3ce07a020d | |||
| 71abef0117 | |||
| a79270b8f8 | |||
| 87db054e22 | |||
| ae06dd2ab8 | |||
| 88c7293838 | |||
| 051e83582b | |||
| 57072bc4f4 | |||
| e8f77256de | |||
| 51fe73bc27 | |||
| 97003f7382 | |||
| c10218a1fa | |||
| 5c59a2ea3e | |||
| 0b79ac1386 | |||
| 4fe95f18b9 | |||
| db5ca49ee2 | |||
| d7158b575f | |||
| 678d70528e | |||
| 02b33766ee | |||
| 9bd45cf7c7 | |||
| c64aebdb17 | |||
| 387ad09c5f | |||
| ea3bd1450e | |||
| 8f4bd9c693 | |||
| cdb4bc5107 | |||
| 446faed9b5 | |||
| d36c928d95 | |||
| 3a3f25c1bc | |||
| 73e65bc18b | |||
| 445491c4ad | |||
| f12499c6bf | |||
| 8c6c65ab6c | |||
| b85e267fdb | |||
| c669d21af7 | |||
| 3e4cef89fd | |||
| a06d1f62d7 | |||
| 2802092231 | |||
| a419e241a6 | |||
| 7aa4bd7f46 | |||
| 2eec76bc1d | |||
| 13fcff9688 | |||
| 1174147d64 | |||
| de53b292a2 | |||
| b50d61428c | |||
| 4bc5343b67 | |||
| da560ffeff | |||
| 59965b1c59 | |||
| 431f4a4797 | |||
| 40c3fe558c | |||
| a12cd8d4a0 | |||
| ac7a469582 | |||
| 2e9376614f | |||
| cd9d1daf17 | |||
| bfa8dd0007 | |||
| 7e35ef258f | |||
| 8bd43a8d53 | |||
| 8b87c0045d | |||
| 77356f0007 | |||
| 4cd6f615b3 | |||
| 65ef1dfd75 | |||
| 5719d513b7 | |||
| b90697264c | |||
| 406a2bb001 | |||
| 3115043b94 | |||
| bfda04daea | |||
| 46504b8b9f | |||
| f48c9175e5 | |||
| bd4d8433ab | |||
| a00e318d73 | |||
| fcf1abb185 | |||
| 13cab79e04 | |||
| fc6ce20e14 | |||
| 9c49d26525 | |||
| d6299b634c | |||
| a6f64b5f03 | |||
| 465635444f | |||
| eedff29acb | |||
| 7c43d15ea5 | |||
| de32ac0c44 | |||
| 3d9d31d6b1 | |||
| b219836b3e | |||
| 26d9fed537 | |||
| d6ba39f292 | |||
| 8576ebce8f | |||
| f08152a1d8 | |||
| 6af2197183 | |||
| 4c7e6807d2 | |||
| 11f0513c62 | |||
| 3d57b4ce6a | |||
| 243bdd78f4 | |||
| b622960b32 | |||
| 06f927aa22 | |||
| f7ffed4b98 | |||
| 0576e4ca0c | |||
| 529fb23555 | |||
| 3543abf7bd | |||
| a5847485b9 | |||
| 2b659656cc | |||
| ac3aa5538f | |||
| c65f32f6a6 | |||
| 86a162c818 | |||
| 1987726a95 | |||
| d2537cd00c | |||
| 61db191835 | |||
| b7ac6a2e33 | |||
| c0178c3e80 | |||
| e58fb29722 | |||
| a1300ec095 | |||
| 73e0216f78 | |||
| d16dfdaee3 | |||
| 02a605f368 | |||
| 71d5756223 | |||
| 2866743ce6 | |||
| e91a5e3793 | |||
| 7f5ad041cc | |||
| 92ea275275 | |||
| 0c114a2ab3 | |||
| e3757880ee | |||
| 88d680ef77 | |||
| e1b3cf027c | |||
| d0a725d1cc | |||
| 07dbd26ba4 | |||
| c93f56e4ec | |||
| c0866b9787 | |||
| 14a9f6c444 | |||
| 588870b479 | |||
| f74bb3c145 | |||
| 7095753410 | |||
| 4f851dc431 | |||
| e89cc336b1 | |||
| 959c588658 | |||
| 7c887c1a5d | |||
| 56bcf9796a | |||
| ee270314f8 | |||
| bda76afe4b | |||
| 4d426a3f31 | |||
| 9d33248c6e | |||
| 46329ceb94 | |||
| 2160f0bc08 | |||
| b231f19ec6 | |||
| 6eb896e7a3 | |||
| d7874315c3 | |||
| c95b27683f | |||
| b0655d0431 | |||
| ad24596d3f | |||
| 80a6cf34e2 | |||
| c13b1800b9 | |||
| b4bb0f011d | |||
| b9ace61ccb | |||
| 21273582a4 | |||
| 53bbabea4f | |||
| 170a78a420 | |||
| 8771ced8e4 | |||
| dc7d2698b7 | |||
| 3d4694a92f | |||
| 8b2f94a6b2 | |||
| 6736164d98 | |||
| 77266fe221 | |||
| 14a48c1182 | |||
| 5f6e52f367 | |||
| e71a87c62c | |||
| b963f177cc | |||
| c3097979f2 | |||
| 21e56d2f53 | |||
| 455ce26741 | |||
| d241f5b3eb | |||
| d34f8eda1a | |||
| 483095c3da | |||
| 856c34016d | |||
| ad80d4f059 | |||
| 0da547a239 | |||
| 16278892d8 | |||
| 8500f404a9 | |||
| 5d782a317c | |||
| af435204a0 | |||
| b4c353e65f | |||
| e42f6c0cad | |||
| bc512a6e4c | |||
| 9cf7edc48d | |||
| 904539df58 | |||
| c9df9c33a8 | |||
| 5c3bfa6a83 | |||
| 149ed04a4f | |||
| e98eaaee6e | |||
| 4b93d801ae | |||
| 5a1cc4c2e7 | |||
| 8016a70bc4 | |||
| 70536d5676 | |||
| 27ce0970c5 | |||
| 49f6634d73 | |||
| 142ee81e66 | |||
| 3b21998d96 | |||
| 0fb307d09b | |||
| c1160d3419 | |||
| 48253f0ff0 | |||
| c3c7ee5453 | |||
| c6aac8cbd9 | |||
| 1b43bc78d0 | |||
| 93a091c7e8 | |||
| 083dde3557 | |||
| 4adc5f2c85 | |||
| ced14819e4 | |||
| 0b42d85c5b | |||
| c4a35020f1 | |||
| 11f052bcc6 | |||
| 0ea11ea806 | |||
| 7cad5a0479 | |||
| 83c53f6a79 | |||
| ae13ed7ded | |||
| b17385120a | |||
| cc0d8da416 | |||
| c796702eba | |||
| 2675442ced | |||
| aa3e6514c6 | |||
| be6d64fbfd | |||
| 4cbab72369 | |||
| 0227b1c68d | |||
| 4c051202af | |||
| 981b9e0595 | |||
| 9e719ba31e | |||
| c65f576f8d | |||
| 2c805bbece | |||
| 02b836698c | |||
| 25112ede58 | |||
| 5888c8a56c | |||
| 1cee7bf397 | |||
| cab7a71a94 | |||
| d7c63e3487 | |||
| bff749fd50 | |||
| 5c286352cb | |||
| 9ec3504c72 | |||
| 26b3e32ca2 | |||
| 4e2c83cc08 | |||
| 17def14eba | |||
| f260de573b | |||
| 4fd45ab278 | |||
| 4a2e9eb927 | |||
| dd8adef9ed | |||
| 9164debf03 | |||
| 534bef8632 | |||
| d8c43d02ba | |||
| ae3738f822 | |||
| be621e1aa7 | |||
| 343d63a28a | |||
| 0a28d6e950 | |||
| b493a62afa | |||
| 37a8c9bd72 | |||
| a9c4345159 | |||
| 5f1153b43f | |||
| 2c213f88d9 | |||
| a236219111 | |||
| 2f9958cca9 | |||
| f26154d0ac | |||
| 5ae87b7c95 | |||
| 219103a4e2 | |||
| 4ec7b9bb3f | |||
| bad8b7fb76 | |||
| a101857cb6 | |||
| a52f92830a | |||
| 40d113a423 | |||
| 7ec8421d19 | |||
| 9048efeb65 | |||
| 43fc200dae | |||
| 6679e93afc |
@@ -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
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"nonew": true,
|
||||
"curly": true,
|
||||
"forin": true,
|
||||
"freeze": true,
|
||||
"freeze": false,
|
||||
"undef": true,
|
||||
"unused": "vars"
|
||||
}
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
# Keep this file in sync with .gitignore.
|
||||
|
||||
.jsdoc
|
||||
node_modules
|
||||
.lock-wscript
|
||||
build/Release
|
||||
coverage
|
||||
lib-cov
|
||||
out
|
||||
reports
|
||||
dist/browser-matrix-dev.js
|
||||
|
||||
# tarball created by 'npm pack'.
|
||||
/matrix-js-sdk-*.tgz
|
||||
|
||||
+146
-7
@@ -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
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
There is a script `release.sh` which does the following, but if you need to do
|
||||
a release manually, here are the steps:
|
||||
|
||||
- `git checkout -b release-v0.x.x`
|
||||
- Update `CHANGELOG.md`
|
||||
- `npm version 0.x.x`
|
||||
- Merge `release-v0.x.x` onto `master`.
|
||||
- Push `master`.
|
||||
- Push the tag: `git push --tags`
|
||||
- `npm publish`
|
||||
- Generate documentation: `npm run gendoc` (this outputs HTML to `.jsdoc`)
|
||||
- Copy the documentation from `.jsdoc` to the `gh-pages` branch and update `index.html`
|
||||
- Merge `master` onto `develop`.
|
||||
- Push `develop`.
|
||||
Vendored
+10864
File diff suppressed because it is too large
Load Diff
+5
File diff suppressed because one or more lines are too long
Vendored
+15420
File diff suppressed because it is too large
Load Diff
+5
File diff suppressed because one or more lines are too long
Vendored
+15577
File diff suppressed because it is too large
Load Diff
+7
File diff suppressed because one or more lines are too long
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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";
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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
@@ -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><video></code> DOM element
|
||||
* to render video to.
|
||||
* @param {Element} localVideoElement a <code><video></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><video></code> DOM element.
|
||||
* @return {Element} The dom element
|
||||
@@ -95,13 +153,23 @@ MatrixCall.prototype.getLocalVideoElement = function() {
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the remote <code><video></code> DOM element.
|
||||
* Retrieve the remote <code><video></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><audio></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><video></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><video></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><video></code> DOM element.
|
||||
*/
|
||||
MatrixCall.prototype.setRemoteVideoElement = function(element) {
|
||||
@@ -134,6 +202,16 @@ MatrixCall.prototype.setRemoteVideoElement = function(element) {
|
||||
_tryPlayRemoteStream(this);
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the remote <code><audio></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><video></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
@@ -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
@@ -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
|
||||
@@ -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({
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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) {
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user