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:
|
||||
- label: ":eslint: JS Lint"
|
||||
command:
|
||||
# We fetch the develop js-sdk to get our latest eslint rules
|
||||
- "echo '--- Install js-sdk'"
|
||||
- "./scripts/ci/install-deps.sh"
|
||||
- "./scripts/ci/install-deps.sh --ignore-scripts"
|
||||
- "echo '+++ Lint'"
|
||||
- "yarn lint:js"
|
||||
plugins:
|
||||
- docker#v3.0.1:
|
||||
|
@ -10,8 +12,9 @@ steps:
|
|||
|
||||
- label: ":eslint: TS Lint"
|
||||
command:
|
||||
- "echo '--- Install js-sdk'"
|
||||
- "./scripts/ci/install-deps.sh"
|
||||
- "echo '--- Install'"
|
||||
- "yarn install --ignore-scripts"
|
||||
- "echo '+++ Lint'"
|
||||
- "yarn lint:ts"
|
||||
plugins:
|
||||
- docker#v3.0.1:
|
||||
|
@ -19,12 +22,21 @@ steps:
|
|||
|
||||
- label: ":eslint: Types Lint"
|
||||
command:
|
||||
- "echo '--- Install js-sdk'"
|
||||
- "./scripts/ci/install-deps.sh"
|
||||
- "echo '--- Install'"
|
||||
- "yarn install --ignore-scripts"
|
||||
- "echo '+++ Lint'"
|
||||
- "yarn lint:types"
|
||||
plugins:
|
||||
- docker#v3.0.1:
|
||||
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"
|
||||
agents:
|
||||
|
@ -33,13 +45,11 @@ steps:
|
|||
queue: "medium"
|
||||
command:
|
||||
- "echo '--- Install js-sdk'"
|
||||
# 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"
|
||||
# We don't use the babel-ed output for anything so we can --ignore-scripts
|
||||
# to save transpiling the files. We run the transpile step explicitly in
|
||||
# the 'build' job.
|
||||
- "./scripts/ci/install-deps.sh --ignore-scripts"
|
||||
- "yarn run reskindex"
|
||||
- "echo '+++ Running Tests'"
|
||||
- "yarn test"
|
||||
plugins:
|
||||
|
@ -48,10 +58,8 @@ steps:
|
|||
|
||||
- label: "🛠 Build"
|
||||
command:
|
||||
- "echo '--- Install js-sdk'"
|
||||
- "./scripts/ci/install-deps.sh"
|
||||
- "echo '+++ Building Project'"
|
||||
- "yarn build"
|
||||
- "echo '+++ Install & Build'"
|
||||
- "yarn install"
|
||||
plugins:
|
||||
- docker#v3.0.1:
|
||||
image: "node:12"
|
||||
|
@ -62,20 +70,19 @@ steps:
|
|||
# e2e tests otherwise take +-8min
|
||||
queue: "xlarge"
|
||||
command:
|
||||
# TODO: Remove hacky chmod for BuildKite
|
||||
- "echo '--- Setup'"
|
||||
- "chmod +x ./scripts/ci/*.sh"
|
||||
- "chmod +x ./scripts/*"
|
||||
- "echo '--- Install js-sdk'"
|
||||
- "./scripts/ci/install-deps.sh"
|
||||
- "echo '--- Running initial build steps'"
|
||||
- "yarn build"
|
||||
- "./scripts/ci/install-deps.sh --ignore-scripts"
|
||||
- "echo '+++ Running Tests'"
|
||||
- "./scripts/ci/end-to-end-tests.sh"
|
||||
plugins:
|
||||
- docker#v3.0.1:
|
||||
image: "matrixdotorg/riotweb-ci-e2etests-env:latest"
|
||||
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"
|
||||
agents:
|
||||
|
@ -83,32 +90,18 @@ steps:
|
|||
# webpack loves to gorge itself on resources.
|
||||
queue: "medium"
|
||||
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'"
|
||||
- "./scripts/ci/riot-unit-tests.sh"
|
||||
env:
|
||||
CHROME_BIN: "/usr/bin/google-chrome-stable"
|
||||
plugins:
|
||||
- docker#v3.0.1:
|
||||
image: "node:10"
|
||||
propagate-environment: true
|
||||
workdir: "/workdir/matrix-react-sdk"
|
||||
|
||||
- label: "🌐 i18n"
|
||||
command:
|
||||
- "echo '--- Fetching Dependencies'"
|
||||
- "yarn install"
|
||||
- "yarn install --ignore-scripts"
|
||||
- "echo '+++ Testing i18n output'"
|
||||
- "yarn diff-i18n"
|
||||
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)
|
||||
===================================================================================================
|
||||
[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",
|
||||
"version": "1.7.6",
|
||||
"version": "2.0.0",
|
||||
"description": "SDK for matrix.org using React",
|
||||
"author": "matrix.org",
|
||||
"repository": {
|
||||
|
@ -31,7 +31,7 @@
|
|||
"typings": "./lib/index.d.ts",
|
||||
"matrix_src_main": "./src/index.js",
|
||||
"scripts": {
|
||||
"prepublish": "yarn build",
|
||||
"prepare": "yarn build",
|
||||
"i18n": "matrix-gen-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",
|
||||
|
@ -54,6 +54,7 @@
|
|||
"test:e2e": "./test/end-to-end-tests/run.sh --riot-url http://localhost:8080"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.8.3",
|
||||
"blueimp-canvas-to-blob": "^3.5.0",
|
||||
"browser-encrypt-attachment": "^0.3.0",
|
||||
"browser-request": "^0.3.3",
|
||||
|
@ -79,7 +80,7 @@
|
|||
"is-ip": "^2.0.0",
|
||||
"linkifyjs": "^2.1.6",
|
||||
"lodash": "^4.17.14",
|
||||
"matrix-js-sdk": "3.0.0",
|
||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
|
||||
"pako": "^1.0.5",
|
||||
"png-chunks-extract": "^1.0.0",
|
||||
"prop-types": "^15.5.8",
|
||||
|
@ -108,13 +109,12 @@
|
|||
"@babel/plugin-proposal-numeric-separator": "^7.7.4",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^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-flow": "^7.7.4",
|
||||
"@babel/preset-react": "^7.7.4",
|
||||
"@babel/preset-typescript": "^7.7.4",
|
||||
"@babel/register": "^7.7.4",
|
||||
"@babel/runtime": "^7.7.6",
|
||||
"@peculiar/webcrypto": "^1.0.22",
|
||||
"babel-eslint": "^10.0.3",
|
||||
"babel-jest": "^24.9.0",
|
||||
|
|
|
@ -338,6 +338,14 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
|
|||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.mx_Dialog_titleImage {
|
||||
vertical-align: middle;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
margin-left: -2px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.mx_Dialog_title {
|
||||
font-size: 22px;
|
||||
line-height: 36px;
|
||||
|
@ -378,7 +386,13 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
|
|||
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;
|
||||
margin-left: 0px;
|
||||
margin-right: 8px;
|
||||
|
@ -394,27 +408,32 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
|
|||
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;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.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;
|
||||
background-color: $accent-color;
|
||||
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;
|
||||
border: solid 1px $warning-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;
|
||||
border: solid 1px $light-fg-color;
|
||||
opacity: 0.7;
|
||||
|
|
|
@ -36,6 +36,7 @@
|
|||
@import "./views/auth/_AuthHeader.scss";
|
||||
@import "./views/auth/_AuthHeaderLogo.scss";
|
||||
@import "./views/auth/_AuthPage.scss";
|
||||
@import "./views/auth/_CompleteSecurityBody.scss";
|
||||
@import "./views/auth/_CountryDropdown.scss";
|
||||
@import "./views/auth/_InteractiveAuthEntryComponents.scss";
|
||||
@import "./views/auth/_LanguageSelector.scss";
|
||||
|
@ -152,6 +153,7 @@
|
|||
@import "./views/rooms/_EditMessageComposer.scss";
|
||||
@import "./views/rooms/_EntityTile.scss";
|
||||
@import "./views/rooms/_EventTile.scss";
|
||||
@import "./views/rooms/_InviteOnlyIcon.scss";
|
||||
@import "./views/rooms/_JumpToBottomButton.scss";
|
||||
@import "./views/rooms/_LinkPreviewWidget.scss";
|
||||
@import "./views/rooms/_MemberDeviceInfo.scss";
|
||||
|
|
|
@ -63,7 +63,7 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.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 {
|
||||
|
|
|
@ -51,8 +51,8 @@ limitations under the License.
|
|||
&.mx_Toast_hasIcon {
|
||||
&::after {
|
||||
content: "";
|
||||
width: 21px;
|
||||
height: 20px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
mask-size: 100%;
|
||||
|
|
|
@ -22,7 +22,7 @@ limitations under the License.
|
|||
.mx_CompleteSecurity_headerIcon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin: 0 4px;
|
||||
margin-right: 4px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
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.
|
||||
|
@ -16,12 +17,12 @@ limitations under the License.
|
|||
|
||||
.mx_AuthBody {
|
||||
width: 500px;
|
||||
font-size: 12px;
|
||||
color: $authpage-secondary-color;
|
||||
background-color: $authpage-body-bg-color;
|
||||
border-radius: 0 4px 4px 0;
|
||||
padding: 25px 60px;
|
||||
box-sizing: border-box;
|
||||
font-size: 12px;
|
||||
color: $authpage-secondary-color;
|
||||
|
||||
h2 {
|
||||
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 {
|
||||
// Prevent the dialog from jumping around randomly when elements change.
|
||||
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 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -22,7 +22,7 @@ limitations under the License.
|
|||
|
||||
.mx_CreateSecretStorageDialog_primaryContainer {
|
||||
/* FIXME: plinth colour in new theme(s). background-color: $accent-color; */
|
||||
padding: 20px;
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.mx_CreateSecretStorageDialog_primaryContainer::after {
|
||||
|
@ -36,9 +36,13 @@ limitations under the License.
|
|||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.mx_Field.mx_CreateSecretStorageDialog_passPhraseField {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
.mx_CreateSecretStorageDialog_passPhraseHelp {
|
||||
flex: 1;
|
||||
height: 85px;
|
||||
height: 64px;
|
||||
margin-left: 20px;
|
||||
font-size: 80%;
|
||||
}
|
||||
|
@ -47,16 +51,8 @@ limitations under the License.
|
|||
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 {
|
||||
width: 200px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
|
@ -82,6 +78,10 @@ limitations under the License.
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
.mx_CreateSecretStorageDialog_recoveryKeyButtons .mx_AccessibleButton {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.mx_CreateSecretStorageDialog_recoveryKeyButtons button {
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
|
|
|
@ -23,15 +23,23 @@ limitations under the License.
|
|||
font-size: 12px;
|
||||
|
||||
.mx_UserInfo_cancel {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
padding: 10px 0 10px 10px;
|
||||
cursor: pointer;
|
||||
mask-image: url('$(res)/img/minimise.svg');
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: 16px center;
|
||||
background-color: $rightpanel-button-color;
|
||||
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;
|
||||
width: 16px;
|
||||
padding: 4px;
|
||||
mask-image: url('$(res)/img/minimise.svg');
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: 7px center;
|
||||
background-color: $rightpanel-button-color;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
|
@ -95,7 +103,7 @@ limitations under the License.
|
|||
justify-content: center;
|
||||
|
||||
// override the calculated sizes so that the letter isn't HUGE
|
||||
font-size: 26px !important;
|
||||
font-size: 56px !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
|
|
|
@ -367,6 +367,11 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody {
|
|||
opacity: 1;
|
||||
}
|
||||
|
||||
.mx_EventTile_e2eIcon_unknown {
|
||||
background-image: url('$(res)/img/e2e/warning.svg');
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.mx_EventTile_e2eIcon_unencrypted {
|
||||
background-image: url('$(res)/img/e2e/warning.svg');
|
||||
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_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;
|
||||
}
|
||||
|
||||
|
@ -427,8 +433,13 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody {
|
|||
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_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;
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
.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;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
// 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_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;
|
||||
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;
|
||||
margin-right: 0; // Counteract the E2EIcon class
|
||||
margin-left: 3px; // Counteract the E2EIcon class
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.mx_MessageComposer_noperm_error {
|
||||
|
|
|
@ -19,7 +19,12 @@ limitations under the License.
|
|||
border-bottom: 1px solid $primary-hairline-color;
|
||||
|
||||
.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;
|
||||
height: 28px;
|
||||
margin: 0 7px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.mx_RoomHeader_avatar .mx_BaseAvatar_image {
|
||||
|
@ -263,24 +269,3 @@ limitations under the License.
|
|||
.mx_RoomHeader_pinsIndicatorUnread {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -117,12 +117,17 @@ limitations under the License.
|
|||
.mx_RoomPreviewBar_actions {
|
||||
flex-direction: column-reverse;
|
||||
.mx_AccessibleButton {
|
||||
padding: 7px 50px;//extra wide
|
||||
padding: 7px 50px; //extra wide
|
||||
}
|
||||
|
||||
& > * {
|
||||
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;
|
||||
}
|
||||
|
||||
// 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 {
|
||||
font-size: 14px;
|
||||
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,
|
||||
// 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_RoomTile_menuButton {
|
||||
display: block;
|
||||
|
@ -201,30 +215,7 @@ limitations under the License.
|
|||
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
|
||||
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-verified-color: #76cfa5; // N.B. *NOT* the same as $accent-color
|
||||
$e2e-unknown-color: #e8bf37;
|
||||
$e2e-unverified-color: #e8bf37;
|
||||
$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
|
||||
|
||||
RIOT_WEB_DIR=riot-web
|
||||
REACT_SDK_DIR=`pwd`
|
||||
|
||||
|
||||
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
|
||||
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
|
||||
# CHROME_PATH=$(which google-chrome-stable) ./run.sh
|
||||
echo "--- Install synapse & other dependencies"
|
||||
|
|
|
@ -6,9 +6,9 @@ scripts/fetchdep.sh matrix-org matrix-js-sdk
|
|||
|
||||
pushd matrix-js-sdk
|
||||
yarn link
|
||||
yarn install
|
||||
yarn install $@
|
||||
yarn build
|
||||
popd
|
||||
|
||||
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
|
||||
|
||||
RIOT_WEB_DIR=riot-web
|
||||
|
||||
scripts/ci/build.sh
|
||||
pushd "$RIOT_WEB_DIR"
|
||||
scripts/ci/layered-riot-web.sh
|
||||
cd ../riot-web
|
||||
yarn build:genfiles # so the tests can run. Faster version of `build`
|
||||
yarn test
|
||||
popd
|
||||
|
|
|
@ -17,7 +17,7 @@ clone() {
|
|||
if [ -n "$branch" ]
|
||||
then
|
||||
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
|
||||
}
|
||||
|
||||
|
|
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 ToastStore from './stores/ToastStore';
|
||||
|
||||
function toastKey(device) {
|
||||
return 'newsession_' + device.deviceId;
|
||||
function toastKey(deviceId) {
|
||||
return 'newsession_' + deviceId;
|
||||
}
|
||||
|
||||
const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000;
|
||||
const THIS_DEVICE_TOAST_KEY = 'setupencryption';
|
||||
|
||||
export default class DeviceListener {
|
||||
static sharedInstance() {
|
||||
if (!global.mx_DeviceListener) global.mx_DeviceListener = new DeviceListener();
|
||||
|
@ -31,44 +34,120 @@ export default class DeviceListener {
|
|||
}
|
||||
|
||||
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')
|
||||
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() {
|
||||
MatrixClientPeg.get().on('crypto.devicesUpdated', this._onDevicesUpdated);
|
||||
MatrixClientPeg.get().on('deviceVerificationChanged', this._onDeviceVerificationChanged);
|
||||
this.recheck();
|
||||
MatrixClientPeg.get().on('userTrustStatusChanged', this._onUserTrustStatusChanged);
|
||||
this._recheck();
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (MatrixClientPeg.get()) {
|
||||
MatrixClientPeg.get().removeListener('crypto.devicesUpdated', this._onDevicesUpdated);
|
||||
MatrixClientPeg.get().removeListener('deviceVerificationChanged', this._onDeviceVerificationChanged);
|
||||
MatrixClientPeg.get().removeListener('userTrustStatusChanged', this._onUserTrustStatusChanged);
|
||||
}
|
||||
this._dismissed.clear();
|
||||
}
|
||||
|
||||
dismissVerification(deviceId) {
|
||||
this._dismissed.add(deviceId);
|
||||
this.recheck();
|
||||
this._recheck();
|
||||
}
|
||||
|
||||
dismissEncryptionSetup() {
|
||||
this._dismissedThisDeviceToast = true;
|
||||
this._recheck();
|
||||
}
|
||||
|
||||
_onDevicesUpdated = (users) => {
|
||||
if (!users.includes(MatrixClientPeg.get().getUserId())) return;
|
||||
this.recheck();
|
||||
this._recheck();
|
||||
}
|
||||
|
||||
_onDeviceVerificationChanged = (users) => {
|
||||
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;
|
||||
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());
|
||||
for (const device of devices) {
|
||||
|
@ -76,16 +155,24 @@ export default class DeviceListener {
|
|||
|
||||
const deviceTrust = await cli.checkDeviceTrust(cli.getUserId(), device.deviceId);
|
||||
if (deviceTrust.isCrossSigningVerified() || this._dismissed.has(device.deviceId)) {
|
||||
ToastStore.sharedInstance().dismissToast(toastKey(device));
|
||||
ToastStore.sharedInstance().dismissToast(toastKey(device.deviceId));
|
||||
} else {
|
||||
this._activeNagToasts.add(device.deviceId);
|
||||
ToastStore.sharedInstance().addOrReplaceToast({
|
||||
key: toastKey(device),
|
||||
key: toastKey(device.deviceId),
|
||||
title: _t("New Session"),
|
||||
icon: "verification_warning",
|
||||
props: {deviceId: device.deviceId},
|
||||
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();
|
||||
|
||||
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 MatrixClientPeg.start();
|
||||
} else {
|
||||
console.warn("Caller requested only auxiliary services be started");
|
||||
await MatrixClientPeg.assign();
|
||||
|
|
|
@ -91,7 +91,7 @@ export default class Markdown {
|
|||
return true;
|
||||
}
|
||||
|
||||
toHTML() {
|
||||
toHTML({ externalLinks = false } = {}) {
|
||||
const renderer = new commonmark.HtmlRenderer({
|
||||
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;
|
||||
|
||||
|
|
|
@ -217,7 +217,7 @@ class _MatrixClientPeg {
|
|||
timelineSupport: true,
|
||||
forceTURN: !SettingsStore.getValue('webRtcAllowPeerToPeer', false),
|
||||
fallbackICEServerAllowed: !!SettingsStore.getValue('fallbackICEServerAllowed'),
|
||||
verificationMethods: [verificationMethods.SAS],
|
||||
verificationMethods: [verificationMethods.SAS, verificationMethods.QR_CODE_SHOW],
|
||||
unstableClientRelationAggregation: true,
|
||||
identityServer: new IdentityAuthClient(),
|
||||
};
|
||||
|
|
77
src/Modal.js
77
src/Modal.js
|
@ -17,87 +17,14 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import Analytics from './Analytics';
|
||||
import * as sdk from './index';
|
||||
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 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 {
|
||||
constructor() {
|
||||
this._counter = 0;
|
||||
|
|
|
@ -20,13 +20,8 @@ import React from 'react';
|
|||
import {MatrixClientPeg} from './MatrixClientPeg';
|
||||
import MultiInviter from './utils/MultiInviter';
|
||||
import Modal from './Modal';
|
||||
import { getAddressType } from './UserAddress';
|
||||
import createRoom from './createRoom';
|
||||
import * as sdk from './';
|
||||
import dis from './dispatcher';
|
||||
import DMRoomMap from './utils/DMRoomMap';
|
||||
import { _t } from './languageHandler';
|
||||
import SettingsStore from "./settings/SettingsStore";
|
||||
import {KIND_DM, KIND_INVITE} from "./components/views/dialogs/InviteDialog";
|
||||
|
||||
/**
|
||||
|
@ -44,64 +39,21 @@ export function inviteMultipleToRoom(roomId, addrs) {
|
|||
}
|
||||
|
||||
export function showStartChatInviteDialog() {
|
||||
if (SettingsStore.isFeatureEnabled("feature_ftue_dms")) {
|
||||
// This new dialog handles the room creation internally - we don't need to worry about it.
|
||||
const InviteDialog = sdk.getComponent("dialogs.InviteDialog");
|
||||
Modal.createTrackedDialog(
|
||||
'Start DM', '', InviteDialog, {kind: KIND_DM},
|
||||
/*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);
|
||||
// This dialog handles the room creation internally - we don't need to worry about it.
|
||||
const InviteDialog = sdk.getComponent("dialogs.InviteDialog");
|
||||
Modal.createTrackedDialog(
|
||||
'Start DM', '', InviteDialog, {kind: KIND_DM},
|
||||
/*className=*/null, /*isPriority=*/false, /*isStatic=*/true,
|
||||
);
|
||||
}
|
||||
|
||||
export function showRoomInviteDialog(roomId) {
|
||||
if (SettingsStore.isFeatureEnabled("feature_ftue_dms")) {
|
||||
// This new dialog handles the room creation internally - we don't need to worry about it.
|
||||
const InviteDialog = sdk.getComponent("dialogs.InviteDialog");
|
||||
Modal.createTrackedDialog(
|
||||
'Invite Users', '', InviteDialog, {kind: KIND_INVITE, roomId},
|
||||
/*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);
|
||||
// This dialog handles the room creation internally - we don't need to worry about it.
|
||||
const InviteDialog = sdk.getComponent("dialogs.InviteDialog");
|
||||
Modal.createTrackedDialog(
|
||||
'Invite Users', '', InviteDialog, {kind: KIND_INVITE, roomId},
|
||||
/*className=*/null, /*isPriority=*/false, /*isStatic=*/true,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -122,60 +74,6 @@ export function isValid3pidInvite(event) {
|
|||
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) {
|
||||
return inviteMultipleToRoom(roomId, userIds).then((result) => {
|
||||
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) {
|
||||
// Show user any errors
|
||||
const failedUsers = Object.keys(addrs).filter(a => addrs[a] === 'error');
|
||||
|
@ -243,15 +123,3 @@ function _showAnyInviteErrors(addrs, room, inviter) {
|
|||
|
||||
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) {
|
||||
// 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);
|
||||
}
|
||||
|
||||
|
@ -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} 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.
|
||||
* 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
|
||||
// IRC-style commands
|
||||
input = input.replace(/\s+$/, '');
|
||||
if (input[0] !== '/') return null; // not a command
|
||||
|
||||
const bits = input.match(/^(\S+?)( +((.|\n)*))?$/);
|
||||
const bits = input.match(/^(\S+?)(?: +((.|\n)*))?$/);
|
||||
let cmd;
|
||||
let args;
|
||||
if (bits) {
|
||||
cmd = bits[1].substring(1).toLowerCase();
|
||||
args = bits[3];
|
||||
args = bits[2];
|
||||
} else {
|
||||
cmd = input;
|
||||
}
|
||||
|
@ -932,11 +934,6 @@ export function processCommandInput(roomId, input) {
|
|||
cmd = aliases[cmd];
|
||||
}
|
||||
if (CommandMap[cmd]) {
|
||||
// if it has no runFn then its an ignored/nop command (autocomplete only) e.g `/me`
|
||||
if (!CommandMap[cmd].runFn) return null;
|
||||
|
||||
return CommandMap[cmd].run(roomId, args);
|
||||
} else {
|
||||
return reject(_t('Unrecognised command:') + ' ' + input);
|
||||
return () => CommandMap[cmd].run(roomId, args);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -275,6 +275,8 @@ function textForRoomAliasesEvent(ev) {
|
|||
// 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.
|
||||
|
||||
const maxShown = 3;
|
||||
|
||||
const senderName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||
const oldAliases = ev.getPrevContent().aliases || [];
|
||||
const newAliases = ev.getContent().aliases || [];
|
||||
|
@ -287,18 +289,40 @@ function textForRoomAliasesEvent(ev) {
|
|||
}
|
||||
|
||||
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.', {
|
||||
senderName: senderName,
|
||||
count: addedAliases.length,
|
||||
addedAddresses: addedAliases.join(', '),
|
||||
});
|
||||
} 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.', {
|
||||
senderName: senderName,
|
||||
count: removedAliases.length,
|
||||
removedAddresses: removedAliases.join(', '),
|
||||
});
|
||||
} 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(
|
||||
'%(senderName)s added %(addedAddresses)s and removed %(removedAddresses)s as addresses for this room.', {
|
||||
senderName: senderName,
|
||||
|
@ -420,10 +444,19 @@ function textForHistoryVisibilityEvent(event) {
|
|||
|
||||
function textForEncryptionEvent(event) {
|
||||
const senderName = event.sender ? event.sender.name : event.getSender();
|
||||
return _t('%(senderName)s turned on end-to-end encryption (algorithm %(algorithm)s).', {
|
||||
senderName,
|
||||
algorithm: event.getContent().algorithm,
|
||||
});
|
||||
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,
|
||||
algorithm: event.getContent().algorithm,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// 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 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -16,6 +16,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as sdk from '../../../../index';
|
||||
import {MatrixClientPeg} from '../../../../MatrixClientPeg';
|
||||
import { scorePassword } from '../../../../utils/PasswordScorer';
|
||||
|
@ -52,6 +53,15 @@ function selectText(target) {
|
|||
* Secret Storage in account data.
|
||||
*/
|
||||
export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||
static propTypes = {
|
||||
hasCancel: PropTypes.bool,
|
||||
accountPassword: PropTypes.string,
|
||||
};
|
||||
|
||||
defaultProps = {
|
||||
hasCancel: true,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
|
@ -70,12 +80,24 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
setPassPhrase: false,
|
||||
backupInfo: 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._queryKeyUploadAuth();
|
||||
|
||||
MatrixClientPeg.get().on('crypto.keyBackupStatus', this._onKeyBackupStatusChange);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
MatrixClientPeg.get().removeListener('crypto.keyBackupStatus', this._onKeyBackupStatusChange);
|
||||
if (this._setZxcvbnResultTimeout !== null) {
|
||||
clearTimeout(this._setZxcvbnResultTimeout);
|
||||
}
|
||||
|
@ -83,7 +105,10 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
|
||||
async _fetchBackupInfo() {
|
||||
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 ?
|
||||
(backupSigStatus.usable ? PHASE_MIGRATE : PHASE_RESTORE_KEY_BACKUP) :
|
||||
|
@ -93,14 +118,41 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
phase,
|
||||
backupInfo,
|
||||
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) => {
|
||||
this._recoveryKeyNode = n;
|
||||
}
|
||||
|
||||
_onMigrateNextClick = () => {
|
||||
_onMigrateFormSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
this._bootstrapSecretStorage();
|
||||
}
|
||||
|
||||
|
@ -127,29 +179,46 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
});
|
||||
}
|
||||
|
||||
_doBootstrapUIAuth = async (makeRequest) => {
|
||||
if (this.state.canUploadKeysWithPasswordOnly && this.state.accountPassword) {
|
||||
await makeRequest({
|
||||
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,
|
||||
});
|
||||
} else {
|
||||
const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog");
|
||||
const { finished } = Modal.createTrackedDialog(
|
||||
'Cross-signing keys dialog', '', InteractiveAuthDialog,
|
||||
{
|
||||
title: _t("Send cross-signing keys to homeserver"),
|
||||
matrixClient: MatrixClientPeg.get(),
|
||||
makeRequest,
|
||||
},
|
||||
);
|
||||
const [confirmed] = await finished;
|
||||
if (!confirmed) {
|
||||
throw new Error("Cross-signing key upload auth canceled");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_bootstrapSecretStorage = async () => {
|
||||
this.setState({
|
||||
phase: PHASE_STORING,
|
||||
error: null,
|
||||
});
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
||||
try {
|
||||
const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog");
|
||||
await cli.bootstrapSecretStorage({
|
||||
authUploadDeviceSigningKeys: async (makeRequest) => {
|
||||
const { finished } = Modal.createTrackedDialog(
|
||||
'Cross-signing keys dialog', '', InteractiveAuthDialog,
|
||||
{
|
||||
title: _t("Send cross-signing keys to homeserver"),
|
||||
matrixClient: MatrixClientPeg.get(),
|
||||
makeRequest,
|
||||
},
|
||||
);
|
||||
const [confirmed] = await finished;
|
||||
if (!confirmed) {
|
||||
throw new Error("Cross-signing key upload auth canceled");
|
||||
}
|
||||
},
|
||||
authUploadDeviceSigningKeys: this._doBootstrapUIAuth,
|
||||
createSecretStorageKey: async () => this._keyInfo,
|
||||
keyBackupInfo: this.state.backupInfo,
|
||||
});
|
||||
|
@ -157,7 +226,14 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
phase: PHASE_DONE,
|
||||
});
|
||||
} catch (e) {
|
||||
this.setState({ error: e });
|
||||
if (this.state.canUploadKeysWithPasswordOnly && e.httpStatus === 401 && e.data.flows) {
|
||||
this.setState({
|
||||
accountPasswordCorrect: false,
|
||||
phase: PHASE_MIGRATE,
|
||||
});
|
||||
} else {
|
||||
this.setState({ error: e });
|
||||
}
|
||||
console.error("Error bootstrapping secret storage", e);
|
||||
}
|
||||
}
|
||||
|
@ -173,7 +249,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
_onRestoreKeyBackupClick = () => {
|
||||
const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog');
|
||||
Modal.createTrackedDialog(
|
||||
'Restore Backup', '', RestoreKeyBackupDialog, null, null,
|
||||
'Restore Backup', '', RestoreKeyBackupDialog, {showSummary: false}, null,
|
||||
/* 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;
|
||||
}
|
||||
|
||||
_onAccountPasswordChange = (e) => {
|
||||
this.setState({
|
||||
accountPassword: e.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
_renderPhaseRestoreKeyBackup() {
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
return <div>
|
||||
|
@ -309,22 +391,47 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
// it automatically.
|
||||
// https://github.com/vector-im/riot-web/issues/11696
|
||||
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(
|
||||
"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.",
|
||||
"Upgrade this device to allow it to verify other devices, " +
|
||||
"granting them access to encrypted messages and marking them " +
|
||||
"as trusted for other users.",
|
||||
)}</p>
|
||||
<div>{authPrompt}</div>
|
||||
<DialogButtons primaryButton={_t('Next')}
|
||||
onPrimaryButtonClick={this._onMigrateNextClick}
|
||||
primaryIsSubmit={true}
|
||||
hasCancel={true}
|
||||
onCancel={this._onCancel}
|
||||
primaryDisabled={this.state.canUploadKeysWithPasswordOnly && !this.state.accountPassword}
|
||||
/>
|
||||
</div>;
|
||||
</form>;
|
||||
}
|
||||
|
||||
_renderPhasePassPhrase() {
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
const Field = sdk.getComponent('views.elements.Field');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
let strengthMeter;
|
||||
let helpText;
|
||||
|
@ -350,51 +457,52 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
|
||||
return <div>
|
||||
<p>{_t(
|
||||
"<b>Warning</b>: You should only set up secret storage from a trusted computer.", {},
|
||||
{ b: sub => <b>{sub}</b> },
|
||||
"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.",
|
||||
)}</p>
|
||||
<p>{_t(
|
||||
"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:",
|
||||
)}</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">
|
||||
<input type="password"
|
||||
onChange={this._onPassPhraseChange}
|
||||
onKeyPress={this._onPassPhraseKeyPress}
|
||||
value={this.state.passPhrase}
|
||||
className="mx_CreateSecretStorageDialog_passPhraseInput"
|
||||
placeholder={_t("Enter a passphrase...")}
|
||||
autoFocus={true}
|
||||
/>
|
||||
<div className="mx_CreateSecretStorageDialog_passPhraseHelp">
|
||||
{strengthMeter}
|
||||
{helpText}
|
||||
</div>
|
||||
<div className="mx_CreateSecretStorageDialog_passPhraseContainer">
|
||||
<Field type="password"
|
||||
className="mx_CreateSecretStorageDialog_passPhraseField"
|
||||
onChange={this._onPassPhraseChange}
|
||||
onKeyPress={this._onPassPhraseKeyPress}
|
||||
value={this.state.passPhrase}
|
||||
label={_t("Enter a passphrase")}
|
||||
autoFocus={true}
|
||||
/>
|
||||
<div className="mx_CreateSecretStorageDialog_passPhraseHelp">
|
||||
{strengthMeter}
|
||||
{helpText}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogButtons primaryButton={_t('Next')}
|
||||
<DialogButtons primaryButton={_t('Continue')}
|
||||
onPrimaryButtonClick={this._onPassPhraseNextClick}
|
||||
hasCancel={false}
|
||||
disabled={!this._passPhraseIsValid()}
|
||||
/>
|
||||
>
|
||||
<button type="button"
|
||||
onClick={this._onCancel}
|
||||
className="danger"
|
||||
>{_t("Skip")}</button>
|
||||
</DialogButtons>
|
||||
|
||||
<details>
|
||||
<summary>{_t("Advanced")}</summary>
|
||||
<p><button onClick={this._onSkipPassPhraseClick} >
|
||||
<p><AccessibleButton kind='primary' onClick={this._onSkipPassPhraseClick} >
|
||||
{_t("Set up with a recovery key")}
|
||||
</button></p>
|
||||
</AccessibleButton></p>
|
||||
</details>
|
||||
</div>;
|
||||
}
|
||||
|
||||
_renderPhasePassPhraseConfirm() {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const Field = sdk.getComponent('views.elements.Field');
|
||||
|
||||
let matchText;
|
||||
if (this.state.passPhraseConfirm === this.state.passPhrase) {
|
||||
|
@ -412,7 +520,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
|
||||
let passPhraseMatch = null;
|
||||
if (matchText) {
|
||||
passPhraseMatch = <div className="mx_CreateSecretStorageDialog_passPhraseMatch">
|
||||
passPhraseMatch = <div>
|
||||
<div>{matchText}</div>
|
||||
<div>
|
||||
<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');
|
||||
return <div>
|
||||
<p>{_t(
|
||||
"Please enter your passphrase a second time to confirm.",
|
||||
"Enter your passphrase a second time to confirm it.",
|
||||
)}</p>
|
||||
<div className="mx_CreateSecretStorageDialog_primaryContainer">
|
||||
<div className="mx_CreateSecretStorageDialog_passPhraseContainer">
|
||||
<div>
|
||||
<input type="password"
|
||||
onChange={this._onPassPhraseConfirmChange}
|
||||
onKeyPress={this._onPassPhraseConfirmKeyPress}
|
||||
value={this.state.passPhraseConfirm}
|
||||
className="mx_CreateSecretStorageDialog_passPhraseInput"
|
||||
placeholder={_t("Repeat your passphrase...")}
|
||||
autoFocus={true}
|
||||
/>
|
||||
</div>
|
||||
<div className="mx_CreateSecretStorageDialog_passPhraseContainer">
|
||||
<Field type="password"
|
||||
id="mx_CreateSecretStorageDialog_passPhraseField"
|
||||
onChange={this._onPassPhraseConfirmChange}
|
||||
onKeyPress={this._onPassPhraseConfirmKeyPress}
|
||||
value={this.state.passPhraseConfirm}
|
||||
className="mx_CreateSecretStorageDialog_passPhraseField"
|
||||
label={_t("Confirm your passphrase")}
|
||||
autoFocus={true}
|
||||
/>
|
||||
<div className="mx_CreateSecretStorageDialog_passPhraseMatch">
|
||||
{passPhraseMatch}
|
||||
</div>
|
||||
</div>
|
||||
<DialogButtons primaryButton={_t('Next')}
|
||||
<DialogButtons primaryButton={_t('Continue')}
|
||||
onPrimaryButtonClick={this._onPassPhraseConfirmNextClick}
|
||||
hasCancel={false}
|
||||
disabled={this.state.passPhrase !== this.state.passPhraseConfirm}
|
||||
/>
|
||||
>
|
||||
<button type="button"
|
||||
onClick={this._onCancel}
|
||||
className="danger"
|
||||
>{_t("Skip")}</button>
|
||||
</DialogButtons>
|
||||
</div>;
|
||||
}
|
||||
|
||||
|
@ -463,6 +575,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
);
|
||||
}
|
||||
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
return <div>
|
||||
<p>{_t(
|
||||
"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>
|
||||
</div>
|
||||
<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")}
|
||||
</button>
|
||||
<button className="mx_Dialog_primary" onClick={this._onDownloadClick}>
|
||||
</AccessibleButton>
|
||||
<AccessibleButton kind='primary' className="mx_Dialog_primary" onClick={this._onDownloadClick}>
|
||||
{_t("Download")}
|
||||
</button>
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -533,7 +646,11 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
return <div>
|
||||
<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>
|
||||
<DialogButtons primaryButton={_t('OK')}
|
||||
onPrimaryButtonClick={this._onDone}
|
||||
|
@ -564,11 +681,11 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
case PHASE_RESTORE_KEY_BACKUP:
|
||||
return _t('Restore your Key Backup');
|
||||
case PHASE_MIGRATE:
|
||||
return _t('Migrate from Key Backup');
|
||||
return _t('Upgrade your encryption');
|
||||
case PHASE_PASSPHRASE:
|
||||
return _t('Secure your encrypted messages with a passphrase');
|
||||
return _t('Set up encryption');
|
||||
case PHASE_PASSPHRASE_CONFIRM:
|
||||
return _t('Confirm your passphrase');
|
||||
return _t('Confirm passphrase');
|
||||
case PHASE_OPTOUT_CONFIRM:
|
||||
return _t('Warning!');
|
||||
case PHASE_SHOWKEY:
|
||||
|
@ -578,9 +695,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
case PHASE_STORING:
|
||||
return _t('Storing secrets...');
|
||||
case PHASE_DONE:
|
||||
return _t('Success!');
|
||||
return this.state.doingUpgrade ? _t('Encryption upgraded') : _t('Encryption setup complete');
|
||||
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 (
|
||||
<BaseDialog className='mx_CreateSecretStorageDialog'
|
||||
onFinished={this.props.onFinished}
|
||||
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>
|
||||
{content}
|
||||
|
|
|
@ -19,9 +19,10 @@ import React from 'react';
|
|||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import Matrix from 'matrix-js-sdk';
|
||||
import {Filter} from 'matrix-js-sdk';
|
||||
import * as sdk from '../../index';
|
||||
import {MatrixClientPeg} from '../../MatrixClientPeg';
|
||||
import EventIndexPeg from "../../indexing/EventIndexPeg";
|
||||
import { _t } from '../../languageHandler';
|
||||
|
||||
/*
|
||||
|
@ -29,6 +30,9 @@ import { _t } from '../../languageHandler';
|
|||
*/
|
||||
const FilePanel = createReactClass({
|
||||
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: {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
|
@ -40,42 +44,147 @@ const FilePanel = createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.updateTimelineSet(this.props.roomId);
|
||||
onRoomTimeline(ev, room, toStartOfTimeline, removed, data) {
|
||||
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();
|
||||
|
||||
await this.updateTimelineSet(this.props.roomId);
|
||||
|
||||
if (!MatrixClientPeg.get().isRoomEncrypted(this.props.roomId)) return;
|
||||
|
||||
// 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(
|
||||
{
|
||||
"room": {
|
||||
"timeline": {
|
||||
"contains_url": true,
|
||||
"types": [
|
||||
"m.room.message",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const filterId = await client.getOrCreateFilter("FILTER_FILES_" + client.credentials.userId, filter);
|
||||
filter.filterId = filterId;
|
||||
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) {
|
||||
const filter = new Matrix.Filter(client.credentials.userId);
|
||||
filter.setDefinition(
|
||||
{
|
||||
"room": {
|
||||
"timeline": {
|
||||
"contains_url": true,
|
||||
"types": [
|
||||
"m.room.message",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
let timelineSet;
|
||||
|
||||
// FIXME: we shouldn't be doing this every time we change room - see comment above.
|
||||
client.getOrCreateFilter("FILTER_FILES_" + client.credentials.userId, filter).then(
|
||||
(filterId)=>{
|
||||
filter.filterId = filterId;
|
||||
const timelineSet = room.getOrCreateFilteredTimelineSet(filter);
|
||||
this.setState({ timelineSet: timelineSet });
|
||||
},
|
||||
(error)=>{
|
||||
console.error("Failed to get or create file panel filter", error);
|
||||
},
|
||||
);
|
||||
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 });
|
||||
} catch (error) {
|
||||
console.error("Failed to get or create file panel filter", error);
|
||||
}
|
||||
} else {
|
||||
console.error("Failed to add filtered timelineSet for FilePanel as no room!");
|
||||
}
|
||||
|
@ -111,6 +220,7 @@ const FilePanel = createReactClass({
|
|||
manageReadMarkers={false}
|
||||
timelineSet={this.state.timelineSet}
|
||||
showUrlPreview = {false}
|
||||
onPaginationRequest={this.onPaginationRequest}
|
||||
tileShape="file_grid"
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
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 PropTypes from 'prop-types';
|
||||
|
||||
import {getEntryComponentForLoginType} from '../views/auth/InteractiveAuthEntryComponents';
|
||||
import getEntryComponentForLoginType from '../views/auth/InteractiveAuthEntryComponents';
|
||||
|
||||
import * as sdk from '../../index';
|
||||
|
||||
|
|
|
@ -129,9 +129,6 @@ const LeftPanel = createReactClass({
|
|||
if (!this.focusedElement) return;
|
||||
|
||||
switch (ev.key) {
|
||||
case Key.TAB:
|
||||
this._onMoveFocus(ev, ev.shiftKey);
|
||||
break;
|
||||
case Key.ARROW_UP:
|
||||
this._onMoveFocus(ev, true, true);
|
||||
break;
|
||||
|
|
|
@ -89,12 +89,15 @@ export const VIEWS = {
|
|||
// showing flow to trust this new device with cross-signing
|
||||
COMPLETE_SECURITY: 6,
|
||||
|
||||
// flow to setup SSSS / cross-signing on this account
|
||||
E2E_SETUP: 7,
|
||||
|
||||
// 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
|
||||
// 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
|
||||
|
@ -253,6 +256,9 @@ export default createReactClass({
|
|||
// logout page.
|
||||
Lifecycle.loadSession({});
|
||||
}
|
||||
|
||||
this._accountPassword = null;
|
||||
this._accountPasswordTimer = null;
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
|
@ -349,6 +355,8 @@ export default createReactClass({
|
|||
window.removeEventListener("focus", this.onFocus);
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
this.state.resizeNotifier.removeListener("middlePanelResized", this._dispatchTimelineResize);
|
||||
|
||||
if (this._accountPasswordTimer !== null) clearTimeout(this._accountPasswordTimer);
|
||||
},
|
||||
|
||||
componentWillUpdate: function(props, state) {
|
||||
|
@ -657,7 +665,9 @@ export default createReactClass({
|
|||
if (
|
||||
!Lifecycle.isSoftLogout() &&
|
||||
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();
|
||||
}
|
||||
|
@ -961,9 +971,9 @@ export default createReactClass({
|
|||
const CreateRoomDialog = sdk.getComponent('dialogs.CreateRoomDialog');
|
||||
const modal = Modal.createTrackedDialog('Create Room', '', CreateRoomDialog);
|
||||
|
||||
const [shouldCreate, createOpts] = await modal.finished;
|
||||
const [shouldCreate, opts] = await modal.finished;
|
||||
if (shouldCreate) {
|
||||
createRoom({createOpts});
|
||||
createRoom(opts);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -1453,7 +1463,6 @@ export default createReactClass({
|
|||
|
||||
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
|
||||
cli.on("crypto.verification.request", request => {
|
||||
console.log(`MatrixChat got a .request ${request.channel.transactionId}`, request.event.getRoomId());
|
||||
if (request.pending) {
|
||||
ToastStore.sharedInstance().addOrReplaceToast({
|
||||
key: 'verifreq_' + request.channel.transactionId,
|
||||
|
@ -1725,6 +1734,10 @@ export default createReactClass({
|
|||
this.showScreen("forgot_password");
|
||||
},
|
||||
|
||||
onRegisterFlowComplete: function(credentials, password) {
|
||||
return this.onUserCompletedLoginFlow(credentials, password);
|
||||
},
|
||||
|
||||
// returns a promise which resolves to the new MatrixClient
|
||||
onRegistered: function(credentials) {
|
||||
return Lifecycle.setLoggedIn(credentials);
|
||||
|
@ -1813,7 +1826,14 @@ export default createReactClass({
|
|||
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)
|
||||
// which is enough to ask the server about account data.
|
||||
const loggedIn = new Promise(resolve => {
|
||||
|
@ -1827,7 +1847,7 @@ export default createReactClass({
|
|||
});
|
||||
|
||||
// Create and start the client in the background
|
||||
Lifecycle.setLoggedIn(credentials);
|
||||
const setLoggedInPromise = Lifecycle.setLoggedIn(credentials);
|
||||
await loggedIn;
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
@ -1848,12 +1868,20 @@ export default createReactClass({
|
|||
|
||||
if (masterKeyInStorage) {
|
||||
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 {
|
||||
this._onLoggedIn();
|
||||
}
|
||||
|
||||
return setLoggedInPromise;
|
||||
},
|
||||
|
||||
onCompleteSecurityFinished() {
|
||||
// complete security / e2e setup has finished
|
||||
onCompleteSecurityE2eSetupFinished() {
|
||||
this._onLoggedIn();
|
||||
},
|
||||
|
||||
|
@ -1873,7 +1901,15 @@ export default createReactClass({
|
|||
const CompleteSecurity = sdk.getComponent('structures.auth.CompleteSecurity');
|
||||
view = (
|
||||
<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) {
|
||||
|
@ -1940,7 +1976,7 @@ export default createReactClass({
|
|||
email={this.props.startingFragmentQueryParams.email}
|
||||
brand={this.props.config.brand}
|
||||
makeRegistrationUrl={this._makeRegistrationUrl}
|
||||
onLoggedIn={this.onRegistered}
|
||||
onLoggedIn={this.onRegisterFlowComplete}
|
||||
onLoginClick={this.onLoginClick}
|
||||
onServerConfigChange={this.onServerConfigChange}
|
||||
{...this.getServerProperties()}
|
||||
|
|
|
@ -31,6 +31,7 @@ import PropTypes from 'prop-types';
|
|||
import RoomTile from "../views/rooms/RoomTile";
|
||||
import LazyRenderList from "../views/elements/LazyRenderList";
|
||||
import {_t} from "../../languageHandler";
|
||||
import {RovingTabIndexWrapper} from "../../accessibility/RovingTabIndex";
|
||||
|
||||
// turn this on for drop & drag console debugging galore
|
||||
const debug = false;
|
||||
|
@ -141,10 +142,6 @@ export default class RoomSubList extends React.PureComponent {
|
|||
|
||||
onHeaderKeyDown = (ev) => {
|
||||
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:
|
||||
// On ARROW_LEFT collapse the room sublist
|
||||
if (!this.state.hidden && !this.props.forceExpand) {
|
||||
|
@ -263,33 +260,6 @@ export default class RoomSubList extends React.PureComponent {
|
|||
const subListNotifCount = subListNotifications.count;
|
||||
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
|
||||
// the full tag name and room count
|
||||
let title;
|
||||
|
@ -305,17 +275,6 @@ export default class RoomSubList extends React.PureComponent {
|
|||
<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;
|
||||
let chevron;
|
||||
if (len) {
|
||||
|
@ -327,25 +286,81 @@ export default class RoomSubList extends React.PureComponent {
|
|||
chevron = (<div className={chevronClasses} />);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_RoomSubList_labelContainer" title={title} ref={this._header} onKeyDown={this.onHeaderKeyDown}>
|
||||
<AccessibleButton
|
||||
onClick={this.onClick}
|
||||
className="mx_RoomSubList_label"
|
||||
tabIndex={0}
|
||||
aria-expanded={!isCollapsed}
|
||||
inputRef={this._headerButton}
|
||||
role="treeitem"
|
||||
aria-level="1"
|
||||
>
|
||||
{ chevron }
|
||||
<span>{this.props.label}</span>
|
||||
{ incomingCall }
|
||||
</AccessibleButton>
|
||||
{ badge }
|
||||
{ addRoomButton }
|
||||
</div>
|
||||
);
|
||||
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 (
|
||||
<div className="mx_RoomSubList_labelContainer" title={title} ref={this._header} onKeyDown={this.onHeaderKeyDown}>
|
||||
<AccessibleButton
|
||||
onFocus={onFocus}
|
||||
tabIndex={tabIndex}
|
||||
inputRef={ref}
|
||||
onClick={this.onClick}
|
||||
className="mx_RoomSubList_label"
|
||||
aria-expanded={!isCollapsed}
|
||||
role="treeitem"
|
||||
aria-level="1"
|
||||
>
|
||||
{ chevron }
|
||||
<span>{this.props.label}</span>
|
||||
{ incomingCall }
|
||||
</AccessibleButton>
|
||||
{ badge }
|
||||
{ addRoomButton }
|
||||
</div>
|
||||
);
|
||||
} }
|
||||
</RovingTabIndexWrapper>;
|
||||
}
|
||||
|
||||
checkOverflow = () => {
|
||||
|
|
|
@ -766,7 +766,7 @@ export default createReactClass({
|
|||
|
||||
onUserVerificationChanged: function(userId, _trustStatus) {
|
||||
const room = this.state.room;
|
||||
if (!room.currentState.getMember(userId)) {
|
||||
if (!room || !room.currentState.getMember(userId)) {
|
||||
return;
|
||||
}
|
||||
this._updateE2EStatus(room);
|
||||
|
@ -796,6 +796,7 @@ export default createReactClass({
|
|||
return;
|
||||
}
|
||||
|
||||
// Duplication between here and _updateE2eStatus in RoomTile
|
||||
/* At this point, the user has encryption on and cross-signing on */
|
||||
const e2eMembers = await room.getEncryptionTargetMembers();
|
||||
const verified = [];
|
||||
|
@ -810,12 +811,12 @@ export default createReactClass({
|
|||
debuglog("e2e verified", verified, "unverified", unverified);
|
||||
|
||||
/* Check all verified user devices. */
|
||||
for (const userId of verified) {
|
||||
for (const userId of [...verified, cli.getUserId()]) {
|
||||
const devices = await cli.getStoredDevicesForUser(userId);
|
||||
const allDevicesVerified = devices.every(({deviceId}) => {
|
||||
return cli.checkDeviceTrust(userId, deviceId).isVerified();
|
||||
const anyDeviceNotVerified = devices.some(({deviceId}) => {
|
||||
return !cli.checkDeviceTrust(userId, deviceId).isVerified();
|
||||
});
|
||||
if (!allDevicesVerified) {
|
||||
if (anyDeviceNotVerified) {
|
||||
this.setState({
|
||||
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) {
|
||||
// We can reject 3pid invites in the same way that we accept them,
|
||||
// using /leave rather than /join. In the short term though, we
|
||||
|
@ -1671,9 +1707,11 @@ export default createReactClass({
|
|||
return (
|
||||
<div className="mx_RoomView">
|
||||
<ErrorBoundary>
|
||||
<RoomPreviewBar onJoinClick={this.onJoinButtonClicked}
|
||||
<RoomPreviewBar
|
||||
onJoinClick={this.onJoinButtonClicked}
|
||||
onForgetClick={this.onForgetClick}
|
||||
onRejectClick={this.onRejectButtonClicked}
|
||||
onRejectAndIgnoreClick={this.onRejectAndIgnoreClick}
|
||||
inviterName={inviterName}
|
||||
canPreview={false}
|
||||
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
|
||||
// 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.
|
||||
|
||||
// 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}
|
||||
onScroll={this.onScroll}
|
||||
className={`mx_ScrollPanel ${this.props.className}`} style={this.props.style}>
|
||||
<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 }
|
||||
</ol>
|
||||
</div>
|
||||
|
|
|
@ -133,9 +133,11 @@ export default createReactClass({
|
|||
return null;
|
||||
}
|
||||
const clearButton = (!this.state.blurred || this.state.searchTerm) ?
|
||||
(<AccessibleButton key="button"
|
||||
className="mx_SearchBox_closeButton"
|
||||
onClick={ () => {this._clearSearch("button"); } }>
|
||||
(<AccessibleButton
|
||||
key="button"
|
||||
tabIndex={-1}
|
||||
className="mx_SearchBox_closeButton"
|
||||
onClick={ () => {this._clearSearch("button"); } }>
|
||||
</AccessibleButton>) : undefined;
|
||||
|
||||
// show a shorter placeholder when blurred, if requested
|
||||
|
|
|
@ -94,6 +94,10 @@ const TimelinePanel = createReactClass({
|
|||
// callback which is called when the read-up-to mark is updated.
|
||||
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
|
||||
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.
|
||||
onMessageListFillRequest: function(backwards) {
|
||||
if (!this._shouldPaginate()) return Promise.resolve(false);
|
||||
|
@ -360,7 +372,7 @@ const TimelinePanel = createReactClass({
|
|||
debuglog("TimelinePanel: Initiating paginate; backwards:"+backwards);
|
||||
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; }
|
||||
|
||||
debuglog("TimelinePanel: paginate complete backwards:"+backwards+"; success:"+r);
|
||||
|
|
|
@ -23,9 +23,11 @@ export default class ToastContainer extends React.Component {
|
|||
constructor() {
|
||||
super();
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
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 {MatrixClientPeg} from '../../MatrixClientPeg';
|
||||
import * as Avatar from '../../Avatar';
|
||||
|
|
|
@ -35,7 +35,21 @@ export default class CompleteSecurity extends React.Component {
|
|||
|
||||
this.state = {
|
||||
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 () => {
|
||||
|
@ -44,14 +58,38 @@ export default class CompleteSecurity extends React.Component {
|
|||
await accessSecretStorage(async () => {
|
||||
await cli.checkOwnCrossSigningTrust();
|
||||
});
|
||||
this.setState({
|
||||
phase: PHASE_DONE,
|
||||
});
|
||||
|
||||
if (cli.getCrossSigningId()) {
|
||||
this.setState({
|
||||
phase: PHASE_DONE,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// 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 = () => {
|
||||
this.setState({
|
||||
phase: PHASE_CONFIRM_SKIP,
|
||||
|
@ -74,8 +112,7 @@ export default class CompleteSecurity extends React.Component {
|
|||
|
||||
render() {
|
||||
const AuthPage = sdk.getComponent("auth.AuthPage");
|
||||
const AuthHeader = sdk.getComponent("auth.AuthHeader");
|
||||
const AuthBody = sdk.getComponent("auth.AuthBody");
|
||||
const CompleteSecurityBody = sdk.getComponent("auth.CompleteSecurityBody");
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
|
||||
const {
|
||||
|
@ -85,7 +122,13 @@ export default class CompleteSecurity extends React.Component {
|
|||
let icon;
|
||||
let title;
|
||||
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>;
|
||||
title = _t("Complete security");
|
||||
body = (
|
||||
|
@ -161,8 +204,7 @@ export default class CompleteSecurity extends React.Component {
|
|||
|
||||
return (
|
||||
<AuthPage>
|
||||
<AuthHeader />
|
||||
<AuthBody>
|
||||
<CompleteSecurityBody>
|
||||
<h2 className="mx_CompleteSecurity_header">
|
||||
{icon}
|
||||
{title}
|
||||
|
@ -170,7 +212,7 @@ export default class CompleteSecurity extends React.Component {
|
|||
<div className="mx_CompleteSecurity_body">
|
||||
{body}
|
||||
</div>
|
||||
</AuthBody>
|
||||
</CompleteSecurityBody>
|
||||
</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',
|
||||
|
||||
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,
|
||||
|
||||
// If true, the component will consider itself busy.
|
||||
|
@ -181,7 +186,7 @@ export default createReactClass({
|
|||
username, phoneCountry, phoneNumber, password,
|
||||
).then((data) => {
|
||||
this.setState({serverIsAlive: true}); // it must be, we logged in.
|
||||
this.props.onLoggedIn(data);
|
||||
this.props.onLoggedIn(data, password);
|
||||
}, (error) => {
|
||||
if (this._unmounted) {
|
||||
return;
|
||||
|
|
|
@ -45,7 +45,13 @@ export default createReactClass({
|
|||
displayName: 'Registration',
|
||||
|
||||
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,
|
||||
|
||||
clientSecret: PropTypes.string,
|
||||
sessionId: PropTypes.string,
|
||||
makeRegistrationUrl: PropTypes.func.isRequired,
|
||||
|
@ -348,7 +354,7 @@ export default createReactClass({
|
|||
homeserverUrl: this.state.matrixClient.getHomeserverUrl(),
|
||||
identityServerUrl: this.state.matrixClient.getIdentityServerUrl(),
|
||||
accessToken: response.access_token,
|
||||
});
|
||||
}, this.state.formVals.password);
|
||||
|
||||
this._setupPushers(cli);
|
||||
// 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...");
|
||||
window.mx_on_recaptcha_loaded = () => {this._onCaptchaLoaded();};
|
||||
let protocol = global.location.protocol;
|
||||
if (protocol === "vector:") {
|
||||
if (protocol !== "http:") {
|
||||
protocol = "https:";
|
||||
}
|
||||
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,
|
||||
];
|
||||
|
||||
export function getEntryComponentForLoginType(loginType) {
|
||||
export default function getEntryComponentForLoginType(loginType) {
|
||||
for (const c of AuthEntryComponents) {
|
||||
if (c.LOGIN_TYPE == loginType) {
|
||||
return c;
|
||||
|
|
|
@ -306,7 +306,7 @@ export default createReactClass({
|
|||
return (
|
||||
<div>
|
||||
<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') }
|
||||
</MenuItem>
|
||||
</div>
|
||||
|
|
|
@ -27,7 +27,7 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
|||
import {MenuItem} from "../../structures/ContextMenu";
|
||||
import * as sdk from "../../../index";
|
||||
|
||||
export class TopLeftMenu extends React.Component {
|
||||
export default class TopLeftMenu extends React.Component {
|
||||
static propTypes = {
|
||||
displayName: PropTypes.string.isRequired,
|
||||
userId: PropTypes.string.isRequired,
|
||||
|
|
|
@ -65,6 +65,9 @@ export default createReactClass({
|
|||
// Title for the dialog.
|
||||
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: 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 (
|
||||
<MatrixClientContext.Provider value={this._matrixClient}>
|
||||
<FocusLock
|
||||
|
@ -135,6 +145,7 @@ export default createReactClass({
|
|||
'mx_Dialog_headerWithButton': !!this.props.headerButton,
|
||||
})}>
|
||||
<div className={classNames('mx_Dialog_title', this.props.titleClass)} id='mx_BaseDialog_title'>
|
||||
{headerImage}
|
||||
{ this.props.title }
|
||||
</div>
|
||||
{ this.props.headerButton }
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
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");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -44,13 +45,13 @@ export default createReactClass({
|
|||
},
|
||||
|
||||
_roomCreateOptions() {
|
||||
const createOpts = {};
|
||||
const opts = {};
|
||||
const createOpts = opts.createOpts = {};
|
||||
createOpts.name = this.state.name;
|
||||
if (this.state.isPublic) {
|
||||
createOpts.visibility = "public";
|
||||
createOpts.preset = "public_chat";
|
||||
// to prevent createRoom from enabling guest access
|
||||
createOpts['initial_state'] = [];
|
||||
opts.guestAccess = false;
|
||||
const {alias} = this.state;
|
||||
const localPart = alias.substr(1, alias.indexOf(":") - 1);
|
||||
createOpts['room_alias_name'] = localPart;
|
||||
|
@ -61,7 +62,7 @@ export default createReactClass({
|
|||
if (this.state.noFederate) {
|
||||
createOpts.creation_content = {'m.federate': false};
|
||||
}
|
||||
return createOpts;
|
||||
return opts;
|
||||
},
|
||||
|
||||
componentDidMount() {
|
||||
|
|
|
@ -33,6 +33,7 @@ import Modal from "../../../Modal";
|
|||
import {humanizeTime} from "../../../utils/humanize";
|
||||
import createRoom from "../../../createRoom";
|
||||
import {inviteMultipleToRoom} from "../../../RoomInvite";
|
||||
import SettingsStore from '../../../settings/SettingsStore';
|
||||
|
||||
export const KIND_DM = "dm";
|
||||
export const KIND_INVITE = "invite";
|
||||
|
@ -337,19 +338,31 @@ export default class InviteDialog extends React.PureComponent {
|
|||
const recents = [];
|
||||
for (const userId in rooms) {
|
||||
// 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 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
|
||||
? room.timeline[room.timeline.length - 1].getTs()
|
||||
: 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});
|
||||
}
|
||||
if (!recents) console.warn("[Invite:Recents] No recents to suggest!");
|
||||
|
||||
// Sort the recents by last active to save us time later
|
||||
recents.sort((a, b) => b.lastActive - a.lastActive);
|
||||
|
@ -493,7 +506,7 @@ export default class InviteDialog extends React.PureComponent {
|
|||
return false;
|
||||
}
|
||||
|
||||
_startDm = () => {
|
||||
_startDm = async () => {
|
||||
this.setState({busy: true});
|
||||
const targetIds = this.state.targets.map(t => t.userId);
|
||||
|
||||
|
@ -510,14 +523,31 @@ export default class InviteDialog extends React.PureComponent {
|
|||
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.
|
||||
// TODO: [Canonical DMs] Remove this check and instead just create the multi-person DM
|
||||
let createRoomPromise = Promise.resolve();
|
||||
if (targetIds.length === 1) {
|
||||
createRoomPromise = createRoom({dmUserId: targetIds[0]});
|
||||
createRoomOptions.dmUserId = targetIds[0];
|
||||
createRoomPromise = createRoom(createRoomOptions);
|
||||
} else {
|
||||
// Create a boring room and try to invite the targets manually.
|
||||
createRoomPromise = createRoom().then(roomId => {
|
||||
createRoomPromise = createRoom(createRoomOptions).then(roomId => {
|
||||
return inviteMultipleToRoom(roomId, targetIds);
|
||||
}).then(result => {
|
||||
if (this._shouldAbortAfterInviteError(result)) {
|
||||
|
@ -586,13 +616,36 @@ export default class InviteDialog extends React.PureComponent {
|
|||
clearTimeout(this._debounceTimer);
|
||||
}
|
||||
this._debounceTimer = setTimeout(async () => {
|
||||
MatrixClientPeg.get().searchUserDirectory({term}).then(r => {
|
||||
MatrixClientPeg.get().searchUserDirectory({term}).then(async r => {
|
||||
if (term !== this.state.filterText) {
|
||||
// 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
|
||||
// more accurate results.
|
||||
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({
|
||||
serverResultsMixin: r.results.map(u => ({
|
||||
userId: u.user_id,
|
||||
|
@ -672,11 +725,16 @@ export default class InviteDialog extends React.PureComponent {
|
|||
};
|
||||
|
||||
_toggleMember = (member: Member) => {
|
||||
let filterText = this.state.filterText;
|
||||
const targets = this.state.targets.map(t => t); // cheap clone for mutation
|
||||
const idx = targets.indexOf(member);
|
||||
if (idx >= 0) targets.splice(idx, 1);
|
||||
else targets.push(member);
|
||||
this.setState({targets});
|
||||
if (idx >= 0) {
|
||||
targets.splice(idx, 1);
|
||||
} else {
|
||||
targets.push(member);
|
||||
filterText = ""; // clear the filter when the user accepts a suggestion
|
||||
}
|
||||
this.setState({targets, filterText});
|
||||
};
|
||||
|
||||
_removeMember = (member: Member) => {
|
||||
|
@ -876,7 +934,7 @@ export default class InviteDialog extends React.PureComponent {
|
|||
key={"input"}
|
||||
rows={1}
|
||||
onChange={this._updateFilter}
|
||||
defaultValue={this.state.filterText}
|
||||
value={this.state.filterText}
|
||||
ref={this._editorRef}
|
||||
onPaste={this._onPaste}
|
||||
/>
|
||||
|
@ -944,7 +1002,7 @@ export default class InviteDialog extends React.PureComponent {
|
|||
|
||||
title = _t("Direct Messages");
|
||||
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>.",
|
||||
{userId},
|
||||
{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}
|
||||
>
|
||||
<div className='mx_InviteDialog_content'>
|
||||
<p>{helpText}</p>
|
||||
<p className='mx_InviteDialog_helpText'>{helpText}</p>
|
||||
<div className='mx_InviteDialog_addressBar'>
|
||||
{this._renderEditor()}
|
||||
<div className='mx_InviteDialog_buttonAndSpinner'>
|
||||
|
@ -987,8 +1045,10 @@ export default class InviteDialog extends React.PureComponent {
|
|||
</div>
|
||||
{this._renderIdentityServerWarning()}
|
||||
<div className='error'>{this.state.errorText}</div>
|
||||
{this._renderSection('recents')}
|
||||
{this._renderSection('suggestions')}
|
||||
<div className='mx_InviteDialog_userSections'>
|
||||
{this._renderSection('recents')}
|
||||
{this._renderSection('suggestions')}
|
||||
</div>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
|
|
|
@ -20,6 +20,8 @@ import { _t } from '../../../languageHandler';
|
|||
import PropTypes from "prop-types";
|
||||
import {MatrixEvent} from "matrix-js-sdk";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import Markdown from '../../../Markdown';
|
||||
|
||||
/*
|
||||
* 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 (
|
||||
<BaseDialog
|
||||
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.")
|
||||
}
|
||||
</p>
|
||||
|
||||
{adminMessage}
|
||||
<Field
|
||||
id="mx_ReportEventDialog_reason"
|
||||
className="mx_ReportEventDialog_reason"
|
||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as sdk from '../../../../index';
|
||||
import {MatrixClientPeg} from '../../../../MatrixClientPeg';
|
||||
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
|
||||
*/
|
||||
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) {
|
||||
super(props);
|
||||
this.state = {
|
||||
|
@ -96,6 +107,10 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
|||
const recoverInfo = await MatrixClientPeg.get().restoreKeyBackupWithPassword(
|
||||
this.state.passPhrase, undefined, undefined, this.state.backupInfo,
|
||||
);
|
||||
if (!this.props.showSummary) {
|
||||
this.props.onFinished(true);
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
loading: false,
|
||||
recoverInfo,
|
||||
|
@ -119,6 +134,10 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
|||
const recoverInfo = await MatrixClientPeg.get().restoreKeyBackupWithRecoveryKey(
|
||||
this.state.recoveryKey, undefined, undefined, this.state.backupInfo,
|
||||
);
|
||||
if (!this.props.showSummary) {
|
||||
this.props.onFinished(true);
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
loading: false,
|
||||
recoverInfo,
|
||||
|
@ -253,6 +272,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
|||
title = _t("Error");
|
||||
content = _t("No backup found!");
|
||||
} else if (this.state.recoverInfo) {
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
title = _t("Backup Restored");
|
||||
let failedToDecrypt;
|
||||
if (this.state.recoverInfo.total > this.state.recoverInfo.imported) {
|
||||
|
@ -264,6 +284,11 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
|||
content = <div>
|
||||
<p>{_t("Restored %(sessionCount)s session keys", {sessionCount: this.state.recoverInfo.imported})}</p>
|
||||
{failedToDecrypt}
|
||||
<DialogButtons primaryButton={_t('OK')}
|
||||
onPrimaryButtonClick={this._onDone}
|
||||
hasCancel={false}
|
||||
focus={true}
|
||||
/>
|
||||
</div>;
|
||||
} else if (backupHasPassphrase && !this.state.forceRecoveryKey) {
|
||||
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"
|
||||
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.
|
||||
onPrimaryButtonClick: PropTypes.func.isRequired,
|
||||
onPrimaryButtonClick: PropTypes.func,
|
||||
|
||||
// should there be a cancel button? default: true
|
||||
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.
|
||||
onCancel: PropTypes.func,
|
||||
|
||||
|
@ -69,16 +76,26 @@ export default createReactClass({
|
|||
primaryButtonClassName += " " + this.props.primaryButtonClass;
|
||||
}
|
||||
let cancelButton;
|
||||
|
||||
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") }
|
||||
</button>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_Dialog_buttons">
|
||||
{ cancelButton }
|
||||
{ this.props.children }
|
||||
<button className={primaryButtonClassName}
|
||||
<button type={this.props.primaryIsSubmit ? 'submit' : 'button'}
|
||||
className={primaryButtonClassName}
|
||||
onClick={this.props.onPrimaryButtonClick}
|
||||
autoFocus={this.props.focus}
|
||||
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 { _t } from '../../../languageHandler';
|
||||
|
||||
import * as recent from './recent';
|
||||
import * as recent from '../../../emojipicker/recent';
|
||||
import {DATA_BY_CATEGORY, getEmojiFromUnicode} from "../../../emoji";
|
||||
|
||||
export const CATEGORY_HEADER_HEIGHT = 22;
|
||||
|
|
|
@ -26,6 +26,7 @@ import classNames from 'classnames';
|
|||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import {ContextMenu, ContextMenuButton, toRightOf} from "../../structures/ContextMenu";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {RovingTabIndexWrapper} from "../../../accessibility/RovingTabIndex";
|
||||
|
||||
// XXX this class copies a lot from RoomTile.js
|
||||
export default createReactClass({
|
||||
|
@ -127,7 +128,8 @@ export default createReactClass({
|
|||
'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 }
|
||||
</div>;
|
||||
|
||||
|
@ -137,16 +139,6 @@ export default createReactClass({
|
|||
});
|
||||
|
||||
const badgeContent = badgeEllipsis ? '\u00B7\u00B7\u00B7' : '!';
|
||||
const badge = (
|
||||
<ContextMenuButton
|
||||
className={badgeClasses}
|
||||
onClick={this.onContextMenuButtonClick}
|
||||
label={_t("Options")}
|
||||
isExpanded={isMenuDisplayed}
|
||||
>
|
||||
{ badgeContent }
|
||||
</ContextMenuButton>
|
||||
);
|
||||
|
||||
let tooltip;
|
||||
if (this.props.collapsed && this.state.hover) {
|
||||
|
@ -171,22 +163,37 @@ export default createReactClass({
|
|||
}
|
||||
|
||||
return <React.Fragment>
|
||||
<AccessibleButton
|
||||
className={classes}
|
||||
onClick={this.onClick}
|
||||
onMouseEnter={this.onMouseEnter}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
onContextMenu={this.onContextMenu}
|
||||
>
|
||||
<div className="mx_RoomTile_avatar">
|
||||
{ av }
|
||||
</div>
|
||||
<div className="mx_RoomTile_nameContainer">
|
||||
{ label }
|
||||
{ badge }
|
||||
</div>
|
||||
{ tooltip }
|
||||
</AccessibleButton>
|
||||
<RovingTabIndexWrapper>
|
||||
{({onFocus, isActive, ref}) =>
|
||||
<AccessibleButton
|
||||
onFocus={onFocus}
|
||||
tabIndex={isActive ? 0 : -1}
|
||||
inputRef={ref}
|
||||
className={classes}
|
||||
onClick={this.onClick}
|
||||
onMouseEnter={this.onMouseEnter}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
onContextMenu={this.onContextMenu}
|
||||
>
|
||||
<div className="mx_RoomTile_avatar">
|
||||
{ av }
|
||||
</div>
|
||||
<div className="mx_RoomTile_nameContainer">
|
||||
{ label }
|
||||
<ContextMenuButton
|
||||
className={badgeClasses}
|
||||
onClick={this.onContextMenuButtonClick}
|
||||
label={_t("Options")}
|
||||
isExpanded={isMenuDisplayed}
|
||||
tabIndex={isActive ? 0 : -1}
|
||||
>
|
||||
{ badgeContent }
|
||||
</ContextMenuButton>
|
||||
</div>
|
||||
{ tooltip }
|
||||
</AccessibleButton>
|
||||
}
|
||||
</RovingTabIndexWrapper>
|
||||
|
||||
{ contextMenu }
|
||||
</React.Fragment>;
|
||||
|
|
|
@ -93,7 +93,7 @@ export default class MKeyVerificationConclusion extends React.Component {
|
|||
}
|
||||
|
||||
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", {
|
||||
mx_KeyVerification_icon_verified: request.done,
|
||||
});
|
||||
|
|
|
@ -85,7 +85,7 @@ export default class MKeyVerificationRequest extends React.Component {
|
|||
if (userId === myUserId) {
|
||||
return _t("You accepted");
|
||||
} 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) {
|
||||
return _t("You cancelled");
|
||||
} 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) {
|
||||
const name = getNameForEventRoom(request.requestingUserId, mxEvent.getRoomId());
|
||||
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">{
|
||||
userLabelForEventRoom(request.requestingUserId, mxEvent)}</div>);
|
||||
userLabelForEventRoom(request.requestingUserId, mxEvent.getRoomId())}</div>);
|
||||
if (request.requested && !request.observeOnly) {
|
||||
stateNode = (<div className="mx_KeyVerification_buttons">
|
||||
<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">{
|
||||
_t("You sent a verification request")}</div>);
|
||||
subtitle = (<div className="mx_KeyVerification_subtitle">{
|
||||
userLabelForEventRoom(request.receivingUserId, mxEvent)}</div>);
|
||||
userLabelForEventRoom(request.receivingUserId, mxEvent.getRoomId())}</div>);
|
||||
}
|
||||
|
||||
if (title) {
|
||||
|
|
|
@ -82,7 +82,7 @@ const _getE2EStatus = (cli, userId, devices) => {
|
|||
return "warning";
|
||||
};
|
||||
|
||||
function openDMForUser(matrixClient, userId) {
|
||||
async function openDMForUser(matrixClient, userId) {
|
||||
const dmRooms = DMRoomMap.shared().getDMRoomsForUserId(userId);
|
||||
const lastActiveRoom = dmRooms.reduce((lastActiveRoom, roomId) => {
|
||||
const room = matrixClient.getRoom(roomId);
|
||||
|
@ -100,9 +100,27 @@ function openDMForUser(matrixClient, userId) {
|
|||
action: 'view_room',
|
||||
room_id: lastActiveRoom.roomId,
|
||||
});
|
||||
} else {
|
||||
createRoom({dmUserId: userId});
|
||||
return;
|
||||
}
|
||||
|
||||
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) {
|
||||
|
@ -1219,10 +1237,9 @@ const UserInfo = ({user, groupId, roomId, onClose}) => {
|
|||
|
||||
let closeButton;
|
||||
if (onClose) {
|
||||
closeButton = <AccessibleButton
|
||||
className="mx_UserInfo_cancel"
|
||||
onClick={onClose}
|
||||
title={_t('Close')} />;
|
||||
closeButton = <AccessibleButton className="mx_UserInfo_cancel" onClick={onClose} title={_t('Close')}>
|
||||
<div />
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
const memberDetails = (
|
||||
|
@ -1308,15 +1325,18 @@ const UserInfo = ({user, groupId, roomId, onClose}) => {
|
|||
userTrust.isVerified();
|
||||
const isMe = user.userId === cli.getUserId();
|
||||
let verifyButton;
|
||||
if (!userVerified && !isMe) {
|
||||
if (isRoomEncrypted && !userVerified && !isMe) {
|
||||
verifyButton = <AccessibleButton className="mx_UserInfo_verify" onClick={() => verifyUser(user)}>
|
||||
{_t("Verify")}
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
const devicesSection = <DevicesSection
|
||||
loading={devices === undefined}
|
||||
devices={devices} userId={user.userId} />;
|
||||
let devicesSection;
|
||||
if (isRoomEncrypted) {
|
||||
devicesSection = <DevicesSection
|
||||
loading={devices === undefined}
|
||||
devices={devices} userId={user.userId} />;
|
||||
}
|
||||
|
||||
const securitySection = (
|
||||
<div className="mx_UserInfo_container">
|
||||
|
@ -1335,32 +1355,32 @@ const UserInfo = ({user, groupId, roomId, onClose}) => {
|
|||
|
||||
return (
|
||||
<div className="mx_UserInfo" role="tabpanel">
|
||||
{ closeButton }
|
||||
{ avatarElement }
|
||||
|
||||
<div className="mx_UserInfo_container">
|
||||
<div className="mx_UserInfo_profile">
|
||||
<div>
|
||||
<h2 aria-label={displayName}>
|
||||
{ e2eIcon }
|
||||
{ displayName }
|
||||
</h2>
|
||||
</div>
|
||||
<div>{ user.userId }</div>
|
||||
<div className="mx_UserInfo_profileStatus">
|
||||
{presenceLabel}
|
||||
{statusLabel}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{ memberDetails && <div className="mx_UserInfo_container mx_UserInfo_memberDetailsContainer">
|
||||
<div className="mx_UserInfo_memberDetails">
|
||||
{ memberDetails }
|
||||
</div>
|
||||
</div> }
|
||||
|
||||
<AutoHideScrollbar className="mx_UserInfo_scrollContainer">
|
||||
{ closeButton }
|
||||
{ avatarElement }
|
||||
|
||||
<div className="mx_UserInfo_container">
|
||||
<div className="mx_UserInfo_profile">
|
||||
<div>
|
||||
<h2 aria-label={displayName}>
|
||||
{ e2eIcon }
|
||||
{ displayName }
|
||||
</h2>
|
||||
</div>
|
||||
<div>{ user.userId }</div>
|
||||
<div className="mx_UserInfo_profileStatus">
|
||||
{presenceLabel}
|
||||
{statusLabel}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{ memberDetails && <div className="mx_UserInfo_container mx_UserInfo_memberDetailsContainer">
|
||||
<div className="mx_UserInfo_memberDetails">
|
||||
{ memberDetails }
|
||||
</div>
|
||||
</div> }
|
||||
|
||||
{ securitySection }
|
||||
<UserOptionsSection
|
||||
devices={devices}
|
||||
|
|
|
@ -17,6 +17,9 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import * as sdk from '../../../index';
|
||||
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 {
|
||||
constructor(props) {
|
||||
|
@ -36,7 +39,8 @@ export default class VerificationPanel extends React.PureComponent {
|
|||
renderStatus() {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
const {request} = this.props;
|
||||
const {request: req} = this.props;
|
||||
const request: VerificationRequest = req;
|
||||
|
||||
if (request.requested) {
|
||||
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}>
|
||||
Verify by emoji
|
||||
</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>);
|
||||
} else if (request.started) {
|
||||
if (this.state.sasWaitingForOtherParty) {
|
||||
|
|
|
@ -209,6 +209,7 @@ export default class BasicMessageEditor extends React.Component {
|
|||
const range = getRangeForSelection(this._editorRef, model, selection);
|
||||
const selectedParts = range.parts.map(p => p.serialize());
|
||||
event.clipboardData.setData("application/x-riot-composer", JSON.stringify(selectedParts));
|
||||
event.clipboardData.setData("text/plain", text); // so plain copy/paste works
|
||||
if (type === "cut") {
|
||||
// Remove the text, updating the model as appropriate
|
||||
this._modifiedFlag = true;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
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.
|
||||
|
@ -14,76 +15,102 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useState} from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import classNames from 'classnames';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import SettingsStore from '../../../settings/SettingsStore';
|
||||
|
||||
export default function(props) {
|
||||
const { isUser } = props;
|
||||
const isNormal = props.status === "normal";
|
||||
const isWarning = props.status === "warning";
|
||||
const isVerified = props.status === "verified";
|
||||
const e2eIconClasses = classNames({
|
||||
import {_t, _td} from '../../../languageHandler';
|
||||
import {useFeatureEnabled} from "../../../hooks/useSettings";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import Tooltip from "../elements/Tooltip";
|
||||
|
||||
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_warning: isWarning,
|
||||
mx_E2EIcon_normal: isNormal,
|
||||
mx_E2EIcon_verified: isVerified,
|
||||
}, props.className);
|
||||
mx_E2EIcon_warning: status === E2E_STATE.WARNING,
|
||||
mx_E2EIcon_normal: status === E2E_STATE.NORMAL,
|
||||
mx_E2EIcon_verified: status === E2E_STATE.VERIFIED,
|
||||
}, className);
|
||||
|
||||
let e2eTitle;
|
||||
|
||||
const crossSigning = SettingsStore.isFeatureEnabled("feature_cross_signing");
|
||||
const crossSigning = useFeatureEnabled("feature_cross_signing");
|
||||
if (crossSigning && isUser) {
|
||||
if (isWarning) {
|
||||
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.",
|
||||
);
|
||||
}
|
||||
e2eTitle = crossSigningUserTitles[status];
|
||||
} else if (crossSigning && !isUser) {
|
||||
if (isWarning) {
|
||||
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.",
|
||||
);
|
||||
}
|
||||
e2eTitle = crossSigningRoomTitles[status];
|
||||
} else if (!crossSigning && isUser) {
|
||||
if (isWarning) {
|
||||
e2eTitle = _t("Some devices for this user are not trusted");
|
||||
} else if (isVerified) {
|
||||
e2eTitle = _t("All devices for this user are trusted");
|
||||
}
|
||||
e2eTitle = legacyUserTitles[status];
|
||||
} else if (!crossSigning && !isUser) {
|
||||
if (isWarning) {
|
||||
e2eTitle = _t("Some devices in this encrypted room are not trusted");
|
||||
} else if (isVerified) {
|
||||
e2eTitle = _t("All devices in this encrypted room are trusted");
|
||||
}
|
||||
e2eTitle = legacyRoomTitles[status];
|
||||
}
|
||||
|
||||
let style = null;
|
||||
if (props.size) {
|
||||
style = {width: `${props.size}px`, height: `${props.size}px`};
|
||||
let style;
|
||||
if (size) {
|
||||
style = {width: `${size}px`, height: `${size}px`};
|
||||
}
|
||||
|
||||
const icon = (<div className={e2eIconClasses} style={style} title={e2eTitle} />);
|
||||
if (props.onClick) {
|
||||
return (<AccessibleButton onClick={props.onClick}>{ icon }</AccessibleButton>);
|
||||
} else {
|
||||
return icon;
|
||||
const onMouseOver = () => setHover(true);
|
||||
const onMouseOut = () => setHover(false);
|
||||
|
||||
let tip;
|
||||
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 * as ObjectUtils from "../../../ObjectUtils";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {E2E_STATE} from "./E2EIcon";
|
||||
|
||||
const eventTileTypes = {
|
||||
'm.room.message': 'messages.MessageEvent',
|
||||
|
@ -235,6 +236,7 @@ export default createReactClass({
|
|||
this._suppressReadReceiptAnimation = false;
|
||||
const client = this.context;
|
||||
client.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||
client.on("userTrustStatusChanged", this.onUserVerificationChanged);
|
||||
this.props.mxEvent.on("Event.decrypted", this._onDecrypted);
|
||||
if (this.props.showReactions) {
|
||||
this.props.mxEvent.on("Event.relationsCreated", this._onReactionsCreated);
|
||||
|
@ -260,6 +262,7 @@ export default createReactClass({
|
|||
componentWillUnmount: function() {
|
||||
const client = this.context;
|
||||
client.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||
client.removeListener("userTrustStatusChanged", this.onUserVerificationChanged);
|
||||
this.props.mxEvent.removeListener("Event.decrypted", this._onDecrypted);
|
||||
if (this.props.showReactions) {
|
||||
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) {
|
||||
if (!mxEvent.isEncrypted()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If we directly trust the device, short-circuit here
|
||||
const verified = await this.context.isEventSenderVerified(mxEvent);
|
||||
if (verified) {
|
||||
this.setState({
|
||||
verified: E2E_STATE.VERIFIED,
|
||||
}, () => {
|
||||
// Decryption may have caused a change in size
|
||||
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: verified,
|
||||
}, () => {
|
||||
// Decryption may have caused a change in size
|
||||
this.props.onHeightChanged();
|
||||
});
|
||||
verified: eventSenderTrust.isVerified() ? E2E_STATE.VERIFIED : E2E_STATE.WARNING,
|
||||
}, this.props.onHeightChanged); // Decryption may have caused a change in size
|
||||
},
|
||||
|
||||
_propsEqual: function(objA, objB) {
|
||||
|
@ -473,8 +514,12 @@ export default createReactClass({
|
|||
|
||||
// event is encrypted, display padlock corresponding to whether or not it is verified
|
||||
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
|
||||
} else if (this.state.verified === E2E_STATE.UNKNOWN) {
|
||||
return (<E2ePadlockUnknown />);
|
||||
} else {
|
||||
return (<E2ePadlockUnverified />);
|
||||
}
|
||||
|
@ -527,6 +572,7 @@ export default createReactClass({
|
|||
console.error("EventTile attempted to get relations for an event without an ID");
|
||||
// Use event's special `toJSON` method to log key data.
|
||||
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");
|
||||
},
|
||||
|
@ -604,8 +650,9 @@ export default createReactClass({
|
|||
mx_EventTile_last: this.props.last,
|
||||
mx_EventTile_contextual: this.props.contextual,
|
||||
mx_EventTile_actionBarFocused: this.state.actionBarFocused,
|
||||
mx_EventTile_verified: !isBubbleMessage && this.state.verified === true,
|
||||
mx_EventTile_unverified: !isBubbleMessage && this.state.verified === false,
|
||||
mx_EventTile_verified: !isBubbleMessage && this.state.verified === E2E_STATE.VERIFIED,
|
||||
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_emote: msgtype === 'm.emote',
|
||||
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 {
|
||||
static propTypes = {
|
||||
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 ContentMessages from '../../../ContentMessages';
|
||||
import E2EIcon from './E2EIcon';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
function ComposerAvatar(props) {
|
||||
const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar');
|
||||
|
@ -168,7 +169,6 @@ export default class MessageComposer extends React.Component {
|
|||
constructor(props) {
|
||||
super(props);
|
||||
this.onInputStateChanged = this.onInputStateChanged.bind(this);
|
||||
this.onEvent = this.onEvent.bind(this);
|
||||
this._onRoomStateEvents = this._onRoomStateEvents.bind(this);
|
||||
this._onRoomViewStoreUpdate = this._onRoomViewStoreUpdate.bind(this);
|
||||
this._onTombstoneClick = this._onTombstoneClick.bind(this);
|
||||
|
@ -182,11 +182,6 @@ export default class MessageComposer extends React.Component {
|
|||
}
|
||||
|
||||
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);
|
||||
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
|
||||
this._waitForOwnMember();
|
||||
|
@ -210,7 +205,6 @@ export default class MessageComposer extends React.Component {
|
|||
|
||||
componentWillUnmount() {
|
||||
if (MatrixClientPeg.get()) {
|
||||
MatrixClientPeg.get().removeListener("event", this.onEvent);
|
||||
MatrixClientPeg.get().removeListener("RoomState.events", this._onRoomStateEvents);
|
||||
}
|
||||
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) {
|
||||
if (ev.getRoomId() !== this.props.room.roomId) return;
|
||||
|
||||
|
@ -282,18 +269,33 @@ export default class MessageComposer extends React.Component {
|
|||
}
|
||||
|
||||
renderPlaceholderText() {
|
||||
const roomIsEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId);
|
||||
if (this.state.isQuoting) {
|
||||
if (roomIsEncrypted) {
|
||||
return _t('Send an encrypted reply…');
|
||||
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
|
||||
if (this.state.isQuoting) {
|
||||
if (this.props.e2eStatus) {
|
||||
return _t('Send an encrypted reply…');
|
||||
} else {
|
||||
return _t('Send a reply…');
|
||||
}
|
||||
} else {
|
||||
return _t('Send a reply (unencrypted)…');
|
||||
if (this.props.e2eStatus) {
|
||||
return _t('Send an encrypted message…');
|
||||
} else {
|
||||
return _t('Send a message…');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (roomIsEncrypted) {
|
||||
return _t('Send an encrypted message…');
|
||||
if (this.state.isQuoting) {
|
||||
if (this.props.e2eStatus) {
|
||||
return _t('Send an encrypted reply…');
|
||||
} else {
|
||||
return _t('Send a reply (unencrypted)…');
|
||||
}
|
||||
} else {
|
||||
return _t('Send a message (unencrypted)…');
|
||||
if (this.props.e2eStatus) {
|
||||
return _t('Send an encrypted message…');
|
||||
} else {
|
||||
return _t('Send a message (unencrypted)…');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -363,7 +363,7 @@ export default class RoomBreadcrumbs extends React.Component {
|
|||
}
|
||||
|
||||
let dmIndicator;
|
||||
if (this._isDmRoom(r.room)) {
|
||||
if (this._isDmRoom(r.room) && !SettingsStore.isFeatureEnabled("feature_cross_signing")) {
|
||||
dmIndicator = <img
|
||||
src={require("../../../../res/img/icon_person.svg")}
|
||||
className="mx_RoomBreadcrumbs_dmIndicator"
|
||||
|
|
|
@ -31,7 +31,9 @@ import ManageIntegsButton from '../elements/ManageIntegsButton';
|
|||
import {CancelButton} from './SimpleRoomHeader';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import RoomHeaderButtons from '../right_panel/RoomHeaderButtons';
|
||||
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||
import E2EIcon from './E2EIcon';
|
||||
import InviteOnlyIcon from './InviteOnlyIcon';
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'RoomHeader',
|
||||
|
@ -160,13 +162,16 @@ export default createReactClass({
|
|||
<E2EIcon status={this.props.e2eStatus} /> :
|
||||
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 joinRule = joinRules && joinRules.getContent().join_rule;
|
||||
const joinRuleClass = classNames("mx_RoomHeader_PrivateIcon",
|
||||
{"mx_RoomHeader_isPrivate": joinRule === "invite"});
|
||||
const privateIcon = SettingsStore.isFeatureEnabled("feature_cross_signing") ?
|
||||
<div className={joinRuleClass} /> :
|
||||
undefined;
|
||||
let privateIcon;
|
||||
// Don't show an invite-only icon for DMs. Users know they're invite-only.
|
||||
if (!dmUserId && SettingsStore.isFeatureEnabled("feature_cross_signing")) {
|
||||
if (joinRule == "invite") {
|
||||
privateIcon = <InviteOnlyIcon />;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.props.onCancelClick) {
|
||||
cancelButton = <CancelButton onClick={this.props.onCancelClick} />;
|
||||
|
@ -310,8 +315,7 @@ export default createReactClass({
|
|||
return (
|
||||
<div className="mx_RoomHeader light-panel">
|
||||
<div className="mx_RoomHeader_wrapper">
|
||||
<div className="mx_RoomHeader_avatar">{ roomAvatar }</div>
|
||||
{ e2eIcon }
|
||||
<div className="mx_RoomHeader_avatar">{ roomAvatar }{ e2eIcon }</div>
|
||||
{ privateIcon }
|
||||
{ name }
|
||||
{ topicElement }
|
||||
|
|
|
@ -39,6 +39,7 @@ import * as sdk from "../../../index";
|
|||
import * as Receipt from "../../../utils/Receipt";
|
||||
import {Resizer} from '../../../resizer';
|
||||
import {Layout, Distributor} from '../../../resizer/distributors/roomsublist2';
|
||||
import {RovingTabIndexProvider} from "../../../accessibility/RovingTabIndex";
|
||||
|
||||
const HIDE_CONFERENCE_CHANS = true;
|
||||
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'],
|
||||
label: _t('People'),
|
||||
label: _t('Direct Messages'),
|
||||
tagName: "im.vector.fake.direct",
|
||||
order: "recent",
|
||||
incomingCall: incomingCallIfTaggedAs('im.vector.fake.direct'),
|
||||
|
@ -776,19 +777,22 @@ export default createReactClass({
|
|||
|
||||
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 (
|
||||
<div
|
||||
{...props}
|
||||
ref={this._collectResizeContainer}
|
||||
className="mx_RoomList"
|
||||
role="tree"
|
||||
aria-label={_t("Rooms")}
|
||||
onMouseMove={this.onMouseMove}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
>
|
||||
{ subListComponents }
|
||||
</div>
|
||||
<RovingTabIndexProvider handleHomeEnd={true} onKeyDown={onKeyDown}>
|
||||
{({onKeyDownHandler}) => <div
|
||||
{...props}
|
||||
onKeyDown={onKeyDownHandler}
|
||||
ref={this._collectResizeContainer}
|
||||
className="mx_RoomList"
|
||||
role="tree"
|
||||
aria-label={_t("Rooms")}
|
||||
onMouseMove={this.onMouseMove}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
>
|
||||
{ subListComponents }
|
||||
</div> }
|
||||
</RovingTabIndexProvider>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -49,6 +49,7 @@ export default createReactClass({
|
|||
propTypes: {
|
||||
onJoinClick: PropTypes.func,
|
||||
onRejectClick: PropTypes.func,
|
||||
onRejectAndIgnoreClick: PropTypes.func,
|
||||
onForgetClick: PropTypes.func,
|
||||
// if inviterName is specified, the preview bar will shown an invite to the room.
|
||||
// You should also specify onRejectClick if specifiying inviterName
|
||||
|
@ -282,6 +283,7 @@ export default createReactClass({
|
|||
|
||||
render: function() {
|
||||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
let showSpinner = false;
|
||||
let darkStyle = false;
|
||||
|
@ -292,6 +294,7 @@ export default createReactClass({
|
|||
let secondaryActionHandler;
|
||||
let secondaryActionLabel;
|
||||
let footer;
|
||||
const extraComponents = [];
|
||||
|
||||
const messageCase = this._getMessageCase();
|
||||
switch (messageCase) {
|
||||
|
@ -469,6 +472,14 @@ export default createReactClass({
|
|||
primaryActionHandler = this.props.onJoinClick;
|
||||
secondaryActionLabel = _t("Reject");
|
||||
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;
|
||||
}
|
||||
case MessageCase.ViewingRoom: {
|
||||
|
@ -505,8 +516,6 @@ export default createReactClass({
|
|||
}
|
||||
}
|
||||
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
let subTitleElements;
|
||||
if (subTitle) {
|
||||
if (!Array.isArray(subTitle)) {
|
||||
|
@ -554,6 +563,7 @@ export default createReactClass({
|
|||
</div>
|
||||
<div className="mx_RoomPreviewBar_actions">
|
||||
{ secondaryButton }
|
||||
{ extraComponents }
|
||||
{ primaryButton }
|
||||
</div>
|
||||
<div className="mx_RoomPreviewBar_footer">
|
||||
|
|
|
@ -32,6 +32,11 @@ import ActiveRoomObserver from '../../../ActiveRoomObserver';
|
|||
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
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({
|
||||
displayName: 'RoomTile',
|
||||
|
@ -69,6 +74,7 @@ export default createReactClass({
|
|||
notificationCount: this.props.room.getUnreadNotificationCount(),
|
||||
selected: this.props.room.roomId === RoomViewStore.getRoomId(),
|
||||
statusMessage: this._getStatusMessage(),
|
||||
e2eStatus: null,
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -101,6 +107,83 @@ export default createReactClass({
|
|||
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) {
|
||||
if (room !== this.props.room) return;
|
||||
this.setState({
|
||||
|
@ -150,10 +233,19 @@ export default createReactClass({
|
|||
},
|
||||
|
||||
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();
|
||||
cli.on("accountData", this.onAccountData);
|
||||
cli.on("Room.name", this.onRoomName);
|
||||
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);
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
|
||||
|
@ -171,6 +263,9 @@ export default createReactClass({
|
|||
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
|
||||
MatrixClientPeg.get().removeListener("Room.name", this.onRoomName);
|
||||
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);
|
||||
dis.unregister(this.dispatcherRef);
|
||||
|
@ -317,7 +412,6 @@ export default createReactClass({
|
|||
'mx_RoomTile_noBadges': !badges,
|
||||
'mx_RoomTile_transparent': this.props.transparent,
|
||||
'mx_RoomTile_hasSubtext': subtext && !this.props.collapsed,
|
||||
'mx_RoomTile_isPrivate': this.state.joinRule == "invite" && !dmUserId,
|
||||
});
|
||||
|
||||
const avatarClasses = classNames({
|
||||
|
@ -352,7 +446,8 @@ export default createReactClass({
|
|||
});
|
||||
|
||||
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) {
|
||||
const Tooltip = sdk.getComponent("elements.Tooltip");
|
||||
tooltip = <Tooltip className="mx_RoomTile_tooltip" label={this.props.room.name} dir="auto" />;
|
||||
|
@ -383,7 +478,9 @@ export default createReactClass({
|
|||
|
||||
let dmIndicator;
|
||||
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
|
||||
src={require("../../../../res/img/icon_person.svg")}
|
||||
className="mx_RoomTile_dm"
|
||||
|
@ -428,40 +525,54 @@ export default createReactClass({
|
|||
|
||||
let privateIcon = null;
|
||||
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>
|
||||
<AccessibleButton
|
||||
tabIndex="0"
|
||||
className={classes}
|
||||
onClick={this.onClick}
|
||||
onMouseEnter={this.onMouseEnter}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
onContextMenu={this.onContextMenu}
|
||||
aria-label={ariaLabel}
|
||||
aria-selected={this.state.selected}
|
||||
role="treeitem"
|
||||
>
|
||||
<div className={avatarClasses}>
|
||||
<div className="mx_RoomTile_avatar_container">
|
||||
<RoomAvatar room={this.props.room} width={24} height={24} />
|
||||
{ dmIndicator }
|
||||
</div>
|
||||
</div>
|
||||
{ privateIcon }
|
||||
<div className="mx_RoomTile_nameContainer">
|
||||
<div className="mx_RoomTile_labelContainer">
|
||||
{ label }
|
||||
{ subtextLabel }
|
||||
</div>
|
||||
{ dmOnline }
|
||||
{ contextMenuButton }
|
||||
{ badge }
|
||||
</div>
|
||||
{ /* { incomingCallBox } */ }
|
||||
{ tooltip }
|
||||
</AccessibleButton>
|
||||
<RovingTabIndexWrapper>
|
||||
{({onFocus, isActive, ref}) =>
|
||||
<AccessibleButton
|
||||
onFocus={onFocus}
|
||||
tabIndex={isActive ? 0 : -1}
|
||||
inputRef={ref}
|
||||
className={classes}
|
||||
onClick={this.onClick}
|
||||
onMouseEnter={this.onMouseEnter}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
onContextMenu={this.onContextMenu}
|
||||
aria-label={ariaLabel}
|
||||
aria-selected={this.state.selected}
|
||||
role="treeitem"
|
||||
>
|
||||
<div className={avatarClasses}>
|
||||
<div className="mx_RoomTile_avatar_container">
|
||||
<RoomAvatar room={this.props.room} width={24} height={24} />
|
||||
{ dmIndicator }
|
||||
{ e2eIcon }
|
||||
</div>
|
||||
</div>
|
||||
{ privateIcon }
|
||||
<div className="mx_RoomTile_nameContainer">
|
||||
<div className="mx_RoomTile_labelContainer">
|
||||
{ label }
|
||||
{ subtextLabel }
|
||||
</div>
|
||||
{ dmOnline }
|
||||
{ contextMenuButton }
|
||||
{ badge }
|
||||
</div>
|
||||
{ /* { incomingCallBox } */ }
|
||||
{ tooltip }
|
||||
</AccessibleButton>
|
||||
}
|
||||
</RovingTabIndexWrapper>
|
||||
|
||||
{ contextMenu }
|
||||
</React.Fragment>;
|
||||
|
|
|
@ -24,6 +24,8 @@ import {
|
|||
containsEmote,
|
||||
stripEmoteCommand,
|
||||
unescapeMessage,
|
||||
startsWith,
|
||||
stripPrefix,
|
||||
} from '../../../editor/serialize';
|
||||
import {CommandPartCreator} from '../../../editor/parts';
|
||||
import BasicMessageComposer from "./BasicMessageComposer";
|
||||
|
@ -33,7 +35,7 @@ import ReplyThread from "../elements/ReplyThread";
|
|||
import {parseEvent} from '../../../editor/deserialize';
|
||||
import {findEditableEvent} from '../../../utils/EventUtils';
|
||||
import SendHistoryManager from "../../../SendHistoryManager";
|
||||
import {processCommandInput} from '../../../SlashCommands';
|
||||
import {getCommand} from '../../../SlashCommands';
|
||||
import * as sdk from '../../../index';
|
||||
import Modal from '../../../Modal';
|
||||
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);
|
||||
if (isEmote) {
|
||||
model = stripEmoteCommand(model);
|
||||
}
|
||||
if (startsWith(model, "//")) {
|
||||
model = stripPrefix(model, "/");
|
||||
}
|
||||
model = unescapeMessage(model);
|
||||
const repliedToEvent = RoomViewStore.getQuotingEvent();
|
||||
|
||||
|
@ -175,20 +181,21 @@ export default class SendMessageComposer extends React.Component {
|
|||
const parts = this.model.parts;
|
||||
const firstPart = parts[0];
|
||||
if (firstPart) {
|
||||
if (firstPart.type === "command") {
|
||||
if (firstPart.type === "command" && firstPart.text.startsWith("/") && !firstPart.text.startsWith("//")) {
|
||||
return true;
|
||||
}
|
||||
// be extra resilient when somehow the AutocompleteWrapperModel or
|
||||
// CommandPartCreator fails to insert a command part, so we don't send
|
||||
// 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 false;
|
||||
}
|
||||
|
||||
async _runSlashCommand() {
|
||||
_getSlashCommand() {
|
||||
const commandText = this.model.parts.reduce((text, part) => {
|
||||
// use mxid to textify user pills in a command
|
||||
if (part.type === "user-pill") {
|
||||
|
@ -196,50 +203,86 @@ export default class SendMessageComposer extends React.Component {
|
|||
}
|
||||
return text + part.text;
|
||||
}, "");
|
||||
const cmd = processCommandInput(this.props.room.roomId, commandText);
|
||||
return [getCommand(this.props.room.roomId, commandText), commandText];
|
||||
}
|
||||
|
||||
if (cmd) {
|
||||
let error = cmd.error;
|
||||
if (cmd.promise) {
|
||||
try {
|
||||
await cmd.promise;
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
async _runSlashCommand(fn) {
|
||||
const cmd = fn();
|
||||
let error = cmd.error;
|
||||
if (cmd.promise) {
|
||||
try {
|
||||
await cmd.promise;
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
if (error) {
|
||||
console.error("Command failure: %s", error);
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
// assume the error is a server error when the command is async
|
||||
const isServerError = !!cmd.promise;
|
||||
const title = isServerError ? _td("Server error") : _td("Command error");
|
||||
}
|
||||
if (error) {
|
||||
console.error("Command failure: %s", error);
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
// assume the error is a server error when the command is async
|
||||
const isServerError = !!cmd.promise;
|
||||
const title = isServerError ? _td("Server error") : _td("Command error");
|
||||
|
||||
let errText;
|
||||
if (typeof error === 'string') {
|
||||
errText = error;
|
||||
} else if (error.message) {
|
||||
errText = error.message;
|
||||
} else {
|
||||
errText = _t("Server unavailable, overloaded, or something else went wrong.");
|
||||
}
|
||||
|
||||
Modal.createTrackedDialog(title, '', ErrorDialog, {
|
||||
title: _t(title),
|
||||
description: errText,
|
||||
});
|
||||
let errText;
|
||||
if (typeof error === 'string') {
|
||||
errText = error;
|
||||
} else if (error.message) {
|
||||
errText = error.message;
|
||||
} else {
|
||||
console.log("Command success.");
|
||||
errText = _t("Server unavailable, overloaded, or something else went wrong.");
|
||||
}
|
||||
|
||||
Modal.createTrackedDialog(title, '', ErrorDialog, {
|
||||
title: _t(title),
|
||||
description: errText,
|
||||
});
|
||||
} else {
|
||||
console.log("Command success.");
|
||||
}
|
||||
}
|
||||
|
||||
_sendMessage() {
|
||||
async _sendMessage() {
|
||||
if (this.model.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
let shouldSend = true;
|
||||
|
||||
if (!containsEmote(this.model) && this._isSlashCommand()) {
|
||||
this._runSlashCommand();
|
||||
} else {
|
||||
const [cmd, commandText] = this._getSlashCommand();
|
||||
if (cmd) {
|
||||
shouldSend = false;
|
||||
this._runSlashCommand(cmd);
|
||||
} 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 {roomId} = this.props.room;
|
||||
const content = createMessageContent(this.model, this.props.permalinkCreator);
|
||||
|
@ -253,6 +296,7 @@ export default class SendMessageComposer extends React.Component {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.sendHistoryManager.save(this.model);
|
||||
// clear composer
|
||||
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 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();
|
||||
}
|
||||
|
@ -280,7 +289,7 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
const PhoneNumbers = sdk.getComponent("views.settings.account.PhoneNumbers");
|
||||
const Spinner = sdk.getComponent("views.elements.Spinner");
|
||||
|
||||
const passwordChangeForm = (
|
||||
let passwordChangeForm = (
|
||||
<ChangePassword
|
||||
className="mx_GeneralUserSettingsTab_changePassword"
|
||||
rowClassName=""
|
||||
|
@ -314,11 +323,18 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
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 (
|
||||
<div className="mx_SettingsTab_section mx_GeneralUserSettingsTab_accountSection">
|
||||
<span className="mx_SettingsTab_subheading">{_t("Account")}</span>
|
||||
<p className="mx_SettingsTab_subsectionText">
|
||||
{_t("Set a new account password...")}
|
||||
{passwordChangeText}
|
||||
</p>
|
||||
{passwordChangeForm}
|
||||
{threepidSection}
|
||||
|
|
|
@ -21,15 +21,10 @@ import {MatrixClientPeg} from "../../../../../MatrixClientPeg";
|
|||
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||
import SdkConfig from "../../../../../SdkConfig";
|
||||
import createRoom from "../../../../../createRoom";
|
||||
import packageJson from "../../../../../../package.json";
|
||||
import Modal from "../../../../../Modal";
|
||||
import * as sdk from "../../../../../";
|
||||
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.
|
||||
const semVerRegex = /^v?(\d+\.\d+\.\d+(?:-rc.+)?)(?:-(?:\d+-g)?([0-9a-fA-F]+))?(?:-dirty)?$/i;
|
||||
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
|
||||
? ghVersionLabel('vector-im/riot-web', this.state.vectorVersion)
|
||||
: 'unknown';
|
||||
|
@ -243,7 +235,6 @@ export default class HelpUserSettingsTab extends React.Component {
|
|||
<div className='mx_SettingsTab_section mx_HelpUserSettingsTab_versions'>
|
||||
<span className='mx_SettingsTab_subheading'>{_t("Versions")}</span>
|
||||
<div className='mx_SettingsTab_subsectionText'>
|
||||
{_t("matrix-react-sdk version:")} {reactSdkVersion}<br />
|
||||
{_t("riot-web version:")} {vectorVersion}<br />
|
||||
{_t("olm version:")} {olmVersion}<br />
|
||||
{updateButton}
|
||||
|
|
|
@ -170,6 +170,7 @@ export default class PreferencesUserSettingsTab extends React.Component {
|
|||
return (
|
||||
<div className="mx_SettingsTab mx_PreferencesUserSettingsTab">
|
||||
<div className="mx_SettingsTab_heading">{_t("Preferences")}</div>
|
||||
|
||||
<div className="mx_SettingsTab_section">
|
||||
<span className="mx_SettingsTab_subheading">{_t("Composer")}</span>
|
||||
{this._renderGroup(PreferencesUserSettingsTab.COMPOSER_SETTINGS)}
|
||||
|
|
|
@ -242,6 +242,7 @@ export default class SecurityUserSettingsTab extends React.Component {
|
|||
render() {
|
||||
const DevicesPanel = sdk.getComponent('views.settings.DevicesPanel');
|
||||
const SettingsFlag = sdk.getComponent('views.elements.SettingsFlag');
|
||||
const EventIndexPanel = sdk.getComponent('views.settings.EventIndexPanel');
|
||||
|
||||
const KeyBackupPanel = sdk.getComponent('views.settings.KeyBackupPanel');
|
||||
const keyBackup = (
|
||||
|
@ -253,6 +254,16 @@ export default class SecurityUserSettingsTab extends React.Component {
|
|||
</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
|
||||
// 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
|
||||
|
@ -281,6 +292,7 @@ export default class SecurityUserSettingsTab extends React.Component {
|
|||
</div>
|
||||
</div>
|
||||
{keyBackup}
|
||||
{eventIndex}
|
||||
{crossSigning}
|
||||
{this._renderCurrentDeviceInfo()}
|
||||
<div className='mx_SettingsTab_section'>
|
||||
|
|
|
@ -32,7 +32,7 @@ export default class VerifySessionToast extends React.PureComponent {
|
|||
DeviceListener.sharedInstance().dismissVerification(this.props.deviceId);
|
||||
};
|
||||
|
||||
_onVerifyClick = async () => {
|
||||
_onReviewClick = async () => {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog');
|
||||
|
||||
|
@ -47,10 +47,10 @@ export default class VerifySessionToast extends React.PureComponent {
|
|||
render() {
|
||||
const FormButton = sdk.getComponent("elements.FormButton");
|
||||
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">
|
||||
<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>);
|
||||
}
|
||||
|
|
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 dis from "../../../dispatcher";
|
||||
import ToastStore from "../../../stores/ToastStore";
|
||||
import Modal from "../../../Modal";
|
||||
|
||||
export default class VerificationRequestToast extends React.PureComponent {
|
||||
constructor(props) {
|
||||
|
@ -38,6 +39,13 @@ export default class VerificationRequestToast extends React.PureComponent {
|
|||
this.setState({counter});
|
||||
}, 1000);
|
||||
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() {
|
||||
|
@ -65,22 +73,27 @@ export default class VerificationRequestToast extends React.PureComponent {
|
|||
accept = async () => {
|
||||
ToastStore.sharedInstance().dismissToast(this.props.toastKey);
|
||||
const {request} = this.props;
|
||||
const {event} = request;
|
||||
// no room id for to_device requests
|
||||
if (event.getRoomId()) {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: event.getRoomId(),
|
||||
should_peek: false,
|
||||
});
|
||||
}
|
||||
try {
|
||||
await request.accept();
|
||||
dis.dispatch({
|
||||
action: "set_right_panel_phase",
|
||||
phase: RIGHT_PANEL_PHASES.EncryptionPanel,
|
||||
refireParams: {verificationRequest: request},
|
||||
});
|
||||
if (request.channel.roomId) {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: request.channel.roomId,
|
||||
should_peek: false,
|
||||
});
|
||||
await request.accept();
|
||||
dis.dispatch({
|
||||
action: "set_right_panel_phase",
|
||||
phase: RIGHT_PANEL_PHASES.EncryptionPanel,
|
||||
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) {
|
||||
console.error(err.message);
|
||||
}
|
||||
|
@ -89,13 +102,13 @@ export default class VerificationRequestToast extends React.PureComponent {
|
|||
render() {
|
||||
const FormButton = sdk.getComponent("elements.FormButton");
|
||||
const {request} = this.props;
|
||||
const {event} = request;
|
||||
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
|
||||
if (nameLabel === userId) {
|
||||
const client = MatrixClientPeg.get();
|
||||
const user = client.getUser(event.getSender());
|
||||
const user = client.getUser(userId);
|
||||
if (user && user.displayName) {
|
||||
nameLabel = _t("%(name)s (%(userId)s)", {name: user.displayName, userId});
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
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");
|
||||
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 {bool=} opts.spinner True to show a modal spinner while the room is created.
|
||||
* 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
|
||||
* action was aborted or failed.
|
||||
|
@ -39,6 +43,8 @@ import {getAddressType} from "./UserAddress";
|
|||
export default function createRoom(opts) {
|
||||
opts = opts || {};
|
||||
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 Loader = sdk.getComponent("elements.Spinner");
|
||||
|
@ -77,18 +83,30 @@ export default function createRoom(opts) {
|
|||
opts.andView = true;
|
||||
}
|
||||
|
||||
createOpts.initial_state = createOpts.initial_state || [];
|
||||
|
||||
// Allow guests by default since the room is private and they'd
|
||||
// need an invite. This means clicking on a 3pid invite email can
|
||||
// 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: {
|
||||
guest_access: 'can_join',
|
||||
},
|
||||
type: 'm.room.guest_access',
|
||||
});
|
||||
}
|
||||
|
||||
if (opts.encryption) {
|
||||
createOpts.initial_state.push({
|
||||
type: 'm.room.encryption',
|
||||
state_key: '',
|
||||
},
|
||||
];
|
||||
content: {
|
||||
algorithm: 'm.megolm.v1.aes-sha2',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
let modal;
|
||||
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) {
|
||||
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) => {
|
||||
if (isQuotedMessage) {
|
||||
parts.push(partCreator.plain(QUOTE_LINE_PREFIX));
|
||||
|
|
|
@ -100,27 +100,71 @@ export function formatRangeAsCode(range) {
|
|||
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) {
|
||||
const {model, parts} = range;
|
||||
const {partCreator} = model;
|
||||
|
||||
const isFormatted = parts.length &&
|
||||
parts[0].text.startsWith(prefix) &&
|
||||
parts[parts.length - 1].text.endsWith(suffix);
|
||||
// compute paragraph [start, end] indexes
|
||||
const paragraphIndexes = [];
|
||||
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
|
||||
|
||||
if (isFormatted) {
|
||||
// remove prefix and suffix
|
||||
const partWithoutPrefix = parts[0].serialize();
|
||||
partWithoutPrefix.text = partWithoutPrefix.text.substr(prefix.length);
|
||||
parts[0] = partCreator.deserializePart(partWithoutPrefix);
|
||||
// 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;
|
||||
}
|
||||
|
||||
const partWithoutSuffix = parts[parts.length - 1].serialize();
|
||||
const suffixPartText = partWithoutSuffix.text;
|
||||
partWithoutSuffix.text = suffixPartText.substring(0, suffixPartText.length - suffix.length);
|
||||
parts[parts.length - 1] = partCreator.deserializePart(partWithoutSuffix);
|
||||
} else {
|
||||
parts.unshift(partCreator.plain(prefix));
|
||||
parts.push(partCreator.plain(suffix));
|
||||
// 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) {
|
||||
// remove prefix and suffix
|
||||
const partWithoutPrefix = parts[base].serialize();
|
||||
partWithoutPrefix.text = partWithoutPrefix.text.substr(prefix.length);
|
||||
parts[base] = partCreator.deserializePart(partWithoutPrefix);
|
||||
|
||||
const partWithoutSuffix = parts[index - 1].serialize();
|
||||
const suffixPartText = partWithoutSuffix.text;
|
||||
partWithoutSuffix.text = suffixPartText.substring(0, suffixPartText.length - suffix.length);
|
||||
parts[index - 1] = partCreator.deserializePart(partWithoutSuffix);
|
||||
} else {
|
||||
parts.splice(index, 0, partCreator.plain(suffix)); // splice in the later one first to not change offset
|
||||
parts.splice(base, 0, partCreator.plain(prefix));
|
||||
offset += 2; // offset index to account for the two items we just spliced in
|
||||
}
|
||||
});
|
||||
|
||||
replaceRangeAndExpandSelection(range, parts);
|
||||
}
|
||||
|
|
|
@ -61,18 +61,26 @@ export function textSerialize(model) {
|
|||
}
|
||||
|
||||
export function containsEmote(model) {
|
||||
return startsWith(model, "/me ");
|
||||
}
|
||||
|
||||
export function startsWith(model, prefix) {
|
||||
const firstPart = model.parts[0];
|
||||
// part type will be "plain" while editing,
|
||||
// and "command" while composing a message.
|
||||
return firstPart &&
|
||||
(firstPart.type === "plain" || firstPart.type === "command") &&
|
||||
firstPart.text.startsWith("/me ");
|
||||
firstPart.text.startsWith(prefix);
|
||||
}
|
||||
|
||||
export function stripEmoteCommand(model) {
|
||||
// trim "/me "
|
||||
return stripPrefix(model, "/me ");
|
||||
}
|
||||
|
||||
export function stripPrefix(model, prefix) {
|
||||
model = model.clone();
|
||||
model.removeText({index: 0, offset: 0}, 4);
|
||||
model.removeText({index: 0, offset: 0}, prefix.length);
|
||||
return model;
|
||||
}
|
||||
|
||||
|
|
|
@ -79,13 +79,13 @@ EMOJIBASE.forEach(emoji => {
|
|||
});
|
||||
|
||||
/**
|
||||
* Strips variation selectors from a string
|
||||
* NB. Skin tone modifers are not variation selectors:
|
||||
* Strips variation selectors from the end of given string
|
||||
* NB. Skin tone modifiers are not variation selectors:
|
||||
* this function does not touch them. (Should it?)
|
||||
*
|
||||
* @param {string} str string to strip
|
||||
* @returns {string} stripped string
|
||||
*/
|
||||
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",
|
||||
"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.",
|
||||
"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",
|
||||
"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",
|
||||
|
@ -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 %(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",
|
||||
"Verify this session": "Verify this session",
|
||||
"Encryption upgrade available": "Encryption upgrade available",
|
||||
"Set up encryption": "Set up encryption",
|
||||
"New Session": "New Session",
|
||||
"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",
|
||||
|
@ -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.",
|
||||
"Only continue if you trust the owner of the server.": "Only continue if you trust the owner of the server.",
|
||||
"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 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",
|
||||
|
@ -121,15 +124,8 @@
|
|||
"Moderator": "Moderator",
|
||||
"Admin": "Admin",
|
||||
"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",
|
||||
"Operation failed": "Operation failed",
|
||||
"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:",
|
||||
"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 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",
|
||||
"Unrecognised command:": "Unrecognised command:",
|
||||
"Reason": "Reason",
|
||||
"%(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.",
|
||||
|
@ -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 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.",
|
||||
"%(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.|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.|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 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.",
|
||||
|
@ -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 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 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",
|
||||
"%(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.",
|
||||
|
@ -373,7 +372,6 @@
|
|||
"Render simple counters in room header": "Render simple counters in room header",
|
||||
"Multiple integration managers": "Multiple integration managers",
|
||||
"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",
|
||||
"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)",
|
||||
|
@ -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)",
|
||||
"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",
|
||||
"Enable message search in encrypted rooms": "Enable message search in encrypted rooms",
|
||||
"Collecting app version information": "Collecting app version information",
|
||||
"Collecting logs": "Collecting logs",
|
||||
"Uploading report": "Uploading report",
|
||||
|
@ -514,8 +513,12 @@
|
|||
"Headphones": "Headphones",
|
||||
"Folder": "Folder",
|
||||
"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",
|
||||
"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",
|
||||
"Decline (%(counter)s)": "Decline (%(counter)s)",
|
||||
"Accept <policyLink /> to continue:": "Accept <policyLink /> to continue:",
|
||||
|
@ -563,6 +566,14 @@
|
|||
"Failed to set display name": "Failed to set display name",
|
||||
"Disable Notifications": "Disable 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...",
|
||||
"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.",
|
||||
|
@ -669,8 +680,8 @@
|
|||
"Profile": "Profile",
|
||||
"Email addresses": "Email addresses",
|
||||
"Phone numbers": "Phone numbers",
|
||||
"Account": "Account",
|
||||
"Set a new account password...": "Set a new account password...",
|
||||
"Account": "Account",
|
||||
"Language and region": "Language and region",
|
||||
"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.",
|
||||
|
@ -694,7 +705,6 @@
|
|||
"Clear cache and reload": "Clear cache and reload",
|
||||
"FAQ": "FAQ",
|
||||
"Versions": "Versions",
|
||||
"matrix-react-sdk version:": "matrix-react-sdk version:",
|
||||
"riot-web version:": "riot-web version:",
|
||||
"olm version:": "olm version:",
|
||||
"Homeserver is": "Homeserver is",
|
||||
|
@ -757,6 +767,7 @@
|
|||
"Accept all %(invitedRooms)s invites": "Accept all %(invitedRooms)s invites",
|
||||
"Reject all %(invitedRooms)s invites": "Reject all %(invitedRooms)s invites",
|
||||
"Key backup": "Key backup",
|
||||
"Message search": "Message search",
|
||||
"Cross-signing": "Cross-signing",
|
||||
"Security & Privacy": "Security & Privacy",
|
||||
"Devices": "Devices",
|
||||
|
@ -888,8 +899,9 @@
|
|||
"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 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.",
|
||||
"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.",
|
||||
"Someone is using an unknown device": "Someone is using an unknown device",
|
||||
"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",
|
||||
"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",
|
||||
|
@ -907,7 +919,9 @@
|
|||
"This message cannot be decrypted": "This message cannot be decrypted",
|
||||
"Encrypted by an unverified device": "Encrypted by an unverified device",
|
||||
"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",
|
||||
"Invite only": "Invite only",
|
||||
"Scroll to bottom of page": "Scroll to bottom of page",
|
||||
"Close preview": "Close preview",
|
||||
"device id: ": "device id: ",
|
||||
|
@ -946,6 +960,7 @@
|
|||
"Invite": "Invite",
|
||||
"Share Link to User": "Share Link to User",
|
||||
"User Options": "User Options",
|
||||
"Start a chat": "Start a chat",
|
||||
"Direct chats": "Direct chats",
|
||||
"Remove recent messages": "Remove recent messages",
|
||||
"Unmute": "Unmute",
|
||||
|
@ -964,8 +979,10 @@
|
|||
"Hangup": "Hangup",
|
||||
"Upload file": "Upload file",
|
||||
"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 a message…": "Send a message…",
|
||||
"Send a reply (unencrypted)…": "Send a reply (unencrypted)…",
|
||||
"Send a message (unencrypted)…": "Send a message (unencrypted)…",
|
||||
"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.",
|
||||
|
@ -1012,7 +1029,7 @@
|
|||
"Community Invites": "Community Invites",
|
||||
"Invites": "Invites",
|
||||
"Favourites": "Favourites",
|
||||
"People": "People",
|
||||
"Direct Messages": "Direct Messages",
|
||||
"Start chat": "Start chat",
|
||||
"Rooms": "Rooms",
|
||||
"Low priority": "Low priority",
|
||||
|
@ -1049,6 +1066,7 @@
|
|||
"Do you want to join %(roomName)s?": "Do you want to join %(roomName)s?",
|
||||
"<userName/> invited you": "<userName/> invited you",
|
||||
"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?",
|
||||
"%(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.",
|
||||
|
@ -1079,6 +1097,11 @@
|
|||
"Server error": "Server error",
|
||||
"Command error": "Command error",
|
||||
"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",
|
||||
"You don't currently have any stickerpacks enabled": "You don't currently have any stickerpacks enabled",
|
||||
"Add some now": "Add some now",
|
||||
|
@ -1464,8 +1487,7 @@
|
|||
"Recent Conversations": "Recent Conversations",
|
||||
"Suggestions": "Suggestions",
|
||||
"Recently Direct Messaged": "Recently Direct Messaged",
|
||||
"Direct Messages": "Direct Messages",
|
||||
"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>.",
|
||||
"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>.",
|
||||
"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>.",
|
||||
"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.",
|
||||
"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 />.",
|
||||
"Upgrade": "Upgrade",
|
||||
"Sign out and remove encryption keys?": "Sign out and remove encryption keys?",
|
||||
"Clear Storage and Sign Out": "Clear Storage and Sign Out",
|
||||
"Send Logs": "Send Logs",
|
||||
|
@ -1973,18 +1994,19 @@
|
|||
"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.",
|
||||
"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.",
|
||||
"<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.",
|
||||
"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.",
|
||||
"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 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.",
|
||||
"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:",
|
||||
"Enter a passphrase": "Enter a passphrase",
|
||||
"Set up with a recovery key": "Set up with a recovery key",
|
||||
"That matches!": "That matches!",
|
||||
"That doesn't match.": "That doesn't match.",
|
||||
"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.",
|
||||
"Repeat your passphrase...": "Repeat your passphrase...",
|
||||
"Enter your passphrase a second time to confirm it.": "Enter your passphrase a second time to confirm it.",
|
||||
"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.": "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.",
|
||||
|
@ -1997,21 +2019,25 @@
|
|||
"<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>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.",
|
||||
"Set up secret storage": "Set up secret storage",
|
||||
"Restore your Key Backup": "Restore your Key Backup",
|
||||
"Migrate from Key Backup": "Migrate from Key Backup",
|
||||
"Secure your encrypted messages with a passphrase": "Secure your encrypted messages with a passphrase",
|
||||
"Confirm your passphrase": "Confirm your passphrase",
|
||||
"Upgrade your encryption": "Upgrade your encryption",
|
||||
"Recovery key": "Recovery key",
|
||||
"Keep it safe": "Keep it safe",
|
||||
"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",
|
||||
"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.",
|
||||
"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",
|
||||
"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.": "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).",
|
||||
|
@ -2019,6 +2045,7 @@
|
|||
"Set up Secure Message Recovery": "Set up Secure Message Recovery",
|
||||
"Secure your backup with a passphrase": "Secure your backup with a passphrase",
|
||||
"Starting backup...": "Starting backup...",
|
||||
"Success!": "Success!",
|
||||
"Create Key Backup": "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.",
|
||||
|
@ -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.",
|
||||
"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 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 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"
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue