Compare commits

...
This repository has been archived on 2022-08-19. You can view files and clone it, but cannot push or open issues or pull requests.

1493 commits

Author SHA1 Message Date
f9aa7b52f8
feat: switch to matrix.test.mystiq.app
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2022-08-19 17:41:05 +05:30
2e54866353
fix: submit path
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2022-08-18 17:51:23 +05:30
ce075eb32b
feat: set custom homeserver and bugreport endpoint
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2022-08-18 17:41:24 +05:30
02a50a19cb
feat: add ci badge
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2022-08-16 17:25:37 +05:30
a33d9981bd
fix: secrets
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2022-08-16 17:05:59 +05:30
8335a50308
feat: switch to python, debian doesn't have make installed by default
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2022-08-16 17:02:10 +05:30
ee9e73d8c7
fix: use debian latest img to get git with git branch --show-current
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2022-08-16 16:59:15 +05:30
63f77feb7b
fix: set project root
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2022-08-16 16:55:52 +05:30
04de39596f
feat: bump ci node to 16
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2022-08-16 16:52:22 +05:30
25b634bb78
fix: use same tests as github actions
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2022-08-16 16:47:23 +05:30
96c9ea8de7
fix: use node 14, same as github actions config
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2022-08-16 16:44:15 +05:30
d80e970117
feat: conditional deploy pipeline
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2022-08-16 16:38:53 +05:30
6db5f34ac2
feat: multi-pipeline workflow 2022-08-16 16:36:05 +05:30
df0000783d
feat: deploy to librepages
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2022-08-16 16:14:27 +05:30
Bruno Windels
c898bcb46a release v0.3.1 2022-08-02 12:16:55 +02:00
Bruno Windels
97391663d3 sdk version 0.1.0 2022-08-01 14:32:26 +02:00
R Midhun Suresh
7d3f22c106
Merge pull request #824 from vector-im/fix-dev-server-1
Fix develop server breaking due to import syntax
2022-08-01 17:29:52 +05:30
RMidhunSuresh
832597447a Add explaining doc 2022-08-01 17:01:36 +05:30
RMidhunSuresh
236a4ab49b Ignore error 2022-08-01 17:01:36 +05:30
RMidhunSuresh
ba8cdea6b4 Use default import if other not found 2022-08-01 17:01:36 +05:30
RMidhunSuresh
ef9f90bc36 Fix imports breaking on dev 2022-08-01 17:01:36 +05:30
R Midhun Suresh
67e94bd642
Merge pull request #825 from vector-im/fix-sdk-fail-1
Fix sdk build failing after derived theme implementation
2022-08-01 16:17:09 +05:30
R Midhun Suresh
f7839135a4
Merge pull request #823 from vector-im/fix-tmp-dir
Fix .tmp being created in `/`
2022-08-01 16:16:35 +05:30
RMidhunSuresh
4571ecd851 Specify theme as array 2022-07-29 23:45:58 +05:30
RMidhunSuresh
5091090795 Produce .tmp directory within root 2022-07-29 23:11:17 +05:30
Bruno Windels
db2b4e693c release v0.3.0 2022-07-29 17:10:24 +02:00
Bruno Windels
eee8412621
Merge pull request #822 from vector-im/bwindels/move-runtime-theme-test-out-of-root
move semi-automatic test for runtime themes into dedicated directory
2022-07-29 15:00:34 +00:00
Bruno Windels
5e83eca3b9 move semi-automatic test for runtime themes into dedicated directory 2022-07-29 16:43:28 +02:00
Bruno Windels
041e628520
Merge pull request #769 from vector-im/implement-derived-theme
Support for derived themes
2022-07-29 14:25:05 +00:00
Bruno Windels
4838e19c92
Merge pull request #811 from vector-im/bwindels/sharekeyswithinvitees
Key sharing based on room history visibility
2022-07-29 14:23:26 +00:00
Bruno Windels
cb0ac846c7 remove obsolete comment 2022-07-29 16:22:01 +02:00
Bruno Windels
b40ce6137e
Merge pull request #676 from vector-im/ts-conversion-domain-navigation
Convert /domain/navigation to typescript
2022-07-29 14:21:17 +00:00
Bruno Windels
fdefea5b88 Merge branch 'master' into ts-conversion-domain-navigation 2022-07-29 16:18:22 +02:00
RMidhunSuresh
39817dc36b Revert back option 2022-07-29 17:33:33 +05:30
RMidhunSuresh
708637e390 No need for this complex resolve 2022-07-29 16:45:25 +05:30
Bruno Windels
b6f795505d fix lint 2022-07-29 12:21:16 +02:00
Bruno Windels
10522cacef
Merge pull request #813 from vector-im/doc-derived-theming
[Documentation] - Add information about derived themes to doc
2022-07-29 10:16:41 +00:00
Bruno Windels
02116103a1
Merge pull request #816 from Kaki-In/restore_last
Opening the last opened room at start
2022-07-29 10:16:23 +00:00
Bruno Windels
06da5a8ae4
clarification 2022-07-29 10:14:58 +00:00
Bruno Windels
02bc7d1d7e
fix typo 2022-07-29 10:14:41 +00:00
Kaki In
09bc77073b
Merge branch 'vector-im:master' into restore_last 2022-07-29 12:06:49 +02:00
Bruno Windels
4a2e14925a
Merge pull request #812 from vector-im/doc-config
[Documentation] - Add type for config options
2022-07-29 10:05:27 +00:00
Bruno Windels
224ab2672a
Merge pull request #809 from Kaki-In/implement-join
Implemented /join
2022-07-29 10:03:18 +00:00
Bruno Windels
170460f5a9 add link to sygnal webpush docs as well 2022-07-29 12:02:09 +02:00
Bruno Windels
2a5e0302dc
Merge pull request #785 from vector-im/hs/log-when-storage-access-fails
Log the error when we can't get storage access
2022-07-29 09:47:58 +00:00
Kaki In
f512bfcfc1 Pretty syntaxed the RoomViewModel 2022-07-29 11:47:47 +02:00
Half-Shot
5b5c852401 Revert "use logging items"
This reverts commit d937b9b14b.
2022-07-29 10:44:37 +01:00
Kaki In
58a2d1f34c Restored the common.js indentation 2022-07-29 11:44:23 +02:00
Half-Shot
d937b9b14b use logging items 2022-07-29 10:39:41 +01:00
Bruno Windels
d3e93196e3
Merge pull request #777 from ibeckermayer/ibeckermayer/ts-conversion-loginviewmodel
TS conversion for `LoginViewModel`
2022-07-29 09:27:10 +00:00
Bruno Windels
62b3a67e33 write unit tests for correctly reading history visibility when needed 2022-07-28 17:09:41 +02:00
Bruno Windels
319ec37864 fix typos preventing to load the history visibility 2022-07-28 11:44:50 +02:00
Kaki In
f5dacb4e42 Fixed last check 2022-07-28 10:26:59 +02:00
Kaki In
302131c447 Review last checks 2022-07-28 10:14:21 +02:00
Kaki In
fb79326747 Forgot one change 2022-07-28 09:26:08 +02:00
Kaki In
3c64f7d49b Finals checks about https://github.com/vector-im/hydrogen-web/pull/809#pullrequestreview-1053501341
- joined the processJoinRoom and joinRoom methods;
 - fixed some precisions miss;
 - removed some useless code;
 - change the error message height from absolute (40px) to relative (auto)
2022-07-28 09:23:30 +02:00
Isaiah Becker-Mayer
a82df95b82 marking private methods as such 2022-07-27 22:09:30 -07:00
Isaiah Becker-Mayer
cadca70946 fixes linter errors and removes some unneeded async/await 2022-07-27 22:09:30 -07:00
Isaiah Becker-Mayer
8b91d8fac8 adds newline 2022-07-27 22:09:30 -07:00
Isaiah Becker-Mayer
a5b9cb6b95 removes unnecessary awaits 2022-07-27 22:09:30 -07:00
Isaiah Becker-Mayer
aeed978789 changes signature of emitChange to require changedProps 2022-07-27 22:09:30 -07:00
Isaiah Becker-Mayer
7b7b19476c updates some signatures to be more verbose, fixes wrong type for attemptLogin 2022-07-27 22:09:30 -07:00
Isaiah Becker-Mayer
ad0bd82bda creating default exports 2022-07-27 22:09:30 -07:00
Isaiah Becker-Mayer
d7657dcc4d first draft of fully typescriptified LoginViewModel.ts 2022-07-27 22:09:30 -07:00
Kaki In
176caf340f Placed the join command outside of the processCommand method 2022-07-27 16:42:44 +02:00
Kaki In
a40bb59dc0 Some fixes :
- fixed a pretty syntax miss (a !== b);
 - fixed a type error : replaced "msgtype" by "type" when instantied the "messinfo" variable;
 - some indentation fixes
2022-07-27 16:36:58 +02:00
Kaki In
ab64ce02b2 Separated the _processCommand and the joinRoom command
- renamed executeJoinCommand as joinRoom;
 - separated the joinRoom process and the parse and result process
2022-07-27 15:18:32 +02:00
Kaki In
2d3b6fe973 Canceled indentation modification. 2022-07-27 12:40:19 +02:00
Kaki In
550b9db4dc Separated the join instructions into a executeJoinCommand method 2022-07-27 12:21:00 +02:00
Bruno Windels
0df66b5aea track room before listing user ids when sharing key 2022-07-27 12:06:55 +02:00
Bruno Windels
f18520a2fe let loadMembers use own txn in case members haven't been fetched yet
if they haven't, it will need a network request, meaning that the txn
will get closed, so we can't reuse it afterwards
2022-07-27 11:39:50 +02:00
Bruno Windels
50b6ee91d7 don't need history visibility here 2022-07-27 11:39:36 +02:00
Kaki In
9b0ab0c8f1 Used "null" instead of "undefined"
When creating the this._lastSessionHash attribute of History
2022-07-27 09:19:36 +02:00
Bruno Windels
402cf17d22 Merge branch 'master' into bwindels/sharekeyswithinvitees 2022-07-27 09:17:31 +02:00
Bruno Windels
bfaba63f47 fix ts error 2022-07-26 17:55:21 +02:00
Bruno Windels
544afef902 test adding and removing when tracking multiple rooms 2022-07-26 17:41:26 +02:00
Bruno Windels
dd878bb8d6 also take rejecting invites into account to remove user identity 2022-07-26 16:58:07 +02:00
Bruno Windels
dea3852425 add some tests for sharing keys with invitees 2022-07-26 16:57:28 +02:00
Bruno Windels
4c17612b05 allow passing txn to loadMembers so we can do it as part of sync txn
to rewrite useridentities upon receiving new history visibility
2022-07-26 16:53:02 +02:00
Kaki In
f9f49b7640 Fixed an error and improving css
If the /join command success, an error was thrown, because of a copy-pasted command not well integrated
The button of the error on "theme.css" contains now an unicode cross. The :after/:before cross was disformed when opening the room informations.
2022-07-26 14:48:03 +02:00
Kaki In
0718f1e77e Fixed the https://github.com/vector-im/hydrogen-web/pull/816#discussion_r929692693 comment
Added the _lastSessionHash attribute inside the History constructor
2022-07-26 11:11:16 +02:00
Kaki In
09fd1a5113 Use "args.join" instead of "message.substring"
into RoomViewModel._processCommands
2022-07-26 10:37:05 +02:00
Kaki In
832b840a15 Merge remote-tracking branch 'origin' into restore_last 2022-07-26 10:06:31 +02:00
Kaki In
adfecf0778 Fix restoring the last url at start
The last session url is now remembered for being restored at the beginning of the session. Thanks for the help of @bwindels
2022-07-26 10:02:20 +02:00
Kaki In
5fa6793958
Merge branch 'vector-im:master' into implement-join 2022-07-25 16:30:50 +02:00
Kaki In
1e5179f835 - Application des différents commentaires du Pull Request (#809)
- Correction des erreurs d'indentations.
2022-07-25 15:22:06 +02:00
Bruno Windels
bc385e2cdc
Merge pull request #778 from vector-im/bwindels/uidocs
more detailed docs for IView, TemplateView and ListView
2022-07-25 13:02:22 +00:00
Kaki In
0bf021ea87 The room is now joined after having actualised the rooms list, to avoid the synchronisations waits that can sometimes disable to enter the room (message "You're not into this room" or simply "You're not in this room yet. *Join the room*") 2022-07-25 13:37:03 +02:00
RMidhunSuresh
fdd60a7516 Add documentation for derived themes 2022-07-25 11:38:50 +05:30
RMidhunSuresh
63bdbee39c Make optional fields optional 2022-07-25 11:33:22 +05:30
RMidhunSuresh
8a976861fb Add type 2022-07-25 11:31:14 +05:30
Bruno Windels
a23df8a545 pass history visibility to device tracker
and delegate adding and removing members to share keys with to it
2022-07-22 17:49:59 +02:00
Bruno Windels
17f42f523a add write method for when history visibility changes
also returning added and removed user ids
2022-07-22 17:49:26 +02:00
Bruno Windels
f6011f3f34 take history visibility into account in device tracker
and return added and removed userids to their userIdentity for the given
room, so room encryption can share and discard the keys for them
2022-07-22 17:48:26 +02:00
Bruno Windels
86c0e9e669 logic for whether a key should be shared by membership and h. visibility 2022-07-22 17:46:53 +02:00
Bruno Windels
f337940202 this migration shouldn't be needed anymore
and undoes the export of addRoomToIdentity, which is somewhat internal
2022-07-22 17:46:29 +02:00
Kaki In
b7fd22c7f9 SyntaxError fixed 2022-07-22 17:10:29 +02:00
Kaki In
66a59e6f4d Error of interpretation of the 403 status at the last update. Fixed 2022-07-22 17:09:43 +02:00
Kaki In
e345d0b33e Added the 403 status when joining an unknown room 2022-07-22 17:06:09 +02:00
Kaki In
be8962cec2 Fixed priority operations when checking request status 2022-07-22 16:59:48 +02:00
Kaki In
8b39346409 The error message can now be closed 2022-07-22 16:34:52 +02:00
Kaki In
fb58d9c9ef Corrected some syntax dismiss 2022-07-22 16:08:53 +02:00
Bruno Windels
22831e710c support async callback in iterateResponseStateEvents 2022-07-22 14:15:26 +02:00
Kaki In
faa8cae532 Added the possibility to join a room using /join (also added the global commands uses, and some others commands like /shrug .) 2022-07-21 13:55:23 +02:00
RMidhunSuresh
8d766ac504 Remove await within loop 2022-07-21 12:05:10 +05:30
Bruno Windels
c8a8eb10b5 get user ids for sharing a new key when the message is sent
rather than when the key happens to get sent
2022-07-20 15:21:33 +02:00
Bruno Windels
d79e5f7806 create key share operations for invitees when history visibility=invited 2022-07-20 15:20:23 +02:00
RMidhunSuresh
7feaa479c0 Typescript update to support .mjs files 2022-07-20 15:55:11 +05:30
RMidhunSuresh
1456e308a8 Add type and fix formatting 2022-07-20 15:36:02 +05:30
RMidhunSuresh
313e65e00c Write tests 2022-07-20 12:30:41 +05:30
RMidhunSuresh
612b878793 Update theme name 2022-07-19 21:21:35 +05:30
RMidhunSuresh
8aa96e8031 Update log label 2022-07-19 21:19:22 +05:30
RMidhunSuresh
7ac2c7c7fa Get tests to work 2022-07-19 21:06:55 +05:30
RMidhunSuresh
de02456641 Add explaining comment 2022-07-19 19:46:36 +05:30
RMidhunSuresh
994667205f Remove change 2022-07-19 19:38:36 +05:30
RMidhunSuresh
ecb3a66dfc WIP 2022-07-19 17:56:08 +05:30
RMidhunSuresh
e1ee258630 Change path 2022-07-19 17:56:08 +05:30
RMidhunSuresh
83b5d3b68e Change directory name 2022-07-19 17:56:08 +05:30
RMidhunSuresh
7a1591e0ce Move code 2022-07-19 17:56:08 +05:30
RMidhunSuresh
07db5450b7 Aliases can also be derived 2022-07-19 17:56:08 +05:30
RMidhunSuresh
081de5afa8 .js --> .mjs 2022-07-19 17:56:08 +05:30
RMidhunSuresh
dece42dce3 Do not store all the manifests in memory 2022-07-19 17:56:08 +05:30
RMidhunSuresh
b29287c47e await in loop --> Promise.all() 2022-07-19 17:56:08 +05:30
RMidhunSuresh
9bdf9c500b Add return types 2022-07-19 17:56:08 +05:30
RMidhunSuresh
9e2d355573 Add logging 2022-07-19 17:56:08 +05:30
RMidhunSuresh
ce5db47708 Support using derived theme as default theme 2022-07-19 17:56:08 +05:30
RMidhunSuresh
da0a918c18 This code should only run once 2022-07-19 17:56:08 +05:30
RMidhunSuresh
043cc9f12c Use ThemeManifest type 2022-07-19 17:56:08 +05:30
RMidhunSuresh
80fb953688 Don't fail on erros; expect the code to throw! 2022-07-19 17:56:08 +05:30
RMidhunSuresh
f15e23762a Add more missing keys to type 2022-07-19 17:56:08 +05:30
RMidhunSuresh
f440457875 Use ThemeManifest type where possible 2022-07-19 17:56:08 +05:30
RMidhunSuresh
a8cab98666 Add mroe missing types 2022-07-19 17:56:08 +05:30
RMidhunSuresh
ac7be0c7a1 WIP 2022-07-19 17:56:08 +05:30
RMidhunSuresh
d731eab51c Support fetching text 2022-07-19 17:56:08 +05:30
RMidhunSuresh
f7b302d34f Don't optimzie colors 2022-07-19 17:56:08 +05:30
RMidhunSuresh
5ba74b1d75 Use script to copy over runtime theme after build 2022-07-19 17:56:08 +05:30
RMidhunSuresh
c5f4a75d4b Split code so that it can be reused 2022-07-19 17:56:08 +05:30
RMidhunSuresh
2f3db89e0a Let ts know that we can use replaceAll() 2022-07-19 17:56:08 +05:30
RMidhunSuresh
1ef382f3a9 Add gruvbox color scheme 2022-07-19 17:56:08 +05:30
RMidhunSuresh
161e29b36e Use existing code 2022-07-19 17:56:08 +05:30
RMidhunSuresh
2947f9f6ff Remove console.log 2022-07-19 17:56:08 +05:30
RMidhunSuresh
c873804543 produce asset hashed icons 2022-07-19 17:56:08 +05:30
RMidhunSuresh
43e8cc9e52 Add svgo for optimizing svgs as dev dependency 2022-07-19 17:56:08 +05:30
RMidhunSuresh
bf87ed7eae Do not add variables to root for runtime theme 2022-07-19 17:56:08 +05:30
RMidhunSuresh
8c02541b69 WIP - 1 2022-07-19 17:56:08 +05:30
RMidhunSuresh
599e519f22 Convert color code to use es6 module 2022-07-19 17:56:08 +05:30
RMidhunSuresh
d5e24bf6e8 Convert color.js to color.mjs 2022-07-19 17:56:08 +05:30
Bruno Windels
bb5711db7e
Merge pull request #802 from vector-im/fix-dev-server
Fix bug that stops hydrogen from running in dev server
2022-07-19 10:22:36 +00:00
RMidhunSuresh
88808b0b06 Fix bug preventing yarn start 2022-07-19 15:50:01 +05:30
R Midhun Suresh
c9bca52e82
Merge pull request #760 from vector-im/refactor-rollup-plugin
Refactor theme builder plugin
2022-07-11 16:54:18 +05:30
RMidhunSuresh
6718198d9c Continue with other items if this throws 2022-07-11 12:40:24 +05:30
Bruno Windels
7b9e681d55 sdk v0.0.15 2022-07-07 15:25:17 +02:00
R Midhun Suresh
8291aea2f7
Merge pull request #790 from vector-im/fix-hide-composer
Pass childOptions to LowPowerLevelViewModel
2022-07-07 18:19:08 +05:30
RMidhunSuresh
f073f40e31 Fix error 2022-07-07 18:16:33 +05:30
R Midhun Suresh
963324c767
Merge pull request #789 from vector-im/support-pl-room-creation
Support power_level_content_override option on room creation
2022-07-07 17:42:19 +05:30
R Midhun Suresh
eac75644e7
Merge pull request #788 from vector-im/pl-composer
Disable composer when user lacks powerlevel needed to send messages
2022-07-07 17:35:29 +05:30
RMidhunSuresh
0bdbb96036 Use same kind 2022-07-07 17:26:43 +05:30
RMidhunSuresh
d292e1f5ad Extract into function 2022-07-07 17:23:23 +05:30
RMidhunSuresh
cd9e00b847 Support power_level_content_override 2022-07-07 17:17:05 +05:30
RMidhunSuresh
3941b7e3f0 Rename method 2022-07-07 16:45:18 +05:30
RMidhunSuresh
efd9f70e92 WIP 2022-07-07 16:39:45 +05:30
Isaiah Becker-Mayer
204948db64 changing filename to ts 2022-07-06 21:06:36 -04:00
Will Hunt
a85d2c96d6
Log the error when we can't get storage access
This is quite useful when debugging why a session isn't working properly.
2022-07-06 10:06:00 +01:00
R Midhun Suresh
28b686dae7
Merge pull request #784 from vector-im/fix-build-race
Fix build error caused due to race in postcss plugin
2022-07-05 20:13:57 +05:30
RMidhunSuresh
dd82469ab4 Don't assume object is available 2022-07-05 20:07:48 +05:30
Bruno Windels
3bf6a46a39 release sdk 0.0.14 2022-07-05 16:02:47 +02:00
Bruno Windels
e42e76a21c
Merge pull request #782 from vector-im/image-view-fix
Do not render images as links if lightboxUrl is empty
2022-07-05 14:00:53 +00:00
RMidhunSuresh
8ec0bd7295 Check if lightbox url is available 2022-07-05 17:55:51 +05:30
Bruno Windels
ff2129f36a
Merge pull request #773 from vector-im/madlittlemods/consistent-test-selector
Add a couple consistent selectors to reference in tests
2022-07-04 14:19:09 +00:00
Bruno Windels
1aa2ff5c10
Merge pull request #781 from vector-im/bwindels/fixlint-2022-7-4
fix lint
2022-07-04 14:18:05 +00:00
Bruno Windels
34ce8a8e3c fix lint 2022-07-04 16:15:59 +02:00
Bruno Windels
652e2c6d3b
Merge pull request #780 from vector-im/bwindels/update-olm-3.2.8
update olm to 3.2.8
2022-07-04 14:15:04 +00:00
Bruno Windels
c0445f2182 update lock file 2022-07-04 15:40:17 +02:00
Bruno Windels
b76fd1d792 update olm to 3.2.8 2022-07-04 15:39:11 +02:00
R Midhun Suresh
751dfa66a8
Merge pull request #758 from vector-im/document-theming
Document theming in Hydrogen
2022-07-04 17:20:53 +05:30
RMidhunSuresh
a3c6d744f5 Add link to ts file 2022-07-04 17:18:50 +05:30
R Midhun Suresh
b9f316e7c3 Better sentence structure
Co-authored-by: Bruno Windels <274386+bwindels@users.noreply.github.com>
2022-07-04 17:16:43 +05:30
R Midhun Suresh
d448ee1722 Fix typo
Co-authored-by: Bruno Windels <274386+bwindels@users.noreply.github.com>
2022-07-04 17:16:43 +05:30
RMidhunSuresh
da87470996 Store images in source tree 2022-07-04 17:16:43 +05:30
RMidhunSuresh
b319c0acb0 Remvoe stray newlines 2022-07-04 17:16:43 +05:30
RMidhunSuresh
e90e573bf9 Add doc 2022-07-04 17:16:43 +05:30
R Midhun Suresh
a68f0bba39
Merge pull request #752 from vector-im/theme-document-manifest
Create a type for theme-manifest
2022-07-04 17:11:08 +05:30
Bruno Windels
ca94c65dac clarify LazyListView constraints 2022-07-04 10:19:56 +02:00
Bruno Windels
fba3275f5b
Merge pull request #746 from vector-im/madlittlemods/assets-path-for-assets
Import SDK assets from the `assets/` directory
2022-07-04 06:53:37 +00:00
Bruno Windels
fc93acfd8d some rewording 2022-07-01 14:09:06 +02:00
Bruno Windels
d398e490eb some rewording 2022-07-01 13:59:57 +02:00
Bruno Windels
0ab611b013 more detailed docs for IView, TemplateView and ListView 2022-07-01 13:08:50 +02:00
Bruno Windels
bb923b8eb9 bump sdk version 2022-06-30 10:54:11 +02:00
Bruno Windels
73cd96fe3a abort release script on error 2022-06-30 10:54:00 +02:00
Bruno Windels
4929839fe9 release v0.2.33 2022-06-30 10:51:11 +02:00
Eric Eastwood
c59f65e43b Add a couple consistent selectors to reference in tests
Using `data-testid` because it seems generic out of the list from:

 - https://docs.cypress.io/guides/core-concepts/cypress-app#Uniqueness
 - https://docs.cypress.io/guides/references/best-practices#How-It-Works
2022-06-29 12:56:20 +02:00
Eric Eastwood
fd3a0f0126 Merge branch 'master' into madlittlemods/assets-path-for-assets 2022-06-28 16:35:54 +02:00
Eric Eastwood
ccfd63dfeb Restore backwards compatible theme paths
See https://github.com/vector-im/hydrogen-web/pull/746#discussion_r901347536
2022-06-28 16:35:30 +02:00
Eric Eastwood
5b54280ac2
Ignore macOS metadata .DS_Store (#770) 2022-06-28 05:08:24 -05:00
Bruno Windels
bd5bf7d456
Merge pull request #761 from vector-im/hs/node-15-replaceal
Require node 15+
2022-06-25 18:22:35 +00:00
Bruno Windels
ad8ad22cc1
Merge pull request #767 from vector-im/bwindels/download-media
Menu option to download attached image or video of event
2022-06-25 18:21:17 +00:00
Bruno Windels
3369bda2f0 offer menu options to download media
also always show status (before sendStatus), not just when isPending
as we are recycling it to show download status as well
2022-06-25 20:15:33 +02:00
Bruno Windels
7430aa7aab allow download media in media view model 2022-06-25 20:14:32 +02:00
Bruno Windels
3bc453d5ca
Merge pull request #766 from vector-im/bwindels/fix-765
Also allow undefined, which means at the end of the paginated direction
2022-06-25 17:40:26 +00:00
Bruno Windels
84bac0afe9 Also allow undefined, which means at the end of the paginated direction
we already detect the end by chunk.length===0, so we just need to not throw
2022-06-25 19:37:36 +02:00
Will Hunt
9cb7d89097 Require node 15.
We use replaceAll in scripts/postcss/svg-colorizer.js which is a ES2021 feature. https://node.green/#ES2021-features--String-prototype-replaceAll
2022-06-24 13:27:09 +01:00
RMidhunSuresh
d688fa4737 Get the theme-collection id from manifest 2022-06-23 15:06:22 +05:30
RMidhunSuresh
0dfd24af22 Update info on path
path is now relative to the manifest!
2022-06-21 12:52:10 +05:30
RMidhunSuresh
34eac94da3 Make everything optional
Now typescript will force us to validate everything.
2022-06-20 21:27:02 +05:30
RMidhunSuresh
fbdd512e06 Split functions into smaller functions 2022-06-20 21:10:11 +05:30
RMidhunSuresh
5eec724712 Locations must be relative to manifest 2022-06-20 20:35:06 +05:30
RMidhunSuresh
93165cb947 runtime theme chunks should also be stored in map
There will be more than one runtime theme file when multiple theme
collections exist.
2022-06-20 13:46:14 +05:30
RMidhunSuresh
e3372f0f2b Don't use theme-name in manifest file names 2022-06-20 12:54:18 +05:30
R Midhun Suresh
5a3cf03f0b
Merge pull request #759 from vector-im/move-scope-down
Refactor out global variables in postcss plugins
2022-06-20 12:14:06 +05:30
R Midhun Suresh
c050ade03c
Merge pull request #756 from vector-im/themeing-improvement-1
Improve code quality in css-url-variables plugin
2022-06-20 11:19:47 +05:30
RMidhunSuresh
cc29dc045d Move scope down in css-url-processor 2022-06-17 16:38:13 +05:30
RMidhunSuresh
09b2437e72 Move scope of variables down in compile-variables 2022-06-17 16:35:18 +05:30
RMidhunSuresh
cfd347335b Move scope of variables down
This was causing icons to be repeated in the css-file
2022-06-16 21:29:33 +05:30
RMidhunSuresh
d322f380ad Fix typo here
This was causing the icons section to be omitted from the source section
of the manifest.
2022-06-16 21:26:16 +05:30
RMidhunSuresh
f658dc2e4b Make comment clearer 2022-06-15 15:06:16 +05:30
RMidhunSuresh
7a3eabf39c Formatting fix 2022-06-15 15:04:33 +05:30
RMidhunSuresh
48da6c782c Remove base key 2022-06-15 15:04:12 +05:30
RMidhunSuresh
b00bbc7daf Fix formatting 2022-06-15 15:03:41 +05:30
RMidhunSuresh
9fbe8a4e32 Change description of version key 2022-06-15 15:02:15 +05:30
Bruno Windels
623939c671 release v0.2.32 2022-06-15 11:29:29 +02:00
Bruno Windels
fccc41f4b9
Merge pull request #753 from vector-im/bwindels/rageshake-submit
Allow sending logs to rageshake server
2022-06-15 11:28:54 +02:00
Bruno Windels
3b66ed8c17 fix type 2022-06-15 11:24:16 +02:00
Bruno Windels
8fe8981ffa add options to send logs to server in settings ui 2022-06-15 11:14:06 +02:00
Bruno Windels
375d8b066c complete settings view model for logs ui 2022-06-15 11:13:46 +02:00
Bruno Windels
69ada73dd4 cleanup rageshake code 2022-06-15 11:13:05 +02:00
Bruno Windels
2129a97588 remove unused param 2022-06-15 11:12:49 +02:00
Bruno Windels
4caabae895 extract map -> formdata conversion and also suppor this for xhr 2022-06-15 10:15:15 +02:00
RMidhunSuresh
d0375141f8 WIP - write type for manifest 2022-06-15 12:11:15 +05:30
Bruno Windels
a644621889 basic support for sending rageshake in view model 2022-06-14 18:46:02 +02:00
Bruno Windels
4ed7e01dfd release v0.2.31 2022-06-14 16:00:35 +02:00
Bruno Windels
e643ffb334
Merge pull request #751 from vector-im/fix-theming-watch
Fix: don't crash on platforms that don't have a preferred color scheme
2022-06-14 16:00:13 +02:00
RMidhunSuresh
d00ea39dc4 No need to throw here 2022-06-14 19:27:18 +05:30
RMidhunSuresh
69d8e6031e This isn't used anywhere 2022-06-14 19:26:59 +05:30
Bruno Windels
abee9baf60 release v0.2.30 2022-06-14 10:15:00 +02:00
Bruno Windels
d4aaa8117b
Merge pull request #742 from vector-im/theme-chooser-improvements
Theme chooser improvements
2022-06-14 10:14:29 +02:00
RMidhunSuresh
be66969c9a Remove font section from manifest 2022-06-14 11:52:45 +05:30
R Midhun Suresh
7bce0d848f
Merge pull request #750 from vector-im/madlittlemods/fix-broken-hydrogen-dev
Fix Vite not being able to analyze dynamic CSS styles import in dev on Windows
2022-06-13 20:04:25 +05:30
RMidhunSuresh
53a8915ffc Parellelize code 2022-06-12 17:05:31 +05:30
RMidhunSuresh
b5fd3656a7 Fix code breaking on dev server 2022-06-12 16:53:25 +05:30
R Midhun Suresh
acffd15002
Add comment
Co-authored-by: Bruno Windels <274386+bwindels@users.noreply.github.com>
2022-06-12 16:52:21 +05:30
R Midhun Suresh
989ecd785a
Lowercase string
Co-authored-by: Bruno Windels <274386+bwindels@users.noreply.github.com>
2022-06-12 16:51:58 +05:30
RMidhunSuresh
9a5a002293 Remove test-variant 2022-06-08 13:35:58 +05:30
Eric Eastwood
2cfd08e500 Remove debug logging 2022-06-07 23:47:38 -05:00
Eric Eastwood
2b4a7f05a6 Fix Vite not being able analyze dynamic CSS styles import in dev
Fix:
```
$ yarn start
[vite] warning:
@theme/default
1  |  import "C:\Users\MLM\Documents\GitHub\element\hydrogen-web\src\platform\web\ui\css\themes\element\theme.css";import "@theme/element/light/variables.css"
   |          ^
The above dynamic import cannot be analyzed by vite.
See https://github.com/rollup/plugins/tree/master/packages/dynamic-import-vars#limitations for supported dynamic import formats. If this is intended to be left as-is, you can use the /* @vite-ignore */ comment inside the import() call to suppress this warning.

  Plugin: vite:import-analysis
  File: @theme/default
```

And in the browser, it results in none of the styles loading because of the following error:
```
Uncaught SyntaxError: Invalid Unicode escape sequence (at default:formatted:1:163)
```

---

Before:
```
import { injectQuery as __vite__injectQuery } from "/@vite/client";import "__vite__injectQuery(C:\Users\MLM\Documents\GitHub\element\hydrogen-web\src\platform\web\ui\css\themes\element\theme.css, 'import')";import "/@id/__x00__@theme/element/light/variables.css"
```

After:
```
import "/ui/css/themes/element/theme.css";import "/@id/__x00__@theme/element/light/variables.css"
```
2022-06-07 23:41:45 -05:00
RMidhunSuresh
d31f127982 Add explaining comment 2022-06-07 13:28:56 +05:30
RMidhunSuresh
d08cfe3a29 Add more logging 2022-06-07 11:57:57 +05:30
RMidhunSuresh
51a837d459 Remove unuseed import 2022-06-06 17:26:39 +05:30
RMidhunSuresh
2f0f7143b5 Simplify code 2022-06-06 17:20:36 +05:30
RMidhunSuresh
0dac00f327 themeVariant is optional 2022-06-06 17:20:16 +05:30
RMidhunSuresh
a639fc5467 Rever to sensisble defaults 2022-06-06 12:20:06 +05:30
RMidhunSuresh
258a604cc6 Don't make defaultTheme compulsory 2022-06-06 12:19:48 +05:30
RMidhunSuresh
a2cbac9e0c Move code into method 2022-06-06 11:53:13 +05:30
RMidhunSuresh
71c3fb39a2 store theme-name and variant in settings 2022-06-05 20:52:47 +05:30
RMidhunSuresh
43244fa026 Add explaining comment 2022-06-05 20:52:47 +05:30
RMidhunSuresh
9e88bc3098 Fix bugs 2022-06-05 20:52:47 +05:30
RMidhunSuresh
b74f4b612b Change UI 2022-06-05 20:52:47 +05:30
RMidhunSuresh
8de91291dd Add more methods to ThemeLoader 2022-06-05 20:52:47 +05:30
RMidhunSuresh
dc2d1ce700 Remove id 2022-06-05 20:52:47 +05:30
RMidhunSuresh
12a8e94243 Move code into ThemeLoader 2022-06-05 20:52:47 +05:30
RMidhunSuresh
9e79b632a8 Extract variable 2022-06-05 20:52:47 +05:30
RMidhunSuresh
efb1a67470 Make method name a verb 2022-06-05 20:52:47 +05:30
RMidhunSuresh
e3235ea3eb Rename themeName --> themeId 2022-06-05 20:52:47 +05:30
RMidhunSuresh
46d2792dac Modify comment 2022-06-05 20:52:47 +05:30
RMidhunSuresh
8ad0b8a726 rename themeName --> variantName 2022-06-05 20:52:47 +05:30
RMidhunSuresh
e8e4c33bae Rephrase comment 2022-06-05 20:52:47 +05:30
RMidhunSuresh
cb03e97e78 Use default theme intially 2022-06-05 20:52:47 +05:30
RMidhunSuresh
f6cec938a7 Add default theme to mapping 2022-06-05 20:52:47 +05:30
RMidhunSuresh
bbec2effe5 Add typing 2022-06-05 20:52:47 +05:30
RMidhunSuresh
d4084da299 Extract code into function 2022-06-05 20:52:47 +05:30
RMidhunSuresh
1f00c8f635 Add a temporary theme to test this PR 2022-06-05 20:52:47 +05:30
RMidhunSuresh
0b98473e85 Render a radio button for default variants 2022-06-05 20:52:47 +05:30
RMidhunSuresh
3afbe1148e Use the new built-asset format in ThemeLoader 2022-06-05 20:52:47 +05:30
RMidhunSuresh
809c522571 Change the format of built-asset 2022-06-05 20:52:47 +05:30
RMidhunSuresh
4474458f4b getActiveTheme should never return undefined
Instead it should throw an error.

This is useful for when we do setTheme(await getActiveTheme()) because
setTheme expects a string.
2022-06-05 20:52:47 +05:30
Eric Eastwood
9d8a578dce Better comment 2022-05-31 15:35:48 -05:00
Eric Eastwood
38c3774869 Import assets from the assets/ directory
> Will be easier towards the future when adding more assets. Probably best to keep style.css for now for backwards compat though.
>
> *-- https://github.com/vector-im/hydrogen-web/pull/693#discussion_r853844282*
2022-05-31 15:30:56 -05:00
Bruno Windels
8b2299852e
Merge pull request #744 from vector-im/bwindels/fix-tracker-changed-key-check
Fix: device with changed key not being properly ignored
2022-05-31 13:51:17 +02:00
Bruno Windels
c62c8da10b fix changed key not being ignored 2022-05-31 13:39:35 +02:00
Bruno Windels
bc51644868 reassignment is not used later on, remove 2022-05-31 13:39:23 +02:00
Bruno Windels
3d3d590334 add failing test for device with changed key being returned 2022-05-31 13:39:05 +02:00
Bruno Windels
11d7535c23 add some basic tests (with mock utils) for DeviceTracker 2022-05-31 13:38:34 +02:00
Bruno Windels
a49d7eae5d
Merge pull request #693 from vector-im/madlittlemods/686-682-local-friendly-development-and-commonjs
Make the SDK friendly to locally link and develop on
2022-05-30 14:45:16 +02:00
Bruno Windels
1b2a6b5d0e
Merge branch 'master' into madlittlemods/686-682-local-friendly-development-and-commonjs 2022-05-30 14:15:19 +02:00
RMidhunSuresh
ba647d012d Fix type in observeNavigation 2022-05-29 20:38:14 +05:30
RMidhunSuresh
fc873757d8 WIP 2022-05-27 22:42:21 +05:30
RMidhunSuresh
ec1cc89cf9 Make URLRouter in options conditional on generic
URLRouter can be passed in option to vm only if the SegmentType used
contains session.
ViewModel.urlCreator returns undefined when used with a SegmentType that
lacks session.
2022-05-27 22:42:21 +05:30
RMidhunSuresh
a336623f3a Generic parameter should extend object 2022-05-27 22:42:21 +05:30
RMidhunSuresh
9300347e9b Give defaultt type 2022-05-27 22:42:21 +05:30
RMidhunSuresh
f49d580d49 WIP 2022-05-27 22:42:21 +05:30
RMidhunSuresh
263948faa3 Remove unwanted export 2022-05-27 22:42:21 +05:30
RMidhunSuresh
52f0690c70 Add return type 2022-05-27 22:42:21 +05:30
RMidhunSuresh
7a24059337 Remove empty line 2022-05-27 22:42:21 +05:30
RMidhunSuresh
4fd1918202 Remove comment 2022-05-27 22:42:21 +05:30
RMidhunSuresh
4ae3a5bf7a Use undefined instead of null 2022-05-27 22:42:21 +05:30
RMidhunSuresh
5be00f051f Use subtype instead of whole SegmentType 2022-05-27 22:42:21 +05:30
RMidhunSuresh
e7f4ce6175 Mark methods as private 2022-05-27 22:42:21 +05:30
RMidhunSuresh
09bc0f1b60 Extract complex type as type alias 2022-05-27 22:42:21 +05:30
RMidhunSuresh
76d04ee277 Make defaultSessionId optional 2022-05-27 22:42:21 +05:30
RMidhunSuresh
f28dfc6964 Type createRouter function 2022-05-27 22:42:21 +05:30
RMidhunSuresh
c14e4f3eed Use segment type 2022-05-27 22:42:21 +05:30
RMidhunSuresh
5d42f372f6 Pass as separate arguments to constructor 2022-05-27 22:42:21 +05:30
RMidhunSuresh
4c3e0a6ff0 Convert URLRouter.js to typescript 2022-05-27 22:42:21 +05:30
RMidhunSuresh
d9bfca10e1 Type function 2022-05-27 22:42:21 +05:30
RMidhunSuresh
bf2fb52691 Fix formatting 2022-05-27 22:42:21 +05:30
RMidhunSuresh
646cbe0fff Make all keys string 2022-05-27 22:42:21 +05:30
RMidhunSuresh
92e8fc8ad3 Remove deprecated method 2022-05-27 22:42:21 +05:30
RMidhunSuresh
92c79c853d Convert index.js to typescript 2022-05-27 22:42:21 +05:30
RMidhunSuresh
55229252d7 Type allowsChild 2022-05-27 22:42:21 +05:30
RMidhunSuresh
3efc426fed Complete converting Navigation.js to ts 2022-05-27 22:42:21 +05:30
RMidhunSuresh
04d5b9bfda WIP - 2 2022-05-27 22:42:21 +05:30
RMidhunSuresh
66f6c4aba1 WIP 2022-05-27 22:42:18 +05:30
Bruno Windels
ed8c98558d release v0.2.29 2022-05-18 21:45:45 +02:00
Bruno Windels
514d5c0a50
add notes about client side caching 2022-05-18 19:44:39 +00:00
Bruno Windels
13428bd03c allow updating cache of unhashed assets (like config) in service worker 2022-05-18 21:41:47 +02:00
Bruno Windels
1555b0f4bc put a message in container node when config file is not found 2022-05-18 21:41:31 +02:00
Bruno Windels
0e46aed0df rename config file to config.sample.json when packaging 2022-05-18 20:52:18 +02:00
Bruno Windels
7b0591be46 explain that push section of config usually doesn't need to be touched 2022-05-18 20:51:50 +02:00
Bruno Windels
f21e103270 add newlines to config file when rewriting with theme stuff 2022-05-18 20:46:38 +02:00
Bruno Windels
7a197c0a1a add deployment instruction now that we have a config file 2022-05-18 20:44:04 +02:00
Bruno Windels
8a5f1ed9cd Merge remote-tracking branch 'origin/move-config-root' 2022-05-18 20:40:12 +02:00
Bruno Windels
36ddd61318
Merge pull request #724 from vector-im/theme-chooser
Implement theme chooser in settings
2022-05-18 20:22:38 +02:00
Bruno Windels
03ab1ee2c7 log theme being loaded 2022-05-18 17:48:03 +02:00
RMidhunSuresh
a550788788 Remove some logging + use wrapOrRun 2022-05-18 18:56:28 +05:30
RMidhunSuresh
683ffa9ed3 injectServiceWorker plugin should accept callback 2022-05-18 17:31:17 +05:30
RMidhunSuresh
7952a34d64 Add logging 2022-05-18 16:09:09 +05:30
RMidhunSuresh
7426d17e33 Precache config and theme manifest 2022-05-18 16:07:26 +05:30
RMidhunSuresh
660a08db3e Give a better name 2022-05-18 14:41:52 +05:30
RMidhunSuresh
1b22a48b54 Treat theme-manifests the same way as config 2022-05-18 14:23:41 +05:30
Eric Eastwood
b725269c7a Clean up index.html in the right spot 2022-05-18 00:21:56 -05:00
Eric Eastwood
639358b146 Merge branch 'master' into madlittlemods/686-682-local-friendly-development-and-commonjs
Conflicts:
	scripts/sdk/base-manifest.json
2022-05-12 12:05:45 -05:00
RMidhunSuresh
34e8b60917 Create config.json in root 2022-05-12 16:05:33 +05:30
RMidhunSuresh
9ba1534390 Remove unused import 2022-05-12 16:03:06 +05:30
RMidhunSuresh
4ddfd3b508 built-asset --> built-assets 2022-05-12 14:56:58 +05:30
RMidhunSuresh
e63440527a Move condition to binding 2022-05-12 13:43:19 +05:30
RMidhunSuresh
0984aeb570 Move code to ThemeLoader 2022-05-12 13:39:57 +05:30
RMidhunSuresh
654e83a5f9 Remove method 2022-05-12 13:28:11 +05:30
RMidhunSuresh
b306344739 Add explaining comment 2022-05-12 12:55:08 +05:30
R Midhun Suresh
4231037345
Update src/platform/web/Platform.js
Co-authored-by: Bruno Windels <274386+bwindels@users.noreply.github.com>
2022-05-12 12:48:41 +05:30
R Midhun Suresh
d5bc9f5d7d
Update src/platform/web/Platform.js
Co-authored-by: Bruno Windels <274386+bwindels@users.noreply.github.com>
2022-05-12 12:48:34 +05:30
Bruno Windels
6fde6bbf6b bump sdk version 2022-05-11 14:58:57 +02:00
RMidhunSuresh
cc88245933 Create themeLoader only if not dev 2022-05-11 15:46:12 +05:30
RMidhunSuresh
174adc0755 Move platform dependent code to Platform 2022-05-11 15:38:37 +05:30
RMidhunSuresh
c26dc04b52 Fix type 2022-05-11 15:03:32 +05:30
RMidhunSuresh
2761789f45 Move theme code to separate file 2022-05-11 14:58:14 +05:30
RMidhunSuresh
213f87378b Use t.if instead of t.map 2022-05-11 12:46:12 +05:30
RMidhunSuresh
855298bdaf Read from manifest 2022-05-11 12:40:32 +05:30
RMidhunSuresh
e8a4ab5ecc built-asset must be a mapping
A mapping from theme-name to location of css file
2022-05-10 16:58:06 +05:30
RMidhunSuresh
5204fe5c99 This emitFile is no longer needed 2022-05-10 14:22:37 +05:30
RMidhunSuresh
c39f0d2efb Don't show theme chooser on dev 2022-05-10 14:12:36 +05:30
RMidhunSuresh
bb3368959f Use sh instead of bash 2022-05-10 14:12:36 +05:30
RMidhunSuresh
af9cbd727f Remove existing stylesheets when changing themes 2022-05-10 14:12:36 +05:30
RMidhunSuresh
12a70469eb Fix formatting 2022-05-10 14:12:36 +05:30
RMidhunSuresh
c611d3f85c Select current theme in dropdown 2022-05-10 14:12:36 +05:30
RMidhunSuresh
ecb83bb277 Store and load theme from setting 2022-05-10 14:12:36 +05:30
RMidhunSuresh
daae7442bb Create theme chooser 2022-05-10 14:12:36 +05:30
RMidhunSuresh
cc2c74fdff Generate theme summary on build 2022-05-10 14:12:36 +05:30
RMidhunSuresh
541cd96eeb Add script to cleanup after build 2022-05-10 14:12:36 +05:30
RMidhunSuresh
f16a2e5d22 Don't add asset hash to manifest json on build 2022-05-10 14:12:36 +05:30
Bruno Windels
b7675f46c4 bump sdk version 2022-05-10 09:59:38 +02:00
R Midhun Suresh
a06474d7ac
Merge pull request #731 from vector-im/fix-tilescollection
Newly created tiles must be given a copy of tilesOptions
2022-05-10 12:33:46 +05:30
Bruno Windels
e903d3a6a4 mark options as readonly 2022-05-09 14:12:31 +02:00
Bruno Windels
3888291758 updateOptions is unused,not the best idea since options is/can be shared 2022-05-09 14:10:50 +02:00
Bruno Windels
6beff7e552 override emitChange so no need to clone option object for all tiles
instead, we don't store the emitChange in the options but rather on
the tile itself.
2022-05-09 14:09:45 +02:00
RMidhunSuresh
139a87de99 Pass a copy of the options to the tiles 2022-05-08 19:14:51 +05:30
Eric Eastwood
e54482e4c0 Add some comments 2022-05-05 17:57:25 -05:00
Eric Eastwood
75098b4712 Merge branch 'master' into madlittlemods/686-682-local-friendly-development-and-commonjs 2022-05-05 17:50:33 -05:00
Eric Eastwood
d053d4388f Update Vite to avoid flakey errors in our PostCSS plugins
Fix https://github.com/vector-im/hydrogen-web/issues/722

Updating Vite to includes fixes from
https://github.com/vitejs/vite/issues/7822 -> https://github.com/vitejs/vite/pull/7827
2022-05-05 14:58:43 -05:00
Bruno Windels
23b621492f
Merge pull request #726 from vector-im/flow-registration
Allow passing in flowSelector from startRegistration method
2022-04-27 11:18:53 +02:00
RMidhunSuresh
83664a1b13 viewClassForTile is needed for TimelineView 2022-04-27 12:38:12 +05:30
RMidhunSuresh
c07a42292c Include Platform change in sdk docs 2022-04-27 12:28:48 +05:30
RMidhunSuresh
049a477008 Pass flowSelector from Client.startRegistration 2022-04-27 12:27:19 +05:30
Bruno Windels
fa34315210 undo refactoring typo from #723 2022-04-25 16:44:31 +02:00
Bruno Windels
bec8cea583 fix for breaking in #725 2022-04-25 14:17:07 +02:00
Bruno Windels
3536d12680
Merge pull request #725 from vector-im/bwindels/templateview-ts
add typing for text bindings in template view
2022-04-25 12:42:37 +02:00
Bruno Windels
ab893f63b5 remove unneeded assignment 2022-04-25 12:40:25 +02:00
Bruno Windels
6c57c96cb9 add typing for text bindings in template view 2022-04-25 12:07:28 +02:00
R Midhun Suresh
6ba5fbeebb
Merge pull request #723 from vector-im/implement-609
Read config.json on app start
2022-04-22 15:00:02 +05:30
RMidhunSuresh
d8da128780 remove await 2022-04-22 14:34:16 +05:30
RMidhunSuresh
7a33c2e00d await 2022-04-22 12:26:29 +05:30
RMidhunSuresh
5a94a2feba Move handleConfigRequest inside handleRequest 2022-04-22 12:22:30 +05:30
RMidhunSuresh
c6691cf1cb Simplify code 2022-04-22 12:10:25 +05:30
RMidhunSuresh
826835e518 No need to rewrite to index.html 2022-04-22 12:07:53 +05:30
RMidhunSuresh
b6e55ef59c Remove comment 2022-04-21 14:46:55 +05:30
RMidhunSuresh
4f23944581 Use named param in Legacy Platform 2022-04-21 14:17:47 +05:30
RMidhunSuresh
1cdc76f5a4 Use undefine instead of null 2022-04-21 14:14:38 +05:30
RMidhunSuresh
468b7e1595 Cache config.json 2022-04-21 12:52:42 +05:30
Eric Eastwood
ce289baba6 Remove extra space 2022-04-20 17:32:12 -05:00
Eric Eastwood
f1e07b6842 Explain what is being deleted by the strange syntax
See https://github.com/vector-im/hydrogen-web/pull/693#discussion_r815284713
2022-04-20 11:59:49 -05:00
Eric Eastwood
e9cee2e6a4 Merge branch 'master' into madlittlemods/686-682-local-friendly-development-and-commonjs
Conflicts:
	scripts/sdk/build.sh
2022-04-20 11:58:39 -05:00
Eric Eastwood
5f8a171c2c
Fix asset build throwing and swallowing errors (#721)
- Fix `svg-colorizer` throwing errors with Windows file paths
 - Fix `css-url-parser` swallowing errors because it was `async`
 - Fail SDK build script (`yarn build:sdk`, `build.sh`) overall when some commands are failing
2022-04-20 11:55:48 -05:00
RMidhunSuresh
6cd3c8ee2b Read config from URL 2022-04-20 12:42:07 +05:30
RMidhunSuresh
2cfcd4653f Use named params 2022-04-20 12:00:33 +05:30
Eric Eastwood
f56dc582a5 Fix tests after theme updates 2022-04-20 00:39:32 -05:00
Eric Eastwood
f61bf6090e Enable extended globs for removing all but some filename !(filename)
See https://github.com/vector-im/hydrogen-web/pull/693#discussion_r853534719
2022-04-19 17:28:09 -05:00
Eric Eastwood
12d6447b06 Merge branch 'master' into madlittlemods/686-682-local-friendly-development-and-commonjs
Conflicts:
	package.json
	scripts/sdk/base-manifest.json
	scripts/sdk/build.sh
2022-04-19 17:19:13 -05:00
Bruno Windels
480c5c1584 update SDK docs with new style location 2022-04-14 13:49:54 +02:00
Bruno Windels
2d6cbcfce0 release v0.2.28 2022-04-14 10:38:17 +02:00
Bruno Windels
78f352b839 avoid white ring around avatars in dark theme 2022-04-14 10:38:08 +02:00
Bruno Windels
cbdd7548da release v0.2.27 2022-04-14 09:53:21 +02:00
Bruno Windels
3b74e2ea7e
Merge pull request #712 from vector-im/theme-convert
Theming - Convert existing theme to use new theming architecture
2022-04-14 09:52:38 +02:00
RMidhunSuresh
3f4dddc004 Add backwards compatibility 2022-04-13 17:49:38 +05:30
RMidhunSuresh
5170329c79 Remove unsued imports 2022-04-13 17:44:07 +05:30
RMidhunSuresh
2d8a3d9f9b Fix SDK build 2022-04-13 17:12:38 +05:30
RMidhunSuresh
83dffef47d Use new theme config 2022-04-13 14:26:40 +05:30
RMidhunSuresh
23aac5cb45 Make theme-name lowercase in manifest 2022-04-13 14:26:40 +05:30
RMidhunSuresh
f7bfab6e08 Add develop only script tag to index.html 2022-04-13 14:26:40 +05:30
RMidhunSuresh
5e7432b5de Make badge font color always white 2022-04-13 14:26:40 +05:30
RMidhunSuresh
2de0450e97 Make colors better looking for dark variant 2022-04-13 14:26:40 +05:30
RMidhunSuresh
f26b51e5da Change colors in more css files 2022-04-13 14:26:40 +05:30
RMidhunSuresh
bf74c3c67b Add more colors to manifest 2022-04-13 14:26:40 +05:30
RMidhunSuresh
3d304be211 Convert theme.css
- Use color variables
- Use colorized icons
2022-04-13 14:26:40 +05:30
RMidhunSuresh
698d47e221 Enable plugins in config 2022-04-13 14:26:40 +05:30
RMidhunSuresh
3e2a2b7942 Add theme manifest 2022-04-13 14:26:40 +05:30
RMidhunSuresh
061dc5f824 Replace icon colors with predefined color 2022-04-13 14:26:40 +05:30
R Midhun Suresh
366e75b242
Merge pull request #716 from vector-im/vite-plugin-dev
Theming - Support theming in dev server
2022-04-13 14:20:04 +05:30
R Midhun Suresh
b76fb70579
Merge pull request #717 from vector-im/fix-css-url-processor
Theming - Fix css-url-processor
2022-04-13 14:19:36 +05:30
R Midhun Suresh
aacd0e6dfb
Merge pull request #718 from vector-im/fix-css-compile-variables
Theming - Some more changes for compile-variables plugin
2022-04-13 14:19:21 +05:30
RMidhunSuresh
bf0cdcd3f1 Add explaining comment 2022-04-13 13:39:20 +05:30
RMidhunSuresh
825c9847fe Don't hardcode theme/variant names 2022-04-13 12:56:14 +05:30
RMidhunSuresh
14523ecc5d Use new theme option in vite-config 2022-04-13 12:40:49 +05:30
RMidhunSuresh
efef7147af Modify jsdoc comment 2022-04-12 21:02:30 +05:30
RMidhunSuresh
39bc827aaf Invert operation for dark theme 2022-04-12 20:58:14 +05:30
RMidhunSuresh
bb9954a36c Let derive function know if theme is dark 2022-04-12 20:57:43 +05:30
RMidhunSuresh
0b241db058 Produce a mapping of aliases to resolved colors 2022-04-12 20:57:03 +05:30
RMidhunSuresh
743bd0db1c Support dark mode and remove dev script tag 2022-04-12 20:39:04 +05:30
RMidhunSuresh
25a8521efc Use hash instead of UUID 2022-04-12 20:15:14 +05:30
RMidhunSuresh
36782fb4fe Use unique filenames
Otherwise newly produced svgs will replace other svgs produced earlier
in the build.
2022-04-12 19:44:29 +05:30
RMidhunSuresh
6456d4ef76 Cache cssPath 2022-04-10 14:59:42 +05:30
RMidhunSuresh
49535807bf Do not run plugin on runtime theme 2022-04-10 14:59:08 +05:30
RMidhunSuresh
0a95eb0940 Fix formatting 2022-04-10 14:52:26 +05:30
RMidhunSuresh
ff98ef4465 Support theming in dev server 2022-04-10 14:49:19 +05:30
Bruno Windels
a6b6fef6d2 sdk release 0.0.10 2022-04-08 17:48:20 +02:00
Bruno Windels
c9bc080aef
Merge pull request #713 from vector-im/bwindels/fix-request-responsecode-error
fix error thrown during request when response code is not used
2022-04-08 15:26:12 +02:00
Bruno Windels
4cbd149c25
Merge pull request #715 from vector-im/bwindels/rename-viewclassfortile
Some timeline refactoring and also make reply tiles of correct custom view class
2022-04-08 15:19:39 +02:00
Bruno Windels
cf780ce259 also apply custom tiles in reply preview in composer 2022-04-08 15:16:22 +02:00
Bruno Windels
d21d10e4f2 pass in viewClassForTile from SessionView
so you can also use custom tiles when using the grid view
2022-04-08 15:15:21 +02:00
Bruno Windels
1fea14dd10 ensure other parameters don't get passed to TemplateView parent ctors 2022-04-08 15:04:38 +02:00
Bruno Windels
1f0cb542c8 pass viewClassForTile to tile views, so they can create reply view with correct subtile 2022-04-08 15:02:07 +02:00
Bruno Windels
57f50cc416 fix lint warnings 2022-04-08 15:01:27 +02:00
Bruno Windels
cda96a35ee rename viewClassForEntry to viewClassForTile 2022-04-08 15:01:06 +02:00
Bruno Windels
e977a6829b
Merge pull request #714 from vector-im/bwindels/custom-tiles
Allow custom timeline tiles for SDK usage
2022-04-08 14:29:54 +02:00
Bruno Windels
ac4bb8ca15 export tile view & view models from SDK 2022-04-08 14:27:08 +02:00
Bruno Windels
a913671f0c make tileClassForEntry optional, as otherwise it is a breaking change 2022-04-08 14:19:34 +02:00
Bruno Windels
5445db2a42 allow injecting the tilesCreator from the Root/Session/RoomViewModel
this changes the API slightly to be more future-proof,
as we'll expose it in the SDK now.

The function now returns a SimpleTile constructor, rather than an
instance. This allows us to test if an entry would render in the
timeline without creating a tile, which is something we might want in
the matrix layer later on.

The function is now called tileClassForEntry, analogue to what we
do in TimelineView.
2022-04-08 12:52:30 +02:00
Bruno Windels
220f35ae03 fix typescript error 2022-04-08 11:52:21 +02:00
Bruno Windels
6aa79cf6e2 allow to inject custom tile view creator fn into timeline view 2022-04-07 17:25:20 +02:00
Bruno Windels
88482292e1
Merge pull request #700 from vector-im/ajbura-patch-2
Add observeNavigation in ViewModel
2022-04-07 14:08:40 +02:00
Bruno Windels
9755062563 fix error thrown during request when response code is not used 2022-04-07 10:35:00 +02:00
R Midhun Suresh
0a225292f0
Merge pull request #704 from vector-im/vite-plugin
Theming - Rollup plugin to enumerate and compile themes (and their variants)
2022-04-07 11:57:03 +05:30
R Midhun Suresh
1b18b1f815
Merge pull request #707 from vector-im/css-url-processor
Theming - Postcss plugin to process URLs
2022-04-07 11:54:53 +05:30
RMidhunSuresh
c0fb8a2c77 Throw error if no replacements were made 2022-04-07 11:53:11 +05:30
RMidhunSuresh
f2b4f2e069 Remove console.log 2022-04-07 11:53:11 +05:30
RMidhunSuresh
7046fcc7c7 Find list of resolved colors from result
and also throw only if secondary color was provided
2022-04-07 11:53:11 +05:30
RMidhunSuresh
8c6400ab2c utf-8 --> utf8 2022-04-07 11:53:11 +05:30
RMidhunSuresh
5d5eb93baa Implement plugin 2022-04-07 11:53:11 +05:30
R Midhun Suresh
4ded893880
Merge pull request #703 from vector-im/css-url-variables-plugin
Theming - Postcss plugin to replace URL values with css variable
2022-04-07 11:44:46 +05:30
RMidhunSuresh
bfd73ae52a Pass derive function as argument 2022-04-07 11:37:20 +05:30
RMidhunSuresh
6d724e27e7 No need to check if icons are already written 2022-04-07 11:35:24 +05:30
RMidhunSuresh
2dd655cd9a Check if icon is in shared var 2022-04-07 11:35:24 +05:30
RMidhunSuresh
9a96112146 Rename function name 2022-04-07 11:35:24 +05:30
RMidhunSuresh
545ff2ec32 Add explaining comment 2022-04-07 11:35:24 +05:30
RMidhunSuresh
5e702171ce Remove console.log 2022-04-07 11:35:24 +05:30
RMidhunSuresh
cd4fce0c6f Populate shared map with collected icons 2022-04-07 11:35:24 +05:30
RMidhunSuresh
1a50effd86 Only extract into variables if file is svg 2022-04-07 11:35:24 +05:30
RMidhunSuresh
b7a47ae901 Give function better name 2022-04-07 11:35:24 +05:30
RMidhunSuresh
0a186dd11b Fix css logic 2022-04-07 11:35:24 +05:30
RMidhunSuresh
f07a3ea5b5 Remove css specific syntax from map 2022-04-07 11:35:24 +05:30
RMidhunSuresh
2d4ec5380e Initialize variables later 2022-04-07 11:35:24 +05:30
RMidhunSuresh
6b4bb762aa Remove unused variable 2022-04-07 11:35:24 +05:30
RMidhunSuresh
97ade0659c Add explaining comment 2022-04-07 11:35:24 +05:30
RMidhunSuresh
b59d6970fc Fix code duplication in tests 2022-04-07 11:35:21 +05:30
RMidhunSuresh
cbff912476 Improve code quality 2022-04-07 11:34:11 +05:30
RMidhunSuresh
3ae2b4dab4 Use two url() in test 2022-04-07 11:34:11 +05:30
RMidhunSuresh
f897e5132c Implement url to variables plugin 2022-04-07 11:34:11 +05:30
R Midhun Suresh
e0bc9b31a9
Merge pull request #709 from vector-im/compile-variables-improvement
Theming - postcss-compile-variables improvement
2022-04-07 11:31:59 +05:30
RMidhunSuresh
f75ee86c0e Change comment 2022-04-06 12:30:26 +05:30
RMidhunSuresh
7f9af5b5fa Add icon to manifest 2022-04-06 12:30:26 +05:30
RMidhunSuresh
b0f082e81f Add derived variables to source section 2022-04-06 12:30:26 +05:30
RMidhunSuresh
d5b5e10230 Produce manifest.jsom 2022-04-06 12:30:26 +05:30
RMidhunSuresh
86c45b5b99 Emit runtime bundle 2022-04-06 12:30:26 +05:30
RMidhunSuresh
32eb95734a Add default themes to index html 2022-04-06 12:30:26 +05:30
RMidhunSuresh
1f6efb4db3 Write plugin code 2022-04-06 12:30:26 +05:30
RMidhunSuresh
48d0242c80 Also derive variables in URLs 2022-04-06 12:23:55 +05:30
Eric Eastwood
2401b7f453 Add way to test whether SDK works in ESM and CommonJS 2022-04-05 19:24:27 -05:00
Eric Eastwood
dd06d78a72 Avoid ERR_REQUIRE_ESM errors when requiring SDK 2022-04-05 18:17:14 -05:00
Eric Eastwood
95d17303c3 Update Vite which includes fixes to importing *.js?url with exports
Update to Vite which includes https://github.com/vitejs/vite/pull/7098
2022-04-05 17:16:55 -05:00
Eric Eastwood
d247bc4e28 Merge branch 'master' into madlittlemods/686-682-local-friendly-development-and-commonjs
Conflicts:
	package.json
	scripts/sdk/base-manifest.json
2022-04-05 17:15:30 -05:00
RMidhunSuresh
454345c9b2 Always set map 2022-04-05 15:08:35 +05:30
RMidhunSuresh
76789eacf1 Use array instead of Set 2022-04-01 20:43:42 +05:30
RMidhunSuresh
859449ed60 Write test for map population 2022-04-01 16:41:00 +05:30
RMidhunSuresh
918a3e42b1 Populate compiled variables map 2022-04-01 16:23:33 +05:30
RMidhunSuresh
4350d2f264 Don't derive variables for runtime theme 2022-04-01 16:20:58 +05:30
RMidhunSuresh
2015fa2d7a Move postcss-value-parser to dev dependency 2022-03-27 20:18:42 +05:30
RMidhunSuresh
e8bd1f3390 Pass result as message 2022-03-27 20:06:26 +05:30
R Midhun Suresh
66304ed7e0
Merge pull request #701 from vector-im/css-compile-variables-plugin
Theming - Postcss plugin to compile variables
2022-03-24 12:14:46 +05:30
RMidhunSuresh
72785e7c3e Remove -- from everywhere 2022-03-23 20:39:24 +05:30
RMidhunSuresh
59ca8e6309 Add explanation of plugin 2022-03-23 17:25:12 +05:30
RMidhunSuresh
5d4323cd1d Remove stray "--" from code 2022-03-23 17:12:14 +05:30
RMidhunSuresh
19a6d669a9 Extract base variables from css 2022-03-14 23:26:37 +05:30
RMidhunSuresh
bca1648df6 Move plugin to /scripts and create eslintrc 2022-03-14 11:35:10 +05:30
RMidhunSuresh
4020ade70c Remove redundant comment 2022-03-10 17:51:25 +05:30
RMidhunSuresh
2c068cc3ce typo 2022-03-10 17:42:12 +05:30
RMidhunSuresh
6f4a7e074a Change confusing doc 2022-03-10 17:27:12 +05:30
RMidhunSuresh
9f77df0bff Match regex only if declaration is a variable 2022-03-10 17:24:32 +05:30
RMidhunSuresh
ff10297bf8 Explicitly convert to number 2022-03-10 17:22:02 +05:30
RMidhunSuresh
f732164b5f Formatting change 2022-03-10 17:21:38 +05:30
RMidhunSuresh
5210123977 Document options 2022-03-10 17:19:04 +05:30
RMidhunSuresh
1663782954 Throw after fetching value 2022-03-10 16:05:13 +05:30
RMidhunSuresh
63c1f2a7a3 Add node as env to eslint 2022-03-09 17:22:45 +05:30
RMidhunSuresh
96fa83b508 Add license header 2022-03-09 17:22:11 +05:30
RMidhunSuresh
79f363fb9d Move code to callback and fix alias code 2022-03-09 17:20:05 +05:30
Bruno Windels
ca211f929b
Merge pull request #702 from vector-im/bwindels/observablemapts
convert (Base)ObservableMap to typescript
2022-03-09 11:53:59 +01:00
Bruno Windels
6150e91c3f fix type error again 2022-03-09 11:51:11 +01:00
Bruno Windels
762925d4a5 fix type error 2022-03-09 11:44:49 +01:00
Bruno Windels
21080d2110 fix tests 2022-03-09 11:41:26 +01:00
Bruno Windels
6d7c983e8e convert (Base)ObservableMap to typescript 2022-03-09 11:33:49 +01:00
RMidhunSuresh
a83850ebf3 Use postcss value parser to find variables 2022-03-09 11:48:53 +05:30
RMidhunSuresh
41f6b6ab6b Use startsWith instead of regex testing 2022-03-07 13:25:53 +05:30
RMidhunSuresh
a5d46bb40c Move over tests to Hydrogen using impunity 2022-03-07 13:10:44 +05:30
RMidhunSuresh
f170ef0206 Switch over to off-color 2022-03-07 11:38:39 +05:30
Ajay Bura
e07abfa02a
Add missing type 2022-03-07 11:33:51 +05:30
RMidhunSuresh
b6f5e68e9e Format file 2022-03-07 11:33:44 +05:30
RMidhunSuresh
92084e8005 Move all code under the Once event
Apparently the other events are common to all plugins.
2022-03-07 11:32:30 +05:30
Bruno Windels
8b8233ff00
Merge pull request #691 from vector-im/madlittlemods/only-crypto-in-secure-context
Only initialize `Crypto` when olm is provided
2022-03-03 17:33:50 +01:00
RMidhunSuresh
60d60e9572 WIP 2022-03-03 19:58:46 +05:30
Ajay Bura
61ce2f9e3d
Add observeNavigation in ViewModel 2022-03-03 15:36:25 +05:30
Eric Eastwood
2f4c639cef Only initialize Crypto when olm is provided
See https://github.com/vector-im/hydrogen-web/pull/691#discussion_r816988082
2022-03-02 03:17:59 -06:00
Eric Eastwood
c09964dc30
Add data-event-id="$xxx" attributes to timeline items for easy selecting in end-to-end tests (#690)
Split out from https://github.com/vector-im/hydrogen-web/pull/653

Example test assertions: db6d3797d7/test/e2e-tests.js (L248-L252)

```js
// Make sure the $abc event on the page has "foobarbaz" text in it
assert.match(
  dom.document.querySelector(`[data-event-id="$abc"]`).outerHTML,
  new RegExp(`.*foobarbaz.*`)
);
```
2022-03-01 18:36:14 -06:00
Bruno Windels
2e1283d199
Merge pull request #670 from vector-im/bwindels/ts-olm
Convert olm code to typescript
2022-03-01 18:53:22 +01:00
Bruno Windels
62ce111938
Merge pull request #692 from ryushar/ryushar/typescriptify
Convert domain/avatar.js and domain/LogoutViewModel.js to Typescript
2022-03-01 18:50:19 +01:00
Bruno Windels
770f7aea00
Merge pull request #689 from vector-im/madlittlemods/add-more-html-elements
Add more HTML form and SVG elements
2022-03-01 18:43:34 +01:00
Bruno Windels
b6d9993ed0 remove unused import 2022-03-01 17:08:49 +01:00
Bruno Windels
643ab1a5f3 cant export this for some reason 2022-03-01 15:48:42 +01:00
Bruno Windels
42141c7063 bump SDK version 2022-03-01 15:45:24 +01:00
Bruno Windels
1087d62705
Merge pull request #695 from vector-im/ajbura-patch-1
Export some more symbols from the SDK
2022-03-01 15:44:51 +01:00
Bruno Windels
ee8e45926f also export observable value classes 2022-03-01 15:42:04 +01:00
Bruno Windels
4c50dbf7ec make SDK exports explicit 2022-03-01 15:41:44 +01:00
Ajay Bura
4a4856a29e
export module 2022-02-28 17:19:01 +05:30
Eric Eastwood
0023ab34ba Add a placeholder for upgrading vite to comment on 2022-02-26 05:19:59 -06:00
Eric Eastwood
8fb2b2755a Fix typos pointing to wrong files 2022-02-26 03:08:16 -06:00
Eric Eastwood
cd007b40e1 Make the SDK friendly to locally link and develop on
Fix https://github.com/vector-im/hydrogen-web/issues/686
Fix https://github.com/vector-im/hydrogen-web/issues/682

Instead of deleting the whole `target/` directory, leave it alone so the symlink
driving the `npm link`/`yarn link` stays in tact.

Leave Vite builds in their build directories (`/lib-build`/`/asset-build`)
so you can `vite build --watch` to build on local changes and still have a
consisent place to reference in the `package.json` `exports`. Previously,
everything relied on `build.sh` which does a bunch of moving and renaming
and made it hard to rebuild on changes.

Add back support for CommonJS (adding the `package.json` `exports`).

The last piece is making sure the `?url` imports (`import workerPath from 'hydrogen-view-sdk/main.js?url';`)
work still. It looks like this may have just been solved via
https://github.com/vitejs/vite/issues/6725 -> https://github.com/vitejs/vite/pull/7073
(literally 2 days ago) and we just need to wait for the next Vite release 🎉
2022-02-26 01:12:00 -06:00
Tushar
17acda7741 typescriptify domain/LogoutViewModel.js 2022-02-25 16:45:07 +05:30
Tushar
7055f02f16 typescriptify domain/avatar.js 2022-02-25 15:52:54 +05:30
Eric Eastwood
0935f2d23a Only try to use window.crypto.subtle in secure contexts to avoid it throwing and stopping all JavaScript
Relevant error if you crypto is used in a non-secure context like a local LAN IP `http://192.168.1.151:3050/`
```
Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'deriveBits')
	at new Crypto
	at new Platform
	at mountHydrogen
```

For my use-case with https://github.com/matrix-org/matrix-public-archive, I don't need crypto/encryption at all.

Docs:

 - https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts
 - https://developer.mozilla.org/en-US/docs/Web/API/Crypto/subtle
    - "Secure context: This feature is available only in secure contexts (HTTPS), in some or all supporting browsers."

---

Related to https://github.com/vector-im/hydrogen-web/issues/579
2022-02-25 01:59:48 -06:00
Eric Eastwood
b993331e06 Add more HTML form and SVG elements
Split off from https://github.com/vector-im/hydrogen-web/pull/653

Personally using `select`, `option`, and `path` currently in https://github.com/matrix-org/matrix-public-archive
but added a few extra SVG elements that seemed common to me.
2022-02-25 01:40:52 -06:00
Bruno Windels
8adc5a9fae these were public actually 2022-02-18 17:24:55 +01:00
Bruno Windels
3f9f0e98c7 remove unused olm property in SenderKeyDecryption 2022-02-18 17:21:27 +01:00
Bruno Windels
82299e5aea
Update src/matrix/e2ee/olm/Decryption.ts
Co-authored-by: R Midhun Suresh <hi@midhun.dev>
2022-02-18 17:18:33 +01:00
Bruno Windels
3330530f68
Update src/matrix/e2ee/DecryptionResult.ts
Co-authored-by: R Midhun Suresh <hi@midhun.dev>
2022-02-18 17:18:25 +01:00
Bruno Windels
620409b3f0 fixup: ctor argument order
as it was an object before, order didn't matter
2022-02-18 17:17:24 +01:00
Bruno Windels
78e0bb1ff0 replace isPreKeyMessage with const enum 2022-02-18 17:00:56 +01:00
Bruno Windels
347edb5988 remove unused storage property 2022-02-18 16:47:47 +01:00
Bruno Windels
0ff1a01b42
Merge pull request #669 from vector-im/bwindels/contrib
Add guide for new contributers
2022-02-17 17:46:57 +01:00
Bruno Windels
91fd0e433a remove changelog notes remainder 2022-02-17 17:44:44 +01:00
Bruno Windels
cdd6112971 finish adapting contribution guide 2022-02-17 17:39:45 +01:00
Bruno Windels
ac48a5a4df bump SDK version to 0.0.8 2022-02-17 10:10:23 +01:00
Bruno Windels
49f6a2c2eb
Merge pull request #679 from vector-im/bwindels/fix-vm-ctor-default-options
always pass options to ViewModel constructor
2022-02-17 10:10:19 +01:00
Bruno Windels
2821f4d396
Merge pull request #680 from vector-im/bwindels/export-roomstatus
export RoomStatus in SDK
2022-02-17 09:51:12 +01:00
Bruno Windels
2472f11ec0 export RoomStatus 2022-02-17 09:47:57 +01:00
Bruno Windels
7f1fed6f8c always pass options to ViewModel constructor 2022-02-17 09:24:18 +01:00
Bruno Windels
d971fd1a47
Merge pull request #678 from vector-im/fix-viewmodel-error
Check options exist on emitChange
2022-02-17 09:08:54 +01:00
RMidhunSuresh
498a43327f Check if options exist in emitChange 2022-02-17 11:30:04 +05:30
Bruno Windels
d9acc83182
Merge pull request #675 from vector-im/bwindels/fix-lint-timeline-import
fix lint
2022-02-16 18:05:52 +01:00
Bruno Windels
60f5da60bb fix lint 2022-02-16 18:01:24 +01:00
Bruno Windels
e3e90ed167 convert olm/Encryption to TS 2022-02-16 18:00:13 +01:00
Bruno Windels
eb5ca200f2 missed rename here 2022-02-16 18:00:03 +01:00
Bruno Windels
61b264be3b bump sdk version to 0.0.7 2022-02-16 10:20:53 +01:00
Bruno Windels
37cec04e9c
Merge pull request #671 from vector-im/token-auth-registration
Implement token authenticated registration
2022-02-16 10:20:33 +01:00
RMidhunSuresh
7a9298328f Return _type from getter 2022-02-16 14:37:18 +05:30
RMidhunSuresh
a76bcd1739 Changes in TokenAuth 2022-02-16 13:36:24 +05:30
RMidhunSuresh
60bc4450f3 Use type from server 2022-02-16 13:21:04 +05:30
RMidhunSuresh
ed151c8567 Return token stage from createRegistrationStage 2022-02-16 12:33:59 +05:30
RMidhunSuresh
c40801efd9 Implement the registration stage 2022-02-16 12:33:24 +05:30
Bruno Windels
a4fd1615dd convert decryption 2022-02-15 18:21:29 +01:00
Bruno Windels
74c640f937 convert Session 2022-02-15 18:21:12 +01:00
Bruno Windels
7aeda70ff6 convert DecryptionResult 2022-02-15 18:20:14 +01:00
Bruno Windels
c6dde63abd
Merge pull request #668 from vector-im/bwindels/ts-viewmodel
convert ViewModel to typescript
2022-02-15 15:38:22 +01:00
Bruno Windels
dea1e7eaf3 bump sdk version 2022-02-15 11:31:50 +01:00
Bruno Windels
7179758c50 also here 2022-02-15 08:22:09 +01:00
Bruno Windels
1a159f9e9a WIP 2022-02-14 18:01:04 +01:00
Bruno Windels
1795f58ba5 rename imports 2022-02-14 17:53:59 +01:00
Bruno Windels
4d82dd22b6 convert ViewModel to typescript 2022-02-14 17:50:17 +01:00
Bruno Windels
460780d602
Merge pull request #666 from vector-im/madlittlemods/explicit-domparser-document-for-consistent-return-with-linkedom-ssr
Fix missing reply text when message body is parsed as HTML in `linkedom` (SSR)
2022-02-14 09:57:31 +01:00
Eric Eastwood
dfed04166e Fix missing reply text when message body is parsed as HTML in [linkedom](https://github.com/WebReflection/linkedom) (SSR).
- [`linkedom`](https://github.com/WebReflection/linkedom) is being used https://github.com/matrix-org/matrix-public-archive to server-side render (SSR) Hydrogen (`hydrogen-view-sdk`)
 - This is being fixed by using a explicit HTML wrapper boilerplate with `DOMParser` to get a matching result in the browser and `linkedom`.

Currently `parseHTML` is only used for HTML content bodies in events. Events with replies have content bodies that look like `<mx-reply>Hello</mx-reply> What's up` so they're parsed as HTML to strip out the `<mx-reply>` part.

Before | After
---  |  ---
![](https://user-images.githubusercontent.com/558581/153692011-2f0e7114-fcb4-481f-b217-49f461b1740a.png) | ![](https://user-images.githubusercontent.com/558581/153692016-52582fdb-abd9-439d-9dce-3f04da6959db.png)

Before:
```js
// Browser (Chrome, Firefox)
new DOMParser().parseFromString(`<div>foo</div>`, "text/html").body.outerHTML;
// '<body><div>foo</div></body>'

// `linkedom` 
new DOMParser().parseFromString(`<div>foo</div>`, "text/html").body.outerHTML;
// '<body></body>'
```

After (consistent matching output):

```js
// Browser (Chrome, Firefox)
new DOMParser().parseFromString(`<!DOCTYPE html><html><body><div>foo</div></body></html>`, "text/html").body.outerHTML;
// '<body><div>foo</div></body>'

// `linkedom`
new DOMParser().parseFromString(`<!DOCTYPE html><html><body><div>foo</div></body></html>`, "text/html").body.outerHTML;
// '<body><div>foo</div></body>'
```

`linkedom` goal is to be close to the current DOM standard, but [not too close](https://github.com/WebReflection/linkedom#faq). Focused on the streamlined cases for server-side rendering (SSR).

Here is some context around getting `DOMParser` to interpret things better. The conclusion was to only support the explicit standard cases with a `<html><body></body></html>` specified instead of adding the magic HTML document creation and massaging that the browser does.

 - https://github.com/WebReflection/linkedom/issues/106
 - https://github.com/WebReflection/linkedom/pull/108

 ---

Part of https://github.com/vector-im/hydrogen-web/pull/653 to support server-side rendering Hydrogen for the [`matrix-public-archive`](https://github.com/matrix-org/matrix-public-archive) project.
2022-02-11 20:10:46 -06:00
Bruno Windels
75e2618f70
Merge pull request #664 from vector-im/bwindels/onlylogsummarykeys
dont log summary valued, as they can contain PII
2022-02-11 18:41:26 +01:00
Bruno Windels
9685ef4dd3 dont log summary valued, as they can contain PII 2022-02-11 18:39:37 +01:00
Bruno Windels
750f3cd8ff release v0.2.26 2022-02-11 17:20:54 +01:00
Bruno Windels
34ec0e2c82
Merge pull request #663 from vector-im/bwindels/fix-reply-not-loading
Fix replies from /context not loading in e2ee rooms
2022-02-11 17:20:06 +01:00
Bruno Windels
ea8f3e5a6a remove argument that is already bound in BaseRoom, making decryption fail 2022-02-11 17:14:56 +01:00
Bruno Windels
a184ad528f
Merge pull request #654 from vector-im/bwindels/create-room
Create rooms
2022-02-11 16:56:24 +01:00
Bruno Windels
57b1542688 use private topic field as public one got removed as not needed in view 2022-02-11 09:37:56 +01:00
Bruno Windels
175f869c83 fix lint 2022-02-10 20:07:27 +01:00
Bruno Windels
a442b4b009 Merge branch 'master' into bwindels/create-room 2022-02-10 20:05:40 +01:00
Bruno Windels
d65b25f084 also adjust m.direct if the room has already been replaced 2022-02-10 20:00:01 +01:00
Bruno Windels
2765f48a64 create user id array in m.direct if it doesn't exist already 2022-02-10 19:59:44 +01:00
Bruno Windels
d2008a336b fix lint errors 2022-02-10 19:54:47 +01:00
Bruno Windels
ff46d382ac adjust m.direct when creating a DM 2022-02-10 19:54:15 +01:00
Bruno Windels
3adb2c3254 fix ts errors 2022-02-10 16:44:40 +01:00
Bruno Windels
8526461d3c split up create code into separate files 2022-02-10 16:43:32 +01:00
Bruno Windels
15eecbb463 cleanup 2022-02-10 16:28:44 +01:00
Bruno Windels
30c8ea29b2 fix bug where the wrong left panel tile is removed when accepting invite
because when comparing a tile to itself it wasn't returned 0
2022-02-10 16:27:32 +01:00
Bruno Windels
b0d790543a push to navigation in SessionViewModel rather than RVO 2022-02-10 14:57:48 +01:00
Bruno Windels
2c1b29e637 remove logging 2022-02-10 14:39:41 +01:00
Bruno Windels
75bbde598d also consider rooms without a name and just you and the other a DM
as we don't process m.direct account data yet
2022-02-10 14:39:18 +01:00
Bruno Windels
955a6bd6f9 styling for button in member details to open DM 2022-02-10 14:38:12 +01:00
Bruno Windels
147810864f add support to set alias and federation flag in create room 2022-02-10 14:09:18 +01:00
Bruno Windels
4c0167ed74 don't show spinner in left panel when room creation fails 2022-02-10 11:19:43 +01:00
Bruno Windels
024a6c06aa handle offline error nicer 2022-02-10 11:11:15 +01:00
Bruno Windels
b5536830d0 improve RoomBeingCreatedView, allow removing the roombeingcreated 2022-02-10 11:07:29 +01:00
Bruno Windels
20493f9e87 cleanup 2022-02-10 11:07:13 +01:00
Bruno Windels
e8c20c28b2 allow passing label into LoadingView
also doesn't need to be a template view, as it doesn't have bindings
or event handlers
2022-02-10 11:06:44 +01:00
Bruno Windels
f12841b2d3 better error handling in RoomBeingCreated 2022-02-10 11:06:20 +01:00
Bruno Windels
d6d1af13d0 rename RoomBeingCreated.localId to id 2022-02-10 11:03:52 +01:00
Bruno Windels
bbb1683dbf fixup: login view styling 2022-02-10 09:40:42 +01:00
Bruno Windels
fed42f13ad textarea styling 2022-02-10 09:40:30 +01:00
Bruno Windels
5f6308e7c4 fix homeserver field style in login view 2022-02-10 09:40:19 +01:00
Bruno Windels
74f7879cb6 fix unrelated bug: invite sorting order wasn't stable in left panel
as the timestamp is the same when you receive the invite during your
first sync
2022-02-10 09:40:03 +01:00
Bruno Windels
5c085efc10 create room view and view model 2022-02-09 19:02:51 +01:00
Bruno Windels
a1e14c4eec rename to not have conflict between method name and instance of CreateRoomViewModel 2022-02-09 19:02:18 +01:00
Bruno Windels
4b1be30dc0 improve form-row classes so they can work with create room form 2022-02-09 19:01:35 +01:00
Bruno Windels
8523f6feaf setup navigation for create room form 2022-02-09 19:00:41 +01:00
Bruno Windels
83d2b58bad add avatar support to creating room 2022-02-09 19:00:00 +01:00
Bruno Windels
afe8e17a6f remove debugging code 2022-02-08 17:00:06 +01:00
Bruno Windels
743f2270e5 have a single tile view that supports all 3 view models 2022-02-08 16:22:44 +01:00
Bruno Windels
5325b0b466 cleanup logging 2022-02-08 14:58:29 +01:00
Bruno Windels
d7b024eac1 unrelated fix: encode user name in matrix.to link 2022-02-08 14:35:14 +01:00
Bruno Windels
45c8e3a793 mark room as DM based on synced state events,rather than just inviteData
as that does not work for rooms we create ourselves
2022-02-08 14:34:34 +01:00
Bruno Windels
e04463c143 WIP for finding DM room 2022-02-07 18:58:53 +01:00
Bruno Windels
26fa2a5d60 add option 2022-02-07 18:58:43 +01:00
Bruno Windels
e1fbd1242e WIP 4 2022-02-07 16:30:44 +01:00
Bruno Windels
b868734378 change sdk version 2022-02-07 11:05:28 +01:00
Bruno Windels
0bb3cfcfad WIP3 2022-02-04 17:49:10 +01:00
Bruno Windels
3ff39a9549
Merge pull request #661 from vector-im/sdk-additions
Export more code from SDK
2022-02-04 17:43:23 +01:00
Bruno Windels
94709fd316
Merge pull request #623 from vector-im/registration
Bootstrap enough registration functionality for embedded-hydrogen work
2022-02-04 17:43:02 +01:00
RMidhunSuresh
4a0db9f984 Add required exports 2022-02-04 18:28:17 +05:30
RMidhunSuresh
28931f4103 Use async/await 2022-02-04 17:48:42 +05:30
RMidhunSuresh
f7f32ac806 responseCodeReject may not exist 2022-02-04 17:39:52 +05:30
RMidhunSuresh
a163cee18d Remove dead imports 2022-02-04 17:25:30 +05:30
RMidhunSuresh
0828ac12b1 Fix params 2022-02-04 17:25:15 +05:30
RMidhunSuresh
b59f916824 Merge branch 'registration' of github.com:vector-im/hydrogen-web into registration 2022-02-04 17:16:32 +05:30
R Midhun Suresh
2ac63e78ca
mark method as internal
Co-authored-by: Bruno Windels <bruno@windels.cloud>
2022-02-04 17:16:15 +05:30
RMidhunSuresh
028b96e4c5 Let type also be undefined 2022-02-04 17:11:33 +05:30
RMidhunSuresh
22d5505a2b Create registration stage in Registration itself 2022-02-04 16:50:22 +05:30
RMidhunSuresh
e66549a067 Remove dead code 2022-02-04 16:40:49 +05:30
RMidhunSuresh
e8c480426a Remove error code 2022-02-04 16:37:43 +05:30
RMidhunSuresh
891375a885 Rename allowerErrors -> allowedStatusCodes 2022-02-04 16:35:47 +05:30
RMidhunSuresh
32af7e6f09 Make more changes
- make setter a method
- lazily create promise
2022-02-04 16:23:39 +05:30
Bruno Windels
0b04612d6c WIP2 2022-02-04 11:16:58 +01:00
RMidhunSuresh
3d8b9cce41 Fix responseCode in Request 2022-02-04 15:41:37 +05:30
Bruno Windels
bc09ede09f WIP 2022-02-03 17:57:35 +01:00
RMidhunSuresh
b6e1d4a7d5 Implement responseCode() 2022-02-03 19:41:14 +05:30
RMidhunSuresh
89a97537b0 Make methods private + some props readonly 2022-02-03 19:41:14 +05:30
RMidhunSuresh
8a3c0afba6 Fix incorrect types 2022-02-03 19:41:11 +05:30
RMidhunSuresh
0ad0ecfcc2 Check response code instead of existence of props 2022-02-03 19:40:25 +05:30
RMidhunSuresh
c4894f2c24 completed is not always present 2022-02-03 19:40:25 +05:30
RMidhunSuresh
e64f4ad7b2 Refactor code
- Move all code that does /register to Registration.ts
- RegistrationStage only deals with the generation of auth data
- Change API so that undefined is returned instead of string when
  registration is over
2022-02-03 19:40:25 +05:30
R Midhun Suresh
2aad5546bf No need for Object.assign here either
Co-authored-by: Bruno Windels <brunow@matrix.org>
2022-02-03 19:40:25 +05:30
RMidhunSuresh
7bacbec5e9 Remove type directory 2022-02-03 19:40:25 +05:30
RMidhunSuresh
e13040a49e Don't mutate flows 2022-02-03 19:40:25 +05:30
R Midhun Suresh
30cb9f6d15 Use includes instead of elaborate find
Co-authored-by: Bruno Windels <brunow@matrix.org>
2022-02-03 19:40:25 +05:30
RMidhunSuresh
a351a185a0 Give proper names 2022-02-03 19:40:25 +05:30
RMidhunSuresh
fe0add01ee Use union of types for RegistrationResponse 2022-02-03 19:40:25 +05:30
RMidhunSuresh
a249a1b2b5 Implement flow seclector 2022-02-03 19:40:25 +05:30
RMidhunSuresh
6798a5e429 Move types to types.ts 2022-02-03 19:40:25 +05:30
RMidhunSuresh
3a67da8830 Refactor type
- Change name
- Move union type down
2022-02-03 19:40:25 +05:30
RMidhunSuresh
1d4b079d0c Type RegistrationResponse 2022-02-03 19:40:25 +05:30
RMidhunSuresh
49ade61ef6 Fill in ts types + change names 2022-02-03 19:40:25 +05:30
RMidhunSuresh
b482d478b4 Add a tos getter 2022-02-03 19:40:25 +05:30
RMidhunSuresh
ac7108b882 Throw error instead of returning it 2022-02-03 19:40:25 +05:30
RMidhunSuresh
7bb7189c6a No need for this export 2022-02-03 19:40:25 +05:30
RMidhunSuresh
6eba60bd75 Use typescript style that was agreed on earlier 2022-02-03 19:40:25 +05:30
RMidhunSuresh
5de1fc1453 Remove unnecessary getters 2022-02-03 19:40:25 +05:30
RMidhunSuresh
2f3865d8cc firstStage should be a local variable 2022-02-03 19:40:25 +05:30
RMidhunSuresh
2d4c106542 REFACTOR: Inline method 2022-02-03 19:40:25 +05:30
RMidhunSuresh
a91ba4370d Change type to show that username is optional 2022-02-03 19:40:25 +05:30
RMidhunSuresh
550a560f40 Remove space 2022-02-03 19:40:25 +05:30
RMidhunSuresh
5f11790f6b Object.assign is overkill here 2022-02-03 19:40:25 +05:30
RMidhunSuresh
e8dbbd876c Give default values to parameters 2022-02-03 19:40:25 +05:30
RMidhunSuresh
755f934eb2 No need to explicitly pass in inhibitLogin 2022-02-03 19:40:25 +05:30
RMidhunSuresh
5e93e048ab Don't cache GET requests 2022-02-03 19:40:25 +05:30
RMidhunSuresh
bb6a885116 Specify what errors are ignored in options 2022-02-03 19:40:25 +05:30
RMidhunSuresh
420c12f202 Copy over username only if it exists 2022-02-03 19:40:25 +05:30
RMidhunSuresh
792d5c62c5 Return username when registration is completed 2022-02-03 19:40:25 +05:30
RMidhunSuresh
fa2e2bc8f3 Allow register without providing username 2022-02-03 19:40:25 +05:30
RMidhunSuresh
170d7a5e55 Add startRegistration method 2022-02-03 19:40:25 +05:30
RMidhunSuresh
8ab8726b8f Implement m.login.terms stage 2022-02-03 19:40:25 +05:30
RMidhunSuresh
18e2fc1089 Pass in params to BaseRegistrationStage 2022-02-03 19:40:25 +05:30
RMidhunSuresh
a59b67ec45 Fix errors 2022-02-03 19:40:25 +05:30
RMidhunSuresh
d76a059525 Temporary fix for 401 errors 2022-02-03 19:40:25 +05:30
RMidhunSuresh
d28ab919bb Implement dummy registration logic 2022-02-03 19:40:25 +05:30
RMidhunSuresh
eb146830ba Implement registration endpoint 2022-02-03 19:40:25 +05:30
RMidhunSuresh
618d02d838 fetch registration flows 2022-02-03 19:40:25 +05:30
Bruno Windels
348de312f9 draft code in matrix layer to create room 2022-02-02 10:19:49 +01:00
Bruno Windels
65dcf8bc36 release v0.2.25 2022-02-01 12:34:42 +01:00
Bruno Windels
2e3616e05d call cursor.update during backup field migration, needs new version 2022-02-01 12:31:10 +01:00
Bruno Windels
00c5e747d2 log total backed up keys during flush operation 2022-02-01 12:30:45 +01:00
Bruno Windels
b29ecd339d add more logging to backup storage migration 2022-02-01 12:18:28 +01:00
Bruno Windels
c6820eccab release v0.2.24 2022-02-01 11:58:47 +01:00
Bruno Windels
247d13f97a
Merge pull request #651 from vector-im/bwindels/write-session-backup
Session backup writing
2022-02-01 11:54:53 +01:00
Bruno Windels
f4fa013ebc mark as not configured yet when re-enabling key backup 2022-02-01 11:32:53 +01:00
Bruno Windels
f4bb420f35 mark key backup properly as disabled 2022-02-01 11:27:42 +01:00
Bruno Windels
02f06724d0 don't block reenabling 4s if already enabled 2022-02-01 11:26:00 +01:00
Bruno Windels
fd4eb6b50d distinguish between "waiting to go online" vs "backup not configured" 2022-02-01 11:08:13 +01:00
Bruno Windels
997666164c remove unused enum variants 2022-01-31 17:37:44 +01:00
Bruno Windels
9c599d53aa allow to inject max delay in key backup 2022-01-31 17:31:01 +01:00
Bruno Windels
62acd458c6 also ask for new key if backup version is not found 2022-01-31 17:30:51 +01:00
Bruno Windels
17275a5390 backup 200 keys per request 2022-01-31 17:30:15 +01:00
Bruno Windels
830786b2fd fixes and cleanup 2022-01-31 16:26:14 +01:00
Bruno Windels
06a1421e97 add backupWriteStatus so binding can take multiple fields into account 2022-01-31 16:26:06 +01:00
Bruno Windels
6541aacf98 don't discount already finished keys in total for previous iterations 2022-01-31 16:23:48 +01:00
Bruno Windels
dacaa86386 fix percentage calculation 2022-01-31 16:22:22 +01:00
Bruno Windels
a757fb3696 better error handling in key backup, cleanup and not overuse observables 2022-01-31 14:37:05 +01:00
Bruno Windels
7eb0d347f5 flush key backup after coming online 2022-01-31 14:36:35 +01:00
Bruno Windels
ae5cc17290 mark all inbound sessions to be backed up again when changing version 2022-01-31 14:36:04 +01:00
Bruno Windels
d9e6164a5c fix ts errors 2022-01-28 16:40:32 +01:00
Bruno Windels
a97d235cf5 flush after enabling key backup 2022-01-28 16:36:42 +01:00
Bruno Windels
c9b5ce6508 clean up key backup vm using flatMap to avoid subscription handling 2022-01-28 16:36:13 +01:00
Bruno Windels
e0df003aba add flatMap operator on observable value 2022-01-28 16:35:49 +01:00
Bruno Windels
c340746a87 also remove text nodes when updating message body
fixes #649
2022-01-28 16:04:56 +01:00
Bruno Windels
eabd303c8e count on the index if we're using one, don't always take the store 2022-01-28 15:14:58 +01:00
Bruno Windels
bd2c70b923 adapt key backup view(model) to changes in session, show backup progress 2022-01-28 15:14:23 +01:00
Bruno Windels
504f420293 make keyBackup an observable and don't have separate needs-key flag 2022-01-28 15:13:58 +01:00
Bruno Windels
eb134a6c47 only take into account non-backed up keys for counting 2022-01-28 13:18:03 +01:00
Bruno Windels
7d3e3b992b some more typing 2022-01-28 13:14:38 +01:00
Bruno Windels
c47bdd5715 flush key backup when creating a new room key 2022-01-28 13:14:11 +01:00
Bruno Windels
b692b3ec4f move key backup operation and flush bookkeeping inside KeyBackup
so we can flush from other places than Session
2022-01-28 13:13:23 +01:00
Bruno Windels
ebc7f1ecd7 needs to be awaited 2022-01-28 13:11:52 +01:00
Bruno Windels
b30db544a3 use idb key range to select non-backed up keys 2022-01-28 13:11:32 +01:00
Bruno Windels
a499689bd8 also write room key that we create ourselves with RoomKey infrastructure
so all keys are written in one place and the flags are always correct
2022-01-28 13:10:48 +01:00
Bruno Windels
c81dde53e7 store key source in inbound session 2022-01-28 10:03:30 +01:00
Bruno Windels
dd2b41ff95 use backup flag in key backup rather than separate store 2022-01-27 16:07:18 +01:00
Bruno Windels
48e72f9b69 replace SessionsNeedingBackup store with backup field on inbound session 2022-01-27 16:00:46 +01:00
Bruno Windels
6f1484005b stop key backup when on the wrong version
users can then enter the new key in the settings to start backing up
again
2022-01-27 15:14:29 +01:00
Bruno Windels
0b4954a9ca log key backup upload requests 2022-01-27 14:20:04 +01:00
Bruno Windels
bf08c0d850 deal with errors when enabling key backup
fixes #449
2022-01-27 14:19:37 +01:00
Bruno Windels
e80acd4d57 add migration when backup is enabled 2022-01-26 16:30:40 +01:00
Bruno Windels
60ed276b8a add progress notification and cancellation to key backup flush 2022-01-26 15:19:31 +01:00
Bruno Windels
554aa45d48 add support for progress notifications in abortable operation 2022-01-26 15:18:23 +01:00
Bruno Windels
524090e27d support idb store/index.count 2022-01-26 15:12:11 +01:00
Bruno Windels
a791641b34 move types to separate file 2022-01-26 12:10:20 +01:00
Bruno Windels
85155a43bb cleanup types 2022-01-26 10:17:31 +01:00
Bruno Windels
cfb94206f9 move curve25519 code to separate file 2022-01-26 10:13:01 +01:00
Bruno Windels
86caa5f9b1 rename session backup to key backup to be consistent with RoomKey 2022-01-26 09:51:48 +01:00
Bruno Windels
933a1b4636 draft of session backup writing + some refactoring 2022-01-25 18:48:19 +01:00
Bruno Windels
ffece4f357 move some validation of into session backup 2022-01-25 18:48:03 +01:00
Bruno Windels
8f4e3c62ce add hs endpoint for backup keys upload 2022-01-25 18:47:42 +01:00
Bruno Windels
290aaad63a add sessionsNeedingBackup store 2022-01-25 18:47:27 +01:00
Bruno Windels
a3e294bb60 small cleanup 2022-01-25 18:45:39 +01:00
Bruno Windels
5d87d8bde3 change store.get return type when no value is found to undefined
IDBRequest.result is undefined according to the official TS type decls.
2022-01-25 18:43:44 +01:00
Bruno Windels
993a86ddb2 convert SessionBackup to typescript and pass in keyloader 2022-01-20 11:16:08 +01:00
Bruno Windels
a4d924acd1 make KeyLoader use proper olm types 2022-01-20 11:15:48 +01:00
Bruno Windels
30438846e9
Merge pull request #645 from vector-im/bwindels/fix-mobile-multiline
remove enterkeyhint attribute as it prevents entering newlines on android
2022-01-18 10:39:35 +01:00
Bruno Windels
e6fee75952 remove enterkeyhint attribute as it prevents entering newlines on android
on Android, by default (without the above attribute set to "send"), you
press enter twice to submit a field. The first time, enter, Android
seems to prevent sending logic by setting the key property on the event
to "Unidentified", but does insert a newline. The second consecutive enter,
it will be set to "Enter" and we'll send.

Having enterkeyhint to send will disable all of that. So we're going with
the default behaviour, which, IIRC, was a bit annoying on iOS as well.
2022-01-18 09:42:01 +01:00
Bruno Windels
acc9167991
Merge pull request #644 from vector-im/bwindels/fix-images-in-replies
fix images not loading in replies
2022-01-17 16:51:09 +01:00
Bruno Windels
b0e8506cb5 ensure images load in reply preview in timeline 2022-01-17 16:48:36 +01:00
Bruno Windels
f379bf2341 ensure images load in reply preview in composer 2022-01-17 16:48:17 +01:00
Bruno Windels
454d2d3666
Merge pull request #643 from vector-im/bwindels/separate-logout-view
Show logout in separate view so it's clear something is happening
2022-01-17 16:40:49 +01:00
Bruno Windels
57bf730241 mention it's better to not close the app 2022-01-17 16:33:57 +01:00
Bruno Windels
4bc421527f also add extra classes to legacy spinner 2022-01-17 16:31:13 +01:00
Bruno Windels
05d23cc745 hook up logout view 2022-01-17 16:31:02 +01:00
Bruno Windels
4c5b884af7 create and hook up logout viewmodel, on /logout/<id> path 2022-01-17 16:30:22 +01:00
Bruno Windels
c6c1d3b3d8 refactor logout in client so we don't need a fully loaded session
instead, we pass the session id in
this will make it easier to first dispose the client when leaving the
/session/<id> and just creating a client without fully loading it
to log out. This way sync is already not running anymore.
2022-01-17 16:29:01 +01:00
Bruno Windels
164d72830f create subclass for inline template views (e.g. without sub classing) 2022-01-17 16:25:48 +01:00
Bruno Windels
c10435e242
Merge pull request #642 from vector-im/update-node-faq
Update node version in FAQ
2022-01-17 09:18:07 +01:00
RMidhunSuresh
2dc9b63051 Update node version in FAQ 2022-01-17 12:49:55 +05:30
Bruno Windels
d673c8714e release v0.2.23 2022-01-14 19:19:18 +01:00
Bruno Windels
412db33c36 click here labels are so nineties 2022-01-14 19:18:12 +01:00
Bruno Windels
000c8b27c3
Merge pull request #637 from vector-im/bwindels/timeline-readme
add basic readme for updates in the timeline
2022-01-14 19:16:25 +01:00
Bruno Windels
46c61953f6
Merge pull request #612 from vector-im/threading-fallback-reply
Threading fallback - PR 2 - Support rich reply
2022-01-14 19:10:39 +01:00
Bruno Windels
a8a8355ea4 fix unit test 2022-01-14 19:05:53 +01:00
Bruno Windels
3d00881508 don't look in remoteEntries when already found 2022-01-14 19:05:30 +01:00
Bruno Windels
7197e5427f don't emit an update when the context entry is loaded sync
also load context entries in parallel
2022-01-14 18:16:52 +01:00
Bruno Windels
3243ce2a90 fix unit test that failed after it finished
crashing the runner on node 16
2022-01-14 18:15:46 +01:00
Bruno Windels
65929194b0 fix lint warnings 2022-01-14 16:23:55 +01:00
Bruno Windels
184a16a194 also define param 2022-01-14 16:23:12 +01:00
Bruno Windels
8201a85c47 ensure these have a fn for tilesCreator 2022-01-14 16:20:38 +01:00
Bruno Windels
2321228981 use this._entry here (once updated by super.updateEntry) 2022-01-14 16:20:14 +01:00
Bruno Windels
5f99c2360c also try to create replyTile from ctor just in case update doesn't come 2022-01-14 16:12:43 +01:00
Bruno Windels
ad335d5088 pass in tilesCreator everywhere, although not needed right now 2022-01-14 16:06:29 +01:00
Bruno Windels
1ea4a347e2 encode url components 2022-01-14 15:53:17 +01:00
Bruno Windels
b578f4ac84 actually add LocationView 2022-01-14 15:50:19 +01:00
Bruno Windels
052ff02571 move TileView type too so we don't have to repeat imports 2022-01-14 15:47:22 +01:00
Bruno Windels
3c59004e72 Merge branch 'master' into threading-fallback-reply 2022-01-14 15:43:24 +01:00
Bruno Windels
17ebc8a066
Merge pull request #611 from vector-im/threading-fallback-relation
Threading fallback - PR 1 - Link events with their related event
2022-01-14 15:35:27 +01:00
Bruno Windels
9220b6675b
Merge pull request #641 from vector-im/bwindels/location-tile
add location tile view so we don't throw when a location is shared
2022-01-14 15:30:39 +01:00
Bruno Windels
18a76025c7 add location tile view so we don't throw when a location is shared 2022-01-14 15:27:46 +01:00
RMidhunSuresh
dac2d5e685 Pass everything down into updateEntry 2022-01-14 19:26:23 +05:30
RMidhunSuresh
0af9f10166 don't store tilesCreator 2022-01-14 19:11:40 +05:30
RMidhunSuresh
d18f4d341c store replyFlags on this 2022-01-14 18:31:22 +05:30
Bruno Windels
b5a1c419ca
Merge pull request #640 from vector-im/bwindels/lazyloadimageswhenpartiallyinview
load image in timeline from when it is partially visible
2022-01-14 13:59:57 +01:00
Bruno Windels
1f9be978b7 load image in timeline from when it is partially visible 2022-01-14 13:57:11 +01:00
RMidhunSuresh
41fffdf155 Remove even more stray new lines 2022-01-14 18:17:49 +05:30
RMidhunSuresh
51215fda16 Rename tileCreator -> tilesCreator 2022-01-14 18:17:49 +05:30
RMidhunSuresh
d639e169ec Move tileCreator to BaseMessageTile 2022-01-14 18:17:49 +05:30
RMidhunSuresh
e1b9b1161d Split ifs and remove ?. abuse 2022-01-14 18:17:49 +05:30
RMidhunSuresh
846e637716 Remove stray newline 2022-01-14 18:17:49 +05:30
RMidhunSuresh
58dd25b58d track reply-tile 2022-01-14 18:17:49 +05:30
RMidhunSuresh
a77b9d9027 Move update logic to BaseMessageTile 2022-01-14 18:17:49 +05:30
RMidhunSuresh
ef5a377bc6 Hide reply option on pending tile 2022-01-14 18:17:49 +05:30
RMidhunSuresh
951af49e04 Emit change on reply tile 2022-01-14 18:17:49 +05:30
RMidhunSuresh
455b747a1c Don't check param for reply 2022-01-14 18:17:49 +05:30
RMidhunSuresh
28a534ee49 Fix reply nesting 2022-01-14 18:17:49 +05:30
RMidhunSuresh
f9f7f6cc6f Fix test 2022-01-14 18:17:49 +05:30
RMidhunSuresh
7f91653208 Rename replyTextTile -> replyTile 2022-01-14 18:17:49 +05:30
RMidhunSuresh
086e0c0320 Inline methods 2022-01-14 18:17:49 +05:30
RMidhunSuresh
273c44424f Throw if viewClass returns undefined 2022-01-14 18:17:49 +05:30
RMidhunSuresh
b134fa7409 Format swtich case properly 2022-01-14 18:17:49 +05:30
RMidhunSuresh
fee6447e22 Don't call render() 2022-01-14 18:17:49 +05:30
RMidhunSuresh
e99cd41ed0 Change check 2022-01-14 18:17:49 +05:30
RMidhunSuresh
af5a008d0f Move links to vm 2022-01-14 18:17:49 +05:30
RMidhunSuresh
27a9f5dd02 Use DOMPurify to remove mx-reply 2022-01-14 18:17:49 +05:30
RMidhunSuresh
cfefe6962a Remove stray space 2022-01-14 18:17:49 +05:30
RMidhunSuresh
88f9ad09a2 Move method as local function 2022-01-14 18:17:49 +05:30
RMidhunSuresh
0ae3c60d6d Remove .js file from rebase 2022-01-14 18:17:49 +05:30
RMidhunSuresh
c34d574385 No need to export renderPart 2022-01-14 18:17:49 +05:30
RMidhunSuresh
2a124d4195 simplify css 2022-01-14 18:17:49 +05:30
RMidhunSuresh
e352867f5a Remove unnecessary ctor 2022-01-14 18:17:49 +05:30
RMidhunSuresh
f645065db7 Remove unused getter 2022-01-14 18:17:49 +05:30
RMidhunSuresh
d69059de68 Use different flag 2022-01-14 18:17:49 +05:30
RMidhunSuresh
0c3f16e5f6 Use 's' flag with regex if available 2022-01-14 18:17:49 +05:30
RMidhunSuresh
cba044eff1 Remove comment 2022-01-14 18:17:49 +05:30
RMidhunSuresh
dee22f7120 Implement render flags 2022-01-14 18:17:49 +05:30
RMidhunSuresh
46b69b3873 Render error 2022-01-14 18:17:49 +05:30
RMidhunSuresh
687aa5a7e3 Remove dead code 2022-01-14 18:17:49 +05:30
RMidhunSuresh
4df3654166 Prevent reply previews from being nested 2022-01-14 18:17:49 +05:30
RMidhunSuresh
4d63b41127 Make reply preview flush left 2022-01-14 18:17:49 +05:30
RMidhunSuresh
1b9f970d7f WIP: Render the whole view instead of messageBody 2022-01-14 18:17:49 +05:30
RMidhunSuresh
7f1b3e25e8 Use t instead of tag 2022-01-14 18:17:49 +05:30
RMidhunSuresh
f01d5d95d9 Reuse code from timeline view 2022-01-14 18:17:49 +05:30
RMidhunSuresh
89d6968139 Show decryption error as well 2022-01-14 18:17:49 +05:30
RMidhunSuresh
2773642406 No need to handle redaction specially 2022-01-14 18:17:49 +05:30
RMidhunSuresh
13cba84445 Remove mapSideEffect 2022-01-14 18:17:49 +05:30
RMidhunSuresh
bb45d0eae9 Render non-text messages as well 2022-01-14 18:17:49 +05:30
RMidhunSuresh
df22db256b No need to pass tileCreator as argument 2022-01-14 18:17:49 +05:30
RMidhunSuresh
e0dc853d74 Fill matrix.to links 2022-01-14 18:17:49 +05:30
RMidhunSuresh
91912bdb8d Create tile using tileCreator 2022-01-14 18:17:49 +05:30
RMidhunSuresh
54004eef4d Integrate into update mechanism 2022-01-14 18:17:49 +05:30
RMidhunSuresh
aa3bb9c6ef Remove allowReplies 2022-01-14 18:17:49 +05:30
RMidhunSuresh
73c5562fd3 Remove code from BaseTextTile 2022-01-14 18:17:49 +05:30
RMidhunSuresh
4a12acf157 Improve error code 2022-01-14 18:17:49 +05:30
RMidhunSuresh
67da746b48 Render error 2022-01-14 18:17:49 +05:30
RMidhunSuresh
545aae31d9 WIP 2022-01-14 18:17:49 +05:30
RMidhunSuresh
3aa29cfc65 Do not remove reply preview 2022-01-14 18:17:49 +05:30
RMidhunSuresh
99f4eb6843 Minimize manual dom manipulation where possible 2022-01-14 18:17:49 +05:30
RMidhunSuresh
61f4d0719f Refactor code 2022-01-14 18:17:49 +05:30
RMidhunSuresh
d6233e7c77 Render static avatar 2022-01-14 18:17:49 +05:30
RMidhunSuresh
540aa6c546 Use contextEntry and pass avatarUrl 2022-01-14 18:17:49 +05:30
RMidhunSuresh
31573b3599 Render reply 2022-01-14 18:17:49 +05:30
RMidhunSuresh
e88ee31991 Add getter for reply body 2022-01-14 18:17:49 +05:30
RMidhunSuresh
f6cf3b378b Strip reply fallback 2022-01-14 18:17:49 +05:30
RMidhunSuresh
35a13842af Implement context endpoint 2022-01-14 18:17:49 +05:30
RMidhunSuresh
65f957f023 WIP 2022-01-14 18:17:49 +05:30
R Midhun Suresh
4fb0a84d0a
Return property from super
Co-authored-by: Bruno Windels <brunow@matrix.org>
2022-01-14 18:16:38 +05:30
RMidhunSuresh
30b8e5b5ea use withReply 2022-01-14 18:15:26 +05:30
RMidhunSuresh
8cd430ac07 Improve test logic 2022-01-14 17:48:25 +05:30
RMidhunSuresh
75012eda9c Fix tests 2022-01-14 17:28:31 +05:30
RMidhunSuresh
e9a49fdf74 Use hsApi mock 2022-01-14 17:07:06 +05:30
RMidhunSuresh
315acf2fbc Remove dead code from test 2022-01-14 16:54:16 +05:30
RMidhunSuresh
310790c84e Use mock storage 2022-01-14 16:51:06 +05:30
RMidhunSuresh
277638b107 Override methods in NonPersistedEventEntry
This will prevent redactions to entries fetched from hs showing "message
is being redacted" and will instead show "message is redacted"
2022-01-14 16:15:16 +05:30
RMidhunSuresh
b238357c53 Use emitUpdateForEntry 2022-01-14 16:14:42 +05:30
RMidhunSuresh
4fa32bac2f check only in remoteEntries 2022-01-14 16:14:06 +05:30
Bruno Windels
58f2192a7e add basic readme for updates in the timeline 2022-01-14 11:13:21 +01:00
RMidhunSuresh
3c28ee1adf Remove unused getter 2022-01-13 21:05:18 +05:30
RMidhunSuresh
2c4610c132 add param to emitUpdateForEntry 2022-01-13 19:20:37 +05:30
RMidhunSuresh
239d16747d Clean test code; try not to peek into internals 2022-01-13 19:14:28 +05:30
RMidhunSuresh
764541d3ca Remove unused method 2022-01-13 18:32:18 +05:30
RMidhunSuresh
ca1831fef6 update contextForEntries 2022-01-13 14:38:05 +05:30
Bruno Windels
1ed8d48ced release SDK 0.0.4 2022-01-12 18:39:13 +01:00
Bruno Windels
48e6bba100
Merge pull request #634 from vector-im/bwindels/fix-sdk-build2
Adjust SDK to not do asset imports anymore in file provided from SDK
2022-01-12 18:37:28 +01:00
Bruno Windels
8c1596d869 update SDK docs to not use paths/vite anymore 2022-01-12 18:32:15 +01:00
Bruno Windels
93eca757d3 dont add paths/vite to sdk output, as it does not work 2022-01-12 18:31:55 +01:00
Bruno Windels
3f60ef8da7 release sdk version 0.0.3 2022-01-12 17:51:48 +01:00
Bruno Windels
5d15fce343
Merge pull request #633 from vector-im/bwindels/fix-sdk-build1
Attempt to fix SDK build and dev server errors for consuming app
2022-01-12 17:48:08 +01:00
Bruno Windels
f526098293 also remove ts types, as we get errors for the untyped files
that don't exist
2022-01-12 17:41:00 +01:00
Bruno Windels
d7290bf750 remove exports field to try and prevent vite bug resolving asset url
downside is that we can't export cjs version anymore
2022-01-12 17:14:52 +01:00
RMidhunSuresh
2f4c0623d0 Restore earlier name 2022-01-12 19:20:32 +05:30
RMidhunSuresh
ed88184757 Remove statement 2022-01-12 19:14:38 +05:30
RMidhunSuresh
d0f7570f5e Fix tests 2022-01-12 18:44:17 +05:30
Bruno Windels
a5eb386f48
Merge pull request #632 from vector-im/bwindels/move-bs58-to-dev-deps
put bs58 in devDeps as we bundle it in the sdk
2022-01-12 10:13:31 +01:00
Bruno Windels
b76f97be93 put bs58 in devDeps as we bundle it in the sdk 2022-01-12 10:11:04 +01:00
RMidhunSuresh
acafae7d3a Implement offline support for context entries 2022-01-11 20:58:27 +05:30
RMidhunSuresh
a59bf7c002 Fix looking in allEntries 2022-01-11 20:57:29 +05:30
RMidhunSuresh
5c1813888c Check in all entries for context 2022-01-11 14:57:22 +05:30
RMidhunSuresh
73733ce145 Guard entry from storage being processed by method 2022-01-11 14:49:59 +05:30
RMidhunSuresh
bf6dfcfcad update comment 2022-01-11 13:28:35 +05:30
RMidhunSuresh
f605608098 getTrackedEntry -> findLoadedEventById 2022-01-11 13:20:42 +05:30
RMidhunSuresh
31a8227e53 stylistic change 2022-01-11 13:14:13 +05:30
RMidhunSuresh
62dcb61536 Rename updateEntry -> emitUpdateForEntry 2022-01-11 13:11:50 +05:30
RMidhunSuresh
fda211e7b3 Remove dead code 2022-01-11 13:10:40 +05:30
RMidhunSuresh
63b6564f70 Pass prop change 2022-01-11 11:54:41 +05:30
RMidhunSuresh
93bbeee400 Don't pass relatedEntry in param 2022-01-11 11:49:06 +05:30
RMidhunSuresh
66fa8d84a7 Make setAsContextOf private 2022-01-10 18:51:12 +05:30
RMidhunSuresh
091b55a265 Rename method and add comment 2022-01-10 18:05:33 +05:30
RMidhunSuresh
ec8f6e8e0a use addLocalRelation 2022-01-10 12:58:45 +05:30
RMidhunSuresh
7ad73bb453 Move check down 2022-01-07 19:56:31 +05:30
RMidhunSuresh
3fecce6fe6 Fix tests 2022-01-07 19:39:51 +05:30
RMidhunSuresh
9d161a0bcf Refactor + put redaction in NonPersistedEventEntry 2022-01-07 19:38:57 +05:30
RMidhunSuresh
8cc04e4c25 Keep calls internal to class 2022-01-07 17:50:36 +05:30
RMidhunSuresh
0a09a50ab9 Move line into if 2022-01-07 17:29:17 +05:30
RMidhunSuresh
c6484f1eac Replace entry in contextEntryNotInTimeline 2022-01-07 17:11:42 +05:30
Bruno Windels
68214156d9
Merge pull request #608 from vector-im/ts-migration-doc
Clarify approach to type data objects in doc
2022-01-06 14:39:06 +01:00
Bruno Windels
314843f5f2
Merge pull request #624 from vector-im/build-fix
Add hash-bang to fix sdk build error
2022-01-06 13:22:10 +01:00
RMidhunSuresh
cfbb6d4250 Add explaining comment 2022-01-06 15:37:58 +05:30
RMidhunSuresh
7adce08eee add more jsdoc comments 2022-01-06 15:33:00 +05:30
RMidhunSuresh
f76217dcce Change method name 2022-01-06 15:14:13 +05:30
RMidhunSuresh
a2ab36480f Add jsdoc comment 2022-01-06 15:02:44 +05:30
RMidhunSuresh
90c9018aa4 Update comment 2022-01-06 12:07:10 +05:30
RMidhunSuresh
595deb3a3d Also copy over contextEntry from otherEntry 2022-01-06 12:07:10 +05:30
RMidhunSuresh
78f97c6532 Remove await from tests 2022-01-06 12:07:10 +05:30
RMidhunSuresh
9f1764c325 Update comment 2022-01-06 12:07:10 +05:30
RMidhunSuresh
4418700589 Add test for move code 2022-01-06 12:07:10 +05:30
RMidhunSuresh
d2c7eec8e0 No need to delete before update on map 2022-01-06 12:07:10 +05:30
RMidhunSuresh
41cf6460d0 Remove dead code 2022-01-06 12:07:10 +05:30
RMidhunSuresh
8ec75ce4bb Rename methods 2022-01-06 12:07:10 +05:30
RMidhunSuresh
a060d54468 Make tests pass 2022-01-06 12:07:10 +05:30
RMidhunSuresh
3fe824dbd1 Propagate updates 2022-01-06 12:07:10 +05:30
RMidhunSuresh
7ef79c92f5 Remove entry from map 2022-01-06 12:07:10 +05:30
RMidhunSuresh
2d5bb82077 Fix bug 2022-01-06 12:07:10 +05:30
RMidhunSuresh
6f8001bd82 Add tests 2022-01-06 12:07:10 +05:30
RMidhunSuresh
640a3fb9fa Check if contextEvent was found 2022-01-06 12:07:10 +05:30
RMidhunSuresh
05d2defa2d Rename fetchedEntries --> contextEntriesNotInTimeline 2022-01-06 12:07:10 +05:30
RMidhunSuresh
c3bef6d4d2 Rename dependents --> contextForEntries 2022-01-06 12:07:10 +05:30
RMidhunSuresh
d1818d2a57 Reuse code in getOrLoadEntry 2022-01-06 12:07:10 +05:30
RMidhunSuresh
f5fadf700e Move event to remoteEntries if needed 2022-01-06 12:07:10 +05:30
RMidhunSuresh
d924dbb723 Add explaining comment 2022-01-06 12:07:10 +05:30
RMidhunSuresh
544dca3b18 Use _updateEntry 2022-01-06 12:07:10 +05:30
RMidhunSuresh
39f68e8c2f Refactor out magic string 2022-01-06 12:07:10 +05:30
RMidhunSuresh
5c0bbdd4c8 Move methods into Timeline 2022-01-06 12:07:10 +05:30
RMidhunSuresh
51b7b21082 Implement readById() in TimelineReader 2022-01-06 12:07:10 +05:30
RMidhunSuresh
0da94e51e0 Use map and fetch from Map if available 2022-01-06 12:07:10 +05:30
RMidhunSuresh
4a6293dcdc Made code more readable 2022-01-06 12:07:10 +05:30
RMidhunSuresh
287212956b findAndUpdate instead of update 2022-01-06 12:07:10 +05:30
RMidhunSuresh
7a91dd9595 Improve comment 2022-01-06 12:07:10 +05:30
RMidhunSuresh
4a81e06e96 Track fetched entries for redactions 2022-01-06 12:07:10 +05:30
RMidhunSuresh
ea89c272b9 Support redaction changes in remoteEntries 2022-01-06 12:07:10 +05:30
RMidhunSuresh
c690de9f7b Support decryption on entries fetched from hs 2022-01-06 12:07:10 +05:30
RMidhunSuresh
7cc3d4b91a Emit updated entries 2022-01-06 12:07:10 +05:30
RMidhunSuresh
053dcf39a5 Use NonPersistedEventEntry 2022-01-06 12:07:10 +05:30
RMidhunSuresh
d191b327c6 Change comment 2022-01-06 12:07:10 +05:30
RMidhunSuresh
06864a65b7 Add contextEventId 2022-01-06 12:07:10 +05:30
RMidhunSuresh
764e38f8c9 Use 'context' instead of 'related' 2022-01-06 12:07:10 +05:30
RMidhunSuresh
696980aca4 Parse display name and avatar of event 2022-01-06 12:07:10 +05:30
RMidhunSuresh
0c42f53a2f Implement context endpoint 2022-01-06 12:07:06 +05:30
RMidhunSuresh
e901142661 await on loading related events 2022-01-06 11:59:58 +05:30
RMidhunSuresh
2265d198a6 Formatting fix 2022-01-06 11:59:58 +05:30
RMidhunSuresh
b753507b8d WIP 2022-01-06 11:59:58 +05:30
Bruno Windels
196e3726cb
Merge pull request #630 from vector-im/bwindels/otk-count-cleanup
Missing OTK count in sync doesn't mean 0, but rather no change
2022-01-05 14:29:51 +01:00
Bruno Windels
c9d11d6f19 missing otk count does not mean 0 but rather no change 2022-01-05 14:26:15 +01:00
Bruno Windels
aabfbf507e typo in comments 2022-01-05 14:25:42 +01:00
RMidhunSuresh
205de7e5c5 Add hash-bang to fix build error 2021-12-27 15:51:25 +05:30
Bruno Windels
908f9a7ce3 try to export stylesheet 2021-12-22 18:04:30 +01:00
Bruno Windels
203a5fd88c
Merge pull request #622 from vector-im/bwindels/sdk-refactoring
Some API cleanup ahead of first SDK release
2021-12-22 17:58:20 +01:00
Bruno Windels
13e77636a9 export paths from vite.js as required by Platform, reorder ctor params
make it easier for SDK users
2021-12-22 17:48:08 +01:00
Bruno Windels
6247ced7b7 dont export, the ctor of these classes is not a public API 2021-12-22 17:24:58 +01:00
Bruno Windels
ba27d20b24 only pass platform into Client
simplifying the API for SDK
2021-12-22 17:20:37 +01:00
Bruno Windels
9238961992 cache olm and olm worker promise inside Platform
as prep to call them every time a Client is created
2021-12-22 17:19:10 +01:00
Bruno Windels
fe26f48c47 rename SessionContainer to Client 2021-12-22 17:09:52 +01:00
Bruno Windels
b5fe65d0cc
Merge pull request #617 from vector-im/bwindels/sdk-build
SDK build
2021-12-22 16:54:03 +01:00
Bruno Windels
24afe1e496 add licenses to readme of things we actually bundle
rather than just depend on
2021-12-22 16:45:08 +01:00
Bruno Windels
5f389e654a add description 2021-12-22 16:37:53 +01:00
Bruno Windels
c31215bc2a less logging during build 2021-12-22 16:31:19 +01:00
Bruno Windels
c3ff571af7 update SDK doc, use it as sdk package readme 2021-12-22 16:31:19 +01:00
Bruno Windels
441fa13bfd change sdk package name to hydrogen-view-sdk
as we might want to also have a lower level sdk later on
2021-12-22 16:31:19 +01:00
Bruno Windels
3bee4b4585 bundle bs58 to avoid pain of bundle transitive dependency for lib users
bs58 depends on safe-buffer, which depends on buffer, which is a bit
of a pain to bundle as it is a built-in node module. You'd typically
replace buffer with a browser polyfill in your build system but:
 a) this is somewhat a pain to setup for simple apps
 b) the polyfill is way more than we need (6kb), so we prefer to bundle
    our minimal buffer replacement that uses Uint8Array. Since it is
    a transitive dependency, we need to bundle bs58 and all of its
    transitive dependencies (2.5kb) as well, so if users of hydrogen-sdk
    also use any of these, they'll be double included in their bundle.
2021-12-22 16:31:19 +01:00
Bruno Windels
b48280905e include path/vite in sdk bundle 2021-12-22 16:31:19 +01:00
Bruno Windels
163dae647b move output of both lib and asset build around for coherent package 2021-12-22 16:31:19 +01:00
Bruno Windels
c921091957 run two vite builds for the sdk build, assets & js separately 2021-12-22 16:31:19 +01:00
Bruno Windels
6add3f1da3 WIP 2021-12-22 16:31:19 +01:00
Bruno Windels
ceb0b5793b somewhat works, but not everything we need
it's missing still:
 - non-css assets like the download sandbox and the olm worker aren't written for some reason
 - the es and cjs lib.js entry points end up in assets with a hash for some reason
 - in these entry files, apart from our exports, something is adding an import statement for every import that was found in the tree
 - all assets are hashed even though the config tries to disable that
 - tests are included
2021-12-22 16:31:18 +01:00
Bruno Windels
14b854ad4f make tsconfig file to build declaration files 2021-12-22 16:31:18 +01:00
Bruno Windels
df6000c706 basic sdk build config file for es and cjs 2021-12-22 16:31:18 +01:00
Bruno Windels
c11f0774eb move common parts of build config to separate file and merge with it 2021-12-22 16:31:18 +01:00
Bruno Windels
f2b822e5d2 move deps that are not used for sdk to devDependencies 2021-12-22 16:31:18 +01:00
Bruno Windels
2d2005934a WIP 2021-12-22 16:31:18 +01:00
Bruno Windels
363cd5b046 include css 2021-12-22 16:31:18 +01:00
Bruno Windels
8922d2aaf2 prototype of sdk build 2021-12-22 16:31:18 +01:00
RMidhunSuresh
10368500f2 Fix formatting 2021-12-10 12:12:52 +05:30
RMidhunSuresh
5ef7ab32df Update doc 2021-12-10 12:09:18 +05:30
Bruno Windels
dacdc1aec6
Merge pull request #597 from vector-im/ts-conversion-matrix-ssss
Convert matrix/ssss to typescript
2021-12-09 18:54:25 +01:00
Bruno Windels
589a002d67
Merge pull request #588 from vector-im/ts-conversion-matrix-net
Convert /matrix/net to typescript
2021-12-09 18:51:33 +01:00
Bruno Windels
21a41e192b Merge branch 'master' into ts-conversion-matrix-net 2021-12-09 18:49:54 +01:00
Bruno Windels
5ea29297cc fix typescript errors 2021-12-09 18:44:44 +01:00
Bruno Windels
c5c08ea34b
Merge pull request #586 from vector-im/bwindels/log-signature-failure
log signature verification failure in logger, not console
2021-12-09 18:40:01 +01:00
Bruno Windels
8d315f2741 Merge branch 'master' into bwindels/log-signature-failure 2021-12-09 18:34:36 +01:00
Bruno Windels
cd0d9dcbba
Merge pull request #548 from vector-im/bwindels/vite-mvp
Convert develop server and build system to using Vite
2021-12-09 18:29:06 +01:00
Bruno Windels
ba84387722 remove commented out code 2021-12-09 18:15:22 +01:00
Bruno Windels
0ec86b6dc1 Merge branch 'master' into bwindels/vite-mvp 2021-12-09 18:07:17 +01:00
Bruno Windels
5c5193ef48 remove old build system and unused dependencies
some of these are for the ie11 legacy build, which has been
postponed. They will be brougth back when we bring back the legacy build
2021-12-09 18:04:11 +01:00
Bruno Windels
d9ff4a8484 sw.js is not part of the sdk yet, so just put the path in index.html 2021-12-09 17:12:08 +01:00
Bruno Windels
dea7e7b4f5 enable minification and source maps 2021-12-09 16:42:35 +01:00
Bruno Windels
62827b92b7 implement placeholder replacement so it still works with minification 2021-12-09 16:37:31 +01:00
Bruno Windels
9a82f88e1f log swSource as build fails in CI 2021-12-09 15:13:19 +01:00
Bruno Windels
23e0d3f2ff get notification badge icon url through import now we transpile the sw 2021-12-09 15:13:05 +01:00
Bruno Windels
a4fac68393 use same method for setting version and build hash placeholder in sw
also better naming in service worker plugin
2021-12-09 14:36:12 +01:00
Bruno Windels
f934262e35 also use global hash var here 2021-12-09 12:22:17 +01:00
Bruno Windels
14dffa4ad4 remove leftover logging 2021-12-09 12:21:34 +01:00
Bruno Windels
8e4da396ea replace global hash in given chunks 2021-12-09 12:15:17 +01:00
Bruno Windels
c344032c0a transpile service worker and cleanup build plugin 2021-12-09 11:39:28 +01:00
Bruno Windels
180681b602 manifest ends up in assets folder, index.html in parent folder 2021-12-08 18:29:32 +01:00
Bruno Windels
fb8149b6cf add base to manifest path, just for completeness 2021-12-08 18:29:07 +01:00
Bruno Windels
4c2c99fc07 actually remove lookbehind 2021-12-08 18:05:57 +01:00
Bruno Windels
c8b0354d07 dont use lookbehind in regular expressions, safari & older firefoxes choke on them 2021-12-08 18:00:37 +01:00
Bruno Windels
c87628b614 cleanup 2021-12-06 15:40:15 +01:00
Bruno Windels
5bd28da4f3 loading olm from the worker was broken, reading the wrong global 2021-12-06 15:35:08 +01:00
Bruno Windels
0e2a22f509 also look in chunks for cacheable assets for service worker 2021-12-06 15:34:39 +01:00
Bruno Windels
91e69a2bd0 fix icons in manifest not being found 2021-12-06 15:25:44 +01:00
Bruno Windels
155cd4c9bd make olmPath absolute if it isn't already 2021-12-06 13:49:14 +01:00
RMidhunSuresh
734ecccb9c Use object instead of Record here 2021-12-03 17:34:23 +05:30
Bruno Windels
9a3f74c6fb load service worker in production mode, adjust development flag 2021-12-03 10:42:38 +01:00
RMidhunSuresh
e2abc312d3 Fix typescript errors 2021-12-03 11:48:01 +05:30
RMidhunSuresh
d6378133d8 Remove length property 2021-12-03 11:40:26 +05:30
RMidhunSuresh
49a56efa82 Remove comment 2021-12-03 11:40:26 +05:30
RMidhunSuresh
640cd88b6e make type string 2021-12-03 11:40:26 +05:30
RMidhunSuresh
66b4f9bfe5 LogItem --> ILogItem 2021-12-03 11:40:26 +05:30
RMidhunSuresh
0541cf8f2b Change object to Record 2021-12-03 11:40:26 +05:30
RMidhunSuresh
bf93bd79c9 types.js --> types 2021-12-03 11:40:26 +05:30
RMidhunSuresh
f89b937ee7 Use object instead of Record 2021-12-03 11:40:26 +05:30
RMidhunSuresh
82de3c9867 Prefer type over interface 2021-12-03 11:40:26 +05:30
RMidhunSuresh
b328c54da8 Change type from Ctor to Options 2021-12-03 11:40:26 +05:30
RMidhunSuresh
e9cea73357 Remove comment 2021-12-03 11:40:26 +05:30
R Midhun Suresh
3fbf65355d Rename Ctor to Options
Co-authored-by: Bruno Windels <brunow@matrix.org>
2021-12-03 11:40:26 +05:30
RMidhunSuresh
b5438f2ba8 Do not set content-length 2021-12-03 11:40:26 +05:30
RMidhunSuresh
4f43398db0 Fix promise resolve type 2021-12-03 11:40:26 +05:30
RMidhunSuresh
05121e32b1 Pull interface out for HomeServerApi 2021-12-03 11:40:26 +05:30
RMidhunSuresh
a8870f2d24 Extract ctor types out 2021-12-03 11:40:26 +05:30
RMidhunSuresh
238b9aafb1 Convert replay.js to ts 2021-12-03 11:40:26 +05:30
RMidhunSuresh
2e6b909173 No need to pass undefined 2021-12-03 11:40:26 +05:30
RMidhunSuresh
4bdcafad4b Rename file to types.ts 2021-12-03 11:40:26 +05:30
RMidhunSuresh
69e67ad5ac Make more functions private 2021-12-03 11:40:26 +05:30
RMidhunSuresh
2dd050bd90 Change object to Record 2021-12-03 11:40:26 +05:30
RMidhunSuresh
9b315d1564 Replace object with Record 2021-12-03 11:40:26 +05:30
RMidhunSuresh
57d24dcf90 Treat wrapper hsapi as HomeServerApi 2021-12-03 11:40:26 +05:30
RMidhunSuresh
8387215efd Add comment 2021-12-03 11:40:26 +05:30
RMidhunSuresh
885abc59be Add return types 2021-12-03 11:40:26 +05:30
RMidhunSuresh
7403cbc389 WIP - HomeServerApi.js to ts conversion 2021-12-03 11:40:26 +05:30
RMidhunSuresh
145b40f28d Fomatting fix 2021-12-03 11:40:26 +05:30
RMidhunSuresh
cf54b78af7 Convert RequestScheduler.js to ts 2021-12-03 11:40:26 +05:30
RMidhunSuresh
0aae31a450 Change year in copyright notice 2021-12-03 11:40:26 +05:30
RMidhunSuresh
f120ce50e6 Convert Reconnector.js to ts 2021-12-03 11:40:26 +05:30
RMidhunSuresh
90e3fde35d Convert MediaRepository.js to ts 2021-12-03 11:40:26 +05:30
RMidhunSuresh
ff53c2757d Convert HomeServerRequest.js to ts 2021-12-03 11:40:26 +05:30
RMidhunSuresh
e1a823400a Convert ExponentialRetryDelay.js to ts 2021-12-03 11:40:26 +05:30
RMidhunSuresh
3a24019d96 Convert common.js to ts 2021-12-03 11:40:26 +05:30
RMidhunSuresh
9688a561b3 Move interface to common.ts 2021-12-03 11:40:26 +05:30
RMidhunSuresh
2a3b13ecce Add request type 2021-12-03 11:40:26 +05:30
RMidhunSuresh
9bffd31ee3 Remove typeof 2021-12-03 11:36:51 +05:30
R Midhun Suresh
6dcebde69d Alias namespace as Olm
Co-authored-by: Bruno Windels <brunow@matrix.org>
2021-12-03 11:36:51 +05:30
RMidhunSuresh
e06a0e9e5a Use olm type from dependency 2021-12-03 11:36:51 +05:30
RMidhunSuresh
7362e38413 Convert interface to type 2021-12-03 11:36:51 +05:30
RMidhunSuresh
d2c09933c7 Type encrypted data 2021-12-03 11:36:51 +05:30
RMidhunSuresh
b2efcb9515 Convert SecretStorage.js to ts 2021-12-03 11:36:51 +05:30
RMidhunSuresh
814c0bed2e Convert recoveryKey.js to ts 2021-12-03 11:36:51 +05:30
RMidhunSuresh
e45f66a199 Convert passphrase.js to ts 2021-12-03 11:36:51 +05:30
RMidhunSuresh
dd4704b818 Fix imports 2021-12-03 11:36:49 +05:30
RMidhunSuresh
9b8ab9fd8d Convert index.js to index.ts 2021-12-03 11:35:12 +05:30
RMidhunSuresh
f9f59fec39 Convert common.js to ts 2021-12-03 11:34:09 +05:30
Bruno Windels
d91aaabeb3
Merge pull request #596 from vector-im/ts-conversion-matrix-sessioninfo
Convert matrix/sessioninfo to typescript
2021-12-02 09:29:11 +01:00
Bruno Windels
9042520916
Merge pull request #595 from vector-im/ts-conversion-matrix-push
Convert /matrix/push to typescript
2021-12-02 09:28:59 +01:00
Bruno Windels
d3ab961364
Merge pull request #593 from vector-im/ts-conversion-matrix-login
Convert /matrix/login to typescript
2021-12-02 09:28:47 +01:00
RMidhunSuresh
0c46460861 Add comment 2021-12-02 11:20:40 +05:30
RMidhunSuresh
9f82e7f7fc Add proper type 2021-12-02 11:17:41 +05:30
RMidhunSuresh
ef3456199c Fix formatting 2021-12-01 22:52:09 +05:30
R Midhun Suresh
928a5c27f3
Add rationale
Co-authored-by: Bruno Windels <brunow@matrix.org>
2021-12-01 22:50:59 +05:30
Bruno Windels
bc86bf2d00 some more sdk impl notes 2021-12-01 18:11:09 +01:00
Bruno Windels
fceca845a9 comment why we do this 2021-12-01 18:11:01 +01:00
Bruno Windels
09338d8aa8 bring back postcss plugins, apart from autoprefixer, which breaks vite 2021-12-01 18:10:25 +01:00
Bruno Windels
a504e74f54 extract function for script tag url 2021-12-01 18:10:02 +01:00
Bruno Windels
f83a0cec4e update postcss plugins so they all use the same version of postcss 2021-12-01 18:09:26 +01:00
Bruno Windels
69e34d03bd sort dependencies 2021-12-01 17:45:33 +01:00
Bruno Windels
261b17d36c fix lint 2021-12-01 17:27:13 +01:00
Bruno Windels
3fd2d39898 remove unused packages and move dev deps accordingly 2021-12-01 17:26:48 +01:00
Bruno Windels
bb9362ee8b only import node-html-parser when running the unit tests 2021-12-01 17:26:00 +01:00
Bruno Windels
1a618dd106 only import fake-indexeddb in tests
as it is a devDependency and can end up in the legacy bundle
under circumstances
2021-12-01 17:25:07 +01:00
Bruno Windels
7fda78ff2f disable legacy build for now 2021-12-01 14:06:15 +01:00
Bruno Windels
70c1e4e3ed move doc paragraphs around 2021-12-01 14:06:05 +01:00
Bruno Windels
b469d03677 config is at same level as paths 2021-12-01 14:05:50 +01:00
Bruno Windels
de24034b22 remove secondary theme as vite puts them in one bundle 2021-12-01 13:30:58 +01:00
Bruno Windels
75bf410320 correct path of main.js for tests 2021-12-01 13:30:17 +01:00
Bruno Windels
7e1818b285 Merge branch 'master' into bwindels/vite-mvp 2021-12-01 12:30:33 +01:00
RMidhunSuresh
73ca2dfb77 Add Record and fix typo 2021-12-01 16:05:16 +05:30
Bruno Windels
dce0ee5ace
Merge pull request #607 from vector-im/bwindels/fixpathsobserv
adjust path
2021-12-01 09:45:57 +01:00
Bruno Windels
85385a0aa7 adjust path 2021-12-01 09:43:58 +01:00
Bruno Windels
08314bd4b5
Merge pull request #606 from vector-im/bwindels/typescript-observable-2
Typescript conversion of yet more observables
2021-11-30 17:09:16 +01:00
Bruno Windels
8c3ae57497 fix Iterator vs IterableIterator confusion 2021-11-30 17:05:39 +01:00
Bruno Windels
de8995fa7e fix handlers in test missing methods, now that observable list is typed 2021-11-30 16:58:56 +01:00
Bruno Windels
581ef47c78 fix conflicting sortedIndex declaration 2021-11-30 16:53:59 +01:00
Bruno Windels
fc3eb7f57f Merge branch 'master' into bwindels/typescript-observable-2 2021-11-30 16:37:43 +01:00
Bruno Windels
49443d4f6e
Update TS-MIGRATION.md 2021-11-30 14:17:51 +00:00
Bruno Windels
2e57e99e34
clarify when to use type and interface 2021-11-30 14:15:25 +00:00
Bruno Windels
ef712b16f5
Merge pull request #584 from vector-im/ts-conversion-utils
Convert /utils to typescript
2021-11-30 14:13:09 +01:00
Bruno Windels
19827a0b5b
Merge pull request #601 from vector-im/filter-token
Ensure unwanted data do not end up in logs
2021-11-30 10:02:07 +01:00
RMidhunSuresh
66fbc37ec4 Remove comments 2021-11-30 14:15:49 +05:30
RMidhunSuresh
6699b71bd5 transformer is optional 2021-11-30 13:38:25 +05:30
RMidhunSuresh
fe77b71c97 use transformer function 2021-11-30 13:28:28 +05:30
Bruno Windels
be5deea1d3
Merge pull request #605 from vector-im/bwindels/try-test-ci-failure
Report unit test failures on CI
2021-11-30 08:20:05 +01:00
Bruno Windels
3322827979 upgrade impunity to propagate exit code 2021-11-30 08:16:28 +01:00
Bruno Windels
7f115b3e4b
Merge pull request #602 from vector-im/bwindels/tests-node-16.12
update impunity to 1.0.8 to run tests on node >= 16.12
2021-11-29 10:53:47 +01:00
Bruno Windels
a134e48ebb update impunity to 1.0.8 to run tests on node >= 16.12 2021-11-29 10:49:26 +01:00
RMidhunSuresh
104590e34d Use ! in test 2021-11-29 11:48:05 +05:30
RMidhunSuresh
d981a85239 Filter token out of stack trace 2021-11-29 11:43:43 +05:30
Bruno Windels
bc8b3d71d5
Merge pull request #600 from vector-im/bwindels/update-impunity
update to version that doesn't use a bash script anymore, which doesn…
2021-11-26 12:48:21 +01:00
Bruno Windels
2802164bb4 update to version that doesn't use a bash script anymore, which doesnt work on macos 2021-11-26 12:45:40 +01:00
Bruno Windels
876fcf532f release v0.2.22 2021-11-26 09:12:08 +01:00
Bruno Windels
92bf28e104
Merge pull request #599 from vector-im/bwindels/fix-emsonelogin
Don't fail login if dehydrated devices are not supported
2021-11-25 15:46:01 +01:00
Bruno Windels
ae7d4d07df use .name so we don't need an import 2021-11-25 15:42:36 +01:00
Bruno Windels
229c584138 don't fail login if dehydrated devices are not supported 2021-11-25 15:38:13 +01:00
RMidhunSuresh
bb18af414b Convert SessionInfoStorage.js to ts 2021-11-25 15:18:03 +05:30
RMidhunSuresh
3d9fbb685a Convert Pusher.js to ts 2021-11-25 13:23:05 +05:30
RMidhunSuresh
346e95c33c Change return type 2021-11-25 12:33:12 +05:30
RMidhunSuresh
a31860dc5f Fix formatting 2021-11-24 14:55:44 +05:30
RMidhunSuresh
c54ca168ed Convert SSOLoginHelper.js to ts 2021-11-24 14:49:08 +05:30
RMidhunSuresh
a1367f8e72 Fix password login 2021-11-24 14:00:26 +05:30
RMidhunSuresh
64037cb32a Convert TokenLoginMethod to ts 2021-11-24 13:56:47 +05:30
RMidhunSuresh
e4c443c73a Convert PasswordLoginMethod to ts 2021-11-24 13:47:26 +05:30
RMidhunSuresh
91f2a96403 Make LoginMethod an interface 2021-11-24 13:40:04 +05:30
Bruno Windels
93abbe83e8
Merge pull request #592 from vector-im/bwindels/lazylist-enhancements
Lazylist enhancements
2021-11-23 14:35:18 +01:00
Bruno Windels
f444160c6a feels ok without overflow margin for now 2021-11-23 14:33:27 +01:00
Bruno Windels
e4be1702c4 add comment for future test 2021-11-23 14:32:42 +01:00
Bruno Windels
7b38df45da i think this is fine now? 2021-11-23 14:31:23 +01:00
Bruno Windels
e34a92e2ec fix copyright 2021-11-23 14:30:11 +01:00
Bruno Windels
35fb84c275 remove old js lazylist 2021-11-23 14:26:15 +01:00
Bruno Windels
9557178ffb padding needs to be on ul, not scroll container, or the list blows up 2021-11-23 14:25:35 +01:00
Bruno Windels
4be2f12a14 subscribe before calling list.length 2021-11-23 14:25:22 +01:00
Bruno Windels
c64a9c1e23 snowpack/esbuild 0.9 doesn't support override keyword 2021-11-23 14:25:00 +01:00
Bruno Windels
7897ea88cd add some spaces and comments 2021-11-23 14:24:43 +01:00
Bruno Windels
c22718811f more tests for queryMove 2021-11-23 08:56:33 +01:00
Bruno Windels
3aa3b7e160 fix end growing larger than totalLength when range shrinks in case of remove 2021-11-23 08:30:52 +01:00
Bruno Windels
cf9f43ab9e WIP2 2021-11-22 20:35:57 +01:00
Bruno Windels
4a64d0ee17 WIP 2021-11-19 22:49:46 +01:00
RMidhunSuresh
d625d57aa4 Fix lastIndex
Signed-off-by: RMidhunSuresh <rmidhunsuresh@gmail.com>
2021-11-19 12:32:00 +01:00
RMidhunSuresh
bbeb909bdc Use createEnum
Signed-off-by: RMidhunSuresh <rmidhunsuresh@gmail.com>
2021-11-19 12:32:00 +01:00
RMidhunSuresh
33ac34b04e Do not break onListChanged
Signed-off-by: RMidhunSuresh <rmidhunsuresh@gmail.com>
2021-11-19 12:31:57 +01:00
RMidhunSuresh
5d54285640 Move ItemRange to separate file
Signed-off-by: RMidhunSuresh <rmidhunsuresh@gmail.com>
2021-11-19 12:12:50 +01:00
RMidhunSuresh
aee135a6cd Jsdoc fix
Signed-off-by: RMidhunSuresh <rmidhunsuresh@gmail.com>
2021-11-19 12:06:40 +01:00
RMidhunSuresh
da715c70b0 Remove forceRender
Signed-off-by: RMidhunSuresh <rmidhunsuresh@gmail.com>
2021-11-19 12:06:40 +01:00
RMidhunSuresh
e10b494f0c Improve containsIndex
Signed-off-by: RMidhunSuresh <rmidhunsuresh@gmail.com>
2021-11-19 12:06:40 +01:00
RMidhunSuresh
3ae52ea1ca Fix bug in onAdd and onRemove
Signed-off-by: RMidhunSuresh <rmidhunsuresh@gmail.com>
2021-11-19 12:06:40 +01:00
RMidhunSuresh
1165683f69 Fix onRemove
Signed-off-by: RMidhunSuresh <rmidhunsuresh@gmail.com>
2021-11-19 12:06:40 +01:00
RMidhunSuresh
83ff2dd810 Fix onAdd
Signed-off-by: RMidhunSuresh <rmidhunsuresh@gmail.com>
2021-11-19 12:06:40 +01:00
RMidhunSuresh
587dd3848e Use existing render function for initial render
Signed-off-by: RMidhunSuresh <rmidhunsuresh@gmail.com>
2021-11-19 12:06:40 +01:00
RMidhunSuresh
a02b6b68d3 Move common code from if-else
Signed-off-by: RMidhunSuresh <rmidhunsuresh@gmail.com>
2021-11-19 12:06:40 +01:00
RMidhunSuresh
168312627d Render only diff of ranges
Signed-off-by: RMidhunSuresh <rmidhunsuresh@gmail.com>
2021-11-19 12:06:40 +01:00
RMidhunSuresh
61402e798e WIP 2 2021-11-19 12:06:40 +01:00
RMidhunSuresh
1a28b4f887 WIP 2021-11-19 12:06:40 +01:00
RMidhunSuresh
d4e923f9de Remove code from loadList
We don't need this method so best to leave it empty.

Signed-off-by: RMidhunSuresh <rmidhunsuresh@gmail.com>
2021-11-19 12:06:40 +01:00
Bruno Windels
c0f7f0a8f1
Merge pull request #590 from vector-im/bwindels/platform-tests
Search platform-specific code for tests too
2021-11-19 10:26:35 +01:00
Bruno Windels
f1a6a4924e commit yarn.lock too 2021-11-19 10:23:59 +01:00
Bruno Windels
ec71e30ecb add Platform as entry point so also platform dependant code gets searched for tests 2021-11-19 10:23:18 +01:00
Bruno Windels
f23227fc8b use latest version of impunity with support for multiple entry points 2021-11-19 10:22:59 +01:00
Bruno Windels
5a747cd829
Merge pull request #589 from vector-im/bwindels/fix-sdk-example
packages processed by post-install need to be in dependencies, or the script fails
2021-11-18 15:26:08 +01:00
Bruno Windels
6980921dab
some impl notes for SDK 2021-11-18 14:16:10 +00:00
RMidhunSuresh
8fcfd713e0 Use IAbortable 2021-11-17 20:28:44 +05:30
RMidhunSuresh
ea2842f37f Return empty string 2021-11-17 20:28:44 +05:30
RMidhunSuresh
64a9892ee2 Use generic T in LockMap 2021-11-17 20:28:44 +05:30
RMidhunSuresh
048547828d Remove type Func 2021-11-17 20:28:44 +05:30
R Midhun Suresh
a14a8c3a07 Create interface IDisposable
Co-authored-by: Bruno Windels <brunow@matrix.org>
2021-11-17 20:28:44 +05:30
RMidhunSuresh
08ef84d112 Mention return type 2021-11-17 20:28:44 +05:30
RMidhunSuresh
5a0c06473c Use undefined instead of null 2021-11-17 20:28:44 +05:30
RMidhunSuresh
1beb153f21 func --> Func 2021-11-17 20:28:44 +05:30
RMidhunSuresh
0c424cb77f Fix imports 2021-11-17 20:28:44 +05:30
RMidhunSuresh
ebd1caf6d1 Convert enum.js to ts 2021-11-17 20:28:44 +05:30
RMidhunSuresh
0e18247184 Use constant type 2021-11-17 20:28:44 +05:30
RMidhunSuresh
a945edfe07 Convert pbkdf2.js to ts 2021-11-17 20:28:44 +05:30
RMidhunSuresh
6c2aa1bf61 Convert hkdf.js to ts 2021-11-17 20:28:44 +05:30
RMidhunSuresh
afecac3e3c Convert timeout.js to ts 2021-11-17 20:28:44 +05:30
RMidhunSuresh
8a169d5ddc Convert sortedIndex.js to ts 2021-11-17 20:28:44 +05:30
RMidhunSuresh
ea0adb4407 Convert RetainedValue.js to ts 2021-11-17 20:28:44 +05:30
RMidhunSuresh
88ec1b575d Convert mergeMap.js to ts 2021-11-17 20:28:44 +05:30
RMidhunSuresh
1549d8add0 Convert LockMap to ts 2021-11-17 20:28:44 +05:30
RMidhunSuresh
c8eb7ea7ac Convert Lock.js to ts 2021-11-17 20:28:44 +05:30
RMidhunSuresh
a3460d8c2a Convert formatSize to ts 2021-11-17 20:28:44 +05:30
RMidhunSuresh
4ce7634201 Convert error.js to ts 2021-11-17 20:28:44 +05:30
RMidhunSuresh
ef53a12f7a Fix imports 2021-11-17 20:28:44 +05:30
RMidhunSuresh
7d12c2ba54 Add return types 2021-11-17 20:28:44 +05:30
RMidhunSuresh
7270918b65 Convert Disposables to typescript 2021-11-17 20:28:44 +05:30
RMidhunSuresh
dd74ed1957 Add types to disposeValue 2021-11-17 20:28:44 +05:30
RMidhunSuresh
7772643b0d Disposables.js --> Disposables.ts 2021-11-17 20:28:44 +05:30
Bruno Windels
0a433b90e3 packages processed by post-install need to be in dependencies, or the script fails 2021-11-17 15:09:25 +01:00
Bruno Windels
efccc1e19e
Merge pull request #583 from vector-im/ts-conversion-logging
Convert src/logging to typescript
2021-11-17 14:37:42 +01:00
Bruno Windels
692ae25e76 remove unused method 2021-11-17 14:35:26 +01:00
Bruno Windels
b5e9eb26ba reduce size of ILogItem interface further 2021-11-17 14:33:58 +01:00
Bruno Windels
4030a4918d explicitly check for undefined 2021-11-17 13:57:11 +01:00
Bruno Windels
41a10d9697 explicitly check for undefined 2021-11-17 13:56:20 +01:00
Bruno Windels
fde0163b97 remove unneeded union type and simplify code 2021-11-17 13:53:27 +01:00
Bruno Windels
42e5fb33ba remove more non-public methods from ILogItem interface 2021-11-17 13:50:56 +01:00
Bruno Windels
526a818269 only used internally 2021-11-17 13:42:49 +01:00
Bruno Windels
afc538e875 explicitly check for type, rather than truthy 2021-11-17 12:58:08 +01:00
Bruno Windels
74fb15e426 add future todo note 2021-11-17 12:54:44 +01:00
Bruno Windels
46dd78162f no need to dig into internals here 2021-11-17 12:54:32 +01:00
Bruno Windels
276d8d4a42 check for undefined, no need for ! 2021-11-17 12:39:57 +01:00
Bruno Windels
b1d20178f8 add explicit void return type 2021-11-17 12:37:50 +01:00
Bruno Windels
5f362cbdbd remove dead code 2021-11-17 11:54:29 +01:00
Bruno Windels
695996d6e2 add ILogger and ILogExport interface, to give export correct return type
also move logging related types to own file
2021-11-17 11:39:12 +01:00
RMidhunSuresh
1b13f32d94 Remove resolved todo comment 2021-11-17 15:39:21 +05:30
Bruno Windels
3ee7e73ff0 item is optional here 2021-11-17 11:08:44 +01:00
Bruno Windels
90d7b73dd4 non-persisted queued items don't have an id yet, find them by ref equality 2021-11-17 11:08:29 +01:00
Bruno Windels
f93bdd962a might as well use generic here 2021-11-17 10:50:55 +01:00
Bruno Windels
1942c31eff still finish item when not returning from sync callback 2021-11-17 10:42:54 +01:00
RMidhunSuresh
d01271fb15 _run return T or void depending on boolean 2021-11-17 13:22:19 +05:30
RMidhunSuresh
07a1130db3 children can be array of ISerializedItem 2021-11-17 12:02:12 +05:30
RMidhunSuresh
835da58b53 Remove ! 2021-11-17 11:59:50 +05:30
R Midhun Suresh
9c8f96e233
value is optional
Co-authored-by: Bruno Windels <brunow@matrix.org>
2021-11-17 11:43:59 +05:30
RMidhunSuresh
b0ab8cd77f Space before { 2021-11-17 11:40:43 +05:30
RMidhunSuresh
8fce29caf7 Explicitly check for undefined 2021-11-17 11:38:33 +05:30
RMidhunSuresh
14eaa57434 No need for type assertion here 2021-11-16 13:14:11 +05:30
RMidhunSuresh
58105824d9 Fix error in reduce 2021-11-16 13:08:13 +05:30
RMidhunSuresh
4704a70cb7 Remove todo comment 2021-11-16 13:06:47 +05:30
RMidhunSuresh
34a8463bf9 Fix jsdoc return type 2021-11-16 12:43:23 +05:30
RMidhunSuresh
e339e730f4 Remove todo comment 2021-11-16 12:42:50 +05:30
RMidhunSuresh
286747c23c Add type annotation for ctor 2021-11-16 12:41:03 +05:30
RMidhunSuresh
3ee1607298 Convert utils to typescript 2021-11-16 12:32:49 +05:30
RMidhunSuresh
4161d31642 Convert NullLogger to typescript 2021-11-16 12:23:06 +05:30
RMidhunSuresh
4c1d7a8f2d Use generics over returning unknown 2021-11-15 22:47:38 +05:30
Bruno Windels
2da450d69d log signature verification failure in logger, not console 2021-11-15 15:27:57 +01:00
RMidhunSuresh
fe69f84c85 Use undefined in LogItem.serialize 2021-11-15 19:32:16 +05:30
RMidhunSuresh
ba5f2032ba Make properties in LogItem optional, not null 2021-11-15 19:17:49 +05:30
RMidhunSuresh
7097ba07d1 Replace LogLabelOrNull type with undefined 2021-11-15 18:59:33 +05:30
RMidhunSuresh
30a384fe1e Make LogFilter optional 2021-11-15 18:44:25 +05:30
RMidhunSuresh
520e0f1b89 Use interface ILogItem 2021-11-15 17:29:08 +05:30
RMidhunSuresh
a7d059b3ed Fix imports 2021-11-14 19:42:18 +05:30
RMidhunSuresh
bba44abf52 Convert console logger to ts 2021-11-14 16:24:16 +05:30
RMidhunSuresh
39d0708cca Add comment 2021-11-14 15:58:51 +05:30
RMidhunSuresh
2d8b719ab0 Add void return types as well 2021-11-14 15:55:42 +05:30
RMidhunSuresh
5efa27c2a3 Add more type annotations 2021-11-14 15:48:59 +05:30
RMidhunSuresh
67e8fc0c43 Add return types to methods in BaseLogger 2021-11-12 23:27:35 +05:30
RMidhunSuresh
8e42e3f21f Add types to returns in LogFilter.ts 2021-11-12 23:17:21 +05:30
RMidhunSuresh
29a8260514 Add explicit types for return in methods 2021-11-12 23:12:15 +05:30
RMidhunSuresh
8c7a765e11 Convert IDBLogger to ts 2021-11-12 15:06:21 +05:30
RMidhunSuresh
f3d0f88f95 Make error public 2021-11-12 15:06:11 +05:30
RMidhunSuresh
2ddd2d16ed IDBLogger.js --> IDBLogger.ts 2021-11-11 16:50:46 +05:30
RMidhunSuresh
9fed2ca41b Use undefined instead of null 2021-11-11 16:25:14 +05:30
RMidhunSuresh
eb7c5c4437 Use undefined only instead of both undefined and null 2021-11-11 16:08:25 +05:30
RMidhunSuresh
09851600f7 Remove unwanted types 2021-11-11 15:35:51 +05:30
RMidhunSuresh
425a3c85a9 Make error prop private and expose via getter 2021-11-11 13:24:52 +05:30
RMidhunSuresh
0f7a78ee25 Make return type explicit 2021-11-11 13:05:12 +05:30
Bruno Windels
7148f6fd41 wip to not babel deps 2021-11-10 19:10:23 +01:00
Bruno Windels
e83781b26a make ie11 compatible 2021-11-10 19:10:06 +01:00
RMidhunSuresh
cd7dccd804 Move interface to top 2021-11-10 19:13:35 +05:30
RMidhunSuresh
7a68c971aa Make field readonly 2021-11-10 19:07:24 +05:30
RMidhunSuresh
cfa7708b57 Use type imports 2021-11-10 18:51:46 +05:30
Bruno Windels
c47f872f6f
Merge pull request #578 from vector-im/fix-progressbar
Fix progressbar not working on attachment uploads
2021-11-10 11:17:02 +01:00
RMidhunSuresh
ef2aad8956 Annotate LogFilter 2021-11-10 15:04:07 +05:30
RMidhunSuresh
ab126729e0 Use LogLevel as type instead of number 2021-11-10 14:49:59 +05:30
RMidhunSuresh
e3c85c585e Log callbacks can return more than Promises 2021-11-10 14:42:43 +05:30
RMidhunSuresh
0b4eca4724 Create alias for LogLevel | null 2021-11-10 14:29:23 +05:30
RMidhunSuresh
142d3ef543 Split LogItemValues into union of types 2021-11-10 13:45:37 +05:30
RMidhunSuresh
ceb52eedaf Fix imports and add type annotations 2021-11-10 12:36:56 +05:30
RMidhunSuresh
772f7a2757 Account for duration being null 2021-11-10 12:17:43 +05:30
RMidhunSuresh
db792ab5a9 Add type annotations to LogItem 2021-11-10 12:06:50 +05:30
RMidhunSuresh
97ec680af2 Remove .js files 2021-11-10 12:06:22 +05:30
RMidhunSuresh
ba4d5453a2 Move type LogCallback to LogItem 2021-11-10 12:05:29 +05:30
Bruno Windels
36a982f7e2 WIP to run rollup twice, first with vite and babel, then to inline deps 2021-11-09 18:10:12 +01:00
RMidhunSuresh
2a5d30d749 Convert to enum 2021-11-09 22:32:02 +05:30
Bruno Windels
122528f9a9 also transpile typescript files
note that these have already been converted to javascript by vite
2021-11-09 17:59:58 +01:00
RMidhunSuresh
55401a746c Move type alias to LogItem
and add more type annotations
2021-11-09 22:28:26 +05:30
RMidhunSuresh
8cbc81b8bb Annotate method arguments 2021-11-09 20:57:47 +05:30
Bruno Windels
da7f66a531 setup babel for legacy build as input transform plugin 2021-11-09 14:52:03 +01:00
RMidhunSuresh
7893a121c0 Initialize in field 2021-11-09 17:36:18 +05:30
RMidhunSuresh
4c5d028509 any --> unknown 2021-11-09 17:34:16 +05:30
RMidhunSuresh
eef116e26b annotate labelOrValues 2021-11-09 17:19:46 +05:30
RMidhunSuresh
8fba3f4ca9 Add explaining comment 2021-11-09 15:39:24 +05:30
RMidhunSuresh
839d3fb689 Throw on export() in ConsoleLogger 2021-11-09 13:53:07 +05:30
RMidhunSuresh
377cc4ca1f Make BaseLogger abstract 2021-11-09 13:52:41 +05:30
RMidhunSuresh
030c46264b type annotate fields 2021-11-09 13:00:37 +05:30
RMidhunSuresh
dad37dece3 .js --> .ts 2021-11-09 11:46:05 +05:30
RMidhunSuresh
57e2c4ea45 No need for handler to be async 2021-11-08 22:20:56 +05:30
RMidhunSuresh
c1a8ffd814 respond with only for GET requests 2021-11-08 18:03:20 +05:30
Bruno Windels
b95c918dc6
Merge pull request #580 from vector-im/bwindels/scroll-composer
add scrollbar when > 5 lines in composer
2021-11-08 12:25:39 +01:00
Bruno Windels
e9586711e0 add scrollbar when > 5 lines in composer 2021-11-08 11:19:24 +01:00
Bruno Windels
ffef4936f9 update caniuse defs again after rebase 2021-11-08 11:14:00 +01:00
Bruno Windels
fcde507183 WIP 2021-11-08 11:13:02 +01:00
Bruno Windels
3b72157e64 apparently, package-overrides should use esm
otherwise they don't get transpiled for some reason
2021-11-08 11:12:19 +01:00
Bruno Windels
7dce579ac3 make babel plugin run after commonjs one added by vite 2021-11-08 11:12:19 +01:00
Bruno Windels
16918ddb7d ie11 build wip 2021-11-08 11:12:13 +01:00
Bruno Windels
b65782e13c ignore tsc errors on vite-specific imports
or not understood by tsc in any case
2021-11-08 11:10:34 +01:00
Bruno Windels
eb60f6717a add comment for improvements 2021-11-08 11:10:34 +01:00
Bruno Windels
923a1a2057 use vite define option to inject version number everywhere 2021-11-08 11:10:34 +01:00
Bruno Windels
216afd45cc vite/rollup plugin to inject and transform manifest & service worker 2021-11-08 11:10:34 +01:00
Bruno Windels
3fe1c0cdc3 tweak build and start command to use vite 2021-11-08 11:10:34 +01:00
Bruno Windels
afadd25885 tweak build settings somewhat for now 2021-11-08 11:10:34 +01:00
Bruno Windels
e2b20f466d remove unneeded package override 2021-11-08 11:10:34 +01:00
Bruno Windels
01712c3f23 make tests run again 2021-11-08 11:10:34 +01:00
Bruno Windels
b6fda8865f make all dependencies use vite and remove post-install script / lib dir 2021-11-08 11:10:34 +01:00
Bruno Windels
db3e8a9c6b rearrange assets and main.js to make them run with vite 2021-11-08 11:10:23 +01:00
RMidhunSuresh
d1491cc203 More checks before returning 2021-11-08 15:11:41 +05:30
RMidhunSuresh
d31371b486 Return on upload in sw 2021-11-08 14:37:32 +05:30
Bruno Windels
2afcddbf49 release v0.2.21 2021-11-05 21:08:49 +01:00
Bruno Windels
25fb645c4b
Merge pull request #577 from vector-im/bwindels/update-caniuse
update caniuse
2021-11-05 21:07:39 +01:00
Bruno Windels
5c689ac5b1 yarn wants deps in different order 2021-11-05 21:02:46 +01:00
Bruno Windels
e1c8088de2 update caniuse browser definitions 2021-11-05 21:02:16 +01:00
Bruno Windels
d40037ef49
Merge pull request #576 from vector-im/bwindels/setup-new-dehydrated-device-when-claiming
Setup new dehydrated device when claiming
2021-11-05 20:56:23 +01:00
Bruno Windels
faa0246e28 setup new dehydrated device when claiming one 2021-11-05 20:53:04 +01:00
Bruno Windels
0749073120 clone key as olm clears it 2021-11-05 20:52:50 +01:00
Bruno Windels
2dccd36a6d
Merge pull request #575 from vector-im/bwindels/composer-layout-improvements
Composer layout improvements
2021-11-05 19:24:34 +01:00
Bruno Windels
23494ab630 bottom align send button, adjust paddings 2021-11-05 19:05:52 +01:00
Bruno Windels
2f15c9a4a7 show either attachment or send button, depending on composer value 2021-11-05 19:05:35 +01:00
Bruno Windels
c3203fdacd explain this value 2021-11-05 19:05:09 +01:00
Bruno Windels
222c616148
Merge pull request #574 from vector-im/bwindels/multiline-composer-fixups
Multiline composer fixups
2021-11-05 18:44:54 +01:00
Bruno Windels
44e7e25cab clear height while sending or clearing, also fix #572 in the process 2021-11-05 18:36:59 +01:00
Bruno Windels
fc1b9abe66 don't add line when hitting enter to send 2021-11-05 18:36:38 +01:00
Bruno Windels
365c8d0953
Merge pull request #566 from vector-im/composer-improvements
Support for multiline messages
2021-11-05 18:20:02 +01:00
Bruno Windels
8ffd98162c don't make the composer shrink for now, timeline loses scroll position 2021-11-05 17:48:44 +01:00
Bruno Windels
c671596c6f only schedule one resize callback per frame 2021-11-05 17:48:08 +01:00
Bruno Windels
b22437840d don't set explicit height, rely on rows=1 for default height 2021-11-05 17:47:20 +01:00
Bruno Windels
4c5fe824c2
Merge pull request #569 from vector-im/fix-400
Show redacted tile for redacted messages in encrypted rooms
2021-11-05 15:57:36 +01:00
Bruno Windels
1fee773313
Merge pull request #571 from vector-im/fix-570
Make progress bar work for image uploads in Chrome
2021-11-05 15:56:33 +01:00
Bruno Windels
6be952491a
Merge pull request #573 from vector-im/fix-517-2
Continue filling gaps that return only non-rendered events in the first backfill
2021-11-05 15:45:45 +01:00
Bruno Windels
781147bf0e add some comments and rename for clarity 2021-11-05 15:42:07 +01:00
RMidhunSuresh
a4cd40c2f8 Keep filling gap until sibling changes 2021-11-05 17:25:29 +05:30
RMidhunSuresh
96a2dd7c72 Add event handler before executing open
Signed-off-by: RMidhunSuresh <hi@midhun.dev>
2021-11-04 00:02:50 +05:30
RMidhunSuresh
f6b7dcbad7 Show redacted tile 2021-11-03 20:03:47 +05:30
Bruno Windels
b011c3df03 release v0.2.20 2021-11-03 02:38:52 +01:00
Bruno Windels
014acbfaf5
Merge pull request #564 from vector-im/bwindels/dehydrated-device
Device dehydration support
2021-11-03 00:52:46 +01:00
Bruno Windels
ee9c9b33ca fix lint again 2021-11-03 02:36:53 +01:00
Bruno Windels
b2d8f5f023 fix lint 2021-11-03 02:35:22 +01:00
Bruno Windels
790b9cbc13 require a flag to enable account setup during login
as it blocks login from progressing, any sdk usage should enable
to indicate they are listening for the AccountSetup status.
2021-11-03 02:28:01 +01:00
Bruno Windels
bffe34fe0a await write key 2021-11-03 02:20:27 +01:00
Bruno Windels
a8022077f6 add minimal logging 2021-11-03 02:20:11 +01:00
Bruno Windels
80a98f04c7 pickle clears the key, so slice it before calling so we can reuse for 4s 2021-11-03 02:08:27 +01:00
Bruno Windels
8a36eb4532 check mac of dehydrated key to match default 4s key mac before adopting 2021-11-03 02:07:57 +01:00
Matthew Hodgson
abef4f0f79
Merge pull request #568 from vector-im/initial-github-action-for-docker-images
Add GH Action to build the docker image
2021-11-02 00:19:45 +00:00
Ben Banfield-Zanin
05fe68823a
Add GH Action to build the docker image
Builds the docker image on pushes for: tags, PRs, master.

Publishes docker images as per:
* `latest` & `<TAG>` for tags
* `master` for `master`

Same as https://github.com/matrix-org/lb/pull/8 & https://github.com/matrix-org/matrix-content-scanner/pull/46
2021-11-01 14:55:32 +00:00
Bruno Windels
567cdd5510 WIP for enabling session backup from dehydration key 2021-10-29 19:17:31 +02:00
Bruno Windels
6d9d8797fe use same UI as in settings to pick between recovery key and passphrase 2021-10-29 16:40:35 +02:00
Bruno Windels
44a26fd340 key backup: add disable button,and enabling add dehydrated device option 2021-10-29 15:48:28 +02:00
Bruno Windels
3b3751c827 remove dehydrated device test button from settings 2021-10-29 15:47:14 +02:00
Bruno Windels
6273d723f1 fix typo 2021-10-29 15:24:24 +02:00
RMidhunSuresh
6863fef7e5 Resize composer with text
Signed-off-by: RMidhunSuresh <hi@midhun.dev>
2021-10-29 17:00:02 +05:30
RMidhunSuresh
3a6e74ae1c Add css style
Signed-off-by: RMidhunSuresh <hi@midhun.dev>
2021-10-29 15:11:34 +05:30
RMidhunSuresh
16bec0a656 Convert input to textarea
Signed-off-by: RMidhunSuresh <hi@midhun.dev>
2021-10-29 15:11:12 +05:30
Bruno Windels
c3dfdde626 be forgiving when dispose has already been called 2021-10-28 12:04:42 +02:00
Bruno Windels
544019f67d ensure olm Account in dehydrated device is freed on error 2021-10-28 11:52:32 +02:00
Bruno Windels
bef12c7a8f prevent double free on olm account when logging out
by ensuring we only dispose the e2ee/Account once, as well as
the Session and other classes
2021-10-28 11:48:25 +02:00
Bruno Windels
3ef37c15c7 remove import session button as it's not supported anymore 2021-10-28 11:47:54 +02:00
Bruno Windels
68a6113c26 add logout button to session load error screen 2021-10-28 11:47:31 +02:00
Bruno Windels
cbccca20d0 remove leftover logging 2021-10-27 18:09:31 +02:00
Bruno Windels
e3378d5636 use correct device_id in signatures for dehydrating device
completely replace device id for dehydrating device
so we don't have to pass it down the stack
2021-10-27 18:08:50 +02:00
Bruno Windels
c89e414bb5 WIP3 2021-10-27 15:08:53 +02:00
Bruno Windels
718b410253 WIP2 2021-10-27 10:26:36 +02:00
Bruno Windels
faf4ea6434 WIP 2021-10-26 18:47:46 +02:00
Bruno Windels
abb802b881 release v0.2.19 2021-10-26 15:38:50 +02:00
Bruno Windels
d9ecf38e42
Merge pull request #563 from vector-im/bwindels/exportlogsonsessionloadfail
add export logs button when session fails to load
2021-10-26 15:35:50 +02:00
Bruno Windels
7ef19e0ead add export logs button when session fails to load 2021-10-26 15:30:52 +02:00
Bruno Windels
c621ccf679 release v0.2.18 2021-10-26 15:10:44 +02:00
Bruno Windels
0f0719eaa2
Merge pull request #560 from vector-im/bwindels/logout
Add Log out
2021-10-26 15:10:21 +02:00
Bruno Windels
5b889f0b32
Merge pull request #562 from vector-im/bwindels/diagnose-561
return static string when member is missing and add logging
2021-10-26 15:10:08 +02:00
Bruno Windels
82a0c1024c return static string when member is missing and add logging 2021-10-26 15:08:51 +02:00
Bruno Windels
af85fe3892 confirm before logging out 2021-10-26 14:48:37 +02:00
Bruno Windels
f998041748 add logout button in settings 2021-10-26 12:49:31 +02:00
Bruno Windels
2b884e73db remove action buttons on session picker
and now that we're adding logout, none of them are something we want to support really
2021-10-26 11:43:38 +02:00
Bruno Windels
e3c5def536 release v0.2.17 2021-10-26 11:32:42 +02:00
Bruno Windels
fae4493abc
Merge pull request #554 from vector-im/bwindels/fix-551
Only keep a limited amount of olm InboundGroupSession objects in memory to prevent OOM
2021-10-26 11:30:10 +02:00
Bruno Windels
67dd929951 put key session check in method 2021-10-26 11:14:46 +02:00
Bruno Windels
805c2657f2 remove unrelated file 2021-10-26 11:07:17 +02:00
Bruno Windels
ab2f15b5a2 prevent cache hiding better keys in storage (+ tests) 2021-10-25 19:17:13 +02:00
Bruno Windels
3c2604b384 test that sessions get free'd correctly 2021-10-25 17:33:33 +02:00
Bruno Windels
12b5bd3a4f
Merge pull request #553 from moritzdietz/moritzdietz/update-faq
Update FAQ: Add note about published builds
2021-10-25 17:21:27 +02:00
Bruno Windels
74e8bc3bda write unit tests 2021-10-25 17:19:48 +02:00
Bruno Windels
6bbce06d93 start writing tests for key loader 2021-10-22 19:01:20 +02:00
Bruno Windels
22361bdf42 don't need to dispose room keys anymore, they are owned by the loader 2021-10-22 18:08:09 +02:00
Bruno Windels
076f450ec7 this can be const 2021-10-22 18:01:26 +02:00
Bruno Windels
6d8ec69a4d fix lint 2021-10-22 18:01:17 +02:00
Bruno Windels
b7e3a54e15 remove now usused code 2021-10-22 17:51:00 +02:00
Bruno Windels
2943cb525f add comment about possible future optimization 2021-10-22 17:50:45 +02:00
Bruno Windels
1278288a42 cleanup RoomKey to changes and better naming 2021-10-22 17:50:30 +02:00
Bruno Windels
66a93ee108 adapt Session and RoomEncryption to megolm/Decryption API changes 2021-10-22 17:48:53 +02:00
Bruno Windels
ac23119838 convert SessionDecryption to TS and adapt to use KeyLoader 2021-10-22 17:48:35 +02:00
Bruno Windels
b55930f084 convert ReplayDetectionEntry to typescript 2021-10-22 17:47:29 +02:00
Bruno Windels
d6e243321b convert megolm/Decryption to typescript and adapt to KeyLoader 2021-10-22 17:46:39 +02:00
Bruno Windels
2ddb3fbf72 cleanup 2021-10-22 17:45:55 +02:00
Bruno Windels
45dc2162dc fix unit tests 2021-10-22 17:30:20 +02:00
Bruno Windels
77d10c93d6 convert groupby and megolm decryption utils to typescript 2021-10-21 14:40:51 +02:00
Bruno Windels
66a77519d7 implement key caching in KeyLoader
merging session cache into it so we can better manage and recycle
keys without exposing too low-level public methods on BaseLRUCache.

Using refCount instead of inUse flag as a key can of course be used
by multiple useKey calls at the same time.
2021-10-21 11:12:54 +02:00
Bruno Windels
3bafc89855 remove unused draft code 2021-10-20 15:25:11 +02:00
Bruno Windels
4fa285e85a convert LRUCache to ts 2021-10-20 15:24:58 +02:00
Bruno Windels
041cedbc58 fix typescript extension change 2021-10-20 15:24:39 +02:00
Bruno Windels
cbf82fcd29 cleanup code so far 2021-10-20 15:14:17 +02:00
Bruno Windels
5dc0c8c0b3 make 'better' better 2021-10-20 13:38:54 +02:00
Moritz Dietz
c83f78044e
Update FAQ to highlight about the existence of published builds 2021-10-20 12:46:37 +02:00
Bruno Windels
d7407ecf66 WIP 2021-10-20 11:39:01 +02:00
Bruno Windels
82aac93f36
Update SDK.md 2021-10-20 07:05:39 +00:00
Bruno Windels
c92d6ecbb6
Merge pull request #549 from danger89/patch-1
Add HTML title & icon
2021-10-08 10:39:08 +02:00
Melroy van den Berg
a20fe2b5a6
Add HTML title & icon
Add missing HTML title and icon
2021-10-06 20:38:39 +02:00
Danila Fedorin
3d2c74a760 Add type annotations to SortedArray 2021-10-03 22:19:46 -07:00
Danila Fedorin
7b2e452cd5 Rename SortedArray to TypeScript 2021-10-03 22:19:46 -07:00
Danila Fedorin
1363af24a7 Add type annotations to MappedList 2021-10-03 22:19:46 -07:00
Danila Fedorin
84187ce109 Make updater optional in BaseObservableList 2021-10-03 22:19:44 -07:00
Danila Fedorin
0466b49520 Rename MappedList to TypeScript 2021-10-03 22:18:12 -07:00
Danila Fedorin
3b131f2db6 Add type annotations to ConcatList 2021-10-03 22:18:12 -07:00
Danila Fedorin
588da9b719 Relax types on BaseObservableList and add helper for tests 2021-10-03 22:18:12 -07:00
Danila Fedorin
ddca467e30 Rename ConcatList to TypeScript 2021-10-03 22:18:12 -07:00
Danila Fedorin
8466a910da Add type annotations to AsyncMappedList 2021-10-03 22:18:12 -07:00
Danila Fedorin
0e6c59983f Generalize BaseMappedList to allow mappers to promises 2021-10-03 22:18:10 -07:00
Danila Fedorin
e6de873b6e Rename AsyncMappedList to TypeScript 2021-10-03 22:16:46 -07:00
Danila Fedorin
b148f3ca9e Add type annotations to ObservableArray 2021-10-03 22:16:46 -07:00
Danila Fedorin
348a9c83f5 Rename ObservableArray to TypeScript 2021-10-03 22:16:46 -07:00
Bruno Windels
6517704850
Merge pull request #545 from vector-im/bwindels/fixbuild
the build currently does not like override for some reason
2021-10-01 14:09:07 +02:00
Bruno Windels
cc58d27122 the build currently does not like override for some reason 2021-10-01 14:07:58 +02:00
Bruno Windels
39d5073f49 remove user and password 2021-10-01 13:37:37 +02:00
Bruno Windels
191cb78d8f
Merge pull request #544 from vector-im/bwindels/sdk-export
Provide very basic SDK interface
2021-10-01 13:33:13 +02:00
Bruno Windels
fbfda03138 don't let tsc check build system specific files 2021-10-01 13:32:09 +02:00
Bruno Windels
23da4ae2dd point to SDK docs from FAQ 2021-10-01 13:27:58 +02:00
Bruno Windels
675ee59e50 Add instructions and example code for basic SDK usage 2021-10-01 13:26:14 +02:00
Bruno Windels
193a0e1a4f move UI docs to doc folder 2021-10-01 13:26:03 +02:00
Bruno Windels
c1d20cb9f9 also call this -Path 2021-10-01 12:56:36 +02:00
Bruno Windels
d2eeaab5f5 get assets paths with import "asset?url" specifically for vitejs 2021-10-01 12:49:41 +02:00
Bruno Windels
aac0e74b9d also export LoadStatus 2021-10-01 12:49:30 +02:00
Bruno Windels
f89d169ef3 provide library entry point that provides convenient reexports of the public classes 2021-10-01 11:30:42 +02:00
Bruno Windels
de22a0790f
Merge pull request #543 from vector-im/bwindels/typescript-observable
Typescript conversion of base observables
2021-10-01 10:10:20 +02:00
Bruno Windels
1c06e36c1c add override keyword 2021-10-01 10:07:17 +02:00
Bruno Windels
3de3481765 prefer optional syntax over '| null' 2021-10-01 10:05:56 +02:00
Bruno Windels
6b50a63e95 missing space 2021-10-01 10:02:32 +02:00
Bruno Windels
393e2f809e make subscription handle return undefined, so we can reassign optional members in one statement 2021-10-01 10:01:52 +02:00
Bruno Windels
a0f443ccc3
Merge pull request #521 from DanilaFe/typescript-observable
Start migrating Observable code to TypeScript.
2021-10-01 09:54:29 +02:00
Bruno Windels
155207ed95
Merge pull request #392 from MidhunSureshR/documentation
Documentation for Hydrogen
2021-09-30 18:38:43 +02:00
Bruno Windels
255e479d47
Merge pull request #541 from vector-im/bwindels/fix-540
scroll room list to top when entering query first
2021-09-30 17:24:00 +02:00
Bruno Windels
e42739ec81 scroll room list to top when entering query first 2021-09-30 17:19:42 +02:00
Bruno Windels
8911588de9 release v0.2.16 2021-09-30 12:48:45 +02:00
Bruno Windels
3fba2c6513
Merge pull request #525 from vector-im/bwindels/fix-sssskey-conversion
fix sssskey not being migrated properly
2021-09-30 10:19:31 +02:00
Bruno Windels
89add8b684 fix sssskey not being migrated properly 2021-09-30 10:18:03 +02:00
Bruno Windels
31a70e1b8e release v0.2.15 2021-09-30 09:52:35 +02:00
Bruno Windels
dcb08f5266
Merge pull request #524 from vector-im/bwindels/fix-492
clear relations for room when forgetting room
2021-09-30 09:51:59 +02:00
Bruno Windels
d10d27c1d6 clear relations for room when forgetting room 2021-09-30 09:49:45 +02:00
Bruno Windels
4cebe26186
Merge pull request #523 from vector-im/bwindels/clear-cache-after-515
clear history cache to purge potential timeline corruption from #515
2021-09-30 09:41:24 +02:00
Bruno Windels
f8f4bb4eac
Merge pull request #520 from vector-im/bwindels/fix-139
Keep backup of e2ee identity in localStorage when idb gets cleared
2021-09-30 09:28:56 +02:00
Bruno Windels
42f1603d81 use correct prefix to remove local storage value 2021-09-30 09:25:35 +02:00
Bruno Windels
6ea835c2d1
Merge pull request #522 from xunzhou/master
Required pkg for aarch64 docker build
2021-09-30 08:48:47 +02:00
Bruno Windels
ae68264db4 don't use switch where single if/else works 2021-09-30 08:42:00 +02:00
Bruno Windels
ccda93cc82 remove leftover logging 2021-09-30 08:40:49 +02:00
Bruno Windels
3556878a1e clear history cache to purge potential timeline corruption from #515 2021-09-30 08:37:33 +02:00
xunzhou
17f24942da Required pkg for aarch64 docker build 2021-09-29 19:31:39 -07:00
Danila Fedorin
c80dfb10a2 Add type annotations to BaseMappedList 2021-09-29 18:41:30 -07:00
Danila Fedorin
99164eb0d8 Rename BaseMappedList to TypeScript 2021-09-29 18:17:38 -07:00
Danila Fedorin
bf53449f66 Add type annotations to common 2021-09-29 18:13:49 -07:00
Danila Fedorin
e53f3d23d5 Rename common to TypeScript 2021-09-29 18:10:09 -07:00
Danila Fedorin
64ba656043 Update ListView and TimelineListView 2021-09-29 18:08:13 -07:00
Danila Fedorin
414280ada9 Add type annotations to BaseObservableList 2021-09-29 18:05:30 -07:00
Danila Fedorin
3952c3b969 Rename BaseObservableList to TypeScript 2021-09-29 17:46:51 -07:00
Danila Fedorin
ab6ce62551 Add type annotations to ObservableValue 2021-09-29 17:43:17 -07:00
Danila Fedorin
a7360f409e Rename ObservableValue to TypeScript 2021-09-29 17:42:57 -07:00
Danila Fedorin
319027e2e3 Add type annotations to BaseObservable 2021-09-29 17:42:57 -07:00
Danila Fedorin
d73dea797a Rename BaseObservable to TypeScript 2021-09-29 17:18:22 -07:00
Bruno Windels
e0b9a3fa50 create e2ee identity also when storage got cleared without backup 2021-09-29 20:07:42 +02:00
Bruno Windels
a1c66738db migration to initialize & restore e2ee identity backup 2021-09-29 19:39:26 +02:00
Bruno Windels
3a064d6796 a IDBRequest when opening the database does not have a source 2021-09-29 19:21:42 +02:00
Bruno Windels
2ef7251079 move ssssKey to e2ee prefix as well so it gets backed up too 2021-09-29 19:21:06 +02:00
Bruno Windels
77bd0d3f3c store e2ee session values as well in localStorage 2021-09-29 11:49:58 +02:00
Bruno Windels
004aa5d3dc
Merge pull request #519 from vector-im/bwindels/rickfixes
Fix immediate errors for sdk usage
2021-09-29 11:12:17 +02:00
Bruno Windels
1b87eaf21e move packages needed for post-install script to (non-dev) deps 2021-09-29 11:02:59 +02:00
Bruno Windels
cd071e47e0 release v0.2.14 2021-09-28 14:59:40 +02:00
Bruno Windels
9a96a5b7bb
Merge pull request #516 from vector-im/bwindels/fix-515
Fix interpreting hex keys as decimal
2021-09-28 14:25:33 +02:00
Bruno Windels
e31d3abc97 fix ts errors in TimelineEventStore unit tests 2021-09-28 14:20:54 +02:00
Bruno Windels
ec2f1b9833 add unit tests for findExistingKeys 2021-09-28 14:20:21 +02:00
Bruno Windels
edc3a1d33c convert storage mock to TS and add utility for mock raw database 2021-09-28 14:19:59 +02:00
Bruno Windels
9036b21b5c don't interpret hex as decimal when decoding the key
this fixes #515 as it was causing the gap not to be closed,
because the fragment id was never equal.
2021-09-28 11:34:55 +02:00
Bruno Windels
0846fcc05d add more logging during gap filling 2021-09-27 16:34:12 +02:00
Bruno Windels
f55f450850 fix findExistingKeys too many (existing but not requested) keys 2021-09-27 16:27:52 +02:00
Bruno Windels
cc0b938a6d release v0.2.13 2021-09-24 18:45:33 +02:00
Bruno Windels
7c44fb8cd4
Merge pull request #510 from vector-im/bwindels/fix-499
move over word-break lines
2021-09-24 18:44:41 +02:00
Bruno Windels
6982f55cd7 move over word-break lines
this didn't get moved over when converting the timeline tile css to css grid
2021-09-24 18:42:47 +02:00
Bruno Windels
e2d7954846
Merge pull request #509 from vector-im/bwindels/fix-menupositioning
automatically position popups using a simpler algorithm
2021-09-24 18:32:36 +02:00
Bruno Windels
71bd797dd4 automatically position popups using a simpler algorithm 2021-09-24 18:28:06 +02:00
Bruno Windels
a7b6fe4b22
Merge pull request #508 from vector-im/bwindels/fix-393
don't (re)link fragments in fill, close gap if overlap w linked fragment
2021-09-24 15:42:49 +02:00
Bruno Windels
b75e2fe0ce decode straight to EventKey object 2021-09-24 15:40:33 +02:00
Bruno Windels
0d798178b0 log neighbor fragment id when closing gap 2021-09-23 18:50:40 +02:00
Bruno Windels
c6ed0abfd7 eventIds is missing 2021-09-23 18:49:23 +02:00
Bruno Windels
e6f7f213ec don't (re)link fragments in fill, close gap if overlap w linked fragment 2021-09-23 18:02:05 +02:00
Bruno Windels
45917eae1d
Merge pull request #494 from vector-im/DanilaFe/backfill-changes
Unit tests for GapWriter, using a new timeline mock utility
2021-09-23 10:15:37 +02:00
Bruno Windels
4b7cb6da9e make backfill limit explicit 2021-09-23 10:10:22 +02:00
Bruno Windels
dd71fdbe08 add comment 2021-09-23 10:04:58 +02:00
Bruno Windels
6c12f0f66f release v0.2.12 2021-09-22 10:45:46 +02:00
Bruno Windels
0848306cb0
Merge pull request #506 from vector-im/bwindels/fix-504
Drop events that have been synced before
2021-09-22 10:44:55 +02:00
Bruno Windels
498c00fe3c no need for try catch here as we already catch in getKeys 2021-09-22 10:38:29 +02:00
Bruno Windels
ac5a4c2bc6 pass log item everywhere to tryInsert 2021-09-22 10:33:40 +02:00
Bruno Windels
b58e10521f don't log tryInsert failures anymore as everything is logged in Store 2021-09-22 10:23:28 +02:00
Bruno Windels
1963635dd7 also log index keys for a value when write fails in Store 2021-09-22 10:22:52 +02:00
Bruno Windels
a19d93dbef don't swallow anything that isn't a request error 2021-09-22 09:36:26 +02:00
Bruno Windels
704d7b32da add tests 2021-09-21 21:04:29 +02:00
Bruno Windels
6cded5319a change timelineEventStore.insert to tryInsert 2021-09-21 21:04:10 +02:00
Bruno Windels
12add19c31 add Store.tryAdd, which prevent abort on ConstraintError 2021-09-21 21:03:29 +02:00
Bruno Windels
0d486a14f6 add the logger property to the null logger as well, forgot this before 2021-09-21 21:01:35 +02:00
Bruno Windels
2d2521cd9a add prototype to show we can prevent the txn from being aborted on error 2021-09-21 20:58:16 +02:00
Danila Fedorin
92dcc6c980 Remove duplicated lines 2021-09-21 09:39:09 -07:00
Bruno Windels
b8548afb71 export temporary vm typ def 2021-09-21 10:41:01 +02:00
Bruno Windels
07c6bf7055
Merge pull request #503 from vector-im/bwindels/fix-reactions-vm-null
use mapped value rather than reading parent vm again
2021-09-21 10:09:05 +02:00
Bruno Windels
a020febf8d use mapped value rather than reading parent vm again 2021-09-21 10:02:17 +02:00
Danila Fedorin
a3a743881d Make test adjustments requested in PR. 2021-09-20 19:37:39 -07:00
Danila Fedorin
50c8b995c3 Undo GapWriter algorithm changes 2021-09-20 18:41:01 -07:00
Bruno Windels
0d6881ad22
Merge pull request #502 from vector-im/fix-util-ts-import
Fix util import
2021-09-20 10:19:45 +02:00
Danila Fedorin
6d84cc0a81 Fix util import 2021-09-17 14:51:20 -07:00
RMidhunSuresh
9e9099f5d0 Restructure and add syntax
Signed-off-by: RMidhunSuresh <rmidhunsuresh@gmail.com>
2021-06-08 23:05:33 +05:30
RMidhunSuresh
4d79279f42 Add some content
Signed-off-by: RMidhunSuresh <rmidhunsuresh@gmail.com>
2021-06-08 21:13:52 +05:30
419 changed files with 17063 additions and 9908 deletions

View file

@ -13,5 +13,13 @@ module.exports = {
"no-empty": "off", "no-empty": "off",
"no-prototype-builtins": "off", "no-prototype-builtins": "off",
"no-unused-vars": "warn" "no-unused-vars": "warn"
},
"globals": {
"DEFINE_VERSION": "readonly",
"DEFINE_GLOBAL_HASH": "readonly",
// only available in sw.js
"DEFINE_UNHASHED_PRECACHED_ASSETS": "readonly",
"DEFINE_HASHED_PRECACHED_ASSETS": "readonly",
"DEFINE_HASHED_CACHED_ON_REQUEST_ASSETS": "readonly"
} }
}; };

44
.github/workflows/docker-publish.yml vendored Normal file
View file

@ -0,0 +1,44 @@
name: Container Image
on:
push:
branches: [ master ]
tags: [ 'v*' ]
pull_request:
branches: [ master ]
env:
IMAGE_NAME: ${{ github.repository }}
REGISTRY: ghcr.io
jobs:
push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Log into registry ${{ env.REGISTRY }}
uses: docker/login-action@v1
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v3
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- name: Build and push Docker image
uses: docker/build-push-action@v2
with:
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

3
.gitignore vendored
View file

@ -1,5 +1,6 @@
*.sublime-project *.sublime-project
*.sublime-workspace *.sublime-workspace
.DS_Store
node_modules node_modules
fetchlogs fetchlogs
sessionexports sessionexports
@ -8,3 +9,5 @@ target
lib lib
*.tar.gz *.tar.gz
.eslintcache .eslintcache
.tmp
tmp/

View file

@ -19,6 +19,7 @@ module.exports = {
], ],
rules: { rules: {
"@typescript-eslint/no-floating-promises": 2, "@typescript-eslint/no-floating-promises": 2,
"@typescript-eslint/no-misused-promises": 2 "@typescript-eslint/no-misused-promises": 2,
"semi": ["error", "always"]
} }
}; };

18
.woodpecker.yml Normal file
View file

@ -0,0 +1,18 @@
pipeline:
buildfrontend:
image: node:16
commands:
- yarn install --prefer-offline --frozen-lockfile
- yarn test
- yarn run lint-ci
- yarn run tsc
- yarn build
deploy:
image: python
when:
event: push
branch: master
commands:
- make ci-deploy
secrets: [ GITEA_WRITE_DEPLOY_KEY, LIBREPAGES_DEPLOY_SECRET ]

150
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,150 @@
Contributing code to hydrogen-web
==================================
Everyone is welcome to contribute code to hydrogen-web, provided that they are
willing to license their contributions under the same license as the project
itself. We follow a simple 'inbound=outbound' model for contributions: the act
of submitting an 'inbound' contribution means that the contributor agrees to
license the code under the same terms as the project's overall 'outbound'
license - in this case, Apache Software License v2 (see
[LICENSE](LICENSE)).
How to contribute
-----------------
The preferred and easiest way to contribute changes to the project is to fork
it on github, and then create a pull request to ask us to pull your changes
into our repo (https://help.github.com/articles/using-pull-requests/)
We use GitHub's pull request workflow to review the contribution, and either
ask you to make any refinements needed or merge it and make them ourselves.
Things that should go into your PR description:
* References to any bugs fixed by the change (in GitHub's `Fixes` notation)
* Describe the why and what is changing in the PR description so it's easy for
onlookers and reviewers to onboard and context switch.
* If your PR makes visual changes, include both **before** and **after** screenshots
to easily compare and discuss what's changing.
* Include a step-by-step testing strategy so that a reviewer can check out the
code locally and easily get to the point of testing your change.
* Add comments to the diff for the reviewer that might help them to understand
why the change is necessary or how they might better understand and review it.
We use continuous integration, and all pull requests get automatically tested:
if your change breaks the build, then the PR will show that there are failed
checks, so please check back after a few minutes.
Tests
-----
If your PR is a feature then we require that the PR also includes tests.
These need to test that your feature works as expected and ideally test edge cases too.
Tests are written as unit tests by exporting a `tests` function from the file to be tested.
The function returns an object where the key is the test label, and the value is a
function that accepts an [assert](https://nodejs.org/api/assert.html) object, and return a Promise or nothing.
Note that there is currently a limitation that files that are not indirectly included from `src/platform/web/main.js` won't be found by the runner.
You can run the tests by running `yarn test`.
This uses the [impunity](https://github.com/bwindels/impunity) runner.
We don't require tests for bug fixes.
In the future we may formalise this more.
Code style
----------
The js-sdk aims to target TypeScript/ES6. All new files should be written in
TypeScript and existing files should use ES6 principles where possible.
Please disable any automatic formatting tools you may have active.
If present, you'll be asked to undo any unrelated whitespace changes during code review.
Members should not be exported as a default export in general.
In general, avoid using `export default`.
The remaining code-style for hydrogen is [in the process of being documented](codestyle.md), but
contributors are encouraged to read the
[code style document for matrix-react-sdk](https://github.com/matrix-org/matrix-react-sdk/blob/master/code_style.md)
and follow the principles set out there.
Please ensure your changes match the cosmetic style of the existing project,
and ***never*** mix cosmetic and functional changes in the same commit, as it
makes it horribly hard to review otherwise.
Attribution
-----------
If you change or create a file, feel free to add yourself to the copyright holders
in the license header of that file.
Sign off
--------
In order to have a concrete record that your contribution is intentional
and you agree to license it under the same terms as the project's license, we've
adopted the same lightweight approach that the Linux Kernel
(https://www.kernel.org/doc/Documentation/SubmittingPatches), Docker
(https://github.com/docker/docker/blob/master/CONTRIBUTING.md), and many other
projects use: the DCO (Developer Certificate of Origin:
http://developercertificate.org/). This is a simple declaration that you wrote
the contribution or otherwise have the right to contribute it to Matrix:
```
Developer Certificate of Origin
Version 1.1
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
660 York Street, Suite 102,
San Francisco, CA 94110 USA
Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.
Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.
```
If you agree to this for your contribution, then all that's needed is to
include the line in your commit or pull request comment:
```
Signed-off-by: Your Name <your@email.example.org>
```
We accept contributions under a legally identifiable name, such as your name on
government documentation or common-law names (names claimed by legitimate usage
or repute). Unfortunately, we cannot accept anonymous contributions at this
time.
Git allows you to add this signoff automatically when using the `-s` flag to
`git commit`, which uses the name and email set in your `user.name` and
`user.email` git configs.
If you forgot to sign off your commits before making your pull request and are
on Git 2.17+ you can mass signoff using rebase:
```
git rebase --signoff origin/develop
```

View file

@ -1,5 +1,5 @@
FROM docker.io/node:alpine as builder FROM docker.io/node:alpine as builder
RUN apk add --no-cache git RUN apk add --no-cache git python3 build-base
COPY . /app COPY . /app
WORKDIR /app WORKDIR /app
RUN yarn install \ RUN yarn install \

View file

@ -1,5 +1,5 @@
FROM docker.io/node:alpine FROM docker.io/node:alpine
RUN apk add --no-cache git RUN apk add --no-cache git python3 build-base
COPY . /code COPY . /code
WORKDIR /code WORKDIR /code
RUN yarn install RUN yarn install

14
Makefile Normal file
View file

@ -0,0 +1,14 @@
ci-deploy: ## Deploy from CI/CD. Only call from within CI
@if [ "${CI}" != "woodpecker" ]; \
then echo "Only call from within CI. Will re-write your local Git configuration. To override, set export CI=woodpecker"; \
exit 1; \
fi
git config --global user.email "${CI_COMMIT_AUTHOR_EMAIL}"
git config --global user.name "${CI_COMMIT_AUTHOR}"
./scripts/ci.sh --commit-files librepages target "${CI_COMMIT_AUTHOR} <${CI_COMMIT_AUTHOR_EMAIL}>"
./scripts/ci.sh --init "$$GITEA_WRITE_DEPLOY_KEY"
./scripts/ci.sh --deploy ${LIBREPAGES_DEPLOY_SECRET} librepages
./scripts/ci.sh --clean
help: ## Prints help for targets with comments
@cat $(MAKEFILE_LIST) | grep -E '^[a-zA-Z_-]+:.*?## .*$$' | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'

View file

@ -1,3 +1,5 @@
[![status-badge](https://ci.batsense.net/api/badges/mystiq/hydrogen-web/status.svg)](https://ci.batsense.net/mystiq/hydrogen-web)
# Hydrogen # Hydrogen
A minimal [Matrix](https://matrix.org/) chat client, focused on performance, offline functionality, and broad browser support. This is work in progress and not yet ready for primetime. Bug reports are welcome, but please don't file any feature requests or other missing things to be on par with Element Web. A minimal [Matrix](https://matrix.org/) chat client, focused on performance, offline functionality, and broad browser support. This is work in progress and not yet ready for primetime. Bug reports are welcome, but please don't file any feature requests or other missing things to be on par with Element Web.
@ -10,13 +12,34 @@ Hydrogen's goals are:
- It is a standalone webapp, but can also be easily embedded into an existing website/webapp to add chat capabilities. - It is a standalone webapp, but can also be easily embedded into an existing website/webapp to add chat capabilities.
- Loading (unused) parts of the application after initial page load should be supported - Loading (unused) parts of the application after initial page load should be supported
For embedded usage, see the [SDK instructions](doc/SDK.md).
If you find this interesting, come and discuss on [`#hydrogen:matrix.org`](https://matrix.to/#/#hydrogen:matrix.org). If you find this interesting, come and discuss on [`#hydrogen:matrix.org`](https://matrix.to/#/#hydrogen:matrix.org).
# How to use # How to use
Hydrogen is deployed to [hydrogen.element.io](https://hydrogen.element.io). You can run it locally `yarn install` (only the first time) and `yarn start` in the terminal, and point your browser to `http://localhost:3000`. If you prefer, you can also [use docker](doc/docker.md). Hydrogen is deployed to [hydrogen.element.io](https://hydrogen.element.io). You can also deploy Hydrogen on your own web server:
Hydrogen uses symbolic links in the codebase, so if you are on Windows, have a look at [making git & symlinks work](https://github.com/git-for-windows/git/wiki/Symbolic-Links) there. 1. Download the [latest release package](https://github.com/vector-im/hydrogen-web/releases).
1. Extract the package to the public directory of your web server.
1. If this is your first deploy:
1. copy `config.sample.json` to `config.json` and if needed, make any modifications (unless you've set up your own [sygnal](https://github.com/matrix-org/sygnal) instance, you don't need to change anything in the `push` section).
1. Disable caching entirely on the server for:
- `index.html`
- `sw.js`
- `config.json`
- All theme manifests referenced in the `themeManifests` of `config.json`, these files are typically called `theme-{name}.json`.
These resources will still be cached client-side by the service worker. Because of this; you'll still need to refresh the app twice before config.json changes are applied.
## Set up a dev environment
You can run Hydrogen locally by the following commands in the terminal:
- `yarn install` (only the first time)
- `yarn start` in the terminal
Now point your browser to `http://localhost:3000`. If you prefer, you can also [use docker](doc/docker.md).
# FAQ # FAQ

View file

@ -8,3 +8,5 @@
otherwise it becomes hard to remember what was a default/named export otherwise it becomes hard to remember what was a default/named export
- should we return promises from storage mutation calls? probably not, as we don't await them anywhere. only read calls should return promises? - should we return promises from storage mutation calls? probably not, as we don't await them anywhere. only read calls should return promises?
- we don't anymore - we don't anymore
- don't use these features, as they are not widely enough supported.
- [lookbehind in regular expressions](https://caniuse.com/js-regexp-lookbehind)

View file

@ -28,14 +28,8 @@ You can only verify by comparing keys manually currently. In Element, go to your
## I want to host my own Hydrogen, how do I do that? ## I want to host my own Hydrogen, how do I do that?
There are no published builds at this point. You need to checkout the version you want to build, or master if you want to run bleeding edge, and run `yarn install` and then `yarn build` in a console (and install nodejs > 14 and yarn if you haven't yet). Now you should find all the files needed to host Hydrogen in the `target/` folder, just copy them all over to your server. As always, don't host your client on the same [origin](https://web.dev/same-origin-policy/#what's-considered-same-origin) as your homeserver. Published builds can be found at https://github.com/vector-im/hydrogen-web/releases. For building your own, you need to checkout the version you want to build, or master if you want to run bleeding edge, and run `yarn install` and then `yarn build` in a console (and install nodejs >= 15 and yarn if you haven't yet). Now you should find all the files needed to host Hydrogen in the `target/` folder, just copy them all over to your server. As always, don't host your client on the same [origin](https://web.dev/same-origin-policy/#what's-considered-same-origin) as your homeserver.
## I want to embed Hydrogen in my website, how should I do that? ## I want to embed Hydrogen in my website, how should I do that?
There are no npm modules yet published for Hydrogen. The easiest is probably to setup your website project, do yarn/npm init if you haven't yet, then add the hydrogen repo as a git http dependency, and import the files/classes you want to use from Hydrogen. Hydrogen aims to be usable as an SDK, and while it is still early days, you can find some documentation how to do that in [SDK.md](SDK.md).
For example, for a single room chat, you could create an instance of `Platform`, you create a new `SessionContainer` with it, call `startWithLogin` on it, observe `sessionContainer.loadStatus` to know when initial sync is done, then do `sessionContainer.session.rooms.get('roomid')` and you create a `RoomViewModel` with it and pass that to a `RoomView`. Then you call `document.appendChild(roomView.mount())` and you should see a syncing room.
Feel free to ask for pointers in #hydrogen:matrix.org as the documentation is still lacking considerably. Note that at this early, pre 1.0 stage of the project, there is no promise of API stability yet.
Also, to make end-to-end encryption work, you'll likely need some tweaks to your build system, see [this issue](https://github.com/vector-im/hydrogen-web/issues/467).

11
doc/IMPORT-ISSUES.md Normal file
View file

@ -0,0 +1,11 @@
## How to import common-js dependency using ES6 syntax
---
Until [#6632](https://github.com/vitejs/vite/issues/6632) is fixed, such imports should be done as follows:
```ts
import * as pkg from "off-color";
// @ts-ignore
const offColor = pkg.offColor ?? pkg.default.offColor;
```
This way build, dev server and unit tests should all work.

116
doc/SDK.md Normal file
View file

@ -0,0 +1,116 @@
# Hydrogen View SDK
The Hydrogen view SDK allows developers to integrate parts of the Hydrogen application into the UI of their own application. Hydrogen is written with the MVVM pattern, so to construct a view, you'd first construct a view model, which you then pass into the view. For most view models, you will first need a running client.
## Example
The Hydrogen SDK requires some assets to be shipped along with your app for things like downloading attachments, and end-to-end encryption. A convenient way to make this happen is provided by the SDK (importing `hydrogen-view-sdk/paths/vite`) but depends on your build system. Currently, only [vite](https://vitejs.dev/) is supported, so that's what we'll be using in the example below.
You can create a vite project using the following commands:
```sh
# you can pick "vanilla-ts" here for project type if you're not using react or vue
yarn create vite
cd <your-project-name>
yarn
yarn add hydrogen-view-sdk
```
You should see a `index.html` in the project root directory, containing an element with `id="app"`. Add the attribute `class="hydrogen"` to this element, as the CSS we'll include from the SDK assumes for now that the app is rendered in an element with this classname.
If you go into the `src` directory, you should see a `main.ts` file. If you put this code in there, you should see a basic timeline after login and initial sync have finished (might take a while before you see anything on the screen actually).
You'll need to provide the username and password of a user that is already in the [#element-dev:matrix.org](https://matrix.to/#/#element-dev:matrix.org) room (or change the room id).
```ts
import {
Platform,
Client,
LoadStatus,
createNavigation,
createRouter,
RoomViewModel,
TimelineView,
viewClassForTile
} from "hydrogen-view-sdk";
import downloadSandboxPath from 'hydrogen-view-sdk/download-sandbox.html?url';
import workerPath from 'hydrogen-view-sdk/main.js?url';
import olmWasmPath from '@matrix-org/olm/olm.wasm?url';
import olmJsPath from '@matrix-org/olm/olm.js?url';
import olmLegacyJsPath from '@matrix-org/olm/olm_legacy.js?url';
const assetPaths = {
downloadSandbox: downloadSandboxPath,
worker: workerPath,
olm: {
wasm: olmWasmPath,
legacyBundle: olmLegacyJsPath,
wasmBundle: olmJsPath
}
};
import "hydrogen-view-sdk/assets/theme-element-light.css";
// OR import "hydrogen-view-sdk/assets/theme-element-dark.css";
async function main() {
const app = document.querySelector<HTMLDivElement>('#app')!
const config = {};
const platform = new Platform({container: app, assetPaths, config, options: { development: import.meta.env.DEV }});
const navigation = createNavigation();
platform.setNavigation(navigation);
const urlRouter = createRouter({
navigation: navigation,
history: platform.history
});
urlRouter.attach();
const client = new Client(platform);
const loginOptions = await client.queryLogin("matrix.org").result;
client.startWithLogin(loginOptions.password("username", "password"));
await client.loadStatus.waitFor((status: string) => {
return status === LoadStatus.Ready ||
status === LoadStatus.Error ||
status === LoadStatus.LoginFailed;
}).promise;
if (client.loginFailure) {
alert("login failed: " + client.loginFailure);
} else if (client.loadError) {
alert("load failed: " + client.loadError.message);
} else {
const {session} = client;
// looks for room corresponding to #element-dev:matrix.org, assuming it is already joined
const room = session.rooms.get("!bEWtlqtDwCLFIAKAcv:matrix.org");
const vm = new RoomViewModel({
room,
ownUserId: session.userId,
platform,
urlCreator: urlRouter,
navigation,
});
await vm.load();
const view = new TimelineView(vm.timelineViewModel, viewClassForTile);
app.appendChild(view.mount());
}
}
main();
```
## Typescript support
Typescript support is not yet available while we're converting the Hydrogen codebase to Typescript.
In your `src` directory, you'll need to add a `.d.ts` (can be called anything, e.g. `deps.d.ts`)
containing this snippet to make Typescript not complain that `hydrogen-view-sdk` doesn't have types:
```ts
declare module "hydrogen-view-sdk";
```
## API Stability
This library follows semantic versioning; there is no API stability promised as long as the major version is still 0. Once 1.0.0 is released, breaking changes will be released with a change in major versioning.
## Third-party licenses
This package bundles the bs58 package ([license](https://github.com/cryptocoinjs/bs58/blob/master/LICENSE)), and the Inter font ([license](https://github.com/rsms/inter/blob/master/LICENSE.txt)).

204
doc/THEMING.md Normal file
View file

@ -0,0 +1,204 @@
# Theming Documentation
## Basic Architecture
A **theme collection** in Hydrogen is represented by a `manifest.json` file and a `theme.css` file.
The manifest specifies variants (eg: dark,light ...) each of which is a **theme** and maps to a single css file in the build output.
Each such theme is produced by changing the values of variables in the base `theme.css` file with those specified in the variant section of the manifest:
![](images/theming-architecture.png)
More in depth explanations can be found in later sections.
## Structure of `manifest.json`
[See theme.ts](../src/platform/types/theme.ts)
## Variables
CSS variables specific to a particular variant are specified in the `variants` section of the manifest:
```json=
"variants": {
"light": {
...
"variables": {
"background-color-primary": "#fff",
"text-color": "#2E2F32",
}
},
"dark": {
...
"variables": {
"background-color-primary": "#21262b",
"text-color": "#fff",
}
}
}
```
These variables will appear in the css file (theme.css):
```css=
body {
background-color: var(--background-color-primary);
color: var(--text-color);
}
```
During the build process, this would result in the creation of two css files (one for each variant) where the variables are substitued with the corresponding values specified in the manifest:
*element-light.css*:
```css=
body {
background-color: #fff;
color: #2E2F32;
}
```
*element-dark.css*:
```css=
body {
background-color: #21262b;
color: #fff;
}
```
## Derived Variables
In addition to simple substitution of variables in the stylesheet, it is also possible to instruct the build system to first produce a new value from the base variable value before the substitution.
Such derived variables have the form `base_css_variable--operation-arg` and can be read as:
apply `operation` to `base_css_variable` with argument `arg`.
Continuing with the previous example, it possible to specify:
```css=
.left-panel {
/* background color should be 20% more darker
than background-color-primary */
background-color: var(--background-color-primary--darker-20);
}
```
Currently supported operations are:
| Operation | Argument | Operates On |
| -------- | -------- | -------- |
| darker | percentage | color |
| lighter | percentage | color |
## Aliases
It is possible give aliases to variables in the `theme.css` file:
```css=
:root {
font-size: 10px;
/* Theme aliases */
--icon-color: var(--background-color-secondary--darker-40);
}
```
It is possible to further derive from these aliased variables:
```css=
div {
background: var(--icon-color--darker-20);
--my-alias: var(--icon-color--darker-20);
/* Derive from aliased variable */
color: var(--my-alias--lighter-15);
}
```
## Colorizing svgs
Along with a change in color-scheme, it may be necessary to change the colors in the svg icons and images.
This can be done by supplying the preferred colors with query parameters:
`my-awesome-logo.svg?primary=base-variable-1&secondary=base-variable-2`
This instructs the build system to colorize the svg with the given primary and secondary colors.
`base-variable-1` and `base-variable-2` are the css-variables specified in the `variables` section of the manifest.
For colorizing svgs, the source svg must use `#ff00ff` as the primary color and `#00ffff` as the secondary color:
| ![](images/svg-icon-example.png) | ![](images/coloring-process.png) |
| :--: |:--: |
| **original source image** | **transformation process** |
## Creating your own theme variant in Hydrogen
If you're looking to change the color-scheme of the existing Element theme, you only need to add your own variant to the existing `manifest.json`.
The steps are fairly simple:
1. Copy over an existing variant to the variants section of the manifest.
2. Change `dark`, `default` and `name` fields.
3. Give new values to each variable in the `variables` section.
4. Build hydrogen.
## Creating your own theme collection in Hydrogen
If a theme variant does not solve your needs, you can create a new theme collection with a different base `theme.css` file.
1. Create a directory for your new theme-collection under `src/platform/web/ui/css/themes/`.
2. Create `manifest.json` and `theme.css` files within the newly created directory.
3. Populate `manifest.json` with the base css variables you wish to use.
4. Write styles in your `theme.css` file using the base variables, derived variables and colorized svg icons.
5. Tell the build system where to find this theme-collection by providing the location of this directory to the `themeBuilder` plugin in `vite.config.js`:
```json=
...
themeBuilder({
themeConfig: {
themes: {
element: "./src/platform/web/ui/css/themes/element",
awesome: "path/to/theme-directory"
},
default: "element",
},
compiledVariables,
}),
...
```
6. Build Hydrogen.
## Changing the default theme
To change the default theme used in Hydrogen, modify the `defaultTheme` field in `config.json` file (which can be found in the build output):
```json=
"defaultTheme": {
"light": theme-id,
"dark": theme-id
}
```
Here *theme-id* is of the form `theme-variant` where `theme` is the key used when specifying the manifest location of the theme collection in `vite.config.js` and `variant` is the key used in variants section of the manifest.
Some examples of theme-ids are `element-dark` and `element-light`.
To find the theme-id of some theme, you can look at the built-asset section of the manifest in the build output.
This default theme will render as "Default" option in the theme-chooser dropdown. If the device preference is for dark theme, the dark default is selected and vice versa.
**You'll need to reload twice so that Hydrogen picks up the config changes!**
# Derived Theme(Collection)
This allows users to theme Hydrogen without the need for rebuilding. Derived theme collections can be thought of as extensions (derivations) of some existing build time theme.
## Creating a derived theme:
Here's how you create a new derived theme:
1. You create a new theme manifest file (eg: theme-awesome.json) and mention which build time theme you're basing your new theme on using the `extends` field. The base css file of the mentioned theme is used for your new theme.
2. You configure the theme manifest as usual by populating the `variants` field with your desired colors.
3. You add your new theme manifest to the list of themes in `config.json`.
Refresh Hydrogen twice (once to refresh cache, and once to load) and the new theme should show up in the theme chooser.
## How does it work?
For every theme collection in hydrogen, the build process emits a runtime css file which like the built theme css file contains variables in the css code. But unlike the theme css file, the runtime css file lacks the definition for these variables:
CSS for the built theme:
```css
:root {
--background-color-primary: #f2f20f;
}
body {
background-color: var(--background-color-primary);
}
```
and the corresponding runtime theme:
```css
/* Notice the lack of definiton for --background-color-primary here! */
body {
background-color: var(--background-color-primary);
}
```
When hydrogen loads a derived theme, it takes the runtime css file of the extended theme and dynamically adds the variable definition based on the values specified in the manifest. Icons are also colored dynamically and injected as variables using Data URIs.

View file

@ -1,7 +1,38 @@
# Typescript migration # Typescript style guide
## Introduce `abstract` & `override` ## Use `type` rather than `interface` for named parameters and POJO return values.
- find all methods and getters that throw or are empty in base classes and turn into abstract method or if all methods are abstract, into an interface. `type` and `interface` can be used somewhat interchangeably, but let's use `type` to describe data and `interface` to describe (polymorphic) behaviour.
- change child impls to not call super.method and to add override
- don't allow implicit override in ts config Good examples of data are option objects to have named parameters, and POJO (plain old javascript objects) without any methods, just fields.
Also see [this playground](https://www.typescriptlang.org/play?#code/C4TwDgpgBACghgJwgO2AeTMAlge2QZygF4oBvAKCiqmTgFsIAuKfYBLZAcwG5LqATCABs4IAPzNkAVzoAjCAl4BfcuVCQoAYQAWWIfwzY8hEvCSpDuAlABkZPlQDGOITgTNW7LstWOR+QjMUYHtqKGcCNilHYDcAChxMK3xmIIsk4wBKewcoFRVyPzgArV19KAgAD2AUfkDEYNDqCM9o2IQEjIJmHT0DLvxsijCw-ClIDsSjAkzeEebjEIYAuE5oEgADABJSKeSAOloGJSgsQh29433nVwQlDbnqfKA)
## Use `type foo = { [key: string]: any }` for types that you intend to fill in later.
For instance, if you have a method such as:
```js
function load(options) {
// ...
}
```
and you intend to type options at some later point, do:
```ts
type Options = { [key: string]: any}
```
This makes it much easier to add the necessary type information at a later time.
## Use `object` or `Record<string, any>` to describe a type that accepts any javascript object.
Sometimes a function or method may genuinely need to accept any object; eg:
```js
function encodeBody(body) {
// ...
}
```
In this scenario:
- Use `object` if you know that you will not access any property
- Use `Record<string, any>` if you need to access some property
Both usages prevent the type from accepting primitives (eg: string, boolean...).
If using `Record`, ensure that you have guards to check that the properties really do exist.

3
doc/UI/index.md Normal file
View file

@ -0,0 +1,3 @@
# Index for UI code
1. [Rendering DOM elements](./render-dom-elements.md)

View file

@ -0,0 +1,47 @@
tldr; Use `tag` from `ui/general/html.js` to quickly create DOM elements.
## Syntax
---
The general syntax is as follows:
```js
tag.tag_name({attribute1: value, attribute2: value, ...}, [child_elements]);
```
**tag_name** can be any one of the following:
```
br, a, ol, ul, li, div, h1, h2, h3, h4, h5, h6,
p, strong, em, span, img, section, main, article, aside,
pre, button, time, input, textarea, label, form, progress, output, video
```
<br />
eg:
Here is an example HTML segment followed with the code to create it in Hydrogen.
```html
<section class="main-section">
<h1>Demo</h1>
<button class="btn_cool">Click me</button>
</section>
```
```js
tag.section({className: "main-section"},[
tag.h1("Demo"),
tag.button({className:"btn_cool"}, "Click me")
]);
```
<br />
**Note:** In views based on `TemplateView`, you will see `t` used instead of `tag`.
`t` is is `TemplateBuilder` object passed to the render function in `TemplateView`.
Although syntactically similar, they are not functionally equivalent.
Primarily `t` **supports** bindings and event handlers while `tag` **does not**.
```js
// The onClick here wont work!!
tag.button({className:"awesome-btn", onClick: () => this.foo()});
render(t, vm){
// The onClick works here.
t.button({className:"awesome-btn", onClick: () => this.foo()});
}
```

206
doc/UI/ui.md Normal file
View file

@ -0,0 +1,206 @@
## IView components
The [interface](https://github.com/vector-im/hydrogen-web/blob/master/src/platform/web/ui/general/types.ts) adopted by view components is agnostic of how they are rendered to the DOM. This has several benefits:
- it allows Hydrogen to not ship a [heavy view framework](https://bundlephobia.com/package/react-dom@18.2.0) that may or may not be used by its SDK users, and also keep bundle size of the app down.
- Given the interface is quite simple, is should be easy to integrate this interface into the render lifecycle of other frameworks.
- The main implementations used in Hydrogen are [`ListView`](https://github.com/vector-im/hydrogen-web/blob/master/src/platform/web/ui/general/ListView.ts) (rendering [`ObservableList`](https://github.com/vector-im/hydrogen-web/blob/master/src/observable/list/BaseObservableList.ts)s) and [`TemplateView`](https://github.com/vector-im/hydrogen-web/blob/master/src/platform/web/ui/general/TemplateView.ts) (templating and one-way databinding), each only a few 100 lines of code and tailored towards their specific use-case. They work straight with the DOM API and have no other dependencies.
- a common inteface allows us to mix and match between these different implementations (and gradually shift if need be in the future) with the code.
## Templates
### Template language
Templates use a mini-DSL language in pure javascript to express declarative templates. This is basically a very thin wrapper around `document.createElement`, `document.createTextNode`, `node.setAttribute` and `node.appendChild` to quickly create DOM trees. The general syntax is as follows:
```js
t.tag_name({attribute1: value, attribute2: value, ...}, [child_elements]);
t.tag_name(child_element);
t.tag_name([child_elements]);
```
**tag_name** can be [most HTML or SVG tags](https://github.com/vector-im/hydrogen-web/blob/master/src/platform/web/ui/general/html.ts#L102-L110).
eg:
Here is an example HTML segment followed with the code to create it in Hydrogen.
```html
<section class="main-section">
<h1>Demo</h1>
<button class="btn_cool">Click me</button>
</section>
```
```js
t.section({className: "main-section"},[
t.h1("Demo"),
t.button({className:"btn_cool"}, "Click me")
]);
```
All these functions return DOM element nodes, e.g. the result of `document.createElement`.
### TemplateView
`TemplateView` builds on top of templating by adopting the IView component model and adding event handling attributes, sub views and one-way databinding.
In views based on `TemplateView`, you will see a render method with a `t` argument.
`t` is `TemplateBuilder` object passed to the render function in `TemplateView`. It also takes a data object to render and bind to, often called `vm`, short for view model from the MVVM pattern Hydrogen uses.
You either subclass `TemplateView` and override the `render` method:
```js
class MyView extends TemplateView {
render(t, vm) {
return t.div(...);
}
}
```
Or you pass a render function to `InlineTemplateView`:
```js
new InlineTemplateView(vm, (t, vm) => {
return t.div(...);
});
```
**Note:** the render function is only called once to build the initial DOM tree and setup bindings, etc ... Any subsequent updates to the DOM of a component happens through bindings.
#### Event handlers
Any attribute starting with `on` and having a function as a value will be attached as an event listener on the given node. The event handler will be removed during unmounting.
```js
t.button({onClick: evt => {
vm.doSomething(evt.target.value);
}}, "Click me");
```
#### Subviews
`t.view(instance)` will mount the sub view (can be any IView) and return its root node so it can be attached in the DOM tree.
All subviews will be unmounted when the parent view gets unmounted.
```js
t.div({className: "Container"}, t.view(new ChildView(vm.childViewModel)));
```
#### One-way data-binding
A binding couples a part of the DOM to a value on the view model. The view model emits an update when any of its properties change, to which the view can subscribe. When an update is received by the view, it will reevaluate all the bindings, and update the DOM accordingly.
A binding can appear in many places where a static value can usually be used in the template tree.
To create a binding, you pass a function that maps the view value to a static value.
##### Text binding
```js
t.p(["I've got ", vm => vm.counter, " beans"])
```
##### Attribute binding
```js
t.button({disabled: vm => vm.isBusy}, "Submit");
```
##### Class-name binding
```js
t.div({className: {
button: true,
active: vm => vm.isActive
}})
```
##### Subview binding
So far, all the bindings can only change node values within our tree, but don't change the structure of the DOM. A sub view binding allows you to conditionally add a subview based on the result of a binding function.
All sub view bindings return a DOM (element or comment) node and can be directly added to the DOM tree by including them in your template.
###### map
`t.mapView` allows you to choose a view based on the result of the binding function:
```js
t.mapView(vm => vm.count, count => {
return count > 5 ? new LargeView(count) : new SmallView(count);
});
```
Every time the first or binding function returns a different value, the second function is run to create a new view to replace the previous view.
You can also return `null` or `undefined` from the second function to indicate a view should not be rendered. In this case a comment node will be used as a placeholder.
There is also a `t.map` which will create a new template view (with the same value) and you directly provide a render function for it:
```js
t.map(vm => vm.shape, (shape, t, vm) => {
switch (shape) {
case "rect": return t.rect();
case "circle": return t.circle();
}
})
```
###### if
`t.ifView` will render the subview if the binding returns a truthy value:
```js
t.ifView(vm => vm.isActive, vm => new View(vm.someValue));
```
You equally have `t.if`, which creates a `TemplateView` and passes you the `TemplateBuilder`:
```js
t.if(vm => vm.isActive, (t, vm) => t.div("active!"));
```
##### Side-effects
Sometimes you want to imperatively modify your DOM tree based on the value of a binding.
`mapSideEffect` makes this easy to do:
```js
let node = t.div();
t.mapSideEffect(vm => vm.color, (color, oldColor) => node.style.background = color);
return node;
```
**Note:** you shouldn't add any bindings, subviews or event handlers from the side-effect callback,
the safest is to not use the `t` argument at all.
If you do, they will be added every time the callback is run and only cleaned up when the view is unmounted.
#### `tag` vs `t`
If you don't need a view component with data-binding, sub views and event handler attributes, the template language also is available in `ui/general/html.js` without any of these bells and whistles, exported as `tag`. As opposed to static templates with `tag`, you always use
`TemplateView` as an instance of a class, as there is some extra state to keep track (bindings, event handlers and subviews).
Although syntactically similar, `TemplateBuilder` and `tag` are not functionally equivalent.
Primarily `t` **supports** bindings and event handlers while `tag` **does not**. This is because to remove event listeners, we need to keep track of them, and thus we need to keep this state somewhere which
we can't do with a simple function call but we can insite the TemplateView class.
```js
// The onClick here wont work!!
tag.button({className:"awesome-btn", onClick: () => this.foo()});
class MyView extends TemplateView {
render(t, vm){
// The onClick works here.
t.button({className:"awesome-btn", onClick: () => this.foo()});
}
}
```
## ListView
A view component that renders and updates a list of sub views for every item in a `ObservableList`.
```js
const list = new ListView({
list: someObservableList
}, listValue => return new ChildView(listValue))
```
As items are added, removed, moved (change position) and updated, the DOM will be kept in sync.
There is also a `LazyListView` that only renders items in and around the current viewport, with the restriction that all items in the list must be rendered with the same height.
### Sub view updates
Unless the `parentProvidesUpdates` option in the constructor is set to `false`, the ListView will call the `update` method on the child `IView` component when it receives an update event for one of the items in the `ObservableList`.
This way, not every sub view has to have an individual listener on it's view model (a value from the observable list), and all updates go from the observable list to the list view, who then notifies the correct sub view.

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

109
doc/impl-thoughts/SDK.md Normal file
View file

@ -0,0 +1,109 @@
SDK:
- we need to compile src/lib.ts to javascript, with a d.ts file generated as well. We need to compile to javascript once for cjs and once of es modules. The package.json looks like this:
```
"main": "./dist/index.cjs",
"exports": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
},
"types": "dist/index.d.ts",
```
we don't need to bundle for the sdk case! we might need to do some transpilation to just plain ES6 (e.g. don't assume ?. and ??) we could use a browserslist query for this e.g. `node 14`. esbuild seems to support this as well, tldraw uses esbuild for their build.
one advantage of not bundling the files for the sdk is that you can still use import overrides in the consuming project build settings. is that an idiomatic way of doing things though?
this way we will support typescript, non-esm javascript and esm javascript using libhydrogen as an SDK
got this from https://medium.com/dazn-tech/publishing-npm-packages-as-native-es-modules-41ffbc0a9dea
how about the assets?
we also need to build the app
we need to be able to version libhydrogen independently from hydrogen the app? as any api breaking changes will need a major version increase. we probably want to end up with a monorepo where the app uses the sdk as well and we just use the local code with yarn link?
## Assets
we want to provide scss/sass files, but also css that can be included
https://github.com/webpack/webpack/issues/7353 seems to imply that we just need to include the assets in the published files and from there on it is the consumer of libhydrogen's problem.
how does all of this tie in with vite?
we want to have hydrogenapp be a consumer of libhydrogen, potentially as two packages in a monorepo ... but we want the SDK to expose views and stylesheets... without having an index.html (which would be in hydrogenapp). this seems a bit odd...?
what would be in hydrogenapp actually? just an index.html file?
I'm not sure it makes sense to have them be 2 different packages in a monorepo, they should really be two artifacts from the same directory.
the stylesheets included in libhydrogen are from the same main.css file as is used in the app
https://www.freecodecamp.org/news/build-a-css-library-with-vitejs/
basically, we import the sass file from src/lib.ts so it is included in the assets there too, and we also create a plugin that emits a file for every sass file as suggested in the link above?
we probably want two different build commands for the app and the sdk though, we could have a parent vite config that both build configs extend from?
### Dependency assets
our dependencies should not be bundled for the SDK case. So if we import aesjs, it would be up to the build system of the consuming project to make that import work.
the paths.ts thingy ... we want to make it easy for people to setup the assets for our dependencies (olm), some assets are also part of the sdk itself. it might make sense to make all of the assets there part of the sdk (e.g. bundle olm.wasm and friends?) although shipping crypto, etc ...
perhaps we should have an include file per build system that treats own assets and dep assets the same by including the package name as wel for our own deps:
```js
import _downloadSandboxPath from "@matrix-org/hydrogen-sdk/download-sandbox.html?url";
import _serviceWorkerPath from "@matrix-org/hydrogen-sdk/sw.js?url"; // not yet sure this is the way to do it
import olmWasmPath from "@matrix-org/olm/olm.wasm?url";
import olmJsPath from "@matrix-org/olm/olm.js?url";
import olmLegacyJsPath from "@matrix-org/olm/olm_legacy.js?url";
export const olmPaths = {
wasm: olmWasmPath,
legacyBundle: olmLegacyJsPath,
wasmBundle: olmJsPath,
};
export const downloadSandboxPath = _downloadSandboxPath;
```
we could put this file per build system, as ESM, in dist as well so you can include it to get the paths
## Tooling
- `vite` a more high-level build tool that takes your index.html and turns it into optimized assets that you can host for production, as well as a very fast dev server. is used to have good default settings for our tools, typescript support, and also deals with asset compiling. good dev server. Would be nice to have the same tool for dev and prod. vite has good support for using `import` for anything that is not javascript, where we had an issue with `snowpack` (to get the prod path of an asset).
- `rollup`: inlines
- `lerna` is used to handle multi-package monorepos
- `esbuild`: a js/ts build tool that we could use for building the lower level sdk where no other assets are involved, `vite` uses it for fast dev builds (`rollup` for prod). For now we won't extract a lower level sdk though.
## TODO
- finish vite app build (without IE11 for now?)
- create vite config to build src/lib.ts in cjs and esm, inheriting from a common base config with the app config
- this will create a dist folder with
- the whole source tree in es and cjs format
- an es file to import get the asset paths as they are expected by Platform, per build system
- assets from hydrogen itself:
- css files and any resource used therein
- download-sandbox.html
- a type declaration file (index.d.ts)
## Questions
- can rollup not bundle the source tree and leave modules intact?
- if we can use a function that creates a chunk per file to pass to manualChunks and disable chunk hashing we can probably do this. See https://rollupjs.org/guide/en/#outputmanualchunks
looks like we should be able to disable chunk name hashing with chunkFileNames https://rollupjs.org/guide/en/#outputoptions-object
we should test this with a vite test config
we also need to compile down to ES6, both for the app and for the sdk

View file

@ -1,19 +1,24 @@
{ {
"name": "hydrogen-web", "name": "hydrogen-web",
"version": "0.2.11", "version": "0.3.1",
"description": "A javascript matrix client prototype, trying to minize RAM usage by offloading as much as possible to IndexedDB", "description": "A javascript matrix client prototype, trying to minize RAM usage by offloading as much as possible to IndexedDB",
"main": "index.js",
"directories": { "directories": {
"doc": "doc" "doc": "doc"
}, },
"enginesStrict": {
"node": ">=15"
},
"scripts": { "scripts": {
"lint": "eslint --cache src/", "lint": "eslint --cache src/",
"lint-ts": "eslint src/ -c .ts-eslintrc.js --ext .ts", "lint-ts": "eslint src/ -c .ts-eslintrc.js --ext .ts",
"lint-ci": "eslint src/", "lint-ci": "eslint src/",
"test": "impunity --entry-point src/main.js --force-esm-dirs lib/ src/", "test": "impunity --entry-point src/platform/web/main.js src/platform/web/Platform.js --force-esm-dirs lib/ src/ --root-dir src/",
"start": "snowpack dev --port 3000", "test:postcss": "impunity --entry-point scripts/postcss/tests/css-compile-variables.test.js scripts/postcss/tests/css-url-to-variables.test.js",
"build": "node --experimental-modules scripts/build.mjs", "test:sdk": "yarn build:sdk && cd ./scripts/sdk/test/ && yarn --no-lockfile && node test-sdk-in-esm-vite-build-env.js && node test-sdk-in-commonjs-env.js",
"postinstall": "node ./scripts/post-install.js" "start": "vite --port 3000",
"build": "vite build && ./scripts/cleanup.sh",
"build:sdk": "./scripts/sdk/build.sh",
"watch:sdk": "./scripts/sdk/build.sh && yarn run vite build -c vite.sdk-lib-config.js --watch"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@ -26,46 +31,36 @@
}, },
"homepage": "https://github.com/vector-im/hydrogen-web/#readme", "homepage": "https://github.com/vector-im/hydrogen-web/#readme",
"devDependencies": { "devDependencies": {
"@babel/core": "^7.11.1",
"@babel/preset-env": "^7.11.0",
"@rollup/plugin-babel": "^5.1.0",
"@rollup/plugin-commonjs": "^15.0.0",
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-multi-entry": "^4.0.0",
"@rollup/plugin-node-resolve": "^9.0.0",
"@typescript-eslint/eslint-plugin": "^4.29.2", "@typescript-eslint/eslint-plugin": "^4.29.2",
"@typescript-eslint/parser": "^4.29.2", "@typescript-eslint/parser": "^4.29.2",
"autoprefixer": "^10.2.6", "acorn": "^8.6.0",
"cheerio": "^1.0.0-rc.3", "acorn-walk": "^8.2.0",
"commander": "^6.0.0", "aes-js": "^3.1.2",
"bs58": "^4.0.1",
"core-js": "^3.6.5", "core-js": "^3.6.5",
"es6-promise": "https://github.com/bwindels/es6-promise.git#bwindels/expose-flush",
"escodegen": "^2.0.0",
"eslint": "^7.32.0", "eslint": "^7.32.0",
"fake-indexeddb": "^3.1.2", "fake-indexeddb": "^3.1.2",
"finalhandler": "^1.1.1", "impunity": "^1.0.9",
"impunity": "^1.0.1",
"mdn-polyfills": "^5.20.0", "mdn-polyfills": "^5.20.0",
"merge-options": "^3.0.4",
"node-html-parser": "^4.0.0", "node-html-parser": "^4.0.0",
"postcss": "^8.1.1", "postcss-css-variables": "^0.18.0",
"postcss-css-variables": "^0.17.0", "postcss-flexbugs-fixes": "^5.0.2",
"postcss-flexbugs-fixes": "^4.2.1", "postcss-value-parser": "^4.2.0",
"postcss-import": "^12.0.1",
"postcss-url": "^8.0.0",
"regenerator-runtime": "^0.13.7", "regenerator-runtime": "^0.13.7",
"rollup": "^2.26.4", "svgo": "^2.8.0",
"rollup-plugin-cleanup": "^3.1.1", "text-encoding": "^0.7.0",
"serve-static": "^1.13.2", "typescript": "^4.7.0",
"snowpack": "^3.8.3", "vite": "^2.9.8",
"typescript": "^4.3.5",
"xxhashjs": "^0.2.2" "xxhashjs": "^0.2.2"
}, },
"dependencies": { "dependencies": {
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz", "@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz",
"aes-js": "^3.1.2",
"another-json": "^0.2.0", "another-json": "^0.2.0",
"base64-arraybuffer": "^0.2.0", "base64-arraybuffer": "^0.2.0",
"bs58": "^4.0.1",
"dompurify": "^2.3.0", "dompurify": "^2.3.0",
"es6-promise": "https://github.com/bwindels/es6-promise.git#bwindels/expose-flush", "off-color": "^2.0.0"
"text-encoding": "^0.7.0"
} }
} }

View file

@ -0,0 +1,100 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<script type="text/javascript">
class IDBError extends Error {
constructor(errorEvent) {
const request = errorEvent.target;
const {error} = request;
super(error.message);
this.name = error.name;
this.errorEvent = errorEvent;
}
preventAbort() {
this.errorEvent.preventDefault();
}
}
class AbortError extends Error {
get name() { return "AbortError"; }
}
function reqAsPromise(req) {
return new Promise(function (resolve, reject) {
req.onsuccess = function(e) {
resolve(e.target.result);
};
req.onerror = function(e) {
reject(new IDBError(e));
};
});
}
function txnAsPromise(txn) {
return new Promise((resolve, reject) => {
txn.addEventListener("complete", () => resolve());
txn.addEventListener("abort", event => {
reject(new AbortError());
});
});
}
function Storage(databaseName) {
this._databaseName = databaseName;
this._database = null;
}
Storage.prototype = {
open: function() {
const req = window.indexedDB.open(this._databaseName);
const self = this;
req.onupgradeneeded = function(ev) {
const db = ev.target.result;
const oldVersion = ev.oldVersion;
self._createStores(db, oldVersion);
};
return reqAsPromise(req).then(function() {
self._database = req.result;
});
},
readWriteTxn: function(storeName) {
return this._database.transaction([storeName], "readwrite");
},
readTxn: function(storeName) {
return this._database.transaction([storeName], "readonly");
},
_createStores: function(db) {
db.createObjectStore("foos", {keyPath: "id"});
}
};
async function main() {
const storage = new Storage("idb-continue-on-constrainterror");
await storage.open();
const txn1 = storage.readWriteTxn("foos");
const store = txn1.objectStore("foos");
await reqAsPromise(store.clear());
console.log("first foo read back", await reqAsPromise(store.get(5)));
await reqAsPromise(store.add({id: 5, name: "Mr Foo"}));
try {
await reqAsPromise(store.add({id: 5, name: "bar"}));
} catch (err) {
console.log("we did get an error", err.name);
err.preventAbort();
}
await txnAsPromise(txn1);
const txn2 = storage.readTxn("foos");
const store2 = txn2.objectStore("foos");
console.log("got name from second txn", await reqAsPromise(store2.get(5)));
}
main().catch(err => console.error(err));
</script>
</body>
</html>

18
scripts/.eslintrc.js Normal file
View file

@ -0,0 +1,18 @@
module.exports = {
"env": {
"node": true,
"es6": true
},
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": 2020,
"sourceType": "module"
},
"rules": {
"no-console": "off",
"no-empty": "off",
"no-prototype-builtins": "off",
"no-unused-vars": "warn"
},
};

View file

@ -0,0 +1,51 @@
const fs = require('fs/promises');
const path = require('path');
module.exports = function injectWebManifest(manifestFile) {
let root;
let base;
let manifestHref;
return {
name: "hydrogen:injectWebManifest",
apply: "build",
configResolved: config => {
root = config.root;
base = config.base;
},
transformIndexHtml: {
transform(html) {
return [{
tag: "link",
attrs: {rel: "manifest", href: manifestHref},
injectTo: "head"
}];
},
},
generateBundle: async function() {
const absoluteManifestFile = path.resolve(root, manifestFile);
const manifestDir = path.dirname(absoluteManifestFile);
const json = await fs.readFile(absoluteManifestFile, {encoding: "utf8"});
const manifest = JSON.parse(json);
for (const icon of manifest.icons) {
const iconFileName = path.resolve(manifestDir, icon.src);
const imgData = await fs.readFile(iconFileName);
const ref = this.emitFile({
type: "asset",
name: path.basename(iconFileName),
source: imgData
});
// we take the basename as getFileName gives the filename
// relative to the output dir, but the manifest is an asset
// just like they icon, so we assume they end up in the same dir
icon.src = path.basename(this.getFileName(ref));
}
const outputName = path.basename(absoluteManifestFile);
const manifestRef = this.emitFile({
type: "asset",
name: outputName,
source: JSON.stringify(manifest)
});
manifestHref = base + this.getFileName(manifestRef);
}
};
}

View file

@ -0,0 +1,376 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
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.
*/
const path = require('path').posix;
const {optimize} = require('svgo');
async function readCSSSource(location) {
const fs = require("fs").promises;
const resolvedLocation = path.resolve(__dirname, "../../", `${location}/theme.css`);
const data = await fs.readFile(resolvedLocation);
return data;
}
function getRootSectionWithVariables(variables) {
return `:root{\n${Object.entries(variables).reduce((acc, [key, value]) => acc + `--${key}: ${value};\n`, "")} }\n\n`;
}
function appendVariablesToCSS(variables, cssSource) {
return cssSource + getRootSectionWithVariables(variables);
}
function addThemesToConfig(bundle, manifestLocations, defaultThemes) {
for (const [fileName, info] of Object.entries(bundle)) {
if (fileName === "config.json") {
const source = new TextDecoder().decode(info.source);
const config = JSON.parse(source);
config["themeManifests"] = manifestLocations;
config["defaultTheme"] = defaultThemes;
info.source = new TextEncoder().encode(JSON.stringify(config, undefined, 2));
}
}
}
/**
* Returns an object where keys are the svg file names and the values
* are the svg code (optimized)
* @param {*} icons Object where keys are css variable names and values are locations of the svg
* @param {*} manifestLocation Location of manifest used for resolving path
*/
async function generateIconSourceMap(icons, manifestLocation) {
const sources = {};
const fileNames = [];
const promises = [];
const fs = require("fs").promises;
for (const icon of Object.values(icons)) {
const [location] = icon.split("?");
// resolve location against manifestLocation
const resolvedLocation = path.resolve(manifestLocation, location);
const iconData = fs.readFile(resolvedLocation);
promises.push(iconData);
const fileName = path.basename(resolvedLocation);
fileNames.push(fileName);
}
const results = await Promise.all(promises);
for (let i = 0; i < results.length; ++i) {
const svgString = results[i].toString();
const result = optimize(svgString, {
plugins: [
{
name: "preset-default",
params: {
overrides: { convertColors: false, },
},
},
],
});
const optimizedSvgString = result.data;
sources[fileNames[i]] = optimizedSvgString;
}
return sources;
}
/**
* Returns a mapping from location (of manifest file) to an array containing all the chunks (of css files) generated from that location.
* To understand what chunk means in this context, see https://rollupjs.org/guide/en/#generatebundle.
* @param {*} bundle Mapping from fileName to AssetInfo | ChunkInfo
*/
function getMappingFromLocationToChunkArray(bundle) {
const chunkMap = new Map();
for (const [fileName, info] of Object.entries(bundle)) {
if (!fileName.endsWith(".css") || info.type === "asset" || info.facadeModuleId?.includes("type=runtime")) {
continue;
}
const location = info.facadeModuleId?.match(/(.+)\/.+\.css/)?.[1];
if (!location) {
throw new Error("Cannot find location of css chunk!");
}
const array = chunkMap.get(location);
if (!array) {
chunkMap.set(location, [info]);
}
else {
array.push(info);
}
}
return chunkMap;
}
/**
* Returns a mapping from unhashed file name (of css files) to AssetInfo.
* To understand what AssetInfo means in this context, see https://rollupjs.org/guide/en/#generatebundle.
* @param {*} bundle Mapping from fileName to AssetInfo | ChunkInfo
*/
function getMappingFromFileNameToAssetInfo(bundle) {
const assetMap = new Map();
for (const [fileName, info] of Object.entries(bundle)) {
if (!fileName.endsWith(".css")) {
continue;
}
if (info.type === "asset") {
/**
* So this is the css assetInfo that contains the asset hashed file name.
* We'll store it in a separate map indexed via fileName (unhashed) to avoid
* searching through the bundle array later.
*/
assetMap.set(info.name, info);
}
}
return assetMap;
}
/**
* Returns a mapping from location (of manifest file) to ChunkInfo of the runtime css asset
* To understand what ChunkInfo means in this context, see https://rollupjs.org/guide/en/#generatebundle.
* @param {*} bundle Mapping from fileName to AssetInfo | ChunkInfo
*/
function getMappingFromLocationToRuntimeChunk(bundle) {
let runtimeThemeChunkMap = new Map();
for (const [fileName, info] of Object.entries(bundle)) {
if (!fileName.endsWith(".css") || info.type === "asset") {
continue;
}
const location = info.facadeModuleId?.match(/(.+)\/.+\.css/)?.[1];
if (!location) {
throw new Error("Cannot find location of css chunk!");
}
if (info.facadeModuleId?.includes("type=runtime")) {
/**
* We have a separate field in manifest.source just for the runtime theme,
* so store this separately.
*/
runtimeThemeChunkMap.set(location, info);
}
}
return runtimeThemeChunkMap;
}
module.exports = function buildThemes(options) {
let manifest, variants, defaultDark, defaultLight, defaultThemes = {};
let isDevelopment = false;
const virtualModuleId = '@theme/'
const resolvedVirtualModuleId = '\0' + virtualModuleId;
const themeToManifestLocation = new Map();
return {
name: "build-themes",
enforce: "pre",
configResolved(config) {
if (config.command === "serve") {
isDevelopment = true;
}
},
async buildStart() {
const { themeConfig } = options;
for (const location of themeConfig.themes) {
manifest = require(`${location}/manifest.json`);
const themeCollectionId = manifest.id;
themeToManifestLocation.set(themeCollectionId, location);
variants = manifest.values.variants;
for (const [variant, details] of Object.entries(variants)) {
const fileName = `theme-${themeCollectionId}-${variant}.css`;
if (themeCollectionId === themeConfig.default && details.default) {
// This is the default theme, stash the file name for later
if (details.dark) {
defaultDark = fileName;
defaultThemes["dark"] = `${themeCollectionId}-${variant}`;
}
else {
defaultLight = fileName;
defaultThemes["light"] = `${themeCollectionId}-${variant}`;
}
}
// emit the css as built theme bundle
if (!isDevelopment) {
this.emitFile({ type: "chunk", id: `${location}/theme.css?variant=${variant}${details.dark ? "&dark=true" : ""}`, fileName, });
}
}
// emit the css as runtime theme bundle
if (!isDevelopment) {
this.emitFile({ type: "chunk", id: `${location}/theme.css?type=runtime`, fileName: `theme-${themeCollectionId}-runtime.css`, });
}
}
},
resolveId(id) {
if (id.startsWith(virtualModuleId)) {
return '\0' + id;
}
},
async load(id) {
if (isDevelopment) {
/**
* To load the theme during dev, we need to take a different approach because emitFile is not supported in dev.
* We solve this by resolving virtual file "@theme/name/variant" into the necessary css import.
* This virtual file import is removed when hydrogen is built (see transform hook).
*/
if (id.startsWith(resolvedVirtualModuleId)) {
let [theme, variant, file] = id.substr(resolvedVirtualModuleId.length).split("/");
if (theme === "default") {
theme = options.themeConfig.default;
}
const location = themeToManifestLocation.get(theme);
const manifest = require(`${location}/manifest.json`);
const variants = manifest.values.variants;
if (!variant || variant === "default") {
// choose the first default variant for now
// this will need to support light/dark variants as well
variant = Object.keys(variants).find(variantName => variants[variantName].default);
}
if (!file) {
file = "index.js";
}
switch (file) {
case "index.js": {
const isDark = variants[variant].dark;
return `import "${path.resolve(`${location}/theme.css`)}${isDark? "?dark=true": ""}";` +
`import "@theme/${theme}/${variant}/variables.css"`;
}
case "variables.css": {
const variables = variants[variant].variables;
const css = getRootSectionWithVariables(variables);
return css;
}
}
}
}
else {
const result = id.match(/(.+)\/theme.css\?variant=([^&]+)/);
if (result) {
const [, location, variant] = result;
const cssSource = await readCSSSource(location);
const config = variants[variant];
return appendVariablesToCSS(config.variables, cssSource);
}
return null;
}
},
transform(code, id) {
if (isDevelopment) {
return;
}
/**
* Removes develop-only script tag; this cannot be done in transformIndexHtml hook because
* by the time that hook runs, the import is added to the bundled js file which would
* result in a runtime error.
*/
const devScriptTag =
/<script type="module"> import "@theme\/.+"; <\/script>/;
if (id.endsWith("index.html")) {
const htmlWithoutDevScript = code.replace(devScriptTag, "");
return htmlWithoutDevScript;
}
},
transformIndexHtml(_, ctx) {
if (isDevelopment) {
// Don't add default stylesheets to index.html on dev
return;
}
let darkThemeLocation, lightThemeLocation;
for (const [, bundle] of Object.entries(ctx.bundle)) {
if (bundle.name === defaultDark) {
darkThemeLocation = bundle.fileName;
}
if (bundle.name === defaultLight) {
lightThemeLocation = bundle.fileName;
}
}
return [
{
tag: "link",
attrs: {
rel: "stylesheet",
type: "text/css",
media: "(prefers-color-scheme: dark)",
href: `./${darkThemeLocation}`,
class: "theme",
}
},
{
tag: "link",
attrs: {
rel: "stylesheet",
type: "text/css",
media: "(prefers-color-scheme: light)",
href: `./${lightThemeLocation}`,
class: "theme",
}
},
];
},
async generateBundle(_, bundle) {
const assetMap = getMappingFromFileNameToAssetInfo(bundle);
const chunkMap = getMappingFromLocationToChunkArray(bundle);
const runtimeThemeChunkMap = getMappingFromLocationToRuntimeChunk(bundle);
const manifestLocations = [];
// Location of the directory containing manifest relative to the root of the build output
const manifestLocation = "assets";
for (const [location, chunkArray] of chunkMap) {
const manifest = require(`${location}/manifest.json`);
const compiledVariables = options.compiledVariables.get(location);
const derivedVariables = compiledVariables["derived-variables"];
const icon = compiledVariables["icon"];
const builtAssets = {};
let themeKey;
for (const chunk of chunkArray) {
const [, name, variant] = chunk.fileName.match(/theme-(.+)-(.+)\.css/);
themeKey = name;
const locationRelativeToBuildRoot = assetMap.get(chunk.fileName).fileName;
const locationRelativeToManifest = path.relative(manifestLocation, locationRelativeToBuildRoot);
builtAssets[`${name}-${variant}`] = locationRelativeToManifest;
}
// Emit the base svg icons as asset
const nameToAssetHashedLocation = [];
const nameToSource = await generateIconSourceMap(icon, location);
for (const [name, source] of Object.entries(nameToSource)) {
const ref = this.emitFile({ type: "asset", name, source });
const assetHashedName = this.getFileName(ref);
nameToAssetHashedLocation[name] = assetHashedName;
}
// Update icon section in output manifest with paths to the icon in build output
for (const [variable, location] of Object.entries(icon)) {
const [locationWithoutQueryParameters, queryParameters] = location.split("?");
const name = path.basename(locationWithoutQueryParameters);
const locationRelativeToBuildRoot = nameToAssetHashedLocation[name];
const locationRelativeToManifest = path.relative(manifestLocation, locationRelativeToBuildRoot);
icon[variable] = `${locationRelativeToManifest}?${queryParameters}`;
}
const runtimeThemeChunk = runtimeThemeChunkMap.get(location);
const runtimeAssetLocation = path.relative(manifestLocation, assetMap.get(runtimeThemeChunk.fileName).fileName);
manifest.source = {
"built-assets": builtAssets,
"runtime-asset": runtimeAssetLocation,
"derived-variables": derivedVariables,
"icon": icon,
};
const name = `theme-${themeKey}.json`;
manifestLocations.push(`${manifestLocation}/${name}`);
this.emitFile({
type: "asset",
name,
source: JSON.stringify(manifest),
});
}
addThemesToConfig(bundle, manifestLocations, defaultThemes);
},
}
}

View file

@ -0,0 +1,157 @@
const fs = require('fs/promises');
const path = require('path');
const xxhash = require('xxhashjs');
function contentHash(str) {
var hasher = new xxhash.h32(0);
hasher.update(str);
return hasher.digest();
}
function injectServiceWorker(swFile, findUnhashedFileNamesFromBundle, placeholdersPerChunk) {
const swName = path.basename(swFile);
let root;
let version;
let logger;
return {
name: "hydrogen:injectServiceWorker",
apply: "build",
enforce: "post",
buildStart() {
this.emitFile({
type: "chunk",
fileName: swName,
id: swFile,
});
},
configResolved: config => {
root = config.root;
version = JSON.parse(config.define.DEFINE_VERSION); // unquote
logger = config.logger;
},
generateBundle: async function(options, bundle) {
const otherUnhashedFiles = findUnhashedFileNamesFromBundle(bundle);
const unhashedFilenames = [swName].concat(otherUnhashedFiles);
const unhashedFileContentMap = unhashedFilenames.reduce((map, fileName) => {
const chunkOrAsset = bundle[fileName];
if (!chunkOrAsset) {
throw new Error("could not get content for uncached asset or chunk " + fileName);
}
map[fileName] = chunkOrAsset.source || chunkOrAsset.code;
return map;
}, {});
const assets = Object.values(bundle);
const hashedFileNames = assets.map(o => o.fileName).filter(fileName => !unhashedFileContentMap[fileName]);
const globalHash = getBuildHash(hashedFileNames, unhashedFileContentMap);
const placeholderValues = {
DEFINE_GLOBAL_HASH: `"${globalHash}"`,
...getCacheFileNamePlaceholderValues(swName, unhashedFilenames, assets, placeholdersPerChunk)
};
replacePlaceholdersInChunks(assets, placeholdersPerChunk, placeholderValues);
logger.info(`\nBuilt ${version} (${globalHash})`);
}
};
}
function getBuildHash(hashedFileNames, unhashedFileContentMap) {
const unhashedHashes = Object.entries(unhashedFileContentMap).map(([fileName, content]) => {
return `${fileName}-${contentHash(Buffer.from(content))}`;
});
const globalHashAssets = hashedFileNames.concat(unhashedHashes);
globalHashAssets.sort();
return contentHash(globalHashAssets.join(",")).toString();
}
const NON_PRECACHED_JS = [
"hydrogen-legacy",
"olm_legacy.js",
// most environments don't need the worker
"main.js"
];
function isPreCached(asset) {
const {name, fileName} = asset;
return name.endsWith(".svg") ||
name.endsWith(".png") ||
name.endsWith(".css") ||
name.endsWith(".wasm") ||
name.endsWith(".html") ||
// the index and vendor chunks don't have an extension in `name`, so check extension on `fileName`
fileName.endsWith(".js") && !NON_PRECACHED_JS.includes(path.basename(name));
}
function getCacheFileNamePlaceholderValues(swName, unhashedFilenames, assets) {
const unhashedPreCachedAssets = [];
const hashedPreCachedAssets = [];
const hashedCachedOnRequestAssets = [];
for (const asset of assets) {
const {name, fileName} = asset;
// the service worker should not be cached at all,
// it's how updates happen
if (fileName === swName) {
continue;
} else if (unhashedFilenames.includes(fileName)) {
unhashedPreCachedAssets.push(fileName);
} else if (isPreCached(asset)) {
hashedPreCachedAssets.push(fileName);
} else {
hashedCachedOnRequestAssets.push(fileName);
}
}
return {
DEFINE_UNHASHED_PRECACHED_ASSETS: JSON.stringify(unhashedPreCachedAssets),
DEFINE_HASHED_PRECACHED_ASSETS: JSON.stringify(hashedPreCachedAssets),
DEFINE_HASHED_CACHED_ON_REQUEST_ASSETS: JSON.stringify(hashedCachedOnRequestAssets)
}
}
function replacePlaceholdersInChunks(assets, placeholdersPerChunk, placeholderValues) {
for (const [name, placeholderMap] of Object.entries(placeholdersPerChunk)) {
const chunk = assets.find(a => a.type === "chunk" && a.name === name);
if (!chunk) {
throw new Error(`could not find chunk ${name} to replace placeholders`);
}
for (const [placeholderName, placeholderLiteral] of Object.entries(placeholderMap)) {
const replacedValue = placeholderValues[placeholderName];
const oldCode = chunk.code;
chunk.code = chunk.code.replaceAll(placeholderLiteral, replacedValue);
if (chunk.code === oldCode) {
throw new Error(`Could not replace ${placeholderName} in ${name}, looking for literal ${placeholderLiteral}:\n${chunk.code}`);
}
}
}
}
/** creates a value to be include in the `define` build settings,
* but can be replace at the end of the build in certain chunks.
* We need this for injecting the global build hash and the final
* filenames in the service worker and index chunk.
* These values are only known in the generateBundle step, so we
* replace them by unique strings wrapped in a prompt call so no
* transformation will touch them (minifying, ...) and we can do a
* string replacement still at the end of the build. */
function definePlaceholderValue(mode, name, devValue) {
if (mode === "production") {
// note that `prompt(...)` will never be in the final output, it's replaced by the final value
// once we know at the end of the build what it is and just used as a temporary value during the build
// as something that will not be transformed.
// I first considered Symbol but it's not inconceivable that babel would transform this.
return `prompt(${JSON.stringify(name)})`;
} else {
return JSON.stringify(devValue);
}
}
function createPlaceholderValues(mode) {
return {
DEFINE_GLOBAL_HASH: definePlaceholderValue(mode, "DEFINE_GLOBAL_HASH", null),
DEFINE_UNHASHED_PRECACHED_ASSETS: definePlaceholderValue(mode, "UNHASHED_PRECACHED_ASSETS", []),
DEFINE_HASHED_PRECACHED_ASSETS: definePlaceholderValue(mode, "HASHED_PRECACHED_ASSETS", []),
DEFINE_HASHED_CACHED_ON_REQUEST_ASSETS: definePlaceholderValue(mode, "HASHED_CACHED_ON_REQUEST_ASSETS", []),
};
}
module.exports = {injectServiceWorker, createPlaceholderValues};

View file

@ -1,578 +0,0 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
Copyright 2020 The Matrix.org Foundation C.I.C.
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.
*/
import {build as snowpackBuild, loadConfiguration} from "snowpack"
import cheerio from "cheerio";
import fsRoot from "fs";
const fs = fsRoot.promises;
import path from "path";
import xxhash from 'xxhashjs';
import { rollup } from 'rollup';
import postcss from "postcss";
import postcssImport from "postcss-import";
import { fileURLToPath } from 'url';
import { dirname } from 'path';
import commander from "commander";
// needed for legacy bundle
import babel from '@rollup/plugin-babel';
// needed to find the polyfill modules in the main-legacy.js bundle
import { nodeResolve } from '@rollup/plugin-node-resolve';
// needed because some of the polyfills are written as commonjs modules
import commonjs from '@rollup/plugin-commonjs';
// multi-entry plugin so we can add polyfill file to main
import multi from '@rollup/plugin-multi-entry';
import removeJsComments from 'rollup-plugin-cleanup';
// replace urls of asset names with content hashed version
import postcssUrl from "postcss-url";
import cssvariables from "postcss-css-variables";
import autoprefixer from "autoprefixer";
import flexbugsFixes from "postcss-flexbugs-fixes";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const projectDir = path.join(__dirname, "../");
const snowpackOutPath = path.join(projectDir, "snowpack-build-output");
const cssSrcDir = path.join(projectDir, "src/platform/web/ui/css/");
const snowpackConfig = await loadConfiguration({buildOptions: {out: snowpackOutPath}}, "snowpack.config.js");
const snowpackOutDir = snowpackConfig.buildOptions.out.substring(projectDir.length);
const srcDir = path.join(projectDir, `${snowpackOutDir}/src/`);
const isPathInSrcDir = path => path.startsWith(srcDir);
const parameters = new commander.Command();
parameters
.option("--modern-only", "don't make a legacy build")
.option("--override-imports <json file>", "pass in a file to override import paths, see doc/SKINNING.md")
.option("--override-css <main css file>", "pass in an alternative main css file")
parameters.parse(process.argv);
/**
* We use Snowpack to handle the translation of TypeScript
* into JavaScript. We thus can't bundle files straight from
* the src directory, since some of them are TypeScript, and since
* they may import Node modules. We thus bundle files after they
* have been processed by Snowpack. This function returns paths
* to the files that have already been pre-processed in this manner.
*/
function srcPath(src) {
return path.join(snowpackOutDir, 'src', src);
}
async function build({modernOnly, overrideImports, overrideCss}) {
await snowpackBuild({config: snowpackConfig});
// get version number
const version = JSON.parse(await fs.readFile(path.join(projectDir, "package.json"), "utf8")).version;
let importOverridesMap;
if (overrideImports) {
importOverridesMap = await readImportOverrides(overrideImports);
}
const devHtml = await fs.readFile(path.join(snowpackOutPath, "index.html"), "utf8");
const doc = cheerio.load(devHtml);
const themes = [];
findThemes(doc, themeName => {
themes.push(themeName);
});
// clear target dir
const targetDir = path.join(projectDir, "target/");
await removeDirIfExists(targetDir);
await createDirs(targetDir, themes);
const assets = new AssetMap(targetDir);
// copy olm assets
const olmAssets = await copyFolder(path.join(projectDir, "lib/olm/"), assets.directory);
assets.addSubMap(olmAssets);
await assets.write(`hydrogen.js`, await buildJs(srcPath("main.js"), [srcPath("platform/web/Platform.js")], importOverridesMap));
if (!modernOnly) {
await assets.write(`hydrogen-legacy.js`, await buildJsLegacy(srcPath("main.js"), [
srcPath('platform/web/legacy-polyfill.js'),
srcPath('platform/web/LegacyPlatform.js')
], importOverridesMap));
await assets.write(`worker.js`, await buildJsLegacy(srcPath("platform/web/worker/main.js"), [srcPath('platform/web/worker/polyfill.js')]));
}
// copy over non-theme assets
const baseConfig = JSON.parse(await fs.readFile(path.join(projectDir, "assets/config.json"), {encoding: "utf8"}));
const downloadSandbox = "download-sandbox.html";
let downloadSandboxHtml = await fs.readFile(path.join(projectDir, `assets/${downloadSandbox}`));
await assets.write(downloadSandbox, downloadSandboxHtml);
// creates the directories where the theme css bundles are placed in,
// and writes to assets, so the build bundles can translate them, so do it first
await copyThemeAssets(themes, assets);
await buildCssBundles(buildCssLegacy, themes, assets, overrideCss);
await buildManifest(assets);
// all assets have been added, create a hash from all assets name to cache unhashed files like index.html
assets.addToHashForAll("index.html", devHtml);
let swSource = await fs.readFile(path.join(snowpackOutPath, "sw.js"), "utf8");
assets.addToHashForAll("sw.js", swSource);
const globalHash = assets.hashForAll();
await buildServiceWorker(swSource, version, globalHash, assets);
await buildHtml(doc, version, baseConfig, globalHash, modernOnly, assets);
await removeDirIfExists(snowpackOutPath);
console.log(`built hydrogen ${version} (${globalHash}) successfully with ${assets.size} files`);
}
async function findThemes(doc, callback) {
doc("link[rel~=stylesheet][title]").each((i, el) => {
const theme = doc(el);
const href = theme.attr("href");
const themesPrefix = "/themes/";
const prefixIdx = href.indexOf(themesPrefix);
if (prefixIdx !== -1) {
const themeNameStart = prefixIdx + themesPrefix.length;
const themeNameEnd = href.indexOf("/", themeNameStart);
const themeName = href.substr(themeNameStart, themeNameEnd - themeNameStart);
callback(themeName, theme);
}
});
}
async function createDirs(targetDir, themes) {
await fs.mkdir(targetDir);
const themeDir = path.join(targetDir, "themes");
await fs.mkdir(themeDir);
for (const theme of themes) {
await fs.mkdir(path.join(themeDir, theme));
}
}
async function copyThemeAssets(themes, assets) {
for (const theme of themes) {
const themeDstFolder = path.join(assets.directory, `themes/${theme}`);
const themeSrcFolder = path.join(cssSrcDir, `themes/${theme}`);
const themeAssets = await copyFolder(themeSrcFolder, themeDstFolder, file => {
return !file.endsWith(".css");
});
assets.addSubMap(themeAssets);
}
return assets;
}
async function buildHtml(doc, version, baseConfig, globalHash, modernOnly, assets) {
// transform html file
// change path to main.css to css bundle
doc("link[rel=stylesheet]:not([title])").attr("href", assets.resolve(`hydrogen.css`));
// adjust file name of icon on iOS
doc("link[rel=apple-touch-icon]").attr("href", assets.resolve(`icon-maskable.png`));
// change paths to all theme stylesheets
findThemes(doc, (themeName, theme) => {
theme.attr("href", assets.resolve(`themes/${themeName}/bundle.css`));
});
const configJSON = JSON.stringify(Object.assign({}, baseConfig, {
worker: assets.has("worker.js") ? assets.resolve(`worker.js`) : null,
downloadSandbox: assets.resolve("download-sandbox.html"),
serviceWorker: "sw.js",
olm: {
wasm: assets.resolve("olm.wasm"),
legacyBundle: assets.resolve("olm_legacy.js"),
wasmBundle: assets.resolve("olm.js"),
}
}));
const modernScript = `import {main, Platform} from "./${assets.resolve(`hydrogen.js`)}"; main(new Platform(document.body, ${configJSON}));`;
const mainScripts = [
`<script type="module">${wrapWithLicenseComments(modernScript)}</script>`
];
if (!modernOnly) {
const legacyScript = `hydrogen.main(new hydrogen.Platform(document.body, ${configJSON}));`;
mainScripts.push(
`<script type="text/javascript" nomodule src="${assets.resolve(`hydrogen-legacy.js`)}"></script>`,
`<script type="text/javascript" nomodule>${wrapWithLicenseComments(legacyScript)}</script>`
);
}
doc("script#main").replaceWith(mainScripts.join(""));
const versionScript = doc("script#version");
versionScript.attr("type", "text/javascript");
let vSource = versionScript.contents().text();
vSource = vSource.replace(`"%%VERSION%%"`, `"${version}"`);
vSource = vSource.replace(`"%%GLOBAL_HASH%%"`, `"${globalHash}"`);
versionScript.text(wrapWithLicenseComments(vSource));
doc("head").append(`<link rel="manifest" href="${assets.resolve("manifest.json")}">`);
await assets.writeUnhashed("index.html", doc.html());
}
async function buildJs(mainFile, extraFiles, importOverrides) {
// create js bundle
const plugins = [multi(), removeJsComments({comments: "none"})];
if (importOverrides) {
plugins.push(overridesAsRollupPlugin(importOverrides));
}
const bundle = await rollup({
// for fake-indexeddb, so usage for tests only doesn't put it in bundle
treeshake: {moduleSideEffects: isPathInSrcDir},
input: extraFiles.concat(mainFile),
plugins
});
const {output} = await bundle.generate({
format: 'es',
// TODO: can remove this?
name: `hydrogen`
});
const code = output[0].code;
return wrapWithLicenseComments(code);
}
async function buildJsLegacy(mainFile, extraFiles, importOverrides) {
// compile down to whatever IE 11 needs
const babelPlugin = babel.babel({
babelHelpers: 'bundled',
exclude: 'node_modules/**',
presets: [
[
"@babel/preset-env",
{
useBuiltIns: "entry",
corejs: "3.4",
targets: "IE 11",
// we provide our own promise polyfill (es6-promise)
// with support for synchronous flushing of
// the queue for idb where needed
exclude: ["es.promise", "es.promise.all-settled", "es.promise.finally"]
}
]
]
});
const plugins = [multi(), commonjs()];
if (importOverrides) {
plugins.push(overridesAsRollupPlugin(importOverrides));
}
plugins.push(nodeResolve(), babelPlugin);
// create js bundle
const rollupConfig = {
// for fake-indexeddb, so usage for tests only doesn't put it in bundle
treeshake: {moduleSideEffects: isPathInSrcDir},
// important the extraFiles come first,
// so polyfills are available in the global scope
// if needed for the mainfile
input: extraFiles.concat(mainFile),
plugins
};
const bundle = await rollup(rollupConfig);
const {output} = await bundle.generate({
format: 'iife',
name: `hydrogen`
});
const code = output[0].code;
return wrapWithLicenseComments(code);
}
function wrapWithLicenseComments(code) {
// Add proper license comments to make GNU LibreJS accept the file
const start = '// @license magnet:?xt=urn:btih:8e4f440f4c65981c5bf93c76d35135ba5064d8b7&dn=apache-2.0.txt Apache-2.0';
const end = '// @license-end';
return `${start}\n${code}\n${end}`;
}
const NON_PRECACHED_JS = [
"hydrogen-legacy.js",
"olm_legacy.js",
"worker.js"
];
function isPreCached(asset) {
return asset.endsWith(".svg") ||
asset.endsWith(".png") ||
asset.endsWith(".css") ||
asset.endsWith(".wasm") ||
asset.endsWith(".html") ||
// most environments don't need the worker
asset.endsWith(".js") && !NON_PRECACHED_JS.includes(asset);
}
async function buildManifest(assets) {
const webManifest = JSON.parse(await fs.readFile(path.join(projectDir, "assets/manifest.json"), "utf8"));
// copy manifest icons
for (const icon of webManifest.icons) {
let iconData = await fs.readFile(path.join(projectDir, icon.src));
const iconTargetPath = path.basename(icon.src);
icon.src = await assets.write(iconTargetPath, iconData);
}
await assets.write("manifest.json", JSON.stringify(webManifest));
}
async function buildServiceWorker(swSource, version, globalHash, assets) {
const unhashedPreCachedAssets = ["index.html"];
const hashedPreCachedAssets = [];
const hashedCachedOnRequestAssets = [];
for (const [unresolved, resolved] of assets) {
if (unresolved === resolved) {
unhashedPreCachedAssets.push(resolved);
} else if (isPreCached(unresolved)) {
hashedPreCachedAssets.push(resolved);
} else {
hashedCachedOnRequestAssets.push(resolved);
}
}
const replaceArrayInSource = (name, value) => {
const newSource = swSource.replace(`${name} = []`, `${name} = ${JSON.stringify(value)}`);
if (newSource === swSource) {
throw new Error(`${name} was not found in the service worker source`);
}
return newSource;
};
const replaceStringInSource = (name, value) => {
const newSource = swSource.replace(new RegExp(`${name}\\s=\\s"[^"]*"`), `${name} = ${JSON.stringify(value)}`);
if (newSource === swSource) {
throw new Error(`${name} was not found in the service worker source`);
}
return newSource;
};
// write service worker
swSource = swSource.replace(`"%%VERSION%%"`, `"${version}"`);
swSource = swSource.replace(`"%%GLOBAL_HASH%%"`, `"${globalHash}"`);
swSource = replaceArrayInSource("UNHASHED_PRECACHED_ASSETS", unhashedPreCachedAssets);
swSource = replaceArrayInSource("HASHED_PRECACHED_ASSETS", hashedPreCachedAssets);
swSource = replaceArrayInSource("HASHED_CACHED_ON_REQUEST_ASSETS", hashedCachedOnRequestAssets);
swSource = replaceStringInSource("NOTIFICATION_BADGE_ICON", assets.resolve("icon.png"));
// service worker should not have a hashed name as it is polled by the browser for updates
await assets.writeUnhashed("sw.js", swSource);
}
async function buildCssBundles(buildFn, themes, assets, mainCssFile = null) {
if (!mainCssFile) {
mainCssFile = path.join(cssSrcDir, "main.css");
}
const bundleCss = await buildFn(mainCssFile);
await assets.write(`hydrogen.css`, bundleCss);
for (const theme of themes) {
const themeRelPath = `themes/${theme}/`;
const themeRoot = path.join(cssSrcDir, themeRelPath);
const assetUrlMapper = ({absolutePath}) => {
if (!absolutePath.startsWith(themeRoot)) {
throw new Error("resource is out of theme directory: " + absolutePath);
}
const relPath = absolutePath.substr(themeRoot.length);
const hashedDstPath = assets.resolve(path.join(themeRelPath, relPath));
if (hashedDstPath) {
return hashedDstPath.substr(themeRelPath.length);
}
};
const themeCss = await buildFn(path.join(themeRoot, `theme.css`), assetUrlMapper);
await assets.write(path.join(themeRelPath, `bundle.css`), themeCss);
}
}
// async function buildCss(entryPath, urlMapper = null) {
// const preCss = await fs.readFile(entryPath, "utf8");
// const options = [postcssImport];
// if (urlMapper) {
// options.push(postcssUrl({url: urlMapper}));
// }
// const cssBundler = postcss(options);
// const result = await cssBundler.process(preCss, {from: entryPath});
// return result.css;
// }
async function buildCssLegacy(entryPath, urlMapper = null) {
const preCss = await fs.readFile(entryPath, "utf8");
const options = [
postcssImport,
cssvariables({
preserve: (declaration) => {
return declaration.value.indexOf("var(--ios-") == 0;
}
}),
autoprefixer({overrideBrowserslist: ["IE 11"], grid: "no-autoplace"}),
flexbugsFixes()
];
if (urlMapper) {
options.push(postcssUrl({url: urlMapper}));
}
const cssBundler = postcss(options);
const result = await cssBundler.process(preCss, {from: entryPath});
return result.css;
}
async function removeDirIfExists(targetDir) {
try {
await fs.rmdir(targetDir, {recursive: true});
} catch (err) {
if (err.code !== "ENOENT") {
throw err;
}
}
}
async function copyFolder(srcRoot, dstRoot, filter, assets = null) {
assets = assets || new AssetMap(dstRoot);
const dirEnts = await fs.readdir(srcRoot, {withFileTypes: true});
for (const dirEnt of dirEnts) {
const dstPath = path.join(dstRoot, dirEnt.name);
const srcPath = path.join(srcRoot, dirEnt.name);
if (dirEnt.isDirectory()) {
await fs.mkdir(dstPath);
await copyFolder(srcPath, dstPath, filter, assets);
} else if ((dirEnt.isFile() || dirEnt.isSymbolicLink()) && (!filter || filter(srcPath))) {
const content = await fs.readFile(srcPath);
await assets.write(dstPath, content);
}
}
return assets;
}
function contentHash(str) {
var hasher = new xxhash.h32(0);
hasher.update(str);
return hasher.digest();
}
class AssetMap {
constructor(targetDir) {
// remove last / if any, so substr in create works well
this._targetDir = path.resolve(targetDir);
this._assets = new Map();
// hashes for unhashed resources so changes in these resources also contribute to the hashForAll
this._unhashedHashes = [];
}
_toRelPath(resourcePath) {
let relPath = resourcePath;
if (path.isAbsolute(resourcePath)) {
if (!resourcePath.startsWith(this._targetDir)) {
throw new Error(`absolute path ${resourcePath} that is not within target dir ${this._targetDir}`);
}
relPath = resourcePath.substr(this._targetDir.length + 1); // + 1 for the /
}
return relPath;
}
_create(resourcePath, content) {
const relPath = this._toRelPath(resourcePath);
const hash = contentHash(Buffer.from(content));
const dir = path.dirname(relPath);
const extname = path.extname(relPath);
const basename = path.basename(relPath, extname);
const dstRelPath = path.join(dir, `${basename}-${hash}${extname}`);
this._assets.set(relPath, dstRelPath);
return dstRelPath;
}
async write(resourcePath, content) {
const relPath = this._create(resourcePath, content);
const fullPath = path.join(this.directory, relPath);
if (typeof content === "string") {
await fs.writeFile(fullPath, content, "utf8");
} else {
await fs.writeFile(fullPath, content);
}
return relPath;
}
async writeUnhashed(resourcePath, content) {
const relPath = this._toRelPath(resourcePath);
this._assets.set(relPath, relPath);
const fullPath = path.join(this.directory, relPath);
if (typeof content === "string") {
await fs.writeFile(fullPath, content, "utf8");
} else {
await fs.writeFile(fullPath, content);
}
return relPath;
}
get directory() {
return this._targetDir;
}
resolve(resourcePath) {
const relPath = this._toRelPath(resourcePath);
const result = this._assets.get(relPath);
if (!result) {
throw new Error(`unknown path: ${relPath}, only know ${Array.from(this._assets.keys()).join(", ")}`);
}
return result;
}
addSubMap(assetMap) {
if (!assetMap.directory.startsWith(this.directory)) {
throw new Error(`map directory doesn't start with this directory: ${assetMap.directory} ${this.directory}`);
}
const relSubRoot = assetMap.directory.substr(this.directory.length + 1);
for (const [key, value] of assetMap._assets.entries()) {
this._assets.set(path.join(relSubRoot, key), path.join(relSubRoot, value));
}
}
[Symbol.iterator]() {
return this._assets.entries();
}
isUnhashed(relPath) {
const resolvedPath = this._assets.get(relPath);
if (!resolvedPath) {
throw new Error("Unknown asset: " + relPath);
}
return relPath === resolvedPath;
}
get size() {
return this._assets.size;
}
has(relPath) {
return this._assets.has(relPath);
}
hashForAll() {
const globalHashAssets = Array.from(this).map(([, resolved]) => resolved);
globalHashAssets.push(...this._unhashedHashes);
globalHashAssets.sort();
return contentHash(globalHashAssets.join(","));
}
addToHashForAll(resourcePath, content) {
this._unhashedHashes.push(`${resourcePath}-${contentHash(Buffer.from(content))}`);
}
}
async function readImportOverrides(filename) {
const json = await fs.readFile(filename, "utf8");
const mapping = new Map(Object.entries(JSON.parse(json)));
return {
basedir: path.dirname(path.resolve(filename))+path.sep,
mapping
};
}
function overridesAsRollupPlugin(importOverrides) {
const {mapping, basedir} = importOverrides;
return {
name: "rewrite-imports",
resolveId (source, importer) {
let file;
if (source.startsWith(path.sep)) {
file = source;
} else {
file = path.join(path.dirname(importer), source);
}
if (file.startsWith(basedir)) {
const searchPath = file.substr(basedir.length);
const replacingPath = mapping.get(searchPath);
if (replacingPath) {
console.info(`replacing ${searchPath} with ${replacingPath}`);
return path.join(basedir, replacingPath);
}
}
return null;
}
};
}
build(parameters).catch(err => console.error(err));

165
scripts/ci.sh Executable file
View file

@ -0,0 +1,165 @@
#!/bin/bash
# ci.sh: Helper script to automate deployment operations on CI/CD
# Copyright © 2022 Aravinth Manivannan <realaravinth@batsense.net>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
set -xEeuo pipefail
#source $(pwd)/scripts/lib.sh
readonly SSH_ID_FILE=/tmp/ci-ssh-id
readonly SSH_REMOTE_NAME=origin-ssh
readonly PROJECT_ROOT=$(pwd)
match_arg() {
if [ $1 == $2 ] || [ $1 == $3 ]
then
return 0
else
return 1
fi
}
help() {
cat << EOF
USAGE: ci.sh [SUBCOMMAND]
Helper script to automate deployment operations on CI/CD
Subcommands
-c --clean cleanup secrets, SSH key and other runtime data
-i --init <SSH_PRIVATE_KEY> initialize environment, write SSH private to file
-d --deploy <PAGES-SECRET> <TARGET BRANCH> push branch to Gitea and call Pages server
-h --help print this help menu
EOF
}
# $1: SSH private key
write_ssh(){
truncate --size 0 $SSH_ID_FILE
echo "$1" > $SSH_ID_FILE
chmod 600 $SSH_ID_FILE
}
set_ssh_remote() {
http_remote_url=$(git remote get-url origin)
remote_hostname=$(echo $http_remote_url | cut -d '/' -f 3)
repository_owner=$(echo $http_remote_url | cut -d '/' -f 4)
repository_name=$(echo $http_remote_url | cut -d '/' -f 5)
ssh_remote="git@$remote_hostname:$repository_owner/$repository_name"
ssh_remote="git@git.batsense.net:mystiq/hydrogen-web.git"
git remote add $SSH_REMOTE_NAME $ssh_remote
}
clean() {
if [ -f $SSH_ID_FILE ]
then
shred $SSH_ID_FILE
rm $SSH_ID_FILE
fi
}
# $1: branch name
# $2: directory containing build assets
# $3: Author in <author-name author@example.com> format
commit_files() {
cd $PROJECT_ROOT
original_branch=$(git branch --show-current)
tmp_dir=$(mktemp -d)
cp -r $2/* $tmp_dir
if [[ -z $(git ls-remote --heads origin ${1}) ]]
then
echo "[*] Creating deployment branch $1"
git checkout --orphan $1
else
echo "[*] Deployment branch $1 exists, pulling changes from remote"
git fetch origin $1
git switch $1
fi
git rm -rf .
/bin/rm -rf *
cp -r $tmp_dir/* .
git add --all
if [ $(git status --porcelain | xargs | sed '/^$/d' | wc -l) -gt 0 ];
then
echo "[*] Repository has changed, committing changes"
git commit \
--author="$3" \
--message="new deploy: $(date --iso-8601=seconds)"
fi
git checkout $original_branch
}
# $1: Pages API secret
# $2: Deployment target branch
deploy() {
if (( "$#" < 2 ))
then
help
else
git -c core.sshCommand="/usr/bin/ssh -oStrictHostKeyChecking=no -i $SSH_ID_FILE"\
push --force $SSH_REMOTE_NAME $2
curl -vv --location --request \
POST "https://deploy.batsense.net/api/v1/update"\
--header 'Content-Type: application/json' \
--data-raw "{ \"secret\": \"$1\", \"branch\": \"$2\" }"
fi
}
if (( "$#" < 1 ))
then
help
exit -1
fi
if match_arg $1 '-i' '--init'
then
if (( "$#" < 2 ))
then
help
exit -1
fi
set_ssh_remote
write_ssh "$2"
elif match_arg $1 '-c' '--clean'
then
clean
elif match_arg $1 '-cf' '--commit-files'
then
if (( "$#" < 4 ))
then
help
exit -1
fi
commit_files $2 $3 $4
elif match_arg $1 '-d' '--deploy'
then
if (( "$#" < 3 ))
then
help
exit -1
fi
deploy $2 $3
elif match_arg $1 '-h' '--help'
then
help
else
help
fi

3
scripts/cleanup.sh Executable file
View file

@ -0,0 +1,3 @@
#!/bin/sh
# Remove icons created in .tmp
rm -rf .tmp

View file

@ -1,12 +0,0 @@
import fsRoot from "fs";
const fs = fsRoot.promises;
export async function removeDirIfExists(targetDir) {
try {
await fs.rmdir(targetDir, {recursive: true});
} catch (err) {
if (err.code !== "ENOENT") {
throw err;
}
}
}

View file

@ -1,6 +0,0 @@
git checkout gh-pages
cp -R target/* .
git add $(find . -maxdepth 1 -type f)
git add themes
git commit -m "update hydrogen"
git checkout master

View file

@ -1,6 +1,7 @@
module.exports = class Buffer { var Buffer = {
static isBuffer(array) {return array instanceof Uint8Array;} isBuffer: function(array) {return array instanceof Uint8Array;},
static from(arrayBuffer) {return arrayBuffer;} from: function(arrayBuffer) {return arrayBuffer;},
static allocUnsafe(size) {return Buffer.alloc(size);} allocUnsafe: function(size) {return Buffer.alloc(size);},
static alloc(size) {return new Uint8Array(size);} alloc: function(size) {return new Uint8Array(size);}
}; };
export default Buffer;

View file

@ -1,4 +0,0 @@
// we have our own main file for this module as we need both these symbols to
// be exported, and we also don't want to auto behaviour that modifies global vars
exports.FDBFactory = require("fake-indexeddb/lib/FDBFactory.js");
exports.FDBKeyRange = require("fake-indexeddb/lib/FDBKeyRange.js");

View file

@ -1 +1,2 @@
module.exports.Buffer = require("buffer"); import Buffer from "buffer";
export {Buffer};

View file

@ -2,6 +2,9 @@ VERSION=$(jq -r ".version" package.json)
PACKAGE=hydrogen-web-$VERSION.tar.gz PACKAGE=hydrogen-web-$VERSION.tar.gz
yarn build yarn build
pushd target pushd target
# move config file so we don't override it
# when deploying a new version
mv config.json config.sample.json
tar -czvf ../$PACKAGE ./ tar -czvf ../$PACKAGE ./
popd popd
echo $PACKAGE echo $PACKAGE

View file

@ -1,132 +0,0 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
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.
*/
const fsRoot = require("fs");
const fs = fsRoot.promises;
const path = require("path");
const { rollup } = require('rollup');
const { fileURLToPath } = require('url');
const { dirname } = require('path');
// needed to translate commonjs modules to esm
const commonjs = require('@rollup/plugin-commonjs');
const json = require('@rollup/plugin-json');
const { nodeResolve } = require('@rollup/plugin-node-resolve');
const projectDir = path.join(__dirname, "../");
async function removeDirIfExists(targetDir) {
try {
await fs.rmdir(targetDir, {recursive: true});
} catch (err) {
if (err.code !== "ENOENT") {
throw err;
}
}
}
/** function used to resolve common-js require calls below. */
function packageIterator(request, start, defaultIterator) {
// this is just working for bs58, would need to tune it further for other dependencies
if (request === "safe-buffer") {
return [path.join(projectDir, "/scripts/package-overrides/safe-buffer")];
} else if (request === "buffer/") {
return [path.join(projectDir, "/scripts/package-overrides/buffer")];
} else {
return defaultIterator();
}
}
async function commonjsToESM(src, dst) {
// create js bundle
const bundle = await rollup({
treeshake: {moduleSideEffects: false},
input: src,
plugins: [commonjs(), json(), nodeResolve({
browser: true,
preferBuiltins: false,
customResolveOptions: {packageIterator}
})]
});
const {output} = await bundle.generate({
format: 'es'
});
const code = output[0].code;
await fs.writeFile(dst, code, "utf8");
}
async function populateLib() {
const libDir = path.join(projectDir, "lib/");
await removeDirIfExists(libDir);
await fs.mkdir(libDir);
const olmSrcDir = path.dirname(require.resolve("@matrix-org/olm"));
const olmDstDir = path.join(libDir, "olm/");
await fs.mkdir(olmDstDir);
for (const file of ["olm.js", "olm.wasm", "olm_legacy.js"]) {
await fs.copyFile(path.join(olmSrcDir, file), path.join(olmDstDir, file));
}
// transpile node-html-parser to esm
await fs.mkdir(path.join(libDir, "node-html-parser/"));
await commonjsToESM(
require.resolve('node-html-parser/dist/index.js'),
path.join(libDir, "node-html-parser/index.js")
);
// transpile another-json to esm
await fs.mkdir(path.join(libDir, "another-json/"));
await commonjsToESM(
require.resolve('another-json/another-json.js'),
path.join(libDir, "another-json/index.js")
);
// transpile bs58 to esm
await fs.mkdir(path.join(libDir, "bs58/"));
await commonjsToESM(
require.resolve('bs58/index.js'),
path.join(libDir, "bs58/index.js")
);
// transpile base64-arraybuffer to esm
await fs.mkdir(path.join(libDir, "base64-arraybuffer/"));
await commonjsToESM(
require.resolve('base64-arraybuffer/lib/base64-arraybuffer.js'),
path.join(libDir, "base64-arraybuffer/index.js")
);
// this probably should no go in here, we can just import "aes-js" from legacy-extras.js
// as that file is never loaded from a browser
// transpile aesjs to esm
await fs.mkdir(path.join(libDir, "aes-js/"));
await commonjsToESM(
require.resolve('aes-js/index.js'),
path.join(libDir, "aes-js/index.js")
);
// es6-promise is already written as an es module,
// but it does need to be babelified, and current we don't babelify
// anything in node_modules in the build script, so make a bundle that
// is conveniently not placed in node_modules rather than symlinking.
await fs.mkdir(path.join(libDir, "es6-promise/"));
await commonjsToESM(
require.resolve('es6-promise/lib/es6-promise/promise.js'),
path.join(libDir, "es6-promise/index.js")
);
// fake-indexeddb, used for tests (but unresolvable bare imports also makes the build complain)
// and might want to use it for in-memory storage too, although we probably do ts->es6 with esm
// directly rather than ts->es5->es6 as we do now. The bundle is 240K currently.
await fs.mkdir(path.join(libDir, "fake-indexeddb/"));
await commonjsToESM(
path.join(projectDir, "/scripts/package-overrides/fake-indexeddb.js"),
path.join(libDir, "fake-indexeddb/index.js")
);
}
populateLib();

View file

@ -0,0 +1,180 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
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.
*/
const valueParser = require("postcss-value-parser");
/**
* This plugin derives new css variables from a given set of base variables.
* A derived css variable has the form --base--operation-argument; meaning that the derived
* variable has a value that is generated from the base variable "base" by applying "operation"
* with given "argument".
*
* eg: given the base variable --foo-color: #40E0D0, --foo-color--darker-20 is a css variable
* derived from foo-color by making it 20% more darker.
*
* All derived variables are added to the :root section.
*
* The actual derivation is done outside the plugin in a callback.
*/
function getValueFromAlias(alias, {aliasMap, baseVariables, resolvedMap}) {
const derivedVariable = aliasMap.get(alias);
return baseVariables.get(derivedVariable) ?? resolvedMap.get(derivedVariable);
}
function parseDeclarationValue(value) {
const parsed = valueParser(value);
const variables = [];
parsed.walk(node => {
if (node.type !== "function") {
return;
}
switch (node.value) {
case "var": {
const variable = node.nodes[0];
variables.push(variable.value);
break;
}
case "url": {
const url = node.nodes[0].value;
// resolve url with some absolute url so that we get the query params without using regex
const params = new URL(url, "file://foo/bar/").searchParams;
const primary = params.get("primary");
const secondary = params.get("secondary");
if (primary) { variables.push(primary); }
if (secondary) { variables.push(secondary); }
break;
}
}
});
return variables;
}
function resolveDerivedVariable(decl, derive, maps, isDark) {
const { baseVariables, resolvedMap } = maps;
const RE_VARIABLE_VALUE = /(?:--)?((.+)--(.+)-(.+))/;
const variableCollection = parseDeclarationValue(decl.value);
for (const variable of variableCollection) {
const matches = variable.match(RE_VARIABLE_VALUE);
if (matches) {
const [, wholeVariable, baseVariable, operation, argument] = matches;
const value = baseVariables.get(baseVariable) ?? getValueFromAlias(baseVariable, maps);
if (!value) {
throw new Error(`Cannot derive from ${baseVariable} because it is neither defined in config nor is it an alias!`);
}
const derivedValue = derive(value, operation, argument, isDark);
resolvedMap.set(wholeVariable, derivedValue);
}
}
}
function extract(decl, {aliasMap, baseVariables}) {
if (decl.variable) {
// see if right side is of form "var(--foo)"
const wholeVariable = decl.value.match(/var\(--(.+)\)/)?.[1];
// remove -- from the prop
const prop = decl.prop.substring(2);
if (wholeVariable) {
aliasMap.set(prop, wholeVariable);
// Since this is an alias, we shouldn't store it in baseVariables
return;
}
baseVariables.set(prop, decl.value);
}
}
function addResolvedVariablesToRootSelector(root, {Rule, Declaration}, {resolvedMap}) {
const newRule = new Rule({ selector: ":root", source: root.source });
// Add derived css variables to :root
resolvedMap.forEach((value, key) => {
const declaration = new Declaration({prop: `--${key}`, value});
newRule.append(declaration);
});
root.append(newRule);
}
function populateMapWithDerivedVariables(map, cssFileLocation, {resolvedMap, aliasMap}) {
const location = cssFileLocation.match(/(.+)\/.+\.css/)?.[1];
const derivedVariables = [
...([...resolvedMap.keys()].filter(v => !aliasMap.has(v))),
...([...aliasMap.entries()].map(([alias, variable]) => `${alias}=${variable}`))
];
const sharedObject = map.get(location);
const output = { "derived-variables": derivedVariables };
if (sharedObject) {
Object.assign(sharedObject, output);
}
else {
map.set(location, output);
}
}
/**
* @callback derive
* @param {string} value - The base value on which an operation is applied
* @param {string} operation - The operation to be applied (eg: darker, lighter...)
* @param {string} argument - The argument for this operation
* @param {boolean} isDark - Indicates whether this theme is dark
*/
/**
*
* @param {Object} opts - Options for the plugin
* @param {derive} opts.derive - The callback which contains the logic for resolving derived variables
* @param {Map} opts.compiledVariables - A map that stores derived variables so that manifest source sections can be produced
*/
module.exports = (opts = {}) => {
const aliasMap = new Map();
const resolvedMap = new Map();
const baseVariables = new Map();
const maps = { aliasMap, resolvedMap, baseVariables };
return {
postcssPlugin: "postcss-compile-variables",
Once(root, {Rule, Declaration, result}) {
const cssFileLocation = root.source.input.from;
if (cssFileLocation.includes("type=runtime")) {
// If this is a runtime theme, don't derive variables.
return;
}
const isDark = cssFileLocation.includes("dark=true");
/*
Go through the CSS file once to extract all aliases and base variables.
We use these when resolving derived variables later.
*/
root.walkDecls(decl => extract(decl, maps));
root.walkDecls(decl => resolveDerivedVariable(decl, opts.derive, maps, isDark));
addResolvedVariablesToRootSelector(root, {Rule, Declaration}, maps);
if (opts.compiledVariables){
populateMapWithDerivedVariables(opts.compiledVariables, cssFileLocation, maps);
}
// Also produce a mapping from alias to completely resolved color
const resolvedAliasMap = new Map();
aliasMap.forEach((value, key) => {
resolvedAliasMap.set(key, resolvedMap.get(value));
});
// Publish the base-variables, derived-variables and resolved aliases to the other postcss-plugins
const combinedMap = new Map([...baseVariables, ...resolvedMap, ...resolvedAliasMap]);
result.messages.push({
type: "resolved-variable-map",
plugin: "postcss-compile-variables",
colorMap: combinedMap,
});
},
};
};
module.exports.postcss = true;

View file

@ -0,0 +1,92 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
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.
*/
const valueParser = require("postcss-value-parser");
const resolve = require("path").resolve;
function colorsFromURL(url, colorMap) {
const params = new URL(`file://${url}`).searchParams;
const primary = params.get("primary");
if (!primary) {
return null;
}
const secondary = params.get("secondary");
const primaryColor = colorMap.get(primary);
const secondaryColor = colorMap.get(secondary);
if (!primaryColor) {
throw new Error(`Variable ${primary} not found in resolved color variables!`);
}
if (secondary && !secondaryColor) {
throw new Error(`Variable ${secondary} not found in resolved color variables!`);
}
return [primaryColor, secondaryColor];
}
function processURL(decl, replacer, colorMap, cssPath) {
const value = decl.value;
const parsed = valueParser(value);
parsed.walk(node => {
if (node.type !== "function" || node.value !== "url") {
return;
}
const urlStringNode = node.nodes[0];
const oldURL = urlStringNode.value;
const oldURLAbsolute = resolve(cssPath, oldURL);
const colors = colorsFromURL(oldURLAbsolute, colorMap);
if (!colors) {
// If no primary color is provided via url params, then this url need not be handled.
return;
}
const newURL = replacer(oldURLAbsolute.replace(/\?.+/, ""), ...colors);
if (!newURL) {
throw new Error("Replacer failed to produce a replacement URL!");
}
urlStringNode.value = newURL;
});
decl.assign({prop: decl.prop, value: parsed.toString()})
}
/* *
* @type {import('postcss').PluginCreator}
*/
module.exports = (opts = {}) => {
return {
postcssPlugin: "postcss-url-to-variable",
Once(root, {result}) {
const cssFileLocation = root.source.input.from;
if (cssFileLocation.includes("type=runtime")) {
// If this is a runtime theme, don't process urls.
return;
}
/*
postcss-compile-variables should have sent the list of resolved colours down via results
*/
const {colorMap} = result.messages.find(m => m.type === "resolved-variable-map");
if (!colorMap) {
throw new Error("Postcss results do not contain resolved colors!");
}
/*
Go through each declaration and if it contains an URL, replace the url with the result
of running replacer(url)
*/
const cssPath = root.source?.input.file.replace(/[^/]*$/, "");
root.walkDecls(decl => processURL(decl, opts.replacer, colorMap, cssPath));
},
};
};
module.exports.postcss = true;

View file

@ -0,0 +1,97 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
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.
*/
const valueParser = require("postcss-value-parser");
/**
* This plugin extracts content inside url() into css variables and adds the variables to the root section.
* This plugin is used in conjunction with css-url-processor plugin to colorize svg icons.
*/
const idToPrepend = "icon-url";
function findAndReplaceUrl(decl, urlVariables, counter) {
const value = decl.value;
const parsed = valueParser(value);
parsed.walk(node => {
if (node.type !== "function" || node.value !== "url") {
return;
}
const url = node.nodes[0].value;
if (!url.match(/\.svg\?primary=.+/)) {
return;
}
const count = counter.next().value;
const variableName = `${idToPrepend}-${count}`;
urlVariables.set(variableName, url);
node.value = "var";
node.nodes = [{ type: "word", value: `--${variableName}` }];
});
decl.assign({prop: decl.prop, value: parsed.toString()})
}
function addResolvedVariablesToRootSelector(root, { Rule, Declaration }, urlVariables) {
const newRule = new Rule({ selector: ":root", source: root.source });
// Add derived css variables to :root
urlVariables.forEach((value, key) => {
const declaration = new Declaration({ prop: `--${key}`, value: `url("${value}")`});
newRule.append(declaration);
});
root.append(newRule);
}
function populateMapWithIcons(map, cssFileLocation, urlVariables) {
const location = cssFileLocation.match(/(.+)\/.+\.css/)?.[1];
const sharedObject = map.get(location);
const output = {"icon": Object.fromEntries(urlVariables)};
if (sharedObject) {
Object.assign(sharedObject, output);
}
else {
map.set(location, output);
}
}
function *createCounter() {
for (let i = 0; ; ++i) {
yield i;
}
}
/* *
* @type {import('postcss').PluginCreator}
*/
module.exports = (opts = {}) => {
return {
postcssPlugin: "postcss-url-to-variable",
Once(root, { Rule, Declaration }) {
const urlVariables = new Map();
const counter = createCounter();
root.walkDecls(decl => findAndReplaceUrl(decl, urlVariables, counter));
const cssFileLocation = root.source.input.from;
if (urlVariables.size && !cssFileLocation.includes("type=runtime")) {
addResolvedVariablesToRootSelector(root, { Rule, Declaration }, urlVariables);
}
if (opts.compiledVariables){
const cssFileLocation = root.source.input.from;
populateMapWithIcons(opts.compiledVariables, cssFileLocation, urlVariables);
}
},
};
};
module.exports.postcss = true;

View file

@ -0,0 +1,51 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
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.
*/
import {readFileSync, mkdirSync, writeFileSync} from "fs";
import {resolve} from "path";
import {h32} from "xxhashjs";
import {getColoredSvgString} from "../../src/platform/web/theming/shared/svg-colorizer.mjs";
function createHash(content) {
const hasher = new h32(0);
hasher.update(content);
return hasher.digest();
}
/**
* Builds a new svg with the colors replaced and returns its location.
* @param {string} svgLocation The location of the input svg file
* @param {string} primaryColor Primary color for the new svg
* @param {string} secondaryColor Secondary color for the new svg
*/
export function buildColorizedSVG(svgLocation, primaryColor, secondaryColor) {
const svgCode = readFileSync(svgLocation, { encoding: "utf8"});
const coloredSVGCode = getColoredSvgString(svgCode, primaryColor, secondaryColor);
const fileName = svgLocation.match(/.+[/\\](.+\.svg)/)[1];
const outputName = `${fileName.substring(0, fileName.length - 4)}-${createHash(coloredSVGCode)}.svg`;
const outputPath = resolve(__dirname, "./.tmp");
try {
mkdirSync(outputPath);
}
catch (e) {
if (e.code !== "EEXIST") {
throw e;
}
}
const outputFile = `${outputPath}/${outputName}`;
writeFileSync(outputFile, coloredSVGCode);
return outputFile;
}

View file

@ -14,16 +14,17 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {LoginMethod} from "./LoginMethod.js"; const postcss = require("postcss");
export class PasswordLoginMethod extends LoginMethod { module.exports.createTestRunner = function (plugin) {
constructor(options) { return async function run(input, output, opts = {}, assert) {
super(options); let result = await postcss([plugin(opts)]).process(input, { from: undefined, });
this.username = options.username; assert.strictEqual(
this.password = options.password; result.css.replaceAll(/\s/g, ""),
} output.replaceAll(/\s/g, "")
);
async login(hsApi, deviceName, log) { assert.strictEqual(result.warnings().length, 0);
return await hsApi.passwordLogin(this.username, this.password, deviceName, {log}).response(); };
}
} }

View file

@ -0,0 +1,156 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
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.
*/
const offColor = require("off-color").offColor;
const postcss = require("postcss");
const plugin = require("../css-compile-variables");
const derive = require("../color").derive;
const run = require("./common").createTestRunner(plugin);
module.exports.tests = function tests() {
return {
"derived variables are resolved": async (assert) => {
const inputCSS = `
:root {
--foo-color: #ff0;
}
div {
background-color: var(--foo-color--lighter-50);
}`;
const transformedColor = offColor("#ff0").lighten(0.5);
const outputCSS =
inputCSS +
`
:root {
--foo-color--lighter-50: ${transformedColor.hex()};
}
`;
await run( inputCSS, outputCSS, {derive}, assert);
},
"derived variables work with alias": async (assert) => {
const inputCSS = `
:root {
--icon-color: #fff;
}
div {
background: var(--icon-color--darker-20);
--my-alias: var(--icon-color--darker-20);
color: var(--my-alias--lighter-15);
}`;
const colorDarker = offColor("#fff").darken(0.2).hex();
const aliasLighter = offColor(colorDarker).lighten(0.15).hex();
const outputCSS = inputCSS + `:root {
--icon-color--darker-20: ${colorDarker};
--my-alias--lighter-15: ${aliasLighter};
}
`;
await run(inputCSS, outputCSS, {derive}, assert);
},
"derived variable throws if base not present in config": async (assert) => {
const css = `:root {
color: var(--icon-color--darker-20);
}`;
assert.rejects(async () => await postcss([plugin({ variables: {} })]).process(css, { from: undefined, }));
},
"multiple derived variable in single declaration is parsed correctly": async (assert) => {
const inputCSS = `
:root {
--foo-color: #ff0;
}
div {
background-color: linear-gradient(var(--foo-color--lighter-50), var(--foo-color--darker-20));
}`;
const transformedColor1 = offColor("#ff0").lighten(0.5);
const transformedColor2 = offColor("#ff0").darken(0.2);
const outputCSS =
inputCSS +
`
:root {
--foo-color--lighter-50: ${transformedColor1.hex()};
--foo-color--darker-20: ${transformedColor2.hex()};
}
`;
await run( inputCSS, outputCSS, {derive}, assert);
},
"multiple aliased-derived variable in single declaration is parsed correctly": async (assert) => {
const inputCSS = `
:root {
--foo-color: #ff0;
}
div {
--my-alias: var(--foo-color);
background-color: linear-gradient(var(--my-alias--lighter-50), var(--my-alias--darker-20));
}`;
const transformedColor1 = offColor("#ff0").lighten(0.5);
const transformedColor2 = offColor("#ff0").darken(0.2);
const outputCSS =
inputCSS +
`
:root {
--my-alias--lighter-50: ${transformedColor1.hex()};
--my-alias--darker-20: ${transformedColor2.hex()};
}
`;
await run( inputCSS, outputCSS, {derive}, assert);
},
"compiledVariables map is populated": async (assert) => {
const compiledVariables = new Map();
const inputCSS = `
:root {
--icon-color: #fff;
}
div {
background: var(--icon-color--darker-20);
--my-alias: var(--icon-color--darker-20);
color: var(--my-alias--lighter-15);
}`;
await postcss([plugin({ derive, compiledVariables })]).process(inputCSS, { from: "/foo/bar/test.css", });
const actualArray = compiledVariables.get("/foo/bar")["derived-variables"];
const expectedArray = ["icon-color--darker-20", "my-alias=icon-color--darker-20", "my-alias--lighter-15"];
assert.deepStrictEqual(actualArray.sort(), expectedArray.sort());
},
"derived variable are supported in urls": async (assert) => {
const inputCSS = `
:root {
--foo-color: #ff0;
}
div {
background-color: var(--foo-color--lighter-50);
background: url("./foo/bar/icon.svg?primary=foo-color--darker-5");
}
a {
background: url("foo/bar/icon.svg");
}`;
const transformedColorLighter = offColor("#ff0").lighten(0.5);
const transformedColorDarker = offColor("#ff0").darken(0.05);
const outputCSS =
inputCSS +
`
:root {
--foo-color--lighter-50: ${transformedColorLighter.hex()};
--foo-color--darker-5: ${transformedColorDarker.hex()};
}
`;
await run( inputCSS, outputCSS, {derive}, assert);
}
};
};

View file

@ -0,0 +1,71 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
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.
*/
const plugin = require("../css-url-to-variables");
const run = require("./common").createTestRunner(plugin);
const postcss = require("postcss");
module.exports.tests = function tests() {
return {
"url is replaced with variable": async (assert) => {
const inputCSS = `div {
background: no-repeat center/80% url("../img/image.svg?primary=main-color--darker-20");
}
button {
background: url("/home/foo/bar/cool.svg?primary=blue&secondary=green");
}`;
const outputCSS =
`div {
background: no-repeat center/80% var(--icon-url-0);
}
button {
background: var(--icon-url-1);
}`+
`
:root {
--icon-url-0: url("../img/image.svg?primary=main-color--darker-20");
--icon-url-1: url("/home/foo/bar/cool.svg?primary=blue&secondary=green");
}
`;
await run(inputCSS, outputCSS, { }, assert);
},
"non svg urls without query params are not replaced": async (assert) => {
const inputCSS = `div {
background: no-repeat url("./img/foo/bar/image.png");
}`;
await run(inputCSS, inputCSS, {}, assert);
},
"map is populated with icons": async (assert) => {
const compiledVariables = new Map();
compiledVariables.set("/foo/bar", { "derived-variables": ["background-color--darker-20", "accent-color--lighter-15"] });
const inputCSS = `div {
background: no-repeat center/80% url("../img/image.svg?primary=main-color--darker-20");
}
button {
background: url("/home/foo/bar/cool.svg?primary=blue&secondary=green");
}`;
const expectedObject = {
"icon-url-0": "../img/image.svg?primary=main-color--darker-20",
"icon-url-1": "/home/foo/bar/cool.svg?primary=blue&secondary=green",
};
await postcss([plugin({compiledVariables})]).process(inputCSS, { from: "/foo/bar/test.css", });
const sharedVariable = compiledVariables.get("/foo/bar");
assert.deepEqual(["background-color--darker-20", "accent-color--lighter-15"], sharedVariable["derived-variables"]);
assert.deepEqual(expectedObject, sharedVariable["icon"]);
}
};
};

View file

@ -1,3 +1,4 @@
set -e
if [ -z "$1" ]; then if [ -z "$1" ]; then
echo "provide a new version, current version is $(jq '.version' package.json)" echo "provide a new version, current version is $(jq '.version' package.json)"
exit 1 exit 1

View file

@ -0,0 +1,19 @@
{
"name": "hydrogen-view-sdk",
"description": "Embeddable matrix client library, including view components",
"version": "0.1.0",
"main": "./lib-build/hydrogen.cjs.js",
"exports": {
".": {
"import": "./lib-build/hydrogen.es.js",
"require": "./lib-build/hydrogen.cjs.js"
},
"./paths/vite": "./paths/vite.js",
"./style.css": "./asset-build/assets/theme-element-light.css",
"./theme-element-light.css": "./asset-build/assets/theme-element-light.css",
"./theme-element-dark.css": "./asset-build/assets/theme-element-dark.css",
"./main.js": "./asset-build/assets/main.js",
"./download-sandbox.html": "./asset-build/assets/download-sandbox.html",
"./assets/*": "./asset-build/assets/*"
}
}

25
scripts/sdk/build.sh Executable file
View file

@ -0,0 +1,25 @@
#!/bin/bash
# Exit whenever one of the commands fail with a non-zero exit code
set -e
set -o pipefail
# Enable extended globs so we can use the `!(filename)` glob syntax
shopt -s extglob
# Only remove the directory contents instead of the whole directory to maintain
# the `npm link`/`yarn link` symlink
rm -rf target/*
yarn run vite build -c vite.sdk-assets-config.js
yarn run vite build -c vite.sdk-lib-config.js
yarn tsc -p tsconfig-declaration.json
./scripts/sdk/create-manifest.js ./target/package.json
mkdir target/paths
# this doesn't work, the ?url imports need to be in the consuming project, so disable for now
# ./scripts/sdk/transform-paths.js ./src/platform/web/sdk/paths/vite.js ./target/paths/vite.js
cp doc/SDK.md target/README.md
pushd target/asset-build
rm index.html
popd
pushd target/asset-build/assets
# Remove all `*.wasm` and `*.js` files except for `main.js`
rm !(main).js *.wasm
popd

23
scripts/sdk/create-manifest.js Executable file
View file

@ -0,0 +1,23 @@
#!/usr/bin/env node
const fs = require("fs");
const appManifest = require("../../package.json");
const baseSDKManifest = require("./base-manifest.json");
/*
Need to leave typescript type definitions out until the
typescript conversion is complete and all imports in the d.ts files
exists.
```
"types": "types/lib.d.ts"
```
*/
const mergeOptions = require('merge-options');
const manifestExtension = {
devDependencies: undefined,
scripts: undefined,
};
const manifest = mergeOptions(appManifest, baseSDKManifest, manifestExtension);
const json = JSON.stringify(manifest, undefined, 2);
const outFile = process.argv[2];
fs.writeFileSync(outFile, json, {encoding: "utf8"});

3
scripts/sdk/test/.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
node_modules
dist
yarn.lock

2
scripts/sdk/test/deps.d.ts vendored Normal file
View file

@ -0,0 +1,2 @@
// Keep TypeScripts from complaining about hydrogen-view-sdk not having types yet
declare module "hydrogen-view-sdk";

View file

@ -0,0 +1,21 @@
import * as hydrogenViewSdk from "hydrogen-view-sdk";
import downloadSandboxPath from 'hydrogen-view-sdk/download-sandbox.html?url';
import workerPath from 'hydrogen-view-sdk/main.js?url';
import olmWasmPath from '@matrix-org/olm/olm.wasm?url';
import olmJsPath from '@matrix-org/olm/olm.js?url';
import olmLegacyJsPath from '@matrix-org/olm/olm_legacy.js?url';
const assetPaths = {
downloadSandbox: downloadSandboxPath,
worker: workerPath,
olm: {
wasm: olmWasmPath,
legacyBundle: olmLegacyJsPath,
wasmBundle: olmJsPath
}
};
import "hydrogen-view-sdk/assets/theme-element-light.css";
console.log('hydrogenViewSdk', hydrogenViewSdk);
console.log('assetPaths', assetPaths);
console.log('Entry ESM works ✅');

View file

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<div id="app" class="hydrogen"></div>
<script type="module" src="./esm-entry.ts"></script>
</body>
</html>

View file

@ -0,0 +1,8 @@
{
"name": "test-sdk",
"version": "0.0.0",
"description": "",
"dependencies": {
"hydrogen-view-sdk": "link:../../../target"
}
}

View file

@ -0,0 +1,13 @@
// Make sure the SDK can be used in a CommonJS environment.
// Usage: node scripts/sdk/test/test-sdk-in-commonjs-env.js
const hydrogenViewSdk = require('hydrogen-view-sdk');
// Test that the "exports" are available:
// Worker
require.resolve('hydrogen-view-sdk/main.js');
// Styles
require.resolve('hydrogen-view-sdk/assets/theme-element-light.css');
// Can access files in the assets/* directory
require.resolve('hydrogen-view-sdk/assets/main.js');
console.log('SDK works in CommonJS ✅');

View file

@ -0,0 +1,19 @@
const { resolve } = require('path');
const { build } = require('vite');
async function main() {
await build({
outDir: './dist',
build: {
rollupOptions: {
input: {
main: resolve(__dirname, 'index.html')
}
}
}
});
console.log('SDK works in Vite build ✅');
}
main();

36
scripts/sdk/transform-paths.js Executable file
View file

@ -0,0 +1,36 @@
#!/usr/bin/env node
/**
This script transforms the string literals in the sdk path files to adjust paths
from what they are at development time to what they will be in the sdk package.
It does this by looking in all string literals in the paths file and looking for file names
that we expect and need replacing (as they are bundled with the sdk).
Usage: ./transform-paths.js <input file> <output file>
*/
const acorn = require("acorn");
const walk = require("acorn-walk")
const escodegen = require("escodegen");
const fs = require("fs");
const code = fs.readFileSync(process.argv[2], {encoding: "utf8"});
const ast = acorn.parse(code, {ecmaVersion: "13", sourceType: "module"});
function changePrefix(value, file, newPrefix = "") {
const idx = value.indexOf(file);
if (idx !== -1) {
return newPrefix + value.substr(idx);
}
return value;
}
walk.simple(ast, {
Literal(node) {
node.value = changePrefix(node.value, "download-sandbox.html", "../");
node.value = changePrefix(node.value, "main.js", "../");
}
});
const transformedCode = escodegen.generate(ast);
fs.writeFileSync(process.argv[3], transformedCode, {encoding: "utf8"})

View file

@ -1,43 +0,0 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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.
*/
const finalhandler = require('finalhandler')
const http = require('http')
const serveStatic = require('serve-static')
const path = require('path');
// Serve up parent directory with cache disabled
const serve = serveStatic(
path.resolve(__dirname, "../"),
{
etag: false,
setHeaders: res => {
res.setHeader("Pragma", "no-cache");
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
res.setHeader("Expires", "Wed, 21 Oct 2015 07:28:00 GMT");
},
index: ['index.html', 'index.htm']
}
);
// Create server
const server = http.createServer(function onRequest (req, res) {
console.log(req.method, req.url);
serve(req, res, finalhandler(req, res))
});
// Listen
server.listen(3000);

View file

@ -0,0 +1,5 @@
#!/bin/sh
cp scripts/test-derived-theme/theme.json target/assets/theme-customer.json
cat target/config.json | jq '.themeManifests += ["assets/theme-customer.json"]' | cat > target/config.temp.json
rm target/config.json
mv target/config.temp.json target/config.json

View file

@ -0,0 +1,51 @@
{
"name": "Customer",
"extends": "element",
"id": "customer",
"values": {
"variants": {
"dark": {
"dark": true,
"default": true,
"name": "Dark",
"variables": {
"background-color-primary": "#21262b",
"background-color-secondary": "#2D3239",
"text-color": "#fff",
"accent-color": "#F03F5B",
"error-color": "#FF4B55",
"fixed-white": "#fff",
"room-badge": "#61708b",
"link-color": "#238cf5"
}
},
"light": {
"default": true,
"name": "Dark",
"variables": {
"background-color-primary": "#21262b",
"background-color-secondary": "#2D3239",
"text-color": "#fff",
"accent-color": "#F03F5B",
"error-color": "#FF4B55",
"fixed-white": "#fff",
"room-badge": "#61708b",
"link-color": "#238cf5"
}
},
"red": {
"name": "Gruvbox",
"variables": {
"background-color-primary": "#282828",
"background-color-secondary": "#3c3836",
"text-color": "#fbf1c7",
"accent-color": "#8ec07c",
"error-color": "#fb4934",
"fixed-white": "#fff",
"room-badge": "#cc241d",
"link-color": "#fe8019"
}
}
}
}
}

View file

@ -1,37 +0,0 @@
// Snowpack Configuration File
// See all supported options: https://www.snowpack.dev/reference/configuration
/** @type {import("snowpack").SnowpackUserConfig } */
module.exports = {
mount: {
// More specific paths before less specific paths (if they overlap)
"src/platform/web/docroot": "/",
"src": "/src",
"lib": {url: "/lib", static: true },
"assets": "/assets",
/* ... */
},
exclude: [
/* Avoid scanning scripts which use dev-dependencies and pull in babel, rollup, etc. */
'**/node_modules/**/*',
'**/scripts/**',
'**/target/**',
'**/prototypes/**',
'**/src/platform/web/legacy-polyfill.js',
'**/src/platform/web/worker/polyfill.js'
],
plugins: [
/* ... */
],
packageOptions: {
/* ... */
},
devOptions: {
open: "none",
hmr: false,
/* ... */
},
buildOptions: {
/* ... */
},
};

View file

@ -0,0 +1,136 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
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.
*/
import {ViewModel} from "./ViewModel";
import {KeyType} from "../matrix/ssss/index";
import {Status} from "./session/settings/KeyBackupViewModel.js";
export class AccountSetupViewModel extends ViewModel {
constructor(options) {
super(options);
this._accountSetup = options.accountSetup;
this._dehydratedDevice = undefined;
this._decryptDehydratedDeviceViewModel = undefined;
if (this._accountSetup.encryptedDehydratedDevice) {
this._decryptDehydratedDeviceViewModel = new DecryptDehydratedDeviceViewModel(this, dehydratedDevice => {
this._dehydratedDevice = dehydratedDevice;
this._decryptDehydratedDeviceViewModel = undefined;
this.emitChange("deviceDecrypted");
});
}
}
get decryptDehydratedDeviceViewModel() {
return this._decryptDehydratedDeviceViewModel;
}
get deviceDecrypted() {
return !!this._dehydratedDevice;
}
get dehydratedDeviceId() {
return this._accountSetup.encryptedDehydratedDevice.deviceId;
}
finish() {
this._accountSetup.finish(this._dehydratedDevice);
}
}
// this vm adopts the same shape as KeyBackupViewModel so the same view can be reused.
class DecryptDehydratedDeviceViewModel extends ViewModel {
constructor(accountSetupViewModel, decryptedCallback) {
super(accountSetupViewModel.options);
this._accountSetupViewModel = accountSetupViewModel;
this._isBusy = false;
this._status = Status.SetupKey;
this._error = undefined;
this._decryptedCallback = decryptedCallback;
}
get decryptAction() {
return this.i18n`Restore`;
}
get purpose() {
return this.i18n`claim your dehydrated device`;
}
get offerDehydratedDeviceSetup() {
return false;
}
get dehydratedDeviceId() {
return this._accountSetupViewModel._dehydratedDevice?.deviceId;
}
get isBusy() {
return this._isBusy;
}
get backupVersion() { return 0; }
get status() {
return this._status;
}
get error() {
return this._error?.message;
}
showPhraseSetup() {
if (this._status === Status.SetupKey) {
this._status = Status.SetupPhrase;
this.emitChange("status");
}
}
showKeySetup() {
if (this._status === Status.SetupPhrase) {
this._status = Status.SetupKey;
this.emitChange("status");
}
}
async _enterCredentials(keyType, credential) {
if (credential) {
try {
this._isBusy = true;
this.emitChange("isBusy");
const {encryptedDehydratedDevice} = this._accountSetupViewModel._accountSetup;
const dehydratedDevice = await encryptedDehydratedDevice.decrypt(keyType, credential);
this._decryptedCallback(dehydratedDevice);
} catch (err) {
console.error(err);
this._error = err;
this.emitChange("error");
} finally {
this._isBusy = false;
this.emitChange("");
}
}
}
enterSecurityPhrase(passphrase) {
this._enterCredentials(KeyType.Passphrase, passphrase);
}
enterSecurityKey(securityKey) {
this._enterCredentials(KeyType.RecoveryKey, securityKey);
}
disable() {}
}

View file

@ -0,0 +1,71 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
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.
*/
import {Options as BaseOptions, ViewModel} from "./ViewModel";
import {Client} from "../matrix/Client.js";
import {SegmentType} from "./navigation/index";
type Options = { sessionId: string; } & BaseOptions;
export class LogoutViewModel extends ViewModel<SegmentType, Options> {
private _sessionId: string;
private _busy: boolean;
private _showConfirm: boolean;
private _error?: Error;
constructor(options: Options) {
super(options);
this._sessionId = options.sessionId;
this._busy = false;
this._showConfirm = true;
this._error = undefined;
}
get showConfirm(): boolean {
return this._showConfirm;
}
get busy(): boolean {
return this._busy;
}
get cancelUrl(): string | undefined {
return this.urlCreator.urlForSegment("session", true);
}
async logout(): Promise<void> {
this._busy = true;
this._showConfirm = false;
this.emitChange("busy");
try {
const client = new Client(this.platform);
await client.startLogout(this._sessionId);
this.navigation.push("session", true);
} catch (err) {
this._error = err;
this._busy = false;
this.emitChange("busy");
}
}
get status(): string {
if (this._error) {
return this.i18n`Could not log out of device: ${this._error.message}`;
} else {
return this.i18n`Logging out… Please don't close the app.`;
}
}
}

View file

@ -14,22 +14,24 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {Client} from "../matrix/Client.js";
import {SessionViewModel} from "./session/SessionViewModel.js"; import {SessionViewModel} from "./session/SessionViewModel.js";
import {SessionLoadViewModel} from "./SessionLoadViewModel.js"; import {SessionLoadViewModel} from "./SessionLoadViewModel.js";
import {LoginViewModel} from "./login/LoginViewModel.js"; import {LoginViewModel} from "./login/LoginViewModel";
import {LogoutViewModel} from "./LogoutViewModel";
import {SessionPickerViewModel} from "./SessionPickerViewModel.js"; import {SessionPickerViewModel} from "./SessionPickerViewModel.js";
import {ViewModel} from "./ViewModel.js"; import {ViewModel} from "./ViewModel";
export class RootViewModel extends ViewModel { export class RootViewModel extends ViewModel {
constructor(options) { constructor(options) {
super(options); super(options);
this._createSessionContainer = options.createSessionContainer;
this._error = null; this._error = null;
this._sessionPickerViewModel = null; this._sessionPickerViewModel = null;
this._sessionLoadViewModel = null; this._sessionLoadViewModel = null;
this._loginViewModel = null; this._loginViewModel = null;
this._logoutViewModel = null;
this._sessionViewModel = null; this._sessionViewModel = null;
this._pendingSessionContainer = null; this._pendingClient = null;
} }
async load() { async load() {
@ -40,29 +42,34 @@ export class RootViewModel extends ViewModel {
} }
async _applyNavigation(shouldRestoreLastUrl) { async _applyNavigation(shouldRestoreLastUrl) {
const isLogin = this.navigation.path.get("login") const isLogin = this.navigation.path.get("login");
const logoutSessionId = this.navigation.path.get("logout")?.value;
const sessionId = this.navigation.path.get("session")?.value; const sessionId = this.navigation.path.get("session")?.value;
const loginToken = this.navigation.path.get("sso")?.value; const loginToken = this.navigation.path.get("sso")?.value;
if (isLogin) { if (isLogin) {
if (this.activeSection !== "login") { if (this.activeSection !== "login") {
this._showLogin(); this._showLogin();
} }
} else if (logoutSessionId) {
if (this.activeSection !== "logout") {
this._showLogout(logoutSessionId);
}
} else if (sessionId === true) { } else if (sessionId === true) {
if (this.activeSection !== "picker") { if (this.activeSection !== "picker") {
this._showPicker(); this._showPicker();
} }
} else if (sessionId) { } else if (sessionId) {
if (!this._sessionViewModel || this._sessionViewModel.id !== sessionId) { if (!this._sessionViewModel || this._sessionViewModel.id !== sessionId) {
// see _showLogin for where _pendingSessionContainer comes from // see _showLogin for where _pendingClient comes from
if (this._pendingSessionContainer && this._pendingSessionContainer.sessionId === sessionId) { if (this._pendingClient && this._pendingClient.sessionId === sessionId) {
const sessionContainer = this._pendingSessionContainer; const client = this._pendingClient;
this._pendingSessionContainer = null; this._pendingClient = null;
this._showSession(sessionContainer); this._showSession(client);
} else { } else {
// this should never happen, but we want to be sure not to leak it // this should never happen, but we want to be sure not to leak it
if (this._pendingSessionContainer) { if (this._pendingClient) {
this._pendingSessionContainer.dispose(); this._pendingClient.dispose();
this._pendingSessionContainer = null; this._pendingClient = null;
} }
this._showSessionLoader(sessionId); this._showSessionLoader(sessionId);
} }
@ -106,8 +113,7 @@ export class RootViewModel extends ViewModel {
this._setSection(() => { this._setSection(() => {
this._loginViewModel = new LoginViewModel(this.childOptions({ this._loginViewModel = new LoginViewModel(this.childOptions({
defaultHomeserver: this.platform.config["defaultHomeServer"], defaultHomeserver: this.platform.config["defaultHomeServer"],
createSessionContainer: this._createSessionContainer, ready: client => {
ready: sessionContainer => {
// we don't want to load the session container again, // we don't want to load the session container again,
// but we also want the change of screen to go through the navigation // but we also want the change of screen to go through the navigation
// so we store the session container in a temporary variable that will be // so we store the session container in a temporary variable that will be
@ -116,28 +122,34 @@ export class RootViewModel extends ViewModel {
// Also, we should not call _setSection before the navigation is in the correct state, // Also, we should not call _setSection before the navigation is in the correct state,
// as url creation (e.g. in RoomTileViewModel) // as url creation (e.g. in RoomTileViewModel)
// won't be using the correct navigation base path. // won't be using the correct navigation base path.
this._pendingSessionContainer = sessionContainer; this._pendingClient = client;
this.navigation.push("session", sessionContainer.sessionId); this.navigation.push("session", client.sessionId);
}, },
loginToken loginToken
})); }));
}); });
} }
_showSession(sessionContainer) { _showLogout(sessionId) {
this._setSection(() => { this._setSection(() => {
this._sessionViewModel = new SessionViewModel(this.childOptions({sessionContainer})); this._logoutViewModel = new LogoutViewModel(this.childOptions({sessionId}));
});
}
_showSession(client) {
this._setSection(() => {
this._sessionViewModel = new SessionViewModel(this.childOptions({client}));
this._sessionViewModel.start(); this._sessionViewModel.start();
}); });
} }
_showSessionLoader(sessionId) { _showSessionLoader(sessionId) {
const sessionContainer = this._createSessionContainer(); const client = new Client(this.platform);
sessionContainer.startWithExistingSession(sessionId); client.startWithExistingSession(sessionId);
this._setSection(() => { this._setSection(() => {
this._sessionLoadViewModel = new SessionLoadViewModel(this.childOptions({ this._sessionLoadViewModel = new SessionLoadViewModel(this.childOptions({
sessionContainer, client,
ready: sessionContainer => this._showSession(sessionContainer) ready: client => this._showSession(client)
})); }));
this._sessionLoadViewModel.start(); this._sessionLoadViewModel.start();
}); });
@ -150,6 +162,8 @@ export class RootViewModel extends ViewModel {
return "session"; return "session";
} else if (this._loginViewModel) { } else if (this._loginViewModel) {
return "login"; return "login";
} else if (this._logoutViewModel) {
return "logout";
} else if (this._sessionPickerViewModel) { } else if (this._sessionPickerViewModel) {
return "picker"; return "picker";
} else if (this._sessionLoadViewModel) { } else if (this._sessionLoadViewModel) {
@ -165,12 +179,14 @@ export class RootViewModel extends ViewModel {
this._sessionPickerViewModel = this.disposeTracked(this._sessionPickerViewModel); this._sessionPickerViewModel = this.disposeTracked(this._sessionPickerViewModel);
this._sessionLoadViewModel = this.disposeTracked(this._sessionLoadViewModel); this._sessionLoadViewModel = this.disposeTracked(this._sessionLoadViewModel);
this._loginViewModel = this.disposeTracked(this._loginViewModel); this._loginViewModel = this.disposeTracked(this._loginViewModel);
this._logoutViewModel = this.disposeTracked(this._logoutViewModel);
this._sessionViewModel = this.disposeTracked(this._sessionViewModel); this._sessionViewModel = this.disposeTracked(this._sessionViewModel);
// now set it again // now set it again
setter(); setter();
this._sessionPickerViewModel && this.track(this._sessionPickerViewModel); this._sessionPickerViewModel && this.track(this._sessionPickerViewModel);
this._sessionLoadViewModel && this.track(this._sessionLoadViewModel); this._sessionLoadViewModel && this.track(this._sessionLoadViewModel);
this._loginViewModel && this.track(this._loginViewModel); this._loginViewModel && this.track(this._loginViewModel);
this._logoutViewModel && this.track(this._logoutViewModel);
this._sessionViewModel && this.track(this._sessionViewModel); this._sessionViewModel && this.track(this._sessionViewModel);
this.emitChange("activeSection"); this.emitChange("activeSection");
} }
@ -178,6 +194,7 @@ export class RootViewModel extends ViewModel {
get error() { return this._error; } get error() { return this._error; }
get sessionViewModel() { return this._sessionViewModel; } get sessionViewModel() { return this._sessionViewModel; }
get loginViewModel() { return this._loginViewModel; } get loginViewModel() { return this._loginViewModel; }
get logoutViewModel() { return this._logoutViewModel; }
get sessionPickerViewModel() { return this._sessionPickerViewModel; } get sessionPickerViewModel() { return this._sessionPickerViewModel; }
get sessionLoadViewModel() { return this._sessionLoadViewModel; } get sessionLoadViewModel() { return this._sessionLoadViewModel; }
} }

View file

@ -14,21 +14,24 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {LoadStatus} from "../matrix/SessionContainer.js"; import {AccountSetupViewModel} from "./AccountSetupViewModel.js";
import {LoadStatus} from "../matrix/Client.js";
import {SyncStatus} from "../matrix/Sync.js"; import {SyncStatus} from "../matrix/Sync.js";
import {ViewModel} from "./ViewModel.js"; import {ViewModel} from "./ViewModel";
export class SessionLoadViewModel extends ViewModel { export class SessionLoadViewModel extends ViewModel {
constructor(options) { constructor(options) {
super(options); super(options);
const {sessionContainer, ready, homeserver, deleteSessionOnCancel} = options; const {client, ready, homeserver, deleteSessionOnCancel} = options;
this._sessionContainer = sessionContainer; this._client = client;
this._ready = ready; this._ready = ready;
this._homeserver = homeserver; this._homeserver = homeserver;
this._deleteSessionOnCancel = deleteSessionOnCancel; this._deleteSessionOnCancel = deleteSessionOnCancel;
this._loading = false; this._loading = false;
this._error = null; this._error = null;
this.backUrl = this.urlCreator.urlForSegment("session", true); this.backUrl = this.urlCreator.urlForSegment("session", true);
this._accountSetupViewModel = undefined;
} }
async start() { async start() {
@ -38,11 +41,16 @@ export class SessionLoadViewModel extends ViewModel {
try { try {
this._loading = true; this._loading = true;
this.emitChange("loading"); this.emitChange("loading");
this._waitHandle = this._sessionContainer.loadStatus.waitFor(s => { this._waitHandle = this._client.loadStatus.waitFor(s => {
if (s === LoadStatus.AccountSetup) {
this._accountSetupViewModel = new AccountSetupViewModel(this.childOptions({accountSetup: this._client.accountSetup}));
} else {
this._accountSetupViewModel = undefined;
}
this.emitChange("loadLabel"); this.emitChange("loadLabel");
// wait for initial sync, but not catchup sync // wait for initial sync, but not catchup sync
const isCatchupSync = s === LoadStatus.FirstSync && const isCatchupSync = s === LoadStatus.FirstSync &&
this._sessionContainer.sync.status.get() === SyncStatus.CatchupSync; this._client.sync.status.get() === SyncStatus.CatchupSync;
return isCatchupSync || return isCatchupSync ||
s === LoadStatus.LoginFailed || s === LoadStatus.LoginFailed ||
s === LoadStatus.Error || s === LoadStatus.Error ||
@ -59,15 +67,15 @@ export class SessionLoadViewModel extends ViewModel {
// much like we will once you are in the app. Probably a good idea // much like we will once you are in the app. Probably a good idea
// did it finish or get stuck at LoginFailed or Error? // did it finish or get stuck at LoginFailed or Error?
const loadStatus = this._sessionContainer.loadStatus.get(); const loadStatus = this._client.loadStatus.get();
const loadError = this._sessionContainer.loadError; const loadError = this._client.loadError;
if (loadStatus === LoadStatus.FirstSync || loadStatus === LoadStatus.Ready) { if (loadStatus === LoadStatus.FirstSync || loadStatus === LoadStatus.Ready) {
const sessionContainer = this._sessionContainer; const client = this._client;
// session container is ready, // session container is ready,
// don't dispose it anymore when // don't dispose it anymore when
// we get disposed // we get disposed
this._sessionContainer = null; this._client = null;
this._ready(sessionContainer); this._ready(client);
} }
if (loadError) { if (loadError) {
console.error("session load error", loadError); console.error("session load error", loadError);
@ -77,16 +85,16 @@ export class SessionLoadViewModel extends ViewModel {
console.error("error thrown during session load", err.stack); console.error("error thrown during session load", err.stack);
} finally { } finally {
this._loading = false; this._loading = false;
// loadLabel in case of sc.loadError also gets updated through this // loadLabel in case of client.loadError also gets updated through this
this.emitChange("loading"); this.emitChange("loading");
} }
} }
dispose() { dispose() {
if (this._sessionContainer) { if (this._client) {
this._sessionContainer.dispose(); this._client.dispose();
this._sessionContainer = null; this._client = null;
} }
if (this._waitHandle) { if (this._waitHandle) {
// rejects with AbortError // rejects with AbortError
@ -97,20 +105,27 @@ export class SessionLoadViewModel extends ViewModel {
// to show a spinner or not // to show a spinner or not
get loading() { get loading() {
const client = this._client;
if (client && client.loadStatus.get() === LoadStatus.AccountSetup) {
return false;
}
return this._loading; return this._loading;
} }
get loadLabel() { get loadLabel() {
const sc = this._sessionContainer; const client = this._client;
const error = this._error || (sc && sc.loadError); const error = this._getError();
if (error || (client && client.loadStatus.get() === LoadStatus.Error)) {
if (error || (sc && sc.loadStatus.get() === LoadStatus.Error)) {
return `Something went wrong: ${error && error.message}.`; return `Something went wrong: ${error && error.message}.`;
} }
// Statuses related to login are handled by respective login view models // Statuses related to login are handled by respective login view models
if (sc) { if (client) {
switch (sc.loadStatus.get()) { switch (client.loadStatus.get()) {
case LoadStatus.QueryAccount:
return `Querying account encryption setup…`;
case LoadStatus.AccountSetup:
return ""; // we'll show a header ing AccountSetupView
case LoadStatus.SessionSetup: case LoadStatus.SessionSetup:
return `Setting up your encryption keys…`; return `Setting up your encryption keys…`;
case LoadStatus.Loading: case LoadStatus.Loading:
@ -118,10 +133,32 @@ export class SessionLoadViewModel extends ViewModel {
case LoadStatus.FirstSync: case LoadStatus.FirstSync:
return `Getting your conversations from the server…`; return `Getting your conversations from the server…`;
default: default:
return this._sessionContainer.loadStatus.get(); return this._client.loadStatus.get();
} }
} }
return `Preparing…`; return `Preparing…`;
} }
_getError() {
return this._error || this._client?.loadError;
}
get hasError() {
return !!this._getError();
}
async exportLogs() {
const logExport = await this.logger.export();
this.platform.saveFileAs(logExport.asBlob(), `hydrogen-logs-${this.platform.clock.now()}.json`);
}
async logout() {
await this._client.logout();
this.navigation.push("session", true);
}
get accountSetupViewModel() {
return this._accountSetupViewModel;
}
} }

View file

@ -15,8 +15,8 @@ limitations under the License.
*/ */
import {SortedArray} from "../observable/index.js"; import {SortedArray} from "../observable/index.js";
import {ViewModel} from "./ViewModel.js"; import {ViewModel} from "./ViewModel";
import {avatarInitials, getIdentifierColorNumber} from "./avatar.js"; import {avatarInitials, getIdentifierColorNumber} from "./avatar";
class SessionItemViewModel extends ViewModel { class SessionItemViewModel extends ViewModel {
constructor(options, pickerVM) { constructor(options, pickerVM) {
@ -33,44 +33,6 @@ class SessionItemViewModel extends ViewModel {
return this._error && this._error.message; return this._error && this._error.message;
} }
async delete() {
this._isDeleting = true;
this.emitChange("isDeleting");
try {
await this._pickerVM.delete(this.id);
} catch(err) {
this._error = err;
console.error(err);
this.emitChange("error");
} finally {
this._isDeleting = false;
this.emitChange("isDeleting");
}
}
async clear() {
this._isClearing = true;
this.emitChange();
try {
await this._pickerVM.clear(this.id);
} catch(err) {
this._error = err;
console.error(err);
this.emitChange("error");
} finally {
this._isClearing = false;
this.emitChange("isClearing");
}
}
get isDeleting() {
return this._isDeleting;
}
get isClearing() {
return this._isClearing;
}
get id() { get id() {
return this._sessionInfo.id; return this._sessionInfo.id;
} }
@ -96,27 +58,6 @@ class SessionItemViewModel extends ViewModel {
return this._exportDataUrl; return this._exportDataUrl;
} }
async export() {
try {
const data = await this._pickerVM._exportData(this._sessionInfo.id);
const json = JSON.stringify(data, undefined, 2);
const blob = new Blob([json], {type: "application/json"});
this._exportDataUrl = URL.createObjectURL(blob);
this.emitChange("exportDataUrl");
} catch (err) {
alert(err.message);
console.error(err);
}
}
clearExport() {
if (this._exportDataUrl) {
URL.revokeObjectURL(this._exportDataUrl);
this._exportDataUrl = null;
this.emitChange("exportDataUrl");
}
}
get avatarColorNumber() { get avatarColorNumber() {
return getIdentifierColorNumber(this._sessionInfo.userId); return getIdentifierColorNumber(this._sessionInfo.userId);
} }
@ -148,43 +89,6 @@ export class SessionPickerViewModel extends ViewModel {
return this._loadViewModel; return this._loadViewModel;
} }
async _exportData(id) {
const sessionInfo = await this.platform.sessionInfoStorage.get(id);
const stores = await this.logger.run("export", log => {
return this.platform.storageFactory.export(id, log);
});
const data = {sessionInfo, stores};
return data;
}
async import(json) {
try {
const data = JSON.parse(json);
const {sessionInfo} = data;
sessionInfo.comment = `Imported on ${new Date().toLocaleString()} from id ${sessionInfo.id}.`;
sessionInfo.id = this._createSessionContainer().createNewSessionId();
await this.logger.run("import", log => {
return this.platform.storageFactory.import(sessionInfo.id, data.stores, log);
});
await this.platform.sessionInfoStorage.add(sessionInfo);
this._sessions.set(new SessionItemViewModel(sessionInfo, this));
} catch (err) {
alert(err.message);
console.error(err);
}
}
async delete(id) {
const idx = this._sessions.array.findIndex(s => s.id === id);
await this.platform.sessionInfoStorage.delete(id);
await this.platform.storageFactory.delete(id);
this._sessions.remove(idx);
}
async clear(id) {
await this.platform.storageFactory.delete(id);
}
get sessions() { get sessions() {
return this._sessions; return this._sessions;
} }

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2020 Bruno Windels <bruno@windels.cloud> Copyright 2020 Bruno Windels <bruno@windels.cloud>
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -19,56 +20,84 @@ limitations under the License.
// we do need to return a disposable from EventEmitter.on, or at least have a method here to easily track a subscription to an EventEmitter // we do need to return a disposable from EventEmitter.on, or at least have a method here to easily track a subscription to an EventEmitter
import {EventEmitter} from "../utils/EventEmitter"; import {EventEmitter} from "../utils/EventEmitter";
import {Disposables} from "../utils/Disposables.js"; import {Disposables} from "../utils/Disposables";
export class ViewModel extends EventEmitter { import type {Disposable} from "../utils/Disposables";
constructor(options = {}) { import type {Platform} from "../platform/web/Platform";
import type {Clock} from "../platform/web/dom/Clock";
import type {ILogger} from "../logging/types";
import type {Navigation} from "./navigation/Navigation";
import type {SegmentType} from "./navigation/index";
import type {IURLRouter} from "./navigation/URLRouter";
export type Options<T extends object = SegmentType> = {
platform: Platform;
logger: ILogger;
urlCreator: IURLRouter<T>;
navigation: Navigation<T>;
emitChange?: (params: any) => void;
}
export class ViewModel<N extends object = SegmentType, O extends Options<N> = Options<N>> extends EventEmitter<{change: never}> {
private disposables?: Disposables;
private _isDisposed = false;
private _options: Readonly<O>;
constructor(options: Readonly<O>) {
super(); super();
this.disposables = null;
this._isDisposed = false;
this._options = options; this._options = options;
} }
childOptions(explicitOptions) { childOptions<T extends Object>(explicitOptions: T): T & Options<N> {
const {navigation, urlCreator, platform} = this._options; return Object.assign({}, this._options, explicitOptions);
return Object.assign({navigation, urlCreator, platform}, explicitOptions);
} }
get options(): Readonly<O> { return this._options; }
// makes it easier to pass through dependencies of a sub-view model // makes it easier to pass through dependencies of a sub-view model
getOption(name) { getOption<N extends keyof O>(name: N): O[N] {
return this._options[name]; return this._options[name];
} }
track(disposable) { observeNavigation<T extends keyof N>(type: T, onChange: (value: N[T], type: T) => void): void {
const segmentObservable = this.navigation.observe(type);
const unsubscribe = segmentObservable.subscribe((value: N[T]) => {
onChange(value, type);
});
this.track(unsubscribe);
}
track<D extends Disposable>(disposable: D): D {
if (!this.disposables) { if (!this.disposables) {
this.disposables = new Disposables(); this.disposables = new Disposables();
} }
return this.disposables.track(disposable); return this.disposables.track(disposable);
} }
untrack(disposable) { untrack(disposable: Disposable): undefined {
if (this.disposables) { if (this.disposables) {
return this.disposables.untrack(disposable); return this.disposables.untrack(disposable);
} }
return null; return undefined;
} }
dispose() { dispose(): void {
if (this.disposables) { if (this.disposables) {
this.disposables.dispose(); this.disposables.dispose();
} }
this._isDisposed = true; this._isDisposed = true;
} }
get isDisposed() { get isDisposed(): boolean {
return this._isDisposed; return this._isDisposed;
} }
disposeTracked(disposable) { disposeTracked(disposable: Disposable | undefined): undefined {
if (this.disposables) { if (this.disposables) {
return this.disposables.disposeTracked(disposable); return this.disposables.disposeTracked(disposable);
} }
return null; return undefined;
} }
// TODO: this will need to support binding // TODO: this will need to support binding
@ -76,7 +105,7 @@ export class ViewModel extends EventEmitter {
// //
// translated string should probably always be bindings, unless we're fine with a refresh when changing the language? // translated string should probably always be bindings, unless we're fine with a refresh when changing the language?
// we probably are, if we're using routing with a url, we could just refresh. // we probably are, if we're using routing with a url, we could just refresh.
i18n(parts, ...expr) { i18n(parts: TemplateStringsArray, ...expr: any[]): string {
// just concat for now // just concat for now
let result = ""; let result = "";
for (let i = 0; i < parts.length; ++i) { for (let i = 0; i < parts.length; ++i) {
@ -88,11 +117,7 @@ export class ViewModel extends EventEmitter {
return result; return result;
} }
updateOptions(options) { emitChange(changedProps: any): void {
this._options = Object.assign(this._options, options);
}
emitChange(changedProps) {
if (this._options.emitChange) { if (this._options.emitChange) {
this._options.emitChange(changedProps); this._options.emitChange(changedProps);
} else { } else {
@ -100,27 +125,24 @@ export class ViewModel extends EventEmitter {
} }
} }
get platform() { get platform(): Platform {
return this._options.platform; return this._options.platform;
} }
get clock() { get clock(): Clock {
return this._options.platform.clock; return this._options.platform.clock;
} }
get logger() { get logger(): ILogger {
return this.platform.logger; return this.platform.logger;
} }
/** get urlCreator(): IURLRouter<N> {
* The url router, only meant to be used to create urls with from view models.
* @return {URLRouter}
*/
get urlCreator() {
return this._options.urlCreator; return this._options.urlCreator;
} }
get navigation() { get navigation(): Navigation<N> {
return this._options.navigation; // typescript needs a little help here
return this._options.navigation as unknown as Navigation<N>;
} }
} }

View file

@ -14,7 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
export function avatarInitials(name) { import { Platform } from "../platform/web/Platform";
import { MediaRepository } from "../matrix/net/MediaRepository";
export function avatarInitials(name: string): string {
let firstChar = name.charAt(0); let firstChar = name.charAt(0);
if (firstChar === "!" || firstChar === "@" || firstChar === "#") { if (firstChar === "!" || firstChar === "@" || firstChar === "#") {
firstChar = name.charAt(1); firstChar = name.charAt(1);
@ -29,10 +32,10 @@ export function avatarInitials(name) {
* *
* @return {number} * @return {number}
*/ */
function hashCode(str) { function hashCode(str: string): number {
let hash = 0; let hash = 0;
let i; let i: number;
let chr; let chr: number;
if (str.length === 0) { if (str.length === 0) {
return hash; return hash;
} }
@ -44,11 +47,11 @@ function hashCode(str) {
return Math.abs(hash); return Math.abs(hash);
} }
export function getIdentifierColorNumber(id) { export function getIdentifierColorNumber(id: string): number {
return (hashCode(id) % 8) + 1; return (hashCode(id) % 8) + 1;
} }
export function getAvatarHttpUrl(avatarUrl, cssSize, platform, mediaRepository) { export function getAvatarHttpUrl(avatarUrl: string, cssSize: number, platform: Platform, mediaRepository: MediaRepository): string | null {
if (avatarUrl) { if (avatarUrl) {
const imageSize = cssSize * platform.devicePixelRatio; const imageSize = cssSize * platform.devicePixelRatio;
return mediaRepository.mxcUrlThumbnail(avatarUrl, imageSize, imageSize, "crop"); return mediaRepository.mxcUrlThumbnail(avatarUrl, imageSize, imageSize, "crop");

View file

@ -14,19 +14,19 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {ViewModel} from "../ViewModel.js"; import {ViewModel} from "../ViewModel";
import {LoginFailure} from "../../matrix/SessionContainer.js"; import {LoginFailure} from "../../matrix/Client.js";
export class CompleteSSOLoginViewModel extends ViewModel { export class CompleteSSOLoginViewModel extends ViewModel {
constructor(options) { constructor(options) {
super(options); super(options);
const { const {
loginToken, loginToken,
sessionContainer, client,
attemptLogin, attemptLogin,
} = options; } = options;
this._loginToken = loginToken; this._loginToken = loginToken;
this._sessionContainer = sessionContainer; this._client = client;
this._attemptLogin = attemptLogin; this._attemptLogin = attemptLogin;
this._errorMessage = ""; this._errorMessage = "";
this.performSSOLoginCompletion(); this.performSSOLoginCompletion();
@ -46,7 +46,7 @@ export class CompleteSSOLoginViewModel extends ViewModel {
const homeserver = await this.platform.settingsStorage.getString("sso_ongoing_login_homeserver"); const homeserver = await this.platform.settingsStorage.getString("sso_ongoing_login_homeserver");
let loginOptions; let loginOptions;
try { try {
loginOptions = await this._sessionContainer.queryLogin(homeserver).result; loginOptions = await this._client.queryLogin(homeserver).result;
} }
catch (err) { catch (err) {
this._showError(err.message); this._showError(err.message);

View file

@ -14,132 +14,176 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {ViewModel} from "../ViewModel.js"; import {Client} from "../../matrix/Client.js";
import {Options as BaseOptions, ViewModel} from "../ViewModel";
import {PasswordLoginViewModel} from "./PasswordLoginViewModel.js"; import {PasswordLoginViewModel} from "./PasswordLoginViewModel.js";
import {StartSSOLoginViewModel} from "./StartSSOLoginViewModel.js"; import {StartSSOLoginViewModel} from "./StartSSOLoginViewModel.js";
import {CompleteSSOLoginViewModel} from "./CompleteSSOLoginViewModel.js"; import {CompleteSSOLoginViewModel} from "./CompleteSSOLoginViewModel.js";
import {LoadStatus} from "../../matrix/SessionContainer.js"; import {LoadStatus} from "../../matrix/Client.js";
import {SessionLoadViewModel} from "../SessionLoadViewModel.js"; import {SessionLoadViewModel} from "../SessionLoadViewModel.js";
import {SegmentType} from "../navigation/index";
export class LoginViewModel extends ViewModel { import type {PasswordLoginMethod, SSOLoginHelper, TokenLoginMethod, ILoginMethod} from "../../matrix/login";
constructor(options) {
type Options = {
defaultHomeserver: string;
ready: ReadyFn;
loginToken?: string;
} & BaseOptions;
export class LoginViewModel extends ViewModel<SegmentType, Options> {
private _ready: ReadyFn;
private _loginToken?: string;
private _client: Client;
private _loginOptions?: LoginOptions;
private _passwordLoginViewModel?: PasswordLoginViewModel;
private _startSSOLoginViewModel?: StartSSOLoginViewModel;
private _completeSSOLoginViewModel?: CompleteSSOLoginViewModel;
private _loadViewModel?: SessionLoadViewModel;
private _loadViewModelSubscription?: () => void;
private _homeserver: string;
private _queriedHomeserver?: string;
private _abortHomeserverQueryTimeout?: () => void;
private _abortQueryOperation?: () => void;
private _hideHomeserver: boolean = false;
private _isBusy: boolean = false;
private _errorMessage: string = "";
constructor(options: Readonly<Options>) {
super(options); super(options);
const {ready, defaultHomeserver, createSessionContainer, loginToken} = options; const {ready, defaultHomeserver, loginToken} = options;
this._createSessionContainer = createSessionContainer;
this._ready = ready; this._ready = ready;
this._loginToken = loginToken; this._loginToken = loginToken;
this._sessionContainer = this._createSessionContainer(); this._client = new Client(this.platform);
this._loginOptions = null;
this._passwordLoginViewModel = null;
this._startSSOLoginViewModel = null;
this._completeSSOLoginViewModel = null;
this._loadViewModel = null;
this._loadViewModelSubscription = null;
this._homeserver = defaultHomeserver; this._homeserver = defaultHomeserver;
this._queriedHomeserver = null;
this._errorMessage = "";
this._hideHomeserver = false;
this._isBusy = false;
this._abortHomeserverQueryTimeout = null;
this._abortQueryOperation = null;
this._initViewModels(); this._initViewModels();
} }
get passwordLoginViewModel() { return this._passwordLoginViewModel; } get passwordLoginViewModel(): PasswordLoginViewModel {
get startSSOLoginViewModel() { return this._startSSOLoginViewModel; } return this._passwordLoginViewModel;
get completeSSOLoginViewModel(){ return this._completeSSOLoginViewModel; } }
get homeserver() { return this._homeserver; }
get resolvedHomeserver() { return this._loginOptions?.homeserver; }
get errorMessage() { return this._errorMessage; }
get showHomeserver() { return !this._hideHomeserver; }
get loadViewModel() {return this._loadViewModel; }
get isBusy() { return this._isBusy; }
get isFetchingLoginOptions() { return !!this._abortQueryOperation; }
goBack() { get startSSOLoginViewModel(): StartSSOLoginViewModel {
return this._startSSOLoginViewModel;
}
get completeSSOLoginViewModel(): CompleteSSOLoginViewModel {
return this._completeSSOLoginViewModel;
}
get homeserver(): string {
return this._homeserver;
}
get resolvedHomeserver(): string | undefined {
return this._loginOptions?.homeserver;
}
get errorMessage(): string {
return this._errorMessage;
}
get showHomeserver(): boolean {
return !this._hideHomeserver;
}
get loadViewModel(): SessionLoadViewModel {
return this._loadViewModel;
}
get isBusy(): boolean {
return this._isBusy;
}
get isFetchingLoginOptions(): boolean {
return !!this._abortQueryOperation;
}
goBack(): void {
this.navigation.push("session"); this.navigation.push("session");
} }
async _initViewModels() { private _initViewModels(): void {
if (this._loginToken) { if (this._loginToken) {
this._hideHomeserver = true; this._hideHomeserver = true;
this._completeSSOLoginViewModel = this.track(new CompleteSSOLoginViewModel( this._completeSSOLoginViewModel = this.track(new CompleteSSOLoginViewModel(
this.childOptions( this.childOptions(
{ {
sessionContainer: this._sessionContainer, client: this._client,
attemptLogin: loginMethod => this.attemptLogin(loginMethod), attemptLogin: (loginMethod: TokenLoginMethod) => this.attemptLogin(loginMethod),
loginToken: this._loginToken loginToken: this._loginToken
}))); })));
this.emitChange("completeSSOLoginViewModel"); this.emitChange("completeSSOLoginViewModel");
} }
else { else {
await this.queryHomeserver(); void this.queryHomeserver();
} }
} }
_showPasswordLogin() { private _showPasswordLogin(): void {
this._passwordLoginViewModel = this.track(new PasswordLoginViewModel( this._passwordLoginViewModel = this.track(new PasswordLoginViewModel(
this.childOptions({ this.childOptions({
loginOptions: this._loginOptions, loginOptions: this._loginOptions,
attemptLogin: loginMethod => this.attemptLogin(loginMethod) attemptLogin: (loginMethod: PasswordLoginMethod) => this.attemptLogin(loginMethod)
}))); })));
this.emitChange("passwordLoginViewModel"); this.emitChange("passwordLoginViewModel");
} }
_showSSOLogin() { private _showSSOLogin(): void {
this._startSSOLoginViewModel = this.track( this._startSSOLoginViewModel = this.track(
new StartSSOLoginViewModel(this.childOptions({loginOptions: this._loginOptions})) new StartSSOLoginViewModel(this.childOptions({loginOptions: this._loginOptions}))
); );
this.emitChange("startSSOLoginViewModel"); this.emitChange("startSSOLoginViewModel");
} }
_showError(message) { private _showError(message: string): void {
this._errorMessage = message; this._errorMessage = message;
this.emitChange("errorMessage"); this.emitChange("errorMessage");
} }
_setBusy(status) { private _setBusy(status: boolean): void {
this._isBusy = status; this._isBusy = status;
this._passwordLoginViewModel?.setBusy(status); this._passwordLoginViewModel?.setBusy(status);
this._startSSOLoginViewModel?.setBusy(status); this._startSSOLoginViewModel?.setBusy(status);
this.emitChange("isBusy"); this.emitChange("isBusy");
} }
async attemptLogin(loginMethod) { async attemptLogin(loginMethod: ILoginMethod): Promise<null> {
this._setBusy(true); this._setBusy(true);
this._sessionContainer.startWithLogin(loginMethod); void this._client.startWithLogin(loginMethod, {inspectAccountSetup: true});
const loadStatus = this._sessionContainer.loadStatus; const loadStatus = this._client.loadStatus;
const handle = loadStatus.waitFor(status => status !== LoadStatus.Login); const handle = loadStatus.waitFor((status: LoadStatus) => status !== LoadStatus.Login);
await handle.promise; await handle.promise;
this._setBusy(false); this._setBusy(false);
const status = loadStatus.get(); const status = loadStatus.get();
if (status === LoadStatus.LoginFailed) { if (status === LoadStatus.LoginFailed) {
return this._sessionContainer.loginFailure; return this._client.loginFailure;
} }
this._hideHomeserver = true; this._hideHomeserver = true;
this.emitChange("hideHomeserver"); this.emitChange("hideHomeserver");
this._disposeViewModels(); this._disposeViewModels();
this._createLoadViewModel(); void this._createLoadViewModel();
return null; return null;
} }
_createLoadViewModel() { private _createLoadViewModel(): void {
this._loadViewModelSubscription = this.disposeTracked(this._loadViewModelSubscription); this._loadViewModelSubscription = this.disposeTracked(this._loadViewModelSubscription);
this._loadViewModel = this.disposeTracked(this._loadViewModel); this._loadViewModel = this.disposeTracked(this._loadViewModel);
this._loadViewModel = this.track( this._loadViewModel = this.track(
new SessionLoadViewModel( new SessionLoadViewModel(
this.childOptions({ this.childOptions({
ready: (sessionContainer) => { ready: (client) => {
// make sure we don't delete the session in dispose when navigating away // make sure we don't delete the session in dispose when navigating away
this._sessionContainer = null; this._client = null;
this._ready(sessionContainer); this._ready(client);
}, },
sessionContainer: this._sessionContainer, client: this._client,
homeserver: this._homeserver homeserver: this._homeserver
}) })
) )
); );
this._loadViewModel.start(); void this._loadViewModel.start();
this.emitChange("loadViewModel"); this.emitChange("loadViewModel");
this._loadViewModelSubscription = this.track( this._loadViewModelSubscription = this.track(
this._loadViewModel.disposableOn("change", () => { this._loadViewModel.disposableOn("change", () => {
@ -151,22 +195,22 @@ export class LoginViewModel extends ViewModel {
); );
} }
_disposeViewModels() { private _disposeViewModels(): void {
this._startSSOLoginViewModel = this.disposeTracked(this._ssoLoginViewModel); this._startSSOLoginViewModel = this.disposeTracked(this._startSSOLoginViewModel);
this._passwordLoginViewModel = this.disposeTracked(this._passwordLoginViewModel); this._passwordLoginViewModel = this.disposeTracked(this._passwordLoginViewModel);
this._completeSSOLoginViewModel = this.disposeTracked(this._completeSSOLoginViewModel); this._completeSSOLoginViewModel = this.disposeTracked(this._completeSSOLoginViewModel);
this.emitChange("disposeViewModels"); this.emitChange("disposeViewModels");
} }
async setHomeserver(newHomeserver) { async setHomeserver(newHomeserver: string): Promise<void> {
this._homeserver = newHomeserver; this._homeserver = newHomeserver;
// clear everything set by queryHomeserver // clear everything set by queryHomeserver
this._loginOptions = null; this._loginOptions = undefined;
this._queriedHomeserver = null; this._queriedHomeserver = undefined;
this._showError(""); this._showError("");
this._disposeViewModels(); this._disposeViewModels();
this._abortQueryOperation = this.disposeTracked(this._abortQueryOperation); this._abortQueryOperation = this.disposeTracked(this._abortQueryOperation);
this.emitChange(); // multiple fields changing this.emitChange("loginViewModels"); // multiple fields changing
// also clear the timeout if it is still running // also clear the timeout if it is still running
this.disposeTracked(this._abortHomeserverQueryTimeout); this.disposeTracked(this._abortHomeserverQueryTimeout);
const timeout = this.clock.createTimeout(1000); const timeout = this.clock.createTimeout(1000);
@ -181,10 +225,10 @@ export class LoginViewModel extends ViewModel {
} }
} }
this._abortHomeserverQueryTimeout = this.disposeTracked(this._abortHomeserverQueryTimeout); this._abortHomeserverQueryTimeout = this.disposeTracked(this._abortHomeserverQueryTimeout);
this.queryHomeserver(); void this.queryHomeserver();
} }
async queryHomeserver() { async queryHomeserver(): Promise<void> {
// don't repeat a query we've just done // don't repeat a query we've just done
if (this._homeserver === this._queriedHomeserver || this._homeserver === "") { if (this._homeserver === this._queriedHomeserver || this._homeserver === "") {
return; return;
@ -200,7 +244,7 @@ export class LoginViewModel extends ViewModel {
// cancel ongoing query operation, if any // cancel ongoing query operation, if any
this._abortQueryOperation = this.disposeTracked(this._abortQueryOperation); this._abortQueryOperation = this.disposeTracked(this._abortQueryOperation);
try { try {
const queryOperation = this._sessionContainer.queryLogin(this._homeserver); const queryOperation = this._client.queryLogin(this._homeserver);
this._abortQueryOperation = this.track(() => queryOperation.abort()); this._abortQueryOperation = this.track(() => queryOperation.abort());
this.emitChange("isFetchingLoginOptions"); this.emitChange("isFetchingLoginOptions");
this._loginOptions = await queryOperation.result; this._loginOptions = await queryOperation.result;
@ -210,7 +254,7 @@ export class LoginViewModel extends ViewModel {
if (e.name === "AbortError") { if (e.name === "AbortError") {
return; //aborted, bail out return; //aborted, bail out
} else { } else {
this._loginOptions = null; this._loginOptions = undefined;
} }
} finally { } finally {
this._abortQueryOperation = this.disposeTracked(this._abortQueryOperation); this._abortQueryOperation = this.disposeTracked(this._abortQueryOperation);
@ -228,12 +272,22 @@ export class LoginViewModel extends ViewModel {
} }
} }
dispose() { dispose(): void {
super.dispose(); super.dispose();
if (this._sessionContainer) { if (this._client) {
// if we move away before we're done with initial sync // if we move away before we're done with initial sync
// delete the session // delete the session
this._sessionContainer.deleteSession(); void this._client.deleteSession();
} }
} }
} }
type ReadyFn = (client: Client) => void;
// TODO: move to Client.js when its converted to typescript.
type LoginOptions = {
homeserver: string;
password?: (username: string, password: string) => PasswordLoginMethod;
sso?: SSOLoginHelper;
token?: (loginToken: string) => TokenLoginMethod;
};

View file

@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {ViewModel} from "../ViewModel.js"; import {ViewModel} from "../ViewModel";
import {LoginFailure} from "../../matrix/SessionContainer.js"; import {LoginFailure} from "../../matrix/Client.js";
export class PasswordLoginViewModel extends ViewModel { export class PasswordLoginViewModel extends ViewModel {
constructor(options) { constructor(options) {

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {ViewModel} from "../ViewModel.js"; import {ViewModel} from "../ViewModel";
export class StartSSOLoginViewModel extends ViewModel{ export class StartSSOLoginViewModel extends ViewModel{
constructor(options) { constructor(options) {

View file

@ -14,29 +14,51 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {BaseObservableValue, ObservableValue} from "../../observable/ObservableValue.js"; import {BaseObservableValue, ObservableValue} from "../../observable/ObservableValue";
export class Navigation {
constructor(allowsChild) { type AllowsChild<T> = (parent: Segment<T> | undefined, child: Segment<T>) => boolean;
/**
* OptionalValue is basically stating that if SegmentType[type] = true:
* - Allow this type to be optional
* - Give it a default value of undefined
* - Also allow it to be true
* This lets us do:
* const s: Segment<SegmentType> = new Segment("create-room");
* instead of
* const s: Segment<SegmentType> = new Segment("create-room", undefined);
*/
export type OptionalValue<T> = T extends true? [(undefined | true)?]: [T];
export class Navigation<T extends object> {
private readonly _allowsChild: AllowsChild<T>;
private _path: Path<T>;
private readonly _observables: Map<keyof T, SegmentObservable<T>> = new Map();
private readonly _pathObservable: ObservableValue<Path<T>>;
constructor(allowsChild: AllowsChild<T>) {
this._allowsChild = allowsChild; this._allowsChild = allowsChild;
this._path = new Path([], allowsChild); this._path = new Path([], allowsChild);
this._observables = new Map();
this._pathObservable = new ObservableValue(this._path); this._pathObservable = new ObservableValue(this._path);
} }
get pathObservable() { get pathObservable(): ObservableValue<Path<T>> {
return this._pathObservable; return this._pathObservable;
} }
get path() { get path(): Path<T> {
return this._path; return this._path;
} }
push(type, value = undefined) { push<K extends keyof T>(type: K, ...value: OptionalValue<T[K]>): void {
return this.applyPath(this.path.with(new Segment(type, value))); const newPath = this.path.with(new Segment(type, ...value));
if (newPath) {
this.applyPath(newPath);
}
} }
applyPath(path) { applyPath(path: Path<T>): void {
// Path is not exported, so you can only create a Path through Navigation, // Path is not exported, so you can only create a Path through Navigation,
// so we assume it respects the allowsChild rules // so we assume it respects the allowsChild rules
const oldPath = this._path; const oldPath = this._path;
@ -60,7 +82,7 @@ export class Navigation {
this._pathObservable.set(this._path); this._pathObservable.set(this._path);
} }
observe(type) { observe(type: keyof T): SegmentObservable<T> {
let observable = this._observables.get(type); let observable = this._observables.get(type);
if (!observable) { if (!observable) {
observable = new SegmentObservable(this, type); observable = new SegmentObservable(this, type);
@ -69,9 +91,9 @@ export class Navigation {
return observable; return observable;
} }
pathFrom(segments) { pathFrom(segments: Segment<any>[]): Path<T> {
let parent; let parent: Segment<any> | undefined;
let i; let i: number;
for (i = 0; i < segments.length; i += 1) { for (i = 0; i < segments.length; i += 1) {
if (!this._allowsChild(parent, segments[i])) { if (!this._allowsChild(parent, segments[i])) {
return new Path(segments.slice(0, i), this._allowsChild); return new Path(segments.slice(0, i), this._allowsChild);
@ -81,12 +103,12 @@ export class Navigation {
return new Path(segments, this._allowsChild); return new Path(segments, this._allowsChild);
} }
segment(type, value) { segment<K extends keyof T>(type: K, ...value: OptionalValue<T[K]>): Segment<T> {
return new Segment(type, value); return new Segment(type, ...value);
} }
} }
function segmentValueEqual(a, b) { function segmentValueEqual<T>(a?: T[keyof T], b?: T[keyof T]): boolean {
if (a === b) { if (a === b) {
return true; return true;
} }
@ -103,24 +125,29 @@ function segmentValueEqual(a, b) {
return false; return false;
} }
export class Segment {
constructor(type, value) { export class Segment<T, K extends keyof T = any> {
this.type = type; public value: T[K];
this.value = value === undefined ? true : value;
constructor(public type: K, ...value: OptionalValue<T[K]>) {
this.value = (value[0] === undefined ? true : value[0]) as unknown as T[K];
} }
} }
class Path { class Path<T> {
constructor(segments = [], allowsChild) { private readonly _segments: Segment<T, any>[];
private readonly _allowsChild: AllowsChild<T>;
constructor(segments: Segment<T>[] = [], allowsChild: AllowsChild<T>) {
this._segments = segments; this._segments = segments;
this._allowsChild = allowsChild; this._allowsChild = allowsChild;
} }
clone() { clone(): Path<T> {
return new Path(this._segments.slice(), this._allowsChild); return new Path(this._segments.slice(), this._allowsChild);
} }
with(segment) { with(segment: Segment<T>): Path<T> | undefined {
let index = this._segments.length - 1; let index = this._segments.length - 1;
do { do {
if (this._allowsChild(this._segments[index], segment)) { if (this._allowsChild(this._segments[index], segment)) {
@ -132,10 +159,10 @@ class Path {
index -= 1; index -= 1;
} while(index >= -1); } while(index >= -1);
// allow -1 as well so we check if the segment is allowed as root // allow -1 as well so we check if the segment is allowed as root
return null; return undefined;
} }
until(type) { until(type: keyof T): Path<T> {
const index = this._segments.findIndex(s => s.type === type); const index = this._segments.findIndex(s => s.type === type);
if (index !== -1) { if (index !== -1) {
return new Path(this._segments.slice(0, index + 1), this._allowsChild) return new Path(this._segments.slice(0, index + 1), this._allowsChild)
@ -143,11 +170,11 @@ class Path {
return new Path([], this._allowsChild); return new Path([], this._allowsChild);
} }
get(type) { get(type: keyof T): Segment<T> | undefined {
return this._segments.find(s => s.type === type); return this._segments.find(s => s.type === type);
} }
replace(segment) { replace(segment: Segment<T>): Path<T> | undefined {
const index = this._segments.findIndex(s => s.type === segment.type); const index = this._segments.findIndex(s => s.type === segment.type);
if (index !== -1) { if (index !== -1) {
const parent = this._segments[index - 1]; const parent = this._segments[index - 1];
@ -160,10 +187,10 @@ class Path {
} }
} }
} }
return null; return undefined;
} }
get segments() { get segments(): Segment<T>[] {
return this._segments; return this._segments;
} }
} }
@ -172,43 +199,49 @@ class Path {
* custom observable so it always returns what is in navigation.path, even if we haven't emitted the change yet. * custom observable so it always returns what is in navigation.path, even if we haven't emitted the change yet.
* This ensures that observers of a segment can also read the most recent value of other segments. * This ensures that observers of a segment can also read the most recent value of other segments.
*/ */
class SegmentObservable extends BaseObservableValue { class SegmentObservable<T extends object> extends BaseObservableValue<T[keyof T] | undefined> {
constructor(navigation, type) { private readonly _navigation: Navigation<T>;
private _type: keyof T;
private _lastSetValue?: T[keyof T];
constructor(navigation: Navigation<T>, type: keyof T) {
super(); super();
this._navigation = navigation; this._navigation = navigation;
this._type = type; this._type = type;
this._lastSetValue = navigation.path.get(type)?.value; this._lastSetValue = navigation.path.get(type)?.value;
} }
get() { get(): T[keyof T] | undefined {
const path = this._navigation.path; const path = this._navigation.path;
const segment = path.get(this._type); const segment = path.get(this._type);
const value = segment?.value; const value = segment?.value;
return value; return value;
} }
emitIfChanged() { emitIfChanged(): void {
const newValue = this.get(); const newValue = this.get();
if (!segmentValueEqual(newValue, this._lastSetValue)) { if (!segmentValueEqual<T>(newValue, this._lastSetValue)) {
this._lastSetValue = newValue; this._lastSetValue = newValue;
this.emit(newValue); this.emit(newValue);
} }
} }
} }
export type {Path};
export function tests() { export function tests() {
function createMockNavigation() { function createMockNavigation() {
return new Navigation((parent, {type}) => { return new Navigation((parent, {type}) => {
switch (parent?.type) { switch (parent?.type) {
case undefined: case undefined:
return type === "1" || "2"; return type === "1" || type === "2";
case "1": case "1":
return type === "1.1"; return type === "1.1";
case "1.1": case "1.1":
return type === "1.1.1"; return type === "1.1.1";
case "2": case "2":
return type === "2.1" || "2.2"; return type === "2.1" || type === "2.2";
default: default:
return false; return false;
} }
@ -216,7 +249,7 @@ export function tests() {
} }
function observeTypes(nav, types) { function observeTypes(nav, types) {
const changes = []; const changes: {type:string, value:any}[] = [];
for (const type of types) { for (const type of types) {
nav.observe(type).subscribe(value => { nav.observe(type).subscribe(value => {
changes.push({type, value}); changes.push({type, value});
@ -225,6 +258,12 @@ export function tests() {
return changes; return changes;
} }
type SegmentType = {
"foo": number;
"bar": number;
"baz": number;
}
return { return {
"applying a path emits an event on the observable": assert => { "applying a path emits an event on the observable": assert => {
const nav = createMockNavigation(); const nav = createMockNavigation();
@ -242,18 +281,18 @@ export function tests() {
assert.equal(changes[1].value, 8); assert.equal(changes[1].value, 8);
}, },
"path.get": assert => { "path.get": assert => {
const path = new Path([new Segment("foo", 5), new Segment("bar", 6)], () => true); const path = new Path<SegmentType>([new Segment("foo", 5), new Segment("bar", 6)], () => true);
assert.equal(path.get("foo").value, 5); assert.equal(path.get("foo")!.value, 5);
assert.equal(path.get("bar").value, 6); assert.equal(path.get("bar")!.value, 6);
}, },
"path.replace success": assert => { "path.replace success": assert => {
const path = new Path([new Segment("foo", 5), new Segment("bar", 6)], () => true); const path = new Path<SegmentType>([new Segment("foo", 5), new Segment("bar", 6)], () => true);
const newPath = path.replace(new Segment("foo", 1)); const newPath = path.replace(new Segment("foo", 1));
assert.equal(newPath.get("foo").value, 1); assert.equal(newPath!.get("foo")!.value, 1);
assert.equal(newPath.get("bar").value, 6); assert.equal(newPath!.get("bar")!.value, 6);
}, },
"path.replace not found": assert => { "path.replace not found": assert => {
const path = new Path([new Segment("foo", 5), new Segment("bar", 6)], () => true); const path = new Path<SegmentType>([new Segment("foo", 5), new Segment("bar", 6)], () => true);
const newPath = path.replace(new Segment("baz", 1)); const newPath = path.replace(new Segment("baz", 1));
assert.equal(newPath, null); assert.equal(newPath, null);
} }

View file

@ -14,28 +14,55 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
export class URLRouter { import type {History} from "../../platform/web/dom/History.js";
constructor({history, navigation, parseUrlPath, stringifyPath}) { import type {Navigation, Segment, Path, OptionalValue} from "./Navigation";
import type {SubscriptionHandle} from "../../observable/BaseObservable";
type ParseURLPath<T> = (urlPath: string, currentNavPath: Path<T>, defaultSessionId?: string) => Segment<T>[];
type StringifyPath<T> = (path: Path<T>) => string;
export interface IURLRouter<T> {
attach(): void;
dispose(): void;
pushUrl(url: string): void;
tryRestoreLastUrl(): boolean;
urlForSegments(segments: Segment<T>[]): string | undefined;
urlForSegment<K extends keyof T>(type: K, ...value: OptionalValue<T[K]>): string | undefined;
urlUntilSegment(type: keyof T): string;
urlForPath(path: Path<T>): string;
openRoomActionUrl(roomId: string): string;
createSSOCallbackURL(): string;
normalizeUrl(): void;
}
export class URLRouter<T extends {session: string | boolean}> implements IURLRouter<T> {
private readonly _history: History;
private readonly _navigation: Navigation<T>;
private readonly _parseUrlPath: ParseURLPath<T>;
private readonly _stringifyPath: StringifyPath<T>;
private _subscription?: SubscriptionHandle;
private _pathSubscription?: SubscriptionHandle;
private _isApplyingUrl: boolean = false;
private _defaultSessionId?: string;
constructor(history: History, navigation: Navigation<T>, parseUrlPath: ParseURLPath<T>, stringifyPath: StringifyPath<T>) {
this._history = history; this._history = history;
this._navigation = navigation; this._navigation = navigation;
this._parseUrlPath = parseUrlPath; this._parseUrlPath = parseUrlPath;
this._stringifyPath = stringifyPath; this._stringifyPath = stringifyPath;
this._subscription = null;
this._pathSubscription = null;
this._isApplyingUrl = false;
this._defaultSessionId = this._getLastSessionId(); this._defaultSessionId = this._getLastSessionId();
} }
_getLastSessionId() { private _getLastSessionId(): string | undefined {
const navPath = this._urlAsNavPath(this._history.getLastUrl() || ""); const navPath = this._urlAsNavPath(this._history.getLastSessionUrl() || "");
const sessionId = navPath.get("session")?.value; const sessionId = navPath.get("session")?.value;
if (typeof sessionId === "string") { if (typeof sessionId === "string") {
return sessionId; return sessionId;
} }
return null; return undefined;
} }
attach() { attach(): void {
this._subscription = this._history.subscribe(url => this._applyUrl(url)); this._subscription = this._history.subscribe(url => this._applyUrl(url));
// subscribe to path before applying initial url // subscribe to path before applying initial url
// so redirects in _applyNavPathToHistory are reflected in url bar // so redirects in _applyNavPathToHistory are reflected in url bar
@ -43,12 +70,12 @@ export class URLRouter {
this._applyUrl(this._history.get()); this._applyUrl(this._history.get());
} }
dispose() { dispose(): void {
this._subscription = this._subscription(); if (this._subscription) { this._subscription = this._subscription(); }
this._pathSubscription = this._pathSubscription(); if (this._pathSubscription) { this._pathSubscription = this._pathSubscription(); }
} }
_applyNavPathToHistory(path) { private _applyNavPathToHistory(path: Path<T>): void {
const url = this.urlForPath(path); const url = this.urlForPath(path);
if (url !== this._history.get()) { if (url !== this._history.get()) {
if (this._isApplyingUrl) { if (this._isApplyingUrl) {
@ -60,7 +87,7 @@ export class URLRouter {
} }
} }
_applyNavPathToNavigation(navPath) { private _applyNavPathToNavigation(navPath: Path<T>): void {
// this will cause _applyNavPathToHistory to be called, // this will cause _applyNavPathToHistory to be called,
// so set a flag whether this request came from ourselves // so set a flag whether this request came from ourselves
// (in which case it is a redirect if the url does not match the current one) // (in which case it is a redirect if the url does not match the current one)
@ -69,22 +96,22 @@ export class URLRouter {
this._isApplyingUrl = false; this._isApplyingUrl = false;
} }
_urlAsNavPath(url) { private _urlAsNavPath(url: string): Path<T> {
const urlPath = this._history.urlAsPath(url); const urlPath = this._history.urlAsPath(url);
return this._navigation.pathFrom(this._parseUrlPath(urlPath, this._navigation.path, this._defaultSessionId)); return this._navigation.pathFrom(this._parseUrlPath(urlPath, this._navigation.path, this._defaultSessionId));
} }
_applyUrl(url) { private _applyUrl(url: string): void {
const navPath = this._urlAsNavPath(url); const navPath = this._urlAsNavPath(url);
this._applyNavPathToNavigation(navPath); this._applyNavPathToNavigation(navPath);
} }
pushUrl(url) { pushUrl(url: string): void {
this._history.pushUrl(url); this._history.pushUrl(url);
} }
tryRestoreLastUrl() { tryRestoreLastUrl(): boolean {
const lastNavPath = this._urlAsNavPath(this._history.getLastUrl() || ""); const lastNavPath = this._urlAsNavPath(this._history.getLastSessionUrl() || "");
if (lastNavPath.segments.length !== 0) { if (lastNavPath.segments.length !== 0) {
this._applyNavPathToNavigation(lastNavPath); this._applyNavPathToNavigation(lastNavPath);
return true; return true;
@ -92,8 +119,8 @@ export class URLRouter {
return false; return false;
} }
urlForSegments(segments) { urlForSegments(segments: Segment<T>[]): string | undefined {
let path = this._navigation.path; let path: Path<T> | undefined = this._navigation.path;
for (const segment of segments) { for (const segment of segments) {
path = path.with(segment); path = path.with(segment);
if (!path) { if (!path) {
@ -103,29 +130,29 @@ export class URLRouter {
return this.urlForPath(path); return this.urlForPath(path);
} }
urlForSegment(type, value) { urlForSegment<K extends keyof T>(type: K, ...value: OptionalValue<T[K]>): string | undefined {
return this.urlForSegments([this._navigation.segment(type, value)]); return this.urlForSegments([this._navigation.segment(type, ...value)]);
} }
urlUntilSegment(type) { urlUntilSegment(type: keyof T): string {
return this.urlForPath(this._navigation.path.until(type)); return this.urlForPath(this._navigation.path.until(type));
} }
urlForPath(path) { urlForPath(path: Path<T>): string {
return this._history.pathAsUrl(this._stringifyPath(path)); return this._history.pathAsUrl(this._stringifyPath(path));
} }
openRoomActionUrl(roomId) { openRoomActionUrl(roomId: string): string {
// not a segment to navigation knowns about, so append it manually // not a segment to navigation knowns about, so append it manually
const urlPath = `${this._stringifyPath(this._navigation.path.until("session"))}/open-room/${roomId}`; const urlPath = `${this._stringifyPath(this._navigation.path.until("session"))}/open-room/${roomId}`;
return this._history.pathAsUrl(urlPath); return this._history.pathAsUrl(urlPath);
} }
createSSOCallbackURL() { createSSOCallbackURL(): string {
return window.location.origin; return window.location.origin;
} }
normalizeUrl() { normalizeUrl(): void {
// Remove any queryParameters from the URL // Remove any queryParameters from the URL
// Gets rid of the loginToken after SSO // Gets rid of the loginToken after SSO
this._history.replaceUrlSilently(`${window.location.origin}/${window.location.hash}`); this._history.replaceUrlSilently(`${window.location.origin}/${window.location.hash}`);

View file

@ -14,25 +14,43 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {Navigation, Segment} from "./Navigation.js"; import {Navigation, Segment} from "./Navigation";
import {URLRouter} from "./URLRouter.js"; import {URLRouter} from "./URLRouter";
import type {Path, OptionalValue} from "./Navigation";
export function createNavigation() { export type SegmentType = {
"login": true;
"session": string | boolean;
"sso": string;
"logout": true;
"room": string;
"rooms": string[];
"settings": true;
"create-room": true;
"empty-grid-tile": number;
"lightbox": string;
"right-panel": true;
"details": true;
"members": true;
"member": string;
};
export function createNavigation(): Navigation<SegmentType> {
return new Navigation(allowsChild); return new Navigation(allowsChild);
} }
export function createRouter({history, navigation}) { export function createRouter({history, navigation}: {history: History, navigation: Navigation<SegmentType>}): URLRouter<SegmentType> {
return new URLRouter({history, navigation, stringifyPath, parseUrlPath}); return new URLRouter(history, navigation, parseUrlPath, stringifyPath);
} }
function allowsChild(parent, child) { function allowsChild(parent: Segment<SegmentType> | undefined, child: Segment<SegmentType>): boolean {
const {type} = child; const {type} = child;
switch (parent?.type) { switch (parent?.type) {
case undefined: case undefined:
// allowed root segments // allowed root segments
return type === "login" || type === "session" || type === "sso"; return type === "login" || type === "session" || type === "sso" || type === "logout";
case "session": case "session":
return type === "room" || type === "rooms" || type === "settings"; return type === "room" || type === "rooms" || type === "settings" || type === "create-room";
case "rooms": case "rooms":
// downside of the approach: both of these will control which tile is selected // downside of the approach: both of these will control which tile is selected
return type === "room" || type === "empty-grid-tile"; return type === "room" || type === "empty-grid-tile";
@ -45,8 +63,9 @@ function allowsChild(parent, child) {
} }
} }
export function removeRoomFromPath(path, roomId) { export function removeRoomFromPath(path: Path<SegmentType>, roomId: string): Path<SegmentType> | undefined {
const rooms = path.get("rooms"); let newPath: Path<SegmentType> | undefined = path;
const rooms = newPath.get("rooms");
let roomIdGridIndex = -1; let roomIdGridIndex = -1;
// first delete from rooms segment // first delete from rooms segment
if (rooms) { if (rooms) {
@ -54,22 +73,22 @@ export function removeRoomFromPath(path, roomId) {
if (roomIdGridIndex !== -1) { if (roomIdGridIndex !== -1) {
const idsWithoutRoom = rooms.value.slice(); const idsWithoutRoom = rooms.value.slice();
idsWithoutRoom[roomIdGridIndex] = ""; idsWithoutRoom[roomIdGridIndex] = "";
path = path.replace(new Segment("rooms", idsWithoutRoom)); newPath = newPath.replace(new Segment("rooms", idsWithoutRoom));
} }
} }
const room = path.get("room"); const room = newPath!.get("room");
// then from room (which occurs with or without rooms) // then from room (which occurs with or without rooms)
if (room && room.value === roomId) { if (room && room.value === roomId) {
if (roomIdGridIndex !== -1) { if (roomIdGridIndex !== -1) {
path = path.with(new Segment("empty-grid-tile", roomIdGridIndex)); newPath = newPath!.with(new Segment("empty-grid-tile", roomIdGridIndex));
} else { } else {
path = path.until("session"); newPath = newPath!.until("session");
} }
} }
return path; return newPath;
} }
function roomsSegmentWithRoom(rooms, roomId, path) { function roomsSegmentWithRoom(rooms: Segment<SegmentType, "rooms">, roomId: string, path: Path<SegmentType>): Segment<SegmentType, "rooms"> {
if(!rooms.value.includes(roomId)) { if(!rooms.value.includes(roomId)) {
const emptyGridTile = path.get("empty-grid-tile"); const emptyGridTile = path.get("empty-grid-tile");
const oldRoom = path.get("room"); const oldRoom = path.get("room");
@ -87,28 +106,28 @@ function roomsSegmentWithRoom(rooms, roomId, path) {
} }
} }
function pushRightPanelSegment(array, segment, value = true) { function pushRightPanelSegment<T extends keyof SegmentType>(array: Segment<SegmentType>[], segment: T, ...value: OptionalValue<SegmentType[T]>): void {
array.push(new Segment("right-panel")); array.push(new Segment("right-panel"));
array.push(new Segment(segment, value)); array.push(new Segment(segment, ...value));
} }
export function addPanelIfNeeded(navigation, path) { export function addPanelIfNeeded<T extends SegmentType>(navigation: Navigation<T>, path: Path<T>): Path<T> {
const segments = navigation.path.segments; const segments = navigation.path.segments;
const i = segments.findIndex(segment => segment.type === "right-panel"); const i = segments.findIndex(segment => segment.type === "right-panel");
let _path = path; let _path = path;
if (i !== -1) { if (i !== -1) {
_path = path.until("room"); _path = path.until("room");
_path = _path.with(segments[i]); _path = _path.with(segments[i])!;
_path = _path.with(segments[i + 1]); _path = _path.with(segments[i + 1])!;
} }
return _path; return _path;
} }
export function parseUrlPath(urlPath, currentNavPath, defaultSessionId) { export function parseUrlPath(urlPath: string, currentNavPath: Path<SegmentType>, defaultSessionId?: string): Segment<SegmentType>[] {
// substr(1) to take of initial / // substring(1) to take of initial /
const parts = urlPath.substr(1).split("/"); const parts = urlPath.substring(1).split("/");
const iterator = parts[Symbol.iterator](); const iterator = parts[Symbol.iterator]();
const segments = []; const segments: Segment<SegmentType>[] = [];
let next; let next;
while (!(next = iterator.next()).done) { while (!(next = iterator.next()).done) {
const type = next.value; const type = next.value;
@ -170,9 +189,9 @@ export function parseUrlPath(urlPath, currentNavPath, defaultSessionId) {
return segments; return segments;
} }
export function stringifyPath(path) { export function stringifyPath(path: Path<SegmentType>): string {
let urlPath = ""; let urlPath = "";
let prevSegment; let prevSegment: Segment<SegmentType> | undefined;
for (const segment of path.segments) { for (const segment of path.segments) {
switch (segment.type) { switch (segment.type) {
case "rooms": case "rooms":
@ -205,9 +224,15 @@ export function stringifyPath(path) {
} }
export function tests() { export function tests() {
function createEmptyPath() {
const nav: Navigation<SegmentType> = new Navigation(allowsChild);
const path = nav.pathFrom([]);
return path;
}
return { return {
"stringify grid url with focused empty tile": assert => { "stringify grid url with focused empty tile": assert => {
const nav = new Navigation(allowsChild); const nav: Navigation<SegmentType> = new Navigation(allowsChild);
const path = nav.pathFrom([ const path = nav.pathFrom([
new Segment("session", 1), new Segment("session", 1),
new Segment("rooms", ["a", "b", "c"]), new Segment("rooms", ["a", "b", "c"]),
@ -217,7 +242,7 @@ export function tests() {
assert.equal(urlPath, "/session/1/rooms/a,b,c/3"); assert.equal(urlPath, "/session/1/rooms/a,b,c/3");
}, },
"stringify grid url with focused room": assert => { "stringify grid url with focused room": assert => {
const nav = new Navigation(allowsChild); const nav: Navigation<SegmentType> = new Navigation(allowsChild);
const path = nav.pathFrom([ const path = nav.pathFrom([
new Segment("session", 1), new Segment("session", 1),
new Segment("rooms", ["a", "b", "c"]), new Segment("rooms", ["a", "b", "c"]),
@ -227,7 +252,7 @@ export function tests() {
assert.equal(urlPath, "/session/1/rooms/a,b,c/1"); assert.equal(urlPath, "/session/1/rooms/a,b,c/1");
}, },
"stringify url with right-panel and details segment": assert => { "stringify url with right-panel and details segment": assert => {
const nav = new Navigation(allowsChild); const nav: Navigation<SegmentType> = new Navigation(allowsChild);
const path = nav.pathFrom([ const path = nav.pathFrom([
new Segment("session", 1), new Segment("session", 1),
new Segment("rooms", ["a", "b", "c"]), new Segment("rooms", ["a", "b", "c"]),
@ -239,13 +264,15 @@ export function tests() {
assert.equal(urlPath, "/session/1/rooms/a,b,c/1/details"); assert.equal(urlPath, "/session/1/rooms/a,b,c/1/details");
}, },
"Parse loginToken query parameter into SSO segment": assert => { "Parse loginToken query parameter into SSO segment": assert => {
const segments = parseUrlPath("?loginToken=a1232aSD123"); const path = createEmptyPath();
const segments = parseUrlPath("?loginToken=a1232aSD123", path);
assert.equal(segments.length, 1); assert.equal(segments.length, 1);
assert.equal(segments[0].type, "sso"); assert.equal(segments[0].type, "sso");
assert.equal(segments[0].value, "a1232aSD123"); assert.equal(segments[0].value, "a1232aSD123");
}, },
"parse grid url path with focused empty tile": assert => { "parse grid url path with focused empty tile": assert => {
const segments = parseUrlPath("/session/1/rooms/a,b,c/3"); const path = createEmptyPath();
const segments = parseUrlPath("/session/1/rooms/a,b,c/3", path);
assert.equal(segments.length, 3); assert.equal(segments.length, 3);
assert.equal(segments[0].type, "session"); assert.equal(segments[0].type, "session");
assert.equal(segments[0].value, "1"); assert.equal(segments[0].value, "1");
@ -255,7 +282,8 @@ export function tests() {
assert.equal(segments[2].value, 3); assert.equal(segments[2].value, 3);
}, },
"parse grid url path with focused room": assert => { "parse grid url path with focused room": assert => {
const segments = parseUrlPath("/session/1/rooms/a,b,c/1"); const path = createEmptyPath();
const segments = parseUrlPath("/session/1/rooms/a,b,c/1", path);
assert.equal(segments.length, 3); assert.equal(segments.length, 3);
assert.equal(segments[0].type, "session"); assert.equal(segments[0].type, "session");
assert.equal(segments[0].value, "1"); assert.equal(segments[0].value, "1");
@ -265,7 +293,8 @@ export function tests() {
assert.equal(segments[2].value, "b"); assert.equal(segments[2].value, "b");
}, },
"parse empty grid url": assert => { "parse empty grid url": assert => {
const segments = parseUrlPath("/session/1/rooms/"); const path = createEmptyPath();
const segments = parseUrlPath("/session/1/rooms/", path);
assert.equal(segments.length, 3); assert.equal(segments.length, 3);
assert.equal(segments[0].type, "session"); assert.equal(segments[0].type, "session");
assert.equal(segments[0].value, "1"); assert.equal(segments[0].value, "1");
@ -275,7 +304,8 @@ export function tests() {
assert.equal(segments[2].value, 0); assert.equal(segments[2].value, 0);
}, },
"parse empty grid url with focus": assert => { "parse empty grid url with focus": assert => {
const segments = parseUrlPath("/session/1/rooms//1"); const path = createEmptyPath();
const segments = parseUrlPath("/session/1/rooms//1", path);
assert.equal(segments.length, 3); assert.equal(segments.length, 3);
assert.equal(segments[0].type, "session"); assert.equal(segments[0].type, "session");
assert.equal(segments[0].value, "1"); assert.equal(segments[0].value, "1");
@ -285,7 +315,7 @@ export function tests() {
assert.equal(segments[2].value, 1); assert.equal(segments[2].value, 1);
}, },
"parse open-room action replacing the current focused room": assert => { "parse open-room action replacing the current focused room": assert => {
const nav = new Navigation(allowsChild); const nav: Navigation<SegmentType> = new Navigation(allowsChild);
const path = nav.pathFrom([ const path = nav.pathFrom([
new Segment("session", 1), new Segment("session", 1),
new Segment("rooms", ["a", "b", "c"]), new Segment("rooms", ["a", "b", "c"]),
@ -301,7 +331,7 @@ export function tests() {
assert.equal(segments[2].value, "d"); assert.equal(segments[2].value, "d");
}, },
"parse open-room action changing focus to an existing room": assert => { "parse open-room action changing focus to an existing room": assert => {
const nav = new Navigation(allowsChild); const nav: Navigation<SegmentType> = new Navigation(allowsChild);
const path = nav.pathFrom([ const path = nav.pathFrom([
new Segment("session", 1), new Segment("session", 1),
new Segment("rooms", ["a", "b", "c"]), new Segment("rooms", ["a", "b", "c"]),
@ -317,7 +347,7 @@ export function tests() {
assert.equal(segments[2].value, "a"); assert.equal(segments[2].value, "a");
}, },
"parse open-room action changing focus to an existing room with details open": assert => { "parse open-room action changing focus to an existing room with details open": assert => {
const nav = new Navigation(allowsChild); const nav: Navigation<SegmentType> = new Navigation(allowsChild);
const path = nav.pathFrom([ const path = nav.pathFrom([
new Segment("session", 1), new Segment("session", 1),
new Segment("rooms", ["a", "b", "c"]), new Segment("rooms", ["a", "b", "c"]),
@ -339,7 +369,7 @@ export function tests() {
assert.equal(segments[4].value, true); assert.equal(segments[4].value, true);
}, },
"open-room action should only copy over previous segments if there are no parts after open-room": assert => { "open-room action should only copy over previous segments if there are no parts after open-room": assert => {
const nav = new Navigation(allowsChild); const nav: Navigation<SegmentType> = new Navigation(allowsChild);
const path = nav.pathFrom([ const path = nav.pathFrom([
new Segment("session", 1), new Segment("session", 1),
new Segment("rooms", ["a", "b", "c"]), new Segment("rooms", ["a", "b", "c"]),
@ -361,7 +391,7 @@ export function tests() {
assert.equal(segments[4].value, "foo"); assert.equal(segments[4].value, "foo");
}, },
"parse open-room action setting a room in an empty tile": assert => { "parse open-room action setting a room in an empty tile": assert => {
const nav = new Navigation(allowsChild); const nav: Navigation<SegmentType> = new Navigation(allowsChild);
const path = nav.pathFrom([ const path = nav.pathFrom([
new Segment("session", 1), new Segment("session", 1),
new Segment("rooms", ["a", "b", "c"]), new Segment("rooms", ["a", "b", "c"]),
@ -377,82 +407,83 @@ export function tests() {
assert.equal(segments[2].value, "d"); assert.equal(segments[2].value, "d");
}, },
"parse session url path without id": assert => { "parse session url path without id": assert => {
const segments = parseUrlPath("/session"); const path = createEmptyPath();
const segments = parseUrlPath("/session", path);
assert.equal(segments.length, 1); assert.equal(segments.length, 1);
assert.equal(segments[0].type, "session"); assert.equal(segments[0].type, "session");
assert.strictEqual(segments[0].value, true); assert.strictEqual(segments[0].value, true);
}, },
"remove active room from grid path turns it into empty tile": assert => { "remove active room from grid path turns it into empty tile": assert => {
const nav = new Navigation(allowsChild); const nav: Navigation<SegmentType> = new Navigation(allowsChild);
const path = nav.pathFrom([ const path = nav.pathFrom([
new Segment("session", 1), new Segment("session", 1),
new Segment("rooms", ["a", "b", "c"]), new Segment("rooms", ["a", "b", "c"]),
new Segment("room", "b") new Segment("room", "b")
]); ]);
const newPath = removeRoomFromPath(path, "b"); const newPath = removeRoomFromPath(path, "b");
assert.equal(newPath.segments.length, 3); assert.equal(newPath?.segments.length, 3);
assert.equal(newPath.segments[0].type, "session"); assert.equal(newPath?.segments[0].type, "session");
assert.equal(newPath.segments[0].value, 1); assert.equal(newPath?.segments[0].value, 1);
assert.equal(newPath.segments[1].type, "rooms"); assert.equal(newPath?.segments[1].type, "rooms");
assert.deepEqual(newPath.segments[1].value, ["a", "", "c"]); assert.deepEqual(newPath?.segments[1].value, ["a", "", "c"]);
assert.equal(newPath.segments[2].type, "empty-grid-tile"); assert.equal(newPath?.segments[2].type, "empty-grid-tile");
assert.equal(newPath.segments[2].value, 1); assert.equal(newPath?.segments[2].value, 1);
}, },
"remove inactive room from grid path": assert => { "remove inactive room from grid path": assert => {
const nav = new Navigation(allowsChild); const nav: Navigation<SegmentType> = new Navigation(allowsChild);
const path = nav.pathFrom([ const path = nav.pathFrom([
new Segment("session", 1), new Segment("session", 1),
new Segment("rooms", ["a", "b", "c"]), new Segment("rooms", ["a", "b", "c"]),
new Segment("room", "b") new Segment("room", "b")
]); ]);
const newPath = removeRoomFromPath(path, "a"); const newPath = removeRoomFromPath(path, "a");
assert.equal(newPath.segments.length, 3); assert.equal(newPath?.segments.length, 3);
assert.equal(newPath.segments[0].type, "session"); assert.equal(newPath?.segments[0].type, "session");
assert.equal(newPath.segments[0].value, 1); assert.equal(newPath?.segments[0].value, 1);
assert.equal(newPath.segments[1].type, "rooms"); assert.equal(newPath?.segments[1].type, "rooms");
assert.deepEqual(newPath.segments[1].value, ["", "b", "c"]); assert.deepEqual(newPath?.segments[1].value, ["", "b", "c"]);
assert.equal(newPath.segments[2].type, "room"); assert.equal(newPath?.segments[2].type, "room");
assert.equal(newPath.segments[2].value, "b"); assert.equal(newPath?.segments[2].value, "b");
}, },
"remove inactive room from grid path with empty tile": assert => { "remove inactive room from grid path with empty tile": assert => {
const nav = new Navigation(allowsChild); const nav: Navigation<SegmentType> = new Navigation(allowsChild);
const path = nav.pathFrom([ const path = nav.pathFrom([
new Segment("session", 1), new Segment("session", 1),
new Segment("rooms", ["a", "b", ""]), new Segment("rooms", ["a", "b", ""]),
new Segment("empty-grid-tile", 3) new Segment("empty-grid-tile", 3)
]); ]);
const newPath = removeRoomFromPath(path, "b"); const newPath = removeRoomFromPath(path, "b");
assert.equal(newPath.segments.length, 3); assert.equal(newPath?.segments.length, 3);
assert.equal(newPath.segments[0].type, "session"); assert.equal(newPath?.segments[0].type, "session");
assert.equal(newPath.segments[0].value, 1); assert.equal(newPath?.segments[0].value, 1);
assert.equal(newPath.segments[1].type, "rooms"); assert.equal(newPath?.segments[1].type, "rooms");
assert.deepEqual(newPath.segments[1].value, ["a", "", ""]); assert.deepEqual(newPath?.segments[1].value, ["a", "", ""]);
assert.equal(newPath.segments[2].type, "empty-grid-tile"); assert.equal(newPath?.segments[2].type, "empty-grid-tile");
assert.equal(newPath.segments[2].value, 3); assert.equal(newPath?.segments[2].value, 3);
}, },
"remove active room": assert => { "remove active room": assert => {
const nav = new Navigation(allowsChild); const nav: Navigation<SegmentType> = new Navigation(allowsChild);
const path = nav.pathFrom([ const path = nav.pathFrom([
new Segment("session", 1), new Segment("session", 1),
new Segment("room", "b") new Segment("room", "b")
]); ]);
const newPath = removeRoomFromPath(path, "b"); const newPath = removeRoomFromPath(path, "b");
assert.equal(newPath.segments.length, 1); assert.equal(newPath?.segments.length, 1);
assert.equal(newPath.segments[0].type, "session"); assert.equal(newPath?.segments[0].type, "session");
assert.equal(newPath.segments[0].value, 1); assert.equal(newPath?.segments[0].value, 1);
}, },
"remove inactive room doesn't do anything": assert => { "remove inactive room doesn't do anything": assert => {
const nav = new Navigation(allowsChild); const nav: Navigation<SegmentType> = new Navigation(allowsChild);
const path = nav.pathFrom([ const path = nav.pathFrom([
new Segment("session", 1), new Segment("session", 1),
new Segment("room", "b") new Segment("room", "b")
]); ]);
const newPath = removeRoomFromPath(path, "a"); const newPath = removeRoomFromPath(path, "a");
assert.equal(newPath.segments.length, 2); assert.equal(newPath?.segments.length, 2);
assert.equal(newPath.segments[0].type, "session"); assert.equal(newPath?.segments[0].type, "session");
assert.equal(newPath.segments[0].value, 1); assert.equal(newPath?.segments[0].value, 1);
assert.equal(newPath.segments[1].type, "room"); assert.equal(newPath?.segments[1].type, "room");
assert.equal(newPath.segments[1].value, "b"); assert.equal(newPath?.segments[1].value, "b");
}, },
} }

65
src/domain/rageshake.ts Normal file
View file

@ -0,0 +1,65 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
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.
*/
import type {BlobHandle} from "../platform/web/dom/BlobHandle";
import type {RequestFunction} from "../platform/types/types";
// see https://github.com/matrix-org/rageshake#readme
type RageshakeData = {
// A textual description of the problem. Included in the details.log.gz file.
text: string | undefined;
// Application user-agent. Included in the details.log.gz file.
userAgent: string;
// Identifier for the application (eg 'riot-web'). Should correspond to a mapping configured in the configuration file for github issue reporting to work.
app: string;
// Application version. Included in the details.log.gz file.
version: string;
// Label to attach to the github issue, and include in the details file.
label: string | undefined;
};
export async function submitLogsToRageshakeServer(data: RageshakeData, logsBlob: BlobHandle, submitUrl: string, request: RequestFunction): Promise<void> {
const formData = new Map<string, string | {name: string, blob: BlobHandle}>();
if (data.text) {
formData.set("text", data.text);
}
formData.set("user_agent", data.userAgent);
formData.set("app", data.app);
formData.set("version", data.version);
if (data.label) {
formData.set("label", data.label);
}
formData.set("file", {name: "logs.json", blob: logsBlob});
const headers: Map<string, string> = new Map();
headers.set("Accept", "application/json");
const result = request(submitUrl, {
method: "POST",
body: formData,
headers
});
let response;
try {
response = await result.response();
} catch (err) {
throw new Error(`Could not submit logs to ${submitUrl}, got error ${err.message}`);
}
const {status, body} = response;
if (status < 200 || status >= 300) {
throw new Error(`Could not submit logs to ${submitUrl}, got status code ${status} with body ${body}`);
}
// we don't bother with reading report_url from the body as the rageshake server doesn't always return it
// and would have to have CORS setup properly for us to be able to read it.
}

View file

@ -0,0 +1,144 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
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.
*/
import {ViewModel} from "../ViewModel";
import {imageToInfo} from "./common.js";
import {RoomType} from "../../matrix/room/common";
export class CreateRoomViewModel extends ViewModel {
constructor(options) {
super(options);
const {session} = options;
this._session = session;
this._name = undefined;
this._topic = undefined;
this._roomAlias = undefined;
this._isPublic = false;
this._isEncrypted = true;
this._isAdvancedShown = false;
this._isFederationDisabled = false;
this._avatarScaledBlob = undefined;
this._avatarFileName = undefined;
this._avatarInfo = undefined;
}
get isPublic() { return this._isPublic; }
get isEncrypted() { return this._isEncrypted; }
get canCreate() { return !!this._name; }
avatarUrl() { return this._avatarScaledBlob.url; }
get avatarTitle() { return this._name; }
get avatarLetter() { return ""; }
get avatarColorNumber() { return 0; }
get hasAvatar() { return !!this._avatarScaledBlob; }
get isFederationDisabled() { return this._isFederationDisabled; }
get isAdvancedShown() { return this._isAdvancedShown; }
setName(name) {
this._name = name;
this.emitChange("canCreate");
}
setRoomAlias(roomAlias) {
this._roomAlias = roomAlias;
}
setTopic(topic) {
this._topic = topic;
}
setPublic(isPublic) {
this._isPublic = isPublic;
this.emitChange("isPublic");
}
setEncrypted(isEncrypted) {
this._isEncrypted = isEncrypted;
this.emitChange("isEncrypted");
}
setFederationDisabled(disable) {
this._isFederationDisabled = disable;
this.emitChange("isFederationDisabled");
}
toggleAdvancedShown() {
this._isAdvancedShown = !this._isAdvancedShown;
this.emitChange("isAdvancedShown");
}
create() {
let avatar;
if (this._avatarScaledBlob) {
avatar = {
info: this._avatarInfo,
name: this._avatarFileName,
blob: this._avatarScaledBlob
}
}
const roomBeingCreated = this._session.createRoom({
type: this.isPublic ? RoomType.Public : RoomType.Private,
name: this._name ?? undefined,
topic: this._topic ?? undefined,
isEncrypted: !this.isPublic && this._isEncrypted,
isFederationDisabled: this._isFederationDisabled,
alias: this.isPublic ? ensureAliasIsLocalPart(this._roomAlias) : undefined,
avatar,
});
this.navigation.push("room", roomBeingCreated.id);
}
async selectAvatar() {
if (!this.platform.hasReadPixelPermission()) {
alert("Please allow canvas image data access, so we can scale your images down.");
return;
}
if (this._avatarScaledBlob) {
this._avatarScaledBlob.dispose();
}
this._avatarScaledBlob = undefined;
this._avatarFileName = undefined;
this._avatarInfo = undefined;
const file = await this.platform.openFile("image/*");
if (!file || !file.blob.mimeType.startsWith("image/")) {
// allow to clear the avatar by not selecting an image
this.emitChange("hasAvatar");
return;
}
let image = await this.platform.loadImage(file.blob);
const limit = 800;
if (image.maxDimension > limit) {
const scaledImage = await image.scale(limit);
image.dispose();
image = scaledImage;
}
this._avatarScaledBlob = image.blob;
this._avatarInfo = imageToInfo(image);
this._avatarFileName = file.name;
this.emitChange("hasAvatar");
}
}
function ensureAliasIsLocalPart(roomAliasLocalPart) {
if (roomAliasLocalPart.startsWith("#")) {
roomAliasLocalPart = roomAliasLocalPart.substr(1);
}
const colonIdx = roomAliasLocalPart.indexOf(":");
if (colonIdx !== -1) {
roomAliasLocalPart = roomAliasLocalPart.substr(0, colonIdx);
}
return roomAliasLocalPart;
}

View file

@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {ViewModel} from "../ViewModel.js"; import {ViewModel} from "../ViewModel";
import {addPanelIfNeeded} from "../navigation/index.js"; import {addPanelIfNeeded} from "../navigation/index";
function dedupeSparse(roomIds) { function dedupeSparse(roomIds) {
return roomIds.map((id, idx) => { return roomIds.map((id, idx) => {
@ -185,8 +185,8 @@ export class RoomGridViewModel extends ViewModel {
} }
} }
import {createNavigation} from "../navigation/index.js"; import {createNavigation} from "../navigation/index";
import {ObservableValue} from "../../observable/ObservableValue.js"; import {ObservableValue} from "../../observable/ObservableValue";
export function tests() { export function tests() {
class RoomVMMock { class RoomVMMock {

View file

@ -14,7 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {ObservableValue} from "../../observable/ObservableValue.js"; import {ObservableValue} from "../../observable/ObservableValue";
import {RoomStatus} from "../../matrix/room/common";
/** /**
Depending on the status of a room (invited, joined, archived, or none), Depending on the status of a room (invited, joined, archived, or none),
@ -34,11 +35,11 @@ the now transferred child view model.
This is also why there is an explicit initialize method, see comment there. This is also why there is an explicit initialize method, see comment there.
*/ */
export class RoomViewModelObservable extends ObservableValue { export class RoomViewModelObservable extends ObservableValue {
constructor(sessionViewModel, roomId) { constructor(sessionViewModel, roomIdOrLocalId) {
super(null); super(null);
this._statusSubscription = null; this._statusSubscription = null;
this._sessionViewModel = sessionViewModel; this._sessionViewModel = sessionViewModel;
this.id = roomId; this.id = roomIdOrLocalId;
} }
/** /**
@ -48,7 +49,7 @@ export class RoomViewModelObservable extends ObservableValue {
are called in that case. are called in that case.
*/ */
async initialize() { async initialize() {
const {session} = this._sessionViewModel._sessionContainer; const {session} = this._sessionViewModel._client;
const statusObservable = await session.observeRoomStatus(this.id); const statusObservable = await session.observeRoomStatus(this.id);
this.set(await this._statusToViewModel(statusObservable.get())); this.set(await this._statusToViewModel(statusObservable.get()));
this._statusSubscription = statusObservable.subscribe(async status => { this._statusSubscription = statusObservable.subscribe(async status => {
@ -59,11 +60,21 @@ export class RoomViewModelObservable extends ObservableValue {
} }
async _statusToViewModel(status) { async _statusToViewModel(status) {
if (status.invited) { if (status & RoomStatus.Replaced) {
if (status & RoomStatus.BeingCreated) {
const {session} = this._sessionViewModel._client;
const roomBeingCreated = session.roomsBeingCreated.get(this.id);
this._sessionViewModel.notifyRoomReplaced(roomBeingCreated.id, roomBeingCreated.roomId);
} else {
throw new Error("Don't know how to replace a room with this status: " + (status ^ RoomStatus.Replaced));
}
} else if (status & RoomStatus.BeingCreated) {
return this._sessionViewModel._createRoomBeingCreatedViewModel(this.id);
} else if (status & RoomStatus.Invited) {
return this._sessionViewModel._createInviteViewModel(this.id); return this._sessionViewModel._createInviteViewModel(this.id);
} else if (status.joined) { } else if (status & RoomStatus.Joined) {
return this._sessionViewModel._createRoomViewModel(this.id); return this._sessionViewModel._createRoomViewModelInstance(this.id);
} else if (status.archived) { } else if (status & RoomStatus.Archived) {
return await this._sessionViewModel._createArchivedRoomViewModel(this.id); return await this._sessionViewModel._createArchivedRoomViewModel(this.id);
} else { } else {
return this._sessionViewModel._createUnknownRoomViewModel(this.id); return this._sessionViewModel._createUnknownRoomViewModel(this.id);

View file

@ -14,9 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {ViewModel} from "../ViewModel.js"; import {ViewModel} from "../ViewModel";
import {createEnum} from "../../utils/enum.js"; import {createEnum} from "../../utils/enum";
import {ConnectionStatus} from "../../matrix/net/Reconnector.js"; import {ConnectionStatus} from "../../matrix/net/Reconnector";
import {SyncStatus} from "../../matrix/Sync.js"; import {SyncStatus} from "../../matrix/Sync.js";
const SessionStatus = createEnum( const SessionStatus = createEnum(
@ -36,7 +36,7 @@ export class SessionStatusViewModel extends ViewModel {
this._reconnector = reconnector; this._reconnector = reconnector;
this._status = this._calculateState(reconnector.connectionStatus.get(), sync.status.get()); this._status = this._calculateState(reconnector.connectionStatus.get(), sync.status.get());
this._session = session; this._session = session;
this._setupSessionBackupUrl = this.urlCreator.urlForSegment("settings"); this._setupKeyBackupUrl = this.urlCreator.urlForSegment("settings");
this._dismissSecretStorage = false; this._dismissSecretStorage = false;
} }
@ -44,17 +44,17 @@ export class SessionStatusViewModel extends ViewModel {
const update = () => this._updateStatus(); const update = () => this._updateStatus();
this.track(this._sync.status.subscribe(update)); this.track(this._sync.status.subscribe(update));
this.track(this._reconnector.connectionStatus.subscribe(update)); this.track(this._reconnector.connectionStatus.subscribe(update));
this.track(this._session.needsSessionBackup.subscribe(() => { this.track(this._session.needsKeyBackup.subscribe(() => {
this.emitChange(); this.emitChange();
})); }));
} }
get setupSessionBackupUrl () { get setupKeyBackupUrl () {
return this._setupSessionBackupUrl; return this._setupKeyBackupUrl;
} }
get isShown() { get isShown() {
return (this._session.needsSessionBackup.get() && !this._dismissSecretStorage) || this._status !== SessionStatus.Syncing; return (this._session.needsKeyBackup.get() && !this._dismissSecretStorage) || this._status !== SessionStatus.Syncing;
} }
get statusLabel() { get statusLabel() {
@ -70,7 +70,7 @@ export class SessionStatusViewModel extends ViewModel {
case SessionStatus.SyncError: case SessionStatus.SyncError:
return this.i18n`Sync failed because of ${this._sync.error}`; return this.i18n`Sync failed because of ${this._sync.error}`;
} }
if (this._session.needsSessionBackup.get()) { if (this._session.needsKeyBackup.get()) {
return this.i18n`Set up session backup to decrypt older messages.`; return this.i18n`Set up session backup to decrypt older messages.`;
} }
return ""; return "";
@ -135,7 +135,7 @@ export class SessionStatusViewModel extends ViewModel {
get isSecretStorageShown() { get isSecretStorageShown() {
// TODO: we need a model here where we can have multiple messages queued up and their buttons don't bleed into each other. // TODO: we need a model here where we can have multiple messages queued up and their buttons don't bleed into each other.
return this._status === SessionStatus.Syncing && this._session.needsSessionBackup.get() && !this._dismissSecretStorage; return this._status === SessionStatus.Syncing && this._session.needsKeyBackup.get() && !this._dismissSecretStorage;
} }
get canDismiss() { get canDismiss() {

View file

@ -19,31 +19,31 @@ import {LeftPanelViewModel} from "./leftpanel/LeftPanelViewModel.js";
import {RoomViewModel} from "./room/RoomViewModel.js"; import {RoomViewModel} from "./room/RoomViewModel.js";
import {UnknownRoomViewModel} from "./room/UnknownRoomViewModel.js"; import {UnknownRoomViewModel} from "./room/UnknownRoomViewModel.js";
import {InviteViewModel} from "./room/InviteViewModel.js"; import {InviteViewModel} from "./room/InviteViewModel.js";
import {RoomBeingCreatedViewModel} from "./room/RoomBeingCreatedViewModel.js";
import {LightboxViewModel} from "./room/LightboxViewModel.js"; import {LightboxViewModel} from "./room/LightboxViewModel.js";
import {SessionStatusViewModel} from "./SessionStatusViewModel.js"; import {SessionStatusViewModel} from "./SessionStatusViewModel.js";
import {RoomGridViewModel} from "./RoomGridViewModel.js"; import {RoomGridViewModel} from "./RoomGridViewModel.js";
import {SettingsViewModel} from "./settings/SettingsViewModel.js"; import {SettingsViewModel} from "./settings/SettingsViewModel.js";
import {ViewModel} from "../ViewModel.js"; import {CreateRoomViewModel} from "./CreateRoomViewModel.js";
import {ViewModel} from "../ViewModel";
import {RoomViewModelObservable} from "./RoomViewModelObservable.js"; import {RoomViewModelObservable} from "./RoomViewModelObservable.js";
import {RightPanelViewModel} from "./rightpanel/RightPanelViewModel.js"; import {RightPanelViewModel} from "./rightpanel/RightPanelViewModel.js";
export class SessionViewModel extends ViewModel { export class SessionViewModel extends ViewModel {
constructor(options) { constructor(options) {
super(options); super(options);
const {sessionContainer} = options; const {client} = options;
this._sessionContainer = this.track(sessionContainer); this._client = this.track(client);
this._sessionStatusViewModel = this.track(new SessionStatusViewModel(this.childOptions({ this._sessionStatusViewModel = this.track(new SessionStatusViewModel(this.childOptions({
sync: sessionContainer.sync, sync: client.sync,
reconnector: sessionContainer.reconnector, reconnector: client.reconnector,
session: sessionContainer.session, session: client.session,
})));
this._leftPanelViewModel = this.track(new LeftPanelViewModel(this.childOptions({
invites: this._sessionContainer.session.invites,
rooms: this._sessionContainer.session.rooms
}))); })));
this._leftPanelViewModel = this.track(new LeftPanelViewModel(this.childOptions({session: this._client.session})));
this._settingsViewModel = null; this._settingsViewModel = null;
this._roomViewModelObservable = null; this._roomViewModelObservable = null;
this._gridViewModel = null; this._gridViewModel = null;
this._createRoomViewModel = null;
this._setupNavigation(); this._setupNavigation();
} }
@ -75,6 +75,12 @@ export class SessionViewModel extends ViewModel {
})); }));
this._updateSettings(settings.get()); this._updateSettings(settings.get());
const createRoom = this.navigation.observe("create-room");
this.track(createRoom.subscribe(createRoomOpen => {
this._updateCreateRoom(createRoomOpen);
}));
this._updateCreateRoom(createRoom.get());
const lightbox = this.navigation.observe("lightbox"); const lightbox = this.navigation.observe("lightbox");
this.track(lightbox.subscribe(eventId => { this.track(lightbox.subscribe(eventId => {
this._updateLightbox(eventId); this._updateLightbox(eventId);
@ -88,7 +94,7 @@ export class SessionViewModel extends ViewModel {
} }
get id() { get id() {
return this._sessionContainer.sessionId; return this._client.sessionId;
} }
start() { start() {
@ -96,7 +102,7 @@ export class SessionViewModel extends ViewModel {
} }
get activeMiddleViewModel() { get activeMiddleViewModel() {
return this._roomViewModelObservable?.get() || this._gridViewModel || this._settingsViewModel; return this._roomViewModelObservable?.get() || this._gridViewModel || this._settingsViewModel || this._createRoomViewModel;
} }
get roomGridViewModel() { get roomGridViewModel() {
@ -119,11 +125,14 @@ export class SessionViewModel extends ViewModel {
return this._roomViewModelObservable?.get(); return this._roomViewModelObservable?.get();
} }
get rightPanelViewModel() { get rightPanelViewModel() {
return this._rightPanelViewModel; return this._rightPanelViewModel;
} }
get createRoomViewModel() {
return this._createRoomViewModel;
}
_updateGrid(roomIds) { _updateGrid(roomIds) {
const changed = !(this._gridViewModel && roomIds); const changed = !(this._gridViewModel && roomIds);
const currentRoomId = this.navigation.path.get("room"); const currentRoomId = this.navigation.path.get("room");
@ -162,8 +171,8 @@ export class SessionViewModel extends ViewModel {
} }
} }
_createRoomViewModel(roomId) { _createRoomViewModelInstance(roomId) {
const room = this._sessionContainer.session.rooms.get(roomId); const room = this._client.session.rooms.get(roomId);
if (room) { if (room) {
const roomVM = new RoomViewModel(this.childOptions({room})); const roomVM = new RoomViewModel(this.childOptions({room}));
roomVM.load(); roomVM.load();
@ -175,12 +184,12 @@ export class SessionViewModel extends ViewModel {
_createUnknownRoomViewModel(roomIdOrAlias) { _createUnknownRoomViewModel(roomIdOrAlias) {
return new UnknownRoomViewModel(this.childOptions({ return new UnknownRoomViewModel(this.childOptions({
roomIdOrAlias, roomIdOrAlias,
session: this._sessionContainer.session, session: this._client.session,
})); }));
} }
async _createArchivedRoomViewModel(roomId) { async _createArchivedRoomViewModel(roomId) {
const room = await this._sessionContainer.session.loadArchivedRoom(roomId); const room = await this._client.session.loadArchivedRoom(roomId);
if (room) { if (room) {
const roomVM = new RoomViewModel(this.childOptions({room})); const roomVM = new RoomViewModel(this.childOptions({room}));
roomVM.load(); roomVM.load();
@ -190,11 +199,22 @@ export class SessionViewModel extends ViewModel {
} }
_createInviteViewModel(roomId) { _createInviteViewModel(roomId) {
const invite = this._sessionContainer.session.invites.get(roomId); const invite = this._client.session.invites.get(roomId);
if (invite) { if (invite) {
return new InviteViewModel(this.childOptions({ return new InviteViewModel(this.childOptions({
invite, invite,
mediaRepository: this._sessionContainer.session.mediaRepository, mediaRepository: this._client.session.mediaRepository,
}));
}
return null;
}
_createRoomBeingCreatedViewModel(localId) {
const roomBeingCreated = this._client.session.roomsBeingCreated.get(localId);
if (roomBeingCreated) {
return new RoomBeingCreatedViewModel(this.childOptions({
roomBeingCreated,
mediaRepository: this._client.session.mediaRepository,
})); }));
} }
return null; return null;
@ -230,13 +250,23 @@ export class SessionViewModel extends ViewModel {
} }
if (settingsOpen) { if (settingsOpen) {
this._settingsViewModel = this.track(new SettingsViewModel(this.childOptions({ this._settingsViewModel = this.track(new SettingsViewModel(this.childOptions({
session: this._sessionContainer.session, client: this._client,
}))); })));
this._settingsViewModel.load(); this._settingsViewModel.load();
} }
this.emitChange("activeMiddleViewModel"); this.emitChange("activeMiddleViewModel");
} }
_updateCreateRoom(createRoomOpen) {
if (this._createRoomViewModel) {
this._createRoomViewModel = this.disposeTracked(this._createRoomViewModel);
}
if (createRoomOpen) {
this._createRoomViewModel = this.track(new CreateRoomViewModel(this.childOptions({session: this._client.session})));
}
this.emitChange("activeMiddleViewModel");
}
_updateLightbox(eventId) { _updateLightbox(eventId) {
if (this._lightboxViewModel) { if (this._lightboxViewModel) {
this._lightboxViewModel = this.disposeTracked(this._lightboxViewModel); this._lightboxViewModel = this.disposeTracked(this._lightboxViewModel);
@ -254,7 +284,7 @@ export class SessionViewModel extends ViewModel {
_roomFromNavigation() { _roomFromNavigation() {
const roomId = this.navigation.path.get("room")?.value; const roomId = this.navigation.path.get("room")?.value;
const room = this._sessionContainer.session.rooms.get(roomId); const room = this._client.session.rooms.get(roomId);
return room; return room;
} }
@ -263,9 +293,12 @@ export class SessionViewModel extends ViewModel {
const enable = !!this.navigation.path.get("right-panel")?.value; const enable = !!this.navigation.path.get("right-panel")?.value;
if (enable) { if (enable) {
const room = this._roomFromNavigation(); const room = this._roomFromNavigation();
this._rightPanelViewModel = this.track(new RightPanelViewModel(this.childOptions({room}))); this._rightPanelViewModel = this.track(new RightPanelViewModel(this.childOptions({room, session: this._client.session})));
} }
this.emitChange("rightPanelViewModel"); this.emitChange("rightPanelViewModel");
} }
notifyRoomReplaced(oldId, newId) {
this.navigation.push("room", newId);
}
} }

View file

@ -14,11 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
export class ReplayDetectionEntry { export function imageToInfo(image) {
constructor(sessionId, messageIndex, event) { return {
this.sessionId = sessionId; w: image.width,
this.messageIndex = messageIndex; h: image.height,
this.eventId = event.event_id; mimetype: image.blob.mimeType,
this.timestamp = event.origin_server_ts; size: image.blob.size
} };
} }

View file

@ -15,10 +15,10 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar.js"; import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar";
import {ViewModel} from "../../ViewModel.js"; import {ViewModel} from "../../ViewModel";
const KIND_ORDER = ["invite", "room"]; const KIND_ORDER = ["roomBeingCreated", "invite", "room"];
export class BaseTileViewModel extends ViewModel { export class BaseTileViewModel extends ViewModel {
constructor(options) { constructor(options) {

View file

@ -1,5 +1,4 @@
/* /*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
Copyright 2020, 2021 The Matrix.org Foundation C.I.C. Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
@ -16,6 +15,7 @@ limitations under the License.
*/ */
import {BaseTileViewModel} from "./BaseTileViewModel.js"; import {BaseTileViewModel} from "./BaseTileViewModel.js";
import {comparePrimitive} from "./common";
export class InviteTileViewModel extends BaseTileViewModel { export class InviteTileViewModel extends BaseTileViewModel {
constructor(options) { constructor(options) {
@ -25,31 +25,40 @@ export class InviteTileViewModel extends BaseTileViewModel {
this._url = this.urlCreator.openRoomActionUrl(this._invite.id); this._url = this.urlCreator.openRoomActionUrl(this._invite.id);
} }
get busy() { get busy() { return this._invite.accepting || this._invite.rejecting; }
return this._invite.accepting || this._invite.rejecting; get kind() { return "invite"; }
} get url() { return this._url; }
get name() { return this._invite.name; }
get kind() { get isHighlighted() { return true; }
return "invite"; get isUnread() { return true; }
} get badgeCount() { return this.i18n`!`; }
get _avatarSource() { return this._invite; }
get url() {
return this._url;
}
/** very important that sorting order is stable and that comparing
* to itself always returns 0, otherwise SortedMapList will
* remove the wrong children, etc ... */
compare(other) { compare(other) {
const parentComparison = super.compare(other); const parentComparison = super.compare(other);
if (parentComparison !== 0) { if (parentComparison !== 0) {
return parentComparison; return parentComparison;
} }
return other._invite.timestamp - this._invite.timestamp; const timeDiff = other._invite.timestamp - this._invite.timestamp;
} if (timeDiff !== 0) {
return timeDiff;
get name() { }
return this._invite.name; return comparePrimitive(this._invite.id, other._invite.id);
} }
}
get _avatarSource() {
return this._invite; export function tests() {
return {
"test compare with timestamp": assert => {
const urlCreator = {openRoomActionUrl() { return "";}}
const vm1 = new InviteTileViewModel({invite: {timestamp: 500, id: "1"}, urlCreator});
const vm2 = new InviteTileViewModel({invite: {timestamp: 250, id: "2"}, urlCreator});
assert(vm1.compare(vm2) < 0);
assert(vm2.compare(vm1) > 0);
assert.equal(vm1.compare(vm1), 0);
},
} }
} }

View file

@ -15,42 +15,47 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {ViewModel} from "../../ViewModel.js"; import {ViewModel} from "../../ViewModel";
import {RoomTileViewModel} from "./RoomTileViewModel.js"; import {RoomTileViewModel} from "./RoomTileViewModel.js";
import {InviteTileViewModel} from "./InviteTileViewModel.js"; import {InviteTileViewModel} from "./InviteTileViewModel.js";
import {RoomBeingCreatedTileViewModel} from "./RoomBeingCreatedTileViewModel.js";
import {RoomFilter} from "./RoomFilter.js"; import {RoomFilter} from "./RoomFilter.js";
import {ApplyMap} from "../../../observable/map/ApplyMap.js"; import {ApplyMap} from "../../../observable/map/ApplyMap.js";
import {addPanelIfNeeded} from "../../navigation/index.js"; import {addPanelIfNeeded} from "../../navigation/index";
export class LeftPanelViewModel extends ViewModel { export class LeftPanelViewModel extends ViewModel {
constructor(options) { constructor(options) {
super(options); super(options);
const {rooms, invites} = options; const {session} = options;
this._tileViewModelsMap = this._mapTileViewModels(rooms, invites); this._tileViewModelsMap = this._mapTileViewModels(session.roomsBeingCreated, session.invites, session.rooms);
this._tileViewModelsFilterMap = new ApplyMap(this._tileViewModelsMap); this._tileViewModelsFilterMap = new ApplyMap(this._tileViewModelsMap);
this._tileViewModels = this._tileViewModelsFilterMap.sortValues((a, b) => a.compare(b)); this._tileViewModels = this._tileViewModelsFilterMap.sortValues((a, b) => a.compare(b));
this._currentTileVM = null; this._currentTileVM = null;
this._setupNavigation(); this._setupNavigation();
this._closeUrl = this.urlCreator.urlForSegment("session"); this._closeUrl = this.urlCreator.urlForSegment("session");
this._settingsUrl = this.urlCreator.urlForSegment("settings"); this._settingsUrl = this.urlCreator.urlForSegment("settings");
this._createRoomUrl = this.urlCreator.urlForSegment("create-room");
} }
_mapTileViewModels(rooms, invites) { _mapTileViewModels(roomsBeingCreated, invites, rooms) {
// join is not commutative, invites will take precedence over rooms // join is not commutative, invites will take precedence over rooms
return invites.join(rooms).mapValues((roomOrInvite, emitChange) => { const allTiles = invites.join(roomsBeingCreated, rooms).mapValues((item, emitChange) => {
let vm; let vm;
if (roomOrInvite.isInvite) { if (item.isBeingCreated) {
vm = new InviteTileViewModel(this.childOptions({invite: roomOrInvite, emitChange})); vm = new RoomBeingCreatedTileViewModel(this.childOptions({roomBeingCreated: item, emitChange}));
} else if (item.isInvite) {
vm = new InviteTileViewModel(this.childOptions({invite: item, emitChange}));
} else { } else {
vm = new RoomTileViewModel(this.childOptions({room: roomOrInvite, emitChange})); vm = new RoomTileViewModel(this.childOptions({room: item, emitChange}));
} }
const isOpen = this.navigation.path.get("room")?.value === roomOrInvite.id; const isOpen = this.navigation.path.get("room")?.value === item.id;
if (isOpen) { if (isOpen) {
vm.open(); vm.open();
this._updateCurrentVM(vm); this._updateCurrentVM(vm);
} }
return vm; return vm;
}); });
return allTiles;
} }
_updateCurrentVM(vm) { _updateCurrentVM(vm) {
@ -69,6 +74,8 @@ export class LeftPanelViewModel extends ViewModel {
return this._settingsUrl; return this._settingsUrl;
} }
get createRoomUrl() { return this._createRoomUrl; }
_setupNavigation() { _setupNavigation() {
const roomObservable = this.navigation.observe("room"); const roomObservable = this.navigation.observe("room");
this.track(roomObservable.subscribe(roomId => this._open(roomId))); this.track(roomObservable.subscribe(roomId => this._open(roomId)));
@ -127,11 +134,14 @@ export class LeftPanelViewModel extends ViewModel {
query = query.trim(); query = query.trim();
if (query.length === 0) { if (query.length === 0) {
this.clearFilter(); this.clearFilter();
return false;
} else { } else {
const startFiltering = !this._tileViewModelsFilterMap.hasApply();
const filter = new RoomFilter(query); const filter = new RoomFilter(query);
this._tileViewModelsFilterMap.setApply((roomId, vm) => { this._tileViewModelsFilterMap.setApply((roomId, vm) => {
vm.hidden = !filter.matches(vm); vm.hidden = !filter.matches(vm);
}); });
return startFiltering;
} }
} }
} }

View file

@ -0,0 +1,70 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
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.
*/
import {BaseTileViewModel} from "./BaseTileViewModel.js";
import {comparePrimitive} from "./common";
export class RoomBeingCreatedTileViewModel extends BaseTileViewModel {
constructor(options) {
super(options);
const {roomBeingCreated} = options;
this._roomBeingCreated = roomBeingCreated;
this._url = this.urlCreator.openRoomActionUrl(this._roomBeingCreated.id);
}
get busy() { return !this._roomBeingCreated.error; }
get kind() { return "roomBeingCreated"; }
get isHighlighted() { return !this.busy; }
get badgeCount() { return !this.busy && this.i18n`Failed`; }
get url() { return this._url; }
get name() { return this._roomBeingCreated.name; }
get _avatarSource() { return this._roomBeingCreated; }
/** very important that sorting order is stable and that comparing
* to itself always returns 0, otherwise SortedMapList will
* remove the wrong children, etc ... */
compare(other) {
const parentCmp = super.compare(other);
if (parentCmp !== 0) {
return parentCmp;
}
const nameCmp = comparePrimitive(this.name, other.name);
if (nameCmp === 0) {
return comparePrimitive(this._roomBeingCreated.id, other._roomBeingCreated.id);
} else {
return nameCmp;
}
}
avatarUrl(size) {
// allow blob url which doesn't need mxc => http resolution
return this._roomBeingCreated.avatarBlobUrl ?? super.avatarUrl(size);
}
}
export function tests() {
return {
"test compare with names": assert => {
const urlCreator = {openRoomActionUrl() { return "";}}
const vm1 = new RoomBeingCreatedTileViewModel({roomBeingCreated: {name: "A", id: "1"}, urlCreator});
const vm2 = new RoomBeingCreatedTileViewModel({roomBeingCreated: {name: "B", id: "2"}, urlCreator});
assert(vm1.compare(vm2) < 0);
assert(vm2.compare(vm1) > 0);
assert.equal(vm1.compare(vm1), 0);
},
}
}

View file

@ -33,6 +33,9 @@ export class RoomTileViewModel extends BaseTileViewModel {
return this._url; return this._url;
} }
/** very important that sorting order is stable and that comparing
* to itself always returns 0, otherwise SortedMapList will
* remove the wrong children, etc ... */
compare(other) { compare(other) {
const parentComparison = super.compare(other); const parentComparison = super.compare(other);
if (parentComparison !== 0) { if (parentComparison !== 0) {

View file

@ -0,0 +1,23 @@
/*
Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
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.
*/
export function comparePrimitive(a, b) {
if (a === b) {
return 0;
} else {
return a < b ? -1 : 1;
}
}

View file

@ -14,8 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {ViewModel} from "../../ViewModel.js"; import {ViewModel} from "../../ViewModel";
import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar.js"; import {RoomType} from "../../../matrix/room/common";
import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar";
export class MemberDetailsViewModel extends ViewModel { export class MemberDetailsViewModel extends ViewModel {
constructor(options) { constructor(options) {
@ -25,6 +26,7 @@ export class MemberDetailsViewModel extends ViewModel {
this._member = this._observableMember.get(); this._member = this._observableMember.get();
this._isEncrypted = options.isEncrypted; this._isEncrypted = options.isEncrypted;
this._powerLevelsObservable = options.powerLevelsObservable; this._powerLevelsObservable = options.powerLevelsObservable;
this._session = options.session;
this.track(this._powerLevelsObservable.subscribe(() => this._onPowerLevelsChange())); this.track(this._powerLevelsObservable.subscribe(() => this._onPowerLevelsChange()));
this.track(this._observableMember.subscribe( () => this._onMemberChange())); this.track(this._observableMember.subscribe( () => this._onMemberChange()));
} }
@ -77,6 +79,19 @@ export class MemberDetailsViewModel extends ViewModel {
} }
get linkToUser() { get linkToUser() {
return `https://matrix.to/#/${this._member.userId}`; return `https://matrix.to/#/${encodeURIComponent(this._member.userId)}`;
}
async openDirectMessage() {
const room = this._session.findDirectMessageForUserId(this.userId);
let roomId = room?.id;
if (!roomId) {
const roomBeingCreated = await this._session.createRoom({
type: RoomType.DirectMessage,
invites: [this.userId]
});
roomId = roomBeingCreated.id;
}
this.navigation.push("room", roomId);
} }
} }

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {ViewModel} from "../../ViewModel.js"; import {ViewModel} from "../../ViewModel";
import {MemberTileViewModel} from "./MemberTileViewModel.js"; import {MemberTileViewModel} from "./MemberTileViewModel.js";
import {createMemberComparator} from "./members/comparator.js"; import {createMemberComparator} from "./members/comparator.js";
import {Disambiguator} from "./members/disambiguator.js"; import {Disambiguator} from "./members/disambiguator.js";

View file

@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {ViewModel} from "../../ViewModel.js"; import {ViewModel} from "../../ViewModel";
import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar.js"; import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar";
export class MemberTileViewModel extends ViewModel { export class MemberTileViewModel extends ViewModel {
constructor(options) { constructor(options) {

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {ViewModel} from "../../ViewModel.js"; import {ViewModel} from "../../ViewModel";
import {RoomDetailsViewModel} from "./RoomDetailsViewModel.js"; import {RoomDetailsViewModel} from "./RoomDetailsViewModel.js";
import {MemberListViewModel} from "./MemberListViewModel.js"; import {MemberListViewModel} from "./MemberListViewModel.js";
import {MemberDetailsViewModel} from "./MemberDetailsViewModel.js"; import {MemberDetailsViewModel} from "./MemberDetailsViewModel.js";
@ -23,6 +23,7 @@ export class RightPanelViewModel extends ViewModel {
constructor(options) { constructor(options) {
super(options); super(options);
this._room = options.room; this._room = options.room;
this._session = options.session;
this._members = null; this._members = null;
this._setupNavigation(); this._setupNavigation();
} }
@ -48,7 +49,13 @@ export class RightPanelViewModel extends ViewModel {
} }
const isEncrypted = this._room.isEncrypted; const isEncrypted = this._room.isEncrypted;
const powerLevelsObservable = await this._room.observePowerLevels(); const powerLevelsObservable = await this._room.observePowerLevels();
return {observableMember, isEncrypted, powerLevelsObservable, mediaRepository: this._room.mediaRepository}; return {
observableMember,
isEncrypted,
powerLevelsObservable,
mediaRepository: this._room.mediaRepository,
session: this._session
};
} }
_setupNavigation() { _setupNavigation() {

View file

@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {ViewModel} from "../../ViewModel.js"; import {ViewModel} from "../../ViewModel";
import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar.js"; import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar";
export class RoomDetailsViewModel extends ViewModel { export class RoomDetailsViewModel extends ViewModel {
constructor(options) { constructor(options) {

View file

@ -14,11 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {ViewModel} from "../../ViewModel.js"; import {ViewModel} from "../../ViewModel";
export class ComposerViewModel extends ViewModel { export class ComposerViewModel extends ViewModel {
constructor(roomVM) { constructor(roomVM) {
super(); super(roomVM.options);
this._roomVM = roomVM; this._roomVM = roomVM;
this._isEmpty = true; this._isEmpty = true;
this._replyVM = null; this._replyVM = null;
@ -30,6 +30,7 @@ export class ComposerViewModel extends ViewModel {
this._replyVM = this.disposeTracked(this._replyVM); this._replyVM = this.disposeTracked(this._replyVM);
if (entry) { if (entry) {
this._replyVM = this.track(this._roomVM._createTile(entry)); this._replyVM = this.track(this._roomVM._createTile(entry));
this._replyVM.notifyVisible();
} }
this.emitChange("replyViewModel"); this.emitChange("replyViewModel");
this.emit("focus"); this.emit("focus");

View file

@ -15,8 +15,8 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar.js"; import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar";
import {ViewModel} from "../../ViewModel.js"; import {ViewModel} from "../../ViewModel";
export class InviteViewModel extends ViewModel { export class InviteViewModel extends ViewModel {
constructor(options) { constructor(options) {

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {ViewModel} from "../../ViewModel.js"; import {ViewModel} from "../../ViewModel";
export class LightboxViewModel extends ViewModel { export class LightboxViewModel extends ViewModel {
constructor(options) { constructor(options) {

View file

@ -1,9 +1,17 @@
# "Room" view models # "Room" view models
InviteViewModel and RoomViewModel are interchangebly used as "room view model": InviteViewModel, RoomViewModel and RoomBeingCreatedViewModel are interchangebly used as "room view model":
- SessionViewModel.roomViewModel can be an instance of either - SessionViewModel.roomViewModel can be an instance of any
- RoomGridViewModel.roomViewModelAt(i) can return an instance of either - RoomGridViewModel.roomViewModelAt(i) can return an instance of any
This is because they are accessed by the same url and need to transition into each other, in these two locations. Having two methods, especially in RoomGridViewModel would have been more cumbersome, even though this is not in line with how different view models are exposed in SessionViewModel. This is because they are accessed by the same url and need to transition into each other, in these two locations. Having two methods, especially in RoomGridViewModel would have been more cumbersome, even though this is not in line with how different view models are exposed in SessionViewModel.
They share an `id` and `kind` property, the latter can be used to differentiate them from the view, and a `focus` method. They share an `id` and `kind` property, the latter can be used to differentiate them from the view, and a `focus` method.
Once we convert this folder to typescript, we should use this interface for all the view models:
```ts
interface IGridItemViewModel {
id: string;
kind: string;
focus();
}
```

View file

@ -0,0 +1,75 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
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.
*/
import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar";
import {ViewModel} from "../../ViewModel";
export class RoomBeingCreatedViewModel extends ViewModel {
constructor(options) {
super(options);
const {roomBeingCreated, mediaRepository} = options;
this._roomBeingCreated = roomBeingCreated;
this._mediaRepository = mediaRepository;
this._onRoomChange = this._onRoomChange.bind(this);
this._closeUrl = this.urlCreator.urlUntilSegment("session");
this._roomBeingCreated.on("change", this._onRoomChange);
}
get kind() { return "roomBeingCreated"; }
get closeUrl() { return this._closeUrl; }
get name() { return this._roomBeingCreated.name; }
get id() { return this._roomBeingCreated.id; }
get isEncrypted() { return this._roomBeingCreated.isEncrypted; }
get error() {
const {error} = this._roomBeingCreated;
if (error) {
if (error.name === "ConnectionError") {
return this.i18n`You seem to be offline`;
} else {
return error.message;
}
}
return "";
}
get avatarLetter() { return avatarInitials(this.name); }
get avatarColorNumber() { return getIdentifierColorNumber(this._roomBeingCreated.avatarColorId); }
get avatarTitle() { return this.name; }
avatarUrl(size) {
// allow blob url which doesn't need mxc => http resolution
return this._roomBeingCreated.avatarBlobUrl ??
getAvatarHttpUrl(this._roomBeingCreated.avatarUrl, size, this.platform, this._mediaRepository);
}
focus() {}
_onRoomChange() {
this.emitChange();
}
cancel() {
this._roomBeingCreated.cancel();
// navigate away from the room
this.navigation.applyPath(this.navigation.path.until("session"));
}
dispose() {
super.dispose();
this._roomBeingCreated.off("change", this._onRoomChange);
}
}

View file

@ -17,25 +17,30 @@ limitations under the License.
import {TimelineViewModel} from "./timeline/TimelineViewModel.js"; import {TimelineViewModel} from "./timeline/TimelineViewModel.js";
import {ComposerViewModel} from "./ComposerViewModel.js" import {ComposerViewModel} from "./ComposerViewModel.js"
import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar.js"; import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar";
import {tilesCreator} from "./timeline/tilesCreator.js"; import {ViewModel} from "../../ViewModel";
import {ViewModel} from "../../ViewModel.js"; import {imageToInfo} from "../common.js";
// TODO: remove fallback so default isn't included in bundle for SDK users that have their custom tileClassForEntry
// this is a breaking SDK change though to make this option mandatory
import {tileClassForEntry as defaultTileClassForEntry} from "./timeline/tiles/index";
import {RoomStatus} from "../../../matrix/room/common";
export class RoomViewModel extends ViewModel { export class RoomViewModel extends ViewModel {
constructor(options) { constructor(options) {
super(options); super(options);
const {room} = options; const {room, tileClassForEntry} = options;
this._room = room; this._room = room;
this._timelineVM = null; this._timelineVM = null;
this._tilesCreator = null; this._tileClassForEntry = tileClassForEntry ?? defaultTileClassForEntry;
this._tileOptions = undefined;
this._onRoomChange = this._onRoomChange.bind(this); this._onRoomChange = this._onRoomChange.bind(this);
this._timelineError = null; this._timelineError = null;
this._sendError = null; this._sendError = null;
this._composerVM = null; this._composerVM = null;
if (room.isArchived) { if (room.isArchived) {
this._composerVM = new ArchivedViewModel(this.childOptions({archivedRoom: room})); this._composerVM = this.track(new ArchivedViewModel(this.childOptions({archivedRoom: room})));
} else { } else {
this._composerVM = new ComposerViewModel(this); this._recreateComposerOnPowerLevelChange();
} }
this._clearUnreadTimout = null; this._clearUnreadTimout = null;
this._closeUrl = this.urlCreator.urlUntilSegment("session"); this._closeUrl = this.urlCreator.urlUntilSegment("session");
@ -45,12 +50,13 @@ export class RoomViewModel extends ViewModel {
this._room.on("change", this._onRoomChange); this._room.on("change", this._onRoomChange);
try { try {
const timeline = await this._room.openTimeline(); const timeline = await this._room.openTimeline();
this._tilesCreator = tilesCreator(this.childOptions({ this._tileOptions = this.childOptions({
roomVM: this, roomVM: this,
timeline, timeline,
})); tileClassForEntry: this._tileClassForEntry,
});
this._timelineVM = this.track(new TimelineViewModel(this.childOptions({ this._timelineVM = this.track(new TimelineViewModel(this.childOptions({
tilesCreator: this._tilesCreator, tileOptions: this._tileOptions,
timeline, timeline,
}))); })));
this.emitChange("timelineViewModel"); this.emitChange("timelineViewModel");
@ -62,6 +68,30 @@ export class RoomViewModel extends ViewModel {
this._clearUnreadAfterDelay(); this._clearUnreadAfterDelay();
} }
async _recreateComposerOnPowerLevelChange() {
const powerLevelObservable = await this._room.observePowerLevels();
const canSendMessage = () => powerLevelObservable.get().canSendType("m.room.message");
let oldCanSendMessage = canSendMessage();
const recreateComposer = newCanSendMessage => {
this._composerVM = this.disposeTracked(this._composerVM);
if (newCanSendMessage) {
this._composerVM = this.track(new ComposerViewModel(this));
}
else {
this._composerVM = this.track(new LowerPowerLevelViewModel(this.childOptions()));
}
this.emitChange("powerLevelObservable")
};
this.track(powerLevelObservable.subscribe(() => {
const newCanSendMessage = canSendMessage();
if (oldCanSendMessage !== newCanSendMessage) {
recreateComposer(newCanSendMessage);
oldCanSendMessage = newCanSendMessage;
}
}));
recreateComposer(oldCanSendMessage);
}
async _clearUnreadAfterDelay() { async _clearUnreadAfterDelay() {
if (this._room.isArchived || this._clearUnreadTimout) { if (this._room.isArchived || this._clearUnreadTimout) {
return; return;
@ -97,9 +127,8 @@ export class RoomViewModel extends ViewModel {
// room doesn't tell us yet which fields changed, // room doesn't tell us yet which fields changed,
// so emit all fields originating from summary // so emit all fields originating from summary
_onRoomChange() { _onRoomChange() {
if (this._room.isArchived) { // propagate the update to the child view models so it's bindings can update based on room changes
this._composerVM.emitChange(); this._composerVM.emitChange();
}
this.emitChange(); this.emitChange();
} }
@ -161,21 +190,97 @@ export class RoomViewModel extends ViewModel {
} }
_createTile(entry) { _createTile(entry) {
return this._tilesCreator(entry); if (this._tileOptions) {
const Tile = this._tileOptions.tileClassForEntry(entry);
if (Tile) {
return new Tile(entry, this._tileOptions);
}
}
} }
async _processCommandJoin(roomName) {
try {
const roomId = await this._options.client.session.joinRoom(roomName);
const roomStatusObserver = await this._options.client.session.observeRoomStatus(roomId);
await roomStatusObserver.waitFor(status => status === RoomStatus.Joined);
this.navigation.push("room", roomId);
} catch (err) {
let exc;
if ((err.statusCode ?? err.status) === 400) {
exc = new Error(`/join : '${roomName}' was not legal room ID or room alias`);
} else if ((err.statusCode ?? err.status) === 404 || (err.statusCode ?? err.status) === 502 || err.message == "Internal Server Error") {
exc = new Error(`/join : room '${roomName}' not found`);
} else if ((err.statusCode ?? err.status) === 403) {
exc = new Error(`/join : you're not invited to join '${roomName}'`);
} else {
exc = err;
}
this._sendError = exc;
this._timelineError = null;
this.emitChange("error");
}
}
async _processCommand (message) {
let msgtype;
const [commandName, ...args] = message.substring(1).split(" ");
switch (commandName) {
case "me":
message = args.join(" ");
msgtype = "m.emote";
break;
case "join":
if (args.length === 1) {
const roomName = args[0];
await this._processCommandJoin(roomName);
} else {
this._sendError = new Error("join syntax: /join <room-id>");
this._timelineError = null;
this.emitChange("error");
}
break;
case "shrug":
message = "¯\\_(ツ)_/¯ " + args.join(" ");
msgtype = "m.text";
break;
case "tableflip":
message = "(╯°□°)╯︵ ┻━┻ " + args.join(" ");
msgtype = "m.text";
break;
case "unflip":
message = "┬──┬ ( ゜-゜ノ) " + args.join(" ");
msgtype = "m.text";
break;
case "lenny":
message = "( ͡° ͜ʖ ͡°) " + args.join(" ");
msgtype = "m.text";
break;
default:
this._sendError = new Error(`no command name "${commandName}". To send the message instead of executing, please type "/${message}"`);
this._timelineError = null;
this.emitChange("error");
message = undefined;
}
return {type: msgtype, message: message};
}
async _sendMessage(message, replyingTo) { async _sendMessage(message, replyingTo) {
if (!this._room.isArchived && message) { if (!this._room.isArchived && message) {
let messinfo = {type : "m.text", message : message};
if (message.startsWith("//")) {
messinfo.message = message.substring(1).trim();
} else if (message.startsWith("/")) {
messinfo = await this._processCommand(message);
}
try { try {
let msgtype = "m.text"; const msgtype = messinfo.type;
if (message.startsWith("/me ")) { const message = messinfo.message;
message = message.substr(4).trim(); if (msgtype && message) {
msgtype = "m.emote"; if (replyingTo) {
} await replyingTo.reply(msgtype, message);
if (replyingTo) { } else {
await replyingTo.reply(msgtype, message); await this._room.sendEvent("m.room.message", {msgtype, body: message});
} else { }
await this._room.sendEvent("m.room.message", {msgtype, body: message});
} }
} catch (err) { } catch (err) {
console.error(`room.sendMessage(): ${err.message}:\n${err.stack}`); console.error(`room.sendMessage(): ${err.message}:\n${err.stack}`);
@ -274,7 +379,9 @@ export class RoomViewModel extends ViewModel {
let image = await this.platform.loadImage(file.blob); let image = await this.platform.loadImage(file.blob);
const limit = await this.platform.settingsStorage.getInt("sentImageSizeLimit"); const limit = await this.platform.settingsStorage.getInt("sentImageSizeLimit");
if (limit && image.maxDimension > limit) { if (limit && image.maxDimension > limit) {
image = await image.scale(limit); const scaledImage = await image.scale(limit);
image.dispose();
image = scaledImage;
} }
const content = { const content = {
body: file.name, body: file.name,
@ -318,15 +425,11 @@ export class RoomViewModel extends ViewModel {
this._composerVM.setReplyingTo(entry); this._composerVM.setReplyingTo(entry);
} }
} }
}
function imageToInfo(image) { dismissError() {
return { this._sendError = null;
w: image.width, this.emitChange("error");
h: image.height, }
mimetype: image.blob.mimeType,
size: image.blob.size
};
} }
function videoToInfo(video) { function videoToInfo(video) {
@ -360,6 +463,16 @@ class ArchivedViewModel extends ViewModel {
} }
get kind() { get kind() {
return "archived"; return "disabled";
}
}
class LowerPowerLevelViewModel extends ViewModel {
get description() {
return this.i18n`You do not have the powerlevel necessary to send messages`;
}
get kind() {
return "disabled";
} }
} }

Some files were not shown because too many files have changed in this diff Show more