Merge branches 'develop' and 't3chguy/e2eedefault' of github.com:matrix-org/matrix-react-sdk into t3chguy/e2eedefault
Conflicts: src/components/views/dialogs/InviteDialog.js
This commit is contained in:
commit
8848a2ea64
305 changed files with 9256 additions and 4200 deletions
|
@ -6,7 +6,6 @@ src/components/structures/RoomView.js
|
|||
src/components/structures/ScrollPanel.js
|
||||
src/components/structures/SearchBox.js
|
||||
src/components/structures/UploadBar.js
|
||||
src/components/views/avatars/BaseAvatar.js
|
||||
src/components/views/avatars/MemberAvatar.js
|
||||
src/components/views/create_room/RoomAlias.js
|
||||
src/components/views/dialogs/SetPasswordDialog.js
|
||||
|
@ -15,9 +14,7 @@ src/components/views/elements/AddressSelector.js
|
|||
src/components/views/elements/DirectorySearchBox.js
|
||||
src/components/views/elements/MemberEventListSummary.js
|
||||
src/components/views/elements/UserSelector.js
|
||||
src/components/views/globals/MatrixToolbar.js
|
||||
src/components/views/globals/NewVersionBar.js
|
||||
src/components/views/globals/UpdateCheckBar.js
|
||||
src/components/views/messages/MFileBody.js
|
||||
src/components/views/messages/TextualBody.js
|
||||
src/components/views/room_settings/ColorSettings.js
|
||||
|
|
281
CHANGELOG.md
281
CHANGELOG.md
|
@ -1,3 +1,284 @@
|
|||
Changes in [2.6.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.6.1) (2020-05-22)
|
||||
===================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.6.0...v2.6.1)
|
||||
|
||||
* Fix key backup restore with SSSS
|
||||
[\#4617](https://github.com/matrix-org/matrix-react-sdk/pull/4617)
|
||||
* Remove SSSS key upgrade check from rageshake
|
||||
[\#4616](https://github.com/matrix-org/matrix-react-sdk/pull/4616)
|
||||
|
||||
Changes in [2.6.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.6.0) (2020-05-19)
|
||||
===================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.6.0-rc.1...v2.6.0)
|
||||
|
||||
* Upgrade to JS SDK 6.1.0
|
||||
* Revert "ImageView make clicking off it easier"
|
||||
[\#4602](https://github.com/matrix-org/matrix-react-sdk/pull/4602)
|
||||
* Remove debugging that causes email addresses to load forever (to release)
|
||||
[\#4598](https://github.com/matrix-org/matrix-react-sdk/pull/4598)
|
||||
|
||||
Changes in [2.6.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.6.0-rc.1) (2020-05-14)
|
||||
=============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.5.0...v2.6.0-rc.1)
|
||||
|
||||
* Upgrade to JS SDK 6.1.0-rc.1
|
||||
* Update from Weblate
|
||||
[\#4596](https://github.com/matrix-org/matrix-react-sdk/pull/4596)
|
||||
* Fix message edits dialog being wrong and sometimes crashing
|
||||
[\#4595](https://github.com/matrix-org/matrix-react-sdk/pull/4595)
|
||||
* Acquire a new session before enacting deactivation
|
||||
[\#4584](https://github.com/matrix-org/matrix-react-sdk/pull/4584)
|
||||
* Remove UI for upgrading 4S to symmetric encryption
|
||||
[\#4581](https://github.com/matrix-org/matrix-react-sdk/pull/4581)
|
||||
* Add copy to SSO prompts during cross-signing setup
|
||||
[\#4555](https://github.com/matrix-org/matrix-react-sdk/pull/4555)
|
||||
* Re-fix OpenID requests from widgets
|
||||
[\#4592](https://github.com/matrix-org/matrix-react-sdk/pull/4592)
|
||||
* Fix persistent widgets on desktop / http
|
||||
[\#4591](https://github.com/matrix-org/matrix-react-sdk/pull/4591)
|
||||
* Updated link and added:Yarn two is not yet used.
|
||||
[\#4589](https://github.com/matrix-org/matrix-react-sdk/pull/4589)
|
||||
* Fix topic dialog not supporting escape as it didn't have a "Close"
|
||||
[\#4578](https://github.com/matrix-org/matrix-react-sdk/pull/4578)
|
||||
* Default to public room when creating room from room directory
|
||||
[\#4579](https://github.com/matrix-org/matrix-react-sdk/pull/4579)
|
||||
* Replace png flags and add Kosovo to country code dropdown
|
||||
[\#4576](https://github.com/matrix-org/matrix-react-sdk/pull/4576)
|
||||
* Rename `trash (custom).svg` as electron doesn't like paths with spaces
|
||||
[\#4583](https://github.com/matrix-org/matrix-react-sdk/pull/4583)
|
||||
* Fix sign in / up links on previewed rooms
|
||||
[\#4582](https://github.com/matrix-org/matrix-react-sdk/pull/4582)
|
||||
* Avoid soft crash if unknown device in verification
|
||||
[\#4580](https://github.com/matrix-org/matrix-react-sdk/pull/4580)
|
||||
* Add slash commands /query and /msg to match IRC
|
||||
[\#4568](https://github.com/matrix-org/matrix-react-sdk/pull/4568)
|
||||
* Send cross-signing debug booleans over rageshake
|
||||
[\#4570](https://github.com/matrix-org/matrix-react-sdk/pull/4570)
|
||||
* Prompt user to specify an alternate server if theirs has registration off
|
||||
[\#4575](https://github.com/matrix-org/matrix-react-sdk/pull/4575)
|
||||
* Don't try and redact redactions for "Remove recent messages"
|
||||
[\#4573](https://github.com/matrix-org/matrix-react-sdk/pull/4573)
|
||||
* View Source should target the replacing event rather than the root one
|
||||
[\#4571](https://github.com/matrix-org/matrix-react-sdk/pull/4571)
|
||||
* Fix passphrase reset in key backup restore dialog
|
||||
[\#4569](https://github.com/matrix-org/matrix-react-sdk/pull/4569)
|
||||
* Ensure key backup gets dealt with correctly during secret storage reset
|
||||
[\#4556](https://github.com/matrix-org/matrix-react-sdk/pull/4556)
|
||||
* Fix crash for broken invites
|
||||
[\#4565](https://github.com/matrix-org/matrix-react-sdk/pull/4565)
|
||||
* Fix rageshake with no matrix client
|
||||
[\#4572](https://github.com/matrix-org/matrix-react-sdk/pull/4572)
|
||||
* Update from Weblate
|
||||
[\#4567](https://github.com/matrix-org/matrix-react-sdk/pull/4567)
|
||||
* Bring back UnknownBody for UISIs
|
||||
[\#4564](https://github.com/matrix-org/matrix-react-sdk/pull/4564)
|
||||
* clear tag panel selection if the community selected is left
|
||||
[\#4559](https://github.com/matrix-org/matrix-react-sdk/pull/4559)
|
||||
* Close ImageView when redacting
|
||||
[\#4560](https://github.com/matrix-org/matrix-react-sdk/pull/4560)
|
||||
* Redesign redactions
|
||||
[\#4484](https://github.com/matrix-org/matrix-react-sdk/pull/4484)
|
||||
* Don't try to reload profile information when closing the user panel
|
||||
[\#4547](https://github.com/matrix-org/matrix-react-sdk/pull/4547)
|
||||
* Fix right panel hiding when viewing room member
|
||||
[\#4558](https://github.com/matrix-org/matrix-react-sdk/pull/4558)
|
||||
* Don't erase password confirm on registration error
|
||||
[\#4540](https://github.com/matrix-org/matrix-react-sdk/pull/4540)
|
||||
* Add a loading state for email addresses/phone numbers in settings
|
||||
[\#4557](https://github.com/matrix-org/matrix-react-sdk/pull/4557)
|
||||
* set the meta tag for theme-color to the same theme css background
|
||||
[\#4554](https://github.com/matrix-org/matrix-react-sdk/pull/4554)
|
||||
* Update Invite Dialog copy to include email addresses
|
||||
[\#4497](https://github.com/matrix-org/matrix-react-sdk/pull/4497)
|
||||
* Fix slider toggle regression.
|
||||
[\#4546](https://github.com/matrix-org/matrix-react-sdk/pull/4546)
|
||||
* Fix a crash where a name could unexpectedly be an empty list
|
||||
[\#4552](https://github.com/matrix-org/matrix-react-sdk/pull/4552)
|
||||
* Solves communities can be dragged from context menu
|
||||
[\#4492](https://github.com/matrix-org/matrix-react-sdk/pull/4492)
|
||||
* Remove prefixes for composer avatar urls
|
||||
[\#4553](https://github.com/matrix-org/matrix-react-sdk/pull/4553)
|
||||
* Fix reply RR spacing getting doubled
|
||||
[\#4541](https://github.com/matrix-org/matrix-react-sdk/pull/4541)
|
||||
* Differentiate copy for own untrusted device dialog
|
||||
[\#4549](https://github.com/matrix-org/matrix-react-sdk/pull/4549)
|
||||
* EventIndex: Reduce the logging the event index is producing.
|
||||
[\#4548](https://github.com/matrix-org/matrix-react-sdk/pull/4548)
|
||||
* Increase rageshake size limit to 5mb
|
||||
[\#4543](https://github.com/matrix-org/matrix-react-sdk/pull/4543)
|
||||
* Update from Weblate
|
||||
[\#4542](https://github.com/matrix-org/matrix-react-sdk/pull/4542)
|
||||
* Guard against race when waiting for cross-signing to be ready
|
||||
[\#4539](https://github.com/matrix-org/matrix-react-sdk/pull/4539)
|
||||
* Wait for user to be verified in e2e setup
|
||||
[\#4537](https://github.com/matrix-org/matrix-react-sdk/pull/4537)
|
||||
* Convert MatrixChat to a TypeScript class
|
||||
[\#4462](https://github.com/matrix-org/matrix-react-sdk/pull/4462)
|
||||
* Mark room as read when escape is pressed
|
||||
[\#4271](https://github.com/matrix-org/matrix-react-sdk/pull/4271)
|
||||
* Only show key backup reminder when confirmed by server to be missing
|
||||
[\#4534](https://github.com/matrix-org/matrix-react-sdk/pull/4534)
|
||||
* Add device name to unverified session toast
|
||||
[\#4535](https://github.com/matrix-org/matrix-react-sdk/pull/4535)
|
||||
* Show progress when loading keys
|
||||
[\#4507](https://github.com/matrix-org/matrix-react-sdk/pull/4507)
|
||||
* Fix device verification toasts not disappearing
|
||||
[\#4532](https://github.com/matrix-org/matrix-react-sdk/pull/4532)
|
||||
* Update toast copy again
|
||||
[\#4529](https://github.com/matrix-org/matrix-react-sdk/pull/4529)
|
||||
* Re-apply theme after login
|
||||
[\#4518](https://github.com/matrix-org/matrix-react-sdk/pull/4518)
|
||||
* Reduce maximum width of toasts & allow multiple lines
|
||||
[\#4525](https://github.com/matrix-org/matrix-react-sdk/pull/4525)
|
||||
* Treat sessions that are there when we log in as old
|
||||
[\#4524](https://github.com/matrix-org/matrix-react-sdk/pull/4524)
|
||||
* Allow resetting storage from the access dialog
|
||||
[\#4521](https://github.com/matrix-org/matrix-react-sdk/pull/4521)
|
||||
* Update (bulk) unverified device toast copy
|
||||
[\#4522](https://github.com/matrix-org/matrix-react-sdk/pull/4522)
|
||||
* Make new device toasts appear above review toasts
|
||||
[\#4519](https://github.com/matrix-org/matrix-react-sdk/pull/4519)
|
||||
* Separate toasts for existing & new device verification
|
||||
[\#4511](https://github.com/matrix-org/matrix-react-sdk/pull/4511)
|
||||
* Slightly darker toggle off bg color
|
||||
[\#4477](https://github.com/matrix-org/matrix-react-sdk/pull/4477)
|
||||
* Fix pill vertical align
|
||||
[\#4514](https://github.com/matrix-org/matrix-react-sdk/pull/4514)
|
||||
* Fix set up encryption toast to use "set up" as action
|
||||
[\#4502](https://github.com/matrix-org/matrix-react-sdk/pull/4502)
|
||||
* Don't enable e2ee when inviting a 3pid
|
||||
[\#4509](https://github.com/matrix-org/matrix-react-sdk/pull/4509)
|
||||
* Fix internal link styling in Security Settings
|
||||
[\#4510](https://github.com/matrix-org/matrix-react-sdk/pull/4510)
|
||||
* Small custom theming fixes
|
||||
[\#4508](https://github.com/matrix-org/matrix-react-sdk/pull/4508)
|
||||
* Fix scaling issues
|
||||
[\#4355](https://github.com/matrix-org/matrix-react-sdk/pull/4355)
|
||||
* Aggregate device verify toasts
|
||||
[\#4506](https://github.com/matrix-org/matrix-react-sdk/pull/4506)
|
||||
* Support setting username and avatar colors in custom themes
|
||||
[\#4503](https://github.com/matrix-org/matrix-react-sdk/pull/4503)
|
||||
* only clear on continuations where the clear isn't done by SenderProfile
|
||||
[\#4501](https://github.com/matrix-org/matrix-react-sdk/pull/4501)
|
||||
* cap width of editable item list item to leave space for its X button
|
||||
[\#4495](https://github.com/matrix-org/matrix-react-sdk/pull/4495)
|
||||
* Add a link from settings / devices to your user profile
|
||||
[\#4498](https://github.com/matrix-org/matrix-react-sdk/pull/4498)
|
||||
* Update from Weblate
|
||||
[\#4496](https://github.com/matrix-org/matrix-react-sdk/pull/4496)
|
||||
* Make icon change in SetupEncryptionDialog
|
||||
[\#4485](https://github.com/matrix-org/matrix-react-sdk/pull/4485)
|
||||
* Remove invite only padlocks feature flag
|
||||
[\#4487](https://github.com/matrix-org/matrix-react-sdk/pull/4487)
|
||||
* Fix incorrect toast if security setup skipped
|
||||
[\#4486](https://github.com/matrix-org/matrix-react-sdk/pull/4486)
|
||||
* Revert "Update emojibase for fixed emoji codepoints and Emoji 13 support"
|
||||
[\#4482](https://github.com/matrix-org/matrix-react-sdk/pull/4482)
|
||||
* Fix widget URL templating (again)
|
||||
[\#4481](https://github.com/matrix-org/matrix-react-sdk/pull/4481)
|
||||
* Fix recovery link on login verification flow
|
||||
[\#4479](https://github.com/matrix-org/matrix-react-sdk/pull/4479)
|
||||
* Make avatars in pills occupy the entire space using cropping
|
||||
[\#4476](https://github.com/matrix-org/matrix-react-sdk/pull/4476)
|
||||
* Use WidgetType more often to avoid breaking new sticker pickers
|
||||
[\#4458](https://github.com/matrix-org/matrix-react-sdk/pull/4458)
|
||||
* Update logging for unmanaged widgets, and add TODO comments for other areas
|
||||
[\#4460](https://github.com/matrix-org/matrix-react-sdk/pull/4460)
|
||||
* Fix OpenID requests from widgets
|
||||
[\#4459](https://github.com/matrix-org/matrix-react-sdk/pull/4459)
|
||||
* Take encrypted message search out of labs
|
||||
[\#4467](https://github.com/matrix-org/matrix-react-sdk/pull/4467)
|
||||
* Fix BigEmoji for replies
|
||||
[\#4475](https://github.com/matrix-org/matrix-react-sdk/pull/4475)
|
||||
* Update login security copy and design to match Figma
|
||||
[\#4472](https://github.com/matrix-org/matrix-react-sdk/pull/4472)
|
||||
* Fix i18n of SSO UIA copy in Deactivate Account Dialog
|
||||
[\#4471](https://github.com/matrix-org/matrix-react-sdk/pull/4471)
|
||||
* Assert type of domNode as HTMLElement to fix build
|
||||
[\#4470](https://github.com/matrix-org/matrix-react-sdk/pull/4470)
|
||||
* Unignored in settings
|
||||
[\#4466](https://github.com/matrix-org/matrix-react-sdk/pull/4466)
|
||||
* Skip auth flow test for signing upload when password present
|
||||
[\#4464](https://github.com/matrix-org/matrix-react-sdk/pull/4464)
|
||||
* If user cannot set email during registration don't tell them to
|
||||
[\#4461](https://github.com/matrix-org/matrix-react-sdk/pull/4461)
|
||||
* Fix post-ts autocomplete, it is not null
|
||||
[\#4463](https://github.com/matrix-org/matrix-react-sdk/pull/4463)
|
||||
* Convert autocomplete stuff to TypeScript
|
||||
[\#4452](https://github.com/matrix-org/matrix-react-sdk/pull/4452)
|
||||
* Add a back button to the devtools verifications panel
|
||||
[\#4455](https://github.com/matrix-org/matrix-react-sdk/pull/4455)
|
||||
* Fix: wait until cross-signing keys are fetched to show verify button
|
||||
[\#4456](https://github.com/matrix-org/matrix-react-sdk/pull/4456)
|
||||
* Handle load error in create secret storage dialog
|
||||
[\#4451](https://github.com/matrix-org/matrix-react-sdk/pull/4451)
|
||||
* Allow iframes and Jitsi URLs in /addwidget
|
||||
[\#4382](https://github.com/matrix-org/matrix-react-sdk/pull/4382)
|
||||
* Support m.jitsi-typed widgets as Jitsi widgets
|
||||
[\#4379](https://github.com/matrix-org/matrix-react-sdk/pull/4379)
|
||||
* Don't recheck DeviceListener until after initial sync is finished
|
||||
[\#4450](https://github.com/matrix-org/matrix-react-sdk/pull/4450)
|
||||
* Fix CSS class in ButtonPlaceholder
|
||||
[\#4449](https://github.com/matrix-org/matrix-react-sdk/pull/4449)
|
||||
* Password Login make sure tab takes user to password field
|
||||
[\#4441](https://github.com/matrix-org/matrix-react-sdk/pull/4441)
|
||||
* Network Dropdown fix things not scrolling properly
|
||||
[\#4439](https://github.com/matrix-org/matrix-react-sdk/pull/4439)
|
||||
* ImageView make clicking off it easier
|
||||
[\#4448](https://github.com/matrix-org/matrix-react-sdk/pull/4448)
|
||||
* Add slash command to send a rageshake
|
||||
[\#4443](https://github.com/matrix-org/matrix-react-sdk/pull/4443)
|
||||
* EventIndex: Filter out events that don't have a propper content value.
|
||||
[\#4446](https://github.com/matrix-org/matrix-react-sdk/pull/4446)
|
||||
* Revert "Fix Filepanel scroll position state lost when room is changed"
|
||||
[\#4445](https://github.com/matrix-org/matrix-react-sdk/pull/4445)
|
||||
* Update seshat copy to remove trailing full stop
|
||||
[\#4442](https://github.com/matrix-org/matrix-react-sdk/pull/4442)
|
||||
* Fix Filepanel scroll position state lost when room is changed
|
||||
[\#4388](https://github.com/matrix-org/matrix-react-sdk/pull/4388)
|
||||
* Fix end-to-end tests for end-to-end encryption verification
|
||||
[\#4436](https://github.com/matrix-org/matrix-react-sdk/pull/4436)
|
||||
* Don't explode if the e2e test directory exists when crashing
|
||||
[\#4437](https://github.com/matrix-org/matrix-react-sdk/pull/4437)
|
||||
* Bump https-proxy-agent from 2.2.1 to 2.2.4 in /test/end-to-end-tests
|
||||
[\#4430](https://github.com/matrix-org/matrix-react-sdk/pull/4430)
|
||||
* Minor updates to e2e test instructions on Windows
|
||||
[\#4432](https://github.com/matrix-org/matrix-react-sdk/pull/4432)
|
||||
* Fix typo
|
||||
[\#4435](https://github.com/matrix-org/matrix-react-sdk/pull/4435)
|
||||
* Catch errors sooner so users can recover more easily
|
||||
[\#4122](https://github.com/matrix-org/matrix-react-sdk/pull/4122)
|
||||
* Rageshake: remind user of unsupported browser and send modernizr report
|
||||
[\#4381](https://github.com/matrix-org/matrix-react-sdk/pull/4381)
|
||||
* Design tweaks for DM Room Tiles
|
||||
[\#4338](https://github.com/matrix-org/matrix-react-sdk/pull/4338)
|
||||
* Don't break spills over multiple lines, ellipsis them at max-1-line
|
||||
[\#4434](https://github.com/matrix-org/matrix-react-sdk/pull/4434)
|
||||
* Turn the end-to-end tests back on and fix the lazy-loading tests
|
||||
[\#4433](https://github.com/matrix-org/matrix-react-sdk/pull/4433)
|
||||
* Fix key backup debug panel
|
||||
[\#4431](https://github.com/matrix-org/matrix-react-sdk/pull/4431)
|
||||
* Convert cross-signing feature flag to setting
|
||||
[\#4416](https://github.com/matrix-org/matrix-react-sdk/pull/4416)
|
||||
* Make RoomPublishSetting import-skinnable
|
||||
[\#4428](https://github.com/matrix-org/matrix-react-sdk/pull/4428)
|
||||
* Iterate cross-signing copy
|
||||
[\#4425](https://github.com/matrix-org/matrix-react-sdk/pull/4425)
|
||||
* Fix: ensure twemoji font is loaded when showing SAS emojis
|
||||
[\#4422](https://github.com/matrix-org/matrix-react-sdk/pull/4422)
|
||||
* Revert "Fix: load Twemoji before login so complete security gets the right
|
||||
emojis during SAS"
|
||||
[\#4421](https://github.com/matrix-org/matrix-react-sdk/pull/4421)
|
||||
* Fix: load Twemoji before login so complete security gets the right emojis
|
||||
during SAS
|
||||
[\#4419](https://github.com/matrix-org/matrix-react-sdk/pull/4419)
|
||||
* consolidate and fix copy to clipboard
|
||||
[\#4410](https://github.com/matrix-org/matrix-react-sdk/pull/4410)
|
||||
* Fix Message Context Menu options not displaying: block
|
||||
[\#4418](https://github.com/matrix-org/matrix-react-sdk/pull/4418)
|
||||
* Fix pills being broken by unescaped characters
|
||||
[\#4411](https://github.com/matrix-org/matrix-react-sdk/pull/4411)
|
||||
|
||||
Changes in [2.5.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.5.0) (2020-05-05)
|
||||
===================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.5.0-rc.6...v2.5.0)
|
||||
|
|
|
@ -4,7 +4,7 @@ Matrix JavaScript/ECMAScript Style Guide
|
|||
The intention of this guide is to make Matrix's JavaScript codebase clean,
|
||||
consistent with other popular JavaScript styles and consistent with the rest of
|
||||
the Matrix codebase. For reference, the Matrix Python style guide can be found
|
||||
at https://github.com/matrix-org/synapse/blob/master/docs/code_style.rst
|
||||
at https://github.com/matrix-org/synapse/blob/master/docs/code_style.md
|
||||
|
||||
This document reflects how we would like Matrix JavaScript code to look, with
|
||||
acknowledgement that a significant amount of code is written to older
|
||||
|
@ -17,7 +17,7 @@ writing in modern ECMAScript and using a transpile step to generate the file
|
|||
that applications can then include. There are significant benefits in being
|
||||
able to use modern ECMAScript, although the tooling for doing so can be awkward
|
||||
for library code, especially with regard to translating source maps and line
|
||||
number throgh from the original code to the final application.
|
||||
number through from the original code to the final application.
|
||||
|
||||
General Style
|
||||
-------------
|
||||
|
@ -151,6 +151,7 @@ General Style
|
|||
Don't set things to undefined. Reserve that value to mean "not yet set to anything."
|
||||
Boolean objects are verboten.
|
||||
- Use JSDoc
|
||||
- Use switch-case statements where there are 5 or more branches running against the same variable.
|
||||
|
||||
ECMAScript
|
||||
----------
|
||||
|
|
11
package.json
11
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "matrix-react-sdk",
|
||||
"version": "2.5.0",
|
||||
"version": "2.6.1",
|
||||
"description": "SDK for matrix.org using React",
|
||||
"author": "matrix.org",
|
||||
"repository": {
|
||||
|
@ -55,6 +55,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.8.3",
|
||||
"await-lock": "^2.0.1",
|
||||
"blueimp-canvas-to-blob": "^3.5.0",
|
||||
"browser-encrypt-attachment": "^0.3.0",
|
||||
"browser-request": "^0.3.3",
|
||||
|
@ -117,9 +118,13 @@
|
|||
"@babel/register": "^7.7.4",
|
||||
"@peculiar/webcrypto": "^1.0.22",
|
||||
"@types/classnames": "^2.2.10",
|
||||
"@types/flux": "^3.1.9",
|
||||
"@types/lodash": "^4.14.152",
|
||||
"@types/modernizr": "^3.5.3",
|
||||
"@types/node": "^12.12.41",
|
||||
"@types/qrcode": "^1.3.4",
|
||||
"@types/react": "16.9",
|
||||
"@types/react": "^16.9",
|
||||
"@types/react-dom": "^16.9.8",
|
||||
"@types/zxcvbn": "^4.4.0",
|
||||
"babel-eslint": "^10.0.3",
|
||||
"babel-jest": "^24.9.0",
|
||||
|
@ -139,6 +144,7 @@
|
|||
"flow-parser": "^0.57.3",
|
||||
"glob": "^5.0.14",
|
||||
"jest": "^24.9.0",
|
||||
"jest-canvas-mock": "^2.2.0",
|
||||
"lolex": "^5.1.2",
|
||||
"matrix-mock-request": "^1.2.3",
|
||||
"matrix-react-test-utils": "^0.2.2",
|
||||
|
@ -158,6 +164,7 @@
|
|||
"testMatch": [
|
||||
"<rootDir>/test/**/*-test.js"
|
||||
],
|
||||
"setupFiles": ["jest-canvas-mock"],
|
||||
"setupFilesAfterEnv": [
|
||||
"<rootDir>/test/setupTests.js"
|
||||
],
|
||||
|
|
|
@ -335,6 +335,9 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
|
|||
.mx_Dialog_header.mx_Dialog_headerWithButton > .mx_Dialog_title {
|
||||
text-align: center;
|
||||
}
|
||||
.mx_Dialog_header.mx_Dialog_headerWithCancel > .mx_Dialog_title {
|
||||
margin-right: 20px; // leave space for the 'X' cancel button
|
||||
}
|
||||
|
||||
.mx_Dialog_title.danger {
|
||||
color: $warning-color;
|
||||
|
|
|
@ -63,7 +63,6 @@
|
|||
@import "./views/dialogs/_DeactivateAccountDialog.scss";
|
||||
@import "./views/dialogs/_DeviceVerifyDialog.scss";
|
||||
@import "./views/dialogs/_DevtoolsDialog.scss";
|
||||
@import "./views/dialogs/_EncryptedEventDialog.scss";
|
||||
@import "./views/dialogs/_GroupAddressPicker.scss";
|
||||
@import "./views/dialogs/_IncomingSasDialog.scss";
|
||||
@import "./views/dialogs/_InviteDialog.scss";
|
||||
|
@ -115,7 +114,9 @@
|
|||
@import "./views/elements/_RichText.scss";
|
||||
@import "./views/elements/_RoleButton.scss";
|
||||
@import "./views/elements/_RoomAliasField.scss";
|
||||
@import "./views/elements/_Slider.scss";
|
||||
@import "./views/elements/_Spinner.scss";
|
||||
@import "./views/elements/_StyledCheckbox.scss";
|
||||
@import "./views/elements/_SyntaxHighlight.scss";
|
||||
@import "./views/elements/_TextWithTooltip.scss";
|
||||
@import "./views/elements/_ToggleSwitch.scss";
|
||||
|
@ -123,7 +124,6 @@
|
|||
@import "./views/elements/_TooltipButton.scss";
|
||||
@import "./views/elements/_Validation.scss";
|
||||
@import "./views/emojipicker/_EmojiPicker.scss";
|
||||
@import "./views/globals/_MatrixToolbar.scss";
|
||||
@import "./views/groups/_GroupPublicityToggle.scss";
|
||||
@import "./views/groups/_GroupRoomList.scss";
|
||||
@import "./views/groups/_GroupUserSettings.scss";
|
||||
|
@ -162,6 +162,8 @@
|
|||
@import "./views/rooms/_EditMessageComposer.scss";
|
||||
@import "./views/rooms/_EntityTile.scss";
|
||||
@import "./views/rooms/_EventTile.scss";
|
||||
@import "./views/rooms/_GroupLayout.scss";
|
||||
@import "./views/rooms/_IRCLayout.scss";
|
||||
@import "./views/rooms/_InviteOnlyIcon.scss";
|
||||
@import "./views/rooms/_JumpToBottomButton.scss";
|
||||
@import "./views/rooms/_LinkPreviewWidget.scss";
|
||||
|
@ -200,10 +202,12 @@
|
|||
@import "./views/settings/_ProfileSettings.scss";
|
||||
@import "./views/settings/_SetIdServer.scss";
|
||||
@import "./views/settings/_SetIntegrationManager.scss";
|
||||
@import "./views/settings/_UpdateCheckButton.scss";
|
||||
@import "./views/settings/tabs/_SettingsTab.scss";
|
||||
@import "./views/settings/tabs/room/_GeneralRoomSettingsTab.scss";
|
||||
@import "./views/settings/tabs/room/_RolesRoomSettingsTab.scss";
|
||||
@import "./views/settings/tabs/room/_SecurityRoomSettingsTab.scss";
|
||||
@import "./views/settings/tabs/user/_AppearanceUserSettingsTab.scss";
|
||||
@import "./views/settings/tabs/user/_GeneralUserSettingsTab.scss";
|
||||
@import "./views/settings/tabs/user/_HelpUserSettingsTab.scss";
|
||||
@import "./views/settings/tabs/user/_MjolnirUserSettingsTab.scss";
|
||||
|
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
$font-1px: 0.067rem;
|
||||
$font-1-5px: 0.100rem;
|
||||
$font-2px: 0.133rem;
|
||||
$font-3px: 0.200rem;
|
||||
$font-4px: 0.267rem;
|
||||
|
|
|
@ -41,10 +41,6 @@ limitations under the License.
|
|||
height: 40px;
|
||||
}
|
||||
|
||||
.mx_MatrixChat_toolbarShowing {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.mx_MatrixChat {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
|
|
@ -63,6 +63,10 @@ limitations under the License.
|
|||
padding-left: 32px;
|
||||
padding-top: 8px;
|
||||
position: relative;
|
||||
|
||||
a {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_NotificationPanel .mx_EventTile_roomName a,
|
||||
|
|
|
@ -69,7 +69,7 @@ limitations under the License.
|
|||
height: 100%;
|
||||
}
|
||||
.mx_TagPanel .mx_TagPanel_tagTileContainer > div {
|
||||
height: $font-40px;
|
||||
height: 40px;
|
||||
padding: 10px 0 9px 0;
|
||||
}
|
||||
|
||||
|
@ -116,7 +116,7 @@ limitations under the License.
|
|||
position: absolute;
|
||||
left: -15px;
|
||||
border-radius: 0 3px 3px 0;
|
||||
top: -8px; // (16px / 2)
|
||||
top: -8px; // (16px from height / 2)
|
||||
}
|
||||
|
||||
.mx_TagPanel .mx_TagTile.mx_AccessibleButton:focus {
|
||||
|
|
|
@ -28,8 +28,8 @@ limitations under the License.
|
|||
margin: 0 4px;
|
||||
grid-row: 2 / 4;
|
||||
grid-column: 1;
|
||||
background-color: white;
|
||||
box-shadow: 0px 4px 12px $menu-box-shadow-color;
|
||||
background-color: $dark-panel-bg-color;
|
||||
box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.5);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
|
@ -37,16 +37,15 @@ limitations under the License.
|
|||
grid-row: 1 / 3;
|
||||
grid-column: 1;
|
||||
color: $primary-fg-color;
|
||||
background-color: $primary-bg-color;
|
||||
box-shadow: 0px 4px 12px $menu-box-shadow-color;
|
||||
background-color: $dark-panel-bg-color;
|
||||
box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.5);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
display: grid;
|
||||
grid-template-columns: 20px 1fr;
|
||||
column-gap: 10px;
|
||||
grid-template-columns: 22px 1fr;
|
||||
column-gap: 8px;
|
||||
row-gap: 4px;
|
||||
padding: 8px;
|
||||
padding-right: 16px;
|
||||
|
||||
&.mx_Toast_hasIcon {
|
||||
&::after {
|
||||
|
@ -68,17 +67,45 @@ limitations under the License.
|
|||
background-image: url("$(res)/img/e2e/warning.svg");
|
||||
}
|
||||
|
||||
h2, .mx_Toast_body {
|
||||
.mx_Toast_title, .mx_Toast_body {
|
||||
grid-column: 2;
|
||||
}
|
||||
}
|
||||
&:not(.mx_Toast_hasIcon) {
|
||||
padding-left: 12px;
|
||||
|
||||
h2 {
|
||||
grid-column: 1 / 3;
|
||||
grid-row: 1;
|
||||
margin: 0;
|
||||
font-size: $font-15px;
|
||||
font-weight: 600;
|
||||
.mx_Toast_title {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_Toast_title,
|
||||
.mx_Toast_description {
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.mx_Toast_title {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
h2 {
|
||||
grid-column: 1 / 3;
|
||||
grid-row: 1;
|
||||
margin: 0;
|
||||
font-size: $font-15px;
|
||||
font-weight: 600;
|
||||
display: inline;
|
||||
width: auto;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
span {
|
||||
padding-left: 8px;
|
||||
float: right;
|
||||
font-size: $font-12px;
|
||||
line-height: $font-22px;
|
||||
color: $muted-fg-color;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_Toast_body {
|
||||
|
@ -87,7 +114,13 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_Toast_buttons {
|
||||
float: right;
|
||||
display: flex;
|
||||
|
||||
.mx_FormButton {
|
||||
min-width: 96px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_Toast_description {
|
||||
|
@ -96,6 +129,15 @@ limitations under the License.
|
|||
text-overflow: ellipsis;
|
||||
margin: 4px 0 11px 0;
|
||||
font-size: $font-12px;
|
||||
|
||||
.mx_AccessibleButton_kind_link {
|
||||
font-size: inherit;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_Toast_deviceID {
|
||||
|
|
|
@ -43,7 +43,7 @@ limitations under the License.
|
|||
margin: 0 7px;
|
||||
mask: url('$(res)/img/feather-customised/dropdown-arrow.svg');
|
||||
mask-repeat: no-repeat;
|
||||
width: 10px;
|
||||
width: $font-22px;
|
||||
height: 6px;
|
||||
background-color: $roomsublist-label-fg-color;
|
||||
}
|
||||
|
|
|
@ -18,8 +18,3 @@ limitations under the License.
|
|||
margin-top: 10px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.mx_GroupAddressPicker_checkboxContainer input[type="checkbox"] {
|
||||
/* Stop flex from shrinking the checkbox */
|
||||
width: 20px;
|
||||
}
|
||||
|
|
|
@ -55,6 +55,7 @@ limitations under the License.
|
|||
margin-left: 5px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-repeat: none;
|
||||
}
|
||||
|
||||
.mx_ShareDialog_split {
|
||||
|
|
|
@ -21,6 +21,10 @@ limitations under the License.
|
|||
mask-image: url('$(res)/img/feather-customised/settings.svg');
|
||||
}
|
||||
|
||||
.mx_UserSettingsDialog_appearanceIcon::before {
|
||||
mask-image: url('$(res)/img/feather-customised/brush.svg');
|
||||
}
|
||||
|
||||
.mx_UserSettingsDialog_voiceIcon::before {
|
||||
mask-image: url('$(res)/img/feather-customised/phone.svg');
|
||||
}
|
||||
|
|
99
res/css/views/elements/_Slider.scss
Normal file
99
res/css/views/elements/_Slider.scss
Normal file
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
.mx_Slider {
|
||||
position: relative;
|
||||
margin: 0px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.mx_Slider_dotContainer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.mx_Slider_bar {
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
height: 1em;
|
||||
width: 100%;
|
||||
padding: 0 0.5em; // half the width of a dot.
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mx_Slider_bar > hr {
|
||||
width: 100%;
|
||||
height: 0.4em;
|
||||
background-color: $slider-background-color;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.mx_Slider_selection {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: calc(100% - 1em); // 2 * half the width of a dot
|
||||
height: 1em;
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.mx_Slider_selectionDot {
|
||||
position: absolute;
|
||||
width: 1.1em;
|
||||
height: 1.1em;
|
||||
background-color: $slider-selection-color;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 6px lightgrey;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.mx_Slider_selection > hr {
|
||||
margin: 0;
|
||||
border: 0.2em solid $slider-selection-color;
|
||||
}
|
||||
|
||||
.mx_Slider_dot {
|
||||
height: 1em;
|
||||
width: 1em;
|
||||
border-radius: 50%;
|
||||
background-color: $slider-background-color;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.mx_Slider_dotActive {
|
||||
background-color: $slider-selection-color;
|
||||
}
|
||||
|
||||
.mx_Slider_dotValue {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
color: $slider-background-color;
|
||||
}
|
||||
|
||||
// The following is a hack to center the labels without adding
|
||||
// any width to the slider's dots.
|
||||
.mx_Slider_labelContainer {
|
||||
width: 1em;
|
||||
}
|
||||
|
||||
.mx_Slider_label {
|
||||
position: relative;
|
||||
width: fit-content;
|
||||
left: -50%;
|
||||
}
|
66
res/css/views/elements/_StyledCheckbox.scss
Normal file
66
res/css/views/elements/_StyledCheckbox.scss
Normal file
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
|
||||
.mx_Checkbox {
|
||||
$size: $font-16px;
|
||||
$border-size: $font-1-5px;
|
||||
$border-radius: $font-4px;
|
||||
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
||||
input[type=checkbox] {
|
||||
display: none;
|
||||
|
||||
& + label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
& + label > .mx_Checkbox_background {
|
||||
display: inline-flex;
|
||||
position: relative;
|
||||
|
||||
flex-shrink: 0;
|
||||
|
||||
height: $size;
|
||||
width: $size;
|
||||
size: 0.5rem;
|
||||
|
||||
border: $border-size solid rgba($muted-fg-color, 0.5);
|
||||
box-sizing: border-box;
|
||||
border-radius: $border-radius;
|
||||
|
||||
img {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
filter: invert(100%);
|
||||
}
|
||||
}
|
||||
|
||||
&:checked + label > .mx_Checkbox_background {
|
||||
background: $accent-color;
|
||||
border-color: $accent-color;
|
||||
}
|
||||
|
||||
& + label > *:not(.mx_Checkbox_background) {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -190,7 +190,7 @@ limitations under the License.
|
|||
|
||||
.mx_EmojiPicker_footer {
|
||||
border-top: 1px solid $message-action-bar-border-color;
|
||||
height: 72px;
|
||||
min-height: 72px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
@ -1,73 +0,0 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_MatrixToolbar {
|
||||
background-color: $accent-color;
|
||||
color: $accent-fg-color;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mx_MatrixToolbar_warning {
|
||||
margin-left: 16px;
|
||||
margin-right: 8px;
|
||||
margin-top: -2px;
|
||||
}
|
||||
|
||||
.mx_MatrixToolbar_info {
|
||||
padding-left: 16px;
|
||||
padding-right: 8px;
|
||||
background-color: $info-bg-color;
|
||||
}
|
||||
|
||||
.mx_MatrixToolbar_error {
|
||||
padding-left: 16px;
|
||||
padding-right: 8px;
|
||||
background-color: $warning-bg-color;
|
||||
}
|
||||
|
||||
.mx_MatrixToolbar_content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.mx_MatrixToolbar_link {
|
||||
color: $accent-fg-color !important;
|
||||
text-decoration: underline !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mx_MatrixToolbar_clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mx_MatrixToolbar_close {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mx_MatrixToolbar_close img {
|
||||
display: block;
|
||||
float: right;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.mx_MatrixToolbar_action {
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.mx_MatrixToolbar_changelog {
|
||||
white-space: pre;
|
||||
}
|
|
@ -96,6 +96,10 @@ $AppsDrawerBodyHeight: 273px;
|
|||
height: $AppsDrawerBodyHeight;
|
||||
}
|
||||
|
||||
.mx_AppTile_persistedWrapper > div {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.mx_AppTile_mini .mx_AppTile_persistedWrapper {
|
||||
height: 114px;
|
||||
}
|
||||
|
|
|
@ -37,7 +37,6 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_EventTile_avatar {
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
left: 8px;
|
||||
cursor: pointer;
|
||||
|
@ -68,11 +67,9 @@ limitations under the License.
|
|||
display: inline-block; /* anti-zalgo, with overflow hidden */
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
padding-left: 65px; /* left gutter */
|
||||
padding-bottom: 0px;
|
||||
padding-top: 0px;
|
||||
margin: 0px;
|
||||
line-height: $font-17px;
|
||||
/* the next three lines, along with overflow hidden, truncate long display names */
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
|
@ -104,9 +101,7 @@ limitations under the License.
|
|||
visibility: hidden;
|
||||
white-space: nowrap;
|
||||
left: 0px;
|
||||
width: 46px; /* 8 + 30 (avatar) + 8 */
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
|
@ -117,10 +112,7 @@ limitations under the License.
|
|||
.mx_EventTile_line, .mx_EventTile_reply {
|
||||
position: relative;
|
||||
padding-left: 65px; /* left gutter */
|
||||
padding-top: 3px;
|
||||
padding-bottom: 3px;
|
||||
border-radius: 4px;
|
||||
line-height: $font-22px;
|
||||
}
|
||||
|
||||
.mx_RoomView_timeline_rr_enabled,
|
||||
|
@ -151,10 +143,6 @@ limitations under the License.
|
|||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.mx_EventTile_info .mx_EventTile_line {
|
||||
padding-left: 83px;
|
||||
}
|
||||
|
||||
/* HACK to override line-height which is already marked important elsewhere */
|
||||
.mx_EventTile_bigEmoji.mx_EventTile_bigEmoji {
|
||||
font-size: 48px !important;
|
||||
|
@ -171,10 +159,15 @@ limitations under the License.
|
|||
}
|
||||
|
||||
// Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies)
|
||||
// The first set is to handle the 'group layout' (default) and the second for the IRC layout
|
||||
.mx_EventTile_last > div > a > .mx_MessageTimestamp,
|
||||
.mx_EventTile:hover > div > a > .mx_MessageTimestamp,
|
||||
.mx_EventTile.mx_EventTile_actionBarFocused > div > a > .mx_MessageTimestamp,
|
||||
.mx_EventTile.focus-visible:focus-within > div > a > .mx_MessageTimestamp {
|
||||
.mx_EventTile.focus-visible:focus-within > div > a > .mx_MessageTimestamp,
|
||||
.mx_IRCLayout .mx_EventTile_last > a > .mx_MessageTimestamp,
|
||||
.mx_IRCLayout .mx_EventTile:hover > a > .mx_MessageTimestamp,
|
||||
.mx_IRCLayout .mx_EventTile.mx_EventTile_actionBarFocused > a > .mx_MessageTimestamp,
|
||||
.mx_IRCLayout .mx_EventTile.focus-visible:focus-within > a > .mx_MessageTimestamp {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
|
@ -560,84 +553,6 @@ limitations under the License.
|
|||
|
||||
/* end of overrides */
|
||||
|
||||
.mx_MatrixChat_useCompactLayout {
|
||||
.mx_EventTile {
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.mx_EventTile.mx_EventTile_info {
|
||||
// same as the padding for non-compact .mx_EventTile.mx_EventTile_info
|
||||
padding-top: 0px;
|
||||
font-size: $font-13px;
|
||||
.mx_EventTile_line, .mx_EventTile_reply {
|
||||
line-height: $font-20px;
|
||||
}
|
||||
.mx_EventTile_avatar {
|
||||
top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_EventTile .mx_SenderProfile {
|
||||
font-size: $font-13px;
|
||||
}
|
||||
|
||||
.mx_EventTile.mx_EventTile_emote {
|
||||
// add a bit more space for emotes so that avatars don't collide
|
||||
padding-top: 8px;
|
||||
.mx_EventTile_avatar {
|
||||
top: 2px;
|
||||
}
|
||||
.mx_EventTile_line, .mx_EventTile_reply {
|
||||
padding-top: 0px;
|
||||
padding-bottom: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_EventTile.mx_EventTile_emote.mx_EventTile_continuation {
|
||||
padding-top: 0;
|
||||
.mx_EventTile_line, .mx_EventTile_reply {
|
||||
padding-top: 0px;
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_EventTile_line, .mx_EventTile_reply {
|
||||
padding-top: 0px;
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
|
||||
.mx_EventTile_avatar {
|
||||
top: 2px;
|
||||
}
|
||||
|
||||
.mx_EventTile_e2eIcon {
|
||||
top: 3px;
|
||||
}
|
||||
|
||||
.mx_EventTile_readAvatars {
|
||||
top: 27px;
|
||||
}
|
||||
|
||||
.mx_EventTile_continuation .mx_EventTile_readAvatars,
|
||||
.mx_EventTile_emote .mx_EventTile_readAvatars {
|
||||
top: 5px;
|
||||
}
|
||||
|
||||
.mx_EventTile_info .mx_EventTile_readAvatars {
|
||||
top: 4px;
|
||||
}
|
||||
|
||||
.mx_RoomView_MessageList h2 {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.mx_EventTile_content .markdown-body {
|
||||
p, ul, ol, dl, blockquote, pre, table {
|
||||
margin-bottom: 4px; // 1/4 of the non-compact margin-bottom
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_EventTile_tileError {
|
||||
color: red;
|
||||
text-align: center;
|
||||
|
|
131
res/css/views/rooms/_GroupLayout.scss
Normal file
131
res/css/views/rooms/_GroupLayout.scss
Normal file
|
@ -0,0 +1,131 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
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.
|
||||
*/
|
||||
|
||||
$left-gutter: 65px;
|
||||
|
||||
.mx_GroupLayout {
|
||||
|
||||
.mx_EventTile {
|
||||
> .mx_SenderProfile {
|
||||
line-height: $font-17px;
|
||||
padding-left: $left-gutter;
|
||||
}
|
||||
|
||||
> .mx_EventTile_line {
|
||||
padding-left: $left-gutter;
|
||||
}
|
||||
|
||||
> .mx_EventTile_avatar {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.mx_MessageTimestamp {
|
||||
position: absolute;
|
||||
width: 46px; /* 8 + 30 (avatar) + 8 */
|
||||
}
|
||||
|
||||
.mx_EventTile_line, .mx_EventTile_reply {
|
||||
padding-top: 3px;
|
||||
padding-bottom: 3px;
|
||||
line-height: $font-22px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_EventTile_info .mx_EventTile_line {
|
||||
padding-left: calc($left-gutter + 18px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Compact layout overrides */
|
||||
|
||||
.mx_MatrixChat_useCompactLayout {
|
||||
.mx_EventTile_line, .mx_EventTile_reply {
|
||||
padding-top: 0px;
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
|
||||
.mx_EventTile {
|
||||
padding-top: 4px;
|
||||
|
||||
&.mx_EventTile_info {
|
||||
// same as the padding for non-compact .mx_EventTile.mx_EventTile_info
|
||||
padding-top: 0px;
|
||||
font-size: $font-13px;
|
||||
.mx_EventTile_line, .mx_EventTile_reply {
|
||||
line-height: $font-20px;
|
||||
}
|
||||
.mx_EventTile_avatar {
|
||||
top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SenderProfile {
|
||||
font-size: $font-13px;
|
||||
}
|
||||
|
||||
&.mx_EventTile_emote {
|
||||
// add a bit more space for emotes so that avatars don't collide
|
||||
padding-top: 8px;
|
||||
.mx_EventTile_avatar {
|
||||
top: 2px;
|
||||
}
|
||||
.mx_EventTile_line, .mx_EventTile_reply {
|
||||
padding-top: 0px;
|
||||
padding-bottom: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
&.mx_EventTile_emote.mx_EventTile_continuation {
|
||||
padding-top: 0;
|
||||
.mx_EventTile_line, .mx_EventTile_reply {
|
||||
padding-top: 0px;
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_EventTile_avatar {
|
||||
top: 2px;
|
||||
}
|
||||
|
||||
.mx_EventTile_e2eIcon {
|
||||
top: 3px;
|
||||
}
|
||||
|
||||
.mx_EventTile_readAvatars {
|
||||
top: 27px;
|
||||
}
|
||||
|
||||
.mx_EventTile_continuation .mx_EventTile_readAvatars,
|
||||
.mx_EventTile_emote .mx_EventTile_readAvatars {
|
||||
top: 5px;
|
||||
}
|
||||
|
||||
.mx_EventTile_info .mx_EventTile_readAvatars {
|
||||
top: 4px;
|
||||
}
|
||||
|
||||
.mx_EventTile_content .markdown-body {
|
||||
p, ul, ol, dl, blockquote, pre, table {
|
||||
margin-bottom: 4px; // 1/4 of the non-compact margin-bottom
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RoomView_MessageList h2 {
|
||||
margin-top: 6px;
|
||||
}
|
||||
}
|
218
res/css/views/rooms/_IRCLayout.scss
Normal file
218
res/css/views/rooms/_IRCLayout.scss
Normal file
|
@ -0,0 +1,218 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
$icon-width: 14px;
|
||||
$timestamp-width: 45px;
|
||||
$right-padding: 5px;
|
||||
$irc-line-height: $font-18px;
|
||||
|
||||
.mx_IRCLayout {
|
||||
--name-width: 70px;
|
||||
|
||||
line-height: $irc-line-height !important;
|
||||
|
||||
.mx_EventTile {
|
||||
|
||||
// timestamps are links which shouldn't be underlined
|
||||
> a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
padding-top: 0;
|
||||
|
||||
> * {
|
||||
margin-right: $right-padding;
|
||||
}
|
||||
|
||||
> .mx_EventTile_msgOption {
|
||||
order: 5;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
> .mx_SenderProfile {
|
||||
order: 2;
|
||||
flex-shrink: 0;
|
||||
width: var(--name-width);
|
||||
text-overflow: ellipsis;
|
||||
text-align: right;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: visible;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.mx_EventTile_line, .mx_EventTile_reply {
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
order: 3;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
> .mx_EventTile_avatar {
|
||||
order: 1;
|
||||
position: relative;
|
||||
top: 0;
|
||||
left: 0;
|
||||
flex-shrink: 0;
|
||||
height: $irc-line-height;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
// Need to use important to override the js provided height and width values.
|
||||
> .mx_BaseAvatar, .mx_BaseAvatar > * {
|
||||
height: $font-14px !important;
|
||||
width: $font-14px !important;
|
||||
font-size: $font-10px !important;
|
||||
line-height: $font-15px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_MessageTimestamp {
|
||||
font-size: $font-10px;
|
||||
width: $timestamp-width;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
> .mx_EventTile_e2eIcon {
|
||||
position: relative;
|
||||
right: unset;
|
||||
left: unset;
|
||||
padding: 0;
|
||||
order: 3;
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.mx_EventTile_line {
|
||||
.mx_EventTile_e2eIcon,
|
||||
.mx_TextualEvent,
|
||||
.mx_MTextBody,
|
||||
.mx_ReplyThread_wrapper_empty {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_EvenTile_line .mx_MessageActionBar,
|
||||
.mx_EvenTile_line .mx_ReplyThread_wrapper {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mx_EventTile_reply {
|
||||
order: 4;
|
||||
}
|
||||
|
||||
.mx_EditMessageComposer_buttons {
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_EventTile_emote {
|
||||
> .mx_EventTile_avatar {
|
||||
margin-left: calc(var(--name-width) + $icon-width + $right-padding);
|
||||
}
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.mx_EventListSummary {
|
||||
> .mx_EventTile_line {
|
||||
padding-left: calc(var(--name-width) + $icon-width + $timestamp-width + 3 * $right-padding); // 15 px of padding
|
||||
}
|
||||
|
||||
.mx_EventListSummary_avatars {
|
||||
padding: 0;
|
||||
margin: 0 9px 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_EventTile.mx_EventTile_info {
|
||||
.mx_EventTile_avatar {
|
||||
left: calc(var(--name-width) + 10px + $icon-width);
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.mx_EventTile_line {
|
||||
left: calc(var(--name-width) + 10px + $icon-width);
|
||||
}
|
||||
|
||||
.mx_TextualEvent {
|
||||
line-height: $irc-line-height;
|
||||
}
|
||||
}
|
||||
|
||||
// Suppress highlight thing from the normal Layout.
|
||||
.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line,
|
||||
.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line,
|
||||
.mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line {
|
||||
padding-left: 0;
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
.mx_SenderProfile_hover {
|
||||
background-color: $primary-bg-color;
|
||||
overflow: hidden;
|
||||
|
||||
> span {
|
||||
display: flex;
|
||||
|
||||
> .mx_SenderProfile_name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SenderProfile:hover {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.mx_SenderProfile_hover:hover {
|
||||
overflow: visible;
|
||||
width: max(auto, 100%);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.mx_ReplyThread {
|
||||
margin: 0;
|
||||
.mx_SenderProfile {
|
||||
width: unset;
|
||||
max-width: var(--name-width);
|
||||
}
|
||||
}
|
||||
|
||||
.mx_ProfileResizer {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 15px;
|
||||
left: calc(80px + var(--name-width));
|
||||
cursor: col-resize;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
// Need to use important to override the js provided height and width values.
|
||||
.mx_Flair > img {
|
||||
height: $font-14px !important;
|
||||
width: $font-14px !important;
|
||||
}
|
||||
}
|
|
@ -20,7 +20,7 @@ limitations under the License.
|
|||
flex-direction: row;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
height: $font-34px;
|
||||
height: 34px;
|
||||
margin: 0;
|
||||
padding: 0 8px 0 10px;
|
||||
position: relative;
|
||||
|
@ -81,6 +81,7 @@ limitations under the License.
|
|||
|
||||
.mx_RoomTile_avatar_container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.mx_RoomTile_avatar {
|
||||
|
|
23
res/css/views/settings/_UpdateCheckButton.scss
Normal file
23
res/css/views/settings/_UpdateCheckButton.scss
Normal file
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
.mx_UpdateCheckButton_summary {
|
||||
margin-left: 16px;
|
||||
|
||||
.mx_AccessibleButton_kind_link {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
.mx_AppearanceUserSettingsTab_fontSlider,
|
||||
.mx_AppearanceUserSettingsTab_themeSection .mx_Field,
|
||||
.mx_AppearanceUserSettingsTab_fontScaling .mx_Field {
|
||||
@mixin mx_Settings_fullWidthField;
|
||||
}
|
||||
|
||||
.mx_AppearanceUserSettingsTab_fontSlider {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 15px;
|
||||
background: $font-slider-bg-color;
|
||||
border-radius: 10px;
|
||||
font-size: 10px;
|
||||
margin-top: 24px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.mx_AppearanceUserSettingsTab_fontSlider_smallText {
|
||||
font-size: 15px;
|
||||
padding-right: 20px;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.mx_AppearanceUserSettingsTab_fontSlider_largeText {
|
||||
font-size: 18px;
|
||||
padding-left: 20px;
|
||||
padding-right: 5px;
|
||||
}
|
|
@ -14,8 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_GeneralUserSettingsTab_changePassword .mx_Field,
|
||||
.mx_GeneralUserSettingsTab_themeSection .mx_Field {
|
||||
.mx_GeneralUserSettingsTab_changePassword .mx_Field {
|
||||
@mixin mx_Settings_fullWidthField;
|
||||
}
|
||||
|
||||
|
|
5
res/img/feather-customised/brush.svg
Normal file
5
res/img/feather-customised/brush.svg
Normal file
|
@ -0,0 +1,5 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 16.5C12 18.9853 9.98528 21 7.5 21C6.21514 21 3 21 3 21C3 21 3 17.7004 3 16.5C3 14.0147 5.01472 12 7.5 12C9.98528 12 12 14.0147 12 16.5Z" stroke="#2E2F32" stroke-linejoin="round"/>
|
||||
<path d="M8.25 12L17.1955 3.69345C18.0632 2.88776 19.4127 2.91274 20.25 3.75V3.75C21.0873 4.58726 21.1122 5.93682 20.3065 6.80449L12 15.75" stroke="#2E2F32"/>
|
||||
<path d="M11.25 9C11.25 9 12.3929 9.45 13.5 10.5C14.6071 11.55 15 12.75 15 12.75" stroke="#2E2F32"/>
|
||||
</svg>
|
After Width: | Height: | Size: 556 B |
|
@ -180,6 +180,9 @@ $breadcrumb-placeholder-bg-color: #272c35;
|
|||
|
||||
$user-tile-hover-bg-color: $header-panel-bg-color;
|
||||
|
||||
// FontSlider colors
|
||||
$font-slider-bg-color: $room-highlight-color;
|
||||
|
||||
// ***** Mixins! *****
|
||||
|
||||
@define-mixin mx_DialogButton {
|
||||
|
|
|
@ -262,6 +262,10 @@ $togglesw-off-color: #c1c9d6;
|
|||
$togglesw-on-color: $accent-color;
|
||||
$togglesw-ball-color: #fff;
|
||||
|
||||
// Slider
|
||||
$slider-selection-color: $accent-color;
|
||||
$slider-background-color: #c1c9d6;
|
||||
|
||||
$progressbar-color: #000;
|
||||
|
||||
$room-warning-bg-color: $yellow-background;
|
||||
|
@ -302,6 +306,9 @@ $breadcrumb-placeholder-bg-color: #e8eef5;
|
|||
|
||||
$user-tile-hover-bg-color: $header-panel-bg-color;
|
||||
|
||||
// FontSlider colors
|
||||
$font-slider-bg-color: rgba($input-darker-bg-color, 0.2);
|
||||
|
||||
// ***** Mixins! *****
|
||||
|
||||
@define-mixin mx_DialogButton {
|
||||
|
|
19
src/@types/common.ts
Normal file
19
src/@types/common.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
// Based on https://stackoverflow.com/a/53229857/3532235
|
||||
export type Without<T, U> = {[P in Exclude<keyof T, keyof U>] ? : never}
|
||||
export type XOR<T, U> = (T | U) extends object ? (Without<T, U> & U) | (Without<U, T> & T) : T | U;
|
9
src/@types/global.d.ts
vendored
9
src/@types/global.d.ts
vendored
|
@ -15,13 +15,22 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import * as ModernizrStatic from "modernizr";
|
||||
import ContentMessages from "../ContentMessages";
|
||||
import { IMatrixClientPeg } from "../MatrixClientPeg";
|
||||
import ToastStore from "../stores/ToastStore";
|
||||
import DeviceListener from "../DeviceListener";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
Modernizr: ModernizrStatic;
|
||||
mxMatrixClientPeg: IMatrixClientPeg;
|
||||
Olm: {
|
||||
init: () => Promise<void>;
|
||||
};
|
||||
|
||||
mx_ContentMessages: ContentMessages;
|
||||
mx_ToastStore: ToastStore;
|
||||
mx_DeviceListener: DeviceListener;
|
||||
}
|
||||
|
||||
// workaround for https://github.com/microsoft/TypeScript/issues/30933
|
||||
|
|
|
@ -19,6 +19,7 @@ import {MatrixClientPeg} from './MatrixClientPeg';
|
|||
import DMRoomMap from './utils/DMRoomMap';
|
||||
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
|
||||
|
||||
// Not to be used for BaseAvatar urls as that has similar default avatar fallback already
|
||||
export function avatarUrlForMember(member, width, height, resizeMethod) {
|
||||
let url;
|
||||
if (member && member.getAvatarUrl) {
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
// @flow
|
||||
|
||||
/*
|
||||
Copyright 2016 Aviral Dasgupta
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
|
@ -19,9 +17,23 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {MatrixClient} from "matrix-js-sdk";
|
||||
import dis from './dispatcher';
|
||||
import {MatrixClient} from "matrix-js-sdk/src/client";
|
||||
import dis from './dispatcher/dispatcher';
|
||||
import BaseEventIndexManager from './indexing/BaseEventIndexManager';
|
||||
import {ActionPayload} from "./dispatcher/payloads";
|
||||
import {CheckUpdatesPayload} from "./dispatcher/payloads/CheckUpdatesPayload";
|
||||
import {Action} from "./dispatcher/actions";
|
||||
import {hideToast as hideUpdateToast} from "./toasts/UpdateToast";
|
||||
|
||||
export enum UpdateCheckStatus {
|
||||
Checking = "CHECKING",
|
||||
Error = "ERROR",
|
||||
NotAvailable = "NOTAVAILABLE",
|
||||
Downloading = "DOWNLOADING",
|
||||
Ready = "READY",
|
||||
}
|
||||
|
||||
const UPDATE_DEFER_KEY = "mx_defer_update";
|
||||
|
||||
/**
|
||||
* Base class for classes that provide platform-specific functionality
|
||||
|
@ -29,27 +41,25 @@ import BaseEventIndexManager from './indexing/BaseEventIndexManager';
|
|||
*
|
||||
* Instances of this class are provided by the application.
|
||||
*/
|
||||
export default class BasePlatform {
|
||||
constructor() {
|
||||
this.notificationCount = 0;
|
||||
this.errorDidOccur = false;
|
||||
export default abstract class BasePlatform {
|
||||
protected notificationCount = 0;
|
||||
protected errorDidOccur = false;
|
||||
|
||||
dis.register(this._onAction.bind(this));
|
||||
constructor() {
|
||||
dis.register(this.onAction);
|
||||
}
|
||||
|
||||
_onAction(payload: Object) {
|
||||
protected onAction = (payload: ActionPayload) => {
|
||||
switch (payload.action) {
|
||||
case 'on_client_not_viable':
|
||||
case 'on_logged_out':
|
||||
this.setNotificationCount(0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Used primarily for Analytics
|
||||
getHumanReadableName(): string {
|
||||
return 'Base Platform';
|
||||
}
|
||||
abstract getHumanReadableName(): string;
|
||||
|
||||
setNotificationCount(count: number) {
|
||||
this.notificationCount = count;
|
||||
|
@ -59,6 +69,53 @@ export default class BasePlatform {
|
|||
this.errorDidOccur = errorDidOccur;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether we can call checkForUpdate on this platform build
|
||||
*/
|
||||
async canSelfUpdate(): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
startUpdateCheck() {
|
||||
hideUpdateToast();
|
||||
localStorage.removeItem(UPDATE_DEFER_KEY);
|
||||
dis.dispatch<CheckUpdatesPayload>({
|
||||
action: Action.CheckUpdates,
|
||||
status: UpdateCheckStatus.Checking,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the currently running app to the latest available version
|
||||
* and replace this instance of the app with the new version.
|
||||
*/
|
||||
installUpdate() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the version update has been deferred and that deferment is still in effect
|
||||
* @param newVersion the version string to check
|
||||
*/
|
||||
protected shouldShowUpdate(newVersion: string): boolean {
|
||||
try {
|
||||
const [version, deferUntil] = JSON.parse(localStorage.getItem(UPDATE_DEFER_KEY));
|
||||
return newVersion !== version || Date.now() > deferUntil;
|
||||
} catch (e) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ignore the pending update and don't prompt about this version
|
||||
* until the next morning (8am).
|
||||
*/
|
||||
deferUpdate(newVersion: string) {
|
||||
const date = new Date(Date.now() + 24 * 60 * 60 * 1000);
|
||||
date.setHours(8, 0, 0, 0); // set to next 8am
|
||||
localStorage.setItem(UPDATE_DEFER_KEY, JSON.stringify([newVersion, date.getTime()]));
|
||||
hideUpdateToast();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the platform supports displaying
|
||||
* notifications, otherwise false.
|
||||
|
@ -84,22 +141,17 @@ export default class BasePlatform {
|
|||
* that is 'granted' if the user allowed the request or
|
||||
* 'denied' otherwise.
|
||||
*/
|
||||
requestNotificationPermission(): Promise<string> {
|
||||
}
|
||||
abstract requestNotificationPermission(): Promise<string>;
|
||||
|
||||
displayNotification(title: string, msg: string, avatarUrl: string, room: Object) {
|
||||
}
|
||||
abstract displayNotification(title: string, msg: string, avatarUrl: string, room: Object);
|
||||
|
||||
loudNotification(ev: Event, room: Object) {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a promise that resolves to a string representing
|
||||
* the current version of the application.
|
||||
* Returns a promise that resolves to a string representing the current version of the application.
|
||||
*/
|
||||
getAppVersion(): Promise<string> {
|
||||
throw new Error("getAppVersion not implemented!");
|
||||
}
|
||||
abstract getAppVersion(): Promise<string>;
|
||||
|
||||
/*
|
||||
* If it's not expected that capturing the screen will work
|
||||
|
@ -114,20 +166,18 @@ export default class BasePlatform {
|
|||
* Restarts the application, without neccessarily reloading
|
||||
* any application code
|
||||
*/
|
||||
reload() {
|
||||
throw new Error("reload not implemented!");
|
||||
}
|
||||
abstract reload();
|
||||
|
||||
supportsAutoLaunch(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
// XXX: Surely this should be a setting like any other?
|
||||
async getAutoLaunchEnabled(): boolean {
|
||||
async getAutoLaunchEnabled(): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
async setAutoLaunchEnabled(enabled: boolean): void {
|
||||
async setAutoLaunchEnabled(enabled: boolean): Promise<void> {
|
||||
throw new Error("Unimplemented");
|
||||
}
|
||||
|
||||
|
@ -135,11 +185,11 @@ export default class BasePlatform {
|
|||
return false;
|
||||
}
|
||||
|
||||
async getAutoHideMenuBarEnabled(): boolean {
|
||||
async getAutoHideMenuBarEnabled(): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
async setAutoHideMenuBarEnabled(enabled: boolean): void {
|
||||
async setAutoHideMenuBarEnabled(enabled: boolean): Promise<void> {
|
||||
throw new Error("Unimplemented");
|
||||
}
|
||||
|
||||
|
@ -147,11 +197,11 @@ export default class BasePlatform {
|
|||
return false;
|
||||
}
|
||||
|
||||
async getMinimizeToTrayEnabled(): boolean {
|
||||
async getMinimizeToTrayEnabled(): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
async setMinimizeToTrayEnabled(enabled: boolean): void {
|
||||
async setMinimizeToTrayEnabled(enabled: boolean): Promise<void> {
|
||||
throw new Error("Unimplemented");
|
||||
}
|
||||
|
||||
|
@ -190,4 +240,35 @@ export default class BasePlatform {
|
|||
onKeyDown(ev: KeyboardEvent): boolean {
|
||||
return false; // no shortcuts implemented
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a previously stored pickle key. The pickle key is used for
|
||||
* encrypting libolm objects.
|
||||
* @param {string} userId the user ID for the user that the pickle key is for.
|
||||
* @param {string} userId the device ID that the pickle key is for.
|
||||
* @returns {string|null} the previously stored pickle key, or null if no
|
||||
* pickle key has been stored.
|
||||
*/
|
||||
async getPickleKey(userId: string, deviceId: string): Promise<string | null> {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and store a pickle key for encrypting libolm objects.
|
||||
* @param {string} userId the user ID for the user that the pickle key is for.
|
||||
* @param {string} userId the device ID that the pickle key is for.
|
||||
* @returns {string|null} the pickle key, or null if the platform does not
|
||||
* support storing pickle keys.
|
||||
*/
|
||||
async createPickleKey(userId: string, deviceId: string): Promise<string | null> {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a previously stored pickle key from storage.
|
||||
* @param {string} userId the user ID for the user that the pickle key is for.
|
||||
* @param {string} userId the device ID that the pickle key is for.
|
||||
*/
|
||||
async destroyPickleKey(userId: string, deviceId: string): Promise<void> {
|
||||
}
|
||||
}
|
|
@ -59,7 +59,7 @@ import Modal from './Modal';
|
|||
import * as sdk from './index';
|
||||
import { _t } from './languageHandler';
|
||||
import Matrix from 'matrix-js-sdk';
|
||||
import dis from './dispatcher';
|
||||
import dis from './dispatcher/dispatcher';
|
||||
import { showUnknownDeviceDialogForCalls } from './cryptodevices';
|
||||
import WidgetUtils from './utils/WidgetUtils';
|
||||
import WidgetEchoStore from './stores/WidgetEchoStore';
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2019 New Vector Ltd
|
||||
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.
|
||||
|
@ -15,17 +16,18 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import React from "react";
|
||||
import extend from './extend';
|
||||
import dis from './dispatcher';
|
||||
import dis from './dispatcher/dispatcher';
|
||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
||||
import {MatrixClient} from "matrix-js-sdk/src/client";
|
||||
import * as sdk from './index';
|
||||
import { _t } from './languageHandler';
|
||||
import Modal from './Modal';
|
||||
import RoomViewStore from './stores/RoomViewStore';
|
||||
import encrypt from "browser-encrypt-attachment";
|
||||
import extractPngChunks from "png-chunks-extract";
|
||||
import Spinner from "./components/views/elements/Spinner";
|
||||
|
||||
// Polyfill for Canvas.toBlob API using Canvas.toDataURL
|
||||
import "blueimp-canvas-to-blob";
|
||||
|
@ -39,6 +41,50 @@ const PHYS_HIDPI = [0x00, 0x00, 0x16, 0x25, 0x00, 0x00, 0x16, 0x25, 0x01];
|
|||
|
||||
export class UploadCanceledError extends Error {}
|
||||
|
||||
type ThumbnailableElement = HTMLImageElement | HTMLVideoElement;
|
||||
|
||||
interface IUpload {
|
||||
fileName: string;
|
||||
roomId: string;
|
||||
total: number;
|
||||
loaded: number;
|
||||
promise: Promise<any>;
|
||||
canceled?: boolean;
|
||||
}
|
||||
|
||||
interface IMediaConfig {
|
||||
"m.upload.size"?: number;
|
||||
}
|
||||
|
||||
interface IContent {
|
||||
body: string;
|
||||
msgtype: string;
|
||||
info: {
|
||||
size: number;
|
||||
mimetype?: string;
|
||||
};
|
||||
file?: string;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
interface IThumbnail {
|
||||
info: {
|
||||
thumbnail_info: {
|
||||
w: number;
|
||||
h: number;
|
||||
mimetype: string;
|
||||
size: number;
|
||||
};
|
||||
w: number;
|
||||
h: number;
|
||||
};
|
||||
thumbnail: Blob;
|
||||
}
|
||||
|
||||
interface IAbortablePromise<T> extends Promise<T> {
|
||||
abort(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a thumbnail for a image DOM element.
|
||||
* The image will be smaller than MAX_WIDTH and MAX_HEIGHT.
|
||||
|
@ -51,13 +97,13 @@ export class UploadCanceledError extends Error {}
|
|||
* about the original image and the thumbnail.
|
||||
*
|
||||
* @param {HTMLElement} element The element to thumbnail.
|
||||
* @param {integer} inputWidth The width of the image in the input element.
|
||||
* @param {integer} inputHeight the width of the image in the input element.
|
||||
* @param {number} inputWidth The width of the image in the input element.
|
||||
* @param {number} inputHeight the width of the image in the input element.
|
||||
* @param {String} mimeType The mimeType to save the blob as.
|
||||
* @return {Promise} A promise that resolves with an object with an info key
|
||||
* and a thumbnail key.
|
||||
*/
|
||||
function createThumbnail(element, inputWidth, inputHeight, mimeType) {
|
||||
function createThumbnail(element: ThumbnailableElement, inputWidth: number, inputHeight: number, mimeType: string): Promise<IThumbnail> {
|
||||
return new Promise((resolve) => {
|
||||
let targetWidth = inputWidth;
|
||||
let targetHeight = inputHeight;
|
||||
|
@ -98,7 +144,7 @@ function createThumbnail(element, inputWidth, inputHeight, mimeType) {
|
|||
* @param {File} imageFile The file to load in an image element.
|
||||
* @return {Promise} A promise that resolves with the html image element.
|
||||
*/
|
||||
async function loadImageElement(imageFile) {
|
||||
async function loadImageElement(imageFile: File) {
|
||||
// Load the file into an html element
|
||||
const img = document.createElement("img");
|
||||
const objectUrl = URL.createObjectURL(imageFile);
|
||||
|
@ -128,8 +174,7 @@ async function loadImageElement(imageFile) {
|
|||
for (const chunk of chunks) {
|
||||
if (chunk.name === 'pHYs') {
|
||||
if (chunk.data.byteLength !== PHYS_HIDPI.length) return;
|
||||
const hidpi = chunk.data.every((val, i) => val === PHYS_HIDPI[i]);
|
||||
return hidpi;
|
||||
return chunk.data.every((val, i) => val === PHYS_HIDPI[i]);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
@ -152,7 +197,7 @@ async function loadImageElement(imageFile) {
|
|||
*/
|
||||
function infoForImageFile(matrixClient, roomId, imageFile) {
|
||||
let thumbnailType = "image/png";
|
||||
if (imageFile.type == "image/jpeg") {
|
||||
if (imageFile.type === "image/jpeg") {
|
||||
thumbnailType = "image/jpeg";
|
||||
}
|
||||
|
||||
|
@ -175,15 +220,15 @@ function infoForImageFile(matrixClient, roomId, imageFile) {
|
|||
* @param {File} videoFile The file to load in an video element.
|
||||
* @return {Promise} A promise that resolves with the video image element.
|
||||
*/
|
||||
function loadVideoElement(videoFile) {
|
||||
function loadVideoElement(videoFile): Promise<HTMLVideoElement> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Load the file into an html element
|
||||
const video = document.createElement("video");
|
||||
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = function(e) {
|
||||
video.src = e.target.result;
|
||||
reader.onload = function(ev) {
|
||||
video.src = ev.target.result as string;
|
||||
|
||||
// Once ready, returns its size
|
||||
// Wait until we have enough data to thumbnail the first frame.
|
||||
|
@ -231,11 +276,11 @@ function infoForVideoFile(matrixClient, roomId, videoFile) {
|
|||
* @return {Promise} A promise that resolves with an ArrayBuffer when the file
|
||||
* is read.
|
||||
*/
|
||||
function readFileAsArrayBuffer(file) {
|
||||
function readFileAsArrayBuffer(file: File | Blob): Promise<ArrayBuffer> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
resolve(e.target.result);
|
||||
resolve(e.target.result as ArrayBuffer);
|
||||
};
|
||||
reader.onerror = function(e) {
|
||||
reject(e);
|
||||
|
@ -257,11 +302,11 @@ function readFileAsArrayBuffer(file) {
|
|||
* If the file is unencrypted then the object will have a "url" key.
|
||||
* If the file is encrypted then the object will have a "file" key.
|
||||
*/
|
||||
function uploadFile(matrixClient, roomId, file, progressHandler) {
|
||||
function uploadFile(matrixClient: MatrixClient, roomId: string, file: File | Blob, progressHandler?: any) {
|
||||
let canceled = false;
|
||||
if (matrixClient.isRoomEncrypted(roomId)) {
|
||||
// If the room is encrypted then encrypt the file before uploading it.
|
||||
// First read the file into memory.
|
||||
let canceled = false;
|
||||
let uploadPromise;
|
||||
let encryptInfo;
|
||||
const prom = readFileAsArrayBuffer(file).then(function(data) {
|
||||
|
@ -278,9 +323,9 @@ function uploadFile(matrixClient, roomId, file, progressHandler) {
|
|||
progressHandler: progressHandler,
|
||||
includeFilename: false,
|
||||
});
|
||||
|
||||
return uploadPromise;
|
||||
}).then(function(url) {
|
||||
if (canceled) throw new UploadCanceledError();
|
||||
// If the attachment is encrypted then bundle the URL along
|
||||
// with the information needed to decrypt the attachment and
|
||||
// add it under a file key.
|
||||
|
@ -290,7 +335,7 @@ function uploadFile(matrixClient, roomId, file, progressHandler) {
|
|||
}
|
||||
return {"file": encryptInfo};
|
||||
});
|
||||
prom.abort = () => {
|
||||
(prom as IAbortablePromise<any>).abort = () => {
|
||||
canceled = true;
|
||||
if (uploadPromise) MatrixClientPeg.get().cancelUpload(uploadPromise);
|
||||
};
|
||||
|
@ -300,55 +345,23 @@ function uploadFile(matrixClient, roomId, file, progressHandler) {
|
|||
progressHandler: progressHandler,
|
||||
});
|
||||
const promise1 = basePromise.then(function(url) {
|
||||
if (canceled) throw new UploadCanceledError();
|
||||
// If the attachment isn't encrypted then include the URL directly.
|
||||
return {"url": url};
|
||||
});
|
||||
// XXX: copy over the abort method to the new promise
|
||||
promise1.abort = basePromise.abort;
|
||||
promise1.abort = () => {
|
||||
canceled = true;
|
||||
MatrixClientPeg.get().cancelUpload(basePromise);
|
||||
};
|
||||
return promise1;
|
||||
}
|
||||
}
|
||||
|
||||
export default class ContentMessages {
|
||||
constructor() {
|
||||
this.inprogress = [];
|
||||
this.nextId = 0;
|
||||
this._mediaConfig = null;
|
||||
}
|
||||
private inprogress: IUpload[] = [];
|
||||
private mediaConfig: IMediaConfig = null;
|
||||
|
||||
static sharedInstance() {
|
||||
if (global.mx_ContentMessages === undefined) {
|
||||
global.mx_ContentMessages = new ContentMessages();
|
||||
}
|
||||
return global.mx_ContentMessages;
|
||||
}
|
||||
|
||||
_isFileSizeAcceptable(file) {
|
||||
if (this._mediaConfig !== null &&
|
||||
this._mediaConfig["m.upload.size"] !== undefined &&
|
||||
file.size > this._mediaConfig["m.upload.size"]) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
_ensureMediaConfigFetched() {
|
||||
if (this._mediaConfig !== null) return;
|
||||
|
||||
console.log("[Media Config] Fetching");
|
||||
return MatrixClientPeg.get().getMediaConfig().then((config) => {
|
||||
console.log("[Media Config] Fetched config:", config);
|
||||
return config;
|
||||
}).catch(() => {
|
||||
// Media repo can't or won't report limits, so provide an empty object (no limits).
|
||||
console.log("[Media Config] Could not fetch config, so not limiting uploads.");
|
||||
return {};
|
||||
}).then((config) => {
|
||||
this._mediaConfig = config;
|
||||
});
|
||||
}
|
||||
|
||||
sendStickerContentToRoom(url, roomId, info, text, matrixClient) {
|
||||
sendStickerContentToRoom(url: string, roomId: string, info: string, text: string, matrixClient: MatrixClient) {
|
||||
return MatrixClientPeg.get().sendStickerMessage(roomId, url, info, text).catch((e) => {
|
||||
console.warn(`Failed to send content with URL ${url} to room ${roomId}`, e);
|
||||
throw e;
|
||||
|
@ -356,14 +369,14 @@ export default class ContentMessages {
|
|||
}
|
||||
|
||||
getUploadLimit() {
|
||||
if (this._mediaConfig !== null && this._mediaConfig["m.upload.size"] !== undefined) {
|
||||
return this._mediaConfig["m.upload.size"];
|
||||
if (this.mediaConfig !== null && this.mediaConfig["m.upload.size"] !== undefined) {
|
||||
return this.mediaConfig["m.upload.size"];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async sendContentListToRoom(files, roomId, matrixClient) {
|
||||
async sendContentListToRoom(files: File[], roomId: string, matrixClient: MatrixClient) {
|
||||
if (matrixClient.isGuest()) {
|
||||
dis.dispatch({action: 'require_registration'});
|
||||
return;
|
||||
|
@ -372,32 +385,32 @@ export default class ContentMessages {
|
|||
const isQuoting = Boolean(RoomViewStore.getQuotingEvent());
|
||||
if (isQuoting) {
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
const shouldUpload = await new Promise((resolve) => {
|
||||
Modal.createTrackedDialog('Upload Reply Warning', '', QuestionDialog, {
|
||||
title: _t('Replying With Files'),
|
||||
description: (
|
||||
<div>{_t(
|
||||
'At this time it is not possible to reply with a file. ' +
|
||||
'Would you like to upload this file without replying?',
|
||||
)}</div>
|
||||
),
|
||||
hasCancelButton: true,
|
||||
button: _t("Continue"),
|
||||
onFinished: (shouldUpload) => {
|
||||
resolve(shouldUpload);
|
||||
},
|
||||
});
|
||||
const {finished} = Modal.createTrackedDialog('Upload Reply Warning', '', QuestionDialog, {
|
||||
title: _t('Replying With Files'),
|
||||
description: (
|
||||
<div>{_t(
|
||||
'At this time it is not possible to reply with a file. ' +
|
||||
'Would you like to upload this file without replying?',
|
||||
)}</div>
|
||||
),
|
||||
hasCancelButton: true,
|
||||
button: _t("Continue"),
|
||||
});
|
||||
const [shouldUpload]: [boolean] = await finished;
|
||||
if (!shouldUpload) return;
|
||||
}
|
||||
|
||||
await this._ensureMediaConfigFetched();
|
||||
if (!this.mediaConfig) { // hot-path optimization to not flash a spinner if we don't need to
|
||||
const modal = Modal.createDialog(Spinner, null, 'mx_Dialog_spinner');
|
||||
await this.ensureMediaConfigFetched();
|
||||
modal.close();
|
||||
}
|
||||
|
||||
const tooBigFiles = [];
|
||||
const okFiles = [];
|
||||
|
||||
for (let i = 0; i < files.length; ++i) {
|
||||
if (this._isFileSizeAcceptable(files[i])) {
|
||||
if (this.isFileSizeAcceptable(files[i])) {
|
||||
okFiles.push(files[i]);
|
||||
} else {
|
||||
tooBigFiles.push(files[i]);
|
||||
|
@ -406,17 +419,12 @@ export default class ContentMessages {
|
|||
|
||||
if (tooBigFiles.length > 0) {
|
||||
const UploadFailureDialog = sdk.getComponent("dialogs.UploadFailureDialog");
|
||||
const uploadFailureDialogPromise = new Promise((resolve) => {
|
||||
Modal.createTrackedDialog('Upload Failure', '', UploadFailureDialog, {
|
||||
badFiles: tooBigFiles,
|
||||
totalFiles: files.length,
|
||||
contentMessages: this,
|
||||
onFinished: (shouldContinue) => {
|
||||
resolve(shouldContinue);
|
||||
},
|
||||
});
|
||||
const {finished} = Modal.createTrackedDialog('Upload Failure', '', UploadFailureDialog, {
|
||||
badFiles: tooBigFiles,
|
||||
totalFiles: files.length,
|
||||
contentMessages: this,
|
||||
});
|
||||
const shouldContinue = await uploadFailureDialogPromise;
|
||||
const [shouldContinue]: [boolean] = await finished;
|
||||
if (!shouldContinue) return;
|
||||
}
|
||||
|
||||
|
@ -428,31 +436,47 @@ export default class ContentMessages {
|
|||
for (let i = 0; i < okFiles.length; ++i) {
|
||||
const file = okFiles[i];
|
||||
if (!uploadAll) {
|
||||
const shouldContinue = await new Promise((resolve) => {
|
||||
Modal.createTrackedDialog('Upload Files confirmation', '', UploadConfirmDialog, {
|
||||
file,
|
||||
currentIndex: i,
|
||||
totalFiles: okFiles.length,
|
||||
onFinished: (shouldContinue, shouldUploadAll) => {
|
||||
if (shouldUploadAll) {
|
||||
uploadAll = true;
|
||||
}
|
||||
resolve(shouldContinue);
|
||||
},
|
||||
});
|
||||
const {finished} = Modal.createTrackedDialog('Upload Files confirmation', '', UploadConfirmDialog, {
|
||||
file,
|
||||
currentIndex: i,
|
||||
totalFiles: okFiles.length,
|
||||
});
|
||||
const [shouldContinue, shouldUploadAll]: [boolean, boolean] = await finished;
|
||||
if (!shouldContinue) break;
|
||||
if (shouldUploadAll) {
|
||||
uploadAll = true;
|
||||
}
|
||||
}
|
||||
promBefore = this._sendContentToRoom(file, roomId, matrixClient, promBefore);
|
||||
promBefore = this.sendContentToRoom(file, roomId, matrixClient, promBefore);
|
||||
}
|
||||
}
|
||||
|
||||
_sendContentToRoom(file, roomId, matrixClient, promBefore) {
|
||||
const content = {
|
||||
getCurrentUploads() {
|
||||
return this.inprogress.filter(u => !u.canceled);
|
||||
}
|
||||
|
||||
cancelUpload(promise: Promise<any>) {
|
||||
let upload: IUpload;
|
||||
for (let i = 0; i < this.inprogress.length; ++i) {
|
||||
if (this.inprogress[i].promise === promise) {
|
||||
upload = this.inprogress[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (upload) {
|
||||
upload.canceled = true;
|
||||
MatrixClientPeg.get().cancelUpload(upload.promise);
|
||||
dis.dispatch({action: 'upload_canceled', upload});
|
||||
}
|
||||
}
|
||||
|
||||
private sendContentToRoom(file: File, roomId: string, matrixClient: MatrixClient, promBefore: Promise<any>) {
|
||||
const content: IContent = {
|
||||
body: file.name || 'Attachment',
|
||||
info: {
|
||||
size: file.size,
|
||||
},
|
||||
msgtype: "", // set later
|
||||
};
|
||||
|
||||
// if we have a mime type for the file, add it to the message metadata
|
||||
|
@ -461,25 +485,25 @@ export default class ContentMessages {
|
|||
}
|
||||
|
||||
const prom = new Promise((resolve) => {
|
||||
if (file.type.indexOf('image/') == 0) {
|
||||
if (file.type.indexOf('image/') === 0) {
|
||||
content.msgtype = 'm.image';
|
||||
infoForImageFile(matrixClient, roomId, file).then((imageInfo)=>{
|
||||
infoForImageFile(matrixClient, roomId, file).then((imageInfo) => {
|
||||
extend(content.info, imageInfo);
|
||||
resolve();
|
||||
}, (error)=>{
|
||||
console.error(error);
|
||||
}, (e) => {
|
||||
console.error(e);
|
||||
content.msgtype = 'm.file';
|
||||
resolve();
|
||||
});
|
||||
} else if (file.type.indexOf('audio/') == 0) {
|
||||
} else if (file.type.indexOf('audio/') === 0) {
|
||||
content.msgtype = 'm.audio';
|
||||
resolve();
|
||||
} else if (file.type.indexOf('video/') == 0) {
|
||||
} else if (file.type.indexOf('video/') === 0) {
|
||||
content.msgtype = 'm.video';
|
||||
infoForVideoFile(matrixClient, roomId, file).then((videoInfo)=>{
|
||||
infoForVideoFile(matrixClient, roomId, file).then((videoInfo) => {
|
||||
extend(content.info, videoInfo);
|
||||
resolve();
|
||||
}, (error)=>{
|
||||
}, (e) => {
|
||||
content.msgtype = 'm.file';
|
||||
resolve();
|
||||
});
|
||||
|
@ -489,11 +513,17 @@ export default class ContentMessages {
|
|||
}
|
||||
});
|
||||
|
||||
const upload = {
|
||||
// create temporary abort handler for before the actual upload gets passed off to js-sdk
|
||||
(prom as IAbortablePromise<any>).abort = () => {
|
||||
upload.canceled = true;
|
||||
};
|
||||
|
||||
const upload: IUpload = {
|
||||
fileName: file.name || 'Attachment',
|
||||
roomId: roomId,
|
||||
total: 0,
|
||||
total: file.size,
|
||||
loaded: 0,
|
||||
promise: prom,
|
||||
};
|
||||
this.inprogress.push(upload);
|
||||
dis.dispatch({action: 'upload_started'});
|
||||
|
@ -501,15 +531,15 @@ export default class ContentMessages {
|
|||
// Focus the composer view
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
|
||||
let error;
|
||||
|
||||
function onProgress(ev) {
|
||||
upload.total = ev.total;
|
||||
upload.loaded = ev.loaded;
|
||||
dis.dispatch({action: 'upload_progress', upload: upload});
|
||||
}
|
||||
|
||||
let error;
|
||||
return prom.then(function() {
|
||||
if (upload.canceled) throw new UploadCanceledError();
|
||||
// XXX: upload.promise must be the promise that
|
||||
// is returned by uploadFile as it has an abort()
|
||||
// method hacked onto it.
|
||||
|
@ -520,16 +550,17 @@ export default class ContentMessages {
|
|||
content.file = result.file;
|
||||
content.url = result.url;
|
||||
});
|
||||
}).then((url) => {
|
||||
}).then(() => {
|
||||
// Await previous message being sent into the room
|
||||
return promBefore;
|
||||
}).then(function() {
|
||||
if (upload.canceled) throw new UploadCanceledError();
|
||||
return matrixClient.sendMessage(roomId, content);
|
||||
}, function(err) {
|
||||
error = err;
|
||||
if (!upload.canceled) {
|
||||
let desc = _t("The file '%(fileName)s' failed to upload.", {fileName: upload.fileName});
|
||||
if (err.http_status == 413) {
|
||||
if (err.http_status === 413) {
|
||||
desc = _t(
|
||||
"The file '%(fileName)s' exceeds this homeserver's size limit for uploads",
|
||||
{fileName: upload.fileName},
|
||||
|
@ -542,11 +573,9 @@ export default class ContentMessages {
|
|||
});
|
||||
}
|
||||
}).finally(() => {
|
||||
const inprogressKeys = Object.keys(this.inprogress);
|
||||
for (let i = 0; i < this.inprogress.length; ++i) {
|
||||
const k = inprogressKeys[i];
|
||||
if (this.inprogress[k].promise === upload.promise) {
|
||||
this.inprogress.splice(k, 1);
|
||||
if (this.inprogress[i].promise === upload.promise) {
|
||||
this.inprogress.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -555,7 +584,7 @@ export default class ContentMessages {
|
|||
// clear the media size limit so we fetch it again next time
|
||||
// we try to upload
|
||||
if (error && error.http_status === 413) {
|
||||
this._mediaConfig = null;
|
||||
this.mediaConfig = null;
|
||||
}
|
||||
dis.dispatch({action: 'upload_failed', upload, error});
|
||||
} else {
|
||||
|
@ -565,24 +594,35 @@ export default class ContentMessages {
|
|||
});
|
||||
}
|
||||
|
||||
getCurrentUploads() {
|
||||
return this.inprogress.filter(u => !u.canceled);
|
||||
private isFileSizeAcceptable(file: File) {
|
||||
if (this.mediaConfig !== null &&
|
||||
this.mediaConfig["m.upload.size"] !== undefined &&
|
||||
file.size > this.mediaConfig["m.upload.size"]) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
cancelUpload(promise) {
|
||||
const inprogressKeys = Object.keys(this.inprogress);
|
||||
let upload;
|
||||
for (let i = 0; i < this.inprogress.length; ++i) {
|
||||
const k = inprogressKeys[i];
|
||||
if (this.inprogress[k].promise === promise) {
|
||||
upload = this.inprogress[k];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (upload) {
|
||||
upload.canceled = true;
|
||||
MatrixClientPeg.get().cancelUpload(upload.promise);
|
||||
dis.dispatch({action: 'upload_canceled', upload});
|
||||
private ensureMediaConfigFetched() {
|
||||
if (this.mediaConfig !== null) return;
|
||||
|
||||
console.log("[Media Config] Fetching");
|
||||
return MatrixClientPeg.get().getMediaConfig().then((config) => {
|
||||
console.log("[Media Config] Fetched config:", config);
|
||||
return config;
|
||||
}).catch(() => {
|
||||
// Media repo can't or won't report limits, so provide an empty object (no limits).
|
||||
console.log("[Media Config] Could not fetch config, so not limiting uploads.");
|
||||
return {};
|
||||
}).then((config) => {
|
||||
this.mediaConfig = config;
|
||||
});
|
||||
}
|
||||
|
||||
static sharedInstance() {
|
||||
if (window.mx_ContentMessages === undefined) {
|
||||
window.mx_ContentMessages = new ContentMessages();
|
||||
}
|
||||
return window.mx_ContentMessages;
|
||||
}
|
||||
}
|
|
@ -14,43 +14,43 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { MatrixClientPeg } from './MatrixClientPeg';
|
||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
||||
import SettingsStore from './settings/SettingsStore';
|
||||
import * as sdk from './index';
|
||||
import { _t } from './languageHandler';
|
||||
import ToastStore from './stores/ToastStore';
|
||||
import {
|
||||
hideToast as hideBulkUnverifiedSessionsToast,
|
||||
showToast as showBulkUnverifiedSessionsToast
|
||||
} from "./toasts/BulkUnverifiedSessionsToast";
|
||||
import {
|
||||
hideToast as hideSetupEncryptionToast,
|
||||
Kind as SetupKind,
|
||||
Kind,
|
||||
showToast as showSetupEncryptionToast
|
||||
} from "./toasts/SetupEncryptionToast";
|
||||
import {
|
||||
hideToast as hideUnverifiedSessionsToast,
|
||||
showToast as showUnverifiedSessionsToast
|
||||
} from "./toasts/UnverifiedSessionToast";
|
||||
|
||||
const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000;
|
||||
const THIS_DEVICE_TOAST_KEY = 'setupencryption';
|
||||
const OTHER_DEVICES_TOAST_KEY = 'reviewsessions';
|
||||
|
||||
function toastKey(deviceId) {
|
||||
return "unverified_session_" + deviceId;
|
||||
}
|
||||
|
||||
export default class DeviceListener {
|
||||
// device IDs for which the user has dismissed the verify toast ('Later')
|
||||
private dismissed = new Set<string>();
|
||||
// has the user dismissed any of the various nag toasts to setup encryption on this device?
|
||||
private dismissedThisDeviceToast = false;
|
||||
// cache of the key backup info
|
||||
private keyBackupInfo: object = null;
|
||||
private keyBackupFetchedAt: number = null;
|
||||
// We keep a list of our own device IDs so we can batch ones that were already
|
||||
// there the last time the app launched into a single toast, but display new
|
||||
// ones in their own toasts.
|
||||
private ourDeviceIdsAtStart: Set<string> = null;
|
||||
// The set of device IDs we're currently displaying toasts for
|
||||
private displayingToastsForDeviceIds = new Set<string>();
|
||||
|
||||
static sharedInstance() {
|
||||
if (!global.mx_DeviceListener) global.mx_DeviceListener = new DeviceListener();
|
||||
return global.mx_DeviceListener;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
// device IDs for which the user has dismissed the verify toast ('Later')
|
||||
this._dismissed = new Set();
|
||||
// has the user dismissed any of the various nag toasts to setup encryption on this device?
|
||||
this._dismissedThisDeviceToast = false;
|
||||
|
||||
// cache of the key backup info
|
||||
this._keyBackupInfo = null;
|
||||
this._keyBackupFetchedAt = null;
|
||||
|
||||
// We keep a list of our own device IDs so we can batch ones that were already
|
||||
// there the last time the app launched into a single toast, but display new
|
||||
// ones in their own toasts.
|
||||
this._ourDeviceIdsAtStart = null;
|
||||
|
||||
// The set of device IDs we're currently displaying toasts for
|
||||
this._displayingToastsForDeviceIds = new Set();
|
||||
if (!window.mx_DeviceListener) window.mx_DeviceListener = new DeviceListener();
|
||||
return window.mx_DeviceListener;
|
||||
}
|
||||
|
||||
start() {
|
||||
|
@ -74,12 +74,12 @@ export default class DeviceListener {
|
|||
MatrixClientPeg.get().removeListener('accountData', this._onAccountData);
|
||||
MatrixClientPeg.get().removeListener('sync', this._onSync);
|
||||
}
|
||||
this._dismissed.clear();
|
||||
this._dismissedThisDeviceToast = false;
|
||||
this._keyBackupInfo = null;
|
||||
this._keyBackupFetchedAt = null;
|
||||
this._ourDeviceIdsAtStart = null;
|
||||
this._displayingToastsForDeviceIds = new Set();
|
||||
this.dismissed.clear();
|
||||
this.dismissedThisDeviceToast = false;
|
||||
this.keyBackupInfo = null;
|
||||
this.keyBackupFetchedAt = null;
|
||||
this.ourDeviceIdsAtStart = null;
|
||||
this.displayingToastsForDeviceIds = new Set();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -87,29 +87,29 @@ export default class DeviceListener {
|
|||
*
|
||||
* @param {String[]} deviceIds List of device IDs to dismiss notifications for
|
||||
*/
|
||||
async dismissUnverifiedSessions(deviceIds) {
|
||||
async dismissUnverifiedSessions(deviceIds: Iterable<string>) {
|
||||
for (const d of deviceIds) {
|
||||
this._dismissed.add(d);
|
||||
this.dismissed.add(d);
|
||||
}
|
||||
|
||||
this._recheck();
|
||||
}
|
||||
|
||||
dismissEncryptionSetup() {
|
||||
this._dismissedThisDeviceToast = true;
|
||||
this.dismissedThisDeviceToast = true;
|
||||
this._recheck();
|
||||
}
|
||||
|
||||
_ensureDeviceIdsAtStartPopulated() {
|
||||
if (this._ourDeviceIdsAtStart === null) {
|
||||
if (this.ourDeviceIdsAtStart === null) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
this._ourDeviceIdsAtStart = new Set(
|
||||
this.ourDeviceIdsAtStart = new Set(
|
||||
cli.getStoredDevicesForUser(cli.getUserId()).map(d => d.deviceId),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_onWillUpdateDevices = async (users, initialFetch) => {
|
||||
_onWillUpdateDevices = async (users: string[], initialFetch?: boolean) => {
|
||||
// If we didn't know about *any* devices before (ie. it's fresh login),
|
||||
// then they are all pre-existing devices, so ignore this and set the
|
||||
// devicesAtStart list to the devices that we see after the fetch.
|
||||
|
@ -122,17 +122,17 @@ export default class DeviceListener {
|
|||
// before we download any new ones.
|
||||
}
|
||||
|
||||
_onDevicesUpdated = (users) => {
|
||||
_onDevicesUpdated = (users: string[]) => {
|
||||
if (!users.includes(MatrixClientPeg.get().getUserId())) return;
|
||||
this._recheck();
|
||||
}
|
||||
|
||||
_onDeviceVerificationChanged = (userId) => {
|
||||
_onDeviceVerificationChanged = (userId: string) => {
|
||||
if (userId !== MatrixClientPeg.get().getUserId()) return;
|
||||
this._recheck();
|
||||
}
|
||||
|
||||
_onUserTrustStatusChanged = (userId, trustLevel) => {
|
||||
_onUserTrustStatusChanged = (userId: string) => {
|
||||
if (userId !== MatrixClientPeg.get().getUserId()) return;
|
||||
this._recheck();
|
||||
}
|
||||
|
@ -163,11 +163,11 @@ export default class DeviceListener {
|
|||
// & cache the result
|
||||
async _getKeyBackupInfo() {
|
||||
const now = (new Date()).getTime();
|
||||
if (!this._keyBackupInfo || this._keyBackupFetchedAt < now - KEY_BACKUP_POLL_INTERVAL) {
|
||||
this._keyBackupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
|
||||
this._keyBackupFetchedAt = now;
|
||||
if (!this.keyBackupInfo || this.keyBackupFetchedAt < now - KEY_BACKUP_POLL_INTERVAL) {
|
||||
this.keyBackupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
|
||||
this.keyBackupFetchedAt = now;
|
||||
}
|
||||
return this._keyBackupInfo;
|
||||
return this.keyBackupInfo;
|
||||
}
|
||||
|
||||
async _recheck() {
|
||||
|
@ -186,48 +186,25 @@ export default class DeviceListener {
|
|||
|
||||
const crossSigningReady = await cli.isCrossSigningReady();
|
||||
|
||||
if (this._dismissedThisDeviceToast) {
|
||||
ToastStore.sharedInstance().dismissToast(THIS_DEVICE_TOAST_KEY);
|
||||
if (this.dismissedThisDeviceToast || crossSigningReady) {
|
||||
hideSetupEncryptionToast();
|
||||
} else {
|
||||
if (!crossSigningReady) {
|
||||
// make sure our keys are finished downlaoding
|
||||
await cli.downloadKeys([cli.getUserId()]);
|
||||
// cross signing isn't enabled - nag to enable it
|
||||
// There are 3 different toasts for:
|
||||
if (cli.getStoredCrossSigningForUser(cli.getUserId())) {
|
||||
// Cross-signing on account but this device doesn't trust the master key (verify this session)
|
||||
ToastStore.sharedInstance().addOrReplaceToast({
|
||||
key: THIS_DEVICE_TOAST_KEY,
|
||||
title: _t("Verify this session"),
|
||||
icon: "verification_warning",
|
||||
props: {kind: 'verify_this_session'},
|
||||
component: sdk.getComponent("toasts.SetupEncryptionToast"),
|
||||
});
|
||||
} else {
|
||||
const backupInfo = await this._getKeyBackupInfo();
|
||||
if (backupInfo) {
|
||||
// No cross-signing on account but key backup available (upgrade encryption)
|
||||
ToastStore.sharedInstance().addOrReplaceToast({
|
||||
key: THIS_DEVICE_TOAST_KEY,
|
||||
title: _t("Encryption upgrade available"),
|
||||
icon: "verification_warning",
|
||||
props: {kind: 'upgrade_encryption'},
|
||||
component: sdk.getComponent("toasts.SetupEncryptionToast"),
|
||||
});
|
||||
} else {
|
||||
// No cross-signing or key backup on account (set up encryption)
|
||||
ToastStore.sharedInstance().addOrReplaceToast({
|
||||
key: THIS_DEVICE_TOAST_KEY,
|
||||
title: _t("Set up encryption"),
|
||||
icon: "verification_warning",
|
||||
props: {kind: 'set_up_encryption'},
|
||||
component: sdk.getComponent("toasts.SetupEncryptionToast"),
|
||||
});
|
||||
}
|
||||
}
|
||||
// make sure our keys are finished downloading
|
||||
await cli.downloadKeys([cli.getUserId()]);
|
||||
// cross signing isn't enabled - nag to enable it
|
||||
// There are 3 different toasts for:
|
||||
if (cli.getStoredCrossSigningForUser(cli.getUserId())) {
|
||||
// Cross-signing on account but this device doesn't trust the master key (verify this session)
|
||||
showSetupEncryptionToast(SetupKind.VERIFY_THIS_SESSION);
|
||||
} else {
|
||||
// cross-signing is ready, and we don't need to upgrade encryption
|
||||
ToastStore.sharedInstance().dismissToast(THIS_DEVICE_TOAST_KEY);
|
||||
const backupInfo = await this._getKeyBackupInfo();
|
||||
if (backupInfo) {
|
||||
// No cross-signing on account but key backup available (upgrade encryption)
|
||||
showSetupEncryptionToast(Kind.UPGRADE_ENCRYPTION);
|
||||
} else {
|
||||
// No cross-signing or key backup on account (set up encryption)
|
||||
showSetupEncryptionToast(Kind.SET_UP_ENCRYPTION);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -239,20 +216,20 @@ export default class DeviceListener {
|
|||
// (technically could just be a boolean: we don't actually
|
||||
// need to remember the device IDs, but for the sake of
|
||||
// symmetry...).
|
||||
const oldUnverifiedDeviceIds = new Set();
|
||||
const oldUnverifiedDeviceIds = new Set<string>();
|
||||
// Unverified devices that have appeared since then
|
||||
const newUnverifiedDeviceIds = new Set();
|
||||
const newUnverifiedDeviceIds = new Set<string>();
|
||||
|
||||
// as long as cross-signing isn't ready,
|
||||
// you can't see or dismiss any device toasts
|
||||
if (crossSigningReady) {
|
||||
const devices = cli.getStoredDevicesForUser(cli.getUserId());
|
||||
for (const device of devices) {
|
||||
if (device.deviceId == cli.deviceId) continue;
|
||||
if (device.deviceId === cli.deviceId) continue;
|
||||
|
||||
const deviceTrust = await cli.checkDeviceTrust(cli.getUserId(), device.deviceId);
|
||||
if (!deviceTrust.isCrossSigningVerified() && !this._dismissed.has(device.deviceId)) {
|
||||
if (this._ourDeviceIdsAtStart.has(device.deviceId)) {
|
||||
if (!deviceTrust.isCrossSigningVerified() && !this.dismissed.has(device.deviceId)) {
|
||||
if (this.ourDeviceIdsAtStart.has(device.deviceId)) {
|
||||
oldUnverifiedDeviceIds.add(device.deviceId);
|
||||
} else {
|
||||
newUnverifiedDeviceIds.add(device.deviceId);
|
||||
|
@ -263,38 +240,23 @@ export default class DeviceListener {
|
|||
|
||||
// Display or hide the batch toast for old unverified sessions
|
||||
if (oldUnverifiedDeviceIds.size > 0) {
|
||||
ToastStore.sharedInstance().addOrReplaceToast({
|
||||
key: OTHER_DEVICES_TOAST_KEY,
|
||||
title: _t("Review where you’re logged in"),
|
||||
icon: "verification_warning",
|
||||
priority: ToastStore.PRIORITY_LOW,
|
||||
props: {
|
||||
deviceIds: oldUnverifiedDeviceIds,
|
||||
},
|
||||
component: sdk.getComponent("toasts.BulkUnverifiedSessionsToast"),
|
||||
});
|
||||
showBulkUnverifiedSessionsToast(oldUnverifiedDeviceIds);
|
||||
} else {
|
||||
ToastStore.sharedInstance().dismissToast(OTHER_DEVICES_TOAST_KEY);
|
||||
hideBulkUnverifiedSessionsToast();
|
||||
}
|
||||
|
||||
// Show toasts for new unverified devices if they aren't already there
|
||||
for (const deviceId of newUnverifiedDeviceIds) {
|
||||
ToastStore.sharedInstance().addOrReplaceToast({
|
||||
key: toastKey(deviceId),
|
||||
title: _t("New login. Was this you?"),
|
||||
icon: "verification_warning",
|
||||
props: { deviceId },
|
||||
component: sdk.getComponent("toasts.UnverifiedSessionToast"),
|
||||
});
|
||||
showUnverifiedSessionsToast(deviceId);
|
||||
}
|
||||
|
||||
// ...and hide any we don't need any more
|
||||
for (const deviceId of this._displayingToastsForDeviceIds) {
|
||||
for (const deviceId of this.displayingToastsForDeviceIds) {
|
||||
if (!newUnverifiedDeviceIds.has(deviceId)) {
|
||||
ToastStore.sharedInstance().dismissToast(toastKey(deviceId));
|
||||
hideUnverifiedSessionsToast(deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
this._displayingToastsForDeviceIds = newUnverifiedDeviceIds;
|
||||
this.displayingToastsForDeviceIds = newUnverifiedDeviceIds;
|
||||
}
|
||||
}
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import URL from 'url';
|
||||
import dis from './dispatcher';
|
||||
import dis from './dispatcher/dispatcher';
|
||||
import WidgetMessagingEndpoint from './WidgetMessagingEndpoint';
|
||||
import ActiveWidgetStore from './stores/ActiveWidgetStore';
|
||||
import {MatrixClientPeg} from "./MatrixClientPeg";
|
||||
|
|
|
@ -22,6 +22,7 @@ import { _t } from './languageHandler';
|
|||
import {MatrixClientPeg} from './MatrixClientPeg';
|
||||
import GroupStore from './stores/GroupStore';
|
||||
import {allSettled} from "./utils/promise";
|
||||
import StyledCheckbox from './components/views/elements/StyledCheckbox';
|
||||
|
||||
export function showGroupInviteDialog(groupId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
@ -61,19 +62,19 @@ export function showGroupAddRoomDialog(groupId) {
|
|||
<div>{ _t("Which rooms would you like to add to this community?") }</div>
|
||||
</div>;
|
||||
|
||||
const checkboxContainer = <label className="mx_GroupAddressPicker_checkboxContainer">
|
||||
<input type="checkbox" onChange={onCheckboxClicked} />
|
||||
<div>
|
||||
{ _t("Show these rooms to non-members on the community page and room list?") }
|
||||
</div>
|
||||
</label>;
|
||||
const checkboxContainer = <StyledCheckbox
|
||||
className="mx_GroupAddressPicker_checkboxContainer"
|
||||
onChange={onCheckboxClicked}
|
||||
>
|
||||
{ _t("Show these rooms to non-members on the community page and room list?") }
|
||||
</StyledCheckbox>;
|
||||
|
||||
const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog");
|
||||
Modal.createTrackedDialog('Add Rooms to Group', '', AddressPickerDialog, {
|
||||
title: _t("Add rooms to the community"),
|
||||
description: description,
|
||||
extraNode: checkboxContainer,
|
||||
placeholder: _t("Room name or alias"),
|
||||
placeholder: _t("Room name or address"),
|
||||
button: _t("Add to community"),
|
||||
pickerType: 'room',
|
||||
validAddressTypes: ['mx-room-id'],
|
||||
|
|
|
@ -1,158 +0,0 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
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 * as sdk from './index';
|
||||
import Modal from './Modal';
|
||||
import SettingsStore from './settings/SettingsStore';
|
||||
|
||||
// TODO: We can remove this once cross-signing is the only way.
|
||||
// https://github.com/vector-im/riot-web/issues/11908
|
||||
export default class KeyRequestHandler {
|
||||
constructor(matrixClient) {
|
||||
this._matrixClient = matrixClient;
|
||||
|
||||
// the user/device for which we currently have a dialog open
|
||||
this._currentUser = null;
|
||||
this._currentDevice = null;
|
||||
|
||||
// userId -> deviceId -> [keyRequest]
|
||||
this._pendingKeyRequests = Object.create(null);
|
||||
}
|
||||
|
||||
handleKeyRequest(keyRequest) {
|
||||
// Ignore own device key requests if cross-signing lab enabled
|
||||
if (SettingsStore.getValue("feature_cross_signing")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const userId = keyRequest.userId;
|
||||
const deviceId = keyRequest.deviceId;
|
||||
const requestId = keyRequest.requestId;
|
||||
|
||||
if (!this._pendingKeyRequests[userId]) {
|
||||
this._pendingKeyRequests[userId] = Object.create(null);
|
||||
}
|
||||
if (!this._pendingKeyRequests[userId][deviceId]) {
|
||||
this._pendingKeyRequests[userId][deviceId] = [];
|
||||
}
|
||||
|
||||
// check if we already have this request
|
||||
const requests = this._pendingKeyRequests[userId][deviceId];
|
||||
if (requests.find((r) => r.requestId === requestId)) {
|
||||
console.log("Already have this key request, ignoring");
|
||||
return;
|
||||
}
|
||||
|
||||
requests.push(keyRequest);
|
||||
|
||||
if (this._currentUser) {
|
||||
// ignore for now
|
||||
console.log("Key request, but we already have a dialog open");
|
||||
return;
|
||||
}
|
||||
|
||||
this._processNextRequest();
|
||||
}
|
||||
|
||||
handleKeyRequestCancellation(cancellation) {
|
||||
// Ignore own device key requests if cross-signing lab enabled
|
||||
if (SettingsStore.getValue("feature_cross_signing")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// see if we can find the request in the queue
|
||||
const userId = cancellation.userId;
|
||||
const deviceId = cancellation.deviceId;
|
||||
const requestId = cancellation.requestId;
|
||||
|
||||
if (userId === this._currentUser && deviceId === this._currentDevice) {
|
||||
console.log(
|
||||
"room key request cancellation for the user we currently have a"
|
||||
+ " dialog open for",
|
||||
);
|
||||
// TODO: update the dialog. For now, we just ignore the
|
||||
// cancellation.
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._pendingKeyRequests[userId]) {
|
||||
return;
|
||||
}
|
||||
const requests = this._pendingKeyRequests[userId][deviceId];
|
||||
if (!requests) {
|
||||
return;
|
||||
}
|
||||
const idx = requests.findIndex((r) => r.requestId === requestId);
|
||||
if (idx < 0) {
|
||||
return;
|
||||
}
|
||||
console.log("Forgetting room key request");
|
||||
requests.splice(idx, 1);
|
||||
if (requests.length === 0) {
|
||||
delete this._pendingKeyRequests[userId][deviceId];
|
||||
if (Object.keys(this._pendingKeyRequests[userId]).length === 0) {
|
||||
delete this._pendingKeyRequests[userId];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_processNextRequest() {
|
||||
const userId = Object.keys(this._pendingKeyRequests)[0];
|
||||
if (!userId) {
|
||||
return;
|
||||
}
|
||||
const deviceId = Object.keys(this._pendingKeyRequests[userId])[0];
|
||||
if (!deviceId) {
|
||||
return;
|
||||
}
|
||||
console.log(`Starting KeyShareDialog for ${userId}:${deviceId}`);
|
||||
|
||||
const finished = (r) => {
|
||||
this._currentUser = null;
|
||||
this._currentDevice = null;
|
||||
|
||||
if (!this._pendingKeyRequests[userId] || !this._pendingKeyRequests[userId][deviceId]) {
|
||||
// request was removed in the time the dialog was displayed
|
||||
this._processNextRequest();
|
||||
return;
|
||||
}
|
||||
|
||||
if (r) {
|
||||
for (const req of this._pendingKeyRequests[userId][deviceId]) {
|
||||
req.share();
|
||||
}
|
||||
}
|
||||
delete this._pendingKeyRequests[userId][deviceId];
|
||||
if (Object.keys(this._pendingKeyRequests[userId]).length === 0) {
|
||||
delete this._pendingKeyRequests[userId];
|
||||
}
|
||||
|
||||
this._processNextRequest();
|
||||
};
|
||||
|
||||
const KeyShareDialog = sdk.getComponent("dialogs.KeyShareDialog");
|
||||
Modal.appendTrackedDialog('Key Share', 'Process Next Request', KeyShareDialog, {
|
||||
matrixClient: this._matrixClient,
|
||||
userId: userId,
|
||||
deviceId: deviceId,
|
||||
onFinished: finished,
|
||||
});
|
||||
this._currentUser = userId;
|
||||
this._currentDevice = deviceId;
|
||||
}
|
||||
}
|
||||
|
|
@ -26,7 +26,7 @@ import Analytics from './Analytics';
|
|||
import Notifier from './Notifier';
|
||||
import UserActivity from './UserActivity';
|
||||
import Presence from './Presence';
|
||||
import dis from './dispatcher';
|
||||
import dis from './dispatcher/dispatcher';
|
||||
import DMRoomMap from './utils/DMRoomMap';
|
||||
import Modal from './Modal';
|
||||
import * as sdk from './index';
|
||||
|
@ -298,6 +298,8 @@ async function _restoreFromLocalStorage(opts) {
|
|||
return false;
|
||||
}
|
||||
|
||||
const pickleKey = await PlatformPeg.get().getPickleKey(userId, deviceId);
|
||||
|
||||
console.log(`Restoring session for ${userId}`);
|
||||
await _doSetLoggedIn({
|
||||
userId: userId,
|
||||
|
@ -306,6 +308,7 @@ async function _restoreFromLocalStorage(opts) {
|
|||
homeserverUrl: hsUrl,
|
||||
identityServerUrl: isUrl,
|
||||
guest: isGuest,
|
||||
pickleKey: pickleKey,
|
||||
}, false);
|
||||
return true;
|
||||
} else {
|
||||
|
@ -348,9 +351,13 @@ async function _handleLoadSessionFailure(e) {
|
|||
*
|
||||
* @returns {Promise} promise which resolves to the new MatrixClient once it has been started
|
||||
*/
|
||||
export function setLoggedIn(credentials) {
|
||||
export async function setLoggedIn(credentials) {
|
||||
stopMatrixClient();
|
||||
return _doSetLoggedIn(credentials, true);
|
||||
const pickleKey = credentials.userId && credentials.deviceId
|
||||
? await PlatformPeg.get().createPickleKey(credentials.userId, credentials.deviceId)
|
||||
: null;
|
||||
|
||||
return _doSetLoggedIn(Object.assign({}, credentials, {pickleKey}), true);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -516,7 +523,9 @@ export function logout() {
|
|||
}
|
||||
|
||||
_isLoggingOut = true;
|
||||
MatrixClientPeg.get().logout().then(onLoggedOut,
|
||||
const client = MatrixClientPeg.get();
|
||||
PlatformPeg.get().destroyPickleKey(client.getUserId(), client.getDeviceId());
|
||||
client.logout().then(onLoggedOut,
|
||||
(err) => {
|
||||
// Just throwing an error here is going to be very unhelpful
|
||||
// if you're trying to log out because your server's down and
|
||||
|
@ -575,10 +584,12 @@ async function startMatrixClient(startSyncing=true) {
|
|||
// to work).
|
||||
dis.dispatch({action: 'will_start_client'}, true);
|
||||
|
||||
// reset things first just in case
|
||||
TypingStore.sharedInstance().reset();
|
||||
ToastStore.sharedInstance().reset();
|
||||
|
||||
Notifier.start();
|
||||
UserActivity.sharedInstance().start();
|
||||
TypingStore.sharedInstance().reset(); // just in case
|
||||
ToastStore.sharedInstance().reset();
|
||||
DMRoomMap.makeShared().start();
|
||||
IntegrationManagers.sharedInstance().startWatching();
|
||||
ActiveWidgetStore.start();
|
||||
|
|
|
@ -17,8 +17,8 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {MatrixClient, MemoryStore} from 'matrix-js-sdk';
|
||||
|
||||
import {MatrixClient} from 'matrix-js-sdk/src/client';
|
||||
import {MemoryStore} from 'matrix-js-sdk/src/store/memory';
|
||||
import * as utils from 'matrix-js-sdk/src/utils';
|
||||
import {EventTimeline} from 'matrix-js-sdk/src/models/event-timeline';
|
||||
import {EventTimelineSet} from 'matrix-js-sdk/src/models/event-timeline-set';
|
||||
|
@ -34,37 +34,25 @@ import IdentityAuthClient from './IdentityAuthClient';
|
|||
import { crossSigningCallbacks } from './CrossSigningManager';
|
||||
import {SHOW_QR_CODE_METHOD} from "matrix-js-sdk/src/crypto/verification/QRCode";
|
||||
|
||||
interface MatrixClientCreds {
|
||||
export interface IMatrixClientCreds {
|
||||
homeserverUrl: string,
|
||||
identityServerUrl: string,
|
||||
userId: string,
|
||||
deviceId: string,
|
||||
accessToken: string,
|
||||
guest: boolean,
|
||||
pickleKey?: string,
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper object for handling the js-sdk Matrix Client object in the react-sdk
|
||||
* Handles the creation/initialisation of client objects.
|
||||
* This module provides a singleton instance of this class so the 'current'
|
||||
* Matrix Client object is available easily.
|
||||
*/
|
||||
class _MatrixClientPeg {
|
||||
constructor() {
|
||||
this.matrixClient = null;
|
||||
this._justRegisteredUserId = null;
|
||||
// TODO: Move this to the js-sdk
|
||||
export interface IOpts {
|
||||
initialSyncLimit?: number;
|
||||
pendingEventOrdering?: "detached" | "chronological";
|
||||
lazyLoadMembers?: boolean;
|
||||
}
|
||||
|
||||
// These are the default options used when when the
|
||||
// client is started in 'start'. These can be altered
|
||||
// at any time up to after the 'will_start_client'
|
||||
// event is finished processing.
|
||||
this.opts = {
|
||||
initialSyncLimit: 20,
|
||||
};
|
||||
// the credentials used to init the current client object.
|
||||
// used if we tear it down & recreate it with a different store
|
||||
this._currentClientCreds = null;
|
||||
}
|
||||
export interface IMatrixClientPeg {
|
||||
opts: IOpts;
|
||||
|
||||
/**
|
||||
* Sets the script href passed to the IndexedDB web worker
|
||||
|
@ -73,19 +61,23 @@ class _MatrixClientPeg {
|
|||
*
|
||||
* @param {string} script href to the script to be passed to the web worker
|
||||
*/
|
||||
setIndexedDbWorkerScript(script) {
|
||||
createMatrixClient.indexedDbWorkerScript = script;
|
||||
}
|
||||
setIndexedDbWorkerScript(script: string): void;
|
||||
|
||||
get(): MatrixClient {
|
||||
return this.matrixClient;
|
||||
}
|
||||
/**
|
||||
* Return the server name of the user's homeserver
|
||||
* Throws an error if unable to deduce the homeserver name
|
||||
* (eg. if the user is not logged in)
|
||||
*
|
||||
* @returns {string} The homeserver name, if present.
|
||||
*/
|
||||
getHomeserverName(): string;
|
||||
|
||||
unset() {
|
||||
this.matrixClient = null;
|
||||
get(): MatrixClient;
|
||||
unset(): void;
|
||||
assign(): Promise<any>;
|
||||
start(): Promise<any>;
|
||||
|
||||
MatrixActionCreators.stop();
|
||||
}
|
||||
getCredentials(): IMatrixClientCreds;
|
||||
|
||||
/**
|
||||
* If we've registered a user ID we set this to the ID of the
|
||||
|
@ -95,9 +87,7 @@ class _MatrixClientPeg {
|
|||
*
|
||||
* @param {string} uid The user ID of the user we've just registered
|
||||
*/
|
||||
setJustRegisteredUserId(uid) {
|
||||
this._justRegisteredUserId = uid;
|
||||
}
|
||||
setJustRegisteredUserId(uid: string): void;
|
||||
|
||||
/**
|
||||
* Returns true if the current user has just been registered by this
|
||||
|
@ -105,23 +95,73 @@ class _MatrixClientPeg {
|
|||
*
|
||||
* @returns {bool} True if user has just been registered
|
||||
*/
|
||||
currentUserIsJustRegistered() {
|
||||
currentUserIsJustRegistered(): boolean;
|
||||
|
||||
/**
|
||||
* Replace this MatrixClientPeg's client with a client instance that has
|
||||
* homeserver / identity server URLs and active credentials
|
||||
*
|
||||
* @param {IMatrixClientCreds} creds The new credentials to use.
|
||||
*/
|
||||
replaceUsingCreds(creds: IMatrixClientCreds): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper object for handling the js-sdk Matrix Client object in the react-sdk
|
||||
* Handles the creation/initialisation of client objects.
|
||||
* This module provides a singleton instance of this class so the 'current'
|
||||
* Matrix Client object is available easily.
|
||||
*/
|
||||
class _MatrixClientPeg implements IMatrixClientPeg {
|
||||
// These are the default options used when when the
|
||||
// client is started in 'start'. These can be altered
|
||||
// at any time up to after the 'will_start_client'
|
||||
// event is finished processing.
|
||||
public opts: IOpts = {
|
||||
initialSyncLimit: 20,
|
||||
};
|
||||
|
||||
private matrixClient: MatrixClient = null;
|
||||
private justRegisteredUserId: string;
|
||||
|
||||
// the credentials used to init the current client object.
|
||||
// used if we tear it down & recreate it with a different store
|
||||
private currentClientCreds: IMatrixClientCreds;
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
public setIndexedDbWorkerScript(script: string): void {
|
||||
createMatrixClient.indexedDbWorkerScript = script;
|
||||
}
|
||||
|
||||
public get(): MatrixClient {
|
||||
return this.matrixClient;
|
||||
}
|
||||
|
||||
public unset(): void {
|
||||
this.matrixClient = null;
|
||||
|
||||
MatrixActionCreators.stop();
|
||||
}
|
||||
|
||||
public setJustRegisteredUserId(uid: string): void {
|
||||
this.justRegisteredUserId = uid;
|
||||
}
|
||||
|
||||
public currentUserIsJustRegistered(): boolean {
|
||||
return (
|
||||
this.matrixClient &&
|
||||
this.matrixClient.credentials.userId === this._justRegisteredUserId
|
||||
this.matrixClient.credentials.userId === this.justRegisteredUserId
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* Replace this MatrixClientPeg's client with a client instance that has
|
||||
* homeserver / identity server URLs and active credentials
|
||||
*/
|
||||
replaceUsingCreds(creds: MatrixClientCreds) {
|
||||
this._currentClientCreds = creds;
|
||||
this._createClient(creds);
|
||||
public replaceUsingCreds(creds: IMatrixClientCreds): void {
|
||||
this.currentClientCreds = creds;
|
||||
this.createClient(creds);
|
||||
}
|
||||
|
||||
async assign() {
|
||||
public async assign(): Promise<any> {
|
||||
for (const dbType of ['indexeddb', 'memory']) {
|
||||
try {
|
||||
const promise = this.matrixClient.store.startup();
|
||||
|
@ -132,7 +172,7 @@ class _MatrixClientPeg {
|
|||
if (dbType === 'indexeddb') {
|
||||
console.error('Error starting matrixclient store - falling back to memory store', err);
|
||||
this.matrixClient.store = new MemoryStore({
|
||||
localStorage: global.localStorage,
|
||||
localStorage: localStorage,
|
||||
});
|
||||
} else {
|
||||
console.error('Failed to start memory store!', err);
|
||||
|
@ -158,9 +198,7 @@ class _MatrixClientPeg {
|
|||
// The js-sdk found a crypto DB too new for it to use
|
||||
const CryptoStoreTooNewDialog =
|
||||
sdk.getComponent("views.dialogs.CryptoStoreTooNewDialog");
|
||||
Modal.createDialog(CryptoStoreTooNewDialog, {
|
||||
host: window.location.host,
|
||||
});
|
||||
Modal.createDialog(CryptoStoreTooNewDialog);
|
||||
}
|
||||
// this can happen for a number of reasons, the most likely being
|
||||
// that the olm library was missing. It's not fatal.
|
||||
|
@ -179,7 +217,7 @@ class _MatrixClientPeg {
|
|||
return opts;
|
||||
}
|
||||
|
||||
async start() {
|
||||
public async start(): Promise<any> {
|
||||
const opts = await this.assign();
|
||||
|
||||
console.log(`MatrixClientPeg: really starting MatrixClient`);
|
||||
|
@ -187,7 +225,7 @@ class _MatrixClientPeg {
|
|||
console.log(`MatrixClientPeg: MatrixClient started`);
|
||||
}
|
||||
|
||||
getCredentials(): MatrixClientCreds {
|
||||
public getCredentials(): IMatrixClientCreds {
|
||||
return {
|
||||
homeserverUrl: this.matrixClient.baseUrl,
|
||||
identityServerUrl: this.matrixClient.idBaseUrl,
|
||||
|
@ -198,12 +236,7 @@ class _MatrixClientPeg {
|
|||
};
|
||||
}
|
||||
|
||||
/*
|
||||
* Return the server name of the user's homeserver
|
||||
* Throws an error if unable to deduce the homeserver name
|
||||
* (eg. if the user is not logged in)
|
||||
*/
|
||||
getHomeserverName() {
|
||||
public getHomeserverName(): string {
|
||||
const matches = /^@.+:(.+)$/.exec(this.matrixClient.credentials.userId);
|
||||
if (matches === null || matches.length < 1) {
|
||||
throw new Error("Failed to derive homeserver name from user ID!");
|
||||
|
@ -211,13 +244,15 @@ class _MatrixClientPeg {
|
|||
return matches[1];
|
||||
}
|
||||
|
||||
_createClient(creds: MatrixClientCreds) {
|
||||
private createClient(creds: IMatrixClientCreds): void {
|
||||
// TODO: Make these opts typesafe with the js-sdk
|
||||
const opts = {
|
||||
baseUrl: creds.homeserverUrl,
|
||||
idBaseUrl: creds.identityServerUrl,
|
||||
accessToken: creds.accessToken,
|
||||
userId: creds.userId,
|
||||
deviceId: creds.deviceId,
|
||||
pickleKey: creds.pickleKey,
|
||||
timelineSupport: true,
|
||||
forceTURN: !SettingsStore.getValue('webRtcAllowPeerToPeer', false),
|
||||
fallbackICEServerAllowed: !!SettingsStore.getValue('fallbackICEServerAllowed'),
|
||||
|
@ -228,9 +263,9 @@ class _MatrixClientPeg {
|
|||
],
|
||||
unstableClientRelationAggregation: true,
|
||||
identityServer: new IdentityAuthClient(),
|
||||
cryptoCallbacks: {},
|
||||
};
|
||||
|
||||
opts.cryptoCallbacks = {};
|
||||
// These are always installed regardless of the labs flag so that
|
||||
// cross-signing features can toggle on without reloading and also be
|
||||
// accessed immediately after login.
|
||||
|
@ -253,8 +288,8 @@ class _MatrixClientPeg {
|
|||
}
|
||||
}
|
||||
|
||||
if (!global.mxMatrixClientPeg) {
|
||||
global.mxMatrixClientPeg = new _MatrixClientPeg();
|
||||
if (!window.mxMatrixClientPeg) {
|
||||
window.mxMatrixClientPeg = new _MatrixClientPeg();
|
||||
}
|
||||
|
||||
export const MatrixClientPeg = global.mxMatrixClientPeg;
|
||||
export const MatrixClientPeg = window.mxMatrixClientPeg;
|
|
@ -18,7 +18,7 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import Analytics from './Analytics';
|
||||
import dis from './dispatcher';
|
||||
import dis from './dispatcher/dispatcher';
|
||||
import {defer} from './utils/promise';
|
||||
import AsyncWrapper from './AsyncWrapper';
|
||||
|
||||
|
|
|
@ -21,11 +21,14 @@ import PlatformPeg from './PlatformPeg';
|
|||
import * as TextForEvent from './TextForEvent';
|
||||
import Analytics from './Analytics';
|
||||
import * as Avatar from './Avatar';
|
||||
import dis from './dispatcher';
|
||||
import dis from './dispatcher/dispatcher';
|
||||
import * as sdk from './index';
|
||||
import { _t } from './languageHandler';
|
||||
import Modal from './Modal';
|
||||
import SettingsStore, {SettingLevel} from "./settings/SettingsStore";
|
||||
import {
|
||||
hideToast as hideNotificationsToast,
|
||||
} from "./toasts/DesktopNotificationsToast";
|
||||
|
||||
/*
|
||||
* Dispatches:
|
||||
|
@ -278,12 +281,7 @@ const Notifier = {
|
|||
|
||||
Analytics.trackEvent('Notifier', 'Set Toolbar Hidden', hidden);
|
||||
|
||||
// XXX: why are we dispatching this here?
|
||||
// this is nothing to do with notifier_enabled
|
||||
dis.dispatch({
|
||||
action: "notifier_enabled",
|
||||
value: this.isEnabled(),
|
||||
});
|
||||
hideNotificationsToast();
|
||||
|
||||
// update the info to localStorage for persistent settings
|
||||
if (persistent && global.localStorage) {
|
||||
|
|
|
@ -84,8 +84,14 @@ export default class PasswordReset {
|
|||
|
||||
try {
|
||||
await this.client.setPassword({
|
||||
// Note: Though this sounds like a login type for identity servers only, it
|
||||
// has a dual purpose of being used for homeservers too.
|
||||
type: "m.login.email.identity",
|
||||
// TODO: Remove `threepid_creds` once servers support proper UIA
|
||||
// See https://github.com/matrix-org/synapse/issues/5665
|
||||
// See https://github.com/matrix-org/matrix-doc/issues/2220
|
||||
threepid_creds: creds,
|
||||
threepidCreds: creds,
|
||||
}, this.password);
|
||||
} catch (err) {
|
||||
if (err.httpStatus === 401) {
|
||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import {MatrixClientPeg} from "./MatrixClientPeg";
|
||||
import dis from "./dispatcher";
|
||||
import dis from "./dispatcher/dispatcher";
|
||||
import Timer from './utils/Timer';
|
||||
|
||||
// Time in ms after that a user is considered as unavailable/away
|
||||
|
|
|
@ -20,7 +20,7 @@ limitations under the License.
|
|||
* registration code.
|
||||
*/
|
||||
|
||||
import dis from './dispatcher';
|
||||
import dis from './dispatcher/dispatcher';
|
||||
import * as sdk from './index';
|
||||
import Modal from './Modal';
|
||||
import { _t } from './languageHandler';
|
||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
||||
import dis from './dispatcher';
|
||||
import dis from './dispatcher/dispatcher';
|
||||
import { EventStatus } from 'matrix-js-sdk';
|
||||
|
||||
export default class Resend {
|
||||
|
|
|
@ -238,7 +238,7 @@ Example:
|
|||
|
||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
||||
import { MatrixEvent } from 'matrix-js-sdk';
|
||||
import dis from './dispatcher';
|
||||
import dis from './dispatcher/dispatcher';
|
||||
import WidgetUtils from './utils/WidgetUtils';
|
||||
import RoomViewStore from './stores/RoomViewStore';
|
||||
import { _t } from './languageHandler';
|
||||
|
|
|
@ -21,7 +21,7 @@ limitations under the License.
|
|||
import * as React from 'react';
|
||||
|
||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
||||
import dis from './dispatcher';
|
||||
import dis from './dispatcher/dispatcher';
|
||||
import * as sdk from './index';
|
||||
import {_t, _td} from './languageHandler';
|
||||
import Modal from './Modal';
|
||||
|
@ -41,6 +41,8 @@ import { parseFragment as parseHtml } from "parse5";
|
|||
import sendBugReport from "./rageshake/submit-rageshake";
|
||||
import SdkConfig from "./SdkConfig";
|
||||
import { ensureDMExists } from "./createRoom";
|
||||
import { ViewUserPayload } from "./dispatcher/payloads/ViewUserPayload";
|
||||
import { Action } from "./dispatcher/actions";
|
||||
|
||||
// XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816
|
||||
interface HTMLInputEvent extends Event {
|
||||
|
@ -448,8 +450,8 @@ export const Commands = [
|
|||
new Command({
|
||||
command: 'join',
|
||||
aliases: ['j', 'goto'],
|
||||
args: '<room-alias>',
|
||||
description: _td('Joins room with given alias'),
|
||||
args: '<room-address>',
|
||||
description: _td('Joins room with given address'),
|
||||
runFn: function(_, args) {
|
||||
if (args) {
|
||||
// Note: we support 2 versions of this command. The first is
|
||||
|
@ -560,7 +562,7 @@ export const Commands = [
|
|||
}),
|
||||
new Command({
|
||||
command: 'part',
|
||||
args: '[<room-alias>]',
|
||||
args: '[<room-address>]',
|
||||
description: _td('Leave room'),
|
||||
runFn: function(roomId, args) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
@ -592,7 +594,7 @@ export const Commands = [
|
|||
}
|
||||
if (targetRoomId) break;
|
||||
}
|
||||
if (!targetRoomId) return reject(_t('Unrecognised room alias:') + ' ' + roomAlias);
|
||||
if (!targetRoomId) return reject(_t('Unrecognised room address:') + ' ' + roomAlias);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -943,8 +945,10 @@ export const Commands = [
|
|||
}
|
||||
|
||||
const member = MatrixClientPeg.get().getRoom(roomId).getMember(userId);
|
||||
dis.dispatch({
|
||||
action: 'view_user',
|
||||
dis.dispatch<ViewUserPayload>({
|
||||
action: Action.ViewUser,
|
||||
// XXX: We should be using a real member object and not assuming what the
|
||||
// receiver wants.
|
||||
member: member || {userId},
|
||||
});
|
||||
return success();
|
||||
|
|
|
@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import dis from './dispatcher';
|
||||
import dis from './dispatcher/dispatcher';
|
||||
import Timer from './utils/Timer';
|
||||
|
||||
// important these are larger than the timeouts of timers
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
/*
|
||||
Copyright 2017 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { asyncAction } from './actionCreators';
|
||||
|
||||
const GroupActions = {};
|
||||
|
||||
/**
|
||||
* Creates an action thunk that will do an asynchronous request to fetch
|
||||
* the groups to which a user is joined.
|
||||
*
|
||||
* @param {MatrixClient} matrixClient the matrix client to query.
|
||||
* @returns {function} an action thunk that will dispatch actions
|
||||
* indicating the status of the request.
|
||||
* @see asyncAction
|
||||
*/
|
||||
GroupActions.fetchJoinedGroups = function(matrixClient) {
|
||||
return asyncAction('GroupActions.fetchJoinedGroups', () => matrixClient.getJoinedGroups());
|
||||
};
|
||||
|
||||
export default GroupActions;
|
34
src/actions/GroupActions.ts
Normal file
34
src/actions/GroupActions.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
Copyright 2017 New Vector Ltd
|
||||
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 { asyncAction } from './actionCreators';
|
||||
import { AsyncActionPayload } from "../dispatcher/payloads";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
|
||||
export default class GroupActions {
|
||||
/**
|
||||
* Creates an action thunk that will do an asynchronous request to fetch
|
||||
* the groups to which a user is joined.
|
||||
*
|
||||
* @param {MatrixClient} matrixClient the matrix client to query.
|
||||
* @returns {AsyncActionPayload} An async action payload.
|
||||
* @see asyncAction
|
||||
*/
|
||||
public static fetchJoinedGroups(matrixClient: MatrixClient): AsyncActionPayload {
|
||||
return asyncAction('GroupActions.fetchJoinedGroups', () => matrixClient.getJoinedGroups(), null);
|
||||
}
|
||||
}
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import dis from '../dispatcher';
|
||||
import dis from '../dispatcher/dispatcher';
|
||||
|
||||
// TODO: migrate from sync_state to MatrixActions.sync so that more js-sdk events
|
||||
// become dispatches in the same place.
|
||||
|
|
|
@ -1,145 +0,0 @@
|
|||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { asyncAction } from './actionCreators';
|
||||
import RoomListStore, {TAG_DM} from '../stores/RoomListStore';
|
||||
import Modal from '../Modal';
|
||||
import * as Rooms from '../Rooms';
|
||||
import { _t } from '../languageHandler';
|
||||
import * as sdk from '../index';
|
||||
|
||||
const RoomListActions = {};
|
||||
|
||||
/**
|
||||
* Creates an action thunk that will do an asynchronous request to
|
||||
* tag room.
|
||||
*
|
||||
* @param {MatrixClient} matrixClient the matrix client to set the
|
||||
* account data on.
|
||||
* @param {Room} room the room to tag.
|
||||
* @param {string} oldTag the tag to remove (unless oldTag ==== newTag)
|
||||
* @param {string} newTag the tag with which to tag the room.
|
||||
* @param {?number} oldIndex the previous position of the room in the
|
||||
* list of rooms.
|
||||
* @param {?number} newIndex the new position of the room in the list
|
||||
* of rooms.
|
||||
* @returns {function} an action thunk.
|
||||
* @see asyncAction
|
||||
*/
|
||||
RoomListActions.tagRoom = function(matrixClient, room, oldTag, newTag, oldIndex, newIndex) {
|
||||
let metaData = null;
|
||||
|
||||
// Is the tag ordered manually?
|
||||
if (newTag && !newTag.match(/^(m\.lowpriority|im\.vector\.fake\.(invite|recent|direct|archived))$/)) {
|
||||
const lists = RoomListStore.getRoomLists();
|
||||
const newList = [...lists[newTag]];
|
||||
|
||||
newList.sort((a, b) => a.tags[newTag].order - b.tags[newTag].order);
|
||||
|
||||
// If the room was moved "down" (increasing index) in the same list we
|
||||
// need to use the orders of the tiles with indices shifted by +1
|
||||
const offset = (
|
||||
newTag === oldTag && oldIndex < newIndex
|
||||
) ? 1 : 0;
|
||||
|
||||
const indexBefore = offset + newIndex - 1;
|
||||
const indexAfter = offset + newIndex;
|
||||
|
||||
const prevOrder = indexBefore <= 0 ?
|
||||
0 : newList[indexBefore].tags[newTag].order;
|
||||
const nextOrder = indexAfter >= newList.length ?
|
||||
1 : newList[indexAfter].tags[newTag].order;
|
||||
|
||||
metaData = {
|
||||
order: (prevOrder + nextOrder) / 2.0,
|
||||
};
|
||||
}
|
||||
|
||||
return asyncAction('RoomListActions.tagRoom', () => {
|
||||
const promises = [];
|
||||
const roomId = room.roomId;
|
||||
|
||||
// Evil hack to get DMs behaving
|
||||
if ((oldTag === undefined && newTag === TAG_DM) ||
|
||||
(oldTag === TAG_DM && newTag === undefined)
|
||||
) {
|
||||
return Rooms.guessAndSetDMRoom(
|
||||
room, newTag === TAG_DM,
|
||||
).catch((err) => {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Failed to set direct chat tag " + err);
|
||||
Modal.createTrackedDialog('Failed to set direct chat tag', '', ErrorDialog, {
|
||||
title: _t('Failed to set direct chat tag'),
|
||||
description: ((err && err.message) ? err.message : _t('Operation failed')),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const hasChangedSubLists = oldTag !== newTag;
|
||||
|
||||
// More evilness: We will still be dealing with moving to favourites/low prio,
|
||||
// but we avoid ever doing a request with TAG_DM.
|
||||
//
|
||||
// if we moved lists, remove the old tag
|
||||
if (oldTag && oldTag !== TAG_DM &&
|
||||
hasChangedSubLists
|
||||
) {
|
||||
const promiseToDelete = matrixClient.deleteRoomTag(
|
||||
roomId, oldTag,
|
||||
).catch(function(err) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Failed to remove tag " + oldTag + " from room: " + err);
|
||||
Modal.createTrackedDialog('Failed to remove tag from room', '', ErrorDialog, {
|
||||
title: _t('Failed to remove tag %(tagName)s from room', {tagName: oldTag}),
|
||||
description: ((err && err.message) ? err.message : _t('Operation failed')),
|
||||
});
|
||||
});
|
||||
|
||||
promises.push(promiseToDelete);
|
||||
}
|
||||
|
||||
// if we moved lists or the ordering changed, add the new tag
|
||||
if (newTag && newTag !== TAG_DM &&
|
||||
(hasChangedSubLists || metaData)
|
||||
) {
|
||||
// metaData is the body of the PUT to set the tag, so it must
|
||||
// at least be an empty object.
|
||||
metaData = metaData || {};
|
||||
|
||||
const promiseToAdd = matrixClient.setRoomTag(roomId, newTag, metaData).catch(function(err) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Failed to add tag " + newTag + " to room: " + err);
|
||||
Modal.createTrackedDialog('Failed to add tag to room', '', ErrorDialog, {
|
||||
title: _t('Failed to add tag %(tagName)s to room', {tagName: newTag}),
|
||||
description: ((err && err.message) ? err.message : _t('Operation failed')),
|
||||
});
|
||||
|
||||
throw err;
|
||||
});
|
||||
|
||||
promises.push(promiseToAdd);
|
||||
}
|
||||
|
||||
return Promise.all(promises);
|
||||
}, () => {
|
||||
// For an optimistic update
|
||||
return {
|
||||
room, oldTag, newTag, metaData,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export default RoomListActions;
|
152
src/actions/RoomListActions.ts
Normal file
152
src/actions/RoomListActions.ts
Normal file
|
@ -0,0 +1,152 @@
|
|||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
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 { asyncAction } from './actionCreators';
|
||||
import { TAG_DM } from '../stores/RoomListStore';
|
||||
import Modal from '../Modal';
|
||||
import * as Rooms from '../Rooms';
|
||||
import { _t } from '../languageHandler';
|
||||
import * as sdk from '../index';
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { AsyncActionPayload } from "../dispatcher/payloads";
|
||||
import { RoomListStoreTempProxy } from "../stores/room-list/RoomListStoreTempProxy";
|
||||
|
||||
export default class RoomListActions {
|
||||
/**
|
||||
* Creates an action thunk that will do an asynchronous request to
|
||||
* tag room.
|
||||
*
|
||||
* @param {MatrixClient} matrixClient the matrix client to set the
|
||||
* account data on.
|
||||
* @param {Room} room the room to tag.
|
||||
* @param {string} oldTag the tag to remove (unless oldTag ==== newTag)
|
||||
* @param {string} newTag the tag with which to tag the room.
|
||||
* @param {?number} oldIndex the previous position of the room in the
|
||||
* list of rooms.
|
||||
* @param {?number} newIndex the new position of the room in the list
|
||||
* of rooms.
|
||||
* @returns {AsyncActionPayload} an async action payload
|
||||
* @see asyncAction
|
||||
*/
|
||||
public static tagRoom(
|
||||
matrixClient: MatrixClient, room: Room,
|
||||
oldTag: string, newTag: string,
|
||||
oldIndex: number | null, newIndex: number | null,
|
||||
): AsyncActionPayload {
|
||||
let metaData = null;
|
||||
|
||||
// Is the tag ordered manually?
|
||||
if (newTag && !newTag.match(/^(m\.lowpriority|im\.vector\.fake\.(invite|recent|direct|archived))$/)) {
|
||||
const lists = RoomListStoreTempProxy.getRoomLists();
|
||||
const newList = [...lists[newTag]];
|
||||
|
||||
newList.sort((a, b) => a.tags[newTag].order - b.tags[newTag].order);
|
||||
|
||||
// If the room was moved "down" (increasing index) in the same list we
|
||||
// need to use the orders of the tiles with indices shifted by +1
|
||||
const offset = (
|
||||
newTag === oldTag && oldIndex < newIndex
|
||||
) ? 1 : 0;
|
||||
|
||||
const indexBefore = offset + newIndex - 1;
|
||||
const indexAfter = offset + newIndex;
|
||||
|
||||
const prevOrder = indexBefore <= 0 ?
|
||||
0 : newList[indexBefore].tags[newTag].order;
|
||||
const nextOrder = indexAfter >= newList.length ?
|
||||
1 : newList[indexAfter].tags[newTag].order;
|
||||
|
||||
metaData = {
|
||||
order: (prevOrder + nextOrder) / 2.0,
|
||||
};
|
||||
}
|
||||
|
||||
return asyncAction('RoomListActions.tagRoom', () => {
|
||||
const promises = [];
|
||||
const roomId = room.roomId;
|
||||
|
||||
// Evil hack to get DMs behaving
|
||||
if ((oldTag === undefined && newTag === TAG_DM) ||
|
||||
(oldTag === TAG_DM && newTag === undefined)
|
||||
) {
|
||||
return Rooms.guessAndSetDMRoom(
|
||||
room, newTag === TAG_DM,
|
||||
).catch((err) => {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Failed to set direct chat tag " + err);
|
||||
Modal.createTrackedDialog('Failed to set direct chat tag', '', ErrorDialog, {
|
||||
title: _t('Failed to set direct chat tag'),
|
||||
description: ((err && err.message) ? err.message : _t('Operation failed')),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const hasChangedSubLists = oldTag !== newTag;
|
||||
|
||||
// More evilness: We will still be dealing with moving to favourites/low prio,
|
||||
// but we avoid ever doing a request with TAG_DM.
|
||||
//
|
||||
// if we moved lists, remove the old tag
|
||||
if (oldTag && oldTag !== TAG_DM &&
|
||||
hasChangedSubLists
|
||||
) {
|
||||
const promiseToDelete = matrixClient.deleteRoomTag(
|
||||
roomId, oldTag,
|
||||
).catch(function (err) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Failed to remove tag " + oldTag + " from room: " + err);
|
||||
Modal.createTrackedDialog('Failed to remove tag from room', '', ErrorDialog, {
|
||||
title: _t('Failed to remove tag %(tagName)s from room', {tagName: oldTag}),
|
||||
description: ((err && err.message) ? err.message : _t('Operation failed')),
|
||||
});
|
||||
});
|
||||
|
||||
promises.push(promiseToDelete);
|
||||
}
|
||||
|
||||
// if we moved lists or the ordering changed, add the new tag
|
||||
if (newTag && newTag !== TAG_DM &&
|
||||
(hasChangedSubLists || metaData)
|
||||
) {
|
||||
// metaData is the body of the PUT to set the tag, so it must
|
||||
// at least be an empty object.
|
||||
metaData = metaData || {};
|
||||
|
||||
const promiseToAdd = matrixClient.setRoomTag(roomId, newTag, metaData).catch(function (err) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Failed to add tag " + newTag + " to room: " + err);
|
||||
Modal.createTrackedDialog('Failed to add tag to room', '', ErrorDialog, {
|
||||
title: _t('Failed to add tag %(tagName)s to room', {tagName: newTag}),
|
||||
description: ((err && err.message) ? err.message : _t('Operation failed')),
|
||||
});
|
||||
|
||||
throw err;
|
||||
});
|
||||
|
||||
promises.push(promiseToAdd);
|
||||
}
|
||||
|
||||
return Promise.all(promises);
|
||||
}, () => {
|
||||
// For an optimistic update
|
||||
return {
|
||||
room, oldTag, newTag, metaData,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,109 +0,0 @@
|
|||
/*
|
||||
Copyright 2017 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import Analytics from '../Analytics';
|
||||
import { asyncAction } from './actionCreators';
|
||||
import TagOrderStore from '../stores/TagOrderStore';
|
||||
|
||||
const TagOrderActions = {};
|
||||
|
||||
/**
|
||||
* Creates an action thunk that will do an asynchronous request to
|
||||
* move a tag in TagOrderStore to destinationIx.
|
||||
*
|
||||
* @param {MatrixClient} matrixClient the matrix client to set the
|
||||
* account data on.
|
||||
* @param {string} tag the tag to move.
|
||||
* @param {number} destinationIx the new position of the tag.
|
||||
* @returns {function} an action thunk that will dispatch actions
|
||||
* indicating the status of the request.
|
||||
* @see asyncAction
|
||||
*/
|
||||
TagOrderActions.moveTag = function(matrixClient, tag, destinationIx) {
|
||||
// Only commit tags if the state is ready, i.e. not null
|
||||
let tags = TagOrderStore.getOrderedTags();
|
||||
let removedTags = TagOrderStore.getRemovedTagsAccountData() || [];
|
||||
if (!tags) {
|
||||
return;
|
||||
}
|
||||
|
||||
tags = tags.filter((t) => t !== tag);
|
||||
tags = [...tags.slice(0, destinationIx), tag, ...tags.slice(destinationIx)];
|
||||
|
||||
removedTags = removedTags.filter((t) => t !== tag);
|
||||
|
||||
const storeId = TagOrderStore.getStoreId();
|
||||
|
||||
return asyncAction('TagOrderActions.moveTag', () => {
|
||||
Analytics.trackEvent('TagOrderActions', 'commitTagOrdering');
|
||||
return matrixClient.setAccountData(
|
||||
'im.vector.web.tag_ordering',
|
||||
{tags, removedTags, _storeId: storeId},
|
||||
);
|
||||
}, () => {
|
||||
// For an optimistic update
|
||||
return {tags, removedTags};
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates an action thunk that will do an asynchronous request to
|
||||
* label a tag as removed in im.vector.web.tag_ordering account data.
|
||||
*
|
||||
* The reason this is implemented with new state `removedTags` is that
|
||||
* we incrementally and initially populate `tags` with groups that
|
||||
* have been joined. If we remove a group from `tags`, it will just
|
||||
* get added (as it looks like a group we've recently joined).
|
||||
*
|
||||
* NB: If we ever support adding of tags (which is planned), we should
|
||||
* take special care to remove the tag from `removedTags` when we add
|
||||
* it.
|
||||
*
|
||||
* @param {MatrixClient} matrixClient the matrix client to set the
|
||||
* account data on.
|
||||
* @param {string} tag the tag to remove.
|
||||
* @returns {function} an action thunk that will dispatch actions
|
||||
* indicating the status of the request.
|
||||
* @see asyncAction
|
||||
*/
|
||||
TagOrderActions.removeTag = function(matrixClient, tag) {
|
||||
// Don't change tags, just removedTags
|
||||
const tags = TagOrderStore.getOrderedTags();
|
||||
const removedTags = TagOrderStore.getRemovedTagsAccountData() || [];
|
||||
|
||||
if (removedTags.includes(tag)) {
|
||||
// Return a thunk that doesn't do anything, we don't even need
|
||||
// an asynchronous action here, the tag is already removed.
|
||||
return () => {};
|
||||
}
|
||||
|
||||
removedTags.push(tag);
|
||||
|
||||
const storeId = TagOrderStore.getStoreId();
|
||||
|
||||
return asyncAction('TagOrderActions.removeTag', () => {
|
||||
Analytics.trackEvent('TagOrderActions', 'removeTag');
|
||||
return matrixClient.setAccountData(
|
||||
'im.vector.web.tag_ordering',
|
||||
{tags, removedTags, _storeId: storeId},
|
||||
);
|
||||
}, () => {
|
||||
// For an optimistic update
|
||||
return {removedTags};
|
||||
});
|
||||
};
|
||||
|
||||
export default TagOrderActions;
|
111
src/actions/TagOrderActions.ts
Normal file
111
src/actions/TagOrderActions.ts
Normal file
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
Copyright 2017 New Vector Ltd
|
||||
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 Analytics from '../Analytics';
|
||||
import { asyncAction } from './actionCreators';
|
||||
import TagOrderStore from '../stores/TagOrderStore';
|
||||
import { AsyncActionPayload } from "../dispatcher/payloads";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
|
||||
export default class TagOrderActions {
|
||||
|
||||
/**
|
||||
* Creates an action thunk that will do an asynchronous request to
|
||||
* move a tag in TagOrderStore to destinationIx.
|
||||
*
|
||||
* @param {MatrixClient} matrixClient the matrix client to set the
|
||||
* account data on.
|
||||
* @param {string} tag the tag to move.
|
||||
* @param {number} destinationIx the new position of the tag.
|
||||
* @returns {AsyncActionPayload} an async action payload that will
|
||||
* dispatch actions indicating the status of the request.
|
||||
* @see asyncAction
|
||||
*/
|
||||
public static moveTag(matrixClient: MatrixClient, tag: string, destinationIx: number): AsyncActionPayload {
|
||||
// Only commit tags if the state is ready, i.e. not null
|
||||
let tags = TagOrderStore.getOrderedTags();
|
||||
let removedTags = TagOrderStore.getRemovedTagsAccountData() || [];
|
||||
if (!tags) {
|
||||
return;
|
||||
}
|
||||
|
||||
tags = tags.filter((t) => t !== tag);
|
||||
tags = [...tags.slice(0, destinationIx), tag, ...tags.slice(destinationIx)];
|
||||
|
||||
removedTags = removedTags.filter((t) => t !== tag);
|
||||
|
||||
const storeId = TagOrderStore.getStoreId();
|
||||
|
||||
return asyncAction('TagOrderActions.moveTag', () => {
|
||||
Analytics.trackEvent('TagOrderActions', 'commitTagOrdering');
|
||||
return matrixClient.setAccountData(
|
||||
'im.vector.web.tag_ordering',
|
||||
{tags, removedTags, _storeId: storeId},
|
||||
);
|
||||
}, () => {
|
||||
// For an optimistic update
|
||||
return {tags, removedTags};
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates an action thunk that will do an asynchronous request to
|
||||
* label a tag as removed in im.vector.web.tag_ordering account data.
|
||||
*
|
||||
* The reason this is implemented with new state `removedTags` is that
|
||||
* we incrementally and initially populate `tags` with groups that
|
||||
* have been joined. If we remove a group from `tags`, it will just
|
||||
* get added (as it looks like a group we've recently joined).
|
||||
*
|
||||
* NB: If we ever support adding of tags (which is planned), we should
|
||||
* take special care to remove the tag from `removedTags` when we add
|
||||
* it.
|
||||
*
|
||||
* @param {MatrixClient} matrixClient the matrix client to set the
|
||||
* account data on.
|
||||
* @param {string} tag the tag to remove.
|
||||
* @returns {function} an async action payload that will dispatch
|
||||
* actions indicating the status of the request.
|
||||
* @see asyncAction
|
||||
*/
|
||||
public static removeTag(matrixClient: MatrixClient, tag: string): AsyncActionPayload {
|
||||
// Don't change tags, just removedTags
|
||||
const tags = TagOrderStore.getOrderedTags();
|
||||
const removedTags = TagOrderStore.getRemovedTagsAccountData() || [];
|
||||
|
||||
if (removedTags.includes(tag)) {
|
||||
// Return a thunk that doesn't do anything, we don't even need
|
||||
// an asynchronous action here, the tag is already removed.
|
||||
return new AsyncActionPayload(() => {});
|
||||
}
|
||||
|
||||
removedTags.push(tag);
|
||||
|
||||
const storeId = TagOrderStore.getStoreId();
|
||||
|
||||
return asyncAction('TagOrderActions.removeTag', () => {
|
||||
Analytics.trackEvent('TagOrderActions', 'removeTag');
|
||||
return matrixClient.setAccountData(
|
||||
'im.vector.web.tag_ordering',
|
||||
{tags, removedTags, _storeId: storeId},
|
||||
);
|
||||
}, () => {
|
||||
// For an optimistic update
|
||||
return {removedTags};
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2017 New Vector Ltd
|
||||
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.
|
||||
|
@ -14,6 +15,8 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { AsyncActionPayload } from "../dispatcher/payloads";
|
||||
|
||||
/**
|
||||
* Create an action thunk that will dispatch actions indicating the current
|
||||
* status of the Promise returned by fn.
|
||||
|
@ -25,9 +28,9 @@ limitations under the License.
|
|||
* @param {function?} pendingFn a function that returns an object to assign
|
||||
* to the `request` key of the ${id}.pending
|
||||
* payload.
|
||||
* @returns {function} an action thunk - a function that uses its single
|
||||
* argument as a dispatch function to dispatch the
|
||||
* following actions:
|
||||
* @returns {AsyncActionPayload} an async action payload. Includes a function
|
||||
* that uses its single argument as a dispatch function
|
||||
* to dispatch the following actions:
|
||||
* `${id}.pending` and either
|
||||
* `${id}.success` or
|
||||
* `${id}.failure`.
|
||||
|
@ -41,12 +44,11 @@ limitations under the License.
|
|||
* result is the result of the promise returned by
|
||||
* `fn`.
|
||||
*/
|
||||
export function asyncAction(id, fn, pendingFn) {
|
||||
return (dispatch) => {
|
||||
export function asyncAction(id: string, fn: () => Promise<any>, pendingFn: () => any | null): AsyncActionPayload {
|
||||
const helper = (dispatch) => {
|
||||
dispatch({
|
||||
action: id + '.pending',
|
||||
request:
|
||||
typeof pendingFn === 'function' ? pendingFn() : undefined,
|
||||
request: typeof pendingFn === 'function' ? pendingFn() : undefined,
|
||||
});
|
||||
fn().then((result) => {
|
||||
dispatch({action: id + '.success', result});
|
||||
|
@ -54,4 +56,5 @@ export function asyncAction(id, fn, pendingFn) {
|
|||
dispatch({action: id + '.failure', err});
|
||||
});
|
||||
};
|
||||
return new AsyncActionPayload(helper);
|
||||
}
|
|
@ -1,206 +0,0 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import {Key} from "../../../Keyboard";
|
||||
import * as sdk from "../../../index";
|
||||
|
||||
// XXX: This component is not cross-signing aware.
|
||||
// https://github.com/vector-im/riot-web/issues/11752 tracks either updating this
|
||||
// component or taking it out to pasture.
|
||||
export default createReactClass({
|
||||
displayName: 'EncryptedEventDialog',
|
||||
|
||||
propTypes: {
|
||||
event: PropTypes.object.isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return { device: null };
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this._unmounted = false;
|
||||
const client = MatrixClientPeg.get();
|
||||
|
||||
// first try to load the device from our store.
|
||||
//
|
||||
this.refreshDevice().then((dev) => {
|
||||
if (dev) {
|
||||
return dev;
|
||||
}
|
||||
|
||||
// tell the client to try to refresh the device list for this user
|
||||
return client.downloadKeys([this.props.event.getSender()], true).then(() => {
|
||||
return this.refreshDevice();
|
||||
});
|
||||
}).then((dev) => {
|
||||
if (this._unmounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ device: dev });
|
||||
client.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||
}, (err)=>{
|
||||
console.log("Error downloading devices", err);
|
||||
});
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this._unmounted = true;
|
||||
const client = MatrixClientPeg.get();
|
||||
if (client) {
|
||||
client.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||
}
|
||||
},
|
||||
|
||||
refreshDevice: function() {
|
||||
// Promise.resolve to handle transition from static result to promise; can be removed
|
||||
// in future
|
||||
return Promise.resolve(MatrixClientPeg.get().getEventSenderDeviceInfo(this.props.event));
|
||||
},
|
||||
|
||||
onDeviceVerificationChanged: function(userId, device) {
|
||||
if (userId === this.props.event.getSender()) {
|
||||
this.refreshDevice().then((dev) => {
|
||||
this.setState({ device: dev });
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onKeyDown: function(e) {
|
||||
if (e.key === Key.ESCAPE) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.props.onFinished(false);
|
||||
}
|
||||
},
|
||||
|
||||
_renderDeviceInfo: function() {
|
||||
const device = this.state.device;
|
||||
if (!device) {
|
||||
return (<i>{ _t('unknown device') }</i>);
|
||||
}
|
||||
|
||||
let verificationStatus = (<b>{ _t('NOT verified') }</b>);
|
||||
if (device.isBlocked()) {
|
||||
verificationStatus = (<b>{ _t('Blacklisted') }</b>);
|
||||
} else if (device.isVerified()) {
|
||||
verificationStatus = _t('verified');
|
||||
}
|
||||
|
||||
return (
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{ _t('Name') }</td>
|
||||
<td>{ device.getDisplayName() }</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{ _t('Device ID') }</td>
|
||||
<td><code>{ device.deviceId }</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{ _t('Verification') }</td>
|
||||
<td>{ verificationStatus }</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{ _t('Ed25519 fingerprint') }</td>
|
||||
<td><code>{ device.getFingerprint() }</code></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
},
|
||||
|
||||
_renderEventInfo: function() {
|
||||
const event = this.props.event;
|
||||
|
||||
return (
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{ _t('User ID') }</td>
|
||||
<td>{ event.getSender() }</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{ _t('Curve25519 identity key') }</td>
|
||||
<td><code>{ event.getSenderKey() || <i>{ _t('none') }</i> }</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{ _t('Claimed Ed25519 fingerprint key') }</td>
|
||||
<td><code>{ event.getKeysClaimed().ed25519 || <i>{ _t('none') }</i> }</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{ _t('Algorithm') }</td>
|
||||
<td>{ event.getWireContent().algorithm || <i>{ _t('unencrypted') }</i> }</td>
|
||||
</tr>
|
||||
{
|
||||
event.getContent().msgtype === 'm.bad.encrypted' ? (
|
||||
<tr>
|
||||
<td>{ _t('Decryption error') }</td>
|
||||
<td>{ event.getContent().body }</td>
|
||||
</tr>
|
||||
) : null
|
||||
}
|
||||
<tr>
|
||||
<td>{ _t('Session ID') }</td>
|
||||
<td><code>{ event.getWireContent().session_id || <i>{ _t('none') }</i> }</code></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const DeviceVerifyButtons = sdk.getComponent('elements.DeviceVerifyButtons');
|
||||
|
||||
let buttons = null;
|
||||
if (this.state.device) {
|
||||
buttons = (
|
||||
<DeviceVerifyButtons device={this.state.device}
|
||||
userId={this.props.event.getSender()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_EncryptedEventDialog" onKeyDown={this.onKeyDown}>
|
||||
<div className="mx_Dialog_title">
|
||||
{ _t('End-to-end encryption information') }
|
||||
</div>
|
||||
<div className="mx_Dialog_content">
|
||||
<h4>{ _t('Event information') }</h4>
|
||||
{ this._renderEventInfo() }
|
||||
|
||||
<h4>{ _t('Sender session information') }</h4>
|
||||
{ this._renderDeviceInfo() }
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button className="mx_Dialog_primary" onClick={this.props.onFinished} autoFocus={true}>
|
||||
{ _t('OK') }
|
||||
</button>
|
||||
{ buttons }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -17,11 +17,12 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import * as sdk from '../../../../index';
|
||||
import PropTypes from 'prop-types';
|
||||
import dis from "../../../../dispatcher";
|
||||
import dis from "../../../../dispatcher/dispatcher";
|
||||
import { _t } from '../../../../languageHandler';
|
||||
|
||||
import SettingsStore, {SettingLevel} from "../../../../settings/SettingsStore";
|
||||
import EventIndexPeg from "../../../../indexing/EventIndexPeg";
|
||||
import {Action} from "../../../../dispatcher/actions";
|
||||
|
||||
/*
|
||||
* Allows the user to disable the Event Index.
|
||||
|
@ -47,7 +48,7 @@ export default class DisableEventIndexDialog extends React.Component {
|
|||
await SettingsStore.setValue('enableEventIndexing', null, SettingLevel.DEVICE, false);
|
||||
await EventIndexPeg.deleteEventIndex();
|
||||
this.props.onFinished();
|
||||
dis.dispatch({ action: 'view_user_settings' });
|
||||
dis.fire(Action.ViewUserSettings);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
|
|
@ -284,8 +284,10 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
|||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
let matchText;
|
||||
let changeText;
|
||||
if (this.state.passPhraseConfirm === this.state.passPhrase) {
|
||||
matchText = _t("That matches!");
|
||||
changeText = _t("Use a different passphrase?");
|
||||
} else if (!this.state.passPhrase.startsWith(this.state.passPhraseConfirm)) {
|
||||
// only tell them they're wrong if they've actually gone wrong.
|
||||
// Security concious readers will note that if you left riot-web unattended
|
||||
|
@ -295,6 +297,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
|||
// Note that not having typed anything at all will not hit this clause and
|
||||
// fall through so empty box === no hint.
|
||||
matchText = _t("That doesn't match.");
|
||||
changeText = _t("Go back to set it again.");
|
||||
}
|
||||
|
||||
let passPhraseMatch = null;
|
||||
|
@ -303,7 +306,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
|||
<div>{matchText}</div>
|
||||
<div>
|
||||
<AccessibleButton element="span" className="mx_linkButton" onClick={this._onSetAgainClick}>
|
||||
{_t("Go back to set it again.")}
|
||||
{changeText}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>;
|
||||
|
|
|
@ -19,9 +19,10 @@ import React from "react";
|
|||
import PropTypes from "prop-types";
|
||||
import * as sdk from "../../../../index";
|
||||
import {MatrixClientPeg} from '../../../../MatrixClientPeg';
|
||||
import dis from "../../../../dispatcher";
|
||||
import dis from "../../../../dispatcher/dispatcher";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import Modal from "../../../../Modal";
|
||||
import {Action} from "../../../../dispatcher/actions";
|
||||
|
||||
export default class NewRecoveryMethodDialog extends React.PureComponent {
|
||||
static propTypes = {
|
||||
|
@ -36,7 +37,7 @@ export default class NewRecoveryMethodDialog extends React.PureComponent {
|
|||
|
||||
onGoToSettingsClick = () => {
|
||||
this.props.onFinished();
|
||||
dis.dispatch({ action: 'view_user_settings' });
|
||||
dis.fire(Action.ViewUserSettings);
|
||||
}
|
||||
|
||||
onSetupClick = async () => {
|
||||
|
|
|
@ -18,9 +18,10 @@ limitations under the License.
|
|||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import * as sdk from "../../../../index";
|
||||
import dis from "../../../../dispatcher";
|
||||
import dis from "../../../../dispatcher/dispatcher";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import Modal from "../../../../Modal";
|
||||
import {Action} from "../../../../dispatcher/actions";
|
||||
|
||||
export default class RecoveryMethodRemovedDialog extends React.PureComponent {
|
||||
static propTypes = {
|
||||
|
@ -29,7 +30,7 @@ export default class RecoveryMethodRemovedDialog extends React.PureComponent {
|
|||
|
||||
onGoToSettingsClick = () => {
|
||||
this.props.onFinished();
|
||||
dis.dispatch({ action: 'view_user_settings' });
|
||||
dis.fire(Action.ViewUserSettings);
|
||||
}
|
||||
|
||||
onSetupClick = () => {
|
||||
|
|
|
@ -201,7 +201,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
type: 'm.id.user',
|
||||
user: MatrixClientPeg.get().getUserId(),
|
||||
},
|
||||
// https://github.com/matrix-org/synapse/issues/5665
|
||||
// TODO: Remove `user` once servers support proper UIA
|
||||
// See https://github.com/matrix-org/synapse/issues/5665
|
||||
user: MatrixClientPeg.get().getUserId(),
|
||||
password: this.state.accountPassword,
|
||||
});
|
||||
|
@ -538,8 +539,10 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
const Field = sdk.getComponent('views.elements.Field');
|
||||
|
||||
let matchText;
|
||||
let changeText;
|
||||
if (this.state.passPhraseConfirm === this.state.passPhrase) {
|
||||
matchText = _t("That matches!");
|
||||
changeText = _t("Use a different passphrase?");
|
||||
} else if (!this.state.passPhrase.startsWith(this.state.passPhraseConfirm)) {
|
||||
// only tell them they're wrong if they've actually gone wrong.
|
||||
// Security concious readers will note that if you left riot-web unattended
|
||||
|
@ -549,6 +552,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
// Note that not having typed anything at all will not hit this clause and
|
||||
// fall through so empty box === no hint.
|
||||
matchText = _t("That doesn't match.");
|
||||
changeText = _t("Go back to set it again.");
|
||||
}
|
||||
|
||||
let passPhraseMatch = null;
|
||||
|
@ -557,7 +561,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
<div>{matchText}</div>
|
||||
<div>
|
||||
<AccessibleButton element="span" className="mx_linkButton" onClick={this._onSetAgainClick}>
|
||||
{_t("Go back to set it again.")}
|
||||
{changeText}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>;
|
||||
|
|
|
@ -69,7 +69,7 @@ export default class EmojiProvider extends AutocompleteProvider {
|
|||
|
||||
constructor() {
|
||||
super(EMOJI_REGEX);
|
||||
this.matcher = new QueryMatcher(EMOJI_SHORTNAMES, {
|
||||
this.matcher = new QueryMatcher<IEmojiShort>(EMOJI_SHORTNAMES, {
|
||||
keys: ['emoji.emoticon', 'shortname'],
|
||||
funcs: [
|
||||
(o) => o.emoji.shortcodes.length > 1 ? o.emoji.shortcodes.slice(1).map(s => `:${s}:`).join(" ") : "", // aliases
|
||||
|
|
|
@ -45,7 +45,7 @@ interface IOptions<T extends {}> {
|
|||
* @param {function[]} options.funcs List of functions that when called with the
|
||||
* object as an arg will return a string to use as an index
|
||||
*/
|
||||
export default class QueryMatcher<T> {
|
||||
export default class QueryMatcher<T extends Object> {
|
||||
private _options: IOptions<T>;
|
||||
private _keys: IOptions<T>["keys"];
|
||||
private _funcs: Required<IOptions<T>["funcs"]>;
|
||||
|
@ -75,7 +75,11 @@ export default class QueryMatcher<T> {
|
|||
this._items = new Map();
|
||||
|
||||
for (const object of objects) {
|
||||
const keyValues = _at(object, this._keys);
|
||||
// Need to use unsafe coerce here because the objects can have any
|
||||
// type for their values. We assume that those values who's keys have
|
||||
// been specified will be string. Also, we cannot infer all the
|
||||
// types of the keys of the objects at compile.
|
||||
const keyValues = _at<string>(<any>object, this._keys);
|
||||
|
||||
for (const f of this._funcs) {
|
||||
keyValues.push(f(object));
|
||||
|
|
|
@ -18,7 +18,7 @@ import React from 'react';
|
|||
import CustomRoomTagStore from '../../stores/CustomRoomTagStore';
|
||||
import AutoHideScrollbar from './AutoHideScrollbar';
|
||||
import * as sdk from '../../index';
|
||||
import dis from '../../dispatcher';
|
||||
import dis from '../../dispatcher/dispatcher';
|
||||
import classNames from 'classnames';
|
||||
import * as FormattingUtils from '../../utils/FormattingUtils';
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ import PropTypes from 'prop-types';
|
|||
import request from 'browser-request';
|
||||
import { _t } from '../../languageHandler';
|
||||
import sanitizeHtml from 'sanitize-html';
|
||||
import dis from '../../dispatcher';
|
||||
import dis from '../../dispatcher/dispatcher';
|
||||
import {MatrixClientPeg} from '../../MatrixClientPeg';
|
||||
import classnames from 'classnames';
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
|
|
|
@ -21,7 +21,7 @@ import createReactClass from 'create-react-class';
|
|||
import PropTypes from 'prop-types';
|
||||
import {MatrixClientPeg} from '../../MatrixClientPeg';
|
||||
import * as sdk from '../../index';
|
||||
import dis from '../../dispatcher';
|
||||
import dis from '../../dispatcher/dispatcher';
|
||||
import { getHostingLink } from '../../utils/HostingLink';
|
||||
import { sanitizedHtmlNode } from '../../HtmlUtils';
|
||||
import { _t, _td } from '../../languageHandler';
|
||||
|
@ -92,7 +92,7 @@ const CategoryRoomList = createReactClass({
|
|||
Modal.createTrackedDialog('Add Rooms to Group Summary', '', AddressPickerDialog, {
|
||||
title: _t('Add rooms to the community summary'),
|
||||
description: _t("Which rooms would you like to add to this summary?"),
|
||||
placeholder: _t("Room name or alias"),
|
||||
placeholder: _t("Room name or address"),
|
||||
button: _t("Add to summary"),
|
||||
pickerType: 'room',
|
||||
validAddressTypes: ['mx-room-id'],
|
||||
|
|
|
@ -21,7 +21,7 @@ import { getHomePageUrl } from "../../utils/pages";
|
|||
import { _t } from "../../languageHandler";
|
||||
import SdkConfig from "../../SdkConfig";
|
||||
import * as sdk from "../../index";
|
||||
import dis from "../../dispatcher";
|
||||
import dis from "../../dispatcher/dispatcher";
|
||||
|
||||
const onClickSendDm = () => dis.dispatch({action: 'view_create_chat'});
|
||||
const onClickExplore = () => dis.dispatch({action: 'view_room_directory'});
|
||||
|
|
|
@ -21,11 +21,12 @@ import PropTypes from 'prop-types';
|
|||
import classNames from 'classnames';
|
||||
import { Key } from '../../Keyboard';
|
||||
import * as sdk from '../../index';
|
||||
import dis from '../../dispatcher';
|
||||
import dis from '../../dispatcher/dispatcher';
|
||||
import * as VectorConferenceHandler from '../../VectorConferenceHandler';
|
||||
import SettingsStore from '../../settings/SettingsStore';
|
||||
import {_t} from "../../languageHandler";
|
||||
import Analytics from "../../Analytics";
|
||||
import RoomList2 from "../views/rooms/RoomList2";
|
||||
|
||||
|
||||
const LeftPanel = createReactClass({
|
||||
|
@ -273,6 +274,29 @@ const LeftPanel = createReactClass({
|
|||
breadcrumbs = (<RoomBreadcrumbs collapsed={this.props.collapsed} />);
|
||||
}
|
||||
|
||||
let roomList = null;
|
||||
if (SettingsStore.isFeatureEnabled("feature_new_room_list")) {
|
||||
roomList = <RoomList2
|
||||
onKeyDown={this._onKeyDown}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
collapsed={this.props.collapsed}
|
||||
searchFilter={this.state.searchFilter}
|
||||
ref={this.collectRoomList}
|
||||
onFocus={this._onFocus}
|
||||
onBlur={this._onBlur}
|
||||
/>;
|
||||
} else {
|
||||
roomList = <RoomList
|
||||
onKeyDown={this._onKeyDown}
|
||||
onFocus={this._onFocus}
|
||||
onBlur={this._onBlur}
|
||||
ref={this.collectRoomList}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
collapsed={this.props.collapsed}
|
||||
searchFilter={this.state.searchFilter}
|
||||
ConferenceHandler={VectorConferenceHandler} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={containerClasses}>
|
||||
{ tagPanelContainer }
|
||||
|
@ -284,15 +308,7 @@ const LeftPanel = createReactClass({
|
|||
{ exploreButton }
|
||||
{ searchBox }
|
||||
</div>
|
||||
<RoomList
|
||||
onKeyDown={this._onKeyDown}
|
||||
onFocus={this._onFocus}
|
||||
onBlur={this._onBlur}
|
||||
ref={this.collectRoomList}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
collapsed={this.props.collapsed}
|
||||
searchFilter={this.state.searchFilter}
|
||||
ConferenceHandler={VectorConferenceHandler} />
|
||||
{roomList}
|
||||
</aside>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -27,11 +27,10 @@ import PageTypes from '../../PageTypes';
|
|||
import CallMediaHandler from '../../CallMediaHandler';
|
||||
import { fixupColorFonts } from '../../utils/FontManager';
|
||||
import * as sdk from '../../index';
|
||||
import dis from '../../dispatcher';
|
||||
import dis from '../../dispatcher/dispatcher';
|
||||
import sessionStore from '../../stores/SessionStore';
|
||||
import {MatrixClientPeg, MatrixClientCreds} from '../../MatrixClientPeg';
|
||||
import {MatrixClientPeg, IMatrixClientCreds} from '../../MatrixClientPeg';
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import RoomListStore from "../../stores/RoomListStore";
|
||||
|
||||
import TagOrderActions from '../../actions/TagOrderActions';
|
||||
import RoomListActions from '../../actions/RoomListActions';
|
||||
|
@ -42,6 +41,17 @@ import * as KeyboardShortcuts from "../../accessibility/KeyboardShortcuts";
|
|||
import HomePage from "./HomePage";
|
||||
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||
import PlatformPeg from "../../PlatformPeg";
|
||||
import { RoomListStoreTempProxy } from "../../stores/room-list/RoomListStoreTempProxy";
|
||||
import { DefaultTagID } from "../../stores/room-list/models";
|
||||
import {
|
||||
showToast as showSetPasswordToast,
|
||||
hideToast as hideSetPasswordToast
|
||||
} from "../../toasts/SetPasswordToast";
|
||||
import {
|
||||
showToast as showServerLimitToast,
|
||||
hideToast as hideServerLimitToast
|
||||
} from "../../toasts/ServerLimitToast";
|
||||
|
||||
// We need to fetch each pinned message individually (if we don't already have it)
|
||||
// so each pinned message may trigger a request. Limit the number per room for sanity.
|
||||
// NB. this is just for server notices rather than pinned messages in general.
|
||||
|
@ -56,7 +66,7 @@ function canElementReceiveInput(el) {
|
|||
|
||||
interface IProps {
|
||||
matrixClient: MatrixClient;
|
||||
onRegistered: (credentials: MatrixClientCreds) => Promise<MatrixClient>;
|
||||
onRegistered: (credentials: IMatrixClientCreds) => Promise<MatrixClient>;
|
||||
viaServers?: string[];
|
||||
hideToSRUsers: boolean;
|
||||
resizeNotifier: ResizeNotifier;
|
||||
|
@ -64,10 +74,6 @@ interface IProps {
|
|||
initialEventPixelOffset: number;
|
||||
leftDisabled: boolean;
|
||||
rightDisabled: boolean;
|
||||
showCookieBar: boolean;
|
||||
hasNewVersion: boolean;
|
||||
userHasGeneratedPassword: boolean;
|
||||
showNotifierToolbar: boolean;
|
||||
page_type: string;
|
||||
autoJoin: boolean;
|
||||
thirdPartyInvite?: object;
|
||||
|
@ -75,7 +81,6 @@ interface IProps {
|
|||
currentRoomId: string;
|
||||
ConferenceHandler?: object;
|
||||
collapseLhs: boolean;
|
||||
checkingForUpdate: boolean;
|
||||
config: {
|
||||
piwik: {
|
||||
policyUrl: string;
|
||||
|
@ -85,10 +90,8 @@ interface IProps {
|
|||
currentUserId?: string;
|
||||
currentGroupId?: string;
|
||||
currentGroupIsNew?: boolean;
|
||||
version?: string;
|
||||
newVersion?: string;
|
||||
newVersionReleaseNotes?: string;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
mouseDown?: {
|
||||
x: number;
|
||||
|
@ -96,8 +99,6 @@ interface IState {
|
|||
};
|
||||
syncErrorData: any;
|
||||
useCompactLayout: boolean;
|
||||
serverNoticeEvents: MatrixEvent[];
|
||||
userHasGeneratedPassword: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -140,11 +141,8 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
|
|||
this.state = {
|
||||
mouseDown: undefined,
|
||||
syncErrorData: undefined,
|
||||
userHasGeneratedPassword: false,
|
||||
// use compact timeline view
|
||||
useCompactLayout: SettingsStore.getValue('useCompactLayout'),
|
||||
// any currently active server notice events
|
||||
serverNoticeEvents: [],
|
||||
};
|
||||
|
||||
// stash the MatrixClient in case we log out before we are unmounted
|
||||
|
@ -178,18 +176,6 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
|
|||
this._loadResizerPreferences();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
// attempt to guess when a banner was opened or closed
|
||||
if (
|
||||
(prevProps.showCookieBar !== this.props.showCookieBar) ||
|
||||
(prevProps.hasNewVersion !== this.props.hasNewVersion) ||
|
||||
(prevState.userHasGeneratedPassword !== this.state.userHasGeneratedPassword) ||
|
||||
(prevProps.showNotifierToolbar !== this.props.showNotifierToolbar)
|
||||
) {
|
||||
this.props.resizeNotifier.notifyBannersChanged();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('keydown', this._onNativeKeyDown, false);
|
||||
this._matrixClient.removeListener("accountData", this.onAccountData);
|
||||
|
@ -219,9 +205,11 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
|
|||
};
|
||||
|
||||
_setStateFromSessionStore = () => {
|
||||
this.setState({
|
||||
userHasGeneratedPassword: Boolean(this._sessionStore.getCachedPassword()),
|
||||
});
|
||||
if (this._sessionStore.getCachedPassword()) {
|
||||
showSetPasswordToast();
|
||||
} else {
|
||||
hideSetPasswordToast();
|
||||
}
|
||||
};
|
||||
|
||||
_createResizer() {
|
||||
|
@ -293,22 +281,37 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
|
|||
|
||||
if (oldSyncState === 'PREPARED' && syncState === 'SYNCING') {
|
||||
this._updateServerNoticeEvents();
|
||||
} else {
|
||||
this._calculateServerLimitToast(data);
|
||||
}
|
||||
};
|
||||
|
||||
onRoomStateEvents = (ev, state) => {
|
||||
const roomLists = RoomListStore.getRoomLists();
|
||||
if (roomLists['m.server_notice'] && roomLists['m.server_notice'].some(r => r.roomId === ev.getRoomId())) {
|
||||
const roomLists = RoomListStoreTempProxy.getRoomLists();
|
||||
if (roomLists[DefaultTagID.ServerNotice] && roomLists[DefaultTagID.ServerNotice].some(r => r.roomId === ev.getRoomId())) {
|
||||
this._updateServerNoticeEvents();
|
||||
}
|
||||
};
|
||||
|
||||
_updateServerNoticeEvents = async () => {
|
||||
const roomLists = RoomListStore.getRoomLists();
|
||||
if (!roomLists['m.server_notice']) return [];
|
||||
_calculateServerLimitToast(syncErrorData, usageLimitEventContent?) {
|
||||
const error = syncErrorData && syncErrorData.error && syncErrorData.error.errcode === "M_RESOURCE_LIMIT_EXCEEDED";
|
||||
if (error) {
|
||||
usageLimitEventContent = syncErrorData.error.data;
|
||||
}
|
||||
|
||||
const pinnedEvents = [];
|
||||
for (const room of roomLists['m.server_notice']) {
|
||||
if (usageLimitEventContent) {
|
||||
showServerLimitToast(usageLimitEventContent.limit_type, usageLimitEventContent.admin_contact, error);
|
||||
} else {
|
||||
hideServerLimitToast();
|
||||
}
|
||||
}
|
||||
|
||||
_updateServerNoticeEvents = async () => {
|
||||
const roomLists = RoomListStoreTempProxy.getRoomLists();
|
||||
if (!roomLists[DefaultTagID.ServerNotice]) return [];
|
||||
|
||||
const events = [];
|
||||
for (const room of roomLists[DefaultTagID.ServerNotice]) {
|
||||
const pinStateEvent = room.currentState.getStateEvents("m.room.pinned_events", "");
|
||||
|
||||
if (!pinStateEvent || !pinStateEvent.getContent().pinned) continue;
|
||||
|
@ -317,12 +320,18 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
|
|||
for (const eventId of pinnedEventIds) {
|
||||
const timeline = await this._matrixClient.getEventTimeline(room.getUnfilteredTimelineSet(), eventId, 0);
|
||||
const event = timeline.getEvents().find(ev => ev.getId() === eventId);
|
||||
if (event) pinnedEvents.push(event);
|
||||
if (event) events.push(event);
|
||||
}
|
||||
}
|
||||
this.setState({
|
||||
serverNoticeEvents: pinnedEvents,
|
||||
|
||||
const usageLimitEvent = events.find((e) => {
|
||||
return (
|
||||
e && e.getType() === 'm.room.message' &&
|
||||
e.getContent()['server_notice_type'] === 'm.server_notice.usage_limit_reached'
|
||||
);
|
||||
});
|
||||
|
||||
this._calculateServerLimitToast(this.state.syncErrorData, usageLimitEvent && usageLimitEvent.getContent());
|
||||
};
|
||||
|
||||
_onPaste = (ev) => {
|
||||
|
@ -598,12 +607,6 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
|
|||
const GroupView = sdk.getComponent('structures.GroupView');
|
||||
const MyGroups = sdk.getComponent('structures.MyGroups');
|
||||
const ToastContainer = sdk.getComponent('structures.ToastContainer');
|
||||
const MatrixToolbar = sdk.getComponent('globals.MatrixToolbar');
|
||||
const CookieBar = sdk.getComponent('globals.CookieBar');
|
||||
const NewVersionBar = sdk.getComponent('globals.NewVersionBar');
|
||||
const UpdateCheckBar = sdk.getComponent('globals.UpdateCheckBar');
|
||||
const PasswordNagBar = sdk.getComponent('globals.PasswordNagBar');
|
||||
const ServerLimitBar = sdk.getComponent('globals.ServerLimitBar');
|
||||
|
||||
let pageElement;
|
||||
|
||||
|
@ -647,46 +650,7 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
|
|||
break;
|
||||
}
|
||||
|
||||
const usageLimitEvent = this.state.serverNoticeEvents.find((e) => {
|
||||
return (
|
||||
e && e.getType() === 'm.room.message' &&
|
||||
e.getContent()['server_notice_type'] === 'm.server_notice.usage_limit_reached'
|
||||
);
|
||||
});
|
||||
|
||||
let topBar;
|
||||
if (this.state.syncErrorData && this.state.syncErrorData.error.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') {
|
||||
topBar = <ServerLimitBar kind='hard'
|
||||
adminContact={this.state.syncErrorData.error.data.admin_contact}
|
||||
limitType={this.state.syncErrorData.error.data.limit_type}
|
||||
/>;
|
||||
} else if (usageLimitEvent) {
|
||||
topBar = <ServerLimitBar kind='soft'
|
||||
adminContact={usageLimitEvent.getContent().admin_contact}
|
||||
limitType={usageLimitEvent.getContent().limit_type}
|
||||
/>;
|
||||
} else if (this.props.showCookieBar &&
|
||||
this.props.config.piwik &&
|
||||
navigator.doNotTrack !== "1"
|
||||
) {
|
||||
const policyUrl = this.props.config.piwik.policyUrl || null;
|
||||
topBar = <CookieBar policyUrl={policyUrl} />;
|
||||
} else if (this.props.hasNewVersion) {
|
||||
topBar = <NewVersionBar version={this.props.version} newVersion={this.props.newVersion}
|
||||
releaseNotes={this.props.newVersionReleaseNotes}
|
||||
/>;
|
||||
} else if (this.props.checkingForUpdate) {
|
||||
topBar = <UpdateCheckBar {...this.props.checkingForUpdate} />;
|
||||
} else if (this.state.userHasGeneratedPassword) {
|
||||
topBar = <PasswordNagBar />;
|
||||
} else if (this.props.showNotifierToolbar) {
|
||||
topBar = <MatrixToolbar />;
|
||||
}
|
||||
|
||||
let bodyClasses = 'mx_MatrixChat';
|
||||
if (topBar) {
|
||||
bodyClasses += ' mx_MatrixChat_toolbarShowing';
|
||||
}
|
||||
if (this.state.useCompactLayout) {
|
||||
bodyClasses += ' mx_MatrixChat_useCompactLayout';
|
||||
}
|
||||
|
@ -701,7 +665,6 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
|
|||
onMouseDown={this._onMouseDown}
|
||||
onMouseUp={this._onMouseUp}
|
||||
>
|
||||
{ topBar }
|
||||
<ToastContainer />
|
||||
<DragDropContext onDragEnd={this._onDragEnd}>
|
||||
<div ref={this._resizeContainer} className={bodyClasses}>
|
||||
|
|
|
@ -17,12 +17,11 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {createRef} from 'react';
|
||||
import {InvalidStoreError} from "matrix-js-sdk/src/errors";
|
||||
import {RoomMember} from "matrix-js-sdk/src/models/room-member";
|
||||
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
|
||||
import React, { createRef } from 'react';
|
||||
import { InvalidStoreError } from "matrix-js-sdk/src/errors";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { isCryptoAvailable } from 'matrix-js-sdk/src/crypto';
|
||||
|
||||
// focus-visible is a Polyfill for the :focus-visible CSS pseudo-attribute used by _AccessibleButton.scss
|
||||
import 'focus-visible';
|
||||
// what-input helps improve keyboard accessibility
|
||||
|
@ -30,17 +29,17 @@ import 'what-input';
|
|||
|
||||
import Analytics from "../../Analytics";
|
||||
import { DecryptionFailureTracker } from "../../DecryptionFailureTracker";
|
||||
import {MatrixClientPeg} from "../../MatrixClientPeg";
|
||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||
import PlatformPeg from "../../PlatformPeg";
|
||||
import SdkConfig from "../../SdkConfig";
|
||||
import * as RoomListSorter from "../../RoomListSorter";
|
||||
import dis from "../../dispatcher";
|
||||
import dis from "../../dispatcher/dispatcher";
|
||||
import Notifier from '../../Notifier';
|
||||
|
||||
import Modal from "../../Modal";
|
||||
import Tinter from "../../Tinter";
|
||||
import * as sdk from '../../index';
|
||||
import { showStartChatInviteDialog, showRoomInviteDialog } from '../../RoomInvite';
|
||||
import { showRoomInviteDialog, showStartChatInviteDialog } from '../../RoomInvite';
|
||||
import * as Rooms from '../../Rooms';
|
||||
import linkifyMatrix from "../../linkify-matrix";
|
||||
import * as Lifecycle from '../../Lifecycle';
|
||||
|
@ -50,23 +49,29 @@ import PageTypes from '../../PageTypes';
|
|||
import { getHomePageUrl } from '../../utils/pages';
|
||||
|
||||
import createRoom from "../../createRoom";
|
||||
import KeyRequestHandler from '../../KeyRequestHandler';
|
||||
import { _t, getCurrentLanguage } from '../../languageHandler';
|
||||
import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
|
||||
import SettingsStore, { SettingLevel } from "../../settings/SettingsStore";
|
||||
import ThemeController from "../../settings/controllers/ThemeController";
|
||||
import { startAnyRegistrationFlow } from "../../Registration.js";
|
||||
import { messageForSyncError } from '../../utils/ErrorUtils';
|
||||
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||
import { ValidatedServerConfig } from "../../utils/AutoDiscoveryUtils";
|
||||
import AutoDiscoveryUtils from "../../utils/AutoDiscoveryUtils";
|
||||
import AutoDiscoveryUtils, { ValidatedServerConfig } from "../../utils/AutoDiscoveryUtils";
|
||||
import DMRoomMap from '../../utils/DMRoomMap';
|
||||
import { countRoomsWithNotif } from '../../RoomNotifs';
|
||||
import { ThemeWatcher } from "../../theme";
|
||||
import ThemeWatcher from "../../settings/watchers/ThemeWatcher";
|
||||
import { FontWatcher } from '../../settings/watchers/FontWatcher';
|
||||
import { storeRoomAliasInCache } from '../../RoomAliasCache';
|
||||
import {defer, IDeferred} from "../../utils/promise";
|
||||
import { defer, IDeferred } from "../../utils/promise";
|
||||
import ToastStore from "../../stores/ToastStore";
|
||||
import * as StorageManager from "../../utils/StorageManager";
|
||||
import type LoggedInViewType from "./LoggedInView";
|
||||
import { ViewUserPayload } from "../../dispatcher/payloads/ViewUserPayload";
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
import {
|
||||
showToast as showAnalyticsToast,
|
||||
hideToast as hideAnalyticsToast
|
||||
} from "../../toasts/AnalyticsToast";
|
||||
import {showToast as showNotificationsToast} from "../../toasts/DesktopNotificationsToast";
|
||||
|
||||
/** constants for MatrixChat.state.view */
|
||||
export enum Views {
|
||||
|
@ -107,7 +112,7 @@ export enum Views {
|
|||
// re-dispatched. NOTE: some actions are non-trivial and would require
|
||||
// re-factoring to be included in this list in future.
|
||||
const ONBOARDING_FLOW_STARTERS = [
|
||||
'view_user_settings',
|
||||
Action.ViewUserSettings,
|
||||
'view_create_chat',
|
||||
'view_create_room',
|
||||
'view_create_group',
|
||||
|
@ -168,12 +173,6 @@ interface IState {
|
|||
leftDisabled: boolean;
|
||||
middleDisabled: boolean;
|
||||
// the right panel's disabled state is tracked in its store.
|
||||
version?: string;
|
||||
newVersion?: string;
|
||||
hasNewVersion: boolean;
|
||||
newVersionReleaseNotes?: string;
|
||||
checkingForUpdate?: string; // updateCheckStatusEnum
|
||||
showCookieBar: boolean;
|
||||
// Parameters used in the registration dance with the IS
|
||||
register_client_secret?: string;
|
||||
register_session_id?: string;
|
||||
|
@ -183,7 +182,6 @@ interface IState {
|
|||
hideToSRUsers: boolean;
|
||||
syncError?: Error;
|
||||
resizeNotifier: ResizeNotifier;
|
||||
showNotifierToolbar: boolean;
|
||||
serverConfig?: ValidatedServerConfig;
|
||||
ready: boolean;
|
||||
thirdPartyInvite?: object;
|
||||
|
@ -216,6 +214,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
private readonly loggedInView: React.RefObject<LoggedInViewType>;
|
||||
private readonly dispatcherRef: any;
|
||||
private readonly themeWatcher: ThemeWatcher;
|
||||
private readonly fontWatcher: FontWatcher;
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
@ -226,17 +225,10 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
leftDisabled: false,
|
||||
middleDisabled: false,
|
||||
|
||||
hasNewVersion: false,
|
||||
newVersionReleaseNotes: null,
|
||||
checkingForUpdate: null,
|
||||
|
||||
showCookieBar: false,
|
||||
|
||||
hideToSRUsers: false,
|
||||
|
||||
syncError: null, // If the current syncing status is ERROR, the error object, otherwise null.
|
||||
resizeNotifier: new ResizeNotifier(),
|
||||
showNotifierToolbar: false,
|
||||
ready: false,
|
||||
};
|
||||
|
||||
|
@ -283,8 +275,11 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
this.accountPasswordTimer = null;
|
||||
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
|
||||
this.themeWatcher = new ThemeWatcher();
|
||||
this.fontWatcher = new FontWatcher();
|
||||
this.themeWatcher.start();
|
||||
this.fontWatcher.start();
|
||||
|
||||
this.focusComposer = false;
|
||||
|
||||
|
@ -334,12 +329,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
});
|
||||
}
|
||||
|
||||
if (SettingsStore.getValue("showCookieBar")) {
|
||||
this.setState({
|
||||
showCookieBar: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (SettingsStore.getValue("analyticsOptIn")) {
|
||||
Analytics.enable();
|
||||
}
|
||||
|
@ -367,6 +356,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
Lifecycle.stopMatrixClient();
|
||||
dis.unregister(this.dispatcherRef);
|
||||
this.themeWatcher.stop();
|
||||
this.fontWatcher.stop();
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
this.state.resizeNotifier.removeListener("middlePanelResized", this.dispatchTimelineResize);
|
||||
|
||||
|
@ -613,7 +603,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
case 'view_indexed_room':
|
||||
this.viewIndexedRoom(payload.roomIndex);
|
||||
break;
|
||||
case 'view_user_settings': {
|
||||
case Action.ViewUserSettings: {
|
||||
const UserSettingsDialog = sdk.getComponent("dialogs.UserSettingsDialog");
|
||||
Modal.createTrackedDialog('User settings', '', UserSettingsDialog, {},
|
||||
/*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
|
||||
|
@ -680,9 +670,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
dis.dispatch({action: 'view_my_groups'});
|
||||
}
|
||||
break;
|
||||
case 'notifier_enabled':
|
||||
this.setState({showNotifierToolbar: Notifier.shouldShowToolbar()});
|
||||
break;
|
||||
case 'hide_left_panel':
|
||||
this.setState({
|
||||
collapseLhs: true,
|
||||
|
@ -730,15 +717,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
case 'client_started':
|
||||
this.onClientStarted();
|
||||
break;
|
||||
case 'new_version':
|
||||
this.onVersion(
|
||||
payload.currentVersion, payload.newVersion,
|
||||
payload.releaseNotes,
|
||||
);
|
||||
break;
|
||||
case 'check_updates':
|
||||
this.setState({ checkingForUpdate: payload.value });
|
||||
break;
|
||||
case 'send_event':
|
||||
this.onSendEvent(payload.room_id, payload.event);
|
||||
break;
|
||||
|
@ -755,19 +733,13 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
case 'accept_cookies':
|
||||
SettingsStore.setValue("analyticsOptIn", null, SettingLevel.DEVICE, true);
|
||||
SettingsStore.setValue("showCookieBar", null, SettingLevel.DEVICE, false);
|
||||
|
||||
this.setState({
|
||||
showCookieBar: false,
|
||||
});
|
||||
hideAnalyticsToast();
|
||||
Analytics.enable();
|
||||
break;
|
||||
case 'reject_cookies':
|
||||
SettingsStore.setValue("analyticsOptIn", null, SettingLevel.DEVICE, false);
|
||||
SettingsStore.setValue("showCookieBar", null, SettingLevel.DEVICE, false);
|
||||
|
||||
this.setState({
|
||||
showCookieBar: false,
|
||||
});
|
||||
hideAnalyticsToast();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
@ -926,9 +898,20 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
});
|
||||
}
|
||||
|
||||
private viewGroup(payload) {
|
||||
private async viewGroup(payload) {
|
||||
const groupId = payload.group_id;
|
||||
|
||||
// Wait for the first sync to complete
|
||||
if (!this.firstSyncComplete) {
|
||||
if (!this.firstSyncPromise) {
|
||||
console.warn('Cannot view a group before first sync. group_id:', groupId);
|
||||
return;
|
||||
}
|
||||
await this.firstSyncPromise.promise;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
view: Views.LOGGED_IN,
|
||||
currentGroupId: groupId,
|
||||
currentGroupIsNew: payload.group_is_new,
|
||||
});
|
||||
|
@ -1245,6 +1228,10 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
|
||||
StorageManager.tryPersistStorage();
|
||||
|
||||
if (SettingsStore.getValue("showCookieBar") && this.props.config.piwik && navigator.doNotTrack !== "1") {
|
||||
showAnalyticsToast(this.props.config.piwik && this.props.config.piwik.policyUrl);
|
||||
}
|
||||
}
|
||||
|
||||
private showScreenAfterLogin() {
|
||||
|
@ -1372,10 +1359,13 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
this.firstSyncComplete = true;
|
||||
this.firstSyncPromise.resolve();
|
||||
|
||||
if (Notifier.shouldShowToolbar()) {
|
||||
showNotificationsToast();
|
||||
}
|
||||
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
this.setState({
|
||||
ready: true,
|
||||
showNotifierToolbar: Notifier.shouldShowToolbar(),
|
||||
});
|
||||
});
|
||||
cli.on('Call.incoming', function(call) {
|
||||
|
@ -1454,16 +1444,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
cli.on("Session.logged_out", () => dft.stop());
|
||||
cli.on("Event.decrypted", (e, err) => dft.eventDecrypted(e, err));
|
||||
|
||||
// TODO: We can remove this once cross-signing is the only way.
|
||||
// https://github.com/vector-im/riot-web/issues/11908
|
||||
const krh = new KeyRequestHandler(cli);
|
||||
cli.on("crypto.roomKeyRequest", (req) => {
|
||||
krh.handleKeyRequest(req);
|
||||
});
|
||||
cli.on("crypto.roomKeyRequestCancellation", (req) => {
|
||||
krh.handleKeyRequestCancellation(req);
|
||||
});
|
||||
|
||||
cli.on("Room", (room) => {
|
||||
if (MatrixClientPeg.get().isCryptoEnabled()) {
|
||||
const blacklistEnabled = SettingsStore.getValueAt(
|
||||
|
@ -1553,7 +1533,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
icon: "verification",
|
||||
props: {request},
|
||||
component: sdk.getComponent("toasts.VerificationRequestToast"),
|
||||
priority: ToastStore.PRIORITY_REALTIME,
|
||||
priority: 90,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -1621,9 +1601,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
action: 'view_create_room',
|
||||
});
|
||||
} else if (screen === 'settings') {
|
||||
dis.dispatch({
|
||||
action: 'view_user_settings',
|
||||
});
|
||||
dis.fire(Action.ViewUserSettings);
|
||||
} else if (screen === 'welcome') {
|
||||
dis.dispatch({
|
||||
action: 'view_welcome_page',
|
||||
|
@ -1755,8 +1733,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
|
||||
const member = new RoomMember(null, userId);
|
||||
if (!member) { return; }
|
||||
dis.dispatch({
|
||||
action: 'view_user',
|
||||
dis.dispatch<ViewUserPayload>({
|
||||
action: Action.ViewUser,
|
||||
member: member,
|
||||
});
|
||||
}
|
||||
|
@ -1829,16 +1807,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
this.showScreen("settings");
|
||||
};
|
||||
|
||||
onVersion(current: string, latest: string, releaseNotes?: string) {
|
||||
this.setState({
|
||||
version: current,
|
||||
newVersion: latest,
|
||||
hasNewVersion: current !== latest,
|
||||
newVersionReleaseNotes: releaseNotes,
|
||||
checkingForUpdate: null,
|
||||
});
|
||||
}
|
||||
|
||||
onSendEvent(roomId: string, event: MatrixEvent) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (!cli) {
|
||||
|
@ -2033,7 +2001,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
onCloseAllSettings={this.onCloseAllSettings}
|
||||
onRegistered={this.onRegistered}
|
||||
currentRoomId={this.state.currentRoomId}
|
||||
showCookieBar={this.state.showCookieBar}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
|
|
|
@ -29,6 +29,7 @@ import SettingsStore from '../../settings/SettingsStore';
|
|||
import {_t} from "../../languageHandler";
|
||||
import {haveTileForEvent} from "../views/rooms/EventTile";
|
||||
import {textForEvent} from "../../TextForEvent";
|
||||
import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResizer";
|
||||
|
||||
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
|
||||
const continuedTypes = ['m.sticker', 'm.room.message'];
|
||||
|
@ -107,10 +108,14 @@ export default class MessagePanel extends React.Component {
|
|||
|
||||
// whether to show reactions for an event
|
||||
showReactions: PropTypes.bool,
|
||||
|
||||
// whether to use the irc layout
|
||||
useIRCLayout: PropTypes.bool,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
// Force props to be loaded for useIRCLayout
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
// previous positions the read marker has been in, so we can
|
||||
|
@ -597,6 +602,7 @@ export default class MessagePanel extends React.Component {
|
|||
isSelectedEvent={highlight}
|
||||
getRelationsForEvent={this.props.getRelationsForEvent}
|
||||
showReactions={this.props.showReactions}
|
||||
useIRCLayout={this.props.useIRCLayout}
|
||||
/>
|
||||
</TileErrorBoundary>
|
||||
</li>,
|
||||
|
@ -792,6 +798,15 @@ export default class MessagePanel extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
let ircResizer = null;
|
||||
if (this.props.useIRCLayout) {
|
||||
ircResizer = <IRCTimelineProfileResizer
|
||||
minWidth={20}
|
||||
maxWidth={600}
|
||||
roomId={this.props.room ? this.props.roomroomId : null}
|
||||
/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<ScrollPanel
|
||||
|
@ -804,6 +819,7 @@ export default class MessagePanel extends React.Component {
|
|||
style={style}
|
||||
stickyBottom={this.props.stickyBottom}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
fixedChildren={ircResizer}
|
||||
>
|
||||
{ topSpinner }
|
||||
{ this._getEventTiles() }
|
||||
|
|
|
@ -19,7 +19,7 @@ import React from 'react';
|
|||
import createReactClass from 'create-react-class';
|
||||
import * as sdk from '../../index';
|
||||
import { _t } from '../../languageHandler';
|
||||
import dis from '../../dispatcher';
|
||||
import dis from '../../dispatcher/dispatcher';
|
||||
import AccessibleButton from '../views/elements/AccessibleButton';
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
import AutoHideScrollbar from "./AutoHideScrollbar";
|
||||
|
|
|
@ -22,7 +22,7 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import * as sdk from '../../index';
|
||||
import dis from '../../dispatcher';
|
||||
import dis from '../../dispatcher/dispatcher';
|
||||
import RateLimitedFunc from '../../ratelimitedfunc';
|
||||
import { showGroupInviteDialog, showGroupAddRoomDialog } from '../../GroupAddressPicker';
|
||||
import GroupStore from '../../stores/GroupStore';
|
||||
|
@ -30,6 +30,7 @@ import SettingsStore from "../../settings/SettingsStore";
|
|||
import {RIGHT_PANEL_PHASES, RIGHT_PANEL_PHASES_NO_ARGS} from "../../stores/RightPanelStorePhases";
|
||||
import RightPanelStore from "../../stores/RightPanelStore";
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
import {Action} from "../../dispatcher/actions";
|
||||
|
||||
export default class RightPanel extends React.Component {
|
||||
static get propTypes() {
|
||||
|
@ -237,7 +238,7 @@ export default class RightPanel extends React.Component {
|
|||
// within a room, so go back to the member panel if we were in the encryption panel,
|
||||
// or the member list if we were in the member panel... phew.
|
||||
dis.dispatch({
|
||||
action: "view_user",
|
||||
action: Action.ViewUser,
|
||||
member: this.state.phase === RIGHT_PANEL_PHASES.EncryptionPanel ?
|
||||
this.state.member : null,
|
||||
});
|
||||
|
@ -266,7 +267,7 @@ export default class RightPanel extends React.Component {
|
|||
if (SettingsStore.getValue("feature_cross_signing")) {
|
||||
const onClose = () => {
|
||||
dis.dispatch({
|
||||
action: "view_user",
|
||||
action: Action.ViewUser,
|
||||
member: null,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -20,7 +20,7 @@ import React from 'react';
|
|||
import createReactClass from 'create-react-class';
|
||||
import {MatrixClientPeg} from "../../MatrixClientPeg";
|
||||
import * as sdk from "../../index";
|
||||
import dis from "../../dispatcher";
|
||||
import dis from "../../dispatcher/dispatcher";
|
||||
import Modal from "../../Modal";
|
||||
import { linkifyAndSanitizeHtml } from '../../HtmlUtils';
|
||||
import PropTypes from 'prop-types';
|
||||
|
@ -199,7 +199,7 @@ export default createReactClass({
|
|||
|
||||
let desc;
|
||||
if (alias) {
|
||||
desc = _t('Delete the room alias %(alias)s and remove %(name)s from the directory?', {alias: alias, name: name});
|
||||
desc = _t('Delete the room address %(alias)s and remove %(name)s from the directory?', {alias, name});
|
||||
} else {
|
||||
desc = _t('Remove %(name)s from the directory?', {name: name});
|
||||
}
|
||||
|
@ -216,7 +216,7 @@ export default createReactClass({
|
|||
|
||||
MatrixClientPeg.get().setRoomDirectoryVisibility(room.room_id, 'private').then(() => {
|
||||
if (!alias) return;
|
||||
step = _t('delete the alias.');
|
||||
step = _t('delete the address.');
|
||||
return MatrixClientPeg.get().deleteAlias(alias);
|
||||
}).then(() => {
|
||||
modal.close();
|
||||
|
|
|
@ -25,7 +25,7 @@ import * as sdk from '../../index';
|
|||
import {MatrixClientPeg} from '../../MatrixClientPeg';
|
||||
import Resend from '../../Resend';
|
||||
import * as cryptodevices from '../../cryptodevices';
|
||||
import dis from '../../dispatcher';
|
||||
import dis from '../../dispatcher/dispatcher';
|
||||
import {messageForResourceLimitError, messageForSendError} from '../../utils/ErrorUtils';
|
||||
|
||||
const STATUS_BAR_HIDDEN = 0;
|
||||
|
|
|
@ -20,7 +20,7 @@ limitations under the License.
|
|||
import React, {createRef} from 'react';
|
||||
import classNames from 'classnames';
|
||||
import * as sdk from '../../index';
|
||||
import dis from '../../dispatcher';
|
||||
import dis from '../../dispatcher/dispatcher';
|
||||
import * as Unread from '../../Unread';
|
||||
import * as RoomNotifs from '../../RoomNotifs';
|
||||
import * as FormattingUtils from '../../utils/FormattingUtils';
|
||||
|
@ -32,7 +32,7 @@ import RoomTile from "../views/rooms/RoomTile";
|
|||
import LazyRenderList from "../views/elements/LazyRenderList";
|
||||
import {_t} from "../../languageHandler";
|
||||
import {RovingTabIndexWrapper} from "../../accessibility/RovingTabIndex";
|
||||
import toRem from "../../utils/rem";
|
||||
import {toPx} from "../../utils/units";
|
||||
|
||||
// turn this on for drop & drag console debugging galore
|
||||
const debug = false;
|
||||
|
@ -420,7 +420,7 @@ export default class RoomSubList extends React.PureComponent {
|
|||
|
||||
setHeight = (height) => {
|
||||
if (this._subList.current) {
|
||||
this._subList.current.style.height = toRem(height);
|
||||
this._subList.current.style.height = toPx(height);
|
||||
}
|
||||
this._updateLazyRenderHeight(height);
|
||||
};
|
||||
|
|
|
@ -34,7 +34,7 @@ import ContentMessages from '../../ContentMessages';
|
|||
import Modal from '../../Modal';
|
||||
import * as sdk from '../../index';
|
||||
import CallHandler from '../../CallHandler';
|
||||
import dis from '../../dispatcher';
|
||||
import dis from '../../dispatcher/dispatcher';
|
||||
import Tinter from '../../Tinter';
|
||||
import rate_limited_func from '../../ratelimitedfunc';
|
||||
import * as ObjectUtils from '../../ObjectUtils';
|
||||
|
@ -164,6 +164,10 @@ export default createReactClass({
|
|||
|
||||
canReact: false,
|
||||
canReply: false,
|
||||
|
||||
useIRCLayout: SettingsStore.getValue("feature_irc_ui"),
|
||||
|
||||
matrixClientIsReady: this.context && this.context.isInitialSyncComplete(),
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -193,6 +197,8 @@ export default createReactClass({
|
|||
|
||||
this._roomView = createRef();
|
||||
this._searchResultsPanel = createRef();
|
||||
|
||||
this._layoutWatcherRef = SettingsStore.watchSetting("feature_irc_ui", null, this.onLayoutChange);
|
||||
},
|
||||
|
||||
_onReadReceiptsChange: function() {
|
||||
|
@ -232,7 +238,8 @@ export default createReactClass({
|
|||
initialEventId: RoomViewStore.getInitialEventId(),
|
||||
isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(),
|
||||
forwardingEvent: RoomViewStore.getForwardingEvent(),
|
||||
shouldPeek: RoomViewStore.shouldPeek(),
|
||||
// we should only peek once we have a ready client
|
||||
shouldPeek: this.state.matrixClientIsReady && RoomViewStore.shouldPeek(),
|
||||
showingPinned: SettingsStore.getValue("PinnedEvents.isOpen", roomId),
|
||||
showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId),
|
||||
};
|
||||
|
@ -532,6 +539,14 @@ export default createReactClass({
|
|||
// no need to do this as Dir & Settings are now overlays. It just burnt CPU.
|
||||
// console.log("Tinter.tint from RoomView.unmount");
|
||||
// Tinter.tint(); // reset colourscheme
|
||||
|
||||
SettingsStore.unwatchSetting(this._layoutWatcherRef);
|
||||
},
|
||||
|
||||
onLayoutChange: function() {
|
||||
this.setState({
|
||||
useIRCLayout: SettingsStore.getValue("feature_irc_ui"),
|
||||
});
|
||||
},
|
||||
|
||||
_onRightPanelStoreUpdate: function() {
|
||||
|
@ -681,6 +696,16 @@ export default createReactClass({
|
|||
});
|
||||
}
|
||||
break;
|
||||
case 'sync_state':
|
||||
if (!this.state.matrixClientIsReady) {
|
||||
this.setState({
|
||||
matrixClientIsReady: this.context && this.context.isInitialSyncComplete(),
|
||||
}, () => {
|
||||
// send another "initial" RVS update to trigger peeking if needed
|
||||
this._onRoomViewStoreUpdate(true);
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -1663,14 +1688,16 @@ export default createReactClass({
|
|||
const ErrorBoundary = sdk.getComponent("elements.ErrorBoundary");
|
||||
|
||||
if (!this.state.room) {
|
||||
const loading = this.state.roomLoading || this.state.peekLoading;
|
||||
const loading = !this.state.matrixClientIsReady || this.state.roomLoading || this.state.peekLoading;
|
||||
if (loading) {
|
||||
// Assume preview loading if we don't have a ready client or a room ID (still resolving the alias)
|
||||
const previewLoading = !this.state.matrixClientIsReady || !this.state.roomId || this.state.peekLoading;
|
||||
return (
|
||||
<div className="mx_RoomView">
|
||||
<ErrorBoundary>
|
||||
<RoomPreviewBar
|
||||
canPreview={false}
|
||||
previewLoading={this.state.peekLoading}
|
||||
previewLoading={previewLoading && !this.state.roomLoadError}
|
||||
error={this.state.roomLoadError}
|
||||
loading={loading}
|
||||
joining={this.state.joining}
|
||||
|
@ -1695,7 +1722,8 @@ export default createReactClass({
|
|||
return (
|
||||
<div className="mx_RoomView">
|
||||
<ErrorBoundary>
|
||||
<RoomPreviewBar onJoinClick={this.onJoinButtonClicked}
|
||||
<RoomPreviewBar
|
||||
onJoinClick={this.onJoinButtonClicked}
|
||||
onForgetClick={this.onForgetClick}
|
||||
onRejectClick={this.onRejectThreepidInviteButtonClicked}
|
||||
canPreview={false} error={this.state.roomLoadError}
|
||||
|
@ -1980,6 +2008,13 @@ export default createReactClass({
|
|||
highlightedEventId = this.state.initialEventId;
|
||||
}
|
||||
|
||||
const messagePanelClassNames = classNames(
|
||||
"mx_RoomView_messagePanel",
|
||||
{
|
||||
"mx_IRCLayout": this.state.useIRCLayout,
|
||||
"mx_GroupLayout": !this.state.useIRCLayout,
|
||||
});
|
||||
|
||||
// console.info("ShowUrlPreview for %s is %s", this.state.room.roomId, this.state.showUrlPreview);
|
||||
const messagePanel = (
|
||||
<TimelinePanel
|
||||
|
@ -1995,11 +2030,12 @@ export default createReactClass({
|
|||
onScroll={this.onMessageListScroll}
|
||||
onReadMarkerUpdated={this._updateTopUnreadMessagesBar}
|
||||
showUrlPreview = {this.state.showUrlPreview}
|
||||
className="mx_RoomView_messagePanel"
|
||||
className={messagePanelClassNames}
|
||||
membersLoaded={this.state.membersLoaded}
|
||||
permalinkCreator={this._getPermalinkCreatorForRoom(this.state.room)}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
showReactions={true}
|
||||
useIRCLayout={this.state.useIRCLayout}
|
||||
/>);
|
||||
|
||||
let topUnreadMessagesBar = null;
|
||||
|
|
|
@ -144,6 +144,11 @@ export default createReactClass({
|
|||
/* resizeNotifier: ResizeNotifier to know when middle column has changed size
|
||||
*/
|
||||
resizeNotifier: PropTypes.object,
|
||||
|
||||
/* fixedChildren: allows for children to be passed which are rendered outside
|
||||
* of the wrapper
|
||||
*/
|
||||
fixedChildren: PropTypes.node,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -881,6 +886,7 @@ export default createReactClass({
|
|||
return (<AutoHideScrollbar wrappedRef={this._collectScroll}
|
||||
onScroll={this.onScroll}
|
||||
className={`mx_ScrollPanel ${this.props.className}`} style={this.props.style}>
|
||||
{ this.props.fixedChildren }
|
||||
<div className="mx_RoomView_messageListWrapper">
|
||||
<ol ref={this._itemlist} className="mx_RoomView_MessageList" aria-live="polite" role="list">
|
||||
{ this.props.children }
|
||||
|
|
|
@ -19,7 +19,7 @@ import React, {createRef} from 'react';
|
|||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Key } from '../../Keyboard';
|
||||
import dis from '../../dispatcher';
|
||||
import dis from '../../dispatcher/dispatcher';
|
||||
import { throttle } from 'lodash';
|
||||
import AccessibleButton from '../../components/views/elements/AccessibleButton';
|
||||
import classNames from 'classnames';
|
||||
|
|
|
@ -22,7 +22,7 @@ import TagOrderStore from '../../stores/TagOrderStore';
|
|||
import GroupActions from '../../actions/GroupActions';
|
||||
|
||||
import * as sdk from '../../index';
|
||||
import dis from '../../dispatcher';
|
||||
import dis from '../../dispatcher/dispatcher';
|
||||
import { _t } from '../../languageHandler';
|
||||
|
||||
import { Droppable } from 'react-beautiful-dnd';
|
||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import * as sdk from '../../index';
|
||||
import dis from '../../dispatcher';
|
||||
import dis from '../../dispatcher/dispatcher';
|
||||
import Modal from '../../Modal';
|
||||
import { _t } from '../../languageHandler';
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ import {MatrixClientPeg} from "../../MatrixClientPeg";
|
|||
import * as ObjectUtils from "../../ObjectUtils";
|
||||
import UserActivity from "../../UserActivity";
|
||||
import Modal from "../../Modal";
|
||||
import dis from "../../dispatcher";
|
||||
import dis from "../../dispatcher/dispatcher";
|
||||
import * as sdk from "../../index";
|
||||
import { Key } from '../../Keyboard';
|
||||
import Timer from '../../utils/Timer';
|
||||
|
@ -112,6 +112,9 @@ const TimelinePanel = createReactClass({
|
|||
|
||||
// whether to show reactions for an event
|
||||
showReactions: PropTypes.bool,
|
||||
|
||||
// whether to use the irc layout
|
||||
useIRCLayout: PropTypes.bool,
|
||||
},
|
||||
|
||||
statics: {
|
||||
|
@ -1447,6 +1450,7 @@ const TimelinePanel = createReactClass({
|
|||
getRelationsForEvent={this.getRelationsForEvent}
|
||||
editState={this.state.editState}
|
||||
showReactions={this.props.showReactions}
|
||||
useIRCLayout={this.props.useIRCLayout}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -15,14 +15,21 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import * as React from "react";
|
||||
import { _t } from '../../languageHandler';
|
||||
import ToastStore from "../../stores/ToastStore";
|
||||
import ToastStore, {IToast} from "../../stores/ToastStore";
|
||||
import classNames from "classnames";
|
||||
|
||||
export default class ToastContainer extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {toasts: ToastStore.sharedInstance().getToasts()};
|
||||
interface IState {
|
||||
toasts: IToast<any>[];
|
||||
countSeen: number;
|
||||
}
|
||||
|
||||
export default class ToastContainer extends React.Component<{}, IState> {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.state = {
|
||||
toasts: ToastStore.sharedInstance().getToasts(),
|
||||
countSeen: ToastStore.sharedInstance().getCountSeen(),
|
||||
};
|
||||
|
||||
// Start listening here rather than in componentDidMount because
|
||||
// toasts may dismiss themselves in their didMount if they find
|
||||
|
@ -36,7 +43,10 @@ export default class ToastContainer extends React.Component {
|
|||
}
|
||||
|
||||
_onToastStoreUpdate = () => {
|
||||
this.setState({toasts: ToastStore.sharedInstance().getToasts()});
|
||||
this.setState({
|
||||
toasts: ToastStore.sharedInstance().getToasts(),
|
||||
countSeen: ToastStore.sharedInstance().getCountSeen(),
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
|
@ -50,14 +60,21 @@ export default class ToastContainer extends React.Component {
|
|||
"mx_Toast_hasIcon": icon,
|
||||
[`mx_Toast_icon_${icon}`]: icon,
|
||||
});
|
||||
const countIndicator = isStacked ? _t(" (1/%(totalCount)s)", {totalCount}) : null;
|
||||
|
||||
let countIndicator;
|
||||
if (isStacked || this.state.countSeen > 0) {
|
||||
countIndicator = ` (${this.state.countSeen + 1}/${this.state.countSeen + totalCount})`;
|
||||
}
|
||||
|
||||
const toastProps = Object.assign({}, props, {
|
||||
key,
|
||||
toastKey: key,
|
||||
});
|
||||
toast = (<div className={toastClasses}>
|
||||
<h2>{title}{countIndicator}</h2>
|
||||
<div className="mx_Toast_title">
|
||||
<h2>{title}</h2>
|
||||
<span>{countIndicator}</span>
|
||||
</div>
|
||||
<div className="mx_Toast_body">{React.createElement(component, toastProps)}</div>
|
||||
</div>);
|
||||
}
|
|
@ -22,7 +22,7 @@ import BaseAvatar from '../views/avatars/BaseAvatar';
|
|||
import {MatrixClientPeg} from '../../MatrixClientPeg';
|
||||
import * as Avatar from '../../Avatar';
|
||||
import { _t } from '../../languageHandler';
|
||||
import dis from "../../dispatcher";
|
||||
import dis from "../../dispatcher/dispatcher";
|
||||
import {ContextMenu, ContextMenuButton} from "./ContextMenu";
|
||||
|
||||
const AVATAR_SIZE = 28;
|
||||
|
|
|
@ -19,7 +19,7 @@ import React from 'react';
|
|||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import ContentMessages from '../../ContentMessages';
|
||||
import dis from "../../dispatcher";
|
||||
import dis from "../../dispatcher/dispatcher";
|
||||
import filesize from "filesize";
|
||||
import { _t } from '../../languageHandler';
|
||||
|
||||
|
|
|
@ -32,7 +32,7 @@ import * as Lifecycle from '../../../Lifecycle';
|
|||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import AuthPage from "../../views/auth/AuthPage";
|
||||
import Login from "../../../Login";
|
||||
import dis from "../../../dispatcher";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
|
||||
// Phases
|
||||
// Show controls to configure server details
|
||||
|
|
|
@ -18,7 +18,7 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import {_t} from '../../../languageHandler';
|
||||
import * as sdk from '../../../index';
|
||||
import dis from '../../../dispatcher';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import * as Lifecycle from '../../../Lifecycle';
|
||||
import Modal from '../../../Modal';
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
|
|
|
@ -355,6 +355,7 @@ export const TermsAuthEntry = createReactClass({
|
|||
allChecked = allChecked && checked;
|
||||
|
||||
checkboxes.push(
|
||||
// XXX: replace with StyledCheckbox
|
||||
<label key={"policy_checkbox_" + policy.id} className="mx_InteractiveAuthEntryComponents_termsPolicy">
|
||||
<input type="checkbox" onChange={() => this._togglePolicy(policy.id)} checked={checked} />
|
||||
<a href={policy.url} target="_blank" rel="noreferrer noopener">{ policy.name }</a>
|
||||
|
@ -538,6 +539,7 @@ export const MsisdnAuthEntry = createReactClass({
|
|||
type: MsisdnAuthEntry.LOGIN_TYPE,
|
||||
// TODO: Remove `threepid_creds` once servers support proper UIA
|
||||
// See https://github.com/vector-im/riot-web/issues/10312
|
||||
// See https://github.com/matrix-org/matrix-doc/issues/2220
|
||||
threepid_creds: creds,
|
||||
threepidCreds: creds,
|
||||
});
|
||||
|
|
|
@ -23,7 +23,8 @@ import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils";
|
|||
import * as ServerType from '../../views/auth/ServerTypeSelector';
|
||||
import ServerConfig from "./ServerConfig";
|
||||
|
||||
const MODULAR_URL = 'https://modular.im/?utm_source=riot-web&utm_medium=web&utm_campaign=riot-web-authentication';
|
||||
const MODULAR_URL = 'https://modular.im/services/matrix-hosting-riot' +
|
||||
'?utm_source=riot-web&utm_medium=web&utm_campaign=riot-web-authentication';
|
||||
|
||||
// TODO: TravisR - Can this extend ServerConfig for most things?
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@ interface IProps {
|
|||
labelStrongPassword?: string;
|
||||
labelAllowedButUnsafe?: string;
|
||||
|
||||
onChange(ev: KeyboardEvent);
|
||||
onChange(ev: React.FormEvent<HTMLElement>);
|
||||
onValidate(result: IValidationResult);
|
||||
}
|
||||
|
||||
|
|
|
@ -238,7 +238,7 @@ export default class PasswordLogin extends React.Component {
|
|||
type="text"
|
||||
label={_t("Phone")}
|
||||
value={this.state.phoneNumber}
|
||||
prefix={phoneCountry}
|
||||
prefixComponent={phoneCountry}
|
||||
onChange={this.onPhoneNumberChanged}
|
||||
onBlur={this.onPhoneNumberBlur}
|
||||
disabled={this.props.disableSubmit}
|
||||
|
|
|
@ -473,7 +473,7 @@ export default createReactClass({
|
|||
type="text"
|
||||
label={phoneLabel}
|
||||
value={this.state.phoneNumber}
|
||||
prefix={phoneCountry}
|
||||
prefixComponent={phoneCountry}
|
||||
onChange={this.onPhoneNumberChange}
|
||||
onValidate={this.onPhoneNumberValidate}
|
||||
/>;
|
||||
|
|
|
@ -22,7 +22,8 @@ import classnames from 'classnames';
|
|||
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
||||
import {makeType} from "../../../utils/TypeUtils";
|
||||
|
||||
const MODULAR_URL = 'https://modular.im/?utm_source=riot-web&utm_medium=web&utm_campaign=riot-web-authentication';
|
||||
const MODULAR_URL = 'https://modular.im/services/matrix-hosting-riot' +
|
||||
'?utm_source=riot-web&utm_medium=web&utm_campaign=riot-web-authentication';
|
||||
|
||||
export const FREE = 'Free';
|
||||
export const PREMIUM = 'Premium';
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 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.
|
||||
|
@ -17,220 +17,182 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, {useCallback, useContext, useEffect, useMemo, useState} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import * as AvatarLogic from '../../../Avatar';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import toRem from "../../../utils/rem";
|
||||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
||||
import {toPx} from "../../../utils/units";
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'BaseAvatar',
|
||||
const useImageUrl = ({url, urls}) => {
|
||||
const [imageUrls, setUrls] = useState([]);
|
||||
const [urlsIndex, setIndex] = useState();
|
||||
|
||||
propTypes: {
|
||||
name: PropTypes.string.isRequired, // The name (first initial used as default)
|
||||
idName: PropTypes.string, // ID for generating hash colours
|
||||
title: PropTypes.string, // onHover title text
|
||||
url: PropTypes.string, // highest priority of them all, shortcut to set in urls[0]
|
||||
urls: PropTypes.array, // [highest_priority, ... , lowest_priority]
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
// XXX resizeMethod not actually used.
|
||||
resizeMethod: PropTypes.string,
|
||||
defaultToInitialLetter: PropTypes.bool, // true to add default url
|
||||
inputRef: PropTypes.oneOfType([
|
||||
// Either a function
|
||||
PropTypes.func,
|
||||
// Or the instance of a DOM native element
|
||||
PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
|
||||
]),
|
||||
},
|
||||
const onError = useCallback(() => {
|
||||
setIndex(i => i + 1); // try the next one
|
||||
}, []);
|
||||
const memoizedUrls = useMemo(() => urls, [JSON.stringify(urls)]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
statics: {
|
||||
contextType: MatrixClientContext,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
width: 40,
|
||||
height: 40,
|
||||
resizeMethod: 'crop',
|
||||
defaultToInitialLetter: true,
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return this._getState(this.props);
|
||||
},
|
||||
|
||||
componentDidMount() {
|
||||
this.unmounted = false;
|
||||
this.context.on('sync', this.onClientSync);
|
||||
},
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unmounted = true;
|
||||
this.context.removeListener('sync', this.onClientSync);
|
||||
},
|
||||
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||
UNSAFE_componentWillReceiveProps: function(nextProps) {
|
||||
// work out if we need to call setState (if the image URLs array has changed)
|
||||
const newState = this._getState(nextProps);
|
||||
const newImageUrls = newState.imageUrls;
|
||||
const oldImageUrls = this.state.imageUrls;
|
||||
if (newImageUrls.length !== oldImageUrls.length) {
|
||||
this.setState(newState); // detected a new entry
|
||||
} else {
|
||||
// check each one to see if they are the same
|
||||
for (let i = 0; i < newImageUrls.length; i++) {
|
||||
if (oldImageUrls[i] !== newImageUrls[i]) {
|
||||
this.setState(newState); // detected a diff
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onClientSync: function(syncState, prevState) {
|
||||
if (this.unmounted) return;
|
||||
|
||||
// Consider the client reconnected if there is no error with syncing.
|
||||
// This means the state could be RECONNECTING, SYNCING, PREPARED or CATCHUP.
|
||||
const reconnected = syncState !== "ERROR" && prevState !== syncState;
|
||||
if (reconnected &&
|
||||
// Did we fall back?
|
||||
this.state.urlsIndex > 0
|
||||
) {
|
||||
// Start from the highest priority URL again
|
||||
this.setState({
|
||||
urlsIndex: 0,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_getState: function(props) {
|
||||
useEffect(() => {
|
||||
// work out the full set of urls to try to load. This is formed like so:
|
||||
// imageUrls: [ props.url, props.urls, default image ]
|
||||
// imageUrls: [ props.url, ...props.urls ]
|
||||
|
||||
let urls = [];
|
||||
let _urls = [];
|
||||
if (!SettingsStore.getValue("lowBandwidth")) {
|
||||
urls = props.urls || [];
|
||||
_urls = memoizedUrls || [];
|
||||
|
||||
if (props.url) {
|
||||
urls.unshift(props.url); // put in urls[0]
|
||||
if (url) {
|
||||
_urls.unshift(url); // put in urls[0]
|
||||
}
|
||||
}
|
||||
|
||||
let defaultImageUrl = null;
|
||||
if (props.defaultToInitialLetter) {
|
||||
defaultImageUrl = AvatarLogic.defaultAvatarUrlForString(
|
||||
props.idName || props.name,
|
||||
);
|
||||
urls.push(defaultImageUrl); // lowest priority
|
||||
}
|
||||
|
||||
// deduplicate URLs
|
||||
urls = Array.from(new Set(urls));
|
||||
_urls = Array.from(new Set(_urls));
|
||||
|
||||
return {
|
||||
imageUrls: urls,
|
||||
defaultImageUrl: defaultImageUrl,
|
||||
urlsIndex: 0,
|
||||
};
|
||||
},
|
||||
setIndex(0);
|
||||
setUrls(_urls);
|
||||
}, [url, memoizedUrls]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
onError: function(ev) {
|
||||
const nextIndex = this.state.urlsIndex + 1;
|
||||
if (nextIndex < this.state.imageUrls.length) {
|
||||
// try the next one
|
||||
this.setState({
|
||||
urlsIndex: nextIndex,
|
||||
});
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const onClientSync = useCallback((syncState, prevState) => {
|
||||
// Consider the client reconnected if there is no error with syncing.
|
||||
// This means the state could be RECONNECTING, SYNCING, PREPARED or CATCHUP.
|
||||
const reconnected = syncState !== "ERROR" && prevState !== syncState;
|
||||
if (reconnected) {
|
||||
setIndex(0);
|
||||
}
|
||||
},
|
||||
}, []);
|
||||
useEventEmitter(cli, "sync", onClientSync);
|
||||
|
||||
render: function() {
|
||||
const imageUrl = this.state.imageUrls[this.state.urlsIndex];
|
||||
const imageUrl = imageUrls[urlsIndex];
|
||||
return [imageUrl, onError];
|
||||
};
|
||||
|
||||
const {
|
||||
name, idName, title, url, urls, width, height, resizeMethod,
|
||||
defaultToInitialLetter, onClick, inputRef,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
const BaseAvatar = (props) => {
|
||||
const {
|
||||
name,
|
||||
idName,
|
||||
title,
|
||||
url,
|
||||
urls,
|
||||
width=40,
|
||||
height=40,
|
||||
resizeMethod="crop", // eslint-disable-line no-unused-vars
|
||||
defaultToInitialLetter=true,
|
||||
onClick,
|
||||
inputRef,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
const [imageUrl, onError] = useImageUrl({url, urls});
|
||||
|
||||
if (!imageUrl && defaultToInitialLetter) {
|
||||
const initialLetter = AvatarLogic.getInitialLetter(name);
|
||||
const textNode = (
|
||||
<span
|
||||
className="mx_BaseAvatar_initial"
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
fontSize: toPx(width * 0.65),
|
||||
width: toPx(width),
|
||||
lineHeight: toPx(height),
|
||||
}}
|
||||
>
|
||||
{ initialLetter }
|
||||
</span>
|
||||
);
|
||||
const imgNode = (
|
||||
<img
|
||||
className="mx_BaseAvatar_image"
|
||||
src={AvatarLogic.defaultAvatarUrlForString(idName || name)}
|
||||
alt=""
|
||||
title={title}
|
||||
onError={onError}
|
||||
style={{
|
||||
width: toPx(width),
|
||||
height: toPx(height),
|
||||
}}
|
||||
aria-hidden="true" />
|
||||
);
|
||||
|
||||
if (imageUrl === this.state.defaultImageUrl) {
|
||||
const initialLetter = AvatarLogic.getInitialLetter(name);
|
||||
const textNode = (
|
||||
<span className="mx_BaseAvatar_initial" aria-hidden="true"
|
||||
style={{
|
||||
fontSize: toRem(width * 0.65),
|
||||
width: toRem(width),
|
||||
lineHeight: toRem(height),
|
||||
}}
|
||||
>
|
||||
{ initialLetter }
|
||||
</span>
|
||||
);
|
||||
const imgNode = (
|
||||
<img className="mx_BaseAvatar_image" src={imageUrl}
|
||||
alt="" title={title} onError={this.onError}
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
width: toRem(width),
|
||||
height: toRem(height)
|
||||
}} />
|
||||
);
|
||||
if (onClick != null) {
|
||||
return (
|
||||
<AccessibleButton element='span' className="mx_BaseAvatar"
|
||||
onClick={onClick} inputRef={inputRef} {...otherProps}
|
||||
>
|
||||
{ textNode }
|
||||
{ imgNode }
|
||||
</AccessibleButton>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<span className="mx_BaseAvatar" ref={inputRef} {...otherProps}>
|
||||
{ textNode }
|
||||
{ imgNode }
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
if (onClick != null) {
|
||||
return (
|
||||
<AccessibleButton
|
||||
className="mx_BaseAvatar mx_BaseAvatar_image"
|
||||
element='img'
|
||||
src={imageUrl}
|
||||
{...otherProps}
|
||||
element="span"
|
||||
className="mx_BaseAvatar"
|
||||
onClick={onClick}
|
||||
onError={this.onError}
|
||||
style={{
|
||||
width: toRem(width),
|
||||
height: toRem(height),
|
||||
}}
|
||||
title={title} alt=""
|
||||
inputRef={inputRef}
|
||||
{...otherProps} />
|
||||
>
|
||||
{ textNode }
|
||||
{ imgNode }
|
||||
</AccessibleButton>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<img
|
||||
className="mx_BaseAvatar mx_BaseAvatar_image"
|
||||
src={imageUrl}
|
||||
onError={this.onError}
|
||||
style={{
|
||||
width: toRem(width),
|
||||
height: toRem(height),
|
||||
}}
|
||||
title={title} alt=""
|
||||
ref={inputRef}
|
||||
{...otherProps} />
|
||||
<span className="mx_BaseAvatar" ref={inputRef} {...otherProps}>
|
||||
{ textNode }
|
||||
{ imgNode }
|
||||
</span>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (onClick != null) {
|
||||
return (
|
||||
<AccessibleButton
|
||||
className="mx_BaseAvatar mx_BaseAvatar_image"
|
||||
element='img'
|
||||
src={imageUrl}
|
||||
onClick={onClick}
|
||||
onError={onError}
|
||||
style={{
|
||||
width: toPx(width),
|
||||
height: toPx(height),
|
||||
}}
|
||||
title={title} alt=""
|
||||
inputRef={inputRef}
|
||||
{...otherProps} />
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<img
|
||||
className="mx_BaseAvatar mx_BaseAvatar_image"
|
||||
src={imageUrl}
|
||||
onError={onError}
|
||||
style={{
|
||||
width: toPx(width),
|
||||
height: toPx(height),
|
||||
}}
|
||||
title={title} alt=""
|
||||
ref={inputRef}
|
||||
{...otherProps} />
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
BaseAvatar.displayName = "BaseAvatar";
|
||||
|
||||
BaseAvatar.propTypes = {
|
||||
name: PropTypes.string.isRequired, // The name (first initial used as default)
|
||||
idName: PropTypes.string, // ID for generating hash colours
|
||||
title: PropTypes.string, // onHover title text
|
||||
url: PropTypes.string, // highest priority of them all, shortcut to set in urls[0]
|
||||
urls: PropTypes.array, // [highest_priority, ... , lowest_priority]
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
// XXX resizeMethod not actually used.
|
||||
resizeMethod: PropTypes.string,
|
||||
defaultToInitialLetter: PropTypes.bool, // true to add default url
|
||||
onClick: PropTypes.func,
|
||||
inputRef: PropTypes.oneOfType([
|
||||
// Either a function
|
||||
PropTypes.func,
|
||||
// Or the instance of a DOM native element
|
||||
PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
|
||||
]),
|
||||
};
|
||||
|
||||
export default BaseAvatar;
|
||||
|
|
|
@ -18,9 +18,10 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import * as Avatar from '../../../Avatar';
|
||||
import * as sdk from "../../../index";
|
||||
import dis from "../../../dispatcher";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'MemberAvatar',
|
||||
|
@ -33,7 +34,7 @@ export default createReactClass({
|
|||
resizeMethod: PropTypes.string,
|
||||
// The onClick to give the avatar
|
||||
onClick: PropTypes.func,
|
||||
// Whether the onClick of the avatar should be overriden to dispatch 'view_user'
|
||||
// Whether the onClick of the avatar should be overriden to dispatch `Action.ViewUser`
|
||||
viewUserOnClick: PropTypes.bool,
|
||||
title: PropTypes.string,
|
||||
},
|
||||
|
@ -61,10 +62,14 @@ export default createReactClass({
|
|||
return {
|
||||
name: props.member.name,
|
||||
title: props.title || props.member.userId,
|
||||
imageUrl: Avatar.avatarUrlForMember(props.member,
|
||||
props.width,
|
||||
props.height,
|
||||
props.resizeMethod),
|
||||
imageUrl: props.member.getAvatarUrl(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
Math.floor(props.width * window.devicePixelRatio),
|
||||
Math.floor(props.height * window.devicePixelRatio),
|
||||
props.resizeMethod,
|
||||
false,
|
||||
false,
|
||||
),
|
||||
};
|
||||
} else if (props.fallbackUserId) {
|
||||
return {
|
||||
|
@ -85,7 +90,7 @@ export default createReactClass({
|
|||
if (viewUserOnClick) {
|
||||
onClick = () => {
|
||||
dis.dispatch({
|
||||
action: 'view_user',
|
||||
action: Action.ViewUser,
|
||||
member: this.props.member,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -23,7 +23,7 @@ import createReactClass from 'create-react-class';
|
|||
import {EventStatus} from 'matrix-js-sdk';
|
||||
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import dis from '../../../dispatcher';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import Modal from '../../../Modal';
|
||||
|
@ -116,11 +116,6 @@ export default createReactClass({
|
|||
this.closeMenu();
|
||||
},
|
||||
|
||||
e2eInfoClicked: function() {
|
||||
this.props.e2eInfoCallback();
|
||||
this.closeMenu();
|
||||
},
|
||||
|
||||
onReportEventClick: function() {
|
||||
const ReportEventDialog = sdk.getComponent("dialogs.ReportEventDialog");
|
||||
Modal.createTrackedDialog('Report Event', '', ReportEventDialog, {
|
||||
|
@ -465,15 +460,6 @@ export default createReactClass({
|
|||
);
|
||||
}
|
||||
|
||||
let e2eInfo;
|
||||
if (this.props.e2eInfoCallback) {
|
||||
e2eInfo = (
|
||||
<MenuItem className="mx_MessageContextMenu_field" onClick={this.e2eInfoClicked}>
|
||||
{ _t('End-to-end encryption information') }
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
let reportEventButton;
|
||||
if (mxEvent.getSender() !== me) {
|
||||
reportEventButton = (
|
||||
|
@ -500,7 +486,6 @@ export default createReactClass({
|
|||
{ quoteButton }
|
||||
{ externalURLButton }
|
||||
{ collapseReplyThread }
|
||||
{ e2eInfo }
|
||||
{ reportEventButton }
|
||||
</div>
|
||||
);
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue