diff --git a/.buildkite/pipeline.yaml b/.buildkite/pipeline.yaml
index a8ce1273fb..4bc69a76bd 100644
--- a/.buildkite/pipeline.yaml
+++ b/.buildkite/pipeline.yaml
@@ -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:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3ae2711e25..881669a1a2 100644
--- a/CHANGELOG.md
+++ b/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)
diff --git a/package.json b/package.json
index aa2cf8bf8b..78bbb5b4c6 100644
--- a/package.json
+++ b/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",
diff --git a/res/css/_common.scss b/res/css/_common.scss
index 51d985efb7..e062e0bd73 100644
--- a/res/css/_common.scss
+++ b/res/css/_common.scss
@@ -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;
diff --git a/res/css/_components.scss b/res/css/_components.scss
index f6a680c438..a527352461 100644
--- a/res/css/_components.scss
+++ b/res/css/_components.scss
@@ -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";
diff --git a/res/css/structures/_GroupView.scss b/res/css/structures/_GroupView.scss
index 4ec53a3c9a..517b8b1922 100644
--- a/res/css/structures/_GroupView.scss
+++ b/res/css/structures/_GroupView.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 {
diff --git a/res/css/structures/_ToastContainer.scss b/res/css/structures/_ToastContainer.scss
index 5634a97c53..5b5c49f357 100644
--- a/res/css/structures/_ToastContainer.scss
+++ b/res/css/structures/_ToastContainer.scss
@@ -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%;
diff --git a/res/css/structures/auth/_CompleteSecurity.scss b/res/css/structures/auth/_CompleteSecurity.scss
index c258ce4ec7..2bf51d9574 100644
--- a/res/css/structures/auth/_CompleteSecurity.scss
+++ b/res/css/structures/auth/_CompleteSecurity.scss
@@ -22,7 +22,7 @@ limitations under the License.
.mx_CompleteSecurity_headerIcon {
width: 24px;
height: 24px;
- margin: 0 4px;
+ margin-right: 4px;
position: relative;
}
diff --git a/res/css/views/auth/_AuthBody.scss b/res/css/views/auth/_AuthBody.scss
index b05629003e..7c5b008535 100644
--- a/res/css/views/auth/_AuthBody.scss
+++ b/res/css/views/auth/_AuthBody.scss
@@ -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;
diff --git a/res/css/views/auth/_CompleteSecurityBody.scss b/res/css/views/auth/_CompleteSecurityBody.scss
new file mode 100644
index 0000000000..c7860fbe74
--- /dev/null
+++ b/res/css/views/auth/_CompleteSecurityBody.scss
@@ -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;
+ }
+}
diff --git a/res/css/views/dialogs/_InviteDialog.scss b/res/css/views/dialogs/_InviteDialog.scss
index 221ad7d48c..71fab50339 100644
--- a/res/css/views/dialogs/_InviteDialog.scss
+++ b/res/css/views/dialogs/_InviteDialog.scss
@@ -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;
}
diff --git a/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss b/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss
index 5899abdf73..53e82670e1 100644
--- a/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss
+++ b/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss
@@ -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;
diff --git a/res/css/views/right_panel/_UserInfo.scss b/res/css/views/right_panel/_UserInfo.scss
index e87fe06a94..ad6254f57c 100644
--- a/res/css/views/right_panel/_UserInfo.scss
+++ b/res/css/views/right_panel/_UserInfo.scss
@@ -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;
}
diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss
index fbac1e932a..d292c729dd 100644
--- a/res/css/views/rooms/_EventTile.scss
+++ b/res/css/views/rooms/_EventTile.scss
@@ -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;
}
diff --git a/res/css/views/rooms/_InviteOnlyIcon.scss b/res/css/views/rooms/_InviteOnlyIcon.scss
new file mode 100644
index 0000000000..e70586bb73
--- /dev/null
+++ b/res/css/views/rooms/_InviteOnlyIcon.scss
@@ -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;
+ }
+}
diff --git a/res/css/views/rooms/_MessageComposer.scss b/res/css/views/rooms/_MessageComposer.scss
index 5efca51844..fae9d0dfe3 100644
--- a/res/css/views/rooms/_MessageComposer.scss
+++ b/res/css/views/rooms/_MessageComposer.scss
@@ -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 {
diff --git a/res/css/views/rooms/_RoomHeader.scss b/res/css/views/rooms/_RoomHeader.scss
index 45b9733faa..6f0377b29c 100644
--- a/res/css/views/rooms/_RoomHeader.scss
+++ b/res/css/views/rooms/_RoomHeader.scss
@@ -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;
- }
-}
diff --git a/res/css/views/rooms/_RoomPreviewBar.scss b/res/css/views/rooms/_RoomPreviewBar.scss
index c7d03e3523..85b6916226 100644
--- a/res/css/views/rooms/_RoomPreviewBar.scss
+++ b/res/css/views/rooms/_RoomPreviewBar.scss
@@ -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;
+ }
}
}
diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss
index cb1137bb2f..a24fdf2629 100644
--- a/res/css/views/rooms/_RoomTile.scss
+++ b/res/css/views/rooms/_RoomTile.scss
@@ -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;
- }
-}
diff --git a/res/img/icons-settings-room.svg b/res/img/icons-settings-room.svg
deleted file mode 100644
index 421eefdefa..0000000000
--- a/res/img/icons-settings-room.svg
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss
index 288fb3cadc..c868c81549 100644
--- a/res/themes/light/css/_light.scss
+++ b/res/themes/light/css/_light.scss
@@ -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;
diff --git a/scripts/ci/build.sh b/scripts/ci/build.sh
deleted file mode 100755
index 0b1fa23093..0000000000
--- a/scripts/ci/build.sh
+++ /dev/null
@@ -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
diff --git a/scripts/ci/end-to-end-tests.sh b/scripts/ci/end-to-end-tests.sh
index a592888292..9bdb512940 100755
--- a/scripts/ci/end-to-end-tests.sh
+++ b/scripts/ci/end-to-end-tests.sh
@@ -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"
diff --git a/scripts/ci/install-deps.sh b/scripts/ci/install-deps.sh
index a2e2e59a45..14b5fc5393 100755
--- a/scripts/ci/install-deps.sh
+++ b/scripts/ci/install-deps.sh
@@ -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 $@
diff --git a/scripts/ci/layered-riot-web.sh b/scripts/ci/layered-riot-web.sh
new file mode 100755
index 0000000000..f58794b451
--- /dev/null
+++ b/scripts/ci/layered-riot-web.sh
@@ -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
diff --git a/scripts/ci/riot-unit-tests.sh b/scripts/ci/riot-unit-tests.sh
index 215af13030..337c0fe6c3 100755
--- a/scripts/ci/riot-unit-tests.sh
+++ b/scripts/ci/riot-unit-tests.sh
@@ -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
diff --git a/scripts/fetchdep.sh b/scripts/fetchdep.sh
index f82752bfc5..0142305797 100755
--- a/scripts/fetchdep.sh
+++ b/scripts/fetchdep.sh
@@ -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
}
diff --git a/src/AsyncWrapper.js b/src/AsyncWrapper.js
new file mode 100644
index 0000000000..b7b81688e1
--- /dev/null
+++ b/src/AsyncWrapper.js
@@ -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 ;
+ } else if (this.state.error) {
+ const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
+ const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
+ return
+ {_t("Unable to load! Check your network connectivity and try again.")}
+
+ ;
+ } else {
+ // show a spinner until the component is loaded.
+ const Spinner = sdk.getComponent("elements.Spinner");
+ return ;
+ }
+ },
+});
+
diff --git a/src/DeviceListener.js b/src/DeviceListener.js
index a4c5785db4..630d1a61c0 100644
--- a/src/DeviceListener.js
+++ b/src/DeviceListener.js
@@ -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;
}
}
diff --git a/src/Lifecycle.js b/src/Lifecycle.js
index 1603c73d25..7488488dd8 100644
--- a/src/Lifecycle.js
+++ b/src/Lifecycle.js
@@ -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();
diff --git a/src/Markdown.js b/src/Markdown.js
index acfea52100..437ceec88b 100644
--- a/src/Markdown.js
+++ b/src/Markdown.js
@@ -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;
diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js
index dbc570c872..450bec8e77 100644
--- a/src/MatrixClientPeg.js
+++ b/src/MatrixClientPeg.js
@@ -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(),
};
diff --git a/src/Modal.js b/src/Modal.js
index 29d3af2e74..b6215b2b5a 100644
--- a/src/Modal.js
+++ b/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 ;
- } else if (this.state.error) {
- const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
- const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
- return
- {_t("Unable to load! Check your network connectivity and try again.")}
-
- ;
- } else {
- // show a spinner until the component is loaded.
- const Spinner = sdk.getComponent("elements.Spinner");
- return ;
- }
- },
-});
-
class ModalManager {
constructor() {
this._counter = 0;
diff --git a/src/RoomInvite.js b/src/RoomInvite.js
index 2eccf69b0f..839d677069 100644
--- a/src/RoomInvite.js
+++ b/src/RoomInvite.js
@@ -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;
-}
diff --git a/src/SlashCommands.js b/src/SlashCommands.js
index 20b8ba76da..2eb34576ac 100644
--- a/src/SlashCommands.js
+++ b/src/SlashCommands.js
@@ -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);
}
}
diff --git a/src/TextForEvent.js b/src/TextForEvent.js
index a0d088affb..cdfea45ad7 100644
--- a/src/TextForEvent.js
+++ b/src/TextForEvent.js
@@ -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
diff --git a/src/accessibility/RovingTabIndex.js b/src/accessibility/RovingTabIndex.js
new file mode 100644
index 0000000000..b481f08fe2
--- /dev/null
+++ b/src/accessibility/RovingTabIndex.js
@@ -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
+ { children({onKeyDownHandler}) }
+ ;
+};
+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});
+};
+
diff --git a/src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.js b/src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.js
new file mode 100644
index 0000000000..120b086ef6
--- /dev/null
+++ b/src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.js
@@ -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 (
+
+ {_t("If disabled, messages from encrypted rooms won't appear in search results.")}
+ {this.state.disabling ? :
}
+
+
+ );
+ }
+}
diff --git a/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.js b/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.js
new file mode 100644
index 0000000000..b7ea87b1b2
--- /dev/null
+++ b/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.js
@@ -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 = (
+
+ {
+ _t( "Riot is securely caching encrypted messages locally for them " +
+ "to appear in search results:",
+ )
+ }
+
+ {_t("Space used:")} {formatBytes(this.state.eventIndexSize, 0)}
+ {_t("Indexed messages:")} {formatCountLong(this.state.eventCount)}
+ {_t("Number of rooms:")} {formatCountLong(this.state.roomCount)}
+ {crawlerState}
+
+
+ );
+
+ const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
+ const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
+
+ return (
+
+ {eventIndexingSettings}
+
+
+ );
+ }
+}
diff --git a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js
index 01b9c9c7c8..0867cae6f4 100644
--- a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js
+++ b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js
@@ -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
@@ -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
+ const Field = sdk.getComponent('views.elements.Field');
+
+ let authPrompt;
+ if (this.state.canUploadKeysWithPasswordOnly) {
+ authPrompt =
+
{_t("Enter your account password to confirm the upgrade:")}
+
+
;
+ } else {
+ authPrompt =
+ {_t("You'll need to authenticate with the server to confirm the upgrade.")}
+
;
+ }
+
+ return
;
+ ;
}
_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
{_t(
- "Warning : You should only set up secret storage from a trusted computer.", {},
- { b: sub => {sub} },
+ "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.",
)}
{_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:",
)}
-
{_t("For maximum security, this should be different from your account password.")}
-
-
-
-
- {strengthMeter}
- {helpText}
-
+
+
+
+ {strengthMeter}
+ {helpText}
-
+ >
+
{_t("Skip")}
+
{_t("Advanced")}
-
+
{_t("Set up with a recovery key")}
-
+
;
}
_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 =
+ passPhraseMatch =
{matchText}
@@ -424,28 +532,32 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return
{_t(
- "Please enter your passphrase a second time to confirm.",
+ "Enter your passphrase a second time to confirm it.",
)}
-
-
-
-
-
+
-
+ >
+
{_t("Skip")}
+
;
}
@@ -463,6 +575,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
);
}
+ const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
return
{_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 {
{this._encodedRecoveryKey}
-
+
{_t("Copy to clipboard")}
-
-
+
+
{_t("Download")}
-
+
@@ -533,7 +646,11 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return
{_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.",
+ )}
+
{_t(
+ "Verify other users in their profile.",
)}
{content}
diff --git a/src/components/structures/FilePanel.js b/src/components/structures/FilePanel.js
index 61b3d2d4b9..4c02f925fc 100644
--- a/src/components/structures/FilePanel.js
+++ b/src/components/structures/FilePanel.js
@@ -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')}
diff --git a/src/components/structures/InteractiveAuth.js b/src/components/structures/InteractiveAuth.js
index 53bb990e26..3d63029b06 100644
--- a/src/components/structures/InteractiveAuth.js
+++ b/src/components/structures/InteractiveAuth.js
@@ -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';
diff --git a/src/components/structures/LeftPanel.js b/src/components/structures/LeftPanel.js
index 8a7d10e5b5..f5e0bca67e 100644
--- a/src/components/structures/LeftPanel.js
+++ b/src/components/structures/LeftPanel.js
@@ -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;
diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js
index 3ac8a93e3d..133d74db45 100644
--- a/src/components/structures/MatrixChat.js
+++ b/src/components/structures/MatrixChat.js
@@ -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 = (
+ );
+ } else if (this.state.view === VIEWS.E2E_SETUP) {
+ const E2eSetup = sdk.getComponent('structures.auth.E2eSetup');
+ view = (
+
);
} 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()}
diff --git a/src/components/structures/RoomSubList.js b/src/components/structures/RoomSubList.js
index c2a644287d..600b418fe0 100644
--- a/src/components/structures/RoomSubList.js
+++ b/src/components/structures/RoomSubList.js
@@ -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 = (
-
-
- { FormattingUtils.formatCount(subListNotifCount) }
-
-
- );
- } else if (this.props.isInvite && this.props.list.length) {
- // no notifications but highlight anyway because this is an invite badge
- badge = (
-
-
- { this.props.list.length }
-
-
- );
- }
- }
-
// 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 {
;
}
- let addRoomButton;
- if (this.props.onAddRoom) {
- addRoomButton = (
-
- );
- }
-
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 = (
);
}
- return (
-
-
- { chevron }
- {this.props.label}
- { incomingCall }
-
- { badge }
- { addRoomButton }
-
- );
+ return
+ {({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 = (
+
+
+ { FormattingUtils.formatCount(subListNotifCount) }
+
+
+ );
+ } else if (this.props.isInvite && this.props.list.length) {
+ // no notifications but highlight anyway because this is an invite badge
+ badge = (
+
+
+ { this.props.list.length }
+
+
+ );
+ }
+ }
+
+ let addRoomButton;
+ if (this.props.onAddRoom) {
+ addRoomButton = (
+
+ );
+ }
+
+ return (
+
+
+ { chevron }
+ {this.props.label}
+ { incomingCall }
+
+ { badge }
+ { addRoomButton }
+
+ );
+ } }
+ ;
}
checkOverflow = () => {
diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js
index 9b02f6d503..2d669f9243 100644
--- a/src/components/structures/RoomView.js
+++ b/src/components/structures/RoomView.js
@@ -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 (
- an explicit role=list because Safari+VoiceOver seems to think an ordered-list with
+ // list-style-type: none; is no longer a list
return (
-
+
{ this.props.children }
diff --git a/src/components/structures/SearchBox.js b/src/components/structures/SearchBox.js
index 3be2f65dc5..873efb64c2 100644
--- a/src/components/structures/SearchBox.js
+++ b/src/components/structures/SearchBox.js
@@ -133,9 +133,11 @@ export default createReactClass({
return null;
}
const clearButton = (!this.state.blurred || this.state.searchTerm) ?
- ( {this._clearSearch("button"); } }>
+ ( {this._clearSearch("button"); } }>
) : undefined;
// show a shorter placeholder when blurred, if requested
diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js
index bd13981d1f..65fb00c305 100644
--- a/src/components/structures/TimelinePanel.js
+++ b/src/components/structures/TimelinePanel.js
@@ -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);
diff --git a/src/components/structures/ToastContainer.js b/src/components/structures/ToastContainer.js
index 8a05f62e61..283fbdd96a 100644
--- a/src/components/structures/ToastContainer.js
+++ b/src/components/structures/ToastContainer.js
@@ -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);
}
diff --git a/src/components/structures/TopLeftMenuButton.js b/src/components/structures/TopLeftMenuButton.js
index 967805d099..ebd7aaae89 100644
--- a/src/components/structures/TopLeftMenuButton.js
+++ b/src/components/structures/TopLeftMenuButton.js
@@ -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';
diff --git a/src/components/structures/auth/CompleteSecurity.js b/src/components/structures/auth/CompleteSecurity.js
index b64f368908..29d8207d0a 100644
--- a/src/components/structures/auth/CompleteSecurity.js
+++ b/src/components/structures/auth/CompleteSecurity.js
@@ -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 = ;
+ } else if (phase === PHASE_INTRO) {
icon = ;
title = _t("Complete security");
body = (
@@ -161,8 +204,7 @@ export default class CompleteSecurity extends React.Component {
return (
-
-
+
{icon}
{title}
@@ -170,7 +212,7 @@ export default class CompleteSecurity extends React.Component {
{body}
-
+
);
}
diff --git a/src/components/structures/auth/E2eSetup.js b/src/components/structures/auth/E2eSetup.js
new file mode 100644
index 0000000000..9b390d24cc
--- /dev/null
+++ b/src/components/structures/auth/E2eSetup.js
@@ -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 (
+
+
+
+
+
+ );
+ }
+}
diff --git a/src/components/structures/auth/Login.js b/src/components/structures/auth/Login.js
index 7bc2dbcbae..c8b2a1ea9c 100644
--- a/src/components/structures/auth/Login.js
+++ b/src/components/structures/auth/Login.js
@@ -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;
diff --git a/src/components/structures/auth/Registration.js b/src/components/structures/auth/Registration.js
index fdf2f51e00..171d3ada26 100644
--- a/src/components/structures/auth/Registration.js
+++ b/src/components/structures/auth/Registration.js
@@ -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
diff --git a/src/components/views/auth/CaptchaForm.js b/src/components/views/auth/CaptchaForm.js
index 2da837f029..efcc450067 100644
--- a/src/components/views/auth/CaptchaForm.js
+++ b/src/components/views/auth/CaptchaForm.js
@@ -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');
diff --git a/src/components/views/auth/CompleteSecurityBody.js b/src/components/views/auth/CompleteSecurityBody.js
new file mode 100644
index 0000000000..d757de9fe0
--- /dev/null
+++ b/src/components/views/auth/CompleteSecurityBody.js
@@ -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
+ { this.props.children }
+
;
+ }
+}
diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.js b/src/components/views/auth/InteractiveAuthEntryComponents.js
index 869e81c1f7..801420da95 100644
--- a/src/components/views/auth/InteractiveAuthEntryComponents.js
+++ b/src/components/views/auth/InteractiveAuthEntryComponents.js
@@ -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;
diff --git a/src/components/views/context_menus/RoomTileContextMenu.js b/src/components/views/context_menus/RoomTileContextMenu.js
index 6e2bd8ebf5..2d8dec29c7 100644
--- a/src/components/views/context_menus/RoomTileContextMenu.js
+++ b/src/components/views/context_menus/RoomTileContextMenu.js
@@ -306,7 +306,7 @@ export default createReactClass({
return (
-
+
{ _t('Settings') }
diff --git a/src/components/views/context_menus/TopLeftMenu.js b/src/components/views/context_menus/TopLeftMenu.js
index 528e4790c2..51ec202b90 100644
--- a/src/components/views/context_menus/TopLeftMenu.js
+++ b/src/components/views/context_menus/TopLeftMenu.js
@@ -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,
diff --git a/src/components/views/dialogs/BaseDialog.js b/src/components/views/dialogs/BaseDialog.js
index 19f22a15ad..9238024b60 100644
--- a/src/components/views/dialogs/BaseDialog.js
+++ b/src/components/views/dialogs/BaseDialog.js
@@ -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 = ;
+ }
+
return (
+ {headerImage}
{ this.props.title }
{ this.props.headerButton }
diff --git a/src/components/views/dialogs/CreateRoomDialog.js b/src/components/views/dialogs/CreateRoomDialog.js
index 288074a891..9380226381 100644
--- a/src/components/views/dialogs/CreateRoomDialog.js
+++ b/src/components/views/dialogs/CreateRoomDialog.js
@@ -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() {
diff --git a/src/components/views/dialogs/InviteDialog.js b/src/components/views/dialogs/InviteDialog.js
index 703b0b5121..de11dbf9fa 100644
--- a/src/components/views/dialogs/InviteDialog.js
+++ b/src/components/views/dialogs/InviteDialog.js
@@ -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 profile link .",
{userId},
{a: (sub) => {sub} },
@@ -970,7 +1028,7 @@ export default class InviteDialog extends React.PureComponent {
title={title}
>
-
{helpText}
+
{helpText}
{this._renderEditor()}
@@ -987,8 +1045,10 @@ export default class InviteDialog extends React.PureComponent {
{this._renderIdentityServerWarning()}
{this.state.errorText}
- {this._renderSection('recents')}
- {this._renderSection('suggestions')}
+
+ {this._renderSection('recents')}
+ {this._renderSection('suggestions')}
+
);
diff --git a/src/components/views/dialogs/ReportEventDialog.js b/src/components/views/dialogs/ReportEventDialog.js
index e77bb0693b..99853582dd 100644
--- a/src/components/views/dialogs/ReportEventDialog.js
+++ b/src/components/views/dialogs/ReportEventDialog.js
@@ -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 =
;
+ }
+
return (
-
+ {adminMessage}
this.state.recoverInfo.imported) {
@@ -264,6 +284,11 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
content =
{_t("Restored %(sessionCount)s session keys", {sessionCount: this.state.recoverInfo.imported})}
{failedToDecrypt}
+
;
} else if (backupHasPassphrase && !this.state.forceRecoveryKey) {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
diff --git a/src/components/views/elements/DialogButtons.js b/src/components/views/elements/DialogButtons.js
index 4e47e73052..ee15bfc3f2 100644
--- a/src/components/views/elements/DialogButtons.js
+++ b/src/components/views/elements/DialogButtons.js
@@ -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 =
+ cancelButton =
{ this.props.cancelButton || _t("Cancel") }
;
}
+
return (
{ cancelButton }
{ this.props.children }
-
;
+ }
+}
diff --git a/src/components/views/emojipicker/EmojiPicker.js b/src/components/views/emojipicker/EmojiPicker.js
index 1f508ceedc..ca8f0c0565 100644
--- a/src/components/views/emojipicker/EmojiPicker.js
+++ b/src/components/views/emojipicker/EmojiPicker.js
@@ -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;
diff --git a/src/components/views/groups/GroupInviteTile.js b/src/components/views/groups/GroupInviteTile.js
index 808cf17d55..91c930525d 100644
--- a/src/components/views/groups/GroupInviteTile.js
+++ b/src/components/views/groups/GroupInviteTile.js
@@ -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 =
+ // XXX: this is a workaround for Firefox giving this div a tabstop :( [tabIndex]
+ const label =
{ groupName }
;
@@ -137,16 +139,6 @@ export default createReactClass({
});
const badgeContent = badgeEllipsis ? '\u00B7\u00B7\u00B7' : '!';
- const badge = (
-
- { badgeContent }
-
- );
let tooltip;
if (this.props.collapsed && this.state.hover) {
@@ -171,22 +163,37 @@ export default createReactClass({
}
return
-
-
- { av }
-
-
- { label }
- { badge }
-
- { tooltip }
-
+
+ {({onFocus, isActive, ref}) =>
+
+
+ { av }
+
+
+ { label }
+
+ { badgeContent }
+
+
+ { tooltip }
+
+ }
+
{ contextMenu }
;
diff --git a/src/components/views/messages/MKeyVerificationConclusion.js b/src/components/views/messages/MKeyVerificationConclusion.js
index f51b97786b..a17dcd8ab0 100644
--- a/src/components/views/messages/MKeyVerificationConclusion.js
+++ b/src/components/views/messages/MKeyVerificationConclusion.js
@@ -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,
});
diff --git a/src/components/views/messages/MKeyVerificationRequest.js b/src/components/views/messages/MKeyVerificationRequest.js
index ae793556d8..49f871d16e 100644
--- a/src/components/views/messages/MKeyVerificationRequest.js
+++ b/src/components/views/messages/MKeyVerificationRequest.js
@@ -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 = (
{
- _t("%(name)s wants to verify", {name: getNameForEventRoom(request.requestingUserId, mxEvent)})}
);
+ _t("%(name)s wants to verify", {name})}
);
subtitle = (
{
- userLabelForEventRoom(request.requestingUserId, mxEvent)}
);
+ userLabelForEventRoom(request.requestingUserId, mxEvent.getRoomId())}
);
if (request.requested && !request.observeOnly) {
stateNode = (
@@ -142,7 +143,7 @@ export default class MKeyVerificationRequest extends React.Component {
title = (
{
_t("You sent a verification request")}
);
subtitle = (
{
- userLabelForEventRoom(request.receivingUserId, mxEvent)}
);
+ userLabelForEventRoom(request.receivingUserId, mxEvent.getRoomId())}
);
}
if (title) {
diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js
index a0819be472..051f92cc9c 100644
--- a/src/components/views/right_panel/UserInfo.js
+++ b/src/components/views/right_panel/UserInfo.js
@@ -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 = ;
+ closeButton =
+
+ ;
}
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 = verifyUser(user)}>
{_t("Verify")}
;
}
- const devicesSection = ;
+ let devicesSection;
+ if (isRoomEncrypted) {
+ devicesSection = ;
+ }
const securitySection = (
@@ -1335,32 +1355,32 @@ const UserInfo = ({user, groupId, roomId, onClose}) => {
return (
- { closeButton }
- { avatarElement }
-
-
-
-
-
- { e2eIcon }
- { displayName }
-
-
-
{ user.userId }
-
- {presenceLabel}
- {statusLabel}
-
-
-
-
- { memberDetails &&
-
- { memberDetails }
-
-
}
-
+ { closeButton }
+ { avatarElement }
+
+
+
+
+
+ { e2eIcon }
+ { displayName }
+
+
+
{ user.userId }
+
+ {presenceLabel}
+ {statusLabel}
+
+
+
+
+ { memberDetails &&
+
+ { memberDetails }
+
+
}
+
{ securitySection }
Waiting for {request.otherUserId} to accept ... );
@@ -44,6 +48,24 @@ export default class VerificationPanel extends React.PureComponent {
const verifyButton =
Verify by emoji
;
+
+ 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 = ;
+ return ({request.otherUserId} is ready, start {verifyButton} or have them scan: {qrCode}
);
+ }
+
return ({request.otherUserId} is ready, start {verifyButton}
);
} else if (request.started) {
if (this.state.sasWaitingForOtherParty) {
diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js
index 73c3d961ee..d2b6f8cbc8 100644
--- a/src/components/views/rooms/BasicMessageComposer.js
+++ b/src/components/views/rooms/BasicMessageComposer.js
@@ -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;
diff --git a/src/components/views/rooms/E2EIcon.js b/src/components/views/rooms/E2EIcon.js
index 545d1fd7ed..df5fe204d4 100644
--- a/src/components/views/rooms/E2EIcon.js
+++ b/src/components/views/rooms/E2EIcon.js
@@ -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 = (
);
- if (props.onClick) {
- return ({ icon } );
- } else {
- return icon;
+ const onMouseOver = () => setHover(true);
+ const onMouseOut = () => setHover(false);
+
+ let tip;
+ if (hover) {
+ tip = ;
}
-}
+
+ if (onClick) {
+ return (
+
+ { tip }
+
+ );
+ }
+
+ return
+ { tip }
+
;
+};
+
+E2EIcon.propTypes = {
+ isUser: PropTypes.bool,
+ status: PropTypes.oneOf(Object.values(E2E_STATE)),
+ className: PropTypes.string,
+ size: PropTypes.number,
+ onClick: PropTypes.func,
+};
+
+export default E2EIcon;
diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js
index dce4dc8a93..7d07e03b58 100644
--- a/src/components/views/rooms/EventTile.js
+++ b/src/components/views/rooms/EventTile.js
@@ -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 ( );
} else {
return ( );
}
@@ -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 (
+
+ );
+}
+
class E2ePadlock extends React.Component {
static propTypes = {
icon: PropTypes.string.isRequired,
diff --git a/src/components/views/rooms/InviteOnlyIcon.js b/src/components/views/rooms/InviteOnlyIcon.js
new file mode 100644
index 0000000000..5afaa7f0f2
--- /dev/null
+++ b/src/components/views/rooms/InviteOnlyIcon.js
@@ -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 = ;
+ }
+ return (
+ { tooltip }
+
);
+ }
+}
diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js
index 8d36f02d02..53e10fa750 100644
--- a/src/components/views/rooms/MessageComposer.js
+++ b/src/components/views/rooms/MessageComposer.js
@@ -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)…');
+ }
}
}
}
diff --git a/src/components/views/rooms/RoomBreadcrumbs.js b/src/components/views/rooms/RoomBreadcrumbs.js
index 5a15a7518b..f9408d3259 100644
--- a/src/components/views/rooms/RoomBreadcrumbs.js
+++ b/src/components/views/rooms/RoomBreadcrumbs.js
@@ -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 = :
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") ?
-
:
- 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 = ;
+ }
+ }
if (this.props.onCancelClick) {
cancelButton = ;
@@ -310,8 +315,7 @@ export default createReactClass({
return (
-
{ roomAvatar }
- { e2eIcon }
+
{ roomAvatar }{ e2eIcon }
{ privateIcon }
{ name }
{ topicElement }
diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js
index 5c12b027a4..f41400ecfc 100644
--- a/src/components/views/rooms/RoomList.js
+++ b/src/components/views/rooms/RoomList.js
@@ -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 (
-
- { subListComponents }
-
+
+ {({onKeyDownHandler}) =>
+ { subListComponents }
+
}
+
);
},
});
diff --git a/src/components/views/rooms/RoomPreviewBar.js b/src/components/views/rooms/RoomPreviewBar.js
index cbc992d67f..9af06190f7 100644
--- a/src/components/views/rooms/RoomPreviewBar.js
+++ b/src/components/views/rooms/RoomPreviewBar.js
@@ -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(
+
+ { _t("Reject & Ignore user") }
+ ,
+ );
+ }
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({
{ secondaryButton }
+ { extraComponents }
{ primaryButton }
diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js
index 8c67be3b2a..41d43476ea 100644
--- a/src/components/views/rooms/RoomTile.js
+++ b/src/components/views/rooms/RoomTile.js
@@ -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 ?
{ subtext } : null;
- label =
{ name }
;
+ // XXX: this is a workaround for Firefox giving this div a tabstop :( [tabIndex]
+ label =
{ name }
;
} else if (this.state.hover) {
const Tooltip = sdk.getComponent("elements.Tooltip");
tooltip =
;
@@ -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 =
;
+ if (this.state.joinRule == "invite" && !dmUserId) {
+ privateIcon =
;
+ }
+ }
+
+ let e2eIcon = null;
+ if (this.state.e2eStatus) {
+ e2eIcon =
;
}
return
-
-
-
-
- { dmIndicator }
-
-
- { privateIcon }
-
-
- { label }
- { subtextLabel }
-
- { dmOnline }
- { contextMenuButton }
- { badge }
-
- { /* { incomingCallBox } */ }
- { tooltip }
-
+
+ {({onFocus, isActive, ref}) =>
+
+
+
+
+ { dmIndicator }
+ { e2eIcon }
+
+
+ { privateIcon }
+
+
+ { label }
+ { subtextLabel }
+
+ { dmOnline }
+ { contextMenuButton }
+ { badge }
+
+ { /* { incomingCallBox } */ }
+ { tooltip }
+
+ }
+
{ contextMenu }
;
diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js
index e3b794b1d0..63e58bf738 100644
--- a/src/components/views/rooms/SendMessageComposer.js
+++ b/src/components/views/rooms/SendMessageComposer.js
@@ -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:
+
+ { _t("Unrecognised command: %(commandText)s", {commandText}) }
+
+
+ { _t("You can use /help
to list available commands. " +
+ "Did you mean to send this as a message?", {}, {
+ code: t => { t }
,
+ }) }
+
+
+ { _t("Hint: Begin your message with //
to start it with a slash.", {}, {
+ code: t => { t }
,
+ }) }
+
+
,
+ 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([]);
diff --git a/src/components/views/settings/EventIndexPanel.js b/src/components/views/settings/EventIndexPanel.js
new file mode 100644
index 0000000000..68faa53e53
--- /dev/null
+++ b/src/components/views/settings/EventIndexPanel.js
@@ -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 = (
+
+
+ {_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.")}
+
+
+
+ );
+ } else if (!this.state.eventIndexingEnabled && EventIndexPeg.supportIsInstalled()) {
+ eventIndexingSettings = (
+
+
+ {_t( "Securely cache encrypted messages locally for them to " +
+ "appear in search results.")}
+
+
+
+ {_t("Enable")}
+
+ {this.state.enabling ?
:
}
+
+
+ );
+ } 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 = (
+
+ {
+ _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
search components added .",
+ {},
+ {
+ 'nativeLink': (sub) =>
{sub} ,
+ },
+ )
+ }
+
+ );
+ } else {
+ eventIndexingSettings = (
+
+ {
+ _t( "Riot can't securely cache encrypted messages locally " +
+ "while running in a web browser. Use
Riot Desktop " +
+ "for encrypted messages to appear in search results.",
+ {},
+ {
+ 'riotLink': (sub) =>
{sub} ,
+ },
+ )
+ }
+
+ );
+ }
+
+ return eventIndexingSettings;
+ }
+}
diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
index b9eaa3efa3..2d56df6be4 100644
--- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
@@ -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 = (
;
}
+ 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 (
{_t("Account")}
- {_t("Set a new account password...")}
+ {passwordChangeText}
{passwordChangeForm}
{threepidSection}
diff --git a/src/components/views/settings/tabs/user/HelpUserSettingsTab.js b/src/components/views/settings/tabs/user/HelpUserSettingsTab.js
index ab71de86b9..a245c7c7b9 100644
--- a/src/components/views/settings/tabs/user/HelpUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/HelpUserSettingsTab.js
@@ -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 || '
';
-
// 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 !== ''
- ? 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 {
{_t("Versions")}
- {_t("matrix-react-sdk version:")} {reactSdkVersion}
{_t("riot-web version:")} {vectorVersion}
{_t("olm version:")} {olmVersion}
{updateButton}
diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js
index db5b95cb4c..bd1b7c2ca4 100644
--- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js
@@ -170,6 +170,7 @@ export default class PreferencesUserSettingsTab extends React.Component {
return (
{_t("Preferences")}
+
{_t("Composer")}
{this._renderGroup(PreferencesUserSettingsTab.COMPOSER_SETTINGS)}
diff --git a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js
index 5eadfc234a..eb5f346714 100644
--- a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js
@@ -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 {
);
+ let eventIndex;
+ if (SettingsStore.isFeatureEnabled("feature_event_indexing")) {
+ eventIndex = (
+
+ {_t("Message search")}
+
+
+ );
+ }
+
// 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 {
{keyBackup}
+ {eventIndex}
{crossSigning}
{this._renderCurrentDeviceInfo()}
diff --git a/src/components/views/toasts/NewSessionToast.js b/src/components/views/toasts/NewSessionToast.js
index f83326121b..3b60f59131 100644
--- a/src/components/views/toasts/NewSessionToast.js
+++ b/src/components/views/toasts/NewSessionToast.js
@@ -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 (
-
{_t("Other users may not trust it")}
+
{_t("Review & verify your new session")}
-
+
);
}
diff --git a/src/components/views/toasts/SetupEncryptionToast.js b/src/components/views/toasts/SetupEncryptionToast.js
new file mode 100644
index 0000000000..841ee66ac7
--- /dev/null
+++ b/src/components/views/toasts/SetupEncryptionToast.js
@@ -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 (
+
{this.getDescription()}
+
+
+
+
+
);
+ }
+}
diff --git a/src/components/views/toasts/VerificationRequestToast.js b/src/components/views/toasts/VerificationRequestToast.js
index 479a3e3f93..b2d1aaf59d 100644
--- a/src/components/views/toasts/VerificationRequestToast.js
+++ b/src/components/views/toasts/VerificationRequestToast.js
@@ -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});
}
diff --git a/src/createRoom.js b/src/createRoom.js
index cde9e8b03e..c25b618dc6 100644
--- a/src/createRoom.js
+++ b/src/createRoom.js
@@ -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');
diff --git a/src/editor/deserialize.js b/src/editor/deserialize.js
index 1fdbf9490c..190963f357 100644
--- a/src/editor/deserialize.js
+++ b/src/editor/deserialize.js
@@ -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));
diff --git a/src/editor/operations.js b/src/editor/operations.js
index e2661faf59..d677d7016c 100644
--- a/src/editor/operations.js
+++ b/src/editor/operations.js
@@ -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(
), 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);
}
diff --git a/src/editor/serialize.js b/src/editor/serialize.js
index a55eed97da..ba380f2809 100644
--- a/src/editor/serialize.js
+++ b/src/editor/serialize.js
@@ -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;
}
diff --git a/src/emoji.js b/src/emoji.js
index 125864e381..20b05531ca 100644
--- a/src/emoji.js
+++ b/src/emoji.js
@@ -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]$/, "");
}
diff --git a/src/components/views/emojipicker/recent.js b/src/emojipicker/recent.js
similarity index 100%
rename from src/components/views/emojipicker/recent.js
rename to src/emojipicker/recent.js
diff --git a/src/hooks/useSettings.js b/src/hooks/useSettings.js
new file mode 100644
index 0000000000..151a6369de
--- /dev/null
+++ b/src/hooks/useSettings.js
@@ -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;
+};
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 61dcd90638..d23a5af28e 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -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 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 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 to continue:": "Accept 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 search components added .": "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 search components added .",
+ "Riot can't securely cache encrypted messages locally while running in a web browser. Use Riot Desktop for encrypted messages to appear in search results.": "Riot can't securely cache encrypted messages locally while running in a web browser. Use Riot Desktop 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?",
" invited you": " 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 /help
to list available commands. Did you mean to send this as a message?": "You can use /help
to list available commands. Did you mean to send this as a message?",
+ "Hint: Begin your message with //
to start it with a slash.": "Hint: Begin your message with //
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 profile link .": "If you can't find someone, ask them for their username, or share your username (%(userId)s) or profile link .",
+ "If you can't find someone, ask them for their username, share your username (%(userId)s) or profile link .": "If you can't find someone, ask them for their username, share your username (%(userId)s) or profile link .",
"Go": "Go",
"If you can't find someone, ask them for their username (e.g. @user:server.com) or share this room .": "If you can't find someone, ask them for their username (e.g. @user:server.com) or share this room .",
"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 report a bug .": "This usually only affects how the room is processed on the server. If you're having problems with your Riot, please report a bug .",
"You'll upgrade this room from to .": "You'll upgrade this room from to .",
- "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.",
- "Warning : You should only set up secret storage from a trusted computer.": "Warning : 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 @@
"Print it and store it somewhere safe": "Print it and store it somewhere safe",
"Save it on a USB key or backup drive": "Save it on a USB key or backup drive",
"Copy it to your personal cloud storage": "Copy it 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"
diff --git a/src/i18n/strings/eo.json b/src/i18n/strings/eo.json
index 4ac519d16a..243aebb52d 100644
--- a/src/i18n/strings/eo.json
+++ b/src/i18n/strings/eo.json
@@ -1851,7 +1851,7 @@
"Error unsubscribing from list": "Eraris malabono de la listo",
"Please try again or view your console for hints.": "Bonvolu reprovi aŭ serĉi helpilojn en via konzolo.",
"You have not ignored anyone.": "Vi neniun malatentis.",
- "You are not subscribed to any lists": "Vi neniun liston abonis.",
+ "You are not subscribed to any lists": "Vi neniun liston abonis",
"Unsubscribe": "Malaboni",
"View rules": "Montri regulojn",
"You are currently subscribed to:": "Vi nun abonas:",
@@ -1867,5 +1867,28 @@
"Widget ID": "Identigilo de fenestraĵo",
"Widgets do not use message encryption.": "Fenestraĵoj ne uzas ĉifradon de mesaĝoj.",
"Widget added by": "Fenestraĵon aldonis",
- "This widget may use cookies.": "Ĉi tiu fenestraĵo povas uzi kuketojn."
+ "This widget may use cookies.": "Ĉi tiu fenestraĵo povas uzi kuketojn.",
+ "%(senderName)s added %(addedAddresses)s and %(count)s other addresses to this room|other": "%(senderName)s aldonis %(addedAddresses)s kaj %(count)s aliajn adresojn al ĉi tiu ĉambro",
+ "%(senderName)s removed %(removedAddresses)s and %(count)s other addresses from this room|other": "%(senderName)s forigis %(removedAddresses)s kaj %(count)s aliajn adresojn el ĉi tiu ĉambro",
+ "%(senderName)s removed %(countRemoved)s and added %(countAdded)s addresses to this room": "%(senderName)s forigis %(countRemoved)s kaj aldonis %(countAdded)s adresojn al ĉi tiu ĉambro",
+ "%(senderName)s turned on end-to-end encryption.": "%(senderName)s ŝaltis tutvojan ĉifradon.",
+ "%(senderName)s turned on end-to-end encryption (unrecognised algorithm %(algorithm)s).": "%(senderName)s ŝaltis tutvojan ĉifradon (nerekonita algoritmo %(algorithm)s).",
+ "a few seconds ago": "antaŭ kelkaj sekundoj",
+ "about a minute ago": "antaŭ ĉirkaŭ minuto",
+ "%(num)s minutes ago": "antaŭ %(num)s minutoj",
+ "about an hour ago": "antaŭ ĉirkaŭ horo",
+ "%(num)s hours ago": "antaŭ %(num)s horoj",
+ "about a day ago": "antaŭ ĉirkaŭ tago",
+ "%(num)s days ago": "antaŭ %(num)s tagoj",
+ "a few seconds from now": "kelkajn sekundojn de nun",
+ "about a minute from now": "ĉirkaŭ minuton de nun",
+ "%(num)s minutes from now": "%(num)s minutojn de nun",
+ "about an hour from now": "ĉirkaŭ horon de nun",
+ "%(num)s hours from now": "%(num)s horojn de nun",
+ "about a day from now": "ĉirkaŭ tagon de nun",
+ "%(num)s days from now": "%(num)s tagojn de nun",
+ "Lock": "Seruro",
+ "Other users may not trust it": "Aliaj uzantoj eble ne kredas ĝin",
+ "Later": "Pli poste",
+ "Verify": "Kontroli"
}
diff --git a/src/i18n/strings/eu.json b/src/i18n/strings/eu.json
index 97cef0c70d..1ab4bd15b1 100644
--- a/src/i18n/strings/eu.json
+++ b/src/i18n/strings/eu.json
@@ -1617,7 +1617,7 @@
"Resend edit": "Birbidali edizioa",
"Resend %(unsentCount)s reaction(s)": "Birbidali %(unsentCount)s erreakzio",
"Resend removal": "Birbidali kentzeko agindua",
- "Forgotten your password?": "Pasahitza ahaztu duzu?",
+ "Forgotten your password?": "Pasahitza ahaztuta?",
"Sign in and regain access to your account.": "Hasi saioa eta berreskuratu zure kontua.",
"You're signed out": "Saioa amaitu duzu",
"Clear personal data": "Garbitu datu pertsonalak",
@@ -2074,5 +2074,41 @@
"Done": "Egina",
"Without completing security on this device, it won’t have access to encrypted messages.": "Gailu honetan segurtasuna osatu ezean, ez du zifratutako mezuetara sarbiderik izango.",
"Go Back": "Joan atzera",
- "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.": "Biltegi sekretua oraingo gakoen babeskopiaren xehetasunak erabiliz ezarriko da. Zure biltegi sekretuaren pasa-esaldia eta berreskuratze gakoa zure gakoen babes-kopiarako zenerabiltzanak izango dira."
+ "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.": "Biltegi sekretua oraingo gakoen babeskopiaren xehetasunak erabiliz ezarriko da. Zure biltegi sekretuaren pasa-esaldia eta berreskuratze gakoa zure gakoen babes-kopiarako zenerabiltzanak izango dira.",
+ "%(senderName)s added %(addedAddresses)s and %(count)s other addresses to this room|other": "%(senderName)s erabiltzaileak %(addedAddresses)s helbideak eta beste %(count)s gehitu dizkio gela honi",
+ "%(senderName)s removed %(removedAddresses)s and %(count)s other addresses from this room|other": "%(senderName)s erabiltzaileak %(removedAddresses)s helbideak eta beste %(count)s kendu dizkio gela honi",
+ "%(senderName)s removed %(countRemoved)s and added %(countAdded)s addresses to this room": "%(senderName)s erabiltzaileak %(countRemoved)s helbide kendu eta %(countAdded)s gehitu dizkio gela honi",
+ "%(senderName)s turned on end-to-end encryption.": "%(senderName)s erabiltzaileak muturretik muturrera zifratzea aktibatu du.",
+ "%(senderName)s turned on end-to-end encryption (unrecognised algorithm %(algorithm)s).": "%(senderName)s erabiltzaileak muturretik muturrera zifratzea gaitu du (%(algorithm)s algoritmo ezezaguna).",
+ "Someone is using an unknown device": "Baten bat gailu ezezagun bat erabiltzen ari da",
+ "This room is end-to-end encrypted": "Gela hau muturretik muturrera zifratuta dago",
+ "Everyone in this room is verified": "Gelako guztiak egiaztatuta daude",
+ "Encrypted by a deleted device": "Ezabatutako gailu batek zifratua",
+ "Invite only": "Gonbidapenez besterik ez",
+ "Send a reply…": "Bidali erantzuna…",
+ "Send a message…": "Bidali mezua…",
+ "Reject & Ignore user": "Ukatu eta ezikusi erabiltzailea",
+ "Unknown Command": "Agindu ezezaguna",
+ "Unrecognised command: %(commandText)s": "Agindu ezezaguna: %(commandText)s",
+ "You can use /help
to list available commands. Did you mean to send this as a message?": "/help
erabili dezakezu agindu erabilgarrien zerrenda ikusteko. Ala mezu gisa bidali nahi zenuen hau?",
+ "Hint: Begin your message with //
to start it with a slash.": "Aholkua: Hasi zure mezua //
idatziz barra batekin hasi nahi baduzu.",
+ "Send as message": "Bidali mezu gisa",
+ "Verify User": "Egiaztatu erabiltzailea",
+ "For extra security, verify this user by checking a one-time code on both of your devices.": "Segurtasun gehiagorako, egiaztatu erabiltzaile hau aldi-bakarrerako kode bat bi gailuetan egiaztatuz.",
+ "For maximum security, do this in person.": "Segurtasun gorenerako, egin hau aurrez aurre.",
+ "Start Verification": "Hasi egiaztaketa",
+ "If you can't find someone, ask them for their username, share your username (%(userId)s) or profile link .": "Ez baduzu baten bat aurkitzen, eskatu bere erabiltzaile-izena, partekatu zurea (%(userId)s) edo partekatu profilaren esteka .",
+ "Enter your account password to confirm the upgrade:": "Sartu zure kontuaren pasa-hitza eguneraketa baieztatzeko:",
+ "You'll need to authenticate with the server to confirm the upgrade.": "Zerbitzariarekin autentifikatu beharko duzu eguneraketa baieztatzeko.",
+ "Upgrade this device to allow it to verify other devices, granting them access to encrypted messages and marking them as trusted for other users.": "Eguneratu gailu hau honek beste gailuak egiaztatu ahal izateko, horrela zifratutako mezuetara sarbidea emanez eta beste erabiltzaileentzat fidagarri gisa markatzeko.",
+ "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.": "Ezarri zifratzea gailu honetan honek beste gailuak egiaztatu ahal izateko, horrela zifratutako mezuetara sarbidea emanez eta beste erabiltzaileentzat fidagarri gisa markatzeko.",
+ "Secure your encryption keys with a passphrase. For maximum security this should be different to your account password:": "Babestu zure zifratze gakoak pasa-esaldi batekin. Segurtasun gorenerako hau eta zure kontuaren pasahitza desberdinak izan beharko lukete:",
+ "Enter a passphrase": "Sartu pasa-esaldia",
+ "Enter your passphrase a second time to confirm it.": "Sartu zure pasa-esaldia berriro hau baieztatzeko.",
+ "This device can now verify other devices, granting them access to encrypted messages and marking them as trusted for other users.": "Gailu honek beste gailuak egiaztatu ditzake, horrela zifratutako mezuetara sarbidea emanez eta beste erabiltzaileentzat fidagarri gisa markatuz.",
+ "Verify other users in their profile.": "Egiaztatu beste erabiltzaileak bere profiletan.",
+ "Upgrade your encryption": "Eguneratu zure zifratzea",
+ "Set up encryption": "Ezarri zifratzea",
+ "Encryption upgraded": "Zifratzea eguneratuta",
+ "Encryption setup complete": "Zifratzearen ezarpena egina"
}
diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json
index d1c4f7379c..1ddcc55544 100644
--- a/src/i18n/strings/fr.json
+++ b/src/i18n/strings/fr.json
@@ -2075,5 +2075,41 @@
"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.": "Le coffre secret sera configuré en utilisant les détails existants de votre sauvegarde de clés. Votre phrase de passe et votre clé de récupération seront les mêmes que celles de votre sauvegarde de clés.",
"New Session": "Nouvelle session",
"Other users may not trust it": "D’autres utilisateurs pourraient ne pas lui faire confiance",
- "Later": "Plus tard"
+ "Later": "Plus tard",
+ "Verify User": "Vérifier l’utilisateur",
+ "For extra security, verify this user by checking a one-time code on both of your devices.": "Pour une meilleure sécurité, vérifiez cet utilisateur en comparant un code à usage unique sur vos deux appareils.",
+ "For maximum security, do this in person.": "Pour une sécurité maximale, faites-le en personne.",
+ "Start Verification": "Commencer la vérification",
+ "Encrypted by a deleted device": "Chiffré par un appareil supprimé",
+ "Unknown Command": "Commande inconnue",
+ "Unrecognised command: %(commandText)s": "Commande non reconnue : %(commandText)s",
+ "You can use /help
to list available commands. Did you mean to send this as a message?": "Vous pouvez utiliser /help
pour obtenir la liste des commandes disponibles. Vouliez-vous envoyer un message ?",
+ "Hint: Begin your message with //
to start it with a slash.": "Astuce : Votre message doit démarrer par //
pour commencer par une barre oblique.",
+ "Send as message": "Envoyer comme message",
+ "%(senderName)s added %(addedAddresses)s and %(count)s other addresses to this room|other": "%(senderName)s a ajouté %(addedAddresses)s et %(count)s autres adresses à ce salon",
+ "%(senderName)s removed %(removedAddresses)s and %(count)s other addresses from this room|other": "%(senderName)s a supprimé %(removedAddresses)s et %(count)s autres adresses de ce salon",
+ "%(senderName)s removed %(countRemoved)s and added %(countAdded)s addresses to this room": "%(senderName)s a supprimé %(countRemoved)s et ajouté %(countAdded)s adresses à ce salon",
+ "Reject & Ignore user": "Rejeter et ignorer l’utilisateur",
+ "Enter your account password to confirm the upgrade:": "Saisissez le mot de passe de votre compte pour confirmer la mise à niveau :",
+ "You'll need to authenticate with the server to confirm the upgrade.": "Vous devrez vous identifier avec le serveur pour confirmer la mise à niveau.",
+ "Upgrade this device to allow it to verify other devices, granting them access to encrypted messages and marking them as trusted for other users.": "Mettez à niveau cet appareil pour lui permettre de vérifier d’autres appareils, qui pourront alors accéder aux messages chiffrés et seront vus comme fiables par les autres utilisateurs.",
+ "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.": "Configurez le chiffrement sur cet appareil pour lui permettre de vérifier d’autres appareils, qui pourront alors accéder aux messages chiffrés et seront vus comme fiables par les autres utilisateurs.",
+ "Secure your encryption keys with a passphrase. For maximum security this should be different to your account password:": "Sécurisez vos clés de chiffrement avec une phrase de passe. Pour une sécurité maximale, elle devrait être différente du mot de passe de votre compte :",
+ "Enter a passphrase": "Saisissez une phrase de passe",
+ "Enter your passphrase a second time to confirm it.": "Saisissez votre phrase de passe une seconde fois pour la confirmer.",
+ "This device can now verify other devices, granting them access to encrypted messages and marking them as trusted for other users.": "Cet appareil peut à présent vérifier d’autres appareils, qui pourront alors accéder aux messages chiffrés et seront vus comme fiables par les autres utilisateurs.",
+ "Verify other users in their profile.": "Vérifiez d’autres utilisateurs dans leur profil.",
+ "Upgrade your encryption": "Mettre à niveau votre chiffrement",
+ "Set up encryption": "Configurer le chiffrement",
+ "Encryption upgraded": "Chiffrement mis à niveau",
+ "Encryption setup complete": "Configuration du chiffrement terminé",
+ "%(senderName)s turned on end-to-end encryption.": "%(senderName)s a activé le chiffrement de bout en bout.",
+ "%(senderName)s turned on end-to-end encryption (unrecognised algorithm %(algorithm)s).": "%(senderName)s a activé le chiffrement de bout en bout (algorithme %(algorithm)s non reconnu).",
+ "Someone is using an unknown device": "Quelqu'un utilise un appareil inconnu",
+ "This room is end-to-end encrypted": "Ce salon est chiffré de bout en bout",
+ "Everyone in this room is verified": "Tout le monde dans ce salon est vérifié",
+ "Invite only": "Uniquement sur invitation",
+ "Send a reply…": "Envoyer une réponse…",
+ "Send a message…": "Envoyer un message…",
+ "If you can't find someone, ask them for their username, share your username (%(userId)s) or profile link .": "Si vous n’arrivez pas à trouver quelqu’un, demandez-lui son nom d’utilisateur, partagez votre nom d’utilisateur (%(userId)s) ou votre lien de profil ."
}
diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json
index e5b9f637f1..d2b877a8de 100644
--- a/src/i18n/strings/hu.json
+++ b/src/i18n/strings/hu.json
@@ -138,7 +138,7 @@
"Failed to toggle moderator status": "Moderátor státuszt nem sikerült átállítani",
"Failed to unban": "Kizárás visszavonása sikertelen",
"Failed to upload profile picture!": "Profil kép feltöltése sikertelen!",
- "Failed to verify email address: make sure you clicked the link in the email": "E-mail cím ellenőrzése sikertelen: ellenőrizd, hogy az e-mailnél lévő linkre rákattintottál",
+ "Failed to verify email address: make sure you clicked the link in the email": "E-mail cím ellenőrzése sikertelen: ellenőrizd, hogy az e-mailben lévő hivatkozásra kattintottál",
"Failure to create room": "Szoba létrehozása sikertelen",
"Favourites": "Kedvencek",
"Fill screen": "Képernyő kitöltése",
@@ -355,7 +355,7 @@
"You need to be able to invite users to do that.": "Hogy ezt csinálhasd meg kell tudnod hívni felhasználókat.",
"You need to be logged in.": "Be kell jelentkezz.",
"Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "Ez az e-mail cím, úgy néz ki, nincs összekötve a Matrix azonosítóval ezen a Matrix szerveren.",
- "Your password was successfully changed. You will not receive push notifications on other devices until you log back in to them": "A jelszavadat sikeresen megváltoztattuk. Nem kapsz \"push\" értesítéseket amíg a többi eszközön vissza nem jelentkezel",
+ "Your password was successfully changed. You will not receive push notifications on other devices until you log back in to them": "A jelszavadat sikeresen megváltoztattuk. Addig nem fogsz leküldéses értesítéseket kapni, amíg a többi eszközön vissza nem jelentkezel",
"You seem to be in a call, are you sure you want to quit?": "Úgy tűnik hívásban vagy, biztosan kilépsz?",
"You seem to be uploading files, are you sure you want to quit?": "Úgy tűnik fájlokat töltesz fel, biztosan kilépsz?",
"You will not be able to undo this change as you are promoting the user to have the same power level as yourself.": "Nem leszel képes visszavonni ezt a változtatást mivel a felhasználót ugyanarra a szintre emeled amin te vagy.",
@@ -451,7 +451,7 @@
"Username available": "Szabad felhasználói név",
"Username not available": "A felhasználói név foglalt",
"Something went wrong!": "Valami tönkrement!",
- "If you already have a Matrix account you can log in instead.": "Ha már van Matrix fiókod akkor beléphetsz helyette.",
+ "If you already have a Matrix account you can log in instead.": "Ha már van Matrix fiókod, akkor beléphetsz helyette.",
"Your browser does not support the required cryptography extensions": "A böngésződ nem támogatja a szükséges titkosítási kiterjesztést",
"Not a valid Riot keyfile": "Nem érvényes Riot kulcsfájl",
"Authentication check failed: incorrect password?": "Azonosítás sikertelen: hibás jelszó?",
@@ -929,7 +929,7 @@
"To continue using the %(homeserverDomain)s homeserver you must review and agree to our terms and conditions.": "A %(homeserverDomain)s szerver használatának folytatásához el kell olvasnod és el kell fogadnod az általános szerződési feltételeket.",
"Review terms and conditions": "Általános Szerződési Feltételek elolvasása",
"To continue, please enter your password:": "Folytatáshoz add meg a jelszavad:",
- "This will make your account permanently unusable. You will not be able to log in, and no one will be able to re-register the same user ID. This will cause your account to leave all rooms it is participating in, and it will remove your account details from your identity server. This action is irreversible. ": "Ez végleg használhatatlanná teszi a fiókodat. Ezután nem fogsz tudni bejelentkezni, és más sem tud majd ezzel az azonosítóval fiókot létrehozni. Minden szobából amibe beléptél ki fogsz lépni, és törölni fogja minden fiók adatod az \"identity\" szerverről. Ez a művelet visszafordíthatatlan. ",
+ "This will make your account permanently unusable. You will not be able to log in, and no one will be able to re-register the same user ID. This will cause your account to leave all rooms it is participating in, and it will remove your account details from your identity server. This action is irreversible. ": "Ez végleg használhatatlanná teszi a fiókodat. Ezután nem fogsz tudni bejelentkezni, és más sem tud majd ezzel az azonosítóval fiókot létrehozni. Minden szobából amibe beléptél ki fogsz lépni, és törölni fogja minden fiók adatod az személyazonosságod biztosító szerverről. Ez a művelet visszafordíthatatlan. ",
"Deactivating your account does not by default cause us to forget messages you have sent. If you would like us to forget your messages, please tick the box below.": "A fiókod felfüggesztése nem jelenti alapértelmezetten azt, hogy az általad küldött üzenetek elfelejtődnek. Ha törölni szeretnéd az általad küldött üzeneteket, pipáld be a jelölőnégyzetet alul.",
"Message visibility in Matrix is similar to email. Our forgetting your messages means that messages you have sent will not be shared with any new or unregistered users, but registered users who already have access to these messages will still have access to their copy.": "Az üzenetek láthatósága a Matrix-ban hasonlít az emailhez. Az általad küldött üzenet törlése azt jelenti, hogy nem osztjuk meg új-, vagy vendég felhasználóval de a már regisztrált felhasználók akik már hozzáfértek az üzenethez továbbra is elérik a saját másolatukat.",
"Please forget all messages I have sent when my account is deactivated (Warning: this will cause future users to see an incomplete view of conversations)": "Kérlek töröld az összes általam küldött üzenetet amikor a fiókomat felfüggesztem (Figyelem: ez azt eredményezheti, hogy a jövőbeni felhasználók csak részleges beszélgetést látnak majd)",
@@ -981,7 +981,7 @@
"Please contact your service administrator to get this limit increased.": "A korlát emelése érdekében kérlek vedd fel a kapcsolatot a szolgáltatás adminisztrátorával .",
"This homeserver has hit its Monthly Active User limit so some users will not be able to log in .": "Ez a Matrix szerver elérte a havi aktív felhasználói korlátját néhány felhasználó nem fog tudni bejelentkezni .",
"This homeserver has exceeded one of its resource limits so some users will not be able to log in .": "Ez a Matrix szerver túllépte valamelyik erőforrás korlátját így néhány felhasználó nem tud majd bejelentkezni .",
- "Upgrade Room Version": "Szoba verziójának frissítése",
+ "Upgrade Room Version": "Szoba verziójának fejlesztése",
"Create a new room with the same name, description and avatar": "Készíts egy új szobát ugyanazzal a névvel, leírással és profilképpel",
"Update any local room aliases to point to the new room": "Állíts át minden helyi alternatív nevet erre a szobára",
"Stop users from speaking in the old version of the room, and post a message advising users to move to the new room": "A felhasználóknak tiltsd meg, hogy a régi szobában beszélgessenek. Küldj egy üzenetet amiben megkéred a felhasználókat, hogy menjenek át az új szobába",
@@ -996,9 +996,9 @@
"The conversation continues here.": "A beszélgetés itt folytatódik.",
"This room is a continuation of another conversation.": "Ebben a szobában folytatódik egy másik beszélgetés.",
"Click here to see older messages.": "Ide kattintva megnézheted a régi üzeneteket.",
- "Failed to upgrade room": "A szoba frissítése sikertelen",
- "The room upgrade could not be completed": "A szoba frissítését nem sikerült befejezni",
- "Upgrade this room to version %(version)s": "A szoba frissítése %(version)s verzióra",
+ "Failed to upgrade room": "A szoba fejlesztése sikertelen",
+ "The room upgrade could not be completed": "A szoba fejlesztését nem sikerült befejezni",
+ "Upgrade this room to version %(version)s": "A szoba fejlesztése %(version)s verzióra",
"Forces the current outbound group session in an encrypted room to be discarded": "A jelenlegi csoport munkamenet törlését kikényszeríti a titkosított szobában",
"Registration Required": "Regisztrációt igényel",
"You need to register to do this. Would you like to register now?": "Hogy ezt megtedd regisztrálnod kell. Szeretnél regisztrálni?",
@@ -1053,7 +1053,7 @@
"Print it and store it somewhere safe": "Nyomtad ki és tárold biztonságos helyen",
"Save it on a USB key or backup drive": "Mentsd el egy Pendrive-ra vagy a biztonsági mentésekhez",
"Copy it to your personal cloud storage": "Másold fel a személyes felhődbe",
- "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another device.": "A Biztonságos Üzenet Visszaállítás beállítása nélkül ha kijelentkezel vagy másik eszközt használsz, akkor nem tudod visszaállítani a régi titkosított üzeneteidet.",
+ "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another device.": "A Biztonságos üzenet-visszaállítás beállítása nélkül ha kijelentkezel vagy másik eszközt használsz, akkor nem tudod visszaállítani a régi titkosított üzeneteidet.",
"Set up Secure Message Recovery": "Biztonságos Üzenet Visszaállítás beállítása",
"Keep it safe": "Tartsd biztonságban",
"Create Key Backup": "Kulcs mentés készítése",
@@ -1140,9 +1140,9 @@
"Invite anyway and never warn me again": "Mindenképpen meghív és ne figyelmeztess többet",
"Invite anyway": "Mindenképpen meghív",
"Whether or not you're logged in (we don't record your username)": "Se akkor ha bejelentkezel se akkor ha nem; mi nem tároljuk a felhasználói nevedet",
- "Upgrades a room to a new version": "Szoba frissítése új verzióra",
+ "Upgrades a room to a new version": "Szoba fejlesztése új verzióra",
"Sets the room name": "Szobanév beállítása",
- "%(senderDisplayName)s upgraded this room.": "%(senderDisplayName)s frissítette a szobát.",
+ "%(senderDisplayName)s upgraded this room.": "%(senderDisplayName)s fejlesztette a szobát.",
"%(displayName)s is typing …": "%(displayName)s gépel …",
"%(names)s and %(count)s others are typing …|other": "%(names)s és %(count)s másik gépelnek …",
"%(names)s and %(count)s others are typing …|one": "%(names)s és még valaki gépelnek …",
@@ -1156,7 +1156,7 @@
"Show avatars in user and room mentions": "Profilkép mutatása a felhasználó és szoba említéseknél",
"Enable big emoji in chat": "Nagy Emojik engedélyezése a csevegésekben",
"Send typing notifications": "Gépelés visszajelzés küldése",
- "Enable Community Filter Panel": "Közösségi szűrő panel engedélyezése",
+ "Enable Community Filter Panel": "Közösségi szűrő panel bekapcsolása",
"Messages containing my username": "Üzenetek amik a nevemet tartalmazzák",
"The other party cancelled the verification.": "A másik fél törölte az ellenőrzést.",
"Verified!": "Ellenőrizve!",
@@ -1460,9 +1460,9 @@
"%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.": "%(senderName)s visszavonta %(targetDisplayName)s a szobába való belépéséhez szükséges meghívóját.",
"Enable desktop notifications for this device": "Asztali értesítések engedélyezése ehhez az eszközhöz",
"Enable audible notifications for this device": "Hallható értesítések engedélyezése ehhez az eszközhöz",
- "Upgrade this room to the recommended room version": "A szoba frissítése a javasolt verzióra",
+ "Upgrade this room to the recommended room version": "A szoba fejlesztése a javasolt verzióra",
"This room is running room version , which this homeserver has marked as unstable .": "A szoba verziója: , amit a Matrix szerver instabilnak tekint.",
- "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.": "A szoba frissítése bezárja ezt a szobát és új, frissített verzióval ugyanezen a néven létrehoz egy újat.",
+ "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.": "A szoba fejlesztése bezárja ezt a szobát és új, frissített verzióval ugyanezen a néven létrehoz egy újat.",
"Failed to revoke invite": "A meghívó visszavonása sikertelen",
"Could not revoke the invite. The server may be experiencing a temporary problem or you do not have sufficient permissions to revoke the invite.": "A meghívót nem lehet visszavonni. Vagy a szervernek átmenetileg problémái vannak vagy nincs megfelelő jogosultságod a meghívó visszavonásához.",
"Revoke invite": "Meghívó visszavonása",
@@ -1507,7 +1507,7 @@
"Sends the given message coloured as a rainbow": "A megadott üzenetet szivárvány színben küldi el",
"Sends the given emote coloured as a rainbow": "A megadott hangulatjelet szivárvány színben küldi el",
"The user's homeserver does not support the version of the room.": "A felhasználó matrix szervere nem támogatja a megadott szoba verziót.",
- "When rooms are upgraded": "Ha a szobák frissültek",
+ "When rooms are upgraded": "Ha a szobák fejlesztésre kerülnek",
"This device is not backing up your keys , but you do have an existing backup you can restore from and add to going forward.": "Ez az eszköz nem menti el a kulcsaidat , de létezik mentés amit visszaállíthatsz és folytathatod.",
"Connect this device to key backup before signing out to avoid losing any keys that may only be on this device.": "Csatlakozz ezzel az eszközzel a kulcs mentéshez kilépés előtt, hogy ne veszíts el kulcsot ami esetleg csak ezen az eszközön van meg.",
"Connect this device to Key Backup": "Csatlakozz ezzel az eszközzel a Kulcs Mentéshez",
@@ -1538,7 +1538,7 @@
"This room doesn't exist. Are you sure you're at the right place?": "Ez a szoba nem létezik. Biztos, hogy jó helyen vagy?",
"Try again later, or ask a room admin to check if you have access.": "Próbálkozz később vagy kérd meg a szoba adminisztrátorát, hogy nézze meg van-e hozzáférésed.",
"%(errcode)s was returned while trying to access the room. If you think you're seeing this message in error, please submit a bug report .": "Amikor a szobát próbáltuk elérni ezt a hibaüzenetet kaptuk: %(errcode)s. Ha úgy gondolod, hogy ez egy hiba légy szívesnyiss egy hibajegyet .",
- "This room has already been upgraded.": "Ez a szoba már frissült.",
+ "This room has already been upgraded.": "Ez a szoba már fejlesztve van.",
"Rotate Left": "Balra forgat",
"Rotate Right": "Jobbra forgat",
"View Servers in Room": "Szerverek megjelenítése a szobában",
@@ -1690,7 +1690,7 @@
"If you don't want to use to discover and be discoverable by existing contacts you know, enter another identity server below.": "Ha felkutatásra és, hogy más ismerősök megtalálhassanak, nem akarod használni ezt a szervert: , akkor adjál meg másik azonosítási szervert alább.",
"Using an identity server is optional. If you choose not to use an identity server, you won't be discoverable by other users and you won't be able to invite others by email or phone.": "Azonosítási szerver használata nem kötelező. Ha úgy döntesz, hogy az azonosítási szervert nem használod más felhasználók nem találnak rád és másokat sem tudsz e-mail cím vagy telefonszám alapján meghívni.",
"Do not use an identity server": "Az azonosítási szerver mellőzése",
- "Upgrade the room": "Szoba frissítése",
+ "Upgrade the room": "Szoba fejlesztése",
"Set an email for account recovery. Use email or phone to optionally be discoverable by existing contacts.": "E-mail cím beállítása a fiók visszaállításához. E-mail cím vagy telefonszám, hogy ismerősök megtalálhassanak.",
"Set an email for account recovery. Use email to optionally be discoverable by existing contacts.": "E-mail cím beállítása a fiók visszaállításához. E-mail cím, hogy ismerősök megtalálhassanak.",
"Enter your custom homeserver URL What does this mean? ": "Add meg a matrix szervered URL-jét Mit jelent ez? ",
@@ -1930,12 +1930,12 @@
"This message cannot be decrypted": "Ezt az üzenetet nem lehet visszafejteni",
"Unencrypted": "Titkosítatlan",
"Automatically invite users": "Felhasználók automatikus meghívása",
- "Upgrade private room": "Privát szoba frissítése",
- "Upgrade public room": "Nyilvános szoba frissítése",
+ "Upgrade private room": "Privát szoba fejlesztése",
+ "Upgrade public room": "Nyilvános szoba fejlesztése",
"Upgrading a room is an advanced action and is usually recommended when a room is unstable due to bugs, missing features or security vulnerabilities.": "A szoba frissítése nem egyszerű művelet, általában a szoba hibás működése, hiányzó funkció vagy biztonsági sérülékenység esetén javasolt.",
"This usually only affects how the room is processed on the server. If you're having problems with your Riot, please report a bug .": "Ez általában a szoba szerver oldali kezelésében jelent változást. Ha a Riotban van problémád, kérlek küldj egy hibajelentést .",
- "You'll upgrade this room from to .": " verzióról verzióra frissíted a szobát.",
- "Upgrade": "Frissítés",
+ "You'll upgrade this room from to .": " verzióról verzióra fejleszted a szobát.",
+ "Upgrade": "Fejlesztés",
"Notification settings": "Értesítések beállítása",
"User Status": "Felhasználó állapota",
"Reactions": "Reakciók",
@@ -2076,5 +2076,35 @@
"Something went wrong trying to invite the users.": "Valami nem sikerült a felhasználók meghívásával.",
"We couldn't invite those users. Please check the users you want to invite and try again.": "Ezeket a felhasználókat nem tudtuk meghívni. Ellenőrizd azokat a felhasználókat akiket meg szeretnél hívni és próbáld újra.",
"Recently Direct Messaged": "Nemrég küldött Közvetlen Üzenetek",
- "If you can't find someone, ask them for their username (e.g. @user:server.com) or share this room .": "Ha nem találsz valakit, akkor kérdezd meg a felhasználói nevét (pl.: @felhasználó:szerver.com) vagy oszd meg ezt a szobát ."
+ "If you can't find someone, ask them for their username (e.g. @user:server.com) or share this room .": "Ha nem találsz valakit, akkor kérdezd meg a felhasználói nevét (pl.: @felhasználó:szerver.com) vagy oszd meg ezt a szobát .",
+ "Verify User": "Felhasználó ellenőrzése",
+ "For extra security, verify this user by checking a one-time code on both of your devices.": "A biztonság fokozásáért ellenőrizd ezt a felhasználót egy egyszeri kód egyeztetésével mindkettőtök készülékén.",
+ "For maximum security, do this in person.": "A legnagyobb biztonság érdekében ezt személyesen tedd meg.",
+ "Start Verification": "Ellenőrzés elindítása",
+ "Encrypted by a deleted device": "Egy már törölt eszköz titkosította",
+ "Unknown Command": "Ismeretlen Parancs",
+ "Unrecognised command: %(commandText)s": "Ismeretlen parancs: %(commandText)s",
+ "You can use /help
to list available commands. Did you mean to send this as a message?": "Használhatod a /help
-et az elérhető parancsok kilistázásához. Ezt üzenetként akartad küldeni?",
+ "Hint: Begin your message with //
to start it with a slash.": "Tipp: Ez üzenetedet kezd ezzel: //
, ha perjellel szeretnéd kezdeni.",
+ "Send as message": "Üzenet küldése",
+ "%(senderName)s added %(addedAddresses)s and %(count)s other addresses to this room|other": "%(senderName)s szoba címnek, %(count)s másikkal együtt, hozzáadta: %(addedAddresses)s",
+ "%(senderName)s removed %(removedAddresses)s and %(count)s other addresses from this room|other": "%(senderName)s %(count)s másikkal együtt törölte a szoba címek közül: %(removedAddresses)s",
+ "%(senderName)s removed %(countRemoved)s and added %(countAdded)s addresses to this room": "%(senderName)s %(countRemoved)s darabot törölt és %(countAdded)s darabot hozzáadott a szoba címekhez",
+ "Someone is using an unknown device": "Valaki ismeretlen eszközt használ",
+ "This room is end-to-end encrypted": "Ez a szoba végpontok közötti titkosítást használ",
+ "Everyone in this room is verified": "A szobába mindenki ellenőrizve van",
+ "Invite only": "Csak meghívóval",
+ "Send a reply…": "Válasz küldése…",
+ "Send a message…": "Üzenet küldése…",
+ "Reject & Ignore user": "Felhasználó elutasítása és figyelmen kívül hagyása",
+ "If you can't find someone, ask them for their username, share your username (%(userId)s) or profile link .": "Ha nem találsz valakit, akkor kérdezd meg a felhasználói nevét, áruld el neki a felhasználói nevedet (%(userId)s) vagy a profil hivatkozásodat .",
+ "Enter your account password to confirm the upgrade:": "A fejlesztés megerősítéséhez add meg a fiók jelszavadat:",
+ "You'll need to authenticate with the server to confirm the upgrade.": "Azonosítanod kell magad a szerveren a fejlesztés megerősítéséhez.",
+ "Enter a passphrase": "Jelmondat bevitele",
+ "Enter your passphrase a second time to confirm it.": "Add meg a jelmondatot másodszor is a biztonság kedvéért.",
+ "Verify other users in their profile.": "Más felhasználók ellenőrzése a profiljukban.",
+ "Upgrade your encryption": "Titkosításod fejlesztése",
+ "Set up encryption": "Titkosítás beállítása",
+ "Encryption upgraded": "Titkosítás fejlesztve",
+ "Encryption setup complete": "Titkosítás beállítása kész"
}
diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json
index bb35bd6d69..c8844fb2bb 100644
--- a/src/i18n/strings/it.json
+++ b/src/i18n/strings/it.json
@@ -2055,5 +2055,57 @@
"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.": "Il Backup Chiavi è attivo sul tuo account ma non è stato impostato da questa sessione. Per impostare un archivio segreto, ripristina il tuo backup chiavi.",
"Restore": "Ripristina",
"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": "L'archivio segreto verrà impostato usando i dettagli esistenti del backup chiavi. La password dell'archivio segreto e la chiave di recupero saranno le stesse del backup chiavi",
- "Restore your Key Backup": "Ripristina il tuo Backup Chiavi"
+ "Restore your Key Backup": "Ripristina il tuo Backup Chiavi",
+ "New Session": "Nuova sessione",
+ "New invite dialog": "Nuova finestra di invito",
+ "Other users may not trust it": "Altri utenti potrebbero non fidarsi",
+ "Later": "Più tardi",
+ "Failed to invite the following users to chat: %(csvUsers)s": "Impossibile invitare i seguenti utenti alla chat: %(csvUsers)s",
+ "We couldn't create your DM. Please check the users you want to invite and try again.": "Impossibile creare il messaggio diretto. Ricontrolla gli utenti che vuoi invitare e riprova.",
+ "Something went wrong trying to invite the users.": "Qualcosa è andato storto provando ad invitare gli utenti.",
+ "We couldn't invite those users. Please check the users you want to invite and try again.": "Impossibile invitare quegli utenti. Ricontrolla gli utenti che vuoi invitare e riprova.",
+ "Recently Direct Messaged": "Contattati direttamente di recente",
+ "If you can't find someone, ask them for their username (e.g. @user:server.com) or share this room .": "Se non riesci a trovare qualcuno, chiedi il nome utente (es. @utente:server.it) o condividi questa stanza .",
+ "Complete security": "Sicurezza completa",
+ "Verify this session to grant it access to encrypted messages.": "Verifica questa sessione per concederle accesso ai messaggi cifrati.",
+ "Start": "Inizia",
+ "Session verified": "Sessione verificata",
+ "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "La tua sessione ora è verificata. Ha accesso ai tuoi messaggi cifrati e gli altri utenti la vedranno come fidata.",
+ "Done": "Fatto",
+ "Without completing security on this device, it won’t have access to encrypted messages.": "Se non completi la sicurezza su questo dispositivo, esso non avrà accesso ai messaggi cifrati.",
+ "Go Back": "Torna",
+ "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.": "L'archivio segreto verrà impostato usando i dettagli del backup chiavi esistente. La password dell'archivio segreto e la chiave di ripristino saranno le stesse del backup chiavi.",
+ "Encrypted by a deleted device": "Cifrato da un dispositivo eliminato",
+ "Verify User": "Verifica utente",
+ "For extra security, verify this user by checking a one-time code on both of your devices.": "Per maggiore sicurezza, verifica questo utente controllando un codice univoco sui vostri dispositivi.",
+ "For maximum security, do this in person.": "Per massima sicurezza, fatelo di persona.",
+ "Start Verification": "Inizia la verifica",
+ "%(senderName)s added %(addedAddresses)s and %(count)s other addresses to this room|other": "%(senderName)s ha aggiunto %(addedAddresses)s e %(count)s altri indirizzi a questa stanza",
+ "%(senderName)s removed %(removedAddresses)s and %(count)s other addresses from this room|other": "%(senderName)s ha rimosso %(removedAddresses)s e %(count)s altri indirizzi da questa stanza",
+ "%(senderName)s removed %(countRemoved)s and added %(countAdded)s addresses to this room": "%(senderName)s ha rimosso %(countRemoved)s e aggiunto %(countAdded)s indirizzi a questa stanza",
+ "Someone is using an unknown device": "Qualcuno sta usando un dispositivo sconosciuto",
+ "This room is end-to-end encrypted": "Questa stanza è cifrata end-to-end",
+ "Everyone in this room is verified": "Tutti in questa stanza sono verificati",
+ "Invite only": "Solo a invito",
+ "Send a reply…": "Invia risposta…",
+ "Send a message…": "Invia un messaggio…",
+ "Reject & Ignore user": "Rifiuta e ignora l'utente",
+ "Unknown Command": "Comando sconosciuto",
+ "Unrecognised command: %(commandText)s": "Comando non riconosciuto: %(commandText)s",
+ "You can use /help
to list available commands. Did you mean to send this as a message?": "Puoi usare /help
per elencare i comandi disponibili. Volevo forse inviarlo come messaggio?",
+ "Hint: Begin your message with //
to start it with a slash.": "Suggerimento: anteponi al tuo messaggio //
per farlo iniziare con uno slash.",
+ "Send as message": "Invia come messaggio",
+ "Enter your account password to confirm the upgrade:": "Inserisci la password del tuo account per confermare l'aggiornamento:",
+ "You'll need to authenticate with the server to confirm the upgrade.": "Dovrai autenticarti con il server per confermare l'aggiornamento.",
+ "Upgrade this device to allow it to verify other devices, granting them access to encrypted messages and marking them as trusted for other users.": "Aggiorna il dispositivo per consentirgli di verificare altri dispositivi, dando loro accesso ai messaggi cifrati e contrassegnandoli come fidati per gli altri utenti.",
+ "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.": "Imposta la cifratura sul dispositivo per consentirgli di verificare altri dispositivi, dando loro accesso ai messaggi cifrati e contrassegnandoli come fidati per gli altri utenti.",
+ "Secure your encryption keys with a passphrase. For maximum security this should be different to your account password:": "Proteggi le chiavi di cifratura con una password. Per massima sicurezza questa dovrebbe essere diversa da quella del tuo account:",
+ "Enter a passphrase": "Inserisci una password",
+ "Enter your passphrase a second time to confirm it.": "Inserisci di nuovo la tua password per confermarla.",
+ "This device can now verify other devices, granting them access to encrypted messages and marking them as trusted for other users.": "Questo dispositivo ora può verificare altri dispositivi, dando loro accesso ai messaggi cifrati e contrassegnandoli come fidati per gli altri utenti.",
+ "Verify other users in their profile.": "Verifica gli altri utenti nel loro profilo.",
+ "Upgrade your encryption": "Aggiorna la tua cifratura",
+ "Set up encryption": "Imposta la cifratura",
+ "Encryption upgraded": "Cifratura aggiornata",
+ "Encryption setup complete": "Impostazione cifratura completata"
}
diff --git a/src/i18n/strings/ja.json b/src/i18n/strings/ja.json
index 55cc8782ef..787dcbc4ca 100644
--- a/src/i18n/strings/ja.json
+++ b/src/i18n/strings/ja.json
@@ -601,7 +601,7 @@
"Copied!": "コピーされました!",
"Failed to copy": "コピーに失敗しました",
"Add an Integration": "統合を追加する",
- "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?": "サードパーティのサイトに移動して、%(integationsUrl)s で使用するためにアカウントを認証できるようになります。続行しますか?",
+ "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?": "サードパーティのサイトに移動して、%(integrationsUrl)s で使用するためにアカウントを認証できるようになります。続行しますか?",
"Removed or unknown message type": "削除されたまたは未知のメッセージタイプ",
"Message removed by %(userId)s": "%(userId)s によってメッセージが削除されました",
"Message removed": "メッセージが削除された",
@@ -630,7 +630,7 @@
"Are you sure you want to remove '%(roomName)s' from %(groupId)s?": "%(roomName)s を %(groupId)s から削除してもよろしいですか?",
"Removing a room from the community will also remove it from the community page.": "コミュニティから部屋を削除すると、コミュニティページからもその部屋が削除されます。",
"Failed to remove room from community": "コミュニティからの部屋の削除に失敗しました",
- "Failed to remove '%(roomName)s' from %(groupId)s": "%(groupName)s から '%(roomName)s' を削除できませんでした",
+ "Failed to remove '%(roomName)s' from %(groupId)s": "%(groupId)s から '%(roomName)s' を削除できませんでした",
"Something went wrong!": "何かが間違っていた!",
"The visibility of '%(roomName)s' in %(groupId)s could not be updated.": "%(groupId)s の '%(roomName)s' の表示を更新できませんでした。",
"Visibility in Room List": "ルームリストの可視性",
@@ -668,7 +668,7 @@
"%(severalUsers)sjoined %(count)s times|one": "%(severalUsers)s が参加しました",
"%(oneUser)sjoined %(count)s times|other": "%(oneUser)s が %(count)s 回参加しました",
"%(oneUser)sjoined %(count)s times|one": "%(oneUser)s が参加しました",
- "%(severalUsers)sleft %(count)s times|other": "%(severalUers)s は %(count)s 回退出しました",
+ "%(severalUsers)sleft %(count)s times|other": "%(severalUsers)s は %(count)s 回退出しました",
"%(severalUsers)sleft %(count)s times|one": "%(severalUsers)s は退出しました",
"%(oneUser)sleft %(count)s times|other": "%(oneUser)s は %(count)s 回退出しました",
"%(oneUser)sleft %(count)s times|one": "%(oneUser)s は退出しました",
diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json
index e833009d59..3c04ff85c0 100644
--- a/src/i18n/strings/sq.json
+++ b/src/i18n/strings/sq.json
@@ -1917,11 +1917,11 @@
"%(senderName)s placed a voice call. (not supported by this browser)": "%(senderName)s bëri një thirrje zanore. (e pambuluar nga ky shfletues)",
"%(senderName)s placed a video call.": "%(senderName)s bëri një thirrje video.",
"%(senderName)s placed a video call. (not supported by this browser)": "%(senderName)s bëri një thirrje video. (e pambuluar nga ky shfletues)",
- "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)": "Aktivizoni cross-signing për të verifikuar me bazë përdorues në vend se me bazë pajisje (në zhvillim)",
"Enable local event indexing and E2EE search (requires restart)": "Aktivizoni indeksim aktesh vendore dhe kërkim E2EE (lyp rinisje)",
"Match system theme": "Përputhe me temën e sistemit",
- "Send cross-signing keys to homeserver": "",
- "Cross-signing public keys:": "",
+ "Send cross-signing keys to homeserver": "Dërgo te shërbyesi Home kyçe cross-signing ",
+ "Cross-signing public keys:": "Kyçe publikë për cross-signing :",
"on device": "në pajisje",
"not found": "s’u gjet",
"in secret storage": "në depozitë të fshehtë",
@@ -2016,5 +2016,88 @@
"Connected to on ": "Lidhur me në ",
"Connected via %(protocolName)s": "Lidhur përmes %(protocolName)s",
"Bridge Info": "Të dhëna Ure",
- "Below is a list of bridges connected to this room.": "Më poshtë keni një listë urash të lidhura në këtë dhomë."
+ "Below is a list of bridges connected to this room.": "Më poshtë keni një listë urash të lidhura në këtë dhomë.",
+ "New Session": "Sesion i Ri",
+ "%(senderName)s added %(addedAddresses)s and %(count)s other addresses to this room|other": "%(senderName)s shtoi %(addedAddresses)s dhe dhe %(count)s adresa të tjera te kjo dhomë",
+ "%(senderName)s removed %(removedAddresses)s and %(count)s other addresses from this room|other": "%(senderName)s hoqi %(removedAddresses)s dhe %(count)s adresa të tjera nga kjo dhomë",
+ "%(senderName)s removed %(countRemoved)s and added %(countAdded)s addresses to this room": "%(senderName)s hoqi %(countRemoved)s dhe shtoi %(countAdded)s adresa te kjo dhomë",
+ "%(senderName)s turned on end-to-end encryption.": "%(senderName)s aktivizoi fshehtëzimin skaj-më-skaj.",
+ "%(senderName)s turned on end-to-end encryption (unrecognised algorithm %(algorithm)s).": "%(senderName)s aktivizoi fshehtëzimin skaj-më-skaj (algoritëm jo i pranuar %(algorithm)s).",
+ "a few seconds ago": "pak sekonda më parë",
+ "about a minute ago": "rreth një minutë më parë",
+ "%(num)s minutes ago": "%(num)s minuta më parë",
+ "about an hour ago": "rreth një orë më parë",
+ "%(num)s hours ago": "%(num)s orë më parë",
+ "about a day ago": "rreth një ditë më parë",
+ "%(num)s days ago": "%(num)s ditë më parë",
+ "a few seconds from now": "pak sekonda nga tani",
+ "about a minute from now": "rreth një minutë nga tani",
+ "%(num)s minutes from now": "%(num)s minuta nga tani",
+ "about an hour from now": "rreth një orë nga tani",
+ "%(num)s hours from now": "%(num)s orë nga tani",
+ "about a day from now": "rreth një ditë nga tani",
+ "%(num)s days from now": "%(num)s ditë nga tani",
+ "Show a presence dot next to DMs in the room list": "Shfaqni një pikë pranie në krah DM-sh te lista e dhomave",
+ "Other users may not trust it": "Përdorues të tjerë mund të mos e besojnë",
+ "Later": "Më vonë",
+ "Cross-signing and secret storage are enabled.": "Cross-signing dhe depozitimi i fshehtë janë aktivizuar.",
+ "Your account has a cross-signing identity in secret storage, but it is not yet trusted by this device.": "Llogaria juaj ka një identitet cross-signing në depozitë të fshehtë, por s’është ende i besuar në këtë pajisje.",
+ "Cross-signing and secret storage are not yet set up.": "Cross-signing dhe depozitimi i fshehtë s’janë ujdisur ende.",
+ "Cross-signing private keys:": "Kyçe privatë për cross-signing :",
+ "Backup key stored in secret storage, but this feature is not enabled on this device. Please enable cross-signing in Labs to modify key backup state.": "Kyçi i kopjeruajtjeve u depozitua në depozitë të fshehtë, po kjo veçori s’është e aktivizuar në këtë pajisje. Ju lutemi, aktivizoni në Labs cross-signing që të modifikoni gjendje kopjeruatjeje kyçesh.",
+ "Labs": "Labs",
+ "Complete": "E plotë",
+ "Someone is using an unknown device": "Dikush po përdor një pajisje të panjohur",
+ "This room is end-to-end encrypted": "Kjo dhomë është e fshehtëzuar skaj-më-skaj",
+ "Everyone in this room is verified": "Gjithkush në këtë dhomë është verifikuar",
+ "Encrypted by a deleted device": "Fshehtëzuar nga një pajisje e fshirë",
+ "Invite only": "Vetëm me ftesa",
+ "Send a reply…": "Dërgoni një përgjigje…",
+ "Send a message…": "Dërgoni një mesazh…",
+ "Reject & Ignore user": "Hidhe poshtë & Shpërfille përdoruesin",
+ "Unknown Command": "Urdhër i Panjohur",
+ "Unrecognised command: %(commandText)s": "Urdhër Jo i Pranuar: %(commandText)s",
+ "You can use /help
to list available commands. Did you mean to send this as a message?": "Mund të përdorni /help
që të shfaqen urdhrat e gatshëm. Donit vërtet ta dërgoni këtë si një mesazh?",
+ "Hint: Begin your message with //
to start it with a slash.": "Ndihmëz: Fillojeni mesazhin tuaj me //
që të nisë me një pjerrake.",
+ "Send as message": "Dërgoni një mesazh",
+ "Verify User": "Verifikoni Përdoruesin",
+ "For extra security, verify this user by checking a one-time code on both of your devices.": "Për siguri ekstra, verifikojeni këtë përdorues duke kontrolluar në të dyja pajisjet tuaja një kod njëpërdorimsh.",
+ "For maximum security, do this in person.": "Për siguri maksimum, bëjeni këtë ju vetë.",
+ "Start Verification": "Fillo Verifikimin",
+ "Failed to invite the following users to chat: %(csvUsers)s": "S’u arrit të ftoheshin për bisedë përdoruesit vijues: %(csvUsers)s",
+ "We couldn't create your DM. Please check the users you want to invite and try again.": "S’e krijuam dot DM-në tuaj. Ju lutemi, kontrolloni përdoruesit që doni të ftoni dhe riprovoni.",
+ "Something went wrong trying to invite the users.": "Diç shkoi ters teksa provohej të ftoheshin përdoruesit.",
+ "We couldn't invite those users. Please check the users you want to invite and try again.": "S’i ftuam dot këta përdorues. Ju lutemi, kontrolloni përdoruesit që doni të ftoni dhe riprovoni.",
+ "Failed to find the following users": "S’u arrit të gjendeshin përdoruesit vijues",
+ "The following users might not exist or are invalid, and cannot be invited: %(csvNames)s": "Përdoruesit vijues mund të mos ekzistojnë ose janë të pavlefshëm, dhe s’mund të ftohen: %(csvNames)s",
+ "Suggestions": "Sugjerime",
+ "If you can't find someone, ask them for their username, share your username (%(userId)s) or profile link .": "Nëse s’gjeni dot dikë, kërkojini emrin e tij të përdoruesit, tregojuni emrin tuaj të përdoruesit (%(userId)s) ose lidhjen e profilit .",
+ "If you can't find someone, ask them for their username (e.g. @user:server.com) or share this room .": "Nëse s’gjeni dot dikë, kërkojini emrin e tij të përdoruesit (p.sh., @përdorues:shërbyes.com) ose tregojuni këtë dhomë .",
+ "Access your secure message history and your cross-signing identity for verifying other devices by entering your passphrase.": "Për verifikim pajisjesh të tjera përmes dhënies së frazëkalimit tuaj, hyni te historiku i mesazheve tuaj të sigurt dhe identiteti juaj për cross-signing .",
+ "Access your secure message history and your cross-signing identity for verifying other devices by entering your recovery key.": "Për verifikim pajisjesh të tjera përmes dhënies së kyçit tuaj të rimarrjes, hyni te historiku i mesazheve tuaj të sigurt dhe identiteti juaj për cross-signing .",
+ "Complete security": "Siguri të plotë",
+ "Verify this session to grant it access to encrypted messages.": "Verifikojeni këtë sesion që t’i akordohet hyrje te mesazhe të fshehtëzuar.",
+ "Start": "Nise",
+ "Session verified": "Sesion i verifikuar",
+ "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "Sesioni juaj i ri tani është i verifikuar. Ka hyrje te mesazhet tuaj të fshehtëzuar dhe përdoruesit e tjerë do ta shohin si të besuar.",
+ "Done": "U bë",
+ "Without completing security on this device, it won’t have access to encrypted messages.": "Pa plotësuar sigurinë në këtë pajisje, s’do të ketë hyrje te mesazhe të fshehtëzuar.",
+ "Go Back": "Shko Mbrapsht",
+ "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.": "Kopjeruajtja e Kyçeve është e aktivizuar në llogarinë tuaj, por nuk është ujdisur që nga ky sesion. Që të ujdisni depozitim të fshehtë, riktheni kopjeruajtjen tuaj të kyçeve.",
+ "Restore": "Riktheje",
+ "Enter your account password to confirm the upgrade:": "Që të ripohohet përmirësimi, jepni fjalëkalimin e llogarisë tuaj:",
+ "You'll need to authenticate with the server to confirm the upgrade.": "Do t’ju duhet të bëni mirëfilltësimin me shërbyesin që të ripohohet përmirësimi.",
+ "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ërmirësojeni këtë pajisje për ta lejuar të verifikojë pajisje të tjera, duke u akorduar hyrje te mesazhe të fshehtëzuar dhe duke u vënë shenjë si të besuara për përdorues të tjerë.",
+ "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.": "Ujdisni fshehtëzim në këtë pajisje që ta lejoni të verifikojë pajisje të tjera, duke u akorduar atyre hyrje te mesazhe të fshehtëzuar dhe duke u vënë shenjë atyre si të besuara për përdorues të tjerë.",
+ "Secure your encryption keys with a passphrase. For maximum security this should be different to your account password:": "Sigurojini kyçet tuaj të fshehtëzimit me një frazëkalim. Për siguri maksimale, ky do të duhej të ishte i ndryshëm nga fjalëkalimi për llogarinë tuaj:",
+ "Enter a passphrase": "Jepni një frazëkalim",
+ "Enter your passphrase a second time to confirm it.": "Që të ripohohet, jepeni edhe një herë frazëkalimin tuaj.",
+ "This device can now verify other devices, granting them access to encrypted messages and marking them as trusted for other users.": "Kjo pajisje mund të verifikojë pajisje të tjera, duke u akorduar hyrje te mesazhe të fshehtëzuar dhe duke u vënë shenjë si të besuara për përdorues të tjerë.",
+ "Verify other users in their profile.": "Verifikoni përdorues të tjerë në profilin e tyre.",
+ "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.": "Pa ujdisur depozitim të fshehtë, s’do të jeni në gjendje të rimerrni hyrje te mesazhe të fshehtëzuar apo te identiteti juaj cross-signing për verifikim pajisjesh të tjera, nëse dilni nga llogaria juaj ose përdorni një pajisje tjetër.",
+ "Restore your Key Backup": "Riktheni Kopjeruajtjen tuaj të Kyçeve",
+ "Upgrade your encryption": "Përmirësoni fshehtëzimin tuaj",
+ "Set up encryption": "Ujdisni fshehtëzim",
+ "Encryption upgraded": "U përmirësua fshehtëzimi",
+ "Encryption setup complete": "Ujdisje fshehtëzimit e plotësuar"
}
diff --git a/src/i18n/strings/tr.json b/src/i18n/strings/tr.json
index 81f0522922..257cb27b42 100644
--- a/src/i18n/strings/tr.json
+++ b/src/i18n/strings/tr.json
@@ -1422,5 +1422,121 @@
"Enable Community Filter Panel": "Toluluk Filtre Panelini Aç",
"Match system theme": "Sistem temasıyla eşle",
"Allow Peer-to-Peer for 1:1 calls": "1:1 çağrılar için eşten-eşe izin ver",
- "Missing media permissions, click the button below to request.": "Medya izinleri eksik, alttaki butona tıkayarak talep edin."
+ "Missing media permissions, click the button below to request.": "Medya izinleri eksik, alttaki butona tıkayarak talep edin.",
+ "Credits": "Katkıda Bulunanlar",
+ "Clear cache and reload": "Belleği temizle ve yeniden yükle",
+ "Customise your experience with experimental labs features. Learn more .": "Deneysel laboratuar özellikler ile deneyiminizi özelleştirebilirsiniz. Daha fazla .",
+ "Ignored/Blocked": "Yoksayılan/Bloklanan",
+ "Error adding ignored user/server": "Yoksayılan kullanıcı/sunucu eklenirken hata",
+ "Error subscribing to list": "Listeye abone olunurken hata",
+ "Error removing ignored user/server": "Yoksayılan kullanıcı/sunucu silinirken hata",
+ "Error unsubscribing from list": "Listeden abonelikten çıkılırken hata",
+ "You are not subscribed to any lists": "Herhangi bir listeye aboneliğiniz bulunmuyor",
+ "⚠ These settings are meant for advanced users.": "⚠ Bu ayarlar ileri düzey kullanıcılar içindir.",
+ "Unignore": "Yoksayma",
+ "This bridge was provisioned by ": "Bu körpü tarafından provize edildi",
+ "Connected to on ": " ağındaki kanala bağlandı",
+ "Below is a list of bridges connected to this room.": "Bu odaya bağlanmış köprülerin bir listesi alttadır.",
+ "Change room avatar": "Oda resmini değiştir",
+ "Members only (since the point in time of selecting this option)": "Sadece üyeler ( bu seçeneği seçtiğinizden itibaren)",
+ "Unable to revoke sharing for email address": "E-posta adresi paylaşımı kaldırılamadı",
+ "Unable to revoke sharing for phone number": "Telefon numarası paylaşımı kaldırılamıyor",
+ "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Bu sayfadaki oda, kullanıcı veya grup ID si gibi betimleyici bilgiler sunucuya gönderilmeden önce silindi.",
+ "Please ask the administrator of your homeserver (%(homeserverDomain)s
) to configure a TURN server in order for calls to work reliably.": "Çağrıların sağlıklı bir şekide yapılabilmesi için lütfen anasunucunuzun (%(homeserverDomain)s
) yöneticisinden bir TURN sunucusu yapılandırmasını isteyin.",
+ "%(severalUsers)schanged their name %(count)s times|other": "%(severalUsers)s kullanıcıları isimlerini %(count)s kez değiştirdiler",
+ "%(oneUser)schanged their name %(count)s times|other": "%(oneUser)s ismini %(count)s kez değiştirdi",
+ "%(severalUsers)smade no changes %(count)s times|one": "%(severalUsers)s değişiklik yapmadı",
+ "%(oneUser)smade no changes %(count)s times|other": "%(oneUser)s %(count)s kez değişiklik yapmadı",
+ "%(oneUser)smade no changes %(count)s times|one": "%(oneUser)s değişiklik yapmadı",
+ "Room alias": "Oda lakabı",
+ "Please provide a room alias": "Lütfen bir oda lakabı belirtin",
+ "This alias is available to use": "Bu lakap kullanmaya uygun",
+ "This alias is already in use": "Bu lakap zaten kullanımda",
+ "And %(count)s more...|other": "ve %(count)s kez daha...",
+ "Alternatively, you can try to use the public server at turn.matrix.org
, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Alternatif olarak,turn.matrix.org
adresindeki herkese açık sunucuyu kullanmayı deneyebilirsiniz. Fakat bu güvenilir olmayabilir. IP adresiniz bu sunucu ile paylaşılacaktır. Ayarlardan yönetebilirsiniz.",
+ "An error ocurred whilst trying to remove the widget from the room": "Görsel bileşen odadan silinmeye çalışılırken bir hata oluştu",
+ "Minimize apps": "Uygulamaları küçült",
+ "Maximize apps": "Uygulamaları büyült",
+ "Popout widget": "Görsel bileşeni göster",
+ "Please create a new issue on GitHub so that we can investigate this bug.": "Lütfen GitHub’da Yeni bir talep oluşturun ki bu hatayı inceleyebilelim.",
+ "Rotate counter-clockwise": "Saat yönünün tersine döndür",
+ "Language Dropdown": "Dil Listesi",
+ "%(severalUsers)sleft %(count)s times|other": "%(severalUsers)s, %(count)s kez ayrıldı",
+ "%(oneUser)sleft %(count)s times|other": "%(oneUser)s %(count)s kez ayrıldı",
+ "%(severalUsers)sjoined and left %(count)s times|other": "%(severalUsers)s %(count)s kez katılıp ve ayrıldı",
+ "%(oneUser)sjoined and left %(count)s times|other": "%(oneUser)s %(count)s kez katıldı ve ayrıldı",
+ "%(severalUsers)sleft and rejoined %(count)s times|one": "%(severalUsers)s ayrıldı ve yeniden katıldı",
+ "%(severalUsers)srejected their invitations %(count)s times|other": "%(severalUsers)s %(count)s kez davetlerini reddetti",
+ "%(severalUsers)srejected their invitations %(count)s times|one": "%(severalUsers)s davetlerini reddetti",
+ "%(oneUser)srejected their invitation %(count)s times|one": "%(oneUser)s davetlerini reddetti",
+ "%(severalUsers)shad their invitations withdrawn %(count)s times|one": "%(severalUsers)s davetlerini geri çekti",
+ "%(oneUser)shad their invitation withdrawn %(count)s times|other": "%(oneUser)s davetini %(count)s kez geri çekti",
+ "%(oneUser)shad their invitation withdrawn %(count)s times|one": "%(oneUser)s davetini geri çekti",
+ "were banned %(count)s times|other": "%(count)s kez yasaklandı",
+ "were banned %(count)s times|one": "yasaklandı",
+ "was banned %(count)s times|other": "%(count)s kez yasaklandı",
+ "was banned %(count)s times|one": "yasaklandı",
+ "were unbanned %(count)s times|other": "%(count)s kez yasak kaldırıldı",
+ "were unbanned %(count)s times|one": "yasak kaldırıldı",
+ "was unbanned %(count)s times|other": "%(count)s kez yasak kaldırıldı",
+ "was unbanned %(count)s times|one": "yasak kaldırıldı",
+ "%(severalUsers)schanged their avatar %(count)s times|one": "%(severalUsers)s resimlerini değiştirdiler",
+ "%(oneUser)schanged their avatar %(count)s times|other": "%(oneUser)s %(count)s kez resmini değiştirdi",
+ "%(oneUser)schanged their avatar %(count)s times|one": "%(oneUser)s resmini değiştirdi",
+ "%(severalUsers)smade no changes %(count)s times|other": "%(severalUsers)s %(count)s kez hiç bir değişiklik yapmadı",
+ "Try using one of the following valid address types: %(validTypesList)s.": "Takip eden geçerli adres tiplerinden birini kullanmayı deneyin: %(validTypesList)s.",
+ "Use an identity server to invite by email. Use the default (%(defaultIdentityServerName)s) or manage in Settings .": "E-posta ile davet etmek için bir kimlik sunucusu kullan. Varsayılanı kullan (%(defaultIdentityServerName)s ya da Ayarlar kullanarak yönetin.",
+ "Use an identity server to invite by email. Manage in Settings .": "E-posta ile davet için bir kimlik sunucu kullan. Ayarlar dan yönet.",
+ "The following users may not exist": "Belirtilen kullanıcılar mevcut olmayabilir",
+ "Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?": "Altta belirtilen Matrix ID li profiller bulunamıyor - Onları yinede davet etmek ister misiniz?",
+ "Please tell us what went wrong or, better, create a GitHub issue that describes the problem.": "Lütfen neyin yanlış gittiğini bize bildirin ya da en güzeli problemi tanımlayan bir GitHub talebi oluşturun.",
+ "Before submitting logs, you must create a GitHub issue to describe your problem.": "Logları göndermeden önce, probleminizi betimleyen bir GitHub talebi oluşturun .",
+ "Community IDs may only contain characters a-z, 0-9, or '=_-./'": "Topluluk ID leri sadece a-z, 0-9 ya da '=_-./' karakterlerini içerebilir",
+ "Set a room alias to easily share your room with other people.": "Odanızı diğer kişilerle kolayca paylaşabilmek için bir oda lakabı ayarların.",
+ "Create a public room": "Halka açık bir oda oluşturun",
+ "Make this room public": "Bu odayı halka açık yap",
+ "To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of Riot to do this": "Sohbet tarihçesini kaybetmemek için, çıkmadan önce odanızın anahtarlarını dışarıya aktarın. Bunu yapabilmek için Riotun daha yeni sürümü gerekli. Ulaşmak için geri gitmeye ihtiyacınız var",
+ "Continue With Encryption Disabled": "Şifreleme Kapalı Şekilde Devam Et",
+ "The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "%(fileName)s dosyası anasunucunun yükleme boyutu limitini aşıyor",
+ "Double check that your server supports the room version chosen and try again.": "Seçtiğiniz oda sürümünün sunucunuz tarafından desteklenip desteklenmediğini iki kez kontrol edin ve yeniden deneyin.",
+ "Changes your avatar in this current room only": "Sadece bu odadaki resminizi değiştirin",
+ "Please supply a https:// or http:// widget URL": "Lütfen bir https:// ya da http:// olarak bir görsel bileşen URL i belirtin",
+ "Sends the given emote coloured as a rainbow": "Verilen ifadeyi bir gökkuşağı gibi renklendirilmiş olarak gönderin",
+ "%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s ekran adını %(displayName)s olarak değiştirdi.",
+ "%(senderDisplayName)s made the room public to whoever knows the link.": "%(senderDisplayName)s odayı adresi bilen herkesin girebileceği şekilde halka açık hale getirdi.",
+ "%(senderDisplayName)s changed the join rule to %(rule)s": "%(senderDisplayName)s katılma kuralını %(rule)s şeklinde değiştirdi",
+ "%(senderDisplayName)s changed guest access to %(rule)s": "%(senderDisplayName)s misafir erişim kuralını %(rule)s şeklinde değiştirdi",
+ "%(senderName)s set the main address for this room to %(address)s.": "%(senderName)s bu odanın ana adresini %(address)s olarak ayarladı.",
+ "%(senderName)s placed a voice call.": "%(senderName)s bir çağrı yaptı.",
+ "%(senderName)s placed a voice call. (not supported by this browser)": "%(senderName)s bir çağrı başlattı. (Bu tarayıcı tarafından desteklenmiyor)",
+ "%(senderName)s placed a video call.": "%(senderName)s bir görüntülü çağrı yaptı.",
+ "%(senderName)s placed a video call. (not supported by this browser)": "%(senderName)s bir görüntülü çağrı yaptı. (bu tarayıcı tarafından desteklenmiyor)",
+ "Ask your Riot admin to check your config for incorrect or duplicate entries.": "Riot yöneticinize yapılandırmanızın hatalı ve mükerrer girdilerini kontrol etmesi için talepte bulunun.",
+ "You can register, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "Kayıt olabilirsiniz, fakat kimlik sunucunuz çevrimiçi olana kadar bazı özellikler mevcut olmayacak. Bu uyarıyı sürekli görüyorsanız, yapılandırmanızı kontrol edin veya sunucu yöneticinizle iletişime geçin.",
+ "You can reset your password, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "Parolanızı sıfırlayabilirsiniz, fakat kimlik sunucunuz çevrimiçi olana kadar bazı özellikler mevcut olmayacak. Bu uyarıyı sürekli görüyorsanız, yapılandırmanızı kontrol edin veya sunucu yöneticinizle iletişime geçin.",
+ "You can log in, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "Oturum açabilirsiniz, fakat kimlik sunucunuz çevrimiçi olana kadar bazı özellikler mevcut olmayacak. Bu uyarıyı sürekli görüyorsanız, yapılandırmanızı kontrol edin veya sunucu yöneticinizle iletişime geçin.",
+ "a few seconds from now": "şu andan itibaren bir kaç saniye",
+ "about a minute from now": "şu andan itibaren yaklaşık bir dakika",
+ "%(num)s minutes from now": "şu andan itibaren %(num)s dakika",
+ "about an hour from now": "şu andan itibaren yaklaşık bir saat",
+ "%(num)s hours from now": "şu andan itibaren %(num)s saat",
+ "about a day from now": "şu andan itibaren yaklaşık bir gün",
+ "%(num)s days from now": "şu andan itibaren %(num)s gün",
+ "The user must be unbanned before they can be invited.": "Kullanıcının davet edilebilmesi için öncesinde yasağının kaldırılması gereklidir.",
+ "Repeats like \"abcabcabc\" are only slightly harder to guess than \"abc\"": "“abcabcabc” gibi tekrarlar “abc” yi tahmin etmekten çok az daha zor olur",
+ "Sequences like abc or 6543 are easy to guess": "abc veya 6543 gibi diziler tahmin için oldukça kolaydır",
+ "Common names and surnames are easy to guess": "Yaygın isimleri ve soyisimleri tahmin etmek oldukça kolay",
+ "Enable cross-signing to verify per-user instead of per-device (in development)": "Her cihaz yerine her kullanıcıyı doğrulamak için çarpraz-imzalamayı aç (geliştiriliyor)",
+ "Show info about bridges in room settings": "Oda ayarlarındaki köprülerin bilgilerini göster",
+ "Show a placeholder for removed messages": "Silinen mesajlar için bir yer tutucu göster",
+ "Show display name changes": "Ekran isim değişikliklerini göster",
+ "Enable URL previews for this room (only affects you)": "Bu oda için URL önizlemeyi aç (sadece sizi etkiler)",
+ "Enable URL previews by default for participants in this room": "Bu odadaki katılımcılar için URL önizlemeyi varsayılan olarak açık hale getir",
+ "Enable widget screenshots on supported widgets": "Desteklenen görsel bileşenlerde anlık görüntüleri aç",
+ "Show recently visited rooms above the room list": "En son ziyaret edilen odaları oda listesinin en üstünde göster",
+ "Show hidden events in timeline": "Zaman çizelgesinde gizli olayları göster",
+ "Encrypted messages in one-to-one chats": "Birebir sohbetlerdeki şifrelenmiş mesajlar",
+ "Encrypted messages in group chats": "Grup sohbetlerdeki şifrelenmiş mesajlar",
+ "This is your list of users/servers you have blocked - don't leave the room!": "Bu sizin engellediğiniz kullanıcılar/sunucular listeniz - odadan ayrılmayın!",
+ "Got It": "Anlaşıldı"
}
diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json
index dbd8319b8a..4a70441317 100644
--- a/src/i18n/strings/zh_Hant.json
+++ b/src/i18n/strings/zh_Hant.json
@@ -2075,5 +2075,41 @@
"Something went wrong trying to invite the users.": "在嘗試邀請使用者時發生錯誤。",
"We couldn't invite those users. Please check the users you want to invite and try again.": "我們無法邀請那些使用者。請檢查您想要邀請的使用者並再試一次。",
"Recently Direct Messaged": "最近傳送過直接訊息",
- "If you can't find someone, ask them for their username (e.g. @user:server.com) or share this room .": "如果您找不到某人,請詢問他們的使用者名稱(範例:@user:server.com)或分享此聊天室 。"
+ "If you can't find someone, ask them for their username (e.g. @user:server.com) or share this room .": "如果您找不到某人,請詢問他們的使用者名稱(範例:@user:server.com)或分享此聊天室 。",
+ "Verify User": "驗證使用者",
+ "For extra security, verify this user by checking a one-time code on both of your devices.": "為了提高安全性,請透過檢查您兩個裝置上的一次性代碼來驗證此使用者。",
+ "For maximum security, do this in person.": "為了取得最強的安全性,請親自進行。",
+ "Start Verification": "開始驗證",
+ "Encrypted by a deleted device": "被已刪除的裝置加密",
+ "Unknown Command": "未知的指令",
+ "Unrecognised command: %(commandText)s": "無法識別的指令:%(commandText)s",
+ "You can use /help
to list available commands. Did you mean to send this as a message?": "您可以使用 /help
來列出可用的指令。您是要傳送此訊息嗎?",
+ "Hint: Begin your message with //
to start it with a slash.": "提示:以 //
開頭讓您的訊息傳送時可以用斜線開頭。",
+ "Send as message": "以訊息傳送",
+ "%(senderName)s added %(addedAddresses)s and %(count)s other addresses to this room|other": "%(senderName)s 向此聊天室新增了 %(addedAddresses)s 與其他 %(count)s 個地址",
+ "%(senderName)s removed %(removedAddresses)s and %(count)s other addresses from this room|other": "%(senderName)s 從此聊天室移除了 %(removedAddresses)s 與其他 %(count)s 個地址",
+ "%(senderName)s removed %(countRemoved)s and added %(countAdded)s addresses to this room": "%(senderName)s 對此聊天室移除了 %(countRemoved)s 個並新增了 %(countAdded)s 地址到此聊天室",
+ "%(senderName)s turned on end-to-end encryption.": "%(senderName)s 開啟了端到端加密。",
+ "%(senderName)s turned on end-to-end encryption (unrecognised algorithm %(algorithm)s).": "%(senderName)s 開啟了端到端加密(無法識別的演算法 %(algorithm)s)。",
+ "Someone is using an unknown device": "某人正在使用未知的裝置",
+ "This room is end-to-end encrypted": "此聊天室已端到端加密",
+ "Everyone in this room is verified": "此聊天室中每個人都已驗證",
+ "Invite only": "僅邀請",
+ "Send a reply…": "傳送回覆……",
+ "Send a message…": "傳送訊息……",
+ "Reject & Ignore user": "回絕並忽略使用者",
+ "If you can't find someone, ask them for their username, share your username (%(userId)s) or profile link .": "如果您找不到某人,請詢問他們以取得他們的使用者名稱,分享您的使用者名稱 (%(userId)s) 或簡介連結 。",
+ "Enter your account password 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.": "升級此裝置以允許其驗證其他裝置,並允許存取加密訊息以及將它們標記為受其他使用者信任。",
+ "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:": "使用通關密語保護您的加密金鑰。為了取得最強的安全性,此通關密語應與您的帳號密碼不同:",
+ "Enter a passphrase": "輸入通關密語",
+ "Enter your passphrase a second time to confirm it.": "輸入您的通關密語兩次以確認。",
+ "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.": "透過他們的簡介驗證其他使用者。",
+ "Upgrade your encryption": "升級您的加密",
+ "Set up encryption": "設定加密",
+ "Encryption upgraded": "加密已升級",
+ "Encryption setup complete": "加密設定完成"
}
diff --git a/src/indexing/BaseEventIndexManager.js b/src/indexing/BaseEventIndexManager.js
index 5e8ca668ad..8ebaddc3ab 100644
--- a/src/indexing/BaseEventIndexManager.js
+++ b/src/indexing/BaseEventIndexManager.js
@@ -62,11 +62,24 @@ export interface SearchArgs {
room_id: ?string;
}
-export interface HistoricEvent {
+export interface EventAndProfile {
event: MatrixEvent;
profile: MatrixProfile;
}
+export interface LoadArgs {
+ roomId: string;
+ limit: number;
+ fromEvent: string;
+ direction: string;
+}
+
+export interface IndexStats {
+ size: number;
+ event_count: number;
+ room_count: number;
+}
+
/**
* Base class for classes that provide platform-specific event indexing.
*
@@ -117,6 +130,16 @@ export default class BaseEventIndexManager {
throw new Error("Unimplemented");
}
+ /**
+ * Get statistical information of the index.
+ *
+ * @return {Promise} A promise that will resolve to the index
+ * statistics.
+ */
+ async getStats(): Promise {
+ throw new Error("Unimplemented");
+ }
+
/**
* Commit the previously queued up events to the index.
*
@@ -145,7 +168,7 @@ export default class BaseEventIndexManager {
*
* This is used to add a batch of events to the index.
*
- * @param {[HistoricEvent]} events The list of events and profiles that
+ * @param {[EventAndProfile]} events The list of events and profiles that
* should be added to the event index.
* @param {[CrawlerCheckpoint]} checkpoint A new crawler checkpoint that
* should be stored in the index which should be used to continue crawling
@@ -158,7 +181,7 @@ export default class BaseEventIndexManager {
* were already added to the index, false otherwise.
*/
async addHistoricEvents(
- events: [HistoricEvent],
+ events: [EventAndProfile],
checkpoint: CrawlerCheckpoint | null,
oldCheckpoint: CrawlerCheckpoint | null,
): Promise {
@@ -201,6 +224,26 @@ export default class BaseEventIndexManager {
throw new Error("Unimplemented");
}
+ /** Load events that contain an mxc URL to a file from the index.
+ *
+ * @param {object} args Arguments object for the method.
+ * @param {string} args.roomId The ID of the room for which the events
+ * should be loaded.
+ * @param {number} args.limit The maximum number of events to return.
+ * @param {string} args.fromEvent An event id of a previous event returned
+ * by this method. Passing this means that we are going to continue loading
+ * events from this point in the history.
+ * @param {string} args.direction The direction to which we should continue
+ * loading events from. This is used only if fromEvent is used as well.
+ *
+ * @return {Promise<[EventAndProfile]>} A promise that will resolve to an
+ * array of Matrix events that contain mxc URLs accompanied with the
+ * historic profile of the sender.
+ */
+ async loadFileEvents(args: LoadArgs): Promise<[EventAndProfile]> {
+ throw new Error("Unimplemented");
+ }
+
/**
* close our event index.
*
diff --git a/src/indexing/EventIndex.js b/src/indexing/EventIndex.js
index c912e31fa5..9628920cd7 100644
--- a/src/indexing/EventIndex.js
+++ b/src/indexing/EventIndex.js
@@ -16,20 +16,26 @@ limitations under the License.
import PlatformPeg from "../PlatformPeg";
import {MatrixClientPeg} from "../MatrixClientPeg";
+import {EventTimeline, RoomMember} from 'matrix-js-sdk';
+import {sleep} from "../utils/promise";
+import {EventEmitter} from "events";
/*
* Event indexing class that wraps the platform specific event indexing.
*/
-export default class EventIndex {
+export default class EventIndex extends EventEmitter {
constructor() {
+ super();
this.crawlerCheckpoints = [];
- // The time that the crawler will wait between /rooms/{room_id}/messages
- // requests
- this._crawlerTimeout = 3000;
+ // The time in ms that the crawler will wait loop iterations if there
+ // have not been any checkpoints to consume in the last iteration.
+ this._crawlerIdleTime = 5000;
+ this._crawlerSleepTime = 3000;
// The maximum number of events our crawler should fetch in a single
// crawl.
this._eventsPerCrawl = 100;
this._crawler = null;
+ this._currentCheckpoint = null;
this.liveEventsForIndex = new Set();
}
@@ -64,59 +70,62 @@ export default class EventIndex {
client.removeListener('Room.timelineReset', this.onTimelineReset);
}
+ /**
+ * Get crawler checkpoints for the encrypted rooms and store them in the index.
+ */
+ async addInitialCheckpoints() {
+ const indexManager = PlatformPeg.get().getEventIndexingManager();
+ const client = MatrixClientPeg.get();
+ const rooms = client.getRooms();
+
+ const isRoomEncrypted = (room) => {
+ return client.isRoomEncrypted(room.roomId);
+ };
+
+ // We only care to crawl the encrypted rooms, non-encrypted
+ // rooms can use the search provided by the homeserver.
+ const encryptedRooms = rooms.filter(isRoomEncrypted);
+
+ console.log("EventIndex: Adding initial crawler checkpoints");
+
+ // Gather the prev_batch tokens and create checkpoints for
+ // our message crawler.
+ await Promise.all(encryptedRooms.map(async (room) => {
+ const timeline = room.getLiveTimeline();
+ const token = timeline.getPaginationToken("b");
+
+ console.log("EventIndex: Got token for indexer",
+ room.roomId, token);
+
+ const backCheckpoint = {
+ roomId: room.roomId,
+ token: token,
+ direction: "b",
+ };
+
+ const forwardCheckpoint = {
+ roomId: room.roomId,
+ token: token,
+ direction: "f",
+ };
+
+ await indexManager.addCrawlerCheckpoint(backCheckpoint);
+ await indexManager.addCrawlerCheckpoint(forwardCheckpoint);
+ this.crawlerCheckpoints.push(backCheckpoint);
+ this.crawlerCheckpoints.push(forwardCheckpoint);
+ }));
+ }
+
onSync = async (state, prevState, data) => {
const indexManager = PlatformPeg.get().getEventIndexingManager();
if (prevState === "PREPARED" && state === "SYNCING") {
- const addInitialCheckpoints = async () => {
- const client = MatrixClientPeg.get();
- const rooms = client.getRooms();
-
- const isRoomEncrypted = (room) => {
- return client.isRoomEncrypted(room.roomId);
- };
-
- // We only care to crawl the encrypted rooms, non-encrypted.
- // rooms can use the search provided by the homeserver.
- const encryptedRooms = rooms.filter(isRoomEncrypted);
-
- console.log("EventIndex: Adding initial crawler checkpoints");
-
- // Gather the prev_batch tokens and create checkpoints for
- // our message crawler.
- await Promise.all(encryptedRooms.map(async (room) => {
- const timeline = room.getLiveTimeline();
- const token = timeline.getPaginationToken("b");
-
- console.log("EventIndex: Got token for indexer",
- room.roomId, token);
-
- const backCheckpoint = {
- roomId: room.roomId,
- token: token,
- direction: "b",
- };
-
- const forwardCheckpoint = {
- roomId: room.roomId,
- token: token,
- direction: "f",
- };
-
- await indexManager.addCrawlerCheckpoint(backCheckpoint);
- await indexManager.addCrawlerCheckpoint(forwardCheckpoint);
- this.crawlerCheckpoints.push(backCheckpoint);
- this.crawlerCheckpoints.push(forwardCheckpoint);
- }));
- };
-
// If our indexer is empty we're most likely running Riot the
// first time with indexing support or running it with an
// initial sync. Add checkpoints to crawl our encrypted rooms.
const eventIndexWasEmpty = await indexManager.isEventIndexEmpty();
- if (eventIndexWasEmpty) await addInitialCheckpoints();
+ if (eventIndexWasEmpty) await this.addInitialCheckpoints();
- // Start our crawler.
this.startCrawler();
return;
}
@@ -170,7 +179,9 @@ export default class EventIndex {
return;
}
- const e = ev.toJSON().decrypted;
+ const jsonEvent = ev.toJSON();
+ const e = ev.isEncrypted() ? jsonEvent.decrypted : jsonEvent;
+
const profile = {
displayname: ev.sender.rawDisplayName,
avatar_url: ev.sender.getMxcAvatarUrl(),
@@ -179,13 +190,11 @@ export default class EventIndex {
indexManager.addEventToIndex(e, profile);
}
- async crawlerFunc() {
- // TODO either put this in a better place or find a library provided
- // method that does this.
- const sleep = async (ms) => {
- return new Promise(resolve => setTimeout(resolve, ms));
- };
+ emitNewCheckpoint() {
+ this.emit("changedCheckpoint", this.currentRoom());
+ }
+ async crawlerFunc() {
let cancelled = false;
console.log("EventIndex: Started crawler function");
@@ -199,11 +208,27 @@ export default class EventIndex {
cancelled = true;
};
+ let idle = false;
+
while (!cancelled) {
// This is a low priority task and we don't want to spam our
// homeserver with /messages requests so we set a hefty timeout
// here.
- await sleep(this._crawlerTimeout);
+ let sleepTime = this._crawlerSleepTime;
+
+ // Don't let the user configure a lower sleep time than 100 ms.
+ sleepTime = Math.max(sleepTime, 100);
+
+ if (idle) {
+ sleepTime = this._crawlerIdleTime;
+ }
+
+ if (this._currentCheckpoint !== null) {
+ this._currentCheckpoint = null;
+ this.emitNewCheckpoint();
+ }
+
+ await sleep(sleepTime);
console.log("EventIndex: Running the crawler loop.");
@@ -216,9 +241,15 @@ export default class EventIndex {
/// There is no checkpoint available currently, one may appear if
// a sync with limited room timelines happens, so go back to sleep.
if (checkpoint === undefined) {
+ idle = true;
continue;
}
+ this._currentCheckpoint = checkpoint;
+ this.emitNewCheckpoint();
+
+ idle = false;
+
console.log("EventIndex: crawling using checkpoint", checkpoint);
// We have a checkpoint, let us fetch some messages, again, very
@@ -238,6 +269,11 @@ export default class EventIndex {
continue;
}
+ if (cancelled) {
+ this.crawlerCheckpoints.push(checkpoint);
+ break;
+ }
+
if (res.chunk.length === 0) {
console.log("EventIndex: Done with the checkpoint", checkpoint);
// We got to the start/end of our timeline, lets just
@@ -305,10 +341,7 @@ export default class EventIndex {
// consume.
const events = filteredEvents.map((ev) => {
const jsonEvent = ev.toJSON();
-
- let e;
- if (ev.isEncrypted()) e = jsonEvent.decrypted;
- else e = jsonEvent;
+ const e = ev.isEncrypted() ? jsonEvent.decrypted : jsonEvent;
let profile = {};
if (e.sender in profiles) profile = profiles[e.sender];
@@ -406,4 +439,223 @@ export default class EventIndex {
const indexManager = PlatformPeg.get().getEventIndexingManager();
return indexManager.searchEventIndex(searchArgs);
}
+
+ /**
+ * Load events that contain URLs from the event index.
+ *
+ * @param {Room} room The room for which we should fetch events containing
+ * URLs
+ *
+ * @param {number} limit The maximum number of events to fetch.
+ *
+ * @param {string} fromEvent From which event should we continue fetching
+ * events from the index. This is only needed if we're continuing to fill
+ * the timeline, e.g. if we're paginating. This needs to be set to a event
+ * id of an event that was previously fetched with this function.
+ *
+ * @param {string} direction The direction in which we will continue
+ * fetching events. EventTimeline.BACKWARDS to continue fetching events that
+ * are older than the event given in fromEvent, EventTimeline.FORWARDS to
+ * fetch newer events.
+ *
+ * @returns {Promise} Resolves to an array of events that
+ * contain URLs.
+ */
+ async loadFileEvents(room, limit = 10, fromEvent = null, direction = EventTimeline.BACKWARDS) {
+ const client = MatrixClientPeg.get();
+ const indexManager = PlatformPeg.get().getEventIndexingManager();
+
+ const loadArgs = {
+ roomId: room.roomId,
+ limit: limit,
+ };
+
+ if (fromEvent) {
+ loadArgs.fromEvent = fromEvent;
+ loadArgs.direction = direction;
+ }
+
+ let events;
+
+ // Get our events from the event index.
+ try {
+ events = await indexManager.loadFileEvents(loadArgs);
+ } catch (e) {
+ console.log("EventIndex: Error getting file events", e);
+ return [];
+ }
+
+ const eventMapper = client.getEventMapper();
+
+ // Turn the events into MatrixEvent objects.
+ const matrixEvents = events.map(e => {
+ const matrixEvent = eventMapper(e.event);
+
+ const member = new RoomMember(room.roomId, matrixEvent.getSender());
+
+ // We can't really reconstruct the whole room state from our
+ // EventIndex to calculate the correct display name. Use the
+ // disambiguated form always instead.
+ member.name = e.profile.displayname + " (" + matrixEvent.getSender() + ")";
+
+ // This is sets the avatar URL.
+ const memberEvent = eventMapper(
+ {
+ content: {
+ membership: "join",
+ avatar_url: e.profile.avatar_url,
+ displayname: e.profile.displayname,
+ },
+ type: "m.room.member",
+ event_id: matrixEvent.getId() + ":eventIndex",
+ room_id: matrixEvent.getRoomId(),
+ sender: matrixEvent.getSender(),
+ origin_server_ts: matrixEvent.getTs(),
+ state_key: matrixEvent.getSender(),
+ },
+ );
+
+ // We set this manually to avoid emitting RoomMember.membership and
+ // RoomMember.name events.
+ member.events.member = memberEvent;
+ matrixEvent.sender = member;
+
+ return matrixEvent;
+ });
+
+ return matrixEvents;
+ }
+
+ /**
+ * Fill a timeline with events that contain URLs.
+ *
+ * @param {TimelineSet} timelineSet The TimelineSet the Timeline belongs to,
+ * used to check if we're adding duplicate events.
+ *
+ * @param {Timeline} timeline The Timeline which should be filed with
+ * events.
+ *
+ * @param {Room} room The room for which we should fetch events containing
+ * URLs
+ *
+ * @param {number} limit The maximum number of events to fetch.
+ *
+ * @param {string} fromEvent From which event should we continue fetching
+ * events from the index. This is only needed if we're continuing to fill
+ * the timeline, e.g. if we're paginating. This needs to be set to a event
+ * id of an event that was previously fetched with this function.
+ *
+ * @param {string} direction The direction in which we will continue
+ * fetching events. EventTimeline.BACKWARDS to continue fetching events that
+ * are older than the event given in fromEvent, EventTimeline.FORWARDS to
+ * fetch newer events.
+ *
+ * @returns {Promise} Resolves to true if events were added to the
+ * timeline, false otherwise.
+ */
+ async populateFileTimeline(timelineSet, timeline, room, limit = 10,
+ fromEvent = null, direction = EventTimeline.BACKWARDS) {
+ const matrixEvents = await this.loadFileEvents(room, limit, fromEvent, direction);
+
+ // If this is a normal fill request, not a pagination request, we need
+ // to get our events in the BACKWARDS direction but populate them in the
+ // forwards direction.
+ // This needs to happen because a fill request might come with an
+ // exisitng timeline e.g. if you close and re-open the FilePanel.
+ if (fromEvent === null) {
+ matrixEvents.reverse();
+ direction = direction == EventTimeline.BACKWARDS ? EventTimeline.FORWARDS: EventTimeline.BACKWARDS;
+ }
+
+ // Add the events to the timeline of the file panel.
+ matrixEvents.forEach(e => {
+ if (!timelineSet.eventIdToTimeline(e.getId())) {
+ timelineSet.addEventToTimeline(e, timeline, direction == EventTimeline.BACKWARDS);
+ }
+ });
+
+ // Set the pagination token to the oldest event that we retrieved.
+ if (matrixEvents.length > 0) {
+ timeline.setPaginationToken(matrixEvents[matrixEvents.length - 1].getId(), EventTimeline.BACKWARDS);
+ return true;
+ } else {
+ timeline.setPaginationToken("", EventTimeline.BACKWARDS);
+ return false;
+ }
+ }
+
+ /**
+ * Emulate a TimelineWindow pagination() request with the event index as the event source
+ *
+ * Might not fetch events from the index if the timeline already contains
+ * events that the window isn't showing.
+ *
+ * @param {Room} room The room for which we should fetch events containing
+ * URLs
+ *
+ * @param {TimelineWindow} timelineWindow The timeline window that should be
+ * populated with new events.
+ *
+ * @param {string} direction The direction in which we should paginate.
+ * EventTimeline.BACKWARDS to paginate back, EventTimeline.FORWARDS to
+ * paginate forwards.
+ *
+ * @param {number} limit The maximum number of events to fetch while
+ * paginating.
+ *
+ * @returns {Promise} Resolves to a boolean which is true if more
+ * events were successfully retrieved.
+ */
+ paginateTimelineWindow(room, timelineWindow, direction, limit) {
+ const tl = timelineWindow.getTimelineIndex(direction);
+
+ if (!tl) return Promise.resolve(false);
+ if (tl.pendingPaginate) return tl.pendingPaginate;
+
+ if (timelineWindow.extend(direction, limit)) {
+ return Promise.resolve(true);
+ }
+
+ const paginationMethod = async (timelineWindow, timeline, room, direction, limit) => {
+ const timelineSet = timelineWindow._timelineSet;
+ const token = timeline.timeline.getPaginationToken(direction);
+
+ const ret = await this.populateFileTimeline(timelineSet, timeline.timeline, room, limit, token, direction);
+
+ timeline.pendingPaginate = null;
+ timelineWindow.extend(direction, limit);
+
+ return ret;
+ };
+
+ const paginationPromise = paginationMethod(timelineWindow, tl, room, direction, limit);
+ tl.pendingPaginate = paginationPromise;
+
+ return paginationPromise;
+ }
+
+ async getStats() {
+ const indexManager = PlatformPeg.get().getEventIndexingManager();
+ return indexManager.getStats();
+ }
+
+ /**
+ * Get the room that we are currently crawling.
+ *
+ * @returns {Room} A MatrixRoom that is being currently crawled, null
+ * if no room is currently being crawled.
+ */
+ currentRoom() {
+ if (this._currentCheckpoint === null && this.crawlerCheckpoints.length === 0) {
+ return null;
+ }
+
+ const client = MatrixClientPeg.get();
+
+ if (this._currentCheckpoint !== null) {
+ return client.getRoom(this._currentCheckpoint.roomId);
+ } else {
+ return client.getRoom(this.crawlerCheckpoints[0].roomId);
+ }
+ }
}
diff --git a/src/indexing/EventIndexPeg.js b/src/indexing/EventIndexPeg.js
index 3746591b1f..067aea3a7f 100644
--- a/src/indexing/EventIndexPeg.js
+++ b/src/indexing/EventIndexPeg.js
@@ -21,17 +21,19 @@ limitations under the License.
import PlatformPeg from "../PlatformPeg";
import EventIndex from "../indexing/EventIndex";
-import SettingsStore from '../settings/SettingsStore';
+import SettingsStore, {SettingLevel} from '../settings/SettingsStore';
class EventIndexPeg {
constructor() {
this.index = null;
+ this._supportIsInstalled = false;
}
/**
- * Create a new EventIndex and initialize it if the platform supports it.
+ * Initialize the EventIndexPeg and if event indexing is enabled initialize
+ * the event index.
*
- * @return {Promise} A promise that will resolve to true if an
+ * @return {Promise} A promise that will resolve to true if an
* EventIndex was successfully initialized, false otherwise.
*/
async init() {
@@ -40,12 +42,33 @@ class EventIndexPeg {
}
const indexManager = PlatformPeg.get().getEventIndexingManager();
- if (!indexManager || await indexManager.supportsEventIndexing() !== true) {
- console.log("EventIndex: Platform doesn't support event indexing,",
- "not initializing.");
+ if (!indexManager) {
+ console.log("EventIndex: Platform doesn't support event indexing, not initializing.");
return false;
}
+ this._supportIsInstalled = await indexManager.supportsEventIndexing();
+
+ if (!this.supportIsInstalled()) {
+ console.log("EventIndex: Event indexing isn't installed for the platform, not initializing.");
+ return false;
+ }
+
+ if (!SettingsStore.getValueAt(SettingLevel.DEVICE, 'enableEventIndexing')) {
+ console.log("EventIndex: Event indexing is disabled, not initializing");
+ return false;
+ }
+
+ return this.initEventIndex();
+ }
+
+ /**
+ * Initialize the event index.
+ *
+ * @returns {boolean} True if the event index was succesfully initialized,
+ * false otherwise.
+ */
+ async initEventIndex() {
const index = new EventIndex();
try {
@@ -60,6 +83,29 @@ class EventIndexPeg {
return true;
}
+ /**
+ * Check if the current platform has support for event indexing.
+ *
+ * @return {boolean} True if it has support, false otherwise. Note that this
+ * does not mean that support is installed.
+ */
+ platformHasSupport(): boolean {
+ return PlatformPeg.get().getEventIndexingManager() !== null;
+ }
+
+ /**
+ * Check if event indexing support is installed for the platfrom.
+ *
+ * Event indexing might require additional optional modules to be installed,
+ * this tells us if those are installed. Note that this should only be
+ * called after the init() method was called.
+ *
+ * @return {boolean} True if support is installed, false otherwise.
+ */
+ supportIsInstalled(): boolean {
+ return this._supportIsInstalled;
+ }
+
/**
* Get the current event index.
*
@@ -69,6 +115,11 @@ class EventIndexPeg {
return this.index;
}
+ start() {
+ if (this.index === null) return;
+ this.index.startCrawler();
+ }
+
stop() {
if (this.index === null) return;
this.index.stopCrawler();
diff --git a/src/settings/Settings.js b/src/settings/Settings.js
index eacf63e55d..37a211e905 100644
--- a/src/settings/Settings.js
+++ b/src/settings/Settings.js
@@ -128,12 +128,6 @@ export const SETTINGS = {
supportedLevels: LEVELS_FEATURE,
default: false,
},
- "feature_ftue_dms": {
- isFeature: true,
- displayName: _td("New invite dialog"),
- supportedLevels: LEVELS_FEATURE,
- default: false,
- },
"feature_presence_in_room_list": {
isFeature: true,
displayName: _td("Show a presence dot next to DMs in the room list"),
@@ -486,4 +480,9 @@ export const SETTINGS = {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
default: RIGHT_PANEL_PHASES.GroupMemberList,
},
+ "enableEventIndexing": {
+ supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
+ displayName: _td("Enable message search in encrypted rooms"),
+ default: true,
+ },
};
diff --git a/src/settings/SettingsStore.js b/src/settings/SettingsStore.js
index 9501bac205..b1d61197a0 100644
--- a/src/settings/SettingsStore.js
+++ b/src/settings/SettingsStore.js
@@ -145,7 +145,7 @@ export default class SettingsStore {
callbackFn(originalSettingName, changedInRoomId, atLevel, newValAtLevel, newValue);
};
- console.log(`Starting watcher for ${settingName}@${roomId || ''}`);
+ console.log(`Starting watcher for ${settingName}@${roomId || ''} as ID ${watcherId}`);
SettingsStore._watchers[watcherId] = localizedCallback;
defaultWatchManager.watchSetting(settingName, roomId, localizedCallback);
@@ -159,8 +159,12 @@ export default class SettingsStore {
* to cancel.
*/
static unwatchSetting(watcherReference) {
- if (!SettingsStore._watchers[watcherReference]) return;
+ if (!SettingsStore._watchers[watcherReference]) {
+ console.warn(`Ending non-existent watcher ID ${watcherReference}`);
+ return;
+ }
+ console.log(`Ending watcher ID ${watcherReference}`);
defaultWatchManager.unwatchSetting(SettingsStore._watchers[watcherReference]);
delete SettingsStore._watchers[watcherReference];
}
diff --git a/src/utils/FormattingUtils.js b/src/utils/FormattingUtils.js
index 1fd7d00feb..b932214530 100644
--- a/src/utils/FormattingUtils.js
+++ b/src/utils/FormattingUtils.js
@@ -30,6 +30,31 @@ export function formatCount(count) {
return (count / 1000000000).toFixed(1) + "B"; // 10B is enough for anyone, right? :S
}
+/**
+ * Format a count showing the whole number but making it a bit more readable.
+ * e.g: 1000 => 1,000
+ */
+export function formatCountLong(count) {
+ const formatter = new Intl.NumberFormat();
+ return formatter.format(count)
+}
+
+/**
+ * format a size in bytes into a human readable form
+ * e.g: 1024 -> 1.00 KB
+ */
+export function formatBytes(bytes, decimals = 2) {
+ if (bytes === 0) return '0 Bytes';
+
+ const k = 1024;
+ const dm = decimals < 0 ? 0 : decimals;
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
+
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
+}
+
/**
* format a key into groups of 4 characters, for easier visual inspection
*
diff --git a/src/utils/KeyVerificationStateObserver.js b/src/utils/KeyVerificationStateObserver.js
index 1a35319186..a29d2ea1aa 100644
--- a/src/utils/KeyVerificationStateObserver.js
+++ b/src/utils/KeyVerificationStateObserver.js
@@ -17,16 +17,15 @@ limitations under the License.
import {MatrixClientPeg} from '../MatrixClientPeg';
import { _t } from '../languageHandler';
-export function getNameForEventRoom(userId, mxEvent) {
- const roomId = mxEvent.getRoomId();
+export function getNameForEventRoom(userId, roomId) {
const client = MatrixClientPeg.get();
const room = client.getRoom(roomId);
- const member = room.getMember(userId);
+ const member = room && room.getMember(userId);
return member ? member.name : userId;
}
-export function userLabelForEventRoom(userId, mxEvent) {
- const name = getNameForEventRoom(userId, mxEvent);
+export function userLabelForEventRoom(userId, roomId) {
+ const name = getNameForEventRoom(userId, roomId);
if (name !== userId) {
return _t("%(name)s (%(userId)s)", {name, userId});
} else {
diff --git a/test/accessibility/RovingTabIndex-test.js b/test/accessibility/RovingTabIndex-test.js
new file mode 100644
index 0000000000..8be4a2976c
--- /dev/null
+++ b/test/accessibility/RovingTabIndex-test.js
@@ -0,0 +1,121 @@
+/*
+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 Adapter from "enzyme-adapter-react-16";
+import { configure, mount } from "enzyme";
+
+import {
+ RovingTabIndexProvider,
+ RovingTabIndexWrapper,
+ useRovingTabIndex,
+} from "../../src/accessibility/RovingTabIndex";
+
+configure({ adapter: new Adapter() });
+
+const Button = (props) => {
+ const [onFocus, isActive, ref] = useRovingTabIndex();
+ return ;
+};
+
+const checkTabIndexes = (buttons, expectations) => {
+ expect(buttons.length).toBe(expectations.length);
+ for (let i = 0; i < buttons.length; i++) {
+ expect(buttons.at(i).prop("tabIndex")).toBe(expectations[i]);
+ }
+};
+
+// give the buttons keys for the fibre reconciler to not treat them all as the same
+const button1 = a ;
+const button2 = b ;
+const button3 = c ;
+const button4 = d ;
+
+describe("RovingTabIndex", () => {
+ it("RovingTabIndexProvider renders children as expected", () => {
+ const wrapper = mount(
+ {() => Test
}
+ );
+ expect(wrapper.text()).toBe("Test");
+ expect(wrapper.html()).toBe('Test
');
+ });
+
+ it("RovingTabIndexProvider works as expected with useRovingTabIndex", () => {
+ const wrapper = mount(
+ {() =>
+ { button1 }
+ { button2 }
+ { button3 }
+ }
+ );
+
+ // should begin with 0th being active
+ checkTabIndexes(wrapper.find("button"), [0, -1, -1]);
+
+ // focus on 2nd button and test it is the only active one
+ wrapper.find("button").at(2).simulate("focus");
+ wrapper.update();
+ checkTabIndexes(wrapper.find("button"), [-1, -1, 0]);
+
+ // focus on 1st button and test it is the only active one
+ wrapper.find("button").at(1).simulate("focus");
+ wrapper.update();
+ checkTabIndexes(wrapper.find("button"), [-1, 0, -1]);
+
+ // check that the active button does not change even on an explicit blur event
+ wrapper.find("button").at(1).simulate("blur");
+ wrapper.update();
+ checkTabIndexes(wrapper.find("button"), [-1, 0, -1]);
+
+ // update the children, it should remain on the same button
+ wrapper.setProps({
+ children: () => [button1, button4, button2, button3],
+ });
+ wrapper.update();
+ checkTabIndexes(wrapper.find("button"), [-1, -1, 0, -1]);
+
+ // update the children, remove the active button, it should move to the next one
+ wrapper.setProps({
+ children: () => [button1, button4, button3],
+ });
+ wrapper.update();
+ checkTabIndexes(wrapper.find("button"), [-1, -1, 0]);
+ });
+
+ it("RovingTabIndexProvider works as expected with RovingTabIndexWrapper", () => {
+ const wrapper = mount(
+ {() =>
+ { button1 }
+ { button2 }
+
+ {({onFocus, isActive, ref}) =>
+ .
+ }
+
+ }
+ );
+
+ // should begin with 0th being active
+ checkTabIndexes(wrapper.find("button"), [0, -1, -1]);
+
+ // focus on 2nd button and test it is the only active one
+ wrapper.find("button").at(2).simulate("focus");
+ wrapper.update();
+ checkTabIndexes(wrapper.find("button"), [-1, -1, 0]);
+ });
+});
+
+
diff --git a/test/components/views/rooms/SendMessageComposer-test.js b/test/components/views/rooms/SendMessageComposer-test.js
new file mode 100644
index 0000000000..d5a143a1fb
--- /dev/null
+++ b/test/components/views/rooms/SendMessageComposer-test.js
@@ -0,0 +1,83 @@
+/*
+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 RoomViewStore from "../../../../src/stores/RoomViewStore";
+import {createMessageContent} from "../../../../src/components/views/rooms/SendMessageComposer";
+import EditorModel from "../../../../src/editor/model";
+import {createPartCreator, createRenderer} from "../../../editor/mock";
+
+jest.mock("../../../../src/stores/RoomViewStore");
+
+describe(' ', () => {
+ describe("createMessageContent", () => {
+ RoomViewStore.getQuotingEvent.mockReturnValue(false);
+ const permalinkCreator = jest.fn();
+
+ it("sends plaintext messages correctly", () => {
+ const model = new EditorModel([], createPartCreator(), createRenderer());
+ model.update("hello world", "insertText", {offset: 11, atNodeEnd: true});
+
+ const content = createMessageContent(model, permalinkCreator);
+
+ expect(content).toEqual({
+ body: "hello world",
+ msgtype: "m.text",
+ });
+ });
+
+ it("sends markdown messages correctly", () => {
+ const model = new EditorModel([], createPartCreator(), createRenderer());
+ model.update("hello *world*", "insertText", {offset: 13, atNodeEnd: true});
+
+ const content = createMessageContent(model, permalinkCreator);
+
+ expect(content).toEqual({
+ body: "hello *world*",
+ msgtype: "m.text",
+ format: "org.matrix.custom.html",
+ formatted_body: "hello world ",
+ });
+ });
+
+ it("strips /me from messages and marks them as m.emote accordingly", () => {
+ const model = new EditorModel([], createPartCreator(), createRenderer());
+ model.update("/me blinks __quickly__", "insertText", {offset: 22, atNodeEnd: true});
+
+ const content = createMessageContent(model, permalinkCreator);
+
+ expect(content).toEqual({
+ body: "blinks __quickly__",
+ msgtype: "m.emote",
+ format: "org.matrix.custom.html",
+ formatted_body: "blinks quickly ",
+ });
+ });
+
+ it("allows sending double-slash escaped slash commands correctly", () => {
+ const model = new EditorModel([], createPartCreator(), createRenderer());
+ model.update("//dev/null is my favourite place", "insertText", {offset: 32, atNodeEnd: true});
+
+ const content = createMessageContent(model, permalinkCreator);
+
+ expect(content).toEqual({
+ body: "/dev/null is my favourite place",
+ msgtype: "m.text",
+ });
+ });
+ });
+});
+
+
diff --git a/test/editor/mock.js b/test/editor/mock.js
index bb1a51d14b..6de65cf23d 100644
--- a/test/editor/mock.js
+++ b/test/editor/mock.js
@@ -67,3 +67,13 @@ export function createPartCreator(completions = []) {
};
return new PartCreator(new MockRoom(), new MockClient(), autoCompleteCreator);
}
+
+export function createRenderer() {
+ const render = (c) => {
+ render.caret = c;
+ render.count += 1;
+ };
+ render.count = 0;
+ render.caret = null;
+ return render;
+}
diff --git a/test/editor/model-test.js b/test/editor/model-test.js
index 826dde3d68..2a3584d508 100644
--- a/test/editor/model-test.js
+++ b/test/editor/model-test.js
@@ -15,17 +15,7 @@ limitations under the License.
*/
import EditorModel from "../../src/editor/model";
-import {createPartCreator} from "./mock";
-
-function createRenderer() {
- const render = (c) => {
- render.caret = c;
- render.count += 1;
- };
- render.count = 0;
- render.caret = null;
- return render;
-}
+import {createPartCreator, createRenderer} from "./mock";
describe('editor/model', function() {
describe('plain text manipulation', function() {
diff --git a/test/editor/operations-test.js b/test/editor/operations-test.js
new file mode 100644
index 0000000000..90a9812306
--- /dev/null
+++ b/test/editor/operations-test.js
@@ -0,0 +1,190 @@
+/*
+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 EditorModel from "../../src/editor/model";
+import {createPartCreator, createRenderer} from "./mock";
+import {toggleInlineFormat} from "../../src/editor/operations";
+
+const SERIALIZED_NEWLINE = {"text": "\n", "type": "newline"};
+
+describe('editor/operations: formatting operations', () => {
+ describe('toggleInlineFormat', () => {
+ it('works for words', () => {
+ const renderer = createRenderer();
+ const pc = createPartCreator();
+ const model = new EditorModel([
+ pc.plain("hello world!"),
+ ], pc, renderer);
+
+ const range = model.startRange(model.positionForOffset(6, false),
+ model.positionForOffset(11, false)); // around "world"
+
+ expect(range.parts[0].text).toBe("world");
+ expect(model.serializeParts()).toEqual([{"text": "hello world!", "type": "plain"}]);
+ toggleInlineFormat(range, "_");
+ expect(model.serializeParts()).toEqual([{"text": "hello _world_!", "type": "plain"}]);
+ });
+
+ it('works for parts of words', () => {
+ const renderer = createRenderer();
+ const pc = createPartCreator();
+ const model = new EditorModel([
+ pc.plain("hello world!"),
+ ], pc, renderer);
+
+ const range = model.startRange(model.positionForOffset(7, false),
+ model.positionForOffset(10, false)); // around "orl"
+
+ expect(range.parts[0].text).toBe("orl");
+ expect(model.serializeParts()).toEqual([{"text": "hello world!", "type": "plain"}]);
+ toggleInlineFormat(range, "*");
+ expect(model.serializeParts()).toEqual([{"text": "hello w*orl*d!", "type": "plain"}]);
+ });
+
+ it('works for around pills', () => {
+ const renderer = createRenderer();
+ const pc = createPartCreator();
+ const model = new EditorModel([
+ pc.plain("hello there "),
+ pc.atRoomPill("@room"),
+ pc.plain(", how are you doing?"),
+ ], pc, renderer);
+
+ const range = model.startRange(model.positionForOffset(6, false),
+ model.positionForOffset(30, false)); // around "there @room, how are you"
+
+ expect(range.parts.map(p => p.text).join("")).toBe("there @room, how are you");
+ expect(model.serializeParts()).toEqual([
+ {"text": "hello there ", "type": "plain"},
+ {"text": "@room", "type": "at-room-pill"},
+ {"text": ", how are you doing?", "type": "plain"},
+ ]);
+ toggleInlineFormat(range, "_");
+ expect(model.serializeParts()).toEqual([
+ {"text": "hello _there ", "type": "plain"},
+ {"text": "@room", "type": "at-room-pill"},
+ {"text": ", how are you_ doing?", "type": "plain"},
+ ]);
+ });
+
+ it('works for a paragraph', () => {
+ const renderer = createRenderer();
+ const pc = createPartCreator();
+ const model = new EditorModel([
+ pc.plain("hello world,"),
+ pc.newline(),
+ pc.plain("how are you doing?"),
+ ], pc, renderer);
+
+ const range = model.startRange(model.positionForOffset(6, false),
+ model.positionForOffset(16, false)); // around "world,\nhow"
+
+ expect(range.parts.map(p => p.text).join("")).toBe("world,\nhow");
+ expect(model.serializeParts()).toEqual([
+ {"text": "hello world,", "type": "plain"},
+ SERIALIZED_NEWLINE,
+ {"text": "how are you doing?", "type": "plain"},
+ ]);
+ toggleInlineFormat(range, "**");
+ expect(model.serializeParts()).toEqual([
+ {"text": "hello **world,", "type": "plain"},
+ SERIALIZED_NEWLINE,
+ {"text": "how** are you doing?", "type": "plain"},
+ ]);
+ });
+
+ it('works for a paragraph with spurious breaks around it in selected range', () => {
+ const renderer = createRenderer();
+ const pc = createPartCreator();
+ const model = new EditorModel([
+ pc.newline(),
+ pc.newline(),
+ pc.plain("hello world,"),
+ pc.newline(),
+ pc.plain("how are you doing?"),
+ pc.newline(),
+ pc.newline(),
+ ], pc, renderer);
+
+ const range = model.startRange(model.positionForOffset(0, false), model.getPositionAtEnd()); // select-all
+
+ expect(range.parts.map(p => p.text).join("")).toBe("\n\nhello world,\nhow are you doing?\n\n");
+ expect(model.serializeParts()).toEqual([
+ SERIALIZED_NEWLINE,
+ SERIALIZED_NEWLINE,
+ {"text": "hello world,", "type": "plain"},
+ SERIALIZED_NEWLINE,
+ {"text": "how are you doing?", "type": "plain"},
+ SERIALIZED_NEWLINE,
+ SERIALIZED_NEWLINE,
+ ]);
+ toggleInlineFormat(range, "**");
+ expect(model.serializeParts()).toEqual([
+ SERIALIZED_NEWLINE,
+ SERIALIZED_NEWLINE,
+ {"text": "**hello world,", "type": "plain"},
+ SERIALIZED_NEWLINE,
+ {"text": "how are you doing?**", "type": "plain"},
+ SERIALIZED_NEWLINE,
+ SERIALIZED_NEWLINE,
+ ]);
+ });
+
+ it('works for multiple paragraph', () => {
+ const renderer = createRenderer();
+ const pc = createPartCreator();
+ const model = new EditorModel([
+ pc.plain("hello world,"),
+ pc.newline(),
+ pc.plain("how are you doing?"),
+ pc.newline(),
+ pc.newline(),
+ pc.plain("new paragraph"),
+ ], pc, renderer);
+
+ let range = model.startRange(model.positionForOffset(0, true), model.getPositionAtEnd()); // select-all
+
+ expect(model.serializeParts()).toEqual([
+ {"text": "hello world,", "type": "plain"},
+ SERIALIZED_NEWLINE,
+ {"text": "how are you doing?", "type": "plain"},
+ SERIALIZED_NEWLINE,
+ SERIALIZED_NEWLINE,
+ {"text": "new paragraph", "type": "plain"},
+ ]);
+ toggleInlineFormat(range, "__");
+ expect(model.serializeParts()).toEqual([
+ {"text": "__hello world,", "type": "plain"},
+ SERIALIZED_NEWLINE,
+ {"text": "how are you doing?__", "type": "plain"},
+ SERIALIZED_NEWLINE,
+ SERIALIZED_NEWLINE,
+ {"text": "__new paragraph__", "type": "plain"},
+ ]);
+ range = model.startRange(model.positionForOffset(0, true), model.getPositionAtEnd()); // select-all
+ console.log("RANGE", range.parts);
+ toggleInlineFormat(range, "__");
+ expect(model.serializeParts()).toEqual([
+ {"text": "hello world,", "type": "plain"},
+ SERIALIZED_NEWLINE,
+ {"text": "how are you doing?", "type": "plain"},
+ SERIALIZED_NEWLINE,
+ SERIALIZED_NEWLINE,
+ {"text": "new paragraph", "type": "plain"},
+ ]);
+ });
+ });
+});
diff --git a/test/editor/range-test.js b/test/editor/range-test.js
index 53fb6cb765..b69ed9eb53 100644
--- a/test/editor/range-test.js
+++ b/test/editor/range-test.js
@@ -15,17 +15,7 @@ limitations under the License.
*/
import EditorModel from "../../src/editor/model";
-import {createPartCreator} from "./mock";
-
-function createRenderer() {
- const render = (c) => {
- render.caret = c;
- render.count += 1;
- };
- render.count = 0;
- render.caret = null;
- return render;
-}
+import {createPartCreator, createRenderer} from "./mock";
const pillChannel = "#riot-dev:matrix.org";
diff --git a/test/end-to-end-tests/src/usecases/invite.js b/test/end-to-end-tests/src/usecases/invite.js
index 6bee5dfd6f..75ebc61a88 100644
--- a/test/end-to-end-tests/src/usecases/invite.js
+++ b/test/end-to-end-tests/src/usecases/invite.js
@@ -31,10 +31,11 @@ module.exports = async function invite(session, userId) {
}
const inviteButton = await session.query(".mx_MemberList_invite");
await inviteButton.click();
- const inviteTextArea = await session.query(".mx_AddressPickerDialog textarea");
+ const inviteTextArea = await session.query(".mx_InviteDialog_editor textarea");
await inviteTextArea.type(userId);
- await inviteTextArea.press("Enter");
- const confirmButton = await session.query(".mx_Dialog_primary");
+ const selectUserItem = await session.query(".mx_InviteDialog_roomTile");
+ await selectUserItem.click();
+ const confirmButton = await session.query(".mx_InviteDialog_goButton");
await confirmButton.click();
session.log.done();
};
diff --git a/yarn.lock b/yarn.lock
index 0177629cd8..b892ac44f6 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -211,6 +211,13 @@
dependencies:
"@babel/types" "^7.7.4"
+"@babel/helper-module-imports@^7.8.3":
+ version "7.8.3"
+ resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.8.3.tgz#7fe39589b39c016331b6b8c3f441e8f0b1419498"
+ integrity sha512-R0Bx3jippsbAEtzkpZ/6FIiuzOURPcMjHp+Z6xPe6DtApDJx+w7UYyOLanZqO8+wKR9G10s/FmHXvxaMd9s6Kg==
+ dependencies:
+ "@babel/types" "^7.8.3"
+
"@babel/helper-module-transforms@^7.7.4", "@babel/helper-module-transforms@^7.7.5":
version "7.7.5"
resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.7.5.tgz#d044da7ffd91ec967db25cd6748f704b6b244835"
@@ -235,6 +242,11 @@
resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0.tgz#bbb3fbee98661c569034237cc03967ba99b4f250"
integrity sha512-CYAOUCARwExnEixLdB6sDm2dIJ/YgEAKDM1MOeMeZu9Ld/bDgVo8aiWrXwcY7OBh+1Ea2uUcVRcxKk0GJvW7QA==
+"@babel/helper-plugin-utils@^7.8.3":
+ version "7.8.3"
+ resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz#9ea293be19babc0f52ff8ca88b34c3611b208670"
+ integrity sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==
+
"@babel/helper-regex@^7.0.0", "@babel/helper-regex@^7.4.4":
version "7.5.5"
resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.5.5.tgz#0aa6824f7100a2e0e89c1527c23936c152cab351"
@@ -735,13 +747,13 @@
dependencies:
"@babel/helper-plugin-utils" "^7.0.0"
-"@babel/plugin-transform-runtime@^7.7.6":
- version "7.7.6"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.7.6.tgz#4f2b548c88922fb98ec1c242afd4733ee3e12f61"
- integrity sha512-tajQY+YmXR7JjTwRvwL4HePqoL3DYxpYXIHKVvrOIvJmeHe2y1w4tz5qz9ObUDC9m76rCzIMPyn4eERuwA4a4A==
+"@babel/plugin-transform-runtime@^7.8.3":
+ version "7.8.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.8.3.tgz#c0153bc0a5375ebc1f1591cb7eea223adea9f169"
+ integrity sha512-/vqUt5Yh+cgPZXXjmaG9NT8aVfThKk7G4OqkVhrXqwsC5soMn/qTCxs36rZ2QFhpfTJcjw4SNDIZ4RUb8OL4jQ==
dependencies:
- "@babel/helper-module-imports" "^7.7.4"
- "@babel/helper-plugin-utils" "^7.0.0"
+ "@babel/helper-module-imports" "^7.8.3"
+ "@babel/helper-plugin-utils" "^7.8.3"
resolve "^1.8.1"
semver "^5.5.1"
@@ -894,7 +906,7 @@
pirates "^4.0.0"
source-map-support "^0.5.16"
-"@babel/runtime@^7.0.0", "@babel/runtime@^7.7.6":
+"@babel/runtime@^7.0.0":
version "7.7.6"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.7.6.tgz#d18c511121aff1b4f2cd1d452f1bac9601dd830f"
integrity sha512-BWAJxpNVa0QlE5gZdWjSxXtemZyZ9RmrmVozxt3NUXeZhVIJ5ANyqmMc0JDrivBZyxUuQvFxlvH4OWWOogGfUw==
@@ -908,6 +920,13 @@
dependencies:
regenerator-runtime "^0.13.2"
+"@babel/runtime@^7.8.3":
+ version "7.8.3"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.8.3.tgz#0811944f73a6c926bb2ad35e918dcc1bfab279f1"
+ integrity sha512-fVHx1rzEmwB130VTkLnxR+HmxcTjGzH12LYQcFFoBwakMd3aOMD4OsRN7tGG/UOYE2ektgFrS8uACAoRk1CY0w==
+ dependencies:
+ regenerator-runtime "^0.13.2"
+
"@babel/template@^7.4.0", "@babel/template@^7.7.4":
version "7.7.4"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.7.4.tgz#428a7d9eecffe27deac0a98e23bf8e3675d2a77b"
@@ -941,6 +960,15 @@
lodash "^4.17.13"
to-fast-properties "^2.0.0"
+"@babel/types@^7.8.3":
+ version "7.8.3"
+ resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.8.3.tgz#5a383dffa5416db1b73dedffd311ffd0788fb31c"
+ integrity sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==
+ dependencies:
+ esutils "^2.0.2"
+ lodash "^4.17.13"
+ to-fast-properties "^2.0.0"
+
"@cnakazawa/watch@^1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.3.tgz#099139eaec7ebf07a27c1786a3ff64f39464d2ef"
@@ -5732,11 +5760,11 @@ mathml-tag-names@^2.0.1:
resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.1.tgz#6dff66c99d55ecf739ca53c492e626f1d12a33cc"
integrity sha512-pWB896KPGSGkp1XtyzRBftpTzwSOL0Gfk0wLvxt4f2mgzjY19o0LxJ3U25vNWTzsh7da+KTbuXQoQ3lOJZ8WHw==
-matrix-js-sdk@3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-3.0.0.tgz#97908f9eda9eeb3ba0333b7e474c45f2b258e50c"
- integrity sha512-lzUMwJAZHw7Dk0K+rubqe6kEpy4+pJ+qCp8n6lisfdKfMDJXdNCkjiiXRnakM1ZD4PFYK8ju89+NfxlyhAAd4A==
+"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop":
+ version "4.0.0"
+ resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/21e4c597d9633aef606871cf9ffffaf039142be3"
dependencies:
+ "@babel/runtime" "^7.8.3"
another-json "^0.2.0"
browser-request "^0.3.3"
bs58 "^4.0.1"