Merge branches 'develop' and 't3chguy/leaks' of github.com:matrix-org/matrix-react-sdk into t3chguy/leaks

 Conflicts:
	src/components/views/avatars/BaseAvatar.js
	test/components/views/messages/TextualBody-test.js
This commit is contained in:
Michael Telatynski 2020-05-23 11:12:58 +01:00
commit 880e16aaa2
222 changed files with 5383 additions and 1207 deletions

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

@ -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,11 @@
"@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/modernizr": "^3.5.3", "@types/modernizr": "^3.5.3",
"@types/qrcode": "^1.3.4", "@types/qrcode": "^1.3.4",
"@types/react": "16.9", "@types/react": "16.9",
"@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",
"chokidar": "^3.3.1", "chokidar": "^3.3.1",

View file

@ -41,6 +41,7 @@
@import "./views/auth/_CountryDropdown.scss"; @import "./views/auth/_CountryDropdown.scss";
@import "./views/auth/_InteractiveAuthEntryComponents.scss"; @import "./views/auth/_InteractiveAuthEntryComponents.scss";
@import "./views/auth/_LanguageSelector.scss"; @import "./views/auth/_LanguageSelector.scss";
@import "./views/auth/_PassphraseField.scss";
@import "./views/auth/_ServerConfig.scss"; @import "./views/auth/_ServerConfig.scss";
@import "./views/auth/_ServerTypeSelector.scss"; @import "./views/auth/_ServerTypeSelector.scss";
@import "./views/auth/_Welcome.scss"; @import "./views/auth/_Welcome.scss";
@ -114,6 +115,7 @@
@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/_SyntaxHighlight.scss"; @import "./views/elements/_SyntaxHighlight.scss";
@import "./views/elements/_TextWithTooltip.scss"; @import "./views/elements/_TextWithTooltip.scss";
@ -161,6 +163,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";
@ -203,6 +207,7 @@
@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

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

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

@ -146,27 +146,3 @@ limitations under the License.
.mx_AuthBody_spinner { .mx_AuthBody_spinner {
margin: 1em 0; margin: 1em 0;
} }
.mx_AuthBody_passwordScore {
width: 100%;
appearance: none;
height: 4px;
border: 0;
border-radius: 2px;
position: absolute;
top: -12px;
&::-moz-progress-bar {
border-radius: 2px;
background-color: $accent-color;
}
&::-webkit-progress-bar,
&::-webkit-progress-value {
border-radius: 2px;
}
&::-webkit-progress-value {
background-color: $accent-color;
}
}

View file

@ -0,0 +1,55 @@
/*
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.
*/
$PassphraseStrengthHigh: $accent-color;
$PassphraseStrengthMedium: $username-variant5-color;
$PassphraseStrengthLow: $notice-primary-color;
@define-mixin ProgressBarColour $colour {
color: $colour;
&::-moz-progress-bar {
background-color: $colour;
}
&::-webkit-progress-value {
background-color: $colour;
}
}
progress.mx_PassphraseField_progress {
appearance: none;
width: 100%;
border: 0;
height: 4px;
position: absolute;
top: -12px;
border-radius: 2px;
&::-moz-progress-bar {
border-radius: 2px;
}
&::-webkit-progress-bar,
&::-webkit-progress-value {
border-radius: 2px;
}
@mixin ProgressBarColour $PassphraseStrengthLow;
&[value="2"], &[value="3"] {
@mixin ProgressBarColour $PassphraseStrengthMedium;
}
&[value="4"] {
@mixin ProgressBarColour $PassphraseStrengthHigh;
}
}

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

@ -35,17 +35,6 @@ limitations under the License.
align-items: flex-start; align-items: flex-start;
} }
.mx_CreateKeyBackupDialog_passPhraseHelp {
flex: 1;
height: 85px;
margin-left: 20px;
font-size: 80%;
}
.mx_CreateKeyBackupDialog_passPhraseHelp progress {
width: 100%;
}
.mx_CreateKeyBackupDialog_passPhraseInput { .mx_CreateKeyBackupDialog_passPhraseInput {
flex: none; flex: none;
width: 250px; width: 250px;

View file

@ -68,17 +68,6 @@ limitations under the License.
margin-top: 0px; margin-top: 0px;
} }
.mx_CreateSecretStorageDialog_passPhraseHelp {
flex: 1;
height: 64px;
margin-left: 20px;
font-size: 80%;
}
.mx_CreateSecretStorageDialog_passPhraseHelp progress {
width: 100%;
}
.mx_CreateSecretStorageDialog_passPhraseMatch { .mx_CreateSecretStorageDialog_passPhraseMatch {
width: 200px; width: 200px;
margin-left: 20px; margin-left: 20px;

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

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

View file

@ -0,0 +1,214 @@
/*
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: 4;
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;
}
> .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;
top: -2px;
padding: 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: 3;
}
.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: 32px;
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,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 {

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,10 @@ 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";
/** /**
* Base class for classes that provide platform-specific functionality * Base class for classes that provide platform-specific functionality
@ -29,27 +28,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;
@ -84,22 +81,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 +106,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 +125,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 +137,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");
} }

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

@ -18,7 +18,7 @@ limitations under the License.
'use strict'; 'use strict';
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 * as sdk from './index'; import * as sdk from './index';
import { _t } from './languageHandler'; import { _t } from './languageHandler';

51
src/FontWatcher.js Normal file
View file

@ -0,0 +1,51 @@
/*
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 dis from './dispatcher/dispatcher';
import SettingsStore, {SettingLevel} from './settings/SettingsStore';
export class FontWatcher {
static MIN_SIZE = 13;
static MAX_SIZE = 20;
constructor() {
this._dispatcherRef = null;
}
start() {
this._setRootFontSize(SettingsStore.getValue("fontSize"));
this._dispatcherRef = dis.register(this._onAction);
}
stop() {
dis.unregister(this._dispatcherRef);
}
_onAction = (payload) => {
if (payload.action === 'update-font-size') {
this._setRootFontSize(payload.size);
}
};
_setRootFontSize = (size) => {
const fontSize = Math.max(Math.min(FontWatcher.MAX_SIZE, size), FontWatcher.MIN_SIZE);
if (fontSize != size) {
SettingsStore.setValue("fontSize", null, SettingLevel.Device, fontSize);
}
document.querySelector(":root").style.fontSize = fontSize + "px";
};
}

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

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

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,7 +21,7 @@ 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';

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

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

@ -15,17 +15,17 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from 'react'; import React, {createRef} from 'react';
import FileSaver from 'file-saver'; import FileSaver from 'file-saver';
import * as sdk from '../../../../index'; import * as sdk from '../../../../index';
import {MatrixClientPeg} from '../../../../MatrixClientPeg'; import {MatrixClientPeg} from '../../../../MatrixClientPeg';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { scorePassword } from '../../../../utils/PasswordScorer'; import {_t, _td} from '../../../../languageHandler';
import { _t } from '../../../../languageHandler';
import { accessSecretStorage } from '../../../../CrossSigningManager'; import { accessSecretStorage } from '../../../../CrossSigningManager';
import SettingsStore from '../../../../settings/SettingsStore'; import SettingsStore from '../../../../settings/SettingsStore';
import AccessibleButton from "../../../../components/views/elements/AccessibleButton"; import AccessibleButton from "../../../../components/views/elements/AccessibleButton";
import {copyNode} from "../../../../utils/strings"; import {copyNode} from "../../../../utils/strings";
import PassphraseField from "../../../../components/views/auth/PassphraseField";
const PHASE_PASSPHRASE = 0; const PHASE_PASSPHRASE = 0;
const PHASE_PASSPHRASE_CONFIRM = 1; const PHASE_PASSPHRASE_CONFIRM = 1;
@ -36,7 +36,6 @@ const PHASE_DONE = 5;
const PHASE_OPTOUT_CONFIRM = 6; const PHASE_OPTOUT_CONFIRM = 6;
const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc. const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc.
const PASSPHRASE_FEEDBACK_DELAY = 500; // How long after keystroke to offer passphrase feedback, ms.
/* /*
* Walks the user through the process of creating an e2e key backup * Walks the user through the process of creating an e2e key backup
@ -52,17 +51,18 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
this._recoveryKeyNode = null; this._recoveryKeyNode = null;
this._keyBackupInfo = null; this._keyBackupInfo = null;
this._setZxcvbnResultTimeout = null;
this.state = { this.state = {
secureSecretStorage: null, secureSecretStorage: null,
phase: PHASE_PASSPHRASE, phase: PHASE_PASSPHRASE,
passPhrase: '', passPhrase: '',
passPhraseValid: false,
passPhraseConfirm: '', passPhraseConfirm: '',
copied: false, copied: false,
downloaded: false, downloaded: false,
zxcvbnResult: null,
}; };
this._passphraseField = createRef();
} }
async componentDidMount() { async componentDidMount() {
@ -81,12 +81,6 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
} }
} }
componentWillUnmount() {
if (this._setZxcvbnResultTimeout !== null) {
clearTimeout(this._setZxcvbnResultTimeout);
}
}
_collectRecoveryKeyNode = (n) => { _collectRecoveryKeyNode = (n) => {
this._recoveryKeyNode = n; this._recoveryKeyNode = n;
} }
@ -180,22 +174,16 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
_onPassPhraseNextClick = async (e) => { _onPassPhraseNextClick = async (e) => {
e.preventDefault(); e.preventDefault();
if (!this._passphraseField.current) return; // unmounting
// If we're waiting for the timeout before updating the result at this point, await this._passphraseField.current.validate({ allowEmpty: false });
// skip ahead and do it now, otherwise we'll deny the attempt to proceed if (!this._passphraseField.current.state.valid) {
// even if the user entered a valid passphrase this._passphraseField.current.focus();
if (this._setZxcvbnResultTimeout !== null) { this._passphraseField.current.validate({ allowEmpty: false, focused: true });
clearTimeout(this._setZxcvbnResultTimeout); return;
this._setZxcvbnResultTimeout = null;
await new Promise((resolve) => {
this.setState({
zxcvbnResult: scorePassword(this.state.passPhrase),
}, resolve);
});
} }
if (this._passPhraseIsValid()) {
this.setState({phase: PHASE_PASSPHRASE_CONFIRM}); this.setState({phase: PHASE_PASSPHRASE_CONFIRM});
}
}; };
_onPassPhraseConfirmNextClick = async (e) => { _onPassPhraseConfirmNextClick = async (e) => {
@ -214,9 +202,9 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
_onSetAgainClick = () => { _onSetAgainClick = () => {
this.setState({ this.setState({
passPhrase: '', passPhrase: '',
passPhraseValid: false,
passPhraseConfirm: '', passPhraseConfirm: '',
phase: PHASE_PASSPHRASE, phase: PHASE_PASSPHRASE,
zxcvbnResult: null,
}); });
} }
@ -226,23 +214,16 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
}); });
} }
_onPassPhraseValidate = (result) => {
this.setState({
passPhraseValid: result.valid,
});
};
_onPassPhraseChange = (e) => { _onPassPhraseChange = (e) => {
this.setState({ this.setState({
passPhrase: e.target.value, passPhrase: e.target.value,
}); });
if (this._setZxcvbnResultTimeout !== null) {
clearTimeout(this._setZxcvbnResultTimeout);
}
this._setZxcvbnResultTimeout = setTimeout(() => {
this._setZxcvbnResultTimeout = null;
this.setState({
// precompute this and keep it in state: zxcvbn is fast but
// we use it in a couple of different places so no point recomputing
// it unnecessarily.
zxcvbnResult: scorePassword(this.state.passPhrase),
});
}, PASSPHRASE_FEEDBACK_DELAY);
} }
_onPassPhraseConfirmChange = (e) => { _onPassPhraseConfirmChange = (e) => {
@ -251,35 +232,9 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
}); });
} }
_passPhraseIsValid() {
return this.state.zxcvbnResult && this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE;
}
_renderPhasePassPhrase() { _renderPhasePassPhrase() {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
let strengthMeter;
let helpText;
if (this.state.zxcvbnResult) {
if (this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE) {
helpText = _t("Great! This recovery passphrase looks strong enough.");
} else {
const suggestions = [];
for (let i = 0; i < this.state.zxcvbnResult.feedback.suggestions.length; ++i) {
suggestions.push(<div key={i}>{this.state.zxcvbnResult.feedback.suggestions[i]}</div>);
}
const suggestionBlock = <div>{suggestions.length > 0 ? suggestions : _t("Keep going...")}</div>;
helpText = <div>
{this.state.zxcvbnResult.feedback.warning}
{suggestionBlock}
</div>;
}
strengthMeter = <div>
<progress max={PASSWORD_MIN_SCORE} value={this.state.zxcvbnResult.score} />
</div>;
}
return <form onSubmit={this._onPassPhraseNextClick}> return <form onSubmit={this._onPassPhraseNextClick}>
<p>{_t( <p>{_t(
"<b>Warning</b>: You should only set up key backup from a trusted computer.", {}, "<b>Warning</b>: You should only set up key backup from a trusted computer.", {},
@ -293,17 +248,19 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
<div className="mx_CreateKeyBackupDialog_primaryContainer"> <div className="mx_CreateKeyBackupDialog_primaryContainer">
<div className="mx_CreateKeyBackupDialog_passPhraseContainer"> <div className="mx_CreateKeyBackupDialog_passPhraseContainer">
<input type="password" <PassphraseField
onChange={this._onPassPhraseChange}
value={this.state.passPhrase}
className="mx_CreateKeyBackupDialog_passPhraseInput" className="mx_CreateKeyBackupDialog_passPhraseInput"
placeholder={_t("Enter a recovery passphrase...")} onChange={this._onPassPhraseChange}
minScore={PASSWORD_MIN_SCORE}
value={this.state.passPhrase}
onValidate={this._onPassPhraseValidate}
fieldRef={this._passphraseField}
autoFocus={true} autoFocus={true}
label={_td("Enter a recovery passphrase")}
labelEnterPassword={_td("Enter a recovery passphrase")}
labelStrongPassword={_td("Great! This recovery passphrase looks strong enough.")}
labelAllowedButUnsafe={_td("Great! This recovery passphrase looks strong enough.")}
/> />
<div className="mx_CreateKeyBackupDialog_passPhraseHelp">
{strengthMeter}
{helpText}
</div>
</div> </div>
</div> </div>
@ -311,7 +268,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
primaryButton={_t('Next')} primaryButton={_t('Next')}
onPrimaryButtonClick={this._onPassPhraseNextClick} onPrimaryButtonClick={this._onPassPhraseNextClick}
hasCancel={false} hasCancel={false}
disabled={!this._passPhraseIsValid()} disabled={!this.state.passPhraseValid}
/> />
<details> <details>

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

@ -15,17 +15,17 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from 'react'; import React, {createRef} 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 { scorePassword } from '../../../../utils/PasswordScorer';
import FileSaver from 'file-saver'; import FileSaver from 'file-saver';
import { _t } from '../../../../languageHandler'; import {_t, _td} from '../../../../languageHandler';
import Modal from '../../../../Modal'; import Modal from '../../../../Modal';
import { promptForBackupPassphrase } from '../../../../CrossSigningManager'; import { promptForBackupPassphrase } from '../../../../CrossSigningManager';
import {copyNode} from "../../../../utils/strings"; import {copyNode} from "../../../../utils/strings";
import {SSOAuthEntry} from "../../../../components/views/auth/InteractiveAuthEntryComponents"; import {SSOAuthEntry} from "../../../../components/views/auth/InteractiveAuthEntryComponents";
import PassphraseField from "../../../../components/views/auth/PassphraseField";
const PHASE_LOADING = 0; const PHASE_LOADING = 0;
const PHASE_LOADERROR = 1; const PHASE_LOADERROR = 1;
@ -39,7 +39,6 @@ const PHASE_DONE = 8;
const PHASE_CONFIRM_SKIP = 9; const PHASE_CONFIRM_SKIP = 9;
const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc. const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc.
const PASSPHRASE_FEEDBACK_DELAY = 500; // How long after keystroke to offer passphrase feedback, ms.
/* /*
* Walks the user through the process of creating a passphrase to guard Secure * Walks the user through the process of creating a passphrase to guard Secure
@ -62,16 +61,15 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
this._recoveryKey = null; this._recoveryKey = null;
this._recoveryKeyNode = null; this._recoveryKeyNode = null;
this._setZxcvbnResultTimeout = null;
this._backupKey = null; this._backupKey = null;
this.state = { this.state = {
phase: PHASE_LOADING, phase: PHASE_LOADING,
passPhrase: '', passPhrase: '',
passPhraseValid: false,
passPhraseConfirm: '', passPhraseConfirm: '',
copied: false, copied: false,
downloaded: false, downloaded: false,
zxcvbnResult: null,
backupInfo: null, backupInfo: null,
backupSigStatus: null, backupSigStatus: null,
// does the server offer a UI auth flow with just m.login.password // does the server offer a UI auth flow with just m.login.password
@ -83,6 +81,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
useKeyBackup: true, useKeyBackup: true,
}; };
this._passphraseField = createRef();
this._fetchBackupInfo(); this._fetchBackupInfo();
if (this.state.accountPassword) { if (this.state.accountPassword) {
// If we have an account password in memory, let's simplify and // If we have an account password in memory, let's simplify and
@ -99,9 +99,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
componentWillUnmount() { componentWillUnmount() {
MatrixClientPeg.get().removeListener('crypto.keyBackupStatus', this._onKeyBackupStatusChange); MatrixClientPeg.get().removeListener('crypto.keyBackupStatus', this._onKeyBackupStatusChange);
if (this._setZxcvbnResultTimeout !== null) {
clearTimeout(this._setZxcvbnResultTimeout);
}
} }
async _fetchBackupInfo() { async _fetchBackupInfo() {
@ -364,22 +361,16 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
_onPassPhraseNextClick = async (e) => { _onPassPhraseNextClick = async (e) => {
e.preventDefault(); e.preventDefault();
if (!this._passphraseField.current) return; // unmounting
// If we're waiting for the timeout before updating the result at this point, await this._passphraseField.current.validate({ allowEmpty: false });
// skip ahead and do it now, otherwise we'll deny the attempt to proceed if (!this._passphraseField.current.state.valid) {
// even if the user entered a valid passphrase this._passphraseField.current.focus();
if (this._setZxcvbnResultTimeout !== null) { this._passphraseField.current.validate({ allowEmpty: false, focused: true });
clearTimeout(this._setZxcvbnResultTimeout); return;
this._setZxcvbnResultTimeout = null;
await new Promise((resolve) => {
this.setState({
zxcvbnResult: scorePassword(this.state.passPhrase),
}, resolve);
});
} }
if (this._passPhraseIsValid()) {
this.setState({phase: PHASE_PASSPHRASE_CONFIRM}); this.setState({phase: PHASE_PASSPHRASE_CONFIRM});
}
}; };
_onPassPhraseConfirmNextClick = async (e) => { _onPassPhraseConfirmNextClick = async (e) => {
@ -399,9 +390,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
_onSetAgainClick = () => { _onSetAgainClick = () => {
this.setState({ this.setState({
passPhrase: '', passPhrase: '',
passPhraseValid: false,
passPhraseConfirm: '', passPhraseConfirm: '',
phase: PHASE_PASSPHRASE, phase: PHASE_PASSPHRASE,
zxcvbnResult: null,
}); });
} }
@ -411,23 +402,16 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
}); });
} }
_onPassPhraseValidate = (result) => {
this.setState({
passPhraseValid: result.valid,
});
};
_onPassPhraseChange = (e) => { _onPassPhraseChange = (e) => {
this.setState({ this.setState({
passPhrase: e.target.value, passPhrase: e.target.value,
}); });
if (this._setZxcvbnResultTimeout !== null) {
clearTimeout(this._setZxcvbnResultTimeout);
}
this._setZxcvbnResultTimeout = setTimeout(() => {
this._setZxcvbnResultTimeout = null;
this.setState({
// precompute this and keep it in state: zxcvbn is fast but
// we use it in a couple of different places so no point recomputing
// it unnecessarily.
zxcvbnResult: scorePassword(this.state.passPhrase),
});
}, PASSPHRASE_FEEDBACK_DELAY);
} }
_onPassPhraseConfirmChange = (e) => { _onPassPhraseConfirmChange = (e) => {
@ -436,10 +420,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
}); });
} }
_passPhraseIsValid() {
return this.state.zxcvbnResult && this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE;
}
_onAccountPasswordChange = (e) => { _onAccountPasswordChange = (e) => {
this.setState({ this.setState({
accountPassword: e.target.value, accountPassword: e.target.value,
@ -502,37 +482,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
_renderPhasePassPhrase() { _renderPhasePassPhrase() {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const Field = sdk.getComponent('views.elements.Field');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const LabelledToggleSwitch = sdk.getComponent('views.elements.LabelledToggleSwitch'); const LabelledToggleSwitch = sdk.getComponent('views.elements.LabelledToggleSwitch');
let strengthMeter;
let helpText;
if (this.state.zxcvbnResult) {
if (this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE) {
helpText = _t("Great! This recovery passphrase looks strong enough.");
} else {
// We take the warning from zxcvbn or failing that, the first
// suggestion. In practice The first is generally the most relevant
// and it's probably better to present the user with one thing to
// improve about their password than a whole collection - it can
// spit out a warning and multiple suggestions which starts getting
// very information-dense.
const suggestion = (
this.state.zxcvbnResult.feedback.warning ||
this.state.zxcvbnResult.feedback.suggestions[0]
);
const suggestionBlock = <div>{suggestion || _t("Keep going...")}</div>;
helpText = <div>
{suggestionBlock}
</div>;
}
strengthMeter = <div>
<progress max={PASSWORD_MIN_SCORE} value={this.state.zxcvbnResult.score} />
</div>;
}
return <form onSubmit={this._onPassPhraseNextClick}> return <form onSubmit={this._onPassPhraseNextClick}>
<p>{_t( <p>{_t(
"Set a recovery passphrase to secure encrypted information and recover it if you log out. " + "Set a recovery passphrase to secure encrypted information and recover it if you log out. " +
@ -540,19 +492,19 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
)}</p> )}</p>
<div className="mx_CreateSecretStorageDialog_passPhraseContainer"> <div className="mx_CreateSecretStorageDialog_passPhraseContainer">
<Field <PassphraseField
type="password"
className="mx_CreateSecretStorageDialog_passPhraseField" className="mx_CreateSecretStorageDialog_passPhraseField"
onChange={this._onPassPhraseChange} onChange={this._onPassPhraseChange}
minScore={PASSWORD_MIN_SCORE}
value={this.state.passPhrase} value={this.state.passPhrase}
label={_t("Enter a recovery passphrase")} onValidate={this._onPassPhraseValidate}
fieldRef={this._passphraseField}
autoFocus={true} autoFocus={true}
autoComplete="new-password" label={_td("Enter a recovery passphrase")}
labelEnterPassword={_td("Enter a recovery passphrase")}
labelStrongPassword={_td("Great! This recovery passphrase looks strong enough.")}
labelAllowedButUnsafe={_td("Great! This recovery passphrase looks strong enough.")}
/> />
<div className="mx_CreateSecretStorageDialog_passPhraseHelp">
{strengthMeter}
{helpText}
</div>
</div> </div>
<LabelledToggleSwitch <LabelledToggleSwitch
@ -564,7 +516,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
primaryButton={_t('Continue')} primaryButton={_t('Continue')}
onPrimaryButtonClick={this._onPassPhraseNextClick} onPrimaryButtonClick={this._onPassPhraseNextClick}
hasCancel={false} hasCancel={false}
disabled={!this._passPhraseIsValid()} disabled={!this.state.passPhraseValid}
> >
<button type="button" <button type="button"
onClick={this._onSkipSetupClick} onClick={this._onSkipSetupClick}

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

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, MatrixClientCreds} 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,8 @@ 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";
// 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.
@ -297,18 +298,18 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
}; };
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 () => { _updateServerNoticeEvents = async () => {
const roomLists = RoomListStore.getRoomLists(); const roomLists = RoomListStoreTempProxy.getRoomLists();
if (!roomLists['m.server_notice']) return []; if (!roomLists[DefaultTagID.ServerNotice]) return [];
const pinnedEvents = []; const pinnedEvents = [];
for (const room of roomLists['m.server_notice']) { 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;

View file

@ -22,7 +22,6 @@ 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
@ -34,13 +33,13 @@ 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';
@ -57,16 +56,18 @@ 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 "../../theme";
import { FontWatcher } from '../../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";
/** constants for MatrixChat.state.view */ /** constants for MatrixChat.state.view */
export enum Views { export enum Views {
@ -107,7 +108,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',
@ -216,6 +217,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);
@ -283,8 +285,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;
@ -367,6 +372,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 +619,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);
@ -1621,9 +1627,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 +1759,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,
}); });
} }

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'];
@ -109,14 +110,16 @@ export default class MessagePanel extends React.Component {
showReactions: PropTypes.bool, showReactions: 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
// display 'ghost' read markers that are animating away // display 'ghost' read markers that are animating away
ghostReadMarkers: [], ghostReadMarkers: [],
showTypingNotifications: SettingsStore.getValue("showTypingNotifications"), showTypingNotifications: SettingsStore.getValue("showTypingNotifications"),
useIRCLayout: this.useIRCLayout(SettingsStore.getValue("feature_irc_ui")),
}; };
// opaque readreceipt info for each userId; used by ReadReceiptMarker // opaque readreceipt info for each userId; used by ReadReceiptMarker
@ -169,6 +172,8 @@ export default class MessagePanel extends React.Component {
this._showTypingNotificationsWatcherRef = this._showTypingNotificationsWatcherRef =
SettingsStore.watchSetting("showTypingNotifications", null, this.onShowTypingNotificationsChange); SettingsStore.watchSetting("showTypingNotifications", null, this.onShowTypingNotificationsChange);
this._layoutWatcherRef = SettingsStore.watchSetting("feature_irc_ui", null, this.onLayoutChange);
} }
componentDidMount() { componentDidMount() {
@ -178,6 +183,7 @@ export default class MessagePanel extends React.Component {
componentWillUnmount() { componentWillUnmount() {
this._isMounted = false; this._isMounted = false;
SettingsStore.unwatchSetting(this._showTypingNotificationsWatcherRef); SettingsStore.unwatchSetting(this._showTypingNotificationsWatcherRef);
SettingsStore.unwatchSetting(this._layoutWatcherRef);
} }
componentDidUpdate(prevProps, prevState) { componentDidUpdate(prevProps, prevState) {
@ -196,6 +202,17 @@ export default class MessagePanel extends React.Component {
}); });
}; };
onLayoutChange = () => {
this.setState({
useIRCLayout: this.useIRCLayout(SettingsStore.getValue("feature_irc_ui")),
});
}
useIRCLayout(ircLayoutSelected) {
// if room is null we are not in a normal room list
return ircLayoutSelected && this.props.room;
}
/* get the DOM node representing the given event */ /* get the DOM node representing the given event */
getNodeForEventId(eventId) { getNodeForEventId(eventId) {
if (!this.eventNodes) { if (!this.eventNodes) {
@ -597,6 +614,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.state.useIRCLayout}
/> />
</TileErrorBoundary> </TileErrorBoundary>
</li>, </li>,
@ -779,6 +797,8 @@ export default class MessagePanel extends React.Component {
this.props.className, this.props.className,
{ {
"mx_MessagePanel_alwaysShowTimestamps": this.props.alwaysShowTimestamps, "mx_MessagePanel_alwaysShowTimestamps": this.props.alwaysShowTimestamps,
"mx_IRCLayout": this.state.useIRCLayout,
"mx_GroupLayout": !this.state.useIRCLayout,
}, },
); );
@ -792,6 +812,15 @@ export default class MessagePanel extends React.Component {
); );
} }
let ircResizer = null;
if (this.state.useIRCLayout) {
ircResizer = <IRCTimelineProfileResizer
minWidth={20}
maxWidth={600}
roomId={this.props.room ? this.props.roomroomId : null}
/>;
}
return ( return (
<ErrorBoundary> <ErrorBoundary>
<ScrollPanel <ScrollPanel
@ -804,6 +833,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';

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

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

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

@ -22,6 +22,7 @@ import {MatrixClientPeg} from "../../MatrixClientPeg";
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';
import HomePage from "./HomePage";
export default class UserView extends React.Component { export default class UserView extends React.Component {
static get propTypes() { static get propTypes() {
@ -79,7 +80,7 @@ export default class UserView extends React.Component {
const RightPanel = sdk.getComponent('structures.RightPanel'); const RightPanel = sdk.getComponent('structures.RightPanel');
const MainSplit = sdk.getComponent('structures.MainSplit'); const MainSplit = sdk.getComponent('structures.MainSplit');
const panel = <RightPanel user={this.state.member} />; const panel = <RightPanel user={this.state.member} />;
return (<MainSplit panel={panel}><div style={{flex: "1"}} /></MainSplit>); return (<MainSplit panel={panel}><HomePage /></MainSplit>);
} else { } else {
return (<div />); return (<div />);
} }

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
@ -243,10 +243,15 @@ export default createReactClass({
}); });
}; };
try { try {
// We do the first registration request ourselves to discover whether we need to
// do SSO instead. If we've already started the UI Auth process though, we don't
// need to.
if (!this.state.doingUIAuth) {
await this._makeRegisterRequest({}); await this._makeRegisterRequest({});
// This should never succeed since we specified an empty // This should never succeed since we specified an empty
// auth object. // auth object.
console.log("Expecting 401 from register request but got success!"); console.log("Expecting 401 from register request but got success!");
}
} catch (e) { } catch (e) {
if (e.httpStatus === 401) { if (e.httpStatus === 401) {
this.setState({ this.setState({

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

@ -412,14 +412,14 @@ export const EmailIdentityAuthEntry = createReactClass({
this.props.onPhaseChange(DEFAULT_PHASE); this.props.onPhaseChange(DEFAULT_PHASE);
}, },
getInitialState: function() {
return {
requestingToken: false,
};
},
render: function() { render: function() {
if (this.state.requestingToken) { // This component is now only displayed once the token has been requested,
// so we know the email has been sent. It can also get loaded after the user
// has clicked the validation link if the server takes a while to propagate
// the validation internally. If we're in the session spawned from clicking
// the validation link, we won't know the email address, so if we don't have it,
// assume that the link has been clicked and the server will realise when we poll.
if (this.props.inputs.emailAddress === undefined) {
const Loader = sdk.getComponent("elements.Spinner"); const Loader = sdk.getComponent("elements.Spinner");
return <Loader />; return <Loader />;
} else { } else {

View file

@ -0,0 +1,125 @@
/*
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 React, {PureComponent, RefCallback, RefObject} from "react";
import classNames from "classnames";
import zxcvbn from "zxcvbn";
import SdkConfig from "../../../SdkConfig";
import withValidation, {IFieldState, IValidationResult} from "../elements/Validation";
import {_t, _td} from "../../../languageHandler";
import Field from "../elements/Field";
interface IProps {
autoFocus?: boolean;
id?: string;
className?: string;
minScore: 0 | 1 | 2 | 3 | 4;
value: string;
fieldRef?: RefCallback<Field> | RefObject<Field>;
label?: string;
labelEnterPassword?: string;
labelStrongPassword?: string;
labelAllowedButUnsafe?: string;
onChange(ev: KeyboardEvent);
onValidate(result: IValidationResult);
}
interface IState {
complexity: zxcvbn.ZXCVBNResult;
}
class PassphraseField extends PureComponent<IProps, IState> {
static defaultProps = {
label: _td("Password"),
labelEnterPassword: _td("Enter password"),
labelStrongPassword: _td("Nice, strong password!"),
labelAllowedButUnsafe: _td("Password is allowed, but unsafe"),
};
state = { complexity: null };
public readonly validate = withValidation<this>({
description: function() {
const complexity = this.state.complexity;
const score = complexity ? complexity.score : 0;
return <progress className="mx_PassphraseField_progress" max={4} value={score} />;
},
rules: [
{
key: "required",
test: ({ value, allowEmpty }) => allowEmpty || !!value,
invalid: () => _t(this.props.labelEnterPassword),
},
{
key: "complexity",
test: async function({ value }) {
if (!value) {
return false;
}
const { scorePassword } = await import('../../../utils/PasswordScorer');
const complexity = scorePassword(value);
this.setState({ complexity });
const safe = complexity.score >= this.props.minScore;
const allowUnsafe = SdkConfig.get()["dangerously_allow_unsafe_and_insecure_passwords"];
return allowUnsafe || safe;
},
valid: function() {
// Unsafe passwords that are valid are only possible through a
// configuration flag. We'll print some helper text to signal
// to the user that their password is allowed, but unsafe.
if (this.state.complexity.score >= this.props.minScore) {
return _t(this.props.labelStrongPassword);
}
return _t(this.props.labelAllowedButUnsafe);
},
invalid: function() {
const complexity = this.state.complexity;
if (!complexity) {
return null;
}
const { feedback } = complexity;
return feedback.warning || feedback.suggestions[0] || _t("Keep going...");
},
},
],
});
onValidate = async (fieldState: IFieldState) => {
const result = await this.validate(fieldState);
this.props.onValidate(result);
return result;
};
render() {
return <Field
id={this.props.id}
autoFocus={this.props.autoFocus}
className={classNames("mx_PassphraseField", this.props.className)}
ref={this.props.fieldRef}
type="password"
autoComplete="new-password"
label={_t(this.props.label)}
value={this.props.value}
onChange={this.props.onChange}
onValidate={this.onValidate}
/>
}
}
export default PassphraseField;

View file

@ -29,6 +29,7 @@ import SdkConfig from '../../../SdkConfig';
import { SAFE_LOCALPART_REGEX } from '../../../Registration'; import { SAFE_LOCALPART_REGEX } from '../../../Registration';
import withValidation from '../elements/Validation'; import withValidation from '../elements/Validation';
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
import PassphraseField from "./PassphraseField";
const FIELD_EMAIL = 'field_email'; const FIELD_EMAIL = 'field_email';
const FIELD_PHONE_NUMBER = 'field_phone_number'; const FIELD_PHONE_NUMBER = 'field_phone_number';
@ -78,7 +79,6 @@ export default createReactClass({
password: this.props.defaultPassword || "", password: this.props.defaultPassword || "",
passwordConfirm: this.props.defaultPassword || "", passwordConfirm: this.props.defaultPassword || "",
passwordComplexity: null, passwordComplexity: null,
passwordSafe: false,
}; };
}, },
@ -264,65 +264,10 @@ export default createReactClass({
}); });
}, },
async onPasswordValidate(fieldState) { onPasswordValidate(result) {
const result = await this.validatePasswordRules(fieldState);
this.markFieldValid(FIELD_PASSWORD, result.valid); this.markFieldValid(FIELD_PASSWORD, result.valid);
return result;
}, },
validatePasswordRules: withValidation({
description: function() {
const complexity = this.state.passwordComplexity;
const score = complexity ? complexity.score : 0;
return <progress
className="mx_AuthBody_passwordScore"
max={PASSWORD_MIN_SCORE}
value={score}
/>;
},
rules: [
{
key: "required",
test: ({ value, allowEmpty }) => allowEmpty || !!value,
invalid: () => _t("Enter password"),
},
{
key: "complexity",
test: async function({ value }) {
if (!value) {
return false;
}
const { scorePassword } = await import('../../../utils/PasswordScorer');
const complexity = scorePassword(value);
const safe = complexity.score >= PASSWORD_MIN_SCORE;
const allowUnsafe = SdkConfig.get()["dangerously_allow_unsafe_and_insecure_passwords"];
this.setState({
passwordComplexity: complexity,
passwordSafe: safe,
});
return allowUnsafe || safe;
},
valid: function() {
// Unsafe passwords that are valid are only possible through a
// configuration flag. We'll print some helper text to signal
// to the user that their password is allowed, but unsafe.
if (!this.state.passwordSafe) {
return _t("Password is allowed, but unsafe");
}
return _t("Nice, strong password!");
},
invalid: function() {
const complexity = this.state.passwordComplexity;
if (!complexity) {
return null;
}
const { feedback } = complexity;
return feedback.warning || feedback.suggestions[0] || _t("Keep going...");
},
},
],
}),
onPasswordConfirmChange(ev) { onPasswordConfirmChange(ev) {
this.setState({ this.setState({
passwordConfirm: ev.target.value, passwordConfirm: ev.target.value,
@ -484,13 +429,10 @@ export default createReactClass({
}, },
renderPassword() { renderPassword() {
const Field = sdk.getComponent('elements.Field'); return <PassphraseField
return <Field
id="mx_RegistrationForm_password" id="mx_RegistrationForm_password"
ref={field => this[FIELD_PASSWORD] = field} fieldRef={field => this[FIELD_PASSWORD] = field}
type="password" minScore={PASSWORD_MIN_SCORE}
autoComplete="new-password"
label={_t("Password")}
value={this.state.password} value={this.state.password}
onChange={this.onPasswordChange} onChange={this.onPasswordChange}
onValidate={this.onPasswordValidate} onValidate={this.onPasswordValidate}

View file

@ -24,7 +24,7 @@ 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 {useEventEmitter} from "../../../hooks/useEventEmitter"; import {useEventEmitter} from "../../../hooks/useEventEmitter";
import toRem from "../../../utils/rem"; import {toPx} from "../../../utils/units";
const useImageUrl = ({url, urls, idName, name, defaultToInitialLetter}) => { const useImageUrl = ({url, urls, idName, name, defaultToInitialLetter}) => {
const [imageUrls, setUrls] = useState([]); const [imageUrls, setUrls] = useState([]);
@ -105,9 +105,9 @@ const BaseAvatar = (props) => {
className="mx_BaseAvatar_initial" className="mx_BaseAvatar_initial"
aria-hidden="true" aria-hidden="true"
style={{ style={{
fontSize: toRem(width * 0.65), fontSize: toPx(width * 0.65),
width: toRem(width), width: toPx(width),
lineHeight: toRem(height), lineHeight: toPx(height),
}} }}
> >
{ initialLetter } { initialLetter }
@ -121,8 +121,8 @@ const BaseAvatar = (props) => {
title={title} title={title}
onError={onError} onError={onError}
style={{ style={{
width: toRem(width), width: toPx(width),
height: toRem(height), height: toPx(height),
}} }}
aria-hidden="true" /> aria-hidden="true" />
); );
@ -159,8 +159,8 @@ const BaseAvatar = (props) => {
onClick={onClick} onClick={onClick}
onError={onError} onError={onError}
style={{ style={{
width: toRem(width), width: toPx(width),
height: toRem(height), height: toPx(height),
}} }}
title={title} alt="" title={title} alt=""
inputRef={inputRef} inputRef={inputRef}
@ -173,8 +173,8 @@ const BaseAvatar = (props) => {
src={imageUrl} src={imageUrl}
onError={onError} onError={onError}
style={{ style={{
width: toRem(width), width: toPx(width),
height: toRem(height), height: toPx(height),
}} }}
title={title} alt="" title={title} alt=""
ref={inputRef} ref={inputRef}

View file

@ -20,7 +20,8 @@ import PropTypes from 'prop-types';
import createReactClass from 'create-react-class'; import createReactClass from 'create-react-class';
import * as Avatar from '../../../Avatar'; 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";
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,
}, },
@ -85,7 +86,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';

View file

@ -24,7 +24,7 @@ import classNames from 'classnames';
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import { _t, _td } from '../../../languageHandler'; import { _t, _td } from '../../../languageHandler';
import {MatrixClientPeg} from '../../../MatrixClientPeg'; import {MatrixClientPeg} from '../../../MatrixClientPeg';
import dis from '../../../dispatcher'; import dis from '../../../dispatcher/dispatcher';
import DMRoomMap from '../../../utils/DMRoomMap'; import DMRoomMap from '../../../utils/DMRoomMap';
import * as Rooms from '../../../Rooms'; import * as Rooms from '../../../Rooms';
import * as RoomNotifs from '../../../RoomNotifs'; import * as RoomNotifs from '../../../RoomNotifs';

View file

@ -18,7 +18,7 @@ limitations under the License.
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import dis from '../../../dispatcher'; import dis from '../../../dispatcher/dispatcher';
import TagOrderActions from '../../../actions/TagOrderActions'; import TagOrderActions from '../../../actions/TagOrderActions';
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import {MenuItem} from "../../structures/ContextMenu"; import {MenuItem} from "../../structures/ContextMenu";

View file

@ -17,7 +17,7 @@ limitations under the License.
import React from 'react'; import React from 'react';
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 LogoutDialog from "../dialogs/LogoutDialog"; import LogoutDialog from "../dialogs/LogoutDialog";
import Modal from "../../../Modal"; import Modal from "../../../Modal";
@ -27,6 +27,7 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg';
import {MenuItem} from "../../structures/ContextMenu"; import {MenuItem} from "../../structures/ContextMenu";
import * as sdk from "../../../index"; import * as sdk from "../../../index";
import {getHomePageUrl} from "../../../utils/pages"; import {getHomePageUrl} from "../../../utils/pages";
import {Action} from "../../../dispatcher/actions";
export default class TopLeftMenu extends React.Component { export default class TopLeftMenu extends React.Component {
static propTypes = { static propTypes = {
@ -134,7 +135,7 @@ export default class TopLeftMenu extends React.Component {
} }
openSettings() { openSettings() {
dis.dispatch({action: 'view_user_settings'}); dis.fire(Action.ViewUserSettings);
this.closeMenu(); this.closeMenu();
} }

View file

@ -24,7 +24,7 @@ import createReactClass from 'create-react-class';
import { _t, _td } from '../../../languageHandler'; import { _t, _td } from '../../../languageHandler';
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 { addressTypes, getAddressType } from '../../../UserAddress.js'; import { addressTypes, getAddressType } from '../../../UserAddress.js';
import GroupStore from '../../../stores/GroupStore'; import GroupStore from '../../../stores/GroupStore';
import * as Email from '../../../email'; import * as Email from '../../../email';
@ -33,6 +33,7 @@ import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from '../../../
import { abbreviateUrl } from '../../../utils/UrlUtils'; import { abbreviateUrl } from '../../../utils/UrlUtils';
import {sleep} from "../../../utils/promise"; import {sleep} from "../../../utils/promise";
import {Key} from "../../../Keyboard"; import {Key} from "../../../Keyboard";
import {Action} from "../../../dispatcher/actions";
const TRUNCATE_QUERY_LIST = 40; const TRUNCATE_QUERY_LIST = 40;
const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200; const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200;
@ -615,7 +616,7 @@ export default createReactClass({
onManageSettingsClick(e) { onManageSettingsClick(e) {
e.preventDefault(); e.preventDefault();
dis.dispatch({ action: 'view_user_settings' }); dis.fire(Action.ViewUserSettings);
this.onCancel(); this.onCancel();
}, },

View file

@ -18,7 +18,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 * 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 {MatrixClientPeg} from '../../../MatrixClientPeg'; import {MatrixClientPeg} from '../../../MatrixClientPeg';

View file

@ -16,7 +16,7 @@ limitations under the License.
import React from 'react'; import React from 'react';
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';

View file

@ -25,7 +25,7 @@ import * as FormattingUtils from '../../../utils/FormattingUtils';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import {verificationMethods} from 'matrix-js-sdk/src/crypto'; import {verificationMethods} from 'matrix-js-sdk/src/crypto';
import {ensureDMExists} from "../../../createRoom"; import {ensureDMExists} from "../../../createRoom";
import dis from "../../../dispatcher"; import dis from "../../../dispatcher/dispatcher";
import SettingsStore from '../../../settings/SettingsStore'; import SettingsStore from '../../../settings/SettingsStore';
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";
import VerificationQREmojiOptions from "../verification/VerificationQREmojiOptions"; import VerificationQREmojiOptions from "../verification/VerificationQREmojiOptions";

View file

@ -18,7 +18,8 @@ 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 {Action} from "../../../dispatcher/actions";
export default class IntegrationsDisabledDialog extends React.Component { export default class IntegrationsDisabledDialog extends React.Component {
static propTypes = { static propTypes = {
@ -31,7 +32,7 @@ export default class IntegrationsDisabledDialog extends React.Component {
_onOpenSettingsClick = () => { _onOpenSettingsClick = () => {
this.props.onFinished(); this.props.onFinished();
dis.dispatch({action: "view_user_settings"}); dis.fire(Action.ViewUserSettings);
}; };
render() { render() {

View file

@ -27,15 +27,17 @@ import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
import * as Email from "../../../email"; import * as Email from "../../../email";
import {getDefaultIdentityServerUrl, useDefaultIdentityServer} from "../../../utils/IdentityServerUtils"; import {getDefaultIdentityServerUrl, useDefaultIdentityServer} from "../../../utils/IdentityServerUtils";
import {abbreviateUrl} from "../../../utils/UrlUtils"; import {abbreviateUrl} from "../../../utils/UrlUtils";
import dis from "../../../dispatcher"; import dis from "../../../dispatcher/dispatcher";
import IdentityAuthClient from "../../../IdentityAuthClient"; import IdentityAuthClient from "../../../IdentityAuthClient";
import Modal from "../../../Modal"; import Modal from "../../../Modal";
import {humanizeTime} from "../../../utils/humanize"; import {humanizeTime} from "../../../utils/humanize";
import createRoom, {canEncryptToAllUsers} from "../../../createRoom"; import createRoom, {canEncryptToAllUsers} from "../../../createRoom";
import {inviteMultipleToRoom} from "../../../RoomInvite"; import {inviteMultipleToRoom} from "../../../RoomInvite";
import SettingsStore from '../../../settings/SettingsStore'; import SettingsStore from '../../../settings/SettingsStore';
import RoomListStore, {TAG_DM} from "../../../stores/RoomListStore";
import {Key} from "../../../Keyboard"; import {Key} from "../../../Keyboard";
import {Action} from "../../../dispatcher/actions";
import {RoomListStoreTempProxy} from "../../../stores/room-list/RoomListStoreTempProxy";
import {DefaultTagID} from "../../../stores/room-list/models";
export const KIND_DM = "dm"; export const KIND_DM = "dm";
export const KIND_INVITE = "invite"; export const KIND_INVITE = "invite";
@ -343,10 +345,10 @@ export default class InviteDialog extends React.PureComponent {
_buildRecents(excludedTargetIds: Set<string>): {userId: string, user: RoomMember, lastActive: number} { _buildRecents(excludedTargetIds: Set<string>): {userId: string, user: RoomMember, lastActive: number} {
const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals(); // map of userId => js-sdk Room const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals(); // map of userId => js-sdk Room
// Also pull in all the rooms tagged as TAG_DM so we don't miss anything. Sometimes the // Also pull in all the rooms tagged as DefaultTagID.DM so we don't miss anything. Sometimes the
// room list doesn't tag the room for the DMRoomMap, but does for the room list. // room list doesn't tag the room for the DMRoomMap, but does for the room list.
const taggedRooms = RoomListStore.getRoomLists(); const taggedRooms = RoomListStoreTempProxy.getRoomLists();
const dmTaggedRooms = taggedRooms[TAG_DM]; const dmTaggedRooms = taggedRooms[DefaultTagID.DM];
const myUserId = MatrixClientPeg.get().getUserId(); const myUserId = MatrixClientPeg.get().getUserId();
for (const dmRoom of dmTaggedRooms) { for (const dmRoom of dmTaggedRooms) {
const otherMembers = dmRoom.getJoinedMembers().filter(u => u.userId !== myUserId); const otherMembers = dmRoom.getJoinedMembers().filter(u => u.userId !== myUserId);
@ -902,7 +904,7 @@ export default class InviteDialog extends React.PureComponent {
_onManageSettingsClick = (e) => { _onManageSettingsClick = (e) => {
e.preventDefault(); e.preventDefault();
dis.dispatch({ action: 'view_user_settings' }); dis.fire(Action.ViewUserSettings);
this.props.onFinished(); this.props.onFinished();
}; };

View file

@ -18,7 +18,7 @@ limitations under the License.
import React from 'react'; import React from 'react';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
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 {MatrixClientPeg} from '../../../MatrixClientPeg'; import {MatrixClientPeg} from '../../../MatrixClientPeg';

View file

@ -27,7 +27,7 @@ import NotificationSettingsTab from "../settings/tabs/room/NotificationSettingsT
import BridgeSettingsTab from "../settings/tabs/room/BridgeSettingsTab"; import BridgeSettingsTab from "../settings/tabs/room/BridgeSettingsTab";
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 SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
export default class RoomSettingsDialog extends React.Component { export default class RoomSettingsDialog extends React.Component {

View file

@ -22,6 +22,7 @@ import {_t, _td} from "../../../languageHandler";
import GeneralUserSettingsTab from "../settings/tabs/user/GeneralUserSettingsTab"; import GeneralUserSettingsTab from "../settings/tabs/user/GeneralUserSettingsTab";
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import LabsUserSettingsTab from "../settings/tabs/user/LabsUserSettingsTab"; import LabsUserSettingsTab from "../settings/tabs/user/LabsUserSettingsTab";
import AppearanceUserSettingsTab from "../settings/tabs/user/AppearanceUserSettingsTab";
import SecurityUserSettingsTab from "../settings/tabs/user/SecurityUserSettingsTab"; import SecurityUserSettingsTab from "../settings/tabs/user/SecurityUserSettingsTab";
import NotificationUserSettingsTab from "../settings/tabs/user/NotificationUserSettingsTab"; import NotificationUserSettingsTab from "../settings/tabs/user/NotificationUserSettingsTab";
import PreferencesUserSettingsTab from "../settings/tabs/user/PreferencesUserSettingsTab"; import PreferencesUserSettingsTab from "../settings/tabs/user/PreferencesUserSettingsTab";
@ -66,6 +67,11 @@ export default class UserSettingsDialog extends React.Component {
"mx_UserSettingsDialog_settingsIcon", "mx_UserSettingsDialog_settingsIcon",
<GeneralUserSettingsTab closeSettingsFn={this.props.onFinished} />, <GeneralUserSettingsTab closeSettingsFn={this.props.onFinished} />,
)); ));
tabs.push(new Tab(
_td("Appearance"),
"mx_UserSettingsDialog_appearanceIcon",
<AppearanceUserSettingsTab />,
));
tabs.push(new Tab( tabs.push(new Tab(
_td("Flair"), _td("Flair"),
"mx_UserSettingsDialog_flairIcon", "mx_UserSettingsDialog_flairIcon",

View file

@ -201,7 +201,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
// `accessSecretStorage` may prompt for storage access as needed. // `accessSecretStorage` may prompt for storage access as needed.
const recoverInfo = await accessSecretStorage(async () => { const recoverInfo = await accessSecretStorage(async () => {
return MatrixClientPeg.get().restoreKeyBackupWithSecretStorage( return MatrixClientPeg.get().restoreKeyBackupWithSecretStorage(
this.state.backupInfo, this.state.backupInfo, undefined, undefined,
{ progressCallback: this._progressCallback }, { progressCallback: this._progressCallback },
); );
}); });

View file

@ -18,7 +18,7 @@ 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 AccessibleButton from './AccessibleButton'; import AccessibleButton from './AccessibleButton';
import dis from '../../../dispatcher'; import dis from '../../../dispatcher/dispatcher';
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import Analytics from '../../../Analytics'; import Analytics from '../../../Analytics';

View file

@ -31,7 +31,7 @@ import AppPermission from './AppPermission';
import AppWarning from './AppWarning'; import AppWarning from './AppWarning';
import MessageSpinner from './MessageSpinner'; import MessageSpinner from './MessageSpinner';
import WidgetUtils from '../../../utils/WidgetUtils'; import WidgetUtils from '../../../utils/WidgetUtils';
import dis from '../../../dispatcher'; import dis from '../../../dispatcher/dispatcher';
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore'; import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
import classNames from 'classnames'; import classNames from 'classnames';
import {IntegrationManagers} from "../../../integrations/IntegrationManagers"; import {IntegrationManagers} from "../../../integrations/IntegrationManagers";

View file

@ -0,0 +1,84 @@
/*
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 React from 'react';
interface IProps {
className: string,
dragFunc: (currentLocation: ILocationState, event: MouseEvent) => ILocationState,
onMouseUp: (event: MouseEvent) => void,
}
interface IState {
onMouseMove: (event: MouseEvent) => void,
onMouseUp: (event: MouseEvent) => void,
location: ILocationState,
}
export interface ILocationState {
currentX: number,
currentY: number,
}
export default class Draggable extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
onMouseMove: this.onMouseMove.bind(this),
onMouseUp: this.onMouseUp.bind(this),
location: {
currentX: 0,
currentY: 0,
},
};
}
private onMouseDown = (event: MouseEvent): void => {
this.setState({
location: {
currentX: event.clientX,
currentY: event.clientY,
},
});
document.addEventListener("mousemove", this.state.onMouseMove);
document.addEventListener("mouseup", this.state.onMouseUp);
console.log("Mouse down")
}
private onMouseUp = (event: MouseEvent): void => {
document.removeEventListener("mousemove", this.state.onMouseMove);
document.removeEventListener("mouseup", this.state.onMouseUp);
this.props.onMouseUp(event);
console.log("Mouse up")
}
private onMouseMove(event: MouseEvent): void {
console.log("Mouse Move")
const newLocation = this.props.dragFunc(this.state.location, event);
this.setState({
location: newLocation,
});
}
render() {
return <div className={this.props.className} onMouseDown={this.onMouseDown.bind(this)} />
}
}

View file

@ -19,7 +19,7 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import FlairStore from '../../../stores/FlairStore'; import FlairStore from '../../../stores/FlairStore';
import dis from '../../../dispatcher'; import dis from '../../../dispatcher/dispatcher';
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";

View file

@ -0,0 +1,88 @@
/*
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 React from 'react';
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
import Draggable, {ILocationState} from './Draggable';
interface IProps {
// Current room
roomId: string,
minWidth: number,
maxWidth: number,
};
interface IState {
width: number,
IRCLayoutRoot: HTMLElement,
};
export default class IRCTimelineProfileResizer extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
width: SettingsStore.getValue("ircDisplayNameWidth", this.props.roomId),
IRCLayoutRoot: null,
}
};
componentDidMount() {
this.setState({
IRCLayoutRoot: document.querySelector(".mx_IRCLayout") as HTMLElement,
}, () => this.updateCSSWidth(this.state.width))
}
private dragFunc = (location: ILocationState, event: React.MouseEvent<Element, MouseEvent>): ILocationState => {
const offset = event.clientX - location.currentX;
const newWidth = this.state.width + offset;
console.log({offset})
// If we're trying to go smaller than min width, don't.
if (newWidth < this.props.minWidth) {
return location;
}
if (newWidth > this.props.maxWidth) {
return location;
}
this.setState({
width: newWidth,
});
this.updateCSSWidth.bind(this)(newWidth);
return {
currentX: event.clientX,
currentY: location.currentY,
}
}
private updateCSSWidth(newWidth: number) {
this.state.IRCLayoutRoot.style.setProperty("--name-width", newWidth + "px");
}
private onMoueUp(event: MouseEvent) {
if (this.props.roomId) {
SettingsStore.setValue("ircDisplayNameWidth", this.props.roomId, SettingLevel.ROOM_DEVICE, this.state.width);
}
}
render() {
return <Draggable className="mx_ProfileResizer" dragFunc={this.dragFunc.bind(this)} onMouseUp={this.onMoueUp.bind(this)}/>
}
};

View file

@ -20,7 +20,7 @@ import PropTypes from 'prop-types';
import ResizeObserver from 'resize-observer-polyfill'; import ResizeObserver from 'resize-observer-polyfill';
import dis from '../../../dispatcher'; import dis from '../../../dispatcher/dispatcher';
// Shamelessly ripped off Modal.js. There's probably a better way // Shamelessly ripped off Modal.js. There's probably a better way
// of doing reusable widgets like dialog boxes & menus where we go and // of doing reusable widgets like dialog boxes & menus where we go and
@ -156,16 +156,70 @@ export default class PersistedElement extends React.Component {
child.style.display = visible ? 'block' : 'none'; child.style.display = visible ? 'block' : 'none';
} }
/*
* Clip element bounding rectangle to that of the parent elements.
* This is not a full visibility check, but prevents the persisted
* element from overflowing parent containers when inside a scrolled
* area.
*/
_getClippedBoundingClientRect(element) {
let parentElement = element.parentElement;
let rect = element.getBoundingClientRect();
rect = new DOMRect(rect.left, rect.top, rect.width, rect.height);
while (parentElement) {
const parentRect = parentElement.getBoundingClientRect();
if (parentRect.left > rect.left) {
rect.width = rect.width - (parentRect.left - rect.left);
rect.x = parentRect.x;
}
if (parentRect.top > rect.top) {
rect.height = rect.height - (parentRect.top - rect.top);
rect.y = parentRect.y;
}
if (parentRect.right < rect.right) {
rect.width = rect.width - (rect.right - parentRect.right);
}
if (parentRect.bottom < rect.bottom) {
rect.height = rect.height - (rect.bottom - parentRect.bottom);
}
parentElement = parentElement.parentElement;
}
if (rect.width < 0) rect.width = 0;
if (rect.height < 0) rect.height = 0;
return rect;
}
updateChildPosition(child, parent) { updateChildPosition(child, parent) {
if (!child || !parent) return; if (!child || !parent) return;
const parentRect = parent.getBoundingClientRect(); const parentRect = parent.getBoundingClientRect();
const clipRect = this._getClippedBoundingClientRect(parent);
Object.assign(child.parentElement.style, {
position: 'absolute',
top: clipRect.top + 'px',
left: clipRect.left + 'px',
width: clipRect.width + 'px',
height: clipRect.height + 'px',
overflow: "hidden",
});
Object.assign(child.style, { Object.assign(child.style, {
position: 'absolute', position: 'absolute',
top: parentRect.top + 'px', top: (parentRect.top - clipRect.top) + 'px',
left: parentRect.left + 'px', left: (parentRect.left - clipRect.left) + 'px',
width: parentRect.width + 'px', width: parentRect.width + 'px',
height: parentRect.height + 'px', height: parentRect.height + 'px',
overflow: "hidden",
}); });
} }

View file

@ -18,7 +18,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 classNames from 'classnames'; import classNames from 'classnames';
import { Room, RoomMember } from 'matrix-js-sdk'; import { Room, RoomMember } from 'matrix-js-sdk';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
@ -26,6 +26,7 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg';
import FlairStore from "../../../stores/FlairStore"; import FlairStore from "../../../stores/FlairStore";
import {getPrimaryPermalinkEntity} from "../../../utils/permalinks/Permalinks"; import {getPrimaryPermalinkEntity} from "../../../utils/permalinks/Permalinks";
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {Action} from "../../../dispatcher/actions";
// For URLs of matrix.to links in the timeline which have been reformatted by // For URLs of matrix.to links in the timeline which have been reformatted by
// HttpUtils transformTags to relative links. This excludes event URLs (with `[^\/]*`) // HttpUtils transformTags to relative links. This excludes event URLs (with `[^\/]*`)
@ -191,7 +192,7 @@ const Pill = createReactClass({
onUserPillClicked: function() { onUserPillClicked: function() {
dis.dispatch({ dis.dispatch({
action: 'view_user', action: Action.ViewUser,
member: this.state.member, member: this.state.member,
}); });
}, },

View file

@ -19,7 +19,7 @@ import React from 'react';
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import {_t} from '../../../languageHandler'; import {_t} from '../../../languageHandler';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import dis from '../../../dispatcher'; import dis from '../../../dispatcher/dispatcher';
import {wantsDateSeparator} from '../../../DateUtils'; import {wantsDateSeparator} from '../../../DateUtils';
import {MatrixEvent} from 'matrix-js-sdk'; import {MatrixEvent} from 'matrix-js-sdk';
import {makeUserPermalink, RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks"; import {makeUserPermalink, RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks";
@ -37,6 +37,8 @@ export default class ReplyThread extends React.Component {
// called when the ReplyThread contents has changed, including EventTiles thereof // called when the ReplyThread contents has changed, including EventTiles thereof
onHeightChanged: PropTypes.func.isRequired, onHeightChanged: PropTypes.func.isRequired,
permalinkCreator: PropTypes.instanceOf(RoomPermalinkCreator).isRequired, permalinkCreator: PropTypes.instanceOf(RoomPermalinkCreator).isRequired,
// Specifies which layout to use.
useIRCLayout: PropTypes.bool,
}; };
static contextType = MatrixClientContext; static contextType = MatrixClientContext;
@ -176,12 +178,17 @@ export default class ReplyThread extends React.Component {
}; };
} }
static makeThread(parentEv, onHeightChanged, permalinkCreator, ref) { static makeThread(parentEv, onHeightChanged, permalinkCreator, ref, useIRCLayout) {
if (!ReplyThread.getParentEventId(parentEv)) { if (!ReplyThread.getParentEventId(parentEv)) {
return <div />; return <div className="mx_ReplyThread_wrapper_empty" />;
} }
return <ReplyThread parentEv={parentEv} onHeightChanged={onHeightChanged} return <ReplyThread
ref={ref} permalinkCreator={permalinkCreator} />; parentEv={parentEv}
onHeightChanged={onHeightChanged}
ref={ref}
permalinkCreator={permalinkCreator}
useIRCLayout={useIRCLayout}
/>;
} }
componentDidMount() { componentDidMount() {
@ -331,11 +338,13 @@ export default class ReplyThread extends React.Component {
onHeightChanged={this.props.onHeightChanged} onHeightChanged={this.props.onHeightChanged}
permalinkCreator={this.props.permalinkCreator} permalinkCreator={this.props.permalinkCreator}
isRedacted={ev.isRedacted()} isRedacted={ev.isRedacted()}
isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")} /> isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")}
useIRCLayout={this.props.useIRCLayout}
/>
</blockquote>; </blockquote>;
}); });
return <div> return <div className="mx_ReplyThread_wrapper">
<div>{ header }</div> <div>{ header }</div>
<div>{ evTiles }</div> <div>{ evTiles }</div>
</div>; </div>;

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