Merge remote-tracking branch 'origin/develop' into hs/bridge-info-pretty
This commit is contained in:
commit
7c0a461cbb
124 changed files with 4572 additions and 1159 deletions
|
@ -1,8 +1,10 @@
|
||||||
steps:
|
steps:
|
||||||
- label: ":eslint: JS Lint"
|
- label: ":eslint: JS Lint"
|
||||||
command:
|
command:
|
||||||
|
# We fetch the develop js-sdk to get our latest eslint rules
|
||||||
- "echo '--- Install js-sdk'"
|
- "echo '--- Install js-sdk'"
|
||||||
- "./scripts/ci/install-deps.sh"
|
- "./scripts/ci/install-deps.sh --ignore-scripts"
|
||||||
|
- "echo '+++ Lint'"
|
||||||
- "yarn lint:js"
|
- "yarn lint:js"
|
||||||
plugins:
|
plugins:
|
||||||
- docker#v3.0.1:
|
- docker#v3.0.1:
|
||||||
|
@ -10,8 +12,9 @@ steps:
|
||||||
|
|
||||||
- label: ":eslint: TS Lint"
|
- label: ":eslint: TS Lint"
|
||||||
command:
|
command:
|
||||||
- "echo '--- Install js-sdk'"
|
- "echo '--- Install'"
|
||||||
- "./scripts/ci/install-deps.sh"
|
- "yarn install --ignore-scripts"
|
||||||
|
- "echo '+++ Lint'"
|
||||||
- "yarn lint:ts"
|
- "yarn lint:ts"
|
||||||
plugins:
|
plugins:
|
||||||
- docker#v3.0.1:
|
- docker#v3.0.1:
|
||||||
|
@ -19,12 +22,21 @@ steps:
|
||||||
|
|
||||||
- label: ":eslint: Types Lint"
|
- label: ":eslint: Types Lint"
|
||||||
command:
|
command:
|
||||||
- "echo '--- Install js-sdk'"
|
- "echo '--- Install'"
|
||||||
- "./scripts/ci/install-deps.sh"
|
- "yarn install --ignore-scripts"
|
||||||
|
- "echo '+++ Lint'"
|
||||||
- "yarn lint:types"
|
- "yarn lint:types"
|
||||||
plugins:
|
plugins:
|
||||||
- docker#v3.0.1:
|
- docker#v3.0.1:
|
||||||
image: "node:12"
|
image: "node:12"
|
||||||
|
- label: ":stylelint: Style Lint"
|
||||||
|
command:
|
||||||
|
- "echo '--- Install'"
|
||||||
|
- "yarn install --ignore-scripts"
|
||||||
|
- "yarn lint:style"
|
||||||
|
plugins:
|
||||||
|
- docker#v3.0.1:
|
||||||
|
image: "node:12"
|
||||||
|
|
||||||
- label: ":jest: Tests"
|
- label: ":jest: Tests"
|
||||||
agents:
|
agents:
|
||||||
|
@ -33,13 +45,11 @@ steps:
|
||||||
queue: "medium"
|
queue: "medium"
|
||||||
command:
|
command:
|
||||||
- "echo '--- Install js-sdk'"
|
- "echo '--- Install js-sdk'"
|
||||||
# TODO: Remove hacky chmod for BuildKite
|
# We don't use the babel-ed output for anything so we can --ignore-scripts
|
||||||
- "chmod +x ./scripts/ci/*.sh"
|
# to save transpiling the files. We run the transpile step explicitly in
|
||||||
- "chmod +x ./scripts/*"
|
# the 'build' job.
|
||||||
- "echo '--- Installing Dependencies'"
|
- "./scripts/ci/install-deps.sh --ignore-scripts"
|
||||||
- "./scripts/ci/install-deps.sh"
|
- "yarn run reskindex"
|
||||||
- "echo '--- Running initial build steps'"
|
|
||||||
- "yarn build"
|
|
||||||
- "echo '+++ Running Tests'"
|
- "echo '+++ Running Tests'"
|
||||||
- "yarn test"
|
- "yarn test"
|
||||||
plugins:
|
plugins:
|
||||||
|
@ -48,10 +58,8 @@ steps:
|
||||||
|
|
||||||
- label: "🛠 Build"
|
- label: "🛠 Build"
|
||||||
command:
|
command:
|
||||||
- "echo '--- Install js-sdk'"
|
- "echo '+++ Install & Build'"
|
||||||
- "./scripts/ci/install-deps.sh"
|
- "yarn install"
|
||||||
- "echo '+++ Building Project'"
|
|
||||||
- "yarn build"
|
|
||||||
plugins:
|
plugins:
|
||||||
- docker#v3.0.1:
|
- docker#v3.0.1:
|
||||||
image: "node:12"
|
image: "node:12"
|
||||||
|
@ -62,20 +70,19 @@ steps:
|
||||||
# e2e tests otherwise take +-8min
|
# e2e tests otherwise take +-8min
|
||||||
queue: "xlarge"
|
queue: "xlarge"
|
||||||
command:
|
command:
|
||||||
# TODO: Remove hacky chmod for BuildKite
|
|
||||||
- "echo '--- Setup'"
|
|
||||||
- "chmod +x ./scripts/ci/*.sh"
|
|
||||||
- "chmod +x ./scripts/*"
|
|
||||||
- "echo '--- Install js-sdk'"
|
- "echo '--- Install js-sdk'"
|
||||||
- "./scripts/ci/install-deps.sh"
|
- "./scripts/ci/install-deps.sh --ignore-scripts"
|
||||||
- "echo '--- Running initial build steps'"
|
|
||||||
- "yarn build"
|
|
||||||
- "echo '+++ Running Tests'"
|
- "echo '+++ Running Tests'"
|
||||||
- "./scripts/ci/end-to-end-tests.sh"
|
- "./scripts/ci/end-to-end-tests.sh"
|
||||||
plugins:
|
plugins:
|
||||||
- docker#v3.0.1:
|
- docker#v3.0.1:
|
||||||
image: "matrixdotorg/riotweb-ci-e2etests-env:latest"
|
image: "matrixdotorg/riotweb-ci-e2etests-env:latest"
|
||||||
propagate-environment: true
|
propagate-environment: true
|
||||||
|
workdir: "/workdir/matrix-react-sdk"
|
||||||
|
retry:
|
||||||
|
automatic:
|
||||||
|
- exit_status: 1 # retry end-to-end tests once as Puppeteer sometimes fails
|
||||||
|
limit: 1
|
||||||
|
|
||||||
- label: "🔧 Riot Tests"
|
- label: "🔧 Riot Tests"
|
||||||
agents:
|
agents:
|
||||||
|
@ -83,32 +90,18 @@ steps:
|
||||||
# webpack loves to gorge itself on resources.
|
# webpack loves to gorge itself on resources.
|
||||||
queue: "medium"
|
queue: "medium"
|
||||||
command:
|
command:
|
||||||
# Install chrome
|
|
||||||
- "echo '--- Installing Chrome'"
|
|
||||||
- "wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -"
|
|
||||||
- "sh -c 'echo \"deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main\" >> /etc/apt/sources.list.d/google.list'"
|
|
||||||
- "apt-get update"
|
|
||||||
- "apt-get install -y google-chrome-stable"
|
|
||||||
# TODO: Remove hacky chmod for BuildKite
|
|
||||||
- "chmod +x ./scripts/ci/*.sh"
|
|
||||||
- "chmod +x ./scripts/*"
|
|
||||||
- "echo '--- Installing Dependencies'"
|
|
||||||
- "./scripts/ci/install-deps.sh"
|
|
||||||
- "echo '--- Running initial build steps'"
|
|
||||||
- "yarn build"
|
|
||||||
- "echo '+++ Running Tests'"
|
- "echo '+++ Running Tests'"
|
||||||
- "./scripts/ci/riot-unit-tests.sh"
|
- "./scripts/ci/riot-unit-tests.sh"
|
||||||
env:
|
|
||||||
CHROME_BIN: "/usr/bin/google-chrome-stable"
|
|
||||||
plugins:
|
plugins:
|
||||||
- docker#v3.0.1:
|
- docker#v3.0.1:
|
||||||
image: "node:10"
|
image: "node:10"
|
||||||
propagate-environment: true
|
propagate-environment: true
|
||||||
|
workdir: "/workdir/matrix-react-sdk"
|
||||||
|
|
||||||
- label: "🌐 i18n"
|
- label: "🌐 i18n"
|
||||||
command:
|
command:
|
||||||
- "echo '--- Fetching Dependencies'"
|
- "echo '--- Fetching Dependencies'"
|
||||||
- "yarn install"
|
- "yarn install --ignore-scripts"
|
||||||
- "echo '+++ Testing i18n output'"
|
- "echo '+++ Testing i18n output'"
|
||||||
- "yarn diff-i18n"
|
- "yarn diff-i18n"
|
||||||
plugins:
|
plugins:
|
||||||
|
|
174
CHANGELOG.md
174
CHANGELOG.md
|
@ -1,3 +1,177 @@
|
||||||
|
Changes in [2.0.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.0.0) (2020-01-27)
|
||||||
|
===================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.0.0-rc.2...v2.0.0)
|
||||||
|
|
||||||
|
* Ensure a plaintext version of the composer ends up on the clipboard
|
||||||
|
[\#3923](https://github.com/matrix-org/matrix-react-sdk/pull/3923)
|
||||||
|
* Move & upgrade babel runtime into dependencies (like it wants)
|
||||||
|
[\#3921](https://github.com/matrix-org/matrix-react-sdk/pull/3921)
|
||||||
|
* Don't list every single alias when there's many
|
||||||
|
[\#3919](https://github.com/matrix-org/matrix-react-sdk/pull/3919)
|
||||||
|
|
||||||
|
Changes in [2.0.0-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.0.0-rc.2) (2020-01-20)
|
||||||
|
=============================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.0.0-rc.1...v2.0.0-rc.2)
|
||||||
|
|
||||||
|
* Add prepublish script
|
||||||
|
[\#3877](https://github.com/matrix-org/matrix-react-sdk/pull/3877)
|
||||||
|
|
||||||
|
Changes in [2.0.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.0.0-rc.1) (2020-01-20)
|
||||||
|
=============================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.7.6...v2.0.0-rc.1)
|
||||||
|
|
||||||
|
BREAKING CHANGES
|
||||||
|
================
|
||||||
|
* The react-sdk node module now exports ES6 rather than ES5. If you
|
||||||
|
wish to supports target that aren't compatible with ES6, you
|
||||||
|
will need to transpile the react-sdk to a suitable dialect.
|
||||||
|
|
||||||
|
All Changes
|
||||||
|
===========
|
||||||
|
* Fix arrows keys moving through edit history
|
||||||
|
[\#3874](https://github.com/matrix-org/matrix-react-sdk/pull/3874)
|
||||||
|
* Fix error about MessagePanel not being available for read markers
|
||||||
|
[\#3867](https://github.com/matrix-org/matrix-react-sdk/pull/3867)
|
||||||
|
* Adjust secret storage to work before sync
|
||||||
|
[\#3864](https://github.com/matrix-org/matrix-react-sdk/pull/3864)
|
||||||
|
* Update from Weblate
|
||||||
|
[\#3872](https://github.com/matrix-org/matrix-react-sdk/pull/3872)
|
||||||
|
* Remove unused deps and dev-deps
|
||||||
|
[\#3870](https://github.com/matrix-org/matrix-react-sdk/pull/3870)
|
||||||
|
* Tidy Jest test stuff and dependencies
|
||||||
|
[\#3869](https://github.com/matrix-org/matrix-react-sdk/pull/3869)
|
||||||
|
* Move feature flag check for new session toast
|
||||||
|
[\#3865](https://github.com/matrix-org/matrix-react-sdk/pull/3865)
|
||||||
|
* Catch exception in checkTerms if no ID server
|
||||||
|
[\#3863](https://github.com/matrix-org/matrix-react-sdk/pull/3863)
|
||||||
|
* Catch exception if passphrase dialog cancelled
|
||||||
|
[\#3862](https://github.com/matrix-org/matrix-react-sdk/pull/3862)
|
||||||
|
* Disable key request dialogs with cross-signing
|
||||||
|
[\#3860](https://github.com/matrix-org/matrix-react-sdk/pull/3860)
|
||||||
|
* Toasts for new, unverified sessions
|
||||||
|
[\#3859](https://github.com/matrix-org/matrix-react-sdk/pull/3859)
|
||||||
|
* Check for a matrixclient before trying to use it
|
||||||
|
[\#3861](https://github.com/matrix-org/matrix-react-sdk/pull/3861)
|
||||||
|
* Room header & message box shields now reflect cross-signing state
|
||||||
|
[\#3850](https://github.com/matrix-org/matrix-react-sdk/pull/3850)
|
||||||
|
* Fix Array.concat undefined
|
||||||
|
[\#3857](https://github.com/matrix-org/matrix-react-sdk/pull/3857)
|
||||||
|
* Update chokidar to fix reskindex not working
|
||||||
|
[\#3856](https://github.com/matrix-org/matrix-react-sdk/pull/3856)
|
||||||
|
* Make the new DM invite dialog work for regular invites too
|
||||||
|
[\#3854](https://github.com/matrix-org/matrix-react-sdk/pull/3854)
|
||||||
|
* Fix event handler leak in MemberStatusMessageAvatar
|
||||||
|
[\#3855](https://github.com/matrix-org/matrix-react-sdk/pull/3855)
|
||||||
|
* Move DM creation logic into DMInviteDialog
|
||||||
|
[\#3843](https://github.com/matrix-org/matrix-react-sdk/pull/3843)
|
||||||
|
* Remove all text when cutting in the composer
|
||||||
|
[\#3848](https://github.com/matrix-org/matrix-react-sdk/pull/3848)
|
||||||
|
* Add a ToastStore
|
||||||
|
[\#3853](https://github.com/matrix-org/matrix-react-sdk/pull/3853)
|
||||||
|
* 'Members' button always toggle the right panel
|
||||||
|
[\#3804](https://github.com/matrix-org/matrix-react-sdk/pull/3804)
|
||||||
|
* Fix timing of when Composer considers itself to be modified
|
||||||
|
[\#3842](https://github.com/matrix-org/matrix-react-sdk/pull/3842)
|
||||||
|
* Compute download file icon immediately
|
||||||
|
[\#3851](https://github.com/matrix-org/matrix-react-sdk/pull/3851)
|
||||||
|
* Fix not being able to open profiles from the timeline
|
||||||
|
[\#3852](https://github.com/matrix-org/matrix-react-sdk/pull/3852)
|
||||||
|
* Add post-login complete security flow
|
||||||
|
[\#3847](https://github.com/matrix-org/matrix-react-sdk/pull/3847)
|
||||||
|
* Added cut/copy and pasting user pills from editor.
|
||||||
|
[\#3828](https://github.com/matrix-org/matrix-react-sdk/pull/3828)
|
||||||
|
* Fix imports for help & support tab
|
||||||
|
[\#3846](https://github.com/matrix-org/matrix-react-sdk/pull/3846)
|
||||||
|
* Humanize the recent DM rooms ourselves for translations
|
||||||
|
[\#3841](https://github.com/matrix-org/matrix-react-sdk/pull/3841)
|
||||||
|
* Improve the quality of invite suggestions by filtering out DMs
|
||||||
|
[\#3840](https://github.com/matrix-org/matrix-react-sdk/pull/3840)
|
||||||
|
* Fix linter and tests on develop
|
||||||
|
[\#3845](https://github.com/matrix-org/matrix-react-sdk/pull/3845)
|
||||||
|
* Fix sourcemaps by refactoring the build system
|
||||||
|
[\#3839](https://github.com/matrix-org/matrix-react-sdk/pull/3839)
|
||||||
|
* Don't error on unverified/unknown devices.
|
||||||
|
[\#3837](https://github.com/matrix-org/matrix-react-sdk/pull/3837)
|
||||||
|
* Padlock icons in room header
|
||||||
|
[\#3835](https://github.com/matrix-org/matrix-react-sdk/pull/3835)
|
||||||
|
* Don't allow upgrade from untrusted key backup.
|
||||||
|
[\#3822](https://github.com/matrix-org/matrix-react-sdk/pull/3822)
|
||||||
|
* Emoji verification: Change name of 🔒 to lock
|
||||||
|
[\#3825](https://github.com/matrix-org/matrix-react-sdk/pull/3825)
|
||||||
|
* Room padlock decorations only if cross-signing is enabled
|
||||||
|
[\#3838](https://github.com/matrix-org/matrix-react-sdk/pull/3838)
|
||||||
|
* Enable end-to-end tests for sourcemaps (+Windows instructions)
|
||||||
|
[\#3827](https://github.com/matrix-org/matrix-react-sdk/pull/3827)
|
||||||
|
* Repair community member info panel
|
||||||
|
[\#3832](https://github.com/matrix-org/matrix-react-sdk/pull/3832)
|
||||||
|
* Add feature flag around the presence indicator in room list
|
||||||
|
[\#3831](https://github.com/matrix-org/matrix-react-sdk/pull/3831)
|
||||||
|
* Display a padlock icon beside invite-only rooms in the room list
|
||||||
|
[\#3821](https://github.com/matrix-org/matrix-react-sdk/pull/3821)
|
||||||
|
* Update from Weblate
|
||||||
|
[\#3830](https://github.com/matrix-org/matrix-react-sdk/pull/3830)
|
||||||
|
* Fix listener leak on RoomView
|
||||||
|
[\#3826](https://github.com/matrix-org/matrix-react-sdk/pull/3826)
|
||||||
|
* Regenerate i18n for sourcemaps branch
|
||||||
|
[\#3824](https://github.com/matrix-org/matrix-react-sdk/pull/3824)
|
||||||
|
* Fix tests for sourcemaps branch
|
||||||
|
[\#3823](https://github.com/matrix-org/matrix-react-sdk/pull/3823)
|
||||||
|
* Jest
|
||||||
|
[\#3724](https://github.com/matrix-org/matrix-react-sdk/pull/3724)
|
||||||
|
* Sourcemaps: develop -> feature branch
|
||||||
|
[\#3817](https://github.com/matrix-org/matrix-react-sdk/pull/3817)
|
||||||
|
* Support pasting a bunch of identifiers into the invite dialog
|
||||||
|
[\#3820](https://github.com/matrix-org/matrix-react-sdk/pull/3820)
|
||||||
|
* Support 3PIDs (email addresses) in the invite dialog
|
||||||
|
[\#3819](https://github.com/matrix-org/matrix-react-sdk/pull/3819)
|
||||||
|
* Placeholder PR for cleaner diffs: ES6
|
||||||
|
[\#3765](https://github.com/matrix-org/matrix-react-sdk/pull/3765)
|
||||||
|
* Misc fixes for ES6 imports/exports
|
||||||
|
[\#3766](https://github.com/matrix-org/matrix-react-sdk/pull/3766)
|
||||||
|
* Wire up the invite targets dialog to a real composer and show selections
|
||||||
|
[\#3815](https://github.com/matrix-org/matrix-react-sdk/pull/3815)
|
||||||
|
* Change ref handling in TextualBody to prevent it parsing generated nodes
|
||||||
|
[\#3711](https://github.com/matrix-org/matrix-react-sdk/pull/3711)
|
||||||
|
* Render encoded html entities in og:description
|
||||||
|
[\#3789](https://github.com/matrix-org/matrix-react-sdk/pull/3789)
|
||||||
|
* Update package.json for new build process + cosmetics
|
||||||
|
[\#3767](https://github.com/matrix-org/matrix-react-sdk/pull/3767)
|
||||||
|
* Convert CommonJS exports to ES6 exports
|
||||||
|
[\#3761](https://github.com/matrix-org/matrix-react-sdk/pull/3761)
|
||||||
|
* Round 2 of CommonJS to ES6 imports
|
||||||
|
[\#3764](https://github.com/matrix-org/matrix-react-sdk/pull/3764)
|
||||||
|
* Strip all variation selectors on emoji
|
||||||
|
[\#3814](https://github.com/matrix-org/matrix-react-sdk/pull/3814)
|
||||||
|
* Use the new js-sdk imports and import from src
|
||||||
|
[\#3763](https://github.com/matrix-org/matrix-react-sdk/pull/3763)
|
||||||
|
* Convert many imports to handle ES6 exports
|
||||||
|
[\#3762](https://github.com/matrix-org/matrix-react-sdk/pull/3762)
|
||||||
|
* Fix userinfo for users not in the room
|
||||||
|
[\#3812](https://github.com/matrix-org/matrix-react-sdk/pull/3812)
|
||||||
|
* Attempt to fix e2e tests
|
||||||
|
[\#3811](https://github.com/matrix-org/matrix-react-sdk/pull/3811)
|
||||||
|
* Add bunch of null-guards and similar to fix React Errors/complaints
|
||||||
|
[\#3752](https://github.com/matrix-org/matrix-react-sdk/pull/3752)
|
||||||
|
* Delegate all room alias validation to the RoomAliasField validator
|
||||||
|
[\#3807](https://github.com/matrix-org/matrix-react-sdk/pull/3807)
|
||||||
|
* Support filtering and searching for users to invite in DMs
|
||||||
|
[\#3802](https://github.com/matrix-org/matrix-react-sdk/pull/3802)
|
||||||
|
* Add suggestions for which users to invite to chat
|
||||||
|
[\#3801](https://github.com/matrix-org/matrix-react-sdk/pull/3801)
|
||||||
|
* Use `flex-start` instead of `start` for postcss
|
||||||
|
[\#3760](https://github.com/matrix-org/matrix-react-sdk/pull/3760)
|
||||||
|
* Define getLanguageFromBrowser() for LanguageDropdown
|
||||||
|
[\#3769](https://github.com/matrix-org/matrix-react-sdk/pull/3769)
|
||||||
|
* Introduce babel's export-default-from plugin to fix build errors
|
||||||
|
[\#3768](https://github.com/matrix-org/matrix-react-sdk/pull/3768)
|
||||||
|
* Add a bit of debugging to incorrect components in the Skinner
|
||||||
|
[\#3770](https://github.com/matrix-org/matrix-react-sdk/pull/3770)
|
||||||
|
* [BREAKING] Refactor the entire build process for babel@7 and TypeScript
|
||||||
|
(chunk 1 of many)
|
||||||
|
[\#3722](https://github.com/matrix-org/matrix-react-sdk/pull/3722)
|
||||||
|
* Implementation of new potential skinning mechanism
|
||||||
|
[\#3723](https://github.com/matrix-org/matrix-react-sdk/pull/3723)
|
||||||
|
|
||||||
Changes in [1.7.6](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.7.6) (2020-01-13)
|
Changes in [1.7.6](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.7.6) (2020-01-13)
|
||||||
===================================================================================================
|
===================================================================================================
|
||||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.7.6-rc.2...v1.7.6)
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.7.6-rc.2...v1.7.6)
|
||||||
|
|
10
package.json
10
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "matrix-react-sdk",
|
"name": "matrix-react-sdk",
|
||||||
"version": "1.7.6",
|
"version": "2.0.0",
|
||||||
"description": "SDK for matrix.org using React",
|
"description": "SDK for matrix.org using React",
|
||||||
"author": "matrix.org",
|
"author": "matrix.org",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -31,7 +31,7 @@
|
||||||
"typings": "./lib/index.d.ts",
|
"typings": "./lib/index.d.ts",
|
||||||
"matrix_src_main": "./src/index.js",
|
"matrix_src_main": "./src/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepublish": "yarn build",
|
"prepare": "yarn build",
|
||||||
"i18n": "matrix-gen-i18n",
|
"i18n": "matrix-gen-i18n",
|
||||||
"prunei18n": "matrix-prune-i18n",
|
"prunei18n": "matrix-prune-i18n",
|
||||||
"diff-i18n": "cp src/i18n/strings/en_EN.json src/i18n/strings/en_EN_orig.json && ./scripts/gen-i18n.js && node scripts/compare-file.js src/i18n/strings/en_EN_orig.json src/i18n/strings/en_EN.json",
|
"diff-i18n": "cp src/i18n/strings/en_EN.json src/i18n/strings/en_EN_orig.json && ./scripts/gen-i18n.js && node scripts/compare-file.js src/i18n/strings/en_EN_orig.json src/i18n/strings/en_EN.json",
|
||||||
|
@ -54,6 +54,7 @@
|
||||||
"test:e2e": "./test/end-to-end-tests/run.sh --riot-url http://localhost:8080"
|
"test:e2e": "./test/end-to-end-tests/run.sh --riot-url http://localhost:8080"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.8.3",
|
||||||
"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",
|
||||||
|
@ -79,7 +80,7 @@
|
||||||
"is-ip": "^2.0.0",
|
"is-ip": "^2.0.0",
|
||||||
"linkifyjs": "^2.1.6",
|
"linkifyjs": "^2.1.6",
|
||||||
"lodash": "^4.17.14",
|
"lodash": "^4.17.14",
|
||||||
"matrix-js-sdk": "3.0.0",
|
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
|
||||||
"pako": "^1.0.5",
|
"pako": "^1.0.5",
|
||||||
"png-chunks-extract": "^1.0.0",
|
"png-chunks-extract": "^1.0.0",
|
||||||
"prop-types": "^15.5.8",
|
"prop-types": "^15.5.8",
|
||||||
|
@ -108,13 +109,12 @@
|
||||||
"@babel/plugin-proposal-numeric-separator": "^7.7.4",
|
"@babel/plugin-proposal-numeric-separator": "^7.7.4",
|
||||||
"@babel/plugin-proposal-object-rest-spread": "^7.7.4",
|
"@babel/plugin-proposal-object-rest-spread": "^7.7.4",
|
||||||
"@babel/plugin-transform-flow-comments": "^7.7.4",
|
"@babel/plugin-transform-flow-comments": "^7.7.4",
|
||||||
"@babel/plugin-transform-runtime": "^7.7.6",
|
"@babel/plugin-transform-runtime": "^7.8.3",
|
||||||
"@babel/preset-env": "^7.7.6",
|
"@babel/preset-env": "^7.7.6",
|
||||||
"@babel/preset-flow": "^7.7.4",
|
"@babel/preset-flow": "^7.7.4",
|
||||||
"@babel/preset-react": "^7.7.4",
|
"@babel/preset-react": "^7.7.4",
|
||||||
"@babel/preset-typescript": "^7.7.4",
|
"@babel/preset-typescript": "^7.7.4",
|
||||||
"@babel/register": "^7.7.4",
|
"@babel/register": "^7.7.4",
|
||||||
"@babel/runtime": "^7.7.6",
|
|
||||||
"@peculiar/webcrypto": "^1.0.22",
|
"@peculiar/webcrypto": "^1.0.22",
|
||||||
"babel-eslint": "^10.0.3",
|
"babel-eslint": "^10.0.3",
|
||||||
"babel-jest": "^24.9.0",
|
"babel-jest": "^24.9.0",
|
||||||
|
|
|
@ -338,6 +338,14 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_Dialog_titleImage {
|
||||||
|
vertical-align: middle;
|
||||||
|
width: 25px;
|
||||||
|
height: 25px;
|
||||||
|
margin-left: -2px;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_Dialog_title {
|
.mx_Dialog_title {
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
line-height: 36px;
|
line-height: 36px;
|
||||||
|
@ -378,7 +386,13 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_Dialog button, .mx_Dialog input[type="submit"] {
|
/* XXX: Our button style are a mess: buttons that happen to appear in dialogs get special styles applied
|
||||||
|
* to them that no button anywhere else in the app gets by default. In practice, buttons in other places
|
||||||
|
* in the app look the same by being AccessibleButtons, or possibly by having explict button classes.
|
||||||
|
* We should go through and have one consistent set of styles for buttons throughout the app.
|
||||||
|
* For now, I am duplicating the selectors here for mx_Dialog and mx_DialogButtons.
|
||||||
|
*/
|
||||||
|
.mx_Dialog button, .mx_Dialog input[type="submit"], .mx_Dialog_buttons button, .mx_Dialog_buttons input[type="submit"] {
|
||||||
@mixin mx_DialogButton;
|
@mixin mx_DialogButton;
|
||||||
margin-left: 0px;
|
margin-left: 0px;
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
|
@ -394,27 +408,32 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
|
||||||
margin-right: 0px;
|
margin-right: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_Dialog button:hover, .mx_Dialog input[type="submit"]:hover {
|
.mx_Dialog button:hover, .mx_Dialog input[type="submit"]:hover, .mx_Dialog_buttons button:hover, .mx_Dialog_buttons input[type="submit"]:hover {
|
||||||
@mixin mx_DialogButton_hover;
|
@mixin mx_DialogButton_hover;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_Dialog button:focus, .mx_Dialog input[type="submit"]:focus {
|
.mx_Dialog button:focus, .mx_Dialog input[type="submit"]:focus, .mx_Dialog_buttons button:focus, .mx_Dialog_buttons input[type="submit"]:focus {
|
||||||
filter: brightness($focus-brightness);
|
filter: brightness($focus-brightness);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_Dialog button.mx_Dialog_primary, .mx_Dialog input[type="submit"].mx_Dialog_primary {
|
.mx_Dialog button.mx_Dialog_primary, .mx_Dialog input[type="submit"].mx_Dialog_primary, .mx_Dialog_buttons button.mx_Dialog_primary, .mx_Dialog_buttons input[type="submit"].mx_Dialog_primary {
|
||||||
color: $accent-fg-color;
|
color: $accent-fg-color;
|
||||||
background-color: $accent-color;
|
background-color: $accent-color;
|
||||||
min-width: 156px;
|
min-width: 156px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_Dialog button.danger, .mx_Dialog input[type="submit"].danger {
|
.mx_Dialog button.danger, .mx_Dialog input[type="submit"].danger, .mx_Dialog_buttons button.danger, .mx_Dialog_buttons input[type="submit"].danger {
|
||||||
background-color: $warning-color;
|
background-color: $warning-color;
|
||||||
border: solid 1px $warning-color;
|
border: solid 1px $warning-color;
|
||||||
color: $accent-fg-color;
|
color: $accent-fg-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_Dialog button:disabled, .mx_Dialog input[type="submit"]:disabled {
|
.mx_Dialog button.warning, .mx_Dialog input[type="submit"].warning {
|
||||||
|
border: solid 1px $warning-color;
|
||||||
|
color: $warning-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_Dialog button:disabled, .mx_Dialog input[type="submit"]:disabled, .mx_Dialog_buttons button:disabled, .mx_Dialog_buttons input[type="submit"]:disabled {
|
||||||
background-color: $light-fg-color;
|
background-color: $light-fg-color;
|
||||||
border: solid 1px $light-fg-color;
|
border: solid 1px $light-fg-color;
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
|
|
|
@ -36,6 +36,7 @@
|
||||||
@import "./views/auth/_AuthHeader.scss";
|
@import "./views/auth/_AuthHeader.scss";
|
||||||
@import "./views/auth/_AuthHeaderLogo.scss";
|
@import "./views/auth/_AuthHeaderLogo.scss";
|
||||||
@import "./views/auth/_AuthPage.scss";
|
@import "./views/auth/_AuthPage.scss";
|
||||||
|
@import "./views/auth/_CompleteSecurityBody.scss";
|
||||||
@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";
|
||||||
|
@ -152,6 +153,7 @@
|
||||||
@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/_InviteOnlyIcon.scss";
|
||||||
@import "./views/rooms/_JumpToBottomButton.scss";
|
@import "./views/rooms/_JumpToBottomButton.scss";
|
||||||
@import "./views/rooms/_LinkPreviewWidget.scss";
|
@import "./views/rooms/_LinkPreviewWidget.scss";
|
||||||
@import "./views/rooms/_MemberDeviceInfo.scss";
|
@import "./views/rooms/_MemberDeviceInfo.scss";
|
||||||
|
|
|
@ -63,7 +63,7 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_GroupHeader_editButton::before {
|
.mx_GroupHeader_editButton::before {
|
||||||
mask-image: url('$(res)/img/icons-settings-room.svg');
|
mask-image: url('$(res)/img/feather-customised/settings.svg');
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_GroupHeader_shareButton::before {
|
.mx_GroupHeader_shareButton::before {
|
||||||
|
|
|
@ -51,8 +51,8 @@ limitations under the License.
|
||||||
&.mx_Toast_hasIcon {
|
&.mx_Toast_hasIcon {
|
||||||
&::after {
|
&::after {
|
||||||
content: "";
|
content: "";
|
||||||
width: 21px;
|
width: 22px;
|
||||||
height: 20px;
|
height: 22px;
|
||||||
grid-column: 1;
|
grid-column: 1;
|
||||||
grid-row: 1;
|
grid-row: 1;
|
||||||
mask-size: 100%;
|
mask-size: 100%;
|
||||||
|
|
|
@ -22,7 +22,7 @@ limitations under the License.
|
||||||
.mx_CompleteSecurity_headerIcon {
|
.mx_CompleteSecurity_headerIcon {
|
||||||
width: 24px;
|
width: 24px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
margin: 0 4px;
|
margin-right: 4px;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2019 New Vector Ltd
|
Copyright 2019 New Vector Ltd
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -16,12 +17,12 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_AuthBody {
|
.mx_AuthBody {
|
||||||
width: 500px;
|
width: 500px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: $authpage-secondary-color;
|
||||||
background-color: $authpage-body-bg-color;
|
background-color: $authpage-body-bg-color;
|
||||||
border-radius: 0 4px 4px 0;
|
border-radius: 0 4px 4px 0;
|
||||||
padding: 25px 60px;
|
padding: 25px 60px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 12px;
|
|
||||||
color: $authpage-secondary-color;
|
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
|
|
42
res/css/views/auth/_CompleteSecurityBody.scss
Normal file
42
res/css/views/auth/_CompleteSecurityBody.scss
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
Copyright 2019 New Vector Ltd
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
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_CompleteSecurityBody {
|
||||||
|
width: 600px;
|
||||||
|
color: $authpage-primary-color;
|
||||||
|
background-color: $authpage-body-bg-color;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:link,
|
||||||
|
a:hover,
|
||||||
|
a:visited {
|
||||||
|
@mixin mx_Dialog_link;
|
||||||
|
}
|
||||||
|
}
|
|
@ -210,4 +210,19 @@ limitations under the License.
|
||||||
.mx_InviteDialog {
|
.mx_InviteDialog {
|
||||||
// Prevent the dialog from jumping around randomly when elements change.
|
// Prevent the dialog from jumping around randomly when elements change.
|
||||||
height: 590px;
|
height: 590px;
|
||||||
|
padding-left: 20px; // the design wants some padding on the left
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_InviteDialog_userSections {
|
||||||
|
margin-top: 10px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-right: 45px;
|
||||||
|
height: 455px; // mx_InviteDialog's height minus some for the upper elements
|
||||||
|
}
|
||||||
|
|
||||||
|
// Right margin for the design. We could apply this to the whole dialog, but then the scrollbar
|
||||||
|
// for the user section gets weird.
|
||||||
|
.mx_InviteDialog_helpText,
|
||||||
|
.mx_InviteDialog_addressBar {
|
||||||
|
margin-right: 45px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2018 New Vector Ltd
|
Copyright 2018 New Vector Ltd
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -22,7 +22,7 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_CreateSecretStorageDialog_primaryContainer {
|
.mx_CreateSecretStorageDialog_primaryContainer {
|
||||||
/* FIXME: plinth colour in new theme(s). background-color: $accent-color; */
|
/* FIXME: plinth colour in new theme(s). background-color: $accent-color; */
|
||||||
padding: 20px;
|
padding-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_CreateSecretStorageDialog_primaryContainer::after {
|
.mx_CreateSecretStorageDialog_primaryContainer::after {
|
||||||
|
@ -36,9 +36,13 @@ limitations under the License.
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_Field.mx_CreateSecretStorageDialog_passPhraseField {
|
||||||
|
margin-top: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_CreateSecretStorageDialog_passPhraseHelp {
|
.mx_CreateSecretStorageDialog_passPhraseHelp {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
height: 85px;
|
height: 64px;
|
||||||
margin-left: 20px;
|
margin-left: 20px;
|
||||||
font-size: 80%;
|
font-size: 80%;
|
||||||
}
|
}
|
||||||
|
@ -47,16 +51,8 @@ limitations under the License.
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_CreateSecretStorageDialog_passPhraseInput {
|
|
||||||
flex: none;
|
|
||||||
width: 250px;
|
|
||||||
border: 1px solid $accent-color;
|
|
||||||
border-radius: 5px;
|
|
||||||
padding: 10px;
|
|
||||||
margin-bottom: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CreateSecretStorageDialog_passPhraseMatch {
|
.mx_CreateSecretStorageDialog_passPhraseMatch {
|
||||||
|
width: 200px;
|
||||||
margin-left: 20px;
|
margin-left: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,6 +78,10 @@ limitations under the License.
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_CreateSecretStorageDialog_recoveryKeyButtons .mx_AccessibleButton {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_CreateSecretStorageDialog_recoveryKeyButtons button {
|
.mx_CreateSecretStorageDialog_recoveryKeyButtons button {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
|
@ -23,15 +23,23 @@ limitations under the License.
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|
||||||
.mx_UserInfo_cancel {
|
.mx_UserInfo_cancel {
|
||||||
|
cursor: pointer;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: $dark-panel-bg-color;
|
||||||
|
margin: 9px;
|
||||||
|
z-index: 1; // render on top of the right panel
|
||||||
|
|
||||||
|
div {
|
||||||
height: 16px;
|
height: 16px;
|
||||||
width: 16px;
|
width: 16px;
|
||||||
padding: 10px 0 10px 10px;
|
padding: 4px;
|
||||||
cursor: pointer;
|
|
||||||
mask-image: url('$(res)/img/minimise.svg');
|
mask-image: url('$(res)/img/minimise.svg');
|
||||||
mask-repeat: no-repeat;
|
mask-repeat: no-repeat;
|
||||||
mask-position: 16px center;
|
mask-position: 7px center;
|
||||||
background-color: $rightpanel-button-color;
|
background-color: $rightpanel-button-color;
|
||||||
position: absolute;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
|
@ -95,7 +103,7 @@ limitations under the License.
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
// override the calculated sizes so that the letter isn't HUGE
|
// override the calculated sizes so that the letter isn't HUGE
|
||||||
font-size: 26px !important;
|
font-size: 56px !important;
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -367,6 +367,11 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_EventTile_e2eIcon_unknown {
|
||||||
|
background-image: url('$(res)/img/e2e/warning.svg');
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_EventTile_e2eIcon_unencrypted {
|
.mx_EventTile_e2eIcon_unencrypted {
|
||||||
background-image: url('$(res)/img/e2e/warning.svg');
|
background-image: url('$(res)/img/e2e/warning.svg');
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
@ -415,7 +420,8 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody {
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line,
|
.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line,
|
||||||
.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line {
|
.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line,
|
||||||
|
.mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line {
|
||||||
padding-left: 60px;
|
padding-left: 60px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -427,8 +433,13 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody {
|
||||||
border-left: $e2e-unverified-color 5px solid;
|
border-left: $e2e-unverified-color 5px solid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line {
|
||||||
|
border-left: $e2e-unknown-color 5px solid;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_EventTile:hover.mx_EventTile_verified.mx_EventTile_info .mx_EventTile_line,
|
.mx_EventTile:hover.mx_EventTile_verified.mx_EventTile_info .mx_EventTile_line,
|
||||||
.mx_EventTile:hover.mx_EventTile_unverified.mx_EventTile_info .mx_EventTile_line {
|
.mx_EventTile:hover.mx_EventTile_unverified.mx_EventTile_info .mx_EventTile_line,
|
||||||
|
.mx_EventTile:hover.mx_EventTile_unknown.mx_EventTile_info .mx_EventTile_line {
|
||||||
padding-left: 78px;
|
padding-left: 78px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -439,14 +450,16 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody {
|
||||||
|
|
||||||
// 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)
|
||||||
.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line > a > .mx_MessageTimestamp,
|
.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line > a > .mx_MessageTimestamp,
|
||||||
.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line > a > .mx_MessageTimestamp {
|
.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line > a > .mx_MessageTimestamp,
|
||||||
|
.mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line > a > .mx_MessageTimestamp {
|
||||||
left: 3px;
|
left: 3px;
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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)
|
||||||
.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line > .mx_EventTile_e2eIcon,
|
.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line > .mx_EventTile_e2eIcon,
|
||||||
.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line > .mx_EventTile_e2eIcon {
|
.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line > .mx_EventTile_e2eIcon,
|
||||||
|
.mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line > .mx_EventTile_e2eIcon {
|
||||||
display: block;
|
display: block;
|
||||||
left: 41px;
|
left: 41px;
|
||||||
}
|
}
|
||||||
|
|
38
res/css/views/rooms/_InviteOnlyIcon.scss
Normal file
38
res/css/views/rooms/_InviteOnlyIcon.scss
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
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_InviteOnlyIcon {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
position: relative;
|
||||||
|
display: block !important;
|
||||||
|
// Align the padlock with unencrypted room names
|
||||||
|
margin-left: 6px;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
background-color: $roomtile-name-color;
|
||||||
|
mask-image: url('$(res)/img/feather-customised/lock-solid.svg');
|
||||||
|
mask-position: center;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-size: contain;
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
}
|
|
@ -76,6 +76,8 @@ limitations under the License.
|
||||||
left: 60px;
|
left: 60px;
|
||||||
margin-right: 0; // Counteract the E2EIcon class
|
margin-right: 0; // Counteract the E2EIcon class
|
||||||
margin-left: 3px; // Counteract the E2EIcon class
|
margin-left: 3px; // Counteract the E2EIcon class
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_MessageComposer_noperm_error {
|
.mx_MessageComposer_noperm_error {
|
||||||
|
|
|
@ -19,7 +19,12 @@ limitations under the License.
|
||||||
border-bottom: 1px solid $primary-hairline-color;
|
border-bottom: 1px solid $primary-hairline-color;
|
||||||
|
|
||||||
.mx_E2EIcon {
|
.mx_E2EIcon {
|
||||||
margin: 0 5px;
|
margin: 0;
|
||||||
|
position: absolute;
|
||||||
|
bottom: -1px;
|
||||||
|
right: -2px;
|
||||||
|
height: 10px;
|
||||||
|
width: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -171,6 +176,7 @@ limitations under the License.
|
||||||
width: 28px;
|
width: 28px;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
margin: 0 7px;
|
margin: 0 7px;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomHeader_avatar .mx_BaseAvatar_image {
|
.mx_RoomHeader_avatar .mx_BaseAvatar_image {
|
||||||
|
@ -263,24 +269,3 @@ limitations under the License.
|
||||||
.mx_RoomHeader_pinsIndicatorUnread {
|
.mx_RoomHeader_pinsIndicatorUnread {
|
||||||
background-color: $pinned-unread-color;
|
background-color: $pinned-unread-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomHeader_PrivateIcon.mx_RoomHeader_isPrivate {
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
position: relative;
|
|
||||||
display: block !important;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
background-color: $roomtile-name-color;
|
|
||||||
mask-image: url('$(res)/img/feather-customised/lock-solid.svg');
|
|
||||||
mask-position: center;
|
|
||||||
mask-repeat: no-repeat;
|
|
||||||
mask-size: contain;
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -123,6 +123,11 @@ limitations under the License.
|
||||||
& > * {
|
& > * {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
}
|
}
|
||||||
|
.mx_AccessibleButton.mx_AccessibleButton_kind_primary {
|
||||||
|
// to account for the padding of the primary button which causes inconsistent look between
|
||||||
|
// subsequent secondary (text) buttons
|
||||||
|
margin-bottom: 7px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -98,6 +98,19 @@ limitations under the License.
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Note we match .mx_E2EIcon to make sure this matches more tightly than just
|
||||||
|
// .mx_E2EIcon on its own
|
||||||
|
.mx_RoomTile_e2eIcon.mx_E2EIcon {
|
||||||
|
height: 10px;
|
||||||
|
width: 10px;
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
bottom: -1px;
|
||||||
|
right: -2px;
|
||||||
|
z-index: 1;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_RoomTile_name {
|
.mx_RoomTile_name {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
padding: 0 6px;
|
padding: 0 6px;
|
||||||
|
@ -142,10 +155,11 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// toggle menuButton and badge on hover/menu displayed
|
// toggle menuButton and badge on menu displayed
|
||||||
.mx_RoomTile_menuDisplayed,
|
.mx_RoomTile_menuDisplayed,
|
||||||
// or on keyboard focus of room tile
|
// or on keyboard focus of room tile
|
||||||
.mx_RoomTile.focus-visible:focus-within,
|
.mx_LeftPanel_container:not(.collapsed) .mx_RoomTile:focus-within,
|
||||||
|
// or on pointer hover
|
||||||
.mx_LeftPanel_container:not(.collapsed) .mx_RoomTile:hover {
|
.mx_LeftPanel_container:not(.collapsed) .mx_RoomTile:hover {
|
||||||
.mx_RoomTile_menuButton {
|
.mx_RoomTile_menuButton {
|
||||||
display: block;
|
display: block;
|
||||||
|
@ -201,30 +215,7 @@ limitations under the License.
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomTile.mx_RoomTile.mx_RoomTile_isPrivate .mx_RoomTile_name {
|
.mx_InviteOnlyIcon + .mx_RoomTile_nameContainer .mx_RoomTile_name {
|
||||||
// Scoot the padding in a bit from 6px to make it look better
|
// Scoot the padding in a bit from 6px to make it look better
|
||||||
padding-left: 3px;
|
padding-left: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomTile.mx_RoomTile_isPrivate .mx_RoomTile_PrivateIcon {
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
position: relative;
|
|
||||||
display: block !important;
|
|
||||||
// Align the padlock with unencrypted room names
|
|
||||||
margin-left: 6px;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
background-color: $roomtile-name-color;
|
|
||||||
mask-image: url('$(res)/img/feather-customised/lock-solid.svg');
|
|
||||||
mask-position: center;
|
|
||||||
mask-repeat: no-repeat;
|
|
||||||
mask-size: contain;
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 23 KiB |
|
@ -224,6 +224,7 @@ $copy-button-url: "$(res)/img/icon_copy_message.svg";
|
||||||
|
|
||||||
// e2e
|
// e2e
|
||||||
$e2e-verified-color: #76cfa5; // N.B. *NOT* the same as $accent-color
|
$e2e-verified-color: #76cfa5; // N.B. *NOT* the same as $accent-color
|
||||||
|
$e2e-unknown-color: #e8bf37;
|
||||||
$e2e-unverified-color: #e8bf37;
|
$e2e-unverified-color: #e8bf37;
|
||||||
$e2e-warning-color: #ba6363;
|
$e2e-warning-color: #ba6363;
|
||||||
|
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
#
|
|
||||||
# script which is run by the CI build (after `yarn test`).
|
|
||||||
#
|
|
||||||
# clones riot-web develop and runs the tests against our version of react-sdk.
|
|
||||||
|
|
||||||
set -ev
|
|
||||||
|
|
||||||
RIOT_WEB_DIR=riot-web
|
|
||||||
REACT_SDK_DIR=`pwd`
|
|
||||||
|
|
||||||
yarn link
|
|
||||||
|
|
||||||
scripts/fetchdep.sh vector-im riot-web
|
|
||||||
|
|
||||||
pushd "$RIOT_WEB_DIR"
|
|
||||||
|
|
||||||
yarn link matrix-js-sdk
|
|
||||||
yarn link matrix-react-sdk
|
|
||||||
|
|
||||||
yarn install
|
|
||||||
|
|
||||||
yarn build
|
|
||||||
|
|
||||||
popd
|
|
|
@ -21,15 +21,16 @@ handle_error() {
|
||||||
|
|
||||||
trap 'handle_error' ERR
|
trap 'handle_error' ERR
|
||||||
|
|
||||||
RIOT_WEB_DIR=riot-web
|
|
||||||
REACT_SDK_DIR=`pwd`
|
|
||||||
|
|
||||||
|
|
||||||
echo "--- Building Riot"
|
echo "--- Building Riot"
|
||||||
scripts/ci/build.sh
|
scripts/ci/layered-riot-web.sh
|
||||||
|
cd ../riot-web
|
||||||
|
riot_web_dir=`pwd`
|
||||||
|
CI_PACKAGE=true yarn build
|
||||||
|
cd ../matrix-react-sdk
|
||||||
# run end to end tests
|
# run end to end tests
|
||||||
pushd test/end-to-end-tests
|
pushd test/end-to-end-tests
|
||||||
ln -s $REACT_SDK_DIR/$RIOT_WEB_DIR riot/riot-web
|
ln -s $riot_web_dir riot/riot-web
|
||||||
# PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true ./install.sh
|
# PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true ./install.sh
|
||||||
# CHROME_PATH=$(which google-chrome-stable) ./run.sh
|
# CHROME_PATH=$(which google-chrome-stable) ./run.sh
|
||||||
echo "--- Install synapse & other dependencies"
|
echo "--- Install synapse & other dependencies"
|
||||||
|
|
|
@ -6,9 +6,9 @@ scripts/fetchdep.sh matrix-org matrix-js-sdk
|
||||||
|
|
||||||
pushd matrix-js-sdk
|
pushd matrix-js-sdk
|
||||||
yarn link
|
yarn link
|
||||||
yarn install
|
yarn install $@
|
||||||
yarn build
|
yarn build
|
||||||
popd
|
popd
|
||||||
|
|
||||||
yarn link matrix-js-sdk
|
yarn link matrix-js-sdk
|
||||||
yarn install
|
yarn install $@
|
||||||
|
|
31
scripts/ci/layered-riot-web.sh
Executable file
31
scripts/ci/layered-riot-web.sh
Executable file
|
@ -0,0 +1,31 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Creates an environment similar to one that riot-web would expect for
|
||||||
|
# development. This means going one directory up (and assuming we're in
|
||||||
|
# a directory like /workdir/matrix-react-sdk) and putting riot-web and
|
||||||
|
# the js-sdk there.
|
||||||
|
|
||||||
|
cd ../ # Assume we're at something like /workdir/matrix-react-sdk
|
||||||
|
|
||||||
|
# Set up the js-sdk first
|
||||||
|
matrix-react-sdk/scripts/fetchdep.sh matrix-org matrix-js-sdk
|
||||||
|
pushd matrix-js-sdk
|
||||||
|
yarn link
|
||||||
|
yarn install
|
||||||
|
popd
|
||||||
|
|
||||||
|
# Now set up the react-sdk
|
||||||
|
pushd matrix-react-sdk
|
||||||
|
yarn link matrix-js-sdk
|
||||||
|
yarn link
|
||||||
|
yarn install
|
||||||
|
popd
|
||||||
|
|
||||||
|
# Finally, set up riot-web
|
||||||
|
matrix-react-sdk/scripts/fetchdep.sh vector-im riot-web
|
||||||
|
pushd riot-web
|
||||||
|
yarn link matrix-js-sdk
|
||||||
|
yarn link matrix-react-sdk
|
||||||
|
yarn install
|
||||||
|
yarn build:res
|
||||||
|
popd
|
|
@ -6,9 +6,7 @@
|
||||||
|
|
||||||
set -ev
|
set -ev
|
||||||
|
|
||||||
RIOT_WEB_DIR=riot-web
|
scripts/ci/layered-riot-web.sh
|
||||||
|
cd ../riot-web
|
||||||
scripts/ci/build.sh
|
yarn build:genfiles # so the tests can run. Faster version of `build`
|
||||||
pushd "$RIOT_WEB_DIR"
|
|
||||||
yarn test
|
yarn test
|
||||||
popd
|
|
||||||
|
|
|
@ -17,7 +17,7 @@ clone() {
|
||||||
if [ -n "$branch" ]
|
if [ -n "$branch" ]
|
||||||
then
|
then
|
||||||
echo "Trying to use $org/$repo#$branch"
|
echo "Trying to use $org/$repo#$branch"
|
||||||
git clone git://github.com/$org/$repo.git $repo --branch "$branch" && exit 0
|
git clone git://github.com/$org/$repo.git $repo --branch "$branch" --depth 1 && exit 0
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
92
src/AsyncWrapper.js
Normal file
92
src/AsyncWrapper.js
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import createReactClass from 'create-react-class';
|
||||||
|
import * as sdk from './index';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { _t } from './languageHandler';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrap an asynchronous loader function with a react component which shows a
|
||||||
|
* spinner until the real component loads.
|
||||||
|
*/
|
||||||
|
export default createReactClass({
|
||||||
|
propTypes: {
|
||||||
|
/** A promise which resolves with the real component
|
||||||
|
*/
|
||||||
|
prom: PropTypes.object.isRequired,
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
|
return {
|
||||||
|
component: null,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillMount: function() {
|
||||||
|
this._unmounted = false;
|
||||||
|
// XXX: temporary logging to try to diagnose
|
||||||
|
// https://github.com/vector-im/riot-web/issues/3148
|
||||||
|
console.log('Starting load of AsyncWrapper for modal');
|
||||||
|
this.props.prom.then((result) => {
|
||||||
|
if (this._unmounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Take the 'default' member if it's there, then we support
|
||||||
|
// passing in just an import()ed module, since ES6 async import
|
||||||
|
// always returns a module *namespace*.
|
||||||
|
const component = result.default ? result.default : result;
|
||||||
|
this.setState({component});
|
||||||
|
}).catch((e) => {
|
||||||
|
console.warn('AsyncWrapper promise failed', e);
|
||||||
|
this.setState({error: e});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillUnmount: function() {
|
||||||
|
this._unmounted = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
_onWrapperCancelClick: function() {
|
||||||
|
this.props.onFinished(false);
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
if (this.state.component) {
|
||||||
|
const Component = this.state.component;
|
||||||
|
return <Component {...this.props} />;
|
||||||
|
} else if (this.state.error) {
|
||||||
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
|
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||||
|
return <BaseDialog onFinished={this.props.onFinished}
|
||||||
|
title={_t("Error")}
|
||||||
|
>
|
||||||
|
{_t("Unable to load! Check your network connectivity and try again.")}
|
||||||
|
<DialogButtons primaryButton={_t("Dismiss")}
|
||||||
|
onPrimaryButtonClick={this._onWrapperCancelClick}
|
||||||
|
hasCancel={false}
|
||||||
|
/>
|
||||||
|
</BaseDialog>;
|
||||||
|
} else {
|
||||||
|
// show a spinner until the component is loaded.
|
||||||
|
const Spinner = sdk.getComponent("elements.Spinner");
|
||||||
|
return <Spinner />;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
|
@ -20,10 +20,13 @@ import * as sdk from './index';
|
||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
import ToastStore from './stores/ToastStore';
|
import ToastStore from './stores/ToastStore';
|
||||||
|
|
||||||
function toastKey(device) {
|
function toastKey(deviceId) {
|
||||||
return 'newsession_' + device.deviceId;
|
return 'newsession_' + deviceId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000;
|
||||||
|
const THIS_DEVICE_TOAST_KEY = 'setupencryption';
|
||||||
|
|
||||||
export default class DeviceListener {
|
export default class DeviceListener {
|
||||||
static sharedInstance() {
|
static sharedInstance() {
|
||||||
if (!global.mx_DeviceListener) global.mx_DeviceListener = new DeviceListener();
|
if (!global.mx_DeviceListener) global.mx_DeviceListener = new DeviceListener();
|
||||||
|
@ -31,44 +34,120 @@ export default class DeviceListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
// set of device IDs we're currently showing toasts for
|
||||||
|
this._activeNagToasts = new Set();
|
||||||
// device IDs for which the user has dismissed the verify toast ('Later')
|
// device IDs for which the user has dismissed the verify toast ('Later')
|
||||||
this._dismissed = new Set();
|
this._dismissed = new Set();
|
||||||
|
// has the user dismissed any of the various nag toasts to setup encryption on this device?
|
||||||
|
this._dismissedThisDeviceToast = false;
|
||||||
|
|
||||||
|
// cache of the key backup info
|
||||||
|
this._keyBackupInfo = null;
|
||||||
|
this._keyBackupFetchedAt = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
MatrixClientPeg.get().on('crypto.devicesUpdated', this._onDevicesUpdated);
|
MatrixClientPeg.get().on('crypto.devicesUpdated', this._onDevicesUpdated);
|
||||||
MatrixClientPeg.get().on('deviceVerificationChanged', this._onDeviceVerificationChanged);
|
MatrixClientPeg.get().on('deviceVerificationChanged', this._onDeviceVerificationChanged);
|
||||||
this.recheck();
|
MatrixClientPeg.get().on('userTrustStatusChanged', this._onUserTrustStatusChanged);
|
||||||
|
this._recheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
if (MatrixClientPeg.get()) {
|
if (MatrixClientPeg.get()) {
|
||||||
MatrixClientPeg.get().removeListener('crypto.devicesUpdated', this._onDevicesUpdated);
|
MatrixClientPeg.get().removeListener('crypto.devicesUpdated', this._onDevicesUpdated);
|
||||||
MatrixClientPeg.get().removeListener('deviceVerificationChanged', this._onDeviceVerificationChanged);
|
MatrixClientPeg.get().removeListener('deviceVerificationChanged', this._onDeviceVerificationChanged);
|
||||||
|
MatrixClientPeg.get().removeListener('userTrustStatusChanged', this._onUserTrustStatusChanged);
|
||||||
}
|
}
|
||||||
this._dismissed.clear();
|
this._dismissed.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
dismissVerification(deviceId) {
|
dismissVerification(deviceId) {
|
||||||
this._dismissed.add(deviceId);
|
this._dismissed.add(deviceId);
|
||||||
this.recheck();
|
this._recheck();
|
||||||
|
}
|
||||||
|
|
||||||
|
dismissEncryptionSetup() {
|
||||||
|
this._dismissedThisDeviceToast = true;
|
||||||
|
this._recheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
_onDevicesUpdated = (users) => {
|
_onDevicesUpdated = (users) => {
|
||||||
if (!users.includes(MatrixClientPeg.get().getUserId())) return;
|
if (!users.includes(MatrixClientPeg.get().getUserId())) return;
|
||||||
this.recheck();
|
this._recheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
_onDeviceVerificationChanged = (users) => {
|
_onDeviceVerificationChanged = (users) => {
|
||||||
if (!users.includes(MatrixClientPeg.get().getUserId())) return;
|
if (!users.includes(MatrixClientPeg.get().getUserId())) return;
|
||||||
this.recheck();
|
this._recheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
async recheck() {
|
_onUserTrustStatusChanged = (userId, trustLevel) => {
|
||||||
|
if (userId !== MatrixClientPeg.get().getUserId()) return;
|
||||||
|
this._recheck();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The server doesn't tell us when key backup is set up, so we poll
|
||||||
|
// & cache the result
|
||||||
|
async _getKeyBackupInfo() {
|
||||||
|
const now = (new Date()).getTime();
|
||||||
|
if (!this._keyBackupInfo || this._keyBackupFetchedAt < now - KEY_BACKUP_POLL_INTERVAL) {
|
||||||
|
this._keyBackupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
|
||||||
|
this._keyBackupFetchedAt = now;
|
||||||
|
}
|
||||||
|
return this._keyBackupInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _recheck() {
|
||||||
if (!SettingsStore.isFeatureEnabled("feature_cross_signing")) return;
|
if (!SettingsStore.isFeatureEnabled("feature_cross_signing")) return;
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
|
|
||||||
if (!cli.isCryptoEnabled()) return false;
|
if (!cli.isCryptoEnabled()) return;
|
||||||
|
if (!cli.getCrossSigningId()) {
|
||||||
|
if (this._dismissedThisDeviceToast) {
|
||||||
|
ToastStore.sharedInstance().dismissToast(THIS_DEVICE_TOAST_KEY);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// cross signing isn't enabled - nag to enable it
|
||||||
|
// There are 3 different toasts for:
|
||||||
|
if (cli.getStoredCrossSigningForUser(cli.getUserId())) {
|
||||||
|
// Cross-signing on account but this device doesn't trust the master key (verify this session)
|
||||||
|
ToastStore.sharedInstance().addOrReplaceToast({
|
||||||
|
key: THIS_DEVICE_TOAST_KEY,
|
||||||
|
title: _t("Verify this session"),
|
||||||
|
icon: "verification_warning",
|
||||||
|
props: {kind: 'verify_this_session'},
|
||||||
|
component: sdk.getComponent("toasts.SetupEncryptionToast"),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const backupInfo = await this._getKeyBackupInfo();
|
||||||
|
if (backupInfo) {
|
||||||
|
// No cross-signing on account but key backup available (upgrade encryption)
|
||||||
|
ToastStore.sharedInstance().addOrReplaceToast({
|
||||||
|
key: THIS_DEVICE_TOAST_KEY,
|
||||||
|
title: _t("Encryption upgrade available"),
|
||||||
|
icon: "verification_warning",
|
||||||
|
props: {kind: 'upgrade_encryption'},
|
||||||
|
component: sdk.getComponent("toasts.SetupEncryptionToast"),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// No cross-signing or key backup on account (set up encryption)
|
||||||
|
ToastStore.sharedInstance().addOrReplaceToast({
|
||||||
|
key: THIS_DEVICE_TOAST_KEY,
|
||||||
|
title: _t("Set up encryption"),
|
||||||
|
icon: "verification_warning",
|
||||||
|
props: {kind: 'set_up_encryption'},
|
||||||
|
component: sdk.getComponent("toasts.SetupEncryptionToast"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
ToastStore.sharedInstance().dismissToast(THIS_DEVICE_TOAST_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newActiveToasts = new Set();
|
||||||
|
|
||||||
const devices = await cli.getStoredDevicesForUser(cli.getUserId());
|
const devices = await cli.getStoredDevicesForUser(cli.getUserId());
|
||||||
for (const device of devices) {
|
for (const device of devices) {
|
||||||
|
@ -76,16 +155,24 @@ export default class DeviceListener {
|
||||||
|
|
||||||
const deviceTrust = await cli.checkDeviceTrust(cli.getUserId(), device.deviceId);
|
const deviceTrust = await cli.checkDeviceTrust(cli.getUserId(), device.deviceId);
|
||||||
if (deviceTrust.isCrossSigningVerified() || this._dismissed.has(device.deviceId)) {
|
if (deviceTrust.isCrossSigningVerified() || this._dismissed.has(device.deviceId)) {
|
||||||
ToastStore.sharedInstance().dismissToast(toastKey(device));
|
ToastStore.sharedInstance().dismissToast(toastKey(device.deviceId));
|
||||||
} else {
|
} else {
|
||||||
|
this._activeNagToasts.add(device.deviceId);
|
||||||
ToastStore.sharedInstance().addOrReplaceToast({
|
ToastStore.sharedInstance().addOrReplaceToast({
|
||||||
key: toastKey(device),
|
key: toastKey(device.deviceId),
|
||||||
title: _t("New Session"),
|
title: _t("New Session"),
|
||||||
icon: "verification_warning",
|
icon: "verification_warning",
|
||||||
props: {deviceId: device.deviceId},
|
props: {deviceId: device.deviceId},
|
||||||
component: sdk.getComponent("toasts.NewSessionToast"),
|
component: sdk.getComponent("toasts.NewSessionToast"),
|
||||||
});
|
});
|
||||||
|
newActiveToasts.add(device.deviceId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// clear any other outstanding toasts (eg. logged out devices)
|
||||||
|
for (const deviceId of this._activeNagToasts) {
|
||||||
|
if (!newActiveToasts.has(deviceId)) ToastStore.sharedInstance().dismissToast(toastKey(deviceId));
|
||||||
|
}
|
||||||
|
this._activeNagToasts = newActiveToasts;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -592,8 +592,11 @@ async function startMatrixClient(startSyncing=true) {
|
||||||
Mjolnir.sharedInstance().start();
|
Mjolnir.sharedInstance().start();
|
||||||
|
|
||||||
if (startSyncing) {
|
if (startSyncing) {
|
||||||
await MatrixClientPeg.start();
|
// The client might want to populate some views with events from the
|
||||||
|
// index (e.g. the FilePanel), therefore initialize the event index
|
||||||
|
// before the client.
|
||||||
await EventIndexPeg.init();
|
await EventIndexPeg.init();
|
||||||
|
await MatrixClientPeg.start();
|
||||||
} else {
|
} else {
|
||||||
console.warn("Caller requested only auxiliary services be started");
|
console.warn("Caller requested only auxiliary services be started");
|
||||||
await MatrixClientPeg.assign();
|
await MatrixClientPeg.assign();
|
||||||
|
|
|
@ -91,7 +91,7 @@ export default class Markdown {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
toHTML() {
|
toHTML({ externalLinks = false } = {}) {
|
||||||
const renderer = new commonmark.HtmlRenderer({
|
const renderer = new commonmark.HtmlRenderer({
|
||||||
safe: false,
|
safe: false,
|
||||||
|
|
||||||
|
@ -125,6 +125,24 @@ export default class Markdown {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
renderer.link = function(node, entering) {
|
||||||
|
const attrs = this.attrs(node);
|
||||||
|
if (entering) {
|
||||||
|
attrs.push(['href', this.esc(node.destination)]);
|
||||||
|
if (node.title) {
|
||||||
|
attrs.push(['title', this.esc(node.title)]);
|
||||||
|
}
|
||||||
|
// Modified link behaviour to treat them all as external and
|
||||||
|
// thus opening in a new tab.
|
||||||
|
if (externalLinks) {
|
||||||
|
attrs.push(['target', '_blank']);
|
||||||
|
attrs.push(['rel', 'noopener']);
|
||||||
|
}
|
||||||
|
this.tag('a', attrs);
|
||||||
|
} else {
|
||||||
|
this.tag('/a');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
renderer.html_inline = html_if_tag_allowed;
|
renderer.html_inline = html_if_tag_allowed;
|
||||||
|
|
||||||
|
|
|
@ -217,7 +217,7 @@ class _MatrixClientPeg {
|
||||||
timelineSupport: true,
|
timelineSupport: true,
|
||||||
forceTURN: !SettingsStore.getValue('webRtcAllowPeerToPeer', false),
|
forceTURN: !SettingsStore.getValue('webRtcAllowPeerToPeer', false),
|
||||||
fallbackICEServerAllowed: !!SettingsStore.getValue('fallbackICEServerAllowed'),
|
fallbackICEServerAllowed: !!SettingsStore.getValue('fallbackICEServerAllowed'),
|
||||||
verificationMethods: [verificationMethods.SAS],
|
verificationMethods: [verificationMethods.SAS, verificationMethods.QR_CODE_SHOW],
|
||||||
unstableClientRelationAggregation: true,
|
unstableClientRelationAggregation: true,
|
||||||
identityServer: new IdentityAuthClient(),
|
identityServer: new IdentityAuthClient(),
|
||||||
};
|
};
|
||||||
|
|
77
src/Modal.js
77
src/Modal.js
|
@ -17,87 +17,14 @@ limitations under the License.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import createReactClass from 'create-react-class';
|
|
||||||
import Analytics from './Analytics';
|
import Analytics from './Analytics';
|
||||||
import * as sdk from './index';
|
|
||||||
import dis from './dispatcher';
|
import dis from './dispatcher';
|
||||||
import { _t } from './languageHandler';
|
import {defer} from './utils/promise';
|
||||||
import {defer} from "./utils/promise";
|
import AsyncWrapper from './AsyncWrapper';
|
||||||
|
|
||||||
const DIALOG_CONTAINER_ID = "mx_Dialog_Container";
|
const DIALOG_CONTAINER_ID = "mx_Dialog_Container";
|
||||||
const STATIC_DIALOG_CONTAINER_ID = "mx_Dialog_StaticContainer";
|
const STATIC_DIALOG_CONTAINER_ID = "mx_Dialog_StaticContainer";
|
||||||
|
|
||||||
/**
|
|
||||||
* Wrap an asynchronous loader function with a react component which shows a
|
|
||||||
* spinner until the real component loads.
|
|
||||||
*/
|
|
||||||
const AsyncWrapper = createReactClass({
|
|
||||||
propTypes: {
|
|
||||||
/** A promise which resolves with the real component
|
|
||||||
*/
|
|
||||||
prom: PropTypes.object.isRequired,
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState: function() {
|
|
||||||
return {
|
|
||||||
component: null,
|
|
||||||
error: null,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
componentWillMount: function() {
|
|
||||||
this._unmounted = false;
|
|
||||||
// XXX: temporary logging to try to diagnose
|
|
||||||
// https://github.com/vector-im/riot-web/issues/3148
|
|
||||||
console.log('Starting load of AsyncWrapper for modal');
|
|
||||||
this.props.prom.then((result) => {
|
|
||||||
if (this._unmounted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Take the 'default' member if it's there, then we support
|
|
||||||
// passing in just an import()ed module, since ES6 async import
|
|
||||||
// always returns a module *namespace*.
|
|
||||||
const component = result.default ? result.default : result;
|
|
||||||
this.setState({component});
|
|
||||||
}).catch((e) => {
|
|
||||||
console.warn('AsyncWrapper promise failed', e);
|
|
||||||
this.setState({error: e});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
|
||||||
this._unmounted = true;
|
|
||||||
},
|
|
||||||
|
|
||||||
_onWrapperCancelClick: function() {
|
|
||||||
this.props.onFinished(false);
|
|
||||||
},
|
|
||||||
|
|
||||||
render: function() {
|
|
||||||
if (this.state.component) {
|
|
||||||
const Component = this.state.component;
|
|
||||||
return <Component {...this.props} />;
|
|
||||||
} else if (this.state.error) {
|
|
||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
|
||||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
|
||||||
return <BaseDialog onFinished={this.props.onFinished}
|
|
||||||
title={_t("Error")}
|
|
||||||
>
|
|
||||||
{_t("Unable to load! Check your network connectivity and try again.")}
|
|
||||||
<DialogButtons primaryButton={_t("Dismiss")}
|
|
||||||
onPrimaryButtonClick={this._onWrapperCancelClick}
|
|
||||||
hasCancel={false}
|
|
||||||
/>
|
|
||||||
</BaseDialog>;
|
|
||||||
} else {
|
|
||||||
// show a spinner until the component is loaded.
|
|
||||||
const Spinner = sdk.getComponent("elements.Spinner");
|
|
||||||
return <Spinner />;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
class ModalManager {
|
class ModalManager {
|
||||||
constructor() {
|
constructor() {
|
||||||
this._counter = 0;
|
this._counter = 0;
|
||||||
|
|
|
@ -20,13 +20,8 @@ import React from 'react';
|
||||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
import {MatrixClientPeg} from './MatrixClientPeg';
|
||||||
import MultiInviter from './utils/MultiInviter';
|
import MultiInviter from './utils/MultiInviter';
|
||||||
import Modal from './Modal';
|
import Modal from './Modal';
|
||||||
import { getAddressType } from './UserAddress';
|
|
||||||
import createRoom from './createRoom';
|
|
||||||
import * as sdk from './';
|
import * as sdk from './';
|
||||||
import dis from './dispatcher';
|
|
||||||
import DMRoomMap from './utils/DMRoomMap';
|
|
||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
import SettingsStore from "./settings/SettingsStore";
|
|
||||||
import {KIND_DM, KIND_INVITE} from "./components/views/dialogs/InviteDialog";
|
import {KIND_DM, KIND_INVITE} from "./components/views/dialogs/InviteDialog";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -44,64 +39,21 @@ export function inviteMultipleToRoom(roomId, addrs) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showStartChatInviteDialog() {
|
export function showStartChatInviteDialog() {
|
||||||
if (SettingsStore.isFeatureEnabled("feature_ftue_dms")) {
|
// This dialog handles the room creation internally - we don't need to worry about it.
|
||||||
// This new dialog handles the room creation internally - we don't need to worry about it.
|
|
||||||
const InviteDialog = sdk.getComponent("dialogs.InviteDialog");
|
const InviteDialog = sdk.getComponent("dialogs.InviteDialog");
|
||||||
Modal.createTrackedDialog(
|
Modal.createTrackedDialog(
|
||||||
'Start DM', '', InviteDialog, {kind: KIND_DM},
|
'Start DM', '', InviteDialog, {kind: KIND_DM},
|
||||||
/*className=*/null, /*isPriority=*/false, /*isStatic=*/true,
|
/*className=*/null, /*isPriority=*/false, /*isStatic=*/true,
|
||||||
);
|
);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog");
|
|
||||||
|
|
||||||
Modal.createTrackedDialog('Start a chat', '', AddressPickerDialog, {
|
|
||||||
title: _t('Start a chat'),
|
|
||||||
description: _t("Who would you like to communicate with?"),
|
|
||||||
placeholder: (validAddressTypes) => {
|
|
||||||
// The set of valid address type can be mutated inside the dialog
|
|
||||||
// when you first have no IS but agree to use one in the dialog.
|
|
||||||
if (validAddressTypes.includes('email')) {
|
|
||||||
return _t("Email, name or Matrix ID");
|
|
||||||
}
|
|
||||||
return _t("Name or Matrix ID");
|
|
||||||
},
|
|
||||||
validAddressTypes: ['mx-user-id', 'email'],
|
|
||||||
button: _t("Start Chat"),
|
|
||||||
onFinished: _onStartDmFinished,
|
|
||||||
}, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showRoomInviteDialog(roomId) {
|
export function showRoomInviteDialog(roomId) {
|
||||||
if (SettingsStore.isFeatureEnabled("feature_ftue_dms")) {
|
// This dialog handles the room creation internally - we don't need to worry about it.
|
||||||
// This new dialog handles the room creation internally - we don't need to worry about it.
|
|
||||||
const InviteDialog = sdk.getComponent("dialogs.InviteDialog");
|
const InviteDialog = sdk.getComponent("dialogs.InviteDialog");
|
||||||
Modal.createTrackedDialog(
|
Modal.createTrackedDialog(
|
||||||
'Invite Users', '', InviteDialog, {kind: KIND_INVITE, roomId},
|
'Invite Users', '', InviteDialog, {kind: KIND_INVITE, roomId},
|
||||||
/*className=*/null, /*isPriority=*/false, /*isStatic=*/true,
|
/*className=*/null, /*isPriority=*/false, /*isStatic=*/true,
|
||||||
);
|
);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog");
|
|
||||||
|
|
||||||
Modal.createTrackedDialog('Chat Invite', '', AddressPickerDialog, {
|
|
||||||
title: _t('Invite new room members'),
|
|
||||||
button: _t('Send Invites'),
|
|
||||||
placeholder: (validAddressTypes) => {
|
|
||||||
// The set of valid address type can be mutated inside the dialog
|
|
||||||
// when you first have no IS but agree to use one in the dialog.
|
|
||||||
if (validAddressTypes.includes('email')) {
|
|
||||||
return _t("Email, name or Matrix ID");
|
|
||||||
}
|
|
||||||
return _t("Name or Matrix ID");
|
|
||||||
},
|
|
||||||
validAddressTypes: ['mx-user-id', 'email'],
|
|
||||||
onFinished: (shouldInvite, addrs) => {
|
|
||||||
_onRoomInviteFinished(roomId, shouldInvite, addrs);
|
|
||||||
},
|
|
||||||
}, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -122,60 +74,6 @@ export function isValid3pidInvite(event) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Canonical DMs replaces this
|
|
||||||
function _onStartDmFinished(shouldInvite, addrs) {
|
|
||||||
if (!shouldInvite) return;
|
|
||||||
|
|
||||||
const addrTexts = addrs.map((addr) => addr.address);
|
|
||||||
|
|
||||||
if (_isDmChat(addrTexts)) {
|
|
||||||
const rooms = _getDirectMessageRooms(addrTexts[0]);
|
|
||||||
if (rooms.length > 0) {
|
|
||||||
// A Direct Message room already exists for this user, so reuse it
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'view_room',
|
|
||||||
room_id: rooms[0],
|
|
||||||
should_peek: false,
|
|
||||||
joining: false,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Start a new DM chat
|
|
||||||
createRoom({dmUserId: addrTexts[0]}).catch((err) => {
|
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
||||||
Modal.createTrackedDialog('Failed to start chat', '', ErrorDialog, {
|
|
||||||
title: _t("Failed to start chat"),
|
|
||||||
description: ((err && err.message) ? err.message : _t("Operation failed")),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if (addrTexts.length === 1) {
|
|
||||||
// Start a new DM chat
|
|
||||||
createRoom({dmUserId: addrTexts[0]}).catch((err) => {
|
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
||||||
Modal.createTrackedDialog('Failed to start chat', '', ErrorDialog, {
|
|
||||||
title: _t("Failed to start chat"),
|
|
||||||
description: ((err && err.message) ? err.message : _t("Operation failed")),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Start multi user chat
|
|
||||||
let room;
|
|
||||||
createRoom().then((roomId) => {
|
|
||||||
room = MatrixClientPeg.get().getRoom(roomId);
|
|
||||||
return inviteMultipleToRoom(roomId, addrTexts);
|
|
||||||
}).then((result) => {
|
|
||||||
return _showAnyInviteErrors(result.states, room, result.inviter);
|
|
||||||
}).catch((err) => {
|
|
||||||
console.error(err.stack);
|
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
||||||
Modal.createTrackedDialog('Failed to invite', '', ErrorDialog, {
|
|
||||||
title: _t("Failed to invite"),
|
|
||||||
description: ((err && err.message) ? err.message : _t("Operation failed")),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function inviteUsersToRoom(roomId, userIds) {
|
export function inviteUsersToRoom(roomId, userIds) {
|
||||||
return inviteMultipleToRoom(roomId, userIds).then((result) => {
|
return inviteMultipleToRoom(roomId, userIds).then((result) => {
|
||||||
const room = MatrixClientPeg.get().getRoom(roomId);
|
const room = MatrixClientPeg.get().getRoom(roomId);
|
||||||
|
@ -190,24 +88,6 @@ export function inviteUsersToRoom(roomId, userIds) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function _onRoomInviteFinished(roomId, shouldInvite, addrs) {
|
|
||||||
if (!shouldInvite) return;
|
|
||||||
|
|
||||||
const addrTexts = addrs.map((addr) => addr.address);
|
|
||||||
|
|
||||||
// Invite new users to a room
|
|
||||||
inviteUsersToRoom(roomId, addrTexts);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Immutable DMs replaces this
|
|
||||||
function _isDmChat(addrTexts) {
|
|
||||||
if (addrTexts.length === 1 && getAddressType(addrTexts[0]) === 'mx-user-id') {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function _showAnyInviteErrors(addrs, room, inviter) {
|
function _showAnyInviteErrors(addrs, room, inviter) {
|
||||||
// Show user any errors
|
// Show user any errors
|
||||||
const failedUsers = Object.keys(addrs).filter(a => addrs[a] === 'error');
|
const failedUsers = Object.keys(addrs).filter(a => addrs[a] === 'error');
|
||||||
|
@ -243,15 +123,3 @@ function _showAnyInviteErrors(addrs, room, inviter) {
|
||||||
|
|
||||||
return addrs;
|
return addrs;
|
||||||
}
|
}
|
||||||
|
|
||||||
function _getDirectMessageRooms(addr) {
|
|
||||||
const dmRoomMap = new DMRoomMap(MatrixClientPeg.get());
|
|
||||||
const dmRooms = dmRoomMap.getDMRoomsForUserId(addr);
|
|
||||||
const rooms = dmRooms.filter((dmRoom) => {
|
|
||||||
const room = MatrixClientPeg.get().getRoom(dmRoom);
|
|
||||||
if (room) {
|
|
||||||
return room.getMyMembership() === 'join';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return rooms;
|
|
||||||
}
|
|
||||||
|
|
|
@ -81,6 +81,8 @@ class Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
run(roomId, args) {
|
run(roomId, args) {
|
||||||
|
// if it has no runFn then its an ignored/nop command (autocomplete only) e.g `/me`
|
||||||
|
if (!this.runFn) return;
|
||||||
return this.runFn.bind(this)(roomId, args);
|
return this.runFn.bind(this)(roomId, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -905,25 +907,25 @@ const aliases = {
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process the given text for /commands and perform them.
|
* Process the given text for /commands and return a bound method to perform them.
|
||||||
* @param {string} roomId The room in which the command was performed.
|
* @param {string} roomId The room in which the command was performed.
|
||||||
* @param {string} input The raw text input by the user.
|
* @param {string} input The raw text input by the user.
|
||||||
* @return {Object|null} An object with the property 'error' if there was an error
|
* @return {null|function(): Object} Function returning an object with the property 'error' if there was an error
|
||||||
* processing the command, or 'promise' if a request was sent out.
|
* processing the command, or 'promise' if a request was sent out.
|
||||||
* Returns null if the input didn't match a command.
|
* Returns null if the input didn't match a command.
|
||||||
*/
|
*/
|
||||||
export function processCommandInput(roomId, input) {
|
export function getCommand(roomId, input) {
|
||||||
// trim any trailing whitespace, as it can confuse the parser for
|
// trim any trailing whitespace, as it can confuse the parser for
|
||||||
// IRC-style commands
|
// IRC-style commands
|
||||||
input = input.replace(/\s+$/, '');
|
input = input.replace(/\s+$/, '');
|
||||||
if (input[0] !== '/') return null; // not a command
|
if (input[0] !== '/') return null; // not a command
|
||||||
|
|
||||||
const bits = input.match(/^(\S+?)( +((.|\n)*))?$/);
|
const bits = input.match(/^(\S+?)(?: +((.|\n)*))?$/);
|
||||||
let cmd;
|
let cmd;
|
||||||
let args;
|
let args;
|
||||||
if (bits) {
|
if (bits) {
|
||||||
cmd = bits[1].substring(1).toLowerCase();
|
cmd = bits[1].substring(1).toLowerCase();
|
||||||
args = bits[3];
|
args = bits[2];
|
||||||
} else {
|
} else {
|
||||||
cmd = input;
|
cmd = input;
|
||||||
}
|
}
|
||||||
|
@ -932,11 +934,6 @@ export function processCommandInput(roomId, input) {
|
||||||
cmd = aliases[cmd];
|
cmd = aliases[cmd];
|
||||||
}
|
}
|
||||||
if (CommandMap[cmd]) {
|
if (CommandMap[cmd]) {
|
||||||
// if it has no runFn then its an ignored/nop command (autocomplete only) e.g `/me`
|
return () => CommandMap[cmd].run(roomId, args);
|
||||||
if (!CommandMap[cmd].runFn) return null;
|
|
||||||
|
|
||||||
return CommandMap[cmd].run(roomId, args);
|
|
||||||
} else {
|
|
||||||
return reject(_t('Unrecognised command:') + ' ' + input);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -275,6 +275,8 @@ function textForRoomAliasesEvent(ev) {
|
||||||
// This feels a bit overkill though, and it's not clear the i18n really needs it
|
// This feels a bit overkill though, and it's not clear the i18n really needs it
|
||||||
// so instead it's landing as a simple textual event.
|
// so instead it's landing as a simple textual event.
|
||||||
|
|
||||||
|
const maxShown = 3;
|
||||||
|
|
||||||
const senderName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
const senderName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||||
const oldAliases = ev.getPrevContent().aliases || [];
|
const oldAliases = ev.getPrevContent().aliases || [];
|
||||||
const newAliases = ev.getContent().aliases || [];
|
const newAliases = ev.getContent().aliases || [];
|
||||||
|
@ -287,18 +289,40 @@ function textForRoomAliasesEvent(ev) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (addedAliases.length && !removedAliases.length) {
|
if (addedAliases.length && !removedAliases.length) {
|
||||||
|
if (addedAliases.length > maxShown) {
|
||||||
|
return _t("%(senderName)s added %(addedAddresses)s and %(count)s other addresses to this room", {
|
||||||
|
senderName: senderName,
|
||||||
|
count: addedAliases.length - maxShown,
|
||||||
|
addedAddresses: addedAliases.slice(0, maxShown).join(', '),
|
||||||
|
});
|
||||||
|
}
|
||||||
return _t('%(senderName)s added %(count)s %(addedAddresses)s as addresses for this room.', {
|
return _t('%(senderName)s added %(count)s %(addedAddresses)s as addresses for this room.', {
|
||||||
senderName: senderName,
|
senderName: senderName,
|
||||||
count: addedAliases.length,
|
count: addedAliases.length,
|
||||||
addedAddresses: addedAliases.join(', '),
|
addedAddresses: addedAliases.join(', '),
|
||||||
});
|
});
|
||||||
} else if (!addedAliases.length && removedAliases.length) {
|
} else if (!addedAliases.length && removedAliases.length) {
|
||||||
|
if (removedAliases.length > maxShown) {
|
||||||
|
return _t("%(senderName)s removed %(removedAddresses)s and %(count)s other addresses from this room", {
|
||||||
|
senderName: senderName,
|
||||||
|
count: removedAliases.length - maxShown,
|
||||||
|
removedAddresses: removedAliases.slice(0, maxShown).join(', '),
|
||||||
|
});
|
||||||
|
}
|
||||||
return _t('%(senderName)s removed %(count)s %(removedAddresses)s as addresses for this room.', {
|
return _t('%(senderName)s removed %(count)s %(removedAddresses)s as addresses for this room.', {
|
||||||
senderName: senderName,
|
senderName: senderName,
|
||||||
count: removedAliases.length,
|
count: removedAliases.length,
|
||||||
removedAddresses: removedAliases.join(', '),
|
removedAddresses: removedAliases.join(', '),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
const combined = addedAliases.length + removedAliases.length;
|
||||||
|
if (combined > maxShown) {
|
||||||
|
return _t("%(senderName)s removed %(countRemoved)s and added %(countAdded)s addresses to this room", {
|
||||||
|
senderName: senderName,
|
||||||
|
countAdded: addedAliases.length,
|
||||||
|
countRemoved: removedAliases.length,
|
||||||
|
});
|
||||||
|
}
|
||||||
return _t(
|
return _t(
|
||||||
'%(senderName)s added %(addedAddresses)s and removed %(removedAddresses)s as addresses for this room.', {
|
'%(senderName)s added %(addedAddresses)s and removed %(removedAddresses)s as addresses for this room.', {
|
||||||
senderName: senderName,
|
senderName: senderName,
|
||||||
|
@ -420,10 +444,19 @@ function textForHistoryVisibilityEvent(event) {
|
||||||
|
|
||||||
function textForEncryptionEvent(event) {
|
function textForEncryptionEvent(event) {
|
||||||
const senderName = event.sender ? event.sender.name : event.getSender();
|
const senderName = event.sender ? event.sender.name : event.getSender();
|
||||||
return _t('%(senderName)s turned on end-to-end encryption (algorithm %(algorithm)s).', {
|
if (event.getContent().algorithm === "m.megolm.v1.aes-sha2") {
|
||||||
|
return _t('%(senderName)s turned on end-to-end encryption.', {
|
||||||
|
senderName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return _t(
|
||||||
|
'%(senderName)s turned on end-to-end encryption ' +
|
||||||
|
'(unrecognised algorithm %(algorithm)s).',
|
||||||
|
{
|
||||||
senderName,
|
senderName,
|
||||||
algorithm: event.getContent().algorithm,
|
algorithm: event.getContent().algorithm,
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Currently will only display a change if a user's power level is changed
|
// Currently will only display a change if a user's power level is changed
|
||||||
|
|
224
src/accessibility/RovingTabIndex.js
Normal file
224
src/accessibility/RovingTabIndex.js
Normal file
|
@ -0,0 +1,224 @@
|
||||||
|
/*
|
||||||
|
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, {
|
||||||
|
createContext,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useLayoutEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useReducer,
|
||||||
|
} from "react";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import {Key} from "../Keyboard";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module to simplify implementing the Roving TabIndex accessibility technique
|
||||||
|
*
|
||||||
|
* Wrap the Widget in an RovingTabIndexContextProvider
|
||||||
|
* and then for all buttons make use of useRovingTabIndex or RovingTabIndexWrapper.
|
||||||
|
* The code will keep track of which tabIndex was most recently focused and expose that information as `isActive` which
|
||||||
|
* can then be used to only set the tabIndex to 0 as expected by the roving tabindex technique.
|
||||||
|
* When the active button gets unmounted the closest button will be chosen as expected.
|
||||||
|
* Initially the first button to mount will be given active state.
|
||||||
|
*
|
||||||
|
* https://developer.mozilla.org/en-US/docs/Web/Accessibility/Keyboard-navigable_JavaScript_widgets#Technique_1_Roving_tabindex
|
||||||
|
*/
|
||||||
|
|
||||||
|
const DOCUMENT_POSITION_PRECEDING = 2;
|
||||||
|
|
||||||
|
const RovingTabIndexContext = createContext({
|
||||||
|
state: {
|
||||||
|
activeRef: null,
|
||||||
|
refs: [], // list of refs in DOM order
|
||||||
|
},
|
||||||
|
dispatch: () => {},
|
||||||
|
});
|
||||||
|
RovingTabIndexContext.displayName = "RovingTabIndexContext";
|
||||||
|
|
||||||
|
// TODO use a TypeScript type here
|
||||||
|
const types = {
|
||||||
|
REGISTER: "REGISTER",
|
||||||
|
UNREGISTER: "UNREGISTER",
|
||||||
|
SET_FOCUS: "SET_FOCUS",
|
||||||
|
};
|
||||||
|
|
||||||
|
const reducer = (state, action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case types.REGISTER: {
|
||||||
|
if (state.refs.length === 0) {
|
||||||
|
// Our list of refs was empty, set activeRef to this first item
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
activeRef: action.payload.ref,
|
||||||
|
refs: [action.payload.ref],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.refs.includes(action.payload.ref)) {
|
||||||
|
return state; // already in refs, this should not happen
|
||||||
|
}
|
||||||
|
|
||||||
|
// find the index of the first ref which is not preceding this one in DOM order
|
||||||
|
let newIndex = state.refs.findIndex(ref => {
|
||||||
|
return ref.current.compareDocumentPosition(action.payload.ref.current) & DOCUMENT_POSITION_PRECEDING;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (newIndex < 0) {
|
||||||
|
newIndex = state.refs.length; // append to the end
|
||||||
|
}
|
||||||
|
|
||||||
|
// update the refs list
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
refs: [
|
||||||
|
...state.refs.slice(0, newIndex),
|
||||||
|
action.payload.ref,
|
||||||
|
...state.refs.slice(newIndex),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case types.UNREGISTER: {
|
||||||
|
// filter out the ref which we are removing
|
||||||
|
const refs = state.refs.filter(r => r !== action.payload.ref);
|
||||||
|
|
||||||
|
if (refs.length === state.refs.length) {
|
||||||
|
return state; // already removed, this should not happen
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.activeRef === action.payload.ref) {
|
||||||
|
// we just removed the active ref, need to replace it
|
||||||
|
// pick the ref which is now in the index the old ref was in
|
||||||
|
const oldIndex = state.refs.findIndex(r => r === action.payload.ref);
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
activeRef: oldIndex >= refs.length ? refs[refs.length - 1] : refs[oldIndex],
|
||||||
|
refs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// update the refs list
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
refs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case types.SET_FOCUS: {
|
||||||
|
// update active ref
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
activeRef: action.payload.ref,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RovingTabIndexProvider = ({children, handleHomeEnd, onKeyDown}) => {
|
||||||
|
const [state, dispatch] = useReducer(reducer, {
|
||||||
|
activeRef: null,
|
||||||
|
refs: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const context = useMemo(() => ({state, dispatch}), [state]);
|
||||||
|
|
||||||
|
const onKeyDownHandler = useCallback((ev) => {
|
||||||
|
let handled = false;
|
||||||
|
if (handleHomeEnd) {
|
||||||
|
// check if we actually have any items
|
||||||
|
switch (ev.key) {
|
||||||
|
case Key.HOME:
|
||||||
|
handled = true;
|
||||||
|
// move focus to first item
|
||||||
|
if (context.state.refs.length > 0) {
|
||||||
|
context.state.refs[0].current.focus();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Key.END:
|
||||||
|
handled = true;
|
||||||
|
// move focus to last item
|
||||||
|
if (context.state.refs.length > 0) {
|
||||||
|
context.state.refs[context.state.refs.length - 1].current.focus();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (handled) {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
} else if (onKeyDown) {
|
||||||
|
return onKeyDown(ev);
|
||||||
|
}
|
||||||
|
}, [context.state, onKeyDown, handleHomeEnd]);
|
||||||
|
|
||||||
|
return <RovingTabIndexContext.Provider value={context}>
|
||||||
|
{ children({onKeyDownHandler}) }
|
||||||
|
</RovingTabIndexContext.Provider>;
|
||||||
|
};
|
||||||
|
RovingTabIndexProvider.propTypes = {
|
||||||
|
handleHomeEnd: PropTypes.bool,
|
||||||
|
onKeyDown: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Hook to register a roving tab index
|
||||||
|
// inputRef parameter specifies the ref to use
|
||||||
|
// onFocus should be called when the index gained focus in any manner
|
||||||
|
// isActive should be used to set tabIndex in a manner such as `tabIndex={isActive ? 0 : -1}`
|
||||||
|
// ref should be passed to a DOM node which will be used for DOM compareDocumentPosition
|
||||||
|
export const useRovingTabIndex = (inputRef) => {
|
||||||
|
const context = useContext(RovingTabIndexContext);
|
||||||
|
let ref = useRef(null);
|
||||||
|
|
||||||
|
if (inputRef) {
|
||||||
|
// if we are given a ref, use it instead of ours
|
||||||
|
ref = inputRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
// setup (after refs)
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
context.dispatch({
|
||||||
|
type: types.REGISTER,
|
||||||
|
payload: {ref},
|
||||||
|
});
|
||||||
|
// teardown
|
||||||
|
return () => {
|
||||||
|
context.dispatch({
|
||||||
|
type: types.UNREGISTER,
|
||||||
|
payload: {ref},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
const onFocus = useCallback(() => {
|
||||||
|
context.dispatch({
|
||||||
|
type: types.SET_FOCUS,
|
||||||
|
payload: {ref},
|
||||||
|
});
|
||||||
|
}, [ref, context]);
|
||||||
|
|
||||||
|
const isActive = context.state.activeRef === ref;
|
||||||
|
return [onFocus, isActive, ref];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Wrapper to allow use of useRovingTabIndex outside of React Functional Components.
|
||||||
|
export const RovingTabIndexWrapper = ({children, inputRef}) => {
|
||||||
|
const [onFocus, isActive, ref] = useRovingTabIndex(inputRef);
|
||||||
|
return children({onFocus, isActive, ref});
|
||||||
|
};
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
/*
|
||||||
|
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 * as sdk from '../../../../index';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import dis from "../../../../dispatcher";
|
||||||
|
import { _t } from '../../../../languageHandler';
|
||||||
|
|
||||||
|
import SettingsStore, {SettingLevel} from "../../../../settings/SettingsStore";
|
||||||
|
import EventIndexPeg from "../../../../indexing/EventIndexPeg";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Allows the user to disable the Event Index.
|
||||||
|
*/
|
||||||
|
export default class DisableEventIndexDialog extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
onFinished: PropTypes.func.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
disabling: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
_onDisable = async () => {
|
||||||
|
this.setState({
|
||||||
|
disabling: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
await SettingsStore.setValue('enableEventIndexing', null, SettingLevel.DEVICE, false);
|
||||||
|
await EventIndexPeg.deleteEventIndex();
|
||||||
|
this.props.onFinished();
|
||||||
|
dis.dispatch({ action: 'view_user_settings' });
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
|
const Spinner = sdk.getComponent('elements.Spinner');
|
||||||
|
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseDialog onFinished={this.props.onFinished} title={_t("Are you sure?")}>
|
||||||
|
{_t("If disabled, messages from encrypted rooms won't appear in search results.")}
|
||||||
|
{this.state.disabling ? <Spinner /> : <div />}
|
||||||
|
<DialogButtons
|
||||||
|
primaryButton={_t('Disable')}
|
||||||
|
onPrimaryButtonClick={this._onDisable}
|
||||||
|
primaryButtonClass="danger"
|
||||||
|
cancelButtonClass="warning"
|
||||||
|
onCancel={this.props.onFinished}
|
||||||
|
disabled={this.state.disabling}
|
||||||
|
/>
|
||||||
|
</BaseDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,154 @@
|
||||||
|
/*
|
||||||
|
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 * as sdk from '../../../../index';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { _t } from '../../../../languageHandler';
|
||||||
|
|
||||||
|
import Modal from '../../../../Modal';
|
||||||
|
import {formatBytes, formatCountLong} from "../../../../utils/FormattingUtils";
|
||||||
|
import EventIndexPeg from "../../../../indexing/EventIndexPeg";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Allows the user to introspect the event index state and disable it.
|
||||||
|
*/
|
||||||
|
export default class ManageEventIndexDialog extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
onFinished: PropTypes.func.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
eventIndexSize: 0,
|
||||||
|
eventCount: 0,
|
||||||
|
roomCount: 0,
|
||||||
|
currentRoom: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateCurrentRoom(room) {
|
||||||
|
const eventIndex = EventIndexPeg.get();
|
||||||
|
const stats = await eventIndex.getStats();
|
||||||
|
let currentRoom = null;
|
||||||
|
|
||||||
|
if (room) currentRoom = room.name;
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
eventIndexSize: stats.size,
|
||||||
|
roomCount: stats.roomCount,
|
||||||
|
eventCount: stats.eventCount,
|
||||||
|
currentRoom: currentRoom,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount(): void {
|
||||||
|
const eventIndex = EventIndexPeg.get();
|
||||||
|
|
||||||
|
if (eventIndex !== null) {
|
||||||
|
eventIndex.removeListener("changedCheckpoint", this.updateCurrentRoom.bind(this));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async componentWillMount(): void {
|
||||||
|
let eventIndexSize = 0;
|
||||||
|
let roomCount = 0;
|
||||||
|
let eventCount = 0;
|
||||||
|
let currentRoom = null;
|
||||||
|
|
||||||
|
const eventIndex = EventIndexPeg.get();
|
||||||
|
|
||||||
|
if (eventIndex !== null) {
|
||||||
|
eventIndex.on("changedCheckpoint", this.updateCurrentRoom.bind(this));
|
||||||
|
|
||||||
|
const stats = await eventIndex.getStats();
|
||||||
|
eventIndexSize = stats.size;
|
||||||
|
roomCount = stats.roomCount;
|
||||||
|
eventCount = stats.eventCount;
|
||||||
|
|
||||||
|
const room = eventIndex.currentRoom();
|
||||||
|
if (room) currentRoom = room.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
eventIndexSize,
|
||||||
|
eventCount,
|
||||||
|
roomCount,
|
||||||
|
currentRoom,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_onDisable = async () => {
|
||||||
|
Modal.createTrackedDialogAsync("Disable message search", "Disable message search",
|
||||||
|
import("./DisableEventIndexDialog"),
|
||||||
|
null, null, /* priority = */ false, /* static = */ true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onDone = () => {
|
||||||
|
this.props.onFinished(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let crawlerState;
|
||||||
|
|
||||||
|
if (this.state.currentRoom === null) {
|
||||||
|
crawlerState = _t("Not currently downloading messages for any room.");
|
||||||
|
} else {
|
||||||
|
crawlerState = (
|
||||||
|
_t("Downloading mesages for %(currentRoom)s.", { currentRoom: this.state.currentRoom })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventIndexingSettings = (
|
||||||
|
<div>
|
||||||
|
{
|
||||||
|
_t( "Riot is securely caching encrypted messages locally for them " +
|
||||||
|
"to appear in search results:",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<div className='mx_SettingsTab_subsectionText'>
|
||||||
|
{_t("Space used:")} {formatBytes(this.state.eventIndexSize, 0)}<br />
|
||||||
|
{_t("Indexed messages:")} {formatCountLong(this.state.eventCount)}<br />
|
||||||
|
{_t("Number of rooms:")} {formatCountLong(this.state.roomCount)}<br />
|
||||||
|
{crawlerState}<br />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
|
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseDialog className='mx_ManageEventIndexDialog'
|
||||||
|
onFinished={this.props.onFinished}
|
||||||
|
title={_t("Message search")}
|
||||||
|
>
|
||||||
|
{eventIndexingSettings}
|
||||||
|
<DialogButtons
|
||||||
|
primaryButton={_t("Done")}
|
||||||
|
onPrimaryButtonClick={this.props.onFinished}
|
||||||
|
primaryButtonClass="primary"
|
||||||
|
cancelButton={_t("Disable")}
|
||||||
|
onCancel={this._onDisable}
|
||||||
|
cancelButtonClass="danger"
|
||||||
|
/>
|
||||||
|
</BaseDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2018, 2019 New Vector Ltd
|
Copyright 2018, 2019 New Vector Ltd
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -16,6 +16,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
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 { scorePassword } from '../../../../utils/PasswordScorer';
|
||||||
|
@ -52,6 +53,15 @@ function selectText(target) {
|
||||||
* Secret Storage in account data.
|
* Secret Storage in account data.
|
||||||
*/
|
*/
|
||||||
export default class CreateSecretStorageDialog extends React.PureComponent {
|
export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
|
static propTypes = {
|
||||||
|
hasCancel: PropTypes.bool,
|
||||||
|
accountPassword: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
defaultProps = {
|
||||||
|
hasCancel: true,
|
||||||
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
|
@ -70,12 +80,24 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
setPassPhrase: false,
|
setPassPhrase: false,
|
||||||
backupInfo: null,
|
backupInfo: null,
|
||||||
backupSigStatus: null,
|
backupSigStatus: null,
|
||||||
|
// does the server offer a UI auth flow with just m.login.password
|
||||||
|
// for /keys/device_signing/upload?
|
||||||
|
canUploadKeysWithPasswordOnly: null,
|
||||||
|
accountPassword: props.accountPassword,
|
||||||
|
accountPasswordCorrect: null,
|
||||||
|
// set if we are 'upgrading' encryption (making an SSSS store from
|
||||||
|
// an existing key backup secret).
|
||||||
|
doingUpgrade: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
this._fetchBackupInfo();
|
this._fetchBackupInfo();
|
||||||
|
this._queryKeyUploadAuth();
|
||||||
|
|
||||||
|
MatrixClientPeg.get().on('crypto.keyBackupStatus', this._onKeyBackupStatusChange);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
MatrixClientPeg.get().removeListener('crypto.keyBackupStatus', this._onKeyBackupStatusChange);
|
||||||
if (this._setZxcvbnResultTimeout !== null) {
|
if (this._setZxcvbnResultTimeout !== null) {
|
||||||
clearTimeout(this._setZxcvbnResultTimeout);
|
clearTimeout(this._setZxcvbnResultTimeout);
|
||||||
}
|
}
|
||||||
|
@ -83,7 +105,10 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
|
|
||||||
async _fetchBackupInfo() {
|
async _fetchBackupInfo() {
|
||||||
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
|
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
|
||||||
const backupSigStatus = await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo);
|
const backupSigStatus = (
|
||||||
|
// we may not have started crypto yet, in which case we definitely don't trust the backup
|
||||||
|
MatrixClientPeg.get().isCryptoEnabled() && await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo)
|
||||||
|
);
|
||||||
|
|
||||||
const phase = backupInfo ?
|
const phase = backupInfo ?
|
||||||
(backupSigStatus.usable ? PHASE_MIGRATE : PHASE_RESTORE_KEY_BACKUP) :
|
(backupSigStatus.usable ? PHASE_MIGRATE : PHASE_RESTORE_KEY_BACKUP) :
|
||||||
|
@ -93,14 +118,41 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
phase,
|
phase,
|
||||||
backupInfo,
|
backupInfo,
|
||||||
backupSigStatus,
|
backupSigStatus,
|
||||||
|
// remember this after this phase so we can use appropriate copy
|
||||||
|
doingUpgrade: phase === PHASE_MIGRATE,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _queryKeyUploadAuth() {
|
||||||
|
try {
|
||||||
|
await MatrixClientPeg.get().uploadDeviceSigningKeys(null, {});
|
||||||
|
// We should never get here: the server should always require
|
||||||
|
// UI auth to upload device signing keys. If we do, we upload
|
||||||
|
// no keys which would be a no-op.
|
||||||
|
console.log("uploadDeviceSigningKeys unexpectedly succeeded without UI auth!");
|
||||||
|
} catch (error) {
|
||||||
|
if (!error.data.flows) {
|
||||||
|
console.log("uploadDeviceSigningKeys advertised no flows!");
|
||||||
|
}
|
||||||
|
const canUploadKeysWithPasswordOnly = error.data.flows.some(f => {
|
||||||
|
return f.stages.length === 1 && f.stages[0] === 'm.login.password';
|
||||||
|
});
|
||||||
|
this.setState({
|
||||||
|
canUploadKeysWithPasswordOnly,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onKeyBackupStatusChange = () => {
|
||||||
|
this._fetchBackupInfo();
|
||||||
|
}
|
||||||
|
|
||||||
_collectRecoveryKeyNode = (n) => {
|
_collectRecoveryKeyNode = (n) => {
|
||||||
this._recoveryKeyNode = n;
|
this._recoveryKeyNode = n;
|
||||||
}
|
}
|
||||||
|
|
||||||
_onMigrateNextClick = () => {
|
_onMigrateFormSubmit = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
this._bootstrapSecretStorage();
|
this._bootstrapSecretStorage();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,16 +179,20 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_bootstrapSecretStorage = async () => {
|
_doBootstrapUIAuth = async (makeRequest) => {
|
||||||
this.setState({
|
if (this.state.canUploadKeysWithPasswordOnly && this.state.accountPassword) {
|
||||||
phase: PHASE_STORING,
|
await makeRequest({
|
||||||
error: null,
|
type: 'm.login.password',
|
||||||
|
identifier: {
|
||||||
|
type: 'm.id.user',
|
||||||
|
user: MatrixClientPeg.get().getUserId(),
|
||||||
|
},
|
||||||
|
// https://github.com/matrix-org/synapse/issues/5665
|
||||||
|
user: MatrixClientPeg.get().getUserId(),
|
||||||
|
password: this.state.accountPassword,
|
||||||
});
|
});
|
||||||
const cli = MatrixClientPeg.get();
|
} else {
|
||||||
try {
|
|
||||||
const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog");
|
const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog");
|
||||||
await cli.bootstrapSecretStorage({
|
|
||||||
authUploadDeviceSigningKeys: async (makeRequest) => {
|
|
||||||
const { finished } = Modal.createTrackedDialog(
|
const { finished } = Modal.createTrackedDialog(
|
||||||
'Cross-signing keys dialog', '', InteractiveAuthDialog,
|
'Cross-signing keys dialog', '', InteractiveAuthDialog,
|
||||||
{
|
{
|
||||||
|
@ -149,7 +205,20 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
if (!confirmed) {
|
if (!confirmed) {
|
||||||
throw new Error("Cross-signing key upload auth canceled");
|
throw new Error("Cross-signing key upload auth canceled");
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_bootstrapSecretStorage = async () => {
|
||||||
|
this.setState({
|
||||||
|
phase: PHASE_STORING,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const cli = MatrixClientPeg.get();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await cli.bootstrapSecretStorage({
|
||||||
|
authUploadDeviceSigningKeys: this._doBootstrapUIAuth,
|
||||||
createSecretStorageKey: async () => this._keyInfo,
|
createSecretStorageKey: async () => this._keyInfo,
|
||||||
keyBackupInfo: this.state.backupInfo,
|
keyBackupInfo: this.state.backupInfo,
|
||||||
});
|
});
|
||||||
|
@ -157,7 +226,14 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
phase: PHASE_DONE,
|
phase: PHASE_DONE,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (this.state.canUploadKeysWithPasswordOnly && e.httpStatus === 401 && e.data.flows) {
|
||||||
|
this.setState({
|
||||||
|
accountPasswordCorrect: false,
|
||||||
|
phase: PHASE_MIGRATE,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
this.setState({ error: e });
|
this.setState({ error: e });
|
||||||
|
}
|
||||||
console.error("Error bootstrapping secret storage", e);
|
console.error("Error bootstrapping secret storage", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -173,7 +249,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
_onRestoreKeyBackupClick = () => {
|
_onRestoreKeyBackupClick = () => {
|
||||||
const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog');
|
const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog');
|
||||||
Modal.createTrackedDialog(
|
Modal.createTrackedDialog(
|
||||||
'Restore Backup', '', RestoreKeyBackupDialog, null, null,
|
'Restore Backup', '', RestoreKeyBackupDialog, {showSummary: false}, null,
|
||||||
/* priority = */ false, /* static = */ true,
|
/* priority = */ false, /* static = */ true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -285,6 +361,12 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
return this.state.zxcvbnResult && this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE;
|
return this.state.zxcvbnResult && this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onAccountPasswordChange = (e) => {
|
||||||
|
this.setState({
|
||||||
|
accountPassword: e.target.value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
_renderPhaseRestoreKeyBackup() {
|
_renderPhaseRestoreKeyBackup() {
|
||||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||||
return <div>
|
return <div>
|
||||||
|
@ -309,22 +391,47 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
// it automatically.
|
// it automatically.
|
||||||
// https://github.com/vector-im/riot-web/issues/11696
|
// https://github.com/vector-im/riot-web/issues/11696
|
||||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||||
return <div>
|
const Field = sdk.getComponent('views.elements.Field');
|
||||||
|
|
||||||
|
let authPrompt;
|
||||||
|
if (this.state.canUploadKeysWithPasswordOnly) {
|
||||||
|
authPrompt = <div>
|
||||||
|
<div>{_t("Enter your account password to confirm the upgrade:")}</div>
|
||||||
|
<div><Field type="password"
|
||||||
|
id="mx_CreateSecretStorage_accountPassword"
|
||||||
|
label={_t("Password")}
|
||||||
|
value={this.state.accountPassword}
|
||||||
|
onChange={this._onAccountPasswordChange}
|
||||||
|
flagInvalid={this.state.accountPasswordCorrect === false}
|
||||||
|
autoFocus={true}
|
||||||
|
/></div>
|
||||||
|
</div>;
|
||||||
|
} else {
|
||||||
|
authPrompt = <p>
|
||||||
|
{_t("You'll need to authenticate with the server to confirm the upgrade.")}
|
||||||
|
</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <form onSubmit={this._onMigrateFormSubmit}>
|
||||||
<p>{_t(
|
<p>{_t(
|
||||||
"Secret Storage will be set up using your existing key backup details. " +
|
"Upgrade this device to allow it to verify other devices, " +
|
||||||
"Your secret storage passphrase and recovery key will be the same as " +
|
"granting them access to encrypted messages and marking them " +
|
||||||
"they were for your key backup.",
|
"as trusted for other users.",
|
||||||
)}</p>
|
)}</p>
|
||||||
|
<div>{authPrompt}</div>
|
||||||
<DialogButtons primaryButton={_t('Next')}
|
<DialogButtons primaryButton={_t('Next')}
|
||||||
onPrimaryButtonClick={this._onMigrateNextClick}
|
primaryIsSubmit={true}
|
||||||
hasCancel={true}
|
hasCancel={true}
|
||||||
onCancel={this._onCancel}
|
onCancel={this._onCancel}
|
||||||
|
primaryDisabled={this.state.canUploadKeysWithPasswordOnly && !this.state.accountPassword}
|
||||||
/>
|
/>
|
||||||
</div>;
|
</form>;
|
||||||
}
|
}
|
||||||
|
|
||||||
_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');
|
||||||
|
|
||||||
let strengthMeter;
|
let strengthMeter;
|
||||||
let helpText;
|
let helpText;
|
||||||
|
@ -350,25 +457,21 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
|
|
||||||
return <div>
|
return <div>
|
||||||
<p>{_t(
|
<p>{_t(
|
||||||
"<b>Warning</b>: You should only set up secret storage from a trusted computer.", {},
|
"Set up encryption on this device to allow it to verify other devices, " +
|
||||||
{ b: sub => <b>{sub}</b> },
|
"granting them access to encrypted messages and marking them as trusted for other users.",
|
||||||
)}</p>
|
)}</p>
|
||||||
<p>{_t(
|
<p>{_t(
|
||||||
"We'll use secret storage to optionally store an encrypted copy of " +
|
"Secure your encryption keys with a passphrase. For maximum security " +
|
||||||
"your cross-signing identity for verifying other devices and message " +
|
"this should be different to your account password:",
|
||||||
"keys on our server. Protect your access to encrypted messages with a " +
|
|
||||||
"passphrase to keep it secure.",
|
|
||||||
)}</p>
|
)}</p>
|
||||||
<p>{_t("For maximum security, this should be different from your account password.")}</p>
|
|
||||||
|
|
||||||
<div className="mx_CreateSecretStorageDialog_primaryContainer">
|
|
||||||
<div className="mx_CreateSecretStorageDialog_passPhraseContainer">
|
<div className="mx_CreateSecretStorageDialog_passPhraseContainer">
|
||||||
<input type="password"
|
<Field type="password"
|
||||||
|
className="mx_CreateSecretStorageDialog_passPhraseField"
|
||||||
onChange={this._onPassPhraseChange}
|
onChange={this._onPassPhraseChange}
|
||||||
onKeyPress={this._onPassPhraseKeyPress}
|
onKeyPress={this._onPassPhraseKeyPress}
|
||||||
value={this.state.passPhrase}
|
value={this.state.passPhrase}
|
||||||
className="mx_CreateSecretStorageDialog_passPhraseInput"
|
label={_t("Enter a passphrase")}
|
||||||
placeholder={_t("Enter a passphrase...")}
|
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
/>
|
/>
|
||||||
<div className="mx_CreateSecretStorageDialog_passPhraseHelp">
|
<div className="mx_CreateSecretStorageDialog_passPhraseHelp">
|
||||||
|
@ -376,25 +479,30 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
{helpText}
|
{helpText}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<DialogButtons primaryButton={_t('Next')}
|
<DialogButtons primaryButton={_t('Continue')}
|
||||||
onPrimaryButtonClick={this._onPassPhraseNextClick}
|
onPrimaryButtonClick={this._onPassPhraseNextClick}
|
||||||
hasCancel={false}
|
hasCancel={false}
|
||||||
disabled={!this._passPhraseIsValid()}
|
disabled={!this._passPhraseIsValid()}
|
||||||
/>
|
>
|
||||||
|
<button type="button"
|
||||||
|
onClick={this._onCancel}
|
||||||
|
className="danger"
|
||||||
|
>{_t("Skip")}</button>
|
||||||
|
</DialogButtons>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>{_t("Advanced")}</summary>
|
<summary>{_t("Advanced")}</summary>
|
||||||
<p><button onClick={this._onSkipPassPhraseClick} >
|
<p><AccessibleButton kind='primary' onClick={this._onSkipPassPhraseClick} >
|
||||||
{_t("Set up with a recovery key")}
|
{_t("Set up with a recovery key")}
|
||||||
</button></p>
|
</AccessibleButton></p>
|
||||||
</details>
|
</details>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
_renderPhasePassPhraseConfirm() {
|
_renderPhasePassPhraseConfirm() {
|
||||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||||
|
const Field = sdk.getComponent('views.elements.Field');
|
||||||
|
|
||||||
let matchText;
|
let matchText;
|
||||||
if (this.state.passPhraseConfirm === this.state.passPhrase) {
|
if (this.state.passPhraseConfirm === this.state.passPhrase) {
|
||||||
|
@ -412,7 +520,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
|
|
||||||
let passPhraseMatch = null;
|
let passPhraseMatch = null;
|
||||||
if (matchText) {
|
if (matchText) {
|
||||||
passPhraseMatch = <div className="mx_CreateSecretStorageDialog_passPhraseMatch">
|
passPhraseMatch = <div>
|
||||||
<div>{matchText}</div>
|
<div>{matchText}</div>
|
||||||
<div>
|
<div>
|
||||||
<AccessibleButton element="span" className="mx_linkButton" onClick={this._onSetAgainClick}>
|
<AccessibleButton element="span" className="mx_linkButton" onClick={this._onSetAgainClick}>
|
||||||
|
@ -424,28 +532,32 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||||
return <div>
|
return <div>
|
||||||
<p>{_t(
|
<p>{_t(
|
||||||
"Please enter your passphrase a second time to confirm.",
|
"Enter your passphrase a second time to confirm it.",
|
||||||
)}</p>
|
)}</p>
|
||||||
<div className="mx_CreateSecretStorageDialog_primaryContainer">
|
|
||||||
<div className="mx_CreateSecretStorageDialog_passPhraseContainer">
|
<div className="mx_CreateSecretStorageDialog_passPhraseContainer">
|
||||||
<div>
|
<Field type="password"
|
||||||
<input type="password"
|
id="mx_CreateSecretStorageDialog_passPhraseField"
|
||||||
onChange={this._onPassPhraseConfirmChange}
|
onChange={this._onPassPhraseConfirmChange}
|
||||||
onKeyPress={this._onPassPhraseConfirmKeyPress}
|
onKeyPress={this._onPassPhraseConfirmKeyPress}
|
||||||
value={this.state.passPhraseConfirm}
|
value={this.state.passPhraseConfirm}
|
||||||
className="mx_CreateSecretStorageDialog_passPhraseInput"
|
className="mx_CreateSecretStorageDialog_passPhraseField"
|
||||||
placeholder={_t("Repeat your passphrase...")}
|
label={_t("Confirm your passphrase")}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
<div className="mx_CreateSecretStorageDialog_passPhraseMatch">
|
||||||
{passPhraseMatch}
|
{passPhraseMatch}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogButtons primaryButton={_t('Next')}
|
<DialogButtons primaryButton={_t('Continue')}
|
||||||
onPrimaryButtonClick={this._onPassPhraseConfirmNextClick}
|
onPrimaryButtonClick={this._onPassPhraseConfirmNextClick}
|
||||||
hasCancel={false}
|
hasCancel={false}
|
||||||
disabled={this.state.passPhrase !== this.state.passPhraseConfirm}
|
disabled={this.state.passPhrase !== this.state.passPhraseConfirm}
|
||||||
/>
|
>
|
||||||
|
<button type="button"
|
||||||
|
onClick={this._onCancel}
|
||||||
|
className="danger"
|
||||||
|
>{_t("Skip")}</button>
|
||||||
|
</DialogButtons>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -463,6 +575,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||||
return <div>
|
return <div>
|
||||||
<p>{_t(
|
<p>{_t(
|
||||||
"Your recovery key is a safety net - you can use it to restore " +
|
"Your recovery key is a safety net - you can use it to restore " +
|
||||||
|
@ -481,12 +594,12 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
<code ref={this._collectRecoveryKeyNode}>{this._encodedRecoveryKey}</code>
|
<code ref={this._collectRecoveryKeyNode}>{this._encodedRecoveryKey}</code>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_CreateSecretStorageDialog_recoveryKeyButtons">
|
<div className="mx_CreateSecretStorageDialog_recoveryKeyButtons">
|
||||||
<button className="mx_Dialog_primary" onClick={this._onCopyClick}>
|
<AccessibleButton kind='primary' className="mx_Dialog_primary" onClick={this._onCopyClick}>
|
||||||
{_t("Copy to clipboard")}
|
{_t("Copy to clipboard")}
|
||||||
</button>
|
</AccessibleButton>
|
||||||
<button className="mx_Dialog_primary" onClick={this._onDownloadClick}>
|
<AccessibleButton kind='primary' className="mx_Dialog_primary" onClick={this._onDownloadClick}>
|
||||||
{_t("Download")}
|
{_t("Download")}
|
||||||
</button>
|
</AccessibleButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -533,7 +646,11 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||||
return <div>
|
return <div>
|
||||||
<p>{_t(
|
<p>{_t(
|
||||||
"Your access to encrypted messages is now protected.",
|
"This device can now verify other devices, granting them access " +
|
||||||
|
"to encrypted messages and marking them as trusted for other users.",
|
||||||
|
)}</p>
|
||||||
|
<p>{_t(
|
||||||
|
"Verify other users in their profile.",
|
||||||
)}</p>
|
)}</p>
|
||||||
<DialogButtons primaryButton={_t('OK')}
|
<DialogButtons primaryButton={_t('OK')}
|
||||||
onPrimaryButtonClick={this._onDone}
|
onPrimaryButtonClick={this._onDone}
|
||||||
|
@ -564,11 +681,11 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
case PHASE_RESTORE_KEY_BACKUP:
|
case PHASE_RESTORE_KEY_BACKUP:
|
||||||
return _t('Restore your Key Backup');
|
return _t('Restore your Key Backup');
|
||||||
case PHASE_MIGRATE:
|
case PHASE_MIGRATE:
|
||||||
return _t('Migrate from Key Backup');
|
return _t('Upgrade your encryption');
|
||||||
case PHASE_PASSPHRASE:
|
case PHASE_PASSPHRASE:
|
||||||
return _t('Secure your encrypted messages with a passphrase');
|
return _t('Set up encryption');
|
||||||
case PHASE_PASSPHRASE_CONFIRM:
|
case PHASE_PASSPHRASE_CONFIRM:
|
||||||
return _t('Confirm your passphrase');
|
return _t('Confirm passphrase');
|
||||||
case PHASE_OPTOUT_CONFIRM:
|
case PHASE_OPTOUT_CONFIRM:
|
||||||
return _t('Warning!');
|
return _t('Warning!');
|
||||||
case PHASE_SHOWKEY:
|
case PHASE_SHOWKEY:
|
||||||
|
@ -578,9 +695,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
case PHASE_STORING:
|
case PHASE_STORING:
|
||||||
return _t('Storing secrets...');
|
return _t('Storing secrets...');
|
||||||
case PHASE_DONE:
|
case PHASE_DONE:
|
||||||
return _t('Success!');
|
return this.state.doingUpgrade ? _t('Encryption upgraded') : _t('Encryption setup complete');
|
||||||
default:
|
default:
|
||||||
return null;
|
return '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -635,11 +752,17 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let headerImage;
|
||||||
|
if (this._titleForPhase(this.state.phase)) {
|
||||||
|
headerImage = require("../../../../../res/img/e2e/normal.svg");
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseDialog className='mx_CreateSecretStorageDialog'
|
<BaseDialog className='mx_CreateSecretStorageDialog'
|
||||||
onFinished={this.props.onFinished}
|
onFinished={this.props.onFinished}
|
||||||
title={this._titleForPhase(this.state.phase)}
|
title={this._titleForPhase(this.state.phase)}
|
||||||
hasCancel={[PHASE_PASSPHRASE, PHASE_DONE].includes(this.state.phase)}
|
headerImage={headerImage}
|
||||||
|
hasCancel={this.props.hasCancel && [PHASE_PASSPHRASE].includes(this.state.phase)}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
{content}
|
{content}
|
||||||
|
|
|
@ -19,9 +19,10 @@ 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 Matrix from 'matrix-js-sdk';
|
import {Filter} from 'matrix-js-sdk';
|
||||||
import * as sdk from '../../index';
|
import * as sdk from '../../index';
|
||||||
import {MatrixClientPeg} from '../../MatrixClientPeg';
|
import {MatrixClientPeg} from '../../MatrixClientPeg';
|
||||||
|
import EventIndexPeg from "../../indexing/EventIndexPeg";
|
||||||
import { _t } from '../../languageHandler';
|
import { _t } from '../../languageHandler';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -29,6 +30,9 @@ import { _t } from '../../languageHandler';
|
||||||
*/
|
*/
|
||||||
const FilePanel = createReactClass({
|
const FilePanel = createReactClass({
|
||||||
displayName: 'FilePanel',
|
displayName: 'FilePanel',
|
||||||
|
// This is used to track if a decrypted event was a live event and should be
|
||||||
|
// added to the timeline.
|
||||||
|
decryptingEvents: new Set(),
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
roomId: PropTypes.string.isRequired,
|
roomId: PropTypes.string.isRequired,
|
||||||
|
@ -40,18 +44,78 @@ const FilePanel = createReactClass({
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount: function() {
|
onRoomTimeline(ev, room, toStartOfTimeline, removed, data) {
|
||||||
this.updateTimelineSet(this.props.roomId);
|
if (room.roomId !== this.props.roomId) return;
|
||||||
|
if (toStartOfTimeline || !data || !data.liveEvent || ev.isRedacted()) return;
|
||||||
|
|
||||||
|
if (ev.isBeingDecrypted()) {
|
||||||
|
this.decryptingEvents.add(ev.getId());
|
||||||
|
} else {
|
||||||
|
this.addEncryptedLiveEvent(ev);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
updateTimelineSet: function(roomId) {
|
onEventDecrypted(ev, err) {
|
||||||
|
if (ev.getRoomId() !== this.props.roomId) return;
|
||||||
|
const eventId = ev.getId();
|
||||||
|
|
||||||
|
if (!this.decryptingEvents.delete(eventId)) return;
|
||||||
|
if (err) return;
|
||||||
|
|
||||||
|
this.addEncryptedLiveEvent(ev);
|
||||||
|
},
|
||||||
|
|
||||||
|
addEncryptedLiveEvent(ev, toStartOfTimeline) {
|
||||||
|
if (!this.state.timelineSet) return;
|
||||||
|
|
||||||
|
const timeline = this.state.timelineSet.getLiveTimeline();
|
||||||
|
if (ev.getType() !== "m.room.message") return;
|
||||||
|
if (["m.file", "m.image", "m.video", "m.audio"].indexOf(ev.getContent().msgtype) == -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.state.timelineSet.eventIdToTimeline(ev.getId())) {
|
||||||
|
this.state.timelineSet.addEventToTimeline(ev, timeline, false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async componentDidMount() {
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
const room = client.getRoom(roomId);
|
|
||||||
|
|
||||||
this.noRoom = !room;
|
await this.updateTimelineSet(this.props.roomId);
|
||||||
|
|
||||||
if (room) {
|
if (!MatrixClientPeg.get().isRoomEncrypted(this.props.roomId)) return;
|
||||||
const filter = new Matrix.Filter(client.credentials.userId);
|
|
||||||
|
// The timelineSets filter makes sure that encrypted events that contain
|
||||||
|
// URLs never get added to the timeline, even if they are live events.
|
||||||
|
// These methods are here to manually listen for such events and add
|
||||||
|
// them despite the filter's best efforts.
|
||||||
|
//
|
||||||
|
// We do this only for encrypted rooms and if an event index exists,
|
||||||
|
// this could be made more general in the future or the filter logic
|
||||||
|
// could be fixed.
|
||||||
|
if (EventIndexPeg.get() !== null) {
|
||||||
|
client.on('Room.timeline', this.onRoomTimeline.bind(this));
|
||||||
|
client.on('Event.decrypted', this.onEventDecrypted.bind(this));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
const client = MatrixClientPeg.get();
|
||||||
|
if (client === null) return;
|
||||||
|
|
||||||
|
if (!MatrixClientPeg.get().isRoomEncrypted(this.props.roomId)) return;
|
||||||
|
|
||||||
|
if (EventIndexPeg.get() !== null) {
|
||||||
|
client.removeListener('Room.timeline', this.onRoomTimeline.bind(this));
|
||||||
|
client.removeListener('Event.decrypted', this.onEventDecrypted.bind(this));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetchFileEventsServer(room) {
|
||||||
|
const client = MatrixClientPeg.get();
|
||||||
|
|
||||||
|
const filter = new Filter(client.credentials.userId);
|
||||||
filter.setDefinition(
|
filter.setDefinition(
|
||||||
{
|
{
|
||||||
"room": {
|
"room": {
|
||||||
|
@ -65,17 +129,62 @@ const FilePanel = createReactClass({
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// FIXME: we shouldn't be doing this every time we change room - see comment above.
|
const filterId = await client.getOrCreateFilter("FILTER_FILES_" + client.credentials.userId, filter);
|
||||||
client.getOrCreateFilter("FILTER_FILES_" + client.credentials.userId, filter).then(
|
|
||||||
(filterId)=>{
|
|
||||||
filter.filterId = filterId;
|
filter.filterId = filterId;
|
||||||
const timelineSet = room.getOrCreateFilteredTimelineSet(filter);
|
const timelineSet = room.getOrCreateFilteredTimelineSet(filter);
|
||||||
|
|
||||||
|
return timelineSet;
|
||||||
|
},
|
||||||
|
|
||||||
|
onPaginationRequest(timelineWindow, direction, limit) {
|
||||||
|
const client = MatrixClientPeg.get();
|
||||||
|
const eventIndex = EventIndexPeg.get();
|
||||||
|
const roomId = this.props.roomId;
|
||||||
|
|
||||||
|
const room = client.getRoom(roomId);
|
||||||
|
|
||||||
|
// We override the pagination request for encrypted rooms so that we ask
|
||||||
|
// the event index to fulfill the pagination request. Asking the server
|
||||||
|
// to paginate won't ever work since the server can't correctly filter
|
||||||
|
// out events containing URLs
|
||||||
|
if (client.isRoomEncrypted(roomId) && eventIndex !== null) {
|
||||||
|
return eventIndex.paginateTimelineWindow(room, timelineWindow, direction, limit);
|
||||||
|
} else {
|
||||||
|
return timelineWindow.paginate(direction, limit);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateTimelineSet(roomId: string) {
|
||||||
|
const client = MatrixClientPeg.get();
|
||||||
|
const room = client.getRoom(roomId);
|
||||||
|
const eventIndex = EventIndexPeg.get();
|
||||||
|
|
||||||
|
this.noRoom = !room;
|
||||||
|
|
||||||
|
if (room) {
|
||||||
|
let timelineSet;
|
||||||
|
|
||||||
|
try {
|
||||||
|
timelineSet = await this.fetchFileEventsServer(room);
|
||||||
|
|
||||||
|
// If this room is encrypted the file panel won't be populated
|
||||||
|
// correctly since the defined filter doesn't support encrypted
|
||||||
|
// events and the server can't check if encrypted events contain
|
||||||
|
// URLs.
|
||||||
|
//
|
||||||
|
// This is where our event index comes into place, we ask the
|
||||||
|
// event index to populate the timelineSet for us. This call
|
||||||
|
// will add 10 events to the live timeline of the set. More can
|
||||||
|
// be requested using pagination.
|
||||||
|
if (client.isRoomEncrypted(roomId) && eventIndex !== null) {
|
||||||
|
const timeline = timelineSet.getLiveTimeline();
|
||||||
|
await eventIndex.populateFileTimeline(timelineSet, timeline, room, 10);
|
||||||
|
}
|
||||||
|
|
||||||
this.setState({ timelineSet: timelineSet });
|
this.setState({ timelineSet: timelineSet });
|
||||||
},
|
} catch (error) {
|
||||||
(error)=>{
|
|
||||||
console.error("Failed to get or create file panel filter", error);
|
console.error("Failed to get or create file panel filter", error);
|
||||||
},
|
}
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
console.error("Failed to add filtered timelineSet for FilePanel as no room!");
|
console.error("Failed to add filtered timelineSet for FilePanel as no room!");
|
||||||
}
|
}
|
||||||
|
@ -111,6 +220,7 @@ const FilePanel = createReactClass({
|
||||||
manageReadMarkers={false}
|
manageReadMarkers={false}
|
||||||
timelineSet={this.state.timelineSet}
|
timelineSet={this.state.timelineSet}
|
||||||
showUrlPreview = {false}
|
showUrlPreview = {false}
|
||||||
|
onPaginationRequest={this.onPaginationRequest}
|
||||||
tileShape="file_grid"
|
tileShape="file_grid"
|
||||||
resizeNotifier={this.props.resizeNotifier}
|
resizeNotifier={this.props.resizeNotifier}
|
||||||
empty={_t('There are no visible files in this room')}
|
empty={_t('There are no visible files in this room')}
|
||||||
|
|
|
@ -20,7 +20,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 {getEntryComponentForLoginType} from '../views/auth/InteractiveAuthEntryComponents';
|
import getEntryComponentForLoginType from '../views/auth/InteractiveAuthEntryComponents';
|
||||||
|
|
||||||
import * as sdk from '../../index';
|
import * as sdk from '../../index';
|
||||||
|
|
||||||
|
|
|
@ -129,9 +129,6 @@ const LeftPanel = createReactClass({
|
||||||
if (!this.focusedElement) return;
|
if (!this.focusedElement) return;
|
||||||
|
|
||||||
switch (ev.key) {
|
switch (ev.key) {
|
||||||
case Key.TAB:
|
|
||||||
this._onMoveFocus(ev, ev.shiftKey);
|
|
||||||
break;
|
|
||||||
case Key.ARROW_UP:
|
case Key.ARROW_UP:
|
||||||
this._onMoveFocus(ev, true, true);
|
this._onMoveFocus(ev, true, true);
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -89,12 +89,15 @@ export const VIEWS = {
|
||||||
// showing flow to trust this new device with cross-signing
|
// showing flow to trust this new device with cross-signing
|
||||||
COMPLETE_SECURITY: 6,
|
COMPLETE_SECURITY: 6,
|
||||||
|
|
||||||
|
// flow to setup SSSS / cross-signing on this account
|
||||||
|
E2E_SETUP: 7,
|
||||||
|
|
||||||
// we are logged in with an active matrix client.
|
// we are logged in with an active matrix client.
|
||||||
LOGGED_IN: 7,
|
LOGGED_IN: 8,
|
||||||
|
|
||||||
// We are logged out (invalid token) but have our local state again. The user
|
// We are logged out (invalid token) but have our local state again. The user
|
||||||
// should log back in to rehydrate the client.
|
// should log back in to rehydrate the client.
|
||||||
SOFT_LOGOUT: 8,
|
SOFT_LOGOUT: 9,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Actions that are redirected through the onboarding process prior to being
|
// Actions that are redirected through the onboarding process prior to being
|
||||||
|
@ -253,6 +256,9 @@ export default createReactClass({
|
||||||
// logout page.
|
// logout page.
|
||||||
Lifecycle.loadSession({});
|
Lifecycle.loadSession({});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._accountPassword = null;
|
||||||
|
this._accountPasswordTimer = null;
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount: function() {
|
componentDidMount: function() {
|
||||||
|
@ -349,6 +355,8 @@ export default createReactClass({
|
||||||
window.removeEventListener("focus", this.onFocus);
|
window.removeEventListener("focus", this.onFocus);
|
||||||
window.removeEventListener('resize', this.handleResize);
|
window.removeEventListener('resize', this.handleResize);
|
||||||
this.state.resizeNotifier.removeListener("middlePanelResized", this._dispatchTimelineResize);
|
this.state.resizeNotifier.removeListener("middlePanelResized", this._dispatchTimelineResize);
|
||||||
|
|
||||||
|
if (this._accountPasswordTimer !== null) clearTimeout(this._accountPasswordTimer);
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillUpdate: function(props, state) {
|
componentWillUpdate: function(props, state) {
|
||||||
|
@ -657,7 +665,9 @@ export default createReactClass({
|
||||||
if (
|
if (
|
||||||
!Lifecycle.isSoftLogout() &&
|
!Lifecycle.isSoftLogout() &&
|
||||||
this.state.view !== VIEWS.LOGIN &&
|
this.state.view !== VIEWS.LOGIN &&
|
||||||
this.state.view !== VIEWS.COMPLETE_SECURITY
|
this.state.view !== VIEWS.REGISTER &&
|
||||||
|
this.state.view !== VIEWS.COMPLETE_SECURITY &&
|
||||||
|
this.state.view !== VIEWS.E2E_SETUP
|
||||||
) {
|
) {
|
||||||
this._onLoggedIn();
|
this._onLoggedIn();
|
||||||
}
|
}
|
||||||
|
@ -961,9 +971,9 @@ export default createReactClass({
|
||||||
const CreateRoomDialog = sdk.getComponent('dialogs.CreateRoomDialog');
|
const CreateRoomDialog = sdk.getComponent('dialogs.CreateRoomDialog');
|
||||||
const modal = Modal.createTrackedDialog('Create Room', '', CreateRoomDialog);
|
const modal = Modal.createTrackedDialog('Create Room', '', CreateRoomDialog);
|
||||||
|
|
||||||
const [shouldCreate, createOpts] = await modal.finished;
|
const [shouldCreate, opts] = await modal.finished;
|
||||||
if (shouldCreate) {
|
if (shouldCreate) {
|
||||||
createRoom({createOpts});
|
createRoom(opts);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -1453,7 +1463,6 @@ export default createReactClass({
|
||||||
|
|
||||||
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
|
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
|
||||||
cli.on("crypto.verification.request", request => {
|
cli.on("crypto.verification.request", request => {
|
||||||
console.log(`MatrixChat got a .request ${request.channel.transactionId}`, request.event.getRoomId());
|
|
||||||
if (request.pending) {
|
if (request.pending) {
|
||||||
ToastStore.sharedInstance().addOrReplaceToast({
|
ToastStore.sharedInstance().addOrReplaceToast({
|
||||||
key: 'verifreq_' + request.channel.transactionId,
|
key: 'verifreq_' + request.channel.transactionId,
|
||||||
|
@ -1725,6 +1734,10 @@ export default createReactClass({
|
||||||
this.showScreen("forgot_password");
|
this.showScreen("forgot_password");
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onRegisterFlowComplete: function(credentials, password) {
|
||||||
|
return this.onUserCompletedLoginFlow(credentials, password);
|
||||||
|
},
|
||||||
|
|
||||||
// returns a promise which resolves to the new MatrixClient
|
// returns a promise which resolves to the new MatrixClient
|
||||||
onRegistered: function(credentials) {
|
onRegistered: function(credentials) {
|
||||||
return Lifecycle.setLoggedIn(credentials);
|
return Lifecycle.setLoggedIn(credentials);
|
||||||
|
@ -1813,7 +1826,14 @@ export default createReactClass({
|
||||||
this._loggedInView = ref;
|
this._loggedInView = ref;
|
||||||
},
|
},
|
||||||
|
|
||||||
async onUserCompletedLoginFlow(credentials) {
|
async onUserCompletedLoginFlow(credentials, password) {
|
||||||
|
this._accountPassword = password;
|
||||||
|
// self-destruct the password after 5mins
|
||||||
|
if (this._accountPasswordTimer !== null) clearTimeout(this._accountPasswordTimer);
|
||||||
|
this._accountPasswordTimer = setTimeout(() => {
|
||||||
|
this._accountPassword = null;
|
||||||
|
this._accountPasswordTimer = null;
|
||||||
|
}, 60 * 5 * 1000);
|
||||||
// Wait for the client to be logged in (but not started)
|
// Wait for the client to be logged in (but not started)
|
||||||
// which is enough to ask the server about account data.
|
// which is enough to ask the server about account data.
|
||||||
const loggedIn = new Promise(resolve => {
|
const loggedIn = new Promise(resolve => {
|
||||||
|
@ -1827,7 +1847,7 @@ export default createReactClass({
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create and start the client in the background
|
// Create and start the client in the background
|
||||||
Lifecycle.setLoggedIn(credentials);
|
const setLoggedInPromise = Lifecycle.setLoggedIn(credentials);
|
||||||
await loggedIn;
|
await loggedIn;
|
||||||
|
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
|
@ -1848,12 +1868,20 @@ export default createReactClass({
|
||||||
|
|
||||||
if (masterKeyInStorage) {
|
if (masterKeyInStorage) {
|
||||||
this.setStateForNewView({ view: VIEWS.COMPLETE_SECURITY });
|
this.setStateForNewView({ view: VIEWS.COMPLETE_SECURITY });
|
||||||
|
} else if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
|
||||||
|
// This will only work if the feature is set to 'enable' in the config,
|
||||||
|
// since it's too early in the lifecycle for users to have turned the
|
||||||
|
// labs flag on.
|
||||||
|
this.setStateForNewView({ view: VIEWS.E2E_SETUP });
|
||||||
} else {
|
} else {
|
||||||
this._onLoggedIn();
|
this._onLoggedIn();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return setLoggedInPromise;
|
||||||
},
|
},
|
||||||
|
|
||||||
onCompleteSecurityFinished() {
|
// complete security / e2e setup has finished
|
||||||
|
onCompleteSecurityE2eSetupFinished() {
|
||||||
this._onLoggedIn();
|
this._onLoggedIn();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -1873,7 +1901,15 @@ export default createReactClass({
|
||||||
const CompleteSecurity = sdk.getComponent('structures.auth.CompleteSecurity');
|
const CompleteSecurity = sdk.getComponent('structures.auth.CompleteSecurity');
|
||||||
view = (
|
view = (
|
||||||
<CompleteSecurity
|
<CompleteSecurity
|
||||||
onFinished={this.onCompleteSecurityFinished}
|
onFinished={this.onCompleteSecurityE2eSetupFinished}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (this.state.view === VIEWS.E2E_SETUP) {
|
||||||
|
const E2eSetup = sdk.getComponent('structures.auth.E2eSetup');
|
||||||
|
view = (
|
||||||
|
<E2eSetup
|
||||||
|
onFinished={this.onCompleteSecurityE2eSetupFinished}
|
||||||
|
accountPassword={this._accountPassword}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (this.state.view === VIEWS.POST_REGISTRATION) {
|
} else if (this.state.view === VIEWS.POST_REGISTRATION) {
|
||||||
|
@ -1940,7 +1976,7 @@ export default createReactClass({
|
||||||
email={this.props.startingFragmentQueryParams.email}
|
email={this.props.startingFragmentQueryParams.email}
|
||||||
brand={this.props.config.brand}
|
brand={this.props.config.brand}
|
||||||
makeRegistrationUrl={this._makeRegistrationUrl}
|
makeRegistrationUrl={this._makeRegistrationUrl}
|
||||||
onLoggedIn={this.onRegistered}
|
onLoggedIn={this.onRegisterFlowComplete}
|
||||||
onLoginClick={this.onLoginClick}
|
onLoginClick={this.onLoginClick}
|
||||||
onServerConfigChange={this.onServerConfigChange}
|
onServerConfigChange={this.onServerConfigChange}
|
||||||
{...this.getServerProperties()}
|
{...this.getServerProperties()}
|
||||||
|
|
|
@ -31,6 +31,7 @@ import PropTypes from 'prop-types';
|
||||||
import RoomTile from "../views/rooms/RoomTile";
|
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";
|
||||||
|
|
||||||
// turn this on for drop & drag console debugging galore
|
// turn this on for drop & drag console debugging galore
|
||||||
const debug = false;
|
const debug = false;
|
||||||
|
@ -141,10 +142,6 @@ export default class RoomSubList extends React.PureComponent {
|
||||||
|
|
||||||
onHeaderKeyDown = (ev) => {
|
onHeaderKeyDown = (ev) => {
|
||||||
switch (ev.key) {
|
switch (ev.key) {
|
||||||
case Key.TAB:
|
|
||||||
// Prevent LeftPanel handling Tab if focus is on the sublist header itself
|
|
||||||
ev.stopPropagation();
|
|
||||||
break;
|
|
||||||
case Key.ARROW_LEFT:
|
case Key.ARROW_LEFT:
|
||||||
// On ARROW_LEFT collapse the room sublist
|
// On ARROW_LEFT collapse the room sublist
|
||||||
if (!this.state.hidden && !this.props.forceExpand) {
|
if (!this.state.hidden && !this.props.forceExpand) {
|
||||||
|
@ -263,33 +260,6 @@ export default class RoomSubList extends React.PureComponent {
|
||||||
const subListNotifCount = subListNotifications.count;
|
const subListNotifCount = subListNotifications.count;
|
||||||
const subListNotifHighlight = subListNotifications.highlight;
|
const subListNotifHighlight = subListNotifications.highlight;
|
||||||
|
|
||||||
let badge;
|
|
||||||
if (!this.props.collapsed) {
|
|
||||||
const badgeClasses = classNames({
|
|
||||||
'mx_RoomSubList_badge': true,
|
|
||||||
'mx_RoomSubList_badgeHighlight': subListNotifHighlight,
|
|
||||||
});
|
|
||||||
// Wrap the contents in a div and apply styles to the child div so that the browser default outline works
|
|
||||||
if (subListNotifCount > 0) {
|
|
||||||
badge = (
|
|
||||||
<AccessibleButton className={badgeClasses} onClick={this._onNotifBadgeClick} aria-label={_t("Jump to first unread room.")}>
|
|
||||||
<div>
|
|
||||||
{ FormattingUtils.formatCount(subListNotifCount) }
|
|
||||||
</div>
|
|
||||||
</AccessibleButton>
|
|
||||||
);
|
|
||||||
} else if (this.props.isInvite && this.props.list.length) {
|
|
||||||
// no notifications but highlight anyway because this is an invite badge
|
|
||||||
badge = (
|
|
||||||
<AccessibleButton className={badgeClasses} onClick={this._onInviteBadgeClick} aria-label={_t("Jump to first invite.")}>
|
|
||||||
<div>
|
|
||||||
{ this.props.list.length }
|
|
||||||
</div>
|
|
||||||
</AccessibleButton>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// When collapsed, allow a long hover on the header to show user
|
// When collapsed, allow a long hover on the header to show user
|
||||||
// the full tag name and room count
|
// the full tag name and room count
|
||||||
let title;
|
let title;
|
||||||
|
@ -305,17 +275,6 @@ export default class RoomSubList extends React.PureComponent {
|
||||||
<IncomingCallBox className="mx_RoomSubList_incomingCall" incomingCall={this.props.incomingCall} />;
|
<IncomingCallBox className="mx_RoomSubList_incomingCall" incomingCall={this.props.incomingCall} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
let addRoomButton;
|
|
||||||
if (this.props.onAddRoom) {
|
|
||||||
addRoomButton = (
|
|
||||||
<AccessibleTooltipButton
|
|
||||||
onClick={this.onAddRoom}
|
|
||||||
className="mx_RoomSubList_addRoom"
|
|
||||||
title={this.props.addRoomLabel || _t("Add room")}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const len = this.props.list.length + this.props.extraTiles.length;
|
const len = this.props.list.length + this.props.extraTiles.length;
|
||||||
let chevron;
|
let chevron;
|
||||||
if (len) {
|
if (len) {
|
||||||
|
@ -327,14 +286,68 @@ export default class RoomSubList extends React.PureComponent {
|
||||||
chevron = (<div className={chevronClasses} />);
|
chevron = (<div className={chevronClasses} />);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return <RovingTabIndexWrapper inputRef={this._headerButton}>
|
||||||
|
{({onFocus, isActive, ref}) => {
|
||||||
|
const tabIndex = isActive ? 0 : -1;
|
||||||
|
|
||||||
|
let badge;
|
||||||
|
if (!this.props.collapsed) {
|
||||||
|
const badgeClasses = classNames({
|
||||||
|
'mx_RoomSubList_badge': true,
|
||||||
|
'mx_RoomSubList_badgeHighlight': subListNotifHighlight,
|
||||||
|
});
|
||||||
|
// Wrap the contents in a div and apply styles to the child div so that the browser default outline works
|
||||||
|
if (subListNotifCount > 0) {
|
||||||
|
badge = (
|
||||||
|
<AccessibleButton
|
||||||
|
tabIndex={tabIndex}
|
||||||
|
className={badgeClasses}
|
||||||
|
onClick={this._onNotifBadgeClick}
|
||||||
|
aria-label={_t("Jump to first unread room.")}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
{ FormattingUtils.formatCount(subListNotifCount) }
|
||||||
|
</div>
|
||||||
|
</AccessibleButton>
|
||||||
|
);
|
||||||
|
} else if (this.props.isInvite && this.props.list.length) {
|
||||||
|
// no notifications but highlight anyway because this is an invite badge
|
||||||
|
badge = (
|
||||||
|
<AccessibleButton
|
||||||
|
tabIndex={tabIndex}
|
||||||
|
className={badgeClasses}
|
||||||
|
onClick={this._onInviteBadgeClick}
|
||||||
|
aria-label={_t("Jump to first invite.")}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
{ this.props.list.length }
|
||||||
|
</div>
|
||||||
|
</AccessibleButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let addRoomButton;
|
||||||
|
if (this.props.onAddRoom) {
|
||||||
|
addRoomButton = (
|
||||||
|
<AccessibleTooltipButton
|
||||||
|
tabIndex={tabIndex}
|
||||||
|
onClick={this.onAddRoom}
|
||||||
|
className="mx_RoomSubList_addRoom"
|
||||||
|
title={this.props.addRoomLabel || _t("Add room")}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_RoomSubList_labelContainer" title={title} ref={this._header} onKeyDown={this.onHeaderKeyDown}>
|
<div className="mx_RoomSubList_labelContainer" title={title} ref={this._header} onKeyDown={this.onHeaderKeyDown}>
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
|
onFocus={onFocus}
|
||||||
|
tabIndex={tabIndex}
|
||||||
|
inputRef={ref}
|
||||||
onClick={this.onClick}
|
onClick={this.onClick}
|
||||||
className="mx_RoomSubList_label"
|
className="mx_RoomSubList_label"
|
||||||
tabIndex={0}
|
|
||||||
aria-expanded={!isCollapsed}
|
aria-expanded={!isCollapsed}
|
||||||
inputRef={this._headerButton}
|
|
||||||
role="treeitem"
|
role="treeitem"
|
||||||
aria-level="1"
|
aria-level="1"
|
||||||
>
|
>
|
||||||
|
@ -346,6 +359,8 @@ export default class RoomSubList extends React.PureComponent {
|
||||||
{ addRoomButton }
|
{ addRoomButton }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
} }
|
||||||
|
</RovingTabIndexWrapper>;
|
||||||
}
|
}
|
||||||
|
|
||||||
checkOverflow = () => {
|
checkOverflow = () => {
|
||||||
|
|
|
@ -766,7 +766,7 @@ export default createReactClass({
|
||||||
|
|
||||||
onUserVerificationChanged: function(userId, _trustStatus) {
|
onUserVerificationChanged: function(userId, _trustStatus) {
|
||||||
const room = this.state.room;
|
const room = this.state.room;
|
||||||
if (!room.currentState.getMember(userId)) {
|
if (!room || !room.currentState.getMember(userId)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._updateE2EStatus(room);
|
this._updateE2EStatus(room);
|
||||||
|
@ -796,6 +796,7 @@ export default createReactClass({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Duplication between here and _updateE2eStatus in RoomTile
|
||||||
/* At this point, the user has encryption on and cross-signing on */
|
/* At this point, the user has encryption on and cross-signing on */
|
||||||
const e2eMembers = await room.getEncryptionTargetMembers();
|
const e2eMembers = await room.getEncryptionTargetMembers();
|
||||||
const verified = [];
|
const verified = [];
|
||||||
|
@ -810,12 +811,12 @@ export default createReactClass({
|
||||||
debuglog("e2e verified", verified, "unverified", unverified);
|
debuglog("e2e verified", verified, "unverified", unverified);
|
||||||
|
|
||||||
/* Check all verified user devices. */
|
/* Check all verified user devices. */
|
||||||
for (const userId of verified) {
|
for (const userId of [...verified, cli.getUserId()]) {
|
||||||
const devices = await cli.getStoredDevicesForUser(userId);
|
const devices = await cli.getStoredDevicesForUser(userId);
|
||||||
const allDevicesVerified = devices.every(({deviceId}) => {
|
const anyDeviceNotVerified = devices.some(({deviceId}) => {
|
||||||
return cli.checkDeviceTrust(userId, deviceId).isVerified();
|
return !cli.checkDeviceTrust(userId, deviceId).isVerified();
|
||||||
});
|
});
|
||||||
if (!allDevicesVerified) {
|
if (anyDeviceNotVerified) {
|
||||||
this.setState({
|
this.setState({
|
||||||
e2eStatus: "warning",
|
e2eStatus: "warning",
|
||||||
});
|
});
|
||||||
|
@ -1367,6 +1368,41 @@ export default createReactClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onRejectAndIgnoreClick: async function() {
|
||||||
|
this.setState({
|
||||||
|
rejecting: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const cli = MatrixClientPeg.get();
|
||||||
|
try {
|
||||||
|
const myMember = this.state.room.getMember(cli.getUserId());
|
||||||
|
const inviteEvent = myMember.events.member;
|
||||||
|
const ignoredUsers = MatrixClientPeg.get().getIgnoredUsers();
|
||||||
|
ignoredUsers.push(inviteEvent.getSender()); // de-duped internally in the js-sdk
|
||||||
|
await cli.setIgnoredUsers(ignoredUsers);
|
||||||
|
|
||||||
|
await cli.leave(this.state.roomId);
|
||||||
|
dis.dispatch({ action: 'view_next_room' });
|
||||||
|
this.setState({
|
||||||
|
rejecting: false,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to reject invite: %s", error);
|
||||||
|
|
||||||
|
const msg = error.message ? error.message : JSON.stringify(error);
|
||||||
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
Modal.createTrackedDialog('Failed to reject invite', '', ErrorDialog, {
|
||||||
|
title: _t("Failed to reject invite"),
|
||||||
|
description: msg,
|
||||||
|
});
|
||||||
|
|
||||||
|
self.setState({
|
||||||
|
rejecting: false,
|
||||||
|
rejectError: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
onRejectThreepidInviteButtonClicked: function(ev) {
|
onRejectThreepidInviteButtonClicked: function(ev) {
|
||||||
// We can reject 3pid invites in the same way that we accept them,
|
// We can reject 3pid invites in the same way that we accept them,
|
||||||
// using /leave rather than /join. In the short term though, we
|
// using /leave rather than /join. In the short term though, we
|
||||||
|
@ -1671,9 +1707,11 @@ export default createReactClass({
|
||||||
return (
|
return (
|
||||||
<div className="mx_RoomView">
|
<div className="mx_RoomView">
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<RoomPreviewBar onJoinClick={this.onJoinButtonClicked}
|
<RoomPreviewBar
|
||||||
|
onJoinClick={this.onJoinButtonClicked}
|
||||||
onForgetClick={this.onForgetClick}
|
onForgetClick={this.onForgetClick}
|
||||||
onRejectClick={this.onRejectButtonClicked}
|
onRejectClick={this.onRejectButtonClicked}
|
||||||
|
onRejectAndIgnoreClick={this.onRejectAndIgnoreClick}
|
||||||
inviterName={inviterName}
|
inviterName={inviterName}
|
||||||
canPreview={false}
|
canPreview={false}
|
||||||
joining={this.state.joining}
|
joining={this.state.joining}
|
||||||
|
|
|
@ -877,11 +877,14 @@ export default createReactClass({
|
||||||
// TODO: the classnames on the div and ol could do with being updated to
|
// TODO: the classnames on the div and ol could do with being updated to
|
||||||
// reflect the fact that we don't necessarily contain a list of messages.
|
// reflect the fact that we don't necessarily contain a list of messages.
|
||||||
// it's not obvious why we have a separate div and ol anyway.
|
// it's not obvious why we have a separate div and ol anyway.
|
||||||
|
|
||||||
|
// give the <ol> an explicit role=list because Safari+VoiceOver seems to think an ordered-list with
|
||||||
|
// list-style-type: none; is no longer a list
|
||||||
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}>
|
||||||
<div className="mx_RoomView_messageListWrapper">
|
<div className="mx_RoomView_messageListWrapper">
|
||||||
<ol ref={this._itemlist} className="mx_RoomView_MessageList" aria-live="polite">
|
<ol ref={this._itemlist} className="mx_RoomView_MessageList" aria-live="polite" role="list">
|
||||||
{ this.props.children }
|
{ this.props.children }
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -133,7 +133,9 @@ export default createReactClass({
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const clearButton = (!this.state.blurred || this.state.searchTerm) ?
|
const clearButton = (!this.state.blurred || this.state.searchTerm) ?
|
||||||
(<AccessibleButton key="button"
|
(<AccessibleButton
|
||||||
|
key="button"
|
||||||
|
tabIndex={-1}
|
||||||
className="mx_SearchBox_closeButton"
|
className="mx_SearchBox_closeButton"
|
||||||
onClick={ () => {this._clearSearch("button"); } }>
|
onClick={ () => {this._clearSearch("button"); } }>
|
||||||
</AccessibleButton>) : undefined;
|
</AccessibleButton>) : undefined;
|
||||||
|
|
|
@ -94,6 +94,10 @@ const TimelinePanel = createReactClass({
|
||||||
// callback which is called when the read-up-to mark is updated.
|
// callback which is called when the read-up-to mark is updated.
|
||||||
onReadMarkerUpdated: PropTypes.func,
|
onReadMarkerUpdated: PropTypes.func,
|
||||||
|
|
||||||
|
// callback which is called when we wish to paginate the timeline
|
||||||
|
// window.
|
||||||
|
onPaginationRequest: PropTypes.func,
|
||||||
|
|
||||||
// maximum number of events to show in a timeline
|
// maximum number of events to show in a timeline
|
||||||
timelineCap: PropTypes.number,
|
timelineCap: PropTypes.number,
|
||||||
|
|
||||||
|
@ -338,6 +342,14 @@ const TimelinePanel = createReactClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onPaginationRequest(timelineWindow, direction, size) {
|
||||||
|
if (this.props.onPaginationRequest) {
|
||||||
|
return this.props.onPaginationRequest(timelineWindow, direction, size);
|
||||||
|
} else {
|
||||||
|
return timelineWindow.paginate(direction, size);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// set off a pagination request.
|
// set off a pagination request.
|
||||||
onMessageListFillRequest: function(backwards) {
|
onMessageListFillRequest: function(backwards) {
|
||||||
if (!this._shouldPaginate()) return Promise.resolve(false);
|
if (!this._shouldPaginate()) return Promise.resolve(false);
|
||||||
|
@ -360,7 +372,7 @@ const TimelinePanel = createReactClass({
|
||||||
debuglog("TimelinePanel: Initiating paginate; backwards:"+backwards);
|
debuglog("TimelinePanel: Initiating paginate; backwards:"+backwards);
|
||||||
this.setState({[paginatingKey]: true});
|
this.setState({[paginatingKey]: true});
|
||||||
|
|
||||||
return this._timelineWindow.paginate(dir, PAGINATE_SIZE).then((r) => {
|
return this.onPaginationRequest(this._timelineWindow, dir, PAGINATE_SIZE).then((r) => {
|
||||||
if (this.unmounted) { return; }
|
if (this.unmounted) { return; }
|
||||||
|
|
||||||
debuglog("TimelinePanel: paginate complete backwards:"+backwards+"; success:"+r);
|
debuglog("TimelinePanel: paginate complete backwards:"+backwards+"; success:"+r);
|
||||||
|
|
|
@ -23,9 +23,11 @@ export default class ToastContainer extends React.Component {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.state = {toasts: ToastStore.sharedInstance().getToasts()};
|
this.state = {toasts: ToastStore.sharedInstance().getToasts()};
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
// Start listening here rather than in componentDidMount because
|
||||||
|
// toasts may dismiss themselves in their didMount if they find
|
||||||
|
// they're already irrelevant by the time they're mounted, and
|
||||||
|
// our own componentDidMount is too late.
|
||||||
ToastStore.sharedInstance().on('update', this._onToastStoreUpdate);
|
ToastStore.sharedInstance().on('update', this._onToastStoreUpdate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {TopLeftMenu} from '../views/context_menus/TopLeftMenu';
|
import TopLeftMenu from '../views/context_menus/TopLeftMenu';
|
||||||
import BaseAvatar from '../views/avatars/BaseAvatar';
|
import BaseAvatar from '../views/avatars/BaseAvatar';
|
||||||
import {MatrixClientPeg} from '../../MatrixClientPeg';
|
import {MatrixClientPeg} from '../../MatrixClientPeg';
|
||||||
import * as Avatar from '../../Avatar';
|
import * as Avatar from '../../Avatar';
|
||||||
|
|
|
@ -35,7 +35,21 @@ export default class CompleteSecurity extends React.Component {
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
phase: PHASE_INTRO,
|
phase: PHASE_INTRO,
|
||||||
|
// this serves dual purpose as the object for the request logic and
|
||||||
|
// the presence of it insidicating that we're in 'verify mode'.
|
||||||
|
// Because of the latter, it lives in the state.
|
||||||
|
verificationRequest: null,
|
||||||
};
|
};
|
||||||
|
MatrixClientPeg.get().on("crypto.verification.request", this.onVerificationRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
if (this.state.verificationRequest) {
|
||||||
|
this.state.verificationRequest.off("change", this.onVerificationRequestChange);
|
||||||
|
}
|
||||||
|
if (MatrixClientPeg.get()) {
|
||||||
|
MatrixClientPeg.get().removeListener("crypto.verification.request", this.onVerificationRequest);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onStartClick = async () => {
|
onStartClick = async () => {
|
||||||
|
@ -44,14 +58,38 @@ export default class CompleteSecurity extends React.Component {
|
||||||
await accessSecretStorage(async () => {
|
await accessSecretStorage(async () => {
|
||||||
await cli.checkOwnCrossSigningTrust();
|
await cli.checkOwnCrossSigningTrust();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (cli.getCrossSigningId()) {
|
||||||
this.setState({
|
this.setState({
|
||||||
phase: PHASE_DONE,
|
phase: PHASE_DONE,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// this will throw if the user hits cancel, so ignore
|
// this will throw if the user hits cancel, so ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onVerificationRequest = (request) => {
|
||||||
|
if (request.otherUserId !== MatrixClientPeg.get().getUserId()) return;
|
||||||
|
|
||||||
|
if (this.state.verificationRequest) {
|
||||||
|
this.state.verificationRequest.off("change", this.onVerificationRequestChange);
|
||||||
|
}
|
||||||
|
request.on("change", this.onVerificationRequestChange);
|
||||||
|
this.setState({
|
||||||
|
verificationRequest: request,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onVerificationRequestChange = () => {
|
||||||
|
if (this.state.verificationRequest.cancelled) {
|
||||||
|
this.state.verificationRequest.off("change", this.onVerificationRequestChange);
|
||||||
|
this.setState({
|
||||||
|
verificationRequest: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onSkipClick = () => {
|
onSkipClick = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
phase: PHASE_CONFIRM_SKIP,
|
phase: PHASE_CONFIRM_SKIP,
|
||||||
|
@ -74,8 +112,7 @@ export default class CompleteSecurity extends React.Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const AuthPage = sdk.getComponent("auth.AuthPage");
|
const AuthPage = sdk.getComponent("auth.AuthPage");
|
||||||
const AuthHeader = sdk.getComponent("auth.AuthHeader");
|
const CompleteSecurityBody = sdk.getComponent("auth.CompleteSecurityBody");
|
||||||
const AuthBody = sdk.getComponent("auth.AuthBody");
|
|
||||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
@ -85,7 +122,13 @@ export default class CompleteSecurity extends React.Component {
|
||||||
let icon;
|
let icon;
|
||||||
let title;
|
let title;
|
||||||
let body;
|
let body;
|
||||||
if (phase === PHASE_INTRO) {
|
|
||||||
|
if (this.state.verificationRequest) {
|
||||||
|
const IncomingSasDialog = sdk.getComponent("views.dialogs.IncomingSasDialog");
|
||||||
|
body = <IncomingSasDialog verifier={this.state.verificationRequest.verifier}
|
||||||
|
onFinished={this.props.onFinished}
|
||||||
|
/>;
|
||||||
|
} else if (phase === PHASE_INTRO) {
|
||||||
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning"></span>;
|
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning"></span>;
|
||||||
title = _t("Complete security");
|
title = _t("Complete security");
|
||||||
body = (
|
body = (
|
||||||
|
@ -161,8 +204,7 @@ export default class CompleteSecurity extends React.Component {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthPage>
|
<AuthPage>
|
||||||
<AuthHeader />
|
<CompleteSecurityBody>
|
||||||
<AuthBody>
|
|
||||||
<h2 className="mx_CompleteSecurity_header">
|
<h2 className="mx_CompleteSecurity_header">
|
||||||
{icon}
|
{icon}
|
||||||
{title}
|
{title}
|
||||||
|
@ -170,7 +212,7 @@ export default class CompleteSecurity extends React.Component {
|
||||||
<div className="mx_CompleteSecurity_body">
|
<div className="mx_CompleteSecurity_body">
|
||||||
{body}
|
{body}
|
||||||
</div>
|
</div>
|
||||||
</AuthBody>
|
</CompleteSecurityBody>
|
||||||
</AuthPage>
|
</AuthPage>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
50
src/components/structures/auth/E2eSetup.js
Normal file
50
src/components/structures/auth/E2eSetup.js
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
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 PropTypes from 'prop-types';
|
||||||
|
import AsyncWrapper from '../../../AsyncWrapper';
|
||||||
|
import * as sdk from '../../../index';
|
||||||
|
|
||||||
|
export default class E2eSetup extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
onFinished: PropTypes.func.isRequired,
|
||||||
|
accountPassword: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
// awkwardly indented because https://github.com/eslint/eslint/issues/11310
|
||||||
|
this._createStorageDialogPromise =
|
||||||
|
import("../../../async-components/views/dialogs/secretstorage/CreateSecretStorageDialog");
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const AuthPage = sdk.getComponent("auth.AuthPage");
|
||||||
|
const CompleteSecurityBody = sdk.getComponent("auth.CompleteSecurityBody");
|
||||||
|
return (
|
||||||
|
<AuthPage>
|
||||||
|
<CompleteSecurityBody>
|
||||||
|
<AsyncWrapper prom={this._createStorageDialogPromise}
|
||||||
|
hasCancel={false}
|
||||||
|
onFinished={this.props.onFinished}
|
||||||
|
accountPassword={this.props.accountPassword}
|
||||||
|
/>
|
||||||
|
</CompleteSecurityBody>
|
||||||
|
</AuthPage>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -58,6 +58,11 @@ export default createReactClass({
|
||||||
displayName: 'Login',
|
displayName: 'Login',
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
|
// Called when the user has logged in. Params:
|
||||||
|
// - The object returned by the login API
|
||||||
|
// - The user's password, if applicable, (may be cached in memory for a
|
||||||
|
// short time so the user is not required to re-enter their password
|
||||||
|
// for operations like uploading cross-signing keys).
|
||||||
onLoggedIn: PropTypes.func.isRequired,
|
onLoggedIn: PropTypes.func.isRequired,
|
||||||
|
|
||||||
// If true, the component will consider itself busy.
|
// If true, the component will consider itself busy.
|
||||||
|
@ -181,7 +186,7 @@ export default createReactClass({
|
||||||
username, phoneCountry, phoneNumber, password,
|
username, phoneCountry, phoneNumber, password,
|
||||||
).then((data) => {
|
).then((data) => {
|
||||||
this.setState({serverIsAlive: true}); // it must be, we logged in.
|
this.setState({serverIsAlive: true}); // it must be, we logged in.
|
||||||
this.props.onLoggedIn(data);
|
this.props.onLoggedIn(data, password);
|
||||||
}, (error) => {
|
}, (error) => {
|
||||||
if (this._unmounted) {
|
if (this._unmounted) {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -45,7 +45,13 @@ export default createReactClass({
|
||||||
displayName: 'Registration',
|
displayName: 'Registration',
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
|
// Called when the user has logged in. Params:
|
||||||
|
// - object with userId, deviceId, homeserverUrl, identityServerUrl, accessToken
|
||||||
|
// - The user's password, if available and applicable (may be cached in memory
|
||||||
|
// for a short time so the user is not required to re-enter their password
|
||||||
|
// for operations like uploading cross-signing keys).
|
||||||
onLoggedIn: PropTypes.func.isRequired,
|
onLoggedIn: PropTypes.func.isRequired,
|
||||||
|
|
||||||
clientSecret: PropTypes.string,
|
clientSecret: PropTypes.string,
|
||||||
sessionId: PropTypes.string,
|
sessionId: PropTypes.string,
|
||||||
makeRegistrationUrl: PropTypes.func.isRequired,
|
makeRegistrationUrl: PropTypes.func.isRequired,
|
||||||
|
@ -348,7 +354,7 @@ export default createReactClass({
|
||||||
homeserverUrl: this.state.matrixClient.getHomeserverUrl(),
|
homeserverUrl: this.state.matrixClient.getHomeserverUrl(),
|
||||||
identityServerUrl: this.state.matrixClient.getIdentityServerUrl(),
|
identityServerUrl: this.state.matrixClient.getIdentityServerUrl(),
|
||||||
accessToken: response.access_token,
|
accessToken: response.access_token,
|
||||||
});
|
}, this.state.formVals.password);
|
||||||
|
|
||||||
this._setupPushers(cli);
|
this._setupPushers(cli);
|
||||||
// we're still busy until we get unmounted: don't show the registration form again
|
// we're still busy until we get unmounted: don't show the registration form again
|
||||||
|
|
|
@ -62,7 +62,7 @@ export default createReactClass({
|
||||||
console.log("Loading recaptcha script...");
|
console.log("Loading recaptcha script...");
|
||||||
window.mx_on_recaptcha_loaded = () => {this._onCaptchaLoaded();};
|
window.mx_on_recaptcha_loaded = () => {this._onCaptchaLoaded();};
|
||||||
let protocol = global.location.protocol;
|
let protocol = global.location.protocol;
|
||||||
if (protocol === "vector:") {
|
if (protocol !== "http:") {
|
||||||
protocol = "https:";
|
protocol = "https:";
|
||||||
}
|
}
|
||||||
const scriptTag = document.createElement('script');
|
const scriptTag = document.createElement('script');
|
||||||
|
|
27
src/components/views/auth/CompleteSecurityBody.js
Normal file
27
src/components/views/auth/CompleteSecurityBody.js
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export default class CompleteSecurityBody extends React.PureComponent {
|
||||||
|
render() {
|
||||||
|
return <div className="mx_CompleteSecurityBody">
|
||||||
|
{ this.props.children }
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
}
|
|
@ -641,7 +641,7 @@ const AuthEntryComponents = [
|
||||||
TermsAuthEntry,
|
TermsAuthEntry,
|
||||||
];
|
];
|
||||||
|
|
||||||
export function getEntryComponentForLoginType(loginType) {
|
export default function getEntryComponentForLoginType(loginType) {
|
||||||
for (const c of AuthEntryComponents) {
|
for (const c of AuthEntryComponents) {
|
||||||
if (c.LOGIN_TYPE == loginType) {
|
if (c.LOGIN_TYPE == loginType) {
|
||||||
return c;
|
return c;
|
||||||
|
|
|
@ -306,7 +306,7 @@ export default createReactClass({
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<MenuItem className="mx_RoomTileContextMenu_tag_field" onClick={this._onClickSettings}>
|
<MenuItem className="mx_RoomTileContextMenu_tag_field" onClick={this._onClickSettings}>
|
||||||
<img className="mx_RoomTileContextMenu_tag_icon" src={require("../../../../res/img/icons-settings-room.svg")} width="15" height="15" alt="" />
|
<img className="mx_RoomTileContextMenu_tag_icon" src={require("../../../../res/img/feather-customised/settings.svg")} width="15" height="15" alt="" />
|
||||||
{ _t('Settings') }
|
{ _t('Settings') }
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -27,7 +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";
|
||||||
|
|
||||||
export class TopLeftMenu extends React.Component {
|
export default class TopLeftMenu extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
displayName: PropTypes.string.isRequired,
|
displayName: PropTypes.string.isRequired,
|
||||||
userId: PropTypes.string.isRequired,
|
userId: PropTypes.string.isRequired,
|
||||||
|
|
|
@ -65,6 +65,9 @@ export default createReactClass({
|
||||||
// Title for the dialog.
|
// Title for the dialog.
|
||||||
title: PropTypes.node.isRequired,
|
title: PropTypes.node.isRequired,
|
||||||
|
|
||||||
|
// Path to an icon to put in the header
|
||||||
|
headerImage: PropTypes.string,
|
||||||
|
|
||||||
// children should be the content of the dialog
|
// children should be the content of the dialog
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
|
|
||||||
|
@ -110,6 +113,13 @@ export default createReactClass({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let headerImage;
|
||||||
|
if (this.props.headerImage) {
|
||||||
|
headerImage = <img className="mx_Dialog_titleImage" src={this.props.headerImage}
|
||||||
|
alt=""
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MatrixClientContext.Provider value={this._matrixClient}>
|
<MatrixClientContext.Provider value={this._matrixClient}>
|
||||||
<FocusLock
|
<FocusLock
|
||||||
|
@ -135,6 +145,7 @@ export default createReactClass({
|
||||||
'mx_Dialog_headerWithButton': !!this.props.headerButton,
|
'mx_Dialog_headerWithButton': !!this.props.headerButton,
|
||||||
})}>
|
})}>
|
||||||
<div className={classNames('mx_Dialog_title', this.props.titleClass)} id='mx_BaseDialog_title'>
|
<div className={classNames('mx_Dialog_title', this.props.titleClass)} id='mx_BaseDialog_title'>
|
||||||
|
{headerImage}
|
||||||
{ this.props.title }
|
{ this.props.title }
|
||||||
</div>
|
</div>
|
||||||
{ this.props.headerButton }
|
{ this.props.headerButton }
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2017 Michael Telatynski <7t3chguy@gmail.com>
|
Copyright 2017 Michael Telatynski <7t3chguy@gmail.com>
|
||||||
|
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.
|
||||||
|
@ -44,13 +45,13 @@ export default createReactClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_roomCreateOptions() {
|
_roomCreateOptions() {
|
||||||
const createOpts = {};
|
const opts = {};
|
||||||
|
const createOpts = opts.createOpts = {};
|
||||||
createOpts.name = this.state.name;
|
createOpts.name = this.state.name;
|
||||||
if (this.state.isPublic) {
|
if (this.state.isPublic) {
|
||||||
createOpts.visibility = "public";
|
createOpts.visibility = "public";
|
||||||
createOpts.preset = "public_chat";
|
createOpts.preset = "public_chat";
|
||||||
// to prevent createRoom from enabling guest access
|
opts.guestAccess = false;
|
||||||
createOpts['initial_state'] = [];
|
|
||||||
const {alias} = this.state;
|
const {alias} = this.state;
|
||||||
const localPart = alias.substr(1, alias.indexOf(":") - 1);
|
const localPart = alias.substr(1, alias.indexOf(":") - 1);
|
||||||
createOpts['room_alias_name'] = localPart;
|
createOpts['room_alias_name'] = localPart;
|
||||||
|
@ -61,7 +62,7 @@ export default createReactClass({
|
||||||
if (this.state.noFederate) {
|
if (this.state.noFederate) {
|
||||||
createOpts.creation_content = {'m.federate': false};
|
createOpts.creation_content = {'m.federate': false};
|
||||||
}
|
}
|
||||||
return createOpts;
|
return opts;
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
|
|
@ -33,6 +33,7 @@ import Modal from "../../../Modal";
|
||||||
import {humanizeTime} from "../../../utils/humanize";
|
import {humanizeTime} from "../../../utils/humanize";
|
||||||
import createRoom from "../../../createRoom";
|
import createRoom from "../../../createRoom";
|
||||||
import {inviteMultipleToRoom} from "../../../RoomInvite";
|
import {inviteMultipleToRoom} from "../../../RoomInvite";
|
||||||
|
import SettingsStore from '../../../settings/SettingsStore';
|
||||||
|
|
||||||
export const KIND_DM = "dm";
|
export const KIND_DM = "dm";
|
||||||
export const KIND_INVITE = "invite";
|
export const KIND_INVITE = "invite";
|
||||||
|
@ -337,19 +338,31 @@ export default class InviteDialog extends React.PureComponent {
|
||||||
const recents = [];
|
const recents = [];
|
||||||
for (const userId in rooms) {
|
for (const userId in rooms) {
|
||||||
// Filter out user IDs that are already in the room / should be excluded
|
// Filter out user IDs that are already in the room / should be excluded
|
||||||
if (excludedTargetIds.includes(userId)) continue;
|
if (excludedTargetIds.includes(userId)) {
|
||||||
|
console.warn(`[Invite:Recents] Excluding ${userId} from recents`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const room = rooms[userId];
|
const room = rooms[userId];
|
||||||
const member = room.getMember(userId);
|
const member = room.getMember(userId);
|
||||||
if (!member) continue; // just skip people who don't have memberships for some reason
|
if (!member) {
|
||||||
|
// just skip people who don't have memberships for some reason
|
||||||
|
console.warn(`[Invite:Recents] ${userId} is missing a member object in their own DM (${room.roomId})`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const lastEventTs = room.timeline && room.timeline.length
|
const lastEventTs = room.timeline && room.timeline.length
|
||||||
? room.timeline[room.timeline.length - 1].getTs()
|
? room.timeline[room.timeline.length - 1].getTs()
|
||||||
: 0;
|
: 0;
|
||||||
if (!lastEventTs) continue; // something weird is going on with this room
|
if (!lastEventTs) {
|
||||||
|
// something weird is going on with this room
|
||||||
|
console.warn(`[Invite:Recents] ${userId} (${room.roomId}) has a weird last timestamp: ${lastEventTs}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
recents.push({userId, user: member, lastActive: lastEventTs});
|
recents.push({userId, user: member, lastActive: lastEventTs});
|
||||||
}
|
}
|
||||||
|
if (!recents) console.warn("[Invite:Recents] No recents to suggest!");
|
||||||
|
|
||||||
// Sort the recents by last active to save us time later
|
// Sort the recents by last active to save us time later
|
||||||
recents.sort((a, b) => b.lastActive - a.lastActive);
|
recents.sort((a, b) => b.lastActive - a.lastActive);
|
||||||
|
@ -493,7 +506,7 @@ export default class InviteDialog extends React.PureComponent {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
_startDm = () => {
|
_startDm = async () => {
|
||||||
this.setState({busy: true});
|
this.setState({busy: true});
|
||||||
const targetIds = this.state.targets.map(t => t.userId);
|
const targetIds = this.state.targets.map(t => t.userId);
|
||||||
|
|
||||||
|
@ -510,14 +523,31 @@ export default class InviteDialog extends React.PureComponent {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const createRoomOptions = {};
|
||||||
|
|
||||||
|
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
|
||||||
|
// Check whether all users have uploaded device keys before.
|
||||||
|
// If so, enable encryption in the new room.
|
||||||
|
const client = MatrixClientPeg.get();
|
||||||
|
const usersToDevicesMap = await client.downloadKeys(targetIds);
|
||||||
|
const allHaveDeviceKeys = Object.values(usersToDevicesMap).every(devices => {
|
||||||
|
// `devices` is an object of the form { deviceId: deviceInfo, ... }.
|
||||||
|
return Object.keys(devices).length > 0;
|
||||||
|
});
|
||||||
|
if (allHaveDeviceKeys) {
|
||||||
|
createRoomOptions.encryption = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check if it's a traditional DM and create the room if required.
|
// Check if it's a traditional DM and create the room if required.
|
||||||
// TODO: [Canonical DMs] Remove this check and instead just create the multi-person DM
|
// TODO: [Canonical DMs] Remove this check and instead just create the multi-person DM
|
||||||
let createRoomPromise = Promise.resolve();
|
let createRoomPromise = Promise.resolve();
|
||||||
if (targetIds.length === 1) {
|
if (targetIds.length === 1) {
|
||||||
createRoomPromise = createRoom({dmUserId: targetIds[0]});
|
createRoomOptions.dmUserId = targetIds[0];
|
||||||
|
createRoomPromise = createRoom(createRoomOptions);
|
||||||
} else {
|
} else {
|
||||||
// Create a boring room and try to invite the targets manually.
|
// Create a boring room and try to invite the targets manually.
|
||||||
createRoomPromise = createRoom().then(roomId => {
|
createRoomPromise = createRoom(createRoomOptions).then(roomId => {
|
||||||
return inviteMultipleToRoom(roomId, targetIds);
|
return inviteMultipleToRoom(roomId, targetIds);
|
||||||
}).then(result => {
|
}).then(result => {
|
||||||
if (this._shouldAbortAfterInviteError(result)) {
|
if (this._shouldAbortAfterInviteError(result)) {
|
||||||
|
@ -586,13 +616,36 @@ export default class InviteDialog extends React.PureComponent {
|
||||||
clearTimeout(this._debounceTimer);
|
clearTimeout(this._debounceTimer);
|
||||||
}
|
}
|
||||||
this._debounceTimer = setTimeout(async () => {
|
this._debounceTimer = setTimeout(async () => {
|
||||||
MatrixClientPeg.get().searchUserDirectory({term}).then(r => {
|
MatrixClientPeg.get().searchUserDirectory({term}).then(async r => {
|
||||||
if (term !== this.state.filterText) {
|
if (term !== this.state.filterText) {
|
||||||
// Discard the results - we were probably too slow on the server-side to make
|
// Discard the results - we were probably too slow on the server-side to make
|
||||||
// these results useful. This is a race we want to avoid because we could overwrite
|
// these results useful. This is a race we want to avoid because we could overwrite
|
||||||
// more accurate results.
|
// more accurate results.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!r.results) r.results = [];
|
||||||
|
|
||||||
|
// While we're here, try and autocomplete a search result for the mxid itself
|
||||||
|
// if there's no matches (and the input looks like a mxid).
|
||||||
|
if (term[0] === '@' && term.indexOf(':') > 1 && r.results.length === 0) {
|
||||||
|
try {
|
||||||
|
const profile = await MatrixClientPeg.get().getProfileInfo(term);
|
||||||
|
if (profile) {
|
||||||
|
// If we have a profile, we have enough information to assume that
|
||||||
|
// the mxid can be invited - add it to the list
|
||||||
|
r.results.push({
|
||||||
|
user_id: term,
|
||||||
|
display_name: profile['displayname'],
|
||||||
|
avatar_url: profile['avatar_url'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Non-fatal error trying to make an invite for a user ID");
|
||||||
|
console.warn(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
serverResultsMixin: r.results.map(u => ({
|
serverResultsMixin: r.results.map(u => ({
|
||||||
userId: u.user_id,
|
userId: u.user_id,
|
||||||
|
@ -672,11 +725,16 @@ export default class InviteDialog extends React.PureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
_toggleMember = (member: Member) => {
|
_toggleMember = (member: Member) => {
|
||||||
|
let filterText = this.state.filterText;
|
||||||
const targets = this.state.targets.map(t => t); // cheap clone for mutation
|
const targets = this.state.targets.map(t => t); // cheap clone for mutation
|
||||||
const idx = targets.indexOf(member);
|
const idx = targets.indexOf(member);
|
||||||
if (idx >= 0) targets.splice(idx, 1);
|
if (idx >= 0) {
|
||||||
else targets.push(member);
|
targets.splice(idx, 1);
|
||||||
this.setState({targets});
|
} else {
|
||||||
|
targets.push(member);
|
||||||
|
filterText = ""; // clear the filter when the user accepts a suggestion
|
||||||
|
}
|
||||||
|
this.setState({targets, filterText});
|
||||||
};
|
};
|
||||||
|
|
||||||
_removeMember = (member: Member) => {
|
_removeMember = (member: Member) => {
|
||||||
|
@ -876,7 +934,7 @@ export default class InviteDialog extends React.PureComponent {
|
||||||
key={"input"}
|
key={"input"}
|
||||||
rows={1}
|
rows={1}
|
||||||
onChange={this._updateFilter}
|
onChange={this._updateFilter}
|
||||||
defaultValue={this.state.filterText}
|
value={this.state.filterText}
|
||||||
ref={this._editorRef}
|
ref={this._editorRef}
|
||||||
onPaste={this._onPaste}
|
onPaste={this._onPaste}
|
||||||
/>
|
/>
|
||||||
|
@ -944,7 +1002,7 @@ export default class InviteDialog extends React.PureComponent {
|
||||||
|
|
||||||
title = _t("Direct Messages");
|
title = _t("Direct Messages");
|
||||||
helpText = _t(
|
helpText = _t(
|
||||||
"If you can't find someone, ask them for their username, or share your " +
|
"If you can't find someone, ask them for their username, share your " +
|
||||||
"username (%(userId)s) or <a>profile link</a>.",
|
"username (%(userId)s) or <a>profile link</a>.",
|
||||||
{userId},
|
{userId},
|
||||||
{a: (sub) => <a href={makeUserPermalink(userId)} rel="noopener" target="_blank">{sub}</a>},
|
{a: (sub) => <a href={makeUserPermalink(userId)} rel="noopener" target="_blank">{sub}</a>},
|
||||||
|
@ -970,7 +1028,7 @@ export default class InviteDialog extends React.PureComponent {
|
||||||
title={title}
|
title={title}
|
||||||
>
|
>
|
||||||
<div className='mx_InviteDialog_content'>
|
<div className='mx_InviteDialog_content'>
|
||||||
<p>{helpText}</p>
|
<p className='mx_InviteDialog_helpText'>{helpText}</p>
|
||||||
<div className='mx_InviteDialog_addressBar'>
|
<div className='mx_InviteDialog_addressBar'>
|
||||||
{this._renderEditor()}
|
{this._renderEditor()}
|
||||||
<div className='mx_InviteDialog_buttonAndSpinner'>
|
<div className='mx_InviteDialog_buttonAndSpinner'>
|
||||||
|
@ -987,9 +1045,11 @@ export default class InviteDialog extends React.PureComponent {
|
||||||
</div>
|
</div>
|
||||||
{this._renderIdentityServerWarning()}
|
{this._renderIdentityServerWarning()}
|
||||||
<div className='error'>{this.state.errorText}</div>
|
<div className='error'>{this.state.errorText}</div>
|
||||||
|
<div className='mx_InviteDialog_userSections'>
|
||||||
{this._renderSection('recents')}
|
{this._renderSection('recents')}
|
||||||
{this._renderSection('suggestions')}
|
{this._renderSection('suggestions')}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,8 @@ import { _t } from '../../../languageHandler';
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import {MatrixEvent} from "matrix-js-sdk";
|
import {MatrixEvent} from "matrix-js-sdk";
|
||||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||||
|
import SdkConfig from '../../../SdkConfig';
|
||||||
|
import Markdown from '../../../Markdown';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* A dialog for reporting an event.
|
* A dialog for reporting an event.
|
||||||
|
@ -95,6 +97,15 @@ export default class ReportEventDialog extends PureComponent {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const adminMessageMD =
|
||||||
|
SdkConfig.get().reportEvent &&
|
||||||
|
SdkConfig.get().reportEvent.adminMessageMD;
|
||||||
|
let adminMessage;
|
||||||
|
if (adminMessageMD) {
|
||||||
|
const html = new Markdown(adminMessageMD).toHTML({ externalLinks: true });
|
||||||
|
adminMessage = <p dangerouslySetInnerHTML={{ __html: html }} />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseDialog
|
<BaseDialog
|
||||||
className="mx_BugReportDialog"
|
className="mx_BugReportDialog"
|
||||||
|
@ -110,7 +121,7 @@ export default class ReportEventDialog extends PureComponent {
|
||||||
"administrator will not be able to read the message text or view any files or images.")
|
"administrator will not be able to read the message text or view any files or images.")
|
||||||
}
|
}
|
||||||
</p>
|
</p>
|
||||||
|
{adminMessage}
|
||||||
<Field
|
<Field
|
||||||
id="mx_ReportEventDialog_reason"
|
id="mx_ReportEventDialog_reason"
|
||||||
className="mx_ReportEventDialog_reason"
|
className="mx_ReportEventDialog_reason"
|
||||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import * as sdk from '../../../../index';
|
import * as sdk from '../../../../index';
|
||||||
import {MatrixClientPeg} from '../../../../MatrixClientPeg';
|
import {MatrixClientPeg} from '../../../../MatrixClientPeg';
|
||||||
import { MatrixClient } from 'matrix-js-sdk';
|
import { MatrixClient } from 'matrix-js-sdk';
|
||||||
|
@ -32,6 +33,16 @@ const RESTORE_TYPE_SECRET_STORAGE = 2;
|
||||||
* Dialog for restoring e2e keys from a backup and the user's recovery key
|
* Dialog for restoring e2e keys from a backup and the user's recovery key
|
||||||
*/
|
*/
|
||||||
export default class RestoreKeyBackupDialog extends React.PureComponent {
|
export default class RestoreKeyBackupDialog extends React.PureComponent {
|
||||||
|
static propTypes = {
|
||||||
|
// if false, will close the dialog as soon as the restore completes succesfully
|
||||||
|
// default: true
|
||||||
|
showSummary: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
defaultProps = {
|
||||||
|
showSummary: true,
|
||||||
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
|
@ -96,6 +107,10 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
||||||
const recoverInfo = await MatrixClientPeg.get().restoreKeyBackupWithPassword(
|
const recoverInfo = await MatrixClientPeg.get().restoreKeyBackupWithPassword(
|
||||||
this.state.passPhrase, undefined, undefined, this.state.backupInfo,
|
this.state.passPhrase, undefined, undefined, this.state.backupInfo,
|
||||||
);
|
);
|
||||||
|
if (!this.props.showSummary) {
|
||||||
|
this.props.onFinished(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.setState({
|
this.setState({
|
||||||
loading: false,
|
loading: false,
|
||||||
recoverInfo,
|
recoverInfo,
|
||||||
|
@ -119,6 +134,10 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
||||||
const recoverInfo = await MatrixClientPeg.get().restoreKeyBackupWithRecoveryKey(
|
const recoverInfo = await MatrixClientPeg.get().restoreKeyBackupWithRecoveryKey(
|
||||||
this.state.recoveryKey, undefined, undefined, this.state.backupInfo,
|
this.state.recoveryKey, undefined, undefined, this.state.backupInfo,
|
||||||
);
|
);
|
||||||
|
if (!this.props.showSummary) {
|
||||||
|
this.props.onFinished(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.setState({
|
this.setState({
|
||||||
loading: false,
|
loading: false,
|
||||||
recoverInfo,
|
recoverInfo,
|
||||||
|
@ -253,6 +272,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
||||||
title = _t("Error");
|
title = _t("Error");
|
||||||
content = _t("No backup found!");
|
content = _t("No backup found!");
|
||||||
} else if (this.state.recoverInfo) {
|
} else if (this.state.recoverInfo) {
|
||||||
|
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||||
title = _t("Backup Restored");
|
title = _t("Backup Restored");
|
||||||
let failedToDecrypt;
|
let failedToDecrypt;
|
||||||
if (this.state.recoverInfo.total > this.state.recoverInfo.imported) {
|
if (this.state.recoverInfo.total > this.state.recoverInfo.imported) {
|
||||||
|
@ -264,6 +284,11 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
||||||
content = <div>
|
content = <div>
|
||||||
<p>{_t("Restored %(sessionCount)s session keys", {sessionCount: this.state.recoverInfo.imported})}</p>
|
<p>{_t("Restored %(sessionCount)s session keys", {sessionCount: this.state.recoverInfo.imported})}</p>
|
||||||
{failedToDecrypt}
|
{failedToDecrypt}
|
||||||
|
<DialogButtons primaryButton={_t('OK')}
|
||||||
|
onPrimaryButtonClick={this._onDone}
|
||||||
|
hasCancel={false}
|
||||||
|
focus={true}
|
||||||
|
/>
|
||||||
</div>;
|
</div>;
|
||||||
} else if (backupHasPassphrase && !this.state.forceRecoveryKey) {
|
} else if (backupHasPassphrase && !this.state.forceRecoveryKey) {
|
||||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||||
|
|
|
@ -34,12 +34,19 @@ export default createReactClass({
|
||||||
// A node to insert into the cancel button instead of default "Cancel"
|
// A node to insert into the cancel button instead of default "Cancel"
|
||||||
cancelButton: PropTypes.node,
|
cancelButton: PropTypes.node,
|
||||||
|
|
||||||
|
// If true, make the primary button a form submit button (input type="submit")
|
||||||
|
primaryIsSubmit: PropTypes.bool,
|
||||||
|
|
||||||
// onClick handler for the primary button.
|
// onClick handler for the primary button.
|
||||||
onPrimaryButtonClick: PropTypes.func.isRequired,
|
onPrimaryButtonClick: PropTypes.func,
|
||||||
|
|
||||||
// should there be a cancel button? default: true
|
// should there be a cancel button? default: true
|
||||||
hasCancel: PropTypes.bool,
|
hasCancel: PropTypes.bool,
|
||||||
|
|
||||||
|
// The class of the cancel button, only used if a cancel button is
|
||||||
|
// enabled
|
||||||
|
cancelButtonClass: PropTypes.node,
|
||||||
|
|
||||||
// onClick handler for the cancel button.
|
// onClick handler for the cancel button.
|
||||||
onCancel: PropTypes.func,
|
onCancel: PropTypes.func,
|
||||||
|
|
||||||
|
@ -69,16 +76,26 @@ export default createReactClass({
|
||||||
primaryButtonClassName += " " + this.props.primaryButtonClass;
|
primaryButtonClassName += " " + this.props.primaryButtonClass;
|
||||||
}
|
}
|
||||||
let cancelButton;
|
let cancelButton;
|
||||||
|
|
||||||
if (this.props.cancelButton || this.props.hasCancel) {
|
if (this.props.cancelButton || this.props.hasCancel) {
|
||||||
cancelButton = <button onClick={this._onCancelClick} disabled={this.props.disabled}>
|
cancelButton = <button
|
||||||
|
// important: the default type is 'submit' and this button comes before the
|
||||||
|
// primary in the DOM so will get form submissions unless we make it not a submit.
|
||||||
|
type="button"
|
||||||
|
onClick={this._onCancelClick}
|
||||||
|
className={this.props.cancelButtonClass}
|
||||||
|
disabled={this.props.disabled}
|
||||||
|
>
|
||||||
{ this.props.cancelButton || _t("Cancel") }
|
{ this.props.cancelButton || _t("Cancel") }
|
||||||
</button>;
|
</button>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons">
|
||||||
{ cancelButton }
|
{ cancelButton }
|
||||||
{ this.props.children }
|
{ this.props.children }
|
||||||
<button className={primaryButtonClassName}
|
<button type={this.props.primaryIsSubmit ? 'submit' : 'button'}
|
||||||
|
className={primaryButtonClassName}
|
||||||
onClick={this.props.onPrimaryButtonClick}
|
onClick={this.props.onPrimaryButtonClick}
|
||||||
autoFocus={this.props.focus}
|
autoFocus={this.props.focus}
|
||||||
disabled={this.props.disabled || this.props.primaryDisabled}
|
disabled={this.props.disabled || this.props.primaryDisabled}
|
||||||
|
|
56
src/components/views/elements/crypto/VerificationQRCode.js
Normal file
56
src/components/views/elements/crypto/VerificationQRCode.js
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
/*
|
||||||
|
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 PropTypes from "prop-types";
|
||||||
|
import {replaceableComponent} from "../../../../utils/replaceableComponent";
|
||||||
|
import * as qs from "qs";
|
||||||
|
import QRCode from "qrcode-react";
|
||||||
|
|
||||||
|
@replaceableComponent("views.elements.crypto.VerificationQRCode")
|
||||||
|
export default class VerificationQRCode extends React.PureComponent {
|
||||||
|
static propTypes = {
|
||||||
|
// Common for all kinds of QR codes
|
||||||
|
keys: PropTypes.array.isRequired, // array of [Key ID, Base64 Key] pairs
|
||||||
|
action: PropTypes.string.isRequired,
|
||||||
|
keyholderUserId: PropTypes.string.isRequired,
|
||||||
|
|
||||||
|
// User verification use case only
|
||||||
|
secret: PropTypes.string,
|
||||||
|
otherUserKey: PropTypes.string, // Base64 key being verified
|
||||||
|
requestEventId: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
action: "verify",
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const query = {
|
||||||
|
request: this.props.requestEventId,
|
||||||
|
action: this.props.action,
|
||||||
|
other_user_key: this.props.otherUserKey,
|
||||||
|
secret: this.props.secret,
|
||||||
|
};
|
||||||
|
for (const key of this.props.keys) {
|
||||||
|
query[`key_${key[0]}`] = key[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
const uri = `https://matrix.to/#/${this.props.keyholderUserId}?${qs.stringify(query)}`;
|
||||||
|
|
||||||
|
return <QRCode value={uri} size={256} logoWidth={48} logo={require("../../../../../res/img/matrix-m.svg")} />;
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,7 +20,7 @@ import PropTypes from 'prop-types';
|
||||||
import * as sdk from '../../../index';
|
import * as sdk from '../../../index';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
|
||||||
import * as recent from './recent';
|
import * as recent from '../../../emojipicker/recent';
|
||||||
import {DATA_BY_CATEGORY, getEmojiFromUnicode} from "../../../emoji";
|
import {DATA_BY_CATEGORY, getEmojiFromUnicode} from "../../../emoji";
|
||||||
|
|
||||||
export const CATEGORY_HEADER_HEIGHT = 22;
|
export const CATEGORY_HEADER_HEIGHT = 22;
|
||||||
|
|
|
@ -26,6 +26,7 @@ import classNames from 'classnames';
|
||||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||||
import {ContextMenu, ContextMenuButton, toRightOf} from "../../structures/ContextMenu";
|
import {ContextMenu, ContextMenuButton, toRightOf} from "../../structures/ContextMenu";
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
|
import {RovingTabIndexWrapper} from "../../../accessibility/RovingTabIndex";
|
||||||
|
|
||||||
// XXX this class copies a lot from RoomTile.js
|
// XXX this class copies a lot from RoomTile.js
|
||||||
export default createReactClass({
|
export default createReactClass({
|
||||||
|
@ -127,7 +128,8 @@ export default createReactClass({
|
||||||
'mx_RoomTile_badgeShown': this.state.badgeHover || isMenuDisplayed,
|
'mx_RoomTile_badgeShown': this.state.badgeHover || isMenuDisplayed,
|
||||||
});
|
});
|
||||||
|
|
||||||
const label = <div title={this.props.group.groupId} className={nameClasses} dir="auto">
|
// XXX: this is a workaround for Firefox giving this div a tabstop :( [tabIndex]
|
||||||
|
const label = <div title={this.props.group.groupId} className={nameClasses} tabIndex={-1} dir="auto">
|
||||||
{ groupName }
|
{ groupName }
|
||||||
</div>;
|
</div>;
|
||||||
|
|
||||||
|
@ -137,16 +139,6 @@ export default createReactClass({
|
||||||
});
|
});
|
||||||
|
|
||||||
const badgeContent = badgeEllipsis ? '\u00B7\u00B7\u00B7' : '!';
|
const badgeContent = badgeEllipsis ? '\u00B7\u00B7\u00B7' : '!';
|
||||||
const badge = (
|
|
||||||
<ContextMenuButton
|
|
||||||
className={badgeClasses}
|
|
||||||
onClick={this.onContextMenuButtonClick}
|
|
||||||
label={_t("Options")}
|
|
||||||
isExpanded={isMenuDisplayed}
|
|
||||||
>
|
|
||||||
{ badgeContent }
|
|
||||||
</ContextMenuButton>
|
|
||||||
);
|
|
||||||
|
|
||||||
let tooltip;
|
let tooltip;
|
||||||
if (this.props.collapsed && this.state.hover) {
|
if (this.props.collapsed && this.state.hover) {
|
||||||
|
@ -171,7 +163,12 @@ export default createReactClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
return <React.Fragment>
|
return <React.Fragment>
|
||||||
|
<RovingTabIndexWrapper>
|
||||||
|
{({onFocus, isActive, ref}) =>
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
|
onFocus={onFocus}
|
||||||
|
tabIndex={isActive ? 0 : -1}
|
||||||
|
inputRef={ref}
|
||||||
className={classes}
|
className={classes}
|
||||||
onClick={this.onClick}
|
onClick={this.onClick}
|
||||||
onMouseEnter={this.onMouseEnter}
|
onMouseEnter={this.onMouseEnter}
|
||||||
|
@ -183,10 +180,20 @@ export default createReactClass({
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_RoomTile_nameContainer">
|
<div className="mx_RoomTile_nameContainer">
|
||||||
{ label }
|
{ label }
|
||||||
{ badge }
|
<ContextMenuButton
|
||||||
|
className={badgeClasses}
|
||||||
|
onClick={this.onContextMenuButtonClick}
|
||||||
|
label={_t("Options")}
|
||||||
|
isExpanded={isMenuDisplayed}
|
||||||
|
tabIndex={isActive ? 0 : -1}
|
||||||
|
>
|
||||||
|
{ badgeContent }
|
||||||
|
</ContextMenuButton>
|
||||||
</div>
|
</div>
|
||||||
{ tooltip }
|
{ tooltip }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
|
}
|
||||||
|
</RovingTabIndexWrapper>
|
||||||
|
|
||||||
{ contextMenu }
|
{ contextMenu }
|
||||||
</React.Fragment>;
|
</React.Fragment>;
|
||||||
|
|
|
@ -93,7 +93,7 @@ export default class MKeyVerificationConclusion extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (title) {
|
if (title) {
|
||||||
const subtitle = userLabelForEventRoom(request.otherUserId, mxEvent);
|
const subtitle = userLabelForEventRoom(request.otherUserId, mxEvent.getRoomId());
|
||||||
const classes = classNames("mx_EventTile_bubble", "mx_KeyVerification", "mx_KeyVerification_icon", {
|
const classes = classNames("mx_EventTile_bubble", "mx_KeyVerification", "mx_KeyVerification_icon", {
|
||||||
mx_KeyVerification_icon_verified: request.done,
|
mx_KeyVerification_icon_verified: request.done,
|
||||||
});
|
});
|
||||||
|
|
|
@ -85,7 +85,7 @@ export default class MKeyVerificationRequest extends React.Component {
|
||||||
if (userId === myUserId) {
|
if (userId === myUserId) {
|
||||||
return _t("You accepted");
|
return _t("You accepted");
|
||||||
} else {
|
} else {
|
||||||
return _t("%(name)s accepted", {name: getNameForEventRoom(userId, this.props.mxEvent)});
|
return _t("%(name)s accepted", {name: getNameForEventRoom(userId, this.props.mxEvent.getRoomId())});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,7 +95,7 @@ export default class MKeyVerificationRequest extends React.Component {
|
||||||
if (userId === myUserId) {
|
if (userId === myUserId) {
|
||||||
return _t("You cancelled");
|
return _t("You cancelled");
|
||||||
} else {
|
} else {
|
||||||
return _t("%(name)s cancelled", {name: getNameForEventRoom(userId, this.props.mxEvent)});
|
return _t("%(name)s cancelled", {name: getNameForEventRoom(userId, this.props.mxEvent.getRoomId())});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -128,10 +128,11 @@ export default class MKeyVerificationRequest extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!request.initiatedByMe) {
|
if (!request.initiatedByMe) {
|
||||||
|
const name = getNameForEventRoom(request.requestingUserId, mxEvent.getRoomId());
|
||||||
title = (<div className="mx_KeyVerification_title">{
|
title = (<div className="mx_KeyVerification_title">{
|
||||||
_t("%(name)s wants to verify", {name: getNameForEventRoom(request.requestingUserId, mxEvent)})}</div>);
|
_t("%(name)s wants to verify", {name})}</div>);
|
||||||
subtitle = (<div className="mx_KeyVerification_subtitle">{
|
subtitle = (<div className="mx_KeyVerification_subtitle">{
|
||||||
userLabelForEventRoom(request.requestingUserId, mxEvent)}</div>);
|
userLabelForEventRoom(request.requestingUserId, mxEvent.getRoomId())}</div>);
|
||||||
if (request.requested && !request.observeOnly) {
|
if (request.requested && !request.observeOnly) {
|
||||||
stateNode = (<div className="mx_KeyVerification_buttons">
|
stateNode = (<div className="mx_KeyVerification_buttons">
|
||||||
<FormButton kind="danger" onClick={this._onRejectClicked} label={_t("Decline")} />
|
<FormButton kind="danger" onClick={this._onRejectClicked} label={_t("Decline")} />
|
||||||
|
@ -142,7 +143,7 @@ export default class MKeyVerificationRequest extends React.Component {
|
||||||
title = (<div className="mx_KeyVerification_title">{
|
title = (<div className="mx_KeyVerification_title">{
|
||||||
_t("You sent a verification request")}</div>);
|
_t("You sent a verification request")}</div>);
|
||||||
subtitle = (<div className="mx_KeyVerification_subtitle">{
|
subtitle = (<div className="mx_KeyVerification_subtitle">{
|
||||||
userLabelForEventRoom(request.receivingUserId, mxEvent)}</div>);
|
userLabelForEventRoom(request.receivingUserId, mxEvent.getRoomId())}</div>);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (title) {
|
if (title) {
|
||||||
|
|
|
@ -82,7 +82,7 @@ const _getE2EStatus = (cli, userId, devices) => {
|
||||||
return "warning";
|
return "warning";
|
||||||
};
|
};
|
||||||
|
|
||||||
function openDMForUser(matrixClient, userId) {
|
async function openDMForUser(matrixClient, userId) {
|
||||||
const dmRooms = DMRoomMap.shared().getDMRoomsForUserId(userId);
|
const dmRooms = DMRoomMap.shared().getDMRoomsForUserId(userId);
|
||||||
const lastActiveRoom = dmRooms.reduce((lastActiveRoom, roomId) => {
|
const lastActiveRoom = dmRooms.reduce((lastActiveRoom, roomId) => {
|
||||||
const room = matrixClient.getRoom(roomId);
|
const room = matrixClient.getRoom(roomId);
|
||||||
|
@ -100,9 +100,27 @@ function openDMForUser(matrixClient, userId) {
|
||||||
action: 'view_room',
|
action: 'view_room',
|
||||||
room_id: lastActiveRoom.roomId,
|
room_id: lastActiveRoom.roomId,
|
||||||
});
|
});
|
||||||
} else {
|
return;
|
||||||
createRoom({dmUserId: userId});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const createRoomOptions = {
|
||||||
|
dmUserId: userId,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
|
||||||
|
// Check whether all users have uploaded device keys before.
|
||||||
|
// If so, enable encryption in the new room.
|
||||||
|
const usersToDevicesMap = await matrixClient.downloadKeys([userId]);
|
||||||
|
const allHaveDeviceKeys = Object.values(usersToDevicesMap).every(devices => {
|
||||||
|
// `devices` is an object of the form { deviceId: deviceInfo, ... }.
|
||||||
|
return Object.keys(devices).length > 0;
|
||||||
|
});
|
||||||
|
if (allHaveDeviceKeys) {
|
||||||
|
createRoomOptions.encryption = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createRoom(createRoomOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
function useIsEncrypted(cli, room) {
|
function useIsEncrypted(cli, room) {
|
||||||
|
@ -1219,10 +1237,9 @@ const UserInfo = ({user, groupId, roomId, onClose}) => {
|
||||||
|
|
||||||
let closeButton;
|
let closeButton;
|
||||||
if (onClose) {
|
if (onClose) {
|
||||||
closeButton = <AccessibleButton
|
closeButton = <AccessibleButton className="mx_UserInfo_cancel" onClick={onClose} title={_t('Close')}>
|
||||||
className="mx_UserInfo_cancel"
|
<div />
|
||||||
onClick={onClose}
|
</AccessibleButton>;
|
||||||
title={_t('Close')} />;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const memberDetails = (
|
const memberDetails = (
|
||||||
|
@ -1308,15 +1325,18 @@ const UserInfo = ({user, groupId, roomId, onClose}) => {
|
||||||
userTrust.isVerified();
|
userTrust.isVerified();
|
||||||
const isMe = user.userId === cli.getUserId();
|
const isMe = user.userId === cli.getUserId();
|
||||||
let verifyButton;
|
let verifyButton;
|
||||||
if (!userVerified && !isMe) {
|
if (isRoomEncrypted && !userVerified && !isMe) {
|
||||||
verifyButton = <AccessibleButton className="mx_UserInfo_verify" onClick={() => verifyUser(user)}>
|
verifyButton = <AccessibleButton className="mx_UserInfo_verify" onClick={() => verifyUser(user)}>
|
||||||
{_t("Verify")}
|
{_t("Verify")}
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const devicesSection = <DevicesSection
|
let devicesSection;
|
||||||
|
if (isRoomEncrypted) {
|
||||||
|
devicesSection = <DevicesSection
|
||||||
loading={devices === undefined}
|
loading={devices === undefined}
|
||||||
devices={devices} userId={user.userId} />;
|
devices={devices} userId={user.userId} />;
|
||||||
|
}
|
||||||
|
|
||||||
const securitySection = (
|
const securitySection = (
|
||||||
<div className="mx_UserInfo_container">
|
<div className="mx_UserInfo_container">
|
||||||
|
@ -1335,6 +1355,7 @@ const UserInfo = ({user, groupId, roomId, onClose}) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_UserInfo" role="tabpanel">
|
<div className="mx_UserInfo" role="tabpanel">
|
||||||
|
<AutoHideScrollbar className="mx_UserInfo_scrollContainer">
|
||||||
{ closeButton }
|
{ closeButton }
|
||||||
{ avatarElement }
|
{ avatarElement }
|
||||||
|
|
||||||
|
@ -1360,7 +1381,6 @@ const UserInfo = ({user, groupId, roomId, onClose}) => {
|
||||||
</div>
|
</div>
|
||||||
</div> }
|
</div> }
|
||||||
|
|
||||||
<AutoHideScrollbar className="mx_UserInfo_scrollContainer">
|
|
||||||
{ securitySection }
|
{ securitySection }
|
||||||
<UserOptionsSection
|
<UserOptionsSection
|
||||||
devices={devices}
|
devices={devices}
|
||||||
|
|
|
@ -17,6 +17,9 @@ limitations under the License.
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import * as sdk from '../../../index';
|
import * as sdk from '../../../index';
|
||||||
import {verificationMethods} from 'matrix-js-sdk/src/crypto';
|
import {verificationMethods} from 'matrix-js-sdk/src/crypto';
|
||||||
|
import VerificationQRCode from "../elements/crypto/VerificationQRCode";
|
||||||
|
import {VerificationRequest} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||||
|
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||||
|
|
||||||
export default class VerificationPanel extends React.PureComponent {
|
export default class VerificationPanel extends React.PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -36,7 +39,8 @@ export default class VerificationPanel extends React.PureComponent {
|
||||||
renderStatus() {
|
renderStatus() {
|
||||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||||
const Spinner = sdk.getComponent('elements.Spinner');
|
const Spinner = sdk.getComponent('elements.Spinner');
|
||||||
const {request} = this.props;
|
const {request: req} = this.props;
|
||||||
|
const request: VerificationRequest = req;
|
||||||
|
|
||||||
if (request.requested) {
|
if (request.requested) {
|
||||||
return (<p>Waiting for {request.otherUserId} to accept ... <Spinner /></p>);
|
return (<p>Waiting for {request.otherUserId} to accept ... <Spinner /></p>);
|
||||||
|
@ -44,6 +48,24 @@ export default class VerificationPanel extends React.PureComponent {
|
||||||
const verifyButton = <AccessibleButton kind="primary" onClick={this._startSAS}>
|
const verifyButton = <AccessibleButton kind="primary" onClick={this._startSAS}>
|
||||||
Verify by emoji
|
Verify by emoji
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
|
|
||||||
|
const crossSigningInfo = MatrixClientPeg.get().getStoredCrossSigningForUser(request.otherUserId);
|
||||||
|
const myKeyId = MatrixClientPeg.get().getCrossSigningId();
|
||||||
|
if (request.requestEvent && request.requestEvent.getId() && crossSigningInfo) {
|
||||||
|
const qrCodeKeys = [
|
||||||
|
[MatrixClientPeg.get().getDeviceId(), MatrixClientPeg.get().getDeviceEd25519Key()],
|
||||||
|
[myKeyId, myKeyId],
|
||||||
|
];
|
||||||
|
const qrCode = <VerificationQRCode
|
||||||
|
keyholderUserId={MatrixClientPeg.get().getUserId()}
|
||||||
|
requestEventId={request.requestEvent.getId()}
|
||||||
|
otherUserKey={crossSigningInfo.getId("master")}
|
||||||
|
secret={request.encodedSharedSecret}
|
||||||
|
keys={qrCodeKeys}
|
||||||
|
/>;
|
||||||
|
return (<p>{request.otherUserId} is ready, start {verifyButton} or have them scan: {qrCode}</p>);
|
||||||
|
}
|
||||||
|
|
||||||
return (<p>{request.otherUserId} is ready, start {verifyButton}</p>);
|
return (<p>{request.otherUserId} is ready, start {verifyButton}</p>);
|
||||||
} else if (request.started) {
|
} else if (request.started) {
|
||||||
if (this.state.sasWaitingForOtherParty) {
|
if (this.state.sasWaitingForOtherParty) {
|
||||||
|
|
|
@ -209,6 +209,7 @@ export default class BasicMessageEditor extends React.Component {
|
||||||
const range = getRangeForSelection(this._editorRef, model, selection);
|
const range = getRangeForSelection(this._editorRef, model, selection);
|
||||||
const selectedParts = range.parts.map(p => p.serialize());
|
const selectedParts = range.parts.map(p => p.serialize());
|
||||||
event.clipboardData.setData("application/x-riot-composer", JSON.stringify(selectedParts));
|
event.clipboardData.setData("application/x-riot-composer", JSON.stringify(selectedParts));
|
||||||
|
event.clipboardData.setData("text/plain", text); // so plain copy/paste works
|
||||||
if (type === "cut") {
|
if (type === "cut") {
|
||||||
// Remove the text, updating the model as appropriate
|
// Remove the text, updating the model as appropriate
|
||||||
this._modifiedFlag = true;
|
this._modifiedFlag = true;
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2019 New Vector Ltd
|
Copyright 2019 New Vector Ltd
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -14,76 +15,102 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import React, {useState} from "react";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { _t } from '../../../languageHandler';
|
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
|
||||||
import SettingsStore from '../../../settings/SettingsStore';
|
|
||||||
|
|
||||||
export default function(props) {
|
import {_t, _td} from '../../../languageHandler';
|
||||||
const { isUser } = props;
|
import {useFeatureEnabled} from "../../../hooks/useSettings";
|
||||||
const isNormal = props.status === "normal";
|
import AccessibleButton from "../elements/AccessibleButton";
|
||||||
const isWarning = props.status === "warning";
|
import Tooltip from "../elements/Tooltip";
|
||||||
const isVerified = props.status === "verified";
|
|
||||||
const e2eIconClasses = classNames({
|
export const E2E_STATE = {
|
||||||
|
VERIFIED: "verified",
|
||||||
|
WARNING: "warning",
|
||||||
|
UNKNOWN: "unknown",
|
||||||
|
NORMAL: "normal",
|
||||||
|
};
|
||||||
|
|
||||||
|
const crossSigningUserTitles = {
|
||||||
|
[E2E_STATE.WARNING]: _td("This user has not verified all of their devices."),
|
||||||
|
[E2E_STATE.NORMAL]: _td("You have not verified this user. This user has verified all of their devices."),
|
||||||
|
[E2E_STATE.VERIFIED]: _td("You have verified this user. This user has verified all of their devices."),
|
||||||
|
};
|
||||||
|
const crossSigningRoomTitles = {
|
||||||
|
[E2E_STATE.WARNING]: _td("Someone is using an unknown device"),
|
||||||
|
[E2E_STATE.NORMAL]: _td("This room is end-to-end encrypted"),
|
||||||
|
[E2E_STATE.VERIFIED]: _td("Everyone in this room is verified"),
|
||||||
|
};
|
||||||
|
|
||||||
|
const legacyUserTitles = {
|
||||||
|
[E2E_STATE.WARNING]: _td("Some devices for this user are not trusted"),
|
||||||
|
[E2E_STATE.VERIFIED]: _td("All devices for this user are trusted"),
|
||||||
|
};
|
||||||
|
const legacyRoomTitles = {
|
||||||
|
[E2E_STATE.WARNING]: _td("Some devices in this encrypted room are not trusted"),
|
||||||
|
[E2E_STATE.VERIFIED]: _td("All devices in this encrypted room are trusted"),
|
||||||
|
};
|
||||||
|
|
||||||
|
const E2EIcon = ({isUser, status, className, size, onClick}) => {
|
||||||
|
const [hover, setHover] = useState(false);
|
||||||
|
|
||||||
|
const classes = classNames({
|
||||||
mx_E2EIcon: true,
|
mx_E2EIcon: true,
|
||||||
mx_E2EIcon_warning: isWarning,
|
mx_E2EIcon_warning: status === E2E_STATE.WARNING,
|
||||||
mx_E2EIcon_normal: isNormal,
|
mx_E2EIcon_normal: status === E2E_STATE.NORMAL,
|
||||||
mx_E2EIcon_verified: isVerified,
|
mx_E2EIcon_verified: status === E2E_STATE.VERIFIED,
|
||||||
}, props.className);
|
}, className);
|
||||||
|
|
||||||
let e2eTitle;
|
let e2eTitle;
|
||||||
|
const crossSigning = useFeatureEnabled("feature_cross_signing");
|
||||||
const crossSigning = SettingsStore.isFeatureEnabled("feature_cross_signing");
|
|
||||||
if (crossSigning && isUser) {
|
if (crossSigning && isUser) {
|
||||||
if (isWarning) {
|
e2eTitle = crossSigningUserTitles[status];
|
||||||
e2eTitle = _t(
|
|
||||||
"This user has not verified all of their devices.",
|
|
||||||
);
|
|
||||||
} else if (isNormal) {
|
|
||||||
e2eTitle = _t(
|
|
||||||
"You have not verified this user. " +
|
|
||||||
"This user has verified all of their devices.",
|
|
||||||
);
|
|
||||||
} else if (isVerified) {
|
|
||||||
e2eTitle = _t(
|
|
||||||
"You have verified this user. " +
|
|
||||||
"This user has verified all of their devices.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else if (crossSigning && !isUser) {
|
} else if (crossSigning && !isUser) {
|
||||||
if (isWarning) {
|
e2eTitle = crossSigningRoomTitles[status];
|
||||||
e2eTitle = _t(
|
|
||||||
"Some users in this encrypted room are not verified by you or " +
|
|
||||||
"they have not verified their own devices.",
|
|
||||||
);
|
|
||||||
} else if (isVerified) {
|
|
||||||
e2eTitle = _t(
|
|
||||||
"All users in this encrypted room are verified by you and " +
|
|
||||||
"they have verified their own devices.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else if (!crossSigning && isUser) {
|
} else if (!crossSigning && isUser) {
|
||||||
if (isWarning) {
|
e2eTitle = legacyUserTitles[status];
|
||||||
e2eTitle = _t("Some devices for this user are not trusted");
|
|
||||||
} else if (isVerified) {
|
|
||||||
e2eTitle = _t("All devices for this user are trusted");
|
|
||||||
}
|
|
||||||
} else if (!crossSigning && !isUser) {
|
} else if (!crossSigning && !isUser) {
|
||||||
if (isWarning) {
|
e2eTitle = legacyRoomTitles[status];
|
||||||
e2eTitle = _t("Some devices in this encrypted room are not trusted");
|
|
||||||
} else if (isVerified) {
|
|
||||||
e2eTitle = _t("All devices in this encrypted room are trusted");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let style = null;
|
let style;
|
||||||
if (props.size) {
|
if (size) {
|
||||||
style = {width: `${props.size}px`, height: `${props.size}px`};
|
style = {width: `${size}px`, height: `${size}px`};
|
||||||
}
|
}
|
||||||
|
|
||||||
const icon = (<div className={e2eIconClasses} style={style} title={e2eTitle} />);
|
const onMouseOver = () => setHover(true);
|
||||||
if (props.onClick) {
|
const onMouseOut = () => setHover(false);
|
||||||
return (<AccessibleButton onClick={props.onClick}>{ icon }</AccessibleButton>);
|
|
||||||
} else {
|
let tip;
|
||||||
return icon;
|
if (hover) {
|
||||||
|
tip = <Tooltip label={e2eTitle ? _t(e2eTitle) : ""} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (onClick) {
|
||||||
|
return (
|
||||||
|
<AccessibleButton
|
||||||
|
onClick={onClick}
|
||||||
|
onMouseOver={onMouseOver}
|
||||||
|
onMouseOut={onMouseOut}
|
||||||
|
className={classes}
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
|
{ tip }
|
||||||
|
</AccessibleButton>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return <div onMouseOver={onMouseOver} onMouseOut={onMouseOut} className={classes} style={style}>
|
||||||
|
{ tip }
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
E2EIcon.propTypes = {
|
||||||
|
isUser: PropTypes.bool,
|
||||||
|
status: PropTypes.oneOf(Object.values(E2E_STATE)),
|
||||||
|
className: PropTypes.string,
|
||||||
|
size: PropTypes.number,
|
||||||
|
onClick: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default E2EIcon;
|
||||||
|
|
|
@ -33,6 +33,7 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||||
import {ALL_RULE_TYPES} from "../../../mjolnir/BanList";
|
import {ALL_RULE_TYPES} from "../../../mjolnir/BanList";
|
||||||
import * as ObjectUtils from "../../../ObjectUtils";
|
import * as ObjectUtils from "../../../ObjectUtils";
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
|
import {E2E_STATE} from "./E2EIcon";
|
||||||
|
|
||||||
const eventTileTypes = {
|
const eventTileTypes = {
|
||||||
'm.room.message': 'messages.MessageEvent',
|
'm.room.message': 'messages.MessageEvent',
|
||||||
|
@ -235,6 +236,7 @@ export default createReactClass({
|
||||||
this._suppressReadReceiptAnimation = false;
|
this._suppressReadReceiptAnimation = false;
|
||||||
const client = this.context;
|
const client = this.context;
|
||||||
client.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
client.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||||
|
client.on("userTrustStatusChanged", this.onUserVerificationChanged);
|
||||||
this.props.mxEvent.on("Event.decrypted", this._onDecrypted);
|
this.props.mxEvent.on("Event.decrypted", this._onDecrypted);
|
||||||
if (this.props.showReactions) {
|
if (this.props.showReactions) {
|
||||||
this.props.mxEvent.on("Event.relationsCreated", this._onReactionsCreated);
|
this.props.mxEvent.on("Event.relationsCreated", this._onReactionsCreated);
|
||||||
|
@ -260,6 +262,7 @@ export default createReactClass({
|
||||||
componentWillUnmount: function() {
|
componentWillUnmount: function() {
|
||||||
const client = this.context;
|
const client = this.context;
|
||||||
client.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
client.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||||
|
client.removeListener("userTrustStatusChanged", this.onUserVerificationChanged);
|
||||||
this.props.mxEvent.removeListener("Event.decrypted", this._onDecrypted);
|
this.props.mxEvent.removeListener("Event.decrypted", this._onDecrypted);
|
||||||
if (this.props.showReactions) {
|
if (this.props.showReactions) {
|
||||||
this.props.mxEvent.removeListener("Event.relationsCreated", this._onReactionsCreated);
|
this.props.mxEvent.removeListener("Event.relationsCreated", this._onReactionsCreated);
|
||||||
|
@ -282,18 +285,56 @@ export default createReactClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onUserVerificationChanged: function(userId, _trustStatus) {
|
||||||
|
if (userId === this.props.mxEvent.getSender()) {
|
||||||
|
this._verifyEvent(this.props.mxEvent);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
_verifyEvent: async function(mxEvent) {
|
_verifyEvent: async function(mxEvent) {
|
||||||
if (!mxEvent.isEncrypted()) {
|
if (!mxEvent.isEncrypted()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we directly trust the device, short-circuit here
|
||||||
const verified = await this.context.isEventSenderVerified(mxEvent);
|
const verified = await this.context.isEventSenderVerified(mxEvent);
|
||||||
|
if (verified) {
|
||||||
this.setState({
|
this.setState({
|
||||||
verified: verified,
|
verified: E2E_STATE.VERIFIED,
|
||||||
}, () => {
|
}, () => {
|
||||||
// Decryption may have caused a change in size
|
// Decryption may have caused a change in size
|
||||||
this.props.onHeightChanged();
|
this.props.onHeightChanged();
|
||||||
});
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If cross-signing is off, the old behaviour is to scream at the user
|
||||||
|
// as if they've done something wrong, which they haven't
|
||||||
|
if (!SettingsStore.isFeatureEnabled("feature_cross_signing")) {
|
||||||
|
this.setState({
|
||||||
|
verified: E2E_STATE.WARNING,
|
||||||
|
}, this.props.onHeightChanged);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.context.checkUserTrust(mxEvent.getSender()).isCrossSigningVerified()) {
|
||||||
|
this.setState({
|
||||||
|
verified: E2E_STATE.NORMAL,
|
||||||
|
}, this.props.onHeightChanged);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventSenderTrust = await this.context.checkEventSenderTrust(mxEvent);
|
||||||
|
if (!eventSenderTrust) {
|
||||||
|
this.setState({
|
||||||
|
verified: E2E_STATE.UNKNOWN,
|
||||||
|
}, this.props.onHeightChanged); // Decryption may have cause a change in size
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
verified: eventSenderTrust.isVerified() ? E2E_STATE.VERIFIED : E2E_STATE.WARNING,
|
||||||
|
}, this.props.onHeightChanged); // Decryption may have caused a change in size
|
||||||
},
|
},
|
||||||
|
|
||||||
_propsEqual: function(objA, objB) {
|
_propsEqual: function(objA, objB) {
|
||||||
|
@ -473,8 +514,12 @@ export default createReactClass({
|
||||||
|
|
||||||
// event is encrypted, display padlock corresponding to whether or not it is verified
|
// event is encrypted, display padlock corresponding to whether or not it is verified
|
||||||
if (ev.isEncrypted()) {
|
if (ev.isEncrypted()) {
|
||||||
if (this.state.verified) {
|
if (this.state.verified === E2E_STATE.NORMAL) {
|
||||||
|
return; // no icon if we've not even cross-signed the user
|
||||||
|
} else if (this.state.verified === E2E_STATE.VERIFIED) {
|
||||||
return; // no icon for verified
|
return; // no icon for verified
|
||||||
|
} else if (this.state.verified === E2E_STATE.UNKNOWN) {
|
||||||
|
return (<E2ePadlockUnknown />);
|
||||||
} else {
|
} else {
|
||||||
return (<E2ePadlockUnverified />);
|
return (<E2ePadlockUnverified />);
|
||||||
}
|
}
|
||||||
|
@ -527,6 +572,7 @@ export default createReactClass({
|
||||||
console.error("EventTile attempted to get relations for an event without an ID");
|
console.error("EventTile attempted to get relations for an event without an ID");
|
||||||
// Use event's special `toJSON` method to log key data.
|
// Use event's special `toJSON` method to log key data.
|
||||||
console.log(JSON.stringify(this.props.mxEvent, null, 4));
|
console.log(JSON.stringify(this.props.mxEvent, null, 4));
|
||||||
|
console.trace("Stacktrace for https://github.com/vector-im/riot-web/issues/11120");
|
||||||
}
|
}
|
||||||
return this.props.getRelationsForEvent(eventId, "m.annotation", "m.reaction");
|
return this.props.getRelationsForEvent(eventId, "m.annotation", "m.reaction");
|
||||||
},
|
},
|
||||||
|
@ -604,8 +650,9 @@ export default createReactClass({
|
||||||
mx_EventTile_last: this.props.last,
|
mx_EventTile_last: this.props.last,
|
||||||
mx_EventTile_contextual: this.props.contextual,
|
mx_EventTile_contextual: this.props.contextual,
|
||||||
mx_EventTile_actionBarFocused: this.state.actionBarFocused,
|
mx_EventTile_actionBarFocused: this.state.actionBarFocused,
|
||||||
mx_EventTile_verified: !isBubbleMessage && this.state.verified === true,
|
mx_EventTile_verified: !isBubbleMessage && this.state.verified === E2E_STATE.VERIFIED,
|
||||||
mx_EventTile_unverified: !isBubbleMessage && this.state.verified === false,
|
mx_EventTile_unverified: !isBubbleMessage && this.state.verified === E2E_STATE.WARNING,
|
||||||
|
mx_EventTile_unknown: !isBubbleMessage && this.state.verified === E2E_STATE.UNKNOWN,
|
||||||
mx_EventTile_bad: isEncryptionFailure,
|
mx_EventTile_bad: isEncryptionFailure,
|
||||||
mx_EventTile_emote: msgtype === 'm.emote',
|
mx_EventTile_emote: msgtype === 'm.emote',
|
||||||
mx_EventTile_redacted: isRedacted,
|
mx_EventTile_redacted: isRedacted,
|
||||||
|
@ -901,6 +948,12 @@ function E2ePadlockUnencrypted(props) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function E2ePadlockUnknown(props) {
|
||||||
|
return (
|
||||||
|
<E2ePadlock title={_t("Encrypted by a deleted device")} icon="unknown" {...props} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
class E2ePadlock extends React.Component {
|
class E2ePadlock extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
icon: PropTypes.string.isRequired,
|
icon: PropTypes.string.isRequired,
|
||||||
|
|
51
src/components/views/rooms/InviteOnlyIcon.js
Normal file
51
src/components/views/rooms/InviteOnlyIcon.js
Normal 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 React from 'react';
|
||||||
|
import { _t } from '../../../languageHandler';
|
||||||
|
import * as sdk from '../../../index';
|
||||||
|
|
||||||
|
export default class InviteOnlyIcon extends React.Component {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
hover: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
onHoverStart = () => {
|
||||||
|
this.setState({hover: true});
|
||||||
|
};
|
||||||
|
|
||||||
|
onHoverEnd = () => {
|
||||||
|
this.setState({hover: false});
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const Tooltip = sdk.getComponent("elements.Tooltip");
|
||||||
|
let tooltip;
|
||||||
|
if (this.state.hover) {
|
||||||
|
tooltip = <Tooltip className="mx_InviteOnlyIcon_tooltip" label={_t("Invite only")} dir="auto" />;
|
||||||
|
}
|
||||||
|
return (<div className="mx_InviteOnlyIcon"
|
||||||
|
onMouseEnter={this.onHoverStart}
|
||||||
|
onMouseLeave={this.onHoverEnd}
|
||||||
|
>
|
||||||
|
{ tooltip }
|
||||||
|
</div>);
|
||||||
|
}
|
||||||
|
}
|
|
@ -26,6 +26,7 @@ import Stickerpicker from './Stickerpicker';
|
||||||
import { makeRoomPermalink } from '../../../utils/permalinks/Permalinks';
|
import { makeRoomPermalink } from '../../../utils/permalinks/Permalinks';
|
||||||
import ContentMessages from '../../../ContentMessages';
|
import ContentMessages from '../../../ContentMessages';
|
||||||
import E2EIcon from './E2EIcon';
|
import E2EIcon from './E2EIcon';
|
||||||
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
|
||||||
function ComposerAvatar(props) {
|
function ComposerAvatar(props) {
|
||||||
const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar');
|
const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar');
|
||||||
|
@ -168,7 +169,6 @@ export default class MessageComposer extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.onInputStateChanged = this.onInputStateChanged.bind(this);
|
this.onInputStateChanged = this.onInputStateChanged.bind(this);
|
||||||
this.onEvent = this.onEvent.bind(this);
|
|
||||||
this._onRoomStateEvents = this._onRoomStateEvents.bind(this);
|
this._onRoomStateEvents = this._onRoomStateEvents.bind(this);
|
||||||
this._onRoomViewStoreUpdate = this._onRoomViewStoreUpdate.bind(this);
|
this._onRoomViewStoreUpdate = this._onRoomViewStoreUpdate.bind(this);
|
||||||
this._onTombstoneClick = this._onTombstoneClick.bind(this);
|
this._onTombstoneClick = this._onTombstoneClick.bind(this);
|
||||||
|
@ -182,11 +182,6 @@ export default class MessageComposer extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
// N.B. using 'event' rather than 'RoomEvents' otherwise the crypto handler
|
|
||||||
// for 'event' fires *after* 'RoomEvent', and our room won't have yet been
|
|
||||||
// marked as encrypted.
|
|
||||||
// XXX: fragile as all hell - fixme somehow, perhaps with a dedicated Room.encryption event or something.
|
|
||||||
MatrixClientPeg.get().on("event", this.onEvent);
|
|
||||||
MatrixClientPeg.get().on("RoomState.events", this._onRoomStateEvents);
|
MatrixClientPeg.get().on("RoomState.events", this._onRoomStateEvents);
|
||||||
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
|
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
|
||||||
this._waitForOwnMember();
|
this._waitForOwnMember();
|
||||||
|
@ -210,7 +205,6 @@ export default class MessageComposer extends React.Component {
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
if (MatrixClientPeg.get()) {
|
if (MatrixClientPeg.get()) {
|
||||||
MatrixClientPeg.get().removeListener("event", this.onEvent);
|
|
||||||
MatrixClientPeg.get().removeListener("RoomState.events", this._onRoomStateEvents);
|
MatrixClientPeg.get().removeListener("RoomState.events", this._onRoomStateEvents);
|
||||||
}
|
}
|
||||||
if (this._roomStoreToken) {
|
if (this._roomStoreToken) {
|
||||||
|
@ -218,13 +212,6 @@ export default class MessageComposer extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onEvent(event) {
|
|
||||||
if (event.getType() !== 'm.room.encryption') return;
|
|
||||||
if (event.getRoomId() !== this.props.room.roomId) return;
|
|
||||||
// TODO: put (encryption state??) in state
|
|
||||||
this.forceUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
_onRoomStateEvents(ev, state) {
|
_onRoomStateEvents(ev, state) {
|
||||||
if (ev.getRoomId() !== this.props.room.roomId) return;
|
if (ev.getRoomId() !== this.props.room.roomId) return;
|
||||||
|
|
||||||
|
@ -282,21 +269,36 @@ export default class MessageComposer extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
renderPlaceholderText() {
|
renderPlaceholderText() {
|
||||||
const roomIsEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId);
|
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
|
||||||
if (this.state.isQuoting) {
|
if (this.state.isQuoting) {
|
||||||
if (roomIsEncrypted) {
|
if (this.props.e2eStatus) {
|
||||||
|
return _t('Send an encrypted reply…');
|
||||||
|
} else {
|
||||||
|
return _t('Send a reply…');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (this.props.e2eStatus) {
|
||||||
|
return _t('Send an encrypted message…');
|
||||||
|
} else {
|
||||||
|
return _t('Send a message…');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (this.state.isQuoting) {
|
||||||
|
if (this.props.e2eStatus) {
|
||||||
return _t('Send an encrypted reply…');
|
return _t('Send an encrypted reply…');
|
||||||
} else {
|
} else {
|
||||||
return _t('Send a reply (unencrypted)…');
|
return _t('Send a reply (unencrypted)…');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (roomIsEncrypted) {
|
if (this.props.e2eStatus) {
|
||||||
return _t('Send an encrypted message…');
|
return _t('Send an encrypted message…');
|
||||||
} else {
|
} else {
|
||||||
return _t('Send a message (unencrypted)…');
|
return _t('Send a message (unencrypted)…');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const controls = [
|
const controls = [
|
||||||
|
|
|
@ -363,7 +363,7 @@ export default class RoomBreadcrumbs extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
let dmIndicator;
|
let dmIndicator;
|
||||||
if (this._isDmRoom(r.room)) {
|
if (this._isDmRoom(r.room) && !SettingsStore.isFeatureEnabled("feature_cross_signing")) {
|
||||||
dmIndicator = <img
|
dmIndicator = <img
|
||||||
src={require("../../../../res/img/icon_person.svg")}
|
src={require("../../../../res/img/icon_person.svg")}
|
||||||
className="mx_RoomBreadcrumbs_dmIndicator"
|
className="mx_RoomBreadcrumbs_dmIndicator"
|
||||||
|
|
|
@ -31,7 +31,9 @@ import ManageIntegsButton from '../elements/ManageIntegsButton';
|
||||||
import {CancelButton} from './SimpleRoomHeader';
|
import {CancelButton} from './SimpleRoomHeader';
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import RoomHeaderButtons from '../right_panel/RoomHeaderButtons';
|
import RoomHeaderButtons from '../right_panel/RoomHeaderButtons';
|
||||||
|
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||||
import E2EIcon from './E2EIcon';
|
import E2EIcon from './E2EIcon';
|
||||||
|
import InviteOnlyIcon from './InviteOnlyIcon';
|
||||||
|
|
||||||
export default createReactClass({
|
export default createReactClass({
|
||||||
displayName: 'RoomHeader',
|
displayName: 'RoomHeader',
|
||||||
|
@ -160,13 +162,16 @@ export default createReactClass({
|
||||||
<E2EIcon status={this.props.e2eStatus} /> :
|
<E2EIcon status={this.props.e2eStatus} /> :
|
||||||
undefined;
|
undefined;
|
||||||
|
|
||||||
|
const dmUserId = DMRoomMap.shared().getUserIdForRoomId(this.props.room.roomId);
|
||||||
const joinRules = this.props.room && this.props.room.currentState.getStateEvents("m.room.join_rules", "");
|
const joinRules = this.props.room && this.props.room.currentState.getStateEvents("m.room.join_rules", "");
|
||||||
const joinRule = joinRules && joinRules.getContent().join_rule;
|
const joinRule = joinRules && joinRules.getContent().join_rule;
|
||||||
const joinRuleClass = classNames("mx_RoomHeader_PrivateIcon",
|
let privateIcon;
|
||||||
{"mx_RoomHeader_isPrivate": joinRule === "invite"});
|
// Don't show an invite-only icon for DMs. Users know they're invite-only.
|
||||||
const privateIcon = SettingsStore.isFeatureEnabled("feature_cross_signing") ?
|
if (!dmUserId && SettingsStore.isFeatureEnabled("feature_cross_signing")) {
|
||||||
<div className={joinRuleClass} /> :
|
if (joinRule == "invite") {
|
||||||
undefined;
|
privateIcon = <InviteOnlyIcon />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (this.props.onCancelClick) {
|
if (this.props.onCancelClick) {
|
||||||
cancelButton = <CancelButton onClick={this.props.onCancelClick} />;
|
cancelButton = <CancelButton onClick={this.props.onCancelClick} />;
|
||||||
|
@ -310,8 +315,7 @@ export default createReactClass({
|
||||||
return (
|
return (
|
||||||
<div className="mx_RoomHeader light-panel">
|
<div className="mx_RoomHeader light-panel">
|
||||||
<div className="mx_RoomHeader_wrapper">
|
<div className="mx_RoomHeader_wrapper">
|
||||||
<div className="mx_RoomHeader_avatar">{ roomAvatar }</div>
|
<div className="mx_RoomHeader_avatar">{ roomAvatar }{ e2eIcon }</div>
|
||||||
{ e2eIcon }
|
|
||||||
{ privateIcon }
|
{ privateIcon }
|
||||||
{ name }
|
{ name }
|
||||||
{ topicElement }
|
{ topicElement }
|
||||||
|
|
|
@ -39,6 +39,7 @@ import * as sdk from "../../../index";
|
||||||
import * as Receipt from "../../../utils/Receipt";
|
import * as Receipt from "../../../utils/Receipt";
|
||||||
import {Resizer} from '../../../resizer';
|
import {Resizer} from '../../../resizer';
|
||||||
import {Layout, Distributor} from '../../../resizer/distributors/roomsublist2';
|
import {Layout, Distributor} from '../../../resizer/distributors/roomsublist2';
|
||||||
|
import {RovingTabIndexProvider} from "../../../accessibility/RovingTabIndex";
|
||||||
|
|
||||||
const HIDE_CONFERENCE_CHANS = true;
|
const HIDE_CONFERENCE_CHANS = true;
|
||||||
const STANDARD_TAGS_REGEX = /^(m\.(favourite|lowpriority|server_notice)|im\.vector\.fake\.(invite|recent|direct|archived))$/;
|
const STANDARD_TAGS_REGEX = /^(m\.(favourite|lowpriority|server_notice)|im\.vector\.fake\.(invite|recent|direct|archived))$/;
|
||||||
|
@ -718,7 +719,7 @@ export default createReactClass({
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
list: this.state.lists['im.vector.fake.direct'],
|
list: this.state.lists['im.vector.fake.direct'],
|
||||||
label: _t('People'),
|
label: _t('Direct Messages'),
|
||||||
tagName: "im.vector.fake.direct",
|
tagName: "im.vector.fake.direct",
|
||||||
order: "recent",
|
order: "recent",
|
||||||
incomingCall: incomingCallIfTaggedAs('im.vector.fake.direct'),
|
incomingCall: incomingCallIfTaggedAs('im.vector.fake.direct'),
|
||||||
|
@ -776,10 +777,12 @@ export default createReactClass({
|
||||||
|
|
||||||
const subListComponents = this._mapSubListProps(subLists);
|
const subListComponents = this._mapSubListProps(subLists);
|
||||||
|
|
||||||
const {resizeNotifier, collapsed, searchFilter, ConferenceHandler, ...props} = this.props; // eslint-disable-line
|
const {resizeNotifier, collapsed, searchFilter, ConferenceHandler, onKeyDown, ...props} = this.props; // eslint-disable-line
|
||||||
return (
|
return (
|
||||||
<div
|
<RovingTabIndexProvider handleHomeEnd={true} onKeyDown={onKeyDown}>
|
||||||
|
{({onKeyDownHandler}) => <div
|
||||||
{...props}
|
{...props}
|
||||||
|
onKeyDown={onKeyDownHandler}
|
||||||
ref={this._collectResizeContainer}
|
ref={this._collectResizeContainer}
|
||||||
className="mx_RoomList"
|
className="mx_RoomList"
|
||||||
role="tree"
|
role="tree"
|
||||||
|
@ -788,7 +791,8 @@ export default createReactClass({
|
||||||
onMouseLeave={this.onMouseLeave}
|
onMouseLeave={this.onMouseLeave}
|
||||||
>
|
>
|
||||||
{ subListComponents }
|
{ subListComponents }
|
||||||
</div>
|
</div> }
|
||||||
|
</RovingTabIndexProvider>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -49,6 +49,7 @@ export default createReactClass({
|
||||||
propTypes: {
|
propTypes: {
|
||||||
onJoinClick: PropTypes.func,
|
onJoinClick: PropTypes.func,
|
||||||
onRejectClick: PropTypes.func,
|
onRejectClick: PropTypes.func,
|
||||||
|
onRejectAndIgnoreClick: PropTypes.func,
|
||||||
onForgetClick: PropTypes.func,
|
onForgetClick: PropTypes.func,
|
||||||
// if inviterName is specified, the preview bar will shown an invite to the room.
|
// if inviterName is specified, the preview bar will shown an invite to the room.
|
||||||
// You should also specify onRejectClick if specifiying inviterName
|
// You should also specify onRejectClick if specifiying inviterName
|
||||||
|
@ -282,6 +283,7 @@ export default createReactClass({
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
const Spinner = sdk.getComponent('elements.Spinner');
|
const Spinner = sdk.getComponent('elements.Spinner');
|
||||||
|
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||||
|
|
||||||
let showSpinner = false;
|
let showSpinner = false;
|
||||||
let darkStyle = false;
|
let darkStyle = false;
|
||||||
|
@ -292,6 +294,7 @@ export default createReactClass({
|
||||||
let secondaryActionHandler;
|
let secondaryActionHandler;
|
||||||
let secondaryActionLabel;
|
let secondaryActionLabel;
|
||||||
let footer;
|
let footer;
|
||||||
|
const extraComponents = [];
|
||||||
|
|
||||||
const messageCase = this._getMessageCase();
|
const messageCase = this._getMessageCase();
|
||||||
switch (messageCase) {
|
switch (messageCase) {
|
||||||
|
@ -469,6 +472,14 @@ export default createReactClass({
|
||||||
primaryActionHandler = this.props.onJoinClick;
|
primaryActionHandler = this.props.onJoinClick;
|
||||||
secondaryActionLabel = _t("Reject");
|
secondaryActionLabel = _t("Reject");
|
||||||
secondaryActionHandler = this.props.onRejectClick;
|
secondaryActionHandler = this.props.onRejectClick;
|
||||||
|
|
||||||
|
if (this.props.onRejectAndIgnoreClick) {
|
||||||
|
extraComponents.push(
|
||||||
|
<AccessibleButton kind="secondary" onClick={this.props.onRejectAndIgnoreClick} key="ignore">
|
||||||
|
{ _t("Reject & Ignore user") }
|
||||||
|
</AccessibleButton>,
|
||||||
|
);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case MessageCase.ViewingRoom: {
|
case MessageCase.ViewingRoom: {
|
||||||
|
@ -505,8 +516,6 @@ export default createReactClass({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
|
||||||
|
|
||||||
let subTitleElements;
|
let subTitleElements;
|
||||||
if (subTitle) {
|
if (subTitle) {
|
||||||
if (!Array.isArray(subTitle)) {
|
if (!Array.isArray(subTitle)) {
|
||||||
|
@ -554,6 +563,7 @@ export default createReactClass({
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_RoomPreviewBar_actions">
|
<div className="mx_RoomPreviewBar_actions">
|
||||||
{ secondaryButton }
|
{ secondaryButton }
|
||||||
|
{ extraComponents }
|
||||||
{ primaryButton }
|
{ primaryButton }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_RoomPreviewBar_footer">
|
<div className="mx_RoomPreviewBar_footer">
|
||||||
|
|
|
@ -32,6 +32,11 @@ import ActiveRoomObserver from '../../../ActiveRoomObserver';
|
||||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import {_t} from "../../../languageHandler";
|
import {_t} from "../../../languageHandler";
|
||||||
|
import {RovingTabIndexWrapper} from "../../../accessibility/RovingTabIndex";
|
||||||
|
import E2EIcon from './E2EIcon';
|
||||||
|
import InviteOnlyIcon from './InviteOnlyIcon';
|
||||||
|
// eslint-disable-next-line camelcase
|
||||||
|
import rate_limited_func from '../../../ratelimitedfunc';
|
||||||
|
|
||||||
export default createReactClass({
|
export default createReactClass({
|
||||||
displayName: 'RoomTile',
|
displayName: 'RoomTile',
|
||||||
|
@ -69,6 +74,7 @@ export default createReactClass({
|
||||||
notificationCount: this.props.room.getUnreadNotificationCount(),
|
notificationCount: this.props.room.getUnreadNotificationCount(),
|
||||||
selected: this.props.room.roomId === RoomViewStore.getRoomId(),
|
selected: this.props.room.roomId === RoomViewStore.getRoomId(),
|
||||||
statusMessage: this._getStatusMessage(),
|
statusMessage: this._getStatusMessage(),
|
||||||
|
e2eStatus: null,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -101,6 +107,83 @@ export default createReactClass({
|
||||||
return statusUser._unstable_statusMessage;
|
return statusUser._unstable_statusMessage;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onRoomStateMember: function(ev, state, member) {
|
||||||
|
// we only care about leaving users
|
||||||
|
// because trust state will change if someone joins a megolm session anyway
|
||||||
|
if (member.membership !== "leave") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// ignore members in other rooms
|
||||||
|
if (member.roomId !== this.props.room.roomId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._updateE2eStatus();
|
||||||
|
},
|
||||||
|
|
||||||
|
onUserVerificationChanged: function(userId, _trustStatus) {
|
||||||
|
if (!this.props.room.getMember(userId)) {
|
||||||
|
// Not in this room
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._updateE2eStatus();
|
||||||
|
},
|
||||||
|
|
||||||
|
onRoomTimeline: function(ev, room) {
|
||||||
|
if (!room) return;
|
||||||
|
if (room.roomId != this.props.room.roomId) return;
|
||||||
|
if (ev.getType() !== "m.room.encryption") return;
|
||||||
|
MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline);
|
||||||
|
this.onFindingRoomToBeEncrypted();
|
||||||
|
},
|
||||||
|
|
||||||
|
onFindingRoomToBeEncrypted: function() {
|
||||||
|
const cli = MatrixClientPeg.get();
|
||||||
|
cli.on("RoomState.members", this.onRoomStateMember);
|
||||||
|
cli.on("userTrustStatusChanged", this.onUserVerificationChanged);
|
||||||
|
|
||||||
|
this._updateE2eStatus();
|
||||||
|
},
|
||||||
|
|
||||||
|
_updateE2eStatus: async function() {
|
||||||
|
const cli = MatrixClientPeg.get();
|
||||||
|
if (!cli.isRoomEncrypted(this.props.room.roomId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!SettingsStore.isFeatureEnabled("feature_cross_signing")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Duplication between here and _updateE2eStatus in RoomView
|
||||||
|
const e2eMembers = await this.props.room.getEncryptionTargetMembers();
|
||||||
|
const verified = [];
|
||||||
|
const unverified = [];
|
||||||
|
e2eMembers.map(({userId}) => userId)
|
||||||
|
.filter((userId) => userId !== cli.getUserId())
|
||||||
|
.forEach((userId) => {
|
||||||
|
(cli.checkUserTrust(userId).isCrossSigningVerified() ?
|
||||||
|
verified : unverified).push(userId);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Check all verified user devices. */
|
||||||
|
for (const userId of [...verified, cli.getUserId()]) {
|
||||||
|
const devices = await cli.getStoredDevicesForUser(userId);
|
||||||
|
const allDevicesVerified = devices.every(({deviceId}) => {
|
||||||
|
return cli.checkDeviceTrust(userId, deviceId).isVerified();
|
||||||
|
});
|
||||||
|
if (!allDevicesVerified) {
|
||||||
|
this.setState({
|
||||||
|
e2eStatus: "warning",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
e2eStatus: unverified.length === 0 ? "verified" : "normal",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
onRoomName: function(room) {
|
onRoomName: function(room) {
|
||||||
if (room !== this.props.room) return;
|
if (room !== this.props.room) return;
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -150,10 +233,19 @@ export default createReactClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount: function() {
|
componentDidMount: function() {
|
||||||
|
/* We bind here rather than in the definition because otherwise we wind up with the
|
||||||
|
method only being callable once every 500ms across all instances, which would be wrong */
|
||||||
|
this._updateE2eStatus = rate_limited_func(this._updateE2eStatus, 500);
|
||||||
|
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
cli.on("accountData", this.onAccountData);
|
cli.on("accountData", this.onAccountData);
|
||||||
cli.on("Room.name", this.onRoomName);
|
cli.on("Room.name", this.onRoomName);
|
||||||
cli.on("RoomState.events", this.onJoinRule);
|
cli.on("RoomState.events", this.onJoinRule);
|
||||||
|
if (cli.isRoomEncrypted(this.props.room.roomId)) {
|
||||||
|
this.onFindingRoomToBeEncrypted();
|
||||||
|
} else {
|
||||||
|
cli.on("Room.timeline", this.onRoomTimeline);
|
||||||
|
}
|
||||||
ActiveRoomObserver.addListener(this.props.room.roomId, this._onActiveRoomChange);
|
ActiveRoomObserver.addListener(this.props.room.roomId, this._onActiveRoomChange);
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
|
|
||||||
|
@ -171,6 +263,9 @@ export default createReactClass({
|
||||||
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
|
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
|
||||||
MatrixClientPeg.get().removeListener("Room.name", this.onRoomName);
|
MatrixClientPeg.get().removeListener("Room.name", this.onRoomName);
|
||||||
cli.removeListener("RoomState.events", this.onJoinRule);
|
cli.removeListener("RoomState.events", this.onJoinRule);
|
||||||
|
cli.removeListener("RoomState.members", this.onRoomStateMember);
|
||||||
|
cli.removeListener("userTrustStatusChanged", this.onUserVerificationChanged);
|
||||||
|
cli.removeListener("Room.timeline", this.onRoomTimeline);
|
||||||
}
|
}
|
||||||
ActiveRoomObserver.removeListener(this.props.room.roomId, this._onActiveRoomChange);
|
ActiveRoomObserver.removeListener(this.props.room.roomId, this._onActiveRoomChange);
|
||||||
dis.unregister(this.dispatcherRef);
|
dis.unregister(this.dispatcherRef);
|
||||||
|
@ -317,7 +412,6 @@ export default createReactClass({
|
||||||
'mx_RoomTile_noBadges': !badges,
|
'mx_RoomTile_noBadges': !badges,
|
||||||
'mx_RoomTile_transparent': this.props.transparent,
|
'mx_RoomTile_transparent': this.props.transparent,
|
||||||
'mx_RoomTile_hasSubtext': subtext && !this.props.collapsed,
|
'mx_RoomTile_hasSubtext': subtext && !this.props.collapsed,
|
||||||
'mx_RoomTile_isPrivate': this.state.joinRule == "invite" && !dmUserId,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const avatarClasses = classNames({
|
const avatarClasses = classNames({
|
||||||
|
@ -352,7 +446,8 @@ export default createReactClass({
|
||||||
});
|
});
|
||||||
|
|
||||||
subtextLabel = subtext ? <span className="mx_RoomTile_subtext">{ subtext }</span> : null;
|
subtextLabel = subtext ? <span className="mx_RoomTile_subtext">{ subtext }</span> : null;
|
||||||
label = <div title={name} className={nameClasses} dir="auto">{ name }</div>;
|
// XXX: this is a workaround for Firefox giving this div a tabstop :( [tabIndex]
|
||||||
|
label = <div title={name} className={nameClasses} tabIndex={-1} dir="auto">{ name }</div>;
|
||||||
} else if (this.state.hover) {
|
} else if (this.state.hover) {
|
||||||
const Tooltip = sdk.getComponent("elements.Tooltip");
|
const Tooltip = sdk.getComponent("elements.Tooltip");
|
||||||
tooltip = <Tooltip className="mx_RoomTile_tooltip" label={this.props.room.name} dir="auto" />;
|
tooltip = <Tooltip className="mx_RoomTile_tooltip" label={this.props.room.name} dir="auto" />;
|
||||||
|
@ -383,7 +478,9 @@ export default createReactClass({
|
||||||
|
|
||||||
let dmIndicator;
|
let dmIndicator;
|
||||||
let dmOnline;
|
let dmOnline;
|
||||||
if (dmUserId) {
|
/* Post-cross-signing we don't show DM indicators at all, instead relying on user
|
||||||
|
context to let them know when that is. */
|
||||||
|
if (dmUserId && !SettingsStore.isFeatureEnabled("feature_cross_signing")) {
|
||||||
dmIndicator = <img
|
dmIndicator = <img
|
||||||
src={require("../../../../res/img/icon_person.svg")}
|
src={require("../../../../res/img/icon_person.svg")}
|
||||||
className="mx_RoomTile_dm"
|
className="mx_RoomTile_dm"
|
||||||
|
@ -428,12 +525,23 @@ export default createReactClass({
|
||||||
|
|
||||||
let privateIcon = null;
|
let privateIcon = null;
|
||||||
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
|
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
|
||||||
privateIcon = <div className="mx_RoomTile_PrivateIcon" />;
|
if (this.state.joinRule == "invite" && !dmUserId) {
|
||||||
|
privateIcon = <InviteOnlyIcon />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let e2eIcon = null;
|
||||||
|
if (this.state.e2eStatus) {
|
||||||
|
e2eIcon = <E2EIcon status={this.state.e2eStatus} className="mx_RoomTile_e2eIcon" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <React.Fragment>
|
return <React.Fragment>
|
||||||
|
<RovingTabIndexWrapper>
|
||||||
|
{({onFocus, isActive, ref}) =>
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
tabIndex="0"
|
onFocus={onFocus}
|
||||||
|
tabIndex={isActive ? 0 : -1}
|
||||||
|
inputRef={ref}
|
||||||
className={classes}
|
className={classes}
|
||||||
onClick={this.onClick}
|
onClick={this.onClick}
|
||||||
onMouseEnter={this.onMouseEnter}
|
onMouseEnter={this.onMouseEnter}
|
||||||
|
@ -447,6 +555,7 @@ export default createReactClass({
|
||||||
<div className="mx_RoomTile_avatar_container">
|
<div className="mx_RoomTile_avatar_container">
|
||||||
<RoomAvatar room={this.props.room} width={24} height={24} />
|
<RoomAvatar room={this.props.room} width={24} height={24} />
|
||||||
{ dmIndicator }
|
{ dmIndicator }
|
||||||
|
{ e2eIcon }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{ privateIcon }
|
{ privateIcon }
|
||||||
|
@ -462,6 +571,8 @@ export default createReactClass({
|
||||||
{ /* { incomingCallBox } */ }
|
{ /* { incomingCallBox } */ }
|
||||||
{ tooltip }
|
{ tooltip }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
|
}
|
||||||
|
</RovingTabIndexWrapper>
|
||||||
|
|
||||||
{ contextMenu }
|
{ contextMenu }
|
||||||
</React.Fragment>;
|
</React.Fragment>;
|
||||||
|
|
|
@ -24,6 +24,8 @@ import {
|
||||||
containsEmote,
|
containsEmote,
|
||||||
stripEmoteCommand,
|
stripEmoteCommand,
|
||||||
unescapeMessage,
|
unescapeMessage,
|
||||||
|
startsWith,
|
||||||
|
stripPrefix,
|
||||||
} from '../../../editor/serialize';
|
} from '../../../editor/serialize';
|
||||||
import {CommandPartCreator} from '../../../editor/parts';
|
import {CommandPartCreator} from '../../../editor/parts';
|
||||||
import BasicMessageComposer from "./BasicMessageComposer";
|
import BasicMessageComposer from "./BasicMessageComposer";
|
||||||
|
@ -33,7 +35,7 @@ import ReplyThread from "../elements/ReplyThread";
|
||||||
import {parseEvent} from '../../../editor/deserialize';
|
import {parseEvent} from '../../../editor/deserialize';
|
||||||
import {findEditableEvent} from '../../../utils/EventUtils';
|
import {findEditableEvent} from '../../../utils/EventUtils';
|
||||||
import SendHistoryManager from "../../../SendHistoryManager";
|
import SendHistoryManager from "../../../SendHistoryManager";
|
||||||
import {processCommandInput} from '../../../SlashCommands';
|
import {getCommand} from '../../../SlashCommands';
|
||||||
import * as sdk from '../../../index';
|
import * as sdk from '../../../index';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import {_t, _td} from '../../../languageHandler';
|
import {_t, _td} from '../../../languageHandler';
|
||||||
|
@ -56,11 +58,15 @@ function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createMessageContent(model, permalinkCreator) {
|
// exported for tests
|
||||||
|
export function createMessageContent(model, permalinkCreator) {
|
||||||
const isEmote = containsEmote(model);
|
const isEmote = containsEmote(model);
|
||||||
if (isEmote) {
|
if (isEmote) {
|
||||||
model = stripEmoteCommand(model);
|
model = stripEmoteCommand(model);
|
||||||
}
|
}
|
||||||
|
if (startsWith(model, "//")) {
|
||||||
|
model = stripPrefix(model, "/");
|
||||||
|
}
|
||||||
model = unescapeMessage(model);
|
model = unescapeMessage(model);
|
||||||
const repliedToEvent = RoomViewStore.getQuotingEvent();
|
const repliedToEvent = RoomViewStore.getQuotingEvent();
|
||||||
|
|
||||||
|
@ -175,20 +181,21 @@ export default class SendMessageComposer extends React.Component {
|
||||||
const parts = this.model.parts;
|
const parts = this.model.parts;
|
||||||
const firstPart = parts[0];
|
const firstPart = parts[0];
|
||||||
if (firstPart) {
|
if (firstPart) {
|
||||||
if (firstPart.type === "command") {
|
if (firstPart.type === "command" && firstPart.text.startsWith("/") && !firstPart.text.startsWith("//")) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// be extra resilient when somehow the AutocompleteWrapperModel or
|
// be extra resilient when somehow the AutocompleteWrapperModel or
|
||||||
// CommandPartCreator fails to insert a command part, so we don't send
|
// CommandPartCreator fails to insert a command part, so we don't send
|
||||||
// a command as a message
|
// a command as a message
|
||||||
if (firstPart.text.startsWith("/") && (firstPart.type === "plain" || firstPart.type === "pill-candidate")) {
|
if (firstPart.text.startsWith("/") && !firstPart.text.startsWith("//")
|
||||||
|
&& (firstPart.type === "plain" || firstPart.type === "pill-candidate")) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async _runSlashCommand() {
|
_getSlashCommand() {
|
||||||
const commandText = this.model.parts.reduce((text, part) => {
|
const commandText = this.model.parts.reduce((text, part) => {
|
||||||
// use mxid to textify user pills in a command
|
// use mxid to textify user pills in a command
|
||||||
if (part.type === "user-pill") {
|
if (part.type === "user-pill") {
|
||||||
|
@ -196,9 +203,11 @@ export default class SendMessageComposer extends React.Component {
|
||||||
}
|
}
|
||||||
return text + part.text;
|
return text + part.text;
|
||||||
}, "");
|
}, "");
|
||||||
const cmd = processCommandInput(this.props.room.roomId, commandText);
|
return [getCommand(this.props.room.roomId, commandText), commandText];
|
||||||
|
}
|
||||||
|
|
||||||
if (cmd) {
|
async _runSlashCommand(fn) {
|
||||||
|
const cmd = fn();
|
||||||
let error = cmd.error;
|
let error = cmd.error;
|
||||||
if (cmd.promise) {
|
if (cmd.promise) {
|
||||||
try {
|
try {
|
||||||
|
@ -231,15 +240,49 @@ export default class SendMessageComposer extends React.Component {
|
||||||
console.log("Command success.");
|
console.log("Command success.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
_sendMessage() {
|
async _sendMessage() {
|
||||||
if (this.model.isEmpty) {
|
if (this.model.isEmpty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let shouldSend = true;
|
||||||
|
|
||||||
if (!containsEmote(this.model) && this._isSlashCommand()) {
|
if (!containsEmote(this.model) && this._isSlashCommand()) {
|
||||||
this._runSlashCommand();
|
const [cmd, commandText] = this._getSlashCommand();
|
||||||
|
if (cmd) {
|
||||||
|
shouldSend = false;
|
||||||
|
this._runSlashCommand(cmd);
|
||||||
} else {
|
} else {
|
||||||
|
// ask the user if their unknown command should be sent as a message
|
||||||
|
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
|
const {finished} = Modal.createTrackedDialog("Unknown command", "", QuestionDialog, {
|
||||||
|
title: _t("Unknown Command"),
|
||||||
|
description: <div>
|
||||||
|
<p>
|
||||||
|
{ _t("Unrecognised command: %(commandText)s", {commandText}) }
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{ _t("You can use <code>/help</code> to list available commands. " +
|
||||||
|
"Did you mean to send this as a message?", {}, {
|
||||||
|
code: t => <code>{ t }</code>,
|
||||||
|
}) }
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{ _t("Hint: Begin your message with <code>//</code> to start it with a slash.", {}, {
|
||||||
|
code: t => <code>{ t }</code>,
|
||||||
|
}) }
|
||||||
|
</p>
|
||||||
|
</div>,
|
||||||
|
button: _t('Send as message'),
|
||||||
|
});
|
||||||
|
const [sendAnyway] = await finished;
|
||||||
|
// if !sendAnyway bail to let the user edit the composer and try again
|
||||||
|
if (!sendAnyway) return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldSend) {
|
||||||
const isReply = !!RoomViewStore.getQuotingEvent();
|
const isReply = !!RoomViewStore.getQuotingEvent();
|
||||||
const {roomId} = this.props.room;
|
const {roomId} = this.props.room;
|
||||||
const content = createMessageContent(this.model, this.props.permalinkCreator);
|
const content = createMessageContent(this.model, this.props.permalinkCreator);
|
||||||
|
@ -253,6 +296,7 @@ export default class SendMessageComposer extends React.Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.sendHistoryManager.save(this.model);
|
this.sendHistoryManager.save(this.model);
|
||||||
// clear composer
|
// clear composer
|
||||||
this.model.reset([]);
|
this.model.reset([]);
|
||||||
|
|
187
src/components/views/settings/EventIndexPanel.js
Normal file
187
src/components/views/settings/EventIndexPanel.js
Normal file
|
@ -0,0 +1,187 @@
|
||||||
|
/*
|
||||||
|
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 { _t } from '../../../languageHandler';
|
||||||
|
import * as sdk from '../../../index';
|
||||||
|
import Modal from '../../../Modal';
|
||||||
|
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
||||||
|
import AccessibleButton from "../elements/AccessibleButton";
|
||||||
|
import {formatBytes, formatCountLong} from "../../../utils/FormattingUtils";
|
||||||
|
import EventIndexPeg from "../../../indexing/EventIndexPeg";
|
||||||
|
|
||||||
|
export default class EventIndexPanel extends React.Component {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
enabling: false,
|
||||||
|
eventIndexSize: 0,
|
||||||
|
roomCount: 0,
|
||||||
|
eventIndexingEnabled:
|
||||||
|
SettingsStore.getValueAt(SettingLevel.DEVICE, 'enableEventIndexing'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateCurrentRoom(room) {
|
||||||
|
const eventIndex = EventIndexPeg.get();
|
||||||
|
const stats = await eventIndex.getStats();
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
eventIndexSize: stats.size,
|
||||||
|
roomCount: stats.roomCount,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount(): void {
|
||||||
|
const eventIndex = EventIndexPeg.get();
|
||||||
|
|
||||||
|
if (eventIndex !== null) {
|
||||||
|
eventIndex.removeListener("changedCheckpoint", this.updateCurrentRoom.bind(this));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async componentWillMount(): void {
|
||||||
|
this.updateState();
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateState() {
|
||||||
|
const eventIndex = EventIndexPeg.get();
|
||||||
|
const eventIndexingEnabled = SettingsStore.getValueAt(SettingLevel.DEVICE, 'enableEventIndexing');
|
||||||
|
const enabling = false;
|
||||||
|
|
||||||
|
let eventIndexSize = 0;
|
||||||
|
let roomCount = 0;
|
||||||
|
|
||||||
|
if (eventIndex !== null) {
|
||||||
|
eventIndex.on("changedCheckpoint", this.updateCurrentRoom.bind(this));
|
||||||
|
|
||||||
|
const stats = await eventIndex.getStats();
|
||||||
|
eventIndexSize = stats.size;
|
||||||
|
roomCount = stats.roomCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
enabling,
|
||||||
|
eventIndexSize,
|
||||||
|
roomCount,
|
||||||
|
eventIndexingEnabled,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_onManage = async () => {
|
||||||
|
Modal.createTrackedDialogAsync('Message search', 'Message search',
|
||||||
|
import('../../../async-components/views/dialogs/eventindex/ManageEventIndexDialog'),
|
||||||
|
{
|
||||||
|
onFinished: () => {},
|
||||||
|
}, null, /* priority = */ false, /* static = */ true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onEnable = async () => {
|
||||||
|
this.setState({
|
||||||
|
enabling: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
await EventIndexPeg.initEventIndex();
|
||||||
|
await EventIndexPeg.get().addInitialCheckpoints();
|
||||||
|
await EventIndexPeg.get().startCrawler();
|
||||||
|
await SettingsStore.setValue('enableEventIndexing', null, SettingLevel.DEVICE, true);
|
||||||
|
await this.updateState();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let eventIndexingSettings = null;
|
||||||
|
const InlineSpinner = sdk.getComponent('elements.InlineSpinner');
|
||||||
|
|
||||||
|
if (EventIndexPeg.get() !== null) {
|
||||||
|
eventIndexingSettings = (
|
||||||
|
<div>
|
||||||
|
<div className='mx_SettingsTab_subsectionText'>
|
||||||
|
{_t( "Securely cache encrypted messages locally for them " +
|
||||||
|
"to appear in search results, using ")
|
||||||
|
} {formatBytes(this.state.eventIndexSize, 0)}
|
||||||
|
{_t( " to store messages from ")}
|
||||||
|
{formatCountLong(this.state.roomCount)} {_t("rooms.")}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<AccessibleButton kind="primary" onClick={this._onManage}>
|
||||||
|
{_t("Manage")}
|
||||||
|
</AccessibleButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (!this.state.eventIndexingEnabled && EventIndexPeg.supportIsInstalled()) {
|
||||||
|
eventIndexingSettings = (
|
||||||
|
<div>
|
||||||
|
<div className='mx_SettingsTab_subsectionText'>
|
||||||
|
{_t( "Securely cache encrypted messages locally for them to " +
|
||||||
|
"appear in search results.")}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<AccessibleButton kind="primary" disabled={this.state.enabling}
|
||||||
|
onClick={this._onEnable}>
|
||||||
|
{_t("Enable")}
|
||||||
|
</AccessibleButton>
|
||||||
|
{this.state.enabling ? <InlineSpinner /> : <div />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (EventIndexPeg.platformHasSupport() && !EventIndexPeg.supportIsInstalled()) {
|
||||||
|
const nativeLink = (
|
||||||
|
"https://github.com/vector-im/riot-web/blob/develop/" +
|
||||||
|
"docs/native-node-modules.md#" +
|
||||||
|
"adding-seshat-for-search-in-e2e-encrypted-rooms"
|
||||||
|
);
|
||||||
|
|
||||||
|
eventIndexingSettings = (
|
||||||
|
<div>
|
||||||
|
{
|
||||||
|
_t( "Riot is missing some components required for securely " +
|
||||||
|
"caching encrypted messages locally. If you'd like to " +
|
||||||
|
"experiment with this feature, build a custom Riot Desktop " +
|
||||||
|
"with <nativeLink>search components added</nativeLink>.",
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
'nativeLink': (sub) => <a href={nativeLink} target="_blank"
|
||||||
|
rel="noopener">{sub}</a>,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
eventIndexingSettings = (
|
||||||
|
<div>
|
||||||
|
{
|
||||||
|
_t( "Riot can't securely cache encrypted messages locally " +
|
||||||
|
"while running in a web browser. Use <riotLink>Riot Desktop</riotLink> " +
|
||||||
|
"for encrypted messages to appear in search results.",
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
'riotLink': (sub) => <a href="https://riot.im/download/desktop"
|
||||||
|
target="_blank" rel="noopener">{sub}</a>,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return eventIndexingSettings;
|
||||||
|
}
|
||||||
|
}
|
|
@ -70,7 +70,16 @@ export default class GeneralUserSettingsTab extends React.Component {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
|
|
||||||
const serverSupportsSeparateAddAndBind = await cli.doesServerSupportSeparateAddAndBind();
|
const serverSupportsSeparateAddAndBind = await cli.doesServerSupportSeparateAddAndBind();
|
||||||
this.setState({serverSupportsSeparateAddAndBind});
|
|
||||||
|
const capabilities = await cli.getCapabilities(); // this is cached
|
||||||
|
const changePasswordCap = capabilities['m.change_password'];
|
||||||
|
|
||||||
|
// You can change your password so long as the capability isn't explicitly disabled. The implicit
|
||||||
|
// behaviour is you can change your password when the capability is missing or has not-false as
|
||||||
|
// the enabled flag value.
|
||||||
|
const canChangePassword = !changePasswordCap || changePasswordCap['enabled'] !== false;
|
||||||
|
|
||||||
|
this.setState({serverSupportsSeparateAddAndBind, canChangePassword});
|
||||||
|
|
||||||
this._getThreepidState();
|
this._getThreepidState();
|
||||||
}
|
}
|
||||||
|
@ -280,7 +289,7 @@ export default class GeneralUserSettingsTab extends React.Component {
|
||||||
const PhoneNumbers = sdk.getComponent("views.settings.account.PhoneNumbers");
|
const PhoneNumbers = sdk.getComponent("views.settings.account.PhoneNumbers");
|
||||||
const Spinner = sdk.getComponent("views.elements.Spinner");
|
const Spinner = sdk.getComponent("views.elements.Spinner");
|
||||||
|
|
||||||
const passwordChangeForm = (
|
let passwordChangeForm = (
|
||||||
<ChangePassword
|
<ChangePassword
|
||||||
className="mx_GeneralUserSettingsTab_changePassword"
|
className="mx_GeneralUserSettingsTab_changePassword"
|
||||||
rowClassName=""
|
rowClassName=""
|
||||||
|
@ -314,11 +323,18 @@ export default class GeneralUserSettingsTab extends React.Component {
|
||||||
threepidSection = <Spinner />;
|
threepidSection = <Spinner />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let passwordChangeText = _t("Set a new account password...");
|
||||||
|
if (!this.state.canChangePassword) {
|
||||||
|
// Just don't show anything if you can't do anything.
|
||||||
|
passwordChangeText = null;
|
||||||
|
passwordChangeForm = null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_SettingsTab_section mx_GeneralUserSettingsTab_accountSection">
|
<div className="mx_SettingsTab_section mx_GeneralUserSettingsTab_accountSection">
|
||||||
<span className="mx_SettingsTab_subheading">{_t("Account")}</span>
|
<span className="mx_SettingsTab_subheading">{_t("Account")}</span>
|
||||||
<p className="mx_SettingsTab_subsectionText">
|
<p className="mx_SettingsTab_subsectionText">
|
||||||
{_t("Set a new account password...")}
|
{passwordChangeText}
|
||||||
</p>
|
</p>
|
||||||
{passwordChangeForm}
|
{passwordChangeForm}
|
||||||
{threepidSection}
|
{threepidSection}
|
||||||
|
|
|
@ -21,15 +21,10 @@ import {MatrixClientPeg} from "../../../../../MatrixClientPeg";
|
||||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||||
import SdkConfig from "../../../../../SdkConfig";
|
import SdkConfig from "../../../../../SdkConfig";
|
||||||
import createRoom from "../../../../../createRoom";
|
import createRoom from "../../../../../createRoom";
|
||||||
import packageJson from "../../../../../../package.json";
|
|
||||||
import Modal from "../../../../../Modal";
|
import Modal from "../../../../../Modal";
|
||||||
import * as sdk from "../../../../../";
|
import * as sdk from "../../../../../";
|
||||||
import PlatformPeg from "../../../../../PlatformPeg";
|
import PlatformPeg from "../../../../../PlatformPeg";
|
||||||
|
|
||||||
// if this looks like a release, use the 'version' from package.json; else use
|
|
||||||
// the git sha. Prepend version with v, to look like riot-web version
|
|
||||||
const REACT_SDK_VERSION = 'dist' in packageJson ? packageJson.version : packageJson.gitHead || '<local>';
|
|
||||||
|
|
||||||
// Simple method to help prettify GH Release Tags and Commit Hashes.
|
// Simple method to help prettify GH Release Tags and Commit Hashes.
|
||||||
const semVerRegex = /^v?(\d+\.\d+\.\d+(?:-rc.+)?)(?:-(?:\d+-g)?([0-9a-fA-F]+))?(?:-dirty)?$/i;
|
const semVerRegex = /^v?(\d+\.\d+\.\d+(?:-rc.+)?)(?:-(?:\d+-g)?([0-9a-fA-F]+))?(?:-dirty)?$/i;
|
||||||
const ghVersionLabel = function(repo, token='') {
|
const ghVersionLabel = function(repo, token='') {
|
||||||
|
@ -188,9 +183,6 @@ export default class HelpUserSettingsTab extends React.Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const reactSdkVersion = REACT_SDK_VERSION !== '<local>'
|
|
||||||
? ghVersionLabel('matrix-org/matrix-react-sdk', REACT_SDK_VERSION)
|
|
||||||
: REACT_SDK_VERSION;
|
|
||||||
const vectorVersion = this.state.vectorVersion
|
const vectorVersion = this.state.vectorVersion
|
||||||
? ghVersionLabel('vector-im/riot-web', this.state.vectorVersion)
|
? ghVersionLabel('vector-im/riot-web', this.state.vectorVersion)
|
||||||
: 'unknown';
|
: 'unknown';
|
||||||
|
@ -243,7 +235,6 @@ export default class HelpUserSettingsTab extends React.Component {
|
||||||
<div className='mx_SettingsTab_section mx_HelpUserSettingsTab_versions'>
|
<div className='mx_SettingsTab_section mx_HelpUserSettingsTab_versions'>
|
||||||
<span className='mx_SettingsTab_subheading'>{_t("Versions")}</span>
|
<span className='mx_SettingsTab_subheading'>{_t("Versions")}</span>
|
||||||
<div className='mx_SettingsTab_subsectionText'>
|
<div className='mx_SettingsTab_subsectionText'>
|
||||||
{_t("matrix-react-sdk version:")} {reactSdkVersion}<br />
|
|
||||||
{_t("riot-web version:")} {vectorVersion}<br />
|
{_t("riot-web version:")} {vectorVersion}<br />
|
||||||
{_t("olm version:")} {olmVersion}<br />
|
{_t("olm version:")} {olmVersion}<br />
|
||||||
{updateButton}
|
{updateButton}
|
||||||
|
|
|
@ -170,6 +170,7 @@ export default class PreferencesUserSettingsTab extends React.Component {
|
||||||
return (
|
return (
|
||||||
<div className="mx_SettingsTab mx_PreferencesUserSettingsTab">
|
<div className="mx_SettingsTab mx_PreferencesUserSettingsTab">
|
||||||
<div className="mx_SettingsTab_heading">{_t("Preferences")}</div>
|
<div className="mx_SettingsTab_heading">{_t("Preferences")}</div>
|
||||||
|
|
||||||
<div className="mx_SettingsTab_section">
|
<div className="mx_SettingsTab_section">
|
||||||
<span className="mx_SettingsTab_subheading">{_t("Composer")}</span>
|
<span className="mx_SettingsTab_subheading">{_t("Composer")}</span>
|
||||||
{this._renderGroup(PreferencesUserSettingsTab.COMPOSER_SETTINGS)}
|
{this._renderGroup(PreferencesUserSettingsTab.COMPOSER_SETTINGS)}
|
||||||
|
|
|
@ -242,6 +242,7 @@ export default class SecurityUserSettingsTab extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
const DevicesPanel = sdk.getComponent('views.settings.DevicesPanel');
|
const DevicesPanel = sdk.getComponent('views.settings.DevicesPanel');
|
||||||
const SettingsFlag = sdk.getComponent('views.elements.SettingsFlag');
|
const SettingsFlag = sdk.getComponent('views.elements.SettingsFlag');
|
||||||
|
const EventIndexPanel = sdk.getComponent('views.settings.EventIndexPanel');
|
||||||
|
|
||||||
const KeyBackupPanel = sdk.getComponent('views.settings.KeyBackupPanel');
|
const KeyBackupPanel = sdk.getComponent('views.settings.KeyBackupPanel');
|
||||||
const keyBackup = (
|
const keyBackup = (
|
||||||
|
@ -253,6 +254,16 @@ export default class SecurityUserSettingsTab extends React.Component {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let eventIndex;
|
||||||
|
if (SettingsStore.isFeatureEnabled("feature_event_indexing")) {
|
||||||
|
eventIndex = (
|
||||||
|
<div className="mx_SettingsTab_section">
|
||||||
|
<span className="mx_SettingsTab_subheading">{_t("Message search")}</span>
|
||||||
|
<EventIndexPanel />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// XXX: There's no such panel in the current cross-signing designs, but
|
// XXX: There's no such panel in the current cross-signing designs, but
|
||||||
// it's useful to have for testing the feature. If there's no interest
|
// it's useful to have for testing the feature. If there's no interest
|
||||||
// in having advanced details here once all flows are implemented, we
|
// in having advanced details here once all flows are implemented, we
|
||||||
|
@ -281,6 +292,7 @@ export default class SecurityUserSettingsTab extends React.Component {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{keyBackup}
|
{keyBackup}
|
||||||
|
{eventIndex}
|
||||||
{crossSigning}
|
{crossSigning}
|
||||||
{this._renderCurrentDeviceInfo()}
|
{this._renderCurrentDeviceInfo()}
|
||||||
<div className='mx_SettingsTab_section'>
|
<div className='mx_SettingsTab_section'>
|
||||||
|
|
|
@ -32,7 +32,7 @@ export default class VerifySessionToast extends React.PureComponent {
|
||||||
DeviceListener.sharedInstance().dismissVerification(this.props.deviceId);
|
DeviceListener.sharedInstance().dismissVerification(this.props.deviceId);
|
||||||
};
|
};
|
||||||
|
|
||||||
_onVerifyClick = async () => {
|
_onReviewClick = async () => {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog');
|
const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog');
|
||||||
|
|
||||||
|
@ -47,10 +47,10 @@ export default class VerifySessionToast extends React.PureComponent {
|
||||||
render() {
|
render() {
|
||||||
const FormButton = sdk.getComponent("elements.FormButton");
|
const FormButton = sdk.getComponent("elements.FormButton");
|
||||||
return (<div>
|
return (<div>
|
||||||
<div className="mx_Toast_description">{_t("Other users may not trust it")}</div>
|
<div className="mx_Toast_description">{_t("Review & verify your new session")}</div>
|
||||||
<div className="mx_Toast_buttons" aria-live="off">
|
<div className="mx_Toast_buttons" aria-live="off">
|
||||||
<FormButton label={_t("Later")} kind="danger" onClick={this._onLaterClick} />
|
<FormButton label={_t("Later")} kind="danger" onClick={this._onLaterClick} />
|
||||||
<FormButton label={_t("Verify")} onClick={this._onVerifyClick} />
|
<FormButton label={_t("Review")} onClick={this._onReviewClick} />
|
||||||
</div>
|
</div>
|
||||||
</div>);
|
</div>);
|
||||||
}
|
}
|
||||||
|
|
68
src/components/views/toasts/SetupEncryptionToast.js
Normal file
68
src/components/views/toasts/SetupEncryptionToast.js
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
/*
|
||||||
|
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 PropTypes from 'prop-types';
|
||||||
|
import * as sdk from "../../../index";
|
||||||
|
import { _t } from '../../../languageHandler';
|
||||||
|
import DeviceListener from '../../../DeviceListener';
|
||||||
|
import { accessSecretStorage } from '../../../CrossSigningManager';
|
||||||
|
|
||||||
|
export default class SetupEncryptionToast extends React.PureComponent {
|
||||||
|
static propTypes = {
|
||||||
|
toastKey: PropTypes.string.isRequired,
|
||||||
|
kind: PropTypes.oneOf(['set_up_encryption', 'verify_this_session', 'upgrade_encryption']).isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
_onLaterClick = () => {
|
||||||
|
DeviceListener.sharedInstance().dismissEncryptionSetup();
|
||||||
|
};
|
||||||
|
|
||||||
|
_onSetupClick = async () => {
|
||||||
|
accessSecretStorage();
|
||||||
|
};
|
||||||
|
|
||||||
|
getDescription() {
|
||||||
|
switch (this.props.kind) {
|
||||||
|
case 'set_up_encryption':
|
||||||
|
case 'upgrade_encryption':
|
||||||
|
return _t('Verify your other devices easier');
|
||||||
|
case 'verify_this_session':
|
||||||
|
return _t('Other users may not trust it');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getSetupCaption() {
|
||||||
|
switch (this.props.kind) {
|
||||||
|
case 'set_up_encryption':
|
||||||
|
case 'upgrade_encryption':
|
||||||
|
return _t('Upgrade');
|
||||||
|
case 'verify_this_session':
|
||||||
|
return _t('Verify');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const FormButton = sdk.getComponent("elements.FormButton");
|
||||||
|
return (<div>
|
||||||
|
<div className="mx_Toast_description">{this.getDescription()}</div>
|
||||||
|
<div className="mx_Toast_buttons" aria-live="off">
|
||||||
|
<FormButton label={_t("Later")} kind="danger" onClick={this._onLaterClick} />
|
||||||
|
<FormButton label={this.getSetupCaption()} onClick={this._onSetupClick} />
|
||||||
|
</div>
|
||||||
|
</div>);
|
||||||
|
}
|
||||||
|
}
|
|
@ -23,6 +23,7 @@ import {RIGHT_PANEL_PHASES} from "../../../stores/RightPanelStorePhases";
|
||||||
import {userLabelForEventRoom} from "../../../utils/KeyVerificationStateObserver";
|
import {userLabelForEventRoom} from "../../../utils/KeyVerificationStateObserver";
|
||||||
import dis from "../../../dispatcher";
|
import dis from "../../../dispatcher";
|
||||||
import ToastStore from "../../../stores/ToastStore";
|
import ToastStore from "../../../stores/ToastStore";
|
||||||
|
import Modal from "../../../Modal";
|
||||||
|
|
||||||
export default class VerificationRequestToast extends React.PureComponent {
|
export default class VerificationRequestToast extends React.PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -38,6 +39,13 @@ export default class VerificationRequestToast extends React.PureComponent {
|
||||||
this.setState({counter});
|
this.setState({counter});
|
||||||
}, 1000);
|
}, 1000);
|
||||||
request.on("change", this._checkRequestIsPending);
|
request.on("change", this._checkRequestIsPending);
|
||||||
|
// We should probably have a separate class managing the active verification toasts,
|
||||||
|
// rather than monitoring this in the toast component itself, since we'll get problems
|
||||||
|
// like the toasdt not going away when the verification is cancelled unless it's the
|
||||||
|
// one on the top (ie. the one that's mounted).
|
||||||
|
// As a quick & dirty fix, check the toast is still relevant when it mounts (this prevents
|
||||||
|
// a toast hanging around after logging in if you did a verification as part of login).
|
||||||
|
this._checkRequestIsPending();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
@ -65,22 +73,27 @@ export default class VerificationRequestToast extends React.PureComponent {
|
||||||
accept = async () => {
|
accept = async () => {
|
||||||
ToastStore.sharedInstance().dismissToast(this.props.toastKey);
|
ToastStore.sharedInstance().dismissToast(this.props.toastKey);
|
||||||
const {request} = this.props;
|
const {request} = this.props;
|
||||||
const {event} = request;
|
|
||||||
// no room id for to_device requests
|
// no room id for to_device requests
|
||||||
if (event.getRoomId()) {
|
try {
|
||||||
|
if (request.channel.roomId) {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'view_room',
|
action: 'view_room',
|
||||||
room_id: event.getRoomId(),
|
room_id: request.channel.roomId,
|
||||||
should_peek: false,
|
should_peek: false,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
try {
|
|
||||||
await request.accept();
|
await request.accept();
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: "set_right_panel_phase",
|
action: "set_right_panel_phase",
|
||||||
phase: RIGHT_PANEL_PHASES.EncryptionPanel,
|
phase: RIGHT_PANEL_PHASES.EncryptionPanel,
|
||||||
refireParams: {verificationRequest: request},
|
refireParams: {verificationRequest: request},
|
||||||
});
|
});
|
||||||
|
} else if (request.channel.deviceId && request.verifier) {
|
||||||
|
// show to_device verifications in dialog still
|
||||||
|
const IncomingSasDialog = sdk.getComponent("views.dialogs.IncomingSasDialog");
|
||||||
|
Modal.createTrackedDialog('Incoming Verification', '', IncomingSasDialog, {
|
||||||
|
verifier: request.verifier,
|
||||||
|
}, null, /* priority = */ false, /* static = */ true);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err.message);
|
console.error(err.message);
|
||||||
}
|
}
|
||||||
|
@ -89,13 +102,13 @@ export default class VerificationRequestToast extends React.PureComponent {
|
||||||
render() {
|
render() {
|
||||||
const FormButton = sdk.getComponent("elements.FormButton");
|
const FormButton = sdk.getComponent("elements.FormButton");
|
||||||
const {request} = this.props;
|
const {request} = this.props;
|
||||||
const {event} = request;
|
|
||||||
const userId = request.otherUserId;
|
const userId = request.otherUserId;
|
||||||
let nameLabel = event.getRoomId() ? userLabelForEventRoom(userId, event) : userId;
|
const roomId = request.channel.roomId;
|
||||||
|
let nameLabel = roomId ? userLabelForEventRoom(userId, roomId) : userId;
|
||||||
// for legacy to_device verification requests
|
// for legacy to_device verification requests
|
||||||
if (nameLabel === userId) {
|
if (nameLabel === userId) {
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
const user = client.getUser(event.getSender());
|
const user = client.getUser(userId);
|
||||||
if (user && user.displayName) {
|
if (user && user.displayName) {
|
||||||
nameLabel = _t("%(name)s (%(userId)s)", {name: user.displayName, userId});
|
nameLabel = _t("%(name)s (%(userId)s)", {name: user.displayName, userId});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -32,6 +32,10 @@ import {getAddressType} from "./UserAddress";
|
||||||
* @param {object=} opts.createOpts set of options to pass to createRoom call.
|
* @param {object=} opts.createOpts set of options to pass to createRoom call.
|
||||||
* @param {bool=} opts.spinner True to show a modal spinner while the room is created.
|
* @param {bool=} opts.spinner True to show a modal spinner while the room is created.
|
||||||
* Default: True
|
* Default: True
|
||||||
|
* @param {bool=} opts.guestAccess Whether to enable guest access.
|
||||||
|
* Default: True
|
||||||
|
* @param {bool=} opts.encryption Whether to enable encryption.
|
||||||
|
* Default: False
|
||||||
*
|
*
|
||||||
* @returns {Promise} which resolves to the room id, or null if the
|
* @returns {Promise} which resolves to the room id, or null if the
|
||||||
* action was aborted or failed.
|
* action was aborted or failed.
|
||||||
|
@ -39,6 +43,8 @@ import {getAddressType} from "./UserAddress";
|
||||||
export default function createRoom(opts) {
|
export default function createRoom(opts) {
|
||||||
opts = opts || {};
|
opts = opts || {};
|
||||||
if (opts.spinner === undefined) opts.spinner = true;
|
if (opts.spinner === undefined) opts.spinner = true;
|
||||||
|
if (opts.guestAccess === undefined) opts.guestAccess = true;
|
||||||
|
if (opts.encryption === undefined) opts.encryption = false;
|
||||||
|
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
const Loader = sdk.getComponent("elements.Spinner");
|
const Loader = sdk.getComponent("elements.Spinner");
|
||||||
|
@ -77,18 +83,30 @@ export default function createRoom(opts) {
|
||||||
opts.andView = true;
|
opts.andView = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createOpts.initial_state = createOpts.initial_state || [];
|
||||||
|
|
||||||
// Allow guests by default since the room is private and they'd
|
// Allow guests by default since the room is private and they'd
|
||||||
// need an invite. This means clicking on a 3pid invite email can
|
// need an invite. This means clicking on a 3pid invite email can
|
||||||
// actually drop you right in to a chat.
|
// actually drop you right in to a chat.
|
||||||
createOpts.initial_state = createOpts.initial_state || [
|
if (opts.guestAccess) {
|
||||||
{
|
createOpts.initial_state.push({
|
||||||
|
type: 'm.room.guest_access',
|
||||||
|
state_key: '',
|
||||||
content: {
|
content: {
|
||||||
guest_access: 'can_join',
|
guest_access: 'can_join',
|
||||||
},
|
},
|
||||||
type: 'm.room.guest_access',
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.encryption) {
|
||||||
|
createOpts.initial_state.push({
|
||||||
|
type: 'm.room.encryption',
|
||||||
state_key: '',
|
state_key: '',
|
||||||
|
content: {
|
||||||
|
algorithm: 'm.megolm.v1.aes-sha2',
|
||||||
},
|
},
|
||||||
];
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let modal;
|
let modal;
|
||||||
if (opts.spinner) modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner');
|
if (opts.spinner) modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner');
|
||||||
|
|
|
@ -250,7 +250,7 @@ function parseHtmlMessage(html, partCreator, isQuotedMessage) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parsePlainTextMessage(body, partCreator, isQuotedMessage) {
|
export function parsePlainTextMessage(body, partCreator, isQuotedMessage) {
|
||||||
const lines = body.split("\n");
|
const lines = body.split(/\r\n|\r|\n/g); // split on any new-line combination not just \n, collapses \r\n
|
||||||
const parts = lines.reduce((parts, line, i) => {
|
const parts = lines.reduce((parts, line, i) => {
|
||||||
if (isQuotedMessage) {
|
if (isQuotedMessage) {
|
||||||
parts.push(partCreator.plain(QUOTE_LINE_PREFIX));
|
parts.push(partCreator.plain(QUOTE_LINE_PREFIX));
|
||||||
|
|
|
@ -100,27 +100,71 @@ export function formatRangeAsCode(range) {
|
||||||
replaceRangeAndExpandSelection(range, parts);
|
replaceRangeAndExpandSelection(range, parts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parts helper methods
|
||||||
|
const isBlank = part => !part.text || !/\S/.test(part.text);
|
||||||
|
const isNL = part => part.type === "newline";
|
||||||
|
|
||||||
export function toggleInlineFormat(range, prefix, suffix = prefix) {
|
export function toggleInlineFormat(range, prefix, suffix = prefix) {
|
||||||
const {model, parts} = range;
|
const {model, parts} = range;
|
||||||
const {partCreator} = model;
|
const {partCreator} = model;
|
||||||
|
|
||||||
const isFormatted = parts.length &&
|
// compute paragraph [start, end] indexes
|
||||||
parts[0].text.startsWith(prefix) &&
|
const paragraphIndexes = [];
|
||||||
parts[parts.length - 1].text.endsWith(suffix);
|
let startIndex = 0;
|
||||||
|
// start at i=2 because we look at i and up to two parts behind to detect paragraph breaks at their end
|
||||||
|
for (let i = 2; i < parts.length; i++) {
|
||||||
|
// paragraph breaks can be denoted in a multitude of ways,
|
||||||
|
// - 2 newline parts in sequence
|
||||||
|
// - newline part, plain(<empty or just spaces>), newline part
|
||||||
|
|
||||||
|
// bump startIndex onto the first non-blank after the paragraph ending
|
||||||
|
if (isBlank(parts[i - 2]) && isNL(parts[i - 1]) && !isNL(parts[i]) && !isBlank(parts[i])) {
|
||||||
|
startIndex = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if at a paragraph break, store the indexes of the paragraph
|
||||||
|
if (isNL(parts[i - 1]) && isNL(parts[i])) {
|
||||||
|
paragraphIndexes.push([startIndex, i - 1]);
|
||||||
|
startIndex = i + 1;
|
||||||
|
} else if (isNL(parts[i - 2]) && isBlank(parts[i - 1]) && isNL(parts[i])) {
|
||||||
|
paragraphIndexes.push([startIndex, i - 2]);
|
||||||
|
startIndex = i + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastNonEmptyPart = parts.map(isBlank).lastIndexOf(false);
|
||||||
|
// If we have not yet included the final paragraph then add it now
|
||||||
|
if (startIndex <= lastNonEmptyPart) {
|
||||||
|
paragraphIndexes.push([startIndex, lastNonEmptyPart + 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// keep track of how many things we have inserted as an offset:=0
|
||||||
|
let offset = 0;
|
||||||
|
paragraphIndexes.forEach(([startIndex, endIndex]) => {
|
||||||
|
// for each paragraph apply the same rule
|
||||||
|
const base = startIndex + offset;
|
||||||
|
const index = endIndex + offset;
|
||||||
|
|
||||||
|
const isFormatted = (index - base > 0) &&
|
||||||
|
parts[base].text.startsWith(prefix) &&
|
||||||
|
parts[index - 1].text.endsWith(suffix);
|
||||||
|
|
||||||
if (isFormatted) {
|
if (isFormatted) {
|
||||||
// remove prefix and suffix
|
// remove prefix and suffix
|
||||||
const partWithoutPrefix = parts[0].serialize();
|
const partWithoutPrefix = parts[base].serialize();
|
||||||
partWithoutPrefix.text = partWithoutPrefix.text.substr(prefix.length);
|
partWithoutPrefix.text = partWithoutPrefix.text.substr(prefix.length);
|
||||||
parts[0] = partCreator.deserializePart(partWithoutPrefix);
|
parts[base] = partCreator.deserializePart(partWithoutPrefix);
|
||||||
|
|
||||||
const partWithoutSuffix = parts[parts.length - 1].serialize();
|
const partWithoutSuffix = parts[index - 1].serialize();
|
||||||
const suffixPartText = partWithoutSuffix.text;
|
const suffixPartText = partWithoutSuffix.text;
|
||||||
partWithoutSuffix.text = suffixPartText.substring(0, suffixPartText.length - suffix.length);
|
partWithoutSuffix.text = suffixPartText.substring(0, suffixPartText.length - suffix.length);
|
||||||
parts[parts.length - 1] = partCreator.deserializePart(partWithoutSuffix);
|
parts[index - 1] = partCreator.deserializePart(partWithoutSuffix);
|
||||||
} else {
|
} else {
|
||||||
parts.unshift(partCreator.plain(prefix));
|
parts.splice(index, 0, partCreator.plain(suffix)); // splice in the later one first to not change offset
|
||||||
parts.push(partCreator.plain(suffix));
|
parts.splice(base, 0, partCreator.plain(prefix));
|
||||||
|
offset += 2; // offset index to account for the two items we just spliced in
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
replaceRangeAndExpandSelection(range, parts);
|
replaceRangeAndExpandSelection(range, parts);
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,18 +61,26 @@ export function textSerialize(model) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function containsEmote(model) {
|
export function containsEmote(model) {
|
||||||
|
return startsWith(model, "/me ");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function startsWith(model, prefix) {
|
||||||
const firstPart = model.parts[0];
|
const firstPart = model.parts[0];
|
||||||
// part type will be "plain" while editing,
|
// part type will be "plain" while editing,
|
||||||
// and "command" while composing a message.
|
// and "command" while composing a message.
|
||||||
return firstPart &&
|
return firstPart &&
|
||||||
(firstPart.type === "plain" || firstPart.type === "command") &&
|
(firstPart.type === "plain" || firstPart.type === "command") &&
|
||||||
firstPart.text.startsWith("/me ");
|
firstPart.text.startsWith(prefix);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function stripEmoteCommand(model) {
|
export function stripEmoteCommand(model) {
|
||||||
// trim "/me "
|
// trim "/me "
|
||||||
|
return stripPrefix(model, "/me ");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stripPrefix(model, prefix) {
|
||||||
model = model.clone();
|
model = model.clone();
|
||||||
model.removeText({index: 0, offset: 0}, 4);
|
model.removeText({index: 0, offset: 0}, prefix.length);
|
||||||
return model;
|
return model;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -79,13 +79,13 @@ EMOJIBASE.forEach(emoji => {
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Strips variation selectors from a string
|
* Strips variation selectors from the end of given string
|
||||||
* NB. Skin tone modifers are not variation selectors:
|
* NB. Skin tone modifiers are not variation selectors:
|
||||||
* this function does not touch them. (Should it?)
|
* this function does not touch them. (Should it?)
|
||||||
*
|
*
|
||||||
* @param {string} str string to strip
|
* @param {string} str string to strip
|
||||||
* @returns {string} stripped string
|
* @returns {string} stripped string
|
||||||
*/
|
*/
|
||||||
function stripVariation(str) {
|
function stripVariation(str) {
|
||||||
return str.replace(/[\uFE00-\uFE0F]/, "");
|
return str.replace(/[\uFE00-\uFE0F]$/, "");
|
||||||
}
|
}
|
||||||
|
|
52
src/hooks/useSettings.js
Normal file
52
src/hooks/useSettings.js
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
/*
|
||||||
|
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 {useEffect, useState} from "react";
|
||||||
|
import SettingsStore from '../settings/SettingsStore';
|
||||||
|
|
||||||
|
// Hook to fetch the value of a setting and dynamically update when it changes
|
||||||
|
export const useSettingValue = (settingName, roomId = null, excludeDefault = false) => {
|
||||||
|
const [value, setValue] = useState(SettingsStore.getValue(settingName, roomId, excludeDefault));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const ref = SettingsStore.watchSetting(settingName, roomId, () => {
|
||||||
|
setValue(SettingsStore.getValue(settingName, roomId, excludeDefault));
|
||||||
|
});
|
||||||
|
// clean-up
|
||||||
|
return () => {
|
||||||
|
SettingsStore.unwatchSetting(ref);
|
||||||
|
};
|
||||||
|
}, [settingName, roomId, excludeDefault]);
|
||||||
|
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Hook to fetch whether a feature is enabled and dynamically update when that changes
|
||||||
|
export const useFeatureEnabled = (featureName, roomId = null) => {
|
||||||
|
const [enabled, setEnabled] = useState(SettingsStore.isFeatureEnabled(featureName, roomId));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const ref = SettingsStore.watchSetting(featureName, roomId, () => {
|
||||||
|
setEnabled(SettingsStore.isFeatureEnabled(featureName, roomId));
|
||||||
|
});
|
||||||
|
// clean-up
|
||||||
|
return () => {
|
||||||
|
SettingsStore.unwatchSetting(ref);
|
||||||
|
};
|
||||||
|
}, [featureName, roomId]);
|
||||||
|
|
||||||
|
return enabled;
|
||||||
|
};
|
|
@ -21,6 +21,9 @@
|
||||||
"Analytics": "Analytics",
|
"Analytics": "Analytics",
|
||||||
"The information being sent to us to help make Riot.im better includes:": "The information being sent to us to help make Riot.im better includes:",
|
"The information being sent to us to help make Riot.im better includes:": "The information being sent to us to help make Riot.im better includes:",
|
||||||
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.",
|
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.",
|
||||||
|
"Error": "Error",
|
||||||
|
"Unable to load! Check your network connectivity and try again.": "Unable to load! Check your network connectivity and try again.",
|
||||||
|
"Dismiss": "Dismiss",
|
||||||
"Call Failed": "Call Failed",
|
"Call Failed": "Call Failed",
|
||||||
"There are unknown devices in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.": "There are unknown devices in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.",
|
"There are unknown devices in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.": "There are unknown devices in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.",
|
||||||
"Review Devices": "Review Devices",
|
"Review Devices": "Review Devices",
|
||||||
|
@ -85,6 +88,9 @@
|
||||||
"%(weekDayName)s, %(monthName)s %(day)s %(time)s": "%(weekDayName)s, %(monthName)s %(day)s %(time)s",
|
"%(weekDayName)s, %(monthName)s %(day)s %(time)s": "%(weekDayName)s, %(monthName)s %(day)s %(time)s",
|
||||||
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s",
|
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s",
|
||||||
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s",
|
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s",
|
||||||
|
"Verify this session": "Verify this session",
|
||||||
|
"Encryption upgrade available": "Encryption upgrade available",
|
||||||
|
"Set up encryption": "Set up encryption",
|
||||||
"New Session": "New Session",
|
"New Session": "New Session",
|
||||||
"Who would you like to add to this community?": "Who would you like to add to this community?",
|
"Who would you like to add to this community?": "Who would you like to add to this community?",
|
||||||
"Warning: any person you add to a community will be publicly visible to anyone who knows the community ID": "Warning: any person you add to a community will be publicly visible to anyone who knows the community ID",
|
"Warning: any person you add to a community will be publicly visible to anyone who knows the community ID": "Warning: any person you add to a community will be publicly visible to anyone who knows the community ID",
|
||||||
|
@ -105,9 +111,6 @@
|
||||||
"This action requires accessing the default identity server <server /> to validate an email address or phone number, but the server does not have any terms of service.": "This action requires accessing the default identity server <server /> to validate an email address or phone number, but the server does not have any terms of service.",
|
"This action requires accessing the default identity server <server /> to validate an email address or phone number, but the server does not have any terms of service.": "This action requires accessing the default identity server <server /> to validate an email address or phone number, but the server does not have any terms of service.",
|
||||||
"Only continue if you trust the owner of the server.": "Only continue if you trust the owner of the server.",
|
"Only continue if you trust the owner of the server.": "Only continue if you trust the owner of the server.",
|
||||||
"Trust": "Trust",
|
"Trust": "Trust",
|
||||||
"Error": "Error",
|
|
||||||
"Unable to load! Check your network connectivity and try again.": "Unable to load! Check your network connectivity and try again.",
|
|
||||||
"Dismiss": "Dismiss",
|
|
||||||
"Riot does not have permission to send you notifications - please check your browser settings": "Riot does not have permission to send you notifications - please check your browser settings",
|
"Riot does not have permission to send you notifications - please check your browser settings": "Riot does not have permission to send you notifications - please check your browser settings",
|
||||||
"Riot was not given permission to send notifications - please try again": "Riot was not given permission to send notifications - please try again",
|
"Riot was not given permission to send notifications - please try again": "Riot was not given permission to send notifications - please try again",
|
||||||
"Unable to enable Notifications": "Unable to enable Notifications",
|
"Unable to enable Notifications": "Unable to enable Notifications",
|
||||||
|
@ -121,15 +124,8 @@
|
||||||
"Moderator": "Moderator",
|
"Moderator": "Moderator",
|
||||||
"Admin": "Admin",
|
"Admin": "Admin",
|
||||||
"Custom (%(level)s)": "Custom (%(level)s)",
|
"Custom (%(level)s)": "Custom (%(level)s)",
|
||||||
"Start a chat": "Start a chat",
|
|
||||||
"Who would you like to communicate with?": "Who would you like to communicate with?",
|
|
||||||
"Email, name or Matrix ID": "Email, name or Matrix ID",
|
|
||||||
"Start Chat": "Start Chat",
|
|
||||||
"Invite new room members": "Invite new room members",
|
|
||||||
"Send Invites": "Send Invites",
|
|
||||||
"Failed to start chat": "Failed to start chat",
|
|
||||||
"Operation failed": "Operation failed",
|
|
||||||
"Failed to invite": "Failed to invite",
|
"Failed to invite": "Failed to invite",
|
||||||
|
"Operation failed": "Operation failed",
|
||||||
"Failed to invite users to the room:": "Failed to invite users to the room:",
|
"Failed to invite users to the room:": "Failed to invite users to the room:",
|
||||||
"Failed to invite the following users to the %(roomName)s room:": "Failed to invite the following users to the %(roomName)s room:",
|
"Failed to invite the following users to the %(roomName)s room:": "Failed to invite the following users to the %(roomName)s room:",
|
||||||
"You need to be logged in.": "You need to be logged in.",
|
"You need to be logged in.": "You need to be logged in.",
|
||||||
|
@ -200,7 +196,6 @@
|
||||||
"Sends the given message coloured as a rainbow": "Sends the given message coloured as a rainbow",
|
"Sends the given message coloured as a rainbow": "Sends the given message coloured as a rainbow",
|
||||||
"Sends the given emote coloured as a rainbow": "Sends the given emote coloured as a rainbow",
|
"Sends the given emote coloured as a rainbow": "Sends the given emote coloured as a rainbow",
|
||||||
"Displays list of commands with usages and descriptions": "Displays list of commands with usages and descriptions",
|
"Displays list of commands with usages and descriptions": "Displays list of commands with usages and descriptions",
|
||||||
"Unrecognised command:": "Unrecognised command:",
|
|
||||||
"Reason": "Reason",
|
"Reason": "Reason",
|
||||||
"%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s accepted the invitation for %(displayName)s.",
|
"%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s accepted the invitation for %(displayName)s.",
|
||||||
"%(targetName)s accepted an invitation.": "%(targetName)s accepted an invitation.",
|
"%(targetName)s accepted an invitation.": "%(targetName)s accepted an invitation.",
|
||||||
|
@ -236,10 +231,13 @@
|
||||||
"%(senderDisplayName)s disabled flair for %(groups)s in this room.": "%(senderDisplayName)s disabled flair for %(groups)s in this room.",
|
"%(senderDisplayName)s disabled flair for %(groups)s in this room.": "%(senderDisplayName)s disabled flair for %(groups)s in this room.",
|
||||||
"%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.": "%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.",
|
"%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.": "%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.",
|
||||||
"%(senderDisplayName)s sent an image.": "%(senderDisplayName)s sent an image.",
|
"%(senderDisplayName)s sent an image.": "%(senderDisplayName)s sent an image.",
|
||||||
|
"%(senderName)s added %(addedAddresses)s and %(count)s other addresses to this room|other": "%(senderName)s added %(addedAddresses)s and %(count)s other addresses to this room",
|
||||||
"%(senderName)s added %(count)s %(addedAddresses)s as addresses for this room.|other": "%(senderName)s added %(addedAddresses)s as addresses for this room.",
|
"%(senderName)s added %(count)s %(addedAddresses)s as addresses for this room.|other": "%(senderName)s added %(addedAddresses)s as addresses for this room.",
|
||||||
"%(senderName)s added %(count)s %(addedAddresses)s as addresses for this room.|one": "%(senderName)s added %(addedAddresses)s as an address for this room.",
|
"%(senderName)s added %(count)s %(addedAddresses)s as addresses for this room.|one": "%(senderName)s added %(addedAddresses)s as an address for this room.",
|
||||||
|
"%(senderName)s removed %(removedAddresses)s and %(count)s other addresses from this room|other": "%(senderName)s removed %(removedAddresses)s and %(count)s other addresses from this room",
|
||||||
"%(senderName)s removed %(count)s %(removedAddresses)s as addresses for this room.|other": "%(senderName)s removed %(removedAddresses)s as addresses for this room.",
|
"%(senderName)s removed %(count)s %(removedAddresses)s as addresses for this room.|other": "%(senderName)s removed %(removedAddresses)s as addresses for this room.",
|
||||||
"%(senderName)s removed %(count)s %(removedAddresses)s as addresses for this room.|one": "%(senderName)s removed %(removedAddresses)s as an address for this room.",
|
"%(senderName)s removed %(count)s %(removedAddresses)s as addresses for this room.|one": "%(senderName)s removed %(removedAddresses)s as an address for this room.",
|
||||||
|
"%(senderName)s removed %(countRemoved)s and added %(countAdded)s addresses to this room": "%(senderName)s removed %(countRemoved)s and added %(countAdded)s addresses to this room",
|
||||||
"%(senderName)s added %(addedAddresses)s and removed %(removedAddresses)s as addresses for this room.": "%(senderName)s added %(addedAddresses)s and removed %(removedAddresses)s as addresses for this room.",
|
"%(senderName)s added %(addedAddresses)s and removed %(removedAddresses)s as addresses for this room.": "%(senderName)s added %(addedAddresses)s and removed %(removedAddresses)s as addresses for this room.",
|
||||||
"%(senderName)s set the main address for this room to %(address)s.": "%(senderName)s set the main address for this room to %(address)s.",
|
"%(senderName)s set the main address for this room to %(address)s.": "%(senderName)s set the main address for this room to %(address)s.",
|
||||||
"%(senderName)s removed the main address for this room.": "%(senderName)s removed the main address for this room.",
|
"%(senderName)s removed the main address for this room.": "%(senderName)s removed the main address for this room.",
|
||||||
|
@ -261,7 +259,8 @@
|
||||||
"%(senderName)s made future room history visible to all room members.": "%(senderName)s made future room history visible to all room members.",
|
"%(senderName)s made future room history visible to all room members.": "%(senderName)s made future room history visible to all room members.",
|
||||||
"%(senderName)s made future room history visible to anyone.": "%(senderName)s made future room history visible to anyone.",
|
"%(senderName)s made future room history visible to anyone.": "%(senderName)s made future room history visible to anyone.",
|
||||||
"%(senderName)s made future room history visible to unknown (%(visibility)s).": "%(senderName)s made future room history visible to unknown (%(visibility)s).",
|
"%(senderName)s made future room history visible to unknown (%(visibility)s).": "%(senderName)s made future room history visible to unknown (%(visibility)s).",
|
||||||
"%(senderName)s turned on end-to-end encryption (algorithm %(algorithm)s).": "%(senderName)s turned on end-to-end encryption (algorithm %(algorithm)s).",
|
"%(senderName)s turned on end-to-end encryption.": "%(senderName)s turned on end-to-end encryption.",
|
||||||
|
"%(senderName)s turned on end-to-end encryption (unrecognised algorithm %(algorithm)s).": "%(senderName)s turned on end-to-end encryption (unrecognised algorithm %(algorithm)s).",
|
||||||
"%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s",
|
"%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s",
|
||||||
"%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s changed the power level of %(powerLevelDiffText)s.",
|
"%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s changed the power level of %(powerLevelDiffText)s.",
|
||||||
"%(senderName)s changed the pinned messages for the room.": "%(senderName)s changed the pinned messages for the room.",
|
"%(senderName)s changed the pinned messages for the room.": "%(senderName)s changed the pinned messages for the room.",
|
||||||
|
@ -373,7 +372,6 @@
|
||||||
"Render simple counters in room header": "Render simple counters in room header",
|
"Render simple counters in room header": "Render simple counters in room header",
|
||||||
"Multiple integration managers": "Multiple integration managers",
|
"Multiple integration managers": "Multiple integration managers",
|
||||||
"Try out new ways to ignore people (experimental)": "Try out new ways to ignore people (experimental)",
|
"Try out new ways to ignore people (experimental)": "Try out new ways to ignore people (experimental)",
|
||||||
"New invite dialog": "New invite dialog",
|
|
||||||
"Show a presence dot next to DMs in the room list": "Show a presence dot next to DMs in the room list",
|
"Show a presence dot next to DMs in the room list": "Show a presence dot next to DMs in the room list",
|
||||||
"Enable cross-signing to verify per-user instead of per-device (in development)": "Enable cross-signing to verify per-user instead of per-device (in development)",
|
"Enable cross-signing to verify per-user instead of per-device (in development)": "Enable cross-signing to verify per-user instead of per-device (in development)",
|
||||||
"Enable local event indexing and E2EE search (requires restart)": "Enable local event indexing and E2EE search (requires restart)",
|
"Enable local event indexing and E2EE search (requires restart)": "Enable local event indexing and E2EE search (requires restart)",
|
||||||
|
@ -416,6 +414,7 @@
|
||||||
"Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)": "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)",
|
"Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)": "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)",
|
||||||
"Send read receipts for messages (requires compatible homeserver to disable)": "Send read receipts for messages (requires compatible homeserver to disable)",
|
"Send read receipts for messages (requires compatible homeserver to disable)": "Send read receipts for messages (requires compatible homeserver to disable)",
|
||||||
"Show previews/thumbnails for images": "Show previews/thumbnails for images",
|
"Show previews/thumbnails for images": "Show previews/thumbnails for images",
|
||||||
|
"Enable message search in encrypted rooms": "Enable message search in encrypted rooms",
|
||||||
"Collecting app version information": "Collecting app version information",
|
"Collecting app version information": "Collecting app version information",
|
||||||
"Collecting logs": "Collecting logs",
|
"Collecting logs": "Collecting logs",
|
||||||
"Uploading report": "Uploading report",
|
"Uploading report": "Uploading report",
|
||||||
|
@ -514,8 +513,12 @@
|
||||||
"Headphones": "Headphones",
|
"Headphones": "Headphones",
|
||||||
"Folder": "Folder",
|
"Folder": "Folder",
|
||||||
"Pin": "Pin",
|
"Pin": "Pin",
|
||||||
"Other users may not trust it": "Other users may not trust it",
|
"Review & verify your new session": "Review & verify your new session",
|
||||||
"Later": "Later",
|
"Later": "Later",
|
||||||
|
"Review": "Review",
|
||||||
|
"Verify your other devices easier": "Verify your other devices easier",
|
||||||
|
"Other users may not trust it": "Other users may not trust it",
|
||||||
|
"Upgrade": "Upgrade",
|
||||||
"Verify": "Verify",
|
"Verify": "Verify",
|
||||||
"Decline (%(counter)s)": "Decline (%(counter)s)",
|
"Decline (%(counter)s)": "Decline (%(counter)s)",
|
||||||
"Accept <policyLink /> to continue:": "Accept <policyLink /> to continue:",
|
"Accept <policyLink /> to continue:": "Accept <policyLink /> to continue:",
|
||||||
|
@ -563,6 +566,14 @@
|
||||||
"Failed to set display name": "Failed to set display name",
|
"Failed to set display name": "Failed to set display name",
|
||||||
"Disable Notifications": "Disable Notifications",
|
"Disable Notifications": "Disable Notifications",
|
||||||
"Enable Notifications": "Enable Notifications",
|
"Enable Notifications": "Enable Notifications",
|
||||||
|
"Securely cache encrypted messages locally for them to appear in search results, using ": "Securely cache encrypted messages locally for them to appear in search results, using ",
|
||||||
|
" to store messages from ": " to store messages from ",
|
||||||
|
"rooms.": "rooms.",
|
||||||
|
"Manage": "Manage",
|
||||||
|
"Securely cache encrypted messages locally for them to appear in search results.": "Securely cache encrypted messages locally for them to appear in search results.",
|
||||||
|
"Enable": "Enable",
|
||||||
|
"Riot is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom Riot Desktop with <nativeLink>search components added</nativeLink>.": "Riot is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom Riot Desktop with <nativeLink>search components added</nativeLink>.",
|
||||||
|
"Riot can't securely cache encrypted messages locally while running in a web browser. Use <riotLink>Riot Desktop</riotLink> for encrypted messages to appear in search results.": "Riot can't securely cache encrypted messages locally while running in a web browser. Use <riotLink>Riot Desktop</riotLink> for encrypted messages to appear in search results.",
|
||||||
"Connecting to integration manager...": "Connecting to integration manager...",
|
"Connecting to integration manager...": "Connecting to integration manager...",
|
||||||
"Cannot connect to integration manager": "Cannot connect to integration manager",
|
"Cannot connect to integration manager": "Cannot connect to integration manager",
|
||||||
"The integration manager is offline or it cannot reach your homeserver.": "The integration manager is offline or it cannot reach your homeserver.",
|
"The integration manager is offline or it cannot reach your homeserver.": "The integration manager is offline or it cannot reach your homeserver.",
|
||||||
|
@ -669,8 +680,8 @@
|
||||||
"Profile": "Profile",
|
"Profile": "Profile",
|
||||||
"Email addresses": "Email addresses",
|
"Email addresses": "Email addresses",
|
||||||
"Phone numbers": "Phone numbers",
|
"Phone numbers": "Phone numbers",
|
||||||
"Account": "Account",
|
|
||||||
"Set a new account password...": "Set a new account password...",
|
"Set a new account password...": "Set a new account password...",
|
||||||
|
"Account": "Account",
|
||||||
"Language and region": "Language and region",
|
"Language and region": "Language and region",
|
||||||
"Theme": "Theme",
|
"Theme": "Theme",
|
||||||
"Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.": "Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.",
|
"Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.": "Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.",
|
||||||
|
@ -694,7 +705,6 @@
|
||||||
"Clear cache and reload": "Clear cache and reload",
|
"Clear cache and reload": "Clear cache and reload",
|
||||||
"FAQ": "FAQ",
|
"FAQ": "FAQ",
|
||||||
"Versions": "Versions",
|
"Versions": "Versions",
|
||||||
"matrix-react-sdk version:": "matrix-react-sdk version:",
|
|
||||||
"riot-web version:": "riot-web version:",
|
"riot-web version:": "riot-web version:",
|
||||||
"olm version:": "olm version:",
|
"olm version:": "olm version:",
|
||||||
"Homeserver is": "Homeserver is",
|
"Homeserver is": "Homeserver is",
|
||||||
|
@ -757,6 +767,7 @@
|
||||||
"Accept all %(invitedRooms)s invites": "Accept all %(invitedRooms)s invites",
|
"Accept all %(invitedRooms)s invites": "Accept all %(invitedRooms)s invites",
|
||||||
"Reject all %(invitedRooms)s invites": "Reject all %(invitedRooms)s invites",
|
"Reject all %(invitedRooms)s invites": "Reject all %(invitedRooms)s invites",
|
||||||
"Key backup": "Key backup",
|
"Key backup": "Key backup",
|
||||||
|
"Message search": "Message search",
|
||||||
"Cross-signing": "Cross-signing",
|
"Cross-signing": "Cross-signing",
|
||||||
"Security & Privacy": "Security & Privacy",
|
"Security & Privacy": "Security & Privacy",
|
||||||
"Devices": "Devices",
|
"Devices": "Devices",
|
||||||
|
@ -888,8 +899,9 @@
|
||||||
"This user has not verified all of their devices.": "This user has not verified all of their devices.",
|
"This user has not verified all of their devices.": "This user has not verified all of their devices.",
|
||||||
"You have not verified this user. This user has verified all of their devices.": "You have not verified this user. This user has verified all of their devices.",
|
"You have not verified this user. This user has verified all of their devices.": "You have not verified this user. This user has verified all of their devices.",
|
||||||
"You have verified this user. This user has verified all of their devices.": "You have verified this user. This user has verified all of their devices.",
|
"You have verified this user. This user has verified all of their devices.": "You have verified this user. This user has verified all of their devices.",
|
||||||
"Some users in this encrypted room are not verified by you or they have not verified their own devices.": "Some users in this encrypted room are not verified by you or they have not verified their own devices.",
|
"Someone is using an unknown device": "Someone is using an unknown device",
|
||||||
"All users in this encrypted room are verified by you and they have verified their own devices.": "All users in this encrypted room are verified by you and they have verified their own devices.",
|
"This room is end-to-end encrypted": "This room is end-to-end encrypted",
|
||||||
|
"Everyone in this room is verified": "Everyone in this room is verified",
|
||||||
"Some devices for this user are not trusted": "Some devices for this user are not trusted",
|
"Some devices for this user are not trusted": "Some devices for this user are not trusted",
|
||||||
"All devices for this user are trusted": "All devices for this user are trusted",
|
"All devices for this user are trusted": "All devices for this user are trusted",
|
||||||
"Some devices in this encrypted room are not trusted": "Some devices in this encrypted room are not trusted",
|
"Some devices in this encrypted room are not trusted": "Some devices in this encrypted room are not trusted",
|
||||||
|
@ -907,7 +919,9 @@
|
||||||
"This message cannot be decrypted": "This message cannot be decrypted",
|
"This message cannot be decrypted": "This message cannot be decrypted",
|
||||||
"Encrypted by an unverified device": "Encrypted by an unverified device",
|
"Encrypted by an unverified device": "Encrypted by an unverified device",
|
||||||
"Unencrypted": "Unencrypted",
|
"Unencrypted": "Unencrypted",
|
||||||
|
"Encrypted by a deleted device": "Encrypted by a deleted device",
|
||||||
"Please select the destination room for this message": "Please select the destination room for this message",
|
"Please select the destination room for this message": "Please select the destination room for this message",
|
||||||
|
"Invite only": "Invite only",
|
||||||
"Scroll to bottom of page": "Scroll to bottom of page",
|
"Scroll to bottom of page": "Scroll to bottom of page",
|
||||||
"Close preview": "Close preview",
|
"Close preview": "Close preview",
|
||||||
"device id: ": "device id: ",
|
"device id: ": "device id: ",
|
||||||
|
@ -946,6 +960,7 @@
|
||||||
"Invite": "Invite",
|
"Invite": "Invite",
|
||||||
"Share Link to User": "Share Link to User",
|
"Share Link to User": "Share Link to User",
|
||||||
"User Options": "User Options",
|
"User Options": "User Options",
|
||||||
|
"Start a chat": "Start a chat",
|
||||||
"Direct chats": "Direct chats",
|
"Direct chats": "Direct chats",
|
||||||
"Remove recent messages": "Remove recent messages",
|
"Remove recent messages": "Remove recent messages",
|
||||||
"Unmute": "Unmute",
|
"Unmute": "Unmute",
|
||||||
|
@ -964,8 +979,10 @@
|
||||||
"Hangup": "Hangup",
|
"Hangup": "Hangup",
|
||||||
"Upload file": "Upload file",
|
"Upload file": "Upload file",
|
||||||
"Send an encrypted reply…": "Send an encrypted reply…",
|
"Send an encrypted reply…": "Send an encrypted reply…",
|
||||||
"Send a reply (unencrypted)…": "Send a reply (unencrypted)…",
|
"Send a reply…": "Send a reply…",
|
||||||
"Send an encrypted message…": "Send an encrypted message…",
|
"Send an encrypted message…": "Send an encrypted message…",
|
||||||
|
"Send a message…": "Send a message…",
|
||||||
|
"Send a reply (unencrypted)…": "Send a reply (unencrypted)…",
|
||||||
"Send a message (unencrypted)…": "Send a message (unencrypted)…",
|
"Send a message (unencrypted)…": "Send a message (unencrypted)…",
|
||||||
"The conversation continues here.": "The conversation continues here.",
|
"The conversation continues here.": "The conversation continues here.",
|
||||||
"This room has been replaced and is no longer active.": "This room has been replaced and is no longer active.",
|
"This room has been replaced and is no longer active.": "This room has been replaced and is no longer active.",
|
||||||
|
@ -1012,7 +1029,7 @@
|
||||||
"Community Invites": "Community Invites",
|
"Community Invites": "Community Invites",
|
||||||
"Invites": "Invites",
|
"Invites": "Invites",
|
||||||
"Favourites": "Favourites",
|
"Favourites": "Favourites",
|
||||||
"People": "People",
|
"Direct Messages": "Direct Messages",
|
||||||
"Start chat": "Start chat",
|
"Start chat": "Start chat",
|
||||||
"Rooms": "Rooms",
|
"Rooms": "Rooms",
|
||||||
"Low priority": "Low priority",
|
"Low priority": "Low priority",
|
||||||
|
@ -1049,6 +1066,7 @@
|
||||||
"Do you want to join %(roomName)s?": "Do you want to join %(roomName)s?",
|
"Do you want to join %(roomName)s?": "Do you want to join %(roomName)s?",
|
||||||
"<userName/> invited you": "<userName/> invited you",
|
"<userName/> invited you": "<userName/> invited you",
|
||||||
"Reject": "Reject",
|
"Reject": "Reject",
|
||||||
|
"Reject & Ignore user": "Reject & Ignore user",
|
||||||
"You're previewing %(roomName)s. Want to join it?": "You're previewing %(roomName)s. Want to join it?",
|
"You're previewing %(roomName)s. Want to join it?": "You're previewing %(roomName)s. Want to join it?",
|
||||||
"%(roomName)s can't be previewed. Do you want to join it?": "%(roomName)s can't be previewed. Do you want to join it?",
|
"%(roomName)s can't be previewed. Do you want to join it?": "%(roomName)s can't be previewed. Do you want to join it?",
|
||||||
"%(roomName)s does not exist.": "%(roomName)s does not exist.",
|
"%(roomName)s does not exist.": "%(roomName)s does not exist.",
|
||||||
|
@ -1079,6 +1097,11 @@
|
||||||
"Server error": "Server error",
|
"Server error": "Server error",
|
||||||
"Command error": "Command error",
|
"Command error": "Command error",
|
||||||
"Server unavailable, overloaded, or something else went wrong.": "Server unavailable, overloaded, or something else went wrong.",
|
"Server unavailable, overloaded, or something else went wrong.": "Server unavailable, overloaded, or something else went wrong.",
|
||||||
|
"Unknown Command": "Unknown Command",
|
||||||
|
"Unrecognised command: %(commandText)s": "Unrecognised command: %(commandText)s",
|
||||||
|
"You can use <code>/help</code> to list available commands. Did you mean to send this as a message?": "You can use <code>/help</code> to list available commands. Did you mean to send this as a message?",
|
||||||
|
"Hint: Begin your message with <code>//</code> to start it with a slash.": "Hint: Begin your message with <code>//</code> to start it with a slash.",
|
||||||
|
"Send as message": "Send as message",
|
||||||
"Failed to connect to integration manager": "Failed to connect to integration manager",
|
"Failed to connect to integration manager": "Failed to connect to integration manager",
|
||||||
"You don't currently have any stickerpacks enabled": "You don't currently have any stickerpacks enabled",
|
"You don't currently have any stickerpacks enabled": "You don't currently have any stickerpacks enabled",
|
||||||
"Add some now": "Add some now",
|
"Add some now": "Add some now",
|
||||||
|
@ -1464,8 +1487,7 @@
|
||||||
"Recent Conversations": "Recent Conversations",
|
"Recent Conversations": "Recent Conversations",
|
||||||
"Suggestions": "Suggestions",
|
"Suggestions": "Suggestions",
|
||||||
"Recently Direct Messaged": "Recently Direct Messaged",
|
"Recently Direct Messaged": "Recently Direct Messaged",
|
||||||
"Direct Messages": "Direct Messages",
|
"If you can't find someone, ask them for their username, share your username (%(userId)s) or <a>profile link</a>.": "If you can't find someone, ask them for their username, share your username (%(userId)s) or <a>profile link</a>.",
|
||||||
"If you can't find someone, ask them for their username, or share your username (%(userId)s) or <a>profile link</a>.": "If you can't find someone, ask them for their username, or share your username (%(userId)s) or <a>profile link</a>.",
|
|
||||||
"Go": "Go",
|
"Go": "Go",
|
||||||
"If you can't find someone, ask them for their username (e.g. @user:server.com) or <a>share this room</a>.": "If you can't find someone, ask them for their username (e.g. @user:server.com) or <a>share this room</a>.",
|
"If you can't find someone, ask them for their username (e.g. @user:server.com) or <a>share this room</a>.": "If you can't find someone, ask them for their username (e.g. @user:server.com) or <a>share this room</a>.",
|
||||||
"You added a new device '%(displayName)s', which is requesting encryption keys.": "You added a new device '%(displayName)s', which is requesting encryption keys.",
|
"You added a new device '%(displayName)s', which is requesting encryption keys.": "You added a new device '%(displayName)s', which is requesting encryption keys.",
|
||||||
|
@ -1510,7 +1532,6 @@
|
||||||
"Upgrading a room is an advanced action and is usually recommended when a room is unstable due to bugs, missing features or security vulnerabilities.": "Upgrading a room is an advanced action and is usually recommended when a room is unstable due to bugs, missing features or security vulnerabilities.",
|
"Upgrading a room is an advanced action and is usually recommended when a room is unstable due to bugs, missing features or security vulnerabilities.": "Upgrading a room is an advanced action and is usually recommended when a room is unstable due to bugs, missing features or security vulnerabilities.",
|
||||||
"This usually only affects how the room is processed on the server. If you're having problems with your Riot, please <a>report a bug</a>.": "This usually only affects how the room is processed on the server. If you're having problems with your Riot, please <a>report a bug</a>.",
|
"This usually only affects how the room is processed on the server. If you're having problems with your Riot, please <a>report a bug</a>.": "This usually only affects how the room is processed on the server. If you're having problems with your Riot, please <a>report a bug</a>.",
|
||||||
"You'll upgrade this room from <oldVersion /> to <newVersion />.": "You'll upgrade this room from <oldVersion /> to <newVersion />.",
|
"You'll upgrade this room from <oldVersion /> to <newVersion />.": "You'll upgrade this room from <oldVersion /> to <newVersion />.",
|
||||||
"Upgrade": "Upgrade",
|
|
||||||
"Sign out and remove encryption keys?": "Sign out and remove encryption keys?",
|
"Sign out and remove encryption keys?": "Sign out and remove encryption keys?",
|
||||||
"Clear Storage and Sign Out": "Clear Storage and Sign Out",
|
"Clear Storage and Sign Out": "Clear Storage and Sign Out",
|
||||||
"Send Logs": "Send Logs",
|
"Send Logs": "Send Logs",
|
||||||
|
@ -1973,18 +1994,19 @@
|
||||||
"Import": "Import",
|
"Import": "Import",
|
||||||
"Key Backup is enabled on your account but has not been set up from this session. To set up secret storage, restore your key backup.": "Key Backup is enabled on your account but has not been set up from this session. To set up secret storage, restore your key backup.",
|
"Key Backup is enabled on your account but has not been set up from this session. To set up secret storage, restore your key backup.": "Key Backup is enabled on your account but has not been set up from this session. To set up secret storage, restore your key backup.",
|
||||||
"Restore": "Restore",
|
"Restore": "Restore",
|
||||||
"Secret Storage will be set up using your existing key backup details. Your secret storage passphrase and recovery key will be the same as they were for your key backup.": "Secret Storage will be set up using your existing key backup details. Your secret storage passphrase and recovery key will be the same as they were for your key backup.",
|
"Enter your account password to confirm the upgrade:": "Enter your account password to confirm the upgrade:",
|
||||||
|
"You'll need to authenticate with the server to confirm the upgrade.": "You'll need to authenticate with the server to confirm the upgrade.",
|
||||||
|
"Upgrade this device to allow it to verify other devices, granting them access to encrypted messages and marking them as trusted for other users.": "Upgrade this device to allow it to verify other devices, granting them access to encrypted messages and marking them as trusted for other users.",
|
||||||
"Great! This passphrase looks strong enough.": "Great! This passphrase looks strong enough.",
|
"Great! This passphrase looks strong enough.": "Great! This passphrase looks strong enough.",
|
||||||
"<b>Warning</b>: You should only set up secret storage from a trusted computer.": "<b>Warning</b>: You should only set up secret storage from a trusted computer.",
|
"Set up encryption on this device to allow it to verify other devices, granting them access to encrypted messages and marking them as trusted for other users.": "Set up encryption on this device to allow it to verify other devices, granting them access to encrypted messages and marking them as trusted for other users.",
|
||||||
"We'll use secret storage to optionally store an encrypted copy of your cross-signing identity for verifying other devices and message keys on our server. Protect your access to encrypted messages with a passphrase to keep it secure.": "We'll use secret storage to optionally store an encrypted copy of your cross-signing identity for verifying other devices and message keys on our server. Protect your access to encrypted messages with a passphrase to keep it secure.",
|
"Secure your encryption keys with a passphrase. For maximum security this should be different to your account password:": "Secure your encryption keys with a passphrase. For maximum security this should be different to your account password:",
|
||||||
"For maximum security, this should be different from your account password.": "For maximum security, this should be different from your account password.",
|
"Enter a passphrase": "Enter a passphrase",
|
||||||
"Enter a passphrase...": "Enter a passphrase...",
|
|
||||||
"Set up with a recovery key": "Set up with a recovery key",
|
"Set up with a recovery key": "Set up with a recovery key",
|
||||||
"That matches!": "That matches!",
|
"That matches!": "That matches!",
|
||||||
"That doesn't match.": "That doesn't match.",
|
"That doesn't match.": "That doesn't match.",
|
||||||
"Go back to set it again.": "Go back to set it again.",
|
"Go back to set it again.": "Go back to set it again.",
|
||||||
"Please enter your passphrase a second time to confirm.": "Please enter your passphrase a second time to confirm.",
|
"Enter your passphrase a second time to confirm it.": "Enter your passphrase a second time to confirm it.",
|
||||||
"Repeat your passphrase...": "Repeat your passphrase...",
|
"Confirm your passphrase": "Confirm your passphrase",
|
||||||
"As a safety net, you can use it to restore your access to encrypted messages if you forget your passphrase.": "As a safety net, you can use it to restore your access to encrypted messages if you forget your passphrase.",
|
"As a safety net, you can use it to restore your access to encrypted messages if you forget your passphrase.": "As a safety net, you can use it to restore your access to encrypted messages if you forget your passphrase.",
|
||||||
"As a safety net, you can use it to restore your access to encrypted messages.": "As a safety net, you can use it to restore your access to encrypted messages.",
|
"As a safety net, you can use it to restore your access to encrypted messages.": "As a safety net, you can use it to restore your access to encrypted messages.",
|
||||||
"Your recovery key is a safety net - you can use it to restore access to your encrypted messages if you forget your passphrase.": "Your recovery key is a safety net - you can use it to restore access to your encrypted messages if you forget your passphrase.",
|
"Your recovery key is a safety net - you can use it to restore access to your encrypted messages if you forget your passphrase.": "Your recovery key is a safety net - you can use it to restore access to your encrypted messages if you forget your passphrase.",
|
||||||
|
@ -1997,21 +2019,25 @@
|
||||||
"<b>Print it</b> and store it somewhere safe": "<b>Print it</b> and store it somewhere safe",
|
"<b>Print it</b> and store it somewhere safe": "<b>Print it</b> and store it somewhere safe",
|
||||||
"<b>Save it</b> on a USB key or backup drive": "<b>Save it</b> on a USB key or backup drive",
|
"<b>Save it</b> on a USB key or backup drive": "<b>Save it</b> on a USB key or backup drive",
|
||||||
"<b>Copy it</b> to your personal cloud storage": "<b>Copy it</b> to your personal cloud storage",
|
"<b>Copy it</b> to your personal cloud storage": "<b>Copy it</b> to your personal cloud storage",
|
||||||
"Your access to encrypted messages is now protected.": "Your access to encrypted messages is now protected.",
|
"This device can now verify other devices, granting them access to encrypted messages and marking them as trusted for other users.": "This device can now verify other devices, granting them access to encrypted messages and marking them as trusted for other users.",
|
||||||
|
"Verify other users in their profile.": "Verify other users in their profile.",
|
||||||
"Without setting up secret storage, you won't be able to restore your access to encrypted messages or your cross-signing identity for verifying other devices if you log out or use another device.": "Without setting up secret storage, you won't be able to restore your access to encrypted messages or your cross-signing identity for verifying other devices if you log out or use another device.",
|
"Without setting up secret storage, you won't be able to restore your access to encrypted messages or your cross-signing identity for verifying other devices if you log out or use another device.": "Without setting up secret storage, you won't be able to restore your access to encrypted messages or your cross-signing identity for verifying other devices if you log out or use another device.",
|
||||||
"Set up secret storage": "Set up secret storage",
|
"Set up secret storage": "Set up secret storage",
|
||||||
"Restore your Key Backup": "Restore your Key Backup",
|
"Restore your Key Backup": "Restore your Key Backup",
|
||||||
"Migrate from Key Backup": "Migrate from Key Backup",
|
"Upgrade your encryption": "Upgrade your encryption",
|
||||||
"Secure your encrypted messages with a passphrase": "Secure your encrypted messages with a passphrase",
|
|
||||||
"Confirm your passphrase": "Confirm your passphrase",
|
|
||||||
"Recovery key": "Recovery key",
|
"Recovery key": "Recovery key",
|
||||||
"Keep it safe": "Keep it safe",
|
"Keep it safe": "Keep it safe",
|
||||||
"Storing secrets...": "Storing secrets...",
|
"Storing secrets...": "Storing secrets...",
|
||||||
"Success!": "Success!",
|
"Encryption upgraded": "Encryption upgraded",
|
||||||
|
"Encryption setup complete": "Encryption setup complete",
|
||||||
"Unable to set up secret storage": "Unable to set up secret storage",
|
"Unable to set up secret storage": "Unable to set up secret storage",
|
||||||
"Retry": "Retry",
|
"Retry": "Retry",
|
||||||
"We'll store an encrypted copy of your keys on our server. Protect your backup with a passphrase to keep it secure.": "We'll store an encrypted copy of your keys on our server. Protect your backup with a passphrase to keep it secure.",
|
"We'll store an encrypted copy of your keys on our server. Protect your backup with a passphrase to keep it secure.": "We'll store an encrypted copy of your keys on our server. Protect your backup with a passphrase to keep it secure.",
|
||||||
|
"For maximum security, this should be different from your account password.": "For maximum security, this should be different from your account password.",
|
||||||
|
"Enter a passphrase...": "Enter a passphrase...",
|
||||||
"Set up with a Recovery Key": "Set up with a Recovery Key",
|
"Set up with a Recovery Key": "Set up with a Recovery Key",
|
||||||
|
"Please enter your passphrase a second time to confirm.": "Please enter your passphrase a second time to confirm.",
|
||||||
|
"Repeat your passphrase...": "Repeat your passphrase...",
|
||||||
"As a safety net, you can use it to restore your encrypted message history if you forget your Recovery Passphrase.": "As a safety net, you can use it to restore your encrypted message history if you forget your Recovery Passphrase.",
|
"As a safety net, you can use it to restore your encrypted message history if you forget your Recovery Passphrase.": "As a safety net, you can use it to restore your encrypted message history if you forget your Recovery Passphrase.",
|
||||||
"As a safety net, you can use it to restore your encrypted message history.": "As a safety net, you can use it to restore your encrypted message history.",
|
"As a safety net, you can use it to restore your encrypted message history.": "As a safety net, you can use it to restore your encrypted message history.",
|
||||||
"Your keys are being backed up (the first backup could take a few minutes).": "Your keys are being backed up (the first backup could take a few minutes).",
|
"Your keys are being backed up (the first backup could take a few minutes).": "Your keys are being backed up (the first backup could take a few minutes).",
|
||||||
|
@ -2019,6 +2045,7 @@
|
||||||
"Set up Secure Message Recovery": "Set up Secure Message Recovery",
|
"Set up Secure Message Recovery": "Set up Secure Message Recovery",
|
||||||
"Secure your backup with a passphrase": "Secure your backup with a passphrase",
|
"Secure your backup with a passphrase": "Secure your backup with a passphrase",
|
||||||
"Starting backup...": "Starting backup...",
|
"Starting backup...": "Starting backup...",
|
||||||
|
"Success!": "Success!",
|
||||||
"Create Key Backup": "Create Key Backup",
|
"Create Key Backup": "Create Key Backup",
|
||||||
"Unable to create key backup": "Unable to create key backup",
|
"Unable to create key backup": "Unable to create key backup",
|
||||||
"Without setting up Secure Message Recovery, you'll lose your secure message history when you log out.": "Without setting up Secure Message Recovery, you'll lose your secure message history when you log out.",
|
"Without setting up Secure Message Recovery, you'll lose your secure message history when you log out.": "Without setting up Secure Message Recovery, you'll lose your secure message history when you log out.",
|
||||||
|
@ -2035,6 +2062,14 @@
|
||||||
"This device has detected that your recovery passphrase and key for Secure Messages have been removed.": "This device has detected that your recovery passphrase and key for Secure Messages have been removed.",
|
"This device has detected that your recovery passphrase and key for Secure Messages have been removed.": "This device has detected that your recovery passphrase and key for Secure Messages have been removed.",
|
||||||
"If you did this accidentally, you can setup Secure Messages on this device which will re-encrypt this device's message history with a new recovery method.": "If you did this accidentally, you can setup Secure Messages on this device which will re-encrypt this device's message history with a new recovery method.",
|
"If you did this accidentally, you can setup Secure Messages on this device which will re-encrypt this device's message history with a new recovery method.": "If you did this accidentally, you can setup Secure Messages on this device which will re-encrypt this device's message history with a new recovery method.",
|
||||||
"If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.",
|
"If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.",
|
||||||
|
"If disabled, messages from encrypted rooms won't appear in search results.": "If disabled, messages from encrypted rooms won't appear in search results.",
|
||||||
|
"Disable": "Disable",
|
||||||
|
"Not currently downloading messages for any room.": "Not currently downloading messages for any room.",
|
||||||
|
"Downloading mesages for %(currentRoom)s.": "Downloading mesages for %(currentRoom)s.",
|
||||||
|
"Riot is securely caching encrypted messages locally for them to appear in search results:": "Riot is securely caching encrypted messages locally for them to appear in search results:",
|
||||||
|
"Space used:": "Space used:",
|
||||||
|
"Indexed messages:": "Indexed messages:",
|
||||||
|
"Number of rooms:": "Number of rooms:",
|
||||||
"Failed to set direct chat tag": "Failed to set direct chat tag",
|
"Failed to set direct chat tag": "Failed to set direct chat tag",
|
||||||
"Failed to remove tag %(tagName)s from room": "Failed to remove tag %(tagName)s from room",
|
"Failed to remove tag %(tagName)s from room": "Failed to remove tag %(tagName)s from room",
|
||||||
"Failed to add tag %(tagName)s to room": "Failed to add tag %(tagName)s to room"
|
"Failed to add tag %(tagName)s to room": "Failed to add tag %(tagName)s to room"
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue