diff --git a/.eslintrc.js b/.eslintrc.js index 62d24ea707..971809f851 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/.travis-test-riot.sh b/.travis-test-riot.sh index 88b3719b3a..d1c2804b2a 100755 --- a/.travis-test-riot.sh +++ b/.travis-test-riot.sh @@ -30,7 +30,7 @@ popd if [ "$TRAVIS_BRANCH" = "develop" ] then # run end to end tests - git clone https://github.com/matrix-org/matrix-react-end-to-end-tests.git --branch master + scripts/fetchdep.sh matrix-org matrix-react-end-to-end-tests master pushd matrix-react-end-to-end-tests ln -s $REACT_SDK_DIR/$RIOT_WEB_DIR riot/riot-web # PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true ./install.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index eea47dcb8f..742b8b4529 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 99025f0e0a..f7c8c8b1c5 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 ac45497dd4..c70f68902b 100644 --- a/README.md +++ b/README.md @@ -128,60 +128,35 @@ Github Issues All issues should be filed under https://github.com/vector-im/riot-web/issues for now. -OUTDATED: To Create Your Own Skin -================================= +Development +=========== -**This is ALL LIES currently, and needs to be updated** +Ensure you have the latest stable Node JS runtime installed (v8.x is the best choice). Then check out +the code and pull in dependencies: -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. +```bash +git clone https://github.com/matrix-org/matrix-react-sdk.git +cd matrix-react-sdk +git checkout develop +npm install +``` -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. +`matrix-react-sdk` depends on `matrix-js-sdk`. To make use of changes in the +latter and to ensure tests run against the develop branch of `matrix-js-sdk`, +you should run the following which will sync changes from the JS sdk here. -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. +```bash +npm link ../matrix-js-sdk +``` -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. +Command assumes a checked out and installed `matrix-js-sdk` folder in parent +folder. -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. +Running tests +============= -If you make a derived skin, you only need copy the files you wish to customise. +Ensure you've followed the above development instructions and then: -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. +```bash +npm run test +``` diff --git a/code_style.md b/code_style.md index 2cac303e54..96f3879ebc 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 b5cdfdf401..6dc9a6bfcf 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", @@ -121,8 +124,9 @@ "eslint-plugin-flowtype": "^2.30.0", "eslint-plugin-react": "^7.7.0", "estree-walker": "^0.5.0", - "expect": "^1.16.0", + "expect": "^23.6.0", "flow-parser": "^0.57.3", + "jest-mock": "^23.2.0", "karma": "^3.0.0", "karma-chrome-launcher": "^0.2.3", "karma-cli": "^1.0.1", diff --git a/res/css/_common.scss b/res/css/_common.scss index 149ec75569..bec4c02c18 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; } @@ -47,7 +47,7 @@ h2 { a:hover, a:link, a:visited { - color: $accent-color; + color: $accent-color-alt; } input[type=text], input[type=password], textarea { @@ -301,7 +301,7 @@ textarea { } .mx_textButton { - @mixin mx_DialogButton_small; + @mixin mx_DialogButton_small; } .mx_textButton:hover { diff --git a/res/css/_components.scss b/res/css/_components.scss index 16bb4938c1..bdcf27ac16 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -26,8 +26,10 @@ @import "./structures/_ViewSource.scss"; @import "./structures/login/_Login.scss"; @import "./views/avatars/_BaseAvatar.scss"; +@import "./views/avatars/_MemberStatusMessageAvatar.scss"; @import "./views/context_menus/_MessageContextMenu.scss"; @import "./views/context_menus/_RoomTileContextMenu.scss"; +@import "./views/context_menus/_StatusMessageContextMenu.scss"; @import "./views/context_menus/_TagTileContextMenu.scss"; @import "./views/context_menus/_TopLeftMenu.scss"; @import "./views/dialogs/_BugReportDialog.scss"; @@ -36,7 +38,6 @@ @import "./views/dialogs/_ChatInviteDialog.scss"; @import "./views/dialogs/_ConfirmUserActionDialog.scss"; @import "./views/dialogs/_CreateGroupDialog.scss"; -@import "./views/dialogs/_CreateKeyBackupDialog.scss"; @import "./views/dialogs/_CreateRoomDialog.scss"; @import "./views/dialogs/_DeactivateAccountDialog.scss"; @import "./views/dialogs/_DevtoolsDialog.scss"; @@ -50,6 +51,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"; @@ -105,6 +107,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/_LeftPanel.scss b/res/css/structures/_LeftPanel.scss index 1fe7a42678..47cb231b58 100644 --- a/res/css/structures/_LeftPanel.scss +++ b/res/css/structures/_LeftPanel.scss @@ -89,12 +89,6 @@ limitations under the License. pointer-events: none; } -.mx_LeftPanel_container.collapsed .mx_RoleButton { - margin-right: 0px ! important; - padding-top: 3px ! important; - padding-bottom: 3px ! important; -} - .mx_BottomLeftMenu_options > div { display: inline-block; } diff --git a/res/css/structures/_RightPanel.scss b/res/css/structures/_RightPanel.scss index 07fe404749..592eea067e 100644 --- a/res/css/structures/_RightPanel.scss +++ b/res/css/structures/_RightPanel.scss @@ -45,7 +45,8 @@ limitations under the License. cursor: pointer; flex: 0 0 auto; vertical-align: top; - padding-left: 4px; + margin-top: 4px; + padding-left: 5px; padding-right: 5px; text-align: center; position: relative; @@ -57,7 +58,7 @@ limitations under the License. } .mx_RightPanel_headerButton_highlight { - border-color: $accent-color; + border-color: $button-bg-color; } .mx_RightPanel_headerButton_badge { diff --git a/res/css/structures/_RoomSubList.scss b/res/css/structures/_RoomSubList.scss index ce28c168b9..2d471ee198 100644 --- a/res/css/structures/_RoomSubList.scss +++ b/res/css/structures/_RoomSubList.scss @@ -19,14 +19,14 @@ limitations under the License. each with a flex-shrink difference of 4 order of magnitude, so they ideally wouldn't affect each other. lowest category: .mx_RoomSubList - flex:-shrink: 10000000 + flex-shrink: 10000000 distribute size of items within the same categery by their size middle category: .mx_RoomSubList.resized-sized - flex:-shrink: 1000 + flex-shrink: 1000 applied when using the resizer, will have a max-height set to it, to limit the size highest category: .mx_RoomSubList.resized-all - flex:-shrink: 1 + flex-shrink: 1 small flex-shrink value (1), is only added if you can drag the resizer so far so in practice you can only assign this category if there is enough space. */ @@ -39,7 +39,7 @@ limitations under the License. } .mx_RoomSubList_nonEmpty { - min-height: 76px; + min-height: 70px; .mx_AutoHideScrollbar_offset { padding-bottom: 4px; @@ -94,7 +94,7 @@ limitations under the License. font-weight: 600; font-size: 12px; padding: 0 5px; - background-color: $accent-color; + background-color: $roomtile-name-color; } .mx_RoomSubList_addRoom, .mx_RoomSubList_badge { @@ -154,7 +154,7 @@ limitations under the License. position: sticky; left: 0; right: 0; - height: 40px; + height: 30px; content: ""; display: block; z-index: 100; @@ -162,20 +162,20 @@ limitations under the License. } &.mx_IndicatorScrollbar_topOverflow > .mx_AutoHideScrollbar_offset { - margin-top: -40px; + margin-top: -30px; } &.mx_IndicatorScrollbar_bottomOverflow > .mx_AutoHideScrollbar_offset { - margin-bottom: -40px; + margin-bottom: -30px; } &.mx_IndicatorScrollbar_topOverflow::before { top: 0; - background: linear-gradient($secondary-accent-color, transparent); + background: linear-gradient(to top, rgba(242,245,248,0), rgba(242,245,248,1)); } &.mx_IndicatorScrollbar_bottomOverflow::after { bottom: 0; - background: linear-gradient(transparent, $secondary-accent-color); + background: linear-gradient(to bottom, rgba(242,245,248,0), rgba(242,245,248,1)); } } diff --git a/res/css/structures/_TagPanel.scss b/res/css/structures/_TagPanel.scss index 85e5c1742f..77eefc7e10 100644 --- a/res/css/structures/_TagPanel.scss +++ b/res/css/structures/_TagPanel.scss @@ -124,10 +124,23 @@ limitations under the License. padding-right: 4px; } +.mx_TagPanel_groupsButton { + flex: 0; + margin: 17px 0 3px 0; +} + +.mx_TagPanel_groupsButton > .mx_GroupsButton:before { + mask: url('../../img/feather-icons/users.svg'); + mask-position: center 11px; +} + +.mx_TagPanel_groupsButton > .mx_TagPanel_report:before { + mask: url('../../img/feather-icons/life-buoy.svg'); + mask-position: center 9px; +} + .mx_TagPanel_groupsButton > .mx_AccessibleButton { - flex: auto; - margin-bottom: 17px; - margin-top: 18px; + margin-bottom: 12px; height: 40px; width: 40px; border-radius: 20px; @@ -138,9 +151,7 @@ limitations under the License. &:before { background-color: $tagpanel-bg-color; - mask: url('../../img/icons-groups-nobg.svg'); mask-repeat: no-repeat; - mask-position: center 8px; content: ''; position: absolute; top: 0; diff --git a/res/css/views/dialogs/_CreateKeyBackupDialog.scss b/res/css/views/avatars/_MemberStatusMessageAvatar.scss similarity index 74% rename from res/css/views/dialogs/_CreateKeyBackupDialog.scss rename to res/css/views/avatars/_MemberStatusMessageAvatar.scss index a422cf858c..29cae9df34 100644 --- a/res/css/views/dialogs/_CreateKeyBackupDialog.scss +++ b/res/css/views/avatars/_MemberStatusMessageAvatar.scss @@ -14,12 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_CreateKeyBackupDialog { - padding-right: 40px; -} - -.mx_CreateKeyBackupDialog_recoveryKey { - padding: 20px; - color: $info-plinth-fg-color; - background-color: $info-plinth-bg-color; +.mx_MemberStatusMessageAvatar_hasStatus { + border: 2px solid $accent-color; + border-radius: 40px; + padding-right: 0 !important; /* Override AccessibleButton styling */ } diff --git a/res/css/views/context_menus/_StatusMessageContextMenu.scss b/res/css/views/context_menus/_StatusMessageContextMenu.scss new file mode 100644 index 0000000000..873ad99495 --- /dev/null +++ b/res/css/views/context_menus/_StatusMessageContextMenu.scss @@ -0,0 +1,55 @@ +/* +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_StatusMessageContextMenu_message { + display: inline-block; + border-radius: 3px 0 0 3px; + border: 1px solid $input-border-color; + font-size: 13px; + padding: 7px 7px 7px 9px; + width: 135px; + background-color: $primary-bg-color !important; +} + +.mx_StatusMessageContextMenu_submit { + display: inline-block; +} + +.mx_StatusMessageContextMenu_submitFaded { + opacity: 0.5; +} + +.mx_StatusMessageContextMenu_submit img { + vertical-align: middle; + margin-left: 8px; +} + +.mx_StatusMessageContextMenu hr { + border: 0.5px solid $menu-border-color; +} + +.mx_StatusMessageContextMenu_clearIcon { + margin: 5px 15px 5px 5px; + vertical-align: middle; +} + +.mx_StatusMessageContextMenu_clear { + padding: 2px; +} + +.mx_StatusMessageContextMenu_hasStatus .mx_StatusMessageContextMenu_clear { + color: $warning-color; +} diff --git a/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss b/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss index 507c89ace7..615b61f842 100644 --- a/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss +++ b/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss @@ -13,27 +13,79 @@ 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_CreateKeyBackupDialog { + padding-right: 40px; +} + +.mx_CreateKeyBackupDialog .mx_Dialog_title { + /* TODO: Consider setting this for all dialog titles. */ + margin-bottom: 1em; +} + .mx_CreateKeyBackupDialog_primaryContainer { /*FIXME: plinth colour in new theme(s). background-color: $accent-color;*/ padding: 20px } +.mx_CreateKeyBackupDialog_primaryContainer::after { + content: ""; + clear: both; + display: block; +} + +.mx_CreateKeyBackupDialog_passPhraseContainer { + display: flex; + align-items: start; +} + +.mx_CreateKeyBackupDialog_passPhraseHelp { + flex: 1; + height: 85px; + margin-left: 20px; + font-size: 80%; +} + +.mx_CreateKeyBackupDialog_passPhraseHelp progress { + width: 100%; +} + .mx_CreateKeyBackupDialog_passPhraseInput { - width: 300px; + flex: none; + width: 250px; border: 1px solid $accent-color; border-radius: 5px; padding: 10px; + margin-bottom: 1em; } .mx_CreateKeyBackupDialog_passPhraseMatch { - float: right; + margin-left: 20px; } -.mx_CreateKeyBackupDialog_recoveryKeyButtons { - float: right; +.mx_CreateKeyBackupDialog_recoveryKeyHeader { + margin-bottom: 1em; +} + +.mx_CreateKeyBackupDialog_recoveryKeyContainer { + display: flex; } .mx_CreateKeyBackupDialog_recoveryKey { - width: 300px; + width: 262px; + padding: 20px; + color: $info-plinth-fg-color; + background-color: $info-plinth-bg-color; + margin-right: 12px; +} + +.mx_CreateKeyBackupDialog_recoveryKeyButtons { + flex: 1; + display: flex; + align-items: center; +} + +.mx_CreateKeyBackupDialog_recoveryKeyButtons button { + flex: 1; + white-space: nowrap; } diff --git a/res/css/views/dialogs/keybackup/_NewRecoveryMethodDialog.scss b/res/css/views/dialogs/keybackup/_NewRecoveryMethodDialog.scss new file mode 100644 index 0000000000..370f82d9ab --- /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/elements/_ResizeHandle.scss b/res/css/views/elements/_ResizeHandle.scss index 8f533a02bc..42ff6e3825 100644 --- a/res/css/views/elements/_ResizeHandle.scss +++ b/res/css/views/elements/_ResizeHandle.scss @@ -17,21 +17,30 @@ limitations under the License. .mx_ResizeHandle { cursor: row-resize; flex: 0 0 auto; - background: $panel-divider-color; - background-clip: content-box; z-index: 100; } .mx_ResizeHandle.mx_ResizeHandle_horizontal { - width: 1px; margin: 0 -5px; padding: 0 5px; cursor: col-resize; } .mx_ResizeHandle.mx_ResizeHandle_vertical { - height: 1px; margin: -5px 0; padding: 5px 0; cursor: row-resize; } + +.mx_ResizeHandle > div { + background: $panel-divider-color; +} + +.mx_ResizeHandle.mx_ResizeHandle_horizontal > div { + width: 1px; + height: 100%; +} + +.mx_ResizeHandle.mx_ResizeHandle_vertical > div { + height: 1px; +} diff --git a/res/css/views/rooms/_EntityTile.scss b/res/css/views/rooms/_EntityTile.scss index 4289b3f2cf..c4d4d944a6 100644 --- a/res/css/views/rooms/_EntityTile.scss +++ b/res/css/views/rooms/_EntityTile.scss @@ -107,3 +107,10 @@ limitations under the License. } */ +.mx_EntityTile_subtext { + font-size: 11px; + opacity: 0.5; + overflow: hidden; + white-space: nowrap; + text-overflow: clip; +} diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 52074563f6..6b22c4fe66 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -445,7 +445,8 @@ limitations under the License. } .mx_EventTile_content .markdown-body a { - color: $accent-color; + color: $accent-color-alt; + text-decoration: underline; } .mx_EventTile_content .markdown-body .hljs { diff --git a/res/css/views/rooms/_MemberInfo.scss b/res/css/views/rooms/_MemberInfo.scss index a027c575dd..4af181a464 100644 --- a/res/css/views/rooms/_MemberInfo.scss +++ b/res/css/views/rooms/_MemberInfo.scss @@ -132,6 +132,13 @@ limitations under the License. margin-left: 8px; } +.mx_MemberInfo_statusMessage { + font-size: 11px; + opacity: 0.5; + overflow: hidden; + white-space: nowrap; + text-overflow: clip; +} .mx_MemberInfo .mx_MemberInfo_scrollContainer { flex: 1; } diff --git a/res/css/views/rooms/_MemberList.scss b/res/css/views/rooms/_MemberList.scss index 8e59eb85d5..567727fb64 100644 --- a/res/css/views/rooms/_MemberList.scss +++ b/res/css/views/rooms/_MemberList.scss @@ -87,7 +87,7 @@ limitations under the License. .mx_MemberList_invite span { margin: 0 auto; - background-image: url('../../img/icon-invite-people.svg'); + background-image: url('../../img/feather-icons/user-add.svg'); background-repeat: no-repeat; background-position: center left; padding-left: 25px; diff --git a/res/css/views/rooms/_RoomHeader.scss b/res/css/views/rooms/_RoomHeader.scss index 7d2cbfe863..0697ccf40f 100644 --- a/res/css/views/rooms/_RoomHeader.scss +++ b/res/css/views/rooms/_RoomHeader.scss @@ -59,8 +59,8 @@ limitations under the License. .mx_RoomHeader_buttons { display: flex; align-items: center; - margin-top: 4px; background-color: $primary-bg-color; + padding-right: 5px; } .mx_RoomHeader_info { @@ -197,7 +197,7 @@ limitations under the License. } .mx_RoomHeader_button { - margin-left: 12px; + margin-left: 10px; cursor: pointer; } diff --git a/res/css/views/rooms/_RoomRecoveryReminder.scss b/res/css/views/rooms/_RoomRecoveryReminder.scss new file mode 100644 index 0000000000..e4e2d19b42 --- /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/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss index ff54da7196..70d505e4ea 100644 --- a/res/css/views/rooms/_RoomTile.scss +++ b/res/css/views/rooms/_RoomTile.scss @@ -19,7 +19,7 @@ limitations under the License. flex-direction: row; align-items: center; cursor: pointer; - height: 40px; + height: 34px; margin: 0; padding: 0 8px 0 10px; position: relative; @@ -39,10 +39,6 @@ limitations under the License. .mx_RoomTile_menuButton { display: block; } - - .mx_RoomTile_badge { - display: none; - } } .mx_RoomTile_tooltip { @@ -52,17 +48,48 @@ limitations under the License. left: -12px; } -.mx_RoomTile_avatar { - flex: 0; - padding: 4px; - width: 32px; - height: 32px; + +.mx_RoomTile_nameContainer { + display: flex; + align-items: center; + flex: 1; + vertical-align: middle; +} + +.mx_RoomTile_labelContainer { + display: flex; + flex-direction: column; + flex: 1; +} + +.mx_RoomTile_subtext { + display: inline-block; + font-size: 11px; + padding: 0 0 0 7px; + margin: 0; + overflow: hidden; + white-space: nowrap; + text-overflow: clip; + position: relative; + bottom: 4px; } .mx_RoomTile_avatar_container { position: relative; } +.mx_RoomTile_avatar { + flex: 0; + padding: 4px; + width: 24px; + vertical-align: middle; +} + +.mx_RoomTile_hasSubtext .mx_RoomTile_avatar { + padding-top: 0; + vertical-align: super; +} + .mx_RoomTile_dm { display: block; position: absolute; @@ -75,7 +102,7 @@ limitations under the License. flex: 1 5 auto; font-size: 14px; font-weight: 600; - padding: 6px; + padding: 0 6px; color: $roomtile-name-color; white-space: nowrap; overflow-x: hidden; @@ -118,7 +145,7 @@ limitations under the License. } .mx_RoomTile_unreadNotify .mx_RoomTile_badge { - background-color: $accent-color; + background-color: $roomtile-name-color; } .mx_RoomTile_highlight .mx_RoomTile_badge { @@ -136,10 +163,6 @@ limitations under the License. .mx_RoomTile_selected { border-radius: 4px; background-color: $roomtile-selected-bg-color; - - .mx_RoomTile_name { - color: $roomtile-selected-color; - } } .mx_DNDRoomTile { @@ -168,4 +191,3 @@ limitations under the License. .mx_RoomTile.mx_RoomTile_transparent:focus { background-color: $roomtile-transparent-focused-color; } - diff --git a/res/css/views/rooms/_TopUnreadMessagesBar.scss b/res/css/views/rooms/_TopUnreadMessagesBar.scss index c4ca035a2e..67579552c1 100644 --- a/res/css/views/rooms/_TopUnreadMessagesBar.scss +++ b/res/css/views/rooms/_TopUnreadMessagesBar.scss @@ -14,6 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +@charset "utf-8"; + .mx_TopUnreadMessagesBar { z-index: 1000; position: absolute; @@ -22,6 +24,22 @@ limitations under the License. width: 38px; } +.mx_TopUnreadMessagesBar:after { + content: "ยท"; + position: absolute; + top: -8px; + left: 11px; + width: 16px; + height: 16px; + border-radius: 16px; + font-weight: 600; + font-size: 30px; + line-height: 14px; + text-align: center; + color: $secondary-accent-color; + background-color: $accent-color; +} + .mx_TopUnreadMessagesBar_scrollUp { height: 38px; border-radius: 19px; diff --git a/res/img/e2e/lock-warning.svg b/res/img/e2e/lock-warning.svg new file mode 100644 index 0000000000..a984ed85a0 --- /dev/null +++ b/res/img/e2e/lock-warning.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/res/img/feather-icons/face.svg b/res/img/feather-icons/face.svg new file mode 100644 index 0000000000..0a359b2dea --- /dev/null +++ b/res/img/feather-icons/face.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/res/img/feather-icons/files.svg b/res/img/feather-icons/files.svg new file mode 100644 index 0000000000..c66d9ad121 --- /dev/null +++ b/res/img/feather-icons/files.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/res/img/feather-icons/grid.svg b/res/img/feather-icons/grid.svg new file mode 100644 index 0000000000..e6912b0cc7 --- /dev/null +++ b/res/img/feather-icons/grid.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/res/img/feather-icons/life-buoy.svg b/res/img/feather-icons/life-buoy.svg new file mode 100644 index 0000000000..20bd0f0b5d --- /dev/null +++ b/res/img/feather-icons/life-buoy.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/res/img/feather-icons/notifications.svg b/res/img/feather-icons/notifications.svg new file mode 100644 index 0000000000..2fe85e810c --- /dev/null +++ b/res/img/feather-icons/notifications.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/res/img/feather-icons/paperclip.svg b/res/img/feather-icons/paperclip.svg new file mode 100644 index 0000000000..ed2bb88681 --- /dev/null +++ b/res/img/feather-icons/paperclip.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/res/img/feather-icons/phone.svg b/res/img/feather-icons/phone.svg new file mode 100644 index 0000000000..58b257f113 --- /dev/null +++ b/res/img/feather-icons/phone.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/res/img/feather-icons/search-input.svg b/res/img/feather-icons/search-input.svg new file mode 100644 index 0000000000..3be5acb32e --- /dev/null +++ b/res/img/feather-icons/search-input.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/res/img/feather-icons/search.svg b/res/img/feather-icons/search.svg new file mode 100644 index 0000000000..8b14246f64 --- /dev/null +++ b/res/img/feather-icons/search.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/res/img/feather-icons/settings.svg b/res/img/feather-icons/settings.svg new file mode 100644 index 0000000000..ea7ce5c55b --- /dev/null +++ b/res/img/feather-icons/settings.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/res/img/feather-icons/share.svg b/res/img/feather-icons/share.svg new file mode 100644 index 0000000000..a012e1b7a5 --- /dev/null +++ b/res/img/feather-icons/share.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/res/img/feather-icons/user-add.svg b/res/img/feather-icons/user-add.svg new file mode 100644 index 0000000000..cbb25934c1 --- /dev/null +++ b/res/img/feather-icons/user-add.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/res/img/feather-icons/user.svg b/res/img/feather-icons/user.svg new file mode 100644 index 0000000000..a789e580d5 --- /dev/null +++ b/res/img/feather-icons/user.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/res/img/feather-icons/users.svg b/res/img/feather-icons/users.svg new file mode 100644 index 0000000000..b0deac0a9e --- /dev/null +++ b/res/img/feather-icons/users.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/res/img/feather-icons/video.svg b/res/img/feather-icons/video.svg new file mode 100644 index 0000000000..a4c456832f --- /dev/null +++ b/res/img/feather-icons/video.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/res/img/icons-checkmark.svg b/res/img/icons-checkmark.svg new file mode 100644 index 0000000000..3c5392003d --- /dev/null +++ b/res/img/icons-checkmark.svg @@ -0,0 +1,17 @@ + + + + Tick + Created with Sketch. + + + + + + + + + + + + diff --git a/res/img/icons-close.svg b/res/img/icons-close.svg index e516140dd2..b2dd44fc26 100644 --- a/res/img/icons-close.svg +++ b/res/img/icons-close.svg @@ -75,22 +75,22 @@ - + - \ No newline at end of file + diff --git a/res/img/icons-room-add.svg b/res/img/icons-room-add.svg index 6dd2e21295..f0b7584df9 100644 --- a/res/img/icons-room-add.svg +++ b/res/img/icons-room-add.svg @@ -1,71 +1,9 @@ - - - - - - image/svg+xml - - - - - - - - - - - + + + + + + + + diff --git a/res/img/topleft-chevron.svg b/res/img/topleft-chevron.svg index fa89852874..1cfeaf6352 100644 --- a/res/img/topleft-chevron.svg +++ b/res/img/topleft-chevron.svg @@ -64,7 +64,7 @@ + style="stroke:#61708b;stroke-width:1.6"> diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 7bb9fcc053..636db5b39e 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -12,6 +12,7 @@ $light-fg-color: #747474; // button UI (white-on-green in light skin) $accent-fg-color: $primary-bg-color; $accent-color: #76CFA6; +$accent-color-alt: $accent-color; $accent-color-50pct: #76CFA67F; $selection-fg-color: $primary-fg-color; @@ -106,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); @@ -184,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/dharma/css/_dharma.scss b/res/themes/dharma/css/_dharma.scss index f1badb35ca..08a287ad71 100644 --- a/res/themes/dharma/css/_dharma.scss +++ b/res/themes/dharma/css/_dharma.scss @@ -20,19 +20,20 @@ $focus-bg-color: #dddddd; // button UI (white-on-green in light skin) $accent-fg-color: #ffffff; -$accent-color: #f56679; -$accent-color-50pct: #f56679; +$accent-color: #7ac9a1; +$accent-color-50pct: #92caad; +$accent-color-alt: #238CF5; $selection-fg-color: $primary-bg-color; -$focus-brightness: 125%; +$focus-brightness: 105%; // red warning colour -$warning-color: #ff0064; +$warning-color: #f56679; // background colour for warnings $warning-bg-color: #DF2A8B; $info-bg-color: #2A9EDF; -$mention-user-pill-bg-color: #ff0064; +$mention-user-pill-bg-color: $warning-color; $other-user-pill-bg-color: rgba(0, 0, 0, 0.1); // pinned events indicator @@ -121,13 +122,13 @@ $rte-group-pill-color: #aaa; $topleftmenu-color: #212121; $roomheader-color: #45474a; -$roomheader-addroom-color: #929eb4; +$roomheader-addroom-color: #91A1C0; $roomtopic-color: #9fa9ba; $eventtile-meta-color: $roomtopic-color; // ******************** -$roomtile-name-color: #929eb4; +$roomtile-name-color: #61708b; $roomtile-selected-color: #212121; $roomtile-notified-color: #212121; $roomtile-selected-bg-color: #fff; @@ -185,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 @@ -192,32 +195,40 @@ $progressbar-color: #000; // it has the appearance of a text box so the controls // appear to be part of the input -:not(.mx_textinput) > input[type=text], -:not(.mx_textinput) > input[type=search], -.mx_textinput { - display: block; - margin: 9px; - box-sizing: border-box; - background-color: transparent; - color: $input-darker-fg-color; - border-radius: 4px; - border: 1px solid #c1c1c1; -} +.mx_MatrixChat { -.mx_textinput { - display: flex; - align-items: center; -} + :not(.mx_textinput) > input[type=text], + :not(.mx_textinput) > input[type=search], + .mx_textinput { + display: block; + margin: 9px; + box-sizing: border-box; + background-color: transparent; + color: $input-darker-fg-color; + border-radius: 4px; + border: 1px solid #c1c1c1; + flex: 0 0 auto; + } -.mx_textinput > input[type=text], -.mx_textinput > input[type=search] { - border: none; - flex: 1; - color: inherit; //from .mx_textinput + .mx_textinput { + display: flex; + align-items: center; + + > input[type=text], + > input[type=search] { + border: none; + flex: 1; + color: $primary-fg-color; + }, + input::placeholder { + color: $roomsublist-label-fg-color; + } + } } input[type=text], -input[type=search] { +input[type=search], +input[type=password] { padding: 9px; font-family: $font-family; font-size: 14px; @@ -257,9 +268,11 @@ input[type=search].mx_textinput_icon { background-position: 10px center; } + +// FIXME THEME - Tint by CSS rather than referencing a duplicate asset input[type=text].mx_textinput_icon.mx_textinput_search, input[type=search].mx_textinput_icon.mx_textinput_search { - background-image: url('../../img/icons-search.svg'); + background-image: url('../../img/feather-icons/search-input.svg'); } // dont search UI as not all browsers support it, @@ -309,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/res/themes/dharma/css/_fonts.scss b/res/themes/dharma/css/_fonts.scss index 392a10206b..bb45432262 100644 --- a/res/themes/dharma/css/_fonts.scss +++ b/res/themes/dharma/css/_fonts.scss @@ -2,7 +2,7 @@ * Nunito. * Includes extended Latin and Vietnamese character sets * Current URLs are v9, derived from the contents of - * https://fonts.googleapis.com/css?family=Nunito:400,400i,600,600i,700,700i&subset=latin-ext,vietnamese + * https://fonts.googleapis.com/css?family=Nunito:400,400i,600,600i,700,700i&subset=latin-ext,vietnamese */ /* the 'src' links are relative to the bundle.css, which is in a subdirectory. @@ -11,37 +11,37 @@ font-family: 'Nunito'; font-style: italic; font-weight: 400; - src: local('Nunito Italic'), local('Nunito-Italic'), url('../../fonts/Nunito/XRXX3I6Li01BKofIMNaDRss.ttf') format('truetype'); + src: url('../../fonts/Nunito/XRXX3I6Li01BKofIMNaDRss.ttf') format('truetype'); } @font-face { font-family: 'Nunito'; font-style: italic; font-weight: 600; - src: local('Nunito SemiBold Italic'), local('Nunito-SemiBoldItalic'), url('../../fonts/Nunito/XRXQ3I6Li01BKofIMN5cYtvKUTo.ttf') format('truetype'); + src: url('../../fonts/Nunito/XRXQ3I6Li01BKofIMN5cYtvKUTo.ttf') format('truetype'); } @font-face { font-family: 'Nunito'; font-style: italic; font-weight: 700; - src: local('Nunito Bold Italic'), local('Nunito-BoldItalic'), url('../../fonts/Nunito/XRXQ3I6Li01BKofIMN44Y9vKUTo.ttf') format('truetype'); + src: url('../../fonts/Nunito/XRXQ3I6Li01BKofIMN44Y9vKUTo.ttf') format('truetype'); } @font-face { font-family: 'Nunito'; font-style: normal; font-weight: 400; - src: local('Nunito Regular'), local('Nunito-Regular'), url('../../fonts/Nunito/XRXV3I6Li01BKofINeaE.ttf') format('truetype'); + src: url('../../fonts/Nunito/XRXV3I6Li01BKofINeaE.ttf') format('truetype'); } @font-face { font-family: 'Nunito'; font-style: normal; font-weight: 600; - src: local('Nunito SemiBold'), local('Nunito-SemiBold'), url('../../fonts/Nunito/XRXW3I6Li01BKofA6sKUYevN.ttf') format('truetype'); + src: url('../../fonts/Nunito/XRXW3I6Li01BKofA6sKUYevN.ttf') format('truetype'); } @font-face { font-family: 'Nunito'; font-style: normal; font-weight: 700; - src: local('Nunito Bold'), local('Nunito-Bold'), url('../../fonts/Nunito/XRXW3I6Li01BKofAjsOUYevN.ttf') format('truetype'); + src: url('../../fonts/Nunito/XRXW3I6Li01BKofAjsOUYevN.ttf') format('truetype'); } /* diff --git a/res/themes/light/css/_base.scss b/res/themes/light/css/_base.scss index 3aaa1dde2a..9fcb58d7f1 100644 --- a/res/themes/light/css/_base.scss +++ b/res/themes/light/css/_base.scss @@ -20,6 +20,7 @@ $focus-bg-color: #dddddd; // button UI (white-on-green in light skin) $accent-fg-color: #ffffff; $accent-color: #76CFA6; +$accent-color-alt: $accent-color; $accent-color-50pct: #76CFA67F; $selection-fg-color: $primary-bg-color; @@ -180,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 { @@ -212,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/fetchdep.sh b/scripts/fetchdep.sh index 73c622133b..95fc4b0603 100755 --- a/scripts/fetchdep.sh +++ b/scripts/fetchdep.sh @@ -1,22 +1,27 @@ #!/bin/sh -set -e - org="$1" repo="$2" +defbranch="$3" + +[ -z "$defbranch" ] && defbranch="develop" rm -r "$repo" || true -curbranch="$TRAVIS_PULL_REQUEST_BRANCH" -[ -z "$curbranch" ] && curbranch="$TRAVIS_BRANCH" -[ -z "$curbranch" ] && curbranch=`"echo $GIT_BRANCH" | sed -e 's/^origin\///'` # jenkins +clone() { + branch=$1 + if [ -n "$branch" ] + then + echo "Trying to use the branch $branch" + git clone https://github.com/$org/$repo.git $repo --branch "$branch" && exit 0 + fi +} -if [ -n "$curbranch" ] -then - echo "Determined branch to be $curbranch" - - git clone https://github.com/$org/$repo.git $repo --branch "$curbranch" && exit 0 -fi - -echo "Checking out develop branch" -git clone https://github.com/$org/$repo.git $repo --branch develop +# Try the PR author's branch in case it exists on the deps as well. +clone $TRAVIS_PULL_REQUEST_BRANCH +# Try the target branch of the push or PR. +clone $TRAVIS_BRANCH +# Try the current branch from Jenkins. +clone `"echo $GIT_BRANCH" | sed -e 's/^origin\///'` +# Use the default branch as the last resort. +clone $defbranch diff --git a/scripts/gen-i18n.js b/scripts/gen-i18n.js index a1a2e6f7c5..3d3d5af116 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 abc9aa0bed..79f0d69e2c 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 fd21977108..f2bbdfafe5 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 b0912c759e..ed057eb020 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 ec55a1e8c7..ca045e36cd 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,45 @@ 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); + + const wellknown = data.well_known; + if (wellknown) { + if (wellknown["m.homeserver"] && wellknown["m.homeserver"]["base_url"]) { + hsUrl = wellknown["m.homeserver"]["base_url"]; + console.log(`Overrode homeserver setting with ${hsUrl} from login response`); + } + if (wellknown["m.identity_server"] && wellknown["m.identity_server"]["base_url"]) { + // TODO: should we prompt here? + isUrl = wellknown["m.identity_server"]["base_url"]; + console.log(`Overrode IS setting with ${isUrl} from login response`); + } + } + + return { + homeserverUrl: hsUrl, + identityServerUrl: isUrl, + userId: data.user_id, + deviceId: data.device_id, + accessToken: data.access_token, + }; +} diff --git a/src/Registration.js b/src/Registration.js index f86c9cc618..98aee3ac83 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 a96d1b2f6b..3547b9195f 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); + 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 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(
), + }); } } - 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 8a34ba7ab1..47c6c26b45 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 { @@ -134,6 +135,18 @@ export const CommandMap = { }, }), + roomname: new Command({ + name: 'roomname', + args: '', + description: _td('Sets the room name'), + runFn: function(roomId, args) { + if (args) { + return success(MatrixClientPeg.get().setRoomName(roomId, args)); + } + return reject(this.getUsage()); + }, + }), + invite: new Command({ name: 'invite', args: '', @@ -142,7 +155,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/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js index 2f43d18072..c593a9b3ea 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, }); }, @@ -164,7 +180,7 @@ export default React.createClass({ }); }, - _onKeepItSafeGotItClick: function() { + _onKeepItSafeBackClick: function() { this.setState({ 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,24 +203,55 @@ 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, "+ ".", {}, { @@ -268,16 +319,18 @@ export default React.createClass({ "somewhere safe.", )}

- {passPhraseMatch} -
- +
+
+ +
+ {passPhraseMatch}

{_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")}
-
- - { - // FIXME REDESIGN: buttons should be adjacent but insufficient room in current design - } -

- +
+ {_t("Your Recovery Key")}
-
- {this._keyBackupInfo.recovery_key} +
+
+ {this._keyBackupInfo.recovery_key} +
+
+ + +

-
-
; }, @@ -341,10 +394,11 @@ export default React.createClass({
  • {_t("Save it on a USB key or backup drive", {}, {b: s => {s}})}
  • {_t("Copy it to your personal cloud storage", {}, {b: s => {s}})}
  • - + + +
    ; }, 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 0000000000..a9df3cca6e --- /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 0000000000..e88e0444bc --- /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/AutoHideScrollbar.js b/src/components/structures/AutoHideScrollbar.js index a328d478bc..47ae24ba0f 100644 --- a/src/components/structures/AutoHideScrollbar.js +++ b/src/components/structures/AutoHideScrollbar.js @@ -69,6 +69,7 @@ export default class AutoHideScrollbar extends React.Component { this.onOverflow = this.onOverflow.bind(this); this.onUnderflow = this.onUnderflow.bind(this); this._collectContainerRef = this._collectContainerRef.bind(this); + this._needsOverflowListener = null; } onOverflow() { @@ -81,21 +82,35 @@ export default class AutoHideScrollbar extends React.Component { this.containerRef.classList.add("mx_AutoHideScrollbar_underflow"); } + checkOverflow() { + if (!this._needsOverflowListener) { + return; + } + if (this.containerRef.scrollHeight > this.containerRef.clientHeight) { + this.onOverflow(); + } else { + this.onUnderflow(); + } + } + + componentDidUpdate() { + this.checkOverflow(); + } + + componentDidMount() { + installBodyClassesIfNeeded(); + this._needsOverflowListener = + document.body.classList.contains("mx_scrollbar_nooverlay"); + if (this._needsOverflowListener) { + this.containerRef.addEventListener("overflow", this.onOverflow); + this.containerRef.addEventListener("underflow", this.onUnderflow); + } + this.checkOverflow(); + } + _collectContainerRef(ref) { if (ref && !this.containerRef) { this.containerRef = ref; - const needsOverflowListener = - document.body.classList.contains("mx_scrollbar_nooverlay"); - - if (needsOverflowListener) { - this.containerRef.addEventListener("overflow", this.onOverflow); - this.containerRef.addEventListener("underflow", this.onUnderflow); - } - if (ref.scrollHeight > ref.clientHeight) { - this.onOverflow(); - } else { - this.onUnderflow(); - } } if (this.props.wrappedRef) { this.props.wrappedRef(ref); @@ -103,14 +118,13 @@ export default class AutoHideScrollbar extends React.Component { } componentWillUnmount() { - if (this.containerRef) { + if (this._needsOverflowListener && this.containerRef) { this.containerRef.removeEventListener("overflow", this.onOverflow); this.containerRef.removeEventListener("underflow", this.onUnderflow); } } render() { - installBodyClassesIfNeeded(); return (
    { + 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; @@ -1272,15 +1274,6 @@ export default React.createClass({ , ); - if (this.props.collapsedRhs) { - rightButtons.push( - - - , - ); - } } const rightPanel = !this.props.collapsedRhs ? : undefined; @@ -1311,7 +1304,7 @@ export default React.createClass({
    { rightButtons }
    - +

    diff --git a/src/components/structures/HomePage.js b/src/components/structures/HomePage.js index 01aabf6115..8f0c270513 100644 --- a/src/components/structures/HomePage.js +++ b/src/components/structures/HomePage.js @@ -91,11 +91,15 @@ class HomePage extends React.Component { this._unmounted = true; } - onLoginClick() { + onLoginClick(ev) { + ev.preventDefault(); + ev.stopPropagation(); dis.dispatch({ action: 'start_login' }); } - onRegisterClick() { + onRegisterClick(ev) { + ev.preventDefault(); + ev.stopPropagation(); dis.dispatch({ action: 'start_registration' }); } diff --git a/src/components/structures/IndicatorScrollbar.js b/src/components/structures/IndicatorScrollbar.js index 247131cfab..0fe246050c 100644 --- a/src/components/structures/IndicatorScrollbar.js +++ b/src/components/structures/IndicatorScrollbar.js @@ -21,41 +21,52 @@ export default class IndicatorScrollbar extends React.Component { constructor(props) { super(props); this._collectScroller = this._collectScroller.bind(this); + this._collectScrollerComponent = this._collectScrollerComponent.bind(this); this.checkOverflow = this.checkOverflow.bind(this); + this._scrollElement = null; + this._autoHideScrollbar = null; } _collectScroller(scroller) { - if (scroller && !this._scroller) { - this._scroller = scroller; - this._scroller.addEventListener("scroll", this.checkOverflow); + if (scroller && !this._scrollElement) { + this._scrollElement = scroller; + this._scrollElement.addEventListener("scroll", this.checkOverflow); this.checkOverflow(); } } + _collectScrollerComponent(autoHideScrollbar) { + this._autoHideScrollbar = autoHideScrollbar; + } + checkOverflow() { - const hasTopOverflow = this._scroller.scrollTop > 0; - const hasBottomOverflow = this._scroller.scrollHeight > - (this._scroller.scrollTop + this._scroller.clientHeight); + const hasTopOverflow = this._scrollElement.scrollTop > 0; + const hasBottomOverflow = this._scrollElement.scrollHeight > + (this._scrollElement.scrollTop + this._scrollElement.clientHeight); if (hasTopOverflow) { - this._scroller.classList.add("mx_IndicatorScrollbar_topOverflow"); + this._scrollElement.classList.add("mx_IndicatorScrollbar_topOverflow"); } else { - this._scroller.classList.remove("mx_IndicatorScrollbar_topOverflow"); + this._scrollElement.classList.remove("mx_IndicatorScrollbar_topOverflow"); } if (hasBottomOverflow) { - this._scroller.classList.add("mx_IndicatorScrollbar_bottomOverflow"); + this._scrollElement.classList.add("mx_IndicatorScrollbar_bottomOverflow"); } else { - this._scroller.classList.remove("mx_IndicatorScrollbar_bottomOverflow"); + this._scrollElement.classList.remove("mx_IndicatorScrollbar_bottomOverflow"); + } + + if (this._autoHideScrollbar) { + this._autoHideScrollbar.checkOverflow(); } } componentWillUnmount() { - if (this._scroller) { - this._scroller.removeEventListener("scroll", this.checkOverflow); + if (this._scrollElement) { + this._scrollElement.removeEventListener("scroll", this.checkOverflow); } } render() { - return ( + return ( { this.props.children } ); } diff --git a/src/components/structures/LeftPanel.js b/src/components/structures/LeftPanel.js index 77f6f1f948..ba0e97366e 100644 --- a/src/components/structures/LeftPanel.js +++ b/src/components/structures/LeftPanel.js @@ -151,7 +151,7 @@ const LeftPanel = React.createClass({ } } while (element && !( classes.contains("mx_RoomTile") || - classes.contains("mx_SearchBox_search"))); + classes.contains("mx_textinput_search"))); if (element) { element.focus(); diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index 56f3fc89f4..74274a5be2 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -63,7 +63,7 @@ const LoggedInView = React.createClass({ // Called with the credentials of a registered user (if they were a ROU that // transitioned to PWLU) onRegistered: PropTypes.func, - + collapsedRhs: PropTypes.bool, teamToken: PropTypes.string, // Used by the RoomView to handle joining rooms @@ -447,7 +447,7 @@ const LoggedInView = React.createClass({ eventPixelOffset={this.props.initialEventPixelOffset} key={this.props.currentRoomId || 'roomview'} disabled={this.props.middleDisabled} - collapsedRhs={this.props.collapseRhs} + collapsedRhs={this.props.collapsedRhs} ConferenceHandler={this.props.ConferenceHandler} />; break; @@ -499,7 +499,7 @@ const LoggedInView = React.createClass({ page_element = ; break; } diff --git a/src/components/structures/MainSplit.js b/src/components/structures/MainSplit.js index 5c69ef6745..0427130eea 100644 --- a/src/components/structures/MainSplit.js +++ b/src/components/structures/MainSplit.js @@ -41,10 +41,13 @@ export default class MainSplit extends React.Component { {onResized: this._onResized}, ); resizer.setClassNames(classNames); - const rhsSize = window.localStorage.getItem("mx_rhs_size"); + let rhsSize = window.localStorage.getItem("mx_rhs_size"); if (rhsSize !== null) { - resizer.forHandleAt(0).resize(parseInt(rhsSize, 10)); + rhsSize = parseInt(rhsSize, 10); + } else { + rhsSize = 350; } + resizer.forHandleAt(0).resize(rhsSize); resizer.attach(); this.resizer = resizer; diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index b67dc7b352..4983e86c49 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}); @@ -161,7 +163,7 @@ export default React.createClass({ viewUserId: null, collapseLhs: false, - collapseRhs: false, + collapsedRhs: window.localStorage.getItem("mx_rhs_collapsed") === "true", leftDisabled: false, middleDisabled: false, rightDisabled: 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) { @@ -555,7 +579,7 @@ export default React.createClass({ break; case 'view_user': // FIXME: ugly hack to expand the RightPanel and then re-dispatch. - if (this.state.collapseRhs) { + if (this.state.collapsedRhs) { setTimeout(()=>{ dis.dispatch({ action: 'show_right_panel', @@ -659,13 +683,15 @@ export default React.createClass({ }); break; case 'hide_right_panel': + window.localStorage.setItem("mx_rhs_collapsed", true); this.setState({ - collapseRhs: true, + collapsedRhs: true, }); break; case 'show_right_panel': + window.localStorage.setItem("mx_rhs_collapsed", false); this.setState({ - collapseRhs: false, + collapsedRhs: false, }); break; case 'panel_disable': { @@ -676,9 +702,11 @@ export default React.createClass({ }); break; } - case 'set_theme': - this._onSetTheme(payload.value); - break; + // case 'set_theme': + // disable changing the theme for now + // as other themes are not compatible with dharma + // this._onSetTheme(payload.value); + // break; case 'on_logging_in': // We are now logging in, so set the state to reflect that // NB. This does not touch 'ready' since if our dispatches @@ -911,6 +939,10 @@ export default React.createClass({ }, _viewHome: function() { + // The home page requires the "logged in" view, so we'll set that. + this.setStateForNewView({ + view: VIEWS.LOGGED_IN, + }); this._setPage(PageTypes.HomePage); this.notifyNewScreen('home'); }, @@ -1167,10 +1199,7 @@ export default React.createClass({ * @param {string} teamToken */ _onLoggedIn: async function(teamToken) { - this.setState({ - view: VIEWS.LOGGED_IN, - }); - + this.setStateForNewView({view: VIEWS.LOGGED_IN}); if (teamToken) { // A team member has logged in, not a guest this._teamToken = teamToken; @@ -1227,7 +1256,7 @@ export default React.createClass({ view: VIEWS.LOGIN, ready: false, collapseLhs: false, - collapseRhs: false, + collapsedRhs: false, currentRoomId: null, page_type: PageTypes.RoomDirectory, }); @@ -1418,6 +1447,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 @@ -1744,6 +1778,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; @@ -1758,7 +1822,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 (
    @@ -1832,6 +1896,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} @@ -1854,6 +1920,8 @@ export default React.createClass({ const ForgotPassword = sdk.getComponent('structures.login.ForgotPassword'); return ( { + this.props.onHeaderClick(isHidden); + }); } else { // The header is stuck, so the click is to be interpreted as a scroll to the header this.props.onHeaderClick(this.state.hidden, this.refs.header.dataset.originalPosition); @@ -268,17 +269,10 @@ const RoomSubList = React.createClass({ let incomingCall; if (this.props.incomingCall) { - const self = this; - // Check if the incoming call is for this section - const incomingCallRoom = this.props.list.filter(function(room) { - return self.props.incomingCall.roomId === room.roomId; - }); - - if (incomingCallRoom.length === 1) { - const IncomingCallBox = sdk.getComponent("voip.IncomingCallBox"); - incomingCall = - ; - } + // 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; @@ -313,6 +307,12 @@ const RoomSubList = React.createClass({ ); }, + checkOverflow: function() { + if (this.refs.scroller) { + this.refs.scroller.checkOverflow(); + } + }, + render: function() { const len = this.props.list.length + this.props.extraTiles.length; if (len) { @@ -330,7 +330,7 @@ const RoomSubList = React.createClass({ tiles.push(...this.props.extraTiles); return
    {this._getHeaderJsx()} - + { tiles }
    ; diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 681b1221e1..59bb4befbe 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,8 @@ module.exports = React.createClass({ MatrixClientPeg.get().on("RoomState.members", this.onRoomStateMember); MatrixClientPeg.get().on("Room.myMembership", this.onMyMembership); MatrixClientPeg.get().on("accountData", this.onAccountData); - + MatrixClientPeg.get().on("crypto.keyBackupStatus", this.onKeyBackupStatus); + this._fetchMediaConfig(); // Start listening for RoomViewStore updates this._roomStoreToken = this.props.roomViewStore.addListener(this._onRoomViewStoreUpdate); this._onRoomViewStoreUpdate(true); @@ -166,6 +172,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; @@ -424,6 +451,7 @@ module.exports = React.createClass({ MatrixClientPeg.get().removeListener("Room.myMembership", this.onMyMembership); MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember); MatrixClientPeg.get().removeListener("accountData", this.onAccountData); + MatrixClientPeg.get().removeListener("crypto.keyBackupStatus", this.onKeyBackupStatus); } window.removeEventListener('beforeunload', this.onPageUnload); @@ -500,6 +528,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(); @@ -578,6 +610,25 @@ 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, + ); + } + }, + + onKeyBackupStatus() { + // Key backup status changes affect whether the in-room recovery + // reminder is displayed. + this.forceUpdate(); + }, + canResetTimeline: function() { if (!this.refs.messagePanel) { return true; @@ -932,6 +983,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) { this.props.roomViewStore.getDispatcher().dispatch({action: 'focus_composer'}); @@ -1483,6 +1543,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) { @@ -1622,6 +1683,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) { @@ -1636,6 +1704,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 = ; @@ -1696,6 +1767,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/SearchBox.js b/src/components/structures/SearchBox.js index 4df3e837c7..ea1fa312c1 100644 --- a/src/components/structures/SearchBox.js +++ b/src/components/structures/SearchBox.js @@ -56,7 +56,6 @@ module.exports = React.createClass({ case 'focus_room_filter': if (this.refs.search) { this.refs.search.focus(); - this.refs.search.select(); } break; } @@ -83,6 +82,10 @@ module.exports = React.createClass({ } }, + _onFocus: function(ev) { + ev.target.select(); + }, + _clearSearch: function(source) { this.refs.search.value = ""; this.onChange(); @@ -108,6 +111,7 @@ module.exports = React.createClass({ ref="search" className="mx_textinput_icon mx_textinput_search" value={ this.state.searchTerm } + onFocus={ this._onFocus } onChange={ this.onChange } onKeyDown={ this._onKeyDown } placeholder={ _t('Filter room names') } diff --git a/src/components/structures/TagPanel.js b/src/components/structures/TagPanel.js index f23ac698ba..7e77a64e62 100644 --- a/src/components/structures/TagPanel.js +++ b/src/components/structures/TagPanel.js @@ -23,6 +23,7 @@ import GroupActions from '../../actions/GroupActions'; import sdk from '../../index'; import dis from '../../dispatcher'; +import Modal from '../../Modal'; import { _t } from '../../languageHandler'; import { Droppable } from 'react-beautiful-dnd'; @@ -47,6 +48,8 @@ const TagPanel = React.createClass({ this.context.matrixClient.on("Group.myMembership", this._onGroupMyMembership); this.context.matrixClient.on("sync", this._onClientSync); + this._dispatcherRef = dis.register(this._onAction); + this._tagOrderStoreToken = TagOrderStore.addListener(() => { if (this.unmounted) { return; @@ -67,6 +70,9 @@ const TagPanel = React.createClass({ if (this._filterStoreToken) { this._filterStoreToken.remove(); } + if (this._dispatcherRef) { + dis.unregister(this._dispatcherRef); + } }, _onGroupMyMembership() { @@ -100,13 +106,21 @@ const TagPanel = React.createClass({ dis.dispatch({action: 'deselect_tags'}); }, + _onAction(payload) { + if (payload.action === "show_redesign_feedback_dialog") { + const RedesignFeedbackDialog = + sdk.getComponent("views.dialogs.RedesignFeedbackDialog"); + Modal.createDialog(RedesignFeedbackDialog); + } + }, + render() { const GroupsButton = sdk.getComponent('elements.GroupsButton'); const DNDTagTile = sdk.getComponent('elements.DNDTagTile'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const TintableSvg = sdk.getComponent('elements.TintableSvg'); const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper"); - + const ActionButton = sdk.getComponent("elements.ActionButton"); const tags = this.state.orderedTags.map((tag, index) => { return
    - + +
    ; }, diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 3a3d6e1e91..bb31510cf6 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 444f391258..5c0e428339 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) { @@ -146,6 +162,18 @@ module.exports = React.createClass({ this.setState(newState); }, + onLoginClick: function(ev) { + ev.preventDefault(); + ev.stopPropagation(); + this.props.onLoginClick(); + }, + + onRegisterClick: function(ev) { + ev.preventDefault(); + ev.stopPropagation(); + this.props.onRegisterClick(); + }, + showErrorDialog: function(body, title) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createTrackedDialog('Forgot Password Error', '', ErrorDialog, { @@ -200,6 +228,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,10 +264,11 @@ module.exports = React.createClass({ { serverConfigSection } - + { errorText } + { _t('Return to login screen') } - + { _t('Create an account') } diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index 92cddb0dc1..321084389b 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, @@ -189,7 +214,10 @@ module.exports = React.createClass({ }).done(); }, - _onLoginAsGuestClick: function() { + _onLoginAsGuestClick: function(ev) { + ev.preventDefault(); + ev.stopPropagation(); + const self = this; self.setState({ busy: true, @@ -221,6 +249,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 +300,65 @@ module.exports = React.createClass({ }); }, + onRegisterClick: function(ev) { + ev.preventDefault(); + ev.stopPropagation(); + this.props.onRegisterClick(); + }, + + _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 +496,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 +522,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 +538,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 }
    ); } @@ -468,7 +576,7 @@ module.exports = React.createClass({ { errorTextSection } { this.componentForStep(this.state.currentFlow) } { serverConfig } - + { _t('Create an account') } { loginAsGuestJsx } diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index 30afaf4f64..fa5a02e881 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.'); @@ -349,6 +363,12 @@ module.exports = React.createClass({ } }, + onLoginClick: function(ev) { + ev.preventDefault(); + ev.stopPropagation(); + this.props.onLoginClick(); + }, + _makeRegisterRequest: function(auth) { // Only send the bind params if we're sending username / pw params // (Since we need to send no params at all to use the ones saved in the @@ -441,19 +461,20 @@ 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 }
    ; } } let signIn; if (!this.state.doingUIAuth) { signIn = ( - + { theme === 'status' ? _t('Sign in') : _t('I already have an account') } ); diff --git a/src/components/views/avatars/MemberStatusMessageAvatar.js b/src/components/views/avatars/MemberStatusMessageAvatar.js new file mode 100644 index 0000000000..aebd1741b7 --- /dev/null +++ b/src/components/views/avatars/MemberStatusMessageAvatar.js @@ -0,0 +1,120 @@ +/* +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 MatrixClientPeg from '../../../MatrixClientPeg'; +import AccessibleButton from '../elements/AccessibleButton'; +import MemberAvatar from '../avatars/MemberAvatar'; +import classNames from 'classnames'; +import * as ContextualMenu from "../../structures/ContextualMenu"; +import StatusMessageContextMenu from "../context_menus/StatusMessageContextMenu"; +import SettingsStore from "../../../settings/SettingsStore"; + +export default class MemberStatusMessageAvatar extends React.Component { + static propTypes = { + member: PropTypes.object.isRequired, + width: PropTypes.number, + height: PropTypes.number, + resizeMethod: PropTypes.string, + }; + + static defaultProps = { + width: 40, + height: 40, + resizeMethod: 'crop', + }; + + constructor(props, context) { + super(props, context); + } + + componentWillMount() { + if (this.props.member.userId !== MatrixClientPeg.get().getUserId()) { + throw new Error("Cannot use MemberStatusMessageAvatar on anyone but the logged in user"); + } + } + + componentDidMount() { + MatrixClientPeg.get().on("RoomState.events", this._onRoomStateEvents); + + if (this.props.member.user) { + this.setState({message: this.props.member.user._unstable_statusMessage}); + } else { + this.setState({message: ""}); + } + } + + componentWillUnmount() { + if (MatrixClientPeg.get()) { + MatrixClientPeg.get().removeListener("RoomState.events", this._onRoomStateEvents); + } + } + + _onRoomStateEvents = (ev, state) => { + if (ev.getStateKey() !== MatrixClientPeg.get().getUserId()) return; + if (ev.getType() !== "im.vector.user_status") return; + // TODO: We should be relying on `this.props.member.user._unstable_statusMessage` + // We don't currently because the js-sdk doesn't emit a specific event for this + // change, and we don't want to race it. This should be improved when we rip out + // the im.vector.user_status stuff and replace it with a complete solution. + this.setState({message: ev.getContent()["status"]}); + }; + + _onClick = (e) => { + e.stopPropagation(); + + const elementRect = e.target.getBoundingClientRect(); + + // The window X and Y offsets are to adjust position when zoomed in to page + const x = (elementRect.left + window.pageXOffset) - (elementRect.width / 2) + 3; + const chevronOffset = 12; + let y = elementRect.top + (elementRect.height / 2) + window.pageYOffset; + y = y - (chevronOffset + 4); // where 4 is 1/4 the height of the chevron + + ContextualMenu.createMenu(StatusMessageContextMenu, { + chevronOffset: chevronOffset, + chevronFace: 'bottom', + left: x, + top: y, + menuWidth: 190, + user: this.props.member.user, + }); + }; + + render() { + if (!SettingsStore.isFeatureEnabled("feature_custom_status")) { + return ; + } + + const hasStatus = this.props.member.user ? !!this.props.member.user._unstable_statusMessage : false; + + const classes = classNames({ + "mx_MemberStatusMessageAvatar": true, + "mx_MemberStatusMessageAvatar_hasStatus": hasStatus, + }); + + return + + ; + } +} diff --git a/src/components/views/context_menus/StatusMessageContextMenu.js b/src/components/views/context_menus/StatusMessageContextMenu.js new file mode 100644 index 0000000000..f07220db44 --- /dev/null +++ b/src/components/views/context_menus/StatusMessageContextMenu.js @@ -0,0 +1,86 @@ +/* +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 { _t } from '../../../languageHandler'; +import MatrixClientPeg from '../../../MatrixClientPeg'; +import AccessibleButton from '../elements/AccessibleButton'; +import classNames from 'classnames'; + +export default class StatusMessageContextMenu extends React.Component { + static propTypes = { + // js-sdk User object. Not required because it might not exist. + user: PropTypes.object, + }; + + constructor(props, context) { + super(props, context); + + this.state = { + message: props.user ? props.user._unstable_statusMessage : "", + }; + } + + _onClearClick = async(e) => { + await MatrixClientPeg.get()._unstable_setStatusMessage(""); + this.setState({message: ""}); + }; + + _onSubmit = (e) => { + e.preventDefault(); + MatrixClientPeg.get()._unstable_setStatusMessage(this.state.message); + }; + + _onStatusChange = (e) => { + this.setState({message: e.target.value}); + }; + + render() { + const formSubmitClasses = classNames({ + "mx_StatusMessageContextMenu_submit": true, + "mx_StatusMessageContextMenu_submitFaded": !this.state.message, // no message == faded + }); + + const form =
    + + + + +
    ; + + const clearIcon = this.state.message ? "img/cancel-red.svg" : "img/cancel.svg"; + const clearButton = + {_t('Clear + {_t("Clear status")} + ; + + const menuClasses = classNames({ + "mx_StatusMessageContextMenu": true, + "mx_StatusMessageContextMenu_hasStatus": this.state.message, + }); + + return
    + { form } +
    + { clearButton } +
    ; + } +} diff --git a/src/components/views/dialogs/AddressPickerDialog.js b/src/components/views/dialogs/AddressPickerDialog.js index abc52f7b1d..cbe80763a6 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 8ec417a59b..3e9052cc34 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/ChangelogDialog.js b/src/components/views/dialogs/ChangelogDialog.js index b93678b2ab..3c9414fd88 100644 --- a/src/components/views/dialogs/ChangelogDialog.js +++ b/src/components/views/dialogs/ChangelogDialog.js @@ -36,8 +36,12 @@ export default class ChangelogDialog extends React.Component { for (let i=0; i { - if (body == null) return; + const url = `https://api.github.com/repos/${REPOS[i]}/compare/${oldVersion}...${newVersion}`; + request(url, (err, response, body) => { + if (response.statusCode < 200 || response.statusCode >= 300) { + this.setState({ [REPOS[i]]: response.statusText }); + return; + } this.setState({[REPOS[i]]: JSON.parse(body).commits}); }); } @@ -58,13 +62,20 @@ export default class ChangelogDialog extends React.Component { const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog'); const logs = REPOS.map(repo => { - if (this.state[repo] == null) return ; + let content; + if (this.state[repo] == null) { + content = ; + } else if (typeof this.state[repo] === "string") { + content = _t("Unable to load commit detail: %(msg)s", { + msg: this.state[repo], + }); + } else { + content = this.state[repo].map(this._elementsForCommit); + } return (

    {repo}

    -
      - {this.state[repo].map(this._elementsForCommit)} -
    +
      {content}
    ); }); diff --git a/src/components/views/dialogs/DeactivateAccountDialog.js b/src/components/views/dialogs/DeactivateAccountDialog.js index 761a1e4209..6e87a816bb 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/RedesignFeedbackDialog.js b/src/components/views/dialogs/RedesignFeedbackDialog.js new file mode 100644 index 0000000000..c428aca16a --- /dev/null +++ b/src/components/views/dialogs/RedesignFeedbackDialog.js @@ -0,0 +1,51 @@ +/* +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 QuestionDialog from './QuestionDialog'; +import { _t } from '../../../languageHandler'; + +export default (props) => { + const existingIssuesUrl = "https://github.com/vector-im/riot-web/issues" + + "?q=is%3Aopen+is%3Aissue+label%3Aredesign+sort%3Areactions-%2B1-desc"; + const newIssueUrl = "https://github.com/vector-im/riot-web/issues/new" + + "?assignees=&labels=redesign&template=redesign_issue.md&title="; + + const description1 = + _t("Thanks for testing the Riot Redesign. " + + "If you run into any bugs or visual issues, " + + "please let us know on GitHub."); + const description2 = _t("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.", {}, + { + existingIssuesLink: (sub) => { + return { sub }; + }, + newIssueLink: (sub) => { + return { sub }; + }, + }); + + return (

    {description1}

    {description2}

    } + button={_t("Go back")} + onFinished={props.onFinished} + />); +}; diff --git a/src/components/views/dialogs/SetMxIdDialog.js b/src/components/views/dialogs/SetMxIdDialog.js index fb892c4a0a..222a2c35fe 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/ActionButton.js b/src/components/views/elements/ActionButton.js index a9d95e4a52..1ca5ab9983 100644 --- a/src/components/views/elements/ActionButton.js +++ b/src/components/views/elements/ActionButton.js @@ -31,6 +31,7 @@ export default React.createClass({ mouseOverAction: PropTypes.string, label: PropTypes.string.isRequired, iconPath: PropTypes.string, + className: PropTypes.string, }, getDefaultProps: function() { @@ -76,8 +77,13 @@ export default React.createClass({ () : undefined; + const classNames = ["mx_RoleButton"]; + if (this.props.className) { + classNames.push(this.props.className); + } + return ( - ); }; GroupsButton.propTypes = { size: PropTypes.string, - tooltip: PropTypes.bool, }; export default GroupsButton; diff --git a/src/components/views/elements/ManageIntegsButton.js b/src/components/views/elements/ManageIntegsButton.js index 024c5feda5..f45053de44 100644 --- a/src/components/views/elements/ManageIntegsButton.js +++ b/src/components/views/elements/ManageIntegsButton.js @@ -91,7 +91,7 @@ export default class ManageIntegsButton extends React.Component { integrationsButton = ( - + { integrationsWarningTriangle } { integrationsErrorPopup } diff --git a/src/components/views/elements/ResizeHandle.js b/src/components/views/elements/ResizeHandle.js index b5487b1fc1..578689b45c 100644 --- a/src/components/views/elements/ResizeHandle.js +++ b/src/components/views/elements/ResizeHandle.js @@ -14,13 +14,14 @@ const ResizeHandle = (props) => { classNames.push('mx_ResizeHandle_reverse'); } return ( -

    +
    ); }; ResizeHandle.propTypes = { vertical: PropTypes.bool, reverse: PropTypes.bool, + id: PropTypes.string, }; export default ResizeHandle; diff --git a/src/components/views/groups/GroupMemberList.js b/src/components/views/groups/GroupMemberList.js index 46653f1599..9a8196f12b 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 a0e5ab0ddb..59d4db379c 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 fe977025ae..137aeada91 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 a6944ec20a..2f04011273 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/messages/SenderProfile.js b/src/components/views/messages/SenderProfile.js index a40addf0d1..a8c52e06b1 100644 --- a/src/components/views/messages/SenderProfile.js +++ b/src/components/views/messages/SenderProfile.js @@ -85,8 +85,8 @@ export default React.createClass({ _getDisplayedGroups(userGroups, relatedGroups) { let displayedGroups = userGroups || []; if (relatedGroups && relatedGroups.length > 0) { - displayedGroups = displayedGroups.filter((groupId) => { - return relatedGroups.includes(groupId); + displayedGroups = relatedGroups.filter((groupId) => { + return displayedGroups.includes(groupId); }); } else { displayedGroups = []; diff --git a/src/components/views/right_panel/GroupHeaderButtons.js b/src/components/views/right_panel/GroupHeaderButtons.js index af54787b2c..6fcba1d815 100644 --- a/src/components/views/right_panel/GroupHeaderButtons.js +++ b/src/components/views/right_panel/GroupHeaderButtons.js @@ -55,23 +55,23 @@ export default class GroupHeaderButtons extends HeaderButtons { } renderButtons() { - const isPhaseGroup = [ + const groupPhases = [ RightPanel.Phase.GroupMemberInfo, RightPanel.Phase.GroupMemberList, - ].includes(this.state.phase); - const isPhaseRoom = [ + ]; + const roomPhases = [ RightPanel.Phase.GroupRoomList, RightPanel.Phase.GroupRoomInfo, - ].includes(this.state.phase); + ]; return [ , , diff --git a/src/components/views/right_panel/HeaderButton.js b/src/components/views/right_panel/HeaderButton.js index a01d3444f1..bb9f613607 100644 --- a/src/components/views/right_panel/HeaderButton.js +++ b/src/components/views/right_panel/HeaderButton.js @@ -36,6 +36,7 @@ export default class HeaderButton extends React.Component { dis.dispatch({ action: 'view_right_panel_phase', phase: this.props.clickPhase, + fromHeader: true, }); } diff --git a/src/components/views/right_panel/HeaderButtons.js b/src/components/views/right_panel/HeaderButtons.js index 3c59c52089..f0479eb8be 100644 --- a/src/components/views/right_panel/HeaderButtons.js +++ b/src/components/views/right_panel/HeaderButtons.js @@ -18,6 +18,7 @@ limitations under the License. */ import React from 'react'; +import PropTypes from 'prop-types'; import dis from '../../../dispatcher'; export default class HeaderButtons extends React.Component { @@ -25,7 +26,7 @@ export default class HeaderButtons extends React.Component { super(props); this.state = { - phase: initialPhase, + phase: props.collapsedRhs ? null : initialPhase, isUserPrivilegedInGroup: null, }; this.onAction = this.onAction.bind(this); @@ -47,11 +48,42 @@ export default class HeaderButtons extends React.Component { }, extras)); } + isPhase(phases) { + if (this.props.collapsedRhs) { + return false; + } + if (Array.isArray(phases)) { + return phases.includes(this.state.phase); + } else { + return phases === this.state.phase; + } + } + onAction(payload) { if (payload.action === "view_right_panel_phase") { - this.setState({ - phase: payload.phase, - }); + // only actions coming from header buttons should collapse the right panel + if (this.state.phase === payload.phase && payload.fromHeader) { + dis.dispatch({ + action: 'hide_right_panel', + }); + this.setState({ + phase: null, + }); + } else { + if (this.props.collapsedRhs && payload.fromHeader) { + dis.dispatch({ + action: 'show_right_panel', + }); + // emit payload again as the RightPanel didn't exist up + // till show_right_panel, just without the fromHeader flag + // as that would hide the right panel again + dis.dispatch(Object.assign({}, payload, {fromHeader: false})); + + } + this.setState({ + phase: payload.phase, + }); + } } } @@ -62,3 +94,7 @@ export default class HeaderButtons extends React.Component {
    ; } } + +HeaderButtons.propTypes = { + collapsedRhs: PropTypes.bool, +}; diff --git a/src/components/views/right_panel/RoomHeaderButtons.js b/src/components/views/right_panel/RoomHeaderButtons.js index ba06bd9953..53835777d5 100644 --- a/src/components/views/right_panel/RoomHeaderButtons.js +++ b/src/components/views/right_panel/RoomHeaderButtons.js @@ -46,24 +46,24 @@ export default class RoomHeaderButtons extends HeaderButtons { } renderButtons() { - const isMembersPhase = [ + const membersPhases = [ RightPanel.Phase.RoomMemberList, RightPanel.Phase.RoomMemberInfo, - ].includes(this.state.phase); + ]; return [ - , - , - , diff --git a/src/components/views/room_settings/AliasSettings.js b/src/components/views/room_settings/AliasSettings.js index de5d3db625..f68670b2f9 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/EntityTile.js b/src/components/views/rooms/EntityTile.js index 851352aa17..3f3bdbf47a 100644 --- a/src/components/views/rooms/EntityTile.js +++ b/src/components/views/rooms/EntityTile.js @@ -70,6 +70,7 @@ const EntityTile = React.createClass({ onClick: PropTypes.func, suppressOnHover: PropTypes.bool, showPresence: PropTypes.bool, + subtextLabel: PropTypes.string, }, getDefaultProps: function() { @@ -129,6 +130,9 @@ const EntityTile = React.createClass({ presenceState={this.props.presenceState} />; nameClasses += ' mx_EntityTile_name_hover'; } + if (this.props.subtextLabel) { + presenceLabel = {this.props.subtextLabel}; + } nameEl = (
    @@ -137,6 +141,15 @@ const EntityTile = React.createClass({ {presenceLabel}
    ); + } else if (this.props.subtextLabel) { + nameEl = ( +
    + + {name} + + {this.props.subtextLabel} +
    + ); } else { nameEl = ( { name } diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 7843beb005..226adb910f 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -40,6 +40,8 @@ import { findReadReceiptFromUserId } from '../../../utils/Receipt'; import withMatrixClient from '../../../wrappers/withMatrixClient'; import AccessibleButton from '../elements/AccessibleButton'; import SdkConfig from '../../../SdkConfig'; +import MultiInviter from "../../../utils/MultiInviter"; +import SettingsStore from "../../../settings/SettingsStore"; module.exports = withMatrixClient(React.createClass({ displayName: 'MemberInfo', @@ -714,12 +716,18 @@ module.exports = withMatrixClient(React.createClass({ const roomId = member && member.roomId ? member.roomId : this.props.roomId; 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")), }); } }; @@ -882,11 +890,16 @@ module.exports = withMatrixClient(React.createClass({ let presenceState; let presenceLastActiveAgo; let presenceCurrentlyActive; + let statusMessage; if (this.props.member.user) { presenceState = this.props.member.user.presence; presenceLastActiveAgo = this.props.member.user.lastActiveAgo; presenceCurrentlyActive = this.props.member.user.currentlyActive; + + if (SettingsStore.isFeatureEnabled("feature_custom_status")) { + statusMessage = this.props.member.user._unstable_statusMessage; + } } const room = this.props.matrixClient.getRoom(this.props.member.roomId); @@ -908,6 +921,11 @@ module.exports = withMatrixClient(React.createClass({ presenceState={presenceState} />; } + let statusLabel = null; + if (statusMessage) { + statusLabel = { statusMessage }; + } + let roomMemberDetails = null; if (this.props.member.roomId) { // is in room const PowerSelector = sdk.getComponent('elements.PowerSelector'); @@ -924,6 +942,7 @@ module.exports = withMatrixClient(React.createClass({
    {presenceLabel} + {statusLabel}
    ; } diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js index 92c486825c..0924a5ec38 100644 --- a/src/components/views/rooms/MemberList.js +++ b/src/components/views/rooms/MemberList.js @@ -68,7 +68,9 @@ module.exports = React.createClass({ // We listen for changes to the lastPresenceTs which is essentially // listening for all presence events (we display most of not all of // the information contained in presence events). - cli.on("User.lastPresenceTs", this.onUserLastPresenceTs); + cli.on("User.lastPresenceTs", this.onUserPresenceChange); + cli.on("User.presence", this.onUserPresenceChange); + cli.on("User.currentlyActive", this.onUserPresenceChange); // cli.on("Room.timeline", this.onRoomTimeline); }, @@ -81,7 +83,9 @@ module.exports = React.createClass({ cli.removeListener("Room.myMembership", this.onMyMembership); cli.removeListener("RoomState.events", this.onRoomStateEvent); cli.removeListener("Room", this.onRoom); - cli.removeListener("User.lastPresenceTs", this.onUserLastPresenceTs); + cli.removeListener("User.lastPresenceTs", this.onUserPresenceChange); + cli.removeListener("User.presence", this.onUserPresenceChange); + cli.removeListener("User.currentlyActive", this.onUserPresenceChange); } // cancel any pending calls to the rate_limited_funcs @@ -132,12 +136,12 @@ module.exports = React.createClass({ }; }, - onUserLastPresenceTs(event, user) { + onUserPresenceChange(event, user) { // Attach a SINGLE listener for global presence changes then locate the // member tile and re-render it. This is more efficient than every tile - // evar attaching their own listener. - // console.log("explicit presence from " + user.userId); + // ever attaching their own listener. const tile = this.refs[user.userId]; + // console.log(`Got presence update for ${user.userId}. hasTile=${!!tile}`); if (tile) { this._updateList(); // reorder the membership list } @@ -181,6 +185,10 @@ module.exports = React.createClass({ }, _updateList: new rate_limited_func(function() { + this._updateListNow(); + }, 500), + + _updateListNow: function() { // console.log("Updating memberlist"); const newState = { loading: false, @@ -189,7 +197,7 @@ module.exports = React.createClass({ newState.filteredJoinedMembers = this._filterMembers(newState.members, 'join', this.state.searchQuery); newState.filteredInvitedMembers = this._filterMembers(newState.members, 'invite', this.state.searchQuery); this.setState(newState); - }, 500), + }, getMembersWithUser: function() { if (!this.props.roomId) return []; @@ -267,7 +275,8 @@ module.exports = React.createClass({ if (!member) { return "(null)"; } else { - return "(" + member.name + ", " + member.powerLevel + ", " + member.user.lastActiveAgo + ", " + member.user.currentlyActive + ")"; + const u = member.user; + return "(" + member.name + ", " + member.powerLevel + ", " + (u ? u.lastActiveAgo : "") + ", " + (u ? u.getLastActiveTs() : "") + ", " + (u ? u.currentlyActive : "") + ", " + (u ? u.presence : "") + ")"; } }, @@ -275,48 +284,59 @@ module.exports = React.createClass({ // returns 0 if a and b are equivalent in ordering // returns positive if a comes after b. memberSort: function(memberA, memberB) { - // order by last active, with "active now" first. - // ...and then by power - // ...and then alphabetically. - // We could tiebreak instead by "last recently spoken in this room" if we wanted to. + // order by presence, with "active now" first. + // ...and then by power level + // ...and then by last active + // ...and then alphabetically. + // We could tiebreak instead by "last recently spoken in this room" if we wanted to. - const userA = memberA.user; - const userB = memberB.user; + // console.log(`Comparing userA=${this.memberString(memberA)} userB=${this.memberString(memberB)}`); - // if (!userA || !userB) { - // console.log("comparing " + memberA.name + " user=" + memberA.user + " with " + memberB.name + " user=" + memberB.user); - // } + const userA = memberA.user; + const userB = memberB.user; - if (!userA && !userB) return 0; - if (userA && !userB) return -1; - if (!userA && userB) return 1; + // if (!userA) console.log("!! MISSING USER FOR A-SIDE: " + memberA.name + " !!"); + // if (!userB) console.log("!! MISSING USER FOR B-SIDE: " + memberB.name + " !!"); - // console.log("comparing " + this.memberString(memberA) + " and " + this.memberString(memberB)); + if (!userA && !userB) return 0; + if (userA && !userB) return -1; + if (!userA && userB) return 1; - if ((userA.currentlyActive && userB.currentlyActive) || !this._showPresence) { - // console.log(memberA.name + " and " + memberB.name + " are both active"); - if (memberA.powerLevel === memberB.powerLevel) { - // console.log(memberA + " and " + memberB + " have same power level"); - if (memberA.name && memberB.name) { - // console.log("comparing names: " + memberA.name + " and " + memberB.name); - const nameA = memberA.name[0] === '@' ? memberA.name.substr(1) : memberA.name; - const nameB = memberB.name[0] === '@' ? memberB.name.substr(1) : memberB.name; - return nameA.localeCompare(nameB); - } else { - return 0; - } - } else { - // console.log("comparing power: " + memberA.powerLevel + " and " + memberB.powerLevel); - return memberB.powerLevel - memberA.powerLevel; - } + // First by presence + if (this._showPresence) { + const convertPresence = (p) => p === 'unavailable' ? 'online' : p; + const presenceIndex = p => { + const order = ['active', 'online', 'offline']; + const idx = order.indexOf(convertPresence(p)); + return idx === -1 ? order.length : idx; // unknown states at the end + }; + + const idxA = presenceIndex(userA.currentlyActive ? 'active' : userA.presence); + const idxB = presenceIndex(userB.currentlyActive ? 'active' : userB.presence); + // console.log(`userA_presenceGroup=${idxA} userB_presenceGroup=${idxB}`); + if (idxA !== idxB) { + // console.log("Comparing on presence group - returning"); + return idxA - idxB; } + } - if (userA.currentlyActive && !userB.currentlyActive) return -1; - if (!userA.currentlyActive && userB.currentlyActive) return 1; + // Second by power level + if (memberA.powerLevel !== memberB.powerLevel) { + // console.log("Comparing on power level - returning"); + return memberB.powerLevel - memberA.powerLevel; + } - // For now, let's just order things by timestamp. It's really annoying - // that a user disappears from sight just because they temporarily go offline + // Third by last active + if (this._showPresence && userA.getLastActiveTs() !== userB.getLastActiveTs()) { + // console.log("Comparing on last active timestamp - returning"); return userB.getLastActiveTs() - userA.getLastActiveTs(); + } + + // Fourth by name (alphabetical) + const nameA = memberA.name[0] === '@' ? memberA.name.substr(1) : memberA.name; + const nameB = memberB.name[0] === '@' ? memberB.name.substr(1) : memberB.name; + // console.log(`Comparing userA_name=${nameA} against userB_name=${nameB} - returning`); + return nameA.localeCompare(nameB); }, onSearchQueryChanged: function(ev) { diff --git a/src/components/views/rooms/MemberTile.js b/src/components/views/rooms/MemberTile.js index 2359bc242c..ba951792d0 100644 --- a/src/components/views/rooms/MemberTile.js +++ b/src/components/views/rooms/MemberTile.js @@ -16,6 +16,8 @@ limitations under the License. 'use strict'; +import SettingsStore from "../../../settings/SettingsStore"; + const React = require('react'); import PropTypes from 'prop-types'; @@ -85,6 +87,11 @@ module.exports = React.createClass({ const active = -1; const presenceState = member.user ? member.user.presence : null; + let statusMessage = null; + if (member.user && SettingsStore.isFeatureEnabled("feature_custom_status")) { + statusMessage = member.user._unstable_statusMessage; + } + const av = ( ); @@ -106,7 +113,9 @@ module.exports = React.createClass({ presenceLastTs={member.user ? member.user.lastPresenceTs : 0} presenceCurrentlyActive={member.user ? member.user.currentlyActive : false} avatarJsx={av} title={this.getPowerLabel()} onClick={this.onClick} - name={name} powerStatus={powerStatus} showPresence={this.props.showPresence} /> + name={name} powerStatus={powerStatus} showPresence={this.props.showPresence} + subtextLabel={statusMessage} + /> ); }, }); diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 61071b4009..e15ca047ac 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -138,7 +138,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) { @@ -146,10 +147,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(this.props.roomViewStore.getQuotingEvent()); @@ -160,23 +172,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 - +
    , ); } @@ -301,17 +337,45 @@ export default class MessageComposer extends React.Component { } else { callButton = - + ; videoCallButton = - + ; } const canSendMessages = !this.state.tombstone && this.props.room.maySendMessage(); + // TODO: Remove temporary logging for riot-web#7838 + // Note: we rip apart the power level event ourselves because we don't want to + // log too much data about it - just the bits we care about. Many of the variables + // logged here are to help figure out where in the stack the 'cannot post in room' + // warning is coming from. This means logging various numbers from the PL event to + // verify RoomState._maySendEventOfType is doing the right thing. + const room = this.props.room; + const plEvent = room.currentState.getStateEvents('m.room.power_levels', ''); + let plEventString = ""; + if (plEvent) { + const content = plEvent.getContent(); + if (!content) { + plEventString = ""; + } else { + const stringifyFalsey = (v) => v === null ? '' : (v === undefined ? '' : v); + const actualUserPl = stringifyFalsey(content.users ? content.users[room.myUserId] : ""); + const usersPl = stringifyFalsey(content.users_default); + const actualEventPl = stringifyFalsey(content.events ? content.events['m.room.message'] : ""); + const eventPl = stringifyFalsey(content.events_default); + plEventString = `actualUserPl=${actualUserPl} defaultUserPl=${usersPl} actualEventPl=${actualEventPl} defaultEventPl=${eventPl}`; + } + } + console.log( + `[riot-web#7838] renderComposer() hasTombstone=${!!this.state.tombstone} maySendMessage=${room.maySendMessage()}` + + ` myMembership=${room.getMyMembership()} maySendEvent=${room.currentState.maySendEvent('m.room.message', room.myUserId)}` + + ` myUserId=${room.myUserId} roomId=${room.roomId} hasPlEvent=${!!plEvent} powerLevels='${plEventString}'` + ); + if (canSendMessages) { // This also currently includes the call buttons. Really we should // check separately for whether we can call, but this is slightly @@ -319,7 +383,7 @@ export default class MessageComposer extends React.Component { const uploadButton = ( - +
    ); } else { + // TODO: Remove temporary logging for riot-web#7838 + console.log("[riot-web#7838] Falling back to showing cannot post in room error"); controls.push(
    { _t('You do not have permission to post to this room') } @@ -460,6 +526,9 @@ MessageComposer.propTypes = { // callback when a file to upload is chosen uploadFile: PropTypes.func.isRequired, + // function to test whether a file should be allowed to be uploaded. + uploadAllowed: PropTypes.func.isRequired, + // string representing the current room app drawer state showApps: PropTypes.bool, roomViewStore: PropTypes.object.isRequired, diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index abf9c7c093..940c5e3424 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -23,7 +23,6 @@ import sdk from '../../../index'; import { _t } from '../../../languageHandler'; import MatrixClientPeg from '../../../MatrixClientPeg'; import Modal from "../../../Modal"; -import dis from "../../../dispatcher"; import RateLimitedFunc from '../../../ratelimitedfunc'; import * as linkify from 'linkifyjs'; @@ -146,10 +145,6 @@ module.exports = React.createClass({ MatrixClientPeg.get().sendStateEvent(this.props.room.roomId, 'm.room.avatar', {url: null}, ''); }, - onShowRhsClick: function(ev) { - dis.dispatch({ action: 'show_right_panel' }); - }, - onShareRoomClick: function(ev) { const ShareDialog = sdk.getComponent("dialogs.ShareDialog"); Modal.createTrackedDialog('share room dialog', '', ShareDialog, { @@ -342,7 +337,7 @@ module.exports = React.createClass({ if (this.props.onSettingsClick) { settingsButton = - + ; } @@ -382,7 +377,7 @@ module.exports = React.createClass({ if (this.props.onSearchClick && this.props.inRoom) { searchButton = - + ; } @@ -390,15 +385,7 @@ module.exports = React.createClass({ if (this.props.inRoom) { shareRoomButton = - - ; - } - - let rightPanelButtons; - if (this.props.collapsedRhs) { - rightPanelButtons = - - + ; } @@ -419,7 +406,6 @@ module.exports = React.createClass({ { manageIntegsButton } { forgetButton } { searchButton } - { rightPanelButtons }
    ; } @@ -433,7 +419,7 @@ module.exports = React.createClass({ { saveButton } { cancelButton } { rightRow } - { !this.props.isGrid ? : undefined } + { !this.props.isGrid ? : undefined }
    ); diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 9e766bdc15..dbfe95dadf 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -71,6 +71,10 @@ module.exports = React.createClass({ getInitialState: function() { + this._subListRefs = { + // key => RoomSubList ref + }; + const sizesJson = window.localStorage.getItem("mx_roomlist_sizes"); const collapsedJson = window.localStorage.getItem("mx_roomlist_collapsed"); this.subListSizes = sizesJson ? JSON.parse(sizesJson) : {}; @@ -79,8 +83,10 @@ module.exports = React.createClass({ isLoadingLeftRooms: false, totalRoomCount: null, lists: {}, + incomingCallTag: null, incomingCall: null, selectedTags: [], + hover: false, }; }, @@ -148,6 +154,8 @@ module.exports = React.createClass({ } this.subListSizes[id] = newSize; window.localStorage.setItem("mx_roomlist_sizes", JSON.stringify(this.subListSizes)); + // update overflow indicators + this._checkSubListsOverflow(); }, componentDidMount: function() { @@ -163,19 +171,24 @@ module.exports = React.createClass({ }); // load stored sizes - Object.entries(this.subListSizes).forEach(([id, size]) => { - const handle = this.resizer.forHandleWithId(id); - if (handle) { - handle.resize(size); - } + Object.keys(this.subListSizes).forEach((key) => { + this._restoreSubListSize(key); }); + this._checkSubListsOverflow(); this.resizer.attach(); this.mounted = true; }, - componentDidUpdate: function() { + componentDidUpdate: function(prevProps) { this._repositionIncomingCallBox(undefined, false); + if (this.props.searchFilter !== prevProps.searchFilter) { + // restore sizes + Object.keys(this.subListSizes).forEach((key) => { + this._restoreSubListSize(key); + }); + this._checkSubListsOverflow(); + } }, onAction: function(payload) { @@ -188,11 +201,13 @@ module.exports = React.createClass({ if (call && call.call_state === 'ringing') { this.setState({ incomingCall: call, + incomingCallTag: this.getTagNameForRoomId(payload.room_id), }); this._repositionIncomingCallBox(undefined, true); } else { this.setState({ incomingCall: null, + incomingCallTag: null, }); } break; @@ -280,6 +295,17 @@ module.exports = React.createClass({ this.forceUpdate(); }, + onMouseEnter: function(ev) { + this.setState({hover: true}); + }, + + onMouseLeave: function(ev) { + this.setState({hover: false}); + + // Refresh the room list just in case the user missed something. + this._delayedRefreshRoomList(); + }, + _delayedRefreshRoomList: new rate_limited_func(function() { this.refreshRoomList(); }, 500), @@ -332,6 +358,11 @@ module.exports = React.createClass({ }, refreshRoomList: function() { + if (this.state.hover) { + // Don't re-sort the list if we're hovering over the list + return; + } + // TODO: ideally we'd calculate this once at start, and then maintain // any changes to it incrementally, updating the appropriate sublists // as needed. @@ -347,11 +378,36 @@ module.exports = React.createClass({ // Do this here so as to not render every time the selected tags // themselves change. selectedTags: TagOrderStore.getSelectedTags(), + }, () => { + // we don't need to restore any size here, do we? + // i guess we could have triggered a new group to appear + // that already an explicit size the last time it appeared ... + this._checkSubListsOverflow(); }); // this._lastRefreshRoomListTs = Date.now(); }, + getTagNameForRoomId: function(roomId) { + const lists = RoomListStore.getRoomLists(); + for (const tagName of Object.keys(lists)) { + for (const room of lists[tagName]) { + // Should be impossible, but guard anyways. + if (!room) { + continue; + } + const myUserId = MatrixClientPeg.get().getUserId(); + if (HIDE_CONFERENCE_CHANS && Rooms.isConfCallRoom(room, myUserId, this.props.ConferenceHandler)) { + continue; + } + + if (room.roomId === roomId) return tagName; + } + } + + return null; + }, + getRoomLists: function() { const lists = RoomListStore.getRoomLists(); @@ -478,9 +534,38 @@ module.exports = React.createClass({ (filter[0] === '#' && room.getAliases().some((alias) => alias.toLowerCase().startsWith(lcFilter)))); }, - _persistCollapsedState: function(key, collapsed) { + _handleCollapsedState: function(key, collapsed) { + // persist collapsed state this.collapsedState[key] = collapsed; window.localStorage.setItem("mx_roomlist_collapsed", JSON.stringify(this.collapsedState)); + // load the persisted size configuration of the expanded sub list + if (!collapsed) { + this._restoreSubListSize(key); + } + // check overflow, as sub lists sizes have changed + // important this happens after calling resize above + this._checkSubListsOverflow(); + }, + + _restoreSubListSize(key) { + const size = this.subListSizes[key]; + const handle = this.resizer.forHandleWithId(key); + if (handle) { + handle.resize(size); + } + }, + + // check overflow for scroll indicator gradient + _checkSubListsOverflow() { + Object.values(this._subListRefs).forEach(l => l.checkOverflow()); + }, + + _subListRef: function(key, ref) { + if (!ref) { + delete this._subListRefs[key]; + } else { + this._subListRefs[key] = ref; + } }, _mapSubListProps: function(subListsProps) { @@ -505,7 +590,7 @@ module.exports = React.createClass({ const {key, label, onHeaderClick, ... otherProps} = props; const chosenKey = key || label; const onSubListHeaderClick = (collapsed) => { - this._persistCollapsedState(chosenKey, collapsed); + this._handleCollapsedState(chosenKey, collapsed); if (onHeaderClick) { onHeaderClick(collapsed); } @@ -513,6 +598,7 @@ module.exports = React.createClass({ const startAsHidden = props.startAsHidden || this.collapsedState[chosenKey]; let subList = ( { + if (!this.state.incomingCall) return null; + if (this.state.incomingCallTag !== tagName) return null; + return this.state.incomingCall; + }; + let subLists = [ { list: [], @@ -547,6 +639,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, }, { @@ -554,6 +647,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'], @@ -561,6 +655,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'})}, }, { @@ -568,6 +663,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'})}, }, ]; @@ -581,6 +677,7 @@ module.exports = React.createClass({ label: labelForTagName(tagName), tagName: tagName, order: "manual", + incomingCall: incomingCallIfTaggedAs(tagName), }; }); subLists = subLists.concat(tagSubLists); @@ -590,11 +687,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, @@ -604,15 +703,17 @@ module.exports = React.createClass({ label: _t('System Alerts'), tagName: "m.lowpriority", order: "recent", + incomingCall: incomingCallIfTaggedAs('m.server_notice'), }, ]); const subListComponents = this._mapSubListProps(subLists); return ( -
    +
    { subListComponents }
    ); }, -}); +}); \ No newline at end of file diff --git a/src/components/views/rooms/RoomRecoveryReminder.js b/src/components/views/rooms/RoomRecoveryReminder.js new file mode 100644 index 0000000000..d03c5fc96d --- /dev/null +++ b/src/components/views/rooms/RoomRecoveryReminder.js @@ -0,0 +1,170 @@ +/* +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"; +import MatrixClientPeg from "../../../MatrixClientPeg"; + +export default class RoomRecoveryReminder extends React.PureComponent { + static propTypes = { + onFinished: PropTypes.func.isRequired, + } + + constructor(props) { + super(props); + + this.state = { + loading: true, + error: null, + unverifiedDevice: null, + }; + } + + componentWillMount() { + this._loadBackupStatus(); + } + + async _loadBackupStatus() { + 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); + this.setState({ + loading: false, + error: e, + }); + return; + } + + let unverifiedDevice; + for (const sig of backupSigStatus.sigs) { + if (!sig.device.isVerified()) { + unverifiedDevice = sig.device; + break; + } + } + this.setState({ + loading: false, + unverifiedDevice, + }); + } + + showSetupDialog = () => { + if (this.state.unverifiedDevice) { + // A key backup exists for this account, but the creating device is not + // verified, so we'll show the device verify dialog. + // 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 (for cases where a key backup exists but the backup creating + // device is unverified). 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. + const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog'); + Modal.createTrackedDialog('Device Verify Dialog', '', DeviceVerifyDialog, { + userId: MatrixClientPeg.get().credentials.userId, + device: this.state.unverifiedDevice, + onFinished: this.props.onFinished, + }); + return; + } + + // The default case assumes that a key backup doesn't exist for this account, so + // we'll show the create key backup flow. + 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.showSetupDialog(); + }, + }, + ); + } + + onSetupClick = () => { + this.showSetupDialog(); + } + + render() { + if (this.state.loading) { + return null; + } + + const AccessibleButton = sdk.getComponent("views.elements.AccessibleButton"); + + let body; + if (this.state.error) { + body =
    + {_t("Unable to load key backup status")} +
    ; + } else if (this.state.unverifiedDevice) { + // A key backup exists for this account, but the creating device is not + // verified. + body = _t( + "To view your secure message history and ensure you can view new " + + "messages on future devices, set up Secure Message Recovery.", + ); + } else { + // The default case assumes that a key backup doesn't exist for this account. + // (This component doesn't currently check that itself.) + body = _t( + "If you log out or use another device, you'll lose your " + + "secure message history. To prevent this, set up Secure " + + "Message Recovery.", + ); + } + + return ( +
    +
    {_t( + "Secure Message Recovery", + )}
    +
    {body}
    +
    + + { _t("Don't ask again") } + + + { _t("Set up") } + +
    +
    + ); + } +} diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 2bc06ecc7a..95073b7be8 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -29,6 +29,7 @@ import * as RoomNotifs from '../../../RoomNotifs'; import * as FormattingUtils from '../../../utils/FormattingUtils'; import AccessibleButton from '../elements/AccessibleButton'; import ActiveRoomObserver from '../../../ActiveRoomObserver'; +import SettingsStore from "../../../settings/SettingsStore"; module.exports = React.createClass({ displayName: 'RoomTile', @@ -250,6 +251,17 @@ module.exports = React.createClass({ const mentionBadges = this.props.highlight && this._shouldShowMentionBadge(); const badges = notifBadges || mentionBadges; + const isJoined = this.props.room.getMyMembership() === "join"; + const looksLikeDm = this.props.room.getInvitedAndJoinedMemberCount() === 2; + let subtext = null; + if (!isInvite && isJoined && looksLikeDm && SettingsStore.isFeatureEnabled("feature_custom_status")) { + const selfId = MatrixClientPeg.get().getUserId(); + const otherMember = this.props.room.currentState.getMembersExcept([selfId])[0]; + if (otherMember && otherMember.user && otherMember.user._unstable_statusMessage) { + subtext = otherMember.user._unstable_statusMessage; + } + } + const classes = classNames({ 'mx_RoomTile': true, 'mx_RoomTile_selected': this.state.selected, @@ -260,6 +272,7 @@ module.exports = React.createClass({ 'mx_RoomTile_menuDisplayed': this.state.menuDisplayed, 'mx_RoomTile_noBadges': !badges, 'mx_RoomTile_transparent': this.props.transparent, + 'mx_RoomTile_hasSubtext': subtext && !this.props.collapsed, }); const avatarClasses = classNames({ @@ -285,6 +298,7 @@ module.exports = React.createClass({ const EmojiText = sdk.getComponent('elements.EmojiText'); let label; + let subtextLabel; let tooltip; if (!this.props.collapsed) { const nameClasses = classNames({ @@ -293,6 +307,8 @@ module.exports = React.createClass({ 'mx_RoomTile_badgeShown': badges || this.state.badgeHover || this.state.menuDisplayed, }); + subtextLabel = subtext ? { subtext } : null; + if (this.state.selected) { const nameSelected = { name }; @@ -332,13 +348,18 @@ module.exports = React.createClass({ >
    - + { dmIndicator }
    - { label } - { contextMenuButton } - { badge } +
    +
    + { label } + { subtextLabel } +
    + { contextMenuButton } + { badge } +
    { /* { incomingCallBox } */ } { tooltip } ; diff --git a/src/components/views/rooms/Stickerpicker.js b/src/components/views/rooms/Stickerpicker.js index 085303eafb..c7d9f890a7 100644 --- a/src/components/views/rooms/Stickerpicker.js +++ b/src/components/views/rooms/Stickerpicker.js @@ -351,7 +351,7 @@ export default class Stickerpicker extends React.Component { onClick={this._onHideStickersClick} ref='target' title={_t("Hide Stickers")}> - + ; } else { // Show show-stickers button @@ -362,7 +362,7 @@ export default class Stickerpicker extends React.Component { className="mx_MessageComposer_stickers" onClick={this._onShowStickersClick} title={_t("Show Stickers")}> - + ; } return
    diff --git a/src/components/views/settings/KeyBackupPanel.js b/src/components/views/settings/KeyBackupPanel.js index b08f4d0e78..03b98d28a0 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 72ad2943aa..40c43e6b2e 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 fffacf786e..0e12104a7d 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 f592ad6441..8822bd6388 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.", @@ -126,6 +133,7 @@ "Changes your display nickname": "Changes your display nickname", "Changes colour scheme of current room": "Changes colour scheme of current room", "Sets the room topic": "Sets the room topic", + "Sets the room name": "Sets the room name", "Invites user with given id to current room": "Invites user with given id to current room", "Joins room with given alias": "Joins room with given alias", "Leave room": "Leave room", @@ -208,11 +216,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,10 +223,42 @@ "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", + "Straight rows of keys are easy to guess": "Straight rows of keys are easy to guess", + "Short keyboard patterns are easy to guess": "Short keyboard patterns 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", "Message Pinning": "Message Pinning", + "Custom user status messages": "Custom user status messages", "Increase performance by only loading room members on first view": "Increase performance by only loading room members on first view", "Backup of encryption keys to server": "Backup of encryption keys to server", "Disable Emoji suggestions while typing": "Disable Emoji suggestions while typing", @@ -237,6 +272,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 +299,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 +314,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 +355,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 +459,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 +473,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 +500,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 +535,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 +567,11 @@ "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", + "To view your secure message history and ensure you can view new messages on future devices, set up Secure Message Recovery.": "To view your secure message history and ensure you can view new messages on future devices, 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.": "If you log out or use another device, you'll lose your secure message history. To prevent this, set up Secure Message Recovery.", + "Secure Message Recovery": "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 +671,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 +728,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 +747,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 +762,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 +878,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", @@ -845,6 +893,7 @@ "What GitHub issue are these logs for?": "What GitHub issue are these logs for?", "Notes:": "Notes:", "Send logs": "Send logs", + "Unable to load commit detail: %(msg)s": "Unable to load commit detail: %(msg)s", "Unavailable": "Unavailable", "Changelog": "Changelog", "Create a new chat or reuse an existing one": "Create a new chat or reuse an existing one", @@ -874,7 +923,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 +967,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 +997,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 +1026,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 +1034,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!", @@ -1050,6 +1070,8 @@ "Forget": "Forget", "Low Priority": "Low Priority", "Direct Chat": "Direct Chat", + "Set a new status...": "Set a new status...", + "Clear status": "Clear status", "View Community": "View Community", "Sorry, your browser is not able to run Riot.": "Sorry, your browser is not able to run Riot.", "Riot uses many advanced browser features, some of which are not available or experimental in your current browser.": "Riot uses many advanced browser features, some of which are not available or experimental in your current browser.", @@ -1057,11 +1079,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 +1107,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 +1126,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 +1140,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 +1149,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 +1183,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 +1200,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 +1214,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 +1290,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 +1332,7 @@ "unknown device": "unknown device", "NOT verified": "NOT verified", "verified": "verified", + "Name": "Name", "Verification": "Verification", "Ed25519 fingerprint": "Ed25519 fingerprint", "User ID": "User ID", @@ -1346,6 +1359,49 @@ "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", + "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", diff --git a/src/i18n/strings/en_US.json b/src/i18n/strings/en_US.json index aa7140aaa8..5e09f1d860 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", @@ -675,6 +677,7 @@ "Integrations Error": "Integrations Error", "NOTE: Apps are not end-to-end encrypted": "NOTE: Apps are not end-to-end encrypted", "Sets the room topic": "Sets the room topic", + "Sets the room name": "Sets the room name", "The maximum permitted number of widgets have already been added to this room.": "The maximum permitted number of widgets have already been added to this room.", "To get started, please pick a username!": "To get started, please pick a username!", "Unable to create widget.": "Unable to create widget.", diff --git a/src/i18n/strings/eu.json b/src/i18n/strings/eu.json index 1c9c07d305..ce5778b749 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 cdb8f78931..a6e2942e38 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 eb73d65a78..4c944f9925 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 9d0589bb17..6b69512d7b 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 045b04cc94..705442e7bf 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 12a30ef657..5671184e0a 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 zรซ 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 242990264c..712911064f 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 b5827f671a..b750dff6d6 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 30d6ea5975..15f645d5f7 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 eeb193cb8a..3df2e70774 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/resizer/resizer.js b/src/resizer/resizer.js index 7ef542a6e1..0e113b3664 100644 --- a/src/resizer/resizer.js +++ b/src/resizer/resizer.js @@ -84,8 +84,10 @@ export class Resizer { } _onMouseDown(event) { - const target = event.target; - if (!this._isResizeHandle(target) || target.parentElement !== this.container) { + // use closest in case the resize handle contains + // child dom nodes that can be the target + const resizeHandle = event.target && event.target.closest(`.${this.classNames.handle}`); + if (!resizeHandle || resizeHandle.parentElement !== this.container) { return; } // prevent starting a drag operation @@ -96,7 +98,7 @@ export class Resizer { this.container.classList.add(this.classNames.resizing); } - const {sizer, distributor} = this._createSizerAndDistributor(target); + const {sizer, distributor} = this._createSizerAndDistributor(resizeHandle); const onMouseMove = (event) => { const offset = sizer.offsetFromEvent(event); diff --git a/src/settings/Settings.js b/src/settings/Settings.js index eb702a729c..14f4bdc6dd 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -83,6 +83,12 @@ export const SETTINGS = { supportedLevels: LEVELS_FEATURE, default: false, }, + "feature_custom_status": { + isFeature: true, + displayName: _td("Custom user status messages"), + supportedLevels: LEVELS_FEATURE, + default: false, + }, "feature_lazyloading": { isFeature: true, displayName: _td("Increase performance by only loading room members on first view"), @@ -151,6 +157,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'), @@ -195,8 +206,8 @@ export const SETTINGS = { default: false, }, "theme": { - supportedLevels: LEVELS_ACCOUNT_SETTINGS, - default: "light", + supportedLevels: ['config'], + default: "dharma", }, "webRtcForceTURN": { supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG, diff --git a/src/shouldHideEvent.js b/src/shouldHideEvent.js index 3aad05a976..adc89a126a 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 bc2be37f51..4ac1e42e2e 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 0f8e5d7b4d..af6a8cc991 100644 --- a/src/stores/RoomListStore.js +++ b/src/stores/RoomListStore.js @@ -224,9 +224,9 @@ class RoomListStore extends Store { } } - // ignore any m. tag names we don't know about + // ignore tags we don't know about tagNames = tagNames.filter((t) => { - return !t.startsWith('m.') || lists[t] !== undefined; + return lists[t] !== undefined; }); if (tagNames.length) { @@ -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 1e0f5c6a4f..02c76d05d9 100644 --- a/src/stores/RoomViewStore.js +++ b/src/stores/RoomViewStore.js @@ -195,6 +195,11 @@ export 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 b3e7fc495a..ad10f28edf 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 0000000000..545686cdb6 --- /dev/null +++ b/src/utils/PasswordScorer.js @@ -0,0 +1,86 @@ +/* +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"); +_td("Straight rows of keys are easy to guess"); +_td("Short keyboard patterns 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/DecryptionFailureTracker-test.js b/test/DecryptionFailureTracker-test.js index 617c9d5d68..baa0545f77 100644 --- a/test/DecryptionFailureTracker-test.js +++ b/test/DecryptionFailureTracker-test.js @@ -54,7 +54,7 @@ describe('DecryptionFailureTracker', function() { // Immediately track the newest failures tracker.trackFailures(); - expect(count).toNotBe(0, 'should track a failure for an event that failed decryption'); + expect(count).not.toBe(0, 'should track a failure for an event that failed decryption'); done(); }); diff --git a/test/components/structures/GroupView-test.js b/test/components/structures/GroupView-test.js index 3b3510f26e..b49c335bdf 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,27 +179,27 @@ 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')); const img = ReactTestUtils.findRenderedDOMComponentWithTag(avatar, 'img'); const avatarImgElement = ReactDOM.findDOMNode(img); - expect(avatarImgElement).toExist(); - expect(avatarImgElement.src).toInclude( + expect(avatarImgElement).toBeTruthy(); + expect(avatarImgElement.src).toContain( 'https://my.home.server/_matrix/media/v1/thumbnail/' + 'someavatarurl?width=48&height=48&method=crop', ); const name = ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView_header_name'); const nameElement = ReactDOM.findDOMNode(name); - expect(nameElement).toExist(); - expect(nameElement.innerText).toInclude('The name of a community'); - expect(nameElement.innerText).toInclude(groupId); + expect(nameElement).toBeTruthy(); + expect(nameElement.innerText).toContain('The name of a community'); + expect(nameElement.innerText).toContain(groupId); const shortDesc = ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView_header_shortDesc'); const shortDescElement = ReactDOM.findDOMNode(shortDesc); - expect(shortDescElement).toExist(); + expect(shortDescElement).toBeTruthy(); expect(shortDescElement.innerText).toBe('This is a community'); }); @@ -214,12 +214,12 @@ 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'); const longDescElement = ReactDOM.findDOMNode(longDesc); - expect(longDescElement).toExist(); + expect(longDescElement).toBeTruthy(); expect(longDescElement.innerText).toBe('This is a LONG description.'); expect(longDescElement.innerHTML).toBe('
    This is a LONG description.
    '); }); @@ -235,11 +235,11 @@ 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); - expect(placeholderElement).toExist(); + expect(placeholderElement).toBeTruthy(); }); httpBackend @@ -255,18 +255,18 @@ 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(); + expect(longDescElement).toBeTruthy(); - expect(longDescElement.innerHTML).toInclude('

    This is a more complicated group page

    '); - expect(longDescElement.innerHTML).toInclude('

    With paragraphs

    '); - expect(longDescElement.innerHTML).toInclude('
      '); - expect(longDescElement.innerHTML).toInclude('
    • And lists!
    • '); + expect(longDescElement.innerHTML).toContain('

      This is a more complicated group page

      '); + expect(longDescElement.innerHTML).toContain('

      With paragraphs

      '); + expect(longDescElement.innerHTML).toContain('
        '); + expect(longDescElement.innerHTML).toContain('
      • And lists!
      • '); const imgSrc = "https://my.home.server/_matrix/media/v1/thumbnail/someimageurl?width=800&height=600"; - expect(longDescElement.innerHTML).toInclude(''); + expect(longDescElement.innerHTML).toContain(''); }); httpBackend @@ -282,14 +282,14 @@ 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(); + expect(longDescElement).toBeTruthy(); // If this fails, the URL could be in an img `src`, which is what we care about but // there's no harm in keeping this simple and checking the entire HTML string. - expect(longDescElement.innerHTML).toExclude('evilimageurl'); + expect(longDescElement.innerHTML).not.toContain('evilimageurl'); }); httpBackend @@ -305,10 +305,10 @@ 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(); + expect(roomDetailListElement).toBeTruthy(); }); httpBackend.when('GET', '/groups/' + groupIdEncoded + '/summary').respond(200, summaryResponse); @@ -322,10 +322,10 @@ 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(); + expect(roomDetailListElement).toBeTruthy(); const roomDetailListRoomName = ReactTestUtils.findRenderedDOMComponentWithClass( root, @@ -333,7 +333,7 @@ describe('GroupView', function() { ); const roomDetailListRoomNameElement = ReactDOM.findDOMNode(roomDetailListRoomName); - expect(roomDetailListRoomNameElement).toExist(); + expect(roomDetailListRoomNameElement).toBeTruthy(); expect(roomDetailListRoomNameElement.innerText).toEqual('Some room name'); }); @@ -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).toBeTruthy(); + 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/structures/login/Registration-test.js b/test/components/structures/login/Registration-test.js index b4b54a6315..7287bb0d95 100644 --- a/test/components/structures/login/Registration-test.js +++ b/test/components/structures/login/Registration-test.js @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +const jest = require('jest-mock'); const React = require('react'); const ReactDOM = require('react-dom'); const ReactTestUtils = require('react-addons-test-utils'); @@ -87,8 +88,8 @@ describe('Registration', function() { }); it('should NOT track a referral following successful registration of a non-team member', function(done) { - const onLoggedIn = expect.createSpy().andCall(function(creds, teamToken) { - expect(teamToken).toNotExist(); + const onLoggedIn = jest.fn(function(creds, teamToken) { + expect(teamToken).toBeFalsy(); done(); }); diff --git a/test/components/views/dialogs/InteractiveAuthDialog-test.js b/test/components/views/dialogs/InteractiveAuthDialog-test.js index 36894fbd21..88d1c804ca 100644 --- a/test/components/views/dialogs/InteractiveAuthDialog-test.js +++ b/test/components/views/dialogs/InteractiveAuthDialog-test.js @@ -83,8 +83,8 @@ describe('InteractiveAuthDialog', function() { submitNode = node; } } - expect(passwordNode).toExist(); - expect(submitNode).toExist(); + expect(passwordNode).toBeTruthy(); + expect(submitNode).toBeTruthy(); // submit should be disabled expect(submitNode.disabled).toBe(true); diff --git a/test/components/views/groups/GroupMemberList-test.js b/test/components/views/groups/GroupMemberList-test.js new file mode 100644 index 0000000000..3922610644 --- /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).toBeTruthy(); + 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).toBeTruthy(); + 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/components/views/login/RegistrationForm-test.js b/test/components/views/login/RegistrationForm-test.js index 14a5d036b4..2d1c1be026 100644 --- a/test/components/views/login/RegistrationForm-test.js +++ b/test/components/views/login/RegistrationForm-test.js @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +const jest = require('jest-mock'); const React = require('react'); const ReactDOM = require("react-dom"); const ReactTestUtils = require('react-addons-test-utils'); @@ -55,14 +56,14 @@ function doInputEmail(inputEmail, onTeamSelected) { } function expectTeamSelectedFromEmailInput(inputEmail, expectedTeam) { - const onTeamSelected = expect.createSpy(); + const onTeamSelected = jest.fn(); doInputEmail(inputEmail, onTeamSelected); expect(onTeamSelected).toHaveBeenCalledWith(expectedTeam); } function expectSupportFromEmailInput(inputEmail, isSupportShown) { - const onTeamSelected = expect.createSpy(); + const onTeamSelected = jest.fn(); const res = doInputEmail(inputEmail, onTeamSelected); expect(res.state.showSupportEmail).toBe(isSupportShown); diff --git a/test/components/views/rooms/MemberList-test.js b/test/components/views/rooms/MemberList-test.js new file mode 100644 index 0000000000..b9d96635a2 --- /dev/null +++ b/test/components/views/rooms/MemberList-test.js @@ -0,0 +1,301 @@ +import React from 'react'; +import ReactTestUtils from 'react-addons-test-utils'; +import ReactDOM from 'react-dom'; +import expect from 'expect'; +import lolex from 'lolex'; + +import * as TestUtils from 'test-utils'; + +import sdk from '../../../../src/index'; +import MatrixClientPeg from '../../../../src/MatrixClientPeg'; + +import {Room, RoomMember, User} from 'matrix-js-sdk'; + +function generateRoomId() { + return '!' + Math.random().toString().slice(2, 10) + ':domain'; +} + + +describe('MemberList', () => { + function createRoom(opts) { + const room = new Room(generateRoomId(), null, client.getUserId()); + if (opts) { + Object.assign(room, opts); + } + return room; + } + + let parentDiv = null; + let sandbox = null; + let client = null; + let root = null; + let clock = null; + let memberListRoom; + let memberList = null; + + let adminUsers = []; + let moderatorUsers = []; + let defaultUsers = []; + + beforeEach(function() { + TestUtils.beforeEach(this); + sandbox = TestUtils.stubClient(sandbox); + client = MatrixClientPeg.get(); + client.hasLazyLoadMembersEnabled = () => false; + + clock = lolex.install(); + + parentDiv = document.createElement('div'); + document.body.appendChild(parentDiv); + + // Make room + memberListRoom = createRoom(); + expect(memberListRoom.roomId).toBeTruthy(); + + // Make users + adminUsers = []; + moderatorUsers = []; + defaultUsers = []; + const usersPerLevel = 2; + for (let i = 0; i < usersPerLevel; i++) { + const adminUser = new RoomMember(memberListRoom.roomId, `@admin${i}:localhost`); + adminUser.membership = "join"; + adminUser.powerLevel = 100; + adminUser.user = new User(adminUser.userId); + adminUser.user.currentlyActive = true; + adminUser.user.presence = 'online'; + adminUser.user.lastPresenceTs = 1000; + adminUser.user.lastActiveAgo = 10; + adminUsers.push(adminUser); + + const moderatorUser = new RoomMember(memberListRoom.roomId, `@moderator${i}:localhost`); + moderatorUser.membership = "join"; + moderatorUser.powerLevel = 50; + moderatorUser.user = new User(moderatorUser.userId); + moderatorUser.user.currentlyActive = true; + moderatorUser.user.presence = 'online'; + moderatorUser.user.lastPresenceTs = 1000; + moderatorUser.user.lastActiveAgo = 10; + moderatorUsers.push(moderatorUser); + + const defaultUser = new RoomMember(memberListRoom.roomId, `@default${i}:localhost`); + defaultUser.membership = "join"; + defaultUser.powerLevel = 0; + defaultUser.user = new User(defaultUser.userId); + defaultUser.user.currentlyActive = true; + defaultUser.user.presence = 'online'; + defaultUser.user.lastPresenceTs = 1000; + defaultUser.user.lastActiveAgo = 10; + defaultUsers.push(defaultUser); + } + + client.getRoom = (roomId) => { + if (roomId === memberListRoom.roomId) return memberListRoom; + else return null; + }; + memberListRoom.currentState = { + members: {}, + getStateEvents: () => [], // ignore 3pid invites + }; + for (const member of [...adminUsers, ...moderatorUsers, ...defaultUsers]) { + memberListRoom.currentState.members[member.userId] = member; + } + + const MemberList = sdk.getComponent('views.rooms.MemberList'); + const WrappedMemberList = TestUtils.wrapInMatrixClientContext(MemberList); + const gatherWrappedRef = (r) => { + memberList = r; + }; + root = ReactDOM.render(, parentDiv); + }); + + afterEach((done) => { + if (parentDiv) { + ReactDOM.unmountComponentAtNode(parentDiv); + parentDiv.remove(); + parentDiv = null; + } + sandbox.restore(); + + clock.uninstall(); + + done(); + }); + + function expectOrderedByPresenceAndPowerLevel(memberTiles, isPresenceEnabled) { + let prevMember = null; + for (const tile of memberTiles) { + const memberA = prevMember; + const memberB = tile.props.member; + prevMember = memberB; // just in case an expect fails, set this early + if (!memberA) { + continue; + } + + console.log("COMPARING A VS B:"); + console.log(memberList.memberString(memberA)); + console.log(memberList.memberString(memberB)); + + const userA = memberA.user; + const userB = memberB.user; + + let groupChange = false; + + if (isPresenceEnabled) { + const convertPresence = (p) => p === 'unavailable' ? 'online' : p; + const presenceIndex = p => { + const order = ['active', 'online', 'offline']; + const idx = order.indexOf(convertPresence(p)); + return idx === -1 ? order.length : idx; // unknown states at the end + }; + + const idxA = presenceIndex(userA.currentlyActive ? 'active' : userA.presence); + const idxB = presenceIndex(userB.currentlyActive ? 'active' : userB.presence); + console.log("Comparing presence groups..."); + expect(idxB).toBeGreaterThanOrEqual(idxA); + groupChange = idxA !== idxB; + } else { + console.log("Skipped presence groups"); + } + + if (!groupChange) { + console.log("Comparing power levels..."); + expect(memberA.powerLevel).toBeGreaterThanOrEqual(memberB.powerLevel); + groupChange = memberA.powerLevel !== memberB.powerLevel; + } else { + console.log("Skipping power level check due to group change"); + } + + if (!groupChange) { + if (isPresenceEnabled) { + console.log("Comparing last active timestamp..."); + expect(userB.getLastActiveTs()).toBeLessThanOrEqual(userA.getLastActiveTs()); + groupChange = userA.getLastActiveTs() !== userB.getLastActiveTs(); + } else { + console.log("Skipping last active timestamp"); + } + } else { + console.log("Skipping last active timestamp check due to group change"); + } + + if (!groupChange) { + const nameA = memberA.name[0] === '@' ? memberA.name.substr(1) : memberA.name; + const nameB = memberB.name[0] === '@' ? memberB.name.substr(1) : memberB.name; + const nameCompare = nameB.localeCompare(nameA); + console.log("Comparing name"); + expect(nameCompare).toBeGreaterThanOrEqual(0); + } else { + console.log("Skipping name check due to group change"); + } + } + } + + function itDoesOrderMembersCorrectly(enablePresence) { + const MemberTile = sdk.getComponent("rooms.MemberTile"); + describe('does order members correctly', () => { + // Note: even if presence is disabled, we still expect that the presence + // tests will pass. All expectOrderedByPresenceAndPowerLevel does is ensure + // the order is perceived correctly, regardless of what we did to the members. + + // Each of the 4 tests here is done to prove that the member list can meet + // all 4 criteria independently. Together, they should work. + + it('by presence state', () => { + // Intentionally pick users that will confuse the power level sorting + const activeUsers = [defaultUsers[0]]; + const onlineUsers = [adminUsers[0]]; + const offlineUsers = [...moderatorUsers, ...adminUsers.slice(1), ...defaultUsers.slice(1)]; + activeUsers.forEach((u) => { + u.user.currentlyActive = true; + u.user.presence = 'online'; + }); + onlineUsers.forEach((u) => { + u.user.currentlyActive = false; + u.user.presence = 'online'; + }); + offlineUsers.forEach((u) => { + u.user.currentlyActive = false; + u.user.presence = 'offline'; + }); + + // Bypass all the event listeners and skip to the good part + memberList._showPresence = enablePresence; + memberList._updateListNow(); + + const tiles = ReactTestUtils.scryRenderedComponentsWithType(root, MemberTile); + expectOrderedByPresenceAndPowerLevel(tiles, enablePresence); + }); + + it('by power level', () => { + // We already have admin, moderator, and default users so leave them alone + + // Bypass all the event listeners and skip to the good part + memberList._showPresence = enablePresence; + memberList._updateListNow(); + + const tiles = ReactTestUtils.scryRenderedComponentsWithType(root, MemberTile); + expectOrderedByPresenceAndPowerLevel(tiles, enablePresence); + }); + + it('by last active timestamp', () => { + // Intentionally pick users that will confuse the power level sorting + // lastActiveAgoTs == lastPresenceTs - lastActiveAgo + const activeUsers = [defaultUsers[0]]; + const semiActiveUsers = [adminUsers[0]]; + const inactiveUsers = [...moderatorUsers, ...adminUsers.slice(1), ...defaultUsers.slice(1)]; + activeUsers.forEach((u) => { + u.powerLevel = 100; // set everyone to the same PL to avoid running that check + u.user.lastPresenceTs = 1000; + u.user.lastActiveAgo = 0; + }); + semiActiveUsers.forEach((u) => { + u.powerLevel = 100; + u.user.lastPresenceTs = 1000; + u.user.lastActiveAgo = 50; + }); + inactiveUsers.forEach((u) => { + u.powerLevel = 100; + u.user.lastPresenceTs = 1000; + u.user.lastActiveAgo = 100; + }); + + // Bypass all the event listeners and skip to the good part + memberList._showPresence = enablePresence; + memberList._updateListNow(); + + const tiles = ReactTestUtils.scryRenderedComponentsWithType(root, MemberTile); + expectOrderedByPresenceAndPowerLevel(tiles, enablePresence); + }); + + it('by name', () => { + // Intentionally put everyone on the same level to force a name comparison + const allUsers = [...adminUsers, ...moderatorUsers, ...defaultUsers]; + allUsers.forEach((u) => { + u.user.currentlyActive = true; + u.user.presence = "online"; + u.user.lastPresenceTs = 1000; + u.user.lastActiveAgo = 0; + u.powerLevel = 100; + }); + + // Bypass all the event listeners and skip to the good part + memberList._showPresence = enablePresence; + memberList._updateListNow(); + + const tiles = ReactTestUtils.scryRenderedComponentsWithType(root, MemberTile); + expectOrderedByPresenceAndPowerLevel(tiles, enablePresence); + }); + }); + } + + describe('when presence is enabled', () => { + itDoesOrderMembersCorrectly(true); + }); + + describe('when presence is not enabled', () => { + itDoesOrderMembersCorrectly(false); + }); +}); + + diff --git a/test/components/views/rooms/MessageComposerInput-test.js b/test/components/views/rooms/MessageComposerInput-test.js index 662fbc7104..ed07c0f233 100644 --- a/test/components/views/rooms/MessageComposerInput-test.js +++ b/test/components/views/rooms/MessageComposerInput-test.js @@ -1,7 +1,7 @@ import React from 'react'; import ReactTestUtils from 'react-addons-test-utils'; import ReactDOM from 'react-dom'; -import expect, {createSpy} from 'expect'; +import expect from 'expect'; import sinon from 'sinon'; import Promise from 'bluebird'; import * as testUtils from '../../../test-utils'; diff --git a/test/components/views/rooms/RoomList-test.js b/test/components/views/rooms/RoomList-test.js index e512b96ba8..0c970edb0b 100644 --- a/test/components/views/rooms/RoomList-test.js +++ b/test/components/views/rooms/RoomList-test.js @@ -69,7 +69,7 @@ describe('RoomList', () => { ReactTestUtils.findRenderedComponentWithType(root, RoomList); movingRoom = createRoom({name: 'Moving room'}); - expect(movingRoom.roomId).toNotBe(null); + expect(movingRoom.roomId).not.toBe(null); // Mock joined member myMember = new RoomMember(movingRoomId, myUserId); @@ -139,7 +139,7 @@ describe('RoomList', () => { throw err; } - expect(expectedRoomTile).toExist(); + expect(expectedRoomTile).toBeTruthy(); expect(expectedRoomTile.props.room).toBe(room); } diff --git a/test/components/views/rooms/RoomSettings-test.js b/test/components/views/rooms/RoomSettings-test.js index ffcecf1725..3bccdcf825 100644 --- a/test/components/views/rooms/RoomSettings-test.js +++ b/test/components/views/rooms/RoomSettings-test.js @@ -1,6 +1,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import expect, {createSpy} from 'expect'; +import expect from 'expect'; +import jest from 'jest-mock'; import Promise from 'bluebird'; import * as testUtils from '../../../test-utils'; import sdk from 'matrix-react-sdk'; @@ -18,12 +19,12 @@ describe('RoomSettings', () => { function expectSentStateEvent(roomId, eventType, expectedEventContent) { let found = false; - for (const call of client.sendStateEvent.calls) { + for (const call of client.sendStateEvent.mock.calls) { const [ actualRoomId, actualEventType, actualEventContent, - ] = call.arguments.slice(0, 3); + ] = call.slice(0, 3); if (roomId === actualRoomId && actualEventType === eventType) { expect(actualEventContent).toEqual(expectedEventContent); @@ -40,20 +41,20 @@ describe('RoomSettings', () => { client = MatrixClientPeg.get(); client.credentials = {userId: '@me:domain.com'}; - client.setRoomName = createSpy().andReturn(Promise.resolve()); - client.setRoomTopic = createSpy().andReturn(Promise.resolve()); - client.setRoomDirectoryVisibility = createSpy().andReturn(Promise.resolve()); + client.setRoomName = jest.fn().mockReturnValue(Promise.resolve()); + client.setRoomTopic = jest.fn().mockReturnValue(Promise.resolve()); + client.setRoomDirectoryVisibility = jest.fn().mockReturnValue(Promise.resolve()); // Covers any room state event (e.g. name, avatar, topic) - client.sendStateEvent = createSpy().andReturn(Promise.resolve()); + client.sendStateEvent = jest.fn().mockReturnValue(Promise.resolve()); // Covers room tagging - client.setRoomTag = createSpy().andReturn(Promise.resolve()); - client.deleteRoomTag = createSpy().andReturn(Promise.resolve()); + client.setRoomTag = jest.fn().mockReturnValue(Promise.resolve()); + client.deleteRoomTag = jest.fn().mockReturnValue(Promise.resolve()); // Covers any setting in the SettingsStore // (including local client settings not stored via matrix) - SettingsStore.setValue = createSpy().andReturn(Promise.resolve()); + SettingsStore.setValue = jest.fn().mockReturnValue(Promise.resolve()); parentDiv = document.createElement('div'); document.body.appendChild(parentDiv); @@ -83,9 +84,9 @@ describe('RoomSettings', () => { it('should not set when no setting is changed', (done) => { roomSettings.save().then(() => { - expect(client.sendStateEvent).toNotHaveBeenCalled(); - expect(client.setRoomTag).toNotHaveBeenCalled(); - expect(client.deleteRoomTag).toNotHaveBeenCalled(); + expect(client.sendStateEvent).not.toHaveBeenCalled(); + expect(client.setRoomTag).not.toHaveBeenCalled(); + expect(client.deleteRoomTag).not.toHaveBeenCalled(); done(); }); }); @@ -93,7 +94,7 @@ describe('RoomSettings', () => { // XXX: Apparently we do call SettingsStore.setValue xit('should not settings via the SettingsStore when no setting is changed', (done) => { roomSettings.save().then(() => { - expect(SettingsStore.setValue).toNotHaveBeenCalled(); + expect(SettingsStore.setValue).not.toHaveBeenCalled(); done(); }); }); @@ -103,7 +104,7 @@ describe('RoomSettings', () => { roomSettings.setName(name); roomSettings.save().then(() => { - expect(client.setRoomName.calls[0].arguments.slice(0, 2)) + expect(client.setRoomName.mock.calls[0].slice(0, 2)) .toEqual(['!DdJkzRliezrwpNebLk:matrix.org', name]); done(); @@ -115,7 +116,7 @@ describe('RoomSettings', () => { roomSettings.setTopic(topic); roomSettings.save().then(() => { - expect(client.setRoomTopic.calls[0].arguments.slice(0, 2)) + expect(client.setRoomTopic.mock.calls[0].slice(0, 2)) .toEqual(['!DdJkzRliezrwpNebLk:matrix.org', topic]); done(); diff --git a/test/matrix-to-test.js b/test/matrix-to-test.js index 70533575c4..26bd3b1a96 100644 --- a/test/matrix-to-test.js +++ b/test/matrix-to-test.js @@ -39,7 +39,7 @@ describe('matrix-to', function() { it('should pick no candidate servers when the room is not found', function() { peg.get().getRoom = () => null; const pickedServers = pickServerCandidates("!somewhere:example.org"); - expect(pickedServers).toExist(); + expect(pickedServers).toBeTruthy(); expect(pickedServers.length).toBe(0); }); @@ -50,7 +50,7 @@ describe('matrix-to', function() { }; }; const pickedServers = pickServerCandidates("!somewhere:example.org"); - expect(pickedServers).toExist(); + expect(pickedServers).toBeTruthy(); expect(pickedServers.length).toBe(0); }); @@ -74,7 +74,7 @@ describe('matrix-to', function() { }; }; const pickedServers = pickServerCandidates("!somewhere:example.org"); - expect(pickedServers).toExist(); + expect(pickedServers).toBeTruthy(); expect(pickedServers.length).toBe(3); expect(pickedServers[0]).toBe("pl_95"); // we don't check the 2nd and 3rd servers because that is done by the next test @@ -112,7 +112,7 @@ describe('matrix-to', function() { }; }; const pickedServers = pickServerCandidates("!somewhere:example.org"); - expect(pickedServers).toExist(); + expect(pickedServers).toBeTruthy(); expect(pickedServers.length).toBe(3); expect(pickedServers[0]).toBe("first"); expect(pickedServers[1]).toBe("second"); @@ -143,14 +143,46 @@ describe('matrix-to', function() { }; }; const pickedServers = pickServerCandidates("!somewhere:example.org"); - expect(pickedServers).toExist(); + expect(pickedServers).toBeTruthy(); expect(pickedServers.length).toBe(3); expect(pickedServers[0]).toBe("first"); expect(pickedServers[1]).toBe("second"); 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).toBeTruthy(); + expect(pickedServers.length).toBe(3); + }); + + it('should not consider IPv4 hosts', function() { peg.get().getRoom = () => { return { getJoinedMembers: () => [ @@ -162,12 +194,11 @@ 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).toBeTruthy(); + expect(pickedServers.length).toBe(0); }); - it('should work with IPv6 hostnames', function() { + it('should not consider IPv6 hosts', function() { peg.get().getRoom = () => { return { getJoinedMembers: () => [ @@ -179,12 +210,11 @@ 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).toBeTruthy(); + 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: () => [ @@ -196,12 +226,11 @@ 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).toBeTruthy(); + 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: () => [ @@ -213,9 +242,8 @@ 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).toBeTruthy(); + expect(pickedServers.length).toBe(0); }); it('should work with hostnames with ports', function() { @@ -230,11 +258,145 @@ describe('matrix-to', function() { }; }; const pickedServers = pickServerCandidates("!somewhere:example.org"); - expect(pickedServers).toExist(); + expect(pickedServers).toBeTruthy(); expect(pickedServers.length).toBe(1); 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).toBeTruthy(); + 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).toBeTruthy(); + 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).toBeTruthy(); + 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).toBeTruthy(); + 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 bc4d29210e..d5bcd9397a 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 (updates == 0) { + inst.componentDidUpdate = cdu; + resolve(); + } if (cdu) cdu(prevProps, prevState, snapshot); - - inst.componentDidUpdate = cdu; }; }); }