diff --git a/.eslintignore.errorfiles b/.eslintignore.errorfiles index 430546d281..8d0821207a 100644 --- a/.eslintignore.errorfiles +++ b/.eslintignore.errorfiles @@ -39,7 +39,6 @@ src/components/views/elements/InlineSpinner.js src/components/views/elements/MemberEventListSummary.js src/components/views/elements/Spinner.js src/components/views/elements/TintableSvg.js -src/components/views/elements/UserInfo.js src/components/views/elements/UserSelector.js src/components/views/globals/MatrixToolbar.js src/components/views/globals/NewVersionBar.js @@ -54,7 +53,6 @@ src/components/views/messages/RoomAvatarEvent.js src/components/views/messages/TextualBody.js src/components/views/room_settings/AliasSettings.js src/components/views/room_settings/ColorSettings.js -src/components/views/room_settings/UrlPreviewSettings.js src/components/views/rooms/Autocomplete.js src/components/views/rooms/AuxPanel.js src/components/views/rooms/EntityTile.js @@ -66,7 +64,6 @@ src/components/views/rooms/MemberTile.js src/components/views/rooms/MessageComposer.js src/components/views/rooms/MessageComposerInput.js src/components/views/rooms/PinnedEventTile.js -src/components/views/rooms/RoomDropTarget.js src/components/views/rooms/RoomList.js src/components/views/rooms/RoomPreviewBar.js src/components/views/rooms/RoomSettings.js @@ -92,7 +89,6 @@ src/Markdown.js src/MatrixClientPeg.js src/Modal.js src/notifications/ContentRules.js -src/notifications/NotificationUtils.js src/notifications/PushRuleVectorState.js src/notifications/StandardActions.js src/notifications/VectorPushRulesDefinitions.js @@ -102,7 +98,6 @@ src/Presence.js src/rageshake/rageshake.js src/rageshake/submit-rageshake.js src/ratelimitedfunc.js -src/RichText.js src/Roles.js src/Rooms.js src/ScalarAuthClient.js diff --git a/CHANGELOG.md b/CHANGELOG.md index d007be0f4b..00b035ac9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,148 @@ +Changes in [0.14.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.14.1) (2018-10-19) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.14.0...v0.14.1) + + * Apply the user's tint once the MatrixClientPeg is moderately ready + [\#2214](https://github.com/matrix-org/matrix-react-sdk/pull/2214) + +Changes in [0.14.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.14.0) (2018-10-16) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.14.0-rc.1...v0.14.0) + + * Phased rollout of lazy loading + [\#2218](https://github.com/matrix-org/matrix-react-sdk/pull/2218) + +Changes in [0.14.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.14.0-rc.1) (2018-10-11) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.13.6...v0.14.0-rc.1) + + * turn LL on by default! + [\#2209](https://github.com/matrix-org/matrix-react-sdk/pull/2209) + * Update from Weblate. + [\#2207](https://github.com/matrix-org/matrix-react-sdk/pull/2207) + * Fix quote post slate update + [\#2206](https://github.com/matrix-org/matrix-react-sdk/pull/2206) + * Handle InvalidStoreError from js-sdk + [\#2205](https://github.com/matrix-org/matrix-react-sdk/pull/2205) + * Fall back to default avatar in RR when member isn't loaded yet + [\#2204](https://github.com/matrix-org/matrix-react-sdk/pull/2204) + * Update to new version of slate + [\#2202](https://github.com/matrix-org/matrix-react-sdk/pull/2202) + * Update karma to webpack 4 + [\#2203](https://github.com/matrix-org/matrix-react-sdk/pull/2203) + * More accessible buttons - take 2 + [\#2194](https://github.com/matrix-org/matrix-react-sdk/pull/2194) + * log correct error code when opening log idb + [\#2200](https://github.com/matrix-org/matrix-react-sdk/pull/2200) + * show warning when LL is disabled but was enabled before + [\#2201](https://github.com/matrix-org/matrix-react-sdk/pull/2201) + * Fall back to another store if indexeddb start fails + [\#2195](https://github.com/matrix-org/matrix-react-sdk/pull/2195) + * Silence bluebird warnings + [\#2198](https://github.com/matrix-org/matrix-react-sdk/pull/2198) + * Use createObjectURL instead of readAsDataURL for videos + [\#2197](https://github.com/matrix-org/matrix-react-sdk/pull/2197) + * Revert "Use createObjectURL instead of readAsDataURL for videos" + [\#2196](https://github.com/matrix-org/matrix-react-sdk/pull/2196) + * Track how far the user travels before dismissing their user settings + [\#2183](https://github.com/matrix-org/matrix-react-sdk/pull/2183) + * Drop (IRC) suffix hacks + [\#2193](https://github.com/matrix-org/matrix-react-sdk/pull/2193) + * Use createObjectURL instead of readAsDataURL for videos + [\#2176](https://github.com/matrix-org/matrix-react-sdk/pull/2176) + * Remove old migration code + [\#2192](https://github.com/matrix-org/matrix-react-sdk/pull/2192) + * Fix brace style in TextForEvent.js + [\#2191](https://github.com/matrix-org/matrix-react-sdk/pull/2191) + * Fix error logging + [\#2190](https://github.com/matrix-org/matrix-react-sdk/pull/2190) + * Fix Promise.defer warning in ScalarAuthClient.js + [\#2188](https://github.com/matrix-org/matrix-react-sdk/pull/2188) + * Communicate early that a 3pid is required during registration if needed + [\#2180](https://github.com/matrix-org/matrix-react-sdk/pull/2180) + * try to encourage people to attach logs to bugs + [\#2185](https://github.com/matrix-org/matrix-react-sdk/pull/2185) + * Show the 'homeserver unavailable' warning when the first sync fails + [\#2182](https://github.com/matrix-org/matrix-react-sdk/pull/2182) + * allow passing initial is_url like hs_url in query params + [\#2083](https://github.com/matrix-org/matrix-react-sdk/pull/2083) + * Update karma + [\#2177](https://github.com/matrix-org/matrix-react-sdk/pull/2177) + * fudge hangup reasons + [\#2184](https://github.com/matrix-org/matrix-react-sdk/pull/2184) + * Provide more helpful errors when i18n generation fails + [\#2181](https://github.com/matrix-org/matrix-react-sdk/pull/2181) + +Changes in [0.14.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.14.0-rc.1) (2018-10-11) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.13.6...v0.14.0-rc.1) + + * turn LL on by default! + [\#2209](https://github.com/matrix-org/matrix-react-sdk/pull/2209) + * Update from Weblate. + [\#2207](https://github.com/matrix-org/matrix-react-sdk/pull/2207) + * Fix quote post slate update + [\#2206](https://github.com/matrix-org/matrix-react-sdk/pull/2206) + * Handle InvalidStoreError from js-sdk + [\#2205](https://github.com/matrix-org/matrix-react-sdk/pull/2205) + * Fall back to default avatar in RR when member isn't loaded yet + [\#2204](https://github.com/matrix-org/matrix-react-sdk/pull/2204) + * Update to new version of slate + [\#2202](https://github.com/matrix-org/matrix-react-sdk/pull/2202) + * Update karma to webpack 4 + [\#2203](https://github.com/matrix-org/matrix-react-sdk/pull/2203) + * More accessible buttons - take 2 + [\#2194](https://github.com/matrix-org/matrix-react-sdk/pull/2194) + * log correct error code when opening log idb + [\#2200](https://github.com/matrix-org/matrix-react-sdk/pull/2200) + * show warning when LL is disabled but was enabled before + [\#2201](https://github.com/matrix-org/matrix-react-sdk/pull/2201) + * Fall back to another store if indexeddb start fails + [\#2195](https://github.com/matrix-org/matrix-react-sdk/pull/2195) + * Silence bluebird warnings + [\#2198](https://github.com/matrix-org/matrix-react-sdk/pull/2198) + * Use createObjectURL instead of readAsDataURL for videos + [\#2197](https://github.com/matrix-org/matrix-react-sdk/pull/2197) + * Revert "Use createObjectURL instead of readAsDataURL for videos" + [\#2196](https://github.com/matrix-org/matrix-react-sdk/pull/2196) + * Track how far the user travels before dismissing their user settings + [\#2183](https://github.com/matrix-org/matrix-react-sdk/pull/2183) + * Drop (IRC) suffix hacks + [\#2193](https://github.com/matrix-org/matrix-react-sdk/pull/2193) + * Use createObjectURL instead of readAsDataURL for videos + [\#2176](https://github.com/matrix-org/matrix-react-sdk/pull/2176) + * Remove old migration code + [\#2192](https://github.com/matrix-org/matrix-react-sdk/pull/2192) + * Fix brace style in TextForEvent.js + [\#2191](https://github.com/matrix-org/matrix-react-sdk/pull/2191) + * Fix error logging + [\#2190](https://github.com/matrix-org/matrix-react-sdk/pull/2190) + * Fix Promise.defer warning in ScalarAuthClient.js + [\#2188](https://github.com/matrix-org/matrix-react-sdk/pull/2188) + * Communicate early that a 3pid is required during registration if needed + [\#2180](https://github.com/matrix-org/matrix-react-sdk/pull/2180) + * try to encourage people to attach logs to bugs + [\#2185](https://github.com/matrix-org/matrix-react-sdk/pull/2185) + * Show the 'homeserver unavailable' warning when the first sync fails + [\#2182](https://github.com/matrix-org/matrix-react-sdk/pull/2182) + * allow passing initial is_url like hs_url in query params + [\#2083](https://github.com/matrix-org/matrix-react-sdk/pull/2083) + * Update karma + [\#2177](https://github.com/matrix-org/matrix-react-sdk/pull/2177) + * fudge hangup reasons + [\#2184](https://github.com/matrix-org/matrix-react-sdk/pull/2184) + * Provide more helpful errors when i18n generation fails + [\#2181](https://github.com/matrix-org/matrix-react-sdk/pull/2181) + +Changes in [0.13.6](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.13.6) (2018-10-08) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.13.5...v0.13.6) + + * Fix resuming session in Firefox private mode/Tor browser being broken + [\#2195](https://github.com/matrix-org/matrix-react-sdk/pull/2195) + * Show warning about using lazy-loading/non-lazy-loading versions simultaneously (/app & /develop) + [\#2201](https://github.com/matrix-org/matrix-react-sdk/pull/2201) + Changes in [0.13.5](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.13.5) (2018-10-01) ===================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.13.5-rc.1...v0.13.5) diff --git a/docs/settings.md b/docs/settings.md index d41aebad3c..cdba01e04a 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -93,6 +93,16 @@ Simply call `SettingsStore.getDisplayName`. The appropriate display name will be Occasionally some parts of the application may be undergoing testing and are not quite production ready. These are commonly known to be behind a "labs flag". Features behind lab flags must go through the granular settings system, and look and act very much normal settings. The exception is that they must supply `isFeature: true` as part of the setting definition and should go through the helper functions on `SettingsStore`. +Although features have levels and a default value, the calculation of those options is blocked by the feature's state. A feature's state is determined from the `SdkConfig` and is a little complex. If `enableLabs` (a legacy flag) is `true` then the feature's state is `labs`, if it is `false`, the state is `disable`. If `enableLabs` is not set then the state is determined from the `features` config, such as in the following: +```json +"features": { + "feature_lazyloading": "labs" +} +``` +In this example, `feature_lazyloading` is in the `labs` state. It may also be in the `enable` or `disable` state with a similar approach. If the state is invalid, the feature is in the `disable` state. A feature's levels are only calculated if it is in the `labs` state, therefore the default only applies in that scenario. If the state is `enable`, the feature is always-on. + +Once a feature flag has served its purpose, it is generally recommended to remove it and the associated feature flag checks. This would enable the feature implicitly as it is part of the application now. + ### Determining if a feature is enabled A simple call to `SettingsStore.isFeatureEnabled` will tell you if the feature is enabled. This will perform all the required calculations to determine if the feature is enabled based upon the configuration and user selection. diff --git a/karma.conf.js b/karma.conf.js index 164cd9ce59..4d699599cb 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -23,6 +23,7 @@ var fs = require('fs'); // var testFile = process.env.KARMA_TEST_FILE || 'test/all-tests.js'; + process.env.PHANTOMJS_BIN = 'node_modules/.bin/phantomjs'; function fileExists(name) { @@ -160,10 +161,9 @@ module.exports = function (config) { webpack: { module: { - loaders: [ - { test: /\.json$/, loader: "json" }, + rules: [ { - test: /\.js$/, loader: "babel", + test: /\.js$/, loader: "babel-loader", include: [path.resolve('./src'), path.resolve('./test'), ] @@ -200,8 +200,9 @@ module.exports = function (config) { 'matrix-react-sdk': path.resolve('test/skinned-sdk.js'), 'sinon': 'sinon/pkg/sinon.js', }, - root: [ + modules: [ path.resolve('./test'), + "node_modules" ], }, devtool: 'inline-source-map', @@ -210,6 +211,8 @@ module.exports = function (config) { // (the 'commonjs' here means it will output a 'require') "electron": "commonjs electron", }, + // make sure we're flagged as development to avoid wasting time optimising + mode: 'development', }, webpackMiddleware: { diff --git a/package.json b/package.json index 72481f2e3c..dabaefe0ad 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "0.13.5", + "version": "0.14.1", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -53,7 +53,7 @@ "test-multi": "karma start" }, "dependencies": { - "babel-runtime": "^6.11.6", + "babel-runtime": "^6.26.0", "bluebird": "^3.5.0", "blueimp-canvas-to-blob": "^3.5.0", "browser-encrypt-attachment": "^0.3.0", @@ -67,15 +67,15 @@ "flux": "2.1.1", "focus-trap-react": "^3.0.5", "fuse.js": "^2.2.0", - "gemini-scrollbar": "matrix-org/gemini-scrollbar#b302279", + "gemini-scrollbar": "github:matrix-org/gemini-scrollbar#b302279", "gfm.css": "^1.1.1", "glob": "^5.0.14", - "highlight.js": "^9.0.0", + "highlight.js": "^9.13.0", "isomorphic-fetch": "^2.2.1", "linkifyjs": "^2.1.6", "lodash": "^4.13.1", "lolex": "2.3.2", - "matrix-js-sdk": "0.11.1", + "matrix-js-sdk": "matrix-org/matrix-js-sdk#develop", "optimist": "^0.6.1", "pako": "^1.0.5", "prop-types": "^15.5.8", @@ -85,54 +85,53 @@ "react-addons-css-transition-group": "15.3.2", "react-beautiful-dnd": "^4.0.1", "react-dom": "^15.6.0", - "react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef", + "react-gemini-scrollbar": "github:matrix-org/react-gemini-scrollbar#5e97aef", "resize-observer-polyfill": "^1.5.0", "sanitize-html": "^1.18.4", - "slate": "0.34.7", + "slate": "^0.41.2", "slate-html-serializer": "^0.6.1", - "slate-md-serializer": "matrix-org/slate-md-serializer#f7c4ad3", - "slate-react": "^0.12.4", + "slate-md-serializer": "github:matrix-org/slate-md-serializer#f7c4ad3", + "slate-react": "^0.18.10", "text-encoding-utf-8": "^1.0.1", "url": "^0.11.0", - "velocity-vector": "vector-im/velocity#059e3b2", + "velocity-vector": "github:vector-im/velocity#059e3b2", "whatwg-fetch": "^1.1.1" }, "devDependencies": { - "babel-cli": "^6.5.2", - "babel-core": "^6.14.0", + "babel-cli": "^6.26.0", + "babel-core": "^6.26.3", "babel-eslint": "^6.1.2", - "babel-loader": "^6.2.5", + "babel-loader": "^7.1.5", "babel-plugin-add-module-exports": "^0.2.1", "babel-plugin-transform-async-to-bluebird": "^1.1.1", - "babel-plugin-transform-class-properties": "^6.16.0", - "babel-plugin-transform-object-rest-spread": "^6.16.0", - "babel-plugin-transform-runtime": "^6.15.0", - "babel-polyfill": "^6.5.0", - "babel-preset-es2015": "^6.14.0", - "babel-preset-es2016": "^6.11.3", - "babel-preset-es2017": "^6.14.0", - "babel-preset-react": "^6.11.1", + "babel-plugin-transform-class-properties": "^6.24.1", + "babel-plugin-transform-object-rest-spread": "^6.26.0", + "babel-plugin-transform-runtime": "^6.23.0", + "babel-polyfill": "^6.26.0", + "babel-preset-es2015": "^6.24.1", + "babel-preset-es2016": "^6.24.1", + "babel-preset-es2017": "^6.24.1", + "babel-preset-react": "^6.24.1", "chokidar": "^1.6.1", "concurrently": "^4.0.1", "eslint": "^3.13.1", "eslint-config-google": "^0.7.1", - "eslint-plugin-babel": "^4.0.1", + "eslint-plugin-babel": "^4.1.2", "eslint-plugin-flowtype": "^2.30.0", "eslint-plugin-react": "^7.7.0", "estree-walker": "^0.5.0", "expect": "^1.16.0", "flow-parser": "^0.57.3", - "json-loader": "^0.5.3", "karma": "^3.0.0", "karma-chrome-launcher": "^0.2.3", - "karma-cli": "^0.1.2", - "karma-junit-reporter": "^1.2.0", + "karma-cli": "^1.0.1", + "karma-junit-reporter": "^0.4.2", "karma-logcapture-reporter": "0.0.1", - "karma-mocha": "^0.2.2", + "karma-mocha": "^1.3.0", "karma-sourcemap-loader": "^0.3.7", "karma-spec-reporter": "^0.0.31", - "karma-summary-reporter": "^1.3.3", - "karma-webpack": "^3.0.5", + "karma-summary-reporter": "^1.5.1", + "karma-webpack": "^4.0.0-beta.0", "matrix-mock-request": "^1.2.1", "matrix-react-test-utils": "^0.1.1", "mocha": "^5.0.5", @@ -142,6 +141,7 @@ "sinon": "^5.0.7", "source-map-loader": "^0.2.3", "walk": "^2.3.9", - "webpack": "^1.12.14" + "webpack": "^4.20.2", + "webpack-cli": "^3.1.1" } } diff --git a/res/css/_common.scss b/res/css/_common.scss index 38f576a532..bf67edc1c3 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -222,6 +222,11 @@ textarea { word-wrap: break-word; } +.mx_Dialog_buttons { + padding-right: 58px; + text-align: right; +} + .mx_Dialog button, .mx_Dialog input[type="submit"] { @mixin mx_DialogButton; margin-left: 0px; diff --git a/res/css/_components.scss b/res/css/_components.scss index be140b2de7..9fd524104d 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -39,7 +39,6 @@ @import "./views/dialogs/_DevtoolsDialog.scss"; @import "./views/dialogs/_EncryptedEventDialog.scss"; @import "./views/dialogs/_GroupAddressPicker.scss"; -@import "./views/dialogs/_QuestionDialog.scss"; @import "./views/dialogs/_RestoreKeyBackupDialog.scss"; @import "./views/dialogs/_RoomUpgradeDialog.scss"; @import "./views/dialogs/_SetEmailDialog.scss"; diff --git a/res/css/structures/_LoginBox.scss b/res/css/structures/_LoginBox.scss index 7f6199c451..0a3e21a980 100644 --- a/res/css/structures/_LoginBox.scss +++ b/res/css/structures/_LoginBox.scss @@ -19,6 +19,7 @@ limitations under the License. height: unset !important; padding-top: 13px !important; padding-bottom: 14px !important; + order: 4; } .mx_LoginBox_loginButton_wrapper { diff --git a/res/css/views/dialogs/_ChatInviteDialog.scss b/res/css/views/dialogs/_ChatInviteDialog.scss index 6fc211743d..dcc0f5921a 100644 --- a/res/css/views/dialogs/_ChatInviteDialog.scss +++ b/res/css/views/dialogs/_ChatInviteDialog.scss @@ -14,14 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_ChatInviteDialog { - /* XXX: padding-left is on mx_Dialog but padding-right has subsequently - * been added on other dialogs. Surely all our dialogs should have consistent - * right hand padding? - */ - padding-right: 58px; -} - /* Using a textarea for this element, to circumvent autofill */ .mx_ChatInviteDialog_input, .mx_ChatInviteDialog_input:focus diff --git a/res/css/views/dialogs/_DevtoolsDialog.scss b/res/css/views/dialogs/_DevtoolsDialog.scss index a4a868bd11..3764bb13b3 100644 --- a/res/css/views/dialogs/_DevtoolsDialog.scss +++ b/res/css/views/dialogs/_DevtoolsDialog.scss @@ -14,6 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ +.mx_DevTools_dialog { + padding-right: 58px; +} + .mx_DevTools_content { margin: 10px 0; } diff --git a/res/css/views/dialogs/_QuestionDialog.scss b/res/css/views/dialogs/_QuestionDialog.scss deleted file mode 100644 index 3d47f17592..0000000000 --- a/res/css/views/dialogs/_QuestionDialog.scss +++ /dev/null @@ -1,18 +0,0 @@ -/* -Copyright 2017 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_QuestionDialog { - padding-right: 58px; -} diff --git a/res/css/views/rooms/_MemberList.scss b/res/css/views/rooms/_MemberList.scss index 83fc70aefb..cfac8797b9 100644 --- a/res/css/views/rooms/_MemberList.scss +++ b/res/css/views/rooms/_MemberList.scss @@ -111,6 +111,3 @@ limitations under the License. width: 100%; } -.mx_MemberList_outerWrapper { - height: 0px; -} diff --git a/res/css/views/rooms/_RoomPreviewBar.scss b/res/css/views/rooms/_RoomPreviewBar.scss index 331eb582ea..8196740499 100644 --- a/res/css/views/rooms/_RoomPreviewBar.scss +++ b/res/css/views/rooms/_RoomPreviewBar.scss @@ -56,3 +56,7 @@ limitations under the License. .mx_RoomPreviewBar_warningIcon { padding: 12px; } + +.mx_RoomPreviewBar_spinnerIntro { + margin-top: 50px; +} \ No newline at end of file diff --git a/res/css/views/rooms/_RoomSettings.scss b/res/css/views/rooms/_RoomSettings.scss index f04042ea77..b3858f3ba7 100644 --- a/res/css/views/rooms/_RoomSettings.scss +++ b/res/css/views/rooms/_RoomSettings.scss @@ -28,6 +28,13 @@ limitations under the License. margin-right: 8px; } +.mx_RoomSettings_devtoolsButton { + @mixin mx_DialogButton; + position: relative; + padding: 4px 1.5em; + margin-top: 8px; +} + .mx_RoomSettings_upgradeButton, .mx_RoomSettings_leaveButton:hover, .mx_RoomSettings_unbanButton:hover { diff --git a/src/ContentMessages.js b/src/ContentMessages.js index a0bf75bccf..fd21977108 100644 --- a/src/ContentMessages.js +++ b/src/ContentMessages.js @@ -153,17 +153,24 @@ function loadVideoElement(videoFile) { // Load the file into an html element const video = document.createElement("video"); - // Wait until we have enough data to thumbnail the first frame. - video.onloadeddata = function() { - URL.revokeObjectURL(video.src); - deferred.resolve(video); + const reader = new FileReader(); + reader.onload = function(e) { + video.src = e.target.result; + + // Once ready, returns its size + // Wait until we have enough data to thumbnail the first frame. + video.onloadeddata = function() { + deferred.resolve(video); + }; + video.onerror = function(e) { + deferred.reject(e); + }; }; - video.onerror = function(e) { + reader.onerror = function(e) { deferred.reject(e); }; - - // We don't use readAsDataURL because massive files and b64 don't mix. - video.src = URL.createObjectURL(videoFile); + reader.readAsDataURL(videoFile); + return deferred.promise; } diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 434975a5bc..b0912c759e 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -159,6 +159,40 @@ export function attemptTokenLogin(queryParams, defaultDeviceDisplayName) { }); } +export function handleInvalidStoreError(e) { + if (e.reason === Matrix.InvalidStoreError.TOGGLED_LAZY_LOADING) { + return Promise.resolve().then(() => { + const lazyLoadEnabled = e.value; + if (lazyLoadEnabled) { + const LazyLoadingResyncDialog = + sdk.getComponent("views.dialogs.LazyLoadingResyncDialog"); + return new Promise((resolve) => { + Modal.createDialog(LazyLoadingResyncDialog, { + onFinished: resolve, + }); + }); + } else { + // show warning about simultaneous use + // between LL/non-LL version on same host. + // as disabling LL when previously enabled + // is a strong indicator of this (/develop & /app) + const LazyLoadingDisabledDialog = + sdk.getComponent("views.dialogs.LazyLoadingDisabledDialog"); + return new Promise((resolve) => { + Modal.createDialog(LazyLoadingDisabledDialog, { + onFinished: resolve, + host: window.location.host, + }); + }); + } + }).then(() => { + return MatrixClientPeg.get().store.deleteAllData(); + }).then(() => { + PlatformPeg.get().reload(); + }); + } +} + function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) { console.log(`Doing guest login on ${hsUrl}`); @@ -238,27 +272,6 @@ async function _restoreFromLocalStorage() { function _handleLoadSessionFailure(e) { console.log("Unable to load session", e); - if (e instanceof Matrix.InvalidStoreError) { - if (e.reason === Matrix.InvalidStoreError.TOGGLED_LAZY_LOADING) { - return Promise.resolve().then(() => { - const lazyLoadEnabled = e.value; - if (lazyLoadEnabled) { - const LazyLoadingResyncDialog = - sdk.getComponent("views.dialogs.LazyLoadingResyncDialog"); - return new Promise((resolve) => { - Modal.createDialog(LazyLoadingResyncDialog, { - onFinished: resolve, - }); - }); - } - }).then(() => { - return MatrixClientPeg.get().store.deleteAllData(); - }).then(() => { - PlatformPeg.get().reload(); - }); - } - } - const def = Promise.defer(); const SessionRestoreErrorDialog = sdk.getComponent('views.dialogs.SessionRestoreErrorDialog'); diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index 9865044717..04b3b47e43 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -18,12 +18,15 @@ limitations under the License. 'use strict'; +import Matrix from 'matrix-js-sdk'; + import utils from 'matrix-js-sdk/lib/utils'; import EventTimeline from 'matrix-js-sdk/lib/models/event-timeline'; import EventTimelineSet from 'matrix-js-sdk/lib/models/event-timeline-set'; import createMatrixClient from './utils/createMatrixClient'; import SettingsStore from './settings/SettingsStore'; import MatrixActionCreators from './actions/MatrixActionCreators'; +import {phasedRollOutExpiredForUser} from "./PhasedRollOut"; interface MatrixClientCreds { homeserverUrl: string, @@ -51,6 +54,9 @@ class MatrixClientPeg { this.opts = { initialSyncLimit: 20, }; + // the credentials used to init the current client object. + // used if we tear it down & recreate it with a different store + this._currentClientCreds = null; } /** @@ -79,10 +85,30 @@ class MatrixClientPeg { * Home Server / Identity Server URLs and active credentials */ replaceUsingCreds(creds: MatrixClientCreds) { + this._currentClientCreds = creds; this._createClient(creds); } async start() { + for (const dbType of ['indexeddb', 'memory']) { + try { + const promise = this.matrixClient.store.startup(); + console.log("MatrixClientPeg: waiting for MatrixClient store to initialise"); + await promise; + break; + } catch (err) { + if (dbType === 'indexeddb') { + console.error('Error starting matrixclient store - falling back to memory store', err); + this.matrixClient.store = new Matrix.MatrixInMemoryStore({ + localStorage: global.localStorage, + }); + } else { + console.error('Failed to start memory store!', err); + throw err; + } + } + } + // try to initialise e2e on the new client try { // check that we have a version of the js-sdk which includes initCrypto @@ -99,22 +125,14 @@ class MatrixClientPeg { // the react sdk doesn't work without this, so don't allow opts.pendingEventOrdering = "detached"; - if (SettingsStore.isFeatureEnabled('feature_lazyloading')) { - opts.lazyLoadMembers = true; + const LAZY_LOADING_FEATURE = "feature_lazyloading"; + if (SettingsStore.isFeatureEnabled(LAZY_LOADING_FEATURE)) { + const userId = this.matrixClient.credentials.userId; + if (phasedRollOutExpiredForUser(userId, LAZY_LOADING_FEATURE, Date.now())) { + opts.lazyLoadMembers = true; + } } - try { - const promise = this.matrixClient.store.startup(); - console.log(`MatrixClientPeg: waiting for MatrixClient store to initialise`); - await promise; - } catch (err) { - // log any errors when starting up the database (if one exists) - console.error('Error starting matrixclient store', err); - } - - // regardless of errors, start the client. If we did error out, we'll - // just end up doing a full initial /sync. - // Connect the matrix client to the dispatcher MatrixActionCreators.start(this.matrixClient); @@ -147,7 +165,7 @@ class MatrixClientPeg { return matches[1]; } - _createClient(creds: MatrixClientCreds) { + _createClient(creds: MatrixClientCreds, useIndexedDb) { const opts = { baseUrl: creds.homeserverUrl, idBaseUrl: creds.identityServerUrl, @@ -158,7 +176,7 @@ class MatrixClientPeg { forceTURN: SettingsStore.getValue('webRtcForceTURN', false), }; - this.matrixClient = createMatrixClient(opts, this.indexedDbWorkerScript); + this.matrixClient = createMatrixClient(opts, useIndexedDb); // we're going to add eventlisteners for each matrix event tile, so the // potential number of event listeners is quite high. diff --git a/src/PhasedRollOut.js b/src/PhasedRollOut.js new file mode 100644 index 0000000000..a9029d07e6 --- /dev/null +++ b/src/PhasedRollOut.js @@ -0,0 +1,65 @@ +/* +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 SdkConfig from './SdkConfig'; + +function hashCode(str) { + let hash = 0; + let i; + let chr; + if (str.length === 0) { + return hash; + } + for (i = 0; i < str.length; i++) { + chr = str.charCodeAt(i); + hash = ((hash << 5) - hash) + chr; + hash |= 0; + } + return Math.abs(hash); +} + +export function phasedRollOutExpiredForUser(username, feature, now, rollOutConfig = SdkConfig.get().phasedRollOut) { + if (!rollOutConfig) { + console.log(`no phased rollout configuration, so enabling ${feature}`); + return true; + } + const featureConfig = rollOutConfig[feature]; + if (!featureConfig) { + console.log(`${feature} doesn't have phased rollout configured, so enabling`); + return true; + } + if (!Number.isFinite(featureConfig.offset) || !Number.isFinite(featureConfig.period)) { + console.error(`phased rollout of ${feature} is misconfigured, ` + + `offset and/or period are not numbers, so disabling`, featureConfig); + return false; + } + + const hash = hashCode(username); + //ms -> min, enable users at minute granularity + const bucketRatio = 1000 * 60; + const bucketCount = featureConfig.period / bucketRatio; + const userBucket = hash % bucketCount; + const userMs = userBucket * bucketRatio; + const enableAt = featureConfig.offset + userMs; + const result = now >= enableAt; + const bucketStr = `(bucket ${userBucket}/${bucketCount})`; + if (result) { + console.log(`${feature} enabled for ${username} ${bucketStr}`); + } else { + console.log(`${feature} will be enabled for ${username} in ${Math.ceil((enableAt - now)/1000)}s ${bucketStr}`); + } + return result; +} diff --git a/src/ScalarAuthClient.js b/src/ScalarAuthClient.js index 40467ec580..2038430576 100644 --- a/src/ScalarAuthClient.js +++ b/src/ScalarAuthClient.js @@ -56,12 +56,12 @@ class ScalarAuthClient { // Something went wrong - try to get a new token. console.warn("Registering for new scalar token"); return this.registerForToken(); - }) + }); } } validateToken(token) { - let url = SdkConfig.get().integrations_rest_url + "/account"; + const url = SdkConfig.get().integrations_rest_url + "/account"; return new Promise(function(resolve, reject) { request({ @@ -80,7 +80,7 @@ class ScalarAuthClient { resolve(body.user_id); } }); - }) + }); } registerForToken() { @@ -114,7 +114,7 @@ class ScalarAuthClient { resolve(body.scalar_token); } }); - }) + }); } getScalarPageTitle(url) { @@ -142,7 +142,7 @@ class ScalarAuthClient { resolve(title); } }); - }) + }); } /** diff --git a/src/VectorConferenceHandler.js b/src/VectorConferenceHandler.js index c53a01d464..e839d3f78b 100644 --- a/src/VectorConferenceHandler.js +++ b/src/VectorConferenceHandler.js @@ -17,9 +17,9 @@ limitations under the License. "use strict"; import Promise from 'bluebird'; -var Matrix = require("matrix-js-sdk"); -var Room = Matrix.Room; -var CallHandler = require('./CallHandler'); +const Matrix = require("matrix-js-sdk"); +const Room = Matrix.Room; +const CallHandler = require('./CallHandler'); // FIXME: this is Riot (Vector) specific code, but will be removed shortly when // we switch over to jitsi entirely for video conferencing. @@ -28,8 +28,8 @@ var CallHandler = require('./CallHandler'); // This is bad because it prevents people running their own ASes from being used. // This isn't permanent and will be customisable in the future: see the proposal // at docs/conferencing.md for more info. -var USER_PREFIX = "fs_"; -var DOMAIN = "matrix.org"; +const USER_PREFIX = "fs_"; +const DOMAIN = "matrix.org"; function ConferenceCall(matrixClient, groupChatRoomId) { this.client = matrixClient; @@ -38,14 +38,14 @@ function ConferenceCall(matrixClient, groupChatRoomId) { } ConferenceCall.prototype.setup = function() { - var self = this; + const self = this; return this._joinConferenceUser().then(function() { return self._getConferenceUserRoom(); }).then(function(room) { // return a call for *this* room to be placed. We also tack on // confUserId to speed up lookups (else we'd need to loop every room // looking for a 1:1 room with this conf user ID!) - var call = Matrix.createNewMatrixCall(self.client, room.roomId); + const call = Matrix.createNewMatrixCall(self.client, room.roomId); call.confUserId = self.confUserId; call.groupRoomId = self.groupRoomId; return call; @@ -54,11 +54,11 @@ ConferenceCall.prototype.setup = function() { ConferenceCall.prototype._joinConferenceUser = function() { // Make sure the conference user is in the group chat room - var groupRoom = this.client.getRoom(this.groupRoomId); + const groupRoom = this.client.getRoom(this.groupRoomId); if (!groupRoom) { return Promise.reject("Bad group room ID"); } - var member = groupRoom.getMember(this.confUserId); + const member = groupRoom.getMember(this.confUserId); if (member && member.membership === "join") { return Promise.resolve(); } @@ -67,10 +67,10 @@ ConferenceCall.prototype._joinConferenceUser = function() { ConferenceCall.prototype._getConferenceUserRoom = function() { // Use an existing 1:1 with the conference user; else make one - var rooms = this.client.getRooms(); - var confRoom = null; - for (var i = 0; i < rooms.length; i++) { - var confUser = rooms[i].getMember(this.confUserId); + const rooms = this.client.getRooms(); + let confRoom = null; + for (let i = 0; i < rooms.length; i++) { + const confUser = rooms[i].getMember(this.confUserId); if (confUser && confUser.membership === "join" && rooms[i].getJoinedMemberCount() === 2) { confRoom = rooms[i]; @@ -82,7 +82,7 @@ ConferenceCall.prototype._getConferenceUserRoom = function() { } return this.client.createRoom({ preset: "private_chat", - invite: [this.confUserId] + invite: [this.confUserId], }).then(function(res) { return new Room(res.room_id, null, client.getUserId()); }); @@ -97,9 +97,9 @@ module.exports.isConferenceUser = function(userId) { if (userId.indexOf("@" + USER_PREFIX) !== 0) { return false; } - var base64part = userId.split(":")[0].substring(1 + USER_PREFIX.length); + const base64part = userId.split(":")[0].substring(1 + USER_PREFIX.length); if (base64part) { - var decoded = new Buffer(base64part, "base64").toString(); + const decoded = new Buffer(base64part, "base64").toString(); // ! $STUFF : $STUFF return /^!.+:.+/.test(decoded); } @@ -108,23 +108,23 @@ module.exports.isConferenceUser = function(userId) { module.exports.getConferenceUserIdForRoom = function(roomId) { // abuse browserify's core node Buffer support (strip padding ='s) - var base64RoomId = new Buffer(roomId).toString("base64").replace(/=/g, ""); + const base64RoomId = new Buffer(roomId).toString("base64").replace(/=/g, ""); return "@" + USER_PREFIX + base64RoomId + ":" + DOMAIN; }; module.exports.createNewMatrixCall = function(client, roomId) { - var confCall = new ConferenceCall( - client, roomId + const confCall = new ConferenceCall( + client, roomId, ); return confCall.setup(); }; module.exports.getConferenceCallForRoom = function(roomId) { // search for a conference 1:1 call for this group chat room ID - var activeCall = CallHandler.getAnyActiveCall(); + const activeCall = CallHandler.getAnyActiveCall(); if (activeCall && activeCall.confUserId) { - var thisRoomConfUserId = module.exports.getConferenceUserIdForRoom( - roomId + const thisRoomConfUserId = module.exports.getConferenceUserIdForRoom( + roomId, ); if (thisRoomConfUserId === activeCall.confUserId) { return activeCall; diff --git a/src/Velociraptor.js b/src/Velociraptor.js index 6a4666305c..ad51f66ae3 100644 --- a/src/Velociraptor.js +++ b/src/Velociraptor.js @@ -68,7 +68,9 @@ module.exports = React.createClass({ if (oldNode && oldNode.style.visibility == 'hidden' && c.props.style.visibility == 'visible') { oldNode.style.visibility = c.props.style.visibility; } - self.children[c.key] = old; + // clone the old element with the props (and children) of the new element + // so prop updates are still received by the children. + self.children[c.key] = React.cloneElement(old, c.props, c.props.children); } else { // new element. If we have a startStyle, use that as the style and go through // the enter animations diff --git a/src/autocomplete/CommandProvider.js b/src/autocomplete/CommandProvider.js index a35a31966a..609a8fa9a1 100644 --- a/src/autocomplete/CommandProvider.js +++ b/src/autocomplete/CommandProvider.js @@ -20,7 +20,7 @@ limitations under the License. import React from 'react'; import {_t} from '../languageHandler'; import AutocompleteProvider from './AutocompleteProvider'; -import FuzzyMatcher from './FuzzyMatcher'; +import QueryMatcher from './QueryMatcher'; import {TextualCompletion} from './Components'; import type {Completion, SelectionRange} from "./Autocompleter"; import {CommandMap} from '../SlashCommands'; @@ -32,7 +32,7 @@ const COMMAND_RE = /(^\/\w*)(?: .*)?/g; export default class CommandProvider extends AutocompleteProvider { constructor() { super(COMMAND_RE); - this.matcher = new FuzzyMatcher(COMMANDS, { + this.matcher = new QueryMatcher(COMMANDS, { keys: ['command', 'args', 'description'], }); } diff --git a/src/autocomplete/CommunityProvider.js b/src/autocomplete/CommunityProvider.js index 6bcf1a02fd..d164fab46a 100644 --- a/src/autocomplete/CommunityProvider.js +++ b/src/autocomplete/CommunityProvider.js @@ -19,7 +19,7 @@ import React from 'react'; import { _t } from '../languageHandler'; import AutocompleteProvider from './AutocompleteProvider'; import MatrixClientPeg from '../MatrixClientPeg'; -import FuzzyMatcher from './FuzzyMatcher'; +import QueryMatcher from './QueryMatcher'; import {PillCompletion} from './Components'; import sdk from '../index'; import _sortBy from 'lodash/sortBy'; @@ -41,7 +41,7 @@ function score(query, space) { export default class CommunityProvider extends AutocompleteProvider { constructor() { super(COMMUNITY_REGEX); - this.matcher = new FuzzyMatcher([], { + this.matcher = new QueryMatcher([], { keys: ['groupId', 'name', 'shortDescription'], }); } diff --git a/src/autocomplete/EmojiProvider.js b/src/autocomplete/EmojiProvider.js index 719550d59f..8c6495101f 100644 --- a/src/autocomplete/EmojiProvider.js +++ b/src/autocomplete/EmojiProvider.js @@ -20,7 +20,7 @@ import React from 'react'; import { _t } from '../languageHandler'; import AutocompleteProvider from './AutocompleteProvider'; import {shortnameToUnicode, asciiRegexp, unicodeRegexp} from 'emojione'; -import FuzzyMatcher from './FuzzyMatcher'; +import QueryMatcher from './QueryMatcher'; import sdk from '../index'; import {PillCompletion} from './Components'; import type {Completion, SelectionRange} from './Autocompleter'; @@ -84,12 +84,12 @@ function score(query, space) { export default class EmojiProvider extends AutocompleteProvider { constructor() { super(EMOJI_REGEX); - this.matcher = new FuzzyMatcher(EMOJI_SHORTNAMES, { + this.matcher = new QueryMatcher(EMOJI_SHORTNAMES, { keys: ['aliases_ascii', 'shortname', 'aliases'], // For matching against ascii equivalents shouldMatchWordsOnly: false, }); - this.nameMatcher = new FuzzyMatcher(EMOJI_SHORTNAMES, { + this.nameMatcher = new QueryMatcher(EMOJI_SHORTNAMES, { keys: ['name'], // For removing punctuation shouldMatchWordsOnly: true, diff --git a/src/autocomplete/FuzzyMatcher.js b/src/autocomplete/FuzzyMatcher.js deleted file mode 100644 index 1aa0782c22..0000000000 --- a/src/autocomplete/FuzzyMatcher.js +++ /dev/null @@ -1,107 +0,0 @@ -/* -Copyright 2017 Aviral Dasgupta - -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 Levenshtein from 'liblevenshtein'; -//import _at from 'lodash/at'; -//import _flatMap from 'lodash/flatMap'; -//import _sortBy from 'lodash/sortBy'; -//import _sortedUniq from 'lodash/sortedUniq'; -//import _keys from 'lodash/keys'; -// -//class KeyMap { -// keys: Array; -// objectMap: {[String]: Array}; -// priorityMap: {[String]: number} -//} -// -//const DEFAULT_RESULT_COUNT = 10; -//const DEFAULT_DISTANCE = 5; - -// FIXME Until Fuzzy matching works better, we use prefix matching. - -import PrefixMatcher from './QueryMatcher'; -export default PrefixMatcher; - -//class FuzzyMatcher { // eslint-disable-line no-unused-vars -// /** -// * @param {object[]} objects the objects to perform a match on -// * @param {string[]} keys an array of keys within each object to match on -// * Keys can refer to object properties by name and as in JavaScript (for nested properties) -// * -// * To use, simply presort objects by required criteria, run through this function and create a FuzzyMatcher with the -// * resulting KeyMap. -// * -// * TODO: Handle arrays and objects (Fuse did this, RoomProvider uses it) -// * @return {KeyMap} -// */ -// static valuesToKeyMap(objects: Array, keys: Array): KeyMap { -// const keyMap = new KeyMap(); -// const map = {}; -// const priorities = {}; -// -// objects.forEach((object, i) => { -// const keyValues = _at(object, keys); -// console.log(object, keyValues, keys); -// for (const keyValue of keyValues) { -// if (!map.hasOwnProperty(keyValue)) { -// map[keyValue] = []; -// } -// map[keyValue].push(object); -// } -// priorities[object] = i; -// }); -// -// keyMap.objectMap = map; -// keyMap.priorityMap = priorities; -// keyMap.keys = _sortBy(_keys(map), [(value) => priorities[value]]); -// return keyMap; -// } -// -// constructor(objects: Array, options: {[Object]: Object} = {}) { -// this.options = options; -// this.keys = options.keys; -// this.setObjects(objects); -// } -// -// setObjects(objects: Array) { -// this.keyMap = FuzzyMatcher.valuesToKeyMap(objects, this.keys); -// console.log(this.keyMap.keys); -// this.matcher = new Levenshtein.Builder() -// .dictionary(this.keyMap.keys, true) -// .algorithm('transposition') -// .sort_candidates(false) -// .case_insensitive_sort(true) -// .include_distance(true) -// .maximum_candidates(this.options.resultCount || DEFAULT_RESULT_COUNT) // result count 0 doesn't make much sense -// .build(); -// } -// -// match(query: String): Array { -// const candidates = this.matcher.transduce(query, this.options.distance || DEFAULT_DISTANCE); -// // TODO FIXME This is hideous. Clean up when possible. -// const val = _sortedUniq(_sortBy(_flatMap(candidates, (candidate) => { -// return this.keyMap.objectMap[candidate[0]].map((value) => { -// return { -// distance: candidate[1], -// ...value, -// }; -// }); -// }), -// [(candidate) => candidate.distance, (candidate) => this.keyMap.priorityMap[candidate]])); -// console.log(val); -// return val; -// } -//} diff --git a/src/autocomplete/QueryMatcher.js b/src/autocomplete/QueryMatcher.js index 9d4d4d0598..a28d3003cf 100644 --- a/src/autocomplete/QueryMatcher.js +++ b/src/autocomplete/QueryMatcher.js @@ -2,6 +2,7 @@ /* Copyright 2017 Aviral Dasgupta Copyright 2018 Michael Telatynski <7t3chguy@gmail.com> +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. @@ -20,99 +21,99 @@ import _at from 'lodash/at'; import _flatMap from 'lodash/flatMap'; import _sortBy from 'lodash/sortBy'; import _uniq from 'lodash/uniq'; -import _keys from 'lodash/keys'; - -class KeyMap { - keys: Array; - objectMap: {[String]: Array}; - priorityMap = new Map(); -} function stripDiacritics(str: string): string { return str.normalize('NFD').replace(/[\u0300-\u036f]/g, ''); } +/** + * Simple search matcher that matches any results with the query string anywhere + * in the search string. Returns matches in the order the query string appears + * in the search key, earliest first, then in the order the items appeared in + * the source array. + * + * @param {Object[]} objects Initial list of objects. Equivalent to calling + * setObjects() after construction + * @param {Object} options Options object + * @param {string[]} options.keys List of keys to use as indexes on the objects + * @param {function[]} options.funcs List of functions that when called with the + * object as an arg will return a string to use as an index + */ export default class QueryMatcher { - /** - * @param {object[]} objects the objects to perform a match on - * @param {string[]} keys an array of keys within each object to match on - * Keys can refer to object properties by name and as in JavaScript (for nested properties) - * - * To use, simply presort objects by required criteria, run through this function and create a QueryMatcher with the - * resulting KeyMap. - * - * TODO: Handle arrays and objects (Fuse did this, RoomProvider uses it) - * @return {KeyMap} - */ - static valuesToKeyMap(objects: Array, keys: Array): KeyMap { - const keyMap = new KeyMap(); - const map = {}; - - objects.forEach((object, i) => { - const keyValues = _at(object, keys); - for (const keyValue of keyValues) { - const key = stripDiacritics(keyValue).toLowerCase(); - if (!map.hasOwnProperty(key)) { - map[key] = []; - } - map[key].push(object); - } - keyMap.priorityMap.set(object, i); - }); - - keyMap.objectMap = map; - keyMap.keys = _keys(map); - return keyMap; - } - constructor(objects: Array, options: {[Object]: Object} = {}) { - this.options = options; - this.keys = options.keys; + this._options = options; + this._keys = options.keys; + this._funcs = options.funcs || []; + this.setObjects(objects); // By default, we remove any non-alphanumeric characters ([^A-Za-z0-9_]) from the // query and the value being queried before matching - if (this.options.shouldMatchWordsOnly === undefined) { - this.options.shouldMatchWordsOnly = true; + if (this._options.shouldMatchWordsOnly === undefined) { + this._options.shouldMatchWordsOnly = true; } // By default, match anywhere in the string being searched. If enabled, only return // matches that are prefixed with the query. - if (this.options.shouldMatchPrefix === undefined) { - this.options.shouldMatchPrefix = false; + if (this._options.shouldMatchPrefix === undefined) { + this._options.shouldMatchPrefix = false; } } setObjects(objects: Array) { - this.keyMap = QueryMatcher.valuesToKeyMap(objects, this.keys); + this._items = new Map(); + + for (const object of objects) { + const keyValues = _at(object, this._keys); + + for (const f of this._funcs) { + keyValues.push(f(object)); + } + + for (const keyValue of keyValues) { + const key = stripDiacritics(keyValue).toLowerCase(); + if (!this._items.has(key)) { + this._items.set(key, []); + } + this._items.get(key).push(object); + } + } } match(query: String): Array { query = stripDiacritics(query).toLowerCase(); - if (this.options.shouldMatchWordsOnly) { + if (this._options.shouldMatchWordsOnly) { query = query.replace(/[^\w]/g, ''); } if (query.length === 0) { return []; } const results = []; - this.keyMap.keys.forEach((key) => { + // Iterate through the map & check each key. + // ES6 Map iteration order is defined to be insertion order, so results + // here will come out in the order they were put in. + for (const key of this._items.keys()) { let resultKey = key; - if (this.options.shouldMatchWordsOnly) { + if (this._options.shouldMatchWordsOnly) { resultKey = resultKey.replace(/[^\w]/g, ''); } const index = resultKey.indexOf(query); - if (index !== -1 && (!this.options.shouldMatchPrefix || index === 0)) { + if (index !== -1 && (!this._options.shouldMatchPrefix || index === 0)) { results.push({key, index}); } + } + + // Sort them by where the query appeared in the search key + // lodash sortBy is a stable sort, so results where the query + // appeared in the same place will retain their order with + // respect to each other. + const sortedResults = _sortBy(results, (candidate) => { + return candidate.index; }); - return _uniq(_flatMap(_sortBy(results, (candidate) => { - return candidate.index; - }).map((candidate) => { - // return an array of objects (those given to setObjects) that have the given - // key as a property. - return this.keyMap.objectMap[candidate.key]; - }))); + // Now map the keys to the result objects. Each result object is a list, so + // flatMap will flatten those lists out into a single list. Also remove any + // duplicates. + return _uniq(_flatMap(sortedResults, (candidate) => this._items.get(candidate.key))); } } diff --git a/src/autocomplete/RoomProvider.js b/src/autocomplete/RoomProvider.js index 38e2ab8373..483506557f 100644 --- a/src/autocomplete/RoomProvider.js +++ b/src/autocomplete/RoomProvider.js @@ -21,7 +21,7 @@ import React from 'react'; import { _t } from '../languageHandler'; import AutocompleteProvider from './AutocompleteProvider'; import MatrixClientPeg from '../MatrixClientPeg'; -import FuzzyMatcher from './FuzzyMatcher'; +import QueryMatcher from './QueryMatcher'; import {PillCompletion} from './Components'; import {getDisplayAliasForRoom} from '../Rooms'; import sdk from '../index'; @@ -43,7 +43,7 @@ function score(query, space) { export default class RoomProvider extends AutocompleteProvider { constructor() { super(ROOM_REGEX); - this.matcher = new FuzzyMatcher([], { + this.matcher = new QueryMatcher([], { keys: ['displayedAlias', 'name'], }); } diff --git a/src/autocomplete/UserProvider.js b/src/autocomplete/UserProvider.js index e9cbf7945b..2eae053d72 100644 --- a/src/autocomplete/UserProvider.js +++ b/src/autocomplete/UserProvider.js @@ -23,7 +23,7 @@ import { _t } from '../languageHandler'; import AutocompleteProvider from './AutocompleteProvider'; import {PillCompletion} from './Components'; import sdk from '../index'; -import FuzzyMatcher from './FuzzyMatcher'; +import QueryMatcher from './QueryMatcher'; import _sortBy from 'lodash/sortBy'; import MatrixClientPeg from '../MatrixClientPeg'; @@ -44,8 +44,9 @@ export default class UserProvider extends AutocompleteProvider { constructor(room) { super(USER_REGEX, FORCED_USER_REGEX); this.room = room; - this.matcher = new FuzzyMatcher([], { - keys: ['name', 'userId'], + this.matcher = new QueryMatcher([], { + keys: ['name'], + funcs: [obj => obj.userId.slice(1)], // index by user id minus the leading '@' shouldMatchPrefix: true, shouldMatchWordsOnly: false, }); @@ -104,7 +105,9 @@ export default class UserProvider extends AutocompleteProvider { const fullMatch = command[0]; // Don't search if the query is a single "@" if (fullMatch && fullMatch !== '@') { - completions = this.matcher.match(fullMatch).map((user) => { + // Don't include the '@' in our search query - it's only used as a way to trigger completion + const query = fullMatch.startsWith('@') ? fullMatch.substring(1) : fullMatch; + completions = this.matcher.match(query).map((user) => { const displayName = (user.name || user.userId || ''); return { // Length of completion should equal length of text in decorator. draft-js diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index d104019a01..5d23194702 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -746,13 +746,37 @@ export default React.createClass({ }); }, + _leaveGroupWarnings: function() { + const warnings = []; + + if (this.state.isUserPrivileged) { + warnings.push(( + + { " " /* Whitespace, otherwise the sentences get smashed together */ } + { _t("You are an administrator of this community. You will not be " + + "able to rejoin without an invite from another administrator.") } + + )); + } + + return warnings; + }, + + _onLeaveClick: function() { const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + const warnings = this._leaveGroupWarnings(); + Modal.createTrackedDialog('Leave Group', '', QuestionDialog, { title: _t("Leave Community"), - description: _t("Leave %(groupName)s?", {groupName: this.props.groupId}), + description: ( + + { _t("Leave %(groupName)s?", {groupName: this.props.groupId}) } + { warnings } + + ), button: _t("Leave"), - danger: true, + danger: this.state.isUserPrivileged, onFinished: async (confirmed) => { if (!confirmed) return; diff --git a/src/components/structures/LeftPanel.js b/src/components/structures/LeftPanel.js index ebe5d7f507..72d640bcac 100644 --- a/src/components/structures/LeftPanel.js +++ b/src/components/structures/LeftPanel.js @@ -181,14 +181,8 @@ var LeftPanel = React.createClass({ const BottomLeftMenu = sdk.getComponent('structures.BottomLeftMenu'); const CallPreview = sdk.getComponent('voip.CallPreview'); - let topBox; - if (this.context.matrixClient.isGuest()) { - const LoginBox = sdk.getComponent('structures.LoginBox'); - topBox = ; - } else { - const SearchBox = sdk.getComponent('structures.SearchBox'); - topBox = ; - } + const SearchBox = sdk.getComponent('structures.SearchBox'); + const topBox = ; const classes = classNames( "mx_LeftPanel", diff --git a/src/components/structures/LoginBox.js b/src/components/structures/LoginBox.js index a9ea1f95c6..a2269706ee 100644 --- a/src/components/structures/LoginBox.js +++ b/src/components/structures/LoginBox.js @@ -1,5 +1,6 @@ /* 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. @@ -16,31 +17,15 @@ limitations under the License. 'use strict'; -var React = require('react'); +const React = require('react'); import { _t } from '../../languageHandler'; -var sdk = require('../../index') -var dis = require('../../dispatcher'); -var rate_limited_func = require('../../ratelimitedfunc'); -var AccessibleButton = require('../../components/views/elements/AccessibleButton'); +const dis = require('../../dispatcher'); +const AccessibleButton = require('../../components/views/elements/AccessibleButton'); module.exports = React.createClass({ displayName: 'LoginBox', propTypes: { - collapsed: React.PropTypes.bool, - }, - - onToggleCollapse: function(show) { - if (show) { - dis.dispatch({ - action: 'show_left_panel', - }); - } - else { - dis.dispatch({ - action: 'hide_left_panel', - }); - } }, onLoginClick: function() { @@ -52,41 +37,20 @@ module.exports = React.createClass({ }, render: function() { - var TintableSvg = sdk.getComponent('elements.TintableSvg'); - - var toggleCollapse; - if (this.props.collapsed) { - toggleCollapse = - - + const loginButton = ( +
+ + { _t("Login") } - } - else { - toggleCollapse = - - + + { _t("Register") } - } +
+ ); - var loginButton; - if (!this.props.collapsed) { - loginButton = ( -
- - { _t("Login") } - - - { _t("Register") } - -
- ); - } - - var self = this; return ( -
+
{ loginButton } - { toggleCollapse }
); } diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 9d7198d5f5..54d461d550 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1034,6 +1034,7 @@ export default React.createClass({ { warnings } ), + button: _t("Leave"), onFinished: (shouldLeave) => { if (shouldLeave) { const d = MatrixClientPeg.get().leave(roomId); @@ -1266,6 +1267,9 @@ export default React.createClass({ dis.dispatch({action: 'sync_state', prevState, state}); if (state === "ERROR" || state === "RECONNECTING") { + if (data.error instanceof Matrix.InvalidStoreError) { + Lifecycle.handleInvalidStoreError(data.error); + } self.setState({syncError: data.error || true}); } else if (self.state.syncError) { self.setState({syncError: null}); @@ -1401,6 +1405,11 @@ export default React.createClass({ break; } }); + + // 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 + const colorScheme = SettingsStore.getValue("roomColor"); + Tinter.tint(colorScheme.primary_color, colorScheme.secondary_color); }, /** @@ -1743,10 +1752,14 @@ export default React.createClass({ } if (this.state.view === VIEWS.LOGGED_IN) { + // store errors stop the client syncing and require user intervention, so we'll + // be showing a dialog. Don't show anything else. + const isStoreError = this.state.syncError && this.state.syncError instanceof Matrix.InvalidStoreError; + // `ready` and `view==LOGGED_IN` may be set before `page_type` (because the // latter is set via the dispatcher). If we don't yet have a `page_type`, // keep showing the spinner for now. - if (this.state.ready && this.state.page_type) { + if (this.state.ready && this.state.page_type && !isStoreError) { /* for now, we stuff the entirety of our props and state into the LoggedInView. * we should go through and figure out what we actually need to pass down, as well * as using something like redux to avoid having a billion bits of state kicking around. @@ -1768,7 +1781,7 @@ export default React.createClass({ // we think we are logged in, but are still waiting for the /sync to complete const Spinner = sdk.getComponent('elements.Spinner'); let errorBox; - if (this.state.syncError) { + if (this.state.syncError && !isStoreError) { errorBox =
{messageForSyncError(this.state.syncError)}
; diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index f5fa2ceabf..bbaea617f4 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -542,7 +542,7 @@ module.exports = React.createClass({ }, // get a list of read receipts that should be shown next to this event - // Receipts are objects which have a 'roomMember' and 'ts'. + // Receipts are objects which have a 'userId', 'roomMember' and 'ts'. _getReadReceiptsForEvent: function(event) { const myUserId = MatrixClientPeg.get().credentials.userId; @@ -560,10 +560,8 @@ module.exports = React.createClass({ return; // ignore ignored users } const member = room.getMember(r.userId); - if (!member) { - return; // ignore unknown user IDs - } receipts.push({ + userId: r.userId, roomMember: member, ts: r.data ? r.data.ts : 0, }); diff --git a/src/components/structures/RightPanel.js b/src/components/structures/RightPanel.js index 86870718e8..9017447a34 100644 --- a/src/components/structures/RightPanel.js +++ b/src/components/structures/RightPanel.js @@ -51,6 +51,7 @@ class HeaderButton extends React.Component { return @@ -345,11 +346,11 @@ module.exports = React.createClass({ // being put in the RoomHeader or GroupView header, so only show the minimise // button on these 2 screens or you won't be able to re-expand the panel. headerButtons.push( -
- -
, + +
, ); } diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js index fec59aadd5..c6a6e1e2d3 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -66,6 +66,10 @@ module.exports = React.createClass({ // result in "X, Y, Z and 100 others are typing." whoIsTypingLimit: PropTypes.number, + // true if the room is being peeked at. This affects components that shouldn't + // logically be shown when peeking, such as a prompt to invite people to a room. + isPeeking: PropTypes.bool, + // callback for when the user clicks on the 'resend all' button in the // 'unsent messages' bar onResendAllClick: PropTypes.func, @@ -223,14 +227,15 @@ module.exports = React.createClass({ ); } + const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); if (!this.props.atEndOfLiveTimeline) { return ( -
{_t("Scroll -
+ ); } @@ -385,7 +390,7 @@ module.exports = React.createClass({ } return
- {_t("Warning")} +
{ title } @@ -456,7 +461,7 @@ module.exports = React.createClass({ } // If you're alone in the room, and have sent a message, suggest to invite someone - if (this.props.sentMessageAndIsAlone) { + if (this.props.sentMessageAndIsAlone && !this.props.isPeeking) { return (
{ _t("There's no one else here! Would you like to invite others " + @@ -485,7 +490,9 @@ module.exports = React.createClass({
{ indicator }
- { content } +
+ { content } +
); }, diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index e9e46a2ff6..e20ac54006 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -195,6 +195,8 @@ module.exports = React.createClass({ editingRoomSettings: RoomViewStore.isEditingSettings(), }; + if (this.state.editingRoomSettings && !newState.editingRoomSettings) dis.dispatch({action: 'focus_composer'}); + // Temporary logging to diagnose https://github.com/vector-im/riot-web/issues/4307 console.log( 'RVS update:', @@ -676,8 +678,8 @@ module.exports = React.createClass({ if (!room) return; console.log("Tinter.tint from updateTint"); - const color_scheme = SettingsStore.getValue("roomColor", room.roomId); - Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color); + const colorScheme = SettingsStore.getValue("roomColor", room.roomId); + Tinter.tint(colorScheme.primary_color, colorScheme.secondary_color); }, onAccountData: function(event) { @@ -692,10 +694,10 @@ module.exports = React.createClass({ if (room.roomId == this.state.roomId) { const type = event.getType(); if (type === "org.matrix.room.color_scheme") { - const color_scheme = event.getContent(); + const colorScheme = event.getContent(); // XXX: we should validate the event console.log("Tinter.tint from onRoomAccountData"); - Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color); + Tinter.tint(colorScheme.primary_color, colorScheme.secondary_color); } else if (type === "org.matrix.room.preview_urls" || type === "im.vector.web.settings") { // non-e2ee url previews are stored in legacy event type `org.matrix.room.preview_urls` this._updatePreviewUrlVisibility(room); @@ -1512,6 +1514,7 @@ module.exports = React.createClass({ canPreview={false} error={this.state.roomLoadError} roomAlias={roomAlias} spinner={this.state.joining} + spinnerState="joining" inviterName={inviterName} invitedEmail={invitedEmail} room={this.state.room} @@ -1556,6 +1559,7 @@ module.exports = React.createClass({ inviterName={inviterName} canPreview={false} spinner={this.state.joining} + spinnerState="joining" room={this.state.room} />
@@ -1593,6 +1597,7 @@ module.exports = React.createClass({ atEndOfLiveTimeline={this.state.atEndOfLiveTimeline} sentMessageAndIsAlone={this.state.isAlone} hasActiveCall={inCall} + isPeeking={myMembership !== "join"} onInviteClick={this.onInviteButtonClick} onStopWarningClick={this.onStopAloneWarningClick} onScrollToBottomClick={this.jumpToLiveTimeline} @@ -1642,6 +1647,7 @@ module.exports = React.createClass({ onForgetClick={this.onForgetClick} onRejectClick={this.onRejectThreepidInviteButtonClicked} spinner={this.state.joining} + spinnerState="joining" inviterName={inviterName} invitedEmail={invitedEmail} canPreview={this.state.canPeek} @@ -1667,7 +1673,7 @@ module.exports = React.createClass({ let messageComposer, searchInfo; const canSpeak = ( // joined and not showing search results - myMembership == 'join' && !this.state.searchResults + myMembership === 'join' && !this.state.searchResults ); if (canSpeak) { messageComposer = @@ -1681,6 +1687,11 @@ module.exports = React.createClass({ />; } + if (MatrixClientPeg.get().isGuest()) { + const LoginBox = sdk.getComponent('structures.LoginBox'); + messageComposer = ; + } + // TODO: Why aren't we storing the term/scope/count in this format // in this.state if this is what RoomHeader desires? if (this.state.searchResults) { diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 0b43b4a34a..eb8f512cbb 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -82,6 +82,7 @@ const SIMPLE_SETTINGS = [ { id: "TagPanel.disableTagPanel" }, { id: "enableWidgetScreenshots" }, { id: "RoomSubList.showEmpty" }, + { id: "showDeveloperTools" }, ]; // These settings must be defined in SettingsStore @@ -842,9 +843,9 @@ module.exports = React.createClass({
{ _t('Privacy is important to us, so we don\'t collect any personal' + ' or identifiable data for our analytics.') } -
+ { _t('Learn more about how we use analytics.') } -
+ { ANALYTICS_SETTINGS.map( this._renderDeviceSetting ) }
; @@ -1076,9 +1077,9 @@ module.exports = React.createClass({ _renderWebRtcDeviceSettings: function() { if (this.state.mediaDevices === false) { return ( -

+ { _t('Missing Media Permissions, click here to request.') } -

+ ); } else if (!this.state.mediaDevices) return; @@ -1245,7 +1246,7 @@ module.exports = React.createClass({ />
- {_t("Remove")}
@@ -1270,7 +1271,7 @@ module.exports = React.createClass({ onValueChanged={this._onAddEmailEditFinished} />
- {_t("Add")} +
); @@ -1339,13 +1340,13 @@ module.exports = React.createClass({
-
+ {_t("Remove -
+
@@ -1406,11 +1407,11 @@ module.exports = React.createClass({
{ _t('Access Token:') + ' ' } - <{ _t("click to reveal") }> - +
{ _t("Homeserver is") } { MatrixClientPeg.get().getHomeserverUrl() } diff --git a/src/components/views/avatars/MemberAvatar.js b/src/components/views/avatars/MemberAvatar.js index a4fe5e280f..d191368b17 100644 --- a/src/components/views/avatars/MemberAvatar.js +++ b/src/components/views/avatars/MemberAvatar.js @@ -26,7 +26,8 @@ module.exports = React.createClass({ displayName: 'MemberAvatar', propTypes: { - member: PropTypes.object.isRequired, + member: PropTypes.object, + fallbackUserId: PropTypes.string, width: PropTypes.number, height: PropTypes.number, resizeMethod: PropTypes.string, @@ -55,23 +56,30 @@ module.exports = React.createClass({ }, _getState: function(props) { - if (!props.member) { - console.error("MemberAvatar called somehow with null member"); + if (props.member) { + return { + name: props.member.name, + title: props.title || props.member.userId, + imageUrl: Avatar.avatarUrlForMember(props.member, + props.width, + props.height, + props.resizeMethod), + }; + } else if (props.fallbackUserId) { + return { + name: props.fallbackUserId, + title: props.fallbackUserId, + }; + } else { + console.error("MemberAvatar called somehow with null member or fallbackUserId"); } - return { - name: props.member.name, - title: props.title || props.member.userId, - imageUrl: Avatar.avatarUrlForMember(props.member, - props.width, - props.height, - props.resizeMethod), - }; }, render: function() { const BaseAvatar = sdk.getComponent("avatars.BaseAvatar"); - let {member, onClick, viewUserOnClick, ...otherProps} = this.props; + let {member, fallbackUserId, onClick, viewUserOnClick, ...otherProps} = this.props; + let userId = member ? member.userId : fallbackUserId; if (viewUserOnClick) { onClick = () => { @@ -84,7 +92,7 @@ module.exports = React.createClass({ return ( + idName={userId} url={this.state.imageUrl} onClick={onClick} /> ); }, }); diff --git a/src/components/views/dialogs/DevtoolsDialog.js b/src/components/views/dialogs/DevtoolsDialog.js index ea198461c5..22ee44f81a 100644 --- a/src/components/views/dialogs/DevtoolsDialog.js +++ b/src/components/views/dialogs/DevtoolsDialog.js @@ -625,7 +625,7 @@ export default class DevtoolsDialog extends React.Component { let body; if (this.state.mode) { - body =
+ body =
{ this.state.mode.getLabel() }
Room ID: { this.props.roomId }
@@ -634,7 +634,7 @@ export default class DevtoolsDialog extends React.Component { } else { const classes = "mx_DevTools_RoomStateExplorer_button"; body =
-
+
{ _t('Toolbox') }
Room ID: { this.props.roomId }
diff --git a/src/components/views/dialogs/LazyLoadingDisabledDialog.js b/src/components/views/dialogs/LazyLoadingDisabledDialog.js new file mode 100644 index 0000000000..d128d8dedd --- /dev/null +++ b/src/components/views/dialogs/LazyLoadingDisabledDialog.js @@ -0,0 +1,39 @@ +/* +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 description1 = + _t("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.", + {host: props.host}); + const description2 = _t("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."); + + return (

{description1}

{description2}

} + button={_t("Clear cache and resync")} + onFinished={props.onFinished} + />); +}; diff --git a/src/components/views/dialogs/LazyLoadingResyncDialog.js b/src/components/views/dialogs/LazyLoadingResyncDialog.js index 0e6259c84d..125d2a9928 100644 --- a/src/components/views/dialogs/LazyLoadingResyncDialog.js +++ b/src/components/views/dialogs/LazyLoadingResyncDialog.js @@ -20,7 +20,8 @@ import { _t } from '../../../languageHandler'; export default (props) => { const description = - _t("Riot now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!"); + _t("Riot now uses 3-5x less memory, by only loading information about other users" + + " when needed. Please wait whilst we resynchronise with the server!"); return (
{ this.props.description } diff --git a/src/components/views/elements/AppWarning.js b/src/components/views/elements/AppWarning.js index f4015ae5b7..38d3476d53 100644 --- a/src/components/views/elements/AppWarning.js +++ b/src/components/views/elements/AppWarning.js @@ -1,12 +1,11 @@ import React from 'react'; // eslint-disable-line no-unused-vars import PropTypes from 'prop-types'; -import { _t } from '../../../languageHandler'; const AppWarning = (props) => { return (
- {_t('Warning!')} +
{ props.errorMsg } diff --git a/src/components/views/elements/DialogButtons.js b/src/components/views/elements/DialogButtons.js index e8e1c78e71..70355b56b7 100644 --- a/src/components/views/elements/DialogButtons.js +++ b/src/components/views/elements/DialogButtons.js @@ -74,6 +74,8 @@ module.exports = React.createClass({ } return (
+ { cancelButton } + { this.props.children } - { this.props.children } - { cancelButton }
); }, diff --git a/src/components/views/globals/CookieBar.js b/src/components/views/globals/CookieBar.js index a63a163dd1..deb1cbffa8 100644 --- a/src/components/views/globals/CookieBar.js +++ b/src/components/views/globals/CookieBar.js @@ -51,7 +51,7 @@ export default class CookieBar extends React.Component { const toolbarClasses = "mx_MatrixToolbar"; return (
- Warning +
{ this.props.policyUrl ? _t( "Please help improve Riot.im by sending anonymous usage data. " + @@ -95,7 +95,7 @@ export default class CookieBar extends React.Component { { _t("Yes, I want to help!") } - + {_t('Close')}
); diff --git a/src/components/views/globals/MatrixToolbar.js b/src/components/views/globals/MatrixToolbar.js index f85a30bcdf..45feede5a0 100644 --- a/src/components/views/globals/MatrixToolbar.js +++ b/src/components/views/globals/MatrixToolbar.js @@ -35,11 +35,11 @@ module.exports = React.createClass({ render: function() { return (
- Warning +
{ _t('You are not receiving desktop notifications') } { _t('Enable them now') }
- + {_t('Close')}/
); }, diff --git a/src/components/views/globals/NewVersionBar.js b/src/components/views/globals/NewVersionBar.js index 527df5f864..1bd0bde526 100644 --- a/src/components/views/globals/NewVersionBar.js +++ b/src/components/views/globals/NewVersionBar.js @@ -96,7 +96,7 @@ export default React.createClass({ } return (
- Warning +
{_t("A new version of Riot is available.")}
diff --git a/src/components/views/globals/PasswordNagBar.js b/src/components/views/globals/PasswordNagBar.js index 4233363b95..5e3da3ad6d 100644 --- a/src/components/views/globals/PasswordNagBar.js +++ b/src/components/views/globals/PasswordNagBar.js @@ -34,7 +34,7 @@ export default React.createClass({ src="img/warning.svg" width="24" height="23" - alt="Warning" + alt="" />
{ _t( diff --git a/src/components/views/globals/UpdateCheckBar.js b/src/components/views/globals/UpdateCheckBar.js index 53801311d8..e499ddab31 100644 --- a/src/components/views/globals/UpdateCheckBar.js +++ b/src/components/views/globals/UpdateCheckBar.js @@ -71,9 +71,9 @@ export default React.createClass({ let image; if (doneStatuses.includes(this.props.status)) { - image = {warning}/; + image = ; } else { - image = {message}/; + image = ; } return ( @@ -83,7 +83,7 @@ export default React.createClass({ {message}
- + {_t('Close')}/
); diff --git a/src/components/views/groups/GroupMemberList.js b/src/components/views/groups/GroupMemberList.js index faf172083f..38c679a5b5 100644 --- a/src/components/views/groups/GroupMemberList.js +++ b/src/components/views/groups/GroupMemberList.js @@ -165,7 +165,7 @@ export default React.createClass({ return (
{ inputBox } - + { joined } { invited } diff --git a/src/components/views/login/InteractiveAuthEntryComponents.js b/src/components/views/login/InteractiveAuthEntryComponents.js index bfdfbb63bf..6e0e5d538a 100644 --- a/src/components/views/login/InteractiveAuthEntryComponents.js +++ b/src/components/views/login/InteractiveAuthEntryComponents.js @@ -22,6 +22,7 @@ import classnames from 'classnames'; import sdk from '../../../index'; import { _t } from '../../../languageHandler'; +import SettingsStore from "../../../settings/SettingsStore"; /* This file contains a collection of components which are used by the * InteractiveAuth to prompt the user to enter the information needed @@ -209,6 +210,125 @@ export const RecaptchaAuthEntry = React.createClass({ }, }); +export const TermsAuthEntry = React.createClass({ + displayName: 'TermsAuthEntry', + + statics: { + LOGIN_TYPE: "m.login.terms", + }, + + propTypes: { + submitAuthDict: PropTypes.func.isRequired, + stageParams: PropTypes.object.isRequired, + errorText: PropTypes.string, + busy: PropTypes.bool, + }, + + componentWillMount: function() { + // example stageParams: + // + // { + // "policies": { + // "privacy_policy": { + // "version": "1.0", + // "en": { + // "name": "Privacy Policy", + // "url": "https://example.org/privacy-1.0-en.html", + // }, + // "fr": { + // "name": "Politique de confidentialité", + // "url": "https://example.org/privacy-1.0-fr.html", + // }, + // }, + // "other_policy": { ... }, + // } + // } + + const allPolicies = this.props.stageParams.policies || {}; + const prefLang = SettingsStore.getValue("language"); + const initToggles = {}; + const pickedPolicies = []; + for (const policyId of Object.keys(allPolicies)) { + const policy = allPolicies[policyId]; + + // Pick a language based on the user's language, falling back to english, + // and finally to the first language available. If there's still no policy + // available then the homeserver isn't respecting the spec. + let langPolicy = policy[prefLang]; + if (!langPolicy) langPolicy = policy["en"]; + if (!langPolicy) { + // last resort + const firstLang = Object.keys(policy).find(e => e !== "version"); + langPolicy = policy[firstLang]; + } + if (!langPolicy) throw new Error("Failed to find a policy to show the user"); + + initToggles[policyId] = false; + + langPolicy.id = policyId; + pickedPolicies.push(langPolicy); + } + + this.setState({ + "toggledPolicies": initToggles, + "policies": pickedPolicies, + }); + }, + + _trySubmit: function(policyId) { + const newToggles = {}; + let allChecked = true; + for (const policy of this.state.policies) { + let checked = this.state.toggledPolicies[policy.id]; + if (policy.id === policyId) checked = !checked; + + newToggles[policy.id] = checked; + allChecked = allChecked && checked; + } + + this.setState({"toggledPolicies": newToggles}); + if (allChecked) this.props.submitAuthDict({type: TermsAuthEntry.LOGIN_TYPE}); + }, + + render: function() { + if (this.props.busy) { + const Loader = sdk.getComponent("elements.Spinner"); + return ; + } + + let checkboxes = []; + let allChecked = true; + for (const policy of this.state.policies) { + const checked = this.state.toggledPolicies[policy.id]; + allChecked = allChecked && checked; + + checkboxes.push( + + ); + } + + let errorSection; + if (this.props.errorText) { + errorSection = ( +
+ { this.props.errorText } +
+ ); + } + + return ( +
+

{_t("Please review and accept the policies of this homeserver:")}

+ { checkboxes } + { errorSection } +
+ ); + }, +}); + export const EmailIdentityAuthEntry = React.createClass({ displayName: 'EmailIdentityAuthEntry', @@ -496,6 +616,7 @@ const AuthEntryComponents = [ RecaptchaAuthEntry, EmailIdentityAuthEntry, MsisdnAuthEntry, + TermsAuthEntry, ]; export function getEntryComponentForLoginType(loginType) { diff --git a/src/components/views/messages/SenderProfile.js b/src/components/views/messages/SenderProfile.js index be40db50a1..0f767675e2 100644 --- a/src/components/views/messages/SenderProfile.js +++ b/src/components/views/messages/SenderProfile.js @@ -96,7 +96,7 @@ export default React.createClass({ render() { const EmojiText = sdk.getComponent('elements.EmojiText'); const {mxEvent} = this.props; - let name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender(); + const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender(); const {msgtype} = mxEvent.getContent(); if (msgtype === 'm.emote') { diff --git a/src/components/views/room_settings/AliasSettings.js b/src/components/views/room_settings/AliasSettings.js index f9bf52cd24..92a025f10c 100644 --- a/src/components/views/room_settings/AliasSettings.js +++ b/src/components/views/room_settings/AliasSettings.js @@ -220,8 +220,9 @@ module.exports = React.createClass({ let canonical_alias_section; if (this.props.canSetCanonicalAlias) { let found = false; + const canonicalValue = this.state.canonicalAlias || ""; canonical_alias_section = ( - { Object.keys(self.state.domainToAliases).map((domain, i) => { diff --git a/src/components/views/rooms/AppsDrawer.js b/src/components/views/rooms/AppsDrawer.js index e6fe445b45..77d912ef2a 100644 --- a/src/components/views/rooms/AppsDrawer.js +++ b/src/components/views/rooms/AppsDrawer.js @@ -30,6 +30,7 @@ import ScalarMessaging from '../../../ScalarMessaging'; import { _t } from '../../../languageHandler'; import WidgetUtils from '../../../utils/WidgetUtils'; import WidgetEchoStore from "../../../stores/WidgetEchoStore"; +import AccessibleButton from '../elements/AccessibleButton'; // The maximum number of widgets that can be added in a room const MAX_WIDGETS = 2; @@ -193,17 +194,15 @@ module.exports = React.createClass({ if (this.props.showApps && this._canUserModify() ) { - addWidget =
[+] { _t('Add a widget') } -
; + ; } let spinner; diff --git a/src/components/views/rooms/Autocomplete.js b/src/components/views/rooms/Autocomplete.js index ee6cc66d2d..757204f0c8 100644 --- a/src/components/views/rooms/Autocomplete.js +++ b/src/components/views/rooms/Autocomplete.js @@ -114,7 +114,7 @@ export default class Autocomplete extends React.Component { processQuery(query, selection) { return this.autocompleter.getCompletions( - query, selection, this.state.forceComplete + query, selection, this.state.forceComplete, ).then((completions) => { // Only ever process the completions for the most recent query being processed if (query !== this.queryRequested) { diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 8c58863249..53c73c8f84 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -277,7 +277,11 @@ module.exports = withMatrixClient(React.createClass({ return false; } for (let j = 0; j < rA.length; j++) { - if (rA[j].roomMember.userId !== rB[j].roomMember.userId) { + if (rA[j].userId !== rB[j].userId) { + return false; + } + // one has a member set and the other doesn't? + if (rA[j].roomMember !== rB[j].roomMember) { return false; } } @@ -359,7 +363,7 @@ module.exports = withMatrixClient(React.createClass({ // else set it proportional to index left = (hidden ? MAX_READ_AVATARS - 1 : i) * -receiptOffset; - const userId = receipt.roomMember.userId; + const userId = receipt.userId; let readReceiptInfo; if (this.props.readReceiptMap) { @@ -373,6 +377,7 @@ module.exports = withMatrixClient(React.createClass({ // add to the start so the most recent is on the end (ie. ends up rightmost) avatars.unshift(
); - } + }, }); diff --git a/src/components/views/rooms/RoomPreviewBar.js b/src/components/views/rooms/RoomPreviewBar.js index 5ec19d185e..9d3dbe5217 100644 --- a/src/components/views/rooms/RoomPreviewBar.js +++ b/src/components/views/rooms/RoomPreviewBar.js @@ -44,9 +44,13 @@ module.exports = React.createClass({ error: PropTypes.object, canPreview: PropTypes.bool, - spinner: PropTypes.bool, room: PropTypes.object, + // When a spinner is present, a spinnerState can be specified to indicate the + // purpose of the spinner. + spinner: PropTypes.bool, + spinnerState: PropTypes.oneOf(["joining"]), + // The alias that was used to access this room, if appropriate // If given, this will be how the room is referred to (eg. // in error messages). @@ -93,7 +97,12 @@ module.exports = React.createClass({ if (this.props.spinner || this.state.busy) { const Spinner = sdk.getComponent("elements.Spinner"); + let spinnerIntro = ""; + if (this.props.spinnerState === "joining") { + spinnerIntro = _t("Joining room..."); + } return (
+

{ spinnerIntro }

); } diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index 46869c1773..b69938a117 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -590,6 +590,11 @@ module.exports = React.createClass({ } }, + _openDevtools: function() { + const DevtoolsDialog = sdk.getComponent('dialogs.DevtoolsDialog'); + Modal.createDialog(DevtoolsDialog, {roomId: this.props.room.roomId}); + }, + _renderEncryptionSection: function() { const SettingsFlag = sdk.getComponent("elements.SettingsFlag"); @@ -942,6 +947,11 @@ module.exports = React.createClass({ ; } + const devtoolsButton = SettingsStore.getValue("showDeveloperTools") ? + ( + { _t("Open Devtools") } + ) : null; + return (
@@ -1055,6 +1065,7 @@ module.exports = React.createClass({ { _t('Internal room ID: ') } { this.props.room.roomId }
{ _t('Room version number: ') } { this.props.room.getVersion() }
{ roomUpgradeButton } + { devtoolsButton }
); diff --git a/src/components/views/rooms/SearchBar.js b/src/components/views/rooms/SearchBar.js index a196c5b78d..05fc661c1c 100644 --- a/src/components/views/rooms/SearchBar.js +++ b/src/components/views/rooms/SearchBar.js @@ -16,11 +16,11 @@ limitations under the License. 'use strict'; -var React = require('react'); -var MatrixClientPeg = require('../../../MatrixClientPeg'); -var sdk = require('../../../index'); -var classNames = require('classnames'); -var AccessibleButton = require('../../../components/views/elements/AccessibleButton'); +const React = require('react'); +const MatrixClientPeg = require('../../../MatrixClientPeg'); +const sdk = require('../../../index'); +const classNames = require('classnames'); +const AccessibleButton = require('../../../components/views/elements/AccessibleButton'); import { _t } from '../../../languageHandler'; module.exports = React.createClass({ @@ -28,7 +28,7 @@ module.exports = React.createClass({ getInitialState: function() { return ({ - scope: 'Room' + scope: 'Room', }); }, @@ -54,18 +54,18 @@ module.exports = React.createClass({ }, render: function() { - var searchButtonClasses = classNames({ mx_SearchBar_searchButton : true, mx_SearchBar_searching: this.props.searchInProgress }); - var thisRoomClasses = classNames({ mx_SearchBar_button : true, mx_SearchBar_unselected : this.state.scope !== 'Room' }); - var allRoomsClasses = classNames({ mx_SearchBar_button : true, mx_SearchBar_unselected : this.state.scope !== 'All' }); + const searchButtonClasses = classNames({ mx_SearchBar_searchButton: true, mx_SearchBar_searching: this.props.searchInProgress }); + const thisRoomClasses = classNames({ mx_SearchBar_button: true, mx_SearchBar_unselected: this.state.scope !== 'Room' }); + const allRoomsClasses = classNames({ mx_SearchBar_button: true, mx_SearchBar_unselected: this.state.scope !== 'All' }); return ( -
- - {_t("Search")}/ +
+ + {_t("Search")} {_t("This Room")} {_t("All Rooms")}
); - } + }, }); diff --git a/src/components/views/rooms/Stickerpicker.js b/src/components/views/rooms/Stickerpicker.js index 841cfb9b03..40b1768282 100644 --- a/src/components/views/rooms/Stickerpicker.js +++ b/src/components/views/rooms/Stickerpicker.js @@ -151,8 +151,8 @@ export default class Stickerpicker extends React.Component {

{ _t("You don't currently have any stickerpacks enabled") }

-

Add some now

- {_t('Add +

{ _t("Add some now") }

+
); } @@ -344,7 +344,7 @@ export default class Stickerpicker extends React.Component { if (this.state.showStickers) { // Show hide-stickers button stickersButton = -
-
; + ; } else { // Show show-stickers button stickersButton = -
-
; + ; } return
{stickersButton} diff --git a/src/components/views/settings/DevicesPanel.js b/src/components/views/settings/DevicesPanel.js index f0fec2cf63..25850819bd 100644 --- a/src/components/views/settings/DevicesPanel.js +++ b/src/components/views/settings/DevicesPanel.js @@ -164,6 +164,7 @@ export default class DevicesPanel extends React.Component { render() { const Spinner = sdk.getComponent("elements.Spinner"); + const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); if (this.state.deviceLoadError !== undefined) { const classes = classNames(this.props.className, "error"); @@ -185,9 +186,9 @@ export default class DevicesPanel extends React.Component { const deleteButton = this.state.deleting ? : -
+ { _t("Delete %(count)s devices", {count: this.state.selectedDevices.length}) } -
; + ; const classes = classNames(this.props.className, "mx_DevicesPanel"); return ( diff --git a/src/components/views/settings/IntegrationsManager.js b/src/components/views/settings/IntegrationsManager.js index 29ae4af93d..a517771f1d 100644 --- a/src/components/views/settings/IntegrationsManager.js +++ b/src/components/views/settings/IntegrationsManager.js @@ -16,10 +16,10 @@ limitations under the License. 'use strict'; -var React = require('react'); -var sdk = require('../../../index'); -var MatrixClientPeg = require('../../../MatrixClientPeg'); -var dis = require('../../../dispatcher'); +const React = require('react'); +const sdk = require('../../../index'); +const MatrixClientPeg = require('../../../MatrixClientPeg'); +const dis = require('../../../dispatcher'); module.exports = React.createClass({ displayName: 'IntegrationsManager', @@ -59,5 +59,5 @@ module.exports = React.createClass({ return ( ); - } + }, }); diff --git a/src/components/views/settings/Notifications.js b/src/components/views/settings/Notifications.js index 39774778e1..ea727a03b5 100644 --- a/src/components/views/settings/Notifications.js +++ b/src/components/views/settings/Notifications.js @@ -26,7 +26,7 @@ import { NotificationUtils, VectorPushRulesDefinitions, PushRuleVectorState, - ContentRules + ContentRules, } from '../../../notifications'; // TODO: this "view" component still has far too much application logic in it, @@ -47,7 +47,7 @@ const LEGACY_RULES = { "im.vector.rule.room_message": ".m.rule.message", "im.vector.rule.invite_for_me": ".m.rule.invite_for_me", "im.vector.rule.call": ".m.rule.call", - "im.vector.rule.notices": ".m.rule.suppress_notices" + "im.vector.rule.notices": ".m.rule.suppress_notices", }; function portLegacyActions(actions) { @@ -67,7 +67,7 @@ module.exports = React.createClass({ phases: { LOADING: "LOADING", // The component is loading or sending data to the hs DISPLAY: "DISPLAY", // The component is ready and display data - ERROR: "ERROR" // There was an error + ERROR: "ERROR", // There was an error }, propTypes: { @@ -79,7 +79,7 @@ module.exports = React.createClass({ getDefaultProps: function() { return { - threepids: [] + threepids: [], }; }, @@ -90,10 +90,10 @@ module.exports = React.createClass({ vectorPushRules: [], // HS default push rules displayed in Vector UI vectorContentRules: { // Keyword push rules displayed in Vector UI vectorState: PushRuleVectorState.ON, - rules: [] + rules: [], }, externalPushRules: [], // Push rules (except content rule) that have been defined outside Vector UI - externalContentRules: [] // Keyword push rules that have been defined outside Vector UI + externalContentRules: [], // Keyword push rules that have been defined outside Vector UI }; }, @@ -104,7 +104,7 @@ module.exports = React.createClass({ onEnableNotificationsChange: function(event) { const self = this; this.setState({ - phase: this.phases.LOADING + phase: this.phases.LOADING, }); MatrixClientPeg.get().setPushRuleEnabled('global', self.state.masterPushRule.kind, self.state.masterPushRule.rule_id, !event.target.checked).done(function() { @@ -145,7 +145,7 @@ module.exports = React.createClass({ onEnableEmailNotificationsChange: function(address, event) { let emailPusherPromise; if (event.target.checked) { - const data = {} + const data = {}; data['brand'] = this.props.brand || 'Riot'; emailPusherPromise = UserSettingsStore.addEmailPusher(address, data); } else { @@ -170,9 +170,8 @@ module.exports = React.createClass({ const newPushRuleVectorState = event.target.className.split("-")[1]; if ("_keywords" === vectorRuleId) { - this._setKeywordsPushRuleVectorState(newPushRuleVectorState) - } - else { + this._setKeywordsPushRuleVectorState(newPushRuleVectorState); + } else { const rule = this.getRule(vectorRuleId); if (rule) { this._setPushRuleVectorState(rule, newPushRuleVectorState); @@ -185,7 +184,7 @@ module.exports = React.createClass({ // Compute the keywords list to display let keywords = []; - for (let i in this.state.vectorContentRules.rules) { + for (const i in this.state.vectorContentRules.rules) { const rule = this.state.vectorContentRules.rules[i]; keywords.push(rule.pattern); } @@ -195,8 +194,7 @@ module.exports = React.createClass({ keywords.sort(); keywords = keywords.join(", "); - } - else { + } else { keywords = ""; } @@ -207,29 +205,28 @@ module.exports = React.createClass({ button: _t('OK'), value: keywords, onFinished: function onFinished(should_leave, newValue) { - if (should_leave && newValue !== keywords) { let newKeywords = newValue.split(','); - for (let i in newKeywords) { + for (const i in newKeywords) { newKeywords[i] = newKeywords[i].trim(); } // Remove duplicates and empty - newKeywords = newKeywords.reduce(function(array, keyword){ + newKeywords = newKeywords.reduce(function(array, keyword) { if (keyword !== "" && array.indexOf(keyword) < 0) { array.push(keyword); } return array; - },[]); + }, []); self._setKeywords(newKeywords); } - } + }, }); }, getRule: function(vectorRuleId) { - for (let i in this.state.vectorPushRules) { + for (const i in this.state.vectorPushRules) { const rule = this.state.vectorPushRules[i]; if (rule.vectorRuleId === vectorRuleId) { return rule; @@ -239,9 +236,8 @@ module.exports = React.createClass({ _setPushRuleVectorState: function(rule, newPushRuleVectorState) { if (rule && rule.vectorState !== newPushRuleVectorState) { - this.setState({ - phase: this.phases.LOADING + phase: this.phases.LOADING, }); const self = this; @@ -255,8 +251,7 @@ module.exports = React.createClass({ if (!actions) { // The new state corresponds to disabling the rule. deferreds.push(cli.setPushRuleEnabled('global', rule.rule.kind, rule.rule.rule_id, false)); - } - else { + } else { // The new state corresponds to enabling the rule and setting specific actions deferreds.push(this._updatePushRuleActions(rule.rule, actions, true)); } @@ -270,7 +265,7 @@ module.exports = React.createClass({ Modal.createTrackedDialog('Failed to change settings', '', ErrorDialog, { title: _t('Failed to change settings'), description: ((error && error.message) ? error.message : _t('Operation failed')), - onFinished: self._refreshFromServer + onFinished: self._refreshFromServer, }); }); } @@ -287,12 +282,12 @@ module.exports = React.createClass({ const cli = MatrixClientPeg.get(); this.setState({ - phase: this.phases.LOADING + phase: this.phases.LOADING, }); // Update all rules in self.state.vectorContentRules const deferreds = []; - for (let i in this.state.vectorContentRules.rules) { + for (const i in this.state.vectorContentRules.rules) { const rule = this.state.vectorContentRules.rules[i]; let enabled, actions; @@ -326,8 +321,7 @@ module.exports = React.createClass({ // Note that the workaround in _updatePushRuleActions will automatically // enable the rule deferreds.push(this._updatePushRuleActions(rule, actions, enabled)); - } - else if (enabled != undefined) { + } else if (enabled != undefined) { deferreds.push(cli.setPushRuleEnabled('global', rule.kind, rule.rule_id, enabled)); } } @@ -340,14 +334,14 @@ module.exports = React.createClass({ Modal.createTrackedDialog('Can\'t update user notifcation settings', '', ErrorDialog, { title: _t('Can\'t update user notification settings'), description: ((error && error.message) ? error.message : _t('Operation failed')), - onFinished: self._refreshFromServer + onFinished: self._refreshFromServer, }); }); }, _setKeywords: function(newKeywords) { this.setState({ - phase: this.phases.LOADING + phase: this.phases.LOADING, }); const self = this; @@ -356,7 +350,7 @@ module.exports = React.createClass({ // Remove per-word push rules of keywords that are no more in the list const vectorContentRulesPatterns = []; - for (let i in self.state.vectorContentRules.rules) { + for (const i in self.state.vectorContentRules.rules) { const rule = self.state.vectorContentRules.rules[i]; vectorContentRulesPatterns.push(rule.pattern); @@ -368,7 +362,7 @@ module.exports = React.createClass({ // If the keyword is part of `externalContentRules`, remove the rule // before recreating it in the right Vector path - for (let i in self.state.externalContentRules) { + for (const i in self.state.externalContentRules) { const rule = self.state.externalContentRules[i]; if (newKeywords.indexOf(rule.pattern) >= 0) { @@ -382,9 +376,9 @@ module.exports = React.createClass({ Modal.createTrackedDialog('Failed to update keywords', '', ErrorDialog, { title: _t('Failed to update keywords'), description: ((error && error.message) ? error.message : _t('Operation failed')), - onFinished: self._refreshFromServer + onFinished: self._refreshFromServer, }); - } + }; // Then, add the new ones Promise.all(removeDeferreds).done(function(resps) { @@ -398,14 +392,13 @@ module.exports = React.createClass({ // Thus, this new rule will join the 'vectorContentRules' set. if (self.state.vectorContentRules.rules.length) { pushRuleVectorStateKind = PushRuleVectorState.contentRuleVectorStateKind(self.state.vectorContentRules.rules[0]); - } - else { + } else { // ON is default - pushRuleVectorStateKind = PushRuleVectorState.ON; + pushRuleVectorStateKind = PushRuleVectorState.ON; } } - for (let i in newKeywords) { + for (const i in newKeywords) { const keyword = newKeywords[i]; if (vectorContentRulesPatterns.indexOf(keyword) < 0) { @@ -413,13 +406,12 @@ module.exports = React.createClass({ deferreds.push(cli.addPushRule ('global', 'content', keyword, { actions: PushRuleVectorState.actionsFor(pushRuleVectorStateKind), - pattern: keyword + pattern: keyword, })); - } - else { + } else { deferreds.push(self._addDisabledPushRule('global', 'content', keyword, { actions: PushRuleVectorState.actionsFor(pushRuleVectorStateKind), - pattern: keyword + pattern: keyword, })); } } @@ -435,7 +427,7 @@ module.exports = React.createClass({ _addDisabledPushRule: function(scope, kind, ruleId, body) { const cli = MatrixClientPeg.get(); return cli.addPushRule(scope, kind, ruleId, body).then(() => - cli.setPushRuleEnabled(scope, kind, ruleId, false) + cli.setPushRuleEnabled(scope, kind, ruleId, false), ); }, @@ -446,7 +438,7 @@ module.exports = React.createClass({ const needsUpdate = []; const cli = MatrixClientPeg.get(); - for (let kind in rulesets.global) { + for (const kind in rulesets.global) { const ruleset = rulesets.global[kind]; for (let i = 0; i < ruleset.length; ++i) { const rule = ruleset[i]; @@ -454,9 +446,9 @@ module.exports = React.createClass({ console.log("Porting legacy rule", rule); needsUpdate.push( function(kind, rule) { return cli.setPushRuleActions( - 'global', kind, LEGACY_RULES[rule.rule_id], portLegacyActions(rule.actions) + 'global', kind, LEGACY_RULES[rule.rule_id], portLegacyActions(rule.actions), ).then(() => - cli.deletePushRule('global', kind, rule.rule_id) + cli.deletePushRule('global', kind, rule.rule_id), ).catch( (e) => { console.warn(`Error when porting legacy rule: ${e}`); }); @@ -469,7 +461,7 @@ module.exports = React.createClass({ // If some of the rules need to be ported then wait for the porting // to happen and then fetch the rules again. return Promise.all(needsUpdate).then(() => - cli.getPushRules() + cli.getPushRules(), ); } else { // Otherwise return the rules that we already have. @@ -480,7 +472,6 @@ module.exports = React.createClass({ _refreshFromServer: function() { const self = this; const pushRulesPromise = MatrixClientPeg.get().getPushRules().then(self._portRulesToNewAPI).then(function(rulesets) { - /// XXX seriously? wtf is this? MatrixClientPeg.get().pushRules = rulesets; @@ -497,7 +488,7 @@ module.exports = React.createClass({ '.m.rule.invite_for_me': 'vector', //'.m.rule.member_event': 'vector', '.m.rule.call': 'vector', - '.m.rule.suppress_notices': 'vector' + '.m.rule.suppress_notices': 'vector', // Others go to others }; @@ -505,7 +496,7 @@ module.exports = React.createClass({ // HS default rules const defaultRules = {master: [], vector: {}, others: []}; - for (let kind in rulesets.global) { + for (const kind in rulesets.global) { for (let i = 0; i < Object.keys(rulesets.global[kind]).length; ++i) { const r = rulesets.global[kind][i]; const cat = rule_categories[r.rule_id]; @@ -514,11 +505,9 @@ module.exports = React.createClass({ if (r.rule_id[0] === '.') { if (cat === 'vector') { defaultRules.vector[r.rule_id] = r; - } - else if (cat === 'master') { + } else if (cat === 'master') { defaultRules.master.push(r); - } - else { + } else { defaultRules['others'].push(r); } } @@ -551,9 +540,9 @@ module.exports = React.createClass({ '.m.rule.invite_for_me', //'im.vector.rule.member_event', '.m.rule.call', - '.m.rule.suppress_notices' + '.m.rule.suppress_notices', ]; - for (let i in vectorRuleIds) { + for (const i in vectorRuleIds) { const vectorRuleId = vectorRuleIds[i]; if (vectorRuleId === '_keywords') { @@ -562,20 +551,19 @@ module.exports = React.createClass({ // it corresponds to all content push rules (stored in self.state.vectorContentRule) self.state.vectorPushRules.push({ "vectorRuleId": "_keywords", - "description" : ( + "description": ( { _t('Messages containing keywords', {}, { 'span': (sub) => - {sub} + {sub}, }, )} ), - "vectorState": self.state.vectorContentRules.vectorState + "vectorState": self.state.vectorContentRules.vectorState, }); - } - else { + } else { const ruleDefinition = VectorPushRulesDefinitions[vectorRuleId]; const rule = defaultRules.vector[vectorRuleId]; @@ -585,7 +573,7 @@ module.exports = React.createClass({ self.state.vectorPushRules.push({ "vectorRuleId": vectorRuleId, - "description" : _t(ruleDefinition.description), // Text from VectorPushRulesDefinitions.js + "description": _t(ruleDefinition.description), // Text from VectorPushRulesDefinitions.js "rule": rule, "vectorState": vectorState, }); @@ -604,7 +592,7 @@ module.exports = React.createClass({ '.m.rule.fallback': _t('Notify me for anything else'), }; - for (let i in defaultRules.others) { + for (const i in defaultRules.others) { const rule = defaultRules.others[i]; const ruleDescription = otherRulesDescriptions[rule.rule_id]; @@ -622,12 +610,12 @@ module.exports = React.createClass({ Promise.all([pushRulesPromise, pushersPromise]).then(function() { self.setState({ - phase: self.phases.DISPLAY + phase: self.phases.DISPLAY, }); }, function(error) { console.error(error); self.setState({ - phase: self.phases.ERROR + phase: self.phases.ERROR, }); }).finally(() => { // actually explicitly update our state having been deep-manipulating it @@ -645,12 +633,12 @@ module.exports = React.createClass({ const cli = MatrixClientPeg.get(); return cli.setPushRuleActions( - 'global', rule.kind, rule.rule_id, actions + 'global', rule.kind, rule.rule_id, actions, ).then( function() { // Then, if requested, enabled or disabled the rule if (undefined != enabled) { return cli.setPushRuleEnabled( - 'global', rule.kind, rule.rule_id, enabled + 'global', rule.kind, rule.rule_id, enabled, ); } }); @@ -689,7 +677,7 @@ module.exports = React.createClass({ renderNotifRulesTableRows: function() { const rows = []; - for (let i in this.state.vectorPushRules) { + for (const i in this.state.vectorPushRules) { const rule = this.state.vectorPushRules[i]; //console.log("rendering: " + rule.description + ", " + rule.vectorRuleId + ", " + rule.vectorState); rows.push(this.renderNotifRulesTableRow(rule.description, rule.vectorRuleId, rule.vectorState)); @@ -769,20 +757,20 @@ module.exports = React.createClass({ // This only supports the first email address in your profile for now emailNotificationsRow = this.emailNotificationsRow( emailThreepids[0].address, - `${_t('Enable email notifications')} (${emailThreepids[0].address})` + `${_t('Enable email notifications')} (${emailThreepids[0].address})`, ); } // Build external push rules const externalRules = []; - for (let i in this.state.externalPushRules) { + for (const i in this.state.externalPushRules) { const rule = this.state.externalPushRules[i]; externalRules.push(
  • { _t(rule.description) }
  • ); } // Show keywords not displayed by the vector UI as a single external push rule let externalKeywords = []; - for (let i in this.state.externalContentRules) { + for (const i in this.state.externalContentRules) { const rule = this.state.externalContentRules[i]; externalKeywords.push(rule.pattern); } @@ -793,7 +781,7 @@ module.exports = React.createClass({ let devicesSection; if (this.state.pushers === undefined) { - devicesSection =
    { _t('Unable to fetch notification target list') }
    + devicesSection =
    { _t('Unable to fetch notification target list') }
    ; } else if (this.state.pushers.length == 0) { devicesSection = null; } else { @@ -824,7 +812,7 @@ module.exports = React.createClass({ advancedSettings = (

    { _t('Advanced notification settings') }

    - { _t('There are advanced notifications which are not shown here') }.
    + { _t('There are advanced notifications which are not shown here') }.
    { _t('You might have configured them in a client other than Riot. You cannot tune them in Riot but they still apply') }.
      { externalRules } @@ -915,5 +903,5 @@ module.exports = React.createClass({
    ); - } + }, }); diff --git a/src/components/views/voip/CallView.js b/src/components/views/voip/CallView.js index 47e8ae22db..1a84d23f9b 100644 --- a/src/components/views/voip/CallView.js +++ b/src/components/views/voip/CallView.js @@ -125,14 +125,15 @@ module.exports = React.createClass({ render: function() { const VideoView = sdk.getComponent('voip.VideoView'); + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); let voice; if (this.state.call && this.state.call.type === "voice" && this.props.showVoice) { const callRoom = MatrixClientPeg.get().getRoom(this.state.call.roomId); voice = ( -
    + { _t("Active call (%(roomName)s)", {roomName: callRoom.name}) } -
    + ); } diff --git a/src/i18n/strings/bg.json b/src/i18n/strings/bg.json index 9e667a30aa..b49384891c 100644 --- a/src/i18n/strings/bg.json +++ b/src/i18n/strings/bg.json @@ -1276,5 +1276,20 @@ "%(senderName)s removed %(count)s %(removedAddresses)s as addresses for this room.|other": "%(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 премахна основния адрес на тази стая." + "%(senderName)s removed the main address for this room.": "%(senderName)s премахна основния адрес на тази стая.", + "Before submitting logs, you must
    create a GitHub issue to describe your problem.": "Преди да изпратите логове, трябва да отворите доклад за проблем в Github.", + "What GitHub issue are these logs for?": "За кой Github проблем са тези логове?", + "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 вече използва 3-5 пъти по-малко памет, като зарежда информация за потребители само когато е нужна. Моля, изчакайте докато ресинхронизираме със сървъра!", + "Updating Riot": "Обновяване на Riot", + "

    HTML for your community's page

    \r\n

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

    \r\n

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

    \r\n": "

    HTML за страницата на Вашата общност

    \n

    \n Използвайте дългото описание за да въведете нови членове в общността,\n или да разпространите важно връзки\n

    \n

    \n Можете дори да използвате 'img' тагове\n

    \n", + "Submit Debug Logs": "Изпратете логове за диагностика", + "An email address is required to register on this homeserver.": "Необходим е имейл адрес за регистрация на този сървър.", + "A phone number is required to register on this homeserver.": "Необходим е телефонен номер за регистрация на този сървър.", + "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.": "Преди сте използвали Riot на %(host)s с включено постепенно зареждане на членове. В тази версия, тази настройка е изключена. Понеже локалният кеш не е съвместим при тези две настройки, Riot трябва да синхронизира акаунта Ви наново.", + "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.": "Ако другата версия на Riot все още е отворена в друг таб, моля затворете я. Използването на Riot на един адрес във версии с постепенно и без постепенно зареждане ще причини проблеми.", + "Incompatible local cache": "Несъвместим локален кеш", + "Clear cache and resync": "Изчисти кеша и ресинхронизирай", + "Please accept all of the policies": "Моля, приемете всички политики", + "Please review and accept the policies of this homeserver:": "Моля, прегледайте и приемете политиките на този сървър:", + "Add some now": "Добави сега" } diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index 88ee8fe75e..5cb856b651 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -1221,8 +1221,8 @@ "numbered-list": "Liste mit Nummern", "Failed to remove widget": "Widget konnte nicht entfernt werden", "An error ocurred whilst trying to remove the widget from the room": "Ein Fehler trat auf, während versucht wurde das Widget aus diesem Raum zu entfernen", - "inline-code": "Quellcode in der Zeile", - "block-quote": "Quellcode im Block", + "inline-code": "Quellcode", + "block-quote": "Zitat", "This homeserver has hit its Monthly Active User limit": "Dieser Heimserver hat sein Limit für monatlich aktive Nutzer erreicht", "Please contact your service administrator to continue using this service.": "Bitte kontaktiere deinen Administrator um diesen Dienst weiter zu nutzen.", "System Alerts": "System-Benachrichtigung", @@ -1277,5 +1277,19 @@ "%(senderName)s removed %(count)s %(removedAddresses)s as addresses for this room.|other": "%(senderName)s entfernte %(removedAddresses)s als Adressen von diesem Raum.", "%(senderName)s set the main address for this room to %(address)s.": "%(senderName)s setzte die Hauptadresse zu diesem Raum auf %(address)s.", "%(senderName)s removed the main address for this room.": "%(senderName)s entfernte die Hauptadresse von diesem Raum.", - "%(senderName)s added %(addedAddresses)s and removed %(removedAddresses)s as addresses for this room.": "%(senderName)s fügte %(addedAddresses)s hinzu und entfernte %(removedAddresses)s als Adressen von diesem Raum." + "%(senderName)s added %(addedAddresses)s and removed %(removedAddresses)s as addresses for this room.": "%(senderName)s fügte %(addedAddresses)s hinzu und entfernte %(removedAddresses)s als Adressen von diesem Raum.", + "Before submitting logs, you must create a GitHub issue to describe your problem.": "Bevor du Log-Dateien übermittelst, musst du ein GitHub-Issue erstellen um dein Problem zu beschreiben.", + "What GitHub issue are these logs for?": "Für welches GitHub-Issue sind diese Logs?", + "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 benutzt nun 3-5x weniger Arbeitsspeicher, indem Informationen über andere Nutzer erst bei Bedarf geladen werden. Bitte warte, während die Daten erneut mit dem Server abgeglichen werden!", + "Updating Riot": "Aktualisiere Riot", + "

    HTML for your community's page

    \r\n

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

    \r\n

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

    \r\n": "

    HTML for deine Community-Seite

    \n

    \n Nutze die lange Beschreibung um die Community neuen Mitgliedern vorzustellen oder um\n einige wichtige Links zu teilen\n

    \n

    \n Du kannst auch 'img'-Tags verwenden\n

    \n", + "Submit Debug Logs": "Fehlerprotokoll senden", + "An email address is required to register on this homeserver.": "Zur Registrierung auf diesem Heimserver ist eine E-Mail-Adresse erforderlich.", + "A phone number is required to register on this homeserver.": "Zur Registrierung auf diesem Heimserver ist eine Telefon-Nummer erforderlich.", + "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.": "Du hast zuvor Riot auf %(host)s ohne verzögertem Laden von Mitgliedern genutzt. In dieser Version war das verzögerte Laden deaktiviert. Da die lokal zwischengespeicherten Daten zwischen diesen Einstellungen nicht kompatibel ist, muss Riot dein Konto neu synchronisieren.", + "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.": "Wenn Riot mit der alten Version in einem anderen Tab geöffnet ist, schließe dies bitte, da das parallele Nutzen von Riot auf demselben Host mit aktivierten und deaktivierten verzögertem Laden, Probleme verursachen wird.", + "Incompatible local cache": "Inkompatibler lokaler Zwischenspeicher", + "Clear cache and resync": "Zwischenspeicher löschen und erneut synchronisieren", + "Please accept all of the policies": "Bitte akzeptiere alle Bedingungen", + "Please review and accept the policies of this homeserver:": "Bitte sieh dir alle Bedingungen dieses Heimservers an und akzeptiere sie:" } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index a434edbcbf..3da5ff15a2 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -43,10 +43,6 @@ "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", @@ -86,7 +82,6 @@ "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", "Riot does not have permission to send you notifications - please check your browser settings": "Riot does not have permission to send you notifications - please check your browser settings", "Riot was not given permission to send notifications - please try again": "Riot was not given permission to send notifications - please try again", "Unable to enable Notifications": "Unable to enable Notifications", @@ -211,6 +206,11 @@ "%(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.", @@ -350,31 +350,6 @@ "Off": "Off", "On": "On", "Noisy": "Noisy", - "Invalid alias format": "Invalid alias format", - "'%(alias)s' is not a valid format for an alias": "'%(alias)s' is not a valid format for an alias", - "Invalid address format": "Invalid address format", - "'%(alias)s' is not a valid format for an address": "'%(alias)s' is not a valid format for an address", - "not specified": "not specified", - "not set": "not set", - "Remote addresses for this room:": "Remote addresses for this room:", - "Addresses": "Addresses", - "The main address for this room is": "The main address for this room is", - "Local addresses for this room:": "Local addresses for this room:", - "This room has no local addresses": "This room has no local addresses", - "New address (e.g. #foo:%(localDomain)s)": "New address (e.g. #foo:%(localDomain)s)", - "Invalid community ID": "Invalid community ID", - "'%(groupId)s' is not a valid community ID": "'%(groupId)s' is not a valid community ID", - "Flair": "Flair", - "Showing flair for these communities:": "Showing flair for these communities:", - "This room is not showing flair for any communities": "This room is not showing flair for any communities", - "New community ID (e.g. +foo:%(localDomain)s)": "New community ID (e.g. +foo:%(localDomain)s)", - "You have enabled URL previews by default.": "You have enabled URL previews by default.", - "You have disabled URL previews by default.": "You have disabled URL previews by default.", - "URL previews are enabled by default for participants in this room.": "URL previews are enabled by default for participants in this room.", - "URL previews are disabled by default for participants in this room.": "URL previews are disabled by default for participants 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.": "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.", "Cannot add any more widgets": "Cannot add any more widgets", "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.", "Add a widget": "Add a widget", @@ -438,6 +413,7 @@ "Make Moderator": "Make Moderator", "Admin Tools": "Admin Tools", "Level:": "Level:", + "Close": "Close", "and %(count)s others...|other": "and %(count)s others...", "and %(count)s others...|one": "and one other...", "Invited": "Invited", @@ -479,11 +455,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", @@ -531,6 +507,7 @@ "You have no historical rooms": "You have no historical rooms", "Historical": "Historical", "System Alerts": "System Alerts", + "Joining room...": "Joining room...", "Unable to ascertain that the address this invite was sent to matches one associated with your account.": "Unable to ascertain that the address this invite was sent to matches one associated with your account.", "This invitation was sent to an email address which is not associated with this account:": "This invitation was sent to an email address which is not associated with this account:", "You may wish to login with a different account, or add this email to this account.": "You may wish to login with a different account, or add this email to this account.", @@ -617,13 +594,37 @@ "All Rooms": "All Rooms", "Cancel": "Cancel", "You don't currently have any stickerpacks enabled": "You don't currently have any stickerpacks enabled", - "Add a stickerpack": "Add a stickerpack", + "Add some now": "Add some now", "Stickerpack": "Stickerpack", "Hide Stickers": "Hide Stickers", "Show Stickers": "Show Stickers", "Scroll to unread messages": "Scroll to unread messages", "Jump to first unread message.": "Jump to first unread message.", - "Close": "Close", + "Invalid alias format": "Invalid alias format", + "'%(alias)s' is not a valid format for an alias": "'%(alias)s' is not a valid format for an alias", + "Invalid address format": "Invalid address format", + "'%(alias)s' is not a valid format for an address": "'%(alias)s' is not a valid format for an address", + "not specified": "not specified", + "not set": "not set", + "Remote addresses for this room:": "Remote addresses for this room:", + "Addresses": "Addresses", + "The main address for this room is": "The main address for this room is", + "Local addresses for this room:": "Local addresses for this room:", + "This room has no local addresses": "This room has no local addresses", + "New address (e.g. #foo:%(localDomain)s)": "New address (e.g. #foo:%(localDomain)s)", + "Invalid community ID": "Invalid community ID", + "'%(groupId)s' is not a valid community ID": "'%(groupId)s' is not a valid community ID", + "Flair": "Flair", + "Showing flair for these communities:": "Showing flair for these communities:", + "This room is not showing flair for any communities": "This room is not showing flair for any communities", + "New community ID (e.g. +foo:%(localDomain)s)": "New community ID (e.g. +foo:%(localDomain)s)", + "You have enabled URL previews by default.": "You have enabled URL previews by default.", + "You have disabled URL previews by default.": "You have disabled URL previews by default.", + "URL previews are enabled by default for participants in this room.": "URL previews are enabled by default for participants in this room.", + "URL previews are disabled by default for participants in this room.": "URL previews are disabled by default for participants 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.": "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.", "Sunday": "Sunday", "Monday": "Monday", "Tuesday": "Tuesday", @@ -662,6 +663,7 @@ "Dismiss": "Dismiss", "To continue, please enter your password.": "To continue, please enter your password.", "Password:": "Password:", + "Please review and accept the policies of this homeserver:": "Please review and accept the policies of this homeserver:", "An email has been sent to %(emailAddress)s": "An email has been sent to %(emailAddress)s", "Please check your email to continue registration.": "Please check your email to continue registration.", "Token incorrect": "Token incorrect", @@ -902,6 +904,10 @@ "Ignore request": "Ignore request", "Loading device info...": "Loading device info...", "Encryption key request": "Encryption key request", + "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.": "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.", + "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.": "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.", + "Incompatible local cache": "Incompatible local cache", + "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", "Failed to upgrade room": "Failed to upgrade room", @@ -1031,7 +1037,7 @@ "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", - "

    HTML for your community's page

    \r\n

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

    \r\n

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

    \r\n": "

    HTML for your community's page

    \r\n

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

    \r\n

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

    \r\n", + "

    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 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", "Add rooms to the community summary": "Add rooms to the community summary", "Which rooms would you like to add to this summary?": "Which rooms would you like to add to this summary?", "Add to summary": "Add to summary", @@ -1049,6 +1055,7 @@ "Failed to update community": "Failed to update community", "Unable to accept invite": "Unable to accept invite", "Unable to join community": "Unable to join community", + "You are an administrator of this community. You will not be able to rejoin without an invite from another administrator.": "You are an administrator of this community. You will not be able to rejoin without an invite from another administrator.", "Leave Community": "Leave Community", "Leave %(groupName)s?": "Leave %(groupName)s?", "Unable to leave community": "Unable to leave community", @@ -1309,5 +1316,7 @@ "Import": "Import", "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" + "Failed to add tag %(tagName)s to room": "Failed to add tag %(tagName)s to room", + "Open Devtools": "Open Devtools", + "Show developer tools": "Show developer tools" } diff --git a/src/i18n/strings/en_US.json b/src/i18n/strings/en_US.json index 6f0708f0c2..d8733ab05a 100644 --- a/src/i18n/strings/en_US.json +++ b/src/i18n/strings/en_US.json @@ -567,7 +567,7 @@ "Active call (%(roomName)s)": "Active call (%(roomName)s)", "Accept": "Accept", "Add": "Add", - "Admin Tools": "Admin tools", + "Admin Tools": "Admin Tools", "Alias (optional)": "Alias (optional)", "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.": "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.", "Click here to join the discussion!": "Click here to join the discussion!", @@ -828,5 +828,15 @@ "Collapse panel": "Collapse panel", "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!", "Checking for an update...": "Checking for an update...", - "There are advanced notifications which are not shown here": "There are advanced notifications which are not shown here" + "There are advanced notifications which are not shown here": "There are advanced notifications which are not shown here", + "The platform you're on": "The platform you're on", + "The version of Riot.im": "The version of Riot.im", + "Whether or not you're logged in (we don't record your user name)": "Whether or not you're logged in (we don't record your user name)", + "Your language of choice": "Your language of choice", + "Which officially provided instance you are using, if any": "Which officially provided instance you are using, if any", + "Whether or not you're using the Richtext mode of the Rich Text Editor": "Whether or not you're using the Richtext mode of the Rich Text Editor", + "Your homeserver's URL": "Your homeserver's URL", + "Your identity server's URL": "Your identity server's URL", + "e.g. %(exampleValue)s": "e.g. %(exampleValue)s", + "Every page you use in the app": "Every page you use in the app" } diff --git a/src/i18n/strings/eo.json b/src/i18n/strings/eo.json index 74cd5b255f..84912dccf9 100644 --- a/src/i18n/strings/eo.json +++ b/src/i18n/strings/eo.json @@ -251,7 +251,7 @@ "Encrypted by a verified device": "Ĉifrita de kontrolita aparato", "Encrypted by an unverified device": "Ĉifrita de nekontrolita aparato", "Unencrypted message": "Neĉifrita mesaĝo", - "Please select the destination room for this message": "Bonvolu elekti celan ĉambron por ĉi tiu mesaĝo", + "Please select the destination room for this message": "Bonvolu elekti celan babilejon por tiu mesaĝo", "Blacklisted": "Senpova legi ĉifritajn mesaĝojn", "Verified": "Kontrolita", "Unverified": "Nekontrolita", @@ -292,19 +292,19 @@ "and %(count)s others...|other": "kaj %(count)s aliaj…", "and %(count)s others...|one": "kaj unu alia…", "Invited": "Invititaj", - "Filter room members": "Filtri ĉambranojn", + "Filter room members": "Filtri babilejanojn", "%(userName)s (power %(powerLevelNumber)s)": "%(userName)s (potenco je %(powerLevelNumber)s)", "Attachment": "Kunsendaĵo", "Upload Files": "Alŝuti dosierojn", "Are you sure you want to upload the following files?": "Ĉu vi certe volas alŝuti la jenajn dosierojn?", - "Encrypted room": "Ĉifrita ĉambro", - "Unencrypted room": "Neĉifrita ĉambro", + "Encrypted room": "Ĉifrita babilejo", + "Unencrypted room": "Neĉifrita babilejo", "Hangup": "Fini vokon", "Voice call": "Voĉvoko", "Video call": "Vidvoko", "Upload file": "Alŝuti dosieron", "Show Text Formatting Toolbar": "Montri tekstaranĝan breton", - "You do not have permission to post to this room": "Mankas al vi permeso afiŝi en la ĉambro", + "You do not have permission to post to this room": "Mankas al vi permeso afiŝi en tiu babilejo", "Turn Markdown on": "Ŝalti Marksubon", "Turn Markdown off": "Malŝalti Marksubon", "Hide Text Formatting Toolbar": "Kaŝi tekstaranĝan breton", @@ -339,7 +339,7 @@ "Offline": "Eksterreta", "Unknown": "Nekonata", "Seen by %(userName)s at %(dateTime)s": "Vidita de %(userName)s je %(dateTime)s", - "Unnamed room": "Sennoma ĉambro", + "Unnamed room": "Sennoma babilejo", "World readable": "Legebla de ĉiuj", "Guests can join": "Gastoj povas aliĝi", "No rooms to show": "Neniuj ĉambroj montreblas", @@ -347,11 +347,11 @@ "Save": "Konservi", "(~%(count)s results)|other": "(~%(count)s rezultoj)", "(~%(count)s results)|one": "(~%(count)s rezulto)", - "Join Room": "Aliĝi al ĉambro", + "Join Room": "Aliĝi al Babilejo", "Upload avatar": "Alŝuti profilbildon", "Remove avatar": "Forigi profilbildon", "Settings": "Agordoj", - "Forget room": "Forgesi ĉambron", + "Forget room": "Forgesi babilejon", "Search": "Serĉi", "Show panel": "Montri panelon", "Drop here to favourite": "Demetu tien ĉi por ŝati", @@ -360,7 +360,7 @@ "Drop here to demote": "Demeti tien ĉi por malpligravigi", "Drop here to tag %(section)s": "Demeti tien ĉi por marki %(section)s", "Press to start a chat with someone": "Premu por komenci babilon kun iu", - "You're not in any rooms yet! Press to make a room or to browse the directory": "Vi ankoraŭ estas en neniu ĉambro! Premu por krei ĉambron aŭ por esplori la ĉambrujon", + "You're not in any rooms yet! Press to make a room or to browse the directory": "Vi ankoraŭ estas en neniuj Babilejoj! Premu por krei Babilejon aŭ por esplori la Babilejujon", "Community Invites": "Komunumaj invitoj", "Invites": "Invitoj", "Favourites": "Ŝatataj", @@ -371,19 +371,19 @@ "Unable to ascertain that the address this invite was sent to matches one associated with your account.": "Ne certigeblas, ke la adreso, kien ĉi tiu invito sendiĝis, kongruas kun tiu rilata al via konto.", "This invitation was sent to an email address which is not associated with this account:": "Ĉi tiu invito sendiĝis al retpoŝtadreso, kiu ne rilatas al ĉi tiu konto:", "You may wish to login with a different account, or add this email to this account.": "Vi povas saluti per alia konto, aŭ aldoni ĉi tiun retpoŝtadreson al tiu ĉi konto.", - "You have been invited to join this room by %(inviterName)s": "%(inviterName)s vin invitis al ĉi tiu ĉambro", + "You have been invited to join this room by %(inviterName)s": "%(inviterName)s vin invitis al ĉi tiu babilejo", "Would you like to accept or decline this invitation?": "Ĉu vi volas akceptirifuzi ĉi tiun inviton?", "Reason: %(reasonText)s": "Kialo: %(reasonText)s", "Rejoin": "Realiĝi", "You have been kicked from %(roomName)s by %(userName)s.": "%(userName)s vin forpelis de %(roomName)s.", - "You have been kicked from this room by %(userName)s.": "%(userName)s vin forpelis de tiu ĉi ĉambro.", + "You have been kicked from this room by %(userName)s.": "%(userName)s vin forpelis de tiu babilejo.", "You have been banned from %(roomName)s by %(userName)s.": "%(userName)s vi forbaris de %(roomName)s.", - "You have been banned from this room by %(userName)s.": "%(userName)s vin forbaris de tiu ĉi ĉambro.", - "This room": "Ĉi tiu ĉambro", + "You have been banned from this room by %(userName)s.": "%(userName)s vin forbaris de tiu babilejo.", + "This room": "Ĉi tiu babilejo", "%(roomName)s does not exist.": "%(roomName)s ne ekzistas.", "%(roomName)s is not accessible at this time.": "%(roomName)s ne estas atingebla nun.", "You are trying to access %(roomName)s.": "Vi provas atingi %(roomName)s.", - "You are trying to access a room.": "Vi provas atingi ĉambron.", + "You are trying to access a room.": "Vi provas aliri babilejon.", "Click here to join the discussion!": "Klaku ĉi tie por aliĝi al la diskuto!", "This is a preview of this room. Room interactions have been disabled": "Tio ĉi estas antaŭrigardo al la ĉambro. Ĉambraj interagoj estas malŝaltitaj", "To change the room's avatar, you must be a": "Por ŝanĝi la ĉambran profilbildon, vi devas esti", @@ -419,7 +419,7 @@ "To link to a room it must have an address.": "Por esti ligebla, ĉambro devas havi adreson.", "Guests cannot join this room even if explicitly invited.": "Gastoj ne povas aliĝi ĉi tiun ĉambron eĉ kun malimplica invito.", "Click here to fix": "Klaku ĉi tie por riparo", - "Who can access this room?": "Kiu povas atingi ĉi tiun ĉambron?", + "Who can access this room?": "Kiu povas aliri ĉi tiun ĉambron?", "Only people who have been invited": "Nur invititaj uzantoj", "Anyone who knows the room's link, apart from guests": "Iu ajn kun la ligilo, krom gastoj", "Anyone who knows the room's link, including guests": "Iu ajn kun la ligilo, inkluzive gastojn", @@ -453,17 +453,17 @@ "not set": "neagordita", "Remote addresses for this room:": "Foraj adresoj de ĉi tiu ĉambro:", "Addresses": "Adresoj", - "The main address for this room is": "La ĉefadreso por ĉi tiu ĉambro estas", - "Local addresses for this room:": "Lokaj adresoj por ĉi tiu ĉambro:", - "This room has no local addresses": "Ĉi tiu ĉambro ne havas lokajn adresojn", + "The main address for this room is": "La ĉefadreso por ĉi tiu babilejo estas", + "Local addresses for this room:": "Lokaj adresoj por ĉi tiu babilejo:", + "This room has no local addresses": "Ĉi tiu babilejo ne havas lokajn adresojn", "New address (e.g. #foo:%(localDomain)s)": "Nova adreso (ekz-e #io:%(localDomain)s)", "Invalid community ID": "Malvalida komunuma identigaĵo", "'%(groupId)s' is not a valid community ID": "‹%(groupId)s› ne estas valida komunuma identigaĵo", "New community ID (e.g. +foo:%(localDomain)s)": "Nova komunuma identigaĵo (ekz-e +io:%(localDomain)s)", "You have enabled URL previews by default.": "Vi ŝaltis implicitajn antaŭrigardojn al retpaĝoj.", "You have disabled URL previews by default.": "Vi malŝaltis implicitajn antaŭrigardojn al retpaĝoj.", - "URL previews are enabled by default for participants in this room.": "Antaŭrigardoj al retpaĝoj estas implicite ŝaltitaj por ĉambranoj ĉi tie.", - "URL previews are disabled by default for participants in this room.": "Antaŭrigardoj al retpaĝoj estas implicite malŝaltitaj por ĉambranoj ĉi tie.", + "URL previews are enabled by default for participants in this room.": "Antaŭrigardoj de URL-oj estas implicite ŝaltitaj por anoj de tiu ĉi babilejo.", + "URL previews are disabled by default for participants in this room.": "Antaŭrigardoj de URL-oj estas implicite malŝaltitaj por anoj de tiu ĉi babilejo.", "URL Previews": "Antaŭrigardoj al retpaĝoj", "Error decrypting audio": "Eraro malĉifrante sonon", "Error decrypting attachment": "Eraro malĉifrante kunsendaĵon", diff --git a/src/i18n/strings/es.json b/src/i18n/strings/es.json index bdbe9232c3..1a55796e6c 100644 --- a/src/i18n/strings/es.json +++ b/src/i18n/strings/es.json @@ -30,7 +30,7 @@ "Bans user with given id": "Veta al usuario con la ID dada", "Blacklisted": "Prohibido", "Bulk Options": "Opciones masivas", - "Call Timeout": "Tiempo de espera de la llamada", + "Call Timeout": "Tiempo de Espera de Llamada", "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or enable unsafe scripts.": "No se puede conectar al servidor doméstico via HTTP, cuando es necesario un enlace HTTPS en la barra de direcciones de tu navegador. Ya sea usando HTTPS o habilitando los scripts inseguros.", "Can't load user settings": "No se puede cargar los ajustes de usuario", "Change Password": "Cambiar Contraseña", @@ -72,7 +72,7 @@ "Decryption error": "Error de descifrado", "Delete": "Eliminar", "Deops user with given id": "Degrada al usuario con la ID dada", - "Default": "Por defecto", + "Default": "Por Defecto", "Device ID": "ID de Dispositivo", "Devices": "Dispositivos", "Devices will not yet be able to decrypt history from before they joined the room": "Los dispositivos todavía no podrán descifrar el historial desde antes de unirse a la sala", @@ -86,7 +86,7 @@ "Ed25519 fingerprint": "Huella digital Ed25519", "Email": "Correo electrónico", "Email address": "Dirección de correo electrónico", - "Email, name or matrix ID": "Correo electrónico, nombre o Matrix ID", + "Email, name or matrix ID": "Correo electrónico, nombre o ID de matrix", "Emoji": "Emoticones", "Enable encryption": "Habilitar cifrado", "Encrypted messages will not be visible on clients that do not yet implement encryption": "Los mensajes cifrados no serán visibles en clientes que aún no implementen el cifrado", @@ -98,10 +98,10 @@ "Error": "Error", "Error decrypting attachment": "Error al descifrar adjunto", "Event information": "Información de eventos", - "Existing Call": "Llamada existente", + "Existing Call": "Llamada Existente", "Export E2E room keys": "Exportar claves de salas con Cifrado de Extremo a Extremo", "Failed to ban user": "Bloqueo del usuario falló", - "Failed to change password. Is your password correct?": "No se pudo cambiar la contraseña. ¿Está usando la correcta?", + "Failed to change password. Is your password correct?": "No se pudo cambiar la contraseña. ¿Estás usando la correcta?", "Failed to change power level": "Falló al cambiar de nivel de acceso", "Failed to forget room %(errCode)s": "No se pudo olvidar la sala %(errCode)s", "Failed to join room": "No se pudo unir a la sala", @@ -233,7 +233,7 @@ "You must join the room to see its files": "Debes unirte a la sala para ver sus archivos", "Reject all %(invitedRooms)s invites": "Rechazar todas las invitaciones a %(invitedRooms)s", "Start new chat": "Iniciar nueva conversación", - "Failed to invite": "Fallo en la invitación", + "Failed to invite": "No se pudo invitar", "Failed to invite user": "No se pudo invitar al usuario", "Failed to invite the following users to the %(roomName)s room:": "No se pudo invitar a los siguientes usuarios a la sala %(roomName)s:", "Unknown error": "Error desconocido", @@ -370,24 +370,24 @@ "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.": "Reiniciar la contraseña también reiniciará las claves de cifrado de extremo a extremo, haciendo ilegible el historial de las conversaciones, salvo que exporte previamente las claves de sala, y las importe posteriormente. Esto será mejorado en futuras versiones.", "Results from DuckDuckGo": "Resultados desde DuckDuckGo", "Return to login screen": "Regresar a la pantalla de inicio de sesión", - "Riot does not have permission to send you notifications - please check your browser settings": "Riot no tiene permisos para enviarle notificaciones - por favor, revisa los ajustes de tu navegador", - "Riot was not given permission to send notifications - please try again": "Riot no pudo obtener permisos para enviar notificaciones - por favor, inténtelo de nuevo", + "Riot does not have permission to send you notifications - please check your browser settings": "Riot no tiene permiso para enviarte notificaciones - por favor, comprueba los ajustes de tu navegador", + "Riot was not given permission to send notifications - please try again": "No se le dio permiso a Riot para enviar notificaciones - por favor, inténtalo nuevamente", "riot-web version:": "versión de riot-web:", - "Room %(roomId)s not visible": "La sala %(roomId)s no es visible", - "Searches DuckDuckGo for results": "Busca en DuckDuckGo", + "Room %(roomId)s not visible": "La sala %(roomId)s no está visible", + "Searches DuckDuckGo for results": "Busca resultados en DuckDuckGo", "Server may be unavailable or overloaded": "El servidor podría estar saturado o desconectado", "Show timestamps in 12 hour format (e.g. 2:30pm)": "Mostrar marcas temporales en formato de 12 horas (ej. 2:30pm)", "The signing key you provided matches the signing key you received from %(userId)s's device %(deviceId)s. Device marked as verified.": "La clave de firma que usted ha proporcionado coincide con la recibida del dispositivo %(deviceId)s de %(userId)s. Dispositivo verificado.", "This email address is already in use": "Esta dirección de correo electrónico ya está en uso", "This email address was not found": "No se encontró esta dirección de correo electrónico", "The email address linked to your account must be entered.": "Debes ingresar la dirección de correo electrónico vinculada a tu cuenta.", - "The file '%(fileName)s' exceeds this home server's size limit for uploads": "El fichero '%(fileName)s' supera el tamaño máximo permitido en este servidor doméstico", + "The file '%(fileName)s' exceeds this home server's size limit for uploads": "El archivo '%(fileName)s' supera el tamaño máximo permitido en este servidor doméstico", "The file '%(fileName)s' failed to upload": "No se pudo subir '%(fileName)s'", "The remote side failed to pick up": "El lado remoto no contestó", "This Home Server does not support login using email address.": "Este Servidor Doméstico no permite identificarse con direcciones e-mail.", "This invitation was sent to an email address which is not associated with this account:": "Esta invitación fue enviada a una dirección de correo electrónico que no está asociada a esta cuenta:", "This room has no local addresses": "Esta sala no tiene direcciones locales", - "This room is not recognised.": "Esta sala no se reconoce.", + "This room is not recognised.": "No se reconoce esta sala.", "These are experimental features that may break in unexpected ways": "Estas son funcionalidades experimentales que pueden romperse de maneras inesperadas", "The visibility of existing history will be unchanged": "La visibilidad del historial previo no se verá afectada", "This doesn't appear to be a valid email address": "Esto no parece un e-mail váido", @@ -415,13 +415,13 @@ "Press to start a chat with someone": "Pulsa para empezar a charlar con alguien", "Add a widget": "Añadir widget", "Allow": "Permitir", - "Changes colour scheme of current room": "Cambia el esquema de colores de esta sala", + "Changes colour scheme of current room": "Cambia el esquema de colores de la sala actual", "Delete widget": "Eliminar widget", "Define the power level of a user": "Define el nivel de autoridad de un usuario", "Edit": "Editar", "Enable automatic language detection for syntax highlighting": "Activar la detección automática del lenguaje para resaltar la sintaxis", "Hide join/leave messages (invites/kicks/bans unaffected)": "Ocultar mensajes de unirse/salir (no afecta a invitaciones/expulsiones/vetos)", - "Sets the room topic": "Configura el tema de la sala", + "Sets the room topic": "Establece el tema de la sala", "To get started, please pick a username!": "Para empezar, ¡por favor elija un nombre de usuario!", "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Se ha intentado cargar cierto punto en la cronología de esta sala, pero no tiene permiso para ver el mensaje solicitado.", "Tried to load a specific point in this room's timeline, but was unable to find it.": "Se ha intentado cargar cierto punto en la cronología de esta sala, pero no se ha podido encontrarlo.", @@ -435,8 +435,8 @@ "Unban": "Quitar Veto", "Unbans user with given id": "Quita el veto al usuario con la ID dada", "Unable to ascertain that the address this invite was sent to matches one associated with your account.": "No se ha podido asegurar que la dirección a la que se envió esta invitación, coincide con una asociada a su cuenta.", - "Unable to capture screen": "No se ha podido capturar la pantalla", - "Unable to enable Notifications": "No se ha podido activar las notificaciones", + "Unable to capture screen": "No es posible capturar la pantalla", + "Unable to enable Notifications": "No es posible habilitar las Notificaciones", "Unable to load device list": "No se ha podido cargar la lista de dispositivos", "Undecryptable": "No se puede descifrar", "Unencrypted room": "Sala sin cifrado", @@ -451,7 +451,7 @@ "Uploading %(filename)s and %(count)s others|one": "Subiendo %(filename)s y otros %(count)s", "Uploading %(filename)s and %(count)s others|other": "Subiendo %(filename)s y otros %(count)s", "Upload avatar": "Subir avatar", - "Upload Failed": "Error al subir", + "Upload Failed": "No Se Pudo Subir", "Upload Files": "Subir Archivos", "Upload file": "Subir archivo", "Upload new:": "Subir nuevo:", @@ -472,7 +472,7 @@ "Voice call": "Llamada de voz", "VoIP conference finished.": "conferencia de vozIP finalizada.", "VoIP conference started.": "conferencia de vozIP iniciada.", - "VoIP is unsupported": "No hay soporte para VoIP", + "VoIP is unsupported": "VoIP no es compatible", "(could not connect media)": "(no se ha podido conectar medio)", "(no answer)": "(sin respuesta)", "(unknown failure: %(reason)s)": "(error desconocido: %(reason)s)", @@ -481,17 +481,17 @@ "WARNING: Device already verified, but keys do NOT MATCH!": "ADVERTENCIA: Dispositivo ya verificado, ¡pero las claves NO COINCIDEN!", "Who can access this room?": "¿Quién puede acceder a esta sala?", "Who can read history?": "¿Quién puede leer el historial?", - "Who would you like to add to this room?": "¿A quién quiere añadir a esta sala?", - "Who would you like to communicate with?": "¿Con quién quiere comunicarse?", + "Who would you like to add to this room?": "¿A quién te gustaría añadir a esta sala?", + "Who would you like to communicate with?": "¿Con quién te gustaría comunicarte?", "%(senderName)s withdrew %(targetName)s's invitation.": "%(senderName)s retiró la invitación de %(targetName)s.", "Would you like to accept or decline this invitation?": "¿Quiere aceptar o rechazar esta invitación?", "You already have existing direct chats with this user:": "Ya tiene conversaciones directas con este usuario:", - "You are already in a call.": "Ya está participando en una llamada.", + "You are already in a call.": "Ya estás participando en una llamada.", "You are not in this room.": "No estás en esta sala.", "You do not have permission to do that in this room.": "No tienes permiso para realizar esa acción en esta sala.", "You're not in any rooms yet! Press to make a room or to browse the directory": "¡Todavía no participa en ninguna sala! Pulsa para crear una sala o para explorar el directorio", "You are trying to access %(roomName)s.": "Estás intentando acceder a %(roomName)s.", - "You cannot place a call with yourself.": "No puede iniciar una llamada con usted mismo.", + "You cannot place a call with yourself.": "No puedes realizar una llamada contigo mismo.", "Cannot add any more widgets": "no es posible agregar mas widgets", "Do you want to load widget from URL:": "desea cargar widget desde URL:", "Integrations Error": "error de integracion", @@ -501,15 +501,15 @@ "NOTE: Apps are not end-to-end encrypted": "NOTA: Las Apps no son cifradas de extremo a extremo", "Revoke widget access": "Revocar acceso del widget", "The maximum permitted number of widgets have already been added to this room.": "La cantidad máxima de widgets permitida ha sido alcanzada en esta sala.", - "To use it, just wait for autocomplete results to load and tab through them.": "Para usar, solo espere a que carguen los resultados de auto-completar y navegue entre ellos.", + "To use it, just wait for autocomplete results to load and tab through them.": "Para utilizarlo, tan solo espera a que se carguen los resultados de autocompletar y navega entre ellos.", "%(senderName)s unbanned %(targetName)s.": "%(senderName)s le quitó el veto a %(targetName)s.", - "unencrypted": "no cifrado", + "unencrypted": "sin cifrar", "Unmute": "Dejar de silenciar", - "Unrecognised command:": "comando no reconocido:", - "Unrecognised room alias:": "alias de sala no reconocido:", + "Unrecognised command:": "Comando no identificado:", + "Unrecognised room alias:": "Alias de sala no reconocido:", "%(userName)s (power %(powerLevelNumber)s)": "%(userName)s (nivel de permisos %(powerLevelNumber)s)", "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!": "ADVERTENCIA: VERIFICACIÓN DE CLAVE FALLO\" La clave de firma para %(userId)s y el dispositivo %(deviceId)s es \"%(fprint)s\" la cual no concuerda con la clave provista por \"%(fingerprint)s\". Esto puede significar que sus comunicaciones están siendo interceptadas!", - "You cannot place VoIP calls in this browser.": "No puede realizar llamadas VoIP en este navegador.", + "You cannot place VoIP calls in this browser.": "No puedes realizar llamadas VoIP en este navegador.", "You do not have permission to post to this room": "No tienes permiso para publicar en esta sala", "You have been banned from %(roomName)s by %(userName)s.": "Has sido vetado de %(roomName)s por %(userName)s.", "You have been invited to join this room by %(inviterName)s": "Ha sido invitado por %(inviterName)s a unirte a esta sala", @@ -520,14 +520,14 @@ "You have no visible notifications": "No tiene notificaciones visibles", "You may wish to login with a different account, or add this email to this account.": "Quizás quieras iniciar sesión con otra cuenta, o añadir este correo electrónico a esta cuenta.", "You must register to use this functionality": "Usted debe ser un registrar para usar esta funcionalidad", - "You need to be able to invite users to do that.": "Usted debe ser capaz de invitar usuarios para hacer eso.", - "You need to be logged in.": "Necesita estar autenticado.", + "You need to be able to invite users to do that.": "Debes ser capaz de invitar usuarios para realizar esa acción.", + "You need to be logged in.": "Necesitas haber iniciado sesión.", "You need to enter a user name.": "Tiene que ingresar un nombre de usuario.", - "Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "Su correo electrónico parece no estar asociado con una ID de Matrix en este Servidor Doméstico.", + "Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "Tu dirección de correo electrónico no parece estar asociada a una ID de Matrix en este Servidor Doméstico.", "Your password has been reset": "Tu contraseña fue restablecida", "Your password was successfully changed. You will not receive push notifications on other devices until you log back in to them": "Su contraseña a sido cambiada exitosamente. No recibirá notificaciones en otros dispositivos hasta que ingrese de nuevo en ellos", "You seem to be in a call, are you sure you want to quit?": "Parece estar en medio de una llamada, ¿esta seguro que desea salir?", - "You seem to be uploading files, are you sure you want to quit?": "Parece estar cargando archivos, ¿esta seguro que desea salir?", + "You seem to be uploading files, are you sure you want to quit?": "Pareces estar subiendo archivos, ¿seguro que quieres salir?", "You should not yet trust it to secure data": "Aún no deberías confiar en él para proteger tus datos", "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.": "No podrás deshacer este cambio porque estás promoviendo al usuario para tener el mismo nivel de autoridad que tú.", "Your home server does not support device management.": "Tu servidor doméstico no suporta la gestión de dispositivos.", @@ -547,10 +547,10 @@ "Jul": "Jul", "Aug": "Ago", "Add rooms to this community": "Agregar salas a esta comunidad", - "Call Failed": "La llamada falló", + "Call Failed": "La Llamada Falló", "Review Devices": "Revisar Dispositivos", - "Call Anyway": "Llamar de todas formas", - "Answer Anyway": "Contestar de todas formas", + "Call Anyway": "Llamar de todos modos", + "Answer Anyway": "Contestar de Todos Modos", "Call": "Llamar", "Answer": "Contestar", "Sep": "Sep", @@ -563,19 +563,19 @@ "Submit debug logs": "Enviar registros de depuración", "The platform you're on": "La plataforma en la que te encuentras", "The version of Riot.im": "La versión de Riot.im", - "Whether or not you're logged in (we don't record your user name)": "Estés identificado o no (no almacenamos tu nombre de usuario)", - "Your language of choice": "El idioma que has elegido", + "Whether or not you're logged in (we don't record your user name)": "Hayas iniciado sesión o no (no almacenamos tu nombre de usuario)", + "Your language of choice": "El idioma de tu elección", "Your homeserver's URL": "La URL de tu servidor doméstico", "Your identity server's URL": "La URL de tu servidor de identidad", - "The information being sent to us to help make Riot.im better includes:": "La información remitida a nosotros para ayudar a mejorar Riot.im incluye:", + "The information being sent to us to help make Riot.im better includes:": "La información que se nos envía para ayudar a mejorar Riot.im incluye:", "Drop here to demote": "Suelta aquí para degradar", - "Whether or not you're using the Richtext mode of the Rich Text Editor": "Estés o no usando el modo Richtext del Editor de Texto Enriquecido", - "Who would you like to add to this community?": "¿A quién deseas añadir a esta comunidad?", - "Warning: any person you add to a community will be publicly visible to anyone who knows the community ID": "Advertencia: cualquier persona que añadas a una comunidad será públicamente visible a cualquier persona que conozca la ID de la comunidad", + "Whether or not you're using the Richtext mode of the Rich Text Editor": "Estés utilizando o no el modo de Texto Enriquecido del Editor de Texto Enriquecido", + "Who would you like to add to this community?": "¿A quién te gustaría añadir a esta comunidad?", + "Warning: any person you add to a community will be publicly visible to anyone who knows the community ID": "Advertencia: cualquier persona que añadas a una comunidad será públicamente visible a cualquiera que conozca la ID de la comunidad", "Invite new community members": "Invita nuevos miembros a la comunidad", "Name or matrix ID": "Nombre o ID de matrix", - "Invite to Community": "Invitar a la comunidad", - "Which rooms would you like to add to this community?": "¿Qué salas deseas añadir a esta comunidad?", + "Invite to Community": "Invitar a la Comunidad", + "Which rooms would you like to add to this community?": "¿Qué salas te gustaría añadir a esta comunidad?", "Fetching third party location failed": "Falló la obtención de la ubicación de un tercero", "A new version of Riot is available.": "Una nueva versión de Riot está disponible.", "I understand the risks and wish to continue": "Entiendo los riesgos y deseo continuar", @@ -737,34 +737,34 @@ "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!": "En su navegador actual, la apariencia y comportamiento de la aplicación puede ser completamente incorrecta, y algunas de las características podrían no funcionar. Si aún desea probarlo puede continuar, pero ¡no podremos ofrecer soporte por cualquier problema que pudiese tener!", "Checking for an update...": "Comprobando actualizaciones...", "There are advanced notifications which are not shown here": "Hay notificaciones avanzadas que no se muestran aquí", - "Every page you use in the app": "Todas las páginas que usas en la aplicación", + "Every page you use in the app": "Cada página que utilizas en la aplicación", "Your User Agent": "Tu Agente de Usuario", "Your device resolution": "La resolución de tu dispositivo", - "There are unknown devices in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.": "Hay dispositivos desconocidos en esta sala: si procede sin verificarlos, será posible que alguien escuche su llamada.", - "Which officially provided instance you are using, if any": "Cuál instancia ofrecida oficialmente está usando, si existe", + "There are unknown devices in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.": "Hay dispositivos desconocidos en esta sala: si continúas sin verificarlos, será posible que alguien escuche tu llamada.", + "Which officially provided instance you are using, if any": "Qué instancia proporcionada oficialmente estás utilizando, si estás utilizando alguna", "e.g. %(exampleValue)s": "ej. %(exampleValue)s", - "e.g. ": "e.g. ", - "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Donde esta página incluye información identificable, como sala, usuario o ID del grupo, esa información se elimina antes de enviarla al servidor.", + "e.g. ": "ej. ", + "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Donde esta página incluya información identificable, como una sala, usuario o ID de grupo, esos datos se eliminan antes de enviarse al servidor.", "A conference call could not be started because the intgrations server is not available": "No se pudo iniciar una llamada de conferencia porque el servidor de integraciones no está disponible", "Call in Progress": "Llamada en Curso", "A call is currently being placed!": "¡Se está realizando una llamada en este momento!", "A call is already in progress!": "¡Ya hay una llamada en curso!", "Permission Required": "Permiso Requerido", - "You do not have permission to start a conference call in this room": "No tiene permiso para comenzar una llamada de conferencia en esta sala", + "You do not have permission to start a conference call in this room": "No tienes permiso para iniciar una llamada de conferencia en esta sala", "%(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, %(monthName)s %(day)s %(fullYear)s", "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s", - "Show these rooms to non-members on the community page and room list?": "¿Mostrar estas salas a personas no registradas en la página de la comunidad y la lista de salas?", - "Add rooms to the community": "Agregar salas a la comunidad", + "Show these rooms to non-members on the community page and room list?": "¿Mostrar estas salas a los que no son miembros en la página de la comunidad y la lista de salas?", + "Add rooms to the community": "Añadir salas a la comunidad", "Room name or alias": "Nombre o alias de sala", - "Add to community": "Agregar a comunidad", - "Failed to invite the following users to %(groupId)s:": "No se pudo invitar a los usuarios siguientes a %(groupId)s:", - "Failed to invite users to community": "Falló la invitación de usuarios a la comunidad", - "Failed to invite users to %(groupId)s": "Falló la invitación de usuarios a %(groupId)s", - "Failed to add the following rooms to %(groupId)s:": "Falló la a agregación de las salas siguientes a %(groupId)s:", + "Add to community": "Añadir a la comunidad", + "Failed to invite the following users to %(groupId)s:": "No se pudo invitar a los siguientes usuarios a %(groupId)s:", + "Failed to invite users to community": "No se pudo invitar usuarios a la comunidad", + "Failed to invite users to %(groupId)s": "No se pudo invitar usuarios a %(groupId)s", + "Failed to add the following rooms to %(groupId)s:": "No se pudo añadir a las siguientes salas a %(groupId)s:", "Restricted": "Restringido", - "Missing roomId.": "Id de sala ausente.", + "Missing roomId.": "Falta el Id de sala.", "Ignores a user, hiding their messages from you": "Ignora a un usuario, ocultando sus mensajes", "Ignored user": "Usuario ignorado", "You are now ignoring %(userId)s": "Ahora está ignorando a %(userId)s", @@ -1058,7 +1058,7 @@ "Click on the button below to start chatting!": "¡Haz clic en el botón a continuación para iniciar una conversación!", "Start Chatting": "Iniciar Conversación", "Confirm Removal": "Confirmar Eliminación", - "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.": "¿Está seguro de querer eliminar (borrar) este evento? Tenga en cuenta que si borra el nombre de una sala o cambia el tema, podría deshacer el cambio.", + "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.": "¿Seguro que quieres eliminar (borrar) este evento? Ten en cuenta que si borras un cambio de nombre o tema de sala, podrías deshacer el cambio.", "Community IDs cannot be empty.": "Las IDs de comunidad no pueden estar vacías.", "Community IDs may only contain characters a-z, 0-9, or '=_-./'": "Las IDs de comunidad sólo pueden contener caracteres a-z, 0-9, ó '=_-./'", "Something went wrong whilst creating your community": "Algo fue mal mientras se creaba la comunidad", @@ -1192,8 +1192,8 @@ "Learn more about how we use analytics.": "Más información sobre el uso de los análisis de estadísticas.", "Updates": "Actualizaciones", "Check for update": "Comprobar actualizaciones", - "Desktop specific": "Específico del escritorio", - "Start automatically after system login": "Iniciar automáticamente después de ingresar en el sistema", + "Desktop specific": "Específico de escritorio", + "Start automatically after system login": "Ejecutar automáticamente después de iniciar sesión en el sistema", "No Audio Outputs detected": "No se detectaron Salidas de Sonido", "Audio Output": "Salida de Sonido", "An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.": "Se envió un correo electrónico a %(emailAddress)s. Una vez hayas seguido el enlace que contiene, haz clic a continuación.", @@ -1203,7 +1203,7 @@ "Sign in to get started": "Ingresar para comenzar", "Set a display name:": "Establece un nombre público:", "Upload an avatar:": "Subir un avatar:", - "This server does not support authentication with a phone number.": "Este servidor no soporta autenticación mediante número telefónico.", + "This server does not support authentication with a phone number.": "Este servidor no es compatible con autenticación mediante número telefónico.", "Missing password.": "Falta la contraseña.", "Passwords don't match.": "Las contraseñas no coinciden.", "Password too short (min %(MIN_PASSWORD_LENGTH)s).": "Contraseña demasiado corta (mínimo %(MIN_PASSWORD_LENGTH)s).", @@ -1213,10 +1213,10 @@ "I already have an account": "Ya tengo una cuenta", "Notify the whole room": "Notificar a toda la sala", "Room Notification": "Notificación de Salas", - "This process allows you to export the keys for messages you have received in encrypted rooms to a local file. You will then be able to import the file into another Matrix client in the future, so that client will also be able to decrypt these messages.": "Este proceso le permite exportar las claves para los mensajes que haya recibido en salas cifradas a un fichero local. Entonces podrá importar el fichero en otro cliente de Matrix en el futuro, de modo que dicho cliente será capaz de descifrar dichos mensajes.", - "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.": "El fichero exportado permitirá a cualquier persona que pueda leerlo la tarea de descifrar todo mensaje cifrado que usted pueda ver, así que debe ser cuidadoso en mantenerlo seguro. Para ayudarle, debería introducir una contraseña debajo, la cual usará para cifrar la información exportada. Sólo será posible importar dicha información usando la misma contraseña.", - "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.": "Este proceso permite importar claves de cifrado que había exportado previamente desde otro cliente de Matrix. Entonces será capaz de descifrar todos los mensajes que el otro cliente así hacía.", - "The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.": "El fichero de exportación se protegerá con una contraseña. Debería introducir aquí la contraseña para descifrar el fichero.", + "This process allows you to export the keys for messages you have received in encrypted rooms to a local file. You will then be able to import the file into another Matrix client in the future, so that client will also be able to decrypt these messages.": "Este proceso te permite exportar las claves para los mensajes que has recibido en salas cifradas a un archivo local. En el futuro, podrás importar el archivo a otro cliente de Matrix, para que ese cliente también sea capaz de descifrar estos mensajes.", + "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.": "El archivo exportado le permitirá descifrar cualquier mensaje cifrado que puedas ver a cualquier persona que pueda leerlo, así que deberías ser cuidadoso para mantenerlo seguro. Para ayudarte, deberías ingresar una frase de contraseña a continuación, la cual será utilizada para cifrar los datos exportados. Solo será posible importar los datos utilizando la misma frase de contraseña.", + "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.": "Este proceso te permite importar claves de cifrado que hayas exportado previamente desde otro cliente de Matrix. Así, podrás descifrar cualquier mensaje que el otro cliente pudiera descifrar.", + "The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.": "El archivo exportado estará protegido con una contraseña. Deberías ingresar la contraseña aquí para descifrar el archivo.", "Internal room ID: ": "ID interno de la sala: ", "Room version number: ": "Número de versión de la sala: ", "There is a known vulnerability affecting this room.": "Hay una vulnerabilidad conocida que afecta a esta sala.", @@ -1255,5 +1255,17 @@ "The room upgrade could not be completed": "La actualización de la sala no pudo ser completada", "Upgrade this room to version %(version)s": "Actualiza esta sala a la versión %(version)s", "Legal": "Legal", - "Unable to connect to Homeserver. Retrying...": "No es posible conectarse al Servidor Doméstico. Volviendo a intentar..." + "Unable to connect to Homeserver. Retrying...": "No es posible conectarse al Servidor Doméstico. Volviendo a intentar...", + "Registration Required": "Se Requiere Registro", + "You need to register to do this. Would you like to register now?": "Necesitas registrarte para hacer esto. ¿Te gustaría registrarte ahora?", + "%(senderName)s added %(count)s %(addedAddresses)s as addresses for this room.|other": "%(senderName)s añadió %(addedAddresses)s como direcciones para esta sala.", + "%(senderName)s added %(count)s %(addedAddresses)s as addresses for this room.|one": "%(senderName)s añadió %(addedAddresses)s como una dirección para esta sala.", + "%(senderName)s removed %(count)s %(removedAddresses)s as addresses for this room.|other": "%(senderName)s eliminó %(removedAddresses)s como direcciones para esta sala.", + "%(senderName)s removed %(count)s %(removedAddresses)s as addresses for this room.|one": "%(senderName)s eliminó %(removedAddresses)s como una dirección para esta sala.", + "%(senderName)s added %(addedAddresses)s and removed %(removedAddresses)s as addresses for this room.": "%(senderName)s añadió %(addedAddresses)s y eliminó %(removedAddresses)s como direcciones para esta sala.", + "%(senderName)s set the main address for this room to %(address)s.": "%(senderName)s estableció la dirección principal para esta sala como %(address)s.", + "%(senderName)s removed the main address for this room.": "%(senderName)s eliminó la dirección principal para esta sala.", + "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 ahora utiliza de 3 a 5 veces menos memoria, porque solo carga información sobre otros usuarios cuando es necesario. Por favor, ¡aguarda mientras volvemos a sincronizar con el servidor!", + "Updating Riot": "Actualizando Riot", + "Unable to query for supported registration methods": "No es posible consultar por los métodos de registro compatibles" } diff --git a/src/i18n/strings/eu.json b/src/i18n/strings/eu.json index 96ea482e08..5adbe476e7 100644 --- a/src/i18n/strings/eu.json +++ b/src/i18n/strings/eu.json @@ -835,7 +835,7 @@ "was invited %(count)s times|one": "gonbidatua izan da", "were banned %(count)s times|other": "%(count)s aldiz debekatuak izan dira", "were banned %(count)s times|one": "debekatuak izan dira", - "was banned %(count)s times|other": "%(count)s aldi debekatuak izan dira", + "was banned %(count)s times|other": "%(count)s aldiz debekatuak izan dira", "were unbanned %(count)s times|other": "%(count)s aldiz kendu zaie debekua", "were unbanned %(count)s times|one": "debekua kendu zaie", "was unbanned %(count)s times|other": "%(count)s aldiz kendu zaio debekua", @@ -1277,5 +1277,20 @@ "%(senderName)s removed %(count)s %(removedAddresses)s as addresses for this room.|other": "%(senderName)s erabiltzaileak %(removedAddresses)s helbideak kendu ditu gela honetatik.", "%(senderName)s added %(addedAddresses)s and removed %(removedAddresses)s as addresses for this room.": "%(senderName)s erabiltzaileak %(addedAddresses)s helbideak gehitu eta %(removedAddresses)s helbideak kendu ditu gela honetatik.", "%(senderName)s set the main address for this room to %(address)s.": "%(senderName)s erabiltzileak %(address)s ezarri du gela honetako helbide nagusi gisa.", - "%(senderName)s removed the main address for this room.": "%(senderName)s erabiltzaileak gela honen helbide nagusia kendu du." + "%(senderName)s removed the main address for this room.": "%(senderName)s erabiltzaileak gela honen helbide nagusia kendu du.", + "Before submitting logs, you must create a GitHub issue to describe your problem.": "Egunkariak bidali aurretik, GitHub arazo bat sortu behar duzu gertatzen zaizuna azaltzeko.", + "What GitHub issue are these logs for?": "Zein GitHub arazorako egunkariak dira hauek?", + "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-ek orain 3-5 aldiz memoria gutxiago darabil, beste erabiltzaileen informazioa behar denean besterik ez kargatzen. Itxaron zerbitzariarekin sinkronizatzen garen bitartean!", + "Updating Riot": "Riot eguneratzen", + "

    HTML for your community's page

    \r\n

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

    \r\n

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

    \r\n": "

    Zure komunitatearen orriaren HTMLa

    \n

    \n Erabili deskripzio luzea kide berriek komunitatea ezagutu dezaten, edo eman ezagutzera esteka garrantzitsuak\n

    \n

    \n 'img' etiketak erabili ditzakezu ere\n

    \n", + "Submit Debug Logs": "Bidali arazketa egunkariak", + "An email address is required to register on this homeserver.": "e-mail helbide bat behar da hasiera-zerbitzari honetan izena emateko.", + "A phone number is required to register on this homeserver.": "telefono zenbaki bat behar da hasiera-zerbitzari honetan izena emateko.", + "Please accept all of the policies": "Onartu mesedez politika guztiak", + "Please review and accept the policies of this homeserver:": "Irakurri eta onartu hasiera zerbitzari honen politikak:", + "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.": "Aurretik Riot erabili duzu %(host)s zerbitzarian kideen karga alferra gaituta zenuela. Bertsio honetan karga alferra desgaituta dago. Katxe lokala bi ezarpen hauen artean bateragarria ez denez, Riotek zure kontua berriro sinkronizatu behar du.", + "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.": "Rioten beste bertsioa oraindik beste fitxat batean irekita badago, itxi ezazu zerbitzari bera aldi berean karga alferra gaituta eta desgaituta erabiltzeak arazoak sor ditzakeelako.", + "Incompatible local cache": "Katxe lokal bateraezina", + "Clear cache and resync": "Garbitu katxea eta sinkronizatu berriro", + "Add some now": "Gehitu batzuk orain" } diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index 947a3227e3..125ac49b79 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -1133,12 +1133,12 @@ "When I'm invited to a room": "Quand je suis invité dans un salon", "Checking for an update...": "Recherche de mise à jour...", "There are advanced notifications which are not shown here": "Il existe une configuration avancée des notifications qui ne peut être affichée ici", - "Logs sent": "Rapports envoyés", + "Logs sent": "Journaux envoyés", "GitHub issue link:": "Lien du signalement GitHub :", - "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.": "Les rapports de débogage contiennent des données d'usage de l'application qui incluent votre nom d'utilisateur, les identifiants ou alias des salons ou groupes auxquels vous avez rendu visite ainsi que les noms des autres utilisateurs. Ils ne contiennent aucun message.", - "Failed to send logs: ": "Échec lors de l'envoi des rapports : ", + "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.": "Les journaux de débogage contiennent des données d'usage de l'application qui incluent votre nom d'utilisateur, les identifiants ou alias des salons ou groupes auxquels vous avez rendu visite ainsi que les noms des autres utilisateurs. Ils ne contiennent aucun message.", + "Failed to send logs: ": "Échec lors de l'envoi des journaux : ", "Notes:": "Notes :", - "Preparing to send logs": "Préparation d'envoi des rapports", + "Preparing to send logs": "Préparation d'envoi des journaux", "Missing roomId.": "Identifiant de salon manquant.", "Picture": "Image", "Popout widget": "Détacher le widget", @@ -1149,7 +1149,7 @@ "Always show encryption icons": "Toujours afficher les icônes de chiffrement", "Riot bugs are tracked on GitHub: create a GitHub issue.": "Les bugs de Riot sont suivis sur GitHub : créer un signalement GitHub.", "Log out and remove encryption keys?": "Se déconnecter et effacer les clés de chiffrement ?", - "Send Logs": "Envoyer les rapports", + "Send Logs": "Envoyer les journaux", "Clear Storage and Sign Out": "Effacer le stockage et se déconnecter", "Refresh": "Rafraîchir", "We encountered an error trying to restore your previous session.": "Une erreur est survenue lors de la récupération de la dernière session.", @@ -1279,5 +1279,20 @@ "%(senderName)s set the canonical address for this room to %(address)s.": "%(senderName)s a défini l'adresse canonique de ce salon comme %(address)s.", "%(senderName)s removed the canonical address for this room.": "%(senderName)s a supprimé l'adresse canonique de ce salon.", "%(senderName)s set the main address for this room to %(address)s.": "%(senderName)s à défini l'adresse principale pour ce salon comme %(address)s.", - "%(senderName)s removed the main address for this room.": "%(senderName)s a supprimé l'adresse principale de ce salon." + "%(senderName)s removed the main address for this room.": "%(senderName)s a supprimé l'adresse principale de ce salon.", + "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 utilise maintenant 3 à 5 fois moins de mémoire, en ne chargeant les informations des autres utilisateurs que quand elles sont nécessaires. Veuillez patienter pendant que l'on se resynchronise avec le serveur !", + "Updating Riot": "Mise à jour de Riot", + "Before submitting logs, you must create a GitHub issue to describe your problem.": "Avant de soumettre vos journaux, vous devez créer une « issue » sur GitHub pour décrire votre problème.", + "What GitHub issue are these logs for?": "Pour quelle « issue » Github sont ces journaux ?", + "

    HTML for your community's page

    \r\n

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

    \r\n

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

    \r\n": "

    HTML pour votre page de communauté

    \n

    \n Utilisez la description longue pour présenter la communauté aux nouveaux membres,\n ou fournir des liens importants\n

    \n

    \n Vous pouvez même utiliser des balises « img »\n

    \n", + "Submit Debug Logs": "Envoyer les journaux de débogage", + "An email address is required to register on this homeserver.": "Une adresse e-mail est nécessaire pour s'enregistrer sur ce serveur d'accueil.", + "A phone number is required to register on this homeserver.": "Un numéro de téléphone est nécessaire pour s'enregistrer sur ce serveur d'accueil.", + "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.": "Vous avez utilisé auparavant Riot sur %(host)s avec le chargement différé activé. Dans cette version le chargement différé est désactivé. Comme le cache local n'est pas compatible entre ces deux réglages, Riot doit resynchroniser votre compte.", + "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.": "Si l'autre version de Riot est encore ouverte dans un autre onglet, merci de le fermer car l'utilisation de Riot sur le même hôte avec le chargement différé activé et désactivé à la fois causera des problèmes.", + "Incompatible local cache": "Cache local incompatible", + "Clear cache and resync": "Vider le cache et resynchroniser", + "Please accept all of the policies": "Veuillez accepter toutes les politiques", + "Please review and accept the policies of this homeserver:": "Veuillez lire et accepter les politiques de ce serveur d'accueil :", + "Add some now": "En ajouter maintenant" } diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index 0d5dc9f457..05dc1d479c 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -1279,5 +1279,21 @@ "%(senderName)s set the canonical address for this room to %(address)s.": "%(senderName)s olvasható címet allított be a szobához: %(address)s.", "%(senderName)s removed the canonical address for this room.": "%(senderName)s törölte a szoba olvasható címét.", "%(senderName)s set the main address for this room to %(address)s.": "%(senderName)s elsődleges szoba címnek beállította: %(address)s.", - "%(senderName)s removed the main address for this room.": "A szoba elsődleges címét %(senderName)s törölte." + "%(senderName)s removed the main address for this room.": "A szoba elsődleges címét %(senderName)s törölte.", + "Before submitting logs, you must create a GitHub issue to describe your problem.": "Mielőtt a naplót elküldöd, egy Github jegyet kell nyitni amiben leírod a problémádat.", + "What GitHub issue are these logs for?": "Melyik Github jegyhez tartozik a napló?", + "Riot now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!": "3-, 5-ször kevesebb memóriát használ a Riot azzal, hogy csak akkor tölti be az információkat a felhasználókról amikor arra szükség van. Kérlek várd meg amíg újraszinkronizáljuk a szerverrel!", + "Updating Riot": "Riot frissítése", + "

    HTML for your community's page

    \r\n

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

    \r\n

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

    \r\n": "

    HTML a közösségi oldaladhoz

    \n

    \n Mutasd be a közösségedet az újoncoknak vagy ossz meg\n pár fontos linket\n

    \n

    \n Még „img” tag-et is használhatsz.\n

    \n", + "An email address is required to register on this homeserver.": "Erre a Matrix szerverre való regisztrációhoz az e-mail címet meg kell adnod.", + "A phone number is required to register on this homeserver.": "Erre a Matrix szerverre való regisztrációhoz a telefonszámot meg kell adnod.", + "Submit Debug Logs": "Hibakeresési napló elküldése", + "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.": "Előzőleg a szoba tagság késleltetett betöltésének engedélyével itt használtad a Riotot: %(host)s. Ebben a verzióban viszont a késleltetett betöltés nem engedélyezett. Mivel a két gyorsítótár nem kompatibilis egymással így Riotnak újra kell szinkronizálnia a fiókot.", + "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.": "Ha a másik Riot verzió fut még egy másik fülön, kérlek zárd be, mivel ha ugyanott használod a Riotot bekapcsolt késleltetett betöltéssel és kikapcsolva is akkor problémák adódhatnak.", + "Incompatible local cache": "A helyi gyorsítótár nem kompatibilis ezzel a verzióval", + "Clear cache and resync": "Gyorsítótár törlése és újraszinkronizálás", + "Please accept all of the policies": "Kérlek fogadd el a felhasználói feltételeket", + "Please review and accept the policies of this homeserver:": "Kérlek nézd át és fogadd el a Matrix szerver felhasználói feltételeit:", + "Add some now": "Adj hozzá párat", + "Joining room...": "Belépés a szobába.." } diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json index b9523b7a7c..962e179cc8 100644 --- a/src/i18n/strings/it.json +++ b/src/i18n/strings/it.json @@ -48,7 +48,7 @@ "Edit": "Modifica", "This email address is already in use": "Questo indirizzo e-mail è già in uso", "This phone number is already in use": "Questo numero di telefono è già in uso", - "Failed to verify email address: make sure you clicked the link in the email": "Impossibile verificare l'indirizzo e-mail: accertati di aver cliccato il link nella e-mail", + "Failed to verify email address: make sure you clicked the link in the email": "Impossibile verificare l'indirizzo e-mail: assicurati di aver cliccato il link nell'e-mail", "VoIP is unsupported": "VoIP non supportato", "You cannot place VoIP calls in this browser.": "Non puoi effettuare chiamate VoIP con questo browser.", "You cannot place a call with yourself.": "Non puoi chiamare te stesso.", @@ -95,10 +95,10 @@ "The version of Riot.im": "La versione di Riot.im", "Whether or not you're logged in (we don't record your user name)": "Se hai eseguito l'accesso o meno (non registriamo il tuo nome utente)", "Your language of choice": "La lingua scelta", - "Which officially provided instance you are using, if any": "Quale istanza fornita ufficialmente stai usando, se presente", - "Whether or not you're using the Richtext mode of the Rich Text Editor": "Se stai usando o meno la modalità Richtext dell'editor Rich Text", - "Your homeserver's URL": "L'URL del tuo homeserver", - "Your identity server's URL": "L'URL del tuo server di identità", + "Which officially provided instance you are using, if any": "Quale istanza ufficialmente fornita stai usando, se ne usi una", + "Whether or not you're using the Richtext mode of the Rich Text Editor": "Se stai usando o meno la modalità richtext dell'editor con testo arricchito", + "Your homeserver's URL": "L'URL del tuo server home", + "Your identity server's URL": "L'URL del tuo server identità", "Analytics": "Statistiche", "The information being sent to us to help make Riot.im better includes:": "Le informazioni inviate per aiutarci a migliorare Riot.im includono:", "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Se questa pagina include informazioni identificabili, come una stanza, utente o ID di gruppo, questi dati sono rimossi prima che vengano inviati al server.", @@ -1276,5 +1276,19 @@ "%(senderName)s removed %(count)s %(removedAddresses)s as addresses for this room.|other": "%(senderName)s ha rimosso %(removedAddresses)s tra gli indirizzi di questa stanza.", "%(senderName)s added %(addedAddresses)s and removed %(removedAddresses)s as addresses for this room.": "%(senderName)s ha aggiunto %(addedAddresses)s e rimosso %(removedAddresses)s tra gli indirizzi di questa stanza.", "%(senderName)s set the main address for this room to %(address)s.": "%(senderName)s ha messo %(address)s come indirizzo principale per questa stanza.", - "%(senderName)s removed the main address for this room.": "%(senderName)s ha rimosso l'indirizzo principale di questa stanza." + "%(senderName)s removed the main address for this room.": "%(senderName)s ha rimosso l'indirizzo principale di questa stanza.", + "Before submitting logs, you must create a GitHub issue to describe your problem.": "Prima di inviare i log, devi creare una segnalazione su GitHub per descrivere il tuo problema.", + "What GitHub issue are these logs for?": "Per quale segnalazione su GitHub sono questi log?", + "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 ora usa da 3 a 5 volte meno memoria, caricando le informazioni degli altri utenti solo quando serve. Si prega di attendere mentre ci risincronizziamo con il server!", + "Updating Riot": "Aggiornamento di Riot", + "

    HTML for your community's page

    \r\n

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

    \r\n

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

    \r\n": "

    HTML per la pagina della tua comunità

    \n

    \n Usa la descrizione estesa per introdurre i nuovi membri alla comunità, o distribuire alcuni link importanti\n

    \n

    \n Puoi anche usare i tag 'img'\n

    \n", + "Submit Debug Logs": "Invia log di debug", + "An email address is required to register on this homeserver.": "È necessario un indirizzo email per registrarsi in questo homeserver.", + "A phone number is required to register on this homeserver.": "È necessario un numero di telefono per registrarsi in questo homeserver.", + "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.": "Hai usato Riot precedentemente su %(host)s con il caricamento lento dei membri attivato. In questa versione il caricamento lento è disattivato. Dato che la cache locale non è compatibile tra queste due impostazioni, Riot deve risincronizzare il tuo account.", + "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.": "Se l'altra versione di Riot è ancora aperta in un'altra scheda, chiudila perchè usare Riot nello stesso host con il caricamento lento sia attivato che disattivato può causare errori.", + "Incompatible local cache": "Cache locale non compatibile", + "Clear cache and resync": "Svuota cache e risincronizza", + "Please accept all of the policies": "Si prega di accettare tutte le condizioni", + "Please review and accept the policies of this homeserver:": "Consulta ed accetta le condizioni di questo homeserver:" } diff --git a/src/i18n/strings/jbo.json b/src/i18n/strings/jbo.json new file mode 100644 index 0000000000..8286a3f70b --- /dev/null +++ b/src/i18n/strings/jbo.json @@ -0,0 +1,315 @@ +{ + "This email address is already in use": ".i ca'o pilno le ve samymri", + "This phone number is already in use": ".i ca'o pilno le fonjudri", + "Failed to verify email address: make sure you clicked the link in the email": ".i na pu facki lo du'u xu kau do ponse le skami te mrilu .i ko birti lo du'u do pu skami cuxna le urli pe le se samymri", + "The platform you're on": "le ciste poi do pilno", + "The version of Riot.im": "le farvi tcini be la nu zunti", + "Whether or not you're logged in (we don't record your user name)": "lo du'u xu kau do cmisau to na vreji le do plicme toi", + "Your language of choice": "le se cuxna be fi lo'i bangu", + "Which officially provided instance you are using, if any": "le klesi poi ca'irselzau se sabji poi do pilno", + "Whether or not you're using the Richtext mode of the Rich Text Editor": "lo du'u xu kau do pilno la .markdaun. lo nu ciski", + "Your homeserver's URL": "le urli be le do samtcise'u", + "Your identity server's URL": "le urli be le do prenu datni samtcise'u", + "e.g. %(exampleValue)s": "mu'a zoi gy. %(exampleValue)s .gy.", + "Every page you use in the app": "ro lo pagbu poi do pilno pe le samtci", + "e.g. ": "mu'a zoi urli. .urli", + "Your User Agent": "le datni be lo do kibyca'o", + "Your device resolution": "le ni vidnysle", + "Analytics": "lo se lanli datni", + "The information being sent to us to help make Riot.im better includes:": ".i ti liste lo datni poi se dunda fi lo favgau te zu'e lo nu xagzengau la nu zunti", + "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": ".i pu lo nu benji fi lo samtcise'u cu vimcu lo datni poi termi'u no'u mu'a lo termi'u be lo kumfa pe'a .o nai lo pilno .o nai lo girzu", + "Call Failed": ".i pu fliba lo nu fonjo'e", + "There are unknown devices in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.": ".i da poi no'e slabu samtciselse'u cu zvati le kumfa pe'a .i je lo nu lo drata cu tirna lo nu fonjo'e cu cumki lo nu do na'e lacri da", + "Review Devices": "za'u re'u viska lo liste be lo samtciselse'u", + "Call Anyway": "je'e fonjo'e", + "Answer Anyway": "je'e spuda", + "Call": "fonjo'e", + "Answer": "spuda", + "You are already in a call.": ".i do ca'o pu zvati lo nu fonjo'e", + "VoIP is unsupported": ".i na kakne tu'a la .voip.", + "You cannot place VoIP calls in this browser.": ".i le kibyca'o na kakne tu'a la .voip.", + "You cannot place a call with yourself.": ".i lo nu do fonjo'e do na cumki", + "Call in Progress": ".i ca'o nu fonjo'e", + "A call is currently being placed!": ".i pu'o nu fonjo'e", + "A call is already in progress!": ".i ca'o drata nu fonjo'e", + "Permission Required": ".i do notci lo nu curmi", + "You do not have permission to start a conference call in this room": ".i na curmi lo nu do co'a nunjmaji fonjo'e ne'i le kumfa pe'a", + "The file '%(fileName)s' failed to upload": ".i pu fliba lo nu kibdu'a la'o ly. %(fileName)s .ly.", + "The file '%(fileName)s' exceeds this home server's size limit for uploads": ".i le datnyvei no'u la'o ly. %(fileName)s .ly. zmadu lo jimte be lo se kibdu'a bei lo ka barda be'o pe le samtcise'u", + "Upload Failed": ".i pu fliba lo nu kibdu'a", + "Failure to create room": ".i fliba lo nu zbasu lo kumfa pe'a", + "Call Timeout": ".i mutce temci lo nu co'a fonjo'e", + "The remote side failed to pick up": ".i lo se fonjo'e na pu spuda", + "Unable to capture screen": ".i na kakne lo nu benji lo vidvi be lo vidni", + "Existing Call": ".i ca'o pu fonjo'e", + "Could not connect to the integration server": ".i na kakne lo nu co'a samjo'e le jmina samtcise'u", + "A conference call could not be started because the intgrations server is not available": ".i na kakne lo nu co'a jmaji fonjo'e kei ri'a lo nu na kakne lo nu co'a samjo'e le jmina samtcise'u", + "Server may be unavailable, overloaded, or you hit a bug.": ".i la'a cu'i lo samtcise'u cu spofu gi'a mutce gunka .i ja samcfi", + "Send anyway": "je'e benji", + "Send": "benji", + "Sun": "nondei", + "Mon": "pavdei", + "Tue": "reldei", + "Wed": "cibdei", + "Thu": "vondei", + "Fri": "mumdei", + "Sat": "xavdei", + "Jan": "pa", + "Feb": "re", + "Mar": "ci", + "Apr": "vo", + "May": "mu", + "Jun": "xa", + "Jul": "ze", + "Aug": "bi", + "Sep": "so", + "Oct": "pa no", + "Nov": "pa pa", + "Dec": "pa re", + "PM": "su'i pa re", + "AM": "su'i no", + "%(weekDayName)s %(time)s": "de'i lo %(weekDayName)s ti'u li %(time)s", + "%(weekDayName)s, %(monthName)s %(day)s %(time)s": "de'i li %(day)s pi'e %(monthName)s noi %(weekDayName)s ge'u ti'u li %(time)s", + "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "de'i li %(day)s pi'e %(monthName)s pi'e %(fullYear)s noi %(weekDayName)s", + "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "de'i li %(day)s pi'e %(monthName)s pi'e %(fullYear)s noi %(weekDayName)s ge'u ti'u li %(time)s", + "Who would you like to add to this community?": ".i do djica lo nu jmina ma le girzu", + "Warning: any person you add to a community will be publicly visible to anyone who knows the community ID": ".i ju'i lo djuno be lo judri be lo girzu cu kakne lo nu viska lo liste be ro lo prenu poi se jmina do gy.", + "Invite new community members": "vi'ecpe lo prenu poi cnino le girzu", + "Name or matrix ID": "lo cmene .o nai lo judri be fi la nacmeimei", + "Invite to Community": "vi'ecpe fi le girzu", + "Which rooms would you like to add to this community?": ".i do djica lo nu jmina ma poi kumfa pe'a po'u le girzu", + "Show these rooms to non-members on the community page and room list?": ".i .au pei le kumfa cu gubni zvati le girzu pagbu .e le liste be lo'i kumfa pe'a", + "Add rooms to the community": "jmina lo kumfa pe'a le girzu", + "Room name or alias": "lo cmene ja datcme be lo kumfa", + "Add to community": "jmina fi le girzu", + "Failed to invite the following users to %(groupId)s:": "lo pilno poi fliba lo nu vi'ecpe ke'a la'o ny. %(groupId)s .ny.", + "Failed to invite users to community": ".i pu fliba lo nu vi'ecpe lo pilno le girzu", + "Failed to invite users to %(groupId)s": ".i pu fliba lo nu vi'ecpe lo pilno la'o ny. %(groupId)s .ny.", + "Failed to add the following rooms to %(groupId)s:": "lo kumfa pe'a poi fliba lo nu jmina ke'a la'o ny. %(groupId)s .ny.", + "Unnamed Room": "lo kumfa pe'a noi no da cmene", + "Riot does not have permission to send you notifications - please check your browser settings": ".i na curmi lo nu la nu zunti cu benji lo sajgau do .i .e'o do cipcta lo te cuxna pe le do kibyca'o", + "Riot was not given permission to send notifications - please try again": ".i na pu curmi lo nu la nu zunti cu benji lo sajgau .i .e'o do za'u re'u troci", + "Unable to enable Notifications": ".i na kakne lo nu co'a kakne lo nu benji lo sajgau", + "This email address was not found": ".i na pu facki fi le ve samymri", + "Your email address does not appear to be associated with a Matrix ID on this Homeserver.": ".i za'a le ve samymri be fo do cu ckini no lo judri be fi la nacmeimei be'o pe le samtcise'u", + "Registration Required": ".i .ei do se cmeveigau", + "You need to register to do this. Would you like to register now?": ".i lo nu cmeveigau do sarcu ti .i do ca .au pei cmeveigau do", + "Register": "cmeveigau", + "Default": "lo zmiselcu'a", + "Restricted": "li so'u", + "Moderator": "li so'i", + "Admin": "li ro", + "Start a chat": "lo nu co'a tavla", + "Who would you like to communicate with?": ".i .au dai do tavla ma", + "Email, name or matrix ID": "lo ve samymri .o nai lo cmene .o nai lo judri be fi la nacmeimei", + "Start Chat": "co'a tavla", + "Invite new room members": "vi'ecpe lo cnino prenu", + "Who would you like to add to this room?": ".i .au dai do jmina ma le kumfa pe'a", + "Send Invites": "mrilu lo ve vi'ecpe", + "Power level must be positive integer.": ".i .ei lo ni vlipa cu kacna'u", + "%(senderName)s changed the power level of %(powerLevelDiffText)s.": ".i la'o ly. %(senderName)s .ly. gafygau %(powerLevelDiffText)s", + "Failed to change power level": ".i pu fliba lo nu gafygau lo ni vlipa", + "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "lo ni la'o ny. %(userId)s .ny. vlipa noi pu du %(fromPowerLevel)s ku %(toPowerLevel)s", + "Failed to invite user": ".i pu fliba lo nu vi'ecpe le pilno", + "Operation failed": ".i pu fliba", + "Failed to invite": ".i pu fliba lo nu vi'ecpe", + "Failed to invite the following users to the %(roomName)s room:": "lo pilno poi fliba lo nu vi'ecpe ke'a la'o ly. %(roomName)s .ly. noi kumfa pe'a", + "You need to be logged in.": ".i .ei do cmisau", + "You need to be able to invite users to do that.": ".i lo nu do kakne lo nu vi'ecpe lo pilno cu sarcu ta", + "Unable to create widget.": ".i na kakne lo nu zbasu lo uidje", + "Missing roomId.": ".i claxu lo judri be lo kumfa pe'a", + "Failed to send request.": ".i pu fliba lo nu benji lo ve cpedu", + "This room is not recognised.": ".i na sanji le kumfa pe'a", + "You are not in this room.": ".i do na zvati le kumfa pe'a", + "You do not have permission to do that in this room.": ".i ne'i le kumfa pe'a na curmi ta poi do troci", + "Missing room_id in request": ".i lo ve cpedu cu claxu lo judri be lo kumfa pe'a", + "Room %(roomId)s not visible": ".i na kakne lo nu viska la'o ly. %(roomId)s .ly. noi kumfa pe'a", + "Missing user_id in request": ".i lo ve cpedu cu claxu lo judri be lo pilno", + "Usage": "lo tadji be lo nu pilno", + "Searches DuckDuckGo for results": ".i sisku se pi'o la datkysisku", + "/ddg is not a command": "zoi ny. /ddg .ny. na nu minde", + "Changes your display nickname": ".i galfi le do cmene", + "Changes colour scheme of current room": ".i gafygau lo se skari be le kumfa pe'a", + "Sets the room topic": ".i ninga'igau lo se casnu pe le kumfa pe'a", + "Invites user with given id to current room": ".i vi'ecpe lo pilno poi se judri ti ku le kumfa pe'a", + "Joins room with given alias": ".i drata judri le kumfa pe'a", + "Leave room": "cliva le kumfa pe'a", + "Unrecognised room alias:": "lo drata judri poi na se sanji", + "Kicks user with given id": ".i rinka lo nu lo pilno poi se judri ti cu cliva", + "Bans user with given id": ".i rinka lo nu lo pilno poi se judri ti cu vitno cliva", + "Unbans user with given id": ".i xruti fo lo nu lo pilno poi se judri ti cu vitno cliva", + "Ignores a user, hiding their messages from you": ".i rinka lo nu no'e jundi lo pilno gi'e mipri lo notci be fi py. do", + "Ignored user": ".i do no'e jundi le pilno", + "You are now ignoring %(userId)s": ".i do ca no'e jundi la'o ny. %(userId)s .ny.", + "Stops ignoring a user, showing their messages going forward": ".i sisti lo nu no'e jundi lo pilno gi'e mipri lo notci be fi py. do", + "Unignored user": ".i do sisti lo nu no'e jundi le pilno", + "You are no longer ignoring %(userId)s": ".i do ca sisti lo nu no'e jundi la'o ny. %(userId)s .ny.", + "Define the power level of a user": ".i ninga'igau lo ni lo pilno cu vlipa", + "Deops user with given id": ".i xruti lo ni lo pilno poi se judri ti cu vlipa", + "Opens the Developer Tools dialog": ".i samymo'i lo favgau se pilno uidje", + "Verifies a user, device, and pubkey tuple": ".i xusra lo du'u do lacri lo pilno joi lo samtciselse'u joi lo gubni termifckiku", + "Unknown (user, device) pair:": "lo pilno ce'o lo samtciselse'u vu'o poi na te djuno", + "Device already verified!": ".i do ca'o pu lacri le samtciselse'u", + "WARNING: Device already verified, but keys do NOT MATCH!": ".i ju'i cai do ca'o pu lacri le samtciselse'u .i je ku'i lo termifckiku ba'e na mapti", + "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!": ".i ju'i cai pu fliba lo nu lacri lo termifckiku .i zoi ny. %(fprint)s .ny. noi se ponse la'o ny. %(userId)s .ny. .e la'o ny. %(deviceId)s .ny. noi samtciselse'u cu termi'u termifckiku gi'e na mapti le termifckiku poi do dunda no'u zoi ny. %(fingerprint)s .ny. .i la'a cu'i lo drata ju'i prenu cu tcidu lo se mrilu be do", + "Verified key": "lo termifckiku poi se lacri", + "The signing key you provided matches the signing key you received from %(userId)s's device %(deviceId)s. Device marked as verified.": ".i lo termi'u termifckiku poi do dunda cu mapti lo termi'u termifckiku poi do te benji la'o ny. %(deviceId)s .ny. noi samtciselse'u po'e la'o ny. %(userId)s .ny. .i do co'a lacri le samtciselse'u", + "Displays action": ".i mrilu lo nu do gasnu", + "Forces the current outbound group session in an encrypted room to be discarded": ".i macnu vimcu lo ca barkla termifckiku gunma lo kumfa pe'a poi mifra", + "Unrecognised command:": "lo se minde poi na te djuno", + "Reason": "lo krinu", + "%(targetName)s accepted the invitation for %(displayName)s.": ".i la'o ly. %(targetName)s .ly. fitytu'i lo ve vi'ecpe be fi la'o ly. %(displayName)s .ly.", + "%(targetName)s accepted an invitation.": ".i la'o ly. %(targetName)s .ly. fitytu'i lo ve vi'ecpe", + "%(senderName)s requested a VoIP conference.": ".i la'o ly. %(senderName)s .ly. cpedu lo .voip. zei nunjmaji", + "%(senderName)s invited %(targetName)s.": ".i la'o ly. %(senderName)s .ly. vi'ecpe la'o ly. %(targetName)s .ly.", + "%(senderName)s banned %(targetName)s.": ".i la'o ly. %(senderName)s .ly. gasnu lo nu la'o ly. %(targetName)s .ly. vitno cliva", + "%(oldDisplayName)s changed their display name to %(displayName)s.": ".i la'o ly. %(oldDisplayName)s .ly. gafygau lo cmene be ri zoi ly. %(displayName)s .ly.", + "%(senderName)s set their display name to %(displayName)s.": ".i la'o ny. %(senderName)s .ny. jmina lo cmene be ri be'o no'u zoi ly. %(displayName)s .ly.", + "%(senderName)s removed their display name (%(oldDisplayName)s).": ".i la'o ny. %(senderName)s .ny. vimcu lo cmene be ri be'o no'u zoi ly. %(oldDisplayName)s .ly.", + "%(senderName)s removed their profile picture.": ".i la'o ly. %(senderName)s .ly. vimcu lo predatni pixra pe ri", + "%(senderName)s changed their profile picture.": ".i la'o ly. %(senderName)s .ly. gafygau lo predatni pixra pe ri", + "%(senderName)s set a profile picture.": ".i la'o ly. %(senderName)s .ly. jmina lo predatni pixra pe ri", + "VoIP conference started.": ".i co'a .voip. zei nunjmaji", + "%(targetName)s joined the room.": ".i la'o ly. %(targetName)s .ly. binxo lo cmima be le kumfa pe'a", + "VoIP conference finished.": ".i mo'u .voip. zei nunjmaji", + "%(targetName)s rejected the invitation.": ".i la'o ly. %(targetName)s .ly. fitytoltu'i lo ve vi'ecpe", + "%(targetName)s left the room.": ".i la'o ly. %(targetName)s .ly. cliva le kumfa pe'a", + "%(senderName)s unbanned %(targetName)s.": ".i la'o ly. %(senderName)s .ly. xruti fo lo nu la'o ly. %(targetName)s .ly. vitno cliva", + "%(senderName)s kicked %(targetName)s.": ".i la'o ly. %(senderName)s .ly. gasnu lo nu la'o ly. %(targetName)s .ly. cliva", + "%(senderName)s withdrew %(targetName)s's invitation.": ".i la'o ly. %(senderName)s .ly. lebna lo ve vi'ecpe be la'o ly. %(targetName)s .ly.", + "%(senderDisplayName)s changed the topic to \"%(topic)s\".": ".i la'o ly. %(senderDisplayName)s .ly. gafygau lo se casnu pe le kumfa pe'a zoi ly. %(topic)s .ly.", + "%(senderDisplayName)s removed the room name.": ".i la'o ly. %(senderDisplayName)s .ly. vimcu lo cmene be le kumfa pe'a", + "%(senderDisplayName)s changed the room name to %(roomName)s.": ".i la'o ly. %(senderDisplayName)s .ly. gafygau lo cmene be le kumfa zoi ly. %(roomName)s .ly.", + "%(senderDisplayName)s sent an image.": ".i la'o ly. %(senderDisplayName)s .ly. mrilu lo pixra", + "%(senderName)s added %(count)s %(addedAddresses)s as addresses for this room.|other": ".i la'o ly. %(senderName)s .ly. jmina zoi ny. %(addedAddresses)s .ny. lo'i judri be le kumfa pe'a", + "%(senderName)s added %(count)s %(addedAddresses)s as addresses for this room.|one": ".i la'o ly. %(senderName)s .ly. jmina zoi ny. %(addedAddresses)s .ny. lo'i judri be le kumfa pe'a", + "%(senderName)s removed %(count)s %(removedAddresses)s as addresses for this room.|other": ".i la'o ly. %(senderName)s .ly. vimcu zoi ny. %(removedAddresses)s .ny. lo'i judri be le kumfa pe'a", + "%(senderName)s removed %(count)s %(removedAddresses)s as addresses for this room.|one": ".i la'o ly. %(senderName)s .ly. vimcu zoi ny. %(removedAddresses)s .ny. lo'i judri be le kumfa pe'a", + "%(senderName)s added %(addedAddresses)s and removed %(removedAddresses)s as addresses for this room.": ".i la'o ly. %(senderName)s .ly. jmina zoi ny. %(addedAddresses)s .ny. lo'i judri be le kumfa pe'a gi'e vimcu zoi ny. %(removedAddresses)s .ny. jy.", + "%(senderName)s set the main address for this room to %(address)s.": ".i la'o ly. %(senderName)s .ly. gafygau lo ralju cmene be le kumfa pe'a zoi ny. %(address)s .ny.", + "%(senderName)s removed the main address for this room.": ".i la'o ly. %(senderName)s .ly. vimcu lo ralju cmene be le kumfa pe'a", + "Someone": "da poi prenu", + "(not supported by this browser)": "to le do kibyca'o na kakne toi", + "%(senderName)s answered the call.": ".i la'o ly. %(senderName)s .ly. spuda lo nu fonjo'e", + "(could not connect media)": "to na kakne lo nu ganvi samjongau toi", + "(no answer)": "to na spuda toi", + "(unknown failure: %(reason)s)": "to na'e te djuno nu fliba fi'o ve skicu zoi gy. %(reason)s .gy. toi", + "%(senderName)s ended the call.": ".i la'o ly. %(senderName)s .ly. sisti lo nu fonjo'e", + "%(senderName)s placed a %(callType)s call.": ".i la'o ly. %(senderName)s .ly. co'a %(callType)s fonjo'e", + "%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.": ".i la'o ly. %(senderName)s .ly. vi'ecpe la'o ly. %(targetDisplayName)s .ly. le kumfa pe'a", + "%(senderName)s made future room history visible to all room members, from the point they are invited.": ".i la'o ly. %(senderName)s .ly. gasnu lo nu ro lo cmima ka'e viska ro lo notci be ba lo mu'e cy. se vi'ecpe", + "%(senderName)s made future room history visible to all room members, from the point they joined.": ".i la'o ly. %(senderName)s .ly. gasnu lo nu ro lo cmima ka'e viska ro lo notci be ba lo mu'e cy. cmibi'o", + "%(senderName)s made future room history visible to all room members.": ".i la'o ly. %(senderName)s .ly. gasnu lo nu ro lo cmima ka'e viska ro lo ba notci", + "%(senderName)s made future room history visible to anyone.": ".i la'o ly. %(senderName)s .ly. gasnu lo nu ro lo prenu ka'e viska ro lo ba notci", + "%(senderName)s made future room history visible to unknown (%(visibility)s).": ".i la'o ly. %(senderName)s .ly. gasnu lo nu zo'e ka'e viska lo notci to cuxna zoi ny. %(visibility)s .ny. toi", + "%(senderName)s turned on end-to-end encryption (algorithm %(algorithm)s).": ".i gau la'o ly. %(senderName)s .ly. co'a mulno mifra fi la'o ny. %(algorithm)s .ny.", + "%(senderName)s changed the pinned messages for the room.": ".i la'o ly. %(senderName)s .ly. gafygau lo vitno notci pe le kumfa pe'a", + "%(widgetName)s widget modified by %(senderName)s": ".i la'o ly. %(senderName)s .ly. gafygau la'o ny. %(widgetName)s .ny. noi uidje", + "%(widgetName)s widget added by %(senderName)s": ".i la'o ly. %(senderName)s .ly. jmina la'o ny. %(widgetName)s .ny. noi uidje", + "%(widgetName)s widget removed by %(senderName)s": ".i la'o ly. %(senderName)s .ly. vimcu la'o ny. %(widgetName)s .ny. noi uidje", + "%(displayName)s is typing": ".i la'o ly. %(displayName)s .ly. ca'o ciska", + "%(names)s and %(count)s others are typing|other": ".i la'o ly. %(names)s .ly. .e %(count)s lo drata ca'o ciska", + "%(names)s and %(count)s others are typing|one": ".i la'o ly. %(names)s .ly. .e pa lo drata ca'o ciska", + "%(names)s and %(lastPerson)s are typing": ".i la'o ly. %(names)s .ly. .e la'o ly. %(lastPerson)s .ly. ca'o ciska", + "This homeserver has hit its Monthly Active User limit.": ".i le samtcise'u cu bancu lo masti jimte be ri bei lo ni ca'o pilno", + "This homeserver has exceeded one of its resource limits.": ".i le samtcise'u cu bancu pa lo jimte be ri", + "Please contact your service administrator to continue using the service.": ".i .e'o ko tavla lo do te selfu admine .i ja nai do djica lo nu ca'o pilno le te selfu", + "Unable to connect to Homeserver. Retrying...": ".i pu fliba lo nu samjo'e le samtcise'u .i za'u re'u ca'o troci", + "Your browser does not support the required cryptography extensions": ".i le do kibyca'o na kakne tu'a le te mifra ciste noi se nitcu", + "Not a valid Riot keyfile": ".i na'e drani ckiku datnyvei", + "Authentication check failed: incorrect password?": ".i pu fliba lo nu birti lo du'u curmi lo nu do jonse .i na'e drani xu japyvla", + "Sorry, your homeserver is too old to participate in this room.": ".i .uu le do samtcise'u cu dukse lo ka laldo ku ja'e lo du'u sy. na kakne lo nu pagbu le kumfa pe'a", + "Please contact your homeserver administrator.": ".i .e'o ko tavla lo admine be le samtcise'u", + "Failed to join room": ".i pu fliba lo nu cmibi'o le kumfa pe'a", + "Message Pinning": "lo du'u xu kau kakne lo nu mrilu lo vitno notci", + "Increase performance by only loading room members on first view": "lo du'u xu kau zenba lo ka sutra ku ta'i lo nu samymo'i lo cmima be lo kumfa pe'a ba po'o lo nu viska cy.", + "Disable Emoji suggestions while typing": "lo du'u xu kau na stidi lo pixra lerfu ca lo nu ciska", + "Use compact timeline layout": "lo du'u xu kau lo liste be lo notci cu tagji", + "Hide removed messages": "lo du'u xu kau mipri lo notci poi se vimcu", + "Hide join/leave messages (invites/kicks/bans unaffected)": "lo du'u xu kau mipri lo cmibi'o ja cliva notci to na mipri lo vi'ecpe ja gasnu bo cliva notci toi", + "Hide avatar changes": "lo du'u xu kau mipri lo nu galfi lo predatni pixra", + "Hide display name changes": "lo du'u xu kau mipri lo nu galfi lo cmene", + "Hide read receipts": "lo du'u xu kau mipri lo te benji datni", + "Show timestamps in 12 hour format (e.g. 2:30pm)": "lo du'u xu kau lo tcika cu se tarmi mu'a lu ti'u li re pi'e ci no su'i pa re li'u", + "Always show message timestamps": "lo du'u xu kau do ro roi viska ka'e lo tcika be tu'a lo notci", + "Autoplay GIFs and videos": "lo du'u xu kau lo vidvi cu zmiku cfari", + "Always show encryption icons": "lo du'u xu kau jarco ro lo ka mifra", + "Enable automatic language detection for syntax highlighting": "lo du'u xu kau zmiku facki lo du'u ma kau bangu ku te zu'e lo nu skari ba'argau lo gensu'a", + "Hide avatars in user and room mentions": "lo du'u xu kau mipri lo pixra pe lo nu casnu lo pilno .a lo kumfa pe'a", + "Disable big emoji in chat": "lo du'u xu kau lo pixra lerfu poi cmalu cu basti lo pixra lerfu poi barda", + "Don't send typing notifications": "lo du'u xu kau na benji lo datni be lo nu ciska", + "Automatically replace plain text Emoji": "lo du'u xu kau zmiku basti lo cinmo lerpoi", + "Mirror local video feed": "lo du'u xu kau minra lo diklo vidvi", + "Disable Community Filter Panel": "lo du'u xu kau na viska le girzu cuxselgre uidje", + "Disable Peer-to-Peer for 1:1 calls": "lo du'u xu kau na sirji samjo'e ca lo nu pa da fonjo'e pa de", + "Send analytics data": "lo du'u xu kau benji lo se lanli datni", + "Never send encrypted messages to unverified devices from this device": "lo du'u xu kau no roi benji lo notci poi mifra ku lo samtciselse'u poi na'e lacri ku ti poi samtciselse'u", + "Never send encrypted messages to unverified devices in this room from this device": "lo du'u xu kau no roi benji lo notci poi mifra ku lo samtciselse'u poi na'e lacri poi zvati le kumfa pe'a ku'o ti poi samtciselse'u", + "Enable inline URL previews by default": "lo zmiselcu'a pe lo du'u xu kau zmiku purzga lo se urli", + "Enable URL previews for this room (only affects you)": "lo du'u xu kau do zmiku purzga lo se urli ne'i le kumfa pe'a", + "Enable URL previews by default for participants in this room": "lo zmiselcu'a pe lo du'u xu kau lo cmima be le kumfa pe'a cu zmiku purzga lo se urli", + "Room Colour": "lo se skari be le kumfa pe'a", + "Enable widget screenshots on supported widgets": "lo du'u xu kau kakne lo nu co'a pixra lo uidje kei lo nu kakne tu'a .ubu", + "Show empty room list headings": "lo du'u xu kau viska lo tcita be lo liste be lo kumfa pe'a be'o poi kunti ca lo nu cuxselgre", + "Collecting app version information": ".i ca'o crepu lo datni be lo favytcinymupli", + "Collecting logs": ".i ca'o crepu lo vreji", + "Uploading report": ".i ca'o kibdu'a lo datnynoi", + "Waiting for response from server": ".i ca'o denpa lo nu le samtcise'u cu spuda", + "Messages containing my display name": "lo notci poi vasru lo cmene be mi", + "Messages containing my user name": "lo notci poi vasru lo plicme be mi", + "Messages in one-to-one chats": "lo notci be fi pa lo prenu bei pa lo prenu", + "Messages in group chats": "lo notci pe lo girzu tavla", + "When I'm invited to a room": "lo nu vi'ecpe mi lo kumfa pe'a", + "Call invitation": "lo nu vi'ecpe mi lo nu fonjo'e", + "Messages sent by bot": "lo notci be fi lo sampre", + "Active call (%(roomName)s)": "le ca fonjo'e ne la'o ly. %(roomName)s .ly.", + "unknown caller": "lo fonjo'e noi na'e te djuno", + "Incoming voice call from %(name)s": ".i la'o ly. %(name)s .ly. ca'o snavi fonjo'e", + "Incoming video call from %(name)s": ".i la'o ly. %(name)s .ly. ca'o vidvi fonjo'e", + "Incoming call from %(name)s": ".i la'o ly. %(name)s .ly. ca'o fonjo'e", + "Decline": "fitytoltu'i", + "Accept": "fitytu'i", + "Error": "lo se srera", + "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains": ".i pu mrilu fi lo se fonjudri be zoi fy. +%(msisdn)s .fy. fu la .symysys. .i .e'o ko ciska lo lacri lerpoi po le se mrilu", + "Incorrect verification code": ".i na'e drani ke lacri lerpoi", + "Enter Code": ".i ko ciska le lerpoi", + "Submit": "benji", + "Phone": "lo fonxa", + "Add phone number": "lo fonjudri", + "Add": "jmina", + "Failed to upload profile picture!": ".i pu fliba lo nu kibdu'a lo predatni pixra", + "No display name": ".i no da cmene", + "New passwords don't match": ".i le'i japyvla poi cnino na simxu lo nu mintu", + "Passwords can't be empty": ".i lu li'u .e'a nai japyvla", + "Warning!": ".i ju'i", + "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.": ".i lo nu galfi lo japyvla cu rinka lo nu galfi ro lo termifckiku pe lo samtciselse'u kei .e lo nu na kakne lo nu tolmifygau .i ja do barbei lo do kumfa pe'a termifckiku gi'e ba nerbei ri .i ta'o le ti pruce ba zenba lo ka frili", + "Export E2E room keys": "barbei lo kumfa pe'a termifckiku", + "Continue": "", + "Do you want to set an email address?": ".i .au pei do jmina lo te samymri", + "Current password": "lo ca japyvla", + "Password": "lo japyvla", + "New Password": "lo japyvla poi cnino", + "Confirm password": "lo za'u re'u japyvla poi cnino", + "Change Password": "galfi lo japyvla", + "Your home server does not support device management.": ".i le do samtcise'u na kakne lo nu jitro lo samtciselse'u", + "Unable to load device list": ".i na kakne lo nu samymo'i lo liste be lo'i samtciselse'u", + "Authentication": "lo nu facki lo du'u do du ma kau", + "Delete %(count)s devices|other": "vimcu %(count)s lo samtciselse'u", + "Delete %(count)s devices|one": "vimcu le samtciselse'u", + "Device ID": "lo judri be lo samtciselse'u", + "Device Name": "lo cmene be lo samtciselse'u", + "Last seen": "lo ro re'u nu viska", + "Select devices": "lo du'u xu kau cuxna lo samtciselse'u", + "Failed to set display name": ".i pu fliba lo nu galfi lo cmene", + "Disable Notifications": "na sajgau", + "Enable Notifications": "sajgau", + "Error saving email notification preferences": ".i pu fliba lo nu co'a vreji lo se cuxna pe lo nu samymri", + "An error occurred whilst saving your email notification preferences.": ".i pu fliba lo nu co'a vreji lo se cuxna pe lo nu samymri sajgau", + "Keywords": "lo midvla", + "Enter keywords separated by a comma:": ".i ko ciska lo midvla ta'i lo nu sepli fi lo lerkoma", + "OK": "je'e", + "Failed to change settings": ".i pu fliba lo nu galfi lo se cuxna", + "Can't update user notification settings": ".i pu fliba lo nu galfi lo se cuxna pe lo nu sajgau", + "Failed to update keywords": ".i pu fliba lo nu galfi lo midvla", + "Messages containing keywords": "lo notci poi vasru lo midvla" +} diff --git a/src/i18n/strings/ko.json b/src/i18n/strings/ko.json index cad2a0b441..be87edcdfa 100644 --- a/src/i18n/strings/ko.json +++ b/src/i18n/strings/ko.json @@ -541,7 +541,7 @@ "File to import": "가져올 파일", "You must join the room to see its files": "파일을 보려면 방에 들어가야만 해요", "Reject all %(invitedRooms)s invites": "모든 %(invitedRooms)s의 초대를 거절하기", - "Start new chat": "새로 대화하기", + "Start new chat": "새 대화 시작하기", "Failed to invite": "초대하지 못했습니다.", "Failed to invite user": "사용자를 초대하지 못했습니다.", "Failed to invite the following users to the %(roomName)s room:": "다음 사용자들을 %(roomName)s 방으로 초대하지 못했습니다:", @@ -902,7 +902,7 @@ "%(senderName)s sent a video": "%(senderName)s가 비디오를 보냈습니다", "%(senderName)s uploaded a file": "%(senderName)s가 파일을 보냈습니다", "Key request sent.": "키 요청을 보냈습니다.", - "If your other devices do not have the key for this message you will not be able to decrypt them.": "다른 기기에", + "If your other devices do not have the key for this message you will not be able to decrypt them.": "당신의 다른 기기에 이 메시지를 읽기 위한 키가 없다면 메시지를 해독할 수 없을 겁니다.", "Encrypting": "암호화 중", "Encrypted, not sent": "암호화 됨, 보내지지 않음", "Disinvite this user?": "이 사용자에 대한 초대를 취소할까요?", @@ -975,15 +975,15 @@ "You are trying to access a room.": "방에 접근하고 있습니다.", "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 main address, you must be a": "방의 매인 주소를 바꾸려면, -여야 합니다.", - "Members only (since they joined)": "구성원만(??한 시점부터)", + "To change the room's main address, you must be a": "방의 메인 주소를 바꾸려면, -여야 합니다.", + "Members only (since they joined)": "구성원만(구성원들이 참여한 시점부터)", "%(severalUsers)sjoined %(count)s times|one": "%(severalUsers)s님이 들어왔습니다", "%(severalUsers)sjoined %(count)s times|other": "%(severalUsers)s님이 %(count)s번 들어왔습니다", "%(oneUser)sjoined %(count)s times|other": "%(oneUser)s님이 %(count)s번 들어왔습니다", "%(oneUser)sjoined %(count)s times|one": "%(oneUser)s님이 들어왔습니다", "%(severalUsers)sjoined and left %(count)s times|other": "%(severalUsers)s님이 %(count)s번 들어왔다가 나갔습니다", "%(severalUsers)sjoined and left %(count)s times|one": "%(severalUsers)s님이 들어왔다가 나갔습니다", - "%(oneUser)sjoined and left %(count)s times|other": "%(oneUser)님이 %(count)s번 들어왔다가 나갔습니다", + "%(oneUser)sjoined and left %(count)s times|other": "%(oneUser)s님이 %(count)s번 들어왔다가 나갔습니다", "%(oneUser)sjoined and left %(count)s times|one": "%(oneUser)s님이 들어왔다가 나갔습니다", "%(severalUsers)sleft and rejoined %(count)s times|one": "%(severalUsers)s님이 나갔다가 다시 들어왔습니다", "%(oneUser)sleft and rejoined %(count)s times|other": "%(oneUser)s님이 %(count)s번 나갔다가 다시 들어왔습니다", @@ -1077,7 +1077,7 @@ "Status.im theme": "Status.im식 테마", "A text message has been sent to %(msisdn)s": "%(msisdn)s님에게 문자 메시지를 보냈습니다.", "Something went wrong when trying to get your communities.": "커뮤니티를 받는 중에 뭔가 잘못됐습니다.", - "Please help improve Riot.im by sending anonymous usage data. This will use a cookie.": "익명으로 사용 데이터를 보내 Riot의 발전을 도와주세요. 이 과정에서 쿠키를 사용합니다.", + "Please help improve Riot.im by sending anonymous usage data. This will use a cookie.": "익명의 이용자 데이터를 보내 Riot.im의 발전을 도와주세요. 이 과정에서 쿠키를 사용합니다.", "Allow": "허가하기", "Visible to everyone": "모두에게 보여짐", "Only visible to community members": "커뮤니티 구성원에게만 보여짐", @@ -1185,5 +1185,55 @@ "Add a Room": "방 추가하기", "Add users to the community summary": "커뮤니티 요약에 사용자 추가하기", "Who would you like to add to this summary?": "이 요약에 누구를 추가하고 싶으세요?", - "Link to most recent message": "가장 최근 메시지로 링크 걸기" + "Link to most recent message": "가장 최근 메시지로 링크 걸기", + "Registration Required": "계정 등록이 필요합니다.", + "You need to register to do this. Would you like to register now?": "계정을 등록해야합니다. 지금 계정을 만드시겠습니까?", + "This homeserver has hit its Monthly Active User limit.": "이 홈서버는 월간 활성 이용자수 한계에 도달했습니다.", + "Please contact your service administrator to continue using the service.": "서비스를 계속 사용하려면 서비스 관리자에게 연락하세요.", + "Unable to connect to Homeserver. Retrying...": "홈서버에 연결할 수 없습니다. 다시 시도하는 중...", + "Please contact your homeserver administrator.": "홈서버 관리자에게 연락하세요.", + "Increase performance by only loading room members on first view": "최초 접속 시의 방 인원만 불러와 성능 향상", + "This room has been replaced and is no longer active.": "이 방은 대체되었으며 더 사용할 수 없습니다.", + "The conversation continues here.": "이 대화는 여기서 이어가세요.", + "System Alerts": "시스템 알림", + "Upgrade room to version %(ver)s": "%(ver)s 버전으로 방을 업그레이드", + "Members only (since the point in time of selecting this option)": "구성원만(이 설정을 선택한 시점부터)", + "Members only (since they were invited)": "구성원만(구성원이 초대받은 시점부터)", + "Room version number: ": "방 버전 넘버: ", + "There is a known vulnerability affecting this room.": "이 방에 영향을 미치는 알려진 취약점이 있습니다.", + "Only room administrators will see this warning": "방 관리자만이 이 경고를 볼 수 있습니다.", + "This room is a continuation of another conversation.": "이 방은 다른 대화방의 연장선입니다.", + "Click here to see older messages.": "여길 눌러 오래된 메시지를 보세요.", + "Robot check is currently unavailable on desktop - please use a web browser": "로봇 확인은 현재 PC에서는 사용할 수 없습니다 - 웹 브라우저를 사용해주세요.", + "Please help improve Riot.im by sending anonymous usage data. This will use a cookie (please see our Cookie Policy).": "익명의 이용자 데이터를 보내 Riot.im의 발전을 도와주세요. 이 과정에서 쿠키를 사용합니다 (우리의 쿠키 정책을 살펴보세요).", + "This homeserver has hit its Monthly Active User limit so some users will not be able to log in.": "이 홈서버는 월간 활성 이용자수 한계에 도달했기 때문에 일부 유저는 로그인할 수 없습니다.", + "Do you want to load widget from URL:": "URL에서 위젯을 불러오시겠습니까:", + "Revoke widget access": "위젯 접속 거부", + "%(nameList)s %(transitionList)s": "%(nameList)s %(transitionList)s", + "%(severalUsers)sleft and rejoined %(count)s times|other": "%(severalUsers)s님이 떠났으며 %(count)s 번 다시 참여했습니다.", + "In reply to ": "답장하기 ", + "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)": "계정을 비활성화한다면 보냈던 모든 메시지는 잊어버리세요 (경고: 이후 이용자들은 불완전한 대화 목록을 볼 수 있을 겁니다)", + "Explore Account Data": "계정 자료 탐색하기", + "Updating Riot": "Riot 업데이트중", + "Upgrade this room to version %(version)s": "이 방을 %(version)s 버전으로 업그레이드", + "Upgrade Room Version": "방 버전 업그레이드", + "Create a new room with the same name, description and avatar": "이름, 설명, 아바타가 같은 새 방 만들기", + "Stop users from speaking in the old version of the room, and post a message advising users to move to the new room": "이전 버전의 방에서 말하는 이용자를 중단시키고, 새 방으로 이동하라는 메시지를 표시합니다.", + "Put a link back to the old room at the start of the new room so people can see old messages": "사람들이 오래된 메시지를 볼 수 있게 새 방의 시작 부분에 오래된 방으로 가는 링크를 놓습니다.", + "Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.": "브라우저 저장소를 청소한다면 문제가 해결될 수도 있지만, 암호하된 대화 기록을 읽을 수 없게 됩니다.", + "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "이용자와 방을 같이 묶는 커뮤니티를 만들어보세요! Matrix 세계에서 당신의 공간을 표시하는 사용자정의 홈페이지도 만드세요.", + "%(count)s Members|other": "", + "%(count)s Members|one": "", + "Invite to this community": "이 커뮤니티에 초대하기", + "You can't send any messages until you review and agree to our terms and conditions.": "우리의 약관을 읽고 동의하시기 전까지는 메시지를 보낼 수 없습니다.", + "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.": "이 홈서버가 월간 이용자수 한계에 도달했기 때문에 메시지를 보낼 수 없었습니다. 서비스를 계속 이용하려면 서비스 관리자에게 연락하세요.", + "%(count)s of your messages have not been sent.|one": "메시지가 보내지지 않았습니다.", + "%(count)s Resend all or cancel all now. You can also select individual messages to resend or cancel.|other": "지금 전부 다시보내기 or 전부 취소하기. 각 메시지를 골라 다시 보내거나 취소할 수도 있습니다.", + "%(count)s Resend all or cancel all now. You can also select individual messages to resend or cancel.|one": "지금 메시지 다시보내기 혹은 메시지 취소하기.", + "Submit Debug Logs": "디버그 로그 제출", + "No Audio Outputs detected": "오디오 출력을 감지하지 못했습니다.", + "Audio Output": "오디오 출력", + "Please contact your service administrator to continue using this service.": "서비스를 계속 이용하려면 서비스 관리자에게 연락하세요.", + "An email address is required to register on this homeserver.": "이 홈서버에 등록하려면 이메일 주소가 필요합니다.", + "A phone number is required to register on this homeserver.": "이 홈서버에 등록하려면 전화번호가 필요합니다." } diff --git a/src/i18n/strings/lt.json b/src/i18n/strings/lt.json index db2a8a1f7b..776445e40d 100644 --- a/src/i18n/strings/lt.json +++ b/src/i18n/strings/lt.json @@ -870,5 +870,7 @@ "numbered-list": "sąrašas su numeriais", "Invites": "Pakvietimai", "You have no historical rooms": "Jūs neturite istorinių kambarių", - "Historical": "Istoriniai" + "Historical": "Istoriniai", + "Every page you use in the app": "Kiekvienas puslapis, kurį naudoji programoje", + "Call Timeout": "Skambučio laikas baigėsi" } diff --git a/src/i18n/strings/nn.json b/src/i18n/strings/nn.json index 2f89ab0d2f..c00189aa47 100644 --- a/src/i18n/strings/nn.json +++ b/src/i18n/strings/nn.json @@ -390,7 +390,7 @@ "Send a reply (unencrypted)…": "Send eit svar (ikkje-kryptert)…", "Send an encrypted message…": "Send ei kryptert melding…", "Send a message (unencrypted)…": "Send ei melding (ikkje-kryptert)…", - "You do not have permission to post to this room": "Du har ikkje tillating til å sende meldingar i dette rommet", + "You do not have permission to post to this room": "Du har ikkje tillating til å senda meldingar i dette rommet", "Turn Markdown on": "Skru Mardown på", "Turn Markdown off": "Skru Markdown av", "Hide Text Formatting Toolbar": "Gøym Tekstformverktøylinje", @@ -438,7 +438,7 @@ "Share room": "Del rom", "Drop here to favourite": "Slepp her for å gjera til yndling", "Drop here to restore": "Slepp her for å gjenoppretta", - "Drop here to demote": "Slepp her for å senke i høgd", + "Drop here to demote": "Slepp her for å senka i høgd", "Press to start a chat with someone": "Trykk på for å starta ei samtale med nokon", "You're not in any rooms yet! Press to make a room or to browse the directory": "Du er enno ikkje i eit rom! Trykk på for å laga eit rom eller for å sjå gjennom utvalet", "Community Invites": "Samfunnsinnbydingar", diff --git a/src/i18n/strings/pl.json b/src/i18n/strings/pl.json index 3bbc9b1d73..977b94c718 100644 --- a/src/i18n/strings/pl.json +++ b/src/i18n/strings/pl.json @@ -143,11 +143,11 @@ "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or enable unsafe scripts.": "Nie można nawiązać połączenia z serwerem przy użyciu HTTP podczas korzystania z HTTPS dla bieżącej strony. Użyj HTTPS lub włącz niebezpieczne skrypty.", "Can't load user settings": "Nie można załadować ustawień użytkownika", "Cannot add any more widgets": "Nie można dodać już więcej widżetów", - "%(senderName)s changed their profile picture.": "%(senderName)s zmienił swoje zdjęcie profilowe.", - "%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s zmienił poziom mocy %(powerLevelDiffText)s.", - "%(senderDisplayName)s changed the room name to %(roomName)s.": "%(senderDisplayName)s zmienił nazwę pokoju na %(roomName)s.", - "%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s usunął nazwę pokoju.", - "%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s zmienił temat na \"%(topic)s\".", + "%(senderName)s changed their profile picture.": "%(senderName)s zmienił(a) swoje zdjęcie profilowe.", + "%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s zmienił(a) poziom mocy %(powerLevelDiffText)s.", + "%(senderDisplayName)s changed the room name to %(roomName)s.": "%(senderDisplayName)s zmienił(a) nazwę pokoju na %(roomName)s.", + "%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s usunął(-ęła) nazwę pokoju.", + "%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s zmienił(a) temat na \"%(topic)s\".", "Changes to who can read history will only apply to future messages in this room": "Zmiany w dostępie do historii będą dotyczyć tylko przyszłych wiadomości w tym pokoju", "Changes your display nickname": "Zmień swój pseudonim", "Changes colour scheme of current room": "Zmień schemat kolorystyczny bieżącego pokoju", @@ -370,8 +370,8 @@ "Rejoin": "Dołącz ponownie", "Remote addresses for this room:": "Adresy zdalne dla tego pokoju:", "Remove Contact Information?": "Usunąć dane kontaktowe?", - "%(senderName)s removed their display name (%(oldDisplayName)s).": "%(senderName)s usunął swoją nazwę ekranową (%(oldDisplayName)s).", - "%(senderName)s removed their profile picture.": "%(senderName)s usunął swoje zdjęcie profilowe.", + "%(senderName)s removed their display name (%(oldDisplayName)s).": "%(senderName)s usunął(-ęła) swoją wyświetlaną nazwę (%(oldDisplayName)s).", + "%(senderName)s removed their profile picture.": "%(senderName)s usunął(-ęła) swoje zdjęcie profilowe.", "Remove %(threePid)s?": "Usunąć %(threePid)s?", "%(senderName)s requested a VoIP conference.": "%(senderName)s zażądał grupowego połączenia głosowego VoIP.", "Results from DuckDuckGo": "Wyniki z DuckDuckGo", @@ -629,9 +629,9 @@ " (unsupported)": " (niewspierany)", "Idle": "Bezczynny", "Check for update": "Sprawdź aktualizacje", - "%(senderDisplayName)s changed the room avatar to ": "%(senderDisplayName)s zmienił awatar pokoju na ", - "%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s usunął awatar pokoju.", - "%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s zmienił awatar %(roomName)s", + "%(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.", + "%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s zmienił(a) awatar %(roomName)s", "This will be your account name on the homeserver, or you can pick a different server.": "To będzie twoja nazwa konta na serwerze domowym; możesz też wybrać inny serwer.", "If you already have a Matrix account you can log in instead.": "Jeśli już posiadasz konto Matrix możesz się zalogować.", "Not a valid Riot keyfile": "Niepoprawny plik klucza Riot", @@ -640,7 +640,7 @@ "Do you want to set an email address?": "Czy chcesz ustawić adres e-mail?", "To return to your account in future you need to set a password": "By móc powrócić do swojego konta w przyszłości musisz ustawić hasło", "Share without verifying": "Udostępnij bez weryfikacji", - "You added a new device '%(displayName)s', which is requesting encryption keys.": "Dodałeś nowe urządzenie '%(displayName)s', które żąda kluczy szyfrujących.", + "You added a new device '%(displayName)s', which is requesting encryption keys.": "Dodałeś(-aś) nowe urządzenie '%(displayName)s', które żąda kluczy szyfrujących.", "Your unverified device '%(displayName)s' is requesting encryption keys.": "Twoje niezweryfikowane urządzenie '%(displayName)s' żąda kluczy szyfrujących.", "Encryption key request": "Żądanie klucza szyfrującego", "Autocomplete Delay (ms):": "Opóźnienie autouzupełniania (ms):", @@ -700,8 +700,8 @@ "Ignored user": "Użytkownik ignorowany", "You are now ignoring %(userId)s": "Ignorujesz teraz %(userId)s", "You are no longer ignoring %(userId)s": "Nie ignorujesz już %(userId)s", - "%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s zmienił swoją wyświetlaną nazwę na %(displayName)s.", - "%(senderName)s changed the pinned messages for the room.": "%(senderName)s zmienił przypiętą wiadomość dla tego pokoju.", + "%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s zmienił(a) swoją wyświetlaną nazwę na %(displayName)s.", + "%(senderName)s changed the pinned messages for the room.": "%(senderName)s zmienił(a) przypiętą wiadomość dla tego pokoju.", "Message Pinning": "Przypinanie wiadomości", "%(names)s and %(count)s others are typing|other": "%(names)s oraz %(count)s innych piszą", "%(names)s and %(count)s others are typing|one": "%(names)s oraz jedna inna osoba piszą", @@ -1146,5 +1146,39 @@ "was invited %(count)s times|one": "został zaproszony", "was banned %(count)s times|one": "został zablokowany", "was kicked %(count)s times|one": "został wyrzucony", - "Whether or not you're using the Richtext mode of the Rich Text Editor": "" + "Whether or not you're using the Richtext mode of the Rich Text Editor": "Niezależnie od tego, czy używasz trybu Richtext edytora tekstu w formacie RTF", + "Call in Progress": "Łączenie w toku", + "Permission Required": "Wymagane Pozwolenie", + "Registration Required": "Wymagana Rejestracja", + "You need to register to do this. Would you like to register now?": "Musisz się zarejestrować, aby to zrobić. Czy chcesz się teraz zarejestrować?", + "underlined": "podkreślenie", + "deleted": "przekreślenie", + "numbered-list": "lista numerowana", + "bulleted-list": "wykropkowana lista", + "block-quote": "blok cytowany", + "A conference call could not be started because the intgrations server is not available": "Połączenie grupowe nie może zostać rozpoczęte, ponieważ serwer jest niedostępny", + "A call is currently being placed!": "W tej chwili trwa rozmowa!", + "A call is already in progress!": "W tej chwili trwa połączenie!", + "You do not have permission to start a conference call in this room": "Nie posiadasz permisji do rozpoczęcia rozmowy grupowej w tym pokoju", + "Unignored user": "Nieignorowany użytkownik", + "Forces the current outbound group session in an encrypted room to be discarded": "Wymusza odrzucenie bieżącej sesji grupy wychodzącej w zaszyfrowanym pokoju", + "%(senderName)s added %(count)s %(addedAddresses)s as addresses for this room.|other": "%(senderName)s dodał(a) %(addedAddresses)s jako adres tego pokoju.", + "%(senderName)s added %(count)s %(addedAddresses)s as addresses for this room.|one": "%(senderName)s dodał(a) %(addedAddresses)s jako adres tego pokoju.", + "%(senderName)s removed %(count)s %(removedAddresses)s as addresses for this room.|other": "%(senderName)s usunął(-ęła) %(removedAddresses)s jako adres tego pokoju.", + "%(senderName)s removed %(count)s %(removedAddresses)s as addresses for this room.|one": "%(senderName)s usunął(-ęła) %(removedAddresses)s jako adres tego pokoju.", + "%(senderName)s added %(addedAddresses)s and removed %(removedAddresses)s as addresses for this room.": "%(senderName)s dodał %(addedAddresses)s i %(removedAddresses)s usunął adresy z tego pokoju.", + "%(senderName)s set the main address for this room to %(address)s.": "%(senderName)s ustawił(a) główny adres dla tego pokoju na %(address)s.", + "%(senderName)s removed the main address for this room.": "%(senderName)s usunął(-ęła) główny adres tego pokoju.", + "This homeserver has hit its Monthly Active User limit.": "Ten serwer osiągnął miesięczny limit aktywnego użytkownika.", + "This homeserver has exceeded one of its resource limits.": "Ten serwer przekroczył jeden z limitów.", + "Please contact your service administrator to continue using the service.": "Proszę, skontaktuj się z administratorem aby korzystać dalej z funkcji.", + "Unable to connect to Homeserver. Retrying...": "Nie można się połączyć z serwerem. Ponawanianie...", + "Sorry, your homeserver is too old to participate in this room.": "Przepraszamy, twój serwer jest zbyt stary by wziąć udział w tym pokoju.", + "Please contact your homeserver administrator.": "Proszę o kontakt z administratorem serwera.", + "Increase performance by only loading room members on first view": "Zwiększ wydajność, ładując tylko członków pokoju w pierwszym widoku", + "Enable widget screenshots on supported widgets": "Włącz widżety zrzutów ekranów na obsługiwanych widżetach", + "Show empty room list headings": "Pokaż nagłówki z pustym pokojem", + "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.": "W zaszyfrowanych pokojach, takich jak ten, podgląd adresów URL jest domyślnie wyłączony, aby upewnić się, że serwer (w którym generowane są podglądy) nie może zbierać informacji o linkach widocznych w tym pokoju.", + "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.": "Gdy ktoś umieści URL w wiadomości, można wyświetlić podgląd adresu URL, aby podać więcej informacji o tym łączu, takich jak tytuł, opis i obraz ze strony internetowej.", + "This event could not be displayed": "Ten event nie może zostać wyświetlony" } diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json index 25e0d0b78d..b711b43447 100644 --- a/src/i18n/strings/ru.json +++ b/src/i18n/strings/ru.json @@ -1234,5 +1234,7 @@ "Put a link back to the old room at the start of the new room so people can see old messages": "Разместим ссылку на старую комнату, чтобы люди могли видеть старые сообщения", "Please contact your service administrator to continue using this service.": "Пожалуйста, обратитесь к вашему администратору, чтобы продолжить использовать этот сервис.", "Increase performance by only loading room members on first view": "Увеличьте производительность, загрузив только список участников комнаты", - "Lazy loading members not supported": "Задержка загрузки элементов не поддерживается" + "Lazy loading members not supported": "Задержка загрузки элементов не поддерживается", + "Registration Required": "Требуется регистрация", + "You need to register to do this. Would you like to register now?": "Необходимо зарегистрироваться. Хотите зарегистрироваться?" } diff --git a/src/i18n/strings/sk.json b/src/i18n/strings/sk.json index 05c102d516..6799458320 100644 --- a/src/i18n/strings/sk.json +++ b/src/i18n/strings/sk.json @@ -1257,5 +1257,26 @@ "Create a new room with the same name, description and avatar": "Vznikne nová miestnosť s rovnakým názvom, témou a obrázkom", "Update any local room aliases to point to the new room": "Všetky lokálne aliasy pôvodnej miestnosti sa aktualizujú tak, aby ukazovali na novú miestnosť", "Stop users from speaking in the old version of the room, and post a message advising users to move to the new room": "V pôvodnej miestnosti bude zverejnené odporúčanie prejsť do novej miestnosti a posielanie do pôvodnej miestnosti bude zakázané pre všetkých používateľov", - "Put a link back to the old room at the start of the new room so people can see old messages": "História novej miestnosti sa začne odkazom do pôvodnej miestnosti, aby si členovia vedeli zobraziť staršie správy" + "Put a link back to the old room at the start of the new room so people can see old messages": "História novej miestnosti sa začne odkazom do pôvodnej miestnosti, aby si členovia vedeli zobraziť staršie správy", + "Registration Required": "Vyžaduje sa registrácia", + "You need to register to do this. Would you like to register now?": "Aby ste mohli uskutočniť túto akciu, musíte sa zaregistrovať. Chcete teraz spustiť registráciu?", + "Forces the current outbound group session in an encrypted room to be discarded": "Vynúti zabudnutie odchádzajúcej skupinovej relácii v šifrovanej miestnosti", + "%(senderName)s added %(count)s %(addedAddresses)s as addresses for this room.|other": "%(senderName)s pridal adresy %(addedAddresses)s do tejto miestnosti.", + "%(senderName)s added %(count)s %(addedAddresses)s as addresses for this room.|one": "%(senderName)s pridal adresu %(addedAddresses)s do tejto miestnosti.", + "%(senderName)s removed %(count)s %(removedAddresses)s as addresses for this room.|other": "%(senderName)s odstránil adresy %(removedAddresses)s z tejto miestnosti.", + "%(senderName)s removed %(count)s %(removedAddresses)s as addresses for this room.|one": "%(senderName)s odstránil adresu %(removedAddresses)s z tejto miestnosti.", + "%(senderName)s added %(addedAddresses)s and removed %(removedAddresses)s as addresses for this room.": "%(senderName)s pridal %(addedAddresses)s a odstránil %(removedAddresses)s z tejto miestnosti.", + "%(senderName)s set the main address for this room to %(address)s.": "%(senderName)s nastavil hlavnú adresu tejto miestnosti %(address)s.", + "%(senderName)s removed the main address for this room.": "%(senderName)s odstránil hlavnú adresu tejto miestnosti.", + "Unable to connect to Homeserver. Retrying...": "Nie je možné sa pripojiť k domovskému serveru. Prebieha pokus o opetovné pripojenie...", + "Before submitting logs, you must create a GitHub issue to describe your problem.": "Pred tým, než odošlete záznamy, musíte nahlásiť váš problém na GitHub. Uvedte prosím podrobný popis.", + "What GitHub issue are these logs for?": "Pre ktoré hlásenie GitHub sú tieto záznamy?", + "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 teraz vyžaduje 3-5× menej pamäte, pretože informácie o ostatných používateľoch načítava len podľa potreby. Prosím počkajte na dokončenie synchronizácie so serverom!", + "Updating Riot": "Prebieha aktualizácia Riot", + "

    HTML for your community's page

    \r\n

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

    \r\n

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

    \r\n": "

    Obsah vo formáte HTML pre vašu stránku komunity

    \n

    \n Do poľa dlhý popis zadajte text, ktorým komunitu predstavíte novým členom, alebo ich\n na nejaké dôležité odkazy\n

    \n

    \n Môžete tiež pridať obrázky použitím značiek 'img'\n

    \n", + "Submit Debug Logs": "Odoslať ladiace záznamy", + "Legal": "Právne", + "Unable to query for supported registration methods": "Nie je možné vyžiadať podporované metódy registrácie", + "An email address is required to register on this homeserver.": "Na registráciu na tomto domovskom servery je vyžadovaná emailová adresa.", + "A phone number is required to register on this homeserver.": "Na registráciu na tomto domovskom servery je vyžadované telefónne číslo." } diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index 680c63e458..72097f8ab1 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -294,5 +294,6 @@ "Collapse panel": "Tkurre panelin", "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" + "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" } diff --git a/src/i18n/strings/zh_Hans.json b/src/i18n/strings/zh_Hans.json index 6931e8883a..90c162e39b 100644 --- a/src/i18n/strings/zh_Hans.json +++ b/src/i18n/strings/zh_Hans.json @@ -17,7 +17,7 @@ "Disinvite": "取消邀请", "Display name": "昵称", "Displays action": "显示操作", - "Don't send typing notifications": "不要发送我的打字状态", + "Don't send typing notifications": "不要发送“正在输入”提示", "Download %(text)s": "下载 %(text)s", "Email": "电子邮箱", "Email address": "邮箱地址", @@ -26,7 +26,7 @@ "Enable encryption": "启用加密", "Encrypted messages will not be visible on clients that do not yet implement encryption": "不支持加密的客户端将看不到加密的消息", "Encrypted room": "加密聊天室", - "%(senderName)s ended the call.": "%(senderName)s 结束了通话。.", + "%(senderName)s ended the call.": "%(senderName)s 结束了通话。", "End-to-end encryption information": "端到端加密信息", "End-to-end encryption is in beta and may not be reliable": "端到端加密现为 beta 版,不一定可靠", "Enter Code": "输入验证码", @@ -48,7 +48,7 @@ "Failed to save settings": "保存设置失败", "Failed to send email": "发送邮件失败", "Failed to send request.": "请求发送失败。", - "Failed to set avatar.": "设置头像失败。.", + "Failed to set avatar.": "设置头像失败。", "Failed to set display name": "设置昵称失败", "Failed to set up conference call": "无法启动群组通话", "Failed to toggle moderator status": "无法切换管理员权限", @@ -62,7 +62,7 @@ "Filter room members": "过滤聊天室成员", "Forget room": "忘记聊天室", "Forgot your password?": "忘记密码?", - "For security, this session has been signed out. Please sign in again.": "出于安全考虑,此会话已被注销。请重新登录。.", + "For security, this session has been signed out. Please sign in again.": "出于安全考虑,此会话已被注销。请重新登录。", "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.": "出于安全考虑,用户注销时会清除浏览器里的端到端加密密钥。如果你想要下次登录 Riot 时能解密过去的聊天记录,请导出你的聊天室密钥。", "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s 从 %(fromPowerLevel)s 变为 %(toPowerLevel)s", "Guests cannot join this room even if explicitly invited.": "即使有人主动邀请,游客也不能加入此聊天室。", @@ -80,7 +80,7 @@ "Invalid address format": "地址格式错误", "Invalid Email Address": "邮箱地址格式错误", "Invalid file%(extra)s": "非法文件%(extra)s", - "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.": "重设密码会导致所有设备上的端到端加密密钥被重置,使得加密的聊天记录不可读,除非你事先导出密钥,修改密码后再导入。此问题将来会得到改善。.", + "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.": "重设密码会导致所有设备上的端到端加密密钥被重置,使得加密的聊天记录不可读,除非你事先导出密钥,修改密码后再导入。此问题将来会得到改善。", "Return to login screen": "返回登录页面", "Riot does not have permission to send you notifications - please check your browser settings": "Riot 没有通知发送权限 - 请检查您的浏览器设置", "Riot was not given permission to send notifications - please try again": "Riot 没有通知发送权限 - 请重试", @@ -97,8 +97,8 @@ "Sender device information": "发送者的设备信息", "Send Invites": "发送邀请", "Send Reset Email": "发送密码重设邮件", - "%(senderDisplayName)s sent an image.": "%(senderDisplayName)s 发了一张图片。.", - "%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.": "%(senderName)s 向 %(targetDisplayName)s 发了加入聊天室的邀请。.", + "%(senderDisplayName)s sent an image.": "%(senderDisplayName)s 发送了一张图片。", + "%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.": "%(senderName)s 向 %(targetDisplayName)s 发了加入聊天室的邀请。", "Server error": "服务器错误", "Server may be unavailable or overloaded": "服务器可能不可用或者超载", "Server may be unavailable, overloaded, or search timed out :(": "服务器可能不可用、超载,或者搜索超时 :(", @@ -106,8 +106,8 @@ "Server may be unavailable, overloaded, or you hit a bug.": "当前服务器可能处于不可用或过载状态,或者您遇到了一个 bug。", "Server unavailable, overloaded, or something else went wrong.": "服务器可能不可用、超载,或者其他东西出错了.", "Session ID": "会话 ID", - "%(senderName)s set a profile picture.": "%(senderName)s 设置了头像。.", - "%(senderName)s set their display name to %(displayName)s.": "%(senderName)s 将昵称改为了 %(displayName)s。.", + "%(senderName)s set a profile picture.": "%(senderName)s 设置了头像。", + "%(senderName)s set their display name to %(displayName)s.": "%(senderName)s 将昵称改为了 %(displayName)s。", "Settings": "设置", "Show panel": "显示侧边栏", "Show timestamps in 12 hour format (e.g. 2:30pm)": "用12小时制显示时间戳 (如:下午 2:30)", @@ -115,7 +115,7 @@ "Sign in": "登录", "Sign out": "注销", "%(count)s of your messages have not been sent.|other": "部分消息未发送。", - "Someone": "某个用户", + "Someone": "某位用户", "Start a chat": "创建聊天", "Start Chat": "开始聊天", "Submit": "提交", @@ -134,7 +134,7 @@ "Always show message timestamps": "总是显示消息时间戳", "%(names)s and %(lastPerson)s are typing": "%(names)s 和 %(lastPerson)s 正在输入", "A new password must be entered.": "必须输入新密码。", - "%(senderName)s answered the call.": "%(senderName)s 接了通话。.", + "%(senderName)s answered the call.": "%(senderName)s 接了通话。", "An error has occurred.": "发生了一个错误。", "Attachment": "附件", "Autoplay GIFs and videos": "自动播放 GIF 与视频", @@ -150,7 +150,7 @@ "Join Room": "加入聊天室", "%(targetName)s joined the room.": "%(targetName)s 已加入聊天室。", "Jump to first unread message.": "跳到第一条未读消息。", - "%(senderName)s kicked %(targetName)s.": "%(senderName)s 把 %(targetName)s 踢出了聊天室。.", + "%(senderName)s kicked %(targetName)s.": "%(senderName)s 把 %(targetName)s 踢出了聊天室。", "Leave room": "退出聊天室", "New password": "新密码", "Add a topic": "添加主题", @@ -260,7 +260,7 @@ "not set": "未设置", "not specified": "未指定", "Notifications": "通知", - "(not supported by this browser)": "(此浏览器不支持)", + "(not supported by this browser)": "(未被此浏览器支持)", "": "<不支持>", "NOT verified": "未验证", "No display name": "无昵称", @@ -452,7 +452,7 @@ "Publish this room to the public in %(domain)s's room directory?": "是否将此聊天室发布至 %(domain)s 的聊天室目录中?", "Manage Integrations": "管理集成", "No users have specific privileges in this room": "此聊天室中没有用户有特殊权限", - "%(senderName)s placed a %(callType)s call.": "%(senderName)s 发起了 %(callType)s 通话。", + "%(senderName)s placed a %(callType)s call.": "%(senderName)s 发起了%(callType)s通话。", "Please check your email and click on the link it contains. Once this is done, click continue.": "请检查你的电子邮箱并点击里面包含的链接。完成时请点击继续。", "Press to start a chat with someone": "按下 来开始和某个人聊天", "%(senderName)s removed their profile picture.": "%(senderName)s 移除了他们的头像。", @@ -488,7 +488,7 @@ "This room is not recognised.": "无法识别此聊天室。", "To get started, please pick a username!": "请点击用户名!", "Unable to add email address": "无法添加邮箱地址", - "Automatically replace plain text Emoji": "文字、表情自动转换", + "Automatically replace plain text Emoji": "将符号表情转换为 Emoji", "To reset your password, enter the email address linked to your account": "要重置你的密码,请输入关联你的帐号的邮箱地址", "Unable to verify email address.": "无法验证邮箱地址。", "Unknown room %(roomId)s": "未知聊天室 %(roomId)s", @@ -498,7 +498,7 @@ "Use with caution": "谨慎使用", "User Interface": "用户界面", "User name": "用户名", - "(no answer)": "(没有回答)", + "(no answer)": "(无响应)", "(warning: cannot be disabled again!)": "(警告:无法再被禁用!)", "WARNING: Device already verified, but keys do NOT MATCH!": "警告:设备已验证,但密钥不匹配!", "Who can access this room?": "谁有权访问此聊天室?", @@ -1210,7 +1210,7 @@ "These rooms are displayed to community members on the community page. Community members can join the rooms by clicking on them.": "这些聊天室对社区成员可见。社区成员可通过点击来加入它们。", "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!": "您的社区还没有详细介绍,一个展示给社区成员的 HTML 页面。
    点击这里即可打开设置添加详细介绍!", "Failed to load %(groupId)s": "%(groupId)s 加载失败", - "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.": "此聊天室不是公开聊天室。如果没有成员邀请,您将无法重新加入。", "Can't leave Server Notices room": "无法退出服务器公告聊天室", "This room is used for important messages from the Homeserver, so you cannot leave it.": "此聊天室是用于发布来自主服务器的重要讯息的,所以您不能退出它。", "Terms and Conditions": "条款与要求", @@ -1268,5 +1268,12 @@ "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.": "您的消息未被发送,因为本主服务器已达到其每月活跃用户限制。请 联系您的服务管理员 以继续使用本服务。", "Please contact your service administrator to continue using the service.": "请 联系您的服务管理员 以继续使用本服务。", "Please contact your homeserver administrator.": "请 联系您主服务器的管理员。", - "Please contact your service administrator to get this limit increased.": "请 联系您的服务管理员 以增加此限制的额度。" + "Please contact your service administrator to get this limit increased.": "请 联系您的服务管理员 以增加此限制的额度。", + "%(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 移除了此聊天室的主地址。" } diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index 8af87415b3..ba23947fc2 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -165,7 +165,7 @@ "Success": "成功", "The default role for new room members is": "此聊天室新成員的預設角色是", "The main address for this room is": "此聊天室的主要地址是", - "This email address is already in use": "此電子郵件地址已經被使用", + "This email address is already in use": "這個電子郵件位址已被使用", "This email address was not found": "未找到此電子郵件地址", "The email address linked to your account must be entered.": "必須輸入和你帳號關聯的電子郵件地址。", "The file '%(fileName)s' exceeds this home server's size limit for uploads": "文件 '%(fileName)s' 超過了這個家伺服器的上傳大小限制", @@ -1277,5 +1277,21 @@ "%(senderName)s removed %(count)s %(removedAddresses)s as addresses for this room.|other": "%(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 移除了此聊天室的主要位置。" + "%(senderName)s removed the main address for this room.": "%(senderName)s 移除了此聊天室的主要位置。", + "Before submitting logs, you must create a GitHub issue to describe your problem.": "在遞交紀錄檔前,您必須建立 GitHub 議題以描述您的問題。", + "What GitHub issue are these logs for?": "這些紀錄檔的 GitHub 議題是什麼?", + "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 現在僅使用三分之一到五分之一的記憶體,僅在需要時才會載入其他使用者的資訊。請等待我們與伺服器重新同步!", + "Updating Riot": "正在更新 Riot", + "

    HTML for your community's page

    \r\n

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

    \r\n

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

    \r\n": "

    您社群頁面的 HTML

    \n

    \n 使用長描述以向新成員介紹社群,或是散佈\n 一些重要連結\n

    \n

    \n 您也可以使用「img」標籤\n

    \n", + "Submit Debug Logs": "遞交除錯紀錄", + "An email address is required to register on this homeserver.": "在此家伺服器上註冊必須填入電子郵件。", + "A phone number is required to register on this homeserver.": "在此伺服器上註冊必須填入電話號碼。", + "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.": "您之前曾在 %(host)s 上使用 Riot 並啟用成員列表的延遲載入。在此版本中延遲載入已停用。由於本機快取在這兩個設定間不相容,Riot 必須重新同步您的帳號。", + "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.": "若其他分頁仍有不同版本的 Riot,請將其關閉,因為在同一個主機上同時啟用和停用延遲載入將會發生問題。", + "Incompatible local cache": "不相容的本機快取", + "Clear cache and resync": "清除快取並重新同步", + "Please accept all of the policies": "請接受所有政策", + "Please review and accept the policies of this homeserver:": "請審閱並接受此家伺服器的政策:", + "Add some now": "現在就新增一些", + "Joining room...": "正在加入聊天室……" } diff --git a/src/languageHandler.js b/src/languageHandler.js index 4e24c04d36..8735150d20 100644 --- a/src/languageHandler.js +++ b/src/languageHandler.js @@ -97,19 +97,17 @@ export function _t(text, variables, tags) { // The translation returns text so there's no XSS vector here (no unsafe HTML, no code execution) const translated = safeCounterpartTranslate(text, args); - let substituted = substitute(translated, variables, tags); + const substituted = substitute(translated, variables, tags); // For development/testing purposes it is useful to also output the original string // Don't do that for release versions if (ANNOTATE_STRINGS) { if (typeof substituted === 'string') { - return `@@${text}##${substituted}@@` - } - else { + return `@@${text}##${substituted}@@`; + } else { return {substituted}; } - } - else { + } else { return substituted; } } diff --git a/src/notifications/ContentRules.js b/src/notifications/ContentRules.js index 25a7bac96e..f7e722dbfe 100644 --- a/src/notifications/ContentRules.js +++ b/src/notifications/ContentRules.js @@ -16,7 +16,7 @@ limitations under the License. 'use strict'; -var PushRuleVectorState = require('./PushRuleVectorState'); +const PushRuleVectorState = require('./PushRuleVectorState'); module.exports = { /** @@ -32,7 +32,7 @@ module.exports = { */ parseContentRules: function(rulesets) { // first categorise the keyword rules in terms of their actions - var contentRules = this._categoriseContentRules(rulesets); + const contentRules = this._categoriseContentRules(rulesets); // Decide which content rules to display in Vector UI. // Vector displays a single global rule for a list of keywords @@ -54,41 +54,38 @@ module.exports = { rules: contentRules.loud, externalRules: [].concat(contentRules.loud_but_disabled, contentRules.on, contentRules.on_but_disabled, contentRules.other), }; - } - else if (contentRules.loud_but_disabled.length) { + } else if (contentRules.loud_but_disabled.length) { return { vectorState: PushRuleVectorState.OFF, rules: contentRules.loud_but_disabled, externalRules: [].concat(contentRules.on, contentRules.on_but_disabled, contentRules.other), }; - } - else if (contentRules.on.length) { + } else if (contentRules.on.length) { return { vectorState: PushRuleVectorState.ON, rules: contentRules.on, externalRules: [].concat(contentRules.on_but_disabled, contentRules.other), }; - } - else if (contentRules.on_but_disabled.length) { + } else if (contentRules.on_but_disabled.length) { return { vectorState: PushRuleVectorState.OFF, rules: contentRules.on_but_disabled, externalRules: contentRules.other, - } - } else { + }; + } else { return { vectorState: PushRuleVectorState.ON, rules: [], externalRules: contentRules.other, - } + }; } }, _categoriseContentRules: function(rulesets) { - var contentRules = {on: [], on_but_disabled:[], loud: [], loud_but_disabled: [], other: []}; - for (var kind in rulesets.global) { - for (var i = 0; i < Object.keys(rulesets.global[kind]).length; ++i) { - var r = rulesets.global[kind][i]; + const contentRules = {on: [], on_but_disabled: [], loud: [], loud_but_disabled: [], other: []}; + for (const kind in rulesets.global) { + for (let i = 0; i < Object.keys(rulesets.global[kind]).length; ++i) { + const r = rulesets.global[kind][i]; // check it's not a default rule if (r.rule_id[0] === '.' || kind !== 'content') { @@ -101,16 +98,14 @@ module.exports = { case PushRuleVectorState.ON: if (r.enabled) { contentRules.on.push(r); - } - else { + } else { contentRules.on_but_disabled.push(r); } break; case PushRuleVectorState.LOUD: if (r.enabled) { contentRules.loud.push(r); - } - else { + } else { contentRules.loud_but_disabled.push(r); } break; diff --git a/src/notifications/NotificationUtils.js b/src/notifications/NotificationUtils.js index c8aeb46854..79c1b38f6d 100644 --- a/src/notifications/NotificationUtils.js +++ b/src/notifications/NotificationUtils.js @@ -24,11 +24,11 @@ module.exports = { // } // to a list of push actions. encodeActions: function(action) { - var notify = action.notify; - var sound = action.sound; - var highlight = action.highlight; + const notify = action.notify; + const sound = action.sound; + const highlight = action.highlight; if (notify) { - var actions = ["notify"]; + const actions = ["notify"]; if (sound) { actions.push({"set_tweak": "sound", "value": sound}); } @@ -50,19 +50,19 @@ module.exports = { // } // If the actions couldn't be decoded then returns null. decodeActions: function(actions) { - var notify = false; - var sound = null; - var highlight = false; + let notify = false; + let sound = null; + let highlight = false; - for (var i = 0; i < actions.length; ++i) { - var action = actions[i]; + for (let i = 0; i < actions.length; ++i) { + const action = actions[i]; if (action === "notify") { notify = true; } else if (action === "dont_notify") { notify = false; } else if (typeof action === 'object') { if (action.set_tweak === "sound") { - sound = action.value + sound = action.value; } else if (action.set_tweak === "highlight") { highlight = action.value; } else { @@ -80,7 +80,7 @@ module.exports = { highlight = true; } - var result = {notify: notify, highlight: highlight}; + const result = {notify: notify, highlight: highlight}; if (sound !== null) { result.sound = sound; } diff --git a/src/notifications/PushRuleVectorState.js b/src/notifications/PushRuleVectorState.js index c838aa20ed..f4ba365b6d 100644 --- a/src/notifications/PushRuleVectorState.js +++ b/src/notifications/PushRuleVectorState.js @@ -16,10 +16,10 @@ limitations under the License. 'use strict'; -var StandardActions = require('./StandardActions'); -var NotificationUtils = require('./NotificationUtils'); +const StandardActions = require('./StandardActions'); +const NotificationUtils = require('./NotificationUtils'); -var states = { +const states = { /** The push rule is disabled */ OFF: "off", @@ -48,8 +48,7 @@ module.exports = { actionsFor: function(pushRuleVectorState) { if (pushRuleVectorState === this.ON) { return StandardActions.ACTION_NOTIFY; - } - else if (pushRuleVectorState === this.LOUD) { + } else if (pushRuleVectorState === this.LOUD) { return StandardActions.ACTION_HIGHLIGHT_DEFAULT_SOUND; } }, @@ -62,21 +61,21 @@ module.exports = { * state. Returns null if it does not match these categories. */ contentRuleVectorStateKind: function(rule) { - var decoded = NotificationUtils.decodeActions(rule.actions); + const decoded = NotificationUtils.decodeActions(rule.actions); if (!decoded) { return null; } // Count tweaks to determine if it is a ON or LOUD rule - var tweaks = 0; + let tweaks = 0; if (decoded.sound) { tweaks++; } if (decoded.highlight) { tweaks++; } - var stateKind = null; + let stateKind = null; switch (tweaks) { case 0: stateKind = this.ON; @@ -89,6 +88,6 @@ module.exports = { }, }; -for (var k in states) { +for (const k in states) { module.exports[k] = states[k]; -}; +} diff --git a/src/notifications/VectorPushRulesDefinitions.js b/src/notifications/VectorPushRulesDefinitions.js index 47e5d56f46..eeb193cb8a 100644 --- a/src/notifications/VectorPushRulesDefinitions.js +++ b/src/notifications/VectorPushRulesDefinitions.js @@ -18,8 +18,8 @@ limitations under the License. import { _td } from '../languageHandler'; -var StandardActions = require('./StandardActions'); -var PushRuleVectorState = require('./PushRuleVectorState'); +const StandardActions = require('./StandardActions'); +const PushRuleVectorState = require('./PushRuleVectorState'); class VectorPushRuleDefinition { constructor(opts) { @@ -30,16 +30,16 @@ class VectorPushRuleDefinition { // Translate the rule actions and its enabled value into vector state ruleToVectorState(rule) { - var enabled = false; - var actions = null; + let enabled = false; + let actions = null; if (rule) { enabled = rule.enabled; actions = rule.actions; } - for (var stateKey in PushRuleVectorState.states) { - var state = PushRuleVectorState.states[stateKey]; - var vectorStateToActions = this.vectorStateToActions[state]; + for (const stateKey in PushRuleVectorState.states) { + const state = PushRuleVectorState.states[stateKey]; + const vectorStateToActions = this.vectorStateToActions[state]; if (!vectorStateToActions) { // No defined actions means that this vector state expects a disabled (or absent) rule @@ -58,7 +58,7 @@ class VectorPushRuleDefinition { JSON.stringify(rule)); return undefined; } -}; +} /** * The descriptions of rules managed by the Vector UI. @@ -71,8 +71,8 @@ module.exports = { vectorStateToActions: { // The actions for each vector state, or null to disable the rule. on: StandardActions.ACTION_NOTIFY, loud: StandardActions.ACTION_HIGHLIGHT_DEFAULT_SOUND, - off: StandardActions.ACTION_DISABLED - } + off: StandardActions.ACTION_DISABLED, + }, }), // Messages containing user's username (localpart/MXID) @@ -82,8 +82,8 @@ module.exports = { vectorStateToActions: { // The actions for each vector state, or null to disable the rule. on: StandardActions.ACTION_NOTIFY, loud: StandardActions.ACTION_HIGHLIGHT_DEFAULT_SOUND, - off: StandardActions.ACTION_DISABLED - } + off: StandardActions.ACTION_DISABLED, + }, }), // Messages just sent to the user in a 1:1 room @@ -93,8 +93,8 @@ module.exports = { vectorStateToActions: { on: StandardActions.ACTION_NOTIFY, loud: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND, - off: StandardActions.ACTION_DONT_NOTIFY - } + off: StandardActions.ACTION_DONT_NOTIFY, + }, }), // Messages just sent to a group chat room @@ -106,8 +106,8 @@ module.exports = { vectorStateToActions: { on: StandardActions.ACTION_NOTIFY, loud: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND, - off: StandardActions.ACTION_DONT_NOTIFY - } + off: StandardActions.ACTION_DONT_NOTIFY, + }, }), // Invitation for the user @@ -117,8 +117,8 @@ module.exports = { vectorStateToActions: { on: StandardActions.ACTION_NOTIFY, loud: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND, - off: StandardActions.ACTION_DISABLED - } + off: StandardActions.ACTION_DISABLED, + }, }), // Incoming call @@ -128,8 +128,8 @@ module.exports = { vectorStateToActions: { on: StandardActions.ACTION_NOTIFY, loud: StandardActions.ACTION_NOTIFY_RING_SOUND, - off: StandardActions.ACTION_DISABLED - } + off: StandardActions.ACTION_DISABLED, + }, }), // Notifications from bots @@ -141,6 +141,6 @@ module.exports = { on: StandardActions.ACTION_DISABLED, loud: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND, off: StandardActions.ACTION_DONT_NOTIFY, - } + }, }), }; diff --git a/src/rageshake/rageshake.js b/src/rageshake/rageshake.js index 93a52ba1aa..29dbe4f41d 100644 --- a/src/rageshake/rageshake.js +++ b/src/rageshake/rageshake.js @@ -60,11 +60,11 @@ class ConsoleLogger { }; Object.keys(consoleFunctionsToLevels).forEach((fnName) => { const level = consoleFunctionsToLevels[fnName]; - let originalFn = consoleObj[fnName].bind(consoleObj); + const originalFn = consoleObj[fnName].bind(consoleObj); consoleObj[fnName] = (...args) => { this.log(level, ...args); originalFn(...args); - } + }; }); } @@ -116,7 +116,7 @@ class IndexedDBLogStore { * @return {Promise} Resolves when the store is ready. */ connect() { - let req = this.indexedDB.open("logs"); + const req = this.indexedDB.open("logs"); return new Promise((resolve, reject) => { req.onsuccess = (event) => { this.db = event.target.result; @@ -127,7 +127,7 @@ class IndexedDBLogStore { req.onerror = (event) => { const err = ( - "Failed to open log database: " + event.target.errorCode + "Failed to open log database: " + event.target.error.name ); console.error(err); reject(new Error(err)); @@ -137,7 +137,7 @@ class IndexedDBLogStore { req.onupgradeneeded = (event) => { const db = event.target.result; const logObjStore = db.createObjectStore("logs", { - keyPath: ["id", "index"] + keyPath: ["id", "index"], }); // Keys in the database look like: [ "instance-148938490", 0 ] // Later on we need to query everything based on an instance id. @@ -146,15 +146,15 @@ class IndexedDBLogStore { logObjStore.add( this._generateLogEntry( - new Date() + " ::: Log database was created." - ) + new Date() + " ::: Log database was created.", + ), ); const lastModifiedStore = db.createObjectStore("logslastmod", { keyPath: "id", }); lastModifiedStore.add(this._generateLastModifiedTime()); - } + }; }); } @@ -206,21 +206,21 @@ class IndexedDBLogStore { resolve(); return; } - let txn = this.db.transaction(["logs", "logslastmod"], "readwrite"); - let objStore = txn.objectStore("logs"); + const txn = this.db.transaction(["logs", "logslastmod"], "readwrite"); + const objStore = txn.objectStore("logs"); txn.oncomplete = (event) => { resolve(); }; txn.onerror = (event) => { console.error( - "Failed to flush logs : ", event + "Failed to flush logs : ", event, ); reject( - new Error("Failed to write logs: " + event.target.errorCode) + new Error("Failed to write logs: " + event.target.errorCode), ); - } + }; objStore.add(this._generateLogEntry(lines)); - let lastModStore = txn.objectStore("logslastmod"); + const lastModStore = txn.objectStore("logslastmod"); lastModStore.put(this._generateLastModifiedTime()); }); return this.flushPromise; @@ -241,20 +241,27 @@ class IndexedDBLogStore { // Returns: a string representing the concatenated logs for this ID. function fetchLogs(id) { - const o = db.transaction("logs", "readonly").objectStore("logs"); - return selectQuery(o.index("id"), IDBKeyRange.only(id), - (cursor) => { - return { - lines: cursor.value.lines, - index: cursor.value.index, - } - }).then((linesArray) => { - // We have been storing logs periodically, so string them all - // together *in order of index* now - linesArray.sort((a, b) => { - return a.index - b.index; - }) - return linesArray.map((l) => l.lines).join(""); + const objectStore = db.transaction("logs", "readonly").objectStore("logs"); + + return new Promise((resolve, reject) => { + const query = objectStore.index("id").openCursor(IDBKeyRange.only(id), 'next'); + let lines = ''; + query.onerror = (event) => { + reject(new Error("Query failed: " + event.target.errorCode)); + }; + query.onsuccess = (event) => { + const cursor = event.target.result; + if (!cursor) { + resolve(lines); + return; // end of results + } + lines += cursor.value.lines; + if (lines.length >= MAX_LOG_SIZE) { + resolve(lines); + } else { + cursor.continue(); + } + }; }); } @@ -262,7 +269,7 @@ class IndexedDBLogStore { function fetchLogIds() { // To gather all the log IDs, query for all records in logslastmod. const o = db.transaction("logslastmod", "readonly").objectStore( - "logslastmod" + "logslastmod", ); return selectQuery(o, undefined, (cursor) => { return { @@ -280,7 +287,7 @@ class IndexedDBLogStore { function deleteLogs(id) { return new Promise((resolve, reject) => { const txn = db.transaction( - ["logs", "logslastmod"], "readwrite" + ["logs", "logslastmod"], "readwrite", ); const o = txn.objectStore("logs"); // only load the key path, not the data which may be huge @@ -292,7 +299,7 @@ class IndexedDBLogStore { } o.delete(cursor.primaryKey); cursor.continue(); - } + }; txn.oncomplete = () => { resolve(); }; @@ -300,8 +307,8 @@ class IndexedDBLogStore { reject( new Error( "Failed to delete logs for " + - `'${id}' : ${event.target.errorCode}` - ) + `'${id}' : ${event.target.errorCode}`, + ), ); }; // delete last modified entries @@ -310,21 +317,18 @@ class IndexedDBLogStore { }); } - let allLogIds = await fetchLogIds(); + const allLogIds = await fetchLogIds(); let removeLogIds = []; - let logs = []; + const logs = []; let size = 0; for (let i = 0; i < allLogIds.length; i++) { - let lines = await fetchLogs(allLogIds[i]); + const lines = await fetchLogs(allLogIds[i]); // always include at least one log file, but only include // subsequent ones if they won't take us over the MAX_LOG_SIZE if (i > 0 && size + lines.length > MAX_LOG_SIZE) { // the remaining log IDs should be removed. If we go out of // bounds this is just [] - // - // XXX: there's nothing stopping the current session exceeding - // MAX_LOG_SIZE. We ought to think about culling it. removeLogIds = allLogIds.slice(i + 1); break; } @@ -343,7 +347,7 @@ class IndexedDBLogStore { console.log(`Removed ${removeLogIds.length} old logs.`); }, (err) => { console.error(err); - }) + }); } return logs; } @@ -352,7 +356,7 @@ class IndexedDBLogStore { return { id: this.id, lines: lines, - index: this.index++ + index: this.index++, }; } @@ -377,7 +381,7 @@ class IndexedDBLogStore { function selectQuery(store, keyRange, resultMapper) { const query = store.openCursor(keyRange); return new Promise((resolve, reject) => { - let results = []; + const results = []; query.onerror = (event) => { reject(new Error("Query failed: " + event.target.errorCode)); }; @@ -390,7 +394,7 @@ function selectQuery(store, keyRange, resultMapper) { } results.push(resultMapper(cursor)); cursor.continue(); - } + }; }); } @@ -414,7 +418,7 @@ module.exports = { let indexedDB; try { indexedDB = window.indexedDB; - } catch(e) {} + } catch (e) {} if (indexedDB) { global.mx_rage_store = new IndexedDBLogStore(indexedDB, global.mx_rage_logger); @@ -451,7 +455,7 @@ module.exports = { getLogsForReport: async function() { if (!global.mx_rage_logger) { throw new Error( - "No console logger, did you forget to call init()?" + "No console logger, did you forget to call init()?", ); } // If in incognito mode, store is null, but we still want bug report @@ -460,8 +464,7 @@ module.exports = { // flush most recent logs await global.mx_rage_store.flush(); return await global.mx_rage_store.consume(); - } - else { + } else { return [{ lines: global.mx_rage_logger.flush(true), id: "-", diff --git a/src/rageshake/submit-rageshake.js b/src/rageshake/submit-rageshake.js index 086cf25d00..a93997a794 100644 --- a/src/rageshake/submit-rageshake.js +++ b/src/rageshake/submit-rageshake.js @@ -22,7 +22,7 @@ import MatrixClientPeg from '../MatrixClientPeg'; import PlatformPeg from '../PlatformPeg'; import { _t } from '../languageHandler'; -import rageshake from './rageshake' +import rageshake from './rageshake'; // polyfill textencoder if necessary @@ -59,8 +59,7 @@ export default async function sendBugReport(bugReportEndpoint, opts) { let version = "UNKNOWN"; try { version = await PlatformPeg.get().getAppVersion(); - } - catch (err) {} // PlatformPeg already logs this. + } catch (err) {} // PlatformPeg already logs this. let userAgent = "UNKNOWN"; if (window.navigator && window.navigator.userAgent) { @@ -85,7 +84,7 @@ export default async function sendBugReport(bugReportEndpoint, opts) { if (opts.sendLogs) { progressCallback(_t("Collecting logs")); const logs = await rageshake.getLogsForReport(); - for (let entry of logs) { + for (const entry of logs) { // encode as UTF-8 const buf = new TextEncoder().encode(entry.lines); diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 3e0c374c8a..3da2cdd668 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -88,7 +88,7 @@ export const SETTINGS = { displayName: _td("Increase performance by only loading room members on first view"), supportedLevels: LEVELS_FEATURE, controller: new LazyLoadingController(), - default: false, + default: true, }, "feature_keybackup": { isFeature: true, @@ -296,4 +296,9 @@ export const SETTINGS = { displayName: _td('Show empty room list headings'), default: true, }, + "showDeveloperTools": { + supportedLevels: LEVELS_ACCOUNT_SETTINGS, + displayName: _td('Show developer tools'), + default: false, + }, }; diff --git a/src/utils/DMRoomMap.js b/src/utils/DMRoomMap.js index bea6e702fa..bb5e4d706b 100644 --- a/src/utils/DMRoomMap.js +++ b/src/utils/DMRoomMap.js @@ -104,7 +104,7 @@ export default class DMRoomMap { .some((ids) => ids.roomId === roomId); }); guessedUserIdsThatChanged.forEach(({userId, roomId}) => { - let roomIds = userToRooms[userId]; + const roomIds = userToRooms[userId]; if (!roomIds) { userToRooms[userId] = [roomId]; } else { diff --git a/src/utils/createMatrixClient.js b/src/utils/createMatrixClient.js index 54312695b6..2acd1fae28 100644 --- a/src/utils/createMatrixClient.js +++ b/src/utils/createMatrixClient.js @@ -32,13 +32,18 @@ try { * @param {Object} opts options to pass to Matrix.createClient. This will be * extended with `sessionStore` and `store` members. * + * @param {bool} useIndexedDb True to attempt to use indexeddb, or false to force + * use of the memory store. Default: true. + * * @property {string} indexedDbWorkerScript Optional URL for a web worker script * for IndexedDB store operations. By default, indexeddb ops are done on * the main thread. * * @returns {MatrixClient} the newly-created MatrixClient */ -export default function createMatrixClient(opts) { +export default function createMatrixClient(opts, useIndexedDb) { + if (useIndexedDb === undefined) useIndexedDb = true; + const storeOpts = { useAuthorizationHeader: true, }; @@ -47,7 +52,7 @@ export default function createMatrixClient(opts) { storeOpts.sessionStore = new Matrix.WebStorageSessionStore(localStorage); } - if (indexedDB && localStorage) { + if (indexedDB && localStorage && useIndexedDb) { storeOpts.store = new Matrix.IndexedDBStore({ indexedDB: indexedDB, dbName: "riot-web-sync", diff --git a/test/PhasedRollOut-test.js b/test/PhasedRollOut-test.js new file mode 100644 index 0000000000..600b9051f7 --- /dev/null +++ b/test/PhasedRollOut-test.js @@ -0,0 +1,72 @@ +/* +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 expect from 'expect'; +import {phasedRollOutExpiredForUser} from '../src/PhasedRollOut'; + +const OFFSET = 6000000; +// phasedRollOutExpiredForUser enables users in bucks of 1 minute +const MS_IN_MINUTE = 60 * 1000; + +describe('PhasedRollOut', function() { + it('should return true if phased rollout is not configured', function() { + expect(phasedRollOutExpiredForUser("@user:hs", "feature_test", 0, null)).toBeTruthy(); + }); + + it('should return true if phased rollout feature is not configured', function() { + expect(phasedRollOutExpiredForUser("@user:hs", "feature_test", 0, { + "feature_other": {offset: 0, period: 0}, + })).toBeTruthy(); + }); + + it('should return false if phased rollout for feature is misconfigured', function() { + expect(phasedRollOutExpiredForUser("@user:hs", "feature_test", 0, { + "feature_test": {}, + })).toBeFalsy(); + }); + + it("should return false if phased rollout hasn't started yet", function() { + expect(phasedRollOutExpiredForUser("@user:hs", "feature_test", 5000000, { + "feature_test": {offset: OFFSET, period: MS_IN_MINUTE}, + })).toBeFalsy(); + }); + + it("should start to return true in bucket 2/10 for '@user:hs'", function() { + expect(phasedRollOutExpiredForUser("@user:hs", "feature_test", + OFFSET + (MS_IN_MINUTE * 2) - 1, { + "feature_test": {offset: OFFSET, period: MS_IN_MINUTE * 10}, + })).toBeFalsy(); + expect(phasedRollOutExpiredForUser("@user:hs", "feature_test", + OFFSET + (MS_IN_MINUTE * 2), { + "feature_test": {offset: OFFSET, period: MS_IN_MINUTE * 10}, + })).toBeTruthy(); + }); + + it("should start to return true in bucket 4/10 for 'alice@other-hs'", function() { + expect(phasedRollOutExpiredForUser("alice@other-hs", "feature_test", + OFFSET + (MS_IN_MINUTE * 4) - 1, { + "feature_test": {offset: OFFSET, period: MS_IN_MINUTE * 10}, + })).toBeFalsy(); + expect(phasedRollOutExpiredForUser("alice@other-hs", "feature_test", + OFFSET + (MS_IN_MINUTE * 4), { + "feature_test": {offset: OFFSET, period: MS_IN_MINUTE * 10}, + })).toBeTruthy(); + }); + + it("should return true after complete rollout period'", function() { + expect(phasedRollOutExpiredForUser("user:hs", "feature_test", + OFFSET + (MS_IN_MINUTE * 20), { + "feature_test": {offset: OFFSET, period: MS_IN_MINUTE * 10}, + })).toBeTruthy(); + }); +}); diff --git a/test/autocomplete/QueryMatcher-test.js b/test/autocomplete/QueryMatcher-test.js new file mode 100644 index 0000000000..864e1da81d --- /dev/null +++ b/test/autocomplete/QueryMatcher-test.js @@ -0,0 +1,175 @@ +/* +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 expect from 'expect'; + +import QueryMatcher from '../../src/autocomplete/QueryMatcher'; + +const OBJECTS = [ + { name: "Mel B", nick: "Scary" }, + { name: "Mel C", nick: "Sporty" }, + { name: "Emma", nick: "Baby" }, + { name: "Geri", nick: "Ginger" }, + { name: "Victoria", nick: "Posh" }, +]; + +const NONWORDOBJECTS = [ + { name: "B.O.B" }, + { name: "bob" }, +]; + +describe('QueryMatcher', function() { + it('Returns results by key', function() { + const qm = new QueryMatcher(OBJECTS, {keys: ["name"]}); + const results = qm.match('Geri'); + + expect(results.length).toBe(1); + expect(results[0].name).toBe('Geri'); + }); + + it('Returns results by prefix', function() { + const qm = new QueryMatcher(OBJECTS, {keys: ["name"]}); + const results = qm.match('Ge'); + + expect(results.length).toBe(1); + expect(results[0].name).toBe('Geri'); + }); + + it('Matches case-insensitive', function() { + const qm = new QueryMatcher(OBJECTS, {keys: ["name"]}); + const results = qm.match('geri'); + + expect(results.length).toBe(1); + expect(results[0].name).toBe('Geri'); + }); + + it('Matches ignoring accents', function() { + const qm = new QueryMatcher([{name: "Gëri", foo: 46}], {keys: ["name"]}); + const results = qm.match('geri'); + + expect(results.length).toBe(1); + expect(results[0].foo).toBe(46); + }); + + it('Returns multiple results in order of search string appearance', function() { + const qm = new QueryMatcher(OBJECTS, {keys: ["name", "nick"]}); + const results = qm.match('or'); + + expect(results.length).toBe(2); + expect(results[0].name).toBe('Mel C'); + expect(results[1].name).toBe('Victoria'); + + + qm.setObjects(OBJECTS.slice().reverse()); + const reverseResults = qm.match('or'); + + // should still be in the same order: search string position + // takes precedence over input order + expect(reverseResults.length).toBe(2); + expect(reverseResults[0].name).toBe('Mel C'); + expect(reverseResults[1].name).toBe('Victoria'); + }); + + it('Returns results with search string in same place in insertion order', function() { + const qm = new QueryMatcher(OBJECTS, {keys: ["name"]}); + const results = qm.match('Mel'); + + expect(results.length).toBe(2); + expect(results[0].name).toBe('Mel B'); + expect(results[1].name).toBe('Mel C'); + + + qm.setObjects(OBJECTS.slice().reverse()); + + const reverseResults = qm.match('Mel'); + + expect(reverseResults.length).toBe(2); + expect(reverseResults[0].name).toBe('Mel C'); + expect(reverseResults[1].name).toBe('Mel B'); + }); + + it('Returns numeric results in correct order (input pos)', function() { + // regression test for depending on object iteration order + const qm = new QueryMatcher([ + {name: "123456badger"}, + {name: "123456"}, + ], {keys: ["name"]}); + const results = qm.match('123456'); + + expect(results.length).toBe(2); + expect(results[0].name).toBe('123456badger'); + expect(results[1].name).toBe('123456'); + }); + + it('Returns numeric results in correct order (query pos)', function() { + const qm = new QueryMatcher([ + {name: "999999123456"}, + {name: "123456badger"}, + ], {keys: ["name"]}); + const results = qm.match('123456'); + + expect(results.length).toBe(2); + expect(results[0].name).toBe('123456badger'); + expect(results[1].name).toBe('999999123456'); + }); + + it('Returns results by function', function() { + const qm = new QueryMatcher(OBJECTS, { + keys: ["name"], + funcs: [x => x.name.replace('Mel', 'Emma')], + }); + + const results = qm.match('Emma'); + expect(results.length).toBe(3); + expect(results[0].name).toBe('Mel B'); + expect(results[1].name).toBe('Mel C'); + expect(results[2].name).toBe('Emma'); + }); + + it('Matches words only by default', function() { + const qm = new QueryMatcher(NONWORDOBJECTS, { keys: ["name"] }); + + const results = qm.match('bob'); + expect(results.length).toBe(2); + expect(results[0].name).toBe('B.O.B'); + expect(results[1].name).toBe('bob'); + }); + + it('Matches all chars with words-only off', function() { + const qm = new QueryMatcher(NONWORDOBJECTS, { + keys: ["name"], + shouldMatchWordsOnly: false, + }); + + const results = qm.match('bob'); + expect(results.length).toBe(1); + expect(results[0].name).toBe('bob'); + }); + + it('Matches only by prefix with shouldMatchPrefix on', function() { + const qm = new QueryMatcher([ + {name: "Victoria"}, + {name: "Tori"}, + ], { + keys: ["name"], + shouldMatchPrefix: true, + }); + + const results = qm.match('tori'); + expect(results.length).toBe(1); + expect(results[0].name).toBe('Tori'); + }); +});