From 7d161de35b4b3988db0db80b4ad4093f9569c4e4 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 3 Jan 2019 15:02:58 +0000 Subject: [PATCH 1/2] Partial merge of develop to experimental Does not include #2336 as the file has been moved out from underneath it: will do this separately --- .eslintrc.js | 3 + CHANGELOG.md | 54 ++ CONTRIBUTING.rst | 2 +- README.md | 58 -- code_style.md | 34 +- package.json | 9 +- res/css/_common.scss | 2 +- res/css/_components.scss | 2 + res/css/structures/_RightPanel.scss | 4 + .../keybackup/_CreateKeyBackupDialog.scss | 20 +- .../keybackup/_NewRecoveryMethodDialog.scss | 41 + .../views/rooms/_RoomRecoveryReminder.scss | 44 + res/img/e2e/lock-warning.svg | 1 + res/themes/dark/css/_dark.scss | 10 + res/themes/light/css/_base.scss | 10 + scripts/gen-i18n.js | 16 +- src/BasePlatform.js | 6 +- src/ContentMessages.js | 4 +- src/Lifecycle.js | 30 +- src/Login.js | 99 +-- src/Notifier.js | 5 + src/Registration.js | 4 + src/RoomInvite.js | 65 +- src/SlashCommands.js | 11 +- src/Tinter.js | 22 +- .../keybackup/CreateKeyBackupDialog.js | 79 +- .../keybackup/IgnoreRecoveryReminderDialog.js | 70 ++ .../keybackup/NewRecoveryMethodDialog.js | 110 +++ src/components/structures/GroupView.js | 18 +- src/components/structures/MatrixChat.js | 81 +- src/components/structures/RoomSubList.js | 15 +- src/components/structures/RoomView.js | 67 +- src/components/structures/UserSettings.js | 46 +- .../structures/login/ForgotPassword.js | 23 + src/components/structures/login/Login.js | 109 ++- .../structures/login/Registration.js | 25 +- .../views/dialogs/AddressPickerDialog.js | 5 + src/components/views/dialogs/BaseDialog.js | 3 +- .../views/dialogs/DeactivateAccountDialog.js | 41 +- src/components/views/dialogs/SetMxIdDialog.js | 15 +- src/components/views/elements/AppTile.js | 37 +- src/components/views/elements/TintableSvg.js | 17 +- .../views/groups/GroupMemberList.js | 39 +- src/components/views/login/PasswordLogin.js | 24 +- .../views/login/RegistrationForm.js | 7 +- src/components/views/login/ServerConfig.js | 17 + .../views/room_settings/AliasSettings.js | 2 +- src/components/views/rooms/MemberInfo.js | 11 +- src/components/views/rooms/MessageComposer.js | 63 +- src/components/views/rooms/RoomList.js | 37 + .../views/rooms/RoomRecoveryReminder.js | 85 ++ .../views/settings/KeyBackupPanel.js | 5 +- .../views/settings/Notifications.js | 6 + src/i18n/strings/de_DE.json | 57 +- src/i18n/strings/en_EN.json | 195 +++-- src/i18n/strings/en_US.json | 2 + src/i18n/strings/eu.json | 80 +- src/i18n/strings/fr.json | 110 ++- src/i18n/strings/hi.json | 341 +++++++- src/i18n/strings/hu.json | 110 ++- src/i18n/strings/pl.json | 34 +- src/i18n/strings/sq.json | 792 ++++++++++++++++-- src/i18n/strings/zh_Hant.json | 109 ++- src/matrix-to.js | 61 +- src/notifications/StandardActions.js | 1 + .../VectorPushRulesDefinitions.js | 54 +- src/settings/Settings.js | 5 + src/shouldHideEvent.js | 12 +- src/stores/GroupStore.js | 6 +- src/stores/RoomListStore.js | 21 +- src/stores/RoomViewStore.js | 5 + src/utils/MultiInviter.js | 45 +- src/utils/PasswordScorer.js | 84 ++ test/components/structures/GroupView-test.js | 37 +- .../views/groups/GroupMemberList-test.js | 149 ++++ test/matrix-to-test.js | 186 +++- test/test-utils.js | 15 +- 77 files changed, 3526 insertions(+), 598 deletions(-) create mode 100644 res/css/views/dialogs/keybackup/_NewRecoveryMethodDialog.scss create mode 100644 res/css/views/rooms/_RoomRecoveryReminder.scss create mode 100644 res/img/e2e/lock-warning.svg create mode 100644 src/async-components/views/dialogs/keybackup/IgnoreRecoveryReminderDialog.js create mode 100644 src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js create mode 100644 src/components/views/rooms/RoomRecoveryReminder.js create mode 100644 src/utils/PasswordScorer.js create mode 100644 test/components/views/groups/GroupMemberList-test.js diff --git a/.eslintrc.js b/.eslintrc.js index 62d24ea707d..971809f851e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -47,6 +47,9 @@ module.exports = { }], "react/jsx-key": ["error"], + // Components in JSX should always be defined. + "react/jsx-no-undef": "error", + // Assert no spacing in JSX curly brackets // // diff --git a/CHANGELOG.md b/CHANGELOG.md index eea47dcb8fa..742b8b45290 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,57 @@ +Changes in [0.14.7](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.14.7) (2018-12-10) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.14.7-rc.2...v0.14.7) + + * No changes since rc.2 + +Changes in [0.14.7-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.14.7-rc.2) (2018-12-06) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.14.7-rc.1...v0.14.7-rc.2) + + * Ship the babelrc file to npm + [\#2332](https://github.com/matrix-org/matrix-react-sdk/pull/2332) + +Changes in [0.14.7-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.14.7-rc.1) (2018-12-06) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.14.6...v0.14.7-rc.1) + + * Suppress CORS errors in the 'failed to join room' dialog + [\#2306](https://github.com/matrix-org/matrix-react-sdk/pull/2306) + * Check if users exist before inviting them and communicate errors + [\#2317](https://github.com/matrix-org/matrix-react-sdk/pull/2317) + * Update from Weblate. + [\#2328](https://github.com/matrix-org/matrix-react-sdk/pull/2328) + * Allow group summary to load when /users fails + [\#2326](https://github.com/matrix-org/matrix-react-sdk/pull/2326) + * Show correct text if passphrase is skipped + [\#2324](https://github.com/matrix-org/matrix-react-sdk/pull/2324) + * Add password strength meter to backup creation UI + [\#2294](https://github.com/matrix-org/matrix-react-sdk/pull/2294) + * Check upload limits before trying to upload large files + [\#1876](https://github.com/matrix-org/matrix-react-sdk/pull/1876) + * Support .well-known discovery + [\#2227](https://github.com/matrix-org/matrix-react-sdk/pull/2227) + * Make create key backup dialog async + [\#2291](https://github.com/matrix-org/matrix-react-sdk/pull/2291) + * Forgot to enable continue button on download + [\#2288](https://github.com/matrix-org/matrix-react-sdk/pull/2288) + * Online incremental megolm backups (v2) + [\#2169](https://github.com/matrix-org/matrix-react-sdk/pull/2169) + * Add recovery key download button + [\#2284](https://github.com/matrix-org/matrix-react-sdk/pull/2284) + * Passphrase Support for e2e backups + [\#2283](https://github.com/matrix-org/matrix-react-sdk/pull/2283) + * Update async dialog interface to use promises + [\#2286](https://github.com/matrix-org/matrix-react-sdk/pull/2286) + * Support for m.login.sso + [\#2279](https://github.com/matrix-org/matrix-react-sdk/pull/2279) + * Added badge to non-autoplay GIFs + [\#2235](https://github.com/matrix-org/matrix-react-sdk/pull/2235) + * Improve terms auth flow + [\#2277](https://github.com/matrix-org/matrix-react-sdk/pull/2277) + * Handle crypto db version upgrade + [\#2282](https://github.com/matrix-org/matrix-react-sdk/pull/2282) + Changes in [0.14.6](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.14.6) (2018-11-22) ===================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.14.5...v0.14.6) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 99025f0e0ac..f7c8c8b1c5c 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -1,4 +1,4 @@ Contributing code to The React SDK ================================== -matrix-react-sdk follows the same pattern as https://github.com/matrix-org/synapse/blob/master/CONTRIBUTING.rst +matrix-react-sdk follows the same pattern as https://github.com/matrix-org/matrix-js-sdk/blob/master/CONTRIBUTING.rst diff --git a/README.md b/README.md index ac45497dd47..ec95fbd1324 100644 --- a/README.md +++ b/README.md @@ -127,61 +127,3 @@ Github Issues All issues should be filed under https://github.com/vector-im/riot-web/issues for now. - -OUTDATED: To Create Your Own Skin -================================= - -**This is ALL LIES currently, and needs to be updated** - -Skins are modules are exported from such a package in the `lib` directory. -`lib/skins` contains one directory per-skin, named after the skin, and the -`modules` directory contains modules as their javascript files. - -A basic skin is provided in the matrix-react-skin package. This also contains -a minimal application that instantiates the basic skin making a working matrix -client. - -You can use matrix-react-sdk directly, but to do this you would have to provide -'views' for each UI component. To get started quickly, use matrix-react-skin. - -To actually change the look of a skin, you can create a base skin (which -does not use views from any other skin) or you can make a derived skin. -Note that derived skins are currently experimental: for example, the CSS -from the skins it is based on will not be automatically included. - -To make a skin, create React classes for any custom components you wish to add -in a skin within `src/skins/`. These can be based off the files in -`views` in the `matrix-react-skin` package, modifying the require() statement -appropriately. - -If you make a derived skin, you only need copy the files you wish to customise. - -Once you've made all your view files, you need to make a `skinfo.json`. This -contains all the metadata for a skin. This is a JSON file with, currently, a -single key, 'baseSkin'. Set this to the empty string if your skin is a base skin, -or for a derived skin, set it to the path of your base skin's skinfo.json file, as -you would use in a require call. - -Now you have the basis of a skin, you need to generate a skindex.json file. The -`reskindex.js` tool in matrix-react-sdk does this for you. It is suggested that -you add an npm script to run this, as in matrix-react-skin. - -For more specific detail on any of these steps, look at matrix-react-skin. - -Alternative instructions: - - * Create a new NPM project. Be sure to directly depend on react, (otherwise - you can end up with two copies of react). - * Create an index.js file that sets up react. Add require statements for - React and matrix-react-sdk. Load a skin using the 'loadSkin' method on the - SDK and call Render. This can be a skin provided by a separate package or - a skin in the same package. - * Add a way to build your project: we suggest copying the scripts block - from matrix-react-skin (which uses babel and webpack). You could use - different tools but remember that at least the skins and modules of - your project should end up in plain (ie. non ES6, non JSX) javascript in - the lib directory at the end of the build process, as well as any - packaging that you might do. - * Create an index.html file pulling in your compiled javascript and the - CSS bundle from the skin you use. For now, you'll also need to manually - import CSS from any skins that your skin inherts from. diff --git a/code_style.md b/code_style.md index 2cac303e54f..96f3879ebc1 100644 --- a/code_style.md +++ b/code_style.md @@ -165,7 +165,6 @@ ECMAScript React ----- -- Use React.createClass rather than ES6 classes for components, as the boilerplate is way too heavy on ES6 currently. ES7 might improve it. - Pull out functions in props to the class, generally as specific event handlers: ```jsx @@ -174,11 +173,38 @@ React // Better // Best, if onFooClick would do anything other than directly calling doStuff ``` - - Not doing so is acceptable in a single case; in function-refs: - + + Not doing so is acceptable in a single case: in function-refs: + ```jsx this.component = self}> ``` + +- Prefer classes that extend `React.Component` (or `React.PureComponent`) instead of `React.createClass` + - You can avoid the need to bind handler functions by using [property initializers](https://reactjs.org/docs/react-component.html#constructor): + + ```js + class Widget extends React.Component + onFooClick = () => { + ... + } + } + ``` + - To define `propTypes`, use a static property: + ```js + class Widget extends React.Component + static propTypes = { + ... + } + } + ``` + - If you need to specify initial component state, [assign it](https://reactjs.org/docs/react-component.html#constructor) to `this.state` in the constructor: + ```js + constructor(props) { + super(props); + // Don't call this.setState() here! + this.state = { counter: 0 }; + } + ``` - Think about whether your component really needs state: are you duplicating information in component state that could be derived from the model? diff --git a/package.json b/package.json index b5cdfdf401c..7a63d554155 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "0.14.6", + "version": "0.14.7", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -10,6 +10,7 @@ "license": "Apache-2.0", "main": "lib/index.js", "files": [ + ".babelrc", ".eslintrc.js", "CHANGELOG.md", "CONTRIBUTING.rst", @@ -72,11 +73,12 @@ "gfm.css": "^1.1.1", "glob": "^5.0.14", "highlight.js": "^9.13.0", + "is-ip": "^2.0.0", "isomorphic-fetch": "^2.2.1", "linkifyjs": "^2.1.6", "lodash": "^4.13.1", "lolex": "2.3.2", - "matrix-js-sdk": "0.14.1", + "matrix-js-sdk": "0.14.2", "optimist": "^0.6.1", "pako": "^1.0.5", "prop-types": "^15.5.8", @@ -96,7 +98,8 @@ "text-encoding-utf-8": "^1.0.1", "url": "^0.11.0", "velocity-vector": "github:vector-im/velocity#059e3b2", - "whatwg-fetch": "^1.1.1" + "whatwg-fetch": "^1.1.1", + "zxcvbn": "^4.4.2" }, "devDependencies": { "babel-cli": "^6.26.0", diff --git a/res/css/_common.scss b/res/css/_common.scss index 797070d4e2a..bec4c02c184 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -32,7 +32,7 @@ body { margin: 0px; } -div.error, div.warning { +.error, .warning { color: $warning-color; } diff --git a/res/css/_components.scss b/res/css/_components.scss index 92e243e8d11..63b1bde2d6f 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -49,6 +49,7 @@ @import "./views/dialogs/_ShareDialog.scss"; @import "./views/dialogs/_UnknownDeviceDialog.scss"; @import "./views/dialogs/keybackup/_CreateKeyBackupDialog.scss"; +@import "./views/dialogs/keybackup/_NewRecoveryMethodDialog.scss"; @import "./views/dialogs/keybackup/_RestoreKeyBackupDialog.scss"; @import "./views/directory/_NetworkDropdown.scss"; @import "./views/elements/_AccessibleButton.scss"; @@ -104,6 +105,7 @@ @import "./views/rooms/_RoomHeader.scss"; @import "./views/rooms/_RoomList.scss"; @import "./views/rooms/_RoomPreviewBar.scss"; +@import "./views/rooms/_RoomRecoveryReminder.scss"; @import "./views/rooms/_RoomSettings.scss"; @import "./views/rooms/_RoomTile.scss"; @import "./views/rooms/_RoomTooltip.scss"; diff --git a/res/css/structures/_RightPanel.scss b/res/css/structures/_RightPanel.scss index 592eea067e4..ae9e7ba981e 100644 --- a/res/css/structures/_RightPanel.scss +++ b/res/css/structures/_RightPanel.scss @@ -57,6 +57,10 @@ limitations under the License. pointer-events: none; } +.mx_RightPanel_headerButton_badgeHighlight .mx_RightPanel_headerButton_badge { + color: $warning-color; +} + .mx_RightPanel_headerButton_highlight { border-color: $button-bg-color; } diff --git a/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss b/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss index 507c89ace71..2cb6b11c0cd 100644 --- a/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss +++ b/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss @@ -19,8 +19,26 @@ limitations under the License. padding: 20px } +.mx_CreateKeyBackupDialog_primaryContainer::after { + content: ""; + clear: both; + display: block; +} + +.mx_CreateKeyBackupDialog_passPhraseHelp { + float: right; + width: 230px; + height: 85px; + margin-left: 20px; + font-size: 80%; +} + +.mx_CreateKeyBackupDialog_passPhraseHelp progress { + width: 100%; +} + .mx_CreateKeyBackupDialog_passPhraseInput { - width: 300px; + width: 250px; border: 1px solid $accent-color; border-radius: 5px; padding: 10px; diff --git a/res/css/views/dialogs/keybackup/_NewRecoveryMethodDialog.scss b/res/css/views/dialogs/keybackup/_NewRecoveryMethodDialog.scss new file mode 100644 index 00000000000..370f82d9ab1 --- /dev/null +++ b/res/css/views/dialogs/keybackup/_NewRecoveryMethodDialog.scss @@ -0,0 +1,41 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_NewRecoveryMethodDialog .mx_Dialog_title { + margin-bottom: 32px; +} + +.mx_NewRecoveryMethodDialog_title { + position: relative; + padding-left: 45px; + padding-bottom: 10px; + + &:before { + mask: url("../../../img/e2e/lock-warning.svg"); + mask-repeat: no-repeat; + background-color: $primary-fg-color; + content: ""; + position: absolute; + top: -6px; + right: 0; + bottom: 0; + left: 0; + } +} + +.mx_NewRecoveryMethodDialog .mx_Dialog_buttons { + margin-top: 36px; +} diff --git a/res/css/views/rooms/_RoomRecoveryReminder.scss b/res/css/views/rooms/_RoomRecoveryReminder.scss new file mode 100644 index 00000000000..e4e2d19b424 --- /dev/null +++ b/res/css/views/rooms/_RoomRecoveryReminder.scss @@ -0,0 +1,44 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_RoomRecoveryReminder { + display: flex; + flex-direction: column; + text-align: center; + background-color: $room-warning-bg-color; + padding: 20px; + border: 1px solid $primary-hairline-color; + border-bottom: unset; +} + +.mx_RoomRecoveryReminder_header { + font-weight: bold; + margin-bottom: 1em; +} + +.mx_RoomRecoveryReminder_body { + margin-bottom: 1em; +} + +.mx_RoomRecoveryReminder_button { + @mixin mx_DialogButton; + margin: 0 10px; +} + +.mx_RoomRecoveryReminder_button.mx_RoomRecoveryReminder_secondary { + @mixin mx_DialogButton_secondary; + background-color: transparent; +} diff --git a/res/img/e2e/lock-warning.svg b/res/img/e2e/lock-warning.svg new file mode 100644 index 00000000000..a984ed85a09 --- /dev/null +++ b/res/img/e2e/lock-warning.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index ed84bde6984..636db5b39ef 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -107,6 +107,8 @@ $voip-accept-color: #80f480; $rte-bg-color: #353535; $rte-code-bg-color: #000; +$room-warning-bg-color: #2d2d2d; + // ******************** $roomtile-name-color: rgba(186, 186, 186, 0.8); @@ -185,6 +187,14 @@ $progressbar-color: #000; outline: none; } +@define-mixin mx_DialogButton_secondary { + // flip colours for the secondary ones + font-weight: 600; + border: 1px solid $accent-color ! important; + color: $accent-color; + background-color: $accent-fg-color; +} + // Nasty hacks to apply a filter to arbitrary monochrome artwork to make it // better match the theme. Typically applied to dark grey 'off' buttons or // light grey 'on' buttons. diff --git a/res/themes/light/css/_base.scss b/res/themes/light/css/_base.scss index 71c1ab5e3cf..9fcb58d7f1f 100644 --- a/res/themes/light/css/_base.scss +++ b/res/themes/light/css/_base.scss @@ -181,6 +181,8 @@ $imagebody-giflabel-border: rgba(0, 0, 0, 0.2); // unused? $progressbar-color: #000; +$room-warning-bg-color: #fff8e3; + // ***** Mixins! ***** @define-mixin mx_DialogButton { @@ -213,3 +215,11 @@ $progressbar-color: #000; font-size: 15px; padding: 0px 1.5em 0px 1.5em; } + +@define-mixin mx_DialogButton_secondary { + // flip colours for the secondary ones + font-weight: 600; + border: 1px solid $accent-color ! important; + color: $accent-color; + background-color: $accent-fg-color; +} diff --git a/scripts/gen-i18n.js b/scripts/gen-i18n.js index a1a2e6f7c5a..3d3d5af116f 100755 --- a/scripts/gen-i18n.js +++ b/scripts/gen-i18n.js @@ -222,10 +222,21 @@ const translatables = new Set(); const walkOpts = { listeners: { + names: function(root, nodeNamesArray) { + // Sort the names case insensitively and alphabetically to + // maintain some sense of order between the different strings. + nodeNamesArray.sort((a, b) => { + a = a.toLowerCase(); + b = b.toLowerCase(); + if (a > b) return 1; + if (a < b) return -1; + return 0; + }); + }, file: function(root, fileStats, next) { const fullPath = path.join(root, fileStats.name); - let ltrs; + let trs; if (fileStats.name.endsWith('.js')) { trs = getTranslationsJs(fullPath); } else if (fileStats.name.endsWith('.html')) { @@ -235,7 +246,8 @@ const walkOpts = { } console.log(`${fullPath} (${trs.size} strings)`); for (const tr of trs.values()) { - translatables.add(tr); + // Convert DOS line endings to unix + translatables.add(tr.replace(/\r\n/g, "\n")); } }, } diff --git a/src/BasePlatform.js b/src/BasePlatform.js index abc9aa0bed3..79f0d69e2c7 100644 --- a/src/BasePlatform.js +++ b/src/BasePlatform.js @@ -3,6 +3,7 @@ /* Copyright 2016 Aviral Dasgupta Copyright 2016 OpenMarket Ltd +Copyright 2018 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -105,11 +106,6 @@ export default class BasePlatform { return "Not implemented"; } - isElectron(): boolean { return false; } - - setupScreenSharingForIframe() { - } - /** * Restarts the application, without neccessarily reloading * any application code diff --git a/src/ContentMessages.js b/src/ContentMessages.js index fd219771084..f2bbdfafe51 100644 --- a/src/ContentMessages.js +++ b/src/ContentMessages.js @@ -377,9 +377,9 @@ class ContentMessages { } } if (error) { - dis.dispatch({action: 'upload_failed', upload: upload}); + dis.dispatch({action: 'upload_failed', upload, error}); } else { - dis.dispatch({action: 'upload_finished', upload: upload}); + dis.dispatch({action: 'upload_finished', upload}); } }); } diff --git a/src/Lifecycle.js b/src/Lifecycle.js index b0912c759e0..ed057eb020c 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -32,6 +32,7 @@ import Modal from './Modal'; import sdk from './index'; import ActiveWidgetStore from './stores/ActiveWidgetStore'; import PlatformPeg from "./PlatformPeg"; +import {sendLoginRequest} from "./Login"; /** * Called at startup, to attempt to build a logged-in Matrix session. It tries @@ -129,27 +130,17 @@ export function attemptTokenLogin(queryParams, defaultDeviceDisplayName) { return Promise.resolve(false); } - // create a temporary MatrixClient to do the login - const client = Matrix.createClient({ - baseUrl: queryParams.homeserver, - }); - - return client.login( + return sendLoginRequest( + queryParams.homeserver, + queryParams.identityServer, "m.login.token", { token: queryParams.loginToken, initial_device_display_name: defaultDeviceDisplayName, }, - ).then(function(data) { + ).then(function(creds) { console.log("Logged in with token"); return _clearStorage().then(() => { - _persistCredentialsToLocalStorage({ - userId: data.user_id, - deviceId: data.device_id, - accessToken: data.access_token, - homeserverUrl: queryParams.homeserver, - identityServerUrl: queryParams.identityServer, - guest: false, - }); + _persistCredentialsToLocalStorage(creds); return true; }); }).catch((err) => { @@ -506,16 +497,7 @@ function _clearStorage() { Analytics.logout(); if (window.localStorage) { - const hsUrl = window.localStorage.getItem("mx_hs_url"); - const isUrl = window.localStorage.getItem("mx_is_url"); window.localStorage.clear(); - - // preserve our HS & IS URLs for convenience - // N.B. we cache them in hsUrl/isUrl and can't really inline them - // as getCurrentHsUrl() may call through to localStorage. - // NB. We do clear the device ID (as well as all the settings) - if (hsUrl) window.localStorage.setItem("mx_hs_url", hsUrl); - if (isUrl) window.localStorage.setItem("mx_is_url", isUrl); } // create a temporary client to clear out the persistent stores. diff --git a/src/Login.js b/src/Login.js index ec55a1e8c7a..330eb8a8f54 100644 --- a/src/Login.js +++ b/src/Login.js @@ -1,6 +1,7 @@ /* Copyright 2015, 2016 OpenMarket Ltd Copyright 2017 Vector Creations Ltd +Copyright 2018 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,7 +18,6 @@ limitations under the License. import Matrix from "matrix-js-sdk"; -import Promise from 'bluebird'; import url from 'url'; export default class Login { @@ -141,60 +141,20 @@ export default class Login { }; Object.assign(loginParams, legacyParams); - const client = this._createTemporaryClient(); - const tryFallbackHs = (originalError) => { - const fbClient = Matrix.createClient({ - baseUrl: self._fallbackHsUrl, - idBaseUrl: this._isUrl, - }); - - return fbClient.login('m.login.password', loginParams).then(function(data) { - return Promise.resolve({ - homeserverUrl: self._fallbackHsUrl, - identityServerUrl: self._isUrl, - userId: data.user_id, - deviceId: data.device_id, - accessToken: data.access_token, - }); - }).catch((fallback_error) => { + return sendLoginRequest( + self._fallbackHsUrl, this._isUrl, 'm.login.password', loginParams, + ).catch((fallback_error) => { console.log("fallback HS login failed", fallback_error); // throw the original error throw originalError; }); }; - const tryLowercaseUsername = (originalError) => { - const loginParamsLowercase = Object.assign({}, loginParams, { - user: username.toLowerCase(), - identifier: { - user: username.toLowerCase(), - }, - }); - return client.login('m.login.password', loginParamsLowercase).then(function(data) { - return Promise.resolve({ - homeserverUrl: self._hsUrl, - identityServerUrl: self._isUrl, - userId: data.user_id, - deviceId: data.device_id, - accessToken: data.access_token, - }); - }).catch((fallback_error) => { - console.log("Lowercase username login failed", fallback_error); - // throw the original error - throw originalError; - }); - }; let originalLoginError = null; - return client.login('m.login.password', loginParams).then(function(data) { - return Promise.resolve({ - homeserverUrl: self._hsUrl, - identityServerUrl: self._isUrl, - userId: data.user_id, - deviceId: data.device_id, - accessToken: data.access_token, - }); - }).catch((error) => { + return sendLoginRequest( + self._hsUrl, self._isUrl, 'm.login.password', loginParams, + ).catch((error) => { originalLoginError = error; if (error.httpStatus === 403) { if (self._fallbackHsUrl) { @@ -202,22 +162,6 @@ export default class Login { } } throw originalLoginError; - }).catch((error) => { - // We apparently squash case at login serverside these days: - // https://github.com/matrix-org/synapse/blob/1189be43a2479f5adf034613e8d10e3f4f452eb9/synapse/handlers/auth.py#L475 - // so this wasn't needed after all. Keeping the code around in case the - // the situation changes... - - /* - if ( - error.httpStatus === 403 && - loginParams.identifier.type === 'm.id.user' && - username.search(/[A-Z]/) > -1 - ) { - return tryLowercaseUsername(originalLoginError); - } - */ - throw originalLoginError; }).catch((error) => { console.log("Login failed", error); throw error; @@ -239,3 +183,32 @@ export default class Login { return client.getSsoLoginUrl(url.format(parsedUrl), loginType); } } + + +/** + * Send a login request to the given server, and format the response + * as a MatrixClientCreds + * + * @param {string} hsUrl the base url of the Homeserver used to log in. + * @param {string} isUrl the base url of the default identity server + * @param {string} loginType the type of login to do + * @param {object} loginParams the parameters for the login + * + * @returns {MatrixClientCreds} + */ +export async function sendLoginRequest(hsUrl, isUrl, loginType, loginParams) { + const client = Matrix.createClient({ + baseUrl: hsUrl, + idBaseUrl: isUrl, + }); + + const data = await client.login(loginType, loginParams); + + return { + homeserverUrl: hsUrl, + identityServerUrl: isUrl, + userId: data.user_id, + deviceId: data.device_id, + accessToken: data.access_token, + }; +} diff --git a/src/Notifier.js b/src/Notifier.js index 80e8be10846..8550f3bf954 100644 --- a/src/Notifier.js +++ b/src/Notifier.js @@ -289,6 +289,11 @@ const Notifier = { const room = MatrixClientPeg.get().getRoom(ev.getRoomId()); const actions = MatrixClientPeg.get().getPushActionsForEvent(ev); if (actions && actions.notify) { + dis.dispatch({ + action: "event_notification", + event: ev, + room: room, + }); if (this.isEnabled()) { this._displayPopupNotification(ev, room); } diff --git a/src/Registration.js b/src/Registration.js index f86c9cc6181..98aee3ac833 100644 --- a/src/Registration.js +++ b/src/Registration.js @@ -26,6 +26,10 @@ import MatrixClientPeg from './MatrixClientPeg'; import Modal from './Modal'; import { _t } from './languageHandler'; +// Regex for what a "safe" or "Matrix-looking" localpart would be. +// TODO: Update as needed for https://github.com/matrix-org/matrix-doc/issues/1514 +export const SAFE_LOCALPART_REGEX = /^[a-z0-9=_\-./]+$/; + /** * Starts either the ILAG or full registration flow, depending * on what the HS supports diff --git a/src/RoomInvite.js b/src/RoomInvite.js index a96d1b2f6b5..3547b9195fd 100644 --- a/src/RoomInvite.js +++ b/src/RoomInvite.js @@ -1,6 +1,6 @@ /* Copyright 2016 OpenMarket Ltd -Copyright 2017 New Vector Ltd +Copyright 2017, 2018 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,6 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import React from 'react'; import MatrixClientPeg from './MatrixClientPeg'; import MultiInviter from './utils/MultiInviter'; import Modal from './Modal'; @@ -25,18 +26,6 @@ import dis from './dispatcher'; import DMRoomMap from './utils/DMRoomMap'; import { _t } from './languageHandler'; -export function inviteToRoom(roomId, addr) { - const addrType = getAddressType(addr); - - if (addrType == 'email') { - return MatrixClientPeg.get().inviteByEmail(roomId, addr); - } else if (addrType == 'mx-user-id') { - return MatrixClientPeg.get().invite(roomId, addr); - } else { - throw new Error('Unsupported address'); - } -} - /** * Invites multiple addresses to a room * Simpler interface to utils/MultiInviter but with @@ -46,9 +35,9 @@ export function inviteToRoom(roomId, addr) { * @param {string[]} addrs Array of strings of addresses to invite. May be matrix IDs or 3pids. * @returns {Promise} Promise */ -export function inviteMultipleToRoom(roomId, addrs) { +function inviteMultipleToRoom(roomId, addrs) { const inviter = new MultiInviter(roomId); - return inviter.invite(addrs); + return inviter.invite(addrs).then(states => Promise.resolve({states, inviter})); } export function showStartChatInviteDialog() { @@ -129,8 +118,8 @@ function _onStartChatFinished(shouldInvite, addrs) { createRoom().then((roomId) => { room = MatrixClientPeg.get().getRoom(roomId); return inviteMultipleToRoom(roomId, addrTexts); - }).then((addrs) => { - return _showAnyInviteErrors(addrs, room); + }).then((result) => { + return _showAnyInviteErrors(result.states, room, result.inviter); }).catch((err) => { console.error(err.stack); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); @@ -148,9 +137,9 @@ function _onRoomInviteFinished(roomId, shouldInvite, addrs) { const addrTexts = addrs.map((addr) => addr.address); // Invite new users to a room - inviteMultipleToRoom(roomId, addrTexts).then((addrs) => { + inviteMultipleToRoom(roomId, addrTexts).then((result) => { const room = MatrixClientPeg.get().getRoom(roomId); - return _showAnyInviteErrors(addrs, room); + return _showAnyInviteErrors(result.states, room, result.inviter); }).catch((err) => { console.error(err.stack); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); @@ -169,22 +158,36 @@ function _isDmChat(addrTexts) { } } -function _showAnyInviteErrors(addrs, room) { +function _showAnyInviteErrors(addrs, room, inviter) { // Show user any errors - const errorList = []; - for (const addr of Object.keys(addrs)) { - if (addrs[addr] === "error") { - errorList.push(addr); - } - } - - if (errorList.length > 0) { + const failedUsers = Object.keys(addrs).filter(a => addrs[a] === 'error'); + if (failedUsers.length === 1 && inviter.fatal) { + // Just get the first message because there was a fatal problem on the first + // user. This usually means that no other users were attempted, making it + // pointless for us to list who failed exactly. const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createTrackedDialog('Failed to invite the following users to the room', '', ErrorDialog, { - title: _t("Failed to invite the following users to the %(roomName)s room:", {roomName: room.name}), - description: errorList.join(", "), + Modal.createTrackedDialog('Failed to invite users to the room', '', ErrorDialog, { + title: _t("Failed to invite users to the room:", {roomName: room.name}), + description: inviter.getErrorText(failedUsers[0]), }); + } else { + const errorList = []; + for (const addr of failedUsers) { + if (addrs[addr] === "error") { + const reason = inviter.getErrorText(addr); + errorList.push(addr + ": " + reason); + } + } + + if (errorList.length > 0) { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createTrackedDialog('Failed to invite the following users to the room', '', ErrorDialog, { + title: _t("Failed to invite the following users to the %(roomName)s room:", {roomName: room.name}), + description: errorList.join(
), + }); + } } + return addrs; } diff --git a/src/SlashCommands.js b/src/SlashCommands.js index 8a34ba7ab13..24328d63729 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -26,6 +26,7 @@ import Modal from './Modal'; import SettingsStore, {SettingLevel} from './settings/SettingsStore'; import {MATRIXTO_URL_PATTERN} from "./linkify-matrix"; import * as querystring from "querystring"; +import MultiInviter from './utils/MultiInviter'; class Command { @@ -142,7 +143,15 @@ export const CommandMap = { if (args) { const matches = args.match(/^(\S+)$/); if (matches) { - return success(MatrixClientPeg.get().invite(roomId, matches[1])); + // We use a MultiInviter to re-use the invite logic, even though + // we're only inviting one user. + const userId = matches[1]; + const inviter = new MultiInviter(roomId); + return success(inviter.invite([userId]).then(() => { + if (inviter.getCompletionState(userId) !== "invited") { + throw new Error(inviter.getErrorText(userId)); + } + })); } } return reject(this.getUsage()); diff --git a/src/Tinter.js b/src/Tinter.js index de9ae940970..80375dead29 100644 --- a/src/Tinter.js +++ b/src/Tinter.js @@ -392,7 +392,7 @@ class Tinter { // XXX: we could just move this all into TintableSvg, but as it's so similar // to the CSS fixup stuff in Tinter (just that the fixups are stored in TintableSvg) // keeping it here for now. - calcSvgFixups(svgs) { + calcSvgFixups(svgs, forceColors) { // go through manually fixing up SVG colours. // we could do this by stylesheets, but keeping the stylesheets // updated would be a PITA, so just brute-force search for the @@ -420,13 +420,21 @@ class Tinter { const tag = tags[j]; for (let k = 0; k < this.svgAttrs.length; k++) { const attr = this.svgAttrs[k]; - for (let l = 0; l < this.keyHex.length; l++) { - if (tag.getAttribute(attr) && - tag.getAttribute(attr).toUpperCase() === this.keyHex[l]) { + for (let m = 0; m < this.keyHex.length; m++) { // dev note: don't use L please. + // We use a different attribute from the one we're setting + // because we may also be using forceColors. If we were to + // check the keyHex against a forceColors value, it may not + // match and therefore not change when we need it to. + const valAttrName = "mx-val-" + attr; + let attribute = tag.getAttribute(valAttrName); + if (!attribute) attribute = tag.getAttribute(attr); // fall back to the original + if (attribute && (attribute.toUpperCase() === this.keyHex[m] || attribute.toLowerCase() === this.keyRgb[m])) { fixups.push({ node: tag, attr: attr, - index: l, + refAttr: valAttrName, + index: m, + forceColors: forceColors, }); } } @@ -442,7 +450,9 @@ class Tinter { if (DEBUG) console.log("applySvgFixups start for " + fixups); for (let i = 0; i < fixups.length; i++) { const svgFixup = fixups[i]; - svgFixup.node.setAttribute(svgFixup.attr, this.colors[svgFixup.index]); + const forcedColor = svgFixup.forceColors ? svgFixup.forceColors[svgFixup.index] : null; + svgFixup.node.setAttribute(svgFixup.attr, forcedColor ? forcedColor : this.colors[svgFixup.index]); + svgFixup.node.setAttribute(svgFixup.refAttr, this.colors[svgFixup.index]); } if (DEBUG) console.log("applySvgFixups end"); } diff --git a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js index 2f43d18072c..0db9d0699b2 100644 --- a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js +++ b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js @@ -17,6 +17,7 @@ limitations under the License. import React from 'react'; import sdk from '../../../../index'; import MatrixClientPeg from '../../../../MatrixClientPeg'; +import { scorePassword } from '../../../../utils/PasswordScorer'; import FileSaver from 'file-saver'; @@ -30,6 +31,8 @@ const PHASE_BACKINGUP = 4; const PHASE_DONE = 5; const PHASE_OPTOUT_CONFIRM = 6; +const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc. + // XXX: copied from ShareDialog: factor out into utils function selectText(target) { const range = document.createRange(); @@ -52,6 +55,8 @@ export default React.createClass({ passPhraseConfirm: '', copied: false, downloaded: false, + zxcvbnResult: null, + setPassPhrase: false, }; }, @@ -87,25 +92,33 @@ export default React.createClass({ }); }, - _createBackup: function() { + _createBackup: async function() { this.setState({ phase: PHASE_BACKINGUP, error: null, }); - this._createBackupPromise = MatrixClientPeg.get().createKeyBackupVersion( - this._keyBackupInfo, - ).then((info) => { - return MatrixClientPeg.get().backupAllGroupSessions(info.version); - }).then(() => { + let info; + try { + info = await MatrixClientPeg.get().createKeyBackupVersion( + this._keyBackupInfo, + ); + await MatrixClientPeg.get().backupAllGroupSessions(info.version); this.setState({ phase: PHASE_DONE, }); - }).catch(e => { + } catch (e) { console.log("Error creating key backup", e); + // TODO: If creating a version succeeds, but backup fails, should we + // delete the version, disable backup, or do nothing? If we just + // disable without deleting, we'll enable on next app reload since + // it is trusted. + if (info) { + MatrixClientPeg.get().deleteKeyBackupVersion(info.version); + } this.setState({ error: e, }); - }); + } }, _onCancel: function() { @@ -128,6 +141,7 @@ export default React.createClass({ this._keyBackupInfo = await MatrixClientPeg.get().prepareKeyBackupVersion(); this.setState({ copied: false, + downloaded: false, phase: PHASE_SHOWKEY, }); }, @@ -145,7 +159,9 @@ export default React.createClass({ _onPassPhraseConfirmNextClick: async function() { this._keyBackupInfo = await MatrixClientPeg.get().prepareKeyBackupVersion(this.state.passPhrase); this.setState({ + setPassPhrase: true, copied: false, + downloaded: false, phase: PHASE_SHOWKEY, }); }, @@ -173,6 +189,10 @@ export default React.createClass({ _onPassPhraseChange: function(e) { this.setState({ passPhrase: e.target.value, + // precompute this and keep it in state: zxcvbn is fast but + // we use it in a couple of different places so no point recomputing + // it unnecessarily. + zxcvbnResult: scorePassword(e.target.value), }); }, @@ -183,17 +203,46 @@ export default React.createClass({ }, _passPhraseIsValid: function() { - return this.state.passPhrase !== ''; + return this.state.zxcvbnResult && this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE; }, _renderPhasePassPhrase: function() { const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + + let strengthMeter; + let helpText; + if (this.state.zxcvbnResult) { + if (this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE) { + helpText = _t("Great! This passphrase looks strong enough."); + } else { + const suggestions = []; + for (let i = 0; i < this.state.zxcvbnResult.feedback.suggestions.length; ++i) { + suggestions.push(
{this.state.zxcvbnResult.feedback.suggestions[i]}
); + } + const suggestionBlock = suggestions.length > 0 ?
+ {suggestions} +
: null; + + helpText =
+ {this.state.zxcvbnResult.feedback.warning} + {suggestionBlock} +
; + } + strengthMeter =
+ +
; + } + return

{_t("Secure your encrypted message history with a Recovery Passphrase.")}

{_t("You'll need it if you log out or lose access to this device.")}

+
+ {strengthMeter} + {helpText} +

{_t( - "If you don't want encrypted message history to be availble on other devices, "+ + "If you don't want encrypted message history to be available on other devices, "+ ".", {}, { @@ -290,9 +339,17 @@ export default React.createClass({ _renderPhaseShowKey: function() { const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + + let bodyText; + if (this.state.setPassPhrase) { + bodyText = _t("As a safety net, you can use it to restore your encrypted message history if you forget your Recovery Passphrase."); + } else { + bodyText = _t("As a safety net, you can use it to restore your encrypted message history."); + } + return

{_t("Make a copy of this Recovery Key and keep it safe.")}

-

{_t("As a safety net, you can use it to restore your encrypted message history if you forget your Recovery Passphrase.")}

+

{bodyText}

{_t("Your Recovery Key")}
diff --git a/src/async-components/views/dialogs/keybackup/IgnoreRecoveryReminderDialog.js b/src/async-components/views/dialogs/keybackup/IgnoreRecoveryReminderDialog.js new file mode 100644 index 00000000000..a9df3cca6e2 --- /dev/null +++ b/src/async-components/views/dialogs/keybackup/IgnoreRecoveryReminderDialog.js @@ -0,0 +1,70 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from "react"; +import PropTypes from "prop-types"; +import sdk from "../../../../index"; +import { _t } from "../../../../languageHandler"; + +export default class IgnoreRecoveryReminderDialog extends React.PureComponent { + static propTypes = { + onDontAskAgain: PropTypes.func.isRequired, + onFinished: PropTypes.func.isRequired, + onSetup: PropTypes.func.isRequired, + } + + onDontAskAgainClick = () => { + this.props.onFinished(); + this.props.onDontAskAgain(); + } + + onSetupClick = () => { + this.props.onFinished(); + this.props.onSetup(); + } + + render() { + const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog"); + const DialogButtons = sdk.getComponent("views.elements.DialogButtons"); + + return ( + +
+

{_t( + "Without setting up Secure Message Recovery, " + + "you'll lose your secure message history when you " + + "log out.", + )}

+

{_t( + "If you don't want to set this up now, you can later " + + "in Settings.", + )}

+
+ +
+
+
+ ); + } +} diff --git a/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js b/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js new file mode 100644 index 00000000000..e88e0444bc8 --- /dev/null +++ b/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js @@ -0,0 +1,110 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from "react"; +import PropTypes from "prop-types"; +import sdk from "../../../../index"; +import MatrixClientPeg from '../../../../MatrixClientPeg'; +import dis from "../../../../dispatcher"; +import { _t } from "../../../../languageHandler"; +import Modal from "../../../../Modal"; + +export default class NewRecoveryMethodDialog extends React.PureComponent { + static propTypes = { + onFinished: PropTypes.func.isRequired, + } + + onGoToSettingsClick = () => { + this.props.onFinished(); + dis.dispatch({ action: 'view_user_settings' }); + } + + onSetupClick = async() => { + // TODO: Should change to a restore key backup flow that checks the + // recovery passphrase while at the same time also cross-signing the + // device as well in a single flow. Since we don't have that yet, we'll + // look for an unverified device and verify it. Note that this means + // we won't restore keys yet; instead we'll only trust the backup for + // sending our own new keys to it. + let backupSigStatus; + try { + const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion(); + backupSigStatus = await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo); + } catch (e) { + console.log("Unable to fetch key backup status", e); + return; + } + + let unverifiedDevice; + for (const sig of backupSigStatus.sigs) { + if (!sig.device.isVerified()) { + unverifiedDevice = sig.device; + break; + } + } + if (!unverifiedDevice) { + console.log("Unable to find a device to verify."); + return; + } + + const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog'); + Modal.createTrackedDialog('Device Verify Dialog', '', DeviceVerifyDialog, { + userId: MatrixClientPeg.get().credentials.userId, + device: unverifiedDevice, + onFinished: this.props.onFinished, + }); + } + + render() { + const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog"); + const DialogButtons = sdk.getComponent("views.elements.DialogButtons"); + const title = + {_t("New Recovery Method")} + ; + + return ( + +
+

{_t( + "A new recovery passphrase and key for Secure " + + "Messages has been detected.", + )}

+

{_t( + "Setting up Secure Messages on this device " + + "will re-encrypt this device's message history with " + + "the new recovery method.", + )}

+

{_t( + "If you didn't set the new recovery method, an " + + "attacker may be trying to access your account. " + + "Change your account password and set a new recovery " + + "method immediately in Settings.", + )}

+ +
+
+ ); + } +} diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index 478126db75b..937e07d31e5 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -473,7 +473,7 @@ export default React.createClass({ GroupStore.registerListener(groupId, this.onGroupStoreUpdated.bind(this, firstInit)); let willDoOnboarding = false; // XXX: This should be more fluxy - let's get the error from GroupStore .getError or something - GroupStore.on('error', (err, errorGroupId) => { + GroupStore.on('error', (err, errorGroupId, stateKey) => { if (this._unmounted || groupId !== errorGroupId) return; if (err.errcode === 'M_GUEST_ACCESS_FORBIDDEN' && !willDoOnboarding) { dis.dispatch({ @@ -486,11 +486,13 @@ export default React.createClass({ dis.dispatch({action: 'require_registration'}); willDoOnboarding = true; } - this.setState({ - summary: null, - error: err, - editing: false, - }); + if (stateKey === GroupStore.STATE_KEY.Summary) { + this.setState({ + summary: null, + error: err, + editing: false, + }); + } }); }, @@ -514,7 +516,6 @@ export default React.createClass({ isUserMember: GroupStore.getGroupMembers(this.props.groupId).some( (m) => m.userId === this._matrixClient.credentials.userId, ), - error: null, }); // XXX: This might not work but this.props.groupIsNew unused anyway if (this.props.groupIsNew && firstInit) { @@ -1079,6 +1080,7 @@ export default React.createClass({ }, _getJoinableNode: function() { + const InlineSpinner = sdk.getComponent('elements.InlineSpinner'); return this.state.editing ?

{ _t('Who can join this community?') } @@ -1160,7 +1162,7 @@ export default React.createClass({ if (this.state.summaryLoading && this.state.error === null || this.state.saving) { return ; - } else if (this.state.summary) { + } else if (this.state.summary && !this.state.error) { const summary = this.state.summary; let avatarNode; diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 187caa69df3..b01174a91c9 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -48,6 +48,8 @@ import SettingsStore, {SettingLevel} from "../../settings/SettingsStore"; import { startAnyRegistrationFlow } from "../../Registration.js"; import { messageForSyncError } from '../../utils/ErrorUtils'; +const AutoDiscovery = Matrix.AutoDiscovery; + // Disable warnings for now: we use deprecated bluebird functions // and need to migrate, but they spam the console with warnings. Promise.config({warnings: false}); @@ -181,6 +183,12 @@ export default React.createClass({ register_is_url: null, register_id_sid: null, + // Parameters used for setting up the login/registration views + defaultServerName: this.props.config.default_server_name, + defaultHsUrl: this.props.config.default_hs_url, + defaultIsUrl: this.props.config.default_is_url, + defaultServerDiscoveryError: null, + // When showing Modal dialogs we need to set aria-hidden on the root app element // and disable it when there are no dialogs hideToSRUsers: false, @@ -199,20 +207,24 @@ export default React.createClass({ }; }, + getDefaultServerName: function() { + return this.state.defaultServerName; + }, + getCurrentHsUrl: function() { if (this.state.register_hs_url) { return this.state.register_hs_url; } else if (MatrixClientPeg.get()) { return MatrixClientPeg.get().getHomeserverUrl(); - } else if (window.localStorage && window.localStorage.getItem("mx_hs_url")) { - return window.localStorage.getItem("mx_hs_url"); } else { return this.getDefaultHsUrl(); } }, - getDefaultHsUrl() { - return this.props.config.default_hs_url || "https://matrix.org"; + getDefaultHsUrl(defaultToMatrixDotOrg) { + defaultToMatrixDotOrg = typeof(defaultToMatrixDotOrg) !== 'boolean' ? true : defaultToMatrixDotOrg; + if (!this.state.defaultHsUrl && defaultToMatrixDotOrg) return "https://matrix.org"; + return this.state.defaultHsUrl; }, getFallbackHsUrl: function() { @@ -224,15 +236,13 @@ export default React.createClass({ return this.state.register_is_url; } else if (MatrixClientPeg.get()) { return MatrixClientPeg.get().getIdentityServerUrl(); - } else if (window.localStorage && window.localStorage.getItem("mx_is_url")) { - return window.localStorage.getItem("mx_is_url"); } else { return this.getDefaultIsUrl(); } }, getDefaultIsUrl() { - return this.props.config.default_is_url || "https://vector.im"; + return this.state.defaultIsUrl || "https://vector.im"; }, componentWillMount: function() { @@ -282,6 +292,20 @@ export default React.createClass({ console.info(`Team token set to ${this._teamToken}`); } + // Set up the default URLs (async) + if (this.getDefaultServerName() && !this.getDefaultHsUrl(false)) { + this.setState({loadingDefaultHomeserver: true}); + this._tryDiscoverDefaultHomeserver(this.getDefaultServerName()); + } else if (this.getDefaultServerName() && this.getDefaultHsUrl(false)) { + // Ideally we would somehow only communicate this to the server admins, but + // given this is at login time we can't really do much besides hope that people + // will check their settings. + this.setState({ + defaultServerName: null, // To un-hide any secrets people might be keeping + defaultServerDiscoveryError: _t("Invalid configuration: Cannot supply a default homeserver URL and a default server name"), + }); + } + // Set a default HS with query param `hs_url` const paramHs = this.props.startingFragmentQueryParams.hs_url; if (paramHs) { @@ -1410,6 +1434,11 @@ export default React.createClass({ break; } }); + cli.on("crypto.keyBackupFailed", () => { + Modal.createTrackedDialogAsync('New Recovery Method', 'New Recovery Method', + import('../../async-components/views/dialogs/keybackup/NewRecoveryMethodDialog'), + ); + }); // Fire the tinter right on startup to ensure the default theme is applied // A later sync can/will correct the tint to be the right value for the user @@ -1736,6 +1765,36 @@ export default React.createClass({ this.setState(newState); }, + _tryDiscoverDefaultHomeserver: async function(serverName) { + try { + const discovery = await AutoDiscovery.findClientConfig(serverName); + const state = discovery["m.homeserver"].state; + if (state !== AutoDiscovery.SUCCESS) { + console.error("Failed to discover homeserver on startup:", discovery); + this.setState({ + defaultServerDiscoveryError: discovery["m.homeserver"].error, + loadingDefaultHomeserver: false, + }); + } else { + const hsUrl = discovery["m.homeserver"].base_url; + const isUrl = discovery["m.identity_server"].state === AutoDiscovery.SUCCESS + ? discovery["m.identity_server"].base_url + : "https://vector.im"; + this.setState({ + defaultHsUrl: hsUrl, + defaultIsUrl: isUrl, + loadingDefaultHomeserver: false, + }); + } + } catch (e) { + console.error(e); + this.setState({ + defaultServerDiscoveryError: _t("Unknown error discovering homeserver"), + loadingDefaultHomeserver: false, + }); + } + }, + _makeRegistrationUrl: function(params) { if (this.props.startingFragmentQueryParams.referrer) { params.referrer = this.props.startingFragmentQueryParams.referrer; @@ -1750,7 +1809,7 @@ export default React.createClass({ render: function() { // console.log(`Rendering MatrixChat with view ${this.state.view}`); - if (this.state.view === VIEWS.LOADING || this.state.view === VIEWS.LOGGING_IN) { + if (this.state.view === VIEWS.LOADING || this.state.view === VIEWS.LOGGING_IN || this.state.loadingDefaultHomeserver) { const Spinner = sdk.getComponent('elements.Spinner'); return (
@@ -1824,6 +1883,8 @@ export default React.createClass({ idSid={this.state.register_id_sid} email={this.props.startingFragmentQueryParams.email} referrer={this.props.startingFragmentQueryParams.referrer} + defaultServerName={this.getDefaultServerName()} + defaultServerDiscoveryError={this.state.defaultServerDiscoveryError} defaultHsUrl={this.getDefaultHsUrl()} defaultIsUrl={this.getDefaultIsUrl()} brand={this.props.config.brand} @@ -1846,6 +1907,8 @@ export default React.createClass({ const ForgotPassword = sdk.getComponent('structures.login.ForgotPassword'); return ( ; - } + // We can assume that if we have an incoming call then it is for this list + const IncomingCallBox = sdk.getComponent("voip.IncomingCallBox"); + incomingCall = + ; } let addRoomButton; diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index bce24ddc8e3..2358ed59066 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -27,6 +27,7 @@ const React = require("react"); const ReactDOM = require("react-dom"); import PropTypes from 'prop-types'; import Promise from 'bluebird'; +import filesize from 'filesize'; const classNames = require("classnames"); import { _t } from '../../languageHandler'; @@ -103,6 +104,10 @@ module.exports = React.createClass({ roomLoading: true, peekLoading: false, shouldPeek: true, + + // Media limits for uploading. + mediaConfig: undefined, + // used to trigger a rerender in TimelinePanel once the members are loaded, // so RR are rendered again (now with the members available), ... membersLoaded: !llMembers, @@ -158,7 +163,7 @@ module.exports = React.createClass({ MatrixClientPeg.get().on("RoomState.members", this.onRoomStateMember); MatrixClientPeg.get().on("Room.myMembership", this.onMyMembership); MatrixClientPeg.get().on("accountData", this.onAccountData); - + this._fetchMediaConfig(); // Start listening for RoomViewStore updates this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate); this._onRoomViewStoreUpdate(true); @@ -166,6 +171,27 @@ module.exports = React.createClass({ WidgetEchoStore.on('update', this._onWidgetEchoStoreUpdate); }, + _fetchMediaConfig: function(invalidateCache: boolean = false) { + /// NOTE: Using global here so we don't make repeated requests for the + /// config every time we swap room. + if(global.mediaConfig !== undefined && !invalidateCache) { + this.setState({mediaConfig: global.mediaConfig}); + return; + } + console.log("[Media Config] Fetching"); + MatrixClientPeg.get().getMediaConfig().then((config) => { + console.log("[Media Config] Fetched config:", config); + return config; + }).catch(() => { + // Media repo can't or won't report limits, so provide an empty object (no limits). + console.log("[Media Config] Could not fetch config, so not limiting uploads."); + return {}; + }).then((config) => { + global.mediaConfig = config; + this.setState({mediaConfig: config}); + }); + }, + _onRoomViewStoreUpdate: function(initial) { if (this.unmounted) { return; @@ -501,6 +527,10 @@ module.exports = React.createClass({ break; case 'notifier_enabled': case 'upload_failed': + // 413: File was too big or upset the server in some way. + if(payload.error.http_status === 413) { + this._fetchMediaConfig(true); + } case 'upload_started': case 'upload_finished': this.forceUpdate(); @@ -579,6 +609,20 @@ module.exports = React.createClass({ } }, + async onRoomRecoveryReminderFinished(backupCreated) { + // If the user cancelled the key backup dialog, it suggests they don't + // want to be reminded anymore. + if (!backupCreated) { + await SettingsStore.setValue( + "showRoomRecoveryReminder", + null, + SettingLevel.ACCOUNT, + false, + ); + } + this.forceUpdate(); + }, + canResetTimeline: function() { if (!this.refs.messagePanel) { return true; @@ -933,6 +977,15 @@ module.exports = React.createClass({ this.setState({ draggingFile: false }); }, + isFileUploadAllowed(file) { + if (this.state.mediaConfig !== undefined && + this.state.mediaConfig["m.upload.size"] !== undefined && + file.size > this.state.mediaConfig["m.upload.size"]) { + return _t("File is too big. Maximum file size is %(fileSize)s", {fileSize: filesize(this.state.mediaConfig["m.upload.size"])}); + } + return true; + }, + uploadFile: async function(file) { dis.dispatch({action: 'focus_composer'}); @@ -1484,6 +1537,7 @@ module.exports = React.createClass({ const Loader = sdk.getComponent("elements.Spinner"); const TimelinePanel = sdk.getComponent("structures.TimelinePanel"); const RoomUpgradeWarningBar = sdk.getComponent("rooms.RoomUpgradeWarningBar"); + const RoomRecoveryReminder = sdk.getComponent("rooms.RoomRecoveryReminder"); if (!this.state.room) { if (this.state.roomLoading || this.state.peekLoading) { @@ -1621,6 +1675,13 @@ module.exports = React.createClass({ this.state.room.userMayUpgradeRoom(MatrixClientPeg.get().credentials.userId) ); + const showRoomRecoveryReminder = ( + SettingsStore.isFeatureEnabled("feature_keybackup") && + SettingsStore.getValue("showRoomRecoveryReminder") && + MatrixClientPeg.get().isRoomEncrypted(this.state.room.roomId) && + !MatrixClientPeg.get().getKeyBackupEnabled() + ); + let aux = null; let hideCancel = false; if (this.state.editingRoomSettings) { @@ -1635,6 +1696,9 @@ module.exports = React.createClass({ } else if (showRoomUpgradeBar) { aux = ; hideCancel = true; + } else if (showRoomRecoveryReminder) { + aux = ; + hideCancel = true; } else if (this.state.showingPinned) { hideCancel = true; // has own cancel aux = ; @@ -1693,6 +1757,7 @@ module.exports = React.createClass({ callState={this.state.callState} disabled={this.props.disabled} showApps={this.state.showApps} + uploadAllowed={this.isFileUploadAllowed} />; } diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 3a3d6e1e91f..bb31510cf62 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -64,6 +64,7 @@ const SIMPLE_SETTINGS = [ { id: "urlPreviewsEnabled" }, { id: "autoplayGifsAndVideos" }, { id: "alwaysShowEncryptionIcons" }, + { id: "showRoomRecoveryReminder" }, { id: "hideReadReceipts" }, { id: "dontSendTypingNotifications" }, { id: "alwaysShowTimestamps" }, @@ -188,9 +189,11 @@ module.exports = React.createClass({ phase: "UserSettings.LOADING", // LOADING, DISPLAY email_add_pending: false, vectorVersion: undefined, + canSelfUpdate: null, rejectingInvites: false, mediaDevices: null, ignoredUsers: [], + autoLaunchEnabled: null, }; }, @@ -209,6 +212,13 @@ module.exports = React.createClass({ }, (e) => { console.log("Failed to fetch app version", e); }); + + PlatformPeg.get().canSelfUpdate().then((canUpdate) => { + if (this._unmounted) return; + this.setState({ + canSelfUpdate: canUpdate, + }); + }); } this._refreshMediaDevices(); @@ -227,11 +237,12 @@ module.exports = React.createClass({ }); this._refreshFromServer(); - if (PlatformPeg.get().isElectron()) { - const {ipcRenderer} = require('electron'); - - ipcRenderer.on('settings', this._electronSettings); - ipcRenderer.send('settings_get'); + if (PlatformPeg.get().supportsAutoLaunch()) { + PlatformPeg.get().getAutoLaunchEnabled().then(enabled => { + this.setState({ + autoLaunchEnabled: enabled, + }); + }); } this.setState({ @@ -262,11 +273,6 @@ module.exports = React.createClass({ if (cli) { cli.removeListener("RoomMember.membership", this._onInviteStateChange); } - - if (PlatformPeg.get().isElectron()) { - const {ipcRenderer} = require('electron'); - ipcRenderer.removeListener('settings', this._electronSettings); - } }, // `UserSettings` assumes that the client peg will not be null, so give it some @@ -285,10 +291,6 @@ module.exports = React.createClass({ }); }, - _electronSettings: function(ev, settings) { - this.setState({ electron_settings: settings }); - }, - _refreshMediaDevices: function(stream) { if (stream) { // kill stream so that we don't leave it lingering around with webcam enabled etc @@ -943,7 +945,7 @@ module.exports = React.createClass({ _renderCheckUpdate: function() { const platform = PlatformPeg.get(); - if ('canSelfUpdate' in platform && platform.canSelfUpdate() && 'startUpdateCheck' in platform) { + if (this.state.canSelfUpdate) { return

{ _t('Updates') }

@@ -988,8 +990,7 @@ module.exports = React.createClass({ }, _renderElectronSettings: function() { - const settings = this.state.electron_settings; - if (!settings) return; + if (!PlatformPeg.get().supportsAutoLaunch()) return; // TODO: This should probably be a granular setting, but it only applies to electron // and ends up being get/set outside of matrix anyways (local system setting). @@ -999,7 +1000,7 @@ module.exports = React.createClass({
@@ -1009,8 +1010,11 @@ module.exports = React.createClass({ }, _onAutoLaunchChanged: function(e) { - const {ipcRenderer} = require('electron'); - ipcRenderer.send('settings_set', 'auto-launch', e.target.checked); + PlatformPeg.get().setAutoLaunchEnabled(e.target.checked).then(() => { + this.setState({ + autoLaunchEnabled: e.target.checked, + }); + }); }, _mapWebRtcDevicesToSpans: function(devices) { @@ -1369,7 +1373,7 @@ module.exports = React.createClass({ { this._renderBulkOptions() } { this._renderBugReport() } - { PlatformPeg.get().isElectron() && this._renderElectronSettings() } + { this._renderElectronSettings() } { this._renderAnalyticsControl() } diff --git a/src/components/structures/login/ForgotPassword.js b/src/components/structures/login/ForgotPassword.js index 444f391258b..559136948ac 100644 --- a/src/components/structures/login/ForgotPassword.js +++ b/src/components/structures/login/ForgotPassword.js @@ -36,6 +36,14 @@ module.exports = React.createClass({ onLoginClick: PropTypes.func, onRegisterClick: PropTypes.func, onComplete: PropTypes.func.isRequired, + + // The default server name to use when the user hasn't specified + // one. This is used when displaying the defaultHsUrl in the UI. + defaultServerName: PropTypes.string, + + // An error passed along from higher up explaining that something + // went wrong when finding the defaultHsUrl. + defaultServerDiscoveryError: PropTypes.string, }, getInitialState: function() { @@ -45,6 +53,7 @@ module.exports = React.createClass({ progress: null, password: null, password2: null, + errorText: null, }; }, @@ -81,6 +90,13 @@ module.exports = React.createClass({ onSubmitForm: function(ev) { ev.preventDefault(); + // Don't allow the user to register if there's a discovery error + // Without this, the user could end up registering on the wrong homeserver. + if (this.props.defaultServerDiscoveryError) { + this.setState({errorText: this.props.defaultServerDiscoveryError}); + return; + } + if (!this.state.email) { this.showErrorDialog(_t('The email address linked to your account must be entered.')); } else if (!this.state.password || !this.state.password2) { @@ -200,6 +216,12 @@ module.exports = React.createClass({ ); } + let errorText = null; + const err = this.state.errorText || this.props.defaultServerDiscoveryError; + if (err) { + errorText =
{ err }
; + } + const LanguageSelector = sdk.getComponent('structures.login.LanguageSelector'); resetPasswordJsx = ( @@ -230,6 +252,7 @@ module.exports = React.createClass({ { serverConfigSection } + { errorText } { _t('Return to login screen') } diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index 92cddb0dc10..b94a1759cfe 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -26,10 +26,17 @@ import Login from '../../../Login'; import SdkConfig from '../../../SdkConfig'; import SettingsStore from "../../../settings/SettingsStore"; import { messageForResourceLimitError } from '../../../utils/ErrorUtils'; +import { AutoDiscovery } from "matrix-js-sdk"; // For validating phone numbers without country codes const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/; +// These are used in several places, and come from the js-sdk's autodiscovery +// stuff. We define them here so that they'll be picked up by i18n. +_td("Invalid homeserver discovery response"); +_td("Invalid identity server discovery response"); +_td("General failure"); + /** * A wire component which glues together login UI components and Login logic */ @@ -50,6 +57,14 @@ module.exports = React.createClass({ // different home server without confusing users. fallbackHsUrl: PropTypes.string, + // The default server name to use when the user hasn't specified + // one. This is used when displaying the defaultHsUrl in the UI. + defaultServerName: PropTypes.string, + + // An error passed along from higher up explaining that something + // went wrong when finding the defaultHsUrl. + defaultServerDiscoveryError: PropTypes.string, + defaultDeviceDisplayName: PropTypes.string, // login shouldn't know or care how registration is done. @@ -74,6 +89,12 @@ module.exports = React.createClass({ phoneCountry: null, phoneNumber: "", currentFlow: "m.login.password", + + // .well-known discovery + discoveredHsUrl: "", + discoveredIsUrl: "", + discoveryError: "", + findingHomeserver: false, }; }, @@ -105,6 +126,10 @@ module.exports = React.createClass({ }, onPasswordLogin: function(username, phoneCountry, phoneNumber, password) { + // Prevent people from submitting their password when homeserver + // discovery went wrong + if (this.state.discoveryError || this.props.defaultServerDiscoveryError) return; + this.setState({ busy: true, errorText: null, @@ -221,6 +246,22 @@ module.exports = React.createClass({ this.setState({ username: username }); }, + onUsernameBlur: function(username) { + this.setState({ username: username }); + if (username[0] === "@") { + const serverName = username.split(':').slice(1).join(':'); + try { + // we have to append 'https://' to make the URL constructor happy + // otherwise we get things like 'protocol: matrix.org, pathname: 8448' + const url = new URL("https://" + serverName); + this._tryWellKnownDiscovery(url.hostname); + } catch (e) { + console.error("Problem parsing URL or unhandled error doing .well-known discovery:", e); + this.setState({discoveryError: _t("Failed to perform homeserver discovery")}); + } + } + }, + onPhoneCountryChanged: function(phoneCountry) { this.setState({ phoneCountry: phoneCountry }); }, @@ -256,6 +297,59 @@ module.exports = React.createClass({ }); }, + _tryWellKnownDiscovery: async function(serverName) { + if (!serverName.trim()) { + // Nothing to discover + this.setState({discoveryError: "", discoveredHsUrl: "", discoveredIsUrl: "", findingHomeserver: false}); + return; + } + + this.setState({findingHomeserver: true}); + try { + const discovery = await AutoDiscovery.findClientConfig(serverName); + const state = discovery["m.homeserver"].state; + if (state !== AutoDiscovery.SUCCESS && state !== AutoDiscovery.PROMPT) { + this.setState({ + discoveredHsUrl: "", + discoveredIsUrl: "", + discoveryError: discovery["m.homeserver"].error, + findingHomeserver: false, + }); + } else if (state === AutoDiscovery.PROMPT) { + this.setState({ + discoveredHsUrl: "", + discoveredIsUrl: "", + discoveryError: "", + findingHomeserver: false, + }); + } else if (state === AutoDiscovery.SUCCESS) { + this.setState({ + discoveredHsUrl: discovery["m.homeserver"].base_url, + discoveredIsUrl: + discovery["m.identity_server"].state === AutoDiscovery.SUCCESS + ? discovery["m.identity_server"].base_url + : "", + discoveryError: "", + findingHomeserver: false, + }); + } else { + console.warn("Unknown state for m.homeserver in discovery response: ", discovery); + this.setState({ + discoveredHsUrl: "", + discoveredIsUrl: "", + discoveryError: _t("Unknown failure discovering homeserver"), + findingHomeserver: false, + }); + } + } catch (e) { + console.error(e); + this.setState({ + findingHomeserver: false, + discoveryError: _t("Unknown error discovering homeserver"), + }); + } + }, + _initLoginLogic: function(hsUrl, isUrl) { const self = this; hsUrl = hsUrl || this.state.enteredHomeserverUrl; @@ -393,11 +487,14 @@ module.exports = React.createClass({ initialPhoneCountry={this.state.phoneCountry} initialPhoneNumber={this.state.phoneNumber} onUsernameChanged={this.onUsernameChanged} + onUsernameBlur={this.onUsernameBlur} onPhoneCountryChanged={this.onPhoneCountryChanged} onPhoneNumberChanged={this.onPhoneNumberChanged} onForgotPasswordClick={this.props.onForgotPasswordClick} loginIncorrect={this.state.loginIncorrect} hsUrl={this.state.enteredHomeserverUrl} + hsName={this.props.defaultServerName} + disableSubmit={this.state.findingHomeserver} /> ); }, @@ -416,6 +513,8 @@ module.exports = React.createClass({ const ServerConfig = sdk.getComponent("login.ServerConfig"); const loader = this.state.busy ?
: null; + const errorText = this.props.defaultServerDiscoveryError || this.state.discoveryError || this.state.errorText; + let loginAsGuestJsx; if (this.props.enableGuest) { loginAsGuestJsx = @@ -430,8 +529,8 @@ module.exports = React.createClass({ if (!SdkConfig.get()['disable_custom_urls']) { serverConfig = { _t('Sign in') } { loader }

; } else { - if (!this.state.errorText) { + if (!errorText) { header =

{ _t('Sign in to get started') } { loader }

; } } let errorTextSection; - if (this.state.errorText) { + if (errorText) { errorTextSection = (
- { this.state.errorText } + { errorText }
); } diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index 30afaf4f644..ad3ea5f19cd 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -57,6 +57,14 @@ module.exports = React.createClass({ }), teamSelected: PropTypes.object, + // The default server name to use when the user hasn't specified + // one. This is used when displaying the defaultHsUrl in the UI. + defaultServerName: PropTypes.string, + + // An error passed along from higher up explaining that something + // went wrong when finding the defaultHsUrl. + defaultServerDiscoveryError: PropTypes.string, + defaultDeviceDisplayName: PropTypes.string, // registration shouldn't know or care how login is done. @@ -170,6 +178,12 @@ module.exports = React.createClass({ }, onFormSubmit: function(formVals) { + // Don't allow the user to register if there's a discovery error + // Without this, the user could end up registering on the wrong homeserver. + if (this.props.defaultServerDiscoveryError) { + this.setState({errorText: this.props.defaultServerDiscoveryError}); + return; + } this.setState({ errorText: "", busy: true, @@ -328,7 +342,7 @@ module.exports = React.createClass({ errMsg = _t('A phone number is required to register on this homeserver.'); break; case "RegistrationForm.ERR_USERNAME_INVALID": - errMsg = _t('User names may only contain letters, numbers, dots, hyphens and underscores.'); + errMsg = _t("Only use lower case letters, numbers and '=_-./'"); break; case "RegistrationForm.ERR_USERNAME_BLANK": errMsg = _t('You need to enter a user name.'); @@ -441,12 +455,13 @@ module.exports = React.createClass({ let header; let errorText; // FIXME: remove hardcoded Status team tweaks at some point - if (theme === 'status' && this.state.errorText) { - header =
{ this.state.errorText }
; + const err = this.state.errorText || this.props.defaultServerDiscoveryError; + if (theme === 'status' && err) { + header =
{ err }
; } else { header =

{ _t('Create an account') }

; - if (this.state.errorText) { - errorText =
{ this.state.errorText }
; + if (err) { + errorText =
{ err }
; } } diff --git a/src/components/views/dialogs/AddressPickerDialog.js b/src/components/views/dialogs/AddressPickerDialog.js index abc52f7b1d3..cbe80763a65 100644 --- a/src/components/views/dialogs/AddressPickerDialog.js +++ b/src/components/views/dialogs/AddressPickerDialog.js @@ -23,6 +23,7 @@ import MatrixClientPeg from '../../../MatrixClientPeg'; import Promise from 'bluebird'; import { addressTypes, getAddressType } from '../../../UserAddress.js'; import GroupStore from '../../../stores/GroupStore'; +import * as Email from "../../../email"; const TRUNCATE_QUERY_LIST = 40; const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200; @@ -419,6 +420,10 @@ module.exports = React.createClass({ // a perfectly valid address if there are close matches. const addrType = getAddressType(query); if (this.props.validAddressTypes.includes(addrType)) { + if (addrType === 'email' && !Email.looksValid(query)) { + this.setState({searchError: _t("That doesn't look like a valid email address")}); + return; + } suggestedList.unshift({ addressType: addrType, address: query, diff --git a/src/components/views/dialogs/BaseDialog.js b/src/components/views/dialogs/BaseDialog.js index 8ec417a59b0..3e9052cc349 100644 --- a/src/components/views/dialogs/BaseDialog.js +++ b/src/components/views/dialogs/BaseDialog.js @@ -57,8 +57,7 @@ export default React.createClass({ className: PropTypes.string, // Title for the dialog. - // (could probably actually be something more complicated than a string if desired) - title: PropTypes.string.isRequired, + title: PropTypes.node.isRequired, // children should be the content of the dialog children: PropTypes.node, diff --git a/src/components/views/dialogs/DeactivateAccountDialog.js b/src/components/views/dialogs/DeactivateAccountDialog.js index 761a1e42096..6e87a816bb7 100644 --- a/src/components/views/dialogs/DeactivateAccountDialog.js +++ b/src/components/views/dialogs/DeactivateAccountDialog.js @@ -35,19 +35,10 @@ export default class DeactivateAccountDialog extends React.Component { this._onPasswordFieldChange = this._onPasswordFieldChange.bind(this); this._onEraseFieldChange = this._onEraseFieldChange.bind(this); - const deactivationPreferences = - MatrixClientPeg.get().getAccountData('im.riot.account_deactivation_preferences'); - - const shouldErase = ( - deactivationPreferences && - deactivationPreferences.getContent() && - deactivationPreferences.getContent().shouldErase - ) || false; - this.state = { confirmButtonEnabled: false, busy: false, - shouldErase, + shouldErase: false, errStr: null, }; } @@ -67,36 +58,6 @@ export default class DeactivateAccountDialog extends React.Component { async _onOk() { this.setState({busy: true}); - // Before we deactivate the account insert an event into - // the user's account data indicating that they wish to be - // erased from the homeserver. - // - // We do this because the API for erasing after deactivation - // might not be supported by the connected homeserver. Leaving - // an indication in account data is only best-effort, and - // in the worse case, the HS maintainer would have to run a - // script to erase deactivated accounts that have shouldErase - // set to true in im.riot.account_deactivation_preferences. - // - // Note: The preferences are scoped to Riot, hence the - // "im.riot..." event type. - // - // Note: This may have already been set on previous attempts - // where, for example, the user entered the wrong password. - // This is fine because the UI always indicates the preference - // prior to us calling `deactivateAccount`. - try { - await MatrixClientPeg.get().setAccountData('im.riot.account_deactivation_preferences', { - shouldErase: this.state.shouldErase, - }); - } catch (err) { - this.setState({ - busy: false, - errStr: _t('Failed to indicate account erasure'), - }); - return; - } - try { // This assumes that the HS requires password UI auth // for this endpoint. In reality it could be any UI auth. diff --git a/src/components/views/dialogs/SetMxIdDialog.js b/src/components/views/dialogs/SetMxIdDialog.js index fb892c4a0a8..222a2c35fed 100644 --- a/src/components/views/dialogs/SetMxIdDialog.js +++ b/src/components/views/dialogs/SetMxIdDialog.js @@ -23,6 +23,7 @@ import MatrixClientPeg from '../../../MatrixClientPeg'; import classnames from 'classnames'; import { KeyCode } from '../../../Keyboard'; import { _t } from '../../../languageHandler'; +import { SAFE_LOCALPART_REGEX } from '../../../Registration'; // The amount of time to wait for further changes to the input username before // sending a request to the server @@ -110,12 +111,11 @@ export default React.createClass({ }, _doUsernameCheck: function() { - // XXX: SPEC-1 - // Check if username is valid - // Naive impl copied from https://github.com/matrix-org/matrix-react-sdk/blob/66c3a6d9ca695780eb6b662e242e88323053ff33/src/components/views/login/RegistrationForm.js#L190 - if (encodeURIComponent(this.state.username) !== this.state.username) { + // We do a quick check ahead of the username availability API to ensure the + // user ID roughly looks okay from a Matrix perspective. + if (!SAFE_LOCALPART_REGEX.test(this.state.username)) { this.setState({ - usernameError: _t('User names may only contain letters, numbers, dots, hyphens and underscores.'), + usernameError: _t("Only use lower case letters, numbers and '=_-./'"), }); return Promise.reject(); } @@ -210,7 +210,6 @@ export default React.createClass({ render: function() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const InteractiveAuth = sdk.getComponent('structures.InteractiveAuth'); - const Spinner = sdk.getComponent('elements.Spinner'); let auth; if (this.state.doingUIAuth) { @@ -230,9 +229,8 @@ export default React.createClass({ }); let usernameIndicator = null; - let usernameBusyIndicator = null; if (this.state.usernameBusy) { - usernameBusyIndicator = ; + usernameIndicator =
{_t("Checking...")}
; } else { const usernameAvailable = this.state.username && this.state.usernameCheckSupport && !this.state.usernameError; @@ -270,7 +268,6 @@ export default React.createClass({ size="30" className={inputClasses} /> - { usernameBusyIndicator }
{ usernameIndicator }

diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 23b24adbb4a..f4f929a3c20 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -22,7 +22,6 @@ import qs from 'querystring'; import React from 'react'; import PropTypes from 'prop-types'; import MatrixClientPeg from '../../../MatrixClientPeg'; -import PlatformPeg from '../../../PlatformPeg'; import ScalarAuthClient from '../../../ScalarAuthClient'; import WidgetMessaging from '../../../WidgetMessaging'; import TintableSvgButton from './TintableSvgButton'; @@ -49,7 +48,6 @@ export default class AppTile extends React.Component { this.state = this._getNewState(props); this._onAction = this._onAction.bind(this); - this._onMessage = this._onMessage.bind(this); this._onLoaded = this._onLoaded.bind(this); this._onEditClick = this._onEditClick.bind(this); this._onDeleteClick = this._onDeleteClick.bind(this); @@ -143,10 +141,6 @@ export default class AppTile extends React.Component { } componentDidMount() { - // Legacy Jitsi widget messaging -- TODO replace this with standard widget - // postMessaging API - window.addEventListener('message', this._onMessage, false); - // Widget action listeners this.dispatcherRef = dis.register(this._onAction); } @@ -155,9 +149,6 @@ export default class AppTile extends React.Component { // Widget action listeners dis.unregister(this.dispatcherRef); - // Jitsi listener - window.removeEventListener('message', this._onMessage); - // if it's not remaining on screen, get rid of the PersistedElement container if (!ActiveWidgetStore.getWidgetPersistence(this.props.id)) { ActiveWidgetStore.destroyPersistentWidget(); @@ -233,32 +224,6 @@ export default class AppTile extends React.Component { } } - // Legacy Jitsi widget messaging - // TODO -- This should be replaced with the new widget postMessaging API - _onMessage(event) { - if (this.props.type !== 'jitsi') { - return; - } - if (!event.origin) { - event.origin = event.originalEvent.origin; - } - - const widgetUrlObj = url.parse(this.state.widgetUrl); - const eventOrigin = url.parse(event.origin); - if ( - eventOrigin.protocol !== widgetUrlObj.protocol || - eventOrigin.host !== widgetUrlObj.host - ) { - return; - } - - if (event.data.widgetAction === 'jitsi_iframe_loaded') { - const iframe = this.refs.appFrame.contentWindow - .document.querySelector('iframe[id^="jitsiConferenceFrame"]'); - PlatformPeg.get().setupScreenSharingForIframe(iframe); - } - } - _canUserModify() { // User widgets should always be modifiable by their creator if (this.props.userWidget && MatrixClientPeg.get().credentials.userId === this.props.creatorUserId) { @@ -544,7 +509,7 @@ export default class AppTile extends React.Component { // Additional iframe feature pemissions // (see - https://sites.google.com/a/chromium.org/dev/Home/chromium-security/deprecating-permissions-in-cross-origin-iframes and https://wicg.github.io/feature-policy/) - const iframeFeatures = "microphone; camera; encrypted-media;"; + const iframeFeatures = "microphone; camera; encrypted-media; autoplay;"; const appTileBodyClass = 'mx_AppTileBody' + (this.props.miniMode ? '_mini ' : ' '); diff --git a/src/components/views/elements/TintableSvg.js b/src/components/views/elements/TintableSvg.js index e04bf87793e..08628c8ca9a 100644 --- a/src/components/views/elements/TintableSvg.js +++ b/src/components/views/elements/TintableSvg.js @@ -29,6 +29,7 @@ var TintableSvg = React.createClass({ width: PropTypes.string.isRequired, height: PropTypes.string.isRequired, className: PropTypes.string, + forceColors: PropTypes.arrayOf(PropTypes.string), }, statics: { @@ -50,6 +51,12 @@ var TintableSvg = React.createClass({ delete TintableSvg.mounts[this.id]; }, + componentDidUpdate: function(prevProps, prevState) { + if (prevProps.forceColors !== this.props.forceColors) { + this.calcAndApplyFixups(this.refs.svgContainer); + } + }, + tint: function() { // TODO: only bother running this if the global tint settings have changed // since we loaded! @@ -57,8 +64,13 @@ var TintableSvg = React.createClass({ }, onLoad: function(event) { - // console.log("TintableSvg.onLoad for " + this.props.src); - this.fixups = Tinter.calcSvgFixups([event.target]); + this.calcAndApplyFixups(event.target); + }, + + calcAndApplyFixups: function(target) { + if (!target) return; + // console.log("TintableSvg.calcAndApplyFixups for " + this.props.src); + this.fixups = Tinter.calcSvgFixups([target], this.props.forceColors); Tinter.applySvgFixups(this.fixups); }, @@ -71,6 +83,7 @@ var TintableSvg = React.createClass({ height={this.props.height} onLoad={this.onLoad} tabIndex="-1" + ref="svgContainer" /> ); }, diff --git a/src/components/views/groups/GroupMemberList.js b/src/components/views/groups/GroupMemberList.js index 46653f15994..9a8196f12b9 100644 --- a/src/components/views/groups/GroupMemberList.js +++ b/src/components/views/groups/GroupMemberList.js @@ -37,7 +37,9 @@ export default React.createClass({ getInitialState: function() { return { members: null, + membersError: null, invitedMembers: null, + invitedMembersError: null, truncateAt: INITIAL_LOAD_NUM_MEMBERS, }; }, @@ -55,6 +57,19 @@ export default React.createClass({ GroupStore.registerListener(groupId, () => { this._fetchMembers(); }); + GroupStore.on('error', (err, errorGroupId, stateKey) => { + if (this._unmounted || groupId !== errorGroupId) return; + if (stateKey === GroupStore.STATE_KEY.GroupMembers) { + this.setState({ + membersError: err, + }); + } + if (stateKey === GroupStore.STATE_KEY.GroupInvitedMembers) { + this.setState({ + invitedMembersError: err, + }); + } + }); }, _fetchMembers: function() { @@ -88,7 +103,11 @@ export default React.createClass({ this.setState({ searchQuery: ev.target.value }); }, - makeGroupMemberTiles: function(query, memberList) { + makeGroupMemberTiles: function(query, memberList, memberListError) { + if (memberListError) { + return

{ _t("Failed to load group members") }
; + } + const GroupMemberTile = sdk.getComponent("groups.GroupMemberTile"); const TruncatedList = sdk.getComponent("elements.TruncatedList"); query = (query || "").toLowerCase(); @@ -166,13 +185,25 @@ export default React.createClass({ ); const joined = this.state.members ?
- { this.makeGroupMemberTiles(this.state.searchQuery, this.state.members) } + { + this.makeGroupMemberTiles( + this.state.searchQuery, + this.state.members, + this.state.membersError, + ) + }
:
; const invited = (this.state.invitedMembers && this.state.invitedMembers.length > 0) ?
-

{ _t("Invited") }

- { this.makeGroupMemberTiles(this.state.searchQuery, this.state.invitedMembers) } +

{_t("Invited")}

+ { + this.makeGroupMemberTiles( + this.state.searchQuery, + this.state.invitedMembers, + this.state.invitedMembersError, + ) + }
:
; let inviteButton; diff --git a/src/components/views/login/PasswordLogin.js b/src/components/views/login/PasswordLogin.js index a0e5ab0ddbb..59d4db379c2 100644 --- a/src/components/views/login/PasswordLogin.js +++ b/src/components/views/login/PasswordLogin.js @@ -30,6 +30,7 @@ class PasswordLogin extends React.Component { static defaultProps = { onError: function() {}, onUsernameChanged: function() {}, + onUsernameBlur: function() {}, onPasswordChanged: function() {}, onPhoneCountryChanged: function() {}, onPhoneNumberChanged: function() {}, @@ -39,6 +40,8 @@ class PasswordLogin extends React.Component { initialPassword: "", loginIncorrect: false, hsDomain: "", + hsName: null, + disableSubmit: false, } constructor(props) { @@ -53,6 +56,7 @@ class PasswordLogin extends React.Component { this.onSubmitForm = this.onSubmitForm.bind(this); this.onUsernameChanged = this.onUsernameChanged.bind(this); + this.onUsernameBlur = this.onUsernameBlur.bind(this); this.onLoginTypeChange = this.onLoginTypeChange.bind(this); this.onPhoneCountryChanged = this.onPhoneCountryChanged.bind(this); this.onPhoneNumberChanged = this.onPhoneNumberChanged.bind(this); @@ -124,6 +128,10 @@ class PasswordLogin extends React.Component { this.props.onUsernameChanged(ev.target.value); } + onUsernameBlur(ev) { + this.props.onUsernameBlur(this.state.username); + } + onLoginTypeChange(loginType) { this.props.onError(null); // send a null error to clear any error messages this.setState({ @@ -167,6 +175,7 @@ class PasswordLogin extends React.Component { type="text" name="username" // make it a little easier for browser's remember-password onChange={this.onUsernameChanged} + onBlur={this.onUsernameBlur} placeholder="joe@example.com" value={this.state.username} autoFocus @@ -182,6 +191,7 @@ class PasswordLogin extends React.Component { type="text" name="username" // make it a little easier for browser's remember-password onChange={this.onUsernameChanged} + onBlur={this.onUsernameBlur} placeholder={SdkConfig.get().disable_custom_urls ? _t("Username on %(hs)s", { hs: this.props.hsUrl.replace(/^https?:\/\//, ''), @@ -242,13 +252,15 @@ class PasswordLogin extends React.Component { ); } - let matrixIdText = ''; - if (this.props.hsUrl) { + let matrixIdText = _t('Matrix ID'); + if (this.props.hsName) { + matrixIdText = _t('%(serverName)s Matrix ID', {serverName: this.props.hsName}); + } else { try { const parsedHsUrl = new URL(this.props.hsUrl); matrixIdText = _t('%(serverName)s Matrix ID', {serverName: parsedHsUrl.hostname}); } catch (e) { - // pass + // ignore } } @@ -280,6 +292,8 @@ class PasswordLogin extends React.Component { ); } + const disableSubmit = this.props.disableSubmit || matrixIdText === ''; + return (
@@ -293,7 +307,7 @@ class PasswordLogin extends React.Component { />
{ forgotPasswordJsx } - +
); @@ -317,6 +331,8 @@ PasswordLogin.propTypes = { onPhoneNumberChanged: PropTypes.func, onPasswordChanged: PropTypes.func, loginIncorrect: PropTypes.bool, + hsName: PropTypes.string, + disableSubmit: PropTypes.bool, }; module.exports = PasswordLogin; diff --git a/src/components/views/login/RegistrationForm.js b/src/components/views/login/RegistrationForm.js index fe977025aea..137aeada914 100644 --- a/src/components/views/login/RegistrationForm.js +++ b/src/components/views/login/RegistrationForm.js @@ -25,7 +25,7 @@ import { looksValid as phoneNumberLooksValid } from '../../../phonenumber'; import Modal from '../../../Modal'; import { _t } from '../../../languageHandler'; import SdkConfig from '../../../SdkConfig'; -import SettingsStore from "../../../settings/SettingsStore"; +import { SAFE_LOCALPART_REGEX } from '../../../Registration'; const FIELD_EMAIL = 'field_email'; const FIELD_PHONE_COUNTRY = 'field_phone_country'; @@ -194,9 +194,8 @@ module.exports = React.createClass({ } else this.markFieldValid(field_id, phoneNumberValid, "RegistrationForm.ERR_PHONE_NUMBER_INVALID"); break; case FIELD_USERNAME: - // XXX: SPEC-1 - var username = this.refs.username.value.trim(); - if (encodeURIComponent(username) != username) { + const username = this.refs.username.value.trim(); + if (!SAFE_LOCALPART_REGEX.test(username)) { this.markFieldValid( field_id, false, diff --git a/src/components/views/login/ServerConfig.js b/src/components/views/login/ServerConfig.js index a6944ec20af..2f040112734 100644 --- a/src/components/views/login/ServerConfig.js +++ b/src/components/views/login/ServerConfig.js @@ -70,6 +70,23 @@ module.exports = React.createClass({ }; }, + componentWillReceiveProps: function(newProps) { + if (newProps.customHsUrl === this.state.hs_url && + newProps.customIsUrl === this.state.is_url) return; + + this.setState({ + hs_url: newProps.customHsUrl, + is_url: newProps.customIsUrl, + configVisible: !newProps.withToggleButton || + (newProps.customHsUrl !== newProps.defaultHsUrl) || + (newProps.customIsUrl !== newProps.defaultIsUrl), + }); + this.props.onServerConfigChange({ + hsUrl: newProps.customHsUrl, + isUrl: newProps.customIsUrl, + }); + }, + onHomeserverChanged: function(ev) { this.setState({hs_url: ev.target.value}, function() { this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, function() { diff --git a/src/components/views/room_settings/AliasSettings.js b/src/components/views/room_settings/AliasSettings.js index de5d3db6258..f68670b2f9b 100644 --- a/src/components/views/room_settings/AliasSettings.js +++ b/src/components/views/room_settings/AliasSettings.js @@ -130,7 +130,7 @@ module.exports = React.createClass({ }, isAliasValid: function(alias) { - // XXX: FIXME SPEC-1 + // XXX: FIXME https://github.com/matrix-org/matrix-doc/issues/668 return (alias.match(/^#([^\/:,]+?):(.+)$/) && encodeURI(alias) === alias); }, diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index ef22f01faac..6c53470645a 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -41,6 +41,7 @@ import withMatrixClient from '../../../wrappers/withMatrixClient'; import AccessibleButton from '../elements/AccessibleButton'; import RoomViewStore from '../../../stores/RoomViewStore'; import SdkConfig from '../../../SdkConfig'; +import MultiInviter from "../../../utils/MultiInviter"; module.exports = withMatrixClient(React.createClass({ displayName: 'MemberInfo', @@ -714,12 +715,18 @@ module.exports = withMatrixClient(React.createClass({ const roomId = member && member.roomId ? member.roomId : RoomViewStore.getRoomId(); const onInviteUserButton = async() => { try { - await cli.invite(roomId, member.userId); + // We use a MultiInviter to re-use the invite logic, even though + // we're only inviting one user. + const inviter = new MultiInviter(roomId); + await inviter.invite([member.userId]).then(() => { + if (inviter.getCompletionState(userId) !== "invited") + throw new Error(inviter.getErrorText(userId)); + }); } catch (err) { const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); Modal.createTrackedDialog('Failed to invite', '', ErrorDialog, { title: _t('Failed to invite'), - description: ((err && err.message) ? err.message : "Operation failed"), + description: ((err && err.message) ? err.message : _t("Operation failed")), }); } }; diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 894bae8e510..3c4a63ed272 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -139,7 +139,8 @@ export default class MessageComposer extends React.Component { } onUploadFileSelected(files) { - this.uploadFiles(files.target.files); + const tfiles = files.target.files; + this.uploadFiles(tfiles); } uploadFiles(files) { @@ -147,10 +148,21 @@ export default class MessageComposer extends React.Component { const TintableSvg = sdk.getComponent("elements.TintableSvg"); const fileList = []; + const acceptedFiles = []; + const failedFiles = []; + for (let i=0; i - { files[i].name || _t('Attachment') } - ); + const fileAcceptedOrError = this.props.uploadAllowed(files[i]); + if (fileAcceptedOrError === true) { + acceptedFiles.push(
  • + { files[i].name || _t('Attachment') } +
  • ); + fileList.push(files[i]); + } else { + failedFiles.push(
  • + { files[i].name || _t('Attachment') }

    { _t('Reason') + ": " + fileAcceptedOrError}

    +
  • ); + } } const isQuoting = Boolean(RoomViewStore.getQuotingEvent()); @@ -161,23 +173,47 @@ export default class MessageComposer extends React.Component { }

    ; } + const acceptedFilesPart = acceptedFiles.length === 0 ? null : ( +
    +

    { _t('Are you sure you want to upload the following files?') }

    +
      + { acceptedFiles } +
    +
    + ); + + const failedFilesPart = failedFiles.length === 0 ? null : ( +
    +

    { _t('The following files cannot be uploaded:') }

    +
      + { failedFiles } +
    +
    + ); + let buttonText; + if (acceptedFiles.length > 0 && failedFiles.length > 0) { + buttonText = "Upload selected" + } else if (failedFiles.length > 0) { + buttonText = "Close" + } + Modal.createTrackedDialog('Upload Files confirmation', '', QuestionDialog, { title: _t('Upload Files'), description: (
    -

    { _t('Are you sure you want to upload the following files?') }

    -
      - { fileList } -
    + { acceptedFilesPart } + { failedFilesPart } { replyToWarning }
    ), + hasCancelButton: acceptedFiles.length > 0, + button: buttonText, onFinished: (shouldUpload) => { if (shouldUpload) { // MessageComposer shouldn't have to rely on its parent passing in a callback to upload a file - if (files) { - for (let i=0; i { + if (!this.state.incomingCall) return null; + if (this.state.incomingCallTag !== tagName) return null; + return this.state.incomingCall; + }; + let subLists = [ { list: [], @@ -593,6 +622,7 @@ module.exports = React.createClass({ list: this.state.lists['im.vector.fake.invite'], label: _t('Invites'), order: "recent", + incomingCall={incomingCallIfTaggedAs('im.vector.fake.invite')}, isInvite: true, }, { @@ -600,6 +630,7 @@ module.exports = React.createClass({ label: _t('Favourites'), tagName: "m.favourite", order: "manual", + incomingCall={incomingCallIfTaggedAs('m.favourite')}, }, { list: this.state.lists['im.vector.fake.direct'], @@ -607,6 +638,7 @@ module.exports = React.createClass({ tagName: "im.vector.fake.direct", headerItems: this._getHeaderItems('im.vector.fake.direct'), order: "recent", + incomingCall={incomingCallIfTaggedAs('im.vector.fake.direct')}, onAddRoom: () => {dis.dispatch({action: 'view_create_chat'})}, }, { @@ -614,6 +646,7 @@ module.exports = React.createClass({ label: _t('Rooms'), headerItems: this._getHeaderItems('im.vector.fake.recent'), order: "recent", + incomingCall={incomingCallIfTaggedAs('im.vector.fake.recent')}, onAddRoom: () => {dis.dispatch({action: 'view_create_room'})}, }, ]; @@ -627,6 +660,7 @@ module.exports = React.createClass({ label: labelForTagName(tagName), tagName: tagName, order: "manual", + incomingCallIfTaggedAs(tagName), }; }); subLists = subLists.concat(tagSubLists); @@ -636,11 +670,13 @@ module.exports = React.createClass({ label: _t('Low priority'), tagName: "m.lowpriority", order: "recent", + incomingCall={incomingCallIfTaggedAs('m.lowpriority')}, }, { list: this.state.lists['im.vector.fake.archived'], label: _t('Historical'), order: "recent", + incomingCall={incomingCallIfTaggedAs('im.vector.fake.archived')}, startAsHidden: true, showSpinner: this.state.isLoadingLeftRooms, onHeaderClick: this.onArchivedHeaderClick, @@ -650,6 +686,7 @@ module.exports = React.createClass({ label: _t('System Alerts'), tagName: "m.lowpriority", order: "recent", + incomingCall={incomingCallIfTaggedAs('m.server_notice')}, }, ]); diff --git a/src/components/views/rooms/RoomRecoveryReminder.js b/src/components/views/rooms/RoomRecoveryReminder.js new file mode 100644 index 00000000000..265bfd3ee39 --- /dev/null +++ b/src/components/views/rooms/RoomRecoveryReminder.js @@ -0,0 +1,85 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from "react"; +import PropTypes from "prop-types"; +import sdk from "../../../index"; +import { _t } from "../../../languageHandler"; +import Modal from "../../../Modal"; + +export default class RoomRecoveryReminder extends React.PureComponent { + static propTypes = { + onFinished: PropTypes.func.isRequired, + } + + showKeyBackupDialog = () => { + Modal.createTrackedDialogAsync("Key Backup", "Key Backup", + import("../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog"), + { + onFinished: this.props.onFinished, + }, + ); + } + + onDontAskAgainClick = () => { + // When you choose "Don't ask again" from the room reminder, we show a + // dialog to confirm the choice. + Modal.createTrackedDialogAsync("Ignore Recovery Reminder", "Ignore Recovery Reminder", + import("../../../async-components/views/dialogs/keybackup/IgnoreRecoveryReminderDialog"), + { + onDontAskAgain: () => { + // Report false to the caller, who should prevent the + // reminder from appearing in the future. + this.props.onFinished(false); + }, + onSetup: () => { + this.showKeyBackupDialog(); + }, + }, + ); + } + + onSetupClick = () => { + this.showKeyBackupDialog(); + } + + render() { + const AccessibleButton = sdk.getComponent("views.elements.AccessibleButton"); + + return ( +
    +
    {_t( + "Secure Message Recovery", + )}
    +
    {_t( + "If you log out or use another device, you'll lose your " + + "secure message history. To prevent this, set up Secure " + + "Message Recovery.", + )}
    +
    + + { _t("Don't ask again") } + + + { _t("Set up") } + +
    +
    + ); + } +} diff --git a/src/components/views/settings/KeyBackupPanel.js b/src/components/views/settings/KeyBackupPanel.js index b08f4d0e78a..03b98d28a02 100644 --- a/src/components/views/settings/KeyBackupPanel.js +++ b/src/components/views/settings/KeyBackupPanel.js @@ -154,6 +154,7 @@ export default class KeyBackupPanel extends React.Component { } let backupSigStatuses = this.state.backupSigStatus.sigs.map((sig, i) => { + const deviceName = sig.device.getDisplayName() || sig.device.deviceId; const sigStatusSubstitutions = { validity: sub => @@ -163,7 +164,7 @@ export default class KeyBackupPanel extends React.Component { {sub} , - device: sub => {sig.device.getDisplayName()}, + device: sub => {deviceName}, }; let sigStatus; if (sig.device.getFingerprint() === MatrixClientPeg.get().getDeviceEd25519Key()) { @@ -174,7 +175,7 @@ export default class KeyBackupPanel extends React.Component { } else if (sig.valid && sig.device.isVerified()) { sigStatus = _t( "Backup has a valid signature from " + - "verified device x", + "verified device ", {}, sigStatusSubstitutions, ); } else if (sig.valid && !sig.device.isVerified()) { diff --git a/src/components/views/settings/Notifications.js b/src/components/views/settings/Notifications.js index 72ad2943aad..40c43e6b2ea 100644 --- a/src/components/views/settings/Notifications.js +++ b/src/components/views/settings/Notifications.js @@ -483,8 +483,11 @@ module.exports = React.createClass({ // The default push rules displayed by Vector UI '.m.rule.contains_display_name': 'vector', '.m.rule.contains_user_name': 'vector', + '.m.rule.roomnotif': 'vector', '.m.rule.room_one_to_one': 'vector', + '.m.rule.encrypted_room_one_to_one': 'vector', '.m.rule.message': 'vector', + '.m.rule.encrypted': 'vector', '.m.rule.invite_for_me': 'vector', //'.m.rule.member_event': 'vector', '.m.rule.call': 'vector', @@ -534,9 +537,12 @@ module.exports = React.createClass({ const vectorRuleIds = [ '.m.rule.contains_display_name', '.m.rule.contains_user_name', + '.m.rule.roomnotif', '_keywords', '.m.rule.room_one_to_one', + '.m.rule.encrypted_room_one_to_one', '.m.rule.message', + '.m.rule.encrypted', '.m.rule.invite_for_me', //'im.vector.rule.member_event', '.m.rule.call', diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index fffacf786e2..0e12104a7d4 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -1300,5 +1300,60 @@ "Open Devtools": "Öffne Entwickler-Werkzeuge", "Show developer tools": "Zeige Entwickler-Werkzeuge", "If you would like to create a Matrix account you can register now.": "Wenn du ein Matrix-Konto erstellen möchtest, kannst du dich jetzt registrieren.", - "You are currently using Riot anonymously as a guest.": "Du benutzt aktuell Riot anonym als Gast." + "You are currently using Riot anonymously as a guest.": "Du benutzt aktuell Riot anonym als Gast.", + "Unable to load! Check your network connectivity and try again.": "Konnte nicht geladen werden! Überprüfe deine Netzwerkverbindung und versuche es erneut.", + "Backup of encryption keys to server": "Sichern der Verschlüsselungs-Schlüssel auf dem Server", + "Delete Backup": "Sicherung löschen", + "Delete backup": "Sicherung löschen", + "This device is uploading keys to this backup": "Dieses Gerät lädt Schlüssel zu dieser Sicherung hoch", + "This device is not uploading keys to this backup": "Dieses Gerät lädt keine Schlüssel zu dieser Sicherung hoch", + "Backup has a valid signature from this device": "Sicherung hat eine valide Signatur von diesem Gerät", + "Backup has an invalid signature from verified device ": "Sicherung hat eine invalide Signatur vom verifiziertem Gerät ", + "Backup has an invalid signature from unverified device ": "Sicherung hat eine invalide Signatur vom unverifiziertem Gerät ", + "Backup has a valid signature from verified device x": "Sicherung hat eine valide Signatur vom verifiziertem Gerät x", + "Backup has a valid signature from unverified device ": "Sicherung hat eine valide Signatur vom unverifiziertem Gerät ", + "Backup is not signed by any of your devices": "Sicherung wurde von keinem deiner Geräte signiert", + "Backup version: ": "Sicherungsversion: ", + "Algorithm: ": "Algorithmus: ", + "Restore backup": "Sicherung wiederherstellen", + "No backup is present": "Keine Sicherung verfügbar", + "Start a new backup": "Starte einen neue Sicherung", + "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": "Um deinen Chatverlauf nicht zu verlieren, musst du deine Raum-Schlüssel exportieren, bevor du dich abmeldest. Du musst zurück zu einer neueren Riot-Version gehen, um dies zu tun", + "Incompatible Database": "Inkompatible Datenbanken", + "Continue With Encryption Disabled": "Mit deaktivierter Verschlüsselung fortfahren", + "You'll need it if you log out or lose access to this device.": "Du wirst es brauchen, wenn du dich abmeldest oder den Zugang zu diesem Gerät verlierst.", + "Enter a passphrase...": "Passphrase eingeben...", + "Next": "Nächstes", + "That matches!": "Das passt!", + "That doesn't match.": "Das passt nicht.", + "Go back to set it again.": "Gehe zurück und setze es erneut.", + "Repeat your passphrase...": "Wiederhole deine Passphrase...", + "Make a copy of this Recovery Key and keep it safe.": "Mache eine Kopie dieses Wiederherstellungsschlüssels und verwahre ihn sicher.", + "Your Recovery Key": "Dein Wiederherstellungsschlüssel", + "Copy to clipboard": "In Zwischenablage kopieren", + "Download": "Herunterladen", + "I've made a copy": "Ich habe eine Kopie gemacht", + "Print it and store it somewhere safe": "Drucke ihn aus und lagere ihn, wo er sicher ist", + "Save it on a USB key or backup drive": "Speichere ihn auf einem USB-Schlüssel oder Sicherungsslaufwerk", + "Copy it to your personal cloud storage": "Kopiere ihn in deinen persönlichen Cloud-Speicher", + "Got it": "Verstanden", + "Backup created": "Sicherung erstellt", + "Your encryption keys are now being backed up to your Homeserver.": "Deine Verschlüsselungsschlüssel sind nun auf deinem Heimserver gesichert wurden.", + "Create a Recovery Passphrase": "Erstelle eine Wiederherstellungs-Passphrase", + "Confirm Recovery Passphrase": "Bestätige Wiederherstellungs-Passphrase", + "Recovery Key": "Wiederherstellungsschlüssel", + "Keep it safe": "Lager ihn sicher", + "Backing up...": "Am sichern...", + "Create Key Backup": "Erzeuge Schlüsselsicherung", + "Unable to create key backup": "Konnte Schlüsselsicherung nicht erstellen", + "Retry": "Erneut probieren", + "Unable to restore backup": "Konnte Sicherung nicht wiederherstellen", + "No backup found!": "Keine Sicherung gefunden!", + "Backup Restored": "Sicherung wiederhergestellt", + "Enter Recovery Passphrase": "Gebe Wiederherstellungs-Passphrase ein", + "Enter Recovery Key": "Gebe Wiederherstellungsschlüssel ein", + "This looks like a valid recovery key!": "Dies sieht nach einem validen Wiederherstellungsschlüssel aus", + "Not a valid recovery key": "Kein valider Wiederherstellungsschlüssel", + "Key Backup": "Schlüsselsicherung", + "Cannot find homeserver": "Konnte Heimserver nicht finden" } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 14e5c145298..fe4b3f4c19b 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -43,6 +43,10 @@ "The file '%(fileName)s' failed to upload": "The file '%(fileName)s' failed to upload", "The file '%(fileName)s' exceeds this home server's size limit for uploads": "The file '%(fileName)s' exceeds this home server's size limit for uploads", "Upload Failed": "Upload Failed", + "Failure to create room": "Failure to create room", + "Server may be unavailable, overloaded, or you hit a bug.": "Server may be unavailable, overloaded, or you hit a bug.", + "Send anyway": "Send anyway", + "Send": "Send", "Sun": "Sun", "Mon": "Mon", "Tue": "Tue", @@ -82,6 +86,8 @@ "Failed to invite users to community": "Failed to invite users to community", "Failed to invite users to %(groupId)s": "Failed to invite users to %(groupId)s", "Failed to add the following rooms to %(groupId)s:": "Failed to add the following rooms to %(groupId)s:", + "Unnamed Room": "Unnamed Room", + "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", @@ -106,6 +112,7 @@ "Failed to invite user": "Failed to invite user", "Operation failed": "Operation failed", "Failed to invite": "Failed to invite", + "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.", "You need to be able to invite users to do that.": "You need to be able to invite users to do that.", @@ -208,11 +215,6 @@ "%(names)s and %(count)s others are typing …|other": "%(names)s and %(count)s others are typing …", "%(names)s and %(count)s others are typing …|one": "%(names)s and one other is typing …", "%(names)s and %(lastPerson)s are typing …": "%(names)s and %(lastPerson)s are typing …", - "Failure to create room": "Failure to create room", - "Server may be unavailable, overloaded, or you hit a bug.": "Server may be unavailable, overloaded, or you hit a bug.", - "Send anyway": "Send anyway", - "Send": "Send", - "Unnamed Room": "Unnamed Room", "This homeserver has hit its Monthly Active User limit.": "This homeserver has hit its Monthly Active User limit.", "This homeserver has exceeded one of its resource limits.": "This homeserver has exceeded one of its resource limits.", "Please contact your service administrator to continue using the service.": "Please contact your service administrator to continue using the service.", @@ -220,6 +222,35 @@ "Your browser does not support the required cryptography extensions": "Your browser does not support the required cryptography extensions", "Not a valid Riot keyfile": "Not a valid Riot keyfile", "Authentication check failed: incorrect password?": "Authentication check failed: incorrect password?", + "You do not have permission to invite people to this room.": "You do not have permission to invite people to this room.", + "User %(user_id)s does not exist": "User %(user_id)s does not exist", + "Unknown server error": "Unknown server error", + "Use a few words, avoid common phrases": "Use a few words, avoid common phrases", + "No need for symbols, digits, or uppercase letters": "No need for symbols, digits, or uppercase letters", + "Use a longer keyboard pattern with more turns": "Use a longer keyboard pattern with more turns", + "Avoid repeated words and characters": "Avoid repeated words and characters", + "Avoid sequences": "Avoid sequences", + "Avoid recent years": "Avoid recent years", + "Avoid years that are associated with you": "Avoid years that are associated with you", + "Avoid dates and years that are associated with you": "Avoid dates and years that are associated with you", + "Capitalization doesn't help very much": "Capitalization doesn't help very much", + "All-uppercase is almost as easy to guess as all-lowercase": "All-uppercase is almost as easy to guess as all-lowercase", + "Reversed words aren't much harder to guess": "Reversed words aren't much harder to guess", + "Predictable substitutions like '@' instead of 'a' don't help very much": "Predictable substitutions like '@' instead of 'a' don't help very much", + "Add another word or two. Uncommon words are better.": "Add another word or two. Uncommon words are better.", + "Repeats like \"aaa\" are easy to guess": "Repeats like \"aaa\" are easy to guess", + "Repeats like \"abcabcabc\" are only slightly harder to guess than \"abc\"": "Repeats like \"abcabcabc\" are only slightly harder to guess than \"abc\"", + "Sequences like abc or 6543 are easy to guess": "Sequences like abc or 6543 are easy to guess", + "Recent years are easy to guess": "Recent years are easy to guess", + "Dates are often easy to guess": "Dates are often easy to guess", + "This is a top-10 common password": "This is a top-10 common password", + "This is a top-100 common password": "This is a top-100 common password", + "This is a very common password": "This is a very common password", + "This is similar to a commonly used password": "This is similar to a commonly used password", + "A word by itself is easy to guess": "A word by itself is easy to guess", + "Names and surnames by themselves are easy to guess": "Names and surnames by themselves are easy to guess", + "Common names and surnames are easy to guess": "Common names and surnames are easy to guess", + "There was an error joining the room": "There was an error joining the room", "Sorry, your homeserver is too old to participate in this room.": "Sorry, your homeserver is too old to participate in this room.", "Please contact your homeserver administrator.": "Please contact your homeserver administrator.", "Failed to join room": "Failed to join room", @@ -237,6 +268,7 @@ "Always show message timestamps": "Always show message timestamps", "Autoplay GIFs and videos": "Autoplay GIFs and videos", "Always show encryption icons": "Always show encryption icons", + "Show a reminder to enable Secure Message Recovery in encrypted rooms": "Show a reminder to enable Secure Message Recovery in encrypted rooms", "Enable automatic language detection for syntax highlighting": "Enable automatic language detection for syntax highlighting", "Hide avatars in user and room mentions": "Hide avatars in user and room mentions", "Disable big emoji in chat": "Disable big emoji in chat", @@ -263,8 +295,11 @@ "Waiting for response from server": "Waiting for response from server", "Messages containing my display name": "Messages containing my display name", "Messages containing my user name": "Messages containing my user name", + "Messages containing @room": "Messages containing @room", "Messages in one-to-one chats": "Messages in one-to-one chats", + "Encrypted messages in one-to-one chats": "Encrypted messages in one-to-one chats", "Messages in group chats": "Messages in group chats", + "Encrypted messages in group chats": "Encrypted messages in group chats", "When I'm invited to a room": "When I'm invited to a room", "Call invitation": "Call invitation", "Messages sent by bot": "Messages sent by bot", @@ -275,7 +310,6 @@ "Incoming call from %(name)s": "Incoming call from %(name)s", "Decline": "Decline", "Accept": "Accept", - "Error": "Error", "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains": "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains", "Incorrect verification code": "Incorrect verification code", "Enter Code": "Enter Code", @@ -317,7 +351,7 @@ "This device is uploading keys to this backup": "This device is uploading keys to this backup", "This device is not uploading keys to this backup": "This device is not uploading keys to this backup", "Backup has a valid signature from this device": "Backup has a valid signature from this device", - "Backup has a valid signature from verified device x": "Backup has a valid signature from verified device x", + "Backup has a valid signature from verified device ": "Backup has a valid signature from verified device ", "Backup has a valid signature from unverified device ": "Backup has a valid signature from unverified device ", "Backup has an invalid signature from verified device ": "Backup has an invalid signature from verified device ", "Backup has an invalid signature from unverified device ": "Backup has an invalid signature from unverified device ", @@ -421,6 +455,7 @@ "Close": "Close", "and %(count)s others...|other": "and %(count)s others...", "and %(count)s others...|one": "and one other...", + "Invite to this room": "Invite to this room", "Invited": "Invited", "Filter room members": "Filter room members", "%(userName)s (power %(powerLevelNumber)s)": "%(userName)s (power %(powerLevelNumber)s)", @@ -434,8 +469,9 @@ "numbered-list": "numbered-list", "Attachment": "Attachment", "At this time it is not possible to reply with a file so this will be sent without being a reply.": "At this time it is not possible to reply with a file so this will be sent without being a reply.", - "Upload Files": "Upload Files", "Are you sure you want to upload the following files?": "Are you sure you want to upload the following files?", + "The following files cannot be uploaded:": "The following files cannot be uploaded:", + "Upload Files": "Upload Files", "Encrypted room": "Encrypted room", "Unencrypted room": "Unencrypted room", "Hangup": "Hangup", @@ -460,11 +496,11 @@ "At this time it is not possible to reply with an emote.": "At this time it is not possible to reply with an emote.", "Markdown is disabled": "Markdown is disabled", "Markdown is enabled": "Markdown is enabled", - "Unpin Message": "Unpin Message", - "Jump to message": "Jump to message", "No pinned messages.": "No pinned messages.", "Loading...": "Loading...", "Pinned Messages": "Pinned Messages", + "Unpin Message": "Unpin Message", + "Jump to message": "Jump to message", "%(duration)ss": "%(duration)ss", "%(duration)sm": "%(duration)sm", "%(duration)sh": "%(duration)sh", @@ -495,21 +531,17 @@ "Forget room": "Forget room", "Search": "Search", "Share room": "Share room", - "Show panel": "Show panel", "Drop here to favourite": "Drop here to favourite", "Drop here to tag direct chat": "Drop here to tag direct chat", "Drop here to restore": "Drop here to restore", "Drop here to demote": "Drop here to demote", "Drop here to tag %(section)s": "Drop here to tag %(section)s", - "Press to start a chat with someone": "Press to start a chat with someone", - "You're not in any rooms yet! Press to make a room or to browse the directory": "You're not in any rooms yet! Press to make a room or to browse the directory", "Community Invites": "Community Invites", "Invites": "Invites", "Favourites": "Favourites", "People": "People", "Rooms": "Rooms", "Low priority": "Low priority", - "You have no historical rooms": "You have no historical rooms", "Historical": "Historical", "System Alerts": "System Alerts", "Joining room...": "Joining room...", @@ -531,6 +563,10 @@ "You are trying to access a room.": "You are trying to access a room.", "Click here to join the discussion!": "Click here to join the discussion!", "This is a preview of this room. Room interactions have been disabled": "This is a preview of this room. Room interactions have been disabled", + "Secure Message Recovery": "Secure Message Recovery", + "If you log out or use another device, you'll lose your secure message history. To prevent this, set up Secure Message Recovery.": "If you log out or use another device, you'll lose your secure message history. To prevent this, set up Secure Message Recovery.", + "Don't ask again": "Don't ask again", + "Set up": "Set up", "To change the room's avatar, you must be a": "To change the room's avatar, you must be a", "To change the room's name, you must be a": "To change the room's name, you must be a", "To change the room's main address, you must be a": "To change the room's main address, you must be a", @@ -630,6 +666,9 @@ "In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.": "In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.", "URL Previews": "URL Previews", "When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.": "When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.", + "Members": "Members", + "Files": "Files", + "Notifications": "Notifications", "Sunday": "Sunday", "Monday": "Monday", "Tuesday": "Tuesday", @@ -684,6 +723,7 @@ "User name": "User name", "Mobile phone number": "Mobile phone number", "Forgot your password?": "Forgot your password?", + "Matrix ID": "Matrix ID", "%(serverName)s Matrix ID": "%(serverName)s Matrix ID", "Sign in with": "Sign in with", "Email address": "Email address", @@ -702,7 +742,9 @@ "Remove this user from community?": "Remove this user from community?", "Failed to withdraw invitation": "Failed to withdraw invitation", "Failed to remove user from community": "Failed to remove user from community", + "Failed to load group members": "Failed to load group members", "Filter community members": "Filter community members", + "Invite to this community": "Invite to this community", "Flair will appear if enabled in room settings": "Flair will appear if enabled in room settings", "Flair will not appear": "Flair will not appear", "Are you sure you want to remove '%(roomName)s' from %(groupId)s?": "Are you sure you want to remove '%(roomName)s' from %(groupId)s?", @@ -715,6 +757,7 @@ "Visibility in Room List": "Visibility in Room List", "Visible to everyone": "Visible to everyone", "Only visible to community members": "Only visible to community members", + "Add rooms to this community": "Add rooms to this community", "Filter community rooms": "Filter community rooms", "Something went wrong when trying to get your communities.": "Something went wrong when trying to get your communities.", "Display your community flair in rooms configured to show it.": "Display your community flair in rooms configured to show it.", @@ -830,9 +873,9 @@ "And %(count)s more...|other": "And %(count)s more...", "ex. @bob:example.com": "ex. @bob:example.com", "Add User": "Add User", - "Matrix ID": "Matrix ID", "Matrix Room ID": "Matrix Room ID", "email address": "email address", + "That doesn't look like a valid email address": "That doesn't look like a valid email address", "You have entered an invalid address.": "You have entered an invalid address.", "Try using one of the following valid address types: %(validTypesList)s.": "Try using one of the following valid address types: %(validTypesList)s.", "Preparing to send logs": "Preparing to send logs", @@ -874,7 +917,6 @@ "You've previously used a newer version of Riot on %(host)s. To use this version again with end to end encryption, you will need to sign out and back in again. ": "You've previously used a newer version of Riot on %(host)s. To use this version again with end to end encryption, you will need to sign out and back in again. ", "Incompatible Database": "Incompatible Database", "Continue With Encryption Disabled": "Continue With Encryption Disabled", - "Failed to indicate account erasure": "Failed to indicate account erasure", "Unknown error": "Unknown error", "Incorrect password": "Incorrect password", "Deactivate Account": "Deactivate Account", @@ -919,6 +961,11 @@ "Clear cache and resync": "Clear cache and resync", "Riot now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!": "Riot now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!", "Updating Riot": "Updating Riot", + "For security, logging out will delete any end-to-end encryption keys from this browser. If you want to be able to decrypt your conversation history from future Riot sessions, please export your room keys for safe-keeping.": "For security, logging out will delete any end-to-end encryption keys from this browser. If you want to be able to decrypt your conversation history from future Riot sessions, please export your room keys for safe-keeping.", + "Thanks for testing the Riot Redesign. If you run into any bugs or visual issues, please let us know on GitHub.": "Thanks for testing the Riot Redesign. If you run into any bugs or visual issues, please let us know on GitHub.", + "To help avoid duplicate issues, please view existing issues first (and add a +1) or create a new issue if you can't find it.": "To help avoid duplicate issues, please view existing issues first (and add a +1) or create a new issue if you can't find it.", + "Report bugs & give feedback": "Report bugs & give feedback", + "Go back": "Go back", "Failed to upgrade room": "Failed to upgrade room", "The room upgrade could not be completed": "The room upgrade could not be completed", "Upgrade this room to version %(version)s": "Upgrade this room to version %(version)s", @@ -944,10 +991,11 @@ "Unable to verify email address.": "Unable to verify email address.", "This will allow you to reset your password and receive notifications.": "This will allow you to reset your password and receive notifications.", "Skip": "Skip", - "User names may only contain letters, numbers, dots, hyphens and underscores.": "User names may only contain letters, numbers, dots, hyphens and underscores.", + "Only use lower case letters, numbers and '=_-./'": "Only use lower case letters, numbers and '=_-./'", "Username not available": "Username not available", "Username invalid: %(errMessage)s": "Username invalid: %(errMessage)s", "An error occurred: %(error_string)s": "An error occurred: %(error_string)s", + "Checking...": "Checking...", "Username available": "Username available", "To get started, please pick a username!": "To get started, please pick a username!", "This will be your account name on the homeserver, or you can pick a different server.": "This will be your account name on the homeserver, or you can pick a different server.", @@ -972,41 +1020,6 @@ "Room contains unknown devices": "Room contains unknown devices", "\"%(RoomName)s\" contains devices that you haven't seen before.": "\"%(RoomName)s\" contains devices that you haven't seen before.", "Unknown devices": "Unknown devices", - "Secure your encrypted message history with a Recovery Passphrase.": "Secure your encrypted message history with a Recovery Passphrase.", - "You'll need it if you log out or lose access to this device.": "You'll need it if you log out or lose access to this device.", - "Enter a passphrase...": "Enter a passphrase...", - "Next": "Next", - "If you don't want encrypted message history to be availble on other devices, .": "If you don't want encrypted message history to be availble on other devices, .", - "Or, if you don't want to create a Recovery Passphrase, skip this step and .": "Or, if you don't want to create a Recovery Passphrase, skip this step and .", - "That matches!": "That matches!", - "That doesn't match.": "That doesn't match.", - "Go back to set it again.": "Go back to set it again.", - "Type in your Recovery Passphrase to confirm you remember it. If it helps, add it to your password manager or store it somewhere safe.": "Type in your Recovery Passphrase to confirm you remember it. If it helps, add it to your password manager or store it somewhere safe.", - "Repeat your passphrase...": "Repeat your passphrase...", - "Make a copy of this Recovery Key and keep it safe.": "Make a copy of this Recovery Key and keep it safe.", - "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.", - "Your Recovery Key": "Your Recovery Key", - "Copy to clipboard": "Copy to clipboard", - "Download": "Download", - "I've made a copy": "I've made a copy", - "Your Recovery Key has been copied to your clipboard, paste it to:": "Your Recovery Key has been copied to your clipboard, paste it to:", - "Your Recovery Key is in your Downloads folder.": "Your Recovery Key is in your Downloads folder.", - "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", - "Got it": "Got it", - "Backup created": "Backup created", - "Your encryption keys are now being backed up to your Homeserver.": "Your encryption keys are now being backed up to your Homeserver.", - "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.": "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.", - "Set up Secure Message Recovery": "Set up Secure Message Recovery", - "Create a Recovery Passphrase": "Create a Recovery Passphrase", - "Confirm Recovery Passphrase": "Confirm Recovery Passphrase", - "Recovery Key": "Recovery Key", - "Keep it safe": "Keep it safe", - "Backing up...": "Backing up...", - "Create Key Backup": "Create Key Backup", - "Unable to create key backup": "Unable to create key backup", - "Retry": "Retry", "Unable to load backup status": "Unable to load backup status", "Unable to restore backup": "Unable to restore backup", "No backup found!": "No backup found!", @@ -1015,6 +1028,7 @@ "Restored %(sessionCount)s session keys": "Restored %(sessionCount)s session keys", "Enter Recovery Passphrase": "Enter Recovery Passphrase", "Access your secure message history and set up secure messaging by entering your recovery passphrase.": "Access your secure message history and set up secure messaging by entering your recovery passphrase.", + "Next": "Next", "If you've forgotten your recovery passphrase you can use your recovery key or set up new recovery options": "If you've forgotten your recovery passphrase you can use your recovery key or set up new recovery options", "Enter Recovery Key": "Enter Recovery Key", "This looks like a valid recovery key!": "This looks like a valid recovery key!", @@ -1057,11 +1071,6 @@ "Safari and Opera work too.": "Safari and Opera work too.", "With your current browser, the look and feel of the application may be completely incorrect, and some or all features may not function. If you want to try it anyway you can continue, but you are on your own in terms of any issues you may encounter!": "With your current browser, the look and feel of the application may be completely incorrect, and some or all features may not function. If you want to try it anyway you can continue, but you are on your own in terms of any issues you may encounter!", "I understand the risks and wish to continue": "I understand the risks and wish to continue", - "Name": "Name", - "Topic": "Topic", - "Make this room private": "Make this room private", - "Share message history with new users": "Share message history with new users", - "Encrypt room": "Encrypt room", "You must register to use this functionality": "You must register to use this functionality", "You must join the room to see its files": "You must join the room to see its files", "There are no visible files in this room": "There are no visible files in this room", @@ -1090,7 +1099,6 @@ "Community Settings": "Community Settings", "Changes made to your community name and avatar might not be seen by other users for up to 30 minutes.": "Changes made to your community name and avatar might not be seen by other users for up to 30 minutes.", "These rooms are displayed to community members on the community page. Community members can join the rooms by clicking on them.": "These rooms are displayed to community members on the community page. Community members can join the rooms by clicking on them.", - "Add rooms to this community": "Add rooms to this community", "Featured Rooms:": "Featured Rooms:", "Featured Users:": "Featured Users:", "%(inviter)s has invited you to join this community": "%(inviter)s has invited you to join this community", @@ -1110,6 +1118,7 @@ "You are currently using Riot anonymously as a guest.": "You are currently using Riot anonymously as a guest.", "If you would like to create a Matrix account you can register now.": "If you would like to create a Matrix account you can register now.", "Login": "Login", + "Invalid configuration: Cannot supply a default homeserver URL and a default server name": "Invalid configuration: Cannot supply a default homeserver URL and a default server name", "Failed to reject invitation": "Failed to reject invitation", "This room is not public. You will not be able to rejoin without an invite.": "This room is not public. You will not be able to rejoin without an invite.", "Are you sure you want to leave the room '%(roomName)s'?": "Are you sure you want to leave the room '%(roomName)s'?", @@ -1123,6 +1132,7 @@ "Review terms and conditions": "Review terms and conditions", "Old cryptography data detected": "Old cryptography data detected", "Data from an older version of Riot has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.": "Data from an older version of Riot has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.", + "Unknown error discovering homeserver": "Unknown error discovering homeserver", "Logout": "Logout", "Your Communities": "Your Communities", "Did you know: you can use communities to filter your Riot.im experience!": "Did you know: you can use communities to filter your Riot.im experience!", @@ -1131,14 +1141,6 @@ "Create a new community": "Create a new community", "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.", "You have no visible notifications": "You have no visible notifications", - "Members": "Members", - "%(count)s Members|other": "%(count)s Members", - "%(count)s Members|one": "%(count)s Member", - "Invite to this room": "Invite to this room", - "Files": "Files", - "Notifications": "Notifications", - "Hide panel": "Hide panel", - "Invite to this community": "Invite to this community", "Failed to get protocol list from Home Server": "Failed to get protocol list from Home Server", "The Home Server may be too old to support third party networks": "The Home Server may be too old to support third party networks", "Failed to get public room list": "Failed to get public room list", @@ -1173,9 +1175,9 @@ "%(count)s new messages|one": "%(count)s new message", "Active call": "Active call", "There's no one else here! Would you like to invite others or stop warning about the empty room?": "There's no one else here! Would you like to invite others or stop warning about the empty room?", - "more": "more", "You seem to be uploading files, are you sure you want to quit?": "You seem to be uploading files, are you sure you want to quit?", "You seem to be in a call, are you sure you want to quit?": "You seem to be in a call, are you sure you want to quit?", + "File is too big. Maximum file size is %(fileSize)s": "File is too big. Maximum file size is %(fileSize)s", "Failed to upload file": "Failed to upload file", "Server may be unavailable, overloaded, or the file too big": "Server may be unavailable, overloaded, or the file too big", "Search failed": "Search failed", @@ -1190,8 +1192,6 @@ "Click to mute video": "Click to mute video", "Click to unmute audio": "Click to unmute audio", "Click to mute audio": "Click to mute audio", - "Expand panel": "Expand panel", - "Collapse panel": "Collapse panel", "Filter room names": "Filter room names", "Clear filter": "Clear filter", "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.", @@ -1206,7 +1206,6 @@ "Status.im theme": "Status.im theme", "Can't load user settings": "Can't load user settings", "Server may be unavailable or overloaded": "Server may be unavailable or overloaded", - "For security, logging out will delete any end-to-end encryption keys from this browser. If you want to be able to decrypt your conversation history from future Riot sessions, please export your room keys for safe-keeping.": "For security, logging out will delete any end-to-end encryption keys from this browser. If you want to be able to decrypt your conversation history from future Riot sessions, please export your room keys for safe-keeping.", "Success": "Success", "Your password was successfully changed. You will not receive push notifications on other devices until you log back in to them": "Your password was successfully changed. You will not receive push notifications on other devices until you log back in to them", "Remove Contact Information?": "Remove Contact Information?", @@ -1283,12 +1282,17 @@ "Confirm your new password": "Confirm your new password", "Send Reset Email": "Send Reset Email", "Create an account": "Create an account", + "Invalid homeserver discovery response": "Invalid homeserver discovery response", + "Invalid identity server discovery response": "Invalid identity server discovery response", + "General failure": "General failure", "This Home Server does not support login using email address.": "This Home Server does not support login using email address.", "Please contact your service administrator to continue using this service.": "Please contact your service administrator to continue using this service.", "Incorrect username and/or password.": "Incorrect username and/or password.", "Please note you are logging into the %(hs)s server, not matrix.org.": "Please note you are logging into the %(hs)s server, not matrix.org.", "Guest access is disabled on this Home Server.": "Guest access is disabled on this Home Server.", + "Failed to perform homeserver discovery": "Failed to perform homeserver discovery", "The phone number entered looks invalid": "The phone number entered looks invalid", + "Unknown failure discovering homeserver": "Unknown failure discovering homeserver", "This homeserver doesn't offer any login flows which are supported by this client.": "This homeserver doesn't offer any login flows which are supported by this client.", "Error: Problem communicating with the given homeserver.": "Error: Problem communicating with the given homeserver.", "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or enable unsafe scripts.": "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or enable unsafe scripts.", @@ -1320,6 +1324,7 @@ "unknown device": "unknown device", "NOT verified": "NOT verified", "verified": "verified", + "Name": "Name", "Verification": "Verification", "Ed25519 fingerprint": "Ed25519 fingerprint", "User ID": "User ID", @@ -1346,11 +1351,51 @@ "The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.": "The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.", "File to import": "File to import", "Import": "Import", + "Great! This passphrase looks strong enough.": "Great! This passphrase looks strong enough.", + "Secure your encrypted message history with a Recovery Passphrase.": "Secure your encrypted message history with a Recovery Passphrase.", + "You'll need it if you log out or lose access to this device.": "You'll need it if you log out or lose access to this device.", + "Enter a passphrase...": "Enter a passphrase...", + "If you don't want encrypted message history to be available on other devices, .": "If you don't want encrypted message history to be available on other devices, .", + "Or, if you don't want to create a Recovery Passphrase, skip this step and .": "Or, if you don't want to create a Recovery Passphrase, skip this step and .", + "That matches!": "That matches!", + "That doesn't match.": "That doesn't match.", + "Go back to set it again.": "Go back to set it again.", + "Type in your Recovery Passphrase to confirm you remember it. If it helps, add it to your password manager or store it somewhere safe.": "Type in your Recovery Passphrase to confirm you remember it. If it helps, add it to your password manager or store it somewhere safe.", + "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.", + "Make a copy of this Recovery Key and keep it safe.": "Make a copy of this Recovery Key and keep it safe.", + "Your Recovery Key": "Your Recovery Key", + "Copy to clipboard": "Copy to clipboard", + "Download": "Download", + "I've made a copy": "I've made a copy", + "Your Recovery Key has been copied to your clipboard, paste it to:": "Your Recovery Key has been copied to your clipboard, paste it to:", + "Your Recovery Key is in your Downloads folder.": "Your Recovery Key is in your Downloads folder.", + "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", + "Got it": "Got it", + "Backup created": "Backup created", + "Your encryption keys are now being backed up to your Homeserver.": "Your encryption keys are now being backed up to your Homeserver.", + "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.": "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.", + "Set up Secure Message Recovery": "Set up Secure Message Recovery", + "Create a Recovery Passphrase": "Create a Recovery Passphrase", + "Confirm Recovery Passphrase": "Confirm Recovery Passphrase", + "Recovery Key": "Recovery Key", + "Keep it safe": "Keep it safe", + "Backing up...": "Backing up...", + "Create Key Backup": "Create Key Backup", + "Unable to create key backup": "Unable to create key backup", + "Retry": "Retry", + "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.", + "If you don't want to set this up now, you can later in Settings.": "If you don't want to set this up now, you can later in Settings.", + "New Recovery Method": "New Recovery Method", + "A new recovery passphrase and key for Secure Messages has been detected.": "A new recovery passphrase and key for Secure Messages has been detected.", + "Setting up Secure Messages on this device will re-encrypt this device's message history with the new recovery method.": "Setting up Secure Messages on this device will re-encrypt this device's message history with the new recovery method.", + "If you didn't set the new 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 set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.", + "Set up Secure Messages": "Set up Secure Messages", + "Go to Settings": "Go to Settings", "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", - "Report bugs & give feedback": "Report bugs & give feedback", - "Thanks for testing the Riot Redesign. If you run into any bugs or visual issues, please let us know on GitHub.": "Thanks for testing the Riot Redesign. If you run into any bugs or visual issues, please let us know on GitHub.", - "To help avoid duplicate issues, please view existing issues first (and add a +1) or create a new issue if you can't find it.": "To help avoid duplicate issues, please view existing issues first (and add a +1) or create a new issue if you can't find it.", - "Go back": "Go back" + "Failed to add tag %(tagName)s to room": "Failed to add tag %(tagName)s to room" } diff --git a/src/i18n/strings/en_US.json b/src/i18n/strings/en_US.json index aa7140aaa80..b96a49eac78 100644 --- a/src/i18n/strings/en_US.json +++ b/src/i18n/strings/en_US.json @@ -134,6 +134,8 @@ "Failed to join room": "Failed to join room", "Failed to kick": "Failed to kick", "Failed to leave room": "Failed to leave room", + "Failed to load %(groupId)s": "Failed to load %(groupId)s", + "Failed to load group members": "Failed to load group members", "Failed to load timeline position": "Failed to load timeline position", "Failed to mute user": "Failed to mute user", "Failed to reject invite": "Failed to reject invite", diff --git a/src/i18n/strings/eu.json b/src/i18n/strings/eu.json index 1c9c07d3057..ce5778b749d 100644 --- a/src/i18n/strings/eu.json +++ b/src/i18n/strings/eu.json @@ -1300,5 +1300,83 @@ "Pin unread rooms to the top of the room list": "Finkatu irakurri gabeko gelak gelen zerrendaren goialdean", "Pin rooms I'm mentioned in to the top of the room list": "Finkatu aipatu nauten gelak gelen zerrendaren goialdean", "If you would like to create a Matrix account you can register now.": "Matrix kontu bat sortu nahi baduzu, izena eman dezakezu.", - "You are currently using Riot anonymously as a guest.": "Riot anonimoki gonbidatu gisa erabiltzen ari zara." + "You are currently using Riot anonymously as a guest.": "Riot anonimoki gonbidatu gisa erabiltzen ari zara.", + "Unable to load! Check your network connectivity and try again.": "Ezin da kargatu! Egiaztatu sare konexioa eta saiatu berriro.", + "Backup of encryption keys to server": "Zerbitzarirako zifratze gakoen babes-kopia", + "Delete Backup": "Ezabatu babes-kopia", + "Delete your backed up encryption keys from the server? You will no longer be able to use your recovery key to read encrypted message history": "Ezabatu zerbitzaritik gakoen babes-kopiak? Ezin izango duzu berreskuratze gakoa erabili zifratutako mezuen historia irakurteko", + "Delete backup": "Ezabatu babes-kopia", + "Unable to load key backup status": "Ezin izan da babes-kopiaren egoera kargatu", + "This device is uploading keys to this backup": "Gailu honek gakoak babes-kopia honetara igotzen ditu", + "This device is not uploading keys to this backup": "Gailu honek ez ditu gakoak igotzen babes-kopia honetara", + "Backup has a valid signature from this device": "Babes-kopiak gailu honen baliozko sinadura du", + "Backup has a valid signature from verified device x": "Babes-kopiak egiaztatutako x gailuaren baliozko sinadura du", + "Backup has a valid signature from unverified device ": "Babes-kopiak egiaztatu gabeko gailu baten baliozko sinadura du", + "Backup has an invalid signature from verified device ": "Babes-kopiak egiaztatutako gailuaren balio gabeko sinadura du", + "Backup has an invalid signature from unverified device ": "Babes-kopiak egiaztatu gabeko gailuaren baliogabeko sinadura du", + "Backup is not signed by any of your devices": "Babes-kopia ez dago zure gailu batek sinauta", + "Backup version: ": "Babes-kopiaren bertsioa: ", + "Algorithm: ": "Algoritmoa: ", + "Restore backup": "Berreskuratu babes-kopia", + "No backup is present": "Ez dago babes-kopiarik", + "Start a new backup": "Hasi babes-kopia berria", + "Please review and accept all of the homeserver's policies": "Berrikusi eta onartu hasiera-zerbitzariaren politika guztiak", + "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": "Zure txaten historiala ez galtzeko, zure gelako gakoak esportatu behar dituzu saioa amaitu aurretik. Riot-en bertsio berriagora bueltatu behar zara hau egiteko", + "You've previously used a newer version of Riot on %(host)s. To use this version again with end to end encryption, you will need to sign out and back in again. ": "Riot-en bertsio berriago bat erabili duzu %(host)s zerbitzarian. Bertsio hau berriro erabiltzeko muturretik muturrerako zifratzearekin, saioa amaitu eta berriro hasi beharko duzu. ", + "Incompatible Database": "Datu-base bateraezina", + "Continue With Encryption Disabled": "Jarraitu zifratzerik gabe", + "Secure your encrypted message history with a Recovery Passphrase.": "Ziurtatu zure zifratutako mezuen historiala berreskuratze pasa-esaldi batekin.", + "You'll need it if you log out or lose access to this device.": "Saioa amaitzen baduzu edo gailu hau erabiltzeko aukera galtzen baduzu, hau beharko duzu.", + "Enter a passphrase...": "Sartu pasa-esaldi bat...", + "Next": "Hurrengoa", + "If you don't want encrypted message history to be availble on other devices, .": "Ez baduzu zifratutako mezuen historiala beste gailuetan eskuragarri egotea, .", + "Or, if you don't want to create a Recovery Passphrase, skip this step and .": "Edo, ez baduzu berreskuratze pasa-esaldi bat sortu nahi, saltatu urrats hau eta .", + "That matches!": "Bat dator!", + "That doesn't match.": "Ez dator bat.", + "Go back to set it again.": "Joan atzera eta berriro ezarri.", + "Type in your Recovery Passphrase to confirm you remember it. If it helps, add it to your password manager or store it somewhere safe.": "Idatzi zure berreskuratze pasa-esaldia gogoratzen duzula berresteko. lagungarria bazaizu, gehitu ezazu zure pasahitz-kudeatzailera edo gorde toki seguru batean.", + "Repeat your passphrase...": "Errepikatu zure pasa-esaldia...", + "Make a copy of this Recovery Key and keep it safe.": "Egin berreskuratze gako honen kopia eta gorde toki seguruan.", + "As a safety net, you can use it to restore your encrypted message history if you forget your Recovery Passphrase.": "Aukeran, berreskuratze pasa-esaldia ahazten baduzu, zure zifratutako mezuen historiala berreskuratzeko erabili dezakezu.", + "Your Recovery Key": "Zure berreskuratze gakoa", + "Copy to clipboard": "Kopiatu arbelera", + "Download": "Deskargatu", + "I've made a copy": "Kopia bat egin dut", + "Your Recovery Key has been copied to your clipboard, paste it to:": "Zure berreskuratze gakoa zure arbelera kopiatu da, itsatsi hemen:", + "Your Recovery Key is in your Downloads folder.": "Zure berreskuratze gakoa zure Deskargak karpetan dago.", + "Print it and store it somewhere safe": "Inprimatu ezazu eta gorde toki seguruan", + "Save it on a USB key or backup drive": "Gorde ezazu USB giltza batean edo babes-kopien diskoan", + "Copy it to your personal cloud storage": "Kopiatu ezazu zure hodeiko biltegi pertsonalean", + "Got it": "Ulertuta", + "Backup created": "Babes-kopia sortuta", + "Your encryption keys are now being backed up to your Homeserver.": "Zure zifratze gakoak zure hasiera-zerbitzarian gordetzen ari dira.", + "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.": "Mezuen berreskuratze segurua ezartzen ez bada, ezin izango duzu zure zifratutako mezuen historiala berreskuratu saioa amaitzen baduzu edo beste gailu bat erabiltzen baduzu.", + "Set up Secure Message Recovery": "Ezarri mezuen berreskuratze segurua", + "Create a Recovery Passphrase": "Sortu berreskuratze pasa-esaldia", + "Confirm Recovery Passphrase": "Berretsi berreskuratze pasa-esaldia", + "Recovery Key": "Berreskuratze gakoa", + "Keep it safe": "Gorde toki seguruan", + "Backing up...": "Babes-kopia egiten...", + "Create Key Backup": "Sortu gakoaren babes-kopia", + "Unable to create key backup": "Ezin izan da gakoaren babes-kopia sortu", + "Retry": "Berriro saiatu", + "Unable to load backup status": "Ezin izan da babes-kopiaren egoera kargatu", + "Unable to restore backup": "Ezin izan da babes-kopia berrezarri", + "No backup found!": "Ez da babes-kopiarik aurkitu!", + "Backup Restored": "Babes-kopia berrezarrita", + "Failed to decrypt %(failedCount)s sessions!": "Ezin izan dira %(failedCount)s saio deszifratu!", + "Restored %(sessionCount)s session keys": "%(sessionCount)s saio gako berrezarrita", + "Enter Recovery Passphrase": "Sartu berreskuratze pasa-esaldia", + "Access your secure message history and set up secure messaging by entering your recovery passphrase.": "Atzitu zure mezu seguruen historiala eta ezarri mezularitza segurua zure berreskuratze pasa-esaldia sartuz.", + "If you've forgotten your recovery passphrase you can use your recovery key or set up new recovery options": "Zure berreskuratze pasa-esaldia ahaztu baduzu berreskuratze gakoa erabili dezakezu edo berreskuratze aukera berriak ezarri ditzakezu", + "Enter Recovery Key": "Sartu berreskuratze gakoa", + "This looks like a valid recovery key!": "Hau baliozko berreskuratze gako bat dirudi!", + "Not a valid recovery key": "Ez da baliozko berreskuratze gako bat", + "Access your secure message history and set up secure messaging by entering your recovery key.": "Atzitu zure mezu seguruen historiala eta ezarri mezularitza segurua zure berreskuratze gakoa sartuz.", + "If you've forgotten your recovery passphrase you can ": "Zure berreskuratze pasa-esaldia ahaztu baduzu ditzakezu", + "Key Backup": "Gakoen babes-kopia", + "Sign in with single sign-on": "Hai saioa urrats batean", + "Failed to perform homeserver discovery": "Huts egin du hasiera-zerbitzarien bilaketak", + "Invalid homeserver discovery response": "Baliogabeko hasiera-zerbitzarien bilaketaren erantzuna", + "Cannot find homeserver": "Ezin izan da hasiera-zerbitzaria aurkitu" } diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index cdb8f789313..a6e2942e387 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -1302,5 +1302,113 @@ "Pin unread rooms to the top of the room list": "Épingler les salons non lus en haut de la liste des salons", "Pin rooms I'm mentioned in to the top of the room list": "Épingler les salons où l'on me mentionne en haut de la liste des salons", "If you would like to create a Matrix account you can register now.": "Si vous souhaitez créer un compte Matrix, vous pouvez vous inscrire maintenant.", - "You are currently using Riot anonymously as a guest.": "Vous utilisez Riot de façon anonyme en tant qu'invité." + "You are currently using Riot anonymously as a guest.": "Vous utilisez Riot de façon anonyme en tant qu'invité.", + "Please review and accept all of the homeserver's policies": "Veuillez lire et accepter toutes les polices du serveur d'accueil", + "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": "Pour éviter de perdre l'historique de vos discussions, vous devez exporter vos clés avant de vous déconnecter. Vous devez revenir à une version plus récente de Riot pour pouvoir le faire", + "You've previously used a newer version of Riot on %(host)s. To use this version again with end to end encryption, you will need to sign out and back in again. ": "Vous avez utilisé une version plus récente de Riot sur %(host)s. Pour utiliser à nouveau cette version avec le chiffrement de bout à bout, vous devez vous déconnecter et vous reconnecter. ", + "Incompatible Database": "Base de données incompatible", + "Continue With Encryption Disabled": "Continuer avec le chiffrement désactivé", + "Sign in with single sign-on": "Se connecter avec l'authentification unique", + "Unable to load! Check your network connectivity and try again.": "Chargement impossible ! Vérifiez votre connexion au réseau et réessayez.", + "Backup of encryption keys to server": "Sauvegarde des clés de chiffrement vers le serveur", + "Delete Backup": "Supprimer la sauvegarde", + "Delete your backed up encryption keys from the server? You will no longer be able to use your recovery key to read encrypted message history": "Supprimer vos clés de chiffrement sauvegardées du serveur ? Vous ne pourrez plus utiliser votre clé de récupération pour lire l'historique de vos messages chiffrés", + "Delete backup": "Supprimer la sauvegarde", + "Unable to load key backup status": "Impossible de charger l'état de sauvegarde des clés", + "This device is uploading keys to this backup": "Cet appareil envoie des clés vers cette sauvegarde", + "This device is not uploading keys to this backup": "Cet appareil n'envoie pas

    de clés vers cette sauvegarde", + "Backup has a valid signature from this device": "La sauvegarde a une signature valide pour cet appareil", + "Backup has a valid signature from verified device x": "La sauvegarde a une signature valide de l'appareil vérifié x", + "Backup has a valid signature from unverified device ": "La sauvegarde a une signature valide de l'appareil non vérifié ", + "Backup has an invalid signature from verified device ": "La sauvegarde a une signature non valide de l'appareil vérifié ", + "Backup has an invalid signature from unverified device ": "La sauvegarde a une signature non valide de l'appareil non vérifié ", + "Backup is not signed by any of your devices": "La sauvegarde n'est signée par aucun de vos appareils", + "Backup version: ": "Version de la sauvegarde : ", + "Algorithm: ": "Algorithme : ", + "Restore backup": "Restaurer la sauvegarde", + "No backup is present": "Il n'y a aucune sauvegarde", + "Start a new backup": "Créer une nouvelle sauvegarde", + "Secure your encrypted message history with a Recovery Passphrase.": "Sécurisez l'historique de vos messages chiffrés avec une phrase de récupération.", + "You'll need it if you log out or lose access to this device.": "Vous en aurez besoin si vous vous déconnectez ou si vous n'avez plus accès à cet appareil.", + "Enter a passphrase...": "Saisissez une phrase de passe…", + "Next": "Suivant", + "If you don't want encrypted message history to be availble on other devices, .": "Si vous ne souhaitez pas que l'historique de vos messages chiffrés soit disponible sur d'autres appareils, .", + "Or, if you don't want to create a Recovery Passphrase, skip this step and .": "Ou si vous ne voulez pas créer une phrase de récupération, sautez cette étape et .", + "That matches!": "Ça correspond !", + "That doesn't match.": "Ça ne correspond pas.", + "Go back to set it again.": "Retournez en arrière pour la redéfinir.", + "Type in your Recovery Passphrase to confirm you remember it. If it helps, add it to your password manager or store it somewhere safe.": "Saisissez votre phrase de récupération pour confirmer que vous vous en souvenez. Si cela peut vous aider, ajoutez-la à votre gestionnaire de mots de passe ou rangez-la dans un endroit sûr.", + "Repeat your passphrase...": "Répétez votre phrase de passe…", + "Make a copy of this Recovery Key and keep it safe.": "Faites une copie de cette clé de récupération et gardez-la en lieu sûr.", + "As a safety net, you can use it to restore your encrypted message history if you forget your Recovery Passphrase.": "Par précaution, vous pouvez l'utiliser pour récupérer l'historique de vos messages chiffrés si vous oubliez votre phrase de récupération.", + "Your Recovery Key": "Votre clé de récupération", + "Copy to clipboard": "Copier dans le presse-papier", + "Download": "Télécharger", + "I've made a copy": "J'ai fait une copie", + "Your Recovery Key has been copied to your clipboard, paste it to:": "Votre clé de récupération a été copiée dans votre presse-papier, collez-la dans :", + "Your Recovery Key is in your Downloads folder.": "Votre clé de récupération est dans votre dossier de téléchargements.", + "Print it and store it somewhere safe": "Imprimez-la et conservez-la dans un endroit sûr", + "Save it on a USB key or backup drive": "Sauvegardez-la sur une clé USB ou un disque de sauvegarde", + "Copy it to your personal cloud storage": "Copiez-la dans votre espace de stockage personnel en ligne", + "Got it": "Compris", + "Backup created": "Sauvegarde créée", + "Your encryption keys are now being backed up to your Homeserver.": "Vos clés de chiffrement sont en train d'être sauvegardées sur votre serveur d'accueil.", + "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.": "Si vous ne configurez pas la récupération de messages sécurisée, vous ne pourrez pas récupérer l'historique de vos messages chiffrés si vous vous déconnectez ou si vous utilisez un autre appareil.", + "Set up Secure Message Recovery": "Configurer la récupération de messages sécurisée", + "Create a Recovery Passphrase": "Créer une phrase de récupération", + "Confirm Recovery Passphrase": "Confirmer la phrase de récupération", + "Recovery Key": "Clé de récupération", + "Keep it safe": "Conservez-la en lieu sûr", + "Backing up...": "Sauvegarde en cours…", + "Create Key Backup": "Créer la sauvegarde des clés", + "Unable to create key backup": "Impossible de créer la sauvegarde des clés", + "Retry": "Réessayer", + "Unable to load backup status": "Impossible de charger l'état de la sauvegarde", + "Unable to restore backup": "Impossible de restaurer la sauvegarde", + "No backup found!": "Aucune sauvegarde n'a été trouvée !", + "Backup Restored": "Sauvegarde restaurée", + "Failed to decrypt %(failedCount)s sessions!": "Le déchiffrement de %(failedCount)s sessions a échoué !", + "Restored %(sessionCount)s session keys": "%(sessionCount)s clés de session ont été restaurées", + "Enter Recovery Passphrase": "Saisissez la phrase de récupération", + "Access your secure message history and set up secure messaging by entering your recovery passphrase.": "Accédez à l'historique sécurisé de vos messages et configurez la messagerie sécurisée en renseignant votre phrase de récupération.", + "If you've forgotten your recovery passphrase you can use your recovery key or set up new recovery options": "Si vous avez oublié votre phrase de récupération vous pouvez utiliser votre clé de récupération ou configurer de nouvelles options de récupération", + "Enter Recovery Key": "Saisissez la clé de récupération", + "This looks like a valid recovery key!": "Cela ressemble à une clé de récupération valide !", + "Not a valid recovery key": "Ce n'est pas une clé de récupération valide", + "Access your secure message history and set up secure messaging by entering your recovery key.": "Accédez à l'historique sécurisé de vos messages et configurez la messagerie sécurisée en renseignant votre clé de récupération.", + "If you've forgotten your recovery passphrase you can ": "Si vous avez oublié votre clé de récupération vous pouvez ", + "Key Backup": "Sauvegarde de clés", + "Failed to perform homeserver discovery": "Échec lors de la découverte du serveur d'accueil", + "Invalid homeserver discovery response": "Réponse de découverte du serveur d'accueil non valide", + "Cannot find homeserver": "Le serveur d'accueil est introuvable", + "File is too big. Maximum file size is %(fileSize)s": "Le fichier est trop gros. La taille maximum est de %(fileSize)s", + "The following files cannot be uploaded:": "Les fichiers suivants n'ont pas pu être envoyés :", + "Use a few words, avoid common phrases": "Utilisez quelques mots, évitez les phrases courantes", + "No need for symbols, digits, or uppercase letters": "Il n'y a pas besoin de symboles, de chiffres ou de majuscules", + "Avoid repeated words and characters": "Évitez de répéter des mots et des caractères", + "Avoid sequences": "Évitez les séquences", + "Avoid recent years": "Évitez les années récentes", + "Avoid years that are associated with you": "Évitez les années qui ont un rapport avec vous", + "Avoid dates and years that are associated with you": "Évitez les dates et les années qui ont un rapport avec vous", + "Capitalization doesn't help very much": "Les majuscules n'aident pas vraiment", + "All-uppercase is almost as easy to guess as all-lowercase": "Uniquement des majuscules, c'est presque aussi facile à deviner qu'uniquement des minuscules", + "Reversed words aren't much harder to guess": "Les mots inversés ne sont pas beaucoup plus difficiles à deviner", + "Predictable substitutions like '@' instead of 'a' don't help very much": "Les substitutions prévisibles comme « @ » à la place de « a » n'aident pas vraiment", + "Add another word or two. Uncommon words are better.": "Ajoutez un ou deux mots. Les mots rares sont à privilégier.", + "Repeats like \"aaa\" are easy to guess": "Les répétitions comme « aaa » sont faciles à deviner", + "Repeats like \"abcabcabc\" are only slightly harder to guess than \"abc\"": "Les répétitions comme « abcabcabc » ne sont pas beaucoup plus difficiles à deviner que « abc »", + "Sequences like abc or 6543 are easy to guess": "Les séquences comme abc ou 6543 sont faciles à deviner", + "Recent years are easy to guess": "Les années récentes sont faciles à deviner", + "Dates are often easy to guess": "Les dates sont généralement faciles à deviner", + "This is a top-10 common password": "Cela fait partie des 10 mots de passe les plus répandus", + "This is a top-100 common password": "Cela fait partie des 100 mots de passe les plus répandus", + "This is a very common password": "C'est un mot de passe très répandu", + "This is similar to a commonly used password": "Cela ressemble à un mot de passe répandu", + "A word by itself is easy to guess": "Un mot seul est facile à deviner", + "Names and surnames by themselves are easy to guess": "Les noms et prénoms seuls sont faciles à deviner", + "Common names and surnames are easy to guess": "Les noms et prénoms répandus sont faciles à deviner", + "Use a longer keyboard pattern with more turns": "Utilisez un schéma plus long et avec plus de variations", + "Great! This passphrase looks strong enough.": "Super ! Cette phrase de passe a l'air assez forte.", + "As a safety net, you can use it to restore your encrypted message history.": "En cas de problème, vous pouvez l'utiliser pour récupérer l'historique de vos messages chiffrés.", + "Failed to load group members": "Échec du chargement des membres du groupe" } diff --git a/src/i18n/strings/hi.json b/src/i18n/strings/hi.json index eb73d65a782..4c944f99251 100644 --- a/src/i18n/strings/hi.json +++ b/src/i18n/strings/hi.json @@ -15,5 +15,344 @@ "Which officially provided instance you are using, if any": "क्या आप कोई अधिकृत संस्करण इस्तेमाल कर रहे हैं? अगर हां, तो कौन सा", "Your homeserver's URL": "आपके होमसर्वर का यू. आर. एल.", "Every page you use in the app": "हर पृष्ठ जिसका आप इस एप में इस्तेमाल करते हैं", - "Your User Agent": "आपका उपभोक्ता प्रतिनिधि" + "Your User Agent": "आपका उपभोक्ता प्रतिनिधि", + "Custom Server Options": "कस्टम सर्वर विकल्प", + "Dismiss": "खारिज", + "powered by Matrix": "मैट्रिक्स द्वारा संचालित", + "Whether or not you're using the Richtext mode of the Rich Text Editor": "चाहे आप रिच टेक्स्ट एडिटर के रिच टेक्स्ट मोड का उपयोग कर रहे हों या नहीं", + "Your identity server's URL": "आपका आइडेंटिटी सर्वर का URL", + "e.g. %(exampleValue)s": "उदाहरणार्थ %(exampleValue)s", + "e.g. ": "उदाहरणार्थ ", + "Your device resolution": "आपके यंत्र का रेसोलुशन", + "Analytics": "एनालिटिक्स", + "The information being sent to us to help make Riot.im better includes:": "Riot.im को बेहतर बनाने के लिए हमें भेजी गई जानकारी में निम्नलिखित शामिल हैं:", + "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "जहां इस पृष्ठ में पहचान योग्य जानकारी शामिल है, जैसे कि रूम, यूजर या समूह आईडी, वह डाटा सर्वर को भेजे से पहले हटा दिया जाता है।", + "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.": "इस रूम में अज्ञात डिवाइस हैं: यदि आप उन्हें सत्यापित किए बिना आगे बढ़ते हैं, तो किसी और के लिए आपकी कॉल पर नजर डालना संभव हो सकता हैं।", + "Review Devices": "डिवाइस की समीक्षा करें", + "Call Anyway": "वैसे भी कॉल करें", + "Answer Anyway": "वैसे भी जवाब दें", + "Call": "कॉल", + "Answer": "उत्तर", + "Call Timeout": "कॉल टाइमआउट", + "The remote side failed to pick up": "दूसरी पार्टी ने जवाब नहीं दिया", + "Unable to capture screen": "स्क्रीन कैप्चर करने में असमर्थ", + "Existing Call": "मौजूदा कॉल", + "You are already in a call.": "आप पहले से ही एक कॉल में हैं।", + "VoIP is unsupported": "VoIP असमर्थित है", + "You cannot place VoIP calls in this browser.": "आप इस ब्राउज़र में VoIP कॉल नहीं कर सकते हैं।", + "You cannot place a call with yourself.": "आप अपने साथ कॉल नहीं कर सकते हैं।", + "Could not connect to the integration server": "इंटीग्रेशन सर्वर से संपर्क नहीं हो सका", + "A conference call could not be started because the intgrations server is not available": "कॉन्फ़्रेंस कॉल प्रारंभ नहीं किया जा सका क्योंकि इंटीग्रेशन सर्वर उपलब्ध नहीं है", + "Call in Progress": "कॉल चालू हैं", + "A call is currently being placed!": "वर्तमान में एक कॉल किया जा रहा है!", + "A call is already in progress!": "कॉल पहले ही प्रगति पर है!", + "Permission Required": "अनुमति आवश्यक है", + "You do not have permission to start a conference call in this room": "आपको इस रूम में कॉन्फ़्रेंस कॉल शुरू करने की अनुमति नहीं है", + "The file '%(fileName)s' failed to upload": "फ़ाइल '%(fileName)s' अपलोड करने में विफल रही", + "The file '%(fileName)s' exceeds this home server's size limit for uploads": "फाइल '%(fileName)s' अपलोड के लिए इस होम सर्वर की आकार सीमा से अधिक है", + "Upload Failed": "अपलोड विफल", + "Sun": "रवि", + "Mon": "सोम", + "Tue": "मंगल", + "Wed": "बुध", + "Thu": "गुरु", + "Fri": "शुक्र", + "Sat": "शनि", + "Jan": "जनवरी", + "Feb": "फ़रवरी", + "Mar": "मार्च", + "Apr": "अप्रैल", + "May": "मई", + "Jun": "जून", + "Jul": "जुलाई", + "Aug": "अगस्त", + "Sep": "सितंबर", + "Oct": "अक्टूबर", + "Nov": "नवंबर", + "Dec": "दिसंबर", + "PM": "अपराह्न", + "AM": "पूर्वाह्न", + "%(weekDayName)s %(time)s": "%(weekDayName)s %(time)s", + "%(weekDayName)s, %(monthName)s %(day)s %(time)s": "%(weekDayName)s %(monthName)s %(day)s %(time)s", + "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s, %(day)s %(monthName)s %(fullYear)s", + "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s", + "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": "चेतावनी: किसी भी कम्युनिटी में जो भी व्यक्ति आप जोड़ते हैं वह सार्वजनिक रूप से किसी को भी दिखाई देगा जो कम्युनिटी आईडी जानता है", + "Invite new community members": "नए कम्युनिटी के सदस्यों को आमंत्रित करें", + "Name or matrix ID": "नाम या मैट्रिक्स ID", + "Invite to Community": "कम्युनिटी में आमंत्रित करें", + "Which rooms would you like to add to this community?": "आप इस समुदाय में कौन से रूम जोड़ना चाहते हैं?", + "Show these rooms to non-members on the community page and room list?": "क्या आप इन मैट्रिक्स रूम को कम्युनिटी पृष्ठ और रूम लिस्ट के गैर सदस्यों को दिखाना चाहते हैं?", + "Add rooms to the community": "कम्युनिटी में रूम जोड़े", + "Room name or alias": "रूम का नाम या उपनाम", + "Add to community": "कम्युनिटी में जोड़ें", + "Failed to invite the following users to %(groupId)s:": "निम्नलिखित उपयोगकर्ताओं को %(groupId)s में आमंत्रित करने में विफल:", + "Failed to invite users to community": "उपयोगकर्ताओं को कम्युनिटी में आमंत्रित करने में विफल", + "Failed to invite users to %(groupId)s": "उपयोगकर्ताओं को %(groupId)s में आमंत्रित करने में विफल", + "Failed to add the following rooms to %(groupId)s:": "निम्नलिखित रूम को %(groupId)s में जोड़ने में विफल:", + "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": "रायट को सूचनाएं भेजने की अनुमति नहीं दी गई थी - कृपया पुनः प्रयास करें", + "Unable to enable Notifications": "अधिसूचनाएं सक्षम करने में असमर्थ", + "This email address was not found": "यह ईमेल पता नहीं मिला था", + "Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "आपका ईमेल पता इस होमसर्वर पर मैट्रिक्स आईडी से जुड़ा प्रतीत नहीं होता है।", + "Registration Required": "पंजीकरण आवश्यक", + "You need to register to do this. Would you like to register now?": "ऐसा करने के लिए आपको पंजीकरण करने की आवश्यकता है। क्या आप अभी पंजीकरण करना चाहते हैं?", + "Register": "पंजीकरण करें", + "Default": "डिफ़ॉल्ट", + "Restricted": "वर्जित", + "Moderator": "मध्यस्थ", + "Admin": "व्यवस्थापक", + "Start a chat": "एक चैट शुरू करें", + "Who would you like to communicate with?": "आप किसके साथ संवाद करना चाहते हैं?", + "Email, name or matrix ID": "ईमेल, नाम या मैट्रिक्स आईडी", + "Start Chat": "चैट शुरू करें", + "Invite new room members": "नए रूम के सदस्यों को आमंत्रित करें", + "Who would you like to add to this room?": "आप इस रूम में किसे जोड़ना चाहेंगे?", + "Send Invites": "आमंत्रण भेजें", + "Failed to invite user": "उपयोगकर्ता को आमंत्रित करने में विफल", + "Operation failed": "कार्रवाई विफल", + "Failed to invite": "आमंत्रित करने में विफल", + "Failed to invite the following users to the %(roomName)s room:": "निम्नलिखित उपयोगकर्ताओं को %(roomName)s रूम में आमंत्रित करने में विफल:", + "You need to be logged in.": "आपको लॉग इन करने की जरूरत है।", + "Unable to load! Check your network connectivity and try again.": "लोड नहीं किया जा सकता! अपनी नेटवर्क कनेक्टिविटी जांचें और पुनः प्रयास करें।", + "You need to be able to invite users to do that.": "आपको उपयोगकर्ताओं को ऐसा करने के लिए आमंत्रित करने में सक्षम होना चाहिए।", + "Unable to create widget.": "विजेट बनाने में असमर्थ।", + "Missing roomId.": "गुमशुदा रूम ID।", + "Failed to send request.": "अनुरोध भेजने में विफल।", + "This room is not recognised.": "यह रूम पहचाना नहीं गया है।", + "Power level must be positive integer.": "पावर स्तर सकारात्मक पूर्णांक होना चाहिए।", + "You are not in this room.": "आप इस रूम में नहीं हैं।", + "You do not have permission to do that in this room.": "आपको इस कमरे में ऐसा करने की अनुमति नहीं है।", + "Missing room_id in request": "अनुरोध में रूम_आईडी गुम है", + "Room %(roomId)s not visible": "%(roomId)s रूम दिखाई नहीं दे रहा है", + "Missing user_id in request": "अनुरोध में user_id गुम है", + "Usage": "प्रयोग", + "Searches DuckDuckGo for results": "परिणामों के लिए DuckDuckGo खोजें", + "/ddg is not a command": "/ddg एक कमांड नहीं है", + "To use it, just wait for autocomplete results to load and tab through them.": "इसका उपयोग करने के लिए, बस स्वत: पूर्ण परिणामों को लोड करने और उनके माध्यम से टैब के लिए प्रतीक्षा करें।", + "Changes your display nickname": "अपना प्रदर्शन उपनाम बदलता है", + "Changes colour scheme of current room": "वर्तमान कमरे की रंग योजना बदलता है", + "Sets the room topic": "कमरे के विषय सेट करता है", + "Invites user with given id to current room": "दिए गए आईडी के साथ उपयोगकर्ता को वर्तमान रूम में आमंत्रित करता है", + "Joins room with given alias": "दिए गए उपनाम के साथ रूम में शामिल हो जाता है", + "Leave room": "रूम छोड़ें", + "Unrecognised room alias:": "अपरिचित रूम उपनाम:", + "Kicks user with given id": "दिए गए आईडी के साथ उपयोगकर्ता को निर्वासन(किक) करता हैं", + "Bans user with given id": "दिए गए आईडी के साथ उपयोगकर्ता को प्रतिबंध लगाता है", + "Unbans user with given id": "दिए गए आईडी के साथ उपयोगकर्ता को अप्रतिबंधित करता हैं", + "Ignores a user, hiding their messages from you": "उपयोगकर्ता को अनदेखा करें और स्वयं से संदेश छुपाएं", + "Ignored user": "अनदेखा उपयोगकर्ता", + "You are now ignoring %(userId)s": "आप %(userId)s को अनदेखा कर रहे हैं", + "Stops ignoring a user, showing their messages going forward": "उपयोगकर्ता को अनदेखा करना बंद करें और एक संदेश प्रदर्शित करें", + "Unignored user": "अनदेखा बंद किया गया उपयोगकर्ता", + "You are no longer ignoring %(userId)s": "अब आप %(userId)s को अनदेखा नहीं कर रहे हैं", + "Define the power level of a user": "उपयोगकर्ता के पावर स्तर को परिभाषित करें", + "Deops user with given id": "दिए गए आईडी के साथ उपयोगकर्ता को देओप्स करना", + "Opens the Developer Tools dialog": "डेवलपर टूल्स संवाद खोलता है", + "Verifies a user, device, and pubkey tuple": "उपयोगकर्ता, डिवाइस और पबकी टुपल को सत्यापित करता है", + "Unknown (user, device) pair:": "अज्ञात (उपयोगकर्ता, डिवाइस) जोड़ी:", + "Device already verified!": "डिवाइस पहले ही सत्यापित है!", + "WARNING: Device already verified, but keys do NOT MATCH!": "चेतावनी: डिवाइस पहले ही सत्यापित है, लेकिन चाबियाँ मेल नहीं खाती हैं!", + "WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!": "चेतावनी: कुंजी सत्यापन विफल! %(userId)s और डिवाइस %(deviceId)s के लिए हस्ताक्षर कुंजी \"%(fprint)s\" है जो प्रदान की गई कुंजी \"%(fingerprint)s\" से मेल नहीं खाती है। इसका मतलब यह हो सकता है कि आपके संचार को अंतरग्रहण किया जा रहा है!", + "Verified key": "सत्यापित कुंजी", + "The signing key you provided matches the signing key you received from %(userId)s's device %(deviceId)s. Device marked as verified.": "आपके द्वारा प्रदान की गई हस्ताक्षर कुंजी %(userId)s के डिवाइस %(deviceId)s से प्राप्त हस्ताक्षर कुंजी से मेल खाती है। डिवाइस सत्यापित के रूप में चिह्नित किया गया है।", + "Displays action": "कार्रवाई प्रदर्शित करता है", + "Forces the current outbound group session in an encrypted room to be discarded": "एक एन्क्रिप्टेड रूम में मौजूदा आउटबाउंड समूह सत्र को त्यागने के लिए मजबूर करता है", + "Unrecognised command:": "अपरिचित आदेश:", + "Reason": "कारण", + "%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s ने %(displayName)s के लिए निमंत्रण को स्वीकार कर लिया है।", + "%(targetName)s accepted an invitation.": "%(targetName)s ने एक निमंत्रण स्वीकार कर लिया।", + "%(senderName)s requested a VoIP conference.": "%(senderName)s ने एक वीओआईपी सम्मेलन का अनुरोध किया।", + "%(senderName)s invited %(targetName)s.": "%(senderName)s ने %(targetName)s को आमंत्रित किया।", + "%(senderName)s banned %(targetName)s.": "%(senderName)s ने %(targetName)s को प्रतिबंधित किया।", + "%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s ने अपना प्रदर्शन नाम %(displayName)s में बदल दिया।", + "%(senderName)s set their display name to %(displayName)s.": "%(senderName)s अपना प्रदर्शन नाम %(displayName)s पर सेट किया।", + "%(senderName)s removed their display name (%(oldDisplayName)s).": "%(senderName)s ने अपना प्रदर्शन नाम हटा दिया (%(oldDisplayName)s)।", + "%(senderName)s removed their profile picture.": "%(senderName)s ने अपनी प्रोफाइल तस्वीर हटा दी।", + "%(senderName)s changed their profile picture.": "%(senderName)s ने अपनी प्रोफाइल तस्वीर बदल दी।", + "%(senderName)s set a profile picture.": "%(senderName)s ने प्रोफाइल तस्वीर सेट कया।", + "VoIP conference started.": "वीओआईपी सम्मेलन शुरू हुआ।", + "%(targetName)s joined the room.": "%(targetName)s रूम में शामिल हो गया।", + "VoIP conference finished.": "वीओआईपी सम्मेलन समाप्त हो गया।", + "%(targetName)s rejected the invitation.": "%(targetName)s ने निमंत्रण को खारिज कर दिया।", + "%(targetName)s left the room.": "%(targetName)s ने रूम छोर दिया।", + "%(senderName)s unbanned %(targetName)s.": "%(senderName)s ने %(targetName)s को अप्रतिबंधित कर दिया।", + "%(senderName)s kicked %(targetName)s.": "%(senderName)s ने %(targetName)s को किक कर दिया।", + "%(senderName)s withdrew %(targetName)s's invitation.": "%(senderName)s ने %(targetName)s की निमंत्रण वापस ले लिया।", + "%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s ने विषय को \"%(topic)s\" में बदल दिया।", + "%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s ने रूम का नाम हटा दिया।", + "%(senderDisplayName)s changed the room name to %(roomName)s.": "%(senderDisplayName)s कमरे का नाम बदलकर %(roomName)s कर दिया।", + "%(senderDisplayName)s sent an image.": "%(senderDisplayName)s ने एक छवि भेजी।", + "%(senderName)s added %(count)s %(addedAddresses)s as addresses for this room.|other": "%(senderName)s ने इस रूम के लिए पते के रूप में %(addedAddresses)s को जोड़ा।", + "%(senderName)s added %(count)s %(addedAddresses)s as addresses for this room.|one": "%(senderName)s ने इस रूम के लिए एक पते के रूप में %(addedAddresses)s को जोड़ा।", + "%(senderName)s removed %(count)s %(removedAddresses)s as addresses for this room.|other": "%(senderName)s ने इस कमरे के लिए पते के रूप में %(removedAddresses)s को हटा दिया।", + "%(senderName)s removed %(count)s %(removedAddresses)s as addresses for this room.|one": "%(senderName)s ने इस कमरे के लिए एक पते के रूप में %(removedAddresses)s को हटा दिया।", + "%(senderName)s added %(addedAddresses)s and removed %(removedAddresses)s as addresses for this room.": "%(senderName)s ने इस कमरे के लिए पते के रूप में %(addedAddresses)s को जोड़ा और %(removedAddresses)s को हटा दिया।", + "%(senderName)s set the main address for this room to %(address)s.": "%(senderName)s ने इस कमरे के लिए मुख्य पता %(address)s पर सेट किया।", + "%(senderName)s removed the main address for this room.": "%(senderName)s ने इस कमरे के लिए मुख्य पता हटा दिया।", + "Someone": "कोई", + "(not supported by this browser)": "(इस ब्राउज़र द्वारा समर्थित नहीं है)", + "%(senderName)s answered the call.": "%(senderName)s ने कॉल का जवाब दिया।", + "(could not connect media)": "(मीडिया कनेक्ट नहीं कर सका)", + "(no answer)": "(कोई जवाब नहीं)", + "(unknown failure: %(reason)s)": "(अज्ञात विफलता: %(reason)s)", + "%(senderName)s ended the call.": "%(senderName)s ने कॉल समाप्त कर दिया।", + "%(senderName)s placed a %(callType)s call.": "%(senderName)s ने %(callType)s कॉल रखा।", + "%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.": "%(senderName)s रूम में शामिल होने के लिए %(targetDisplayName)s को निमंत्रण भेजा।", + "%(senderName)s made future room history visible to all room members, from the point they are invited.": "%(senderName)s ने भविष्य के रूम का इतिहास सभी रूम के सदस्यों के लिए प्रकाशित कर दिया जिस बिंदु से उन्हें आमंत्रित किया गया था।", + "%(senderName)s made future room history visible to all room members, from the point they joined.": "%(senderName)s ने भविष्य के रूम का इतिहास सभी रूम के सदस्यों के लिए दृश्यमान किया, जिस बिंदु में वे शामिल हुए थे।", + "%(senderName)s made future room history visible to all room members.": "%(senderName)s ने भविष्य के रूम का इतिहास सभी रूम के सदस्यों के लिए दृश्यमान बना दिया।", + "%(senderName)s made future room history visible to anyone.": "%(senderName)s ने भविष्य के रूम का इतिहास हर किसी के लिए दृश्यमान बना दिया।", + "%(senderName)s made future room history visible to unknown (%(visibility)s).": "%(senderName)s ने भविष्य के रूम का इतिहास अज्ञात (%(visibility)s) के लिए दृश्यमान बनाया।", + "%(senderName)s turned on end-to-end encryption (algorithm %(algorithm)s).": "%(senderName)s ने एंड-टू-एंड एन्क्रिप्शन (एल्गोरिदम %(algorithm)s) चालू कर दिया।", + "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s का %(fromPowerLevel)s से %(toPowerLevel)s", + "%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s ने %(powerLevelDiffText)s के पावर स्तर को बदल दिया।", + "%(senderName)s changed the pinned messages for the room.": "%(senderName)s ने रूम के लिए पिन किए गए संदेश को बदल दिया।", + "%(widgetName)s widget modified by %(senderName)s": "%(widgetName)s विजेट %(senderName)s द्वारा संशोधित", + "%(widgetName)s widget added by %(senderName)s": "%(widgetName)s विजेट %(senderName)s द्वारा जोड़ा गया", + "%(widgetName)s widget removed by %(senderName)s": "%(widgetName)s विजेट %(senderName)s द्वारा हटा दिया गया", + "%(displayName)s is typing": "%(displayName)s टाइप कर रहा है", + "%(names)s and %(count)s others are typing|other": "%(names)s और %(count)s अन्य टाइप कर रहे हैं", + "%(names)s and %(count)s others are typing|one": "%(names)s और एक दूसरा व्यक्ति टाइप कर रहे हैं", + "%(names)s and %(lastPerson)s are typing": "%(names)s और %(lastPerson)s टाइप कर रहे हैं", + "Failure to create room": "रूम बनाने में विफलता", + "Server may be unavailable, overloaded, or you hit a bug.": "सर्वर अनुपलब्ध, अधिभारित हो सकता है, या अपने एक सॉफ्टवेयर गर्बरी को पाया।", + "Send anyway": "वैसे भी भेजें", + "Send": "भेजें", + "Unnamed Room": "अनाम रूम", + "This homeserver has hit its Monthly Active User limit.": "इस होमसर्वर ने अपनी मासिक सक्रिय उपयोगकर्ता सीमा को प्राप्त कर लिया हैं।", + "This homeserver has exceeded one of its resource limits.": "यह होम सर्वर अपनी संसाधन सीमाओं में से एक से अधिक हो गया है।", + "Please contact your service administrator to continue using the service.": "सेवा का उपयोग जारी रखने के लिए कृपया अपने सेवा व्यवस्थापक से संपर्क करें ।", + "Unable to connect to Homeserver. Retrying...": "होमसर्वर से कनेक्ट करने में असमर्थ। पुनः प्रयास किया जा रहा हैं...", + "Your browser does not support the required cryptography extensions": "आपका ब्राउज़र आवश्यक क्रिप्टोग्राफी एक्सटेंशन का समर्थन नहीं करता है", + "Not a valid Riot keyfile": "यह एक वैध रायट कीकुंजी नहीं है", + "Authentication check failed: incorrect password?": "प्रमाणीकरण जांच विफल: गलत पासवर्ड?", + "Sorry, your homeserver is too old to participate in this room.": "क्षमा करें, इस रूम में भाग लेने के लिए आपका होमसर्वर बहुत पुराना है।", + "Please contact your homeserver administrator.": "कृपया अपने होमसर्वर व्यवस्थापक से संपर्क करें।", + "Failed to join room": "रूम में शामिल होने में विफल", + "Message Pinning": "संदेश पिनिंग", + "Increase performance by only loading room members on first view": "पहले दृश्य पर केवल कमरे के सदस्यों को लोड करके प्रदर्शन बढ़ाएं", + "Backup of encryption keys to server": "सर्वर पर एन्क्रिप्शन कुंजी का बैकअप", + "Disable Emoji suggestions while typing": "टाइप करते समय इमोजी सुझाव अक्षम करें", + "Use compact timeline layout": "कॉम्पैक्ट टाइमलाइन लेआउट का प्रयोग करें", + "Hide removed messages": "हटाए गए संदेशों को छुपाएं", + "Hide join/leave messages (invites/kicks/bans unaffected)": "शामिल होने/छोड़ने के सन्देश छुपाएं (आमंत्रित / किक/ प्रतिबंध अप्रभावित)", + "Hide avatar changes": "अवतार परिवर्तन छुपाएं", + "Hide display name changes": "प्रदर्शन नाम परिवर्तन छुपाएं", + "Hide read receipts": "पढ़ी रसीदें छुपाएं", + "Show timestamps in 12 hour format (e.g. 2:30pm)": "१२ घंटे प्रारूप में टाइमस्टैम्प दिखाएं (उदहारण:२:३० अपराह्न बजे)", + "Always show message timestamps": "हमेशा संदेश टाइमस्टैम्प दिखाएं", + "Autoplay GIFs and videos": "जीआईएफ और वीडियो को स्वत: प्ले करें", + "Always show encryption icons": "हमेशा एन्क्रिप्शन आइकन दिखाएं", + "Enable automatic language detection for syntax highlighting": "वाक्यविन्यास हाइलाइटिंग के लिए स्वत: भाषा का पता प्रणाली सक्षम करें", + "Hide avatars in user and room mentions": "उपयोगकर्ता और रूम के उल्लेखों में अवतार छुपाएं", + "Disable big emoji in chat": "बातचीत में बड़ा इमोजी अक्षम करें", + "Don't send typing notifications": "टाइपिंग नोटिफिकेशन न भेजें", + "Automatically replace plain text Emoji": "स्वचालित रूप से सादा पाठ इमोजी को प्रतिस्थापित करें", + "Mirror local video feed": "स्थानीय वीडियो फ़ीड को आईना करें", + "Disable Community Filter Panel": "सामुदायिक फ़िल्टर पैनल अक्षम करें", + "Disable Peer-to-Peer for 1:1 calls": "१:१ कॉल के लिए पीयर-टू-पीयर अक्षम करें", + "Send analytics data": "विश्लेषण डेटा भेजें", + "Never send encrypted messages to unverified devices from this device": "इस डिवाइस से असत्यापित डिवाइस पर एन्क्रिप्टेड संदेश कभी न भेजें", + "Never send encrypted messages to unverified devices in this room from this device": "इस डिवाइस से असत्यापित डिवाइस पर एन्क्रिप्टेड संदेश कभी न भेजें", + "Enable inline URL previews by default": "डिफ़ॉल्ट रूप से इनलाइन यूआरएल पूर्वावलोकन सक्षम करें", + "Enable URL previews for this room (only affects you)": "इस रूम के लिए यूआरएल पूर्वावलोकन सक्षम करें (केवल आपको प्रभावित करता है)", + "Enable URL previews by default for participants in this room": "इस रूम में प्रतिभागियों के लिए डिफ़ॉल्ट रूप से यूआरएल पूर्वावलोकन सक्षम करें", + "Room Colour": "रूम का रंग", + "Pin rooms I'm mentioned in to the top of the room list": "रूम की सूची के शीर्ष पर पिन रूम का उल्लेख करें", + "Pin unread rooms to the top of the room list": "रूम की सूची के शीर्ष पर अपठित रूम पिन करें", + "Enable widget screenshots on supported widgets": "समर्थित विजेट्स पर विजेट स्क्रीनशॉट सक्षम करें", + "Show empty room list headings": "खाली रूम सूची शीर्षलेख दिखाएं", + "Show developer tools": "डेवलपर टूल दिखाएं", + "Collecting app version information": "ऐप संस्करण जानकारी एकत्रित कर रहा हैं", + "Collecting logs": "लॉग एकत्रित कर रहा हैं", + "Uploading report": "रिपोर्ट अपलोड हो रहा है", + "Waiting for response from server": "सर्वर से प्रतिक्रिया की प्रतीक्षा कर रहा है", + "Messages containing my display name": "मेरे प्रदर्शन नाम वाले संदेश", + "Messages containing my user name": "मेरे उपयोगकर्ता नाम युक्त संदेश", + "Messages in one-to-one chats": "एक-से-एक चैट में संदेश", + "Messages in group chats": "समूह चैट में संदेश", + "When I'm invited to a room": "जब मुझे एक रूम में आमंत्रित किया जाता है", + "Call invitation": "कॉल आमंत्रण", + "Messages sent by bot": "रोबॉट द्वारा भेजे गए संदेश", + "Active call (%(roomName)s)": "सक्रिय कॉल (%(roomName)s)", + "unknown caller": "अज्ञात फ़ोन करने वाला", + "Incoming voice call from %(name)s": "%(name)s से आने वाली ध्वनि कॉल", + "Incoming video call from %(name)s": "%(name)s से आने वाली वीडियो कॉल", + "Incoming call from %(name)s": "%(name)s से आने वाली कॉल", + "Decline": "पतन", + "Accept": "स्वीकार", + "Error": "त्रुटि", + "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains": "एक टेक्स्ट संदेश %(msisdn)s को भेजा गया है। कृपया इसमें सत्यापन कोड दर्ज करें", + "Incorrect verification code": "गलत सत्यापन कोड", + "Enter Code": "कोड दर्ज करें", + "Submit": "जमा करें", + "Phone": "फ़ोन", + "Add phone number": "फोन नंबर डालें", + "Add": "जोड़े", + "Failed to upload profile picture!": "प्रोफाइल तस्वीर अपलोड करने में विफल!", + "Upload new:": "नया अपलोड करें:", + "No display name": "कोई प्रदर्शन नाम नहीं", + "New passwords don't match": "नए पासवर्ड मेल नहीं खाते हैं", + "Passwords can't be empty": "पासवर्ड खाली नहीं हो सकते हैं", + "Warning!": "चेतावनी!", + "Changing password will currently reset any end-to-end encryption keys on all devices, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "पासवर्ड बदलना वर्तमान में सभी उपकरणों पर किसी भी एंड-टू-एंड एन्क्रिप्शन कुंजी को रीसेट कर देगा, एन्क्रिप्टेड चैट इतिहास को अपठनीय बनायेगा, जब तक कि आप पहले अपनी रूम कुंजियां निर्यात न करें और बाद में उन्हें फिर से आयात न करें। भविष्य में यह सुधार होगा।", + "Export E2E room keys": "E2E रूम कुंजी निर्यात करें", + "Do you want to set an email address?": "क्या आप एक ईमेल पता सेट करना चाहते हैं?", + "Current password": "वर्तमान पासवर्ड", + "Password": "पासवर्ड", + "New Password": "नया पासवर्ड", + "Confirm password": "पासवर्ड की पुष्टि कीजिये", + "Change Password": "पासवर्ड बदलें", + "Your home server does not support device management.": "आपका होम सर्वर डिवाइस प्रबंधन का समर्थन नहीं करता है।", + "Unable to load device list": "डिवाइस सूची लोड करने में असमर्थ", + "Authentication": "प्रमाणीकरण", + "Delete %(count)s devices|other": "%(count)s यंत्र हटाएं", + "Delete %(count)s devices|one": "यंत्र हटाएं", + "Device ID": "यंत्र आईडी", + "Device Name": "यंत्र का नाम", + "Last seen": "अंतिम बार देखा गया", + "Select devices": "यंत्रो का चयन करें", + "Failed to set display name": "प्रदर्शन नाम सेट करने में विफल", + "Disable Notifications": "नोटीफिकेशन निष्क्रिय करें", + "Enable Notifications": "सूचनाएं सक्षम करें", + "Delete Backup": "बैकअप हटाएं", + "Delete your backed up encryption keys from the server? You will no longer be able to use your recovery key to read encrypted message history": "सर्वर से अपनी बैक अप एन्क्रिप्शन कुंजी हटाएं? एन्क्रिप्टेड संदेश इतिहास पढ़ने के लिए अब आप अपनी रिकवरी कुंजी का उपयोग नहीं कर पाएंगे", + "Delete backup": "बैकअप हटाएं", + "Unable to load key backup status": "कुंजी बैकअप स्थिति लोड होने में असमर्थ", + "This device is uploading keys to this backup": "यह यंत्र इस बैकअप में कुंजी अपलोड कर रहा है", + "This device is not uploading keys to this backup": "यह यंत्र बैकअप में कुंजी अपलोड नहीं कर रहा है", + "Backup has a valid signature from this device": "इस डिवाइस से बैकअप में वैध हस्ताक्षर है", + "Backup has a valid signature from verified device x": "सत्यापित डिवाइस x से बैकअप में मान्य हस्ताक्षर है", + "Backup has a valid signature from unverified device ": "असत्यापित डिवाइस से बैकअप में मान्य हस्ताक्षर है", + "Backup has an invalid signature from verified device ": "सत्यापित डिवाइस से बैकअप में अमान्य हस्ताक्षर है", + "Backup has an invalid signature from unverified device ": "असत्यापित डिवाइस से बैकअप में अमान्य हस्ताक्षर है", + "Verify...": "सत्यापित करें ...", + "Backup is not signed by any of your devices": "बैकअप आपके किसी भी डिवाइस द्वारा हस्ताक्षरित नहीं है", + "Backup version: ": "बैकअप संस्करण: ", + "Algorithm: ": "कलन विधि: ", + "Restore backup": "बैकअप बहाल करें", + "No backup is present": "कोई बैकअप प्रस्तुत नहीं है", + "Start a new backup": "एक नया बैकअप शुरू करें", + "Error saving email notification preferences": "ईमेल अधिसूचना प्राथमिकताओं को सहेजने में त्रुटि", + "An error occurred whilst saving your email notification preferences.": "आपकी ईमेल अधिसूचना वरीयताओं को सहेजते समय एक त्रुटि हुई।", + "Keywords": "कीवर्ड", + "Enter keywords separated by a comma:": "अल्पविराम से अलग करके कीवर्ड दर्ज करें:", + "OK": "ठीक", + "Failed to change settings": "सेटिंग्स बदलने में विफल", + "Can't update user notification settings": "उपयोगकर्ता अधिसूचना सेटिंग्स अद्यतन नहीं कर सकते हैं", + "Failed to update keywords": "कीवर्ड अपडेट करने में विफल", + "Messages containing keywords": "कीवर्ड युक्त संदेश", + "Notify for all other messages/rooms": "अन्य सभी संदेशों/रूम के लिए सूचित करें", + "Notify me for anything else": "मुझे किसी और चीज़ के लिए सूचित करें", + "Enable notifications for this account": "इस खाते के लिए अधिसूचनाएं सक्षम करें", + "All notifications are currently disabled for all targets.": "सभी सूचनाएं वर्तमान में सभी लक्ष्यों के लिए अक्षम हैं।", + "Add an email address above to configure email notifications": "ईमेल अधिसूचनाओं को कॉन्फ़िगर करने के लिए उपरोक्त एक ईमेल पता जोड़ें", + "Enable email notifications": "ईमेल अधिसूचनाएं सक्षम करें", + "Notifications on the following keywords follow rules which can’t be displayed here:": "निम्नलिखित कीवर्ड पर अधिसूचनाएं नियमों का पालन करती हैं जिन्हें यहां प्रदर्शित नहीं किया जा सकता है:", + "Unable to fetch notification target list": "अधिसूचना लक्ष्य सूची लाने में असमर्थ", + "Notification targets": "अधिसूचना के लक्ष्य", + "Advanced notification settings": "उन्नत अधिसूचना सेटिंग्स", + "There are advanced notifications which are not shown here": "उन्नत सूचनाएं हैं जो यहां दिखाई नहीं दी गई हैं" } diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index 9d0589bb179..6b69512d7bb 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -1302,5 +1302,113 @@ "Pin unread rooms to the top of the room list": "Nem olvasott üzeneteket tartalmazó szobák a szobalista elejére", "Pin rooms I'm mentioned in to the top of the room list": "Megemlítéseket tartalmazó szobák a szobalista elejére", "If you would like to create a Matrix account you can register now.": "Ha létre szeretnél hozni egy Matrix fiókot most regisztrálhatsz.", - "You are currently using Riot anonymously as a guest.": "A Riotot ismeretlen vendégként használod." + "You are currently using Riot anonymously as a guest.": "A Riotot ismeretlen vendégként használod.", + "Please review and accept all of the homeserver's policies": "Kérlek nézd át és fogadd el a Matrix szerver felhasználási feltételeit", + "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": "Hogy a régi üzenetekhez továbbra is hozzáférhess kijelentkezés előtt ki kell mentened a szobák titkosító kulcsait. Ehhez a Riot egy frissebb verzióját kell használnod", + "You've previously used a newer version of Riot on %(host)s. To use this version again with end to end encryption, you will need to sign out and back in again. ": "Előzőleg a Riot egy frissebb verzióját használtad itt: %(host)s. Ki-, és vissza kell jelentkezned, hogy megint ezt a verziót használhasd végponttól végpontig titkosításhoz. ", + "Incompatible Database": "Nem kompatibilis adatbázis", + "Continue With Encryption Disabled": "Folytatás a titkosítás kikapcsolásával", + "Sign in with single sign-on": "Bejelentkezés „egyszeri bejelentkezéssel”", + "Unable to load! Check your network connectivity and try again.": "A betöltés sikertelen! Ellenőrizd a hálózati kapcsolatot és próbáld újra.", + "Backup of encryption keys to server": "Titkosítási kulcsok mentése a szerverre", + "Delete Backup": "Mentés törlése", + "Delete your backed up encryption keys from the server? You will no longer be able to use your recovery key to read encrypted message history": "Törlöd az elmentett titkosítási kulcsokat a szerverről? Később nem tudod használni helyreállítási kulcsot a régi titkosított üzenetek elolvasásához", + "Delete backup": "Mentés törlése", + "Unable to load key backup status": "A mentett kulcsok állapotát nem lehet lekérdezni", + "This device is uploading keys to this backup": "Ez az eszköz kulcsokat tölt fel ebbe a mentésbe", + "This device is not uploading keys to this backup": "Ez az eszköz nem tölt fel kulcsokat ebbe a mentésbe", + "Backup has a valid signature from this device": "A mentés érvényes aláírást tartalmaz az eszközről", + "Backup has a valid signature from verified device x": "A mentés érvényes aláírást tartalmaz erről az ellenőrzött eszközről: x", + "Backup has a valid signature from unverified device ": "A mentés érvényes aláírást tartalmaz erről az ellenőrizetlen eszközről: ", + "Backup has an invalid signature from verified device ": "A mentés érvénytelen aláírást tartalmaz erről az ellenőrzött eszközről: ", + "Backup has an invalid signature from unverified device ": "A mentés érvénytelen aláírást tartalmaz erről az ellenőrizetlen eszközről: ", + "Backup is not signed by any of your devices": "A mentés nincs aláírva egyetlen eszközöd által sem", + "Backup version: ": "Mentés verzió: ", + "Algorithm: ": "Algoritmus: ", + "Restore backup": "Mentés visszaállítása", + "No backup is present": "Mentés nem található", + "Start a new backup": "Új mentés indítása", + "Secure your encrypted message history with a Recovery Passphrase.": "Helyezd biztonságba a titkosított üzenetek olvasásának a lehetőségét a Helyreállítási jelmondattal.", + "You'll need it if you log out or lose access to this device.": "Szükséged lesz rá ha kijelentkezel vagy nem férsz többé hozzá az eszközödhöz.", + "Enter a passphrase...": "Add meg a jelmondatot...", + "Next": "Következő", + "If you don't want encrypted message history to be availble on other devices, .": "Ha nincs szükséged a régi titkosított üzenetekre más eszközön, .", + "Or, if you don't want to create a Recovery Passphrase, skip this step and .": "Vagy, ha nem szeretnél Helyreállítási jelmondatot megadni, hagyd ki ezt a lépést és .", + "That matches!": "Egyeznek!", + "That doesn't match.": "Nem egyeznek.", + "Go back to set it again.": "Lépj vissza és állítsd be újra.", + "Type in your Recovery Passphrase to confirm you remember it. If it helps, add it to your password manager or store it somewhere safe.": "Add meg a Helyreállítási jelmondatot, hogy bizonyítsd, hogy emlékszel rá. Ha az segít írd be a jelszó menedzseredbe vagy tárold más biztonságos helyen.", + "Repeat your passphrase...": "Ismételd meg a jelmondatot...", + "Make a copy of this Recovery Key and keep it safe.": "Készíts másolatot a Helyreállítási kulcsból és tárold biztonságos helyen.", + "As a safety net, you can use it to restore your encrypted message history if you forget your Recovery Passphrase.": "Mint egy biztonsági háló, ha elfelejted a Helyreállítási jelmondatot felhasználhatod, hogy hozzáférj a régi titkosított üzeneteidhez.", + "Your Recovery Key": "A Helyreállítási kulcsod", + "Copy to clipboard": "Másolás a vágólapra", + "Download": "Letölt", + "I've made a copy": "Készítettem másolatot", + "Your Recovery Key has been copied to your clipboard, paste it to:": "A Helyreállítási kulcsod a vágólapra lett másolva, beillesztés ide:", + "Your Recovery Key is in your Downloads folder.": "A Helyreállítási kulcs a Letöltések mappádban van.", + "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", + "Got it": "Értem", + "Backup created": "Mentés elkészült", + "Your encryption keys are now being backed up to your Homeserver.": "A titkosítási kulcsaid a Matrix szervereden vannak elmentve.", + "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", + "Create a Recovery Passphrase": "Helyreállítási jelmondat megadása", + "Confirm Recovery Passphrase": "Helyreállítási jelmondat megerősítése", + "Recovery Key": "Helyreállítási kulcs", + "Keep it safe": "Tartsd biztonságban", + "Backing up...": "Mentés...", + "Create Key Backup": "Kulcs mentés készítése", + "Unable to create key backup": "Kulcs mentés sikertelen", + "Retry": "Újra", + "Unable to load backup status": "A mentés állapotát nem lehet lekérdezni", + "Unable to restore backup": "A mentést nem lehet visszaállítani", + "No backup found!": "Mentés nem található!", + "Backup Restored": "Mentés visszaállítva", + "Failed to decrypt %(failedCount)s sessions!": "%(failedCount)s kapcsolatot nem lehet visszafejteni!", + "Restored %(sessionCount)s session keys": "%(sessionCount)s kapcsolati kulcsok visszaállítva", + "Enter Recovery Passphrase": "Add meg a Helyreállítási jelmondatot", + "Access your secure message history and set up secure messaging by entering your recovery passphrase.": "A helyreállítási jelmondattal hozzáférsz a régi titkosított üzeneteidhez és beállíthatod a biztonságos üzenetküldést.", + "If you've forgotten your recovery passphrase you can use your recovery key or set up new recovery options": "Ha elfelejtetted a helyreállítási jelmondatodat használhatod a helyreállítási kulcsodat vagy új helyreállítási paramétereket állíthatsz be", + "Enter Recovery Key": "Add meg a Helyreállítási kulcsot", + "This looks like a valid recovery key!": "Ez érvényes helyreállítási kulcsnak tűnik!", + "Not a valid recovery key": "Nem helyreállítási kulcs", + "Access your secure message history and set up secure messaging by entering your recovery key.": "A helyreállítási kulcs megadásával hozzáférhetsz a régi biztonságos üzeneteidhez és beállíthatod a biztonságos üzenetküldést.", + "If you've forgotten your recovery passphrase you can ": "Ha elfelejtetted a helyreállítási jelmondatot ", + "Key Backup": "Kulcs mentés", + "Failed to perform homeserver discovery": "A Matrix szerver felderítése sikertelen", + "Invalid homeserver discovery response": "A Matrix szerver felderítésére kapott válasz érvénytelen", + "Cannot find homeserver": "Matrix szerver nem található", + "File is too big. Maximum file size is %(fileSize)s": "A fájl túl nagy. A maximális fájl méret: %(fileSize)s", + "The following files cannot be uploaded:": "Az alábbi fájlokat nem lehetett feltölteni:", + "Use a few words, avoid common phrases": "Néhány szót használj és kerüld el a szokásos szövegeket", + "No need for symbols, digits, or uppercase letters": "Nincs szükség szimbólumokra, számokra vagy nagy betűkre", + "Use a longer keyboard pattern with more turns": "Használj hosszabb billentyűzet mintát több kanyarral", + "Avoid repeated words and characters": "Kerüld a szó-, vagy betűismétlést", + "Avoid sequences": "Kerüld a sorozatokat", + "Avoid recent years": "Kerüld a közeli éveket", + "Avoid years that are associated with you": "Kerüld azokat az éveket amik összefüggésbe hozhatók veled", + "Avoid dates and years that are associated with you": "Kerüld a dátumokat és évszámokat amik összefüggésbe hozhatók veled", + "Capitalization doesn't help very much": "A nagybetűk nem igazán segítenek", + "All-uppercase is almost as easy to guess as all-lowercase": "A csupa nagybetűset majdnem olyan könnyű kitalálni mint a csupa kisbetűset", + "Reversed words aren't much harder to guess": "A megfordított betűrendet sem sokkal nehezebb kitalálni", + "Predictable substitutions like '@' instead of 'a' don't help very much": "Megjósolható helyettesítések mint az „a” helyett a „@” nem sokat segítenek", + "Add another word or two. Uncommon words are better.": "Adj hozzá még egy-két szót. A ritkán használt szavak jobbak.", + "Repeats like \"aaa\" are easy to guess": "Ismétlések mint az „aaa” könnyen kitalálhatók", + "Repeats like \"abcabcabc\" are only slightly harder to guess than \"abc\"": "Az „abcabcabc” sorozatot csak kicsivel nehezebb kitalálni mint az „abc”-t", + "Sequences like abc or 6543 are easy to guess": "Az olyan mint az abc vagy 6543 sorokat könnyű kitalálni", + "Recent years are easy to guess": "A közelmúlt évszámait könnyű kitalálni", + "Dates are often easy to guess": "Általában a dátumokat könnyű kitalálni", + "This is a top-10 common password": "Ez benne van a 10 legelterjedtebb jelszó listájában", + "This is a top-100 common password": "Ez benne van a 100 legelterjedtebb jelszó listájában", + "This is a very common password": "Ez egy nagyon gyakori jelszó", + "This is similar to a commonly used password": "Ez nagyon hasonlít egy gyakori jelszóhoz", + "A word by itself is easy to guess": "Egy szót magában könnyű kitalálni", + "Names and surnames by themselves are easy to guess": "Neveket egymagukban könnyű kitalálni", + "Common names and surnames are easy to guess": "Elterjedt neveket könnyű kitalálni", + "Great! This passphrase looks strong enough.": "Szuper! Ez a jelmondat elég erősnek látszik.", + "As a safety net, you can use it to restore your encrypted message history.": "Használhatod egy biztonsági hálóként a titkosított üzenetek visszaállításához.", + "Failed to load group members": "A közösség tagságokat nem sikerült betölteni" } diff --git a/src/i18n/strings/pl.json b/src/i18n/strings/pl.json index 045b04cc948..705442e7bf7 100644 --- a/src/i18n/strings/pl.json +++ b/src/i18n/strings/pl.json @@ -515,7 +515,7 @@ "You cannot place VoIP calls in this browser.": "Nie możesz przeprowadzić rozmowy głosowej VoIP w tej przeglądarce.", "You do not have permission to post to this room": "Nie jesteś uprawniony do pisania w tym pokoju", "You have been banned from %(roomName)s by %(userName)s.": "Zostałeś permanentnie usunięty z pokoju %(roomName)s przez %(userName)s.", - "You have been invited to join this room by %(inviterName)s": "Zostałeś zaproszony do dołączenia do tego pokoju przez %(inviterName)s", + "You have been invited to join this room by %(inviterName)s": "Zostałeś(-aś) zaproszony(-a) do dołączenia do tego pokoju przez %(inviterName)s", "You have been kicked from %(roomName)s by %(userName)s.": "Zostałeś usunięty z %(roomName)s przez %(userName)s.", "You have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, sign in again on each device": "Wylogowałeś się ze wszystkich urządzeń i nie będziesz już otrzymywał powiadomień push. Aby ponownie aktywować powiadomienia zaloguj się ponownie na każdym urządzeniu", "You have disabled URL previews by default.": "Masz domyślnie wyłączone podglądy linków.", @@ -627,7 +627,7 @@ "Tried to load a specific point in this room's timeline, but was unable to find it.": "Próbowano załadować konkretny punkt na osi czasu w tym pokoju, ale nie nie można go znaleźć.", "The exported file will allow anyone who can read it to decrypt any encrypted messages that you can see, so you should be careful to keep it secure. To help with this, you should enter a passphrase below, which will be used to encrypt the exported data. It will only be possible to import the data by using the same passphrase.": "Wyeksportowany plik pozwoli każdej osobie będącej w stanie go odczytać na deszyfrację jakichkolwiek zaszyfrowanych wiadomości, które możesz zobaczyć, tak więc zalecane jest zachowanie ostrożności. Aby w tym pomóc, powinieneś/aś wpisać hasło poniżej; hasło to będzie użyte do zaszyfrowania wyeksportowanych danych. Późniejsze zaimportowanie tych danych będzie możliwe tylko po uprzednim podaniu owego hasła.", " (unsupported)": " (niewspierany)", - "Idle": "Bezczynny", + "Idle": "Bezczynny(-a)", "Check for update": "Sprawdź aktualizacje", "%(senderDisplayName)s changed the room avatar to ": "%(senderDisplayName)s zmienił(a) awatar pokoju na ", "%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s usunął(-ęła) awatar pokoju.", @@ -743,7 +743,7 @@ "Loading...": "Ładowanie...", "Pinned Messages": "Przypięte Wiadomości", "Online for %(duration)s": "Online przez %(duration)s", - "Idle for %(duration)s": "Nieaktywny przez %(duration)s", + "Idle for %(duration)s": "Bezczynny(-a) przez %(duration)s", "Offline for %(duration)s": "Offline przez %(duration)s", "Unknown for %(duration)s": "Nieznany przez %(duration)s", "Unknown": "Nieznany", @@ -1207,5 +1207,31 @@ "Clear cache and resync": "Wyczyść pamięć podręczną i zsynchronizuj ponownie", "Riot now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!": "Riot używa teraz 3-5x mniej pamięci, ładując informacje o innych użytkownikach tylko wtedy, gdy jest to konieczne. Poczekaj, aż ponownie zsynchronizujemy się z serwerem!", "If the other version of Riot is still open in another tab, please close it as using Riot on the same host with both lazy loading enabled and disabled simultaneously will cause issues.": "Jeśli inna wersja Riot jest nadal otwarta w innej zakładce, proszę zamknij ją, ponieważ używanie Riot na tym samym komputerze z włączonym i wyłączonym jednocześnie leniwym ładowaniem będzie powodować problemy.", - "And %(count)s more...|other": "I %(count)s więcej…" + "And %(count)s more...|other": "I %(count)s więcej…", + "Delete Backup": "Usuń Kopię Zapasową", + "Delete backup": "Usuń Kopię Zapasową", + "Unable to load! Check your network connectivity and try again.": "Nie można załadować! Sprawdź połączenie sieciowe i spróbuj ponownie.", + "Algorithm: ": "Algorytm: ", + "Pin unread rooms to the top of the room list": "Przypnij nieprzeczytanie pokoje na górę listy pokojów", + "Use a few words, avoid common phrases": "Użyj kilku słów, unikaj typowych zwrotów", + "Avoid repeated words and characters": "Unikaj powtarzających się słów i znaków", + "Avoid sequences": "Unikaj sekwencji", + "Avoid recent years": "Unikaj ostatnich lat", + "Avoid years that are associated with you": "Unikaj lat, które są z tobą związane z Tobą", + "Avoid dates and years that are associated with you": "Unikaj dat i lat, które są z tobą związane z Tobą", + "Add another word or two. Uncommon words are better.": "Dodaj kolejne słowo lub dwa. Niezwykłe słowa są lepsze.", + "Recent years are easy to guess": "Ostatnie lata są łatwe do odgadnięcia", + "Dates are often easy to guess": "Daty są często łatwe do odgadnięcia", + "This is a very common password": "To jest bardzo popularne hasło", + "Backup version: ": "Wersja kopii zapasowej: ", + "Restore backup": "Przywróć kopię zapasową", + "Room version number: ": "Numer wersji pokoju: ", + "Reversed words aren't much harder to guess": "Odwrócone słowa nie są trudniejsze do odgadnięcia", + "Predictable substitutions like '@' instead of 'a' don't help very much": "Przewidywalne podstawienia, takie jak \"@\" zamiast \"a\", nie pomagają zbytnio", + "Repeats like \"aaa\" are easy to guess": "Powtórzenia takie jak \"aaa\" są łatwe do odgadnięcia", + "Repeats like \"abcabcabc\" are only slightly harder to guess than \"abc\"": "Powtórzenia takie jak \"abcabcabc\" są tylko trochę trudniejsze do odgadnięcia niż \"abc\"", + "Sequences like abc or 6543 are easy to guess": "Sekwencje takie jak abc lub 6543 są łatwe do odgadnięcia", + "A word by itself is easy to guess": "Samo słowo jest łatwe do odgadnięcia", + "Names and surnames by themselves are easy to guess": "Imiona i nazwiska same w sobie są łatwe do odgadnięcia", + "Common names and surnames are easy to guess": "Popularne imiona i nazwiska są łatwe do odgadnięcia" } diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index 12a30ef657d..5671184e0af 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -1,32 +1,32 @@ { "This email address is already in use": "Kjo adresë email është tashmë në përdorim", "This phone number is already in use": "Ky numër telefoni është tashmë në përdorim", - "Failed to verify email address: make sure you clicked the link in the email": "Vërtetimi i adresës e-mail i pasukseshëm: Sigurohu që ke klikuar lidhjen në e-mail", + "Failed to verify email address: make sure you clicked the link in the email": "S’u arrit të verifikohej adresë email: sigurohuni se keni klikuar lidhjen te email-i", "The platform you're on": "Platforma ku gjendeni", "The version of Riot.im": "Versioni i Riot.im-it", - "Whether or not you're logged in (we don't record your user name)": "A je i lajmëruar apo jo (ne nuk do të inçizojmë emrin përdorues tëndë)", + "Whether or not you're logged in (we don't record your user name)": "Nëse jeni apo të futur në llogarinë tuaj (nuk e regjistrojmë emrin tuaj të përdoruesit)", "Your language of choice": "Gjuha juaj e zgjedhur", "Which officially provided instance you are using, if any": "Cilën instancë të furnizuar zyrtarish po përdorni, në pastë", - "Whether or not you're using the Richtext mode of the Rich Text Editor": "A je duke e përdorur mënyrën e tekstit të pasuruar të redaktionuesit të tekstit të pasuruar apo jo", + "Whether or not you're using the Richtext mode of the Rich Text Editor": "Nëse po përdorni apo jo mënyrën Richtext të Përpunuesit të Teksteve të Pasur", "Your homeserver's URL": "URL e Shërbyesit tuaj Home", "Your identity server's URL": "URL e shërbyesit tuaj të identiteteve", "Analytics": "Analiza", - "The information being sent to us to help make Riot.im better includes:": "Informacionet që dërgohen për t'i ndihmuar Riot.im-it të përmirësohet përmbajnë:", - "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Kur kjo faqe pëmban informacione që mund të të identifikojnë, sikur një dhomë, përdorues apo identifikatues grupi, këto të dhëna do të mënjanohen para se t‘i dërgohën një server-it.", + "The information being sent to us to help make Riot.im better includes:": "Të dhënat që na dërgohen për të na ndihmuar ta bëjmë më të mirë Riot.im-in përfshijnë:", + "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Kur kjo faqe përfshin të dhëna të identifikueshme, të tilla si një ID dhome përdoruesi apo grupi, këto të dhëna hiqen përpara se të dërgohet te shërbyesi.", "Call Failed": "Thirrja Dështoi", - "There are unknown devices in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.": "Pajisje të panjohura ndodhen në këtë dhomë: nësë vazhdon pa i vërtetuar, është e mundshme që dikush të jua përgjon thirrjen.", - "Review Devices": "Rishiko pajisjet", + "There are unknown devices in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.": "Në këtë dhomë ka pajisje të panjohura: nëse vazhdoni pa i verifikuar ato, për dikë do të jetë e mundur të përgjojë thirrjen tuaj.", + "Review Devices": "Shqyrtoni Pajisje", "Call Anyway": "Thirre Sido Qoftë", "Answer Anyway": "Përgjigju Sido Qoftë", "Call": "Thirrje", "Answer": "Përgjigje", - "Call Timeout": "Skadim kohe thirrjeje", + "Call Timeout": "Mbarim kohe Thirrjeje", "The remote side failed to pick up": "Ana e largët dështoi të përgjigjet", - "Unable to capture screen": "Ekrani nuk mundi të inçizohej", - "Existing Call": "Thirrje aktuale", + "Unable to capture screen": "S’arrihet të fotografohet ekrani", + "Existing Call": "Thirrje Ekzistuese", "You are already in a call.": "Jeni tashmë në një thirrje.", "VoIP is unsupported": "VoIP nuk mbulohet", - "You cannot place VoIP calls in this browser.": "Thirrjet me VoIP nuk mbulohen nga ky kërkues uebi.", + "You cannot place VoIP calls in this browser.": "S’mund të bëni thirrje VoIP që nga ky shfletues.", "You cannot place a call with yourself.": "S’mund të bëni thirrje me vetveten.", "Conference calls are not supported in this client": "Thirrjet konference nuk mbulohen nga ky klienti", "Conference calls are not supported in encrypted rooms": "Thirrjet konference nuk mbulohen në dhoma të shifruara", @@ -35,10 +35,10 @@ "Failed to set up conference call": "Thirrja konference nuk mundi të realizohej", "Conference call failed.": "Thirrja konference dështoi.", "The file '%(fileName)s' failed to upload": "Dështoi ngarkimi i kartelës '%(fileName)s'", - "The file '%(fileName)s' exceeds this home server's size limit for uploads": "Fajli '%(fileName)s' tejkalon kufirin madhësie për mbartje e këtij server-i shtëpiak", + "The file '%(fileName)s' exceeds this home server's size limit for uploads": "Kartela '%(fileName)s' tejkalon kufirin e këtij shërbyesi Home për madhësinë e ngarkimeve", "Upload Failed": "Ngarkimi Dështoi", - "Failure to create room": "Dhoma nuk mundi të krijohet", - "Server may be unavailable, overloaded, or you hit a bug.": "Server-i është i padisponueshëm, i ngarkuar tej mase, apo ka një gabim.", + "Failure to create room": "S’u arrit të krijohej dhomë", + "Server may be unavailable, overloaded, or you hit a bug.": "Shërbyesi mund të jetë i pakapshëm, i mbingarkuar, ose hasët një të metë.", "Send anyway": "Dërgoje sido qoftë", "Send": "Dërgoje", "Sun": "Die", @@ -48,16 +48,16 @@ "Thu": "Enj", "Fri": "Pre", "Sat": "Sht", - "%(weekDayName)s, %(monthName)s %(day)s %(time)s": "%(weekDayName)s, %(day)s %(monthName)s %(time)s", + "%(weekDayName)s, %(monthName)s %(day)s %(time)s": "%(weekDayName)s, %(day)s %(monthName)s më %(time)s", "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s, %(day)s %(monthName)s %(fullYear)s", - "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s, %(day)s %(monthName)s %(fullYear)s %(time)s", + "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s, %(day)s %(monthName)s %(fullYear)s më %(time)s", "Who would you like to add to this community?": "Kë do të donit të shtonit te kjo bashkësi?", - "Warning: any person you add to a community will be publicly visible to anyone who knows the community ID": "Paralajmërim: se cili që e shton në një komunitet do t‘i doket se cilit që e di identifikatuesin e komunitetit", + "Warning: any person you add to a community will be publicly visible to anyone who knows the community ID": "Kujdes: cilido person që shtoni te një bashkësi do të jetë publikisht i dukshëm për cilindo që di ID-në e bashkësisë", "Invite new community members": "Ftoni anëtarë të rinj bashkësie", "Name or matrix ID": "Emër ose ID matrix-i", - "Invite to Community": "Fto në komunitet", - "Which rooms would you like to add to this community?": "Cilët dhoma kishe dashur t‘i shtosh në këtë komunitet?", - "Show these rooms to non-members on the community page and room list?": "A t‘i duken dhomat joanëtarëvë ne faqën komuniteti si dhe listën dhome?", + "Invite to Community": "Ftoni në Bashkësi", + "Which rooms would you like to add to this community?": "Cilat dhoma do të donit të shtonit te kjo bashkësi?", + "Show these rooms to non-members on the community page and room list?": "T’u shfaqen këto dhoma te faqja e bashkësisë dhe lista e dhomave atyre që s’janë anëtarë?", "Add rooms to the community": "Shtoni dhoma te bashkësia", "Add to community": "Shtoje te kjo bashkësi", "Jan": "Jan", @@ -73,14 +73,14 @@ "Nov": "Nën", "Dec": "Dhj", "%(weekDayName)s %(time)s": "%(weekDayName)s %(time)s", - "Failed to invite the following users to %(groupId)s:": "Ky përdorues vijues nuk mundi të ftohet në %(groupId)s:", - "Failed to invite users to community": "Përdoruesit nuk mundën të ftohën", - "Failed to invite users to %(groupId)s": "Nuk mundën të ftohën përdoruesit në %(groupId)s", - "Failed to add the following rooms to %(groupId)s:": "Nuk mundën të shtohen dhomat vijuese në %(groupId)s:", + "Failed to invite the following users to %(groupId)s:": "S’u arrit të ftoheshin përdoruesit vijues te %(groupId)s:", + "Failed to invite users to community": "S’u arrit të ftoheshin përdorues te bashkësia", + "Failed to invite users to %(groupId)s": "S’u arrit të ftoheshin përdorues te %(groupId)s", + "Failed to add the following rooms to %(groupId)s:": "S’u arrit të shtoheshin dhomat vijuese te %(groupId)s:", "Unnamed Room": "Dhomë e Paemërtuar", - "Riot does not have permission to send you notifications - please check your browser settings": "Riot nuk ka lejim të të dergojë lajmërime - të lutem kontrollo rregullimet e kërkuesit ueb tëndë", - "Riot was not given permission to send notifications - please try again": "Riot-it nuk i është dhënë leje të dërgojë lajmërime - të lutëm përpjeku serish", - "Unable to enable Notifications": "Lajmërimet nuk mundën të lëshohen", + "Riot does not have permission to send you notifications - please check your browser settings": "Riot-i s’ka leje t’ju dërgojë njoftime - Ju lutemi, kontrolloni rregullimet e shfletuesit tuajPlease wait whilst we resynchronise with the server", + "Riot was not given permission to send notifications - please try again": "Riot-it s’iu dha leje të dërgojë njoftime - ju lutemi, riprovoni", + "Unable to enable Notifications": "S’arrihet të aktivizohen njoftimet", "This email address was not found": "Kjo adresë email s’u gjet", "Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "Adresa juaj email s’duket të jetë e përshoqëruar me një ID Matrix në këtë shërbyes Home.", "Default": "Parazgjedhje", @@ -94,26 +94,26 @@ "Invite new room members": "Ftoni anëtarë të rinj dhome", "Who would you like to add to this room?": "Kë do të donit të shtonit te kjo dhomë?", "Send Invites": "Dërgoni Ftesa", - "Failed to invite user": "Përdoruesi nuk mundi të ftohej", + "Failed to invite user": "S’u arrit të ftohej përdorues", "Operation failed": "Veprimi dështoi", - "Failed to invite": "Nuk mundi të ftohet", - "Failed to invite the following users to the %(roomName)s room:": "Përdoruesit vijuesë nuk mundën të ftohen në dhomën %(roomName)s:", + "Failed to invite": "S’u arrit të ftohej", + "Failed to invite the following users to the %(roomName)s room:": "S’u arrit të ftoheshin përdoruesit vijues te dhoma %(roomName)s:", "You need to be logged in.": "Lypset të jeni i futur në llogarinë tuaj.", "You need to be able to invite users to do that.": "Që ta bëni këtë, lypset të jeni në gjendje të ftoni përdorues.", - "Unable to create widget.": "Widget-i nuk mundi të krijohet.", - "Failed to send request.": "Lutja nuk mundi të dërgohej.", + "Unable to create widget.": "S’arrihet të krijohet widget-i.", + "Failed to send request.": "S’u arrit të dërgohej kërkesë.", "This room is not recognised.": "Kjo dhomë s’është e pranuar.", - "Power level must be positive integer.": "Niveli fuqie duhet të jetë numër i plotë pozitiv.", + "Power level must be positive integer.": "Shkalla e pushtetit duhet të jetë një numër i plotë pozitiv.", "You are not in this room.": "S’gjendeni në këtë dhomë.", "You do not have permission to do that in this room.": "S’keni leje për ta bërë këtë në këtë dhomë.", "Room %(roomId)s not visible": "Dhoma %(roomId)s s’është e dukshme", "Usage": "Përdorim", - "/ddg is not a command": "/ddg s'është komandë", - "To use it, just wait for autocomplete results to load and tab through them.": "Për të përdorur, thjesht prit derisa të mbushën rezultatat vetëplotësuese dhe pastaj shfletoji.", + "/ddg is not a command": "/ddg s’është urdhër", + "To use it, just wait for autocomplete results to load and tab through them.": "Për ta përdorur, thjesht pritni që të ngarkohen përfundimet e vetëplotësimit dhe shihini një nga një.", "Unrecognised room alias:": "Alias dhome jo i pranuar:", "Ignored user": "Përdorues i shpërfillur", "You are now ignoring %(userId)s": "Tani po e shpërfillni %(userId)s", - "Unignored user": "Përdorues jo më i shpërfillur", + "Unignored user": "U hoq shpërfillja për përdoruesin", "Fetching third party location failed": "Dështoi prurja e vendndodhjes së palës së tretë", "A new version of Riot is available.": "Ka gati një version të ri Riot-it.", "Couldn't load home page": "S’u ngarkua dot faqja hyrëse", @@ -137,13 +137,13 @@ "Filter room names": "Filtroni emra dhomash", "Changelog": "Regjistër ndryshimesh", "Reject": "Hidheni tej", - "Waiting for response from server": "Po pritet për përgjigje shërbyesi", - "Failed to change password. Is your password correct?": "S’u arrit të ndryshohet fjalëkalimi. A është i saktë fjalëkalimi juaj?", + "Waiting for response from server": "Po pritet për përgjigje nga shërbyesi", + "Failed to change password. Is your password correct?": "S’u arrit të ndryshohej fjalëkalimi. A është i saktë fjalëkalimi juaj?", "Uploaded on %(date)s by %(user)s": "Ngarkuar më %(date)s nga %(user)s", "OK": "OK", "Send Custom Event": "Dërgoni Akt Vetjak", "Advanced notification settings": "Rregullime të mëtejshme për njoftimet", - "Failed to send logs: ": "S’u arrit të dërgohen regjistra: ", + "Failed to send logs: ": "S’u arrit të dërgoheshin regjistra: ", "delete the alias.": "fshije aliasin.", "To return to your account in future you need to set a password": "Që të riktheheni te llogaria juaj në të ardhmen, lypset të caktoni një fjalëkalim", "Forget": "Harroje", @@ -159,9 +159,9 @@ "Room not found": "Dhoma s’u gjet", "Downloading update...": "Po shkarkohet përditësim…", "Messages in one-to-one chats": "Mesazhe në fjalosje tek për tek", - "Unavailable": "S’kapet", + "Unavailable": "", "View Decrypted Source": "Shihni Burim të Shfshehtëzuar", - "Failed to update keywords": "S’u arrit të përditësohen fjalëkyçe", + "Failed to update keywords": "S’u arrit të përditësoheshin fjalëkyçe", "Notes:": "Shënime:", "Notifications on the following keywords follow rules which can’t be displayed here:": "Njoftimet e shkaktuara nga fjalëkyçet vijuese ndjekin rregulla që s’mund të shfaqen këtu:", "Safari and Opera work too.": "Safari dhe Opera bëjnë, po ashtu.", @@ -171,8 +171,8 @@ "Favourite": "E parapëlqyer", "All Rooms": "Krejt Dhomat", "Explore Room State": "Eksploroni Gjendje Dhome", - "Source URL": "URL-ja e Burimit", - "Messages sent by bot": "Mesazhe të dërguar nga bot", + "Source URL": "URL Burimi", + "Messages sent by bot": "Mesazhe të dërguar nga boti", "Cancel": "Anuloje", "Filter results": "Filtroni përfundimet", "Members": "Anëtarë", @@ -203,7 +203,7 @@ "Unnamed room": "Dhomë e paemërtuar", "Dismiss": "Mos e merr parasysh", "Explore Account Data": "Eksploroni të Dhëna Llogarie", - "All messages (noisy)": "Tërë Mesazhet (e zhurmshme)", + "All messages (noisy)": "Krejt mesazhet (e zhurmshme)", "Saturday": "E shtunë", "Remember, you can always set an email address in user settings if you change your mind.": "Mos harroni, mundeni përherë të caktoni një adresë email te rregullimet e përdoruesit, nëse ndërroni mendje.", "Direct Chat": "Fjalosje e Drejtpërdrejtë", @@ -214,13 +214,13 @@ "Download this file": "Shkarkoje këtë kartelë", "Remove from Directory": "Hiqe prej Drejtorie", "Enable them now": "Aktivizoji tani", - "Messages containing my user name": "Mesazhe që përmbajnë emrin tim", + "Messages containing my user name": "Mesazhe që përmbajnë emrin tim të përdoruesit", "Toolbox": "Grup mjetesh", "Collecting logs": "Po grumbullohen regjistra", "more": "më tepër", "GitHub issue link:": "Lidhje çështjeje GitHub:", - "Failed to get public room list": "S’u të merrej listë dhomash publike", - "Search": "Kërkim", + "Failed to get public room list": "S’u arrit të merrej listë dhomash publike", + "Search": "Kërkoni", "Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Regjistrat e diagnostikimeve përmbajnë të dhëna përdorimi të aplikacioneve, përfshi emrin tuaj të përdoruesit, ID ose aliase të dhomave apo grupeve që keni vizituar dhe emrat e përdoruesve të përdoruesve të tjerë. Nuk përmbajnë mesazhe.", "(HTTP status %(httpStatus)s)": "(Gjendje HTTP %(httpStatus)s)", "Failed to forget room %(errCode)s": "S’u arrit të harrohej dhoma %(errCode)s", @@ -235,23 +235,23 @@ "Call invitation": "Ftesë për thirrje", "Thank you!": "Faleminderit!", "Messages containing my display name": "Mesazhe që përmbajnë emrin tim të ekranit", - "State Key": "Kyç Gjendjeje", + "State Key": "", "Failed to send custom event.": "S’u arrit të dërgohet akt vetjak.", "What's new?": "Ç’ka të re?", "Notify me for anything else": "Njoftomë për gjithçka tjetër", "When I'm invited to a room": "Kur ftohem në një dhomë", "Close": "Mbylle", "Can't update user notification settings": "S’përditësohen dot rregullime njoftimi të përdoruesit", - "Notify for all other messages/rooms": "Njoftim për krejt mesazhet/dhomat e tjera", + "Notify for all other messages/rooms": "Njofto për krejt mesazhet/dhomat e tjera", "Unable to look up room ID from server": "S’arrihet të kërkohet ID dhome nga shërbyesi", "Couldn't find a matching Matrix room": "S’u gjet dot një dhomë Matrix me përputhje", - "Invite to this room": "Ftoje te kjo dhomë", + "Invite to this room": "Ftojeni te kjo dhomë", "You cannot delete this message. (%(code)s)": "S’mund ta fshini këtë mesazh. (%(code)s)", "Thursday": "E enjte", "I understand the risks and wish to continue": "I kuptoj rreziqet dhe dua të vazhdoj", "Logs sent": "Regjistrat u dërguan", "Back": "Mbrapsht", - "Reply": "Përgjigjuni", + "Reply": "Përgjigje", "Show message in desktop notification": "Shfaq mesazh në njoftim për desktop", "You must specify an event type!": "Duhet të përcaktoni një lloj akti!", "Unhide Preview": "Shfshihe Paraparjen", @@ -271,7 +271,7 @@ "Off": "Off", "Edit": "Përpuno", "Riot does not know how to join a room on this network": "Riot-i nuk di si të hyjë në një dhomë në këtë rrjet", - "Mentions only": "Vetëm @përmendje", + "Mentions only": "Vetëm përmendje", "remove %(name)s from the directory.": "hiqe %(name)s prej drejtorie.", "You can now return to your account after signing out, and sign in on other devices.": "Mund të ktheheni te llogaria juaj, pasi të keni bërë daljen, dhe të bëni hyrjen nga pajisje të tjera.", "Continue": "Vazhdo", @@ -295,14 +295,14 @@ "With your current browser, the look and feel of the application may be completely incorrect, and some or all features may not function. If you want to try it anyway you can continue, but you are on your own in terms of any issues you may encounter!": "Me shfletuesin tuaj të tanishëm, pamja dhe ndjesitë nga aplikacioni mund të jenë plotësisht të pasakta, dhe disa nga ose krejt veçoritë të mos funksionojnë. Nëse doni ta provoni sido qoftë, mund të vazhdoni, por mos u ankoni për çfarëdo problemesh që mund të hasni!", "Checking for an update...": "Po kontrollohet për një përditësim…", "There are advanced notifications which are not shown here": "Ka njoftime të thelluara që nuk shfaqen këtu", - "Show empty room list headings": "Shfaqi emrat e listave të zbrazëta dhomash", + "Show empty room list headings": "Shfaq krye liste dhomash të zbrazëta", "PM": "PM", "AM": "AM", "Room name or alias": "Emër dhome ose alias", "Unknown (user, device) pair:": "Çift (përdorues, pajisje) i panjohur:", "Device already verified!": "Pajisjeje tashmë e verifikuar!", "Verified key": "Kyç i verifikuar", - "Unrecognised command:": "Urdhër jo i pranuar: ", + "Unrecognised command:": "Urdhër jo i pranuar:", "Reason": "Arsye", "%(senderName)s requested a VoIP conference.": "%(senderName)s kërkoi një konferencë VoIP.", "VoIP conference started.": "Konferenca VoIP filloi.", @@ -390,7 +390,7 @@ "Make Moderator": "Kaloje Moderator", "Admin Tools": "Mjete Përgjegjësi", "Level:": "Nivel:", - "and %(count)s others...|other": "dhe %{count} të tjerë…", + "and %(count)s others...|other": "dhe %(count)s të tjerë…", "and %(count)s others...|one": "dhe një tjetër…", "Filter room members": "Filtroni anëtarë dhome", "Attachment": "Bashkëngjitje", @@ -453,8 +453,8 @@ "This is a preview of this room. Room interactions have been disabled": "Kjo është një paraparje e kësaj dhome. Ndërveprimet në dhomë janë çaktivizuar", "Banned by %(displayName)s": "Dëbuar nga %(displayName)s", "Privacy warning": "Sinjalizim privatësie", - "The visibility of existing history will be unchanged": "Dukshmëria e historikut ekzistues nuk do të ndryshohet.", - "You should not yet trust it to secure data": "S’duhet t’i zini ende besë për sigurim të dhënash.", + "The visibility of existing history will be unchanged": "Dukshmëria e historikut ekzistues nuk do të ndryshohet", + "You should not yet trust it to secure data": "S’duhet t’i zini ende besë për sigurim të dhënash", "Enable encryption": "Aktivizoni fshehtëzim", "Encryption is enabled in this room": "Në këtë dhomë është i aktivizuar fshehtëzimi", "Encryption is not enabled in this room": "Në këtë dhomë s’është i aktivizuar fshehtëzimi", @@ -462,7 +462,7 @@ "Privileged Users": "Përdorues të Privilegjuar", "Banned users": "Përdorues të dëbuar", "Leave room": "Dilni nga dhomë", - "Tagged as: ": "Etiketuar me:", + "Tagged as: ": "Etiketuar me: ", "Click here to fix": "Klikoni këtu për ta ndrequr", "Who can access this room?": "Kush mund të hyjë në këtë dhomë?", "Only people who have been invited": "Vetëm persona që janë ftuar", @@ -508,7 +508,7 @@ "You're not currently a member of any communities.": "Hëpërhë, s’jeni anëtar i ndonjë bashkësie.", "Unknown Address": "Adresë e Panjohur", "Allow": "Lejoje", - "Revoke widget access": "Shfuqizo hyrje widget", + "Revoke widget access": "Shfuqizo hyrje në widget", "Create new room": "Krijoni dhomë të re", "Unblacklist": "Hiqe nga listë e zezë", "Blacklist": "Listë e zezë", @@ -576,7 +576,7 @@ "This doesn't appear to be a valid email address": "Kjo s’duket se është adresë email e vlefshme", "Verification Pending": "Verifikim Në Pritje të Miratimit", "Skip": "Anashkaloje", - "User names may only contain letters, numbers, dots, hyphens and underscores.": "Emrat e përdoruesve mund të përmbajnë vetëm shkronja, numra, pika, vija ndarëse dhe nënvija", + "User names may only contain letters, numbers, dots, hyphens and underscores.": "Emrat e përdoruesve mund të përmbajnë vetëm shkronja, numra, pika, vija ndarëse dhe nënvija.", "Username not available": "Emri i përdoruesit s’është i lirë", "Username invalid: %(errMessage)s": "Emër përdoruesi i pavlefshëm: %(errMessage)s", "Username available": "Emri i përdoruesit është i lirë", @@ -598,9 +598,9 @@ "Who would you like to add to this summary?": "Kë do të donit të shtonit te kjo përmbledhje?", "Add a User": "Shtoni një Përdorues", "Leave Community": "Braktiseni Bashkësinë", - "Leave %(groupName)s?": "Të braktiset {groupName}?", + "Leave %(groupName)s?": "Të braktiset %(groupName)s?", "Community Settings": "Rregullime Bashkësie", - "%(inviter)s has invited you to join this community": "%s ju ftoi të bëheni pjesë e kësaj bashkësie", + "%(inviter)s has invited you to join this community": "%(inviter)s ju ftoi të bëheni pjesë e kësaj bashkësie", "You are an administrator of this community": "Jeni një përgjegjës i kësaj bashkësie", "You are a member of this community": "Jeni anëtar i këtij ekipi", "Long Description (HTML)": "Përshkrim i Gjatë (HTML)", @@ -613,7 +613,7 @@ "Logout": "Dalje", "Your Communities": "Bashkësitë Tuaja", "Create a new community": "Krijoni një bashkësi të re", - "You have no visible notifications": "S’keni njoftime të dukshme.", + "You have no visible notifications": "S’keni njoftime të dukshme", "%(count)s of your messages have not been sent.|other": "Disa nga mesazhet tuaj s’janë dërguar.", "%(count)s of your messages have not been sent.|one": "Mesazhi juaj s’u dërgua.", "%(count)s new messages|other": "%(count)s mesazhe të rinj", @@ -677,7 +677,7 @@ "Incorrect username and/or password.": "Emër përdoruesi dhe/ose fjalëkalim i pasaktë.", "The phone number entered looks invalid": "Numri i telefonit që u dha duket i pavlefshëm", "Sign in to get started": "Që t’ia filloni, bëni hyrjen", - "Set a display name:": "Caktoni emër ekrani", + "Set a display name:": "Caktoni emër ekrani:", "Upload an avatar:": "Ngarkoni një avatar:", "This server does not support authentication with a phone number.": "Ky shërbyes nuk mbulon mirëfilltësim me një numër telefoni.", "Missing password.": "Mungon fjalëkalimi.", @@ -719,5 +719,667 @@ "Export": "Eksporto", "Import room keys": "Importo kyçe dhome", "This process allows you to import encryption keys that you had previously exported from another Matrix client. You will then be able to decrypt any messages that the other client could decrypt.": "Ky proces ju lejon të importoni kyçe fshehtëzimi që keni eksportuar më parë nga një tjetër klient Matrix. Mandej do të jeni në gjendje të shfshehtëzoni çfarëdo mesazhesh që mund të shfshehtëzojë ai klient tjetër.", - "The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.": "Kartela e eksportit është e mbrojtur me një frazëkalim. Që të shfshehtëzoni kartelën, duhet ta jepni frazëkalimin këtu." + "The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.": "Kartela e eksportit është e mbrojtur me një frazëkalim. Që të shfshehtëzoni kartelën, duhet ta jepni frazëkalimin këtu.", + "Missing room_id in request": "Mungon room_id te kërkesa", + "Missing user_id in request": "Mungon user_id te kërkesa", + "%(names)s and %(count)s others are typing|other": "%(names)s dhe %(count)s të tjerë po shtypin", + "%(names)s and %(lastPerson)s are typing": "%(names)s dhe %(lastPerson)s të tjerë po shtypin", + "Failed to join room": "S’u arrit të hyhej në dhomë", + "Hide removed messages": "Fshih mesazhe të hequr", + "Hide avatar changes": "Fshih ndryshime avatarësh", + "Hide display name changes": "Fshih ndryshime emrash ekrani", + "Hide read receipts": "Fshih dëftesa leximi", + "Mirror local video feed": "Pasqyro prurje vendore videoje", + "Never send encrypted messages to unverified devices from this device": "Mos dërgo kurrë mesazhe të fshehtëzuar, nga kjo pajisje te pajisje të paverifikuara", + "Never send encrypted messages to unverified devices in this room from this device": "Mos dërgo kurrë mesazhe të fshehtëzuar, nga kjo pajisje te pajisje të paverifikuara në këtë dhomë", + "Incoming voice call from %(name)s": "Thirrje audio ardhëse nga %(name)s", + "Incoming video call from %(name)s": "Thirrje video ardhëse nga %(name)s", + "Incoming call from %(name)s": "Thirrje ardhëse nga %(name)s", + "Failed to upload profile picture!": "S’u arrit të ngarkohej foto profili!", + "Unable to load device list": "S’arrihet të ngarkohet listë pajisjesh", + "New address (e.g. #foo:%(localDomain)s)": "Adresë e re (p.sh. #foo:%(localDomain)s)", + "New community ID (e.g. +foo:%(localDomain)s)": "ID bashkësie të re (p.sh. +foo:%(localDomain)s)", + "Ongoing conference call%(supportedText)s.": "Thirrje konference që po zhvillohet%(supportedText)s.", + "Failed to kick": "S’u arrit të përzihej", + "Unban this user?": "Të hiqet dëbimi për këtë përdorues?", + "Failed to ban user": "S’u arrit të dëbohej përdoruesi", + "Failed to mute user": "S’u arrit t’i hiqej zëri përdoruesit", + "Failed to change power level": "S’u arrit të ndryshohej shkalla e pushtetit", + "Unmute": "Ktheji zërin", + "Invited": "I ftuar", + "Hangup": "Mbylle Thirrjen", + "Turn Markdown on": "Aktivizo sintaksën Markdown", + "Turn Markdown off": "Çaktivizo sintaksën Markdown", + "Hide Text Formatting Toolbar": "Fshih Panel Formatimi Tekstesh", + "No pinned messages.": "S’ka mesazhe të fiksuar.", + "Replying": "Po përgjigjet", + "Failed to set avatar.": "S’u arrit të caktohej avatar.", + "To change the room's avatar, you must be a": "Që të ndryshoni avatarin e dhomës, duhet të jeni një", + "To change the room's name, you must be a": "Që të ndryshoni emrin e dhomës, duhet të jeni një", + "To change the room's main address, you must be a": "Që të ndryshoni adresën kryesore të dhomës, duhet të jeni një", + "To change the permissions in the room, you must be a": "Që të ndryshoni lejet në këtë dhomë, duhet të jeni një", + "To change the topic, you must be a": "Që të ndryshoni temën e dhomës, duhet të jeni një", + "To modify widgets in the room, you must be a": "Që të modifikoni widget-e te dhoma, duhet të jeni një", + "Failed to unban": "S’u arrit t’i hiqej dëbimi", + "Once encryption is enabled for a room it cannot be turned off again (for now)": "Pasi fshehtëzimi të jetë aktivizuar për një dhomë, s’mund të çaktivizohet më (hëpërhë)", + "To send messages, you must be a": "Që të dërgoni mesazhe, duhet të jeni një", + "To invite users into the room, you must be a": "Që të ftoni përdorues te dhoma, duhet të jeni një", + "To configure the room, you must be a": "Që të formësoni dhomën, duhet të jeni një", + "To kick users, you must be a": "Që të përzini përdorues, duhet të jeni një", + "To ban users, you must be a": "Që të dëboni përdorues, duhet të jeni një", + "To link to a room it must have an address.": "Që të lidhni një dhomë, ajo duhet të ketë një adresë.", + "Members only (since the point in time of selecting this option)": "Vetëm anëtarët (që nga çasti i përzgjedhjes së kësaj mundësie)", + "Members only (since they were invited)": "Vetëm anëtarë (që kur qenë ftuar)", + "Members only (since they joined)": "Vetëm anëtarë (që kur janë bërë pjesë)", + "Scroll to unread messages": "Rrëshqit për te mesazhe të palexuar", + "Jump to first unread message.": "Hidhu te mesazhi i parë i palexuar.", + "Failed to copy": "S’u arrit të kopjohej", + "Message removed by %(userId)s": "Mesazhi u hoq nga %(userId)s", + "Message removed": "Mesazhi u hoq", + "To continue, please enter your password.": "Që të vazhdohet, ju lutemi, jepni fjalëkalimin tuaj.", + "Token incorrect": "Token i pasaktë", + "Remove from community": "Hiqe prej bashkësie", + "Remove this user from community?": "Të hiqet ky përdoruesin prej bashkësisë?", + "Failed to withdraw invitation": "S’u arrit të tërhiqej mbrapsht ftesa", + "Failed to remove user from community": "S’u arrit të hiqej përdoruesi nga bashkësia", + "Failed to remove room from community": "S’u arrit të hiqej dhoma nga bashkësia", + "Minimize apps": "Minimizoji aplikacionet", + "%(oneUser)schanged their avatar %(count)s times|one": "%(oneUser)sndryshoi avatarin e vet", + "Start chatting": "Filloni të bisedoni", + "Start Chatting": "Filloni të Bisedoni", + "This setting cannot be changed later!": "Ky rregullim s’mund të ndryshohet më vonë!", + "To verify that this device can be trusted, please contact its owner using some other means (e.g. in person or a phone call) and ask them whether the key they see in their User Settings for this device matches the key below:": "Që të verifikoni se kësaj pajisje mund t’i zihet besë, ju lutemi, lidhuni me të zotët e saj përmes ndonjë rruge tjetër (p.sh., personalisht, ose përmes një thirrjeje telefonike) dhe kërkojuni nëse kyçi që shohin te Rregullime të tyret të Përdoruesit për këtë pajisje përputhet me kyçin më poshtë:", + "If it matches, press the verify button below. If it doesn't, then someone else is intercepting this device and you probably want to press the blacklist button instead.": "Nëse përputhet, shtypni butonin e verifikimit më poshtë. Nëse jo, atëherë dikush tjetër po e përgjon këtë pajisje dhe duhet ta kaloni në listë të zezë.", + "In future this verification process will be more sophisticated.": "Në të ardhmen, ky proces verifikimi do të jetë më i sofistikuar.", + "I verify that the keys match": "Verifikoj se kyçet përputhen", + "Unable to restore session": "S’arrihet të rikthehet sesioni", + "Please check your email and click on the link it contains. Once this is done, click continue.": "Ju lutemi, kontrolloni email-in tuaj dhe klikoni mbi lidhjen që përmban. Pasi të jetë bërë kjo, klikoni që të vazhdohet.", + "Unable to add email address": "S’arrihet të shtohet adresë email", + "Unable to verify email address.": "S’arrihet të verifikohet adresë email.", + "To get started, please pick a username!": "Që t’ia filloni, ju lutemi, zgjidhni një emër përdoruesi!", + "There are no visible files in this room": "S’ka kartela të dukshme në këtë dhomë", + "Failed to upload image": "S’u arrit të ngarkohej figurë", + "Failed to update community": "S’u arrit të përditësohej bashkësia", + "Unable to accept invite": "S’arrihet të pranohet ftesë", + "Unable to reject invite": "S’arrihet të hidhet tej ftesa", + "Featured Rooms:": "Dhoma të Zgjedhura:", + "Featured Users:": "Përdorues të Zgjedhur:", + "This Home server does not support communities": "Ky shërbyes Home s’mbulon bashkësi", + "Failed to load %(groupId)s": "S’u arrit të ngarkohej %(groupId)s", + "Failed to reject invitation": "S’u arrit të hidhej poshtë ftesa", + "Failed to leave room": "S’u arrit të braktisej", + "Scroll to bottom of page": "Rrëshqit te fundi i faqes", + "Message not sent due to unknown devices being present": "Mesazhi s’u dërgua, për shkak të pranisë së pajisjeve të panjohura", + "Failed to upload file": "S’u arrit të ngarkohej kartelë", + "Unknown room %(roomId)s": "Dhomë e panjohur %(roomId)s", + "Failed to save settings": "S’u arrit të ruheshin rregullimet", + "Fill screen": "Mbushe ekranin", + "Tried to load a specific point in this room's timeline, but was unable to find it.": "U provua të ngarkohej një pikë të dhënë prej rrjedhës kohore në këtë dhomë, por s’u arrit të gjendej.", + "Failed to load timeline position": "S’u arrit të ngarkohej pozicion rrjedhe kohore", + "Remove Contact Information?": "Të hiqen Të dhëna Kontakti?", + "Unable to remove contact information": "S’arrihet të hiqen të dhëna kontakti", + "Import E2E room keys": "Importo kyçe E2E dhome", + "To return to your account in future you need to set a password": "Që të riktheheni te llogaria juaj në të ardhmen, lypset të caktoni një fjalëkalim", + "Homeserver is": "Shërbyesi Home është", + "matrix-react-sdk version:": "Version matrix-react-sdk:", + "Failed to send email": "S’u arrit të dërgohej email", + "I have verified my email address": "E kam verifikuar adresën time email", + "To reset your password, enter the email address linked to your account": "Që të ricaktoni fjalëkalimin tuaj, jepni adresën email të lidhur me llogarinë tuaj", + "Failed to fetch avatar URL": "S’u arrit të sillej URL avatari", + "Invites user with given id to current room": "Fton te dhoma e tanishme përdoruesin me ID-në e dhënë", + "Joins room with given alias": "Hyn në dhomë me aliasin e dhënë", + "Searches DuckDuckGo for results": "Kërkon te DuckDuckGo për përfundime", + "Ignores a user, hiding their messages from you": "Shpërfill një përdorues, duke ju fshehur krejt mesazhet prej tij", + "File to import": "Kartelë për importim", + "Import": "Importo", + "%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s pranoi ftesën për %(displayName)s.", + "%(targetName)s accepted an invitation.": "%(targetName)s pranoi një ftesë.", + "%(senderName)s invited %(targetName)s.": "%(senderName)s ftoi %(targetName)s.", + "%(senderName)s banned %(targetName)s.": "%(senderName)s dëboi %(targetName)s.", + "%(senderName)s removed their profile picture.": "%(senderName)s hoqi foton e vet të profilit.", + "%(senderName)s set a profile picture.": "%(senderName)s caktoi një foto profili.", + "%(targetName)s joined the room.": "%(targetName)s hyri në dhomë.", + "%(targetName)s rejected the invitation.": "%(targetName)s hodhi tej ftesën.", + "%(targetName)s left the room.": "%(targetName)s doli nga dhoma.", + "%(senderName)s kicked %(targetName)s.": "%(senderName)s përzuri %(targetName)s.", + "%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s ndryshoi temën në \"%(topic)s\".", + "%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s hoqi emrin e dhomës.", + "%(senderDisplayName)s sent an image.": "%(senderDisplayName)s dërgoi një figurë.", + "%(senderName)s answered the call.": "%(senderName)s iu përgjigj thirrjes.", + "(could not connect media)": "(s’lidhi dot median)", + "(unknown failure: %(reason)s)": "(dështim i panjohur: %(reason)s)", + "%(senderName)s ended the call.": "%(senderName)s e përfundoi thirrjen.", + "Don't send typing notifications": "Mos dërgo njoftime shtypjesh", + "Disable Community Filter Panel": "Çaktivizo Panel Filtrash Bashkësie", + "Delete %(count)s devices|other": "Fshi %(count)s pajisje", + "Failed to set display name": "S’u arrit të caktohej emër ekrani", + "'%(alias)s' is not a valid format for an alias": "'%(alias)s' s’është format i vlefshëm aliasesh", + "'%(alias)s' is not a valid format for an address": "'%(alias)s' s’është format i vlefshëm adresash", + "'%(groupId)s' is not a valid community ID": "'%(groupId)s' s’është ID i vlefshëm bashkësish", + "Cannot add any more widgets": "S’mund të shtohen më tepër widget-e", + "Re-request encryption keys from your other devices.": "Rikërkoni kyçe fshehtëzimi prej pajisjesh tuaja të tjera.", + "%(userName)s (power %(powerLevelNumber)s)": "%(userName)s (pushtet %(powerLevelNumber)si)", + "(~%(count)s results)|other": "(~%(count)s përfundime)", + "(~%(count)s results)|one": "(~%(count)s përfundim)", + "Drop here to favourite": "Hidheni këtu që të bëhet e parapëlqyer", + "Drop here to restore": "Hidheni këtu që të bëhet rikthim", + "Click here to join the discussion!": "Klikoni këtu që të merrni pjesë të diskutimi!", + "Changes to who can read history will only apply to future messages in this room": "Ndryshime se cilët mund të lexojnë historikun do të vlejnë vetëm për mesazhe të ardhshëm në këtë dhomë", + "End-to-end encryption is in beta and may not be reliable": "Fshehtëzimi skaj-më-skaj është në fazën beta dhe mund të mos jetë i qëndrueshëm", + "Devices will not yet be able to decrypt history from before they joined the room": "Pajisjet s’do të jenë ende në gjendje të shfshehtëzojnë historik nga periudha përpara se të merrnin pjesë te dhomë", + "Encrypted messages will not be visible on clients that do not yet implement encryption": "Mesazhet e fshehtëzuar s’do të jenë të dukshëm në klientë që nuk e sendërtojnë ende fshehtëzimin", + "(warning: cannot be disabled again!)": "(kujdes: s’mund të çaktivizohet më!)", + "%(user)s is a %(userRole)s": "%(user)s është një %(userRole)s", + "Error decrypting audio": "Gabim në shfshehtëzim audioje", + "Download %(text)s": "Shkarko %(text)s", + "Error decrypting image": "Gabim në shfshehtëzim figure", + "Error decrypting video": "Gabim në shfshehtëzim videoje", + "Removed or unknown message type": "Lloj mesazhi i hequr ose i panjohur", + "An email has been sent to %(emailAddress)s": "U dërgua një email te %(emailAddress)s", + "%(serverName)s Matrix ID": "ID matrix-i në %(serverName)s", + "NOTE: Apps are not end-to-end encrypted": "SHËNIM: Aplikacionet s’janë të fshehtëzuara skaj-më-skaj", + "Delete Widget": "Fshije Widget-in", + "Delete widget": "Fshije widget-in", + "%(severalUsers)sjoined and left %(count)s times|one": "%(severalUsers)shynë dhe dolën", + "%(oneUser)sjoined and left %(count)s times|one": "%(oneUser)shyri dhe doli", + "%(severalUsers)srejected their invitations %(count)s times|one": "%(severalUsers)shodhën poshtë ftesat e tyre", + "%(oneUser)srejected their invitation %(count)s times|one": "%(oneUser)shodhi poshtë ftesën e tyre", + "%(severalUsers)schanged their name %(count)s times|one": "%(severalUsers)sndryshuan emrat e tyre", + "%(oneUser)schanged their name %(count)s times|one": "%(oneUser)sndryshoi emrin e vet", + "%(severalUsers)schanged their avatar %(count)s times|one": "%(severalUsers)sndryshuan avatarët e tyre", + "And %(count)s more...|other": "Dhe %(count)s të tjerë…", + "ex. @bob:example.com": "p.sh., @bob:example.com", + "Click on the button below to start chatting!": "Klikoni mbi butonin më poshtë që të filloni të bisedoni!", + "An error occurred: %(error_string)s": "Ndodhi një gabim: %(error_string)s", + "Connectivity to the server has been lost.": "Humbi lidhja me shërbyesin.", + "Click to mute video": "Klikoni që të heshtet videoja", + "Click to mute audio": "Klikoni që të heshtet audioja", + "": "", + "Clear Cache and Reload": "Pastro Fshehtinën dhe Ringarkoje", + "A new password must be entered.": "Duhet dhënë një fjalëkalim i ri.", + "An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.": "Te %(emailAddress)s u dërgua një email. Pasi të ndiqni lidhjen që përmban, klikoni më poshtë.", + "An unknown error occurred.": "Ndodhi një gabim i panjohur.", + "Displays action": "Shfaq veprimin", + "Define the power level of a user": "Përcaktoni shkallë pushteti të një përdoruesi", + "Deops user with given id": "I heq cilësinë e operatorit përdoruesit me ID-në e dhënë", + "Changes your display nickname": "Ndryshon nofkën tuaj në ekran", + "Emoji": "Emoji", + "Ed25519 fingerprint": "Shenja gishtash Ed25519", + "Failed to set direct chat tag": "S’u arrit të caktohej etiketa e fjalosjes së drejtpërdrejtë", + "You are no longer ignoring %(userId)s": "Nuk e shpërfillni më %(userId)s", + "%(senderName)s set their display name to %(displayName)s.": "%(senderName)s caktoi për veten emër ekrani %(displayName)s.", + "%(senderName)s removed their display name (%(oldDisplayName)s).": "%(senderName)s hoqi emrin e tij në ekran (%(oldDisplayName)s).", + "%(senderName)s changed their profile picture.": "%(senderName)s ndryshoi foton e vet të profilit.", + "%(senderName)s unbanned %(targetName)s.": "%(senderName)s hoqi dëbimin për %(targetName)s.", + "%(senderName)s withdrew %(targetName)s's invitation.": "%(senderName)s tërhoqi mbrapsht ftesën për %(targetName)s.", + "%(senderDisplayName)s changed the room name to %(roomName)s.": "%(senderDisplayName)s ndryshoi emrin e dhomës në %(roomName)s.", + "(not supported by this browser)": "(s’mbulohet nga ky shfletues)", + "%(senderName)s placed a %(callType)s call.": "%(senderName)s bëri një thirrje %(callType)s.", + "%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.": "%(senderName)s dërgoi një ftesë për %(targetDisplayName)s që të marrë pjesë në dhomë.", + "%(senderName)s made future room history visible to unknown (%(visibility)s).": "%(senderName)s e kaloi historikun e ardhshëm të dhomës të dukshëm për të panjohurit (%(visibility)s).", + "%(widgetName)s widget removed by %(senderName)s": "Widget-i %(widgetName)s u hoq nga %(senderName)s", + "%(names)s and %(count)s others are typing|one": "%(names)s dhe një tjetër po shtypin", + "Authentication check failed: incorrect password?": "Dështoi kontrolli i mirëfilltësimit: fjalëkalim i pasaktë?", + "Message Pinning": "Fiksim Mesazhi", + "Disable Emoji suggestions while typing": "Çaktivizoje sugjerime emoji-sh teksa shtypet", + "Autoplay GIFs and videos": "Vetëluaj GIF-e dhe video", + "Disable big emoji in chat": "Çaktivizo emoji-t e mëdhenj në fjalosje", + "Active call (%(roomName)s)": "Thirrje aktive (%(roomName)s)", + "%(senderName)s uploaded a file": "%(senderName)s ngarkoi një kartelë", + "Seen by %(displayName)s (%(userName)s) at %(dateTime)s": "Parë nga %(displayName)s (%(userName)s) më %(dateTime)s", + "Drop here to tag direct chat": "Hidheni këtu që të caktohet etiketa e fjalosjes së drejtpërdrejtë", + "%(roomName)s is not accessible at this time.": "Te %(roomName)s s’hyhet dot tani.", + "You are trying to access %(roomName)s.": "Po provoni të hyni te %(roomName)s.", + "To change the room's history visibility, you must be a": "Që të ndryshoni dukshmërinë e historikut të dhomës, duhet të jeni një", + "Error decrypting attachment": "Gabim në shfshehtëzim bashkëngjitjeje", + "Invalid file%(extra)s": "Kartelë e pavlefshme%(extra)s", + "%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s ndryshoi avatarin në %(roomName)s", + "%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s hoqi avatarin e dhomës.", + "Are you sure you want to remove '%(roomName)s' from %(groupId)s?": "Jeni i sigurt se doni të hiqet '%(roomName)s' nga %(groupId)s?", + "%(severalUsers)sjoined %(count)s times|other": "%(severalUsers)shynë %(count)s herë", + "%(severalUsers)sjoined %(count)s times|one": "Hynë %(severalUsers)s", + "%(oneUser)sjoined %(count)s times|other": "%(oneUser)shyri %(count)s herë", + "%(oneUser)sjoined %(count)s times|one": "%(oneUser)shyri", + "%(severalUsers)sleft %(count)s times|other": "%(severalUsers)sdolën %(count)s herë", + "%(severalUsers)sleft %(count)s times|one": "Doli %(severalUsers)s", + "%(oneUser)sleft %(count)s times|other": "%(oneUser)sdoli %(count)s herë", + "%(oneUser)sleft %(count)s times|one": "%(oneUser)sdoli", + "%(severalUsers)sleft and rejoined %(count)s times|one": "%(severalUsers)sdolën dhe rihynë", + "%(oneUser)sleft and rejoined %(count)s times|one": "%(oneUser)sdoli dhe rihyri", + "%(severalUsers)shad their invitations withdrawn %(count)s times|one": "U tërhoqën mbrapsht ftesat për %(severalUsers)s", + "were invited %(count)s times|other": "janë ftuar %(count)s herë", + "were banned %(count)s times|other": "janë dëbuar %(count)s herë", + "were unbanned %(count)s times|other": "janë dëbuar %(count)s herë", + "were kicked %(count)s times|other": "janë përzënë %(count)s herë", + "Which rooms would you like to add to this summary?": "Cilat dhoma do të donit të shtonit te kjo përmbledhje?", + "Community %(groupId)s not found": "S’u gjet bashkësia %(groupId)s", + "You seem to be uploading files, are you sure you want to quit?": "Duket se jeni duke ngarkuar kartela, jeni i sigurt se doni të dilet?", + "Click to unmute video": "Klikoni që të hiqet heshtja për videon", + "Click to unmute audio": "Klikoni që të hiqet heshtja për audion", + "Autocomplete Delay (ms):": "Vonesë Vetëplotësimi (ms):", + "Desktop specific": "Në desktop", + "click to reveal": "klikoni që të zbulohet", + "Call in Progress": "Thirrje në Kryerje e Sipër", + "A call is already in progress!": "Ka tashmë një thirrje në kryerje e sipër!", + "%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s ndryshoi emrin e tij në ekran si %(displayName)s.", + "%(senderName)s set the main address for this room to %(address)s.": "%(senderName)s caktoi %(address)s si adresë kryesore për këtë dhomë.", + "%(widgetName)s widget modified by %(senderName)s": "Widget-i %(widgetName)s u modifikua nga %(senderName)s", + "%(widgetName)s widget added by %(senderName)s": "Widget-i %(widgetName)s u shtua nga %(senderName)s", + "Always show encryption icons": "Shfaq përherë ikona fshehtëzimi", + "block-quote": "bllok citimi", + "bulleted-list": "listë me toptha", + "Add some now": "Shtohen ca tani", + "Click here to see older messages.": "Klikoni këtu për të parë mesazhe më të vjetër.", + "%(severalUsers)sjoined and left %(count)s times|other": "%(severalUsers)shynë dhe dolën %(count)s herë", + "%(severalUsers)sleft and rejoined %(count)s times|other": "%(severalUsers)sdolën dhe rihynë %(count)s herë", + "%(oneUser)sleft and rejoined %(count)s times|other": "%(oneUser)sdoli dhe rihyri %(count)s herë", + "%(severalUsers)schanged their name %(count)s times|other": "%(severalUsers)sndryshuan emrat e tyre %(count)s herë", + "%(oneUser)schanged their name %(count)s times|other": "%(oneUser)sndryshoi emrin e vet %(count)s herë", + "%(severalUsers)schanged their avatar %(count)s times|other": "%(severalUsers)sndryshuan avatarët e tyre %(count)s herë", + "%(oneUser)schanged their avatar %(count)s times|other": "%(oneUser)sndryshoi avatarin e vet %(count)s herë", + "Clear cache and resync": "Pastro fshehtinën dhe rinjëkohëso", + "Clear Storage and Sign Out": "Pastro Depon dhe Dil", + "COPY": "KOPJOJE", + "A phone number is required to register on this homeserver.": "Që të regjistroheni në këtë shërbyes home, lypset numër telefoni.", + "e.g. %(exampleValue)s": "p.sh., %(exampleValue)s", + "e.g. ": "p.sh., ", + "Permission Required": "Lypset Leje", + "Registration Required": "Lyp Regjistrim", + "This homeserver has hit its Monthly Active User limit.": "Ky shërbyes home ka tejkaluar kufirin e vet Përdorues Aktivë Mujorë.", + "This homeserver has exceeded one of its resource limits.": "Ky shërbyes home ka tejkaluar një nga kufijtë e tij mbi burimet.", + "Please contact your service administrator to continue using the service.": "Ju lutemi, që të vazhdoni të përdorni shërbimin, lidhuni me përgjegjësin e shërbimit tuaj.", + "Unable to connect to Homeserver. Retrying...": "S’u arrit të lidhej me shërbyesin Home. Po riprovohet…", + "Sorry, your homeserver is too old to participate in this room.": "Na ndjeni, shërbyesi juaj Home është shumë i vjetër për të marrë pjesë në këtë dhomë.", + "Please contact your homeserver administrator.": "Ju lutemi, lidhuni me përgjegjësin e shërbyesit tuaj Home.", + "Increase performance by only loading room members on first view": "Përmirësoni punimin duke ngarkuar anëtarë dhome vetëm kur sillen para syve", + "Send analytics data": "Dërgo të dhëna analitike", + "This event could not be displayed": "Ky akt s’u shfaq dot", + "Encrypting": "Fshehtëzim", + "Encrypted, not sent": "I fshehtëzuar, i padërguar", + "underlined": "nënvizuar", + "inline-code": "kod brendazi", + "numbered-list": "listë e numërtuar", + "The conversation continues here.": "Biseda vazhdon këtu.", + "System Alerts": "Sinjalizime Sistemi", + "Joining room...": "Po bëhet pjesë…", + "To notify everyone in the room, you must be a": "Që të njoftoni këdo te dhoma, duhet të jeni një", + "Muted Users": "Përdorues të Heshtur", + "Upgrade room to version %(ver)s": "Përmirësoni versionin e dhomës me versionin %(ver)s", + "Internal room ID: ": "ID e brendshme dhome: ", + "Room version number: ": "Numër versioni dhome: ", + "There is a known vulnerability affecting this room.": "Ka një cenueshmëri të njohur që ndikon në këtë dhomë.", + "Only room administrators will see this warning": "Këtë sinjalizim mund ta shohin vetëm përgjegjësit e dhomës", + "Hide Stickers": "Fshihi Ngjitësat", + "Show Stickers": "Shfaq Ngjitës", + "The email field must not be blank.": "Fusha email s’duhet të jetë e zbrazët.", + "The user name field must not be blank.": "Fusha emër përdoruesi s’duhet të jetë e zbrazët.", + "The phone number field must not be blank.": "Fusha numër telefoni s’duhet të jetë e zbrazët.", + "The password field must not be blank.": "Fusha fjalëkalim s’duhet të jetë e zbrazët.", + "Yes, I want to help!": "Po, dua të ndihmoj!", + "This homeserver has hit its Monthly Active User limit so some users will not be able to log in.": "Ky shërbyes home ka tejkaluar kufirin e vet të Përdoruesve Aktivë Mujorë, ndaj disa përdorues s’do të jenë në gjendje të bëjnë hyrjen.", + "This homeserver has exceeded one of its resource limits so some users will not be able to log in.": "Ky shërbyes home ka tejkaluar një nga kufijtë mbi burimet, ndaj disa përdorues s’do të jenë në gjendje të bëjnë hyrjen.", + "Failed to remove widget": "S’u arrit të hiqej widget-i", + "Reload widget": "Ringarkoje widget-in", + "Popout widget": "Widget flluskë", + "Picture": "Foto", + "Failed to indicate account erasure": "S’u arrit të tregohej fshirje llogarie", + "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.": "Dukshmëria e mesazheve në Matrix është e ngjashme me atë në email. Harrimi i mesazheve nga ana jonë do të thotë që mesazhet që keni dërguar nuk do të ndahen me çfarëdo përdoruesi të ri apo të paregjistruar, por përdoruesit e regjistruar, që kanë tashmë hyrje në këto mesazhe, do të kenë prapëseprapë hyrje te kopja e tyre.", + "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)": "Të lutem, harro krejt mesazhet që kamë dërguar, kur të çaktivizohet llogaria ime (Kujdes: kjo do të bëjë që përdorues të ardhshëm të shohin një pamje jo të plotë të bisedave)", + "To continue, please enter your password:": "Që të vazhdohet, ju lutemi, jepni fjalëkalimin tuaj:", + "password": "fjalëkalim", + "Incompatible local cache": "Fshehtinë vendore e papërputhshme", + "Updating Riot": "Riot-i po përditësohet", + "Failed to upgrade room": "S’u arrit të përmirësohej dhoma", + "The room upgrade could not be completed": "Përmirësimi i dhomës s’u plotësua", + "Upgrade Room Version": "Përmirësoni Versionin e Dhomës", + "Send Logs": "Dërgo regjistra", + "Refresh": "Rifreskoje", + "Link to most recent message": "Lidhje për te mesazhet më të freskët", + "Link to selected message": "Lidhje për te mesazhi i përzgjedhur", + "Join this community": "Bëhuni pjesë e kësaj bashkësie", + "Leave this community": "Braktiseni këtë bashkësi", + "Who can join this community?": "Cilët mund të bëhen pjesë e kësaj bashkësie?", + "Everyone": "Cilido", + "Terms and Conditions": "Terma dhe Kushte", + "To continue using the %(homeserverDomain)s homeserver you must review and agree to our terms and conditions.": "Që të vazhdohet të përdoret shërbyesi home %(homeserverDomain)s, duhet të shqyrtoni dhe pajtoheni me termat dhe kushtet.", + "Review terms and conditions": "Shqyrtoni terma & kushte", + "Failed to reject invite": "S’u arrit të hidhet tej ftesa", + "Submit Debug Logs": "Parashtro Regjistra Diagnostikimi", + "Legal": "Ligjore", + "Please contact your service administrator to continue using this service.": "Ju lutemi, që të vazhdoni të përdorni këtë shërbim, lidhuni me përgjegjësin e shërbimit tuaj.", + "Try the app first": "Së pari, provoni aplikacionin", + "Open Devtools": "Hapni Mjete Zhvilluesi", + "Show developer tools": "Shfaq mjete zhvilluesi", + "Your User Agent": "Agjent Përdoruesi i Juaj", + "Your device resolution": "Qartësi e pajisjes tuaj", + "A call is currently being placed!": "Është duke u bërë një thirrje!", + "You do not have permission to start a conference call in this room": "S’keni leje për të nisur një thirrje konferencë këtë në këtë dhomë", + "Missing roomId.": "Mungon roomid.", + "%(senderName)s added %(count)s %(addedAddresses)s as addresses for this room.|one": "%(senderName)s shtoi %(addedAddresses)s si një adresë për këtë dhomë.", + "%(senderName)s removed %(count)s %(removedAddresses)s as addresses for this room.|other": "%(senderName)s hoqi %(removedAddresses)s si adresa për këtë dhomë.", + "%(senderName)s removed %(count)s %(removedAddresses)s as addresses for this room.|one": "%(senderName)s hoqi %(removedAddresses)s si adresë për këtë dhomë.", + "%(senderName)s removed the main address for this room.": "%(senderName)s hoqi adresën kryesore për këtë dhomë.", + "deleted": "u fshi", + "This room has been replaced and is no longer active.": "Kjo dhomë është zëvendësuar dhe s’është më aktive.", + "At this time it is not possible to reply with an emote.": "Sot për sot s’është e mundur të përgjigjeni me një emote.", + "Share room": "Ndani dhomë me të tjerë", + "Drop here to demote": "Hidheni këtu t’i ulet përparësia", + "You don't currently have any stickerpacks enabled": "Hëpërhë, s’keni të aktivizuar ndonjë pako ngjitësesh", + "%(senderDisplayName)s changed the room avatar to ": "%(senderDisplayName)s ndryshoi avatarin e dhomës në ", + "This room is a continuation of another conversation.": "Kjo dhomë është një vazhdim i një bisede tjetër.", + "%(oneUser)sjoined and left %(count)s times|other": "%(oneUser)shyri dhe doli %(count)s herë", + "%(severalUsers)srejected their invitations %(count)s times|other": "%(severalUsers)shodhën poshtë ftesat e tyre %(count)s herë", + "%(oneUser)srejected their invitation %(count)s times|other": "%(oneUser)shodhi poshtë ftesën e vet %(count)s herë", + "%(severalUsers)shad their invitations withdrawn %(count)s times|other": "Për %(severalUsers)s u hodhën poshtë ftesat e tyre %(count)s herë", + "%(oneUser)shad their invitation withdrawn %(count)s times|other": "Për %(oneUser)s përdorues ftesa u tërhoq mbrapsht %(count)s herë", + "%(oneUser)shad their invitation withdrawn %(count)s times|one": "U tërhoq mbrapsht ftesa për %(oneUser)s", + "What GitHub issue are these logs for?": "Për cilat çështje në GitHub janë këta regjistra?", + "Community IDs cannot be empty.": "ID-të e bashkësisë s’mund të jenë të zbrazëta.", + "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.": "Kjo do ta bëjë llogarinë tuaj përgjithmonë të papërdorshme. S’do të jeni në gjendje të hyni në llogarinë tuaj, dhe askush s’do të jetë në gjendje të riregjistrojë të njëjtën ID përdoruesi. Kjo do të shkaktojë daljen e llogarisë tuaj nga krejt dhomat ku merrni pjesë, dhe do të heqë hollësitë e llogarisë tuaj nga shërbyesi juaj i identiteteve. Ky veprim është i paprapakthyeshëm.", + "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.": "Çaktivizimi i llogarisë tuaj nuk shkakton, si parazgjedhje, harrimin nga ne të mesazheve që keni dërguar. Nëse do të donit të harrojmë mesazhet tuaja, ju lutemi, i vini shenjë kutizës më poshtë.", + "Upgrade this room to version %(version)s": "Përmirësojeni këtë dhomë me versionin %(version)s", + "Share Room": "Ndani Dhomë Me të Tjerë", + "Share Community": "Ndani Bashkësi Me të Tjerë", + "Share Room Message": "Ndani Me të Tjerë Mesazh Dhome", + "Share Message": "Ndani Mesazh me të tjerë", + "Collapse Reply Thread": "Tkurre Rrjedhën e Përgjigjeve", + "Failed to add the following users to the summary of %(groupId)s:": "S’u arrit të ftoheshin përdoruesit vijues te përmbledhja e %(groupId)s:", + "Unable to join community": "S’arrihet të bëhet pjesë e bashkësisë", + "Unable to leave community": "S’arrihet të braktiset bashkësia", + "Lazy loading members not supported": "Nuk mbulohet lazy-load për anëtarët", + "An email address is required to register on this homeserver.": "Që të regjistroheni në këtë shërbyes home, lypset një adresë email.", + "Claimed Ed25519 fingerprint key": "U pretendua për shenja gishtash Ed25519", + "Every page you use in the app": "Çdo faqe që përdorni te aplikacioni", + "A conference call could not be started because the intgrations server is not available": "S’u nis dot një thirrje konferencë, ngaqë shërbyesi i integrimit s’është i kapshëm", + "Changes colour scheme of current room": "Ndryshon skemë e ngjyrave të dhomës së tanishme", + "%(senderName)s added %(count)s %(addedAddresses)s as addresses for this room.|other": "%(senderName)s shtoi %(addedAddresses)s si adresa për këtë dhomë.", + "%(senderName)s added %(addedAddresses)s and removed %(removedAddresses)s as addresses for this room.": "%(senderName)s shtoi %(addedAddresses)s dhe hoqi %(removedAddresses)s si adresa për këtë dhomë.", + "%(senderName)s turned on end-to-end encryption (algorithm %(algorithm)s).": "%(senderName)s aktivizoi fshehtëzimin skaj-më-skaj (algorithm %(algorithm)s).", + "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s nga %(fromPowerLevel)s në %(toPowerLevel)s", + "%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s ndryshoi shkallën e pushtetit të %(powerLevelDiffText)s.", + "%(senderName)s changed the pinned messages for the room.": "%(senderName)s ndryshoi mesazhin e fiksuar për këtë dhomë.", + "Hide join/leave messages (invites/kicks/bans unaffected)": "Fshihi mesazhet e hyrjeve/daljeve (kjo nuk prek mesazhe ftesash/përzëniesh/dëbimesh)", + "Enable automatic language detection for syntax highlighting": "Aktivizo pikasje të vetvetishme të gjuhës për theksim sintakse", + "Hide avatars in user and room mentions": "Fshihi avatarët në përmendje përdoruesish dhe dhomash", + "Automatically replace plain text Emoji": "Zëvendëso automatikisht emotikone tekst të thjeshtë me Emoji", + "Enable URL previews for this room (only affects you)": "Aktivizo paraparje URL-sh për këtë dhomë (prek vetëm ju)", + "Enable URL previews by default for participants in this room": "Aktivizo, si parazgjedhje, paraparje URL-sh për pjesëmarrësit në këtë dhomë", + "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains": "Te +%(msisdn)s u dërgua një mesazh tekst. Ju lutemi, verifikoni kodin që përmban", + "At this time it is not possible to reply with a file so this will be sent without being a reply.": "Sot për sot s’është e mundur të përgjigjeni me një kartelë, ndaj kjo do të dërgohet pa qenë një përgjigje.", + "A text message has been sent to %(msisdn)s": "Te %(msisdn)s u dërgua një mesazh tekst", + "Failed to remove '%(roomName)s' from %(groupId)s": "S’u arrit të hiqej '%(roomName)s' nga %(groupId)s", + "Do you want to load widget from URL:": "Doni të ngarkohet widget nga URL-ja:", + "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "Fshirja e një widget-i e heq atë për krejt përdoruesit në këtë dhomë. Jeni i sigurt se doni të fshihet ky widget?", + "An error ocurred whilst trying to remove the widget from the room": "Ndodhi një gabim teksa provohej të hiqej widget-i nga dhoma", + "%(nameList)s %(transitionList)s": "%(nameList)s %(transitionList)s", + "Before submitting logs, you must create a GitHub issue to describe your problem.": "Përpara se të parashtroni regjistra, duhet të krijoni një çështje në GitHub issue që të përshkruani problemin tuaj.", + "Create a new chat or reuse an existing one": "Krijoni një fjalosje të re ose përdorni një ekzistuese", + "Community IDs may only contain characters a-z, 0-9, or '=_-./'": "ID-të e bashkësive mund të përmbajnë vetëm shenjat a-z, 0-9, ose '=_-./'", + "Block users on other matrix homeservers from joining this room": "Bllokoju hyrjen në këtë dhomë përdoruesve në shërbyes të tjerë Matrix home", + "Create a new room with the same name, description and avatar": "Krijoni një dhomë të re me po atë emër, përshkrim dhe avatar", + "\"%(RoomName)s\" contains devices that you haven't seen before.": "\"%(RoomName)s\" përmban pajisje që s’i keni parë më parë.", + "

    HTML for your community's page

    \n

    \n Use the long description to introduce new members to the community, or distribute\n some important links\n

    \n

    \n You can even use 'img' tags\n

    \n": "

    HTML për faqen e bashkësisë tuaj

    \n

    \n Përshkrimin e gjatë përdoreni për t’u paraqitur përdoruesve të rinj bashkësinë, ose për të dhënë\n një a disa lidhje të rëndësishme\n

    \n

    \n Mund të përdorni madje etiketa 'img'\n

    \n", + "Failed to add the following rooms to the summary of %(groupId)s:": "S’u arrit të shtoheshin dhomat vijuese te përmbledhja e %(groupId)s:", + "Failed to remove the room from the summary of %(groupId)s": "S’u arrit të hiqej dhoma prej përmbledhjes së %(groupId)s", + "Failed to remove a user from the summary of %(groupId)s": "S’u arrit të hiqej një përdorues nga përmbledhja e %(groupId)s", + "Changes made to your community name and avatar might not be seen by other users for up to 30 minutes.": "Ndryshimet e bëra te emri dhe avatari i bashkësisë tuaj mund të mos shihen nga përdoruesit e tjera para deri 30 minutash.", + "Can't leave Server Notices room": "Dhoma Njoftime Shërbyesi, s’braktiset dot", + "For security, this session has been signed out. Please sign in again.": "Për hir të sigurisë, është bërë dalja nga ky sesion. Ju lutemi, ribëni hyrjen.", + "Data from an older version of Riot has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.": "Janë pikasur të dhëna nga një version i dikurshëm i Riot-it. Kjo do të bëjë që kriptografia skaj-më-skaj te versioni i dikurshëm të mos punojë si duhet. Mesazhet e fshehtëzuar skaj-më-skaj tani së fundi teksa përdorej versioni i dikurshëm mund të mos jenë të shfshehtëzueshëm në këtë version. Kjo mund bëjë edhe që mesazhet e shkëmbyera me këtë version të dështojnë. Nëse ju dalin probleme, bëni daljen dhe rihyni në llogari. Që të ruhet historiku i mesazheve, eksportoni dhe ri-importoni kyçet tuaj.", + "Did you know: you can use communities to filter your Riot.im experience!": "E dinit se: mund t’i përdorni bashkësitë për të filtruar punimin tuaj në Riot.im?", + "Error whilst fetching joined communities": "Gabim teksa silleshin bashkësitë ku merret pjesë", + "Show devices, send anyway or cancel.": "Shfaq pajisje, dërgoje sido qoftë ose anuloje.", + "%(count)s Resend all or cancel all now. You can also select individual messages to resend or cancel.|other": "Ridërgojini krejt ose anulojini krejt tani. Për ridërgim ose anulim, mundeni edhe të përzgjidhni mesazhe individualë.", + "%(count)s Resend all or cancel all now. You can also select individual messages to resend or cancel.|one": "Ridërgojeni mesazhin ose anulojeni mesazhin tani.", + "For security, logging out will delete any end-to-end encryption keys from this browser. If you want to be able to decrypt your conversation history from future Riot sessions, please export your room keys for safe-keeping.": "Për hir të sigurisë, dalja nga llogaria do të sjellë fshirjen në këtë shfletues të çfarëdo kyçesh fshehtëzimi skaj-më-skaj. Nëse doni të jeni në gjendje të fshehtëzoni historikun e bisedave tuaja që nga sesione të ardhshëm Riot, ju lutemi, eksportoni kyçet tuaj të dhomës, për t’i ruajtur të parrezikuar diku.", + "Audio Output": "Sinjal Audio", + "Error: Problem communicating with the given homeserver.": "Gabimr: Problem komunikimi me shërbyesin e dhënë Home.", + "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or enable unsafe scripts.": "S’lidhet dot te shërbyes Home përmes HTTP-je, kur te shtylla e shfletuesit tuaj jepet një URL HTTPS. Ose përdorni HTTPS-në, ose aktivizoni përdorimin e programtheve jo të sigurt.", + "Can't connect to homeserver - please check your connectivity, ensure your homeserver's SSL certificate is trusted, and that a browser extension is not blocking requests.": "S’lidhet dot te shërbyes Home - ju lutemi, kontrolloni lidhjen tuaj, sigurohuni që dëshmia SSL e shërbyesit tuaj Home besohet, dhe që s’ka ndonjë zgjerim shfletuesi që po bllokon kërkesat tuaja.", + "Failed to remove tag %(tagName)s from room": "S’u arrit të hiqej etiketa %(tagName)s nga dhoma", + "Failed to add tag %(tagName)s to room": "S’u arrit të shtohej në dhomë etiketa %(tagName)s", + "Pin unread rooms to the top of the room list": "Fiksoji dhomat e palexuara në krye të listës së dhomave", + "Pin rooms I'm mentioned in to the top of the room list": "Fiksoji dhomat ku përmendem në krye të listës së dhomave", + "Enable widget screenshots on supported widgets": "Aktivizo foto ekrani widget-esh për widget-e që e mbulojnë", + "Changing password will currently reset any end-to-end encryption keys on all devices, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "Ndryshimi i fjalëkalimit do të sjellë zerimin e çfarëdo kyçesh fshehtëzimi skaj-më-skaj në krejt pajisjet, duke e bërë të palexueshëm historikun e fshehtëzuar të bisedave, hiq rastin kur i eksportoni më parë kyçet tuaj të dhomës dhe i ri-importoni ata më pas. Në të ardhmen kjo do të përmirësohet.", + "Join as voice or video.": "Merrni pjesë me ose me video.", + "Key share requests are sent to your other devices automatically. If you rejected or dismissed the key share request on your other devices, click here to request the keys for this session again.": "Kërkesat për ndarje kyçesh dërgohen automatikisht te pajisjet tuaja të tjera. Nëse s’e pranuat ose e hodhët tej kërkesën për ndarje kyçesh në pajisjet tuaja të tjera, klikoni këtu që të rikërkoni kyçe për këtë sesion.", + "If your other devices do not have the key for this message you will not be able to decrypt them.": "Nëse pajisjet tuaja të tjera nuk kanë kyçin për këtë mesazh, s’do të jeni në gjendje ta shfshehtëzoni.", + "Demote yourself?": "Të zhgradohet vetvetja?", + "Demote": "Zhgradoje", + "Failed to toggle moderator status": "S’u arrit të këmbehet gjendje moderatori", + "Server unavailable, overloaded, or something else went wrong.": "Shërbyesi është i pakapshëm, i mbingarkuar, ose diç tjetër shkoi ters.", + "Drop here to tag %(section)s": "Hidheni këtu që të caktohet etiketë për %(section)s", + "Press to start a chat with someone": "Shtypni që të nisni një bisedë me dikë", + "No users have specific privileges in this room": "S’ka përdorues me privilegje të caktuara në këtë dhomë", + "Guests cannot join this room even if explicitly invited.": "Vizitorët s’mund të marrin pjesë në këtë edhe po të jenë ftuar shprehimisht.", + "Publish this room to the public in %(domain)s's room directory?": "Të bëhet publike kjo dhomë te drejtoria e dhomave %(domain)s?", + "Click here to upgrade to the latest room version and ensure room integrity is protected.": "Klikoni këtu që ta përmirësoni me versionin më të ri të dhomë dhe të garantoni mbrojtjen e paprekshmërisë së dhomës.", + "In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.": "Në dhoma të fshehtëzuara, si kjo, paraparja e URL-ve është e çaktivizuar, si parazgjedhje, për të garantuar që shërbyesi juaj home (ku edhe prodhohen paraparjet) të mos grumbullojë të dhëna rreth lidhjesh që shihni në këtë dhomë.", + "Please review and accept the policies of this homeserver:": "Ju lutemi, shqyrtoni dhe pranoni rregullat e këtij shërbyesi home:", + "If you don't specify an email address, you won't be able to reset your password. Are you sure?": "Nëse nuk përcaktoni një adresë email, s’do të jeni në gjendje të bëni ricaktime të fjalëkalimit tuaj. Jeni i sigurt?", + "Removing a room from the community will also remove it from the community page.": "Heqja e një dhome nga bashkësia do ta heqë atë edhe nga faqja e bashkësisë.", + "Please help improve Riot.im by sending anonymous usage data. This will use a cookie (please see our Cookie Policy).": "Ju lutemi, ndihmoni të përmirësohet Riot.im duke dërguar të dhëna anonime përdorimi. Për këtë do të përdoret një cookie (ju lutemi, shihni Rregullat tona mbi Cookie-t).", + "Please help improve Riot.im by sending anonymous usage data. This will use a cookie.": "Ju lutemi, ndihmoni të përmirësohet Riot.im duke dërguar të dhëna anonime përdorimi. Për këtë do të përdoret një cookie.", + "Please contact your service administrator to get this limit increased.": "Ju lutemi, që të shtohet ky kufi, lidhuni me përgjegjësin e shërbimit.", + "If the other version of Riot is still open in another tab, please close it as using Riot on the same host with both lazy loading enabled and disabled simultaneously will cause issues.": "Nëse versioni tjetër i Riot-it është ende i hapur në një skedë tjetër, ju lutemi, mbylleni, ngaqë përdorimi njëkohësisht i Riot-it në të njëjtën strehë, në njërën anë me lazy loading të aktivizuar dhe në anën tjetër të çaktivizuar do të shkaktojë probleme.", + "Riot now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!": "Riot-i tani përdor 3 deri 5 herë më pak kujtesë, duke ngarkuar të dhëna mbi përdorues të tjerë vetëm kur duhen. Ju lutemi, prisni, teksa njëkohësojmë të dhënat me shërbyesin!", + "Put a link back to the old room at the start of the new room so people can see old messages": "Vendosni në krye të dhomës së re një lidhje për te dhoma e vjetër, që njerëzit të mund të shohin mesazhet e vjetër", + "Log out and remove encryption keys?": "Të dilet dhe të hiqen kyçet e fshehtëzimit?", + "If you have previously used a more recent version of Riot, your session may be incompatible with this version. Close this window and return to the more recent version.": "Nëse më herët keni përdorur një version më të freskët të Riot-it, sesioni juaj mund të jetë i papërputhshëm me këtë version. Mbylleni këtë dritare dhe kthehuni te versioni më i ri.", + "Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.": "Pastrimi i gjërave të depozituara në shfletuesin tuaj mund ta ndreqë problemin, por kjo do të sjellë nxjerrjen tuaj nga llogari dhe do ta bëjë të palexueshëm çfarëdo historiku të fshehtëzuar të bisedës.", + "If you would like to create a Matrix account you can register now.": "Nëse do të donit të krijoni një llogari Matrix, mund të regjistroheni që tani.", + "If you already have a Matrix account you can log in instead.": "Nëse keni tashmë një llogari Matrix, mund të bëni hyrjen.", + "Share message history with new users": "Ndani me përdorues të rinj historik mesazhesh", + "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Krijoni një bashkësi që bëni tok përdorues dhe dhoma! Krijoni një faqe hyrëse vetjake, që të ravijëzoni hapësirën tuaj në universin Matrix.", + "Sent messages will be stored until your connection has returned.": "Mesazhet e dërguar do të depozitohen deri sa lidhja juaj të jetë rikthyer.", + "Server may be unavailable, overloaded, or the file too big": "Shërbyesi mund të jetë i pakapshëm, i mbingarkuar, ose kartela është shumë e madhe", + "Server may be unavailable, overloaded, or search timed out :(": "Shërbyesi mund të jetë i pakapshëm, i mbingarkuar, ose kërkimit i mbaroi koha :(", + "If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Nëse parashtruar një të metë përmes GitHub-it, regjistrat e diagnostikimit mund të na ndihmojnë të ndjekim problemin. Regjistrat e diagnostikimit përmbajnë të dhëna përdorimi, përfshi emrin tuaj të përdoruesit, ID-të ose aliaset e dhomave apo grupeve që keni vizituar dhe emrat e përdoruesve të përdoruesve të tjerë. Në to nuk përmbahen mesazhet.", + "Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.": "Privatësia është e rëndësishme për ne, ndaj nuk grumbullojmë ndonjë të dhënë personale apo të identifikueshme për analizat tona.", + "Learn more about how we use analytics.": "Mësoni më tepër se si i përdorim analizat.", + "Lazy loading is not supported by your current homeserver.": "Lazy loading nuk mbulohet nga shërbyesi juaj i tanishëm Home.", + "Reject all %(invitedRooms)s invites": "Mos prano asnjë ftesë për në %(invitedRooms)s", + "Missing Media Permissions, click here to request.": "Mungojnë Leje Mediash, klikoni këtu që të kërkohen.", + "New passwords must match each other.": "Fjalëkalimet e rinj duhet të përputhen me njëri-tjetrin.", + "Please note you are logging into the %(hs)s server, not matrix.org.": "Ju lutemi, kini parasysh se jeni futur te shërbyesi %(hs)s, jo te matrix.org.", + "Guest access is disabled on this Home Server.": "Në këtë shërbyes Home është çaktivizuar hyrja si vizitor.", + "Password too short (min %(MIN_PASSWORD_LENGTH)s).": "Fjalëkalim shumë i shkurtër (minimumi %(MIN_PASSWORD_LENGTH)s).", + "You need to register to do this. Would you like to register now?": "Për ta bërë këtë, lypset të regjistroheni. Doni të regjistroheni që tani?", + "Stops ignoring a user, showing their messages going forward": "Resht shpërfilljen e një përdoruesi, duke i shfaqur mesazhet e tij të dërgohen", + "Verifies a user, device, and pubkey tuple": "Verifikon një përdorues, pajisje dhe një set kyçesh publikë", + "WARNING: Device already verified, but keys do NOT MATCH!": "KUJDES: Pajisje tashmë e verifikuar, por kyçet NUK PËRPUTHEN!", + "WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!": "KUJDES: VERIFIKIMI I KYÇIT DËSHTOI! Kyçi i nënshkrimit për %(userId)s dhe pajisjen %(deviceId)s është \"%(fprint)s\", që nuk përpythet me kyçin e dhënë \"%(fingerprint)s\". Kjo mund të jetë shenjë se komunikimet tuaja po përgjohen!", + "The signing key you provided matches the signing key you received from %(userId)s's device %(deviceId)s. Device marked as verified.": "Kyçi i nënshkrimit që dhatë përputhet me kyçin e nënshkrimit që morët nga pajisja e %(userId)s %(deviceId)s. Pajisja u shënua si e verifikuar.", + "Your browser does not support the required cryptography extensions": "Shfletuesi juaj nuk mbulon zgjerimet kriptografike të domosdoshme", + "Show timestamps in 12 hour format (e.g. 2:30pm)": "Vulat kohore shfaqi në formatin 12 orësh (p.sh. 2:30pm)", + "Enable inline URL previews by default": "Aktivizo, si parazgjedhje, paraparje URL-sh brendazi", + "Your home server does not support device management.": "Shërbyesi juaj Home nuk mbulon administrim pajisjesh.", + "The maximum permitted number of widgets have already been added to this room.": "Në këtë dhomë është shtuar tashmë numri maksimum i lejuar për widget-et.", + "Your key share request has been sent - please check your other devices for key share requests.": "Kërkesa juaj për shkëmbim kyçesh u dërgua - ju lutemi, kontrolloni pajisjet tuaja të tjera për kërkesa shkëmbimi kyçesh.", + "Undecryptable": "I pafshehtëzueshëm", + "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.": "S’do të jeni në gjendje ta zhbëni këtë, ngaqë po zhgradoni veten, nëse jeni përdoruesi i fundit i privilegjuar te dhoma do të jetë e pamundur të rifitoni privilegjet.", + "Jump to read receipt": "Hidhuni te leximi i faturës", + "Unable to reply": "S’arrihet të përgjigjet", + "Unknown for %(duration)s": "I panjohur për %(duration)s", + "You're not in any rooms yet! Press to make a room or to browse the directory": "S’jeni ende në ndonjë dhomë! Shtypni që të krijoni një dhomë ose që të shfletoni drejtorinë", + "Unable to ascertain that the address this invite was sent to matches one associated with your account.": "S’arrihet të sigurohet që adresa prej nga qe dërguar kjo ftesë përputhet me atë përshoqëruar llogarisë tuaj.", + "This invitation was sent to an email address which is not associated with this account:": "Kjo ftesë qe dërguar për një adresë email e cila nuk i përshoqërohet kësaj llogarie:", + "Would you like to accept or decline this invitation?": "Do të donit ta pranoni apo hidhni tej këtë ftesë?", + "To remove other users' messages, you must be a": "Që të hiqni mesazhe përdoruesish të tjerë, duhet të jeni një", + "This room is not accessible by remote Matrix servers": "Kjo dhomë nuk është e përdorshme nga shërbyes Matrix të largët", + "To send events of type , you must be a": "Që të dërgoni akte të llojit , duhet të jeni", + "This room version is vulnerable to malicious modification of room state.": "Ky version i dhomës është i cenueshëm nga modifikime dashakaqe të gjendjes së dhomës.", + "Stickerpack": "Paketë ngjitësish", + "You have enabled URL previews by default.": "E keni aktivizuar, si parazgjedhje, paraparjen e URL-ve.", + "You have disabled URL previews by default.": "E keni çaktivizuar, si parazgjedhje, paraparjen e URL-ve.", + "URL previews are enabled by default for participants in this room.": "Për pjesëmarrësit në këtë dhomë paraparja e URL-ve është e aktivizuar, si parazgjedhje.", + "URL previews are disabled by default for participants in this room.": "Për pjesëmarrësit në këtë dhomë paraparja e URL-ve është e çaktivizuar, si parazgjedhje.", + "When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.": "Kur dikush vë një URL në mesazh, për të dhënë rreth lidhjes më tepër të dhëna, të tilla si titulli, përshkrimi dhe një figurë e sajtit, do të shfaqet një paraparje e URL-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?": "Ju ndan një hap nga shpënia te një sajt palë e tretë, që kështu të mund të mirëfilltësoni llogarinë tuaj me %(integrationsUrl)s. Doni të vazhdohet?", + "This allows you to use this app with an existing Matrix account on a different home server.": "Kjo ju lejon ta përdorni këtë aplikacion me një llogari Matrix ekxistuese në një shërbyes tjetër Home.", + "You can also set a custom identity server but this will typically prevent interaction with users based on email address.": "Mund të ujdisni edhe një shërbyes vetjak identitetesh, por kjo normalisht do të pengojë ndërveprim mes përdoruesish bazuar në adresë email.", + "The visibility of '%(roomName)s' in %(groupId)s could not be updated.": "Dukshmëria e '%(roomName)s' te %(groupId)s s’u përditësua dot.", + "Something went wrong when trying to get your communities.": "Diç shkoi ters teksa provohej të merreshin bashkësitë tuaja.", + "Warning: This widget might use cookies.": "Kujdes: Ky widget mund të përdorë cookies.", + "Unable to load event that was replied to, it either does not exist or you do not have permission to view it.": "S’arrihet të ngarkohet akti të cilit iu përgjigj, ose nuk ekziston, ose s’keni leje ta shihni.", + "Try using one of the following valid address types: %(validTypesList)s.": "Provoni të përdorni një nga llojet e vlefshme të adresave më poshtë: %(validTypesList)s.", + "You already have existing direct chats with this user:": "Keni tashmë fjalosje të drejtpërdrejta me këtë përdorues:", + "Something went wrong whilst creating your community": "Diç shkoi ters teksa krijohej bashkësia juaj", + "You've previously used Riot on %(host)s with lazy loading of members enabled. In this version lazy loading is disabled. As the local cache is not compatible between these two settings, Riot needs to resync your account.": "Më parë përdornit Riot në %(host)s me lazy loading anëtarësh të aktivizuar. Në këtë version lazy loading është çaktivizuar. Ngaqë fshehtina vendore s’është e përputhshme mes këtyre dy rregullimeve, Riot-i lyp të rinjëkohësohet llogaria juaj.", + "Upgrading this room requires closing down the current instance of the room and creating a new room it its place. To give room members the best possible experience, we will:": "Përmirësimi i kësaj dhome lyp mbylljen e instancës së tanishme të dhomës dhe krijimin në vend të saj të një dhome të re. Për t’u dhënë anëtareve të dhomës më të mirën e mundshme, do të:", + "Update any local room aliases to point to the new room": "Përditësoni çfarëdo aliasesh dhomash vendore që të shpien te dhoma e re", + "Stop users from speaking in the old version of the room, and post a message advising users to move to the new room": "Ndalojuni përdoruesve të flasin në versionin e vjetër të dhomës, dhe postoni një mesazh që u këshillon atyre të hidhen te dhoma e re", + "We encountered an error trying to restore your previous session.": "Hasëm një gabim teksa provohej të rikthehej sesioni juaj i dikurshëm.", + "This will allow you to reset your password and receive notifications.": "Kjo do t’ju lejojë të ricaktoni fjalëkalimin tuaj dhe të merrni njoftime.", + "This will be your account name on the homeserver, or you can pick a different server.": "Ky do të jetë emri i llogarisë tuaj te shërbyesi home, ose mund të zgjidhni një shërbyes tjetër.", + "You are currently using Riot anonymously as a guest.": "Hëpërhë po e përdorni Riot-in në mënyrë anonime, si një vizitor.", + "You are currently blacklisting unverified devices; to send messages to these devices you must verify them.": "Po kaloni në listë të zezë pajisje të paverifikuara; që të dërgoni mesazhe te këto pajisje, duhet t’i verifikoni.", + "We recommend you go through the verification process for each device to confirm they belong to their legitimate owner, but you can resend the message without verifying if you prefer.": "Këshillojmë të përshkoni procesin e verifikimit për çdo pajisje, që t’u bindur se u takojnë të zotëve të ligjshëm, por, nëse parapëlqeni, mund ta dërgoni mesazhin pa verifikuar gjë.", + "You must join the room to see its files": "Duhet të hyni në dhomë, pa të shihni kartelat e saj", + "The room '%(roomName)s' could not be removed from the summary.": "Dhoma '%(roomName)s' s’u hoq dot nga përmbledhja.", + "The user '%(displayName)s' could not be removed from the summary.": "Përdoruesi '%(displayName)s' s’u hoq dot nga përmbledhja.", + "You are an administrator of this community. You will not be able to rejoin without an invite from another administrator.": "Jeni një përgjegjës i kësaj bashkësie. S’do të jeni në gjendje të rihyni pa një ftesë nga një tjetër përgjegjës.", + "These rooms are displayed to community members on the community page. Community members can join the rooms by clicking on them.": "Këto dhoma u shfaqen anëtarëve të bashkësisë te faqja e bashkësisë. Anëtarët e bashkësisë mund të marrin pjesë në dhoma duke klikuar mbi to.", + "Your community hasn't got a Long Description, a HTML page to show to community members.
    Click here to open settings and give it one!": "Bashkësia juaj s’ka ndonjë Përshkrim të Gjatë, një faqe HTML për t’ua shfaqur anëtarëve të bashkësisë.
    Klikoni këtu që të hapni rregullimet dhe t’i krijoni një të tillë!", + "This room is not public. You will not be able to rejoin without an invite.": "Kjo dhomë s’është publike. S’do të jeni në gjendje të rihyni në të pa një ftesë.", + "This room is used for important messages from the Homeserver, so you cannot leave it.": "Kjo dhomë përdoret për mesazhe të rëndësishëm nga shërbyesi Home, ndaj s’mund ta braktisni.", + "To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "Që të ndërtoni një filtër, tërhiqeni avatarin e një bashkësie te paneli i filtrimeve në skajin e majtë të ekranit. Për të parë vetëm dhomat dhe personat e përshoqëruar asaj bashkësie, mund të klikoni në çfarëdo kohe mbi një avatar te panelit të filtrimeve.", + "You can't send any messages until you review and agree to our terms and conditions.": "S’mund të dërgoni ndonjë mesazh, përpara se të shqyrtoni dhe pajtoheni me termat dhe kushtet tona.", + "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please contact your service administrator to continue using the service.": "Mesazhi juaj s’u dërgua, ngaqë ky shërbyes Home ka mbërritur në Kufirin Mujor të Përdoruesve Aktivë. Ju lutemi, që të vazhdoni ta përdorni këtë shërbim, lidhuni me përgjegjësin e shërbimit tuaj.", + "Your message wasn't sent because this homeserver has exceeded a resource limit. Please contact your service administrator to continue using the service.": "Mesazhi juaj s’u dërgua, ngaqë ky shërbyes Home ka tejkaluar kufirin e një burimi. Ju lutemi, që të vazhdoni ta përdorni këtë shërbim, lidhuni me përgjegjësin e shërbimit tuaj.", + "There's no one else here! Would you like to invite others or stop warning about the empty room?": "S’ka njeri këtu! Do të donit të ftoni të tjerë apo të reshtet së njoftuari për dhomë të zbrazët?", + "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "U provua të ngarkohej një pikë e caktuar në kronologjinë e kësaj dhome, por nuk keni leje për ta parë mesazhin në fjalë.", + "Your password was successfully changed. You will not receive push notifications on other devices until you log back in to them": "Fjalëkalimi juaj u ndryshua me sukses. Nuk do të merrni njoftime push në pajisjet tuaja të tjera, veç në hyfshi sërish në llogarinë tuaj në to", + "Start automatically after system login": "Nisu vetvetiu pas hyrjes në sistem", + "You may need to manually permit Riot to access your microphone/webcam": "Lypset të lejoni dorazi Riot-in të përdorë mikrofonin/kamerën tuaj web", + "No Audio Outputs detected": "S’u pikasën Sinjale Audio Në Dalje", + "Resetting password will currently reset any end-to-end encryption keys on all devices, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "Ricaktimi i fjalëkalimit do të shkaktojë në fakt edhe zerimin e çfarëdo kyçi fshehtëzimesh skaj-më-skaj në krejt pajisjet, duke e bërë kështu të palexueshëm historikun e bisedës së fshehtëzuar, veç në paçi eksportuar më parë kyçet e dhomës tuaj dhe i rim-importoni më pas. Në të ardhmen kjo punë do të përmirësohet.", + "You have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, sign in again on each device": "Jeni nxjerrë jashtë krejt pajisjeve dhe nuk do të merrni më njoftime push. Që të riaktivizoni njoftimet, bëni sërish hyrjen në çdo pajisje", + "This Home Server does not support login using email address.": "Ky shërbyes Home nuk mbulon hyrje përmes adresash email.", + "This homeserver doesn't offer any login flows which are supported by this client.": "Ky shërbyes home nuk ofron ndonjë mënyrë hyrjesh që mbulohet nga ky klient.", + "Unable to query for supported registration methods": "S’arrihet të kërkohet për metoda regjistrimi që mbulohen", + "The exported file will allow anyone who can read it to decrypt any encrypted messages that you can see, so you should be careful to keep it secure. To help with this, you should enter a passphrase below, which will be used to encrypt the exported data. It will only be possible to import the data by using the same passphrase.": "Kartela e eksportuar do t’i lejojë kujtdo që e lexon të shfshehtëzojë çfarëdo mesazhesh të fshehtëzuar që mund të shihni, ndaj duhet të jeni i kujdesshëm për ta mbajtur të parrezikuar. Si ndihmë për këtë, duhet të jepni më poshtë një frazëkalim, që do të përdoret për të fshehtëzuar të dhënat e eksportuara. Importimi i të dhënave do të jetë i mundur vetëm duke përdorur të njëjtin frazëkalim.", + "Not a valid Riot keyfile": "S’është kartelë kyçesh Riot e vlefshme", + "Revoke Moderator": "Shfuqizoje Si Moderator", + "You have no historical rooms": "S’keni dhoma të dikurshme", + "Historical": "Të dikurshme", + "Flair": "Simbole", + "Showing flair for these communities:": "Shfaqen simbole për këto bashkësi:", + "This room is not showing flair for any communities": "Kjo dhomë nuk shfaq simbole për ndonjë bashkësi", + "Robot check is currently unavailable on desktop - please use a web browser": "Kontrolli për robot hëpërhë s’është i përdorshëm në desktop - ju lutemi, përdorni një shfletues", + "Please review and accept all of the homeserver's policies": "Ju lutemi, shqyrtoni dhe pranoni krejt rregullat e këtij shërbyesi home", + "Flair will appear if enabled in room settings": "Simbolet do të shfaqen nëse aktivizohen te rregullimet e dhomës", + "Flair will not appear": "Simbolet nuk do të shfaqen", + "Display your community flair in rooms configured to show it.": "Shfaqni simbolet e bashkësisë tuaj në dhoma të formësuara për t’i shfaqur ato.", + "Are you sure you wish to remove (delete) this event? Note that if you delete a room name or topic change, it could undo the change.": "Jeni i sigurt se doni të hiqet (fshihet) ky akt? Mbani parasysh se nëse fshini emrin e një dhome ose ndryshimin e temës, kjo mund të sjellë zhbërjen e ndryshimit.", + "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": "Që të shmanget humbja e historikut të fjalosjes tuaj, duhet të eksportoni kyçet e dhomës tuaj përpara se të dilni nga llogari. Që ta bëni këtë, duhe të riktheheni te versioni më i ri i Riot-it", + "You've previously used a newer version of Riot on %(host)s. To use this version again with end to end encryption, you will need to sign out and back in again. ": "Më parë përdorët një version më të ri të Riot-it në %(host)s. Që ta përdorni sërish këtë version me fshehtëzim skaj-më-skaj, duhet të dilni dhe rihyni te llogaria juaj. ", + "Incompatible Database": "Bazë të dhënash e Papërputhshme", + "Continue With Encryption Disabled": "Vazhdo Me Fshehtëzimin të Çaktivizuar", + "Unable to load! Check your network connectivity and try again.": "S’arrihet të ngarkohet! Kontrolloni lidhjen tuaj në rrjet dhe riprovoni.", + "Forces the current outbound group session in an encrypted room to be discarded": "Forces the current outbound group session in an encrypted room to be discarded", + "Backup of encryption keys to server": "Kopjeruajtje kyçesh fshehtëzimi në shërbyes", + "Delete Backup": "Fshije Kopjeruajtjen", + "Delete your backed up encryption keys from the server? You will no longer be able to use your recovery key to read encrypted message history": "Të fshihen nga shërbyesi kyçet e kopjeruajtur të fshehtëzimit? S’do të jeni më në gjendje të përdorni kyçin tuaj të rimarrjeve për lexim historiku mesazhesh të fshehtëzuar", + "Delete backup": "Fshije kopjeruajtjen", + "Unable to load key backup status": "S’arrihet të ngarkohet gjendje kopjeruajtjeje kyçesh", + "This device is uploading keys to this backup": "Kjo pajisje po ngarkon kyçe te kjo kopjeruajtje", + "This device is not uploading keys to this backup": "Kjo pajisje nuk po ngarkon kyçe te kjo kopjeruajtje", + "Backup has a valid signature from this device": "Kopjeruajtja ka një nënshkrim të vlefshëm prej kësaj pajisjeje", + "Backup has a valid signature from verified device x": "Kopjeruajtja ka një nënshkrim të vlefshëm prej pajisjes së verifikuar x", + "Backup has a valid signature from unverified device ": "Kopjeruajtja ka një nënshkrim të vlefshëm prej pajisjes së paverifikuar ", + "Backup has an invalid signature from verified device ": "Kopjeruajtja ka një nënshkrim të pavlefshëm prej pajisjes së verifikuar ", + "Backup has an invalid signature from unverified device ": "Kopjeruajtja ka një nënshkrim të pavlefshëm prej pajisjes së paverifikuar ", + "Backup is not signed by any of your devices": "Kopjeruajtja s’është nënshkruar nga ndonjë prej pajisjeve tuaja", + "Backup version: ": "Version kopjeruajtjeje: ", + "Algorithm: ": "Algoritëm: ", + "Restore backup": "Riktheje kopjeruajtjen", + "No backup is present": "S’ka kopjeruajtje të pranishëm", + "Start a new backup": "Filloni një kopjeruajtje të re", + "Secure your encrypted message history with a Recovery Passphrase.": "Sigurojeni historikun e mesazheve tuaj të fshehtëzuar me një Frazëkalim Rimarrjesh.", + "You'll need it if you log out or lose access to this device.": "Do t’ju duhet, nëse dilni nga llogaria ose nëse s’përdorni më dot pajisjen.", + "Enter a passphrase...": "Jepni një frazëkalim…", + "Next": "Pasuesja", + "If you don't want encrypted message history to be availble on other devices, .": "Nëse s’doni që historiku i mesazheve të fshehtëzuara të jetë i përdorshëm në pajisje të tjera, .", + "Or, if you don't want to create a Recovery Passphrase, skip this step and .": "Ose, nëse s’doni të krijohet një Frazëkalim Rimarrjesh, anashkalojeni këtë hap dhe .", + "That matches!": "U përputhën!", + "That doesn't match.": "S’përputhen.", + "Go back to set it again.": "Shkoni mbrapsht që ta ricaktoni.", + "Type in your Recovery Passphrase to confirm you remember it. If it helps, add it to your password manager or store it somewhere safe.": "Shtypeni Frazëkalimin tuaj të Rimarrjeve që të ripohoni se e mbani mend. Nëse bën punë, shtojeni te përgjegjësi juaj i fjalëkalimeve ose depozitojeni diku pa rrezik.", + "Repeat your passphrase...": "Përsëritni frazëkalimin tuaj…", + "Make a copy of this Recovery Key and keep it safe.": "Bëni një kopje të këtij Kyçi RImarrjesh dhe mbajeni të parrezikuar.", + "As a safety net, you can use it to restore your encrypted message history if you forget your Recovery Passphrase.": "Si rrjet i parrezikuar, mund ta përdoreni për të rikthyer historikun e mesazheve tuaj të fshehtëzuar, nëse harroni Frazëkalimin e Rimarrjeve.", + "Your Recovery Key": "Kyçi Juaj i Rimarrjeve", + "Copy to clipboard": "Kopjoje në të papastër", + "Download": "Shkarkoje", + "I've made a copy": "Kam bërë një kopje", + "Your Recovery Key has been copied to your clipboard, paste it to:": "Kyçi juaj i Fshehtëzimeve është kopjuar te e papastra juaj, ngjiteni te:", + "Your Recovery Key is in your Downloads folder.": "Kyçi juaj i Fshehtëzimeve gjendet te dosja juaj Shkarkime.", + "Print it and store it somewhere safe": "Shtypeni dhe ruajeni diku pa rrezik", + "Save it on a USB key or backup drive": "Ruajeni në një diskth USB ose disk kopjeruajtjesh", + "Copy it to your personal cloud storage": "Kopjojeni te depoja juaj personale në re", + "Got it": "E mora vesh", + "Backup created": "Kopjeruajtja u krijua", + "Your encryption keys are now being backed up to your Homeserver.": "Kyçet tuaj të fshehtëzimit tani po kopjeruhen te shërbyesi juaj Home.", + "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.": "Pa rregulluar Rimarrje të Siguruar, s’do të jeni në gjendje të riktheni historikun e mesazheve tuaj të fshehtëzuar, nëse bëni daljen ose përdorni një pajisje tjetër.", + "Set up Secure Message Recovery": "Rregulloni Rimarrje të Siguruar Mesazhesh", + "Create a Recovery Passphrase": "Krijoni Frazëkalim Rimarrjeje", + "Confirm Recovery Passphrase": "Ripohoni Frazëkalim Rimarrjeje", + "Recovery Key": "Kyç Rimarrjesh", + "Keep it safe": "Mbajeni të parrezikuar", + "Backing up...": "Po kopjeruhet…", + "Create Key Backup": "Krijo Kopjeruajtje Kyçesh", + "Unable to create key backup": "S’arrihet të krijojhet kopjeruajtje kyçesh", + "Retry": "Riprovo", + "Unable to load backup status": "S’arrihet të ngarkohet gjendje kopjeruajtjeje", + "Unable to restore backup": "S’arrihet të rikthehet kopjeruajtje", + "No backup found!": "S’u gjet kopjeruajtje!", + "Backup Restored": "Kopjeruajtja u Rikthye", + "Failed to decrypt %(failedCount)s sessions!": "S’u arrit të shfshehtëzohet sesioni %(failedCount)s!", + "Restored %(sessionCount)s session keys": "U rikthyen kyçet e sesionit %(sessionCount)s", + "Enter Recovery Passphrase": "Jepni Frazëkalim Rimarrjeje", + "Access your secure message history and set up secure messaging by entering your recovery passphrase.": "Hyni te historiku i mesazheve tuaj të siguruar dhe rregulloni shkëmbim mesazhesh të sigurt duke dhënë frazëkalimin tuaj të rimarrjeve.", + "If you've forgotten your recovery passphrase you can use your recovery key or set up new recovery options": "Nëse keni harruar frazëkalimin tuaj të rimarrjeve, mund të përdorni kyçin tuaj të rimarrjeve ose rregulloni mundësi të reja rimarrjeje", + "Enter Recovery Key": "Jepni Kyç Rimarrjeje", + "This looks like a valid recovery key!": "Ky duket si kyç i vlefshëm rimarrjesh!", + "Not a valid recovery key": "Kyç rimarrjesh jo i vlefshëm", + "Access your secure message history and set up secure messaging by entering your recovery key.": "Hyni te historiku i mesazheve tuaj të siguruar dhe rregulloni shkëmbim mesazhesh të sigurt duke dhënë kyçin tuaj të rimarrjeve.", + "If you've forgotten your recovery passphrase you can ": "Nëse keni harruar frazëkalimin tuaj të rimarrjeve, mund të rregulloni mundësi të reja rimarrjeje", + "Key Backup": "Kopjeruajtje Kyçi", + "Sign in with single sign-on": "Bëni hyrjen me hyrje njëshe", + "Disable Peer-to-Peer for 1:1 calls": "Çaktivizoje mekanizmin Peer-to-Peer për thirrje 1 me 1", + "Failed to perform homeserver discovery": "S’u arrit të kryhej zbulim shërbyesi Home", + "Invalid homeserver discovery response": "Përgjigje e pavlefshme zbulimi shërbyesi Home", + "Cannot find homeserver": "S’gjendet dot shërbyesi Home", + "File is too big. Maximum file size is %(fileSize)s": "Kartela është shumë e madhe. Madhësia maksimum për kartelat është %(fileSize)s", + "The following files cannot be uploaded:": "Kartelat vijuese s’mund të ngarkohen:", + "Use a few words, avoid common phrases": "Përdorni ca fjalë, shmangni fraza të rëndomta", + "No need for symbols, digits, or uppercase letters": "S’ka nevojë për simbole, shifra apo shkronja të mëdha", + "Use a longer keyboard pattern with more turns": "Përdorni një rregullsi më të gjatë tastiere, me më tepër kthesa", + "Avoid repeated words and characters": "Shmangi përsëritje fjalësh dhe përsëritje shkronjash", + "Avoid sequences": "Shmangi togfjalësha", + "Avoid recent years": "Shmangni vitet e fundit", + "Avoid years that are associated with you": "Shmangni vite që kanë lidhje me ju", + "Avoid dates and years that are associated with you": "Shmangni data dhe vite që kanë lidhje me ju", + "Capitalization doesn't help very much": "Shkrimi i shkronjës së parë me të madhe nuk ndihmon kushedi çë", + "All-uppercase is almost as easy to guess as all-lowercase": "Fjalë shkruar krejt me të mëdha janë thuajse po aq të lehta për t’i hamendësuar sa ato me krejt të vogla", + "Reversed words aren't much harder to guess": "Fjalët së prapthi s’janë të vështira për t’i marrë me mend", + "Predictable substitutions like '@' instead of 'a' don't help very much": "Zëvendësime të parashikueshme, të tilla si '@', në vend të 'a', nuk ndihmojnë kushedi çë", + "Add another word or two. Uncommon words are better.": "Shtoni një a dy fjalë të tjera. Fjalë jo të rëndomta janë më të përshtatshme.", + "Repeats like \"aaa\" are easy to guess": "Përsëritje të tilla si \"aaa\" janë të lehta për t’u hamendësuar", + "Repeats like \"abcabcabc\" are only slightly harder to guess than \"abc\"": "Përsëritje të tilla si \"abcabcabc\" janë vetëm pak më të vështira për t’u hamendësuar se sa \"abc\"", + "Sequences like abc or 6543 are easy to guess": "Sekuenca të tilla si abc ose 6543 janë të lehta për t’u hamendsuar", + "Recent years are easy to guess": "Vitet tani afër janë të lehtë për t’u hamendësuar", + "Dates are often easy to guess": "Datat shpesh janë të lehta për t’i gjetur", + "This is a top-10 common password": "Ky fjalëkalim është nga 10 më të rëndomtët", + "This is a top-100 common password": "Ky fjalëkalim është nga 100 më të rëndomtët", + "This is a very common password": "Ky është një fjalëkalim shumë i rëndomtë", + "This is similar to a commonly used password": "Ky është i ngjashëm me një fjalëkalim të përdorur rëndom", + "A word by itself is easy to guess": "Një fjalë më vete është e lehtë të hamendësohet", + "Names and surnames by themselves are easy to guess": "Emrat dhe mbiemrat në vetvete janë të lehtë për t’i hamendësuar", + "Common names and surnames are easy to guess": "Emra dhe mbiemra të rëndomtë janë të kollajtë për t’u hamendësuar", + "Great! This passphrase looks strong enough.": "Bukur! Ky frazëkalim duket goxha i fuqishëm.", + "Failed to load group members": "S'u arrit të ngarkoheshin anëtarë grupi", + "As a safety net, you can use it to restore your encrypted message history.": "Si një rrjet sigurie, mund ta përdorni për të rikthyer historikun e mesazheve tuaj të fshehtëzuar." } diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index 242990264c0..712911064fa 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -1300,5 +1300,112 @@ "You are currently using Riot anonymously as a guest.": "您目前是以訪客的身份匿名使用 Riot。", "You are an administrator of this community. You will not be able to rejoin without an invite from another administrator.": "您是此社群的管理員。您將無法在沒有其他管理員的邀請下重新加入。", "Open Devtools": "開啟開發者工具", - "Show developer tools": "顯示開發者工具" + "Show developer tools": "顯示開發者工具", + "Unable to load! Check your network connectivity and try again.": "無法載入!請檢查您的網路連線狀態並再試一次。", + "Backup of encryption keys to server": "將加密金鑰備份到伺服器", + "Delete Backup": "刪除備份", + "Delete your backed up encryption keys from the server? You will no longer be able to use your recovery key to read encrypted message history": "從伺服器刪除您已備份的加密金鑰?您將無法再使用您的復原金鑰來讀取加密的訊息歷史", + "Delete backup": "刪除備份", + "Unable to load key backup status": "無法載入金鑰備份狀態", + "This device is uploading keys to this backup": "此裝置正在上傳金鑰到此備份", + "This device is not uploading keys to this backup": "此裝置並未上傳金鑰到此備份", + "Backup has a valid signature from this device": "備份有從此裝置而來的有效簽章", + "Backup has a valid signature from verified device x": "備份有從已驗證的 x 裝置而來的有效簽章", + "Backup has a valid signature from unverified device ": "備份有從未驗證的 裝置而來的有效簽章", + "Backup has an invalid signature from verified device ": "備份有從已驗證的 裝置而來的無效簽章", + "Backup has an invalid signature from unverified device ": "備份有從未驗證的 裝置而來的無效簽章", + "Backup is not signed by any of your devices": "備份未被您的任何裝置簽署", + "Backup version: ": "備份版本: ", + "Algorithm: ": "演算法: ", + "Restore backup": "恢復備份", + "No backup is present": "沒有備份", + "Start a new backup": "開始新備份", + "Please review and accept all of the homeserver's policies": "請審閱並接受家伺服器的所有政策", + "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": "為了避免遺失您的聊天歷史,您必須在登出前匯出您的聊天室金鑰。您必須回到較新的 Riot 才能執行此動作", + "You've previously used a newer version of Riot on %(host)s. To use this version again with end to end encryption, you will need to sign out and back in again. ": "您先前在 %(host)s 上使用較新的 Riot 版本。要再次與此版本一同使用端到端加密,您將需要登出並再次登入。 ", + "Incompatible Database": "不相容的資料庫", + "Continue With Encryption Disabled": "在停用加密的情況下繼續", + "Secure your encrypted message history with a Recovery Passphrase.": "以復原密碼保證您的加密訊息歷史安全。", + "You'll need it if you log out or lose access to this device.": "如果您登出或是遺失對此裝置的存取權,您將會需要它。", + "Enter a passphrase...": "輸入密碼……", + "Next": "下一個", + "If you don't want encrypted message history to be availble on other devices, .": "如果您不想要讓加密的訊息歷史在其他裝置上可用,。", + "Or, if you don't want to create a Recovery Passphrase, skip this step and .": "或是,如果您不想建立復原密碼,跳過此步驟並。", + "That matches!": "符合!", + "That doesn't match.": "不符合。", + "Go back to set it again.": "回去重新設定它。", + "Type in your Recovery Passphrase to confirm you remember it. If it helps, add it to your password manager or store it somewhere safe.": "輸入您的復原密碼以確認您記得它。如果可以的話,把它加入到您的密碼管理員或是把它儲存在其他安全的地方。", + "Repeat your passphrase...": "重覆您的密碼……", + "Make a copy of this Recovery Key and keep it safe.": "複製這把復原金鑰並把它放在安全的地方。", + "As a safety net, you can use it to restore your encrypted message history if you forget your Recovery Passphrase.": "做為安全網,您可以在忘記您的復原密碼時使用它來復原您的加密訊息歷史。", + "Your Recovery Key": "您的復原金鑰", + "Copy to clipboard": "複製到剪貼簿", + "Download": "下載", + "I've made a copy": "我已經有副本了", + "Your Recovery Key has been copied to your clipboard, paste it to:": "您的復原金鑰已複製到您的剪貼簿,將它貼上到:", + "Your Recovery Key is in your Downloads folder.": "您的復原金鑰在您的下載資料夾。", + "Print it and store it somewhere safe": "列印它並存放在安全的地方", + "Save it on a USB key or backup drive": "將它儲存到 USB 金鑰或備份磁碟上", + "Copy it to your personal cloud storage": "將它複製 到您的個人雲端儲存", + "Got it": "知道了", + "Backup created": "備份已建立", + "Your encryption keys are now being backed up to your Homeserver.": "您的加密金鑰已經備份到您的家伺服器了。", + "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.": "在沒有設定安全訊息復原的狀況下,您將無法在登出或使用其他裝置後復原您的已加密訊息歷史。", + "Set up Secure Message Recovery": "設定安全訊息復原", + "Create a Recovery Passphrase": "建立復原密碼", + "Confirm Recovery Passphrase": "確認復原密碼", + "Recovery Key": "復原金鑰", + "Keep it safe": "保持安全", + "Backing up...": "正在備份……", + "Create Key Backup": "建立金鑰備份", + "Unable to create key backup": "無法建立金鑰備份", + "Retry": "重試", + "Unable to load backup status": "無法載入備份狀態", + "Unable to restore backup": "無法復原備份", + "No backup found!": "找不到備份!", + "Backup Restored": "備份已復原", + "Failed to decrypt %(failedCount)s sessions!": "解密 %(failedCount)s 工作階段失敗!", + "Restored %(sessionCount)s session keys": "%(sessionCount)s 工作階段金鑰已復原", + "Enter Recovery Passphrase": "輸入復原密碼", + "Access your secure message history and set up secure messaging by entering your recovery passphrase.": "存取您的安全訊息歷史並透過輸入您的復原密碼來設定安全訊息。", + "If you've forgotten your recovery passphrase you can use your recovery key or set up new recovery options": "如果您忘記您的復原密碼,您可以使用您的復原金鑰設定新的復原選項", + "Enter Recovery Key": "輸入復原金鑰", + "This looks like a valid recovery key!": "看起來是有效的復原金鑰!", + "Not a valid recovery key": "不是有效的復原金鑰", + "Access your secure message history and set up secure messaging by entering your recovery key.": "存取您的安全訊息歷史並趟過輸入您的復原金鑰來設定安全傳訊。", + "If you've forgotten your recovery passphrase you can ": "如果您忘記您的復原密碼,您可以", + "Key Backup": "金鑰備份", + "Failed to perform homeserver discovery": "執行家伺服器探索失敗", + "Invalid homeserver discovery response": "無效的家伺服器探索回應", + "Cannot find homeserver": "找不到家伺服器", + "Sign in with single sign-on": "以單一登入來登入", + "File is too big. Maximum file size is %(fileSize)s": "檔案太大了。最大的檔案大小為 %(fileSize)s", + "The following files cannot be uploaded:": "下列檔案無法上傳:", + "Use a few words, avoid common phrases": "使用數個字,但避免常用片語", + "No need for symbols, digits, or uppercase letters": "不需要符號、數字或大寫字母", + "Use a longer keyboard pattern with more turns": "以更多變化使用較長的鍵盤模式", + "Avoid repeated words and characters": "避免重覆的文字與字母", + "Avoid sequences": "避免序列", + "Avoid recent years": "避免最近的年份", + "Avoid years that are associated with you": "避免關於您的年份", + "Avoid dates and years that are associated with you": "避免關於您的日期與年份", + "Capitalization doesn't help very much": "大寫並沒有太大的協助", + "All-uppercase is almost as easy to guess as all-lowercase": "全大寫通常比全小寫好猜", + "Reversed words aren't much harder to guess": "反向拼字不會比較難猜", + "Predictable substitutions like '@' instead of 'a' don't help very much": "如「@」而非「a」這樣的預期中的替換並沒有太多的協助", + "Add another word or two. Uncommon words are better.": "加入一個或兩個額外的單字。最好是不常用的。", + "Repeats like \"aaa\" are easy to guess": "如「aaa」這樣的重覆易於猜測", + "Repeats like \"abcabcabc\" are only slightly harder to guess than \"abc\"": "如「abcabcabc」這樣的重覆只比「abc」難猜一點", + "Sequences like abc or 6543 are easy to guess": "如 abc 或 6543 這樣的序列易於猜測", + "Recent years are easy to guess": "最近的年份易於猜測", + "Dates are often easy to guess": "日期通常比較好猜", + "This is a top-10 common password": "這是十大最常見的密碼", + "This is a top-100 common password": "這是百大最常見的密碼", + "This is a very common password": "這是非常常見的密碼", + "This is similar to a commonly used password": "這與常見使用的密碼很類似", + "A word by itself is easy to guess": "單字本身很容易猜測", + "Names and surnames by themselves are easy to guess": "姓名與姓氏本身很容易猜測", + "Common names and surnames are easy to guess": "常見的名字與姓氏易於猜測", + "Great! This passphrase looks strong enough.": "很好!這個密碼看起來夠強了。", + "As a safety net, you can use it to restore your encrypted message history.": "做為安全網,您可以使用它來復原您已加密的訊息歷史。" } diff --git a/src/matrix-to.js b/src/matrix-to.js index b5827f671a8..b750dff6d67 100644 --- a/src/matrix-to.js +++ b/src/matrix-to.js @@ -15,6 +15,8 @@ limitations under the License. */ import MatrixClientPeg from "./MatrixClientPeg"; +import isIp from "is-ip"; +import utils from 'matrix-js-sdk/lib/utils'; export const host = "matrix.to"; export const baseUrl = `https://${host}`; @@ -90,7 +92,9 @@ export function pickServerCandidates(roomId) { // Rationale for popular servers: It's hard to get rid of people when // they keep flocking in from a particular server. Sure, the server could // be ACL'd in the future or for some reason be evicted from the room - // however an event like that is unlikely the larger the room gets. + // however an event like that is unlikely the larger the room gets. If + // the server is ACL'd at the time of generating the link however, we + // shouldn't pick them. We also don't pick IP addresses. // Note: we don't pick the server the room was created on because the // homeserver should already be using that server as a last ditch attempt @@ -104,12 +108,29 @@ export function pickServerCandidates(roomId) { // The receiving user can then manually append the known-good server to // the list and magically have the link work. + const bannedHostsRegexps = []; + let allowedHostsRegexps = [new RegExp(".*")]; // default allow everyone + if (room.currentState) { + const aclEvent = room.currentState.getStateEvents("m.room.server_acl", ""); + if (aclEvent && aclEvent.getContent()) { + const getRegex = (hostname) => new RegExp("^" + utils.globToRegexp(hostname, false) + "$"); + + const denied = aclEvent.getContent().deny || []; + denied.forEach(h => bannedHostsRegexps.push(getRegex(h))); + + const allowed = aclEvent.getContent().allow || []; + allowedHostsRegexps = []; // we don't want to use the default rule here + allowed.forEach(h => allowedHostsRegexps.push(getRegex(h))); + } + } + const populationMap: {[server:string]:number} = {}; const highestPlUser = {userId: null, powerLevel: 0, serverName: null}; for (const member of room.getJoinedMembers()) { const serverName = member.userId.split(":").splice(1).join(":"); - if (member.powerLevel > highestPlUser.powerLevel) { + if (member.powerLevel > highestPlUser.powerLevel && !isHostnameIpAddress(serverName) + && !isHostInRegex(serverName, bannedHostsRegexps) && isHostInRegex(serverName, allowedHostsRegexps)) { highestPlUser.userId = member.userId; highestPlUser.powerLevel = member.powerLevel; highestPlUser.serverName = serverName; @@ -125,8 +146,9 @@ export function pickServerCandidates(roomId) { const beforePopulation = candidates.length; const serversByPopulation = Object.keys(populationMap) .sort((a, b) => populationMap[b] - populationMap[a]) - .filter(a => !candidates.includes(a)); - for (let i = beforePopulation; i <= MAX_SERVER_CANDIDATES; i++) { + .filter(a => !candidates.includes(a) && !isHostnameIpAddress(a) + && !isHostInRegex(a, bannedHostsRegexps) && isHostInRegex(a, allowedHostsRegexps)); + for (let i = beforePopulation; i < MAX_SERVER_CANDIDATES; i++) { const idx = i - beforePopulation; if (idx >= serversByPopulation.length) break; candidates.push(serversByPopulation[idx]); @@ -134,3 +156,34 @@ export function pickServerCandidates(roomId) { return candidates; } + +function getHostnameFromMatrixDomain(domain) { + if (!domain) return null; + + // The hostname might have a port, so we convert it to a URL and + // split out the real hostname. + const parser = document.createElement('a'); + parser.href = "https://" + domain; + return parser.hostname; +} + +function isHostInRegex(hostname, regexps) { + hostname = getHostnameFromMatrixDomain(hostname); + if (!hostname) return true; // assumed + if (regexps.length > 0 && !regexps[0].test) throw new Error(regexps[0]); + + return regexps.filter(h => h.test(hostname)).length > 0; +} + +function isHostnameIpAddress(hostname) { + hostname = getHostnameFromMatrixDomain(hostname); + if (!hostname) return false; + + // is-ip doesn't want IPv6 addresses surrounded by brackets, so + // take them off. + if (hostname.startsWith("[") && hostname.endsWith("]")) { + hostname = hostname.substring(1, hostname.length - 1); + } + + return isIp(hostname); +} diff --git a/src/notifications/StandardActions.js b/src/notifications/StandardActions.js index 30d6ea59754..15f645d5f7e 100644 --- a/src/notifications/StandardActions.js +++ b/src/notifications/StandardActions.js @@ -24,6 +24,7 @@ module.exports = { ACTION_NOTIFY: encodeActions({notify: true}), ACTION_NOTIFY_DEFAULT_SOUND: encodeActions({notify: true, sound: "default"}), ACTION_NOTIFY_RING_SOUND: encodeActions({notify: true, sound: "ring"}), + ACTION_HIGHLIGHT: encodeActions({notify: true, highlight: true}), ACTION_HIGHLIGHT_DEFAULT_SOUND: encodeActions({notify: true, sound: "default", highlight: true}), ACTION_DONT_NOTIFY: encodeActions({notify: false}), ACTION_DISABLED: null, diff --git a/src/notifications/VectorPushRulesDefinitions.js b/src/notifications/VectorPushRulesDefinitions.js index eeb193cb8a9..3df2e707740 100644 --- a/src/notifications/VectorPushRulesDefinitions.js +++ b/src/notifications/VectorPushRulesDefinitions.js @@ -20,6 +20,7 @@ import { _td } from '../languageHandler'; const StandardActions = require('./StandardActions'); const PushRuleVectorState = require('./PushRuleVectorState'); +const { decodeActions } = require('./NotificationUtils'); class VectorPushRuleDefinition { constructor(opts) { @@ -31,13 +32,11 @@ class VectorPushRuleDefinition { // Translate the rule actions and its enabled value into vector state ruleToVectorState(rule) { let enabled = false; - let actions = null; if (rule) { enabled = rule.enabled; - actions = rule.actions; } - for (const stateKey in PushRuleVectorState.states) { + for (const stateKey in PushRuleVectorState.states) { // eslint-disable-line guard-for-in const state = PushRuleVectorState.states[stateKey]; const vectorStateToActions = this.vectorStateToActions[state]; @@ -47,15 +46,21 @@ class VectorPushRuleDefinition { return state; } } else { - // The actions must match to the ones expected by vector state - if (enabled && JSON.stringify(rule.actions) === JSON.stringify(vectorStateToActions)) { + // The actions must match to the ones expected by vector state. + // Use `decodeActions` on both sides to canonicalize things like + // value: true vs. unspecified for highlight (which defaults to + // true, making them equivalent). + if (enabled && + JSON.stringify(decodeActions(rule.actions)) === + JSON.stringify(decodeActions(vectorStateToActions))) { return state; } } } - console.error("Cannot translate rule actions into Vector rule state. Rule: " + - JSON.stringify(rule)); + console.error(`Cannot translate rule actions into Vector rule state. ` + + `Rule: ${JSON.stringify(rule)}, ` + + `Expected: ${JSON.stringify(this.vectorStateToActions)}`); return undefined; } } @@ -86,6 +91,17 @@ module.exports = { }, }), + // Messages containing @room + ".m.rule.roomnotif": new VectorPushRuleDefinition({ + kind: "override", + description: _td("Messages containing @room"), // passed through _t() translation in src/components/views/settings/Notifications.js + vectorStateToActions: { // The actions for each vector state, or null to disable the rule. + on: StandardActions.ACTION_NOTIFY, + loud: StandardActions.ACTION_HIGHLIGHT, + off: StandardActions.ACTION_DISABLED, + }, + }), + // Messages just sent to the user in a 1:1 room ".m.rule.room_one_to_one": new VectorPushRuleDefinition({ kind: "underride", @@ -97,6 +113,17 @@ module.exports = { }, }), + // Encrypted messages just sent to the user in a 1:1 room + ".m.rule.encrypted_room_one_to_one": new VectorPushRuleDefinition({ + kind: "underride", + description: _td("Encrypted messages in one-to-one chats"), // passed through _t() translation in src/components/views/settings/Notifications.js + vectorStateToActions: { + on: StandardActions.ACTION_NOTIFY, + loud: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND, + off: StandardActions.ACTION_DONT_NOTIFY, + }, + }), + // Messages just sent to a group chat room // 1:1 room messages are catched by the .m.rule.room_one_to_one rule if any defined // By opposition, all other room messages are from group chat rooms. @@ -110,6 +137,19 @@ module.exports = { }, }), + // Encrypted messages just sent to a group chat room + // Encrypted 1:1 room messages are catched by the .m.rule.encrypted_room_one_to_one rule if any defined + // By opposition, all other room messages are from group chat rooms. + ".m.rule.encrypted": new VectorPushRuleDefinition({ + kind: "underride", + description: _td("Encrypted messages in group chats"), // passed through _t() translation in src/components/views/settings/Notifications.js + vectorStateToActions: { + on: StandardActions.ACTION_NOTIFY, + loud: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND, + off: StandardActions.ACTION_DONT_NOTIFY, + }, + }), + // Invitation for the user ".m.rule.invite_for_me": new VectorPushRuleDefinition({ kind: "underride", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index d0b4b9b9d6c..56e66844dcb 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -151,6 +151,11 @@ export const SETTINGS = { displayName: _td('Always show encryption icons'), default: true, }, + "showRoomRecoveryReminder": { + supportedLevels: LEVELS_ACCOUNT_SETTINGS, + displayName: _td('Show a reminder to enable Secure Message Recovery in encrypted rooms'), + default: true, + }, "enableSyntaxHighlightLanguageDetection": { supportedLevels: LEVELS_ACCOUNT_SETTINGS, displayName: _td('Enable automatic language detection for syntax highlighting'), diff --git a/src/shouldHideEvent.js b/src/shouldHideEvent.js index 3aad05a9760..adc89a126ab 100644 --- a/src/shouldHideEvent.js +++ b/src/shouldHideEvent.js @@ -38,18 +38,20 @@ function memberEventDiff(ev) { } export default function shouldHideEvent(ev) { - // Wrap getValue() for readability + // Wrap getValue() for readability. Calling the SettingsStore can be + // fairly resource heavy, so the checks below should avoid hitting it + // where possible. const isEnabled = (name) => SettingsStore.getValue(name, ev.getRoomId()); // Hide redacted events - if (isEnabled('hideRedactions') && ev.isRedacted()) return true; + if (ev.isRedacted() && isEnabled('hideRedactions')) return true; const eventDiff = memberEventDiff(ev); if (eventDiff.isMemberEvent) { - if (isEnabled('hideJoinLeaves') && (eventDiff.isJoin || eventDiff.isPart)) return true; - if (isEnabled('hideAvatarChanges') && eventDiff.isAvatarChange) return true; - if (isEnabled('hideDisplaynameChanges') && eventDiff.isDisplaynameChange) return true; + if ((eventDiff.isJoin || eventDiff.isPart) && isEnabled('hideJoinLeaves')) return true; + if (eventDiff.isAvatarChange && isEnabled('hideAvatarChanges')) return true; + if (eventDiff.isDisplaynameChange && isEnabled('hideDisplaynameChanges')) return true; } return false; diff --git a/src/stores/GroupStore.js b/src/stores/GroupStore.js index bc2be37f519..4ac1e42e2ee 100644 --- a/src/stores/GroupStore.js +++ b/src/stores/GroupStore.js @@ -122,10 +122,6 @@ class GroupStore extends EventEmitter { ); }, }; - - this.on('error', (err, groupId) => { - console.error(`GroupStore encountered error whilst fetching data for ${groupId}`, err); - }); } _fetchResource(stateKey, groupId) { @@ -148,7 +144,7 @@ class GroupStore extends EventEmitter { } console.error(`Failed to get resource ${stateKey} for ${groupId}`, err); - this.emit('error', err, groupId); + this.emit('error', err, groupId, stateKey); }).finally(() => { // Indicate finished request, allow for future fetches delete this._fetchResourcePromise[stateKey][groupId]; diff --git a/src/stores/RoomListStore.js b/src/stores/RoomListStore.js index c636c536316..af6a8cc9915 100644 --- a/src/stores/RoomListStore.js +++ b/src/stores/RoomListStore.js @@ -300,6 +300,10 @@ class RoomListStore extends Store { const ts = this._tsOfNewestEvent(room); this._updateCachedRoomState(roomId, "timestamp", ts); return ts; + } else if (type === "unread-muted") { + const unread = Unread.doesRoomHaveUnreadMessages(room); + this._updateCachedRoomState(roomId, "unread-muted", unread); + return unread; } else if (type === "unread") { const unread = room.getUnreadNotificationCount() > 0; this._updateCachedRoomState(roomId, "unread", unread); @@ -358,8 +362,21 @@ class RoomListStore extends Store { } if (pinUnread) { - const unreadA = this._getRoomState(roomA, "unread"); - const unreadB = this._getRoomState(roomB, "unread"); + let unreadA = this._getRoomState(roomA, "unread"); + let unreadB = this._getRoomState(roomB, "unread"); + if (unreadA && !unreadB) return -1; + if (!unreadA && unreadB) return 1; + + // If they both have unread messages, sort by timestamp + // If nether have unread message (the fourth check not shown + // here), then just sort by timestamp anyways. + if (unreadA && unreadB) return timestampDiff; + + // Unread can also mean "unread without badge", which is + // different from what the above checks for. We're also + // going to sort those here. + unreadA = this._getRoomState(roomA, "unread-muted"); + unreadB = this._getRoomState(roomB, "unread-muted"); if (unreadA && !unreadB) return -1; if (!unreadA && unreadB) return 1; diff --git a/src/stores/RoomViewStore.js b/src/stores/RoomViewStore.js index f15925f4804..9e048e5d8e6 100644 --- a/src/stores/RoomViewStore.js +++ b/src/stores/RoomViewStore.js @@ -224,6 +224,11 @@ class RoomViewStore extends Store { err: err, }); let msg = err.message ? err.message : JSON.stringify(err); + // XXX: We are relying on the error message returned by browsers here. + // This isn't great, but it does generalize the error being shown to users. + if (msg && msg.startsWith("CORS request rejected")) { + msg = _t("There was an error joining the room"); + } if (err.errcode === 'M_INCOMPATIBLE_ROOM_VERSION') { msg =
    {_t("Sorry, your homeserver is too old to participate in this room.")}
    diff --git a/src/utils/MultiInviter.js b/src/utils/MultiInviter.js index b3e7fc495a9..ad10f28edf5 100644 --- a/src/utils/MultiInviter.js +++ b/src/utils/MultiInviter.js @@ -1,6 +1,6 @@ /* Copyright 2016 OpenMarket Ltd -Copyright 2017 New Vector Ltd +Copyright 2017, 2018 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,9 +17,9 @@ limitations under the License. import MatrixClientPeg from '../MatrixClientPeg'; import {getAddressType} from '../UserAddress'; -import {inviteToRoom} from '../RoomInvite'; import GroupStore from '../stores/GroupStore'; import Promise from 'bluebird'; +import {_t} from "../languageHandler"; /** * Invites multiple addresses to a room or group, handling rate limiting from the server @@ -49,7 +49,7 @@ export default class MultiInviter { * Invite users to this room. This may only be called once per * instance of the class. * - * @param {array} addresses Array of addresses to invite + * @param {array} addrs Array of addresses to invite * @returns {Promise} Resolved when all invitations in the queue are complete */ invite(addrs) { @@ -88,12 +88,30 @@ export default class MultiInviter { return this.errorTexts[addr]; } + async _inviteToRoom(roomId, addr) { + const addrType = getAddressType(addr); + + if (addrType === 'email') { + return MatrixClientPeg.get().inviteByEmail(roomId, addr); + } else if (addrType === 'mx-user-id') { + const profile = await MatrixClientPeg.get().getProfileInfo(addr); + if (!profile) { + return Promise.reject({errcode: "M_NOT_FOUND", error: "User does not have a profile."}); + } + + return MatrixClientPeg.get().invite(roomId, addr); + } else { + throw new Error('Unsupported address'); + } + } + + _inviteMore(nextIndex) { if (this._canceled) { return; } - if (nextIndex == this.addrs.length) { + if (nextIndex === this.addrs.length) { this.busy = false; this.deferred.resolve(this.completionStates); return; @@ -111,7 +129,7 @@ export default class MultiInviter { // don't re-invite (there's no way in the UI to do this, but // for sanity's sake) - if (this.completionStates[addr] == 'invited') { + if (this.completionStates[addr] === 'invited') { this._inviteMore(nextIndex + 1); return; } @@ -120,7 +138,7 @@ export default class MultiInviter { if (this.groupId !== null) { doInvite = GroupStore.inviteUserToGroup(this.groupId, addr); } else { - doInvite = inviteToRoom(this.roomId, addr); + doInvite = this._inviteToRoom(this.roomId, addr); } doInvite.then(() => { @@ -129,29 +147,34 @@ export default class MultiInviter { this.completionStates[addr] = 'invited'; this._inviteMore(nextIndex + 1); - }, (err) => { + }).catch((err) => { if (this._canceled) { return; } let errorText; let fatal = false; - if (err.errcode == 'M_FORBIDDEN') { + if (err.errcode === 'M_FORBIDDEN') { fatal = true; - errorText = 'You do not have permission to invite people to this room.'; - } else if (err.errcode == 'M_LIMIT_EXCEEDED') { + errorText = _t('You do not have permission to invite people to this room.'); + } else if (err.errcode === 'M_LIMIT_EXCEEDED') { // we're being throttled so wait a bit & try again setTimeout(() => { this._inviteMore(nextIndex); }, 5000); return; + } else if(err.errcode === "M_NOT_FOUND") { + errorText = _t("User %(user_id)s does not exist", {user_id: addr}); } else { - errorText = 'Unknown server error'; + errorText = _t('Unknown server error'); } this.completionStates[addr] = 'error'; this.errorTexts[addr] = errorText; this.busy = !fatal; + this.fatal = fatal; if (!fatal) { this._inviteMore(nextIndex + 1); + } else { + this.deferred.resolve(this.completionStates); } }); } diff --git a/src/utils/PasswordScorer.js b/src/utils/PasswordScorer.js new file mode 100644 index 00000000000..e4bbec1637b --- /dev/null +++ b/src/utils/PasswordScorer.js @@ -0,0 +1,84 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import Zxcvbn from 'zxcvbn'; + +import MatrixClientPeg from '../MatrixClientPeg'; +import { _t, _td } from '../languageHandler'; + +const ZXCVBN_USER_INPUTS = [ + 'riot', + 'matrix', +]; + +// Translations for zxcvbn's suggestion strings +_td("Use a few words, avoid common phrases"); +_td("No need for symbols, digits, or uppercase letters"); +_td("Use a longer keyboard pattern with more turns"); +_td("Avoid repeated words and characters"); +_td("Avoid sequences"); +_td("Avoid recent years"); +_td("Avoid years that are associated with you"); +_td("Avoid dates and years that are associated with you"); +_td("Capitalization doesn't help very much"); +_td("All-uppercase is almost as easy to guess as all-lowercase"); +_td("Reversed words aren't much harder to guess"); +_td("Predictable substitutions like '@' instead of 'a' don't help very much"); +_td("Add another word or two. Uncommon words are better."); + +// and warnings +_td("Repeats like \"aaa\" are easy to guess"); +_td("Repeats like \"abcabcabc\" are only slightly harder to guess than \"abc\""); +_td("Sequences like abc or 6543 are easy to guess"); +_td("Recent years are easy to guess"); +_td("Dates are often easy to guess"); +_td("This is a top-10 common password"); +_td("This is a top-100 common password"); +_td("This is a very common password"); +_td("This is similar to a commonly used password"); +_td("A word by itself is easy to guess"); +_td("Names and surnames by themselves are easy to guess"); +_td("Common names and surnames are easy to guess"); + +/** + * Wrapper around zxcvbn password strength estimation + * Include this only from async components: it pulls in zxcvbn + * (obviously) which is large. + */ +export function scorePassword(password) { + if (password.length === 0) return null; + + const userInputs = ZXCVBN_USER_INPUTS.slice(); + userInputs.push(MatrixClientPeg.get().getUserIdLocalpart()); + + let zxcvbnResult = Zxcvbn(password, userInputs); + // Work around https://github.com/dropbox/zxcvbn/issues/216 + if (password.includes(' ')) { + const resultNoSpaces = Zxcvbn(password.replace(/ /g, ''), userInputs); + if (resultNoSpaces.score < zxcvbnResult.score) zxcvbnResult = resultNoSpaces; + } + + for (let i = 0; i < zxcvbnResult.feedback.suggestions.length; ++i) { + // translate suggestions + zxcvbnResult.feedback.suggestions[i] = _t(zxcvbnResult.feedback.suggestions[i]); + } + // and warning, if any + if (zxcvbnResult.feedback.warning) { + zxcvbnResult.feedback.warning = _t(zxcvbnResult.feedback.warning); + } + + return zxcvbnResult; +} diff --git a/test/components/structures/GroupView-test.js b/test/components/structures/GroupView-test.js index 3b3510f26e0..89632dcc48f 100644 --- a/test/components/structures/GroupView-test.js +++ b/test/components/structures/GroupView-test.js @@ -164,7 +164,7 @@ describe('GroupView', function() { it('should indicate failure after failed /summary', function() { const groupView = ReactTestUtils.findRenderedComponentWithType(root, GroupView); - const prom = waitForUpdate(groupView).then(() => { + const prom = waitForUpdate(groupView, 4).then(() => { ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView_error'); }); @@ -179,7 +179,7 @@ describe('GroupView', function() { it('should show a group avatar, name, id and short description after successful /summary', function() { const groupView = ReactTestUtils.findRenderedComponentWithType(root, GroupView); - const prom = waitForUpdate(groupView).then(() => { + const prom = waitForUpdate(groupView, 4).then(() => { ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView'); const avatar = ReactTestUtils.findRenderedComponentWithType(root, sdk.getComponent('avatars.GroupAvatar')); @@ -214,7 +214,7 @@ describe('GroupView', function() { it('should show a simple long description after successful /summary', function() { const groupView = ReactTestUtils.findRenderedComponentWithType(root, GroupView); - const prom = waitForUpdate(groupView).then(() => { + const prom = waitForUpdate(groupView, 4).then(() => { ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView'); const longDesc = ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView_groupDesc'); @@ -235,7 +235,7 @@ describe('GroupView', function() { it('should show a placeholder if a long description is not set', function() { const groupView = ReactTestUtils.findRenderedComponentWithType(root, GroupView); - const prom = waitForUpdate(groupView).then(() => { + const prom = waitForUpdate(groupView, 4).then(() => { const placeholder = ReactTestUtils .findRenderedDOMComponentWithClass(root, 'mx_GroupView_groupDesc_placeholder'); const placeholderElement = ReactDOM.findDOMNode(placeholder); @@ -255,7 +255,7 @@ describe('GroupView', function() { it('should show a complicated long description after successful /summary', function() { const groupView = ReactTestUtils.findRenderedComponentWithType(root, GroupView); - const prom = waitForUpdate(groupView).then(() => { + const prom = waitForUpdate(groupView, 4).then(() => { const longDesc = ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView_groupDesc'); const longDescElement = ReactDOM.findDOMNode(longDesc); expect(longDescElement).toExist(); @@ -282,7 +282,7 @@ describe('GroupView', function() { it('should disallow images with non-mxc URLs', function() { const groupView = ReactTestUtils.findRenderedComponentWithType(root, GroupView); - const prom = waitForUpdate(groupView).then(() => { + const prom = waitForUpdate(groupView, 4).then(() => { const longDesc = ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView_groupDesc'); const longDescElement = ReactDOM.findDOMNode(longDesc); expect(longDescElement).toExist(); @@ -305,7 +305,7 @@ describe('GroupView', function() { it('should show a RoomDetailList after a successful /summary & /rooms (no rooms returned)', function() { const groupView = ReactTestUtils.findRenderedComponentWithType(root, GroupView); - const prom = waitForUpdate(groupView).then(() => { + const prom = waitForUpdate(groupView, 4).then(() => { const roomDetailList = ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_RoomDetailList'); const roomDetailListElement = ReactDOM.findDOMNode(roomDetailList); expect(roomDetailListElement).toExist(); @@ -322,7 +322,7 @@ describe('GroupView', function() { it('should show a RoomDetailList after a successful /summary & /rooms (with a single room)', function() { const groupView = ReactTestUtils.findRenderedComponentWithType(root, GroupView); - const prom = waitForUpdate(groupView).then(() => { + const prom = waitForUpdate(groupView, 4).then(() => { const roomDetailList = ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_RoomDetailList'); const roomDetailListElement = ReactDOM.findDOMNode(roomDetailList); expect(roomDetailListElement).toExist(); @@ -355,4 +355,25 @@ describe('GroupView', function() { httpBackend.flush(undefined, undefined, 0); return prom; }); + + it('should show a summary even if /users fails', function() { + const groupView = ReactTestUtils.findRenderedComponentWithType(root, GroupView); + + // Only wait for 3 updates in this test since we don't change state for + // the /users error case. + const prom = waitForUpdate(groupView, 3).then(() => { + const shortDesc = ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView_header_shortDesc'); + const shortDescElement = ReactDOM.findDOMNode(shortDesc); + expect(shortDescElement).toExist(); + expect(shortDescElement.innerText).toBe('This is a community'); + }); + + httpBackend.when('GET', '/groups/' + groupIdEncoded + '/summary').respond(200, summaryResponse); + httpBackend.when('GET', '/groups/' + groupIdEncoded + '/users').respond(500, {}); + httpBackend.when('GET', '/groups/' + groupIdEncoded + '/invited_users').respond(200, { chunk: [] }); + httpBackend.when('GET', '/groups/' + groupIdEncoded + '/rooms').respond(200, { chunk: [] }); + + httpBackend.flush(undefined, undefined, 0); + return prom; + }); }); diff --git a/test/components/views/groups/GroupMemberList-test.js b/test/components/views/groups/GroupMemberList-test.js new file mode 100644 index 00000000000..d71d0377d7e --- /dev/null +++ b/test/components/views/groups/GroupMemberList-test.js @@ -0,0 +1,149 @@ +/* +Copyright 2018 New Vector Ltd. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from "react"; +import ReactDOM from "react-dom"; +import ReactTestUtils from "react-dom/test-utils"; +import expect from "expect"; + +import MockHttpBackend from "matrix-mock-request"; +import MatrixClientPeg from "../../../../src/MatrixClientPeg"; +import sdk from "matrix-react-sdk"; +import Matrix from "matrix-js-sdk"; + +import * as TestUtils from "test-utils"; +const { waitForUpdate } = TestUtils; + +const GroupMemberList = sdk.getComponent("views.groups.GroupMemberList"); +const WrappedGroupMemberList = TestUtils.wrapInMatrixClientContext(GroupMemberList); + +describe("GroupMemberList", function() { + let root; + let rootElement; + let httpBackend; + let summaryResponse; + let groupId; + let groupIdEncoded; + + // Summary response fields + const user = { + is_privileged: true, // can edit the group + is_public: true, // appear as a member to non-members + is_publicised: true, // display flair + }; + const usersSection = { + roles: {}, + total_user_count_estimate: 0, + users: [], + }; + const roomsSection = { + categories: {}, + rooms: [], + total_room_count_estimate: 0, + }; + + // Users response fields + const usersResponse = { + chunk: [ + { + user_id: "@test:matrix.org", + displayname: "Test", + avatar_url: "mxc://matrix.org/oUxxDyzQOHdVDMxgwFzyCWEe", + is_public: true, + is_privileged: true, + attestation: {}, + }, + ], + }; + + beforeEach(function() { + TestUtils.beforeEach(this); + + httpBackend = new MockHttpBackend(); + + Matrix.request(httpBackend.requestFn); + + MatrixClientPeg.get = () => Matrix.createClient({ + baseUrl: "https://my.home.server", + userId: "@me:here", + accessToken: "123456789", + }); + + summaryResponse = { + profile: { + avatar_url: "mxc://someavatarurl", + is_openly_joinable: true, + is_public: true, + long_description: "This is a LONG description.", + name: "The name of a community", + short_description: "This is a community", + }, + user, + users_section: usersSection, + rooms_section: roomsSection, + }; + + groupId = "+" + Math.random().toString(16).slice(2) + ":domain"; + groupIdEncoded = encodeURIComponent(groupId); + + rootElement = document.createElement("div"); + root = ReactDOM.render(, rootElement); + }); + + afterEach(function() { + ReactDOM.unmountComponentAtNode(rootElement); + }); + + it("should show group member list after successful /users", function() { + const groupMemberList = ReactTestUtils.findRenderedComponentWithType(root, GroupMemberList); + const prom = waitForUpdate(groupMemberList, 4).then(() => { + ReactTestUtils.findRenderedDOMComponentWithClass(root, "mx_MemberList"); + + const memberList = ReactTestUtils.findRenderedDOMComponentWithClass(root, "mx_MemberList_joined"); + const memberListElement = ReactDOM.findDOMNode(memberList); + expect(memberListElement).toExist(); + expect(memberListElement.innerText).toBe("Test"); + }); + + httpBackend.when("GET", "/groups/" + groupIdEncoded + "/summary").respond(200, summaryResponse); + httpBackend.when("GET", "/groups/" + groupIdEncoded + "/users").respond(200, usersResponse); + httpBackend.when("GET", "/groups/" + groupIdEncoded + "/invited_users").respond(200, { chunk: [] }); + httpBackend.when("GET", "/groups/" + groupIdEncoded + "/rooms").respond(200, { chunk: [] }); + + httpBackend.flush(undefined, undefined, 0); + return prom; + }); + + it("should show error message after failed /users", function() { + const groupMemberList = ReactTestUtils.findRenderedComponentWithType(root, GroupMemberList); + const prom = waitForUpdate(groupMemberList, 4).then(() => { + ReactTestUtils.findRenderedDOMComponentWithClass(root, "mx_MemberList"); + + const memberList = ReactTestUtils.findRenderedDOMComponentWithClass(root, "mx_MemberList_joined"); + const memberListElement = ReactDOM.findDOMNode(memberList); + expect(memberListElement).toExist(); + expect(memberListElement.innerText).toBe("Failed to load group members"); + }); + + httpBackend.when("GET", "/groups/" + groupIdEncoded + "/summary").respond(200, summaryResponse); + httpBackend.when("GET", "/groups/" + groupIdEncoded + "/users").respond(500, {}); + httpBackend.when("GET", "/groups/" + groupIdEncoded + "/invited_users").respond(200, { chunk: [] }); + httpBackend.when("GET", "/groups/" + groupIdEncoded + "/rooms").respond(200, { chunk: [] }); + + httpBackend.flush(undefined, undefined, 0); + return prom; + }); +}); diff --git a/test/matrix-to-test.js b/test/matrix-to-test.js index 70533575c48..6392e326e9e 100644 --- a/test/matrix-to-test.js +++ b/test/matrix-to-test.js @@ -150,7 +150,39 @@ describe('matrix-to', function() { expect(pickedServers[2]).toBe("third"); }); - it('should work with IPv4 hostnames', function() { + it('should pick a maximum of 3 candidate servers', function() { + peg.get().getRoom = () => { + return { + getJoinedMembers: () => [ + { + userId: "@alice:alpha", + powerLevel: 100, + }, + { + userId: "@alice:bravo", + powerLevel: 0, + }, + { + userId: "@alice:charlie", + powerLevel: 0, + }, + { + userId: "@alice:delta", + powerLevel: 0, + }, + { + userId: "@alice:echo", + powerLevel: 0, + }, + ], + }; + }; + const pickedServers = pickServerCandidates("!somewhere:example.org"); + expect(pickedServers).toExist(); + expect(pickedServers.length).toBe(3); + }); + + it('should not consider IPv4 hosts', function() { peg.get().getRoom = () => { return { getJoinedMembers: () => [ @@ -163,11 +195,10 @@ describe('matrix-to', function() { }; const pickedServers = pickServerCandidates("!somewhere:example.org"); expect(pickedServers).toExist(); - expect(pickedServers.length).toBe(1); - expect(pickedServers[0]).toBe("127.0.0.1"); + expect(pickedServers.length).toBe(0); }); - it('should work with IPv6 hostnames', function() { + it('should not consider IPv6 hosts', function() { peg.get().getRoom = () => { return { getJoinedMembers: () => [ @@ -180,11 +211,10 @@ describe('matrix-to', function() { }; const pickedServers = pickServerCandidates("!somewhere:example.org"); expect(pickedServers).toExist(); - expect(pickedServers.length).toBe(1); - expect(pickedServers[0]).toBe("[::1]"); + expect(pickedServers.length).toBe(0); }); - it('should work with IPv4 hostnames with ports', function() { + it('should not consider IPv4 hostnames with ports', function() { peg.get().getRoom = () => { return { getJoinedMembers: () => [ @@ -197,11 +227,10 @@ describe('matrix-to', function() { }; const pickedServers = pickServerCandidates("!somewhere:example.org"); expect(pickedServers).toExist(); - expect(pickedServers.length).toBe(1); - expect(pickedServers[0]).toBe("127.0.0.1:8448"); + expect(pickedServers.length).toBe(0); }); - it('should work with IPv6 hostnames with ports', function() { + it('should not consider IPv6 hostnames with ports', function() { peg.get().getRoom = () => { return { getJoinedMembers: () => [ @@ -214,8 +243,7 @@ describe('matrix-to', function() { }; const pickedServers = pickServerCandidates("!somewhere:example.org"); expect(pickedServers).toExist(); - expect(pickedServers.length).toBe(1); - expect(pickedServers[0]).toBe("[::1]:8448"); + expect(pickedServers.length).toBe(0); }); it('should work with hostnames with ports', function() { @@ -235,6 +263,140 @@ describe('matrix-to', function() { expect(pickedServers[0]).toBe("example.org:8448"); }); + it('should not consider servers explicitly denied by ACLs', function() { + peg.get().getRoom = () => { + return { + getJoinedMembers: () => [ + { + userId: "@alice:evilcorp.com", + powerLevel: 100, + }, + { + userId: "@bob:chat.evilcorp.com", + powerLevel: 0, + }, + ], + currentState: { + getStateEvents: (type, key) => { + if (type !== "m.room.server_acl" || key !== "") return null; + return { + getContent: () => { + return { + deny: ["evilcorp.com", "*.evilcorp.com"], + allow: ["*"], + }; + }, + }; + }, + }, + }; + }; + const pickedServers = pickServerCandidates("!somewhere:example.org"); + expect(pickedServers).toExist(); + expect(pickedServers.length).toBe(0); + }); + + it('should not consider servers not allowed by ACLs', function() { + peg.get().getRoom = () => { + return { + getJoinedMembers: () => [ + { + userId: "@alice:evilcorp.com", + powerLevel: 100, + }, + { + userId: "@bob:chat.evilcorp.com", + powerLevel: 0, + }, + ], + currentState: { + getStateEvents: (type, key) => { + if (type !== "m.room.server_acl" || key !== "") return null; + return { + getContent: () => { + return { + deny: [], + allow: [], // implies "ban everyone" + }; + }, + }; + }, + }, + }; + }; + const pickedServers = pickServerCandidates("!somewhere:example.org"); + expect(pickedServers).toExist(); + expect(pickedServers.length).toBe(0); + }); + + it('should consider servers not explicitly banned by ACLs', function() { + peg.get().getRoom = () => { + return { + getJoinedMembers: () => [ + { + userId: "@alice:evilcorp.com", + powerLevel: 100, + }, + { + userId: "@bob:chat.evilcorp.com", + powerLevel: 0, + }, + ], + currentState: { + getStateEvents: (type, key) => { + if (type !== "m.room.server_acl" || key !== "") return null; + return { + getContent: () => { + return { + deny: ["*.evilcorp.com"], // evilcorp.com is still good though + allow: ["*"], + }; + }, + }; + }, + }, + }; + }; + const pickedServers = pickServerCandidates("!somewhere:example.org"); + expect(pickedServers).toExist(); + expect(pickedServers.length).toBe(1); + expect(pickedServers[0]).toEqual("evilcorp.com"); + }); + + it('should consider servers not disallowed by ACLs', function() { + peg.get().getRoom = () => { + return { + getJoinedMembers: () => [ + { + userId: "@alice:evilcorp.com", + powerLevel: 100, + }, + { + userId: "@bob:chat.evilcorp.com", + powerLevel: 0, + }, + ], + currentState: { + getStateEvents: (type, key) => { + if (type !== "m.room.server_acl" || key !== "") return null; + return { + getContent: () => { + return { + deny: [], + allow: ["evilcorp.com"], // implies "ban everyone else" + }; + }, + }; + }, + }, + }; + }; + const pickedServers = pickServerCandidates("!somewhere:example.org"); + expect(pickedServers).toExist(); + expect(pickedServers.length).toBe(1); + expect(pickedServers[0]).toEqual("evilcorp.com"); + }); + it('should generate an event permalink for room IDs with no candidate servers', function() { peg.get().getRoom = () => null; const result = makeEventPermalink("!somewhere:example.org", "$something:example.com"); diff --git a/test/test-utils.js b/test/test-utils.js index bc4d29210ef..d5bcd9397a5 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -310,19 +310,26 @@ export function wrapInMatrixClientContext(WrappedComponent) { /** * Call fn before calling componentDidUpdate on a react component instance, inst. * @param {React.Component} inst an instance of a React component. + * @param {integer} updates Number of updates to wait for. (Defaults to 1.) * @returns {Promise} promise that resolves when componentDidUpdate is called on * given component instance. */ -export function waitForUpdate(inst) { +export function waitForUpdate(inst, updates = 1) { return new Promise((resolve, reject) => { const cdu = inst.componentDidUpdate; + console.log(`Waiting for ${updates} update(s)`); + inst.componentDidUpdate = (prevProps, prevState, snapshot) => { - resolve(); + updates--; + console.log(`Got update, ${updates} remaining`); - if (cdu) cdu(prevProps, prevState, snapshot); + if (updates == 0) { + inst.componentDidUpdate = cdu; + resolve(); + } - inst.componentDidUpdate = cdu; + if (cdu) cdu(prevProps, prevState, snapshot); }; }); } From 982e037822f7afb0e41fe18e2de5141d3cd4aa49 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 3 Jan 2019 18:05:38 +0000 Subject: [PATCH 2/2] Fix merge --- res/css/structures/_RightPanel.scss | 4 ---- res/themes/dharma/css/_dharma.scss | 10 ++++++++++ src/components/views/rooms/RoomList.js | 16 ++++++++-------- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/res/css/structures/_RightPanel.scss b/res/css/structures/_RightPanel.scss index ae9e7ba981e..592eea067e4 100644 --- a/res/css/structures/_RightPanel.scss +++ b/res/css/structures/_RightPanel.scss @@ -57,10 +57,6 @@ limitations under the License. pointer-events: none; } -.mx_RightPanel_headerButton_badgeHighlight .mx_RightPanel_headerButton_badge { - color: $warning-color; -} - .mx_RightPanel_headerButton_highlight { border-color: $button-bg-color; } diff --git a/res/themes/dharma/css/_dharma.scss b/res/themes/dharma/css/_dharma.scss index 0851762be20..08a287ad718 100644 --- a/res/themes/dharma/css/_dharma.scss +++ b/res/themes/dharma/css/_dharma.scss @@ -186,6 +186,8 @@ $lightbox-border-color: #ffffff; // unused? $progressbar-color: #000; +$room-warning-bg-color: #fff8e3; + /*** form elements ***/ // .mx_textinput is a container for a text input @@ -320,3 +322,11 @@ input[type=search]::-webkit-search-results-decoration { font-size: 15px; padding: 0px 1.5em 0px 1.5em; } + +@define-mixin mx_DialogButton_secondary { + // flip colours for the secondary ones + font-weight: 600; + border: 1px solid $accent-color ! important; + color: $accent-color; + background-color: $accent-fg-color; +} diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 181df6c6130..df2a2428527 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -622,7 +622,7 @@ module.exports = React.createClass({ list: this.state.lists['im.vector.fake.invite'], label: _t('Invites'), order: "recent", - incomingCall={incomingCallIfTaggedAs('im.vector.fake.invite')}, + incomingCall: incomingCallIfTaggedAs('im.vector.fake.invite'), isInvite: true, }, { @@ -630,7 +630,7 @@ module.exports = React.createClass({ label: _t('Favourites'), tagName: "m.favourite", order: "manual", - incomingCall={incomingCallIfTaggedAs('m.favourite')}, + incomingCall: incomingCallIfTaggedAs('m.favourite'), }, { list: this.state.lists['im.vector.fake.direct'], @@ -638,7 +638,7 @@ module.exports = React.createClass({ tagName: "im.vector.fake.direct", headerItems: this._getHeaderItems('im.vector.fake.direct'), order: "recent", - incomingCall={incomingCallIfTaggedAs('im.vector.fake.direct')}, + incomingCall: incomingCallIfTaggedAs('im.vector.fake.direct'), onAddRoom: () => {dis.dispatch({action: 'view_create_chat'})}, }, { @@ -646,7 +646,7 @@ module.exports = React.createClass({ label: _t('Rooms'), headerItems: this._getHeaderItems('im.vector.fake.recent'), order: "recent", - incomingCall={incomingCallIfTaggedAs('im.vector.fake.recent')}, + incomingCall: incomingCallIfTaggedAs('im.vector.fake.recent'), onAddRoom: () => {dis.dispatch({action: 'view_create_room'})}, }, ]; @@ -660,7 +660,7 @@ module.exports = React.createClass({ label: labelForTagName(tagName), tagName: tagName, order: "manual", - incomingCallIfTaggedAs(tagName), + incomingCall: incomingCallIfTaggedAs(tagName), }; }); subLists = subLists.concat(tagSubLists); @@ -670,13 +670,13 @@ module.exports = React.createClass({ label: _t('Low priority'), tagName: "m.lowpriority", order: "recent", - incomingCall={incomingCallIfTaggedAs('m.lowpriority')}, + incomingCall: incomingCallIfTaggedAs('m.lowpriority'), }, { list: this.state.lists['im.vector.fake.archived'], label: _t('Historical'), order: "recent", - incomingCall={incomingCallIfTaggedAs('im.vector.fake.archived')}, + incomingCall: incomingCallIfTaggedAs('im.vector.fake.archived'), startAsHidden: true, showSpinner: this.state.isLoadingLeftRooms, onHeaderClick: this.onArchivedHeaderClick, @@ -686,7 +686,7 @@ module.exports = React.createClass({ label: _t('System Alerts'), tagName: "m.lowpriority", order: "recent", - incomingCall={incomingCallIfTaggedAs('m.server_notice')}, + incomingCall: incomingCallIfTaggedAs('m.server_notice'), }, ]);