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:
Michael Telatynski 2020-06-01 21:42:31 +01:00
commit 8848a2ea64
305 changed files with 9256 additions and 4200 deletions

View file

@ -6,7 +6,6 @@ src/components/structures/RoomView.js
src/components/structures/ScrollPanel.js src/components/structures/ScrollPanel.js
src/components/structures/SearchBox.js src/components/structures/SearchBox.js
src/components/structures/UploadBar.js src/components/structures/UploadBar.js
src/components/views/avatars/BaseAvatar.js
src/components/views/avatars/MemberAvatar.js src/components/views/avatars/MemberAvatar.js
src/components/views/create_room/RoomAlias.js src/components/views/create_room/RoomAlias.js
src/components/views/dialogs/SetPasswordDialog.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/DirectorySearchBox.js
src/components/views/elements/MemberEventListSummary.js src/components/views/elements/MemberEventListSummary.js
src/components/views/elements/UserSelector.js src/components/views/elements/UserSelector.js
src/components/views/globals/MatrixToolbar.js
src/components/views/globals/NewVersionBar.js src/components/views/globals/NewVersionBar.js
src/components/views/globals/UpdateCheckBar.js
src/components/views/messages/MFileBody.js src/components/views/messages/MFileBody.js
src/components/views/messages/TextualBody.js src/components/views/messages/TextualBody.js
src/components/views/room_settings/ColorSettings.js src/components/views/room_settings/ColorSettings.js

View file

@ -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) 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) [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.5.0-rc.6...v2.5.0)

View file

@ -4,7 +4,7 @@ Matrix JavaScript/ECMAScript Style Guide
The intention of this guide is to make Matrix's JavaScript codebase clean, 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 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 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 This document reflects how we would like Matrix JavaScript code to look, with
acknowledgement that a significant amount of code is written to older 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 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 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 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 General Style
------------- -------------
@ -151,6 +151,7 @@ General Style
Don't set things to undefined. Reserve that value to mean "not yet set to anything." Don't set things to undefined. Reserve that value to mean "not yet set to anything."
Boolean objects are verboten. Boolean objects are verboten.
- Use JSDoc - Use JSDoc
- Use switch-case statements where there are 5 or more branches running against the same variable.
ECMAScript ECMAScript
---------- ----------

View file

@ -1,6 +1,6 @@
{ {
"name": "matrix-react-sdk", "name": "matrix-react-sdk",
"version": "2.5.0", "version": "2.6.1",
"description": "SDK for matrix.org using React", "description": "SDK for matrix.org using React",
"author": "matrix.org", "author": "matrix.org",
"repository": { "repository": {
@ -55,6 +55,7 @@
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.8.3", "@babel/runtime": "^7.8.3",
"await-lock": "^2.0.1",
"blueimp-canvas-to-blob": "^3.5.0", "blueimp-canvas-to-blob": "^3.5.0",
"browser-encrypt-attachment": "^0.3.0", "browser-encrypt-attachment": "^0.3.0",
"browser-request": "^0.3.3", "browser-request": "^0.3.3",
@ -117,9 +118,13 @@
"@babel/register": "^7.7.4", "@babel/register": "^7.7.4",
"@peculiar/webcrypto": "^1.0.22", "@peculiar/webcrypto": "^1.0.22",
"@types/classnames": "^2.2.10", "@types/classnames": "^2.2.10",
"@types/flux": "^3.1.9",
"@types/lodash": "^4.14.152",
"@types/modernizr": "^3.5.3", "@types/modernizr": "^3.5.3",
"@types/node": "^12.12.41",
"@types/qrcode": "^1.3.4", "@types/qrcode": "^1.3.4",
"@types/react": "16.9", "@types/react": "^16.9",
"@types/react-dom": "^16.9.8",
"@types/zxcvbn": "^4.4.0", "@types/zxcvbn": "^4.4.0",
"babel-eslint": "^10.0.3", "babel-eslint": "^10.0.3",
"babel-jest": "^24.9.0", "babel-jest": "^24.9.0",
@ -139,6 +144,7 @@
"flow-parser": "^0.57.3", "flow-parser": "^0.57.3",
"glob": "^5.0.14", "glob": "^5.0.14",
"jest": "^24.9.0", "jest": "^24.9.0",
"jest-canvas-mock": "^2.2.0",
"lolex": "^5.1.2", "lolex": "^5.1.2",
"matrix-mock-request": "^1.2.3", "matrix-mock-request": "^1.2.3",
"matrix-react-test-utils": "^0.2.2", "matrix-react-test-utils": "^0.2.2",
@ -158,6 +164,7 @@
"testMatch": [ "testMatch": [
"<rootDir>/test/**/*-test.js" "<rootDir>/test/**/*-test.js"
], ],
"setupFiles": ["jest-canvas-mock"],
"setupFilesAfterEnv": [ "setupFilesAfterEnv": [
"<rootDir>/test/setupTests.js" "<rootDir>/test/setupTests.js"
], ],

View file

@ -335,6 +335,9 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
.mx_Dialog_header.mx_Dialog_headerWithButton > .mx_Dialog_title { .mx_Dialog_header.mx_Dialog_headerWithButton > .mx_Dialog_title {
text-align: center; 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 { .mx_Dialog_title.danger {
color: $warning-color; color: $warning-color;

View file

@ -63,7 +63,6 @@
@import "./views/dialogs/_DeactivateAccountDialog.scss"; @import "./views/dialogs/_DeactivateAccountDialog.scss";
@import "./views/dialogs/_DeviceVerifyDialog.scss"; @import "./views/dialogs/_DeviceVerifyDialog.scss";
@import "./views/dialogs/_DevtoolsDialog.scss"; @import "./views/dialogs/_DevtoolsDialog.scss";
@import "./views/dialogs/_EncryptedEventDialog.scss";
@import "./views/dialogs/_GroupAddressPicker.scss"; @import "./views/dialogs/_GroupAddressPicker.scss";
@import "./views/dialogs/_IncomingSasDialog.scss"; @import "./views/dialogs/_IncomingSasDialog.scss";
@import "./views/dialogs/_InviteDialog.scss"; @import "./views/dialogs/_InviteDialog.scss";
@ -115,7 +114,9 @@
@import "./views/elements/_RichText.scss"; @import "./views/elements/_RichText.scss";
@import "./views/elements/_RoleButton.scss"; @import "./views/elements/_RoleButton.scss";
@import "./views/elements/_RoomAliasField.scss"; @import "./views/elements/_RoomAliasField.scss";
@import "./views/elements/_Slider.scss";
@import "./views/elements/_Spinner.scss"; @import "./views/elements/_Spinner.scss";
@import "./views/elements/_StyledCheckbox.scss";
@import "./views/elements/_SyntaxHighlight.scss"; @import "./views/elements/_SyntaxHighlight.scss";
@import "./views/elements/_TextWithTooltip.scss"; @import "./views/elements/_TextWithTooltip.scss";
@import "./views/elements/_ToggleSwitch.scss"; @import "./views/elements/_ToggleSwitch.scss";
@ -123,7 +124,6 @@
@import "./views/elements/_TooltipButton.scss"; @import "./views/elements/_TooltipButton.scss";
@import "./views/elements/_Validation.scss"; @import "./views/elements/_Validation.scss";
@import "./views/emojipicker/_EmojiPicker.scss"; @import "./views/emojipicker/_EmojiPicker.scss";
@import "./views/globals/_MatrixToolbar.scss";
@import "./views/groups/_GroupPublicityToggle.scss"; @import "./views/groups/_GroupPublicityToggle.scss";
@import "./views/groups/_GroupRoomList.scss"; @import "./views/groups/_GroupRoomList.scss";
@import "./views/groups/_GroupUserSettings.scss"; @import "./views/groups/_GroupUserSettings.scss";
@ -162,6 +162,8 @@
@import "./views/rooms/_EditMessageComposer.scss"; @import "./views/rooms/_EditMessageComposer.scss";
@import "./views/rooms/_EntityTile.scss"; @import "./views/rooms/_EntityTile.scss";
@import "./views/rooms/_EventTile.scss"; @import "./views/rooms/_EventTile.scss";
@import "./views/rooms/_GroupLayout.scss";
@import "./views/rooms/_IRCLayout.scss";
@import "./views/rooms/_InviteOnlyIcon.scss"; @import "./views/rooms/_InviteOnlyIcon.scss";
@import "./views/rooms/_JumpToBottomButton.scss"; @import "./views/rooms/_JumpToBottomButton.scss";
@import "./views/rooms/_LinkPreviewWidget.scss"; @import "./views/rooms/_LinkPreviewWidget.scss";
@ -200,10 +202,12 @@
@import "./views/settings/_ProfileSettings.scss"; @import "./views/settings/_ProfileSettings.scss";
@import "./views/settings/_SetIdServer.scss"; @import "./views/settings/_SetIdServer.scss";
@import "./views/settings/_SetIntegrationManager.scss"; @import "./views/settings/_SetIntegrationManager.scss";
@import "./views/settings/_UpdateCheckButton.scss";
@import "./views/settings/tabs/_SettingsTab.scss"; @import "./views/settings/tabs/_SettingsTab.scss";
@import "./views/settings/tabs/room/_GeneralRoomSettingsTab.scss"; @import "./views/settings/tabs/room/_GeneralRoomSettingsTab.scss";
@import "./views/settings/tabs/room/_RolesRoomSettingsTab.scss"; @import "./views/settings/tabs/room/_RolesRoomSettingsTab.scss";
@import "./views/settings/tabs/room/_SecurityRoomSettingsTab.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/_GeneralUserSettingsTab.scss";
@import "./views/settings/tabs/user/_HelpUserSettingsTab.scss"; @import "./views/settings/tabs/user/_HelpUserSettingsTab.scss";
@import "./views/settings/tabs/user/_MjolnirUserSettingsTab.scss"; @import "./views/settings/tabs/user/_MjolnirUserSettingsTab.scss";

View file

@ -15,6 +15,7 @@ limitations under the License.
*/ */
$font-1px: 0.067rem; $font-1px: 0.067rem;
$font-1-5px: 0.100rem;
$font-2px: 0.133rem; $font-2px: 0.133rem;
$font-3px: 0.200rem; $font-3px: 0.200rem;
$font-4px: 0.267rem; $font-4px: 0.267rem;

View file

@ -41,10 +41,6 @@ limitations under the License.
height: 40px; height: 40px;
} }
.mx_MatrixChat_toolbarShowing {
height: auto;
}
.mx_MatrixChat { .mx_MatrixChat {
width: 100%; width: 100%;
height: 100%; height: 100%;

View file

@ -63,6 +63,10 @@ limitations under the License.
padding-left: 32px; padding-left: 32px;
padding-top: 8px; padding-top: 8px;
position: relative; position: relative;
a {
display: flex;
}
} }
.mx_NotificationPanel .mx_EventTile_roomName a, .mx_NotificationPanel .mx_EventTile_roomName a,

View file

@ -69,7 +69,7 @@ limitations under the License.
height: 100%; height: 100%;
} }
.mx_TagPanel .mx_TagPanel_tagTileContainer > div { .mx_TagPanel .mx_TagPanel_tagTileContainer > div {
height: $font-40px; height: 40px;
padding: 10px 0 9px 0; padding: 10px 0 9px 0;
} }
@ -116,7 +116,7 @@ limitations under the License.
position: absolute; position: absolute;
left: -15px; left: -15px;
border-radius: 0 3px 3px 0; border-radius: 0 3px 3px 0;
top: -8px; // (16px / 2) top: -8px; // (16px from height / 2)
} }
.mx_TagPanel .mx_TagTile.mx_AccessibleButton:focus { .mx_TagPanel .mx_TagTile.mx_AccessibleButton:focus {

View file

@ -28,8 +28,8 @@ limitations under the License.
margin: 0 4px; margin: 0 4px;
grid-row: 2 / 4; grid-row: 2 / 4;
grid-column: 1; grid-column: 1;
background-color: white; background-color: $dark-panel-bg-color;
box-shadow: 0px 4px 12px $menu-box-shadow-color; box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.5);
border-radius: 8px; border-radius: 8px;
} }
@ -37,16 +37,15 @@ limitations under the License.
grid-row: 1 / 3; grid-row: 1 / 3;
grid-column: 1; grid-column: 1;
color: $primary-fg-color; color: $primary-fg-color;
background-color: $primary-bg-color; background-color: $dark-panel-bg-color;
box-shadow: 0px 4px 12px $menu-box-shadow-color; box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.5);
border-radius: 8px; border-radius: 8px;
overflow: hidden; overflow: hidden;
display: grid; display: grid;
grid-template-columns: 20px 1fr; grid-template-columns: 22px 1fr;
column-gap: 10px; column-gap: 8px;
row-gap: 4px; row-gap: 4px;
padding: 8px; padding: 8px;
padding-right: 16px;
&.mx_Toast_hasIcon { &.mx_Toast_hasIcon {
&::after { &::after {
@ -68,17 +67,45 @@ limitations under the License.
background-image: url("$(res)/img/e2e/warning.svg"); background-image: url("$(res)/img/e2e/warning.svg");
} }
h2, .mx_Toast_body { .mx_Toast_title, .mx_Toast_body {
grid-column: 2; grid-column: 2;
} }
} }
&:not(.mx_Toast_hasIcon) {
padding-left: 12px;
h2 { .mx_Toast_title {
grid-column: 1 / 3; grid-column: 1 / -1;
grid-row: 1; }
margin: 0; }
font-size: $font-15px;
font-weight: 600; .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 { .mx_Toast_body {
@ -87,7 +114,13 @@ limitations under the License.
} }
.mx_Toast_buttons { .mx_Toast_buttons {
float: right;
display: flex; display: flex;
.mx_FormButton {
min-width: 96px;
box-sizing: border-box;
}
} }
.mx_Toast_description { .mx_Toast_description {
@ -96,6 +129,15 @@ limitations under the License.
text-overflow: ellipsis; text-overflow: ellipsis;
margin: 4px 0 11px 0; margin: 4px 0 11px 0;
font-size: $font-12px; font-size: $font-12px;
.mx_AccessibleButton_kind_link {
font-size: inherit;
padding: 0;
}
a {
text-decoration: none;
}
} }
.mx_Toast_deviceID { .mx_Toast_deviceID {

View file

@ -43,7 +43,7 @@ limitations under the License.
margin: 0 7px; margin: 0 7px;
mask: url('$(res)/img/feather-customised/dropdown-arrow.svg'); mask: url('$(res)/img/feather-customised/dropdown-arrow.svg');
mask-repeat: no-repeat; mask-repeat: no-repeat;
width: 10px; width: $font-22px;
height: 6px; height: 6px;
background-color: $roomsublist-label-fg-color; background-color: $roomsublist-label-fg-color;
} }

View file

@ -18,8 +18,3 @@ limitations under the License.
margin-top: 10px; margin-top: 10px;
display: flex; display: flex;
} }
.mx_GroupAddressPicker_checkboxContainer input[type="checkbox"] {
/* Stop flex from shrinking the checkbox */
width: 20px;
}

View file

@ -55,6 +55,7 @@ limitations under the License.
margin-left: 5px; margin-left: 5px;
width: 20px; width: 20px;
height: 20px; height: 20px;
background-repeat: none;
} }
.mx_ShareDialog_split { .mx_ShareDialog_split {

View file

@ -21,6 +21,10 @@ limitations under the License.
mask-image: url('$(res)/img/feather-customised/settings.svg'); 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 { .mx_UserSettingsDialog_voiceIcon::before {
mask-image: url('$(res)/img/feather-customised/phone.svg'); mask-image: url('$(res)/img/feather-customised/phone.svg');
} }

View 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%;
}

View 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;
}
}
}

View file

@ -190,7 +190,7 @@ limitations under the License.
.mx_EmojiPicker_footer { .mx_EmojiPicker_footer {
border-top: 1px solid $message-action-bar-border-color; border-top: 1px solid $message-action-bar-border-color;
height: 72px; min-height: 72px;
display: flex; display: flex;
align-items: center; align-items: center;

View file

@ -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;
}

View file

@ -96,6 +96,10 @@ $AppsDrawerBodyHeight: 273px;
height: $AppsDrawerBodyHeight; height: $AppsDrawerBodyHeight;
} }
.mx_AppTile_persistedWrapper > div {
height: 100%;
}
.mx_AppTile_mini .mx_AppTile_persistedWrapper { .mx_AppTile_mini .mx_AppTile_persistedWrapper {
height: 114px; height: 114px;
} }

View file

@ -37,7 +37,6 @@ limitations under the License.
} }
.mx_EventTile_avatar { .mx_EventTile_avatar {
position: absolute;
top: 14px; top: 14px;
left: 8px; left: 8px;
cursor: pointer; cursor: pointer;
@ -68,11 +67,9 @@ limitations under the License.
display: inline-block; /* anti-zalgo, with overflow hidden */ display: inline-block; /* anti-zalgo, with overflow hidden */
overflow: hidden; overflow: hidden;
cursor: pointer; cursor: pointer;
padding-left: 65px; /* left gutter */
padding-bottom: 0px; padding-bottom: 0px;
padding-top: 0px; padding-top: 0px;
margin: 0px; margin: 0px;
line-height: $font-17px;
/* the next three lines, along with overflow hidden, truncate long display names */ /* the next three lines, along with overflow hidden, truncate long display names */
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
@ -104,9 +101,7 @@ limitations under the License.
visibility: hidden; visibility: hidden;
white-space: nowrap; white-space: nowrap;
left: 0px; left: 0px;
width: 46px; /* 8 + 30 (avatar) + 8 */
text-align: center; text-align: center;
position: absolute;
user-select: none; user-select: none;
} }
@ -117,10 +112,7 @@ limitations under the License.
.mx_EventTile_line, .mx_EventTile_reply { .mx_EventTile_line, .mx_EventTile_reply {
position: relative; position: relative;
padding-left: 65px; /* left gutter */ padding-left: 65px; /* left gutter */
padding-top: 3px;
padding-bottom: 3px;
border-radius: 4px; border-radius: 4px;
line-height: $font-22px;
} }
.mx_RoomView_timeline_rr_enabled, .mx_RoomView_timeline_rr_enabled,
@ -151,10 +143,6 @@ limitations under the License.
margin-right: 10px; margin-right: 10px;
} }
.mx_EventTile_info .mx_EventTile_line {
padding-left: 83px;
}
/* HACK to override line-height which is already marked important elsewhere */ /* HACK to override line-height which is already marked important elsewhere */
.mx_EventTile_bigEmoji.mx_EventTile_bigEmoji { .mx_EventTile_bigEmoji.mx_EventTile_bigEmoji {
font-size: 48px !important; 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) // 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_last > div > a > .mx_MessageTimestamp,
.mx_EventTile:hover > div > a > .mx_MessageTimestamp, .mx_EventTile:hover > div > a > .mx_MessageTimestamp,
.mx_EventTile.mx_EventTile_actionBarFocused > 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; visibility: visible;
} }
@ -560,84 +553,6 @@ limitations under the License.
/* end of overrides */ /* 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 { .mx_EventTile_tileError {
color: red; color: red;
text-align: center; text-align: center;

View 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;
}
}

View 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;
}
}

View file

@ -20,7 +20,7 @@ limitations under the License.
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;
height: $font-34px; height: 34px;
margin: 0; margin: 0;
padding: 0 8px 0 10px; padding: 0 8px 0 10px;
position: relative; position: relative;
@ -81,6 +81,7 @@ limitations under the License.
.mx_RoomTile_avatar_container { .mx_RoomTile_avatar_container {
position: relative; position: relative;
display: flex;
} }
.mx_RoomTile_avatar { .mx_RoomTile_avatar {

View 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;
}
}

View file

@ -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;
}

View file

@ -14,8 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.mx_GeneralUserSettingsTab_changePassword .mx_Field, .mx_GeneralUserSettingsTab_changePassword .mx_Field {
.mx_GeneralUserSettingsTab_themeSection .mx_Field {
@mixin mx_Settings_fullWidthField; @mixin mx_Settings_fullWidthField;
} }

View 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

View file

@ -180,6 +180,9 @@ $breadcrumb-placeholder-bg-color: #272c35;
$user-tile-hover-bg-color: $header-panel-bg-color; $user-tile-hover-bg-color: $header-panel-bg-color;
// FontSlider colors
$font-slider-bg-color: $room-highlight-color;
// ***** Mixins! ***** // ***** Mixins! *****
@define-mixin mx_DialogButton { @define-mixin mx_DialogButton {

View file

@ -262,6 +262,10 @@ $togglesw-off-color: #c1c9d6;
$togglesw-on-color: $accent-color; $togglesw-on-color: $accent-color;
$togglesw-ball-color: #fff; $togglesw-ball-color: #fff;
// Slider
$slider-selection-color: $accent-color;
$slider-background-color: #c1c9d6;
$progressbar-color: #000; $progressbar-color: #000;
$room-warning-bg-color: $yellow-background; $room-warning-bg-color: $yellow-background;
@ -302,6 +306,9 @@ $breadcrumb-placeholder-bg-color: #e8eef5;
$user-tile-hover-bg-color: $header-panel-bg-color; $user-tile-hover-bg-color: $header-panel-bg-color;
// FontSlider colors
$font-slider-bg-color: rgba($input-darker-bg-color, 0.2);
// ***** Mixins! ***** // ***** Mixins! *****
@define-mixin mx_DialogButton { @define-mixin mx_DialogButton {

19
src/@types/common.ts Normal file
View 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;

View file

@ -15,13 +15,22 @@ limitations under the License.
*/ */
import * as ModernizrStatic from "modernizr"; import * as ModernizrStatic from "modernizr";
import ContentMessages from "../ContentMessages";
import { IMatrixClientPeg } from "../MatrixClientPeg";
import ToastStore from "../stores/ToastStore";
import DeviceListener from "../DeviceListener";
declare global { declare global {
interface Window { interface Window {
Modernizr: ModernizrStatic; Modernizr: ModernizrStatic;
mxMatrixClientPeg: IMatrixClientPeg;
Olm: { Olm: {
init: () => Promise<void>; init: () => Promise<void>;
}; };
mx_ContentMessages: ContentMessages;
mx_ToastStore: ToastStore;
mx_DeviceListener: DeviceListener;
} }
// workaround for https://github.com/microsoft/TypeScript/issues/30933 // workaround for https://github.com/microsoft/TypeScript/issues/30933

View file

@ -19,6 +19,7 @@ import {MatrixClientPeg} from './MatrixClientPeg';
import DMRoomMap from './utils/DMRoomMap'; import DMRoomMap from './utils/DMRoomMap';
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo"; 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) { export function avatarUrlForMember(member, width, height, resizeMethod) {
let url; let url;
if (member && member.getAvatarUrl) { if (member && member.getAvatarUrl) {

View file

@ -1,5 +1,3 @@
// @flow
/* /*
Copyright 2016 Aviral Dasgupta Copyright 2016 Aviral Dasgupta
Copyright 2016 OpenMarket Ltd Copyright 2016 OpenMarket Ltd
@ -19,9 +17,23 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {MatrixClient} from "matrix-js-sdk"; import {MatrixClient} from "matrix-js-sdk/src/client";
import dis from './dispatcher'; import dis from './dispatcher/dispatcher';
import BaseEventIndexManager from './indexing/BaseEventIndexManager'; 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 * 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. * Instances of this class are provided by the application.
*/ */
export default class BasePlatform { export default abstract class BasePlatform {
constructor() { protected notificationCount = 0;
this.notificationCount = 0; protected errorDidOccur = false;
this.errorDidOccur = false;
dis.register(this._onAction.bind(this)); constructor() {
dis.register(this.onAction);
} }
_onAction(payload: Object) { protected onAction = (payload: ActionPayload) => {
switch (payload.action) { switch (payload.action) {
case 'on_client_not_viable': case 'on_client_not_viable':
case 'on_logged_out': case 'on_logged_out':
this.setNotificationCount(0); this.setNotificationCount(0);
break; break;
} }
} };
// Used primarily for Analytics // Used primarily for Analytics
getHumanReadableName(): string { abstract getHumanReadableName(): string;
return 'Base Platform';
}
setNotificationCount(count: number) { setNotificationCount(count: number) {
this.notificationCount = count; this.notificationCount = count;
@ -59,6 +69,53 @@ export default class BasePlatform {
this.errorDidOccur = errorDidOccur; 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 * Returns true if the platform supports displaying
* notifications, otherwise false. * notifications, otherwise false.
@ -84,22 +141,17 @@ export default class BasePlatform {
* that is 'granted' if the user allowed the request or * that is 'granted' if the user allowed the request or
* 'denied' otherwise. * '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) { loudNotification(ev: Event, room: Object) {
} };
/** /**
* Returns a promise that resolves to a string representing * Returns a promise that resolves to a string representing the current version of the application.
* the current version of the application.
*/ */
getAppVersion(): Promise<string> { abstract getAppVersion(): Promise<string>;
throw new Error("getAppVersion not implemented!");
}
/* /*
* If it's not expected that capturing the screen will work * 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 * Restarts the application, without neccessarily reloading
* any application code * any application code
*/ */
reload() { abstract reload();
throw new Error("reload not implemented!");
}
supportsAutoLaunch(): boolean { supportsAutoLaunch(): boolean {
return false; return false;
} }
// XXX: Surely this should be a setting like any other? // XXX: Surely this should be a setting like any other?
async getAutoLaunchEnabled(): boolean { async getAutoLaunchEnabled(): Promise<boolean> {
return false; return false;
} }
async setAutoLaunchEnabled(enabled: boolean): void { async setAutoLaunchEnabled(enabled: boolean): Promise<void> {
throw new Error("Unimplemented"); throw new Error("Unimplemented");
} }
@ -135,11 +185,11 @@ export default class BasePlatform {
return false; return false;
} }
async getAutoHideMenuBarEnabled(): boolean { async getAutoHideMenuBarEnabled(): Promise<boolean> {
return false; return false;
} }
async setAutoHideMenuBarEnabled(enabled: boolean): void { async setAutoHideMenuBarEnabled(enabled: boolean): Promise<void> {
throw new Error("Unimplemented"); throw new Error("Unimplemented");
} }
@ -147,11 +197,11 @@ export default class BasePlatform {
return false; return false;
} }
async getMinimizeToTrayEnabled(): boolean { async getMinimizeToTrayEnabled(): Promise<boolean> {
return false; return false;
} }
async setMinimizeToTrayEnabled(enabled: boolean): void { async setMinimizeToTrayEnabled(enabled: boolean): Promise<void> {
throw new Error("Unimplemented"); throw new Error("Unimplemented");
} }
@ -190,4 +240,35 @@ export default class BasePlatform {
onKeyDown(ev: KeyboardEvent): boolean { onKeyDown(ev: KeyboardEvent): boolean {
return false; // no shortcuts implemented 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> {
}
} }

View file

@ -59,7 +59,7 @@ import Modal from './Modal';
import * as sdk from './index'; import * as sdk from './index';
import { _t } from './languageHandler'; import { _t } from './languageHandler';
import Matrix from 'matrix-js-sdk'; import Matrix from 'matrix-js-sdk';
import dis from './dispatcher'; import dis from './dispatcher/dispatcher';
import { showUnknownDeviceDialogForCalls } from './cryptodevices'; import { showUnknownDeviceDialogForCalls } from './cryptodevices';
import WidgetUtils from './utils/WidgetUtils'; import WidgetUtils from './utils/WidgetUtils';
import WidgetEchoStore from './stores/WidgetEchoStore'; import WidgetEchoStore from './stores/WidgetEchoStore';

View file

@ -1,6 +1,7 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 New Vector 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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -15,17 +16,18 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
'use strict'; import React from "react";
import extend from './extend'; import extend from './extend';
import dis from './dispatcher'; import dis from './dispatcher/dispatcher';
import {MatrixClientPeg} from './MatrixClientPeg'; import {MatrixClientPeg} from './MatrixClientPeg';
import {MatrixClient} from "matrix-js-sdk/src/client";
import * as sdk from './index'; import * as sdk from './index';
import { _t } from './languageHandler'; import { _t } from './languageHandler';
import Modal from './Modal'; import Modal from './Modal';
import RoomViewStore from './stores/RoomViewStore'; import RoomViewStore from './stores/RoomViewStore';
import encrypt from "browser-encrypt-attachment"; import encrypt from "browser-encrypt-attachment";
import extractPngChunks from "png-chunks-extract"; import extractPngChunks from "png-chunks-extract";
import Spinner from "./components/views/elements/Spinner";
// Polyfill for Canvas.toBlob API using Canvas.toDataURL // Polyfill for Canvas.toBlob API using Canvas.toDataURL
import "blueimp-canvas-to-blob"; 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 {} 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. * Create a thumbnail for a image DOM element.
* The image will be smaller than MAX_WIDTH and MAX_HEIGHT. * 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. * about the original image and the thumbnail.
* *
* @param {HTMLElement} element The element to thumbnail. * @param {HTMLElement} element The element to thumbnail.
* @param {integer} inputWidth The width of the image in the input element. * @param {number} inputWidth The width of the image in the input element.
* @param {integer} inputHeight 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. * @param {String} mimeType The mimeType to save the blob as.
* @return {Promise} A promise that resolves with an object with an info key * @return {Promise} A promise that resolves with an object with an info key
* and a thumbnail 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) => { return new Promise((resolve) => {
let targetWidth = inputWidth; let targetWidth = inputWidth;
let targetHeight = inputHeight; let targetHeight = inputHeight;
@ -98,7 +144,7 @@ function createThumbnail(element, inputWidth, inputHeight, mimeType) {
* @param {File} imageFile The file to load in an image element. * @param {File} imageFile The file to load in an image element.
* @return {Promise} A promise that resolves with the html 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 // Load the file into an html element
const img = document.createElement("img"); const img = document.createElement("img");
const objectUrl = URL.createObjectURL(imageFile); const objectUrl = URL.createObjectURL(imageFile);
@ -128,8 +174,7 @@ async function loadImageElement(imageFile) {
for (const chunk of chunks) { for (const chunk of chunks) {
if (chunk.name === 'pHYs') { if (chunk.name === 'pHYs') {
if (chunk.data.byteLength !== PHYS_HIDPI.length) return; if (chunk.data.byteLength !== PHYS_HIDPI.length) return;
const hidpi = chunk.data.every((val, i) => val === PHYS_HIDPI[i]); return chunk.data.every((val, i) => val === PHYS_HIDPI[i]);
return hidpi;
} }
} }
return false; return false;
@ -152,7 +197,7 @@ async function loadImageElement(imageFile) {
*/ */
function infoForImageFile(matrixClient, roomId, imageFile) { function infoForImageFile(matrixClient, roomId, imageFile) {
let thumbnailType = "image/png"; let thumbnailType = "image/png";
if (imageFile.type == "image/jpeg") { if (imageFile.type === "image/jpeg") {
thumbnailType = "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. * @param {File} videoFile The file to load in an video element.
* @return {Promise} A promise that resolves with the video image 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) => { return new Promise((resolve, reject) => {
// Load the file into an html element // Load the file into an html element
const video = document.createElement("video"); const video = document.createElement("video");
const reader = new FileReader(); const reader = new FileReader();
reader.onload = function(e) { reader.onload = function(ev) {
video.src = e.target.result; video.src = ev.target.result as string;
// Once ready, returns its size // Once ready, returns its size
// Wait until we have enough data to thumbnail the first frame. // 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 * @return {Promise} A promise that resolves with an ArrayBuffer when the file
* is read. * is read.
*/ */
function readFileAsArrayBuffer(file) { function readFileAsArrayBuffer(file: File | Blob): Promise<ArrayBuffer> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = function(e) { reader.onload = function(e) {
resolve(e.target.result); resolve(e.target.result as ArrayBuffer);
}; };
reader.onerror = function(e) { reader.onerror = function(e) {
reject(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 unencrypted then the object will have a "url" key.
* If the file is encrypted then the object will have a "file" 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 (matrixClient.isRoomEncrypted(roomId)) {
// If the room is encrypted then encrypt the file before uploading it. // If the room is encrypted then encrypt the file before uploading it.
// First read the file into memory. // First read the file into memory.
let canceled = false;
let uploadPromise; let uploadPromise;
let encryptInfo; let encryptInfo;
const prom = readFileAsArrayBuffer(file).then(function(data) { const prom = readFileAsArrayBuffer(file).then(function(data) {
@ -278,9 +323,9 @@ function uploadFile(matrixClient, roomId, file, progressHandler) {
progressHandler: progressHandler, progressHandler: progressHandler,
includeFilename: false, includeFilename: false,
}); });
return uploadPromise; return uploadPromise;
}).then(function(url) { }).then(function(url) {
if (canceled) throw new UploadCanceledError();
// If the attachment is encrypted then bundle the URL along // If the attachment is encrypted then bundle the URL along
// with the information needed to decrypt the attachment and // with the information needed to decrypt the attachment and
// add it under a file key. // add it under a file key.
@ -290,7 +335,7 @@ function uploadFile(matrixClient, roomId, file, progressHandler) {
} }
return {"file": encryptInfo}; return {"file": encryptInfo};
}); });
prom.abort = () => { (prom as IAbortablePromise<any>).abort = () => {
canceled = true; canceled = true;
if (uploadPromise) MatrixClientPeg.get().cancelUpload(uploadPromise); if (uploadPromise) MatrixClientPeg.get().cancelUpload(uploadPromise);
}; };
@ -300,55 +345,23 @@ function uploadFile(matrixClient, roomId, file, progressHandler) {
progressHandler: progressHandler, progressHandler: progressHandler,
}); });
const promise1 = basePromise.then(function(url) { const promise1 = basePromise.then(function(url) {
if (canceled) throw new UploadCanceledError();
// If the attachment isn't encrypted then include the URL directly. // If the attachment isn't encrypted then include the URL directly.
return {"url": url}; return {"url": url};
}); });
// XXX: copy over the abort method to the new promise promise1.abort = () => {
promise1.abort = basePromise.abort; canceled = true;
MatrixClientPeg.get().cancelUpload(basePromise);
};
return promise1; return promise1;
} }
} }
export default class ContentMessages { export default class ContentMessages {
constructor() { private inprogress: IUpload[] = [];
this.inprogress = []; private mediaConfig: IMediaConfig = null;
this.nextId = 0;
this._mediaConfig = null;
}
static sharedInstance() { sendStickerContentToRoom(url: string, roomId: string, info: string, text: string, matrixClient: MatrixClient) {
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) {
return MatrixClientPeg.get().sendStickerMessage(roomId, url, info, text).catch((e) => { return MatrixClientPeg.get().sendStickerMessage(roomId, url, info, text).catch((e) => {
console.warn(`Failed to send content with URL ${url} to room ${roomId}`, e); console.warn(`Failed to send content with URL ${url} to room ${roomId}`, e);
throw e; throw e;
@ -356,14 +369,14 @@ export default class ContentMessages {
} }
getUploadLimit() { getUploadLimit() {
if (this._mediaConfig !== null && this._mediaConfig["m.upload.size"] !== undefined) { if (this.mediaConfig !== null && this.mediaConfig["m.upload.size"] !== undefined) {
return this._mediaConfig["m.upload.size"]; return this.mediaConfig["m.upload.size"];
} else { } else {
return null; return null;
} }
} }
async sendContentListToRoom(files, roomId, matrixClient) { async sendContentListToRoom(files: File[], roomId: string, matrixClient: MatrixClient) {
if (matrixClient.isGuest()) { if (matrixClient.isGuest()) {
dis.dispatch({action: 'require_registration'}); dis.dispatch({action: 'require_registration'});
return; return;
@ -372,32 +385,32 @@ export default class ContentMessages {
const isQuoting = Boolean(RoomViewStore.getQuotingEvent()); const isQuoting = Boolean(RoomViewStore.getQuotingEvent());
if (isQuoting) { if (isQuoting) {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const shouldUpload = await new Promise((resolve) => { const {finished} = Modal.createTrackedDialog('Upload Reply Warning', '', QuestionDialog, {
Modal.createTrackedDialog('Upload Reply Warning', '', QuestionDialog, { title: _t('Replying With Files'),
title: _t('Replying With Files'), description: (
description: ( <div>{_t(
<div>{_t( 'At this time it is not possible to reply with a file. ' +
'At this time it is not possible to reply with a file. ' + 'Would you like to upload this file without replying?',
'Would you like to upload this file without replying?', )}</div>
)}</div> ),
), hasCancelButton: true,
hasCancelButton: true, button: _t("Continue"),
button: _t("Continue"),
onFinished: (shouldUpload) => {
resolve(shouldUpload);
},
});
}); });
const [shouldUpload]: [boolean] = await finished;
if (!shouldUpload) return; 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 tooBigFiles = [];
const okFiles = []; const okFiles = [];
for (let i = 0; i < files.length; ++i) { for (let i = 0; i < files.length; ++i) {
if (this._isFileSizeAcceptable(files[i])) { if (this.isFileSizeAcceptable(files[i])) {
okFiles.push(files[i]); okFiles.push(files[i]);
} else { } else {
tooBigFiles.push(files[i]); tooBigFiles.push(files[i]);
@ -406,17 +419,12 @@ export default class ContentMessages {
if (tooBigFiles.length > 0) { if (tooBigFiles.length > 0) {
const UploadFailureDialog = sdk.getComponent("dialogs.UploadFailureDialog"); const UploadFailureDialog = sdk.getComponent("dialogs.UploadFailureDialog");
const uploadFailureDialogPromise = new Promise((resolve) => { const {finished} = Modal.createTrackedDialog('Upload Failure', '', UploadFailureDialog, {
Modal.createTrackedDialog('Upload Failure', '', UploadFailureDialog, { badFiles: tooBigFiles,
badFiles: tooBigFiles, totalFiles: files.length,
totalFiles: files.length, contentMessages: this,
contentMessages: this,
onFinished: (shouldContinue) => {
resolve(shouldContinue);
},
});
}); });
const shouldContinue = await uploadFailureDialogPromise; const [shouldContinue]: [boolean] = await finished;
if (!shouldContinue) return; if (!shouldContinue) return;
} }
@ -428,31 +436,47 @@ export default class ContentMessages {
for (let i = 0; i < okFiles.length; ++i) { for (let i = 0; i < okFiles.length; ++i) {
const file = okFiles[i]; const file = okFiles[i];
if (!uploadAll) { if (!uploadAll) {
const shouldContinue = await new Promise((resolve) => { const {finished} = Modal.createTrackedDialog('Upload Files confirmation', '', UploadConfirmDialog, {
Modal.createTrackedDialog('Upload Files confirmation', '', UploadConfirmDialog, { file,
file, currentIndex: i,
currentIndex: i, totalFiles: okFiles.length,
totalFiles: okFiles.length,
onFinished: (shouldContinue, shouldUploadAll) => {
if (shouldUploadAll) {
uploadAll = true;
}
resolve(shouldContinue);
},
});
}); });
const [shouldContinue, shouldUploadAll]: [boolean, boolean] = await finished;
if (!shouldContinue) break; 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) { getCurrentUploads() {
const content = { 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', body: file.name || 'Attachment',
info: { info: {
size: file.size, size: file.size,
}, },
msgtype: "", // set later
}; };
// if we have a mime type for the file, add it to the message metadata // 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) => { const prom = new Promise((resolve) => {
if (file.type.indexOf('image/') == 0) { if (file.type.indexOf('image/') === 0) {
content.msgtype = 'm.image'; content.msgtype = 'm.image';
infoForImageFile(matrixClient, roomId, file).then((imageInfo)=>{ infoForImageFile(matrixClient, roomId, file).then((imageInfo) => {
extend(content.info, imageInfo); extend(content.info, imageInfo);
resolve(); resolve();
}, (error)=>{ }, (e) => {
console.error(error); console.error(e);
content.msgtype = 'm.file'; content.msgtype = 'm.file';
resolve(); resolve();
}); });
} else if (file.type.indexOf('audio/') == 0) { } else if (file.type.indexOf('audio/') === 0) {
content.msgtype = 'm.audio'; content.msgtype = 'm.audio';
resolve(); resolve();
} else if (file.type.indexOf('video/') == 0) { } else if (file.type.indexOf('video/') === 0) {
content.msgtype = 'm.video'; content.msgtype = 'm.video';
infoForVideoFile(matrixClient, roomId, file).then((videoInfo)=>{ infoForVideoFile(matrixClient, roomId, file).then((videoInfo) => {
extend(content.info, videoInfo); extend(content.info, videoInfo);
resolve(); resolve();
}, (error)=>{ }, (e) => {
content.msgtype = 'm.file'; content.msgtype = 'm.file';
resolve(); 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', fileName: file.name || 'Attachment',
roomId: roomId, roomId: roomId,
total: 0, total: file.size,
loaded: 0, loaded: 0,
promise: prom,
}; };
this.inprogress.push(upload); this.inprogress.push(upload);
dis.dispatch({action: 'upload_started'}); dis.dispatch({action: 'upload_started'});
@ -501,15 +531,15 @@ export default class ContentMessages {
// Focus the composer view // Focus the composer view
dis.dispatch({action: 'focus_composer'}); dis.dispatch({action: 'focus_composer'});
let error;
function onProgress(ev) { function onProgress(ev) {
upload.total = ev.total; upload.total = ev.total;
upload.loaded = ev.loaded; upload.loaded = ev.loaded;
dis.dispatch({action: 'upload_progress', upload: upload}); dis.dispatch({action: 'upload_progress', upload: upload});
} }
let error;
return prom.then(function() { return prom.then(function() {
if (upload.canceled) throw new UploadCanceledError();
// XXX: upload.promise must be the promise that // XXX: upload.promise must be the promise that
// is returned by uploadFile as it has an abort() // is returned by uploadFile as it has an abort()
// method hacked onto it. // method hacked onto it.
@ -520,16 +550,17 @@ export default class ContentMessages {
content.file = result.file; content.file = result.file;
content.url = result.url; content.url = result.url;
}); });
}).then((url) => { }).then(() => {
// Await previous message being sent into the room // Await previous message being sent into the room
return promBefore; return promBefore;
}).then(function() { }).then(function() {
if (upload.canceled) throw new UploadCanceledError();
return matrixClient.sendMessage(roomId, content); return matrixClient.sendMessage(roomId, content);
}, function(err) { }, function(err) {
error = err; error = err;
if (!upload.canceled) { if (!upload.canceled) {
let desc = _t("The file '%(fileName)s' failed to upload.", {fileName: upload.fileName}); 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( desc = _t(
"The file '%(fileName)s' exceeds this homeserver's size limit for uploads", "The file '%(fileName)s' exceeds this homeserver's size limit for uploads",
{fileName: upload.fileName}, {fileName: upload.fileName},
@ -542,11 +573,9 @@ export default class ContentMessages {
}); });
} }
}).finally(() => { }).finally(() => {
const inprogressKeys = Object.keys(this.inprogress);
for (let i = 0; i < this.inprogress.length; ++i) { for (let i = 0; i < this.inprogress.length; ++i) {
const k = inprogressKeys[i]; if (this.inprogress[i].promise === upload.promise) {
if (this.inprogress[k].promise === upload.promise) { this.inprogress.splice(i, 1);
this.inprogress.splice(k, 1);
break; break;
} }
} }
@ -555,7 +584,7 @@ export default class ContentMessages {
// clear the media size limit so we fetch it again next time // clear the media size limit so we fetch it again next time
// we try to upload // we try to upload
if (error && error.http_status === 413) { if (error && error.http_status === 413) {
this._mediaConfig = null; this.mediaConfig = null;
} }
dis.dispatch({action: 'upload_failed', upload, error}); dis.dispatch({action: 'upload_failed', upload, error});
} else { } else {
@ -565,24 +594,35 @@ export default class ContentMessages {
}); });
} }
getCurrentUploads() { private isFileSizeAcceptable(file: File) {
return this.inprogress.filter(u => !u.canceled); if (this.mediaConfig !== null &&
this.mediaConfig["m.upload.size"] !== undefined &&
file.size > this.mediaConfig["m.upload.size"]) {
return false;
}
return true;
} }
cancelUpload(promise) { private ensureMediaConfigFetched() {
const inprogressKeys = Object.keys(this.inprogress); if (this.mediaConfig !== null) return;
let upload;
for (let i = 0; i < this.inprogress.length; ++i) { console.log("[Media Config] Fetching");
const k = inprogressKeys[i]; return MatrixClientPeg.get().getMediaConfig().then((config) => {
if (this.inprogress[k].promise === promise) { console.log("[Media Config] Fetched config:", config);
upload = this.inprogress[k]; return config;
break; }).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.");
if (upload) { return {};
upload.canceled = true; }).then((config) => {
MatrixClientPeg.get().cancelUpload(upload.promise); this.mediaConfig = config;
dis.dispatch({action: 'upload_canceled', upload}); });
}
static sharedInstance() {
if (window.mx_ContentMessages === undefined) {
window.mx_ContentMessages = new ContentMessages();
} }
return window.mx_ContentMessages;
} }
} }

View file

@ -14,43 +14,43 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { MatrixClientPeg } from './MatrixClientPeg'; import {MatrixClientPeg} from './MatrixClientPeg';
import SettingsStore from './settings/SettingsStore'; import SettingsStore from './settings/SettingsStore';
import * as sdk from './index'; import {
import { _t } from './languageHandler'; hideToast as hideBulkUnverifiedSessionsToast,
import ToastStore from './stores/ToastStore'; 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 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 { 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() { static sharedInstance() {
if (!global.mx_DeviceListener) global.mx_DeviceListener = new DeviceListener(); if (!window.mx_DeviceListener) window.mx_DeviceListener = new DeviceListener();
return global.mx_DeviceListener; return window.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();
} }
start() { start() {
@ -74,12 +74,12 @@ export default class DeviceListener {
MatrixClientPeg.get().removeListener('accountData', this._onAccountData); MatrixClientPeg.get().removeListener('accountData', this._onAccountData);
MatrixClientPeg.get().removeListener('sync', this._onSync); MatrixClientPeg.get().removeListener('sync', this._onSync);
} }
this._dismissed.clear(); this.dismissed.clear();
this._dismissedThisDeviceToast = false; this.dismissedThisDeviceToast = false;
this._keyBackupInfo = null; this.keyBackupInfo = null;
this._keyBackupFetchedAt = null; this.keyBackupFetchedAt = null;
this._ourDeviceIdsAtStart = null; this.ourDeviceIdsAtStart = null;
this._displayingToastsForDeviceIds = new Set(); this.displayingToastsForDeviceIds = new Set();
} }
/** /**
@ -87,29 +87,29 @@ export default class DeviceListener {
* *
* @param {String[]} deviceIds List of device IDs to dismiss notifications for * @param {String[]} deviceIds List of device IDs to dismiss notifications for
*/ */
async dismissUnverifiedSessions(deviceIds) { async dismissUnverifiedSessions(deviceIds: Iterable<string>) {
for (const d of deviceIds) { for (const d of deviceIds) {
this._dismissed.add(d); this.dismissed.add(d);
} }
this._recheck(); this._recheck();
} }
dismissEncryptionSetup() { dismissEncryptionSetup() {
this._dismissedThisDeviceToast = true; this.dismissedThisDeviceToast = true;
this._recheck(); this._recheck();
} }
_ensureDeviceIdsAtStartPopulated() { _ensureDeviceIdsAtStartPopulated() {
if (this._ourDeviceIdsAtStart === null) { if (this.ourDeviceIdsAtStart === null) {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
this._ourDeviceIdsAtStart = new Set( this.ourDeviceIdsAtStart = new Set(
cli.getStoredDevicesForUser(cli.getUserId()).map(d => d.deviceId), 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), // 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 // then they are all pre-existing devices, so ignore this and set the
// devicesAtStart list to the devices that we see after the fetch. // 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. // before we download any new ones.
} }
_onDevicesUpdated = (users) => { _onDevicesUpdated = (users: string[]) => {
if (!users.includes(MatrixClientPeg.get().getUserId())) return; if (!users.includes(MatrixClientPeg.get().getUserId())) return;
this._recheck(); this._recheck();
} }
_onDeviceVerificationChanged = (userId) => { _onDeviceVerificationChanged = (userId: string) => {
if (userId !== MatrixClientPeg.get().getUserId()) return; if (userId !== MatrixClientPeg.get().getUserId()) return;
this._recheck(); this._recheck();
} }
_onUserTrustStatusChanged = (userId, trustLevel) => { _onUserTrustStatusChanged = (userId: string) => {
if (userId !== MatrixClientPeg.get().getUserId()) return; if (userId !== MatrixClientPeg.get().getUserId()) return;
this._recheck(); this._recheck();
} }
@ -163,11 +163,11 @@ export default class DeviceListener {
// & cache the result // & cache the result
async _getKeyBackupInfo() { async _getKeyBackupInfo() {
const now = (new Date()).getTime(); const now = (new Date()).getTime();
if (!this._keyBackupInfo || this._keyBackupFetchedAt < now - KEY_BACKUP_POLL_INTERVAL) { if (!this.keyBackupInfo || this.keyBackupFetchedAt < now - KEY_BACKUP_POLL_INTERVAL) {
this._keyBackupInfo = await MatrixClientPeg.get().getKeyBackupVersion(); this.keyBackupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
this._keyBackupFetchedAt = now; this.keyBackupFetchedAt = now;
} }
return this._keyBackupInfo; return this.keyBackupInfo;
} }
async _recheck() { async _recheck() {
@ -186,48 +186,25 @@ export default class DeviceListener {
const crossSigningReady = await cli.isCrossSigningReady(); const crossSigningReady = await cli.isCrossSigningReady();
if (this._dismissedThisDeviceToast) { if (this.dismissedThisDeviceToast || crossSigningReady) {
ToastStore.sharedInstance().dismissToast(THIS_DEVICE_TOAST_KEY); hideSetupEncryptionToast();
} else { } else {
if (!crossSigningReady) { // make sure our keys are finished downloading
// make sure our keys are finished downlaoding await cli.downloadKeys([cli.getUserId()]);
await cli.downloadKeys([cli.getUserId()]); // cross signing isn't enabled - nag to enable it
// cross signing isn't enabled - nag to enable it // There are 3 different toasts for:
// There are 3 different toasts for: if (cli.getStoredCrossSigningForUser(cli.getUserId())) {
if (cli.getStoredCrossSigningForUser(cli.getUserId())) { // Cross-signing on account but this device doesn't trust the master key (verify this session)
// Cross-signing on account but this device doesn't trust the master key (verify this session) showSetupEncryptionToast(SetupKind.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"),
});
}
}
} else { } else {
// cross-signing is ready, and we don't need to upgrade encryption const backupInfo = await this._getKeyBackupInfo();
ToastStore.sharedInstance().dismissToast(THIS_DEVICE_TOAST_KEY); 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 // (technically could just be a boolean: we don't actually
// need to remember the device IDs, but for the sake of // need to remember the device IDs, but for the sake of
// symmetry...). // symmetry...).
const oldUnverifiedDeviceIds = new Set(); const oldUnverifiedDeviceIds = new Set<string>();
// Unverified devices that have appeared since then // Unverified devices that have appeared since then
const newUnverifiedDeviceIds = new Set(); const newUnverifiedDeviceIds = new Set<string>();
// as long as cross-signing isn't ready, // as long as cross-signing isn't ready,
// you can't see or dismiss any device toasts // you can't see or dismiss any device toasts
if (crossSigningReady) { if (crossSigningReady) {
const devices = cli.getStoredDevicesForUser(cli.getUserId()); const devices = cli.getStoredDevicesForUser(cli.getUserId());
for (const device of devices) { 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); const deviceTrust = await cli.checkDeviceTrust(cli.getUserId(), device.deviceId);
if (!deviceTrust.isCrossSigningVerified() && !this._dismissed.has(device.deviceId)) { if (!deviceTrust.isCrossSigningVerified() && !this.dismissed.has(device.deviceId)) {
if (this._ourDeviceIdsAtStart.has(device.deviceId)) { if (this.ourDeviceIdsAtStart.has(device.deviceId)) {
oldUnverifiedDeviceIds.add(device.deviceId); oldUnverifiedDeviceIds.add(device.deviceId);
} else { } else {
newUnverifiedDeviceIds.add(device.deviceId); newUnverifiedDeviceIds.add(device.deviceId);
@ -263,38 +240,23 @@ export default class DeviceListener {
// Display or hide the batch toast for old unverified sessions // Display or hide the batch toast for old unverified sessions
if (oldUnverifiedDeviceIds.size > 0) { if (oldUnverifiedDeviceIds.size > 0) {
ToastStore.sharedInstance().addOrReplaceToast({ showBulkUnverifiedSessionsToast(oldUnverifiedDeviceIds);
key: OTHER_DEVICES_TOAST_KEY,
title: _t("Review where youre logged in"),
icon: "verification_warning",
priority: ToastStore.PRIORITY_LOW,
props: {
deviceIds: oldUnverifiedDeviceIds,
},
component: sdk.getComponent("toasts.BulkUnverifiedSessionsToast"),
});
} else { } else {
ToastStore.sharedInstance().dismissToast(OTHER_DEVICES_TOAST_KEY); hideBulkUnverifiedSessionsToast();
} }
// Show toasts for new unverified devices if they aren't already there // Show toasts for new unverified devices if they aren't already there
for (const deviceId of newUnverifiedDeviceIds) { for (const deviceId of newUnverifiedDeviceIds) {
ToastStore.sharedInstance().addOrReplaceToast({ showUnverifiedSessionsToast(deviceId);
key: toastKey(deviceId),
title: _t("New login. Was this you?"),
icon: "verification_warning",
props: { deviceId },
component: sdk.getComponent("toasts.UnverifiedSessionToast"),
});
} }
// ...and hide any we don't need any more // ...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)) { if (!newUnverifiedDeviceIds.has(deviceId)) {
ToastStore.sharedInstance().dismissToast(toastKey(deviceId)); hideUnverifiedSessionsToast(deviceId);
} }
} }
this._displayingToastsForDeviceIds = newUnverifiedDeviceIds; this.displayingToastsForDeviceIds = newUnverifiedDeviceIds;
} }
} }

View file

@ -17,7 +17,7 @@ limitations under the License.
*/ */
import URL from 'url'; import URL from 'url';
import dis from './dispatcher'; import dis from './dispatcher/dispatcher';
import WidgetMessagingEndpoint from './WidgetMessagingEndpoint'; import WidgetMessagingEndpoint from './WidgetMessagingEndpoint';
import ActiveWidgetStore from './stores/ActiveWidgetStore'; import ActiveWidgetStore from './stores/ActiveWidgetStore';
import {MatrixClientPeg} from "./MatrixClientPeg"; import {MatrixClientPeg} from "./MatrixClientPeg";

View file

@ -22,6 +22,7 @@ import { _t } from './languageHandler';
import {MatrixClientPeg} from './MatrixClientPeg'; import {MatrixClientPeg} from './MatrixClientPeg';
import GroupStore from './stores/GroupStore'; import GroupStore from './stores/GroupStore';
import {allSettled} from "./utils/promise"; import {allSettled} from "./utils/promise";
import StyledCheckbox from './components/views/elements/StyledCheckbox';
export function showGroupInviteDialog(groupId) { export function showGroupInviteDialog(groupId) {
return new Promise((resolve, reject) => { 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>{ _t("Which rooms would you like to add to this community?") }</div>
</div>; </div>;
const checkboxContainer = <label className="mx_GroupAddressPicker_checkboxContainer"> const checkboxContainer = <StyledCheckbox
<input type="checkbox" onChange={onCheckboxClicked} /> className="mx_GroupAddressPicker_checkboxContainer"
<div> onChange={onCheckboxClicked}
{ _t("Show these rooms to non-members on the community page and room list?") } >
</div> { _t("Show these rooms to non-members on the community page and room list?") }
</label>; </StyledCheckbox>;
const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog"); const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog");
Modal.createTrackedDialog('Add Rooms to Group', '', AddressPickerDialog, { Modal.createTrackedDialog('Add Rooms to Group', '', AddressPickerDialog, {
title: _t("Add rooms to the community"), title: _t("Add rooms to the community"),
description: description, description: description,
extraNode: checkboxContainer, extraNode: checkboxContainer,
placeholder: _t("Room name or alias"), placeholder: _t("Room name or address"),
button: _t("Add to community"), button: _t("Add to community"),
pickerType: 'room', pickerType: 'room',
validAddressTypes: ['mx-room-id'], validAddressTypes: ['mx-room-id'],

View file

@ -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;
}
}

View file

@ -26,7 +26,7 @@ import Analytics from './Analytics';
import Notifier from './Notifier'; import Notifier from './Notifier';
import UserActivity from './UserActivity'; import UserActivity from './UserActivity';
import Presence from './Presence'; import Presence from './Presence';
import dis from './dispatcher'; import dis from './dispatcher/dispatcher';
import DMRoomMap from './utils/DMRoomMap'; import DMRoomMap from './utils/DMRoomMap';
import Modal from './Modal'; import Modal from './Modal';
import * as sdk from './index'; import * as sdk from './index';
@ -298,6 +298,8 @@ async function _restoreFromLocalStorage(opts) {
return false; return false;
} }
const pickleKey = await PlatformPeg.get().getPickleKey(userId, deviceId);
console.log(`Restoring session for ${userId}`); console.log(`Restoring session for ${userId}`);
await _doSetLoggedIn({ await _doSetLoggedIn({
userId: userId, userId: userId,
@ -306,6 +308,7 @@ async function _restoreFromLocalStorage(opts) {
homeserverUrl: hsUrl, homeserverUrl: hsUrl,
identityServerUrl: isUrl, identityServerUrl: isUrl,
guest: isGuest, guest: isGuest,
pickleKey: pickleKey,
}, false); }, false);
return true; return true;
} else { } else {
@ -348,9 +351,13 @@ async function _handleLoadSessionFailure(e) {
* *
* @returns {Promise} promise which resolves to the new MatrixClient once it has been started * @returns {Promise} promise which resolves to the new MatrixClient once it has been started
*/ */
export function setLoggedIn(credentials) { export async function setLoggedIn(credentials) {
stopMatrixClient(); 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; _isLoggingOut = true;
MatrixClientPeg.get().logout().then(onLoggedOut, const client = MatrixClientPeg.get();
PlatformPeg.get().destroyPickleKey(client.getUserId(), client.getDeviceId());
client.logout().then(onLoggedOut,
(err) => { (err) => {
// Just throwing an error here is going to be very unhelpful // Just throwing an error here is going to be very unhelpful
// if you're trying to log out because your server's down and // if you're trying to log out because your server's down and
@ -575,10 +584,12 @@ async function startMatrixClient(startSyncing=true) {
// to work). // to work).
dis.dispatch({action: 'will_start_client'}, true); dis.dispatch({action: 'will_start_client'}, true);
// reset things first just in case
TypingStore.sharedInstance().reset();
ToastStore.sharedInstance().reset();
Notifier.start(); Notifier.start();
UserActivity.sharedInstance().start(); UserActivity.sharedInstance().start();
TypingStore.sharedInstance().reset(); // just in case
ToastStore.sharedInstance().reset();
DMRoomMap.makeShared().start(); DMRoomMap.makeShared().start();
IntegrationManagers.sharedInstance().startWatching(); IntegrationManagers.sharedInstance().startWatching();
ActiveWidgetStore.start(); ActiveWidgetStore.start();

View file

@ -17,8 +17,8 @@ See the License for the specific language governing permissions and
limitations under the License. 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 * as utils from 'matrix-js-sdk/src/utils';
import {EventTimeline} from 'matrix-js-sdk/src/models/event-timeline'; import {EventTimeline} from 'matrix-js-sdk/src/models/event-timeline';
import {EventTimelineSet} from 'matrix-js-sdk/src/models/event-timeline-set'; import {EventTimelineSet} from 'matrix-js-sdk/src/models/event-timeline-set';
@ -34,37 +34,25 @@ import IdentityAuthClient from './IdentityAuthClient';
import { crossSigningCallbacks } from './CrossSigningManager'; import { crossSigningCallbacks } from './CrossSigningManager';
import {SHOW_QR_CODE_METHOD} from "matrix-js-sdk/src/crypto/verification/QRCode"; import {SHOW_QR_CODE_METHOD} from "matrix-js-sdk/src/crypto/verification/QRCode";
interface MatrixClientCreds { export interface IMatrixClientCreds {
homeserverUrl: string, homeserverUrl: string,
identityServerUrl: string, identityServerUrl: string,
userId: string, userId: string,
deviceId: string, deviceId: string,
accessToken: string, accessToken: string,
guest: boolean, guest: boolean,
pickleKey?: string,
} }
/** // TODO: Move this to the js-sdk
* Wrapper object for handling the js-sdk Matrix Client object in the react-sdk export interface IOpts {
* Handles the creation/initialisation of client objects. initialSyncLimit?: number;
* This module provides a singleton instance of this class so the 'current' pendingEventOrdering?: "detached" | "chronological";
* Matrix Client object is available easily. lazyLoadMembers?: boolean;
*/ }
class _MatrixClientPeg {
constructor() {
this.matrixClient = null;
this._justRegisteredUserId = null;
// These are the default options used when when the export interface IMatrixClientPeg {
// client is started in 'start'. These can be altered opts: IOpts;
// 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;
}
/** /**
* Sets the script href passed to the IndexedDB web worker * 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 * @param {string} script href to the script to be passed to the web worker
*/ */
setIndexedDbWorkerScript(script) { setIndexedDbWorkerScript(script: string): void;
createMatrixClient.indexedDbWorkerScript = script;
}
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() { get(): MatrixClient;
this.matrixClient = null; 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 * 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 * @param {string} uid The user ID of the user we've just registered
*/ */
setJustRegisteredUserId(uid) { setJustRegisteredUserId(uid: string): void;
this._justRegisteredUserId = uid;
}
/** /**
* Returns true if the current user has just been registered by this * 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 * @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 ( return (
this.matrixClient && this.matrixClient &&
this.matrixClient.credentials.userId === this._justRegisteredUserId this.matrixClient.credentials.userId === this.justRegisteredUserId
); );
} }
/* public replaceUsingCreds(creds: IMatrixClientCreds): void {
* Replace this MatrixClientPeg's client with a client instance that has this.currentClientCreds = creds;
* homeserver / identity server URLs and active credentials this.createClient(creds);
*/
replaceUsingCreds(creds: MatrixClientCreds) {
this._currentClientCreds = creds;
this._createClient(creds);
} }
async assign() { public async assign(): Promise<any> {
for (const dbType of ['indexeddb', 'memory']) { for (const dbType of ['indexeddb', 'memory']) {
try { try {
const promise = this.matrixClient.store.startup(); const promise = this.matrixClient.store.startup();
@ -132,7 +172,7 @@ class _MatrixClientPeg {
if (dbType === 'indexeddb') { if (dbType === 'indexeddb') {
console.error('Error starting matrixclient store - falling back to memory store', err); console.error('Error starting matrixclient store - falling back to memory store', err);
this.matrixClient.store = new MemoryStore({ this.matrixClient.store = new MemoryStore({
localStorage: global.localStorage, localStorage: localStorage,
}); });
} else { } else {
console.error('Failed to start memory store!', err); 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 // The js-sdk found a crypto DB too new for it to use
const CryptoStoreTooNewDialog = const CryptoStoreTooNewDialog =
sdk.getComponent("views.dialogs.CryptoStoreTooNewDialog"); sdk.getComponent("views.dialogs.CryptoStoreTooNewDialog");
Modal.createDialog(CryptoStoreTooNewDialog, { Modal.createDialog(CryptoStoreTooNewDialog);
host: window.location.host,
});
} }
// this can happen for a number of reasons, the most likely being // this can happen for a number of reasons, the most likely being
// that the olm library was missing. It's not fatal. // that the olm library was missing. It's not fatal.
@ -179,7 +217,7 @@ class _MatrixClientPeg {
return opts; return opts;
} }
async start() { public async start(): Promise<any> {
const opts = await this.assign(); const opts = await this.assign();
console.log(`MatrixClientPeg: really starting MatrixClient`); console.log(`MatrixClientPeg: really starting MatrixClient`);
@ -187,7 +225,7 @@ class _MatrixClientPeg {
console.log(`MatrixClientPeg: MatrixClient started`); console.log(`MatrixClientPeg: MatrixClient started`);
} }
getCredentials(): MatrixClientCreds { public getCredentials(): IMatrixClientCreds {
return { return {
homeserverUrl: this.matrixClient.baseUrl, homeserverUrl: this.matrixClient.baseUrl,
identityServerUrl: this.matrixClient.idBaseUrl, identityServerUrl: this.matrixClient.idBaseUrl,
@ -198,12 +236,7 @@ class _MatrixClientPeg {
}; };
} }
/* public getHomeserverName(): string {
* 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() {
const matches = /^@.+:(.+)$/.exec(this.matrixClient.credentials.userId); const matches = /^@.+:(.+)$/.exec(this.matrixClient.credentials.userId);
if (matches === null || matches.length < 1) { if (matches === null || matches.length < 1) {
throw new Error("Failed to derive homeserver name from user ID!"); throw new Error("Failed to derive homeserver name from user ID!");
@ -211,13 +244,15 @@ class _MatrixClientPeg {
return matches[1]; return matches[1];
} }
_createClient(creds: MatrixClientCreds) { private createClient(creds: IMatrixClientCreds): void {
// TODO: Make these opts typesafe with the js-sdk
const opts = { const opts = {
baseUrl: creds.homeserverUrl, baseUrl: creds.homeserverUrl,
idBaseUrl: creds.identityServerUrl, idBaseUrl: creds.identityServerUrl,
accessToken: creds.accessToken, accessToken: creds.accessToken,
userId: creds.userId, userId: creds.userId,
deviceId: creds.deviceId, deviceId: creds.deviceId,
pickleKey: creds.pickleKey,
timelineSupport: true, timelineSupport: true,
forceTURN: !SettingsStore.getValue('webRtcAllowPeerToPeer', false), forceTURN: !SettingsStore.getValue('webRtcAllowPeerToPeer', false),
fallbackICEServerAllowed: !!SettingsStore.getValue('fallbackICEServerAllowed'), fallbackICEServerAllowed: !!SettingsStore.getValue('fallbackICEServerAllowed'),
@ -228,9 +263,9 @@ class _MatrixClientPeg {
], ],
unstableClientRelationAggregation: true, unstableClientRelationAggregation: true,
identityServer: new IdentityAuthClient(), identityServer: new IdentityAuthClient(),
cryptoCallbacks: {},
}; };
opts.cryptoCallbacks = {};
// These are always installed regardless of the labs flag so that // These are always installed regardless of the labs flag so that
// cross-signing features can toggle on without reloading and also be // cross-signing features can toggle on without reloading and also be
// accessed immediately after login. // accessed immediately after login.
@ -253,8 +288,8 @@ class _MatrixClientPeg {
} }
} }
if (!global.mxMatrixClientPeg) { if (!window.mxMatrixClientPeg) {
global.mxMatrixClientPeg = new _MatrixClientPeg(); window.mxMatrixClientPeg = new _MatrixClientPeg();
} }
export const MatrixClientPeg = global.mxMatrixClientPeg; export const MatrixClientPeg = window.mxMatrixClientPeg;

View file

@ -18,7 +18,7 @@ limitations under the License.
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import Analytics from './Analytics'; import Analytics from './Analytics';
import dis from './dispatcher'; import dis from './dispatcher/dispatcher';
import {defer} from './utils/promise'; import {defer} from './utils/promise';
import AsyncWrapper from './AsyncWrapper'; import AsyncWrapper from './AsyncWrapper';

View file

@ -21,11 +21,14 @@ import PlatformPeg from './PlatformPeg';
import * as TextForEvent from './TextForEvent'; import * as TextForEvent from './TextForEvent';
import Analytics from './Analytics'; import Analytics from './Analytics';
import * as Avatar from './Avatar'; import * as Avatar from './Avatar';
import dis from './dispatcher'; import dis from './dispatcher/dispatcher';
import * as sdk from './index'; import * as sdk from './index';
import { _t } from './languageHandler'; import { _t } from './languageHandler';
import Modal from './Modal'; import Modal from './Modal';
import SettingsStore, {SettingLevel} from "./settings/SettingsStore"; import SettingsStore, {SettingLevel} from "./settings/SettingsStore";
import {
hideToast as hideNotificationsToast,
} from "./toasts/DesktopNotificationsToast";
/* /*
* Dispatches: * Dispatches:
@ -278,12 +281,7 @@ const Notifier = {
Analytics.trackEvent('Notifier', 'Set Toolbar Hidden', hidden); Analytics.trackEvent('Notifier', 'Set Toolbar Hidden', hidden);
// XXX: why are we dispatching this here? hideNotificationsToast();
// this is nothing to do with notifier_enabled
dis.dispatch({
action: "notifier_enabled",
value: this.isEnabled(),
});
// update the info to localStorage for persistent settings // update the info to localStorage for persistent settings
if (persistent && global.localStorage) { if (persistent && global.localStorage) {

View file

@ -84,8 +84,14 @@ export default class PasswordReset {
try { try {
await this.client.setPassword({ 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", 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, threepid_creds: creds,
threepidCreds: creds,
}, this.password); }, this.password);
} catch (err) { } catch (err) {
if (err.httpStatus === 401) { if (err.httpStatus === 401) {

View file

@ -17,7 +17,7 @@ limitations under the License.
*/ */
import {MatrixClientPeg} from "./MatrixClientPeg"; import {MatrixClientPeg} from "./MatrixClientPeg";
import dis from "./dispatcher"; import dis from "./dispatcher/dispatcher";
import Timer from './utils/Timer'; import Timer from './utils/Timer';
// Time in ms after that a user is considered as unavailable/away // Time in ms after that a user is considered as unavailable/away

View file

@ -20,7 +20,7 @@ limitations under the License.
* registration code. * registration code.
*/ */
import dis from './dispatcher'; import dis from './dispatcher/dispatcher';
import * as sdk from './index'; import * as sdk from './index';
import Modal from './Modal'; import Modal from './Modal';
import { _t } from './languageHandler'; import { _t } from './languageHandler';

View file

@ -16,7 +16,7 @@ limitations under the License.
*/ */
import {MatrixClientPeg} from './MatrixClientPeg'; import {MatrixClientPeg} from './MatrixClientPeg';
import dis from './dispatcher'; import dis from './dispatcher/dispatcher';
import { EventStatus } from 'matrix-js-sdk'; import { EventStatus } from 'matrix-js-sdk';
export default class Resend { export default class Resend {

View file

@ -238,7 +238,7 @@ Example:
import {MatrixClientPeg} from './MatrixClientPeg'; import {MatrixClientPeg} from './MatrixClientPeg';
import { MatrixEvent } from 'matrix-js-sdk'; import { MatrixEvent } from 'matrix-js-sdk';
import dis from './dispatcher'; import dis from './dispatcher/dispatcher';
import WidgetUtils from './utils/WidgetUtils'; import WidgetUtils from './utils/WidgetUtils';
import RoomViewStore from './stores/RoomViewStore'; import RoomViewStore from './stores/RoomViewStore';
import { _t } from './languageHandler'; import { _t } from './languageHandler';

View file

@ -21,7 +21,7 @@ limitations under the License.
import * as React from 'react'; import * as React from 'react';
import {MatrixClientPeg} from './MatrixClientPeg'; import {MatrixClientPeg} from './MatrixClientPeg';
import dis from './dispatcher'; import dis from './dispatcher/dispatcher';
import * as sdk from './index'; import * as sdk from './index';
import {_t, _td} from './languageHandler'; import {_t, _td} from './languageHandler';
import Modal from './Modal'; import Modal from './Modal';
@ -41,6 +41,8 @@ import { parseFragment as parseHtml } from "parse5";
import sendBugReport from "./rageshake/submit-rageshake"; import sendBugReport from "./rageshake/submit-rageshake";
import SdkConfig from "./SdkConfig"; import SdkConfig from "./SdkConfig";
import { ensureDMExists } from "./createRoom"; 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 // XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816
interface HTMLInputEvent extends Event { interface HTMLInputEvent extends Event {
@ -448,8 +450,8 @@ export const Commands = [
new Command({ new Command({
command: 'join', command: 'join',
aliases: ['j', 'goto'], aliases: ['j', 'goto'],
args: '<room-alias>', args: '<room-address>',
description: _td('Joins room with given alias'), description: _td('Joins room with given address'),
runFn: function(_, args) { runFn: function(_, args) {
if (args) { if (args) {
// Note: we support 2 versions of this command. The first is // Note: we support 2 versions of this command. The first is
@ -560,7 +562,7 @@ export const Commands = [
}), }),
new Command({ new Command({
command: 'part', command: 'part',
args: '[<room-alias>]', args: '[<room-address>]',
description: _td('Leave room'), description: _td('Leave room'),
runFn: function(roomId, args) { runFn: function(roomId, args) {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
@ -592,7 +594,7 @@ export const Commands = [
} }
if (targetRoomId) break; 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); const member = MatrixClientPeg.get().getRoom(roomId).getMember(userId);
dis.dispatch({ dis.dispatch<ViewUserPayload>({
action: 'view_user', action: Action.ViewUser,
// XXX: We should be using a real member object and not assuming what the
// receiver wants.
member: member || {userId}, member: member || {userId},
}); });
return success(); return success();

View file

@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import dis from './dispatcher'; import dis from './dispatcher/dispatcher';
import Timer from './utils/Timer'; import Timer from './utils/Timer';
// important these are larger than the timeouts of timers // important these are larger than the timeouts of timers

View file

@ -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;

View 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);
}
}

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import dis from '../dispatcher'; import dis from '../dispatcher/dispatcher';
// TODO: migrate from sync_state to MatrixActions.sync so that more js-sdk events // TODO: migrate from sync_state to MatrixActions.sync so that more js-sdk events
// become dispatches in the same place. // become dispatches in the same place.

View file

@ -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;

View 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,
};
});
}
}

View file

@ -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;

View 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};
});
}
}

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2017 New Vector Ltd Copyright 2017 New Vector Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -14,6 +15,8 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { AsyncActionPayload } from "../dispatcher/payloads";
/** /**
* Create an action thunk that will dispatch actions indicating the current * Create an action thunk that will dispatch actions indicating the current
* status of the Promise returned by fn. * 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 * @param {function?} pendingFn a function that returns an object to assign
* to the `request` key of the ${id}.pending * to the `request` key of the ${id}.pending
* payload. * payload.
* @returns {function} an action thunk - a function that uses its single * @returns {AsyncActionPayload} an async action payload. Includes a function
* argument as a dispatch function to dispatch the * that uses its single argument as a dispatch function
* following actions: * to dispatch the following actions:
* `${id}.pending` and either * `${id}.pending` and either
* `${id}.success` or * `${id}.success` or
* `${id}.failure`. * `${id}.failure`.
@ -41,12 +44,11 @@ limitations under the License.
* result is the result of the promise returned by * result is the result of the promise returned by
* `fn`. * `fn`.
*/ */
export function asyncAction(id, fn, pendingFn) { export function asyncAction(id: string, fn: () => Promise<any>, pendingFn: () => any | null): AsyncActionPayload {
return (dispatch) => { const helper = (dispatch) => {
dispatch({ dispatch({
action: id + '.pending', action: id + '.pending',
request: request: typeof pendingFn === 'function' ? pendingFn() : undefined,
typeof pendingFn === 'function' ? pendingFn() : undefined,
}); });
fn().then((result) => { fn().then((result) => {
dispatch({action: id + '.success', result}); dispatch({action: id + '.success', result});
@ -54,4 +56,5 @@ export function asyncAction(id, fn, pendingFn) {
dispatch({action: id + '.failure', err}); dispatch({action: id + '.failure', err});
}); });
}; };
return new AsyncActionPayload(helper);
} }

View file

@ -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>
);
},
});

View file

@ -17,11 +17,12 @@ limitations under the License.
import React from 'react'; import React from 'react';
import * as sdk from '../../../../index'; import * as sdk from '../../../../index';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import dis from "../../../../dispatcher"; import dis from "../../../../dispatcher/dispatcher";
import { _t } from '../../../../languageHandler'; import { _t } from '../../../../languageHandler';
import SettingsStore, {SettingLevel} from "../../../../settings/SettingsStore"; import SettingsStore, {SettingLevel} from "../../../../settings/SettingsStore";
import EventIndexPeg from "../../../../indexing/EventIndexPeg"; import EventIndexPeg from "../../../../indexing/EventIndexPeg";
import {Action} from "../../../../dispatcher/actions";
/* /*
* Allows the user to disable the Event Index. * 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 SettingsStore.setValue('enableEventIndexing', null, SettingLevel.DEVICE, false);
await EventIndexPeg.deleteEventIndex(); await EventIndexPeg.deleteEventIndex();
this.props.onFinished(); this.props.onFinished();
dis.dispatch({ action: 'view_user_settings' }); dis.fire(Action.ViewUserSettings);
} }
render() { render() {

View file

@ -284,8 +284,10 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
let matchText; let matchText;
let changeText;
if (this.state.passPhraseConfirm === this.state.passPhrase) { if (this.state.passPhraseConfirm === this.state.passPhrase) {
matchText = _t("That matches!"); matchText = _t("That matches!");
changeText = _t("Use a different passphrase?");
} else if (!this.state.passPhrase.startsWith(this.state.passPhraseConfirm)) { } else if (!this.state.passPhrase.startsWith(this.state.passPhraseConfirm)) {
// only tell them they're wrong if they've actually gone wrong. // only tell them they're wrong if they've actually gone wrong.
// Security concious readers will note that if you left riot-web unattended // 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 // Note that not having typed anything at all will not hit this clause and
// fall through so empty box === no hint. // fall through so empty box === no hint.
matchText = _t("That doesn't match."); matchText = _t("That doesn't match.");
changeText = _t("Go back to set it again.");
} }
let passPhraseMatch = null; let passPhraseMatch = null;
@ -303,7 +306,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
<div>{matchText}</div> <div>{matchText}</div>
<div> <div>
<AccessibleButton element="span" className="mx_linkButton" onClick={this._onSetAgainClick}> <AccessibleButton element="span" className="mx_linkButton" onClick={this._onSetAgainClick}>
{_t("Go back to set it again.")} {changeText}
</AccessibleButton> </AccessibleButton>
</div> </div>
</div>; </div>;

View file

@ -19,9 +19,10 @@ import React from "react";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import * as sdk from "../../../../index"; import * as sdk from "../../../../index";
import {MatrixClientPeg} from '../../../../MatrixClientPeg'; import {MatrixClientPeg} from '../../../../MatrixClientPeg';
import dis from "../../../../dispatcher"; import dis from "../../../../dispatcher/dispatcher";
import { _t } from "../../../../languageHandler"; import { _t } from "../../../../languageHandler";
import Modal from "../../../../Modal"; import Modal from "../../../../Modal";
import {Action} from "../../../../dispatcher/actions";
export default class NewRecoveryMethodDialog extends React.PureComponent { export default class NewRecoveryMethodDialog extends React.PureComponent {
static propTypes = { static propTypes = {
@ -36,7 +37,7 @@ export default class NewRecoveryMethodDialog extends React.PureComponent {
onGoToSettingsClick = () => { onGoToSettingsClick = () => {
this.props.onFinished(); this.props.onFinished();
dis.dispatch({ action: 'view_user_settings' }); dis.fire(Action.ViewUserSettings);
} }
onSetupClick = async () => { onSetupClick = async () => {

View file

@ -18,9 +18,10 @@ limitations under the License.
import React from "react"; import React from "react";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import * as sdk from "../../../../index"; import * as sdk from "../../../../index";
import dis from "../../../../dispatcher"; import dis from "../../../../dispatcher/dispatcher";
import { _t } from "../../../../languageHandler"; import { _t } from "../../../../languageHandler";
import Modal from "../../../../Modal"; import Modal from "../../../../Modal";
import {Action} from "../../../../dispatcher/actions";
export default class RecoveryMethodRemovedDialog extends React.PureComponent { export default class RecoveryMethodRemovedDialog extends React.PureComponent {
static propTypes = { static propTypes = {
@ -29,7 +30,7 @@ export default class RecoveryMethodRemovedDialog extends React.PureComponent {
onGoToSettingsClick = () => { onGoToSettingsClick = () => {
this.props.onFinished(); this.props.onFinished();
dis.dispatch({ action: 'view_user_settings' }); dis.fire(Action.ViewUserSettings);
} }
onSetupClick = () => { onSetupClick = () => {

View file

@ -201,7 +201,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
type: 'm.id.user', type: 'm.id.user',
user: MatrixClientPeg.get().getUserId(), 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(), user: MatrixClientPeg.get().getUserId(),
password: this.state.accountPassword, password: this.state.accountPassword,
}); });
@ -538,8 +539,10 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
const Field = sdk.getComponent('views.elements.Field'); const Field = sdk.getComponent('views.elements.Field');
let matchText; let matchText;
let changeText;
if (this.state.passPhraseConfirm === this.state.passPhrase) { if (this.state.passPhraseConfirm === this.state.passPhrase) {
matchText = _t("That matches!"); matchText = _t("That matches!");
changeText = _t("Use a different passphrase?");
} else if (!this.state.passPhrase.startsWith(this.state.passPhraseConfirm)) { } else if (!this.state.passPhrase.startsWith(this.state.passPhraseConfirm)) {
// only tell them they're wrong if they've actually gone wrong. // only tell them they're wrong if they've actually gone wrong.
// Security concious readers will note that if you left riot-web unattended // 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 // Note that not having typed anything at all will not hit this clause and
// fall through so empty box === no hint. // fall through so empty box === no hint.
matchText = _t("That doesn't match."); matchText = _t("That doesn't match.");
changeText = _t("Go back to set it again.");
} }
let passPhraseMatch = null; let passPhraseMatch = null;
@ -557,7 +561,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
<div>{matchText}</div> <div>{matchText}</div>
<div> <div>
<AccessibleButton element="span" className="mx_linkButton" onClick={this._onSetAgainClick}> <AccessibleButton element="span" className="mx_linkButton" onClick={this._onSetAgainClick}>
{_t("Go back to set it again.")} {changeText}
</AccessibleButton> </AccessibleButton>
</div> </div>
</div>; </div>;

View file

@ -69,7 +69,7 @@ export default class EmojiProvider extends AutocompleteProvider {
constructor() { constructor() {
super(EMOJI_REGEX); super(EMOJI_REGEX);
this.matcher = new QueryMatcher(EMOJI_SHORTNAMES, { this.matcher = new QueryMatcher<IEmojiShort>(EMOJI_SHORTNAMES, {
keys: ['emoji.emoticon', 'shortname'], keys: ['emoji.emoticon', 'shortname'],
funcs: [ funcs: [
(o) => o.emoji.shortcodes.length > 1 ? o.emoji.shortcodes.slice(1).map(s => `:${s}:`).join(" ") : "", // aliases (o) => o.emoji.shortcodes.length > 1 ? o.emoji.shortcodes.slice(1).map(s => `:${s}:`).join(" ") : "", // aliases

View file

@ -45,7 +45,7 @@ interface IOptions<T extends {}> {
* @param {function[]} options.funcs List of functions that when called with the * @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 * 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 _options: IOptions<T>;
private _keys: IOptions<T>["keys"]; private _keys: IOptions<T>["keys"];
private _funcs: Required<IOptions<T>["funcs"]>; private _funcs: Required<IOptions<T>["funcs"]>;
@ -75,7 +75,11 @@ export default class QueryMatcher<T> {
this._items = new Map(); this._items = new Map();
for (const object of objects) { 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) { for (const f of this._funcs) {
keyValues.push(f(object)); keyValues.push(f(object));

View file

@ -18,7 +18,7 @@ import React from 'react';
import CustomRoomTagStore from '../../stores/CustomRoomTagStore'; import CustomRoomTagStore from '../../stores/CustomRoomTagStore';
import AutoHideScrollbar from './AutoHideScrollbar'; import AutoHideScrollbar from './AutoHideScrollbar';
import * as sdk from '../../index'; import * as sdk from '../../index';
import dis from '../../dispatcher'; import dis from '../../dispatcher/dispatcher';
import classNames from 'classnames'; import classNames from 'classnames';
import * as FormattingUtils from '../../utils/FormattingUtils'; import * as FormattingUtils from '../../utils/FormattingUtils';

View file

@ -23,7 +23,7 @@ import PropTypes from 'prop-types';
import request from 'browser-request'; import request from 'browser-request';
import { _t } from '../../languageHandler'; import { _t } from '../../languageHandler';
import sanitizeHtml from 'sanitize-html'; import sanitizeHtml from 'sanitize-html';
import dis from '../../dispatcher'; import dis from '../../dispatcher/dispatcher';
import {MatrixClientPeg} from '../../MatrixClientPeg'; import {MatrixClientPeg} from '../../MatrixClientPeg';
import classnames from 'classnames'; import classnames from 'classnames';
import MatrixClientContext from "../../contexts/MatrixClientContext"; import MatrixClientContext from "../../contexts/MatrixClientContext";

View file

@ -21,7 +21,7 @@ import createReactClass from 'create-react-class';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import {MatrixClientPeg} from '../../MatrixClientPeg'; import {MatrixClientPeg} from '../../MatrixClientPeg';
import * as sdk from '../../index'; import * as sdk from '../../index';
import dis from '../../dispatcher'; import dis from '../../dispatcher/dispatcher';
import { getHostingLink } from '../../utils/HostingLink'; import { getHostingLink } from '../../utils/HostingLink';
import { sanitizedHtmlNode } from '../../HtmlUtils'; import { sanitizedHtmlNode } from '../../HtmlUtils';
import { _t, _td } from '../../languageHandler'; import { _t, _td } from '../../languageHandler';
@ -92,7 +92,7 @@ const CategoryRoomList = createReactClass({
Modal.createTrackedDialog('Add Rooms to Group Summary', '', AddressPickerDialog, { Modal.createTrackedDialog('Add Rooms to Group Summary', '', AddressPickerDialog, {
title: _t('Add rooms to the community summary'), title: _t('Add rooms to the community summary'),
description: _t("Which rooms would you like to add to this 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"), button: _t("Add to summary"),
pickerType: 'room', pickerType: 'room',
validAddressTypes: ['mx-room-id'], validAddressTypes: ['mx-room-id'],

View file

@ -21,7 +21,7 @@ import { getHomePageUrl } from "../../utils/pages";
import { _t } from "../../languageHandler"; import { _t } from "../../languageHandler";
import SdkConfig from "../../SdkConfig"; import SdkConfig from "../../SdkConfig";
import * as sdk from "../../index"; import * as sdk from "../../index";
import dis from "../../dispatcher"; import dis from "../../dispatcher/dispatcher";
const onClickSendDm = () => dis.dispatch({action: 'view_create_chat'}); const onClickSendDm = () => dis.dispatch({action: 'view_create_chat'});
const onClickExplore = () => dis.dispatch({action: 'view_room_directory'}); const onClickExplore = () => dis.dispatch({action: 'view_room_directory'});

View file

@ -21,11 +21,12 @@ import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import { Key } from '../../Keyboard'; import { Key } from '../../Keyboard';
import * as sdk from '../../index'; import * as sdk from '../../index';
import dis from '../../dispatcher'; import dis from '../../dispatcher/dispatcher';
import * as VectorConferenceHandler from '../../VectorConferenceHandler'; import * as VectorConferenceHandler from '../../VectorConferenceHandler';
import SettingsStore from '../../settings/SettingsStore'; import SettingsStore from '../../settings/SettingsStore';
import {_t} from "../../languageHandler"; import {_t} from "../../languageHandler";
import Analytics from "../../Analytics"; import Analytics from "../../Analytics";
import RoomList2 from "../views/rooms/RoomList2";
const LeftPanel = createReactClass({ const LeftPanel = createReactClass({
@ -273,6 +274,29 @@ const LeftPanel = createReactClass({
breadcrumbs = (<RoomBreadcrumbs collapsed={this.props.collapsed} />); 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 ( return (
<div className={containerClasses}> <div className={containerClasses}>
{ tagPanelContainer } { tagPanelContainer }
@ -284,15 +308,7 @@ const LeftPanel = createReactClass({
{ exploreButton } { exploreButton }
{ searchBox } { searchBox }
</div> </div>
<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} />
</aside> </aside>
</div> </div>
); );

View file

@ -27,11 +27,10 @@ import PageTypes from '../../PageTypes';
import CallMediaHandler from '../../CallMediaHandler'; import CallMediaHandler from '../../CallMediaHandler';
import { fixupColorFonts } from '../../utils/FontManager'; import { fixupColorFonts } from '../../utils/FontManager';
import * as sdk from '../../index'; import * as sdk from '../../index';
import dis from '../../dispatcher'; import dis from '../../dispatcher/dispatcher';
import sessionStore from '../../stores/SessionStore'; import sessionStore from '../../stores/SessionStore';
import {MatrixClientPeg, MatrixClientCreds} from '../../MatrixClientPeg'; import {MatrixClientPeg, IMatrixClientCreds} from '../../MatrixClientPeg';
import SettingsStore from "../../settings/SettingsStore"; import SettingsStore from "../../settings/SettingsStore";
import RoomListStore from "../../stores/RoomListStore";
import TagOrderActions from '../../actions/TagOrderActions'; import TagOrderActions from '../../actions/TagOrderActions';
import RoomListActions from '../../actions/RoomListActions'; import RoomListActions from '../../actions/RoomListActions';
@ -42,6 +41,17 @@ import * as KeyboardShortcuts from "../../accessibility/KeyboardShortcuts";
import HomePage from "./HomePage"; import HomePage from "./HomePage";
import ResizeNotifier from "../../utils/ResizeNotifier"; import ResizeNotifier from "../../utils/ResizeNotifier";
import PlatformPeg from "../../PlatformPeg"; 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) // 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. // 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. // NB. this is just for server notices rather than pinned messages in general.
@ -56,7 +66,7 @@ function canElementReceiveInput(el) {
interface IProps { interface IProps {
matrixClient: MatrixClient; matrixClient: MatrixClient;
onRegistered: (credentials: MatrixClientCreds) => Promise<MatrixClient>; onRegistered: (credentials: IMatrixClientCreds) => Promise<MatrixClient>;
viaServers?: string[]; viaServers?: string[];
hideToSRUsers: boolean; hideToSRUsers: boolean;
resizeNotifier: ResizeNotifier; resizeNotifier: ResizeNotifier;
@ -64,10 +74,6 @@ interface IProps {
initialEventPixelOffset: number; initialEventPixelOffset: number;
leftDisabled: boolean; leftDisabled: boolean;
rightDisabled: boolean; rightDisabled: boolean;
showCookieBar: boolean;
hasNewVersion: boolean;
userHasGeneratedPassword: boolean;
showNotifierToolbar: boolean;
page_type: string; page_type: string;
autoJoin: boolean; autoJoin: boolean;
thirdPartyInvite?: object; thirdPartyInvite?: object;
@ -75,7 +81,6 @@ interface IProps {
currentRoomId: string; currentRoomId: string;
ConferenceHandler?: object; ConferenceHandler?: object;
collapseLhs: boolean; collapseLhs: boolean;
checkingForUpdate: boolean;
config: { config: {
piwik: { piwik: {
policyUrl: string; policyUrl: string;
@ -85,10 +90,8 @@ interface IProps {
currentUserId?: string; currentUserId?: string;
currentGroupId?: string; currentGroupId?: string;
currentGroupIsNew?: boolean; currentGroupIsNew?: boolean;
version?: string;
newVersion?: string;
newVersionReleaseNotes?: string;
} }
interface IState { interface IState {
mouseDown?: { mouseDown?: {
x: number; x: number;
@ -96,8 +99,6 @@ interface IState {
}; };
syncErrorData: any; syncErrorData: any;
useCompactLayout: boolean; useCompactLayout: boolean;
serverNoticeEvents: MatrixEvent[];
userHasGeneratedPassword: boolean;
} }
/** /**
@ -140,11 +141,8 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
this.state = { this.state = {
mouseDown: undefined, mouseDown: undefined,
syncErrorData: undefined, syncErrorData: undefined,
userHasGeneratedPassword: false,
// use compact timeline view // use compact timeline view
useCompactLayout: SettingsStore.getValue('useCompactLayout'), useCompactLayout: SettingsStore.getValue('useCompactLayout'),
// any currently active server notice events
serverNoticeEvents: [],
}; };
// stash the MatrixClient in case we log out before we are unmounted // 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(); 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() { componentWillUnmount() {
document.removeEventListener('keydown', this._onNativeKeyDown, false); document.removeEventListener('keydown', this._onNativeKeyDown, false);
this._matrixClient.removeListener("accountData", this.onAccountData); this._matrixClient.removeListener("accountData", this.onAccountData);
@ -219,9 +205,11 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
}; };
_setStateFromSessionStore = () => { _setStateFromSessionStore = () => {
this.setState({ if (this._sessionStore.getCachedPassword()) {
userHasGeneratedPassword: Boolean(this._sessionStore.getCachedPassword()), showSetPasswordToast();
}); } else {
hideSetPasswordToast();
}
}; };
_createResizer() { _createResizer() {
@ -293,22 +281,37 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
if (oldSyncState === 'PREPARED' && syncState === 'SYNCING') { if (oldSyncState === 'PREPARED' && syncState === 'SYNCING') {
this._updateServerNoticeEvents(); this._updateServerNoticeEvents();
} else {
this._calculateServerLimitToast(data);
} }
}; };
onRoomStateEvents = (ev, state) => { onRoomStateEvents = (ev, state) => {
const roomLists = RoomListStore.getRoomLists(); const roomLists = RoomListStoreTempProxy.getRoomLists();
if (roomLists['m.server_notice'] && roomLists['m.server_notice'].some(r => r.roomId === ev.getRoomId())) { if (roomLists[DefaultTagID.ServerNotice] && roomLists[DefaultTagID.ServerNotice].some(r => r.roomId === ev.getRoomId())) {
this._updateServerNoticeEvents(); this._updateServerNoticeEvents();
} }
}; };
_updateServerNoticeEvents = async () => { _calculateServerLimitToast(syncErrorData, usageLimitEventContent?) {
const roomLists = RoomListStore.getRoomLists(); const error = syncErrorData && syncErrorData.error && syncErrorData.error.errcode === "M_RESOURCE_LIMIT_EXCEEDED";
if (!roomLists['m.server_notice']) return []; if (error) {
usageLimitEventContent = syncErrorData.error.data;
}
const pinnedEvents = []; if (usageLimitEventContent) {
for (const room of roomLists['m.server_notice']) { 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", ""); const pinStateEvent = room.currentState.getStateEvents("m.room.pinned_events", "");
if (!pinStateEvent || !pinStateEvent.getContent().pinned) continue; if (!pinStateEvent || !pinStateEvent.getContent().pinned) continue;
@ -317,12 +320,18 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
for (const eventId of pinnedEventIds) { for (const eventId of pinnedEventIds) {
const timeline = await this._matrixClient.getEventTimeline(room.getUnfilteredTimelineSet(), eventId, 0); const timeline = await this._matrixClient.getEventTimeline(room.getUnfilteredTimelineSet(), eventId, 0);
const event = timeline.getEvents().find(ev => ev.getId() === eventId); 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) => { _onPaste = (ev) => {
@ -598,12 +607,6 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
const GroupView = sdk.getComponent('structures.GroupView'); const GroupView = sdk.getComponent('structures.GroupView');
const MyGroups = sdk.getComponent('structures.MyGroups'); const MyGroups = sdk.getComponent('structures.MyGroups');
const ToastContainer = sdk.getComponent('structures.ToastContainer'); 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; let pageElement;
@ -647,46 +650,7 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
break; 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'; let bodyClasses = 'mx_MatrixChat';
if (topBar) {
bodyClasses += ' mx_MatrixChat_toolbarShowing';
}
if (this.state.useCompactLayout) { if (this.state.useCompactLayout) {
bodyClasses += ' mx_MatrixChat_useCompactLayout'; bodyClasses += ' mx_MatrixChat_useCompactLayout';
} }
@ -701,7 +665,6 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
onMouseDown={this._onMouseDown} onMouseDown={this._onMouseDown}
onMouseUp={this._onMouseUp} onMouseUp={this._onMouseUp}
> >
{ topBar }
<ToastContainer /> <ToastContainer />
<DragDropContext onDragEnd={this._onDragEnd}> <DragDropContext onDragEnd={this._onDragEnd}>
<div ref={this._resizeContainer} className={bodyClasses}> <div ref={this._resizeContainer} className={bodyClasses}>

View file

@ -17,12 +17,11 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, {createRef} from 'react'; import React, { createRef } from 'react';
import {InvalidStoreError} from "matrix-js-sdk/src/errors"; import { InvalidStoreError } from "matrix-js-sdk/src/errors";
import {RoomMember} from "matrix-js-sdk/src/models/room-member"; import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import {MatrixEvent} from "matrix-js-sdk/src/models/event"; import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { isCryptoAvailable } from 'matrix-js-sdk/src/crypto'; import { isCryptoAvailable } from 'matrix-js-sdk/src/crypto';
// focus-visible is a Polyfill for the :focus-visible CSS pseudo-attribute used by _AccessibleButton.scss // focus-visible is a Polyfill for the :focus-visible CSS pseudo-attribute used by _AccessibleButton.scss
import 'focus-visible'; import 'focus-visible';
// what-input helps improve keyboard accessibility // what-input helps improve keyboard accessibility
@ -30,17 +29,17 @@ import 'what-input';
import Analytics from "../../Analytics"; import Analytics from "../../Analytics";
import { DecryptionFailureTracker } from "../../DecryptionFailureTracker"; import { DecryptionFailureTracker } from "../../DecryptionFailureTracker";
import {MatrixClientPeg} from "../../MatrixClientPeg"; import { MatrixClientPeg } from "../../MatrixClientPeg";
import PlatformPeg from "../../PlatformPeg"; import PlatformPeg from "../../PlatformPeg";
import SdkConfig from "../../SdkConfig"; import SdkConfig from "../../SdkConfig";
import * as RoomListSorter from "../../RoomListSorter"; import * as RoomListSorter from "../../RoomListSorter";
import dis from "../../dispatcher"; import dis from "../../dispatcher/dispatcher";
import Notifier from '../../Notifier'; import Notifier from '../../Notifier';
import Modal from "../../Modal"; import Modal from "../../Modal";
import Tinter from "../../Tinter"; import Tinter from "../../Tinter";
import * as sdk from '../../index'; import * as sdk from '../../index';
import { showStartChatInviteDialog, showRoomInviteDialog } from '../../RoomInvite'; import { showRoomInviteDialog, showStartChatInviteDialog } from '../../RoomInvite';
import * as Rooms from '../../Rooms'; import * as Rooms from '../../Rooms';
import linkifyMatrix from "../../linkify-matrix"; import linkifyMatrix from "../../linkify-matrix";
import * as Lifecycle from '../../Lifecycle'; import * as Lifecycle from '../../Lifecycle';
@ -50,23 +49,29 @@ import PageTypes from '../../PageTypes';
import { getHomePageUrl } from '../../utils/pages'; import { getHomePageUrl } from '../../utils/pages';
import createRoom from "../../createRoom"; import createRoom from "../../createRoom";
import KeyRequestHandler from '../../KeyRequestHandler';
import { _t, getCurrentLanguage } from '../../languageHandler'; import { _t, getCurrentLanguage } from '../../languageHandler';
import SettingsStore, {SettingLevel} from "../../settings/SettingsStore"; import SettingsStore, { SettingLevel } from "../../settings/SettingsStore";
import ThemeController from "../../settings/controllers/ThemeController"; import ThemeController from "../../settings/controllers/ThemeController";
import { startAnyRegistrationFlow } from "../../Registration.js"; import { startAnyRegistrationFlow } from "../../Registration.js";
import { messageForSyncError } from '../../utils/ErrorUtils'; import { messageForSyncError } from '../../utils/ErrorUtils';
import ResizeNotifier from "../../utils/ResizeNotifier"; import ResizeNotifier from "../../utils/ResizeNotifier";
import { ValidatedServerConfig } from "../../utils/AutoDiscoveryUtils"; import AutoDiscoveryUtils, { ValidatedServerConfig } from "../../utils/AutoDiscoveryUtils";
import AutoDiscoveryUtils from "../../utils/AutoDiscoveryUtils";
import DMRoomMap from '../../utils/DMRoomMap'; import DMRoomMap from '../../utils/DMRoomMap';
import { countRoomsWithNotif } from '../../RoomNotifs'; 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 { storeRoomAliasInCache } from '../../RoomAliasCache';
import {defer, IDeferred} from "../../utils/promise"; import { defer, IDeferred } from "../../utils/promise";
import ToastStore from "../../stores/ToastStore"; import ToastStore from "../../stores/ToastStore";
import * as StorageManager from "../../utils/StorageManager"; import * as StorageManager from "../../utils/StorageManager";
import type LoggedInViewType from "./LoggedInView"; 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 */ /** constants for MatrixChat.state.view */
export enum Views { export enum Views {
@ -107,7 +112,7 @@ export enum Views {
// re-dispatched. NOTE: some actions are non-trivial and would require // re-dispatched. NOTE: some actions are non-trivial and would require
// re-factoring to be included in this list in future. // re-factoring to be included in this list in future.
const ONBOARDING_FLOW_STARTERS = [ const ONBOARDING_FLOW_STARTERS = [
'view_user_settings', Action.ViewUserSettings,
'view_create_chat', 'view_create_chat',
'view_create_room', 'view_create_room',
'view_create_group', 'view_create_group',
@ -168,12 +173,6 @@ interface IState {
leftDisabled: boolean; leftDisabled: boolean;
middleDisabled: boolean; middleDisabled: boolean;
// the right panel's disabled state is tracked in its store. // 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 // Parameters used in the registration dance with the IS
register_client_secret?: string; register_client_secret?: string;
register_session_id?: string; register_session_id?: string;
@ -183,7 +182,6 @@ interface IState {
hideToSRUsers: boolean; hideToSRUsers: boolean;
syncError?: Error; syncError?: Error;
resizeNotifier: ResizeNotifier; resizeNotifier: ResizeNotifier;
showNotifierToolbar: boolean;
serverConfig?: ValidatedServerConfig; serverConfig?: ValidatedServerConfig;
ready: boolean; ready: boolean;
thirdPartyInvite?: object; thirdPartyInvite?: object;
@ -216,6 +214,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
private readonly loggedInView: React.RefObject<LoggedInViewType>; private readonly loggedInView: React.RefObject<LoggedInViewType>;
private readonly dispatcherRef: any; private readonly dispatcherRef: any;
private readonly themeWatcher: ThemeWatcher; private readonly themeWatcher: ThemeWatcher;
private readonly fontWatcher: FontWatcher;
constructor(props, context) { constructor(props, context) {
super(props, context); super(props, context);
@ -226,17 +225,10 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
leftDisabled: false, leftDisabled: false,
middleDisabled: false, middleDisabled: false,
hasNewVersion: false,
newVersionReleaseNotes: null,
checkingForUpdate: null,
showCookieBar: false,
hideToSRUsers: false, hideToSRUsers: false,
syncError: null, // If the current syncing status is ERROR, the error object, otherwise null. syncError: null, // If the current syncing status is ERROR, the error object, otherwise null.
resizeNotifier: new ResizeNotifier(), resizeNotifier: new ResizeNotifier(),
showNotifierToolbar: false,
ready: false, ready: false,
}; };
@ -283,8 +275,11 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.accountPasswordTimer = null; this.accountPasswordTimer = null;
this.dispatcherRef = dis.register(this.onAction); this.dispatcherRef = dis.register(this.onAction);
this.themeWatcher = new ThemeWatcher(); this.themeWatcher = new ThemeWatcher();
this.fontWatcher = new FontWatcher();
this.themeWatcher.start(); this.themeWatcher.start();
this.fontWatcher.start();
this.focusComposer = false; 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")) { if (SettingsStore.getValue("analyticsOptIn")) {
Analytics.enable(); Analytics.enable();
} }
@ -367,6 +356,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
Lifecycle.stopMatrixClient(); Lifecycle.stopMatrixClient();
dis.unregister(this.dispatcherRef); dis.unregister(this.dispatcherRef);
this.themeWatcher.stop(); this.themeWatcher.stop();
this.fontWatcher.stop();
window.removeEventListener('resize', this.handleResize); window.removeEventListener('resize', this.handleResize);
this.state.resizeNotifier.removeListener("middlePanelResized", this.dispatchTimelineResize); this.state.resizeNotifier.removeListener("middlePanelResized", this.dispatchTimelineResize);
@ -613,7 +603,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
case 'view_indexed_room': case 'view_indexed_room':
this.viewIndexedRoom(payload.roomIndex); this.viewIndexedRoom(payload.roomIndex);
break; break;
case 'view_user_settings': { case Action.ViewUserSettings: {
const UserSettingsDialog = sdk.getComponent("dialogs.UserSettingsDialog"); const UserSettingsDialog = sdk.getComponent("dialogs.UserSettingsDialog");
Modal.createTrackedDialog('User settings', '', UserSettingsDialog, {}, Modal.createTrackedDialog('User settings', '', UserSettingsDialog, {},
/*className=*/null, /*isPriority=*/false, /*isStatic=*/true); /*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'}); dis.dispatch({action: 'view_my_groups'});
} }
break; break;
case 'notifier_enabled':
this.setState({showNotifierToolbar: Notifier.shouldShowToolbar()});
break;
case 'hide_left_panel': case 'hide_left_panel':
this.setState({ this.setState({
collapseLhs: true, collapseLhs: true,
@ -730,15 +717,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
case 'client_started': case 'client_started':
this.onClientStarted(); this.onClientStarted();
break; 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': case 'send_event':
this.onSendEvent(payload.room_id, payload.event); this.onSendEvent(payload.room_id, payload.event);
break; break;
@ -755,19 +733,13 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
case 'accept_cookies': case 'accept_cookies':
SettingsStore.setValue("analyticsOptIn", null, SettingLevel.DEVICE, true); SettingsStore.setValue("analyticsOptIn", null, SettingLevel.DEVICE, true);
SettingsStore.setValue("showCookieBar", null, SettingLevel.DEVICE, false); SettingsStore.setValue("showCookieBar", null, SettingLevel.DEVICE, false);
hideAnalyticsToast();
this.setState({
showCookieBar: false,
});
Analytics.enable(); Analytics.enable();
break; break;
case 'reject_cookies': case 'reject_cookies':
SettingsStore.setValue("analyticsOptIn", null, SettingLevel.DEVICE, false); SettingsStore.setValue("analyticsOptIn", null, SettingLevel.DEVICE, false);
SettingsStore.setValue("showCookieBar", null, SettingLevel.DEVICE, false); SettingsStore.setValue("showCookieBar", null, SettingLevel.DEVICE, false);
hideAnalyticsToast();
this.setState({
showCookieBar: false,
});
break; 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; 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({ this.setState({
view: Views.LOGGED_IN,
currentGroupId: groupId, currentGroupId: groupId,
currentGroupIsNew: payload.group_is_new, currentGroupIsNew: payload.group_is_new,
}); });
@ -1245,6 +1228,10 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
} }
StorageManager.tryPersistStorage(); 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() { private showScreenAfterLogin() {
@ -1372,10 +1359,13 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.firstSyncComplete = true; this.firstSyncComplete = true;
this.firstSyncPromise.resolve(); this.firstSyncPromise.resolve();
if (Notifier.shouldShowToolbar()) {
showNotificationsToast();
}
dis.dispatch({action: 'focus_composer'}); dis.dispatch({action: 'focus_composer'});
this.setState({ this.setState({
ready: true, ready: true,
showNotifierToolbar: Notifier.shouldShowToolbar(),
}); });
}); });
cli.on('Call.incoming', function(call) { 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("Session.logged_out", () => dft.stop());
cli.on("Event.decrypted", (e, err) => dft.eventDecrypted(e, err)); 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) => { cli.on("Room", (room) => {
if (MatrixClientPeg.get().isCryptoEnabled()) { if (MatrixClientPeg.get().isCryptoEnabled()) {
const blacklistEnabled = SettingsStore.getValueAt( const blacklistEnabled = SettingsStore.getValueAt(
@ -1553,7 +1533,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
icon: "verification", icon: "verification",
props: {request}, props: {request},
component: sdk.getComponent("toasts.VerificationRequestToast"), 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', action: 'view_create_room',
}); });
} else if (screen === 'settings') { } else if (screen === 'settings') {
dis.dispatch({ dis.fire(Action.ViewUserSettings);
action: 'view_user_settings',
});
} else if (screen === 'welcome') { } else if (screen === 'welcome') {
dis.dispatch({ dis.dispatch({
action: 'view_welcome_page', action: 'view_welcome_page',
@ -1755,8 +1733,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
const member = new RoomMember(null, userId); const member = new RoomMember(null, userId);
if (!member) { return; } if (!member) { return; }
dis.dispatch({ dis.dispatch<ViewUserPayload>({
action: 'view_user', action: Action.ViewUser,
member: member, member: member,
}); });
} }
@ -1829,16 +1807,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.showScreen("settings"); 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) { onSendEvent(roomId: string, event: MatrixEvent) {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
if (!cli) { if (!cli) {
@ -2033,7 +2001,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
onCloseAllSettings={this.onCloseAllSettings} onCloseAllSettings={this.onCloseAllSettings}
onRegistered={this.onRegistered} onRegistered={this.onRegistered}
currentRoomId={this.state.currentRoomId} currentRoomId={this.state.currentRoomId}
showCookieBar={this.state.showCookieBar}
/> />
); );
} else { } else {

View file

@ -29,6 +29,7 @@ import SettingsStore from '../../settings/SettingsStore';
import {_t} from "../../languageHandler"; import {_t} from "../../languageHandler";
import {haveTileForEvent} from "../views/rooms/EventTile"; import {haveTileForEvent} from "../views/rooms/EventTile";
import {textForEvent} from "../../TextForEvent"; import {textForEvent} from "../../TextForEvent";
import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResizer";
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
const continuedTypes = ['m.sticker', 'm.room.message']; 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 // whether to show reactions for an event
showReactions: PropTypes.bool, showReactions: PropTypes.bool,
// whether to use the irc layout
useIRCLayout: PropTypes.bool,
}; };
constructor() { // Force props to be loaded for useIRCLayout
super(); constructor(props) {
super(props);
this.state = { this.state = {
// previous positions the read marker has been in, so we can // previous positions the read marker has been in, so we can
@ -597,6 +602,7 @@ export default class MessagePanel extends React.Component {
isSelectedEvent={highlight} isSelectedEvent={highlight}
getRelationsForEvent={this.props.getRelationsForEvent} getRelationsForEvent={this.props.getRelationsForEvent}
showReactions={this.props.showReactions} showReactions={this.props.showReactions}
useIRCLayout={this.props.useIRCLayout}
/> />
</TileErrorBoundary> </TileErrorBoundary>
</li>, </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 ( return (
<ErrorBoundary> <ErrorBoundary>
<ScrollPanel <ScrollPanel
@ -804,6 +819,7 @@ export default class MessagePanel extends React.Component {
style={style} style={style}
stickyBottom={this.props.stickyBottom} stickyBottom={this.props.stickyBottom}
resizeNotifier={this.props.resizeNotifier} resizeNotifier={this.props.resizeNotifier}
fixedChildren={ircResizer}
> >
{ topSpinner } { topSpinner }
{ this._getEventTiles() } { this._getEventTiles() }

View file

@ -19,7 +19,7 @@ import React from 'react';
import createReactClass from 'create-react-class'; import createReactClass from 'create-react-class';
import * as sdk from '../../index'; import * as sdk from '../../index';
import { _t } from '../../languageHandler'; import { _t } from '../../languageHandler';
import dis from '../../dispatcher'; import dis from '../../dispatcher/dispatcher';
import AccessibleButton from '../views/elements/AccessibleButton'; import AccessibleButton from '../views/elements/AccessibleButton';
import MatrixClientContext from "../../contexts/MatrixClientContext"; import MatrixClientContext from "../../contexts/MatrixClientContext";
import AutoHideScrollbar from "./AutoHideScrollbar"; import AutoHideScrollbar from "./AutoHideScrollbar";

View file

@ -22,7 +22,7 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import * as sdk from '../../index'; import * as sdk from '../../index';
import dis from '../../dispatcher'; import dis from '../../dispatcher/dispatcher';
import RateLimitedFunc from '../../ratelimitedfunc'; import RateLimitedFunc from '../../ratelimitedfunc';
import { showGroupInviteDialog, showGroupAddRoomDialog } from '../../GroupAddressPicker'; import { showGroupInviteDialog, showGroupAddRoomDialog } from '../../GroupAddressPicker';
import GroupStore from '../../stores/GroupStore'; 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 {RIGHT_PANEL_PHASES, RIGHT_PANEL_PHASES_NO_ARGS} from "../../stores/RightPanelStorePhases";
import RightPanelStore from "../../stores/RightPanelStore"; import RightPanelStore from "../../stores/RightPanelStore";
import MatrixClientContext from "../../contexts/MatrixClientContext"; import MatrixClientContext from "../../contexts/MatrixClientContext";
import {Action} from "../../dispatcher/actions";
export default class RightPanel extends React.Component { export default class RightPanel extends React.Component {
static get propTypes() { 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, // 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. // or the member list if we were in the member panel... phew.
dis.dispatch({ dis.dispatch({
action: "view_user", action: Action.ViewUser,
member: this.state.phase === RIGHT_PANEL_PHASES.EncryptionPanel ? member: this.state.phase === RIGHT_PANEL_PHASES.EncryptionPanel ?
this.state.member : null, this.state.member : null,
}); });
@ -266,7 +267,7 @@ export default class RightPanel extends React.Component {
if (SettingsStore.getValue("feature_cross_signing")) { if (SettingsStore.getValue("feature_cross_signing")) {
const onClose = () => { const onClose = () => {
dis.dispatch({ dis.dispatch({
action: "view_user", action: Action.ViewUser,
member: null, member: null,
}); });
}; };

View file

@ -20,7 +20,7 @@ import React from 'react';
import createReactClass from 'create-react-class'; import createReactClass from 'create-react-class';
import {MatrixClientPeg} from "../../MatrixClientPeg"; import {MatrixClientPeg} from "../../MatrixClientPeg";
import * as sdk from "../../index"; import * as sdk from "../../index";
import dis from "../../dispatcher"; import dis from "../../dispatcher/dispatcher";
import Modal from "../../Modal"; import Modal from "../../Modal";
import { linkifyAndSanitizeHtml } from '../../HtmlUtils'; import { linkifyAndSanitizeHtml } from '../../HtmlUtils';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
@ -199,7 +199,7 @@ export default createReactClass({
let desc; let desc;
if (alias) { 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 { } else {
desc = _t('Remove %(name)s from the directory?', {name: name}); 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(() => { MatrixClientPeg.get().setRoomDirectoryVisibility(room.room_id, 'private').then(() => {
if (!alias) return; if (!alias) return;
step = _t('delete the alias.'); step = _t('delete the address.');
return MatrixClientPeg.get().deleteAlias(alias); return MatrixClientPeg.get().deleteAlias(alias);
}).then(() => { }).then(() => {
modal.close(); modal.close();

View file

@ -25,7 +25,7 @@ import * as sdk from '../../index';
import {MatrixClientPeg} from '../../MatrixClientPeg'; import {MatrixClientPeg} from '../../MatrixClientPeg';
import Resend from '../../Resend'; import Resend from '../../Resend';
import * as cryptodevices from '../../cryptodevices'; import * as cryptodevices from '../../cryptodevices';
import dis from '../../dispatcher'; import dis from '../../dispatcher/dispatcher';
import {messageForResourceLimitError, messageForSendError} from '../../utils/ErrorUtils'; import {messageForResourceLimitError, messageForSendError} from '../../utils/ErrorUtils';
const STATUS_BAR_HIDDEN = 0; const STATUS_BAR_HIDDEN = 0;

View file

@ -20,7 +20,7 @@ limitations under the License.
import React, {createRef} from 'react'; import React, {createRef} from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import * as sdk from '../../index'; import * as sdk from '../../index';
import dis from '../../dispatcher'; import dis from '../../dispatcher/dispatcher';
import * as Unread from '../../Unread'; import * as Unread from '../../Unread';
import * as RoomNotifs from '../../RoomNotifs'; import * as RoomNotifs from '../../RoomNotifs';
import * as FormattingUtils from '../../utils/FormattingUtils'; import * as FormattingUtils from '../../utils/FormattingUtils';
@ -32,7 +32,7 @@ import RoomTile from "../views/rooms/RoomTile";
import LazyRenderList from "../views/elements/LazyRenderList"; import LazyRenderList from "../views/elements/LazyRenderList";
import {_t} from "../../languageHandler"; import {_t} from "../../languageHandler";
import {RovingTabIndexWrapper} from "../../accessibility/RovingTabIndex"; import {RovingTabIndexWrapper} from "../../accessibility/RovingTabIndex";
import toRem from "../../utils/rem"; import {toPx} from "../../utils/units";
// turn this on for drop & drag console debugging galore // turn this on for drop & drag console debugging galore
const debug = false; const debug = false;
@ -420,7 +420,7 @@ export default class RoomSubList extends React.PureComponent {
setHeight = (height) => { setHeight = (height) => {
if (this._subList.current) { if (this._subList.current) {
this._subList.current.style.height = toRem(height); this._subList.current.style.height = toPx(height);
} }
this._updateLazyRenderHeight(height); this._updateLazyRenderHeight(height);
}; };

View file

@ -34,7 +34,7 @@ import ContentMessages from '../../ContentMessages';
import Modal from '../../Modal'; import Modal from '../../Modal';
import * as sdk from '../../index'; import * as sdk from '../../index';
import CallHandler from '../../CallHandler'; import CallHandler from '../../CallHandler';
import dis from '../../dispatcher'; import dis from '../../dispatcher/dispatcher';
import Tinter from '../../Tinter'; import Tinter from '../../Tinter';
import rate_limited_func from '../../ratelimitedfunc'; import rate_limited_func from '../../ratelimitedfunc';
import * as ObjectUtils from '../../ObjectUtils'; import * as ObjectUtils from '../../ObjectUtils';
@ -164,6 +164,10 @@ export default createReactClass({
canReact: false, canReact: false,
canReply: 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._roomView = createRef();
this._searchResultsPanel = createRef(); this._searchResultsPanel = createRef();
this._layoutWatcherRef = SettingsStore.watchSetting("feature_irc_ui", null, this.onLayoutChange);
}, },
_onReadReceiptsChange: function() { _onReadReceiptsChange: function() {
@ -232,7 +238,8 @@ export default createReactClass({
initialEventId: RoomViewStore.getInitialEventId(), initialEventId: RoomViewStore.getInitialEventId(),
isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(), isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(),
forwardingEvent: RoomViewStore.getForwardingEvent(), 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), showingPinned: SettingsStore.getValue("PinnedEvents.isOpen", roomId),
showReadReceipts: SettingsStore.getValue("showReadReceipts", 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. // no need to do this as Dir & Settings are now overlays. It just burnt CPU.
// console.log("Tinter.tint from RoomView.unmount"); // console.log("Tinter.tint from RoomView.unmount");
// Tinter.tint(); // reset colourscheme // Tinter.tint(); // reset colourscheme
SettingsStore.unwatchSetting(this._layoutWatcherRef);
},
onLayoutChange: function() {
this.setState({
useIRCLayout: SettingsStore.getValue("feature_irc_ui"),
});
}, },
_onRightPanelStoreUpdate: function() { _onRightPanelStoreUpdate: function() {
@ -681,6 +696,16 @@ export default createReactClass({
}); });
} }
break; 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"); const ErrorBoundary = sdk.getComponent("elements.ErrorBoundary");
if (!this.state.room) { 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) { 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 ( return (
<div className="mx_RoomView"> <div className="mx_RoomView">
<ErrorBoundary> <ErrorBoundary>
<RoomPreviewBar <RoomPreviewBar
canPreview={false} canPreview={false}
previewLoading={this.state.peekLoading} previewLoading={previewLoading && !this.state.roomLoadError}
error={this.state.roomLoadError} error={this.state.roomLoadError}
loading={loading} loading={loading}
joining={this.state.joining} joining={this.state.joining}
@ -1695,7 +1722,8 @@ export default createReactClass({
return ( return (
<div className="mx_RoomView"> <div className="mx_RoomView">
<ErrorBoundary> <ErrorBoundary>
<RoomPreviewBar onJoinClick={this.onJoinButtonClicked} <RoomPreviewBar
onJoinClick={this.onJoinButtonClicked}
onForgetClick={this.onForgetClick} onForgetClick={this.onForgetClick}
onRejectClick={this.onRejectThreepidInviteButtonClicked} onRejectClick={this.onRejectThreepidInviteButtonClicked}
canPreview={false} error={this.state.roomLoadError} canPreview={false} error={this.state.roomLoadError}
@ -1980,6 +2008,13 @@ export default createReactClass({
highlightedEventId = this.state.initialEventId; 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); // console.info("ShowUrlPreview for %s is %s", this.state.room.roomId, this.state.showUrlPreview);
const messagePanel = ( const messagePanel = (
<TimelinePanel <TimelinePanel
@ -1995,11 +2030,12 @@ export default createReactClass({
onScroll={this.onMessageListScroll} onScroll={this.onMessageListScroll}
onReadMarkerUpdated={this._updateTopUnreadMessagesBar} onReadMarkerUpdated={this._updateTopUnreadMessagesBar}
showUrlPreview = {this.state.showUrlPreview} showUrlPreview = {this.state.showUrlPreview}
className="mx_RoomView_messagePanel" className={messagePanelClassNames}
membersLoaded={this.state.membersLoaded} membersLoaded={this.state.membersLoaded}
permalinkCreator={this._getPermalinkCreatorForRoom(this.state.room)} permalinkCreator={this._getPermalinkCreatorForRoom(this.state.room)}
resizeNotifier={this.props.resizeNotifier} resizeNotifier={this.props.resizeNotifier}
showReactions={true} showReactions={true}
useIRCLayout={this.state.useIRCLayout}
/>); />);
let topUnreadMessagesBar = null; let topUnreadMessagesBar = null;

View file

@ -144,6 +144,11 @@ export default createReactClass({
/* resizeNotifier: ResizeNotifier to know when middle column has changed size /* resizeNotifier: ResizeNotifier to know when middle column has changed size
*/ */
resizeNotifier: PropTypes.object, resizeNotifier: PropTypes.object,
/* fixedChildren: allows for children to be passed which are rendered outside
* of the wrapper
*/
fixedChildren: PropTypes.node,
}, },
getDefaultProps: function() { getDefaultProps: function() {
@ -881,6 +886,7 @@ export default createReactClass({
return (<AutoHideScrollbar wrappedRef={this._collectScroll} return (<AutoHideScrollbar wrappedRef={this._collectScroll}
onScroll={this.onScroll} onScroll={this.onScroll}
className={`mx_ScrollPanel ${this.props.className}`} style={this.props.style}> className={`mx_ScrollPanel ${this.props.className}`} style={this.props.style}>
{ this.props.fixedChildren }
<div className="mx_RoomView_messageListWrapper"> <div className="mx_RoomView_messageListWrapper">
<ol ref={this._itemlist} className="mx_RoomView_MessageList" aria-live="polite" role="list"> <ol ref={this._itemlist} className="mx_RoomView_MessageList" aria-live="polite" role="list">
{ this.props.children } { this.props.children }

View file

@ -19,7 +19,7 @@ import React, {createRef} from 'react';
import createReactClass from 'create-react-class'; import createReactClass from 'create-react-class';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Key } from '../../Keyboard'; import { Key } from '../../Keyboard';
import dis from '../../dispatcher'; import dis from '../../dispatcher/dispatcher';
import { throttle } from 'lodash'; import { throttle } from 'lodash';
import AccessibleButton from '../../components/views/elements/AccessibleButton'; import AccessibleButton from '../../components/views/elements/AccessibleButton';
import classNames from 'classnames'; import classNames from 'classnames';

View file

@ -22,7 +22,7 @@ import TagOrderStore from '../../stores/TagOrderStore';
import GroupActions from '../../actions/GroupActions'; import GroupActions from '../../actions/GroupActions';
import * as sdk from '../../index'; import * as sdk from '../../index';
import dis from '../../dispatcher'; import dis from '../../dispatcher/dispatcher';
import { _t } from '../../languageHandler'; import { _t } from '../../languageHandler';
import { Droppable } from 'react-beautiful-dnd'; import { Droppable } from 'react-beautiful-dnd';

View file

@ -17,7 +17,7 @@ limitations under the License.
import React from 'react'; import React from 'react';
import createReactClass from 'create-react-class'; import createReactClass from 'create-react-class';
import * as sdk from '../../index'; import * as sdk from '../../index';
import dis from '../../dispatcher'; import dis from '../../dispatcher/dispatcher';
import Modal from '../../Modal'; import Modal from '../../Modal';
import { _t } from '../../languageHandler'; import { _t } from '../../languageHandler';

View file

@ -29,7 +29,7 @@ import {MatrixClientPeg} from "../../MatrixClientPeg";
import * as ObjectUtils from "../../ObjectUtils"; import * as ObjectUtils from "../../ObjectUtils";
import UserActivity from "../../UserActivity"; import UserActivity from "../../UserActivity";
import Modal from "../../Modal"; import Modal from "../../Modal";
import dis from "../../dispatcher"; import dis from "../../dispatcher/dispatcher";
import * as sdk from "../../index"; import * as sdk from "../../index";
import { Key } from '../../Keyboard'; import { Key } from '../../Keyboard';
import Timer from '../../utils/Timer'; import Timer from '../../utils/Timer';
@ -112,6 +112,9 @@ const TimelinePanel = createReactClass({
// whether to show reactions for an event // whether to show reactions for an event
showReactions: PropTypes.bool, showReactions: PropTypes.bool,
// whether to use the irc layout
useIRCLayout: PropTypes.bool,
}, },
statics: { statics: {
@ -1447,6 +1450,7 @@ const TimelinePanel = createReactClass({
getRelationsForEvent={this.getRelationsForEvent} getRelationsForEvent={this.getRelationsForEvent}
editState={this.state.editState} editState={this.state.editState}
showReactions={this.props.showReactions} showReactions={this.props.showReactions}
useIRCLayout={this.props.useIRCLayout}
/> />
); );
}, },

View file

@ -15,14 +15,21 @@ limitations under the License.
*/ */
import * as React from "react"; import * as React from "react";
import { _t } from '../../languageHandler'; import ToastStore, {IToast} from "../../stores/ToastStore";
import ToastStore from "../../stores/ToastStore";
import classNames from "classnames"; import classNames from "classnames";
export default class ToastContainer extends React.Component { interface IState {
constructor() { toasts: IToast<any>[];
super(); countSeen: number;
this.state = {toasts: ToastStore.sharedInstance().getToasts()}; }
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 // Start listening here rather than in componentDidMount because
// toasts may dismiss themselves in their didMount if they find // toasts may dismiss themselves in their didMount if they find
@ -36,7 +43,10 @@ export default class ToastContainer extends React.Component {
} }
_onToastStoreUpdate = () => { _onToastStoreUpdate = () => {
this.setState({toasts: ToastStore.sharedInstance().getToasts()}); this.setState({
toasts: ToastStore.sharedInstance().getToasts(),
countSeen: ToastStore.sharedInstance().getCountSeen(),
});
}; };
render() { render() {
@ -50,14 +60,21 @@ export default class ToastContainer extends React.Component {
"mx_Toast_hasIcon": icon, "mx_Toast_hasIcon": icon,
[`mx_Toast_icon_${icon}`]: 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, { const toastProps = Object.assign({}, props, {
key, key,
toastKey: key, toastKey: key,
}); });
toast = (<div className={toastClasses}> 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 className="mx_Toast_body">{React.createElement(component, toastProps)}</div>
</div>); </div>);
} }

View file

@ -22,7 +22,7 @@ import BaseAvatar from '../views/avatars/BaseAvatar';
import {MatrixClientPeg} from '../../MatrixClientPeg'; import {MatrixClientPeg} from '../../MatrixClientPeg';
import * as Avatar from '../../Avatar'; import * as Avatar from '../../Avatar';
import { _t } from '../../languageHandler'; import { _t } from '../../languageHandler';
import dis from "../../dispatcher"; import dis from "../../dispatcher/dispatcher";
import {ContextMenu, ContextMenuButton} from "./ContextMenu"; import {ContextMenu, ContextMenuButton} from "./ContextMenu";
const AVATAR_SIZE = 28; const AVATAR_SIZE = 28;

View file

@ -19,7 +19,7 @@ import React from 'react';
import createReactClass from 'create-react-class'; import createReactClass from 'create-react-class';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ContentMessages from '../../ContentMessages'; import ContentMessages from '../../ContentMessages';
import dis from "../../dispatcher"; import dis from "../../dispatcher/dispatcher";
import filesize from "filesize"; import filesize from "filesize";
import { _t } from '../../languageHandler'; import { _t } from '../../languageHandler';

View file

@ -32,7 +32,7 @@ import * as Lifecycle from '../../../Lifecycle';
import {MatrixClientPeg} from "../../../MatrixClientPeg"; import {MatrixClientPeg} from "../../../MatrixClientPeg";
import AuthPage from "../../views/auth/AuthPage"; import AuthPage from "../../views/auth/AuthPage";
import Login from "../../../Login"; import Login from "../../../Login";
import dis from "../../../dispatcher"; import dis from "../../../dispatcher/dispatcher";
// Phases // Phases
// Show controls to configure server details // Show controls to configure server details

View file

@ -18,7 +18,7 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import {_t} from '../../../languageHandler'; import {_t} from '../../../languageHandler';
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import dis from '../../../dispatcher'; import dis from '../../../dispatcher/dispatcher';
import * as Lifecycle from '../../../Lifecycle'; import * as Lifecycle from '../../../Lifecycle';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import {MatrixClientPeg} from "../../../MatrixClientPeg"; import {MatrixClientPeg} from "../../../MatrixClientPeg";

View file

@ -355,6 +355,7 @@ export const TermsAuthEntry = createReactClass({
allChecked = allChecked && checked; allChecked = allChecked && checked;
checkboxes.push( checkboxes.push(
// XXX: replace with StyledCheckbox
<label key={"policy_checkbox_" + policy.id} className="mx_InteractiveAuthEntryComponents_termsPolicy"> <label key={"policy_checkbox_" + policy.id} className="mx_InteractiveAuthEntryComponents_termsPolicy">
<input type="checkbox" onChange={() => this._togglePolicy(policy.id)} checked={checked} /> <input type="checkbox" onChange={() => this._togglePolicy(policy.id)} checked={checked} />
<a href={policy.url} target="_blank" rel="noreferrer noopener">{ policy.name }</a> <a href={policy.url} target="_blank" rel="noreferrer noopener">{ policy.name }</a>
@ -538,6 +539,7 @@ export const MsisdnAuthEntry = createReactClass({
type: MsisdnAuthEntry.LOGIN_TYPE, type: MsisdnAuthEntry.LOGIN_TYPE,
// TODO: Remove `threepid_creds` once servers support proper UIA // TODO: Remove `threepid_creds` once servers support proper UIA
// See https://github.com/vector-im/riot-web/issues/10312 // See https://github.com/vector-im/riot-web/issues/10312
// See https://github.com/matrix-org/matrix-doc/issues/2220
threepid_creds: creds, threepid_creds: creds,
threepidCreds: creds, threepidCreds: creds,
}); });

View file

@ -23,7 +23,8 @@ import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils";
import * as ServerType from '../../views/auth/ServerTypeSelector'; import * as ServerType from '../../views/auth/ServerTypeSelector';
import ServerConfig from "./ServerConfig"; 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? // TODO: TravisR - Can this extend ServerConfig for most things?

View file

@ -36,7 +36,7 @@ interface IProps {
labelStrongPassword?: string; labelStrongPassword?: string;
labelAllowedButUnsafe?: string; labelAllowedButUnsafe?: string;
onChange(ev: KeyboardEvent); onChange(ev: React.FormEvent<HTMLElement>);
onValidate(result: IValidationResult); onValidate(result: IValidationResult);
} }

View file

@ -238,7 +238,7 @@ export default class PasswordLogin extends React.Component {
type="text" type="text"
label={_t("Phone")} label={_t("Phone")}
value={this.state.phoneNumber} value={this.state.phoneNumber}
prefix={phoneCountry} prefixComponent={phoneCountry}
onChange={this.onPhoneNumberChanged} onChange={this.onPhoneNumberChanged}
onBlur={this.onPhoneNumberBlur} onBlur={this.onPhoneNumberBlur}
disabled={this.props.disableSubmit} disabled={this.props.disableSubmit}

View file

@ -473,7 +473,7 @@ export default createReactClass({
type="text" type="text"
label={phoneLabel} label={phoneLabel}
value={this.state.phoneNumber} value={this.state.phoneNumber}
prefix={phoneCountry} prefixComponent={phoneCountry}
onChange={this.onPhoneNumberChange} onChange={this.onPhoneNumberChange}
onValidate={this.onPhoneNumberValidate} onValidate={this.onPhoneNumberValidate}
/>; />;

View file

@ -22,7 +22,8 @@ import classnames from 'classnames';
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
import {makeType} from "../../../utils/TypeUtils"; 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 FREE = 'Free';
export const PREMIUM = 'Premium'; export const PREMIUM = 'Premium';

View file

@ -2,7 +2,7 @@
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016 OpenMarket Ltd
Copyright 2018 New Vector Ltd Copyright 2018 New Vector Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> 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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -17,220 +17,182 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from 'react'; import React, {useCallback, useContext, useEffect, useMemo, useState} from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import * as AvatarLogic from '../../../Avatar'; import * as AvatarLogic from '../../../Avatar';
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import toRem from "../../../utils/rem"; import {useEventEmitter} from "../../../hooks/useEventEmitter";
import {toPx} from "../../../utils/units";
export default createReactClass({ const useImageUrl = ({url, urls}) => {
displayName: 'BaseAvatar', const [imageUrls, setUrls] = useState([]);
const [urlsIndex, setIndex] = useState();
propTypes: { const onError = useCallback(() => {
name: PropTypes.string.isRequired, // The name (first initial used as default) setIndex(i => i + 1); // try the next one
idName: PropTypes.string, // ID for generating hash colours }, []);
title: PropTypes.string, // onHover title text const memoizedUrls = useMemo(() => urls, [JSON.stringify(urls)]); // eslint-disable-line react-hooks/exhaustive-deps
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) }),
]),
},
statics: { useEffect(() => {
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) {
// work out the full set of urls to try to load. This is formed like so: // 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")) { if (!SettingsStore.getValue("lowBandwidth")) {
urls = props.urls || []; _urls = memoizedUrls || [];
if (props.url) { if (url) {
urls.unshift(props.url); // put in urls[0] _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 // deduplicate URLs
urls = Array.from(new Set(urls)); _urls = Array.from(new Set(_urls));
return { setIndex(0);
imageUrls: urls, setUrls(_urls);
defaultImageUrl: defaultImageUrl, }, [url, memoizedUrls]); // eslint-disable-line react-hooks/exhaustive-deps
urlsIndex: 0,
};
},
onError: function(ev) { const cli = useContext(MatrixClientContext);
const nextIndex = this.state.urlsIndex + 1; const onClientSync = useCallback((syncState, prevState) => {
if (nextIndex < this.state.imageUrls.length) { // Consider the client reconnected if there is no error with syncing.
// try the next one // This means the state could be RECONNECTING, SYNCING, PREPARED or CATCHUP.
this.setState({ const reconnected = syncState !== "ERROR" && prevState !== syncState;
urlsIndex: nextIndex, if (reconnected) {
}); setIndex(0);
} }
}, }, []);
useEventEmitter(cli, "sync", onClientSync);
render: function() { const imageUrl = imageUrls[urlsIndex];
const imageUrl = this.state.imageUrls[this.state.urlsIndex]; return [imageUrl, onError];
};
const { const BaseAvatar = (props) => {
name, idName, title, url, urls, width, height, resizeMethod, const {
defaultToInitialLetter, onClick, inputRef, name,
...otherProps idName,
} = this.props; 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) { if (onClick != null) {
return ( return (
<AccessibleButton <AccessibleButton
className="mx_BaseAvatar mx_BaseAvatar_image" {...otherProps}
element='img' element="span"
src={imageUrl} className="mx_BaseAvatar"
onClick={onClick} onClick={onClick}
onError={this.onError}
style={{
width: toRem(width),
height: toRem(height),
}}
title={title} alt=""
inputRef={inputRef} inputRef={inputRef}
{...otherProps} /> >
{ textNode }
{ imgNode }
</AccessibleButton>
); );
} else { } else {
return ( return (
<img <span className="mx_BaseAvatar" ref={inputRef} {...otherProps}>
className="mx_BaseAvatar mx_BaseAvatar_image" { textNode }
src={imageUrl} { imgNode }
onError={this.onError} </span>
style={{
width: toRem(width),
height: toRem(height),
}}
title={title} alt=""
ref={inputRef}
{...otherProps} />
); );
} }
}, }
});
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;

View file

@ -18,9 +18,10 @@ limitations under the License.
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import createReactClass from 'create-react-class'; import createReactClass from 'create-react-class';
import * as Avatar from '../../../Avatar';
import * as sdk from "../../../index"; 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({ export default createReactClass({
displayName: 'MemberAvatar', displayName: 'MemberAvatar',
@ -33,7 +34,7 @@ export default createReactClass({
resizeMethod: PropTypes.string, resizeMethod: PropTypes.string,
// The onClick to give the avatar // The onClick to give the avatar
onClick: PropTypes.func, 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, viewUserOnClick: PropTypes.bool,
title: PropTypes.string, title: PropTypes.string,
}, },
@ -61,10 +62,14 @@ export default createReactClass({
return { return {
name: props.member.name, name: props.member.name,
title: props.title || props.member.userId, title: props.title || props.member.userId,
imageUrl: Avatar.avatarUrlForMember(props.member, imageUrl: props.member.getAvatarUrl(
props.width, MatrixClientPeg.get().getHomeserverUrl(),
props.height, Math.floor(props.width * window.devicePixelRatio),
props.resizeMethod), Math.floor(props.height * window.devicePixelRatio),
props.resizeMethod,
false,
false,
),
}; };
} else if (props.fallbackUserId) { } else if (props.fallbackUserId) {
return { return {
@ -85,7 +90,7 @@ export default createReactClass({
if (viewUserOnClick) { if (viewUserOnClick) {
onClick = () => { onClick = () => {
dis.dispatch({ dis.dispatch({
action: 'view_user', action: Action.ViewUser,
member: this.props.member, member: this.props.member,
}); });
}; };

View file

@ -23,7 +23,7 @@ import createReactClass from 'create-react-class';
import {EventStatus} from 'matrix-js-sdk'; import {EventStatus} from 'matrix-js-sdk';
import {MatrixClientPeg} from '../../../MatrixClientPeg'; import {MatrixClientPeg} from '../../../MatrixClientPeg';
import dis from '../../../dispatcher'; import dis from '../../../dispatcher/dispatcher';
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
@ -116,11 +116,6 @@ export default createReactClass({
this.closeMenu(); this.closeMenu();
}, },
e2eInfoClicked: function() {
this.props.e2eInfoCallback();
this.closeMenu();
},
onReportEventClick: function() { onReportEventClick: function() {
const ReportEventDialog = sdk.getComponent("dialogs.ReportEventDialog"); const ReportEventDialog = sdk.getComponent("dialogs.ReportEventDialog");
Modal.createTrackedDialog('Report Event', '', 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; let reportEventButton;
if (mxEvent.getSender() !== me) { if (mxEvent.getSender() !== me) {
reportEventButton = ( reportEventButton = (
@ -500,7 +486,6 @@ export default createReactClass({
{ quoteButton } { quoteButton }
{ externalURLButton } { externalURLButton }
{ collapseReplyThread } { collapseReplyThread }
{ e2eInfo }
{ reportEventButton } { reportEventButton }
</div> </div>
); );

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