From c99f5c0df87a0894c61888a057a280019326aa99 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Thu, 3 May 2018 16:28:46 +0100 Subject: [PATCH 001/237] Check upload limits before trying to upload large files --- src/components/views/rooms/MessageComposer.js | 63 +++++++++++++++---- src/i18n/strings/en_EN.json | 5 +- 2 files changed, 55 insertions(+), 13 deletions(-) diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index bac996e65c..ae72908b82 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -16,6 +16,7 @@ limitations under the License. */ import React from 'react'; import PropTypes from 'prop-types'; +import filesize from "filesize"; import { _t } from '../../../languageHandler'; import CallHandler from '../../../CallHandler'; import MatrixClientPeg from '../../../MatrixClientPeg'; @@ -97,18 +98,40 @@ export default class MessageComposer extends React.Component { } onUploadFileSelected(files) { - this.uploadFiles(files.target.files); + const tfiles = files.target.files; + MatrixClientPeg.get().getMediaLimits().then((limits) => { + this.uploadFiles(tfiles, limits); + }); } - uploadFiles(files) { + isFileUploadAllowed(file, limits) { + const sizeLimit = limits.size || -1; + if (sizeLimit !== -1 && file.size > sizeLimit) { + return _t("File is too big. Maximum file size is %(fileSize)s", {fileSize: filesize(sizeLimit)}); + } + return true; + } + + uploadFiles(files, limits) { const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const TintableSvg = sdk.getComponent("elements.TintableSvg"); const fileList = []; + const acceptedFiles = []; + const failedFiles = []; + for (let i=0; i - { files[i].name || _t('Attachment') } - ); + const fileAcceptedOrError = this.isFileUploadAllowed(files[i], limits); + if (fileAcceptedOrError === true) { + acceptedFiles.push(
  • + { files[i].name || _t('Attachment') } +
  • ); + fileList.push(files[i]); + } else { + failedFiles.push(
  • + { files[i].name || _t('Attachment') } { _t('Reason') + ": " + fileAcceptedOrError} +
  • ); + } } const isQuoting = Boolean(RoomViewStore.getQuotingEvent()); @@ -119,23 +142,39 @@ export default class MessageComposer extends React.Component { }

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

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

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

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

    +
      + { failedFiles } +
    +
    + ); + Modal.createTrackedDialog('Upload Files confirmation', '', QuestionDialog, { title: _t('Upload Files'), description: (
    -

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

    -
      - { fileList } -
    + { acceptedFilesPart } + { failedFilesPart } { replyToWarning }
    ), onFinished: (shouldUpload) => { if (shouldUpload) { // MessageComposer shouldn't have to rely on its parent passing in a callback to upload a file - if (files) { - for (let i=0; i Date: Thu, 3 May 2018 16:37:25 +0100 Subject: [PATCH 002/237] Linting --- src/components/views/rooms/MessageComposer.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index ae72908b82..8194240319 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -101,13 +101,15 @@ export default class MessageComposer extends React.Component { const tfiles = files.target.files; MatrixClientPeg.get().getMediaLimits().then((limits) => { this.uploadFiles(tfiles, limits); + }).catch(() => { + // HS can't or won't provide limits, so don't give any. + this.uploadFiles(tfiles, {}); }); } isFileUploadAllowed(file, limits) { - const sizeLimit = limits.size || -1; - if (sizeLimit !== -1 && file.size > sizeLimit) { - return _t("File is too big. Maximum file size is %(fileSize)s", {fileSize: filesize(sizeLimit)}); + if (limits.size != null && file.size > limits.size) { + return _t("File is too big. Maximum file size is %(fileSize)s", {fileSize: filesize(limits.size)}); } return true; } From 7c0811dd921d37f8835d7539aa35e47d315e0a49 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Thu, 3 May 2018 17:02:37 +0100 Subject: [PATCH 003/237] size > upload_size as per spec feedback --- src/components/views/rooms/MessageComposer.js | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 8194240319..7d4b4f690f 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -65,6 +65,13 @@ export default class MessageComposer extends React.Component { // XXX: fragile as all hell - fixme somehow, perhaps with a dedicated Room.encryption event or something. MatrixClientPeg.get().on("event", this.onEvent); this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate); + + MatrixClientPeg.get().getMediaLimits().then((limits) => { + this._uploadLimits = limits; + }).catch(() => { + // HS can't or won't provide limits, so don't give any. + this._uploadLimits = {}; + }) } componentWillUnmount() { @@ -99,17 +106,12 @@ export default class MessageComposer extends React.Component { onUploadFileSelected(files) { const tfiles = files.target.files; - MatrixClientPeg.get().getMediaLimits().then((limits) => { - this.uploadFiles(tfiles, limits); - }).catch(() => { - // HS can't or won't provide limits, so don't give any. - this.uploadFiles(tfiles, {}); - }); + this.uploadFiles(tfiles); } - isFileUploadAllowed(file, limits) { - if (limits.size != null && file.size > limits.size) { - return _t("File is too big. Maximum file size is %(fileSize)s", {fileSize: filesize(limits.size)}); + isFileUploadAllowed(file) { + if (this._uploadLimits.upload_size != null && file.size > this._uploadLimits.upload_size) { + return _t("File is too big. Maximum file size is %(fileSize)s", {fileSize: filesize(this._uploadLimits.upload_size)}); } return true; } @@ -123,7 +125,7 @@ export default class MessageComposer extends React.Component { const failedFiles = []; for (let i=0; i { files[i].name || _t('Attachment') } From 76f0f15e491ad72249e9c8911890bbb02e94d849 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Thu, 21 Jun 2018 09:36:41 +0100 Subject: [PATCH 004/237] Fix nitpicks --- src/components/views/rooms/MessageComposer.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 7d4b4f690f..bb8f7d78d2 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -16,7 +16,7 @@ limitations under the License. */ import React from 'react'; import PropTypes from 'prop-types'; -import filesize from "filesize"; +import filesize from 'filesize'; import { _t } from '../../../languageHandler'; import CallHandler from '../../../CallHandler'; import MatrixClientPeg from '../../../MatrixClientPeg'; @@ -110,13 +110,13 @@ export default class MessageComposer extends React.Component { } isFileUploadAllowed(file) { - if (this._uploadLimits.upload_size != null && file.size > this._uploadLimits.upload_size) { + if (this._uploadLimits.upload_size !== undefined && file.size > this._uploadLimits.upload_size) { return _t("File is too big. Maximum file size is %(fileSize)s", {fileSize: filesize(this._uploadLimits.upload_size)}); } return true; } - uploadFiles(files, limits) { + uploadFiles(files) { const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const TintableSvg = sdk.getComponent("elements.TintableSvg"); From caf2086585110de7e606e34118e42bda01f234f0 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Thu, 21 Jun 2018 10:08:41 +0100 Subject: [PATCH 005/237] Restructure limits to be set at RoomView, so they may change if the upload fails --- src/components/structures/RoomView.js | 23 ++++++++++++++++++- src/components/views/rooms/MessageComposer.js | 17 ++++++-------- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 4beafb099c..cb6b94c2e6 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -49,6 +49,8 @@ import SettingsStore, {SettingLevel} from "../../settings/SettingsStore"; const DEBUG = false; let debuglog = function() {}; +let mediaLimitCache = null; + const BROWSER_SUPPORTS_SANDBOX = 'sandbox' in document.createElement('iframe'); if (DEBUG) { @@ -94,6 +96,9 @@ module.exports = React.createClass({ roomLoading: true, peekLoading: false, shouldPeek: true, + + // Media limits for uploading, this may change. + mediaLimits: {}, // The event to be scrolled to initially initialEventId: null, @@ -147,12 +152,26 @@ module.exports = React.createClass({ MatrixClientPeg.get().on("RoomState.members", this.onRoomStateMember); MatrixClientPeg.get().on("RoomMember.membership", this.onRoomMemberMembership); MatrixClientPeg.get().on("accountData", this.onAccountData); - + this._fetchMediaLimits(); // Start listening for RoomViewStore updates this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate); this._onRoomViewStoreUpdate(true); }, + _fetchMediaLimits: function(invalidateCache: boolean = false) { + let limits; + if(invalidateCache || mediaLimitCache == null) { + MatrixClientPeg.get().getMediaLimits().then((_limits) => { + limits = _limits; + }).catch(() => { + // Media repo can't or won't report limits, so provide an empty object (no limits). + limits = {}; + }); + mediaLimitCache = limits; + } + this.state.mediaLimits = limits; + }, + _onRoomViewStoreUpdate: function(initial) { if (this.unmounted) { return; @@ -481,6 +500,7 @@ module.exports = React.createClass({ break; case 'notifier_enabled': case 'upload_failed': + this._fetchMediaLimits(true); case 'upload_started': case 'upload_finished': this.forceUpdate(); @@ -1654,6 +1674,7 @@ module.exports = React.createClass({ callState={this.state.callState} disabled={this.props.disabled} showApps={this.state.showApps} + mediaLimits={this.state.mediaLimits} />; } diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index bb8f7d78d2..1a80a0be4f 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -65,13 +65,6 @@ export default class MessageComposer extends React.Component { // XXX: fragile as all hell - fixme somehow, perhaps with a dedicated Room.encryption event or something. MatrixClientPeg.get().on("event", this.onEvent); this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate); - - MatrixClientPeg.get().getMediaLimits().then((limits) => { - this._uploadLimits = limits; - }).catch(() => { - // HS can't or won't provide limits, so don't give any. - this._uploadLimits = {}; - }) } componentWillUnmount() { @@ -110,8 +103,10 @@ export default class MessageComposer extends React.Component { } isFileUploadAllowed(file) { - if (this._uploadLimits.upload_size !== undefined && file.size > this._uploadLimits.upload_size) { - return _t("File is too big. Maximum file size is %(fileSize)s", {fileSize: filesize(this._uploadLimits.upload_size)}); + if (this.props.mediaLimits !== undefined && + this.props.mediaLimits.upload_size !== undefined && + file.size > this.props.mediaLimits.upload_size) { + return _t("File is too big. Maximum file size is %(fileSize)s", {fileSize: filesize(this.mediaLimits.upload_size)}); } return true; } @@ -430,6 +425,8 @@ MessageComposer.propTypes = { // callback when a file to upload is chosen uploadFile: PropTypes.func.isRequired, + mediaLimits: PropTypes.object, + // string representing the current room app drawer state - showApps: PropTypes.bool, + showApps: PropTypes.bool }; From 736b76bbb0ddcedabd348c1d849cba3595535cbe Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Thu, 21 Jun 2018 11:29:06 +0100 Subject: [PATCH 006/237] If HttpStatus == 413, refresh media limits --- src/ContentMessages.js | 4 ++-- src/components/structures/RoomView.js | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/ContentMessages.js b/src/ContentMessages.js index fd21977108..f2bbdfafe5 100644 --- a/src/ContentMessages.js +++ b/src/ContentMessages.js @@ -377,9 +377,9 @@ class ContentMessages { } } if (error) { - dis.dispatch({action: 'upload_failed', upload: upload}); + dis.dispatch({action: 'upload_failed', upload, error}); } else { - dis.dispatch({action: 'upload_finished', upload: upload}); + dis.dispatch({action: 'upload_finished', upload}); } }); } diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index cb6b94c2e6..01485deecb 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -500,7 +500,10 @@ module.exports = React.createClass({ break; case 'notifier_enabled': case 'upload_failed': - this._fetchMediaLimits(true); + // 413: File was too big or upset the server in some way. + if(payload.data.error.http_status === 413) { + this._fetchMediaLimits(true); + } case 'upload_started': case 'upload_finished': this.forceUpdate(); From 541f1d71fb3e7f444b00e77f242804c7e8e3fa3c Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Sat, 23 Jun 2018 14:28:20 +0100 Subject: [PATCH 007/237] Move upload verification logic to RoomView. Improve upload dialog ux --- src/components/structures/RoomView.js | 55 ++++++++++++------- src/components/views/rooms/MessageComposer.js | 25 ++++----- 2 files changed, 47 insertions(+), 33 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 01485deecb..4866fe3f60 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -26,6 +26,7 @@ const React = require("react"); const ReactDOM = require("react-dom"); import PropTypes from 'prop-types'; import Promise from 'bluebird'; +import filesize from 'filesize'; const classNames = require("classnames"); import { _t } from '../../languageHandler'; @@ -49,8 +50,6 @@ import SettingsStore, {SettingLevel} from "../../settings/SettingsStore"; const DEBUG = false; let debuglog = function() {}; -let mediaLimitCache = null; - const BROWSER_SUPPORTS_SANDBOX = 'sandbox' in document.createElement('iframe'); if (DEBUG) { @@ -96,9 +95,9 @@ module.exports = React.createClass({ roomLoading: true, peekLoading: false, shouldPeek: true, - - // Media limits for uploading, this may change. - mediaLimits: {}, + + // Media limits for uploading. + mediaConfig: undefined, // The event to be scrolled to initially initialEventId: null, @@ -152,24 +151,31 @@ module.exports = React.createClass({ MatrixClientPeg.get().on("RoomState.members", this.onRoomStateMember); MatrixClientPeg.get().on("RoomMember.membership", this.onRoomMemberMembership); MatrixClientPeg.get().on("accountData", this.onAccountData); - this._fetchMediaLimits(); + this._fetchMediaConfig(); // Start listening for RoomViewStore updates this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate); this._onRoomViewStoreUpdate(true); }, - _fetchMediaLimits: function(invalidateCache: boolean = false) { - let limits; - if(invalidateCache || mediaLimitCache == null) { - MatrixClientPeg.get().getMediaLimits().then((_limits) => { - limits = _limits; - }).catch(() => { - // Media repo can't or won't report limits, so provide an empty object (no limits). - limits = {}; - }); - mediaLimitCache = limits; + _fetchMediaConfig: function(invalidateCache: boolean = false) { + /// NOTE: Using global here so we don't make repeated requests for the + /// config every time we swap room. + if(global.mediaConfig !== undefined && !invalidateCache) { + this.setState({mediaConfig: global.mediaConfig}); + return; } - this.state.mediaLimits = limits; + console.log("[Media Config] Fetching"); + MatrixClientPeg.get().getMediaConfig().then((config) => { + console.log("[Media Config] Fetched config:", config); + return config; + }).catch(() => { + // Media repo can't or won't report limits, so provide an empty object (no limits). + console.log("[Media Config] Could not fetch config, so not limiting uploads."); + return {}; + }).then((config) => { + global.mediaConfig = config; + this.setState({mediaConfig: config}); + }); }, _onRoomViewStoreUpdate: function(initial) { @@ -501,8 +507,8 @@ module.exports = React.createClass({ case 'notifier_enabled': case 'upload_failed': // 413: File was too big or upset the server in some way. - if(payload.data.error.http_status === 413) { - this._fetchMediaLimits(true); + if(payload.error.http_status === 413) { + this._fetchMediaConfig(true); } case 'upload_started': case 'upload_finished': @@ -935,6 +941,15 @@ module.exports = React.createClass({ this.setState({ draggingFile: false }); }, + isFileUploadAllowed(file) { + if (this.state.mediaConfig !== undefined && + this.state.mediaConfig["m.upload.size"] !== undefined && + file.size > this.state.mediaConfig["m.upload.size"]) { + return _t("File is too big. Maximum file size is %(fileSize)s", {fileSize: filesize(this.state.mediaConfig["m.upload.size"])}); + } + return true; + }, + uploadFile: async function(file) { dis.dispatch({action: 'focus_composer'}); @@ -1677,7 +1692,7 @@ module.exports = React.createClass({ callState={this.state.callState} disabled={this.props.disabled} showApps={this.state.showApps} - mediaLimits={this.state.mediaLimits} + uploadAllowed={this.isFileUploadAllowed} />; } diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 1a80a0be4f..f0c73a67af 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -16,7 +16,6 @@ limitations under the License. */ import React from 'react'; import PropTypes from 'prop-types'; -import filesize from 'filesize'; import { _t } from '../../../languageHandler'; import CallHandler from '../../../CallHandler'; import MatrixClientPeg from '../../../MatrixClientPeg'; @@ -102,15 +101,6 @@ export default class MessageComposer extends React.Component { this.uploadFiles(tfiles); } - isFileUploadAllowed(file) { - if (this.props.mediaLimits !== undefined && - this.props.mediaLimits.upload_size !== undefined && - file.size > this.props.mediaLimits.upload_size) { - return _t("File is too big. Maximum file size is %(fileSize)s", {fileSize: filesize(this.mediaLimits.upload_size)}); - } - return true; - } - uploadFiles(files) { const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const TintableSvg = sdk.getComponent("elements.TintableSvg"); @@ -120,7 +110,7 @@ export default class MessageComposer extends React.Component { const failedFiles = []; for (let i=0; i { files[i].name || _t('Attachment') } @@ -128,7 +118,7 @@ export default class MessageComposer extends React.Component { fileList.push(files[i]); } else { failedFiles.push(
  • - { files[i].name || _t('Attachment') } { _t('Reason') + ": " + fileAcceptedOrError} + { files[i].name || _t('Attachment') }

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

  • ); } } @@ -158,6 +148,12 @@ export default class MessageComposer extends React.Component { ); + let buttonText; + if (acceptedFiles.length > 0 && failedFiles.length > 0) { + buttonText = "Upload selected" + } else if (failedFiles.length > 0) { + buttonText = "Close" + } Modal.createTrackedDialog('Upload Files confirmation', '', QuestionDialog, { title: _t('Upload Files'), @@ -168,6 +164,8 @@ export default class MessageComposer extends React.Component { { replyToWarning } ), + hasCancelButton: acceptedFiles.length > 0, + button: buttonText, onFinished: (shouldUpload) => { if (shouldUpload) { // MessageComposer shouldn't have to rely on its parent passing in a callback to upload a file @@ -425,7 +423,8 @@ MessageComposer.propTypes = { // callback when a file to upload is chosen uploadFile: PropTypes.func.isRequired, - mediaLimits: PropTypes.object, + // function to test whether a file should be allowed to be uploaded. + uploadAllowed: PropTypes.func.isRequired, // string representing the current room app drawer state showApps: PropTypes.bool From 67d39a40d3c9f5c477bff0310975f0d9b8b6d401 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Tue, 16 Oct 2018 11:26:04 +0100 Subject: [PATCH 008/237] Sneaky , --- src/i18n/strings/en_EN.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 122c798b12..fe70da61d9 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1264,7 +1264,7 @@ "Failed to add tag %(tagName)s to room": "Failed to add tag %(tagName)s to room", "File is too big. Maximum file size is %(fileSize)s": "File is too big. Maximum file size is %(fileSize)s", "Reason": "Reason", - "The following files cannot be uploaded:": "The following files cannot be uploaded:" + "The following files cannot be uploaded:": "The following files cannot be uploaded:", "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", From 0030ba7015c8e2626d739abe6cdad77ea0a4f27c Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 18 Oct 2018 16:42:54 -0600 Subject: [PATCH 009/237] Support .well-known discovery Fixes https://github.com/vector-im/riot-web/issues/7253 --- src/components/structures/login/Login.js | 152 +++++++++++++++++++- src/components/views/login/PasswordLogin.js | 9 +- src/components/views/login/ServerConfig.js | 17 +++ src/i18n/strings/en_EN.json | 2 + 4 files changed, 173 insertions(+), 7 deletions(-) diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index 45f523f141..2ab922c827 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -26,6 +26,7 @@ import Login from '../../../Login'; import SdkConfig from '../../../SdkConfig'; import SettingsStore from "../../../settings/SettingsStore"; import { messageForResourceLimitError } from '../../../utils/ErrorUtils'; +import request from 'browser-request'; // For validating phone numbers without country codes const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/; @@ -74,6 +75,11 @@ module.exports = React.createClass({ phoneCountry: null, phoneNumber: "", currentFlow: "m.login.password", + + // .well-known discovery + discoveredHsUrl: "", + discoveredIsUrl: "", + discoveryError: "", }; }, @@ -102,6 +108,15 @@ module.exports = React.createClass({ }, onPasswordLogin: function(username, phoneCountry, phoneNumber, password) { + // Prevent people from submitting their password when homeserver + // discovery went wrong + if (this.state.discoveryError) return; + + if (this.state.discoveredHsUrl) { + console.log("Rewriting username because the homeserver was discovered"); + username = username.substring(1).split(":")[0]; + } + this.setState({ busy: true, errorText: null, @@ -218,8 +233,12 @@ module.exports = React.createClass({ }).done(); }, - onUsernameChanged: function(username) { + onUsernameChanged: function(username, endOfInput) { this.setState({ username: username }); + if (username[0] === "@" && endOfInput) { + const serverName = username.split(':').slice(1).join(':'); + this._tryWellKnownDiscovery(serverName); + } }, onPhoneCountryChanged: function(phoneCountry) { @@ -257,6 +276,125 @@ module.exports = React.createClass({ }); }, + _tryWellKnownDiscovery: async function(serverName) { + if (!serverName.trim()) { + // Nothing to discover + this.setState({discoveryError: "", discoveredHsUrl: "", discoveredIsUrl: ""}); + return; + } + + try { + const wellknown = await this._getWellKnownObject(`https://${serverName}/.well-known/matrix/client`); + if (!wellknown["m.homeserver"]) { + console.error("No m.homeserver key in well-known response"); + this.setState({discoveryError: _t("Invalid homeserver discovery response")}); + return; + } + + const hsUrl = this._sanitizeWellKnownUrl(wellknown["m.homeserver"]["base_url"]); + if (!hsUrl) { + console.error("Invalid base_url for m.homeserver"); + this.setState({discoveryError: _t("Invalid homeserver discovery response")}); + return; + } + + console.log("Verifying homeserver URL: " + hsUrl); + const hsVersions = await this._getWellKnownObject(`${hsUrl}/_matrix/client/versions`); + if (!hsVersions["versions"]) { + console.error("Invalid /versions response"); + this.setState({discoveryError: _t("Invalid homeserver discovery response")}); + return; + } + + let isUrl = ""; + if (wellknown["m.identity_server"]) { + isUrl = this._sanitizeWellKnownUrl(wellknown["m.identity_server"]["base_url"]); + if (!isUrl) { + console.error("Invalid base_url for m.identity_server"); + this.setState({discoveryError: _t("Invalid homeserver discovery response")}); + return; + } + + // XXX: We don't verify the identity server URL because sydent doesn't register + // the route we need. + + // console.log("Verifying identity server URL: " + isUrl); + // const isResponse = await this._getWellKnownObject(`${isUrl}/_matrix/identity/api/v1`); + // if (!isResponse) { + // console.error("Invalid /api/v1 response"); + // this.setState({discoveryError: _t("Invalid homeserver discovery response")}); + // return; + // } + } + + this.setState({discoveredHsUrl: hsUrl, discoveredIsUrl: isUrl, discoveryError: ""}); + } catch (e) { + console.error(e); + if (e.wkAction) { + if (e.wkAction === "FAIL_ERROR" || e.wkAction === "FAIL_PROMPT") { + // We treat FAIL_ERROR and FAIL_PROMPT the same to avoid having the user + // submit their details to the wrong homeserver. In practice, the custom + // server options will show up to try and guide the user into entering + // the required information. + this.setState({discoveryError: _t("Cannot find homeserver")}); + return; + } else if (e.wkAction === "IGNORE") { + // Nothing to discover + this.setState({discoveryError: "", discoveredHsUrl: "", discoveredIsUrl: ""}); + return; + } + } + + throw e; + } + }, + + _sanitizeWellKnownUrl: function(url) { + if (!url) return false; + + const parser = document.createElement('a'); + parser.href = url; + + if (parser.protocol !== "http:" && parser.protocol !== "https:") return false; + if (!parser.hostname) return false; + + const port = parser.port ? `:${parser.port}` : ""; + const path = parser.pathname ? parser.pathname : ""; + let saferUrl = `${parser.protocol}//${parser.hostname}${port}${path}`; + if (saferUrl.endsWith("/")) saferUrl = saferUrl.substring(0, saferUrl.length - 1); + return saferUrl; + }, + + _getWellKnownObject: function(url) { + return new Promise(function(resolve, reject) { + request( + { method: "GET", url: url }, + (err, response, body) => { + if (err || response.status < 200 || response.status >= 300) { + let action = "FAIL_ERROR"; + if (response.status === 404) { + // We could just resolve with an empty object, but that + // causes a different series of branches when the m.homeserver + // bit of the JSON is missing. + action = "IGNORE"; + } + reject({err: err, response: response, wkAction: action}); + return; + } + + try { + resolve(JSON.parse(body)); + }catch (e) { + console.error(e); + if (e.name === "SyntaxError") { + reject({wkAction: "FAIL_PROMPT", wkError: "Invalid JSON"}); + } else throw e; + } + }, + ); + }); + }, + _initLoginLogic: function(hsUrl, isUrl) { const self = this; hsUrl = hsUrl || this.state.enteredHomeserverUrl; @@ -418,6 +556,8 @@ module.exports = React.createClass({ const ServerConfig = sdk.getComponent("login.ServerConfig"); const loader = this.state.busy ?
    : null; + const errorText = this.state.discoveryError || this.state.errorText; + let loginAsGuestJsx; if (this.props.enableGuest) { loginAsGuestJsx = @@ -432,8 +572,8 @@ module.exports = React.createClass({ if (!SdkConfig.get()['disable_custom_urls']) { serverConfig = { _t('Sign in') } { loader }; } else { - if (!this.state.errorText) { + if (!errorText) { header =

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

    ; } } let errorTextSection; - if (this.state.errorText) { + if (errorText) { errorTextSection = (
    - { this.state.errorText } + { errorText }
    ); } diff --git a/src/components/views/login/PasswordLogin.js b/src/components/views/login/PasswordLogin.js index a0e5ab0ddb..0e3aed1187 100644 --- a/src/components/views/login/PasswordLogin.js +++ b/src/components/views/login/PasswordLogin.js @@ -53,6 +53,7 @@ class PasswordLogin extends React.Component { this.onSubmitForm = this.onSubmitForm.bind(this); this.onUsernameChanged = this.onUsernameChanged.bind(this); + this.onUsernameBlur = this.onUsernameBlur.bind(this); this.onLoginTypeChange = this.onLoginTypeChange.bind(this); this.onPhoneCountryChanged = this.onPhoneCountryChanged.bind(this); this.onPhoneNumberChanged = this.onPhoneNumberChanged.bind(this); @@ -121,7 +122,11 @@ class PasswordLogin extends React.Component { onUsernameChanged(ev) { this.setState({username: ev.target.value}); - this.props.onUsernameChanged(ev.target.value); + this.props.onUsernameChanged(ev.target.value, false); + } + + onUsernameBlur(ev) { + this.props.onUsernameChanged(this.state.username, true); } onLoginTypeChange(loginType) { @@ -167,6 +172,7 @@ class PasswordLogin extends React.Component { type="text" name="username" // make it a little easier for browser's remember-password onChange={this.onUsernameChanged} + onBlur={this.onUsernameBlur} placeholder="joe@example.com" value={this.state.username} autoFocus @@ -182,6 +188,7 @@ class PasswordLogin extends React.Component { type="text" name="username" // make it a little easier for browser's remember-password onChange={this.onUsernameChanged} + onBlur={this.onUsernameBlur} placeholder={SdkConfig.get().disable_custom_urls ? _t("Username on %(hs)s", { hs: this.props.hsUrl.replace(/^https?:\/\//, ''), diff --git a/src/components/views/login/ServerConfig.js b/src/components/views/login/ServerConfig.js index a6944ec20a..2f04011273 100644 --- a/src/components/views/login/ServerConfig.js +++ b/src/components/views/login/ServerConfig.js @@ -70,6 +70,23 @@ module.exports = React.createClass({ }; }, + componentWillReceiveProps: function(newProps) { + if (newProps.customHsUrl === this.state.hs_url && + newProps.customIsUrl === this.state.is_url) return; + + this.setState({ + hs_url: newProps.customHsUrl, + is_url: newProps.customIsUrl, + configVisible: !newProps.withToggleButton || + (newProps.customHsUrl !== newProps.defaultHsUrl) || + (newProps.customIsUrl !== newProps.defaultIsUrl), + }); + this.props.onServerConfigChange({ + hsUrl: newProps.customHsUrl, + isUrl: newProps.customIsUrl, + }); + }, + onHomeserverChanged: function(ev) { this.setState({hs_url: ev.target.value}, function() { this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, function() { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index e42d3ce434..be0963656d 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1205,6 +1205,8 @@ "Please note you are logging into the %(hs)s server, not matrix.org.": "Please note you are logging into the %(hs)s server, not matrix.org.", "Guest access is disabled on this Home Server.": "Guest access is disabled on this Home Server.", "The phone number entered looks invalid": "The phone number entered looks invalid", + "Invalid homeserver discovery response": "Invalid homeserver discovery response", + "Cannot find homeserver": "Cannot find homeserver", "This homeserver doesn't offer any login flows which are supported by this client.": "This homeserver doesn't offer any login flows which are supported by this client.", "Error: Problem communicating with the given homeserver.": "Error: Problem communicating with the given homeserver.", "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or enable unsafe scripts.": "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or enable unsafe scripts.", From 71a97170d79fdf1f5941f4cb6c742b05fb5cc7f1 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 18 Oct 2018 17:06:47 -0600 Subject: [PATCH 010/237] Linting --- src/components/structures/login/Login.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index 2ab922c827..6a33e9369b 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -384,7 +384,7 @@ module.exports = React.createClass({ try { resolve(JSON.parse(body)); - }catch (e) { + } catch (e) { console.error(e); if (e.name === "SyntaxError") { reject({wkAction: "FAIL_PROMPT", wkError: "Invalid JSON"}); From 328d57f06320d839992a6f4eff0fae0eac4c5dfe Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 29 Oct 2018 22:57:33 +0000 Subject: [PATCH 011/237] Remove temporary account_deactivation_preferences Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../views/dialogs/DeactivateAccountDialog.js | 41 +------------------ 1 file changed, 1 insertion(+), 40 deletions(-) diff --git a/src/components/views/dialogs/DeactivateAccountDialog.js b/src/components/views/dialogs/DeactivateAccountDialog.js index 761a1e4209..6e87a816bb 100644 --- a/src/components/views/dialogs/DeactivateAccountDialog.js +++ b/src/components/views/dialogs/DeactivateAccountDialog.js @@ -35,19 +35,10 @@ export default class DeactivateAccountDialog extends React.Component { this._onPasswordFieldChange = this._onPasswordFieldChange.bind(this); this._onEraseFieldChange = this._onEraseFieldChange.bind(this); - const deactivationPreferences = - MatrixClientPeg.get().getAccountData('im.riot.account_deactivation_preferences'); - - const shouldErase = ( - deactivationPreferences && - deactivationPreferences.getContent() && - deactivationPreferences.getContent().shouldErase - ) || false; - this.state = { confirmButtonEnabled: false, busy: false, - shouldErase, + shouldErase: false, errStr: null, }; } @@ -67,36 +58,6 @@ export default class DeactivateAccountDialog extends React.Component { async _onOk() { this.setState({busy: true}); - // Before we deactivate the account insert an event into - // the user's account data indicating that they wish to be - // erased from the homeserver. - // - // We do this because the API for erasing after deactivation - // might not be supported by the connected homeserver. Leaving - // an indication in account data is only best-effort, and - // in the worse case, the HS maintainer would have to run a - // script to erase deactivated accounts that have shouldErase - // set to true in im.riot.account_deactivation_preferences. - // - // Note: The preferences are scoped to Riot, hence the - // "im.riot..." event type. - // - // Note: This may have already been set on previous attempts - // where, for example, the user entered the wrong password. - // This is fine because the UI always indicates the preference - // prior to us calling `deactivateAccount`. - try { - await MatrixClientPeg.get().setAccountData('im.riot.account_deactivation_preferences', { - shouldErase: this.state.shouldErase, - }); - } catch (err) { - this.setState({ - busy: false, - errStr: _t('Failed to indicate account erasure'), - }); - return; - } - try { // This assumes that the HS requires password UI auth // for this endpoint. In reality it could be any UI auth. From 810405c67b4961602cfc2c4927d7c116940157ab Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Fri, 16 Nov 2018 07:01:08 +0000 Subject: [PATCH 012/237] Translated using Weblate (Albanian) Currently translated at 64.8% (827 of 1276 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/sq/ --- src/i18n/strings/sq.json | 144 ++++++++++++++++++++++++++++++++++----- 1 file changed, 128 insertions(+), 16 deletions(-) diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index 12a30ef657..389238a5be 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -1,7 +1,7 @@ { "This email address is already in use": "Kjo adresë email është tashmë në përdorim", "This phone number is already in use": "Ky numër telefoni është tashmë në përdorim", - "Failed to verify email address: make sure you clicked the link in the email": "Vërtetimi i adresës e-mail i pasukseshëm: Sigurohu që ke klikuar lidhjen në e-mail", + "Failed to verify email address: make sure you clicked the link in the email": "S’u arrit të verifikohej adresë email: sigurohuni se keni klikuar lidhjen te email-i", "The platform you're on": "Platforma ku gjendeni", "The version of Riot.im": "Versioni i Riot.im-it", "Whether or not you're logged in (we don't record your user name)": "A je i lajmëruar apo jo (ne nuk do të inçizojmë emrin përdorues tëndë)", @@ -15,14 +15,14 @@ "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Kur kjo faqe pëmban informacione që mund të të identifikojnë, sikur një dhomë, përdorues apo identifikatues grupi, këto të dhëna do të mënjanohen para se t‘i dërgohën një server-it.", "Call Failed": "Thirrja Dështoi", "There are unknown devices in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.": "Pajisje të panjohura ndodhen në këtë dhomë: nësë vazhdon pa i vërtetuar, është e mundshme që dikush të jua përgjon thirrjen.", - "Review Devices": "Rishiko pajisjet", + "Review Devices": "Shqyrtoni Pajisje", "Call Anyway": "Thirre Sido Qoftë", "Answer Anyway": "Përgjigju Sido Qoftë", "Call": "Thirrje", "Answer": "Përgjigje", "Call Timeout": "Skadim kohe thirrjeje", "The remote side failed to pick up": "Ana e largët dështoi të përgjigjet", - "Unable to capture screen": "Ekrani nuk mundi të inçizohej", + "Unable to capture screen": "S’arrihet të fotografohet ekrani", "Existing Call": "Thirrje aktuale", "You are already in a call.": "Jeni tashmë në një thirrje.", "VoIP is unsupported": "VoIP nuk mbulohet", @@ -37,7 +37,7 @@ "The file '%(fileName)s' failed to upload": "Dështoi ngarkimi i kartelës '%(fileName)s'", "The file '%(fileName)s' exceeds this home server's size limit for uploads": "Fajli '%(fileName)s' tejkalon kufirin madhësie për mbartje e këtij server-i shtëpiak", "Upload Failed": "Ngarkimi Dështoi", - "Failure to create room": "Dhoma nuk mundi të krijohet", + "Failure to create room": "S’u arrit të krijohej dhomë", "Server may be unavailable, overloaded, or you hit a bug.": "Server-i është i padisponueshëm, i ngarkuar tej mase, apo ka një gabim.", "Send anyway": "Dërgoje sido qoftë", "Send": "Dërgoje", @@ -55,7 +55,7 @@ "Warning: any person you add to a community will be publicly visible to anyone who knows the community ID": "Paralajmërim: se cili që e shton në një komunitet do t‘i doket se cilit që e di identifikatuesin e komunitetit", "Invite new community members": "Ftoni anëtarë të rinj bashkësie", "Name or matrix ID": "Emër ose ID matrix-i", - "Invite to Community": "Fto në komunitet", + "Invite to Community": "Ftoni në Bashkësi", "Which rooms would you like to add to this community?": "Cilët dhoma kishe dashur t‘i shtosh në këtë komunitet?", "Show these rooms to non-members on the community page and room list?": "A t‘i duken dhomat joanëtarëvë ne faqën komuniteti si dhe listën dhome?", "Add rooms to the community": "Shtoni dhoma te bashkësia", @@ -74,13 +74,13 @@ "Dec": "Dhj", "%(weekDayName)s %(time)s": "%(weekDayName)s %(time)s", "Failed to invite the following users to %(groupId)s:": "Ky përdorues vijues nuk mundi të ftohet në %(groupId)s:", - "Failed to invite users to community": "Përdoruesit nuk mundën të ftohën", + "Failed to invite users to community": "S’u arrit të ftoheshin përdorues te bashkësia", "Failed to invite users to %(groupId)s": "Nuk mundën të ftohën përdoruesit në %(groupId)s", "Failed to add the following rooms to %(groupId)s:": "Nuk mundën të shtohen dhomat vijuese në %(groupId)s:", "Unnamed Room": "Dhomë e Paemërtuar", "Riot does not have permission to send you notifications - please check your browser settings": "Riot nuk ka lejim të të dergojë lajmërime - të lutem kontrollo rregullimet e kërkuesit ueb tëndë", "Riot was not given permission to send notifications - please try again": "Riot-it nuk i është dhënë leje të dërgojë lajmërime - të lutëm përpjeku serish", - "Unable to enable Notifications": "Lajmërimet nuk mundën të lëshohen", + "Unable to enable Notifications": "S’arrihet të aktivizohen njoftimet", "This email address was not found": "Kjo adresë email s’u gjet", "Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "Adresa juaj email s’duket të jetë e përshoqëruar me një ID Matrix në këtë shërbyes Home.", "Default": "Parazgjedhje", @@ -94,16 +94,16 @@ "Invite new room members": "Ftoni anëtarë të rinj dhome", "Who would you like to add to this room?": "Kë do të donit të shtonit te kjo dhomë?", "Send Invites": "Dërgoni Ftesa", - "Failed to invite user": "Përdoruesi nuk mundi të ftohej", + "Failed to invite user": "S’u arrit të ftohej përdorues", "Operation failed": "Veprimi dështoi", - "Failed to invite": "Nuk mundi të ftohet", + "Failed to invite": "S’u arrit të ftohej", "Failed to invite the following users to the %(roomName)s room:": "Përdoruesit vijuesë nuk mundën të ftohen në dhomën %(roomName)s:", "You need to be logged in.": "Lypset të jeni i futur në llogarinë tuaj.", "You need to be able to invite users to do that.": "Që ta bëni këtë, lypset të jeni në gjendje të ftoni përdorues.", - "Unable to create widget.": "Widget-i nuk mundi të krijohet.", - "Failed to send request.": "Lutja nuk mundi të dërgohej.", + "Unable to create widget.": "S’arrihet të krijohet widget-i", + "Failed to send request.": "S’u arrit të dërgohej kërkesë.", "This room is not recognised.": "Kjo dhomë s’është e pranuar.", - "Power level must be positive integer.": "Niveli fuqie duhet të jetë numër i plotë pozitiv.", + "Power level must be positive integer.": "Shkalla e pushtetit duhet të jetë një numër i plotë pozitiv.", "You are not in this room.": "S’gjendeni në këtë dhomë.", "You do not have permission to do that in this room.": "S’keni leje për ta bërë këtë në këtë dhomë.", "Room %(roomId)s not visible": "Dhoma %(roomId)s s’është e dukshme", @@ -113,7 +113,7 @@ "Unrecognised room alias:": "Alias dhome jo i pranuar:", "Ignored user": "Përdorues i shpërfillur", "You are now ignoring %(userId)s": "Tani po e shpërfillni %(userId)s", - "Unignored user": "Përdorues jo më i shpërfillur", + "Unignored user": "U hoq shpërfillja për përdoruesin", "Fetching third party location failed": "Dështoi prurja e vendndodhjes së palës së tretë", "A new version of Riot is available.": "Ka gati një version të ri Riot-it.", "Couldn't load home page": "S’u ngarkua dot faqja hyrëse", @@ -138,7 +138,7 @@ "Changelog": "Regjistër ndryshimesh", "Reject": "Hidheni tej", "Waiting for response from server": "Po pritet për përgjigje shërbyesi", - "Failed to change password. Is your password correct?": "S’u arrit të ndryshohet fjalëkalimi. A është i saktë fjalëkalimi juaj?", + "Failed to change password. Is your password correct?": "S’u arrit të ndryshohej fjalëkalimi. A është i saktë fjalëkalimi juaj?", "Uploaded on %(date)s by %(user)s": "Ngarkuar më %(date)s nga %(user)s", "OK": "OK", "Send Custom Event": "Dërgoni Akt Vetjak", @@ -220,7 +220,7 @@ "more": "më tepër", "GitHub issue link:": "Lidhje çështjeje GitHub:", "Failed to get public room list": "S’u të merrej listë dhomash publike", - "Search": "Kërkim", + "Search": "Kërkoni", "Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Regjistrat e diagnostikimeve përmbajnë të dhëna përdorimi të aplikacioneve, përfshi emrin tuaj të përdoruesit, ID ose aliase të dhomave apo grupeve që keni vizituar dhe emrat e përdoruesve të përdoruesve të tjerë. Nuk përmbajnë mesazhe.", "(HTTP status %(httpStatus)s)": "(Gjendje HTTP %(httpStatus)s)", "Failed to forget room %(errCode)s": "S’u arrit të harrohej dhoma %(errCode)s", @@ -719,5 +719,117 @@ "Export": "Eksporto", "Import room keys": "Importo kyçe dhome", "This process allows you to import encryption keys that you had previously exported from another Matrix client. You will then be able to decrypt any messages that the other client could decrypt.": "Ky proces ju lejon të importoni kyçe fshehtëzimi që keni eksportuar më parë nga një tjetër klient Matrix. Mandej do të jeni në gjendje të shfshehtëzoni çfarëdo mesazhesh që mund të shfshehtëzojë ai klient tjetër.", - "The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.": "Kartela e eksportit është e mbrojtur me një frazëkalim. Që të shfshehtëzoni kartelën, duhet ta jepni frazëkalimin këtu." + "The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.": "Kartela e eksportit është e mbrojtur me një frazëkalim. Që të shfshehtëzoni kartelën, duhet ta jepni frazëkalimin këtu.", + "Missing room_id in request": "Mungon room_id te kërkesa", + "Missing user_id in request": "Mungon user_id te kërkesa", + "%(names)s and %(count)s others are typing|other": "%(names)s dhe %(count)s të tjerë po shtypin", + "%(names)s and %(lastPerson)s are typing": "%(names)s dhe %(lastPerson)s të tjerë po shtypin", + "Failed to join room": "S’u arrit të hyhej në dhomë", + "Hide removed messages": "Fshih mesazhe të hequr", + "Hide avatar changes": "Fshih ndryshime avatarësh", + "Hide display name changes": "Fshih ndryshime emrash ekrani", + "Hide read receipts": "Fshih dëftesa leximi", + "Mirror local video feed": "Pasqyro prurje vendore videoje", + "Never send encrypted messages to unverified devices from this device": "Mos dërgo kurrë mesazhe të fshehtëzuar, nga kjo pajisje te pajisje të paverifikuara", + "Never send encrypted messages to unverified devices in this room from this device": "Mos dërgo kurrë mesazhe të fshehtëzuar, nga kjo pajisje te pajisje të paverifikuara në këtë dhomë", + "Incoming voice call from %(name)s": "Thirrje audio ardhëse nga %(name)s", + "Incoming video call from %(name)s": "Thirrje video ardhëse nga %(name)s", + "Incoming call from %(name)s": "Thirrje ardhëse nga %(name)s", + "Failed to upload profile picture!": "S’u arrit të ngarkohej foto profili!", + "Unable to load device list": "S’arrihet të ngarkohet listë pajisjesh", + "New address (e.g. #foo:%(localDomain)s)": "Adresë e re (p.sh. #foo:%(localDomain)s)", + "New community ID (e.g. +foo:%(localDomain)s)": "ID bashkësie të re (p.sh. +foo:%(localDomain)s)", + "Ongoing conference call%(supportedText)s.": "Thirrje konference që po zhvillohet%(supportedText)s.", + "Failed to kick": "S’u arrit të përzihej", + "Unban this user?": "Të hiqet dëbimi për këtë përdorues?", + "Failed to ban user": "S’u arrit të dëbohej përdoruesi", + "Failed to mute user": "S’u arrit t’i hiqej zëri përdoruesit", + "Failed to change power level": "S’u arrit të ndryshohej shkalla e pushtetit", + "Unmute": "Ktheji zërin", + "Invited": "I ftuar", + "Hangup": "Mbylle Thirrjen", + "Turn Markdown on": "Aktivizo sintaksën Markdown", + "Turn Markdown off": "Çaktivizo sintaksën Markdown", + "Hide Text Formatting Toolbar": "Fshih Panel Formatimi Tekstesh", + "No pinned messages.": "S’ka mesazhe të fiksuar", + "Replying": "Po përgjigjet", + "Failed to set avatar.": "S’u arrit të caktohej avatar.", + "To change the room's avatar, you must be a": "Që të ndryshoni avatarin e dhomës, duhet të jeni një", + "To change the room's name, you must be a": "Që të ndryshoni emrin e dhomës, duhet të jeni një", + "To change the room's main address, you must be a": "Që të ndryshoni adresën kryesore të dhomës, duhet të jeni një", + "To change the permissions in the room, you must be a": "Që të ndryshoni lejet në këtë dhomë, duhet të jeni një", + "To change the topic, you must be a": "Që të ndryshoni temën e dhomës, duhet të jeni një", + "To modify widgets in the room, you must be a": "Që të modifikoni widget-e te dhoma, duhet të jeni një", + "Failed to unban": "S’u arrit t’i hiqej dëbimi", + "Once encryption is enabled for a room it cannot be turned off again (for now)": "Pasi fshehtëzimi të jetë aktivizuar për një dhomë, s’mund të çaktivizohet më (hëpërhë).", + "To send messages, you must be a": "Që të dërgoni mesazhe, duhet të jeni një", + "To invite users into the room, you must be a": "Që të ftoni përdorues te dhoma, duhet të jeni një", + "To configure the room, you must be a": "Që të formësoni dhomën, duhet të jeni një", + "To kick users, you must be a": "Që të përzini përdorues, duhet të jeni një", + "To ban users, you must be a": "Që të dëboni përdorues, duhet të jeni një", + "To link to a room it must have an address.": "Që të lidhni një dhomë, ajo duhet të ketë një adresë.", + "Members only (since the point in time of selecting this option)": "Vetëm anëtarët (që nga çasti i përzgjedhjes së kësaj mundësie)", + "Members only (since they were invited)": "Vetëm anëtarë (që kur qenë ftuar)", + "Members only (since they joined)": "Vetëm anëtarë (që kur janë bërë pjesë)", + "Scroll to unread messages": "Rrëshqit për te mesazhe të palexuar", + "Jump to first unread message.": "Hidhu te mesazhi i parë i palexuar.", + "Failed to copy": "S’u arrit të kopjohej", + "Message removed by %(userId)s": "Mesazhi u hoq nga %(userId)s", + "Message removed": "Mesazhi u hoq", + "To continue, please enter your password.": "Që të vazhdohet, ju lutemi, jepni fjalëkalimin tuaj.", + "Token incorrect": "Token i pasaktë", + "Remove from community": "Hiqe prej bashkësie", + "Remove this user from community?": "Të hiqet ky përdoruesin prej bashkësisë?", + "Failed to withdraw invitation": "S’u arrit të tërhiqej mbrapsht ftesa", + "Failed to remove user from community": "S’u arrit të hiqej përdoruesi nga bashkësia", + "Failed to remove room from community": "S’u arrit të hiqej dhoma nga bashkësia", + "Minimize apps": "Minimizoji aplikacionet", + "%(oneUser)schanged their avatar %(count)s times|one": "%(oneUser)sndryshoi avatarin e vet", + "Start chatting": "Filloni të bisedoni", + "Start Chatting": "Filloni të Bisedoni", + "This setting cannot be changed later!": "Ky rregullim s’mund të ndryshohet më vonë!", + "To verify that this device can be trusted, please contact its owner using some other means (e.g. in person or a phone call) and ask them whether the key they see in their User Settings for this device matches the key below:": "Që të verifikoni se kësaj pajisje mund t’i zihet besë, ju lutemi, lidhuni me të zotët e saj përmes ndonjë rruge tjetër (p.sh., personalisht, ose përmes një thirrjeje telefonike) dhe kërkojuni nëse kyçi që shohin te Rregullime të tyret të Përdoruesit për këtë pajisje përputhet me kyçin më poshtë:", + "If it matches, press the verify button below. If it doesn't, then someone else is intercepting this device and you probably want to press the blacklist button instead.": "Nëse përputhet, shtypni butonin e verifikimit më poshtë. Nëse jo, atëherë dikush tjetër po e përgjon këtë pajisje dhe duhet ta kaloni në listë të zezë.", + "In future this verification process will be more sophisticated.": "Në të ardhmen, ky proces verifikimi do të jetë më i sofistikuar.", + "I verify that the keys match": "Verifikoj se kyçet përputhen", + "Unable to restore session": "S’arrihet të rikthehet sesioni", + "Please check your email and click on the link it contains. Once this is done, click continue.": "Ju lutemi, kontrolloni email-in tuaj dhe klikoni mbi lidhjen që përmban. Pasi të jetë bërë kjo, klikoni që të vazhdohet.", + "Unable to add email address": "S’arrihet të shtohet adresë email", + "Unable to verify email address.": "S’arrihet të verifikohet adresë email.", + "To get started, please pick a username!": "Që t’ia filloni, ju lutemi, zgjidhni një emër përdoruesi!", + "There are no visible files in this room": "S’ka kartela të dukshme në këtë dhomë", + "Failed to upload image": "S’u arrit të ngarkohej figurë", + "Failed to update community": "S’u arrit të përditësohej bashkësia", + "Unable to accept invite": "S’arrihet të pranohet ftesë", + "Unable to reject invite": "S’arrihet të hidhet tej ftesa", + "Featured Rooms:": "Dhoma të Zgjedhura:", + "Featured Users:": "Përdorues të Zgjedhur:", + "This Home server does not support communities": "Ky shërbyes Home s’mbulon bashkësi", + "Failed to load %(groupId)s": "S’u arrit të ngarkohej %(groupId)s", + "Failed to reject invitation": "S’u arrit të hidhej poshtë ftesa", + "Failed to leave room": "S’u arrit të braktisej", + "Scroll to bottom of page": "Rrëshqit te fundi i faqes", + "Message not sent due to unknown devices being present": "Mesazhi s’u dërgua, për shkak të pranisë së pajisjeve të panjohura", + "Failed to upload file": "S’u arrit të ngarkohej kartelë", + "Unknown room %(roomId)s": "Dhomë e panjohur %(roomId)s", + "Failed to save settings": "S’u arrit të ruheshin rregullimet", + "Fill screen": "Mbushe ekranin", + "Tried to load a specific point in this room's timeline, but was unable to find it.": "U provua të ngarkohej një pikë të dhënë prej rrjedhës kohore në këtë dhomë, por s’u arrit të gjendej.", + "Failed to load timeline position": "S’u arrit të ngarkohej pozicion rrjedhe kohore", + "Remove Contact Information?": "Të hiqen Të dhëna Kontakti?", + "Unable to remove contact information": "S’arrihet të hiqen të dhëna kontakti", + "Import E2E room keys": "Importo kyçe E2E dhome", + "To return to your account in future you need to set a password": "Që të riktheheni te llogaria juaj në të ardhmen, lypset të caktoni një fjalëkalim", + "Homeserver is": "Shërbyesi Home është", + "matrix-react-sdk version:": "Version matrix-react-sdk:", + "Failed to send email": "S’u arrit të dërgohej email", + "I have verified my email address": "E kam verifikuar adresën time email", + "To reset your password, enter the email address linked to your account": "Që të ricaktoni fjalëkalimin tuaj, jepni adresën email të lidhur me llogarinë tuaj", + "Failed to fetch avatar URL": "S’u arrit të sillej URL avatari", + "Invites user with given id to current room": "Fton te dhoma e tanishme përdoruesin me ID-në e dhënë", + "Joins room with given alias": "Hyn në dhomë me aliasin e dhënë", + "Searches DuckDuckGo for results": "Kërkon te DuckDuckGo për përfundime", + "Ignores a user, hiding their messages from you": "Shpërfill një përdorues, duke ju fshehur krejt mesazhet prej tij", + "File to import": "Kartelë për importim", + "Import": "Importo" } From 3c3c503105240466e4f551f820f10b0b7d7d799f Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Fri, 16 Nov 2018 07:51:00 +0000 Subject: [PATCH 013/237] Translated using Weblate (Albanian) Currently translated at 70.6% (901 of 1276 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/sq/ --- src/i18n/strings/sq.json | 80 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 77 insertions(+), 3 deletions(-) diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index 389238a5be..a3083beba0 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -20,7 +20,7 @@ "Answer Anyway": "Përgjigju Sido Qoftë", "Call": "Thirrje", "Answer": "Përgjigje", - "Call Timeout": "Skadim kohe thirrjeje", + "Call Timeout": "Mbarim kohe Thirrjeje", "The remote side failed to pick up": "Ana e largët dështoi të përgjigjet", "Unable to capture screen": "S’arrihet të fotografohet ekrani", "Existing Call": "Thirrje aktuale", @@ -108,7 +108,7 @@ "You do not have permission to do that in this room.": "S’keni leje për ta bërë këtë në këtë dhomë.", "Room %(roomId)s not visible": "Dhoma %(roomId)s s’është e dukshme", "Usage": "Përdorim", - "/ddg is not a command": "/ddg s'është komandë", + "/ddg is not a command": "/ddg s’është urdhër", "To use it, just wait for autocomplete results to load and tab through them.": "Për të përdorur, thjesht prit derisa të mbushën rezultatat vetëplotësuese dhe pastaj shfletoji.", "Unrecognised room alias:": "Alias dhome jo i pranuar:", "Ignored user": "Përdorues i shpërfillur", @@ -831,5 +831,79 @@ "Searches DuckDuckGo for results": "Kërkon te DuckDuckGo për përfundime", "Ignores a user, hiding their messages from you": "Shpërfill një përdorues, duke ju fshehur krejt mesazhet prej tij", "File to import": "Kartelë për importim", - "Import": "Importo" + "Import": "Importo", + "%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s pranoi ftesën për %(displayName)s.", + "%(targetName)s accepted an invitation.": "%(targetName)s pranoi një ftesë.", + "%(senderName)s invited %(targetName)s.": "%(senderName)s ftoi %(targetName)s.", + "%(senderName)s banned %(targetName)s.": "%(senderName)s dëboi %(targetName)s.", + "%(senderName)s removed their profile picture.": "%(senderName)s hoqi foton e vet të profilit.", + "%(senderName)s set a profile picture.": "%(senderName)s caktoi një foto profili.", + "%(targetName)s joined the room.": "%(targetName)s hyri në dhomë.", + "%(targetName)s rejected the invitation.": "%(targetName)s hodhi tej ftesën.", + "%(targetName)s left the room.": "%(targetName)s doli nga dhoma.", + "%(senderName)s kicked %(targetName)s.": "%(senderName)s përzuri %(targetName)s.", + "%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s ndryshoi temën në \"%(topic)s\".", + "%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s hoqi emrin e dhomës.", + "%(senderDisplayName)s sent an image.": "%(senderDisplayName)s dërgoi një figurë.", + "%(senderName)s answered the call.": "%(senderName)s iu përgjigj thirrjes.", + "(could not connect media)": "(s’lidhi dot median)", + "(unknown failure: %(reason)s)": "(dështim i panjohur: %(reason)s)", + "%(senderName)s ended the call.": "%(senderName)s e përfundoi thirrjen.", + "Don't send typing notifications": "Mos dërgo njoftime shtypjesh", + "Disable Community Filter Panel": "Çaktivizo Panel Filtrash Bashkësie", + "Delete %(count)s devices|other": "Fshi %(count)s pajisje", + "Failed to set display name": "S’u arrit të caktohej emër ekrani", + "'%(alias)s' is not a valid format for an alias": "'%(alias)s' s’është format i vlefshëm aliasesh", + "'%(alias)s' is not a valid format for an address": "'%(alias)s' s’është format i vlefshëm adresash", + "'%(groupId)s' is not a valid community ID": "'%(groupId)s' s’është ID i vlefshëm bashkësish", + "Cannot add any more widgets": "S’mund të shtohen më tepër widget-e", + "Re-request encryption keys from your other devices.": "Rikërkoni kyçe fshehtëzimi prej pajisjesh tuaja të tjera.", + "%(userName)s (power %(powerLevelNumber)s)": "%(userName)s (pushtet %(powerLevelNumber)si)", + "(~%(count)s results)|other": "(~%(count)s përfundime)", + "(~%(count)s results)|one": "(~%(count)s përfundim)", + "Drop here to favourite": "Hidheni këtu që të bëhet e parapëlqyer", + "Drop here to restore": "Hidheni këtu që të bëhet rikthim", + "Click here to join the discussion!": "Klikoni këtu që të merrni pjesë të diskutimi!", + "Changes to who can read history will only apply to future messages in this room": "Ndryshime se cilët mund të lexojnë historikun do të vlejnë vetëm për mesazhe të ardhshëm në këtë dhomë", + "End-to-end encryption is in beta and may not be reliable": "Fshehtëzimi skaj-më-skaj është në fazën beta dhe mund të mos jetë i qëndrueshëm", + "Devices will not yet be able to decrypt history from before they joined the room": "Pajisjet s’do të jenë ende në gjendje të shfshehtëzojnë historik nga periudha përpara se të merrnin pjesë te dhomë", + "Encrypted messages will not be visible on clients that do not yet implement encryption": "Mesazhet e fshehtëzuar s’do të jenë të dukshëm në klientë që nuk e sendërtojnë ende fshehtëzimin", + "(warning: cannot be disabled again!)": "(kujdes: s’mund të çaktivizohet më!)", + "%(user)s is a %(userRole)s": "%(user)s është një %(userRole)s", + "Error decrypting audio": "Gabim në shfshehtëzim audioje", + "Download %(text)s": "Shkarko %(text)s", + "Error decrypting image": "Gabim në shfshehtëzim figure", + "Error decrypting video": "Gabim në shfshehtëzim videoje", + "Removed or unknown message type": "Lloj mesazhi i hequr ose i panjohur", + "An email has been sent to %(emailAddress)s": "U dërgua një email te %(emailAddress)s", + "%(serverName)s Matrix ID": "ID matrix-i në %(serverName)s", + "NOTE: Apps are not end-to-end encrypted": "SHËNIM: Aplikacionet s’janë të fshehtëzuara skaj-më-skaj", + "Delete Widget": "Fshije Widget-in", + "Delete widget": "Fshije widget-in", + "%(severalUsers)sjoined and left %(count)s times|one": "%(severalUsers)shynë dhe dolën", + "%(oneUser)sjoined and left %(count)s times|one": "%(oneUser)shyri dhe doli", + "%(severalUsers)srejected their invitations %(count)s times|one": "%(severalUsers)shodhën poshtë ftesat e tyre", + "%(oneUser)srejected their invitation %(count)s times|one": "%(oneUser)shodhi poshtë ftesën e tyre", + "%(severalUsers)schanged their name %(count)s times|one": "%(severalUsers)sndryshuan emrat e tyre", + "%(oneUser)schanged their name %(count)s times|one": "%(oneUser)sndryshoi emrin e vet", + "%(severalUsers)schanged their avatar %(count)s times|one": "%(severalUsers)sndryshuan avatarët e tyre", + "And %(count)s more...|other": "Dhe %(count)s të tjerë…", + "ex. @bob:example.com": "p.sh., @bob:example.com", + "Click on the button below to start chatting!": "Klikoni mbi butonin më poshtë që të filloni të bisedoni!", + "An error occurred: %(error_string)s": "Ndodhi një gabim: %(error_string)s", + "Connectivity to the server has been lost.": "Humbi lidhja me shërbyesin.", + "Click to mute video": "Klikoni që të heshtet videoja", + "Click to mute audio": "Klikoni që të heshtet audioja", + "": "", + "Clear Cache and Reload": "Pastro Fshehtinën dhe Ringarkoje", + "A new password must be entered.": "Duhet dhënë një fjalëkalim i ri.", + "An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.": "Te %(emailAddress)s u dërgua një email. Pasi të ndiqni lidhjen që përmban, klikoni më poshtë.", + "An unknown error occurred.": "Ndodhi një gabim i panjohur.", + "Displays action": "Shfaq veprimin", + "Define the power level of a user": "Përcaktoni shkallë pushteti të një përdoruesi", + "Deops user with given id": "I heq cilësinë e operatorit përdoruesit me ID-në e dhënë", + "Changes your display nickname": "Ndryshon nofkën tuaj në ekran", + "Emoji": "Emoji", + "Ed25519 fingerprint": "Shenja gishtash Ed25519", + "Failed to set direct chat tag": "S’u arrit të caktohej etiketa e fjalosjes së drejtpërdrejtë" } From 2e86bced226f630d76f71974facf1f69176b929e Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Fri, 16 Nov 2018 08:19:38 +0000 Subject: [PATCH 014/237] Translated using Weblate (Albanian) Currently translated at 74.7% (954 of 1276 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/sq/ --- src/i18n/strings/sq.json | 61 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 57 insertions(+), 4 deletions(-) diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index a3083beba0..29359383ae 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -23,7 +23,7 @@ "Call Timeout": "Mbarim kohe Thirrjeje", "The remote side failed to pick up": "Ana e largët dështoi të përgjigjet", "Unable to capture screen": "S’arrihet të fotografohet ekrani", - "Existing Call": "Thirrje aktuale", + "Existing Call": "Thirrje Ekzistuese", "You are already in a call.": "Jeni tashmë në një thirrje.", "VoIP is unsupported": "VoIP nuk mbulohet", "You cannot place VoIP calls in this browser.": "Thirrjet me VoIP nuk mbulohen nga ky kërkues uebi.", @@ -56,7 +56,7 @@ "Invite new community members": "Ftoni anëtarë të rinj bashkësie", "Name or matrix ID": "Emër ose ID matrix-i", "Invite to Community": "Ftoni në Bashkësi", - "Which rooms would you like to add to this community?": "Cilët dhoma kishe dashur t‘i shtosh në këtë komunitet?", + "Which rooms would you like to add to this community?": "Cilat dhoma do të donit të shtonit te kjo bashkësi?", "Show these rooms to non-members on the community page and room list?": "A t‘i duken dhomat joanëtarëvë ne faqën komuniteti si dhe listën dhome?", "Add rooms to the community": "Shtoni dhoma te bashkësia", "Add to community": "Shtoje te kjo bashkësi", @@ -75,7 +75,7 @@ "%(weekDayName)s %(time)s": "%(weekDayName)s %(time)s", "Failed to invite the following users to %(groupId)s:": "Ky përdorues vijues nuk mundi të ftohet në %(groupId)s:", "Failed to invite users to community": "S’u arrit të ftoheshin përdorues te bashkësia", - "Failed to invite users to %(groupId)s": "Nuk mundën të ftohën përdoruesit në %(groupId)s", + "Failed to invite users to %(groupId)s": "S’u arrit të ftoheshin përdorues te %(groupId)s", "Failed to add the following rooms to %(groupId)s:": "Nuk mundën të shtohen dhomat vijuese në %(groupId)s:", "Unnamed Room": "Dhomë e Paemërtuar", "Riot does not have permission to send you notifications - please check your browser settings": "Riot nuk ka lejim të të dergojë lajmërime - të lutem kontrollo rregullimet e kërkuesit ueb tëndë", @@ -905,5 +905,58 @@ "Changes your display nickname": "Ndryshon nofkën tuaj në ekran", "Emoji": "Emoji", "Ed25519 fingerprint": "Shenja gishtash Ed25519", - "Failed to set direct chat tag": "S’u arrit të caktohej etiketa e fjalosjes së drejtpërdrejtë" + "Failed to set direct chat tag": "S’u arrit të caktohej etiketa e fjalosjes së drejtpërdrejtë", + "You are no longer ignoring %(userId)s": "Nuk e shpërfillni më %(userId)s", + "%(senderName)s set their display name to %(displayName)s.": "%(senderName)s caktoi për veten emër ekrani %(displayName)s.", + "%(senderName)s removed their display name (%(oldDisplayName)s).": "%(senderName)s hoqi emrin e tij në ekran (%(oldDisplayName)s).", + "%(senderName)s changed their profile picture.": "%(senderName)s ndryshoi foton e vet të profilit.", + "%(senderName)s unbanned %(targetName)s.": "%(senderName)s hoqi dëbimin për %(targetName)s.", + "%(senderName)s withdrew %(targetName)s's invitation.": "%(senderName)s tërhoqi mbrapsht ftesën për %(targetName)s.", + "%(senderDisplayName)s changed the room name to %(roomName)s.": "%(senderDisplayName)s ndryshoi emrin e dhomës në %(roomName)s.", + "(not supported by this browser)": "(s’mbulohet nga ky shfletues)", + "%(senderName)s placed a %(callType)s call.": "%(senderName)s bëri një thirrje %(callType)s.", + "%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.": "%(senderName)s dërgoi një ftesë për %(targetDisplayName)s që të marrë pjesë në dhomë.", + "%(senderName)s made future room history visible to unknown (%(visibility)s).": "%(senderName)s e kaloi historikun e ardhshëm të dhomës të dukshëm për të panjohurit (%(visibility)s).", + "%(widgetName)s widget removed by %(senderName)s": "Widget-i %(widgetName)s u hoq nga %(senderName)s", + "%(names)s and %(count)s others are typing|one": "%(names)s dhe një tjetër po shtypin", + "Authentication check failed: incorrect password?": "Dështoi kontrolli i mirëfilltësimit: fjalëkalim i pasaktë?", + "Message Pinning": "Fiksim Mesazhi", + "Disable Emoji suggestions while typing": "Çaktivizoje sugjerime emoji-sh teksa shtypet", + "Autoplay GIFs and videos": "Vetëluaj GIF-e dhe video", + "Disable big emoji in chat": "Çaktivizo emoji-t e mëdhenj në fjalosje", + "Active call (%(roomName)s)": "Thirrje aktive (%(roomName)s)", + "%(senderName)s uploaded a file": "%(senderName)s ngarkoi një kartelë", + "Seen by %(displayName)s (%(userName)s) at %(dateTime)s": "Parë nga %(displayName)s (%(userName)s) më %(dateTime)s", + "Drop here to tag direct chat": "Hidheni këtu që të caktohet etiketa e fjalosjes së drejtpërdrejtë", + "%(roomName)s is not accessible at this time.": "Te %(roomName)s s’hyhet dot tani.", + "You are trying to access %(roomName)s.": "Po provoni të hyni te %(roomName)s.", + "To change the room's history visibility, you must be a": "Që të ndryshoni dukshmërinë e historikut të dhomës, duhet të jeni një", + "Error decrypting attachment": "Gabim në shfshehtëzim bashkëngjitjeje", + "Invalid file%(extra)s": "Kartelë e pavlefshme%(extra)s", + "%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s ndryshoi avatarin në %(roomName)s", + "%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s hoqi avatarin e dhomës.", + "Are you sure you want to remove '%(roomName)s' from %(groupId)s?": "Jeni i sigurt se doni të hiqet '%(roomName)s' nga %(groupId)s?", + "%(severalUsers)sjoined %(count)s times|other": "%(severalUsers)shynë %(count)s herë", + "%(severalUsers)sjoined %(count)s times|one": "Hynë %(severalUsers)s", + "%(oneUser)sjoined %(count)s times|other": "%(oneUser)shyri %(count)s herë", + "%(oneUser)sjoined %(count)s times|one": "%(oneUser)shyri", + "%(severalUsers)sleft %(count)s times|other": "%(severalUsers)sdolën %(count)s herë", + "%(severalUsers)sleft %(count)s times|one": "Doli %(severalUsers)s", + "%(oneUser)sleft %(count)s times|other": "%(oneUser)sdoli %(count)s herë", + "%(oneUser)sleft %(count)s times|one": "%(oneUser)sdoli", + "%(severalUsers)sleft and rejoined %(count)s times|one": "%(severalUsers)sdolën dhe rihynë", + "%(oneUser)sleft and rejoined %(count)s times|one": "%(oneUser)sdoli dhe rihyri", + "%(severalUsers)shad their invitations withdrawn %(count)s times|one": "U tërhoqën mbrapsht ftesat për %(severalUsers)s", + "were invited %(count)s times|other": "janë ftuar %(count)s herë", + "were banned %(count)s times|other": "janë dëbuar %(count)s herë", + "were unbanned %(count)s times|other": "janë dëbuar %(count)s herë", + "were kicked %(count)s times|other": "janë përzënë %(count)s herë", + "Which rooms would you like to add to this summary?": "Cilat dhoma do të donit të shtonit te kjo përmbledhje?", + "Community %(groupId)s not found": "S’u gjet bashkësia %(groupId)s", + "You seem to be uploading files, are you sure you want to quit?": "Duket se jeni duke ngarkuar kartela, jeni i sigurt se doni të dilet?", + "Click to unmute video": "Klikoni që të hiqet heshtja për videon", + "Click to unmute audio": "Klikoni që të hiqet heshtja për audion", + "Autocomplete Delay (ms):": "Vonesë Vetëplotësimi (ms):", + "Desktop specific": "Në desktop", + "click to reveal": "klikoni që të zbulohet" } From 7a01468ab23cd4b1a69927d4954e823fe0637964 Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Fri, 16 Nov 2018 08:23:04 +0000 Subject: [PATCH 015/237] Translated using Weblate (Albanian) Currently translated at 74.7% (954 of 1276 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/sq/ --- src/i18n/strings/sq.json | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index 29359383ae..bdab90b412 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -100,7 +100,7 @@ "Failed to invite the following users to the %(roomName)s room:": "Përdoruesit vijuesë nuk mundën të ftohen në dhomën %(roomName)s:", "You need to be logged in.": "Lypset të jeni i futur në llogarinë tuaj.", "You need to be able to invite users to do that.": "Që ta bëni këtë, lypset të jeni në gjendje të ftoni përdorues.", - "Unable to create widget.": "S’arrihet të krijohet widget-i", + "Unable to create widget.": "S’arrihet të krijohet widget-i.", "Failed to send request.": "S’u arrit të dërgohej kërkesë.", "This room is not recognised.": "Kjo dhomë s’është e pranuar.", "Power level must be positive integer.": "Shkalla e pushtetit duhet të jetë një numër i plotë pozitiv.", @@ -302,7 +302,7 @@ "Unknown (user, device) pair:": "Çift (përdorues, pajisje) i panjohur:", "Device already verified!": "Pajisjeje tashmë e verifikuar!", "Verified key": "Kyç i verifikuar", - "Unrecognised command:": "Urdhër jo i pranuar: ", + "Unrecognised command:": "Urdhër jo i pranuar:", "Reason": "Arsye", "%(senderName)s requested a VoIP conference.": "%(senderName)s kërkoi një konferencë VoIP.", "VoIP conference started.": "Konferenca VoIP filloi.", @@ -390,7 +390,7 @@ "Make Moderator": "Kaloje Moderator", "Admin Tools": "Mjete Përgjegjësi", "Level:": "Nivel:", - "and %(count)s others...|other": "dhe %{count} të tjerë…", + "and %(count)s others...|other": "dhe %(count)s të tjerë…", "and %(count)s others...|one": "dhe një tjetër…", "Filter room members": "Filtroni anëtarë dhome", "Attachment": "Bashkëngjitje", @@ -453,8 +453,8 @@ "This is a preview of this room. Room interactions have been disabled": "Kjo është një paraparje e kësaj dhome. Ndërveprimet në dhomë janë çaktivizuar", "Banned by %(displayName)s": "Dëbuar nga %(displayName)s", "Privacy warning": "Sinjalizim privatësie", - "The visibility of existing history will be unchanged": "Dukshmëria e historikut ekzistues nuk do të ndryshohet.", - "You should not yet trust it to secure data": "S’duhet t’i zini ende besë për sigurim të dhënash.", + "The visibility of existing history will be unchanged": "Dukshmëria e historikut ekzistues nuk do të ndryshohet", + "You should not yet trust it to secure data": "S’duhet t’i zini ende besë për sigurim të dhënash", "Enable encryption": "Aktivizoni fshehtëzim", "Encryption is enabled in this room": "Në këtë dhomë është i aktivizuar fshehtëzimi", "Encryption is not enabled in this room": "Në këtë dhomë s’është i aktivizuar fshehtëzimi", @@ -462,7 +462,7 @@ "Privileged Users": "Përdorues të Privilegjuar", "Banned users": "Përdorues të dëbuar", "Leave room": "Dilni nga dhomë", - "Tagged as: ": "Etiketuar me:", + "Tagged as: ": "Etiketuar me: ", "Click here to fix": "Klikoni këtu për ta ndrequr", "Who can access this room?": "Kush mund të hyjë në këtë dhomë?", "Only people who have been invited": "Vetëm persona që janë ftuar", @@ -576,7 +576,7 @@ "This doesn't appear to be a valid email address": "Kjo s’duket se është adresë email e vlefshme", "Verification Pending": "Verifikim Në Pritje të Miratimit", "Skip": "Anashkaloje", - "User names may only contain letters, numbers, dots, hyphens and underscores.": "Emrat e përdoruesve mund të përmbajnë vetëm shkronja, numra, pika, vija ndarëse dhe nënvija", + "User names may only contain letters, numbers, dots, hyphens and underscores.": "Emrat e përdoruesve mund të përmbajnë vetëm shkronja, numra, pika, vija ndarëse dhe nënvija.", "Username not available": "Emri i përdoruesit s’është i lirë", "Username invalid: %(errMessage)s": "Emër përdoruesi i pavlefshëm: %(errMessage)s", "Username available": "Emri i përdoruesit është i lirë", @@ -613,7 +613,7 @@ "Logout": "Dalje", "Your Communities": "Bashkësitë Tuaja", "Create a new community": "Krijoni një bashkësi të re", - "You have no visible notifications": "S’keni njoftime të dukshme.", + "You have no visible notifications": "S’keni njoftime të dukshme", "%(count)s of your messages have not been sent.|other": "Disa nga mesazhet tuaj s’janë dërguar.", "%(count)s of your messages have not been sent.|one": "Mesazhi juaj s’u dërgua.", "%(count)s new messages|other": "%(count)s mesazhe të rinj", @@ -677,7 +677,7 @@ "Incorrect username and/or password.": "Emër përdoruesi dhe/ose fjalëkalim i pasaktë.", "The phone number entered looks invalid": "Numri i telefonit që u dha duket i pavlefshëm", "Sign in to get started": "Që t’ia filloni, bëni hyrjen", - "Set a display name:": "Caktoni emër ekrani", + "Set a display name:": "Caktoni emër ekrani:", "Upload an avatar:": "Ngarkoni një avatar:", "This server does not support authentication with a phone number.": "Ky shërbyes nuk mbulon mirëfilltësim me një numër telefoni.", "Missing password.": "Mungon fjalëkalimi.", @@ -751,7 +751,7 @@ "Turn Markdown on": "Aktivizo sintaksën Markdown", "Turn Markdown off": "Çaktivizo sintaksën Markdown", "Hide Text Formatting Toolbar": "Fshih Panel Formatimi Tekstesh", - "No pinned messages.": "S’ka mesazhe të fiksuar", + "No pinned messages.": "S’ka mesazhe të fiksuar.", "Replying": "Po përgjigjet", "Failed to set avatar.": "S’u arrit të caktohej avatar.", "To change the room's avatar, you must be a": "Që të ndryshoni avatarin e dhomës, duhet të jeni një", From f8fd2a5397df669e6c0ff80a1b23bfbc98d4ab86 Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Fri, 16 Nov 2018 08:25:06 +0000 Subject: [PATCH 016/237] Translated using Weblate (Albanian) Currently translated at 74.7% (954 of 1276 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/sq/ --- src/i18n/strings/sq.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index bdab90b412..7040e87252 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -598,9 +598,9 @@ "Who would you like to add to this summary?": "Kë do të donit të shtonit te kjo përmbledhje?", "Add a User": "Shtoni një Përdorues", "Leave Community": "Braktiseni Bashkësinë", - "Leave %(groupName)s?": "Të braktiset {groupName}?", + "Leave %(groupName)s?": "Të braktiset %(groupName)s?", "Community Settings": "Rregullime Bashkësie", - "%(inviter)s has invited you to join this community": "%s ju ftoi të bëheni pjesë e kësaj bashkësie", + "%(inviter)s has invited you to join this community": "%(inviter)s ju ftoi të bëheni pjesë e kësaj bashkësie", "You are an administrator of this community": "Jeni një përgjegjës i kësaj bashkësie", "You are a member of this community": "Jeni anëtar i këtij ekipi", "Long Description (HTML)": "Përshkrim i Gjatë (HTML)", @@ -761,7 +761,7 @@ "To change the topic, you must be a": "Që të ndryshoni temën e dhomës, duhet të jeni një", "To modify widgets in the room, you must be a": "Që të modifikoni widget-e te dhoma, duhet të jeni një", "Failed to unban": "S’u arrit t’i hiqej dëbimi", - "Once encryption is enabled for a room it cannot be turned off again (for now)": "Pasi fshehtëzimi të jetë aktivizuar për një dhomë, s’mund të çaktivizohet më (hëpërhë).", + "Once encryption is enabled for a room it cannot be turned off again (for now)": "Pasi fshehtëzimi të jetë aktivizuar për një dhomë, s’mund të çaktivizohet më (hëpërhë)", "To send messages, you must be a": "Që të dërgoni mesazhe, duhet të jeni një", "To invite users into the room, you must be a": "Që të ftoni përdorues te dhoma, duhet të jeni një", "To configure the room, you must be a": "Që të formësoni dhomën, duhet të jeni një", From 0e874fe17a44d308ac6868a2e26122e291428c88 Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Fri, 16 Nov 2018 08:54:41 +0000 Subject: [PATCH 017/237] Translated using Weblate (Albanian) Currently translated at 76.4% (976 of 1276 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/sq/ --- src/i18n/strings/sq.json | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index 7040e87252..fe56108377 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -203,7 +203,7 @@ "Unnamed room": "Dhomë e paemërtuar", "Dismiss": "Mos e merr parasysh", "Explore Account Data": "Eksploroni të Dhëna Llogarie", - "All messages (noisy)": "Tërë Mesazhet (e zhurmshme)", + "All messages (noisy)": "Krejt mesazhet (e zhurmshme)", "Saturday": "E shtunë", "Remember, you can always set an email address in user settings if you change your mind.": "Mos harroni, mundeni përherë të caktoni një adresë email te rregullimet e përdoruesit, nëse ndërroni mendje.", "Direct Chat": "Fjalosje e Drejtpërdrejtë", @@ -958,5 +958,27 @@ "Click to unmute audio": "Klikoni që të hiqet heshtja për audion", "Autocomplete Delay (ms):": "Vonesë Vetëplotësimi (ms):", "Desktop specific": "Në desktop", - "click to reveal": "klikoni që të zbulohet" + "click to reveal": "klikoni që të zbulohet", + "Call in Progress": "Thirrje në Kryerje e Sipër", + "A call is already in progress!": "Ka tashmë një thirrje në kryerje e sipër!", + "%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s ndryshoi emrin e tij në ekran si %(displayName)s.", + "%(senderName)s set the main address for this room to %(address)s.": "%(senderName)s caktoi %(displayName)s si adresë kryesore për këtë dhomë.", + "%(widgetName)s widget modified by %(senderName)s": "Widget-i %(widgetName)s u modifikua nga %(senderName)s", + "%(widgetName)s widget added by %(senderName)s": "Widget-i %(widgetName)s u shtua nga %(senderName)s", + "Always show encryption icons": "Shfaq përherë ikona fshehtëzimi", + "block-quote": "bllok citimi", + "bulleted-list": "listë me toptha", + "Add some now": "Shtohen ca tani", + "Click here to see older messages.": "Klikoni këtu për të parë mesazhe më të vjetër.", + "%(severalUsers)sjoined and left %(count)s times|other": "%(severalUsers)shynë dhe dolën %(count)s herë", + "%(severalUsers)sleft and rejoined %(count)s times|other": "%(severalUsers)sdolën dhe rihynë %(count)s herë", + "%(oneUser)sleft and rejoined %(count)s times|other": "%(oneUser)sdoli dhe rihyri %(count)s herë", + "%(severalUsers)schanged their name %(count)s times|other": "%(severalUsers)sndryshuan emrat e tyre %(count)s herë", + "%(oneUser)schanged their name %(count)s times|other": "%(oneUser)sndryshoi emrin e vet %(count)s herë", + "%(severalUsers)schanged their avatar %(count)s times|other": "%(severalUsers)sndryshuan avatarët e tyre %(count)s herë", + "%(oneUser)schanged their avatar %(count)s times|other": "%(oneUser)sndryshoi avatarin e vet %(count)s herë", + "Clear cache and resync": "Pastro fshehtinën dhe rinjëkohëso", + "Clear Storage and Sign Out": "Pastro Depon dhe Dil", + "COPY": "KOPJOJE", + "A phone number is required to register on this homeserver.": "Që të regjistroheni në këtë shërbyes home, lypset numër telefoni." } From 2122bf8384b7c681913cf9d9b6314069664d6f64 Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Fri, 16 Nov 2018 09:20:39 +0000 Subject: [PATCH 018/237] Translated using Weblate (Albanian) Currently translated at 81.8% (1045 of 1276 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/sq/ --- src/i18n/strings/sq.json | 95 ++++++++++++++++++++++++++++++++++------ 1 file changed, 82 insertions(+), 13 deletions(-) diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index fe56108377..9c53a6dba3 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -73,7 +73,7 @@ "Nov": "Nën", "Dec": "Dhj", "%(weekDayName)s %(time)s": "%(weekDayName)s %(time)s", - "Failed to invite the following users to %(groupId)s:": "Ky përdorues vijues nuk mundi të ftohet në %(groupId)s:", + "Failed to invite the following users to %(groupId)s:": "S’u arrit të ftoheshin përdoruesit vijues te %(groupId)s:", "Failed to invite users to community": "S’u arrit të ftoheshin përdorues te bashkësia", "Failed to invite users to %(groupId)s": "S’u arrit të ftoheshin përdorues te %(groupId)s", "Failed to add the following rooms to %(groupId)s:": "Nuk mundën të shtohen dhomat vijuese në %(groupId)s:", @@ -137,13 +137,13 @@ "Filter room names": "Filtroni emra dhomash", "Changelog": "Regjistër ndryshimesh", "Reject": "Hidheni tej", - "Waiting for response from server": "Po pritet për përgjigje shërbyesi", + "Waiting for response from server": "Po pritet për përgjigje nga shërbyesi", "Failed to change password. Is your password correct?": "S’u arrit të ndryshohej fjalëkalimi. A është i saktë fjalëkalimi juaj?", "Uploaded on %(date)s by %(user)s": "Ngarkuar më %(date)s nga %(user)s", "OK": "OK", "Send Custom Event": "Dërgoni Akt Vetjak", "Advanced notification settings": "Rregullime të mëtejshme për njoftimet", - "Failed to send logs: ": "S’u arrit të dërgohen regjistra: ", + "Failed to send logs: ": "S’u arrit të dërgoheshin regjistra: ", "delete the alias.": "fshije aliasin.", "To return to your account in future you need to set a password": "Që të riktheheni te llogaria juaj në të ardhmen, lypset të caktoni një fjalëkalim", "Forget": "Harroje", @@ -161,7 +161,7 @@ "Messages in one-to-one chats": "Mesazhe në fjalosje tek për tek", "Unavailable": "S’kapet", "View Decrypted Source": "Shihni Burim të Shfshehtëzuar", - "Failed to update keywords": "S’u arrit të përditësohen fjalëkyçe", + "Failed to update keywords": "S’u arrit të përditësoheshin fjalëkyçe", "Notes:": "Shënime:", "Notifications on the following keywords follow rules which can’t be displayed here:": "Njoftimet e shkaktuara nga fjalëkyçet vijuese ndjekin rregulla që s’mund të shfaqen këtu:", "Safari and Opera work too.": "Safari dhe Opera bëjnë, po ashtu.", @@ -171,8 +171,8 @@ "Favourite": "E parapëlqyer", "All Rooms": "Krejt Dhomat", "Explore Room State": "Eksploroni Gjendje Dhome", - "Source URL": "URL-ja e Burimit", - "Messages sent by bot": "Mesazhe të dërguar nga bot", + "Source URL": "URL Burimi", + "Messages sent by bot": "Mesazhe të dërguar nga boti", "Cancel": "Anuloje", "Filter results": "Filtroni përfundimet", "Members": "Anëtarë", @@ -214,12 +214,12 @@ "Download this file": "Shkarkoje këtë kartelë", "Remove from Directory": "Hiqe prej Drejtorie", "Enable them now": "Aktivizoji tani", - "Messages containing my user name": "Mesazhe që përmbajnë emrin tim", + "Messages containing my user name": "Mesazhe që përmbajnë emrin tim të përdoruesit", "Toolbox": "Grup mjetesh", "Collecting logs": "Po grumbullohen regjistra", "more": "më tepër", "GitHub issue link:": "Lidhje çështjeje GitHub:", - "Failed to get public room list": "S’u të merrej listë dhomash publike", + "Failed to get public room list": "S’u arrit të merrej listë dhomash publike", "Search": "Kërkoni", "Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Regjistrat e diagnostikimeve përmbajnë të dhëna përdorimi të aplikacioneve, përfshi emrin tuaj të përdoruesit, ID ose aliase të dhomave apo grupeve që keni vizituar dhe emrat e përdoruesve të përdoruesve të tjerë. Nuk përmbajnë mesazhe.", "(HTTP status %(httpStatus)s)": "(Gjendje HTTP %(httpStatus)s)", @@ -242,16 +242,16 @@ "When I'm invited to a room": "Kur ftohem në një dhomë", "Close": "Mbylle", "Can't update user notification settings": "S’përditësohen dot rregullime njoftimi të përdoruesit", - "Notify for all other messages/rooms": "Njoftim për krejt mesazhet/dhomat e tjera", + "Notify for all other messages/rooms": "Njofto për krejt mesazhet/dhomat e tjera", "Unable to look up room ID from server": "S’arrihet të kërkohet ID dhome nga shërbyesi", "Couldn't find a matching Matrix room": "S’u gjet dot një dhomë Matrix me përputhje", - "Invite to this room": "Ftoje te kjo dhomë", + "Invite to this room": "Ftojeni te kjo dhomë", "You cannot delete this message. (%(code)s)": "S’mund ta fshini këtë mesazh. (%(code)s)", "Thursday": "E enjte", "I understand the risks and wish to continue": "I kuptoj rreziqet dhe dua të vazhdoj", "Logs sent": "Regjistrat u dërguan", "Back": "Mbrapsht", - "Reply": "Përgjigjuni", + "Reply": "Përgjigje", "Show message in desktop notification": "Shfaq mesazh në njoftim për desktop", "You must specify an event type!": "Duhet të përcaktoni një lloj akti!", "Unhide Preview": "Shfshihe Paraparjen", @@ -271,7 +271,7 @@ "Off": "Off", "Edit": "Përpuno", "Riot does not know how to join a room on this network": "Riot-i nuk di si të hyjë në një dhomë në këtë rrjet", - "Mentions only": "Vetëm @përmendje", + "Mentions only": "Vetëm përmendje", "remove %(name)s from the directory.": "hiqe %(name)s prej drejtorie.", "You can now return to your account after signing out, and sign in on other devices.": "Mund të ktheheni te llogaria juaj, pasi të keni bërë daljen, dhe të bëni hyrjen nga pajisje të tjera.", "Continue": "Vazhdo", @@ -980,5 +980,74 @@ "Clear cache and resync": "Pastro fshehtinën dhe rinjëkohëso", "Clear Storage and Sign Out": "Pastro Depon dhe Dil", "COPY": "KOPJOJE", - "A phone number is required to register on this homeserver.": "Që të regjistroheni në këtë shërbyes home, lypset numër telefoni." + "A phone number is required to register on this homeserver.": "Që të regjistroheni në këtë shërbyes home, lypset numër telefoni.", + "e.g. %(exampleValue)s": "p.sh., %(exampleValue)s", + "e.g. ": "p.sh., ", + "Permission Required": "Lypset Leje", + "Registration Required": "Lyp Regjistrim", + "This homeserver has hit its Monthly Active User limit.": "Ky shërbyes home ka tejkaluar kufirin e vet Përdorues Aktivë Mujorë.", + "This homeserver has exceeded one of its resource limits.": "Ky shërbyes home ka tejkaluar një nga kufijtë e tij mbi burimet.", + "Please contact your service administrator to continue using the service.": "Ju lutemi, që të vazhdoni të përdorni shërbimin, lidhuni me përgjegjësin e shërbimit tuaj.", + "Unable to connect to Homeserver. Retrying...": "S’u arrit të lidhej me shërbyesin Home. Po riprovohet…", + "Sorry, your homeserver is too old to participate in this room.": "Na ndjeni, shërbyesi juaj Home është shumë i vjetër për të marrë pjesë në këtë dhomë.", + "Please contact your homeserver administrator.": "Ju lutemi, lidhuni me përgjegjësin e shërbyesit tuaj Home.", + "Increase performance by only loading room members on first view": "Përmirësoni punimin duke ngarkuar anëtarë dhome vetëm kur sillen para syve.", + "Send analytics data": "Dërgo të dhëna analitike", + "This event could not be displayed": "Ky akt s’u shfaq dot", + "Encrypting": "Fshehtëzim", + "Encrypted, not sent": "I fshehtëzuar, i padërguar", + "underlined": "nënvizuar", + "inline-code": "kod brendazi", + "numbered-list": "listë e numërtuar", + "The conversation continues here.": "Biseda vazhdon këtu.", + "System Alerts": "Sinjalizime Sistemi", + "Joining room...": "Po bëhet pjesë…", + "To notify everyone in the room, you must be a": "Që të njoftoni këdo te dhoma, duhet të jeni një", + "Muted Users": "Përdorues të Heshtur", + "Upgrade room to version %(ver)s": "Përmirësoni versionin e dhomës me versionin %(ver)s", + "Internal room ID: ": "ID e brendshme dhome: ", + "Room version number: ": "Numër versioni dhome: ", + "There is a known vulnerability affecting this room.": "Ka një cenueshmëri të njohur që ndikon në këtë dhomë.", + "Only room administrators will see this warning": "Këtë sinjalizim mund ta shohin vetëm përgjegjësit e dhomës", + "Hide Stickers": "Fshihi Ngjitësat", + "Show Stickers": "Shfaq Ngjitës", + "The email field must not be blank.": "Fusha email s’duhet të jetë e zbrazët.", + "The user name field must not be blank.": "Fusha emër përdoruesi s’duhet të jetë e zbrazët.", + "The phone number field must not be blank.": "Fusha numër telefoni s’duhet të jetë e zbrazët.", + "The password field must not be blank.": "Fusha fjalëkalim s’duhet të jetë e zbrazët.", + "Yes, I want to help!": "Po, dua të ndihmoj!", + "This homeserver has hit its Monthly Active User limit so some users will not be able to log in.": "Ky shërbyes home ka tejkaluar kufirin e vet Përdorues Aktivë Mujorë, ndaj disa përdorues s’do të jenë në gjendje të bëjnë hyrjen.", + "This homeserver has exceeded one of its resource limits so some users will not be able to log in.": "Ky shërbyes home ka tejkaluar një nga kufijtë mbi burimet, ndaj disa përdorues s’do të jenë në gjendje të bëjnë hyrjen.", + "Failed to remove widget": "S’u arrit të hiqej widget-i", + "Reload widget": "Ringarkoje widget-in", + "Popout widget": "Widget flluskë", + "Picture": "Foto", + "Failed to indicate account erasure": "S’u arrit të tregohej fshirje llogarie", + "Message visibility in Matrix is similar to email. Our forgetting your messages means that messages you have sent will not be shared with any new or unregistered users, but registered users who already have access to these messages will still have access to their copy.": "Dukshmëria e mesazheve në Matrix është e ngjashme me atë në email. Harrimi i mesazheve nga ana jonë do të thotë që mesazhet që keni dërguar nuk do të ndahen me çfarëdo përdoruesi të ri apo të paregjistruar, por përdoruesit e regjistruar, që kanë tashmë hyrje në këto mesazhe, do të kenë prapëseprapë hyrje te kopja e tyre.", + "Please forget all messages I have sent when my account is deactivated (Warning: this will cause future users to see an incomplete view of conversations)": "Të lutem, harro krejt mesazhet që kamë dërguar, kur të çaktivizohet llogaria ime (Kujdes: kjo do të bëjë që përdorues të ardhshëm të shohin një pamje jo të plotë të bisedave)", + "To continue, please enter your password:": "Që të vazhdohet, ju lutemi, jepni fjalëkalimin tuaj:", + "password": "fjalëkalim", + "Incompatible local cache": "Fshehtinë vendore e papërputhshme", + "Updating Riot": "Riot-i po përditësohet", + "Failed to upgrade room": "S’u arrit të përmirësohej dhoma", + "The room upgrade could not be completed": "Përmirësimi i dhomës s’u plotësua", + "Upgrade Room Version": "Përmirësoni Versionin e Dhomës", + "Send Logs": "Dërgo regjistra", + "Refresh": "Rifreskoje", + "Link to most recent message": "Lidhje për te mesazhet më të freskët", + "Link to selected message": "Lidhje për te mesazhi i përzgjedhur", + "Join this community": "Bëhuni pjesë e kësaj bashkësie", + "Leave this community": "Braktiseni këtë bashkësi", + "Who can join this community?": "Cilët mund të bëhen pjesë e kësaj bashkësie?", + "Everyone": "Cilido", + "Terms and Conditions": "Terma dhe Kushte", + "To continue using the %(homeserverDomain)s homeserver you must review and agree to our terms and conditions.": "Që të vazhdohet të përdoret shërbyesi home %(homeserverDomain)s, duhet të shqyrtoni dhe pajtoheni me termat dhe kushtet.", + "Review terms and conditions": "Shqyrtoni terma & kushte", + "Failed to reject invite": "S’u arrit të hidhet tej ftesa", + "Submit Debug Logs": "Parashtro Regjistra Diagnostikimi", + "Legal": "Ligjore", + "Please contact your service administrator to continue using this service.": "Ju lutemi, që të vazhdoni të përdorni këtë shërbim, lidhuni me përgjegjësin e shërbimit tuaj.", + "Try the app first": "Së pari, provoni aplikacionin", + "Open Devtools": "Hapni Mjete Zhvilluesi", + "Show developer tools": "Shfaq mjete zhvilluesi" } From 69a3b89405f6efcff2622645aba0f25e8f5c9085 Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Fri, 16 Nov 2018 10:16:33 +0000 Subject: [PATCH 019/237] Translated using Weblate (Albanian) Currently translated at 84.9% (1084 of 1276 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/sq/ --- src/i18n/strings/sq.json | 47 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index 9c53a6dba3..3c172075b9 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -76,7 +76,7 @@ "Failed to invite the following users to %(groupId)s:": "S’u arrit të ftoheshin përdoruesit vijues te %(groupId)s:", "Failed to invite users to community": "S’u arrit të ftoheshin përdorues te bashkësia", "Failed to invite users to %(groupId)s": "S’u arrit të ftoheshin përdorues te %(groupId)s", - "Failed to add the following rooms to %(groupId)s:": "Nuk mundën të shtohen dhomat vijuese në %(groupId)s:", + "Failed to add the following rooms to %(groupId)s:": "S’u arrit të shtoheshin dhomat vijuese te %(groupId)s:", "Unnamed Room": "Dhomë e Paemërtuar", "Riot does not have permission to send you notifications - please check your browser settings": "Riot nuk ka lejim të të dergojë lajmërime - të lutem kontrollo rregullimet e kërkuesit ueb tëndë", "Riot was not given permission to send notifications - please try again": "Riot-it nuk i është dhënë leje të dërgojë lajmërime - të lutëm përpjeku serish", @@ -97,7 +97,7 @@ "Failed to invite user": "S’u arrit të ftohej përdorues", "Operation failed": "Veprimi dështoi", "Failed to invite": "S’u arrit të ftohej", - "Failed to invite the following users to the %(roomName)s room:": "Përdoruesit vijuesë nuk mundën të ftohen në dhomën %(roomName)s:", + "Failed to invite the following users to the %(roomName)s room:": "S’u arrit të ftoheshin përdoruesit vijues te dhoma %(roomName)s:", "You need to be logged in.": "Lypset të jeni i futur në llogarinë tuaj.", "You need to be able to invite users to do that.": "Që ta bëni këtë, lypset të jeni në gjendje të ftoni përdorues.", "Unable to create widget.": "S’arrihet të krijohet widget-i.", @@ -991,7 +991,7 @@ "Unable to connect to Homeserver. Retrying...": "S’u arrit të lidhej me shërbyesin Home. Po riprovohet…", "Sorry, your homeserver is too old to participate in this room.": "Na ndjeni, shërbyesi juaj Home është shumë i vjetër për të marrë pjesë në këtë dhomë.", "Please contact your homeserver administrator.": "Ju lutemi, lidhuni me përgjegjësin e shërbyesit tuaj Home.", - "Increase performance by only loading room members on first view": "Përmirësoni punimin duke ngarkuar anëtarë dhome vetëm kur sillen para syve.", + "Increase performance by only loading room members on first view": "Përmirësoni punimin duke ngarkuar anëtarë dhome vetëm kur sillen para syve", "Send analytics data": "Dërgo të dhëna analitike", "This event could not be displayed": "Ky akt s’u shfaq dot", "Encrypting": "Fshehtëzim", @@ -1049,5 +1049,44 @@ "Please contact your service administrator to continue using this service.": "Ju lutemi, që të vazhdoni të përdorni këtë shërbim, lidhuni me përgjegjësin e shërbimit tuaj.", "Try the app first": "Së pari, provoni aplikacionin", "Open Devtools": "Hapni Mjete Zhvilluesi", - "Show developer tools": "Shfaq mjete zhvilluesi" + "Show developer tools": "Shfaq mjete zhvilluesi", + "Your User Agent": "Agjent Përdoruesi i Juaj", + "Your device resolution": "Qartësi e pajisjes tuaj", + "A call is currently being placed!": "Është duke u bërë një thirrje!", + "You do not have permission to start a conference call in this room": "S’keni leje për të nisur një thirrje konferencë këtë në këtë dhomë", + "Missing roomId.": "Mungon roomid.", + "%(senderName)s added %(count)s %(addedAddresses)s as addresses for this room.|one": "%(senderName)s shtoi %(addedAddresses)s si një adresë për këtë dhomë.", + "%(senderName)s removed %(count)s %(removedAddresses)s as addresses for this room.|other": "%(senderName)s hoqi %(removedAddresses)s si adresa për këtë dhomë.", + "%(senderName)s removed %(count)s %(removedAddresses)s as addresses for this room.|one": "%(senderName)s hoqi %(removedAddresses)s si adresë për këtë dhomë.", + "%(senderName)s removed the main address for this room.": "%(senderName)s hoqi adresën kryesore për këtë dhomë.", + "deleted": "u fshi", + "This room has been replaced and is no longer active.": "Kjo dhomë është zëvendësuar dhe s’është më aktive.", + "At this time it is not possible to reply with an emote.": "Sot për sot s’është e mundur të përgjigjeni me një emote.", + "Share room": "Ndani dhomë me të tjerë", + "Drop here to demote": "Hidheni këtu t’i ulet përparësia", + "You don't currently have any stickerpacks enabled": "Hëpërhë, s’keni të aktivizuar ndonjë pako ngjitësesh", + "%(senderDisplayName)s changed the room avatar to ": "%(senderDisplayName)s ndryshoi avatarin e dhomës në ", + "This room is a continuation of another conversation.": "Kjo dhomë është një vazhdim i një bisede tjetër.", + "%(oneUser)sjoined and left %(count)s times|other": "%(oneUser)shyri dhe doli %(count)s herë", + "%(severalUsers)srejected their invitations %(count)s times|other": "%(severalUsers)shodhën poshtë ftesat e tyre %(count)s herë", + "%(oneUser)srejected their invitation %(count)s times|other": "%(oneUser)shodhi poshtë ftesën e vet %(count)s herë", + "%(severalUsers)shad their invitations withdrawn %(count)s times|other": "Për %(severalUsers)s u hodhën poshtë ftesat e tyre %(count)s herë", + "%(oneUser)shad their invitation withdrawn %(count)s times|other": "Për %(oneUser)s përdorues ftesa u tërhoq mbrapsht %(count)s herë", + "%(oneUser)shad their invitation withdrawn %(count)s times|one": "U tërhoq mbrapsht ftesa për %(oneUser)s", + "What GitHub issue are these logs for?": "Për cilat çështje në GitHub janë këta regjistra?", + "Community IDs cannot be empty.": "ID-të e bashkësisë s’mund të jenë të zbrazëta.", + "This will make your account permanently unusable. You will not be able to log in, and no one will be able to re-register the same user ID. This will cause your account to leave all rooms it is participating in, and it will remove your account details from your identity server. This action is irreversible.": "Kjo do ta bëjë llogarinë tuaj përgjithmonë të papërdorshme. S’do të jeni në gjendje të hyni në llogarinë tuaj, dhe askush s’do të jetë në gjendje të riregjistrojë të njëjtën ID përdoruesi. Kjo do të shkaktojë daljen e llogarisë tuaj nga krejt dhomat ku merr pjesë, dhe do të heqë hollësitë e llogarisë tuaj nga shërbyesi juaj i identiteteve. Ky veprim është i paprapakthyeshëm.", + "Deactivating your account does not by default cause us to forget messages you have sent. If you would like us to forget your messages, please tick the box below.": "Çaktivizimi i llogarisë tuaj nuk shkakton, si parazgjedhje, harrimin nga ne të mesazheve që keni dërguar. Nëse do të donit të harrojmë mesazhet tuaja, ju lutemi, i vini shenjë kutizës më poshtë.", + "Upgrade this room to version %(version)s": "Përmirësojeni këtë dhomë me versionin %(version)s", + "Share Room": "Ndani Dhomë Me të Tjerë", + "Share Community": "Ndani Bashkësi Me të Tjerë", + "Share Room Message": "Ndani Me të Tjerë Mesazh Dhome", + "Share Message": "Ndani Mesazh me të tjerë", + "Collapse Reply Thread": "Tkurre Rrjedhën e Përgjigjeve", + "Failed to add the following users to the summary of %(groupId)s:": "S’u arrit të ftoheshin përdoruesit vijues te përmbledhja e %(groupId)s:", + "Unable to join community": "S’arrihet të bëhet pjesë e bashkësisë", + "Unable to leave community": "S’arrihet të braktiset bashkësia", + "Lazy loading members not supported": "Nuk mbulohet lazy-load për anëtarët", + "An email address is required to register on this homeserver.": "Që të regjistroheni në këtë shërbyes home, lypset një adresë email.", + "Claimed Ed25519 fingerprint key": "U pretendua për shenja gishtash Ed25519" } From 400ba219f13c52253a5fdc2c346e129d7cd18951 Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Fri, 16 Nov 2018 11:34:50 +0000 Subject: [PATCH 020/237] Translated using Weblate (Albanian) Currently translated at 88.7% (1133 of 1276 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/sq/ --- src/i18n/strings/sq.json | 55 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 3 deletions(-) diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index 3c172075b9..6d6570659b 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -48,9 +48,9 @@ "Thu": "Enj", "Fri": "Pre", "Sat": "Sht", - "%(weekDayName)s, %(monthName)s %(day)s %(time)s": "%(weekDayName)s, %(day)s %(monthName)s %(time)s", + "%(weekDayName)s, %(monthName)s %(day)s %(time)s": "%(weekDayName)s, %(day)s %(monthName)s më %(time)s", "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s, %(day)s %(monthName)s %(fullYear)s", - "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s, %(day)s %(monthName)s %(fullYear)s %(time)s", + "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s, %(day)s %(monthName)s %(fullYear)s më %(time)s", "Who would you like to add to this community?": "Kë do të donit të shtonit te kjo bashkësi?", "Warning: any person you add to a community will be publicly visible to anyone who knows the community ID": "Paralajmërim: se cili që e shton në një komunitet do t‘i doket se cilit që e di identifikatuesin e komunitetit", "Invite new community members": "Ftoni anëtarë të rinj bashkësie", @@ -1088,5 +1088,54 @@ "Unable to leave community": "S’arrihet të braktiset bashkësia", "Lazy loading members not supported": "Nuk mbulohet lazy-load për anëtarët", "An email address is required to register on this homeserver.": "Që të regjistroheni në këtë shërbyes home, lypset një adresë email.", - "Claimed Ed25519 fingerprint key": "U pretendua për shenja gishtash Ed25519" + "Claimed Ed25519 fingerprint key": "U pretendua për shenja gishtash Ed25519", + "Every page you use in the app": "Çdo faqe që përdorni te aplikacioni", + "A conference call could not be started because the intgrations server is not available": "S’u nis dot një thirrje konferencë, ngaqë shërbyesi i integrimit s’është i kapshëm", + "Changes colour scheme of current room": "Ndryshon skemë e ngjyrave të dhomës së tanishme", + "%(senderName)s added %(count)s %(addedAddresses)s as addresses for this room.|other": "%(senderName)s shtoi %(addedAddresses)s si adresa për këtë dhomë.", + "%(senderName)s added %(addedAddresses)s and removed %(removedAddresses)s as addresses for this room.": "%(senderName)s shtoi %(addedAddresses)s dhe hoqi %(removedAddresses)s si adresa për këtë dhomë.", + "%(senderName)s turned on end-to-end encryption (algorithm %(algorithm)s).": "%(senderName)s aktivizoi fshehtëzimin skaj-më-skaj (algorithm %(algorithm)s).", + "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s nga %(fromPowerLevel)s në %(toPowerLevel)s", + "%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s ndryshoi shkallën e pushtetit të %(powerLevelDiffText)s.", + "%(senderName)s changed the pinned messages for the room.": "%(senderName)s ndryshoi mesazhin e fiksuar për këtë dhomë.", + "Hide join/leave messages (invites/kicks/bans unaffected)": "Fshihi mesazhet e hyrjeve/daljeve (kjo nuk prek mesazhe ftesash/përzëniesh/dëbimesh)", + "Enable automatic language detection for syntax highlighting": "Aktivizo pikasje të vetvetishme të gjuhës për theksim sintakse", + "Hide avatars in user and room mentions": "Fshihi avatarët në përmendje përdoruesish dhe dhomash", + "Automatically replace plain text Emoji": "Zëvendëso automatikisht emotikone tekst të thjeshtë me Emoji", + "Enable URL previews for this room (only affects you)": "Aktivizo paraparje URL-sh për këtë dhomë (prek vetëm ju)", + "Enable URL previews by default for participants in this room": "Aktivizo, si parazgjedhje, paraparje URL-sh për pjesëmarrësit në këtë dhomë", + "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains": "Te +%(msisdn)s u dërgua një mesazh tekst. Ju lutemi, verifikoni kodin që përmban", + "At this time it is not possible to reply with a file so this will be sent without being a reply.": "Sot për sot s’është e mundur të përgjigjeni me një kartelë, ndaj kjo do të dërgohet pa qenë një përgjigje.", + "A text message has been sent to %(msisdn)s": "Te %(msisdn)s u dërgua një mesazh tekst", + "Failed to remove '%(roomName)s' from %(groupId)s": "S’u arrit të hiqej '%(roomName)s' nga %(groupId)s", + "Do you want to load widget from URL:": "Doni të ngarkohet widget nga URL-ja:", + "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "Fshirja e një widget-i e heq atë për krejt përdoruesit në këtë dhomë. Jeni i sigurt se doni të fshihet ky widget?", + "An error ocurred whilst trying to remove the widget from the room": "Ndodhi një gabim teksa provohej të hiqej widget-i nga dhoma", + "%(nameList)s %(transitionList)s": "%(nameList)s %(transitionList)s", + "Before submitting logs, you must create a GitHub issue to describe your problem.": "Përpara se të parashtroni regjistra, duhet të krijoni një çështje në GitHub issue që të përshkruani problemin tuaj.", + "Create a new chat or reuse an existing one": "Krijoni një fjalosje të re ose përdorni një ekzistuese", + "Community IDs may only contain characters a-z, 0-9, or '=_-./'": "ID-të e bashkësive mund të përmbajnë vetëm shenjat a-z, 0-9, ose '=_-./'", + "Block users on other matrix homeservers from joining this room": "Bllokoju hyrjen në këtë dhomë përdoruesve në shërbyes të tjerë Matrix home", + "Create a new room with the same name, description and avatar": "Krijoni një dhomë të re me po atë emër, përshkrim dhe avatar", + "\"%(RoomName)s\" contains devices that you haven't seen before.": "\"%(RoomName)s\" përmban pajisje që s’i keni parë më parë.", + "

    HTML for your community's page

    \n

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

    \n

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

    \n": "

    HTML për faqen e bashkësisë tuaj

    \n

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

    \n

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

    \n", + "Failed to add the following rooms to the summary of %(groupId)s:": "S’u arrit të shtoheshin dhomat vijuese te përmbledhja e %(groupId)s:", + "Failed to remove the room from the summary of %(groupId)s": "S’u arrit të hiqej dhoma prej përmbledhjes së %(groupId)s", + "Failed to remove a user from the summary of %(groupId)s": "S’u arrit të hiqej një përdorues nga përmbledhja e %(groupId)s", + "Changes made to your community name and avatar might not be seen by other users for up to 30 minutes.": "Ndryshimet e bëra te emri dhe avatari i bashkësisë tuaj mund të mos shihen nga përdoruesit e tjera para deri 30 minutash.", + "Can't leave Server Notices room": "Dhoma Njoftime Shërbyesi, s’braktiset dot", + "For security, this session has been signed out. Please sign in again.": "Për hir të sigurisë, është bërë dalja nga ky sesion. Ju lutemi, ribëni hyrjen.", + "Data from an older version of Riot has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.": "Janë pikasur të dhëna nga një version i dikurshëm i Riot-it. Kjo do të bëjë që kriptografia skaj-më-skaj te versioni i dikurshëm të mos punojë si duhet. Mesazhet e fshehtëzuar skaj-më-skaj tani së fundi teksa përdorej versioni i dikurshëm mund të mos jenë të shfshehtëzueshëm në këtë version. Kjo mund bëjë edhe që mesazhet e shkëmbyera me këtë version të dështojnë. Nëse ju dalin probleme, bëni daljen dhe rihyni në llogari. Që të ruhet historiku i mesazheve, eksportoni dhe ri-importoni kyçet tuaj.", + "Did you know: you can use communities to filter your Riot.im experience!": "E dinit se: mund t’i përdorni bashkësitë për të flitruar punimin tuaj në Riot.im experience?!?", + "Error whilst fetching joined communities": "Gabim teksa silleshin bashkësitë ku merret pjesë", + "Show devices, send anyway or cancel.": "Shfaq pajisje, dërgoje sido qoftë ose anuloje.", + "%(count)s Resend all or cancel all now. You can also select individual messages to resend or cancel.|other": "Ridërgojini krejt ose anulojini krejt tani. Për ridërgim ose anulim, mundeni edhe të përzgjidhni mesazhe individualë.", + "%(count)s Resend all or cancel all now. You can also select individual messages to resend or cancel.|one": "Ridërgojeni mesazhin ose anulojeni mesazhin tani.", + "For security, logging out will delete any end-to-end encryption keys from this browser. If you want to be able to decrypt your conversation history from future Riot sessions, please export your room keys for safe-keeping.": "Për hir të sigurisë, dalja nga llogaria do të sjellë fshirjen në këtë shfletues të çfarëdo kyçesh fshehtëzimi skaj-më-skaj. Nëse doni të jeni në gjendje të fshehtëzoni historikun e bisedave tuaja që nga sesione të ardhshëm Riot, ju lutemi, eksportoni kyçet tuaj të dhomës, për t’i ruajtur të parrezikuar diku.", + "Audio Output": "Sinjal Audio", + "Error: Problem communicating with the given homeserver.": "Gabimr: Problem komunikimi me shërbyesin e dhënë Home.", + "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or enable unsafe scripts.": "S’lidhet dot te shërbyes Home përmes HTTP-je, kur te shtylla e shfletuesit tuaj jepet një URL HTTPS. Ose përdorni HTTPS-në, ose aktivizoni përdorimin e programtheve jo të sigurt.", + "Can't connect to homeserver - please check your connectivity, ensure your homeserver's SSL certificate is trusted, and that a browser extension is not blocking requests.": "S’lidhet dot te shërbyes Home - ju lutemi, kontrolloni lidhjen tuaj, sigurohuni që dëshmia SSL e shërbyesit tuaj Home besohet, dhe që s’ka ndonjë zgjerim shfletuesi që po bllokon kërkesat tuaja.", + "Failed to remove tag %(tagName)s from room": "S’u arrit të hiqej etiketa %(tagName)s nga dhoma", + "Failed to add tag %(tagName)s to room": "S’u arrit të shtohej në dhomë etiketa %(tagName)s" } From ae6c46806c04d264363dfdc3eea86e20c46b0eb0 Mon Sep 17 00:00:00 2001 From: Akarshan Biswas Date: Fri, 16 Nov 2018 17:30:21 +0000 Subject: [PATCH 021/237] Translated using Weblate (Hindi) Currently translated at 4.1% (53 of 1276 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/hi/ --- src/i18n/strings/hi.json | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/hi.json b/src/i18n/strings/hi.json index eb73d65a78..4e052f3a3f 100644 --- a/src/i18n/strings/hi.json +++ b/src/i18n/strings/hi.json @@ -15,5 +15,41 @@ "Which officially provided instance you are using, if any": "क्या आप कोई अधिकृत संस्करण इस्तेमाल कर रहे हैं? अगर हां, तो कौन सा", "Your homeserver's URL": "आपके होमसर्वर का यू. आर. एल.", "Every page you use in the app": "हर पृष्ठ जिसका आप इस एप में इस्तेमाल करते हैं", - "Your User Agent": "आपका उपभोक्ता प्रतिनिधि" + "Your User Agent": "आपका उपभोक्ता प्रतिनिधि", + "Custom Server Options": "कस्टम सर्वर विकल्प", + "Dismiss": "खारिज", + "powered by Matrix": "मैट्रिक्स द्वारा संचालित", + "Whether or not you're using the Richtext mode of the Rich Text Editor": "चाहे आप रिच टेक्स्ट एडिटर के रिच टेक्स्ट मोड का उपयोग कर रहे हों या नहीं", + "Your identity server's URL": "आपका आइडेंटिटी सर्वर का URL", + "e.g. %(exampleValue)s": "उदाहरणार्थ %(exampleValue)s", + "e.g. ": "उदाहरणार्थ ", + "Your device resolution": "आपके यंत्र का रेसोलुशन", + "Analytics": "एनालिटिक्स", + "The information being sent to us to help make Riot.im better includes:": "Riot.im को बेहतर बनाने के लिए हमें भेजी गई जानकारी में निम्नलिखित शामिल हैं:", + "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "जहां इस पृष्ठ में पहचान योग्य जानकारी शामिल है, जैसे कि रूम, यूजर या समूह आईडी, वह डाटा सर्वर को भेजे से पहले हटा दिया जाता है।", + "Call Failed": "कॉल विफल", + "There are unknown devices in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.": "इस रूम में अज्ञात डिवाइस हैं: यदि आप उन्हें सत्यापित किए बिना आगे बढ़ते हैं, तो किसी और के लिए आपकी कॉल पर नजर डालना संभव हो सकता हैं।", + "Review Devices": "डिवाइस की समीक्षा करें", + "Call Anyway": "वैसे भी कॉल करें", + "Answer Anyway": "वैसे भी जवाब दें", + "Call": "कॉल", + "Answer": "उत्तर", + "Call Timeout": "कॉल टाइमआउट", + "The remote side failed to pick up": "दूसरी पार्टी ने जवाब नहीं दिया", + "Unable to capture screen": "स्क्रीन कैप्चर करने में असमर्थ", + "Existing Call": "मौजूदा कॉल", + "You are already in a call.": "आप पहले से ही एक कॉल में हैं।", + "VoIP is unsupported": "VoIP असमर्थित है", + "You cannot place VoIP calls in this browser.": "आप इस ब्राउज़र में VoIP कॉल नहीं कर सकते हैं।", + "You cannot place a call with yourself.": "आप अपने साथ कॉल नहीं कर सकते हैं।", + "Could not connect to the integration server": "इंटीग्रेशन सर्वर से संपर्क नहीं हो सका", + "A conference call could not be started because the intgrations server is not available": "कॉन्फ़्रेंस कॉल प्रारंभ नहीं किया जा सका क्योंकि इंटीग्रेशन सर्वर उपलब्ध नहीं है", + "Call in Progress": "कॉल चालू हैं", + "A call is currently being placed!": "वर्तमान में एक कॉल किया जा रहा है!", + "A call is already in progress!": "कॉल पहले ही प्रगति पर है!", + "Permission Required": "अनुमति आवश्यक है", + "You do not have permission to start a conference call in this room": "आपको इस रूम में कॉन्फ़्रेंस कॉल शुरू करने की अनुमति नहीं है", + "The file '%(fileName)s' failed to upload": "फ़ाइल '%(fileName)s' अपलोड करने में विफल रही", + "The file '%(fileName)s' exceeds this home server's size limit for uploads": "फाइल '%(fileName)s' अपलोड के लिए इस होम सर्वर की आकार सीमा से अधिक है", + "Upload Failed": "अपलोड विफल" } From 2787ebcc72b4e6ae47e21cbf927bbff7c987c3af Mon Sep 17 00:00:00 2001 From: Akarshan Biswas Date: Fri, 16 Nov 2018 18:23:11 +0000 Subject: [PATCH 022/237] Translated using Weblate (Hindi) Currently translated at 9.0% (116 of 1280 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/hi/ --- src/i18n/strings/hi.json | 65 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/hi.json b/src/i18n/strings/hi.json index 4e052f3a3f..6f168c1849 100644 --- a/src/i18n/strings/hi.json +++ b/src/i18n/strings/hi.json @@ -51,5 +51,68 @@ "You do not have permission to start a conference call in this room": "आपको इस रूम में कॉन्फ़्रेंस कॉल शुरू करने की अनुमति नहीं है", "The file '%(fileName)s' failed to upload": "फ़ाइल '%(fileName)s' अपलोड करने में विफल रही", "The file '%(fileName)s' exceeds this home server's size limit for uploads": "फाइल '%(fileName)s' अपलोड के लिए इस होम सर्वर की आकार सीमा से अधिक है", - "Upload Failed": "अपलोड विफल" + "Upload Failed": "अपलोड विफल", + "Sun": "रवि", + "Mon": "सोम", + "Tue": "मंगल", + "Wed": "बुध", + "Thu": "गुरु", + "Fri": "शुक्र", + "Sat": "शनि", + "Jan": "जनवरी", + "Feb": "फ़रवरी", + "Mar": "मार्च", + "Apr": "अप्रैल", + "May": "मई", + "Jun": "जून", + "Jul": "जुलाई", + "Aug": "अगस्त", + "Sep": "सितंबर", + "Oct": "अक्टूबर", + "Nov": "नवंबर", + "Dec": "दिसंबर", + "PM": "अपराह्न", + "AM": "पूर्वाह्न", + "%(weekDayName)s %(time)s": "%(weekDayName)s %(time)s", + "%(weekDayName)s, %(monthName)s %(day)s %(time)s": "%(weekDayName)s %(monthName)s %(day)s %(time)s", + "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s, %(day)s %(monthName)s %(fullYear)s", + "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s", + "Who would you like to add to this community?": "आप इस कम्युनिटी में किसे जोड़ना चाहेंगे?", + "Warning: any person you add to a community will be publicly visible to anyone who knows the community ID": "चेतावनी: किसी भी कम्युनिटी में जो भी व्यक्ति आप जोड़ते हैं वह सार्वजनिक रूप से किसी को भी दिखाई देगा जो कम्युनिटी आईडी जानता है", + "Invite new community members": "नए कम्युनिटी के सदस्यों को आमंत्रित करें", + "Name or matrix ID": "नाम या मैट्रिक्स ID", + "Invite to Community": "कम्युनिटी में आमंत्रित करें", + "Which rooms would you like to add to this community?": "आप इस समुदाय में कौन से रूम जोड़ना चाहते हैं?", + "Show these rooms to non-members on the community page and room list?": "क्या आप इन मैट्रिक्स रूम को कम्युनिटी पृष्ठ और रूम लिस्ट के गैर सदस्यों को दिखाना चाहते हैं?", + "Add rooms to the community": "कम्युनिटी में रूम जोड़े", + "Room name or alias": "रूम का नाम या उपनाम", + "Add to community": "कम्युनिटी में जोड़ें", + "Failed to invite the following users to %(groupId)s:": "निम्नलिखित उपयोगकर्ताओं को %(groupId)s में आमंत्रित करने में विफल:", + "Failed to invite users to community": "उपयोगकर्ताओं को कम्युनिटी में आमंत्रित करने में विफल", + "Failed to invite users to %(groupId)s": "उपयोगकर्ताओं को %(groupId)s में आमंत्रित करने में विफल", + "Failed to add the following rooms to %(groupId)s:": "निम्नलिखित रूम को %(groupId)s में जोड़ने में विफल:", + "Riot does not have permission to send you notifications - please check your browser settings": "आपको सूचनाएं भेजने की रायट की अनुमति नहीं है - कृपया अपनी ब्राउज़र सेटिंग्स जांचें", + "Riot was not given permission to send notifications - please try again": "रायट को सूचनाएं भेजने की अनुमति नहीं दी गई थी - कृपया पुनः प्रयास करें", + "Unable to enable Notifications": "अधिसूचनाएं सक्षम करने में असमर्थ", + "This email address was not found": "यह ईमेल पता नहीं मिला था", + "Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "आपका ईमेल पता इस होमसर्वर पर मैट्रिक्स आईडी से जुड़ा प्रतीत नहीं होता है।", + "Registration Required": "पंजीकरण आवश्यक", + "You need to register to do this. Would you like to register now?": "ऐसा करने के लिए आपको पंजीकरण करने की आवश्यकता है। क्या आप अभी पंजीकरण करना चाहते हैं?", + "Register": "पंजीकरण करें", + "Default": "डिफ़ॉल्ट", + "Restricted": "वर्जित", + "Moderator": "मध्यस्थ", + "Admin": "व्यवस्थापक", + "Start a chat": "एक चैट शुरू करें", + "Who would you like to communicate with?": "आप किसके साथ संवाद करना चाहते हैं?", + "Email, name or matrix ID": "ईमेल, नाम या मैट्रिक्स आईडी", + "Start Chat": "चैट शुरू करें", + "Invite new room members": "नए रूम के सदस्यों को आमंत्रित करें", + "Who would you like to add to this room?": "आप इस रूम में किसे जोड़ना चाहेंगे?", + "Send Invites": "आमंत्रण भेजें", + "Failed to invite user": "उपयोगकर्ता को आमंत्रित करने में विफल", + "Operation failed": "कार्रवाई विफल", + "Failed to invite": "आमंत्रित करने में विफल", + "Failed to invite the following users to the %(roomName)s room:": "निम्नलिखित उपयोगकर्ताओं को %(roomName)s रूम में आमंत्रित करने में विफल:", + "You need to be logged in.": "आपको लॉग इन करने की जरूरत है।" } From ed7ed063f870644842ccb7ea7b2d1e06d53f1029 Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Fri, 16 Nov 2018 20:01:43 +0000 Subject: [PATCH 023/237] Translated using Weblate (Albanian) Currently translated at 92.1% (1180 of 1281 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/sq/ --- src/i18n/strings/sq.json | 59 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 53 insertions(+), 6 deletions(-) diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index 6d6570659b..4faabfe5d7 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -38,7 +38,7 @@ "The file '%(fileName)s' exceeds this home server's size limit for uploads": "Fajli '%(fileName)s' tejkalon kufirin madhësie për mbartje e këtij server-i shtëpiak", "Upload Failed": "Ngarkimi Dështoi", "Failure to create room": "S’u arrit të krijohej dhomë", - "Server may be unavailable, overloaded, or you hit a bug.": "Server-i është i padisponueshëm, i ngarkuar tej mase, apo ka një gabim.", + "Server may be unavailable, overloaded, or you hit a bug.": "Shërbyesi mund të jetë i pakapshëm, i mbingarkuar, ose hasët një të metë.", "Send anyway": "Dërgoje sido qoftë", "Send": "Dërgoje", "Sun": "Die", @@ -78,8 +78,8 @@ "Failed to invite users to %(groupId)s": "S’u arrit të ftoheshin përdorues te %(groupId)s", "Failed to add the following rooms to %(groupId)s:": "S’u arrit të shtoheshin dhomat vijuese te %(groupId)s:", "Unnamed Room": "Dhomë e Paemërtuar", - "Riot does not have permission to send you notifications - please check your browser settings": "Riot nuk ka lejim të të dergojë lajmërime - të lutem kontrollo rregullimet e kërkuesit ueb tëndë", - "Riot was not given permission to send notifications - please try again": "Riot-it nuk i është dhënë leje të dërgojë lajmërime - të lutëm përpjeku serish", + "Riot does not have permission to send you notifications - please check your browser settings": "Riot-i s’ka leje t’ju dërgojë njoftime - Ju lutemi, kontrolloni rregullimet e shfletuesit tuajPlease wait whilst we resynchronise with the server", + "Riot was not given permission to send notifications - please try again": "Riot-it s’iu dha leje të dërgojë njoftime - ju lutemi, riprovoni", "Unable to enable Notifications": "S’arrihet të aktivizohen njoftimet", "This email address was not found": "Kjo adresë email s’u gjet", "Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "Adresa juaj email s’duket të jetë e përshoqëruar me një ID Matrix në këtë shërbyes Home.", @@ -508,7 +508,7 @@ "You're not currently a member of any communities.": "Hëpërhë, s’jeni anëtar i ndonjë bashkësie.", "Unknown Address": "Adresë e Panjohur", "Allow": "Lejoje", - "Revoke widget access": "Shfuqizo hyrje widget", + "Revoke widget access": "Shfuqizo hyrje në widget", "Create new room": "Krijoni dhomë të re", "Unblacklist": "Hiqe nga listë e zezë", "Blacklist": "Listë e zezë", @@ -1126,7 +1126,7 @@ "Can't leave Server Notices room": "Dhoma Njoftime Shërbyesi, s’braktiset dot", "For security, this session has been signed out. Please sign in again.": "Për hir të sigurisë, është bërë dalja nga ky sesion. Ju lutemi, ribëni hyrjen.", "Data from an older version of Riot has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.": "Janë pikasur të dhëna nga një version i dikurshëm i Riot-it. Kjo do të bëjë që kriptografia skaj-më-skaj te versioni i dikurshëm të mos punojë si duhet. Mesazhet e fshehtëzuar skaj-më-skaj tani së fundi teksa përdorej versioni i dikurshëm mund të mos jenë të shfshehtëzueshëm në këtë version. Kjo mund bëjë edhe që mesazhet e shkëmbyera me këtë version të dështojnë. Nëse ju dalin probleme, bëni daljen dhe rihyni në llogari. Që të ruhet historiku i mesazheve, eksportoni dhe ri-importoni kyçet tuaj.", - "Did you know: you can use communities to filter your Riot.im experience!": "E dinit se: mund t’i përdorni bashkësitë për të flitruar punimin tuaj në Riot.im experience?!?", + "Did you know: you can use communities to filter your Riot.im experience!": "E dinit se: mund t’i përdorni bashkësitë për të filtruar punimin tuaj në Riot.im?!?", "Error whilst fetching joined communities": "Gabim teksa silleshin bashkësitë ku merret pjesë", "Show devices, send anyway or cancel.": "Shfaq pajisje, dërgoje sido qoftë ose anuloje.", "%(count)s Resend all or cancel all now. You can also select individual messages to resend or cancel.|other": "Ridërgojini krejt ose anulojini krejt tani. Për ridërgim ose anulim, mundeni edhe të përzgjidhni mesazhe individualë.", @@ -1137,5 +1137,52 @@ "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or enable unsafe scripts.": "S’lidhet dot te shërbyes Home përmes HTTP-je, kur te shtylla e shfletuesit tuaj jepet një URL HTTPS. Ose përdorni HTTPS-në, ose aktivizoni përdorimin e programtheve jo të sigurt.", "Can't connect to homeserver - please check your connectivity, ensure your homeserver's SSL certificate is trusted, and that a browser extension is not blocking requests.": "S’lidhet dot te shërbyes Home - ju lutemi, kontrolloni lidhjen tuaj, sigurohuni që dëshmia SSL e shërbyesit tuaj Home besohet, dhe që s’ka ndonjë zgjerim shfletuesi që po bllokon kërkesat tuaja.", "Failed to remove tag %(tagName)s from room": "S’u arrit të hiqej etiketa %(tagName)s nga dhoma", - "Failed to add tag %(tagName)s to room": "S’u arrit të shtohej në dhomë etiketa %(tagName)s" + "Failed to add tag %(tagName)s to room": "S’u arrit të shtohej në dhomë etiketa %(tagName)s", + "Pin unread rooms to the top of the room list": "Fiksoji dhomat e palexuara në krye të listës së dhomave", + "Pin rooms I'm mentioned in to the top of the room list": "Fiksoji dhomat ku përmendem në krye të listës së dhomave", + "Enable widget screenshots on supported widgets": "Aktivizo foto ekrani widget-esh për widget-e që e mbulojnë", + "Changing password will currently reset any end-to-end encryption keys on all devices, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "Ndryshimi i fjalëkalimit do të sjellë zerimin e çfarëdo kyçesh fshehtëzimi skaj-më-skaj në krejt pajisjet, duke e bërë të palexueshëm historikun e fshehtëzuar të bisedave, hiq rastin kur i eksportoni më parë kyçet tuaj të dhomës dhe i ri-importoni ata më pas. Në të ardhmen kjo do të përmirësohet.", + "Join as voice or video.": "Merrni pjesë me ose me video.", + "Key share requests are sent to your other devices automatically. If you rejected or dismissed the key share request on your other devices, click here to request the keys for this session again.": "Kërkesat për ndarje kyçesh dërgohen automatikisht te pajisjet tuaja të tjera. Nëse s’e pranuat ose e hodhët tej kërkesën për ndarje kyçesh në pajisjet tuaja të tjera, klikoni këtu që të rikërkoni kyçe për këtë sesion.", + "If your other devices do not have the key for this message you will not be able to decrypt them.": "Nëse pajisjet tuaja të tjera nuk kanë kyçin për këtë mesazh, s’do të jeni në gjendje ta shfshehtëzoni.", + "Demote yourself?": "Të zhgradohet vetvetja?", + "Demote": "Zhgradoje", + "Failed to toggle moderator status": "S’u arrit të këmbehet gjendje moderatori", + "Server unavailable, overloaded, or something else went wrong.": "Shërbyesi është i pakapshëm, i mbingarkuar, ose diç tjetër shkoi ters.", + "Drop here to tag %(section)s": "Hidheni këtu që të caktohet etiketë për %(section)s", + "Press to start a chat with someone": "Shtypni që të nisni një bisedë me dikë", + "No users have specific privileges in this room": "S’ka përdorues me privilegje të caktuara në këtë dhomë", + "Guests cannot join this room even if explicitly invited.": "Vizitorët s’mund të marrin pjesë në këtë edhe po të jenë ftuar shprehimisht", + "Publish this room to the public in %(domain)s's room directory?": "Të bëhet publike kjo dhomë te drejtoria e dhomave %(domain)s?", + "Click here to upgrade to the latest room version and ensure room integrity is protected.": "Klikoni këtu që ta përmirësoni me versionin më të ri të dhomë dhe të garantoni mbrojtjen e paprekshmërisë së dhomës.", + "In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.": "Në dhoma të fshehtëzuara, si kjo, paraparja e URL-ve është e çaktivizuar, si parazgjedhje, për të garantuar që shërbyesi juaj home (ku edhe prodhohen paraparjet) të mos grumbullojë të dhëna rreth lidhjesh që shihni në këtë dhomë.", + "Please review and accept the policies of this homeserver:": "Ju lutemi, shqyrtoni dhe pranoni rregullat e këtij shërbyesi home:", + "If you don't specify an email address, you won't be able to reset your password. Are you sure?": "Nëse nuk përcaktoni një adresë email, s’do të jeni në gjendje të bëni ricaktime të fjalëkalimit tuaj. Jeni i sigurt?", + "Removing a room from the community will also remove it from the community page.": "Heqja e një dhome nga bashkësia do ta heqë atë edhe nga faqja e bashkësisë.", + "Please help improve Riot.im by sending anonymous usage data. This will use a cookie (please see our Cookie Policy).": "Ju lutemi, ndihmoni të përmirësohet Riot.im duke dërguar të dhëna anonime përdorimi. Për këtë do të përdoret një cookie (ju lutemi, shihni Rregullat tona mbi Cookie-t).", + "Please help improve Riot.im by sending anonymous usage data. This will use a cookie.": "Ju lutemi, ndihmoni të përmirësohet Riot.im duke dërguar të dhëna anonime përdorimi. Për këtë do të përdoret një cookie.", + "Please contact your service administrator to get this limit increased.": "Ju lutemi, që të shtohet ky kufi, lidhuni me përgjegjësin e shërbimit.", + "If the other version of Riot is still open in another tab, please close it as using Riot on the same host with both lazy loading enabled and disabled simultaneously will cause issues.": "Nëse versioni tjetër i Riot-it është ende i hapur në një skedë tjetër, ju lutemi, mbylleni, ngaqë përdorimi njëkohësisht i Riot-it në të njëjtën strehë, në njërën anë me lazy loading të aktivizuar dhe në anën tjetër të çaktivizuar do të shkaktojë probleme.", + "Riot now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!": "Riot-i tani përdor 3 deri 5 herë më pak kujtesë, duke ngarkuar të dhëna mbi përdorues të tjerë vetëm kur duhen. Ju lutemi, prisni, teksa njëkohësojmë të dhënat me shërbyesin!", + "Put a link back to the old room at the start of the new room so people can see old messages": "Vendosni në krye të dhomës së re një lidhje për te dhoma e vjetër, që njerëzit të mund të shohin mesazhet e vjetër", + "Log out and remove encryption keys?": "Të dilet dhe të hiqen kyçet e fshehtëzimit?", + "If you have previously used a more recent version of Riot, your session may be incompatible with this version. Close this window and return to the more recent version.": "Nëse më herët keni përdorur një version më të freskët të Riot-it, sesioni juaj mund të jetë i papërputhshëm me këtë version. Mbylleni këtë dritare dhe kthehuni te versioni më i ri.", + "Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.": "Pastrimi i gjërave të depozituara në shfletuesin tuaj mund ta ndreqë problemin, por kjo do të sjellë nxjerrjen tuaj nga llogari dhe do ta bëjë të palexueshëm çfarëdo historiku të fshehtëzuar të bisedës.", + "If you would like to create a Matrix account you can register now.": "Nëse do të donit të krijoni një llogari Matrix, mund të regjistroheni që tani.", + "If you already have a Matrix account you can log in instead.": "Nëse keni tashmë një llogari Matrix, mund të bëni hyrjen.", + "Share message history with new users": "Ndani me përdorues të rinj historik mesazhesh", + "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Krijoni një bashkësi që bëni tok përdorues dhe dhoma! Krijoni një faqe hyrëse vetjake, që të ravijëzoni hapësirën tuaj në universin Matrix.", + "Sent messages will be stored until your connection has returned.": "Mesazhet e dërguar do të depozitohen deri sa lidhja juaj të jetë rikthyer.", + "Server may be unavailable, overloaded, or the file too big": "Shërbyesi mund të jetë i pakapshëm, i mbingarkuar, ose kartela është shumë e madhe", + "Server may be unavailable, overloaded, or search timed out :(": "Shërbyesi mund të jetë i pakapshëm, i mbingarkuar, ose kërkimit i mbaroi koha :(", + "If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Nëse parashtruar një të metë përmes GitHub-it, regjistrat e diagnostikimit mund të na ndihmojnë të ndjekim problemin. Regjistrat e diagnostikimit përmbajnë të dhëna përdorimi, përfshi emrin tuaj të përdoruesit, ID-të ose aliaset e dhomave apo grupeve që keni vizituar dhe emrat e përdoruesve të përdoruesve të tjerë. Në to nuk përmbahen mesazhet.", + "Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.": "Privatësia është e rëndësishme për ne, ndaj nuk grumbullojmë ndonjë të dhënë personale apo të identifikueshme për analizat tona.", + "Learn more about how we use analytics.": "Mësoni më tepër se si i përdorim analizat.", + "Lazy loading is not supported by your current homeserver.": "Lazy loading nuk mbulohet nga shërbyesi juaj i tanishëm Home.", + "Reject all %(invitedRooms)s invites": "Mos prano asnjë ftesë për në %(invitedRooms)s", + "Missing Media Permissions, click here to request.": "Mungojnë Leje Mediash, klikoni këtu që të kërkohen.", + "New passwords must match each other.": "Fjalëkalimet e rinj duhet të përputhen me njëri-tjetrin.", + "Please note you are logging into the %(hs)s server, not matrix.org.": "Ju lutemi, kini parasysh se jeni futur te shërbyesi %(hs)s, jo te matrix.org.", + "Guest access is disabled on this Home Server.": "Në këtë shërbyes Home është çaktivizuar hyrja si vizitor", + "Password too short (min %(MIN_PASSWORD_LENGTH)s).": "Fjalëkalim shumë i shkurtër (minimumi %(MIN_PASSWORD_LENGTH)s)." } From e18dc4bcddb8409b20cb38d00cc664517e32757a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20C?= Date: Sat, 17 Nov 2018 08:29:58 +0000 Subject: [PATCH 024/237] Translated using Weblate (French) Currently translated at 100.0% (1281 of 1281 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/fr/ --- src/i18n/strings/fr.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index cdb8f78931..61fb430213 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -1302,5 +1302,10 @@ "Pin unread rooms to the top of the room list": "Épingler les salons non lus en haut de la liste des salons", "Pin rooms I'm mentioned in to the top of the room list": "Épingler les salons où l'on me mentionne en haut de la liste des salons", "If you would like to create a Matrix account you can register now.": "Si vous souhaitez créer un compte Matrix, vous pouvez vous inscrire maintenant.", - "You are currently using Riot anonymously as a guest.": "Vous utilisez Riot de façon anonyme en tant qu'invité." + "You are currently using Riot anonymously as a guest.": "Vous utilisez Riot de façon anonyme en tant qu'invité.", + "Please review and accept all of the homeserver's policies": "Veuillez lire et accepter toutes les polices du serveur d'accueil", + "To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of Riot to do this": "Pour éviter de perdre l'historique de vos discussions, vous devez exporter vos clés avant de vous déconnecter. Vous devez revenir à une version plus récente de Riot pour pouvoir le faire", + "You've previously used a newer version of Riot on %(host)s. To use this version again with end to end encryption, you will need to sign out and back in again. ": "Vous avez utilisé une version plus récente de Riot sur %(host)s. Pour utiliser à nouveau cette version avec le chiffrement de bout à bout, vous devez vous déconnecter et vous reconnecter. ", + "Incompatible Database": "Base de données incompatible", + "Continue With Encryption Disabled": "Continuer avec le chiffrement désactivé" } From 3ab8adc8e48d8224095f64096f918dcb5755f6b9 Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Sat, 17 Nov 2018 16:29:36 +0000 Subject: [PATCH 025/237] Translated using Weblate (Albanian) Currently translated at 97.9% (1255 of 1281 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/sq/ --- src/i18n/strings/sq.json | 101 ++++++++++++++++++++++++++++++++++----- 1 file changed, 88 insertions(+), 13 deletions(-) diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index 4faabfe5d7..b651861298 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -4,17 +4,17 @@ "Failed to verify email address: make sure you clicked the link in the email": "S’u arrit të verifikohej adresë email: sigurohuni se keni klikuar lidhjen te email-i", "The platform you're on": "Platforma ku gjendeni", "The version of Riot.im": "Versioni i Riot.im-it", - "Whether or not you're logged in (we don't record your user name)": "A je i lajmëruar apo jo (ne nuk do të inçizojmë emrin përdorues tëndë)", + "Whether or not you're logged in (we don't record your user name)": "Nëse jeni apo të futur në llogarinë tuaj (nuk e regjistrojmë emrin tuaj të përdoruesit)", "Your language of choice": "Gjuha juaj e zgjedhur", "Which officially provided instance you are using, if any": "Cilën instancë të furnizuar zyrtarish po përdorni, në pastë", - "Whether or not you're using the Richtext mode of the Rich Text Editor": "A je duke e përdorur mënyrën e tekstit të pasuruar të redaktionuesit të tekstit të pasuruar apo jo", + "Whether or not you're using the Richtext mode of the Rich Text Editor": "Nëse po përdorni apo jo mënyrën Richtext të Përpunuesit të Teksteve të Pasur", "Your homeserver's URL": "URL e Shërbyesit tuaj Home", "Your identity server's URL": "URL e shërbyesit tuaj të identiteteve", "Analytics": "Analiza", - "The information being sent to us to help make Riot.im better includes:": "Informacionet që dërgohen për t'i ndihmuar Riot.im-it të përmirësohet përmbajnë:", - "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Kur kjo faqe pëmban informacione që mund të të identifikojnë, sikur një dhomë, përdorues apo identifikatues grupi, këto të dhëna do të mënjanohen para se t‘i dërgohën një server-it.", + "The information being sent to us to help make Riot.im better includes:": "Të dhënat që na dërgohen për të na ndihmuar ta bëjmë më të mirë Riot.im-in përfshijnë:", + "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Kur kjo faqe përfshin të dhëna të identifikueshme, të tilla si një ID dhome përdoruesi apo grupi, këto të dhëna hiqen përpara se të dërgohet te shërbyesi.", "Call Failed": "Thirrja Dështoi", - "There are unknown devices in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.": "Pajisje të panjohura ndodhen në këtë dhomë: nësë vazhdon pa i vërtetuar, është e mundshme që dikush të jua përgjon thirrjen.", + "There are unknown devices in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.": "Në këtë dhomë ka pajisje të panjohura: nëse vazhdoni pa i verifikuar ato, për dikë do të jetë e mundur të përgjojë thirrjen tuaj.", "Review Devices": "Shqyrtoni Pajisje", "Call Anyway": "Thirre Sido Qoftë", "Answer Anyway": "Përgjigju Sido Qoftë", @@ -26,7 +26,7 @@ "Existing Call": "Thirrje Ekzistuese", "You are already in a call.": "Jeni tashmë në një thirrje.", "VoIP is unsupported": "VoIP nuk mbulohet", - "You cannot place VoIP calls in this browser.": "Thirrjet me VoIP nuk mbulohen nga ky kërkues uebi.", + "You cannot place VoIP calls in this browser.": "S’mund të bëni thirrje VoIP që nga ky shfletues.", "You cannot place a call with yourself.": "S’mund të bëni thirrje me vetveten.", "Conference calls are not supported in this client": "Thirrjet konference nuk mbulohen nga ky klienti", "Conference calls are not supported in encrypted rooms": "Thirrjet konference nuk mbulohen në dhoma të shifruara", @@ -35,7 +35,7 @@ "Failed to set up conference call": "Thirrja konference nuk mundi të realizohej", "Conference call failed.": "Thirrja konference dështoi.", "The file '%(fileName)s' failed to upload": "Dështoi ngarkimi i kartelës '%(fileName)s'", - "The file '%(fileName)s' exceeds this home server's size limit for uploads": "Fajli '%(fileName)s' tejkalon kufirin madhësie për mbartje e këtij server-i shtëpiak", + "The file '%(fileName)s' exceeds this home server's size limit for uploads": "Kartela '%(fileName)s' tejkalon kufirin e këtij shërbyesi Home për madhësinë e ngarkimeve", "Upload Failed": "Ngarkimi Dështoi", "Failure to create room": "S’u arrit të krijohej dhomë", "Server may be unavailable, overloaded, or you hit a bug.": "Shërbyesi mund të jetë i pakapshëm, i mbingarkuar, ose hasët një të metë.", @@ -52,12 +52,12 @@ "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s, %(day)s %(monthName)s %(fullYear)s", "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s, %(day)s %(monthName)s %(fullYear)s më %(time)s", "Who would you like to add to this community?": "Kë do të donit të shtonit te kjo bashkësi?", - "Warning: any person you add to a community will be publicly visible to anyone who knows the community ID": "Paralajmërim: se cili që e shton në një komunitet do t‘i doket se cilit që e di identifikatuesin e komunitetit", + "Warning: any person you add to a community will be publicly visible to anyone who knows the community ID": "Kujdes: cilido person që shtoni te një bashkësi do të jetë publikisht i dukshëm për cilindo që di ID-në e bashkësisë", "Invite new community members": "Ftoni anëtarë të rinj bashkësie", "Name or matrix ID": "Emër ose ID matrix-i", "Invite to Community": "Ftoni në Bashkësi", "Which rooms would you like to add to this community?": "Cilat dhoma do të donit të shtonit te kjo bashkësi?", - "Show these rooms to non-members on the community page and room list?": "A t‘i duken dhomat joanëtarëvë ne faqën komuniteti si dhe listën dhome?", + "Show these rooms to non-members on the community page and room list?": "T’u shfaqen këto dhoma te faqja e bashkësisë dhe lista e dhomave atyre që s’janë anëtarë?", "Add rooms to the community": "Shtoni dhoma te bashkësia", "Add to community": "Shtoje te kjo bashkësi", "Jan": "Jan", @@ -109,7 +109,7 @@ "Room %(roomId)s not visible": "Dhoma %(roomId)s s’është e dukshme", "Usage": "Përdorim", "/ddg is not a command": "/ddg s’është urdhër", - "To use it, just wait for autocomplete results to load and tab through them.": "Për të përdorur, thjesht prit derisa të mbushën rezultatat vetëplotësuese dhe pastaj shfletoji.", + "To use it, just wait for autocomplete results to load and tab through them.": "Për ta përdorur, thjesht pritni që të ngarkohen përfundimet e vetëplotësimit dhe shihini një nga një.", "Unrecognised room alias:": "Alias dhome jo i pranuar:", "Ignored user": "Përdorues i shpërfillur", "You are now ignoring %(userId)s": "Tani po e shpërfillni %(userId)s", @@ -1152,7 +1152,7 @@ "Drop here to tag %(section)s": "Hidheni këtu që të caktohet etiketë për %(section)s", "Press to start a chat with someone": "Shtypni që të nisni një bisedë me dikë", "No users have specific privileges in this room": "S’ka përdorues me privilegje të caktuara në këtë dhomë", - "Guests cannot join this room even if explicitly invited.": "Vizitorët s’mund të marrin pjesë në këtë edhe po të jenë ftuar shprehimisht", + "Guests cannot join this room even if explicitly invited.": "Vizitorët s’mund të marrin pjesë në këtë edhe po të jenë ftuar shprehimisht.", "Publish this room to the public in %(domain)s's room directory?": "Të bëhet publike kjo dhomë te drejtoria e dhomave %(domain)s?", "Click here to upgrade to the latest room version and ensure room integrity is protected.": "Klikoni këtu që ta përmirësoni me versionin më të ri të dhomë dhe të garantoni mbrojtjen e paprekshmërisë së dhomës.", "In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.": "Në dhoma të fshehtëzuara, si kjo, paraparja e URL-ve është e çaktivizuar, si parazgjedhje, për të garantuar që shërbyesi juaj home (ku edhe prodhohen paraparjet) të mos grumbullojë të dhëna rreth lidhjesh që shihni në këtë dhomë.", @@ -1183,6 +1183,81 @@ "Missing Media Permissions, click here to request.": "Mungojnë Leje Mediash, klikoni këtu që të kërkohen.", "New passwords must match each other.": "Fjalëkalimet e rinj duhet të përputhen me njëri-tjetrin.", "Please note you are logging into the %(hs)s server, not matrix.org.": "Ju lutemi, kini parasysh se jeni futur te shërbyesi %(hs)s, jo te matrix.org.", - "Guest access is disabled on this Home Server.": "Në këtë shërbyes Home është çaktivizuar hyrja si vizitor", - "Password too short (min %(MIN_PASSWORD_LENGTH)s).": "Fjalëkalim shumë i shkurtër (minimumi %(MIN_PASSWORD_LENGTH)s)." + "Guest access is disabled on this Home Server.": "Në këtë shërbyes Home është çaktivizuar hyrja si vizitor.", + "Password too short (min %(MIN_PASSWORD_LENGTH)s).": "Fjalëkalim shumë i shkurtër (minimumi %(MIN_PASSWORD_LENGTH)s).", + "You need to register to do this. Would you like to register now?": "Për ta bërë këtë, lypset të regjistroheni. Doni të regjistroheni që tani?", + "Stops ignoring a user, showing their messages going forward": "Resht shpërfilljen e një përdoruesi, duke i shfaqur mesazhet e tij të dërgohen", + "Verifies a user, device, and pubkey tuple": "Verifikon një përdorues, pajisje dhe një set kyçesh publikë", + "WARNING: Device already verified, but keys do NOT MATCH!": "KUJDES: Pajisje tashmë e verifikuar, por kyçet NUK PËRPUTHEN!", + "WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!": "KUJDES: VERIFIKIMI I KYÇIT DËSHTOI! Kyçi i nënshkrimit për %(userId)s dhe pajisjen %(deviceId)s është \"%(fprint)s\", që nuk përpythet me kyçin e dhënë \"%(fingerprint)s\". Kjo mund të jetë shenjë se komunikimet tuaja po përgjohen!", + "The signing key you provided matches the signing key you received from %(userId)s's device %(deviceId)s. Device marked as verified.": "Kyçi i nënshkrimit që dhatë përputhet me kyçin e nënshkrimit që morët nga pajisja e %(userId)s %(deviceId)s. Pajisja u shënua si e verifikuar.", + "Your browser does not support the required cryptography extensions": "Shfletuesi juaj nuk mbulon zgjerimet kriptografike të domosdoshme", + "Show timestamps in 12 hour format (e.g. 2:30pm)": "Vulat kohore shfaqi në formatin 12 orësh (p.sh. 2:30pm)", + "Enable inline URL previews by default": "Aktivizo, si parazgjedhje, paraparje URL-sh brendazi", + "Your home server does not support device management.": "Shërbyesi juaj Home nuk mbulon administrim pajisjesh.", + "The maximum permitted number of widgets have already been added to this room.": "Në këtë dhomë është shtuar tashmë numri maksimum i lejuar për widget-et.", + "Your key share request has been sent - please check your other devices for key share requests.": "Kërkesa juaj për shkëmbim kyçesh u dërgua - ju lutemi, kontrolloni pajisjet tuaja të tjera për kërkesa shkëmbimi kyçesh.", + "Undecryptable": "I pafshehtëzueshëm", + "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.": "S’do të jeni në gjendje ta zhbëni këtë, ngaqë po zhgradoni veten, nëse jeni përdoruesi i fundit i privilegjuar te dhoma do të jetë e pamundur të rifitoni privilegjet.", + "Jump to read receipt": "Hidhuni te leximi i faturës", + "Unable to reply": "S’arrihet të përgjigjet", + "Unknown for %(duration)s": "I panjohur për %(duration)s", + "You're not in any rooms yet! Press to make a room or to browse the directory": "S’jeni ende në ndonjë dhomë! Shtypni që të krijoni një dhomë ose që të shfletoni drejtorinë", + "Unable to ascertain that the address this invite was sent to matches one associated with your account.": "S’arrihet të sigurohet që adresa prej nga qe dërguar kjo ftesë përputhet me atë përshoqëruar llogarisë tuaj.", + "This invitation was sent to an email address which is not associated with this account:": "Kjo ftesë qe dërguar për një adresë email e cila nuk i përshoqërohet kësaj llogarie:", + "Would you like to accept or decline this invitation?": "Do të donit ta pranoni apo hidhni tej këtë ftesë?", + "To remove other users' messages, you must be a": "Që të hiqni mesazhe përdoruesish të tjerë, duhet të jeni një", + "This room is not accessible by remote Matrix servers": "Kjo dhomë nuk është e përdorshme nga shërbyes Matrix të largët", + "To send events of type , you must be a": "Që të dërgoni akte të llojit , duhet të jeni", + "This room version is vulnerable to malicious modification of room state.": "Ky version i dhomës është i cenueshëm nga modifikime dashakaqe të gjendjes së dhomës.", + "Stickerpack": "Paketë ngjitësish", + "You have enabled URL previews by default.": "E keni aktivizuar, si parazgjedhje, paraparjen e URL-ve.", + "You have disabled URL previews by default.": "E keni çaktivizuar, si parazgjedhje, paraparjen e URL-ve.", + "URL previews are enabled by default for participants in this room.": "Për pjesëmarrësit në këtë dhomë paraparja e URL-ve është e aktivizuar, si parazgjedhje.", + "URL previews are disabled by default for participants in this room.": "Për pjesëmarrësit në këtë dhomë paraparja e URL-ve është e çaktivizuar, si parazgjedhje.", + "When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.": "Kur dikush vë një URL në mesazh, për të dhënë rreth lidhjes më tepër të dhëna, të tilla si titulli, përshkrimi dhe një figurë e sajtit, do të shfaqet një paraparje e URL-së.", + "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?": "Ju ndan një hap nga shpënia te një sajt palë e tretë, që kështu të mund të mirëfilltësoni llogarinë tuaj me %(integrationsUrl)s. Doni të vazhdohet?", + "This allows you to use this app with an existing Matrix account on a different home server.": "Kjo ju lejon ta përdorni këtë aplikacion me një llogari Matrix ekxistuese në një shërbyes tjetër Home.", + "You can also set a custom identity server but this will typically prevent interaction with users based on email address.": "Mund të ujdisni edhe një shërbyes vetjak identitetesh, por kjo normalisht do të pengojë ndërveprim mes përdoruesish bazuar në adresë email.", + "The visibility of '%(roomName)s' in %(groupId)s could not be updated.": "Dukshmëria e '%(roomName)s' te %(groupId)s s’u përditësua dot.", + "Something went wrong when trying to get your communities.": "Diç shkoi ters teksa provohej të merreshin bashkësitë tuaja.", + "Warning: This widget might use cookies.": "Kujdes: Ky widget mund të përdorë cookies.", + "Unable to load event that was replied to, it either does not exist or you do not have permission to view it.": "S’arrihet të ngarkohet akti të cilit iu përgjigj, ose nuk ekziston, ose s’keni leje ta shihni.", + "Try using one of the following valid address types: %(validTypesList)s.": "Provoni të përdorni një nga llojet e vlefshme të adresave më poshtë: %(validTypesList)s.", + "You already have existing direct chats with this user:": "Keni tashmë fjalosje të drejtpërdrejta me këtë përdorues:", + "Something went wrong whilst creating your community": "Diç shkoi ters teksa krijohej bashkësia juaj", + "You've previously used Riot on %(host)s with lazy loading of members enabled. In this version lazy loading is disabled. As the local cache is not compatible between these two settings, Riot needs to resync your account.": "Më parë përdornit Riot në %(host)s me lazy loading anëtarësh të aktivizuar. Në këtë version lazy loading është çaktivizuar. Ngaqë fshehtina vendore s’është e përputhshme mes këtyre dy rregullimeve, Riot-i lyp të rinjëkohësohet llogaria juaj.", + "Upgrading this room requires closing down the current instance of the room and creating a new room it its place. To give room members the best possible experience, we will:": "Përmirësimi i kësaj dhome lyp mbylljen e instancës së tanishme të dhomës dhe krijimin në vend të saj të një dhome të re. Për t’u dhënë anëtareve të dhomës më të mirën e mundshme, do të:", + "Update any local room aliases to point to the new room": "Përditësoni çfarëdo aliasesh dhomash vendore që të shpien te dhoma e re", + "Stop users from speaking in the old version of the room, and post a message advising users to move to the new room": "Ndalojuni përdoruesve të flasin në versionin e vjetër të dhomës, dhe postoni një mesazh që u këshillon atyre të hidhen te dhoma e re", + "We encountered an error trying to restore your previous session.": "Hasëm një gabim teksa provohej të rikthehej sesioni juaj i dikurshëm.", + "This will allow you to reset your password and receive notifications.": "Kjo do t’ju lejojë të ricaktoni fjalëkalimin tuaj dhe të merrni njoftime.", + "This will be your account name on the homeserver, or you can pick a different server.": "Ky do të jetë emri i llogarisë tuaj te shërbyesi home, ose mund të zgjidhni një shërbyes tjetër.", + "You are currently using Riot anonymously as a guest.": "Hëpërhë po e përdorni Riot-in në mënyrë anonime, si një vizitor.", + "You are currently blacklisting unverified devices; to send messages to these devices you must verify them.": "Po kaloni në listë të zezë pajisje të paverifikuara; që të dërgoni mesazhe te këto pajisje, duhet t’i verifikoni.", + "We recommend you go through the verification process for each device to confirm they belong to their legitimate owner, but you can resend the message without verifying if you prefer.": "Këshillojmë të përshkoni procesin e verifikimit për çdo pajisje, që t’u bindur se u takojnë të zotëve të ligjshëm, por, nëse parapëlqeni, mund ta dërgoni mesazhin pa verifikuar gjë.", + "You must join the room to see its files": "Duhet të hyni në dhomë, pa të shihni kartelat e saj", + "The room '%(roomName)s' could not be removed from the summary.": "Dhoma '%(roomName)s' s’u hoq dot nga përmbledhja.", + "The user '%(displayName)s' could not be removed from the summary.": "Përdoruesi '%(displayName)s' s’u hoq dot nga përmbledhja.", + "You are an administrator of this community. You will not be able to rejoin without an invite from another administrator.": "Jeni një përgjegjës i kësaj bashkësie. S’do të jeni në gjendje të rihyni pa një ftesë nga një tjetër përgjegjës.", + "These rooms are displayed to community members on the community page. Community members can join the rooms by clicking on them.": "Këto dhoma u shfaqen anëtarëve të bashkësisë te faqja e bashkësisë. Anëtarët e bashkësisë mund të marrin pjesë në dhoma duke klikuar mbi to.", + "Your community hasn't got a Long Description, a HTML page to show to community members.
    Click here to open settings and give it one!": "Bashkësia juaj s’ka ndonjë Përshkrim të Gjatë, një faqe HTML për t’ua shfaqur anëtarëve të bashkësisë.
    Klikoni këtu që të hapni rregullimet dhe t’i krijoni një të tillë!", + "This room is not public. You will not be able to rejoin without an invite.": "Kjo dhomë s’është publike. S’do të jeni në gjendje të rihyni në të pa një ftesë.", + "This room is used for important messages from the Homeserver, so you cannot leave it.": "Kjo dhomë përdoret për mesazhe të rëndësishëm nga shërbyesi Home, ndaj s’mund ta braktisni.", + "To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "Që të ndërtoni një filtër, tërhiqeni avatarin e një bashkësie te paneli i filtrimeve në skajin e majtë të ekranit. Për të parë vetëm dhomat dhe personat e përshoqëruar asaj bashkësie, mund të klikoni në çfarëdo kohe mbi një avatar te panelit të filtrimeve.", + "You can't send any messages until you review and agree to our terms and conditions.": "S’mund të dërgoni ndonjë mesazh, përpara se të shqyrtoni dhe pajtoheni me termat dhe kushtet tona.", + "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please contact your service administrator to continue using the service.": "Mesazhi juaj s’u dërgua, ngaqë ky shërbyes Home ka mbërritur në Kufirin Mujor të Përdoruesve Aktivë. Ju lutemi, që të vazhdoni ta përdorni këtë shërbim, lidhuni me përgjegjësin e shërbimit tuaj.", + "Your message wasn't sent because this homeserver has exceeded a resource limit. Please contact your service administrator to continue using the service.": "Mesazhi juaj s’u dërgua, ngaqë ky shërbyes Home ka tejkaluar kufirin e një burimi. Ju lutemi, që të vazhdoni ta përdorni këtë shërbim, lidhuni me përgjegjësin e shërbimit tuaj.", + "There's no one else here! Would you like to invite others or stop warning about the empty room?": "S’ka njeri këtu! Do të donit të ftoni të tjerë apo të reshtet së njoftuari për dhomë të zbrazët?", + "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "U provua të ngarkohej një pikë e caktuar në kronologjinë e kësaj dhome, por nuk keni leje për ta parë mesazhin në fjalë.", + "Your password was successfully changed. You will not receive push notifications on other devices until you log back in to them": "Fjalëkalimi juaj u ndryshua me sukses. Nuk do të merrni njoftime push në pajisjet tuaja të tjera, veç në hyfshi sërish në llogarinë tuaj në to", + "Start automatically after system login": "Nisu vetvetiu pas hyrjes në sistem", + "You may need to manually permit Riot to access your microphone/webcam": "Lypset të lejoni dorazi Riot-in të përdorë mikrofonin/kamerën tuaj web", + "No Audio Outputs detected": "S’u pikasën Sinjale Audio Në Dalje", + "Resetting password will currently reset any end-to-end encryption keys on all devices, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "Ricaktimi i fjalëkalimit do të shkaktojë në fakt edhe zerimin e çfarëdo kyçi fshehtëzimesh skaj-më-skaj në krejt pajisjet, duke e bërë kështu të palexueshëm historikun e bisedës së fshehtëzuar, veç në paçi eksportuar më parë kyçet e dhomës tuaj dhe i rim-importoni më pas. Në të ardhmen kjo punë do të përmirësohet.", + "You have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, sign in again on each device": "Jeni nxjerrë jashtë krejt pajisjeve dhe nuk do të merrni më njoftime push. Që të riaktivizoni njoftimet, bëni sërish hyrjen në çdo pajisje", + "This Home Server does not support login using email address.": "Ky shërbyes Home nuk mbulon hyrje përmes adresash email.", + "This homeserver doesn't offer any login flows which are supported by this client.": "Ky shërbyes home nuk ofron ndonjë mënyrë hyrjesh që mbulohet nga ky klient.", + "Unable to query for supported registration methods": "S’arrihet të kërkohet për metoda regjistrimi që mbulohen", + "The exported file will allow anyone who can read it to decrypt any encrypted messages that you can see, so you should be careful to keep it secure. To help with this, you should enter a passphrase below, which will be used to encrypt the exported data. It will only be possible to import the data by using the same passphrase.": "Kartela e eksportuar do t’i lejojë kujtdo që e lexon të shfshehtëzojë çfarëdo mesazhesh të fshehtëzuar që mund të shihni, ndaj duhet të jeni i kujdesshëm për ta mbajtur të parrezikuar. Si ndihmë për këtë, duhet të jepni më poshtë një frazëkalim, që do të përdoret për të fshehtëzuar të dhënat e eksportuara. Importimi i të dhënave do të jetë i mundur vetëm duke përdorur të njëjtin frazëkalim." } From 9697d759c80386ed8249aeafe285a22589f41c19 Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Sat, 17 Nov 2018 17:11:56 +0000 Subject: [PATCH 026/237] Translated using Weblate (Albanian) Currently translated at 99.2% (1272 of 1281 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/sq/ --- src/i18n/strings/sq.json | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index b651861298..d29b781de6 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -1118,7 +1118,7 @@ "Block users on other matrix homeservers from joining this room": "Bllokoju hyrjen në këtë dhomë përdoruesve në shërbyes të tjerë Matrix home", "Create a new room with the same name, description and avatar": "Krijoni një dhomë të re me po atë emër, përshkrim dhe avatar", "\"%(RoomName)s\" contains devices that you haven't seen before.": "\"%(RoomName)s\" përmban pajisje që s’i keni parë më parë.", - "

    HTML for your community's page

    \n

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

    \n

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

    \n": "

    HTML për faqen e bashkësisë tuaj

    \n

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

    \n

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

    \n", + "

    HTML for your community's page

    \n

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

    \n

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

    \n": "

    HTML për faqen e bashkësisë tuaj

    \n

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

    \n

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

    \n", "Failed to add the following rooms to the summary of %(groupId)s:": "S’u arrit të shtoheshin dhomat vijuese te përmbledhja e %(groupId)s:", "Failed to remove the room from the summary of %(groupId)s": "S’u arrit të hiqej dhoma prej përmbledhjes së %(groupId)s", "Failed to remove a user from the summary of %(groupId)s": "S’u arrit të hiqej një përdorues nga përmbledhja e %(groupId)s", @@ -1259,5 +1259,22 @@ "This Home Server does not support login using email address.": "Ky shërbyes Home nuk mbulon hyrje përmes adresash email.", "This homeserver doesn't offer any login flows which are supported by this client.": "Ky shërbyes home nuk ofron ndonjë mënyrë hyrjesh që mbulohet nga ky klient.", "Unable to query for supported registration methods": "S’arrihet të kërkohet për metoda regjistrimi që mbulohen", - "The exported file will allow anyone who can read it to decrypt any encrypted messages that you can see, so you should be careful to keep it secure. To help with this, you should enter a passphrase below, which will be used to encrypt the exported data. It will only be possible to import the data by using the same passphrase.": "Kartela e eksportuar do t’i lejojë kujtdo që e lexon të shfshehtëzojë çfarëdo mesazhesh të fshehtëzuar që mund të shihni, ndaj duhet të jeni i kujdesshëm për ta mbajtur të parrezikuar. Si ndihmë për këtë, duhet të jepni më poshtë një frazëkalim, që do të përdoret për të fshehtëzuar të dhënat e eksportuara. Importimi i të dhënave do të jetë i mundur vetëm duke përdorur të njëjtin frazëkalim." + "The exported file will allow anyone who can read it to decrypt any encrypted messages that you can see, so you should be careful to keep it secure. To help with this, you should enter a passphrase below, which will be used to encrypt the exported data. It will only be possible to import the data by using the same passphrase.": "Kartela e eksportuar do t’i lejojë kujtdo që e lexon të shfshehtëzojë çfarëdo mesazhesh të fshehtëzuar që mund të shihni, ndaj duhet të jeni i kujdesshëm për ta mbajtur të parrezikuar. Si ndihmë për këtë, duhet të jepni më poshtë një frazëkalim, që do të përdoret për të fshehtëzuar të dhënat e eksportuara. Importimi i të dhënave do të jetë i mundur vetëm duke përdorur të njëjtin frazëkalim.", + "Not a valid Riot keyfile": "S’është kartelë kyçesh Riot e vlefshme", + "Revoke Moderator": "Shfuqizoje Si Moderator", + "You have no historical rooms": "S’keni dhoma të dikurshme", + "Historical": "Të dikurshme", + "Flair": "Simbole", + "Showing flair for these communities:": "Shfaqen simbole për këto bashkësi:", + "This room is not showing flair for any communities": "Kjo dhomë nuk shfaq simbole për ndonjë bashkësi", + "Robot check is currently unavailable on desktop - please use a web browser": "Kontrolli për robot hëpërhë s’është i përdorshëm në desktop - ju lutemi, përdorni një shfletues", + "Please review and accept all of the homeserver's policies": "Ju lutemi, shqyrtoni dhe pranoni krejt rregullat e këtij shërbyesi home", + "Flair will appear if enabled in room settings": "Simbolet do të shfaqen nëse aktivizohen te rregullimet e dhomës", + "Flair will not appear": "Simbolet nuk do të shfaqen", + "Display your community flair in rooms configured to show it.": "Shfaqni simbolet e bashkësisë tuaj në dhoma të formësuara për t’i shfaqur ato.", + "Are you sure you wish to remove (delete) this event? Note that if you delete a room name or topic change, it could undo the change.": "Jeni i sigurt se doni të hiqet (fshihet) ky akt? Mbani parasysh se nëse fshini emrin e një dhome ose ndryshimin e temës, kjo mund të sjellë zhbërjen e ndryshimit.", + "To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of Riot to do this": "Që të shmanget humbja e historikut të fjalosjes tuaj, duhet të eksportoni kyçet e dhomës tuaj përpara se të dilni nga llogari. Që ta bëni këtë, duhe të riktheheni te versioni më i ri i Riot-it", + "You've previously used a newer version of Riot on %(host)s. To use this version again with end to end encryption, you will need to sign out and back in again. ": "Më parë përdorët një version më të ri të Riot-it në %(host)s. Që ta përdorni sërish këtë version me fshehtëzim skaj-më-skaj, duhet të dilni dhe rihyni te llogaria juaj. ", + "Incompatible Database": "Bazë të dhënash e Papërputhshme", + "Continue With Encryption Disabled": "Vazhdo Me Fshehtëzimin të Çaktivizuar" } From dbb8de50d0b59eb518465f13d4562fcea070d243 Mon Sep 17 00:00:00 2001 From: Szimszon Date: Sat, 17 Nov 2018 19:00:08 +0000 Subject: [PATCH 027/237] Translated using Weblate (Hungarian) Currently translated at 100.0% (1281 of 1281 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/hu/ --- src/i18n/strings/hu.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index 9d0589bb17..f5ce9ea7c7 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -1302,5 +1302,10 @@ "Pin unread rooms to the top of the room list": "Nem olvasott üzeneteket tartalmazó szobák a szobalista elejére", "Pin rooms I'm mentioned in to the top of the room list": "Megemlítéseket tartalmazó szobák a szobalista elejére", "If you would like to create a Matrix account you can register now.": "Ha létre szeretnél hozni egy Matrix fiókot most regisztrálhatsz.", - "You are currently using Riot anonymously as a guest.": "A Riotot ismeretlen vendégként használod." + "You are currently using Riot anonymously as a guest.": "A Riotot ismeretlen vendégként használod.", + "Please review and accept all of the homeserver's policies": "Kérlek nézd át és fogadd el a Matrix szerver felhasználási feltételeit", + "To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of Riot to do this": "Hogy a régi üzenetekhez továbbra is hozzáférhess kijelentkezés előtt ki kell mentened a szobák titkosító kulcsait. Ehhez a Riot egy frissebb verzióját kell használnod", + "You've previously used a newer version of Riot on %(host)s. To use this version again with end to end encryption, you will need to sign out and back in again. ": "Előzőleg a Riot egy frissebb verzióját használtad itt: %(host)s. Ki-, és vissza kell jelentkezned, hogy megint ezt a verziót használhasd végponttól végpontig titkosításhoz. ", + "Incompatible Database": "Nem kompatibilis adatbázis", + "Continue With Encryption Disabled": "Folytatás a titkosítás kikapcsolásával" } From 414ff55fe69313a67c83a24f2cd916fd35ce2550 Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Sat, 17 Nov 2018 17:28:20 +0000 Subject: [PATCH 028/237] Translated using Weblate (Albanian) Currently translated at 99.0% (1269 of 1281 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/sq/ --- src/i18n/strings/sq.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index d29b781de6..0f08634323 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -159,7 +159,7 @@ "Room not found": "Dhoma s’u gjet", "Downloading update...": "Po shkarkohet përditësim…", "Messages in one-to-one chats": "Mesazhe në fjalosje tek për tek", - "Unavailable": "S’kapet", + "Unavailable": "", "View Decrypted Source": "Shihni Burim të Shfshehtëzuar", "Failed to update keywords": "S’u arrit të përditësoheshin fjalëkyçe", "Notes:": "Shënime:", @@ -235,7 +235,7 @@ "Call invitation": "Ftesë për thirrje", "Thank you!": "Faleminderit!", "Messages containing my display name": "Mesazhe që përmbajnë emrin tim të ekranit", - "State Key": "Kyç Gjendjeje", + "State Key": "", "Failed to send custom event.": "S’u arrit të dërgohet akt vetjak.", "What's new?": "Ç’ka të re?", "Notify me for anything else": "Njoftomë për gjithçka tjetër", @@ -295,7 +295,7 @@ "With your current browser, the look and feel of the application may be completely incorrect, and some or all features may not function. If you want to try it anyway you can continue, but you are on your own in terms of any issues you may encounter!": "Me shfletuesin tuaj të tanishëm, pamja dhe ndjesitë nga aplikacioni mund të jenë plotësisht të pasakta, dhe disa nga ose krejt veçoritë të mos funksionojnë. Nëse doni ta provoni sido qoftë, mund të vazhdoni, por mos u ankoni për çfarëdo problemesh që mund të hasni!", "Checking for an update...": "Po kontrollohet për një përditësim…", "There are advanced notifications which are not shown here": "Ka njoftime të thelluara që nuk shfaqen këtu", - "Show empty room list headings": "Shfaqi emrat e listave të zbrazëta dhomash", + "Show empty room list headings": "", "PM": "PM", "AM": "AM", "Room name or alias": "Emër dhome ose alias", @@ -1126,7 +1126,7 @@ "Can't leave Server Notices room": "Dhoma Njoftime Shërbyesi, s’braktiset dot", "For security, this session has been signed out. Please sign in again.": "Për hir të sigurisë, është bërë dalja nga ky sesion. Ju lutemi, ribëni hyrjen.", "Data from an older version of Riot has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.": "Janë pikasur të dhëna nga një version i dikurshëm i Riot-it. Kjo do të bëjë që kriptografia skaj-më-skaj te versioni i dikurshëm të mos punojë si duhet. Mesazhet e fshehtëzuar skaj-më-skaj tani së fundi teksa përdorej versioni i dikurshëm mund të mos jenë të shfshehtëzueshëm në këtë version. Kjo mund bëjë edhe që mesazhet e shkëmbyera me këtë version të dështojnë. Nëse ju dalin probleme, bëni daljen dhe rihyni në llogari. Që të ruhet historiku i mesazheve, eksportoni dhe ri-importoni kyçet tuaj.", - "Did you know: you can use communities to filter your Riot.im experience!": "E dinit se: mund t’i përdorni bashkësitë për të filtruar punimin tuaj në Riot.im?!?", + "Did you know: you can use communities to filter your Riot.im experience!": "E dinit se: mund t’i përdorni bashkësitë për të filtruar punimin tuaj në Riot.im?", "Error whilst fetching joined communities": "Gabim teksa silleshin bashkësitë ku merret pjesë", "Show devices, send anyway or cancel.": "Shfaq pajisje, dërgoje sido qoftë ose anuloje.", "%(count)s Resend all or cancel all now. You can also select individual messages to resend or cancel.|other": "Ridërgojini krejt ose anulojini krejt tani. Për ridërgim ose anulim, mundeni edhe të përzgjidhni mesazhe individualë.", From 4e18635840c5698b6d6a6b8925bc4b773e1f0cdd Mon Sep 17 00:00:00 2001 From: Szimszon Date: Sat, 17 Nov 2018 19:00:30 +0000 Subject: [PATCH 029/237] Translated using Weblate (Hungarian) Currently translated at 100.0% (1281 of 1281 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/hu/ --- src/i18n/strings/hu.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index f5ce9ea7c7..056b4186ed 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -1307,5 +1307,6 @@ "To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of Riot to do this": "Hogy a régi üzenetekhez továbbra is hozzáférhess kijelentkezés előtt ki kell mentened a szobák titkosító kulcsait. Ehhez a Riot egy frissebb verzióját kell használnod", "You've previously used a newer version of Riot on %(host)s. To use this version again with end to end encryption, you will need to sign out and back in again. ": "Előzőleg a Riot egy frissebb verzióját használtad itt: %(host)s. Ki-, és vissza kell jelentkezned, hogy megint ezt a verziót használhasd végponttól végpontig titkosításhoz. ", "Incompatible Database": "Nem kompatibilis adatbázis", - "Continue With Encryption Disabled": "Folytatás a titkosítás kikapcsolásával" + "Continue With Encryption Disabled": "Folytatás a titkosítás kikapcsolásával", + "Sign in with single sign-on": "Bejelentkezés „egyszeri bejelentkezéssel”" } From 3a91f003353406049861406ba9253896e6af14d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20C?= Date: Sat, 17 Nov 2018 08:30:11 +0000 Subject: [PATCH 030/237] Translated using Weblate (French) Currently translated at 100.0% (1281 of 1281 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/fr/ --- src/i18n/strings/fr.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index 61fb430213..602f8a25d2 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -1307,5 +1307,6 @@ "To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of Riot to do this": "Pour éviter de perdre l'historique de vos discussions, vous devez exporter vos clés avant de vous déconnecter. Vous devez revenir à une version plus récente de Riot pour pouvoir le faire", "You've previously used a newer version of Riot on %(host)s. To use this version again with end to end encryption, you will need to sign out and back in again. ": "Vous avez utilisé une version plus récente de Riot sur %(host)s. Pour utiliser à nouveau cette version avec le chiffrement de bout à bout, vous devez vous déconnecter et vous reconnecter. ", "Incompatible Database": "Base de données incompatible", - "Continue With Encryption Disabled": "Continuer avec le chiffrement désactivé" + "Continue With Encryption Disabled": "Continuer avec le chiffrement désactivé", + "Sign in with single sign-on": "Se connecter avec l'authentification unique" } From d324c31027c0563ae4e796374ccee23bfbdd26d7 Mon Sep 17 00:00:00 2001 From: Szimszon Date: Wed, 21 Nov 2018 21:07:36 +0000 Subject: [PATCH 031/237] Translated using Weblate (Hungarian) Currently translated at 100.0% (1350 of 1350 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/hu/ --- src/i18n/strings/hu.json | 71 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index 056b4186ed..3bf4da30cb 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -1308,5 +1308,74 @@ "You've previously used a newer version of Riot on %(host)s. To use this version again with end to end encryption, you will need to sign out and back in again. ": "Előzőleg a Riot egy frissebb verzióját használtad itt: %(host)s. Ki-, és vissza kell jelentkezned, hogy megint ezt a verziót használhasd végponttól végpontig titkosításhoz. ", "Incompatible Database": "Nem kompatibilis adatbázis", "Continue With Encryption Disabled": "Folytatás a titkosítás kikapcsolásával", - "Sign in with single sign-on": "Bejelentkezés „egyszeri bejelentkezéssel”" + "Sign in with single sign-on": "Bejelentkezés „egyszeri bejelentkezéssel”", + "Unable to load! Check your network connectivity and try again.": "A betöltés sikertelen! Ellenőrizd a hálózati kapcsolatot és próbáld újra.", + "Backup of encryption keys to server": "Titkosítási kulcsok mentése a szerverre", + "Delete Backup": "Mentés törlése", + "Delete your backed up encryption keys from the server? You will no longer be able to use your recovery key to read encrypted message history": "Törlöd az elmentett titkosítási kulcsokat a szerverről? Később nem tudod használni helyreállítási kulcsot a régi titkosított üzenetek elolvasásához", + "Delete backup": "Mentés törlése", + "Unable to load key backup status": "A mentett kulcsok állapotát nem lehet lekérdezni", + "This device is uploading keys to this backup": "Ez az eszköz kulcsokat tölt fel ebbe a mentésbe", + "This device is not uploading keys to this backup": "Ez az eszköz nem tölt fel kulcsokat ebbe a mentésbe", + "Backup has a valid signature from this device": "A mentés érvényes aláírást tartalmaz az eszközről", + "Backup has a valid signature from verified device x": "A mentés érvényes aláírást tartalmaz erről az ellenőrzött eszközről: x", + "Backup has a valid signature from unverified device ": "A mentés érvényes aláírást tartalmaz erről az ellenőrizetlen eszközről: ", + "Backup has an invalid signature from verified device ": "A mentés érvénytelen aláírást tartalmaz erről az ellenőrzött eszközről: ", + "Backup has an invalid signature from unverified device ": "A mentés érvénytelen aláírást tartalmaz erről az ellenőrizetlen eszközről: ", + "Backup is not signed by any of your devices": "A mentés nincs aláírva egyetlen eszközöd által sem", + "Backup version: ": "Mentés verzió: ", + "Algorithm: ": "Algoritmus: ", + "Restore backup": "Mentés visszaállítása", + "No backup is present": "Mentés nem található", + "Start a new backup": "Új mentés indítása", + "Secure your encrypted message history with a Recovery Passphrase.": "Helyezd biztonságba a titkosított üzenetek olvasásának a lehetőségét a Helyreállítási jelmondattal.", + "You'll need it if you log out or lose access to this device.": "Szükséged lesz rá ha kijelentkezel vagy nem férsz többé hozzá az eszközödhöz.", + "Enter a passphrase...": "Add meg a jelmondatot...", + "Next": "Következő", + "If you don't want encrypted message history to be availble on other devices, .": "Ha nincs szükséged a régi titkosított üzenetekre más eszközön, .", + "Or, if you don't want to create a Recovery Passphrase, skip this step and .": "Vagy, ha nem szeretnél Helyreállítási jelmondatot megadni, hagyd ki ezt a lépést és .", + "That matches!": "Egyeznek!", + "That doesn't match.": "Nem egyeznek.", + "Go back to set it again.": "Lépj vissza és állítsd be újra.", + "Type in your Recovery Passphrase to confirm you remember it. If it helps, add it to your password manager or store it somewhere safe.": "Add meg a Helyreállítási jelmondatot, hogy bizonyítsd, hogy emlékszel rá. Ha az segít írd be a jelszó menedzseredbe vagy tárold más biztonságos helyen.", + "Repeat your passphrase...": "Ismételd meg a jelmondatot...", + "Make a copy of this Recovery Key and keep it safe.": "Készíts másolatot a Helyreállítási kulcsból és tárold biztonságos helyen.", + "As a safety net, you can use it to restore your encrypted message history if you forget your Recovery Passphrase.": "Mint egy biztonsági háló, ha elfelejted a Helyreállítási jelmondatot felhasználhatod, hogy hozzáférj a régi titkosított üzeneteidhez.", + "Your Recovery Key": "A Helyreállítási kulcsod", + "Copy to clipboard": "Másolás a vágólapra", + "Download": "Letölt", + "I've made a copy": "Készítettem másolatot", + "Your Recovery Key has been copied to your clipboard, paste it to:": "A Helyreállítási kulcsod a vágólapra lett másolva, beillesztés ide:", + "Your Recovery Key is in your Downloads folder.": "A Helyreállítási kulcs a Letöltések mappádban van.", + "Print it and store it somewhere safe": "Nyomtad ki és tárold biztonságos helyen", + "Save it on a USB key or backup drive": "Mentsd el egy Pendrive-ra vagy a biztonsági mentésekhez", + "Copy it to your personal cloud storage": "Másold fel a személyes felhődbe", + "Got it": "Értem", + "Backup created": "Mentés elkészült", + "Your encryption keys are now being backed up to your Homeserver.": "A titkosítási kulcsaid a Matrix szervereden vannak elmentve.", + "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another device.": "A Biztonságos Üzenet Visszaállítás beállítása nélkül ha kijelentkezel vagy másik eszközt használsz, akkor nem tudod visszaállítani a régi titkosított üzeneteidet.", + "Set up Secure Message Recovery": "Biztonságos Üzenet Visszaállítás beállítása", + "Create a Recovery Passphrase": "Helyreállítási jelmondat megadása", + "Confirm Recovery Passphrase": "Helyreállítási jelmondat megerősítése", + "Recovery Key": "Helyreállítási kulcs", + "Keep it safe": "Tartsd biztonságban", + "Backing up...": "Mentés...", + "Create Key Backup": "Kulcs mentés készítése", + "Unable to create key backup": "Kulcs mentés sikertelen", + "Retry": "Újra", + "Unable to load backup status": "A mentés állapotát nem lehet lekérdezni", + "Unable to restore backup": "A mentést nem lehet visszaállítani", + "No backup found!": "Mentés nem található!", + "Backup Restored": "Mentés visszaállítva", + "Failed to decrypt %(failedCount)s sessions!": "%(failedCount)s kapcsolatot nem lehet visszafejteni!", + "Restored %(sessionCount)s session keys": "%(sessionCount)s kapcsolati kulcsok visszaállítva", + "Enter Recovery Passphrase": "Add meg a Helyreállítási jelmondatot", + "Access your secure message history and set up secure messaging by entering your recovery passphrase.": "A helyreállítási jelmondattal hozzáférsz a régi titkosított üzeneteidhez és beállíthatod a biztonságos üzenetküldést.", + "If you've forgotten your recovery passphrase you can use your recovery key or set up new recovery options": "Ha elfelejtetted a helyreállítási jelmondatodat használhatod a helyreállítási kulcsodat vagy új helyreállítási paramétereket állíthatsz be", + "Enter Recovery Key": "Add meg a Helyreállítási kulcsot", + "This looks like a valid recovery key!": "Ez érvényes helyreállítási kulcsnak tűnik!", + "Not a valid recovery key": "Nem helyreállítási kulcs", + "Access your secure message history and set up secure messaging by entering your recovery key.": "A helyreállítási kulcs megadásával hozzáférhetsz a régi biztonságos üzeneteidhez és beállíthatod a biztonságos üzenetküldést.", + "If you've forgotten your recovery passphrase you can ": "Ha elfelejtetted a helyreállítási jelmondatot ", + "Key Backup": "Kulcs mentés" } From 4cfefe4c3c58bca6a19ed12b06fa8df86fcd875d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 21 Nov 2018 14:13:56 -0700 Subject: [PATCH 032/237] Introduce an onUsernameBlur and fix hostname parsing --- src/components/structures/login/Login.js | 20 +++++++++++++++++--- src/components/views/login/PasswordLogin.js | 5 +++-- src/i18n/strings/en_EN.json | 1 + 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index 67d9bb7d38..fae7cc37df 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -232,11 +232,24 @@ module.exports = React.createClass({ }).done(); }, - onUsernameChanged: function(username, endOfInput) { + onUsernameChanged: function(username) { this.setState({ username: username }); - if (username[0] === "@" && endOfInput) { + }, + + onUsernameBlur: function(username) { + this.setState({ username: username }); + if (username[0] === "@") { const serverName = username.split(':').slice(1).join(':'); - this._tryWellKnownDiscovery(serverName); + try { + // we have to append 'https://' to make the URL constructor happy + // otherwise we get things like 'protocol: matrix.org, pathname: 8448' + const url = new URL("https://" + serverName); + this._tryWellKnownDiscovery(url.hostname); + } catch (e) { + console.error("Problem parsing URL or unhandled error doing .well-known discovery"); + console.error(e); + this.setState({discoveryError: _t("Failed to perform homeserver discovery")}); + } } }, @@ -531,6 +544,7 @@ module.exports = React.createClass({ initialPhoneCountry={this.state.phoneCountry} initialPhoneNumber={this.state.phoneNumber} onUsernameChanged={this.onUsernameChanged} + onUsernameBlur={this.onUsernameBlur} onPhoneCountryChanged={this.onPhoneCountryChanged} onPhoneNumberChanged={this.onPhoneNumberChanged} onForgotPasswordClick={this.props.onForgotPasswordClick} diff --git a/src/components/views/login/PasswordLogin.js b/src/components/views/login/PasswordLogin.js index 0e3aed1187..6a5577fb62 100644 --- a/src/components/views/login/PasswordLogin.js +++ b/src/components/views/login/PasswordLogin.js @@ -30,6 +30,7 @@ class PasswordLogin extends React.Component { static defaultProps = { onError: function() {}, onUsernameChanged: function() {}, + onUsernameBlur: function() {}, onPasswordChanged: function() {}, onPhoneCountryChanged: function() {}, onPhoneNumberChanged: function() {}, @@ -122,11 +123,11 @@ class PasswordLogin extends React.Component { onUsernameChanged(ev) { this.setState({username: ev.target.value}); - this.props.onUsernameChanged(ev.target.value, false); + this.props.onUsernameChanged(ev.target.value); } onUsernameBlur(ev) { - this.props.onUsernameChanged(this.state.username, true); + this.props.onUsernameBlur(this.state.username); } onLoginTypeChange(loginType) { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index eb0a7fb1db..f9980f5645 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1288,6 +1288,7 @@ "Incorrect username and/or password.": "Incorrect username and/or password.", "Please note you are logging into the %(hs)s server, not matrix.org.": "Please note you are logging into the %(hs)s server, not matrix.org.", "Guest access is disabled on this Home Server.": "Guest access is disabled on this Home Server.", + "Failed to perform homeserver discovery": "Failed to perform homeserver discovery", "The phone number entered looks invalid": "The phone number entered looks invalid", "Invalid homeserver discovery response": "Invalid homeserver discovery response", "Cannot find homeserver": "Cannot find homeserver", From 5600a9d02adc61f9420234a4090d9342743443db Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 21 Nov 2018 14:14:08 -0700 Subject: [PATCH 033/237] Validate the identity server --- src/components/structures/login/Login.js | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index fae7cc37df..6002f058f4 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -115,11 +115,6 @@ module.exports = React.createClass({ // discovery went wrong if (this.state.discoveryError) return; - if (this.state.discoveredHsUrl) { - console.log("Rewriting username because the homeserver was discovered"); - username = username.substring(1).split(":")[0]; - } - this.setState({ busy: true, errorText: null, @@ -327,16 +322,13 @@ module.exports = React.createClass({ return; } - // XXX: We don't verify the identity server URL because sydent doesn't register - // the route we need. - - // console.log("Verifying identity server URL: " + isUrl); - // const isResponse = await this._getWellKnownObject(`${isUrl}/_matrix/identity/api/v1`); - // if (!isResponse) { - // console.error("Invalid /api/v1 response"); - // this.setState({discoveryError: _t("Invalid homeserver discovery response")}); - // return; - // } + console.log("Verifying identity server URL: " + isUrl); + const isResponse = await this._getWellKnownObject(`${isUrl}/_matrix/identity/api/v1`); + if (!isResponse) { + console.error("Invalid /api/v1 response"); + this.setState({discoveryError: _t("Invalid homeserver discovery response")}); + return; + } } this.setState({discoveredHsUrl: hsUrl, discoveredIsUrl: isUrl, discoveryError: ""}); From 5810f596b85773af6b4e456e7d9d3279341b816d Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Thu, 22 Nov 2018 08:41:14 +0000 Subject: [PATCH 034/237] Translated using Weblate (Albanian) Currently translated at 99.1% (1339 of 1350 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/sq/ --- src/i18n/strings/sq.json | 77 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 74 insertions(+), 3 deletions(-) diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index 0f08634323..24809fb332 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -962,7 +962,7 @@ "Call in Progress": "Thirrje në Kryerje e Sipër", "A call is already in progress!": "Ka tashmë një thirrje në kryerje e sipër!", "%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s ndryshoi emrin e tij në ekran si %(displayName)s.", - "%(senderName)s set the main address for this room to %(address)s.": "%(senderName)s caktoi %(displayName)s si adresë kryesore për këtë dhomë.", + "%(senderName)s set the main address for this room to %(address)s.": "%(senderName)s caktoi %(address)s si adresë kryesore për këtë dhomë.", "%(widgetName)s widget modified by %(senderName)s": "Widget-i %(widgetName)s u modifikua nga %(senderName)s", "%(widgetName)s widget added by %(senderName)s": "Widget-i %(widgetName)s u shtua nga %(senderName)s", "Always show encryption icons": "Shfaq përherë ikona fshehtëzimi", @@ -1016,7 +1016,7 @@ "The phone number field must not be blank.": "Fusha numër telefoni s’duhet të jetë e zbrazët.", "The password field must not be blank.": "Fusha fjalëkalim s’duhet të jetë e zbrazët.", "Yes, I want to help!": "Po, dua të ndihmoj!", - "This homeserver has hit its Monthly Active User limit so some users will not be able to log in.": "Ky shërbyes home ka tejkaluar kufirin e vet Përdorues Aktivë Mujorë, ndaj disa përdorues s’do të jenë në gjendje të bëjnë hyrjen.", + "This homeserver has hit its Monthly Active User limit so some users will not be able to log in.": "Ky shërbyes home ka tejkaluar kufirin e vet të Përdoruesve Aktivë Mujorë, ndaj disa përdorues s’do të jenë në gjendje të bëjnë hyrjen.", "This homeserver has exceeded one of its resource limits so some users will not be able to log in.": "Ky shërbyes home ka tejkaluar një nga kufijtë mbi burimet, ndaj disa përdorues s’do të jenë në gjendje të bëjnë hyrjen.", "Failed to remove widget": "S’u arrit të hiqej widget-i", "Reload widget": "Ringarkoje widget-in", @@ -1276,5 +1276,76 @@ "To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of Riot to do this": "Që të shmanget humbja e historikut të fjalosjes tuaj, duhet të eksportoni kyçet e dhomës tuaj përpara se të dilni nga llogari. Që ta bëni këtë, duhe të riktheheni te versioni më i ri i Riot-it", "You've previously used a newer version of Riot on %(host)s. To use this version again with end to end encryption, you will need to sign out and back in again. ": "Më parë përdorët një version më të ri të Riot-it në %(host)s. Që ta përdorni sërish këtë version me fshehtëzim skaj-më-skaj, duhet të dilni dhe rihyni te llogaria juaj. ", "Incompatible Database": "Bazë të dhënash e Papërputhshme", - "Continue With Encryption Disabled": "Vazhdo Me Fshehtëzimin të Çaktivizuar" + "Continue With Encryption Disabled": "Vazhdo Me Fshehtëzimin të Çaktivizuar", + "Unable to load! Check your network connectivity and try again.": "S’arrihet të ngarkohet! Kontrolloni lidhjen tuaj në rrjet dhe riprovoni.", + "Forces the current outbound group session in an encrypted room to be discarded": "Forces the current outbound group session in an encrypted room to be discarded", + "Backup of encryption keys to server": "Kopjeruajtje kyçesh fshehtëzimi në shërbyes", + "Delete Backup": "Fshije Kopjeruajtjen", + "Delete your backed up encryption keys from the server? You will no longer be able to use your recovery key to read encrypted message history": "Të fshihen nga shërbyesi kyçet e kopjeruajtur të fshehtëzimit? S’do të jeni më në gjendje të përdorni kyçin tuaj të rimarrjeve për lexim historiku mesazhesh të fshehtëzuar", + "Delete backup": "Fshije kopjeruajtjen", + "Unable to load key backup status": "S’arrihet të ngarkohet gjendje kopjeruajtjeje kyçesh", + "This device is uploading keys to this backup": "Kjo pajisje po ngarkon kyçe te kjo kopjeruajtje", + "This device is not uploading keys to this backup": "Kjo pajisje nuk po ngarkon kyçe te kjo kopjeruajtje", + "Backup has a valid signature from this device": "Kopjeruajtja ka një nënshkrim të vlefshëm prej kësaj pajisjeje", + "Backup has a valid signature from verified device x": "Kopjeruajtja ka një nënshkrim të vlefshëm prej pajisjes së verifikuar x", + "Backup has a valid signature from unverified device ": "Kopjeruajtja ka një nënshkrim të vlefshëm prej pajisjes së paverifikuar ", + "Backup has an invalid signature from verified device ": "Kopjeruajtja ka një nënshkrim të pavlefshëm prej pajisjes së verifikuar ", + "Backup has an invalid signature from unverified device ": "Kopjeruajtja ka një nënshkrim të pavlefshëm prej pajisjes së paverifikuar ", + "Backup is not signed by any of your devices": "Kopjeruajtja s’është nënshkruar nga ndonjë prej pajisjeve tuaja", + "Backup version: ": "Version kopjeruajtjeje: ", + "Algorithm: ": "Algoritëm: ", + "Restore backup": "Riktheje kopjeruajtjen", + "No backup is present": "S’ka kopjeruajtje të pranishëm", + "Start a new backup": "Filloni një kopjeruajtje të re", + "Secure your encrypted message history with a Recovery Passphrase.": "Sigurojeni historikun e mesazheve tuaj të fshehtëzuar me një Frazëkalim Rimarrjesh.", + "You'll need it if you log out or lose access to this device.": "Do t’ju duhet, nëse dilni nga llogaria ose nëse s’përdorni më dot pajisjen.", + "Enter a passphrase...": "Jepni një frazëkalim…", + "Next": "Pasuesja", + "If you don't want encrypted message history to be availble on other devices, .": "Nëse s’doni që historiku i mesazheve të fshehtëzuara të jetë i përdorshëm në pajisje të tjera, .", + "Or, if you don't want to create a Recovery Passphrase, skip this step and .": "Ose, nëse s’doni të krijohet një Frazëkalim Rimarrjesh, anashkalojeni këtë hap dhe .", + "That matches!": "U përputhën!", + "That doesn't match.": "S’përputhen.", + "Go back to set it again.": "Shkoni mbrapsht që ta ricaktoni.", + "Type in your Recovery Passphrase to confirm you remember it. If it helps, add it to your password manager or store it somewhere safe.": "Shtypeni Frazëkalimin tuaj të Rimarrjeve që të ripohoni se e mbani mend. Nëse bën punë, shtojeni te përgjegjësi juaj i fjalëkalimeve ose depozitojeni diku pa rrezik.", + "Repeat your passphrase...": "Përsëritni frazëkalimin tuaj…", + "Make a copy of this Recovery Key and keep it safe.": "Bëni një kopje të këtij Kyçi RImarrjesh dhe mbajeni të parrezikuar.", + "As a safety net, you can use it to restore your encrypted message history if you forget your Recovery Passphrase.": "Si rrjet i parrezikuar, mund ta përdoreni për të rikthyer historikun e mesazheve tuaj të fshehtëzuar, nëse harroni Frazëkalimin e Rimarrjeve.", + "Your Recovery Key": "Kyçi Juaj i Rimarrjeve", + "Copy to clipboard": "Kopjoje në të papastër", + "Download": "Shkarkoje", + "I've made a copy": "Kam bërë një kopje", + "Your Recovery Key has been copied to your clipboard, paste it to:": "Kyçi juaj i Fshehtëzimeve është kopjuar te e papastra juaj, ngjiteni te:", + "Your Recovery Key is in your Downloads folder.": "Kyçi juaj i Fshehtëzimeve gjendet te dosja juaj Shkarkime.", + "Print it and store it somewhere safe": "Shtypeni dhe ruajeni diku pa rrezik", + "Save it on a USB key or backup drive": "Ruajeni në një diskth USB ose disk kopjeruajtjesh", + "Copy it to your personal cloud storage": "Kopjojeni te depoja juaj personale në re", + "Got it": "E mora vesh", + "Backup created": "Kopjeruajtja u krijua", + "Your encryption keys are now being backed up to your Homeserver.": "Kyçet tuaj të fshehtëzimit tani po kopjeruhen te shërbyesi juaj Home.", + "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another device.": "Pa rregulluar Rimarrje të Siguruar, s’do të jeni në gjendje të riktheni historikun e mesazheve tuaj të fshehtëzuar, nëse bëni daljen ose përdorni një pajisje tjetër.", + "Set up Secure Message Recovery": "Rregulloni Rimarrje të Siguruar Mesazhesh", + "Create a Recovery Passphrase": "Krijoni Frazëkalim Rimarrjeje", + "Confirm Recovery Passphrase": "Ripohoni Frazëkalim Rimarrjeje", + "Recovery Key": "Kyç Rimarrjesh", + "Keep it safe": "Mbajeni të parrezikuar", + "Backing up...": "Po kopjeruhet…", + "Create Key Backup": "Krijo Kopjeruajtje Kyçesh", + "Unable to create key backup": "S’arrihet të krijojhet kopjeruajtje kyçesh", + "Retry": "Riprovo", + "Unable to load backup status": "S’arrihet të ngarkohet gjendje kopjeruajtjeje", + "Unable to restore backup": "S’arrihet të rikthehet kopjeruajtje", + "No backup found!": "S’u gjet kopjeruajtje!", + "Backup Restored": "Kopjeruajtja u Rikthye", + "Failed to decrypt %(failedCount)s sessions!": "S’u arrit të shfshehtëzohet sesioni %(failedCount)s!", + "Restored %(sessionCount)s session keys": "U rikthyen kyçet e sesionit %(sessionCount)s", + "Enter Recovery Passphrase": "Jepni Frazëkalim Rimarrjeje", + "Access your secure message history and set up secure messaging by entering your recovery passphrase.": "Hyni te historiku i mesazheve tuaj të siguruar dhe rregulloni shkëmbim mesazhesh të sigurt duke dhënë frazëkalimin tuaj të rimarrjeve.", + "If you've forgotten your recovery passphrase you can use your recovery key or set up new recovery options": "Nëse keni harruar frazëkalimin tuaj të rimarrjeve, mund të përdorni kyçin tuaj të rimarrjeve ose rregulloni mundësi të reja rimarrjeje", + "Enter Recovery Key": "Jepni Kyç Rimarrjeje", + "This looks like a valid recovery key!": "Ky duket si kyç i vlefshëm rimarrjesh!", + "Not a valid recovery key": "Kyç rimarrjesh jo i vlefshëm", + "Access your secure message history and set up secure messaging by entering your recovery key.": "Hyni te historiku i mesazheve tuaj të siguruar dhe rregulloni shkëmbim mesazhesh të sigurt duke dhënë kyçin tuaj të rimarrjeve.", + "If you've forgotten your recovery passphrase you can ": "Nëse keni harruar frazëkalimin tuaj të rimarrjeve, mund të rregulloni mundësi të reja rimarrjeje", + "Key Backup": "Kopjeruajtje Kyçi", + "Sign in with single sign-on": "Bëni hyrjen me hyrje njëshe" } From 328ab4d82434934949994f3b1264e24c7daebf34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20C?= Date: Thu, 22 Nov 2018 10:19:23 +0000 Subject: [PATCH 035/237] Translated using Weblate (French) Currently translated at 100.0% (1350 of 1350 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/fr/ --- src/i18n/strings/fr.json | 71 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index 602f8a25d2..7a629a1e78 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -1308,5 +1308,74 @@ "You've previously used a newer version of Riot on %(host)s. To use this version again with end to end encryption, you will need to sign out and back in again. ": "Vous avez utilisé une version plus récente de Riot sur %(host)s. Pour utiliser à nouveau cette version avec le chiffrement de bout à bout, vous devez vous déconnecter et vous reconnecter. ", "Incompatible Database": "Base de données incompatible", "Continue With Encryption Disabled": "Continuer avec le chiffrement désactivé", - "Sign in with single sign-on": "Se connecter avec l'authentification unique" + "Sign in with single sign-on": "Se connecter avec l'authentification unique", + "Unable to load! Check your network connectivity and try again.": "Chargement impossible ! Vérifiez votre connexion au réseau et réessayez.", + "Backup of encryption keys to server": "Sauvegarde des clés de chiffrement vers le serveur", + "Delete Backup": "Supprimer la sauvegarde", + "Delete your backed up encryption keys from the server? You will no longer be able to use your recovery key to read encrypted message history": "Supprimer vos clés de chiffrement sauvegardées du serveur ? Vous ne pourrez plus utiliser votre clé de récupération pour lire l'historique de vos messages chiffrés", + "Delete backup": "Supprimer la sauvegarde", + "Unable to load key backup status": "Impossible de charger l'état de sauvegarde des clés", + "This device is uploading keys to this backup": "Cet appareil envoie des clés vers cette sauvegarde", + "This device is not uploading keys to this backup": "Cet appareil n'envoie pas

    de clés vers cette sauvegarde", + "Backup has a valid signature from this device": "La sauvegarde a une signature valide pour cet appareil", + "Backup has a valid signature from verified device x": "La sauvegarde a une signature valide de l'appareil vérifié x", + "Backup has a valid signature from unverified device ": "La sauvegarde a une signature valide de l'appareil non vérifié ", + "Backup has an invalid signature from verified device ": "La sauvegarde a une signature non valide de l'appareil vérifié ", + "Backup has an invalid signature from unverified device ": "La sauvegarde a une signature non valide de l'appareil non vérifié ", + "Backup is not signed by any of your devices": "La sauvegarde n'est signée par aucun de vos appareils", + "Backup version: ": "Version de la sauvegarde : ", + "Algorithm: ": "Algorithme : ", + "Restore backup": "Restaurer la sauvegarde", + "No backup is present": "Il n'y a aucune sauvegarde", + "Start a new backup": "Créer une nouvelle sauvegarde", + "Secure your encrypted message history with a Recovery Passphrase.": "Sécurisez l'historique de vos messages chiffrés avec une phrase de récupération.", + "You'll need it if you log out or lose access to this device.": "Vous en aurez besoin si vous vous déconnectez ou si vous n'avez plus accès à cet appareil.", + "Enter a passphrase...": "Saisissez une phrase de passe…", + "Next": "Suivant", + "If you don't want encrypted message history to be availble on other devices, .": "Si vous ne souhaitez pas que l'historique de vos messages chiffrés soit disponible sur d'autres appareils, .", + "Or, if you don't want to create a Recovery Passphrase, skip this step and .": "Ou si vous ne voulez pas créer une phrase de récupération, sautez cette étape et .", + "That matches!": "Ça correspond !", + "That doesn't match.": "Ça ne correspond pas.", + "Go back to set it again.": "Retournez en arrière pour la redéfinir.", + "Type in your Recovery Passphrase to confirm you remember it. If it helps, add it to your password manager or store it somewhere safe.": "Saisissez votre phrase de récupération pour confirmer que vous vous en souvenez. Si cela peut vous aider, ajoutez-la à votre gestionnaire de mots de passe ou rangez-la dans un endroit sûr.", + "Repeat your passphrase...": "Répétez votre phrase de passe…", + "Make a copy of this Recovery Key and keep it safe.": "Faites une copie de cette clé de récupération et gardez-la en lieu sûr.", + "As a safety net, you can use it to restore your encrypted message history if you forget your Recovery Passphrase.": "Par précaution, vous pouvez l'utiliser pour récupérer l'historique de vos messages chiffrés si vous oubliez votre phrase de récupération.", + "Your Recovery Key": "Votre clé de récupération", + "Copy to clipboard": "Copier dans le presse-papier", + "Download": "Télécharger", + "I've made a copy": "J'ai fait une copie", + "Your Recovery Key has been copied to your clipboard, paste it to:": "Votre clé de récupération a été copiée dans votre presse-papier, collez-la dans :", + "Your Recovery Key is in your Downloads folder.": "Votre clé de récupération est dans votre dossier de téléchargements.", + "Print it and store it somewhere safe": "Imprimez-la et conservez-la dans un endroit sûr", + "Save it on a USB key or backup drive": "Sauvegardez-la sur une clé USB ou un disque de sauvegarde", + "Copy it to your personal cloud storage": "Copiez-la dans votre espace de stockage personnel en ligne", + "Got it": "Compris", + "Backup created": "Sauvegarde créée", + "Your encryption keys are now being backed up to your Homeserver.": "Vos clés de chiffrement sont en train d'être sauvegardées sur votre serveur d'accueil.", + "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another device.": "Si vous ne configurez pas la récupération de messages sécurisée, vous ne pourrez pas récupérer l'historique de vos messages chiffrés si vous vous déconnectez ou si vous utilisez un autre appareil.", + "Set up Secure Message Recovery": "Configurer la récupération de messages sécurisée", + "Create a Recovery Passphrase": "Créer une phrase de récupération", + "Confirm Recovery Passphrase": "Confirmer la phrase de récupération", + "Recovery Key": "Clé de récupération", + "Keep it safe": "Conservez-la en lieu sûr", + "Backing up...": "Sauvegarde en cours…", + "Create Key Backup": "Créer la sauvegarde des clés", + "Unable to create key backup": "Impossible de créer la sauvegarde des clés", + "Retry": "Réessayer", + "Unable to load backup status": "Impossible de charger l'état de la sauvegarde", + "Unable to restore backup": "Impossible de restaurer la sauvegarde", + "No backup found!": "Aucune sauvegarde n'a été trouvée !", + "Backup Restored": "Sauvegarde restaurée", + "Failed to decrypt %(failedCount)s sessions!": "Le déchiffrement de %(failedCount)s sessions a échoué !", + "Restored %(sessionCount)s session keys": "%(sessionCount)s clés de session ont été restaurées", + "Enter Recovery Passphrase": "Saisissez la phrase de récupération", + "Access your secure message history and set up secure messaging by entering your recovery passphrase.": "Accédez à l'historique sécurisé de vos messages et configurez la messagerie sécurisée en renseignant votre phrase de récupération.", + "If you've forgotten your recovery passphrase you can use your recovery key or set up new recovery options": "Si vous avez oublié votre phrase de récupération vous pouvez utiliser votre clé de récupération ou configurer de nouvelles options de récupération", + "Enter Recovery Key": "Saisissez la clé de récupération", + "This looks like a valid recovery key!": "Cela ressemble à une clé de récupération valide !", + "Not a valid recovery key": "Ce n'est pas une clé de récupération valide", + "Access your secure message history and set up secure messaging by entering your recovery key.": "Accédez à l'historique sécurisé de vos messages et configurez la messagerie sécurisée en renseignant votre clé de récupération.", + "If you've forgotten your recovery passphrase you can ": "Si vous avez oublié votre clé de récupération vous pouvez ", + "Key Backup": "Sauvegarde de clés" } From c6f317b83b8929e4ccf0b2bb759e16cee648c415 Mon Sep 17 00:00:00 2001 From: Karol Kosek Date: Wed, 21 Nov 2018 21:13:35 +0000 Subject: [PATCH 036/237] Translated using Weblate (Polish) Currently translated at 88.5% (1195 of 1350 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/pl/ --- src/i18n/strings/pl.json | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/i18n/strings/pl.json b/src/i18n/strings/pl.json index 045b04cc94..b16fdd7889 100644 --- a/src/i18n/strings/pl.json +++ b/src/i18n/strings/pl.json @@ -515,7 +515,7 @@ "You cannot place VoIP calls in this browser.": "Nie możesz przeprowadzić rozmowy głosowej VoIP w tej przeglądarce.", "You do not have permission to post to this room": "Nie jesteś uprawniony do pisania w tym pokoju", "You have been banned from %(roomName)s by %(userName)s.": "Zostałeś permanentnie usunięty z pokoju %(roomName)s przez %(userName)s.", - "You have been invited to join this room by %(inviterName)s": "Zostałeś zaproszony do dołączenia do tego pokoju przez %(inviterName)s", + "You have been invited to join this room by %(inviterName)s": "Zostałeś(-aś) zaproszony(-a) do dołączenia do tego pokoju przez %(inviterName)s", "You have been kicked from %(roomName)s by %(userName)s.": "Zostałeś usunięty z %(roomName)s przez %(userName)s.", "You have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, sign in again on each device": "Wylogowałeś się ze wszystkich urządzeń i nie będziesz już otrzymywał powiadomień push. Aby ponownie aktywować powiadomienia zaloguj się ponownie na każdym urządzeniu", "You have disabled URL previews by default.": "Masz domyślnie wyłączone podglądy linków.", @@ -627,7 +627,7 @@ "Tried to load a specific point in this room's timeline, but was unable to find it.": "Próbowano załadować konkretny punkt na osi czasu w tym pokoju, ale nie nie można go znaleźć.", "The exported file will allow anyone who can read it to decrypt any encrypted messages that you can see, so you should be careful to keep it secure. To help with this, you should enter a passphrase below, which will be used to encrypt the exported data. It will only be possible to import the data by using the same passphrase.": "Wyeksportowany plik pozwoli każdej osobie będącej w stanie go odczytać na deszyfrację jakichkolwiek zaszyfrowanych wiadomości, które możesz zobaczyć, tak więc zalecane jest zachowanie ostrożności. Aby w tym pomóc, powinieneś/aś wpisać hasło poniżej; hasło to będzie użyte do zaszyfrowania wyeksportowanych danych. Późniejsze zaimportowanie tych danych będzie możliwe tylko po uprzednim podaniu owego hasła.", " (unsupported)": " (niewspierany)", - "Idle": "Bezczynny", + "Idle": "Bezczynny(-a)", "Check for update": "Sprawdź aktualizacje", "%(senderDisplayName)s changed the room avatar to ": "%(senderDisplayName)s zmienił(a) awatar pokoju na ", "%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s usunął(-ęła) awatar pokoju.", @@ -743,7 +743,7 @@ "Loading...": "Ładowanie...", "Pinned Messages": "Przypięte Wiadomości", "Online for %(duration)s": "Online przez %(duration)s", - "Idle for %(duration)s": "Nieaktywny przez %(duration)s", + "Idle for %(duration)s": "Bezczynny(-a) przez %(duration)s", "Offline for %(duration)s": "Offline przez %(duration)s", "Unknown for %(duration)s": "Nieznany przez %(duration)s", "Unknown": "Nieznany", @@ -1207,5 +1207,7 @@ "Clear cache and resync": "Wyczyść pamięć podręczną i zsynchronizuj ponownie", "Riot now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!": "Riot używa teraz 3-5x mniej pamięci, ładując informacje o innych użytkownikach tylko wtedy, gdy jest to konieczne. Poczekaj, aż ponownie zsynchronizujemy się z serwerem!", "If the other version of Riot is still open in another tab, please close it as using Riot on the same host with both lazy loading enabled and disabled simultaneously will cause issues.": "Jeśli inna wersja Riot jest nadal otwarta w innej zakładce, proszę zamknij ją, ponieważ używanie Riot na tym samym komputerze z włączonym i wyłączonym jednocześnie leniwym ładowaniem będzie powodować problemy.", - "And %(count)s more...|other": "I %(count)s więcej…" + "And %(count)s more...|other": "I %(count)s więcej…", + "Delete Backup": "Usuń Kopię Zapasową", + "Delete backup": "Usuń Kopię Zapasową" } From 7d45994c075f520d9418af7bed0484fe8c5b6cb0 Mon Sep 17 00:00:00 2001 From: Osoitz Date: Fri, 23 Nov 2018 05:44:09 +0000 Subject: [PATCH 037/237] Translated using Weblate (Basque) Currently translated at 95.8% (1294 of 1350 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/eu/ --- src/i18n/strings/eu.json | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/eu.json b/src/i18n/strings/eu.json index 1c9c07d305..bfc4d18a62 100644 --- a/src/i18n/strings/eu.json +++ b/src/i18n/strings/eu.json @@ -1300,5 +1300,24 @@ "Pin unread rooms to the top of the room list": "Finkatu irakurri gabeko gelak gelen zerrendaren goialdean", "Pin rooms I'm mentioned in to the top of the room list": "Finkatu aipatu nauten gelak gelen zerrendaren goialdean", "If you would like to create a Matrix account you can register now.": "Matrix kontu bat sortu nahi baduzu, izena eman dezakezu.", - "You are currently using Riot anonymously as a guest.": "Riot anonimoki gonbidatu gisa erabiltzen ari zara." + "You are currently using Riot anonymously as a guest.": "Riot anonimoki gonbidatu gisa erabiltzen ari zara.", + "Unable to load! Check your network connectivity and try again.": "Ezin da kargatu! Egiaztatu sare konexioa eta saiatu berriro.", + "Backup of encryption keys to server": "Zerbitzarirako zifratze gakoen babes-kopia", + "Delete Backup": "Ezabatu babes-kopia", + "Delete your backed up encryption keys from the server? You will no longer be able to use your recovery key to read encrypted message history": "Ezabatu zerbitzaritik gakoen babes-kopiak? Ezin izango duzu berreskuratze gakoa erabili zifratutako mezuen historia irakurteko", + "Delete backup": "Ezabatu babes-kopia", + "Unable to load key backup status": "Ezin izan da babes-kopiaren egoera kargatu", + "This device is uploading keys to this backup": "Gailu honek gakoak babes-kopia honetara igotzen ditu", + "This device is not uploading keys to this backup": "Gailu honek ez ditu gakoak igotzen babes-kopia honetara", + "Backup has a valid signature from this device": "Babes-kopiak gailu honen baliozko sinadura du", + "Backup has a valid signature from verified device x": "Babes-kopiak egiaztatutako x gailuaren baliozko sinadura du", + "Backup has a valid signature from unverified device ": "Babes-kopiak egiaztatu gabeko gailu baten baliozko sinadura du", + "Backup has an invalid signature from verified device ": "Babes-kopiak egiaztatutako gailuaren balio gabeko sinadura du", + "Backup has an invalid signature from unverified device ": "Babes-kopiak egiaztatu gabeko gailuaren baliogabeko sinadura du", + "Backup is not signed by any of your devices": "Babes-kopia ez dago zure gailu batek sinauta", + "Backup version: ": "Babes-kopiaren bertsioa: ", + "Algorithm: ": "Algoritmoa: ", + "Restore backup": "Berreskuratu babes-kopia", + "No backup is present": "Ez dago babes-kopiarik", + "Start a new backup": "Hasi babes-kopia berria" } From 075c13a5bd591ba94ec7289eccfaacd0714377c4 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 23 Nov 2018 15:50:23 +0000 Subject: [PATCH 038/237] Add password strength meter to backup creation UI https://github.com/vector-im/riot-meta/issues/227 --- package.json | 3 +- .../keybackup/_CreateKeyBackupDialog.scss | 20 +++- .../keybackup/CreateKeyBackupDialog.js | 39 +++++++- src/i18n/strings/en_EN.json | 98 ++++++++++++------- src/utils/PasswordScorer.js | 84 ++++++++++++++++ 5 files changed, 205 insertions(+), 39 deletions(-) create mode 100644 src/utils/PasswordScorer.js diff --git a/package.json b/package.json index b5cdfdf401..67d1f3ba1e 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,8 @@ "text-encoding-utf-8": "^1.0.1", "url": "^0.11.0", "velocity-vector": "github:vector-im/velocity#059e3b2", - "whatwg-fetch": "^1.1.1" + "whatwg-fetch": "^1.1.1", + "zxcvbn": "^4.4.2" }, "devDependencies": { "babel-cli": "^6.26.0", diff --git a/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss b/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss index 507c89ace7..2cb6b11c0c 100644 --- a/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss +++ b/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss @@ -19,8 +19,26 @@ limitations under the License. padding: 20px } +.mx_CreateKeyBackupDialog_primaryContainer::after { + content: ""; + clear: both; + display: block; +} + +.mx_CreateKeyBackupDialog_passPhraseHelp { + float: right; + width: 230px; + height: 85px; + margin-left: 20px; + font-size: 80%; +} + +.mx_CreateKeyBackupDialog_passPhraseHelp progress { + width: 100%; +} + .mx_CreateKeyBackupDialog_passPhraseInput { - width: 300px; + width: 250px; border: 1px solid $accent-color; border-radius: 5px; padding: 10px; diff --git a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js index 2f43d18072..7aa3133874 100644 --- a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js +++ b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js @@ -17,6 +17,7 @@ limitations under the License. import React from 'react'; import sdk from '../../../../index'; import MatrixClientPeg from '../../../../MatrixClientPeg'; +import { scorePassword } from '../../../../utils/PasswordScorer'; import FileSaver from 'file-saver'; @@ -30,6 +31,8 @@ const PHASE_BACKINGUP = 4; const PHASE_DONE = 5; const PHASE_OPTOUT_CONFIRM = 6; +const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc. + // XXX: copied from ShareDialog: factor out into utils function selectText(target) { const range = document.createRange(); @@ -52,6 +55,7 @@ export default React.createClass({ passPhraseConfirm: '', copied: false, downloaded: false, + zxcvbnResult: null, }; }, @@ -173,6 +177,10 @@ export default React.createClass({ _onPassPhraseChange: function(e) { this.setState({ passPhrase: e.target.value, + // precompute this and keep it in state: zxcvbn is fast but + // we use it in a couple of different places so so point recomputing + // it unnecessarily. + zxcvbnResult: scorePassword(e.target.value), }); }, @@ -183,17 +191,46 @@ export default React.createClass({ }, _passPhraseIsValid: function() { - return this.state.passPhrase !== ''; + return this.state.zxcvbnResult && this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE; }, _renderPhasePassPhrase: function() { const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + + let strengthMeter; + let helpText; + if (this.state.zxcvbnResult) { + if (this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE) { + helpText = _t("Great! This passphrase looks strong enough."); + } else { + const suggestions = []; + for (let i = 0; i < this.state.zxcvbnResult.feedback.suggestions.length; ++i) { + suggestions.push(
    {this.state.zxcvbnResult.feedback.suggestions[i]}
    ); + } + const suggestionBlock = suggestions.length > 0 ?
    + {suggestions} +
    : null; + + helpText =
    + {this.state.zxcvbnResult.feedback.warning} + {suggestionBlock} +
    ; + } + strengthMeter =
    + +
    ; + } + return

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

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

    +
    + {strengthMeter} + {helpText} +
    opt out.": "If you don't want encrypted message history to be availble on other devices, .", - "Or, if you don't want to create a Recovery Passphrase, skip this step and .": "Or, if you don't want to create a Recovery Passphrase, skip this step and .", - "That matches!": "That matches!", - "That doesn't match.": "That doesn't match.", - "Go back to set it again.": "Go back to set it again.", - "Type in your Recovery Passphrase to confirm you remember it. If it helps, add it to your password manager or store it somewhere safe.": "Type in your Recovery Passphrase to confirm you remember it. If it helps, add it to your password manager or store it somewhere safe.", - "Repeat your passphrase...": "Repeat your passphrase...", - "Make a copy of this Recovery Key and keep it safe.": "Make a copy of this Recovery Key and keep it safe.", - "As a safety net, you can use it to restore your encrypted message history if you forget your Recovery Passphrase.": "As a safety net, you can use it to restore your encrypted message history if you forget your Recovery Passphrase.", - "Your Recovery Key": "Your Recovery Key", - "Copy to clipboard": "Copy to clipboard", - "Download": "Download", - "I've made a copy": "I've made a copy", - "Your Recovery Key has been copied to your clipboard, paste it to:": "Your Recovery Key has been copied to your clipboard, paste it to:", - "Your Recovery Key is in your Downloads folder.": "Your Recovery Key is in your Downloads folder.", - "Print it and store it somewhere safe": "Print it and store it somewhere safe", - "Save it on a USB key or backup drive": "Save it on a USB key or backup drive", - "Copy it to your personal cloud storage": "Copy it to your personal cloud storage", - "Got it": "Got it", - "Backup created": "Backup created", - "Your encryption keys are now being backed up to your Homeserver.": "Your encryption keys are now being backed up to your Homeserver.", - "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another device.": "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another device.", - "Set up Secure Message Recovery": "Set up Secure Message Recovery", - "Create a Recovery Passphrase": "Create a Recovery Passphrase", - "Confirm Recovery Passphrase": "Confirm Recovery Passphrase", - "Recovery Key": "Recovery Key", - "Keep it safe": "Keep it safe", - "Backing up...": "Backing up...", - "Create Key Backup": "Create Key Backup", - "Unable to create key backup": "Unable to create key backup", - "Retry": "Retry", "Unable to load backup status": "Unable to load backup status", "Unable to restore backup": "Unable to restore backup", "No backup found!": "No backup found!", @@ -1016,6 +1006,7 @@ "Restored %(sessionCount)s session keys": "Restored %(sessionCount)s session keys", "Enter Recovery Passphrase": "Enter Recovery Passphrase", "Access your secure message history and set up secure messaging by entering your recovery passphrase.": "Access your secure message history and set up secure messaging by entering your recovery passphrase.", + "Next": "Next", "If you've forgotten your recovery passphrase you can use your recovery key or set up new recovery options": "If you've forgotten your recovery passphrase you can use your recovery key or set up new recovery options", "Enter Recovery Key": "Enter Recovery Key", "This looks like a valid recovery key!": "This looks like a valid recovery key!", @@ -1346,6 +1337,41 @@ "The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.": "The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.", "File to import": "File to import", "Import": "Import", + "Great! This passphrase looks strong enough.": "Great! This passphrase looks strong enough.", + "Secure your encrypted message history with a Recovery Passphrase.": "Secure your encrypted message history with a Recovery Passphrase.", + "You'll need it if you log out or lose access to this device.": "You'll need it if you log out or lose access to this device.", + "Enter a passphrase...": "Enter a passphrase...", + "If you don't want encrypted message history to be availble on other devices, .": "If you don't want encrypted message history to be availble on other devices, .", + "Or, if you don't want to create a Recovery Passphrase, skip this step and .": "Or, if you don't want to create a Recovery Passphrase, skip this step and .", + "That matches!": "That matches!", + "That doesn't match.": "That doesn't match.", + "Go back to set it again.": "Go back to set it again.", + "Type in your Recovery Passphrase to confirm you remember it. If it helps, add it to your password manager or store it somewhere safe.": "Type in your Recovery Passphrase to confirm you remember it. If it helps, add it to your password manager or store it somewhere safe.", + "Repeat your passphrase...": "Repeat your passphrase...", + "Make a copy of this Recovery Key and keep it safe.": "Make a copy of this Recovery Key and keep it safe.", + "As a safety net, you can use it to restore your encrypted message history if you forget your Recovery Passphrase.": "As a safety net, you can use it to restore your encrypted message history if you forget your Recovery Passphrase.", + "Your Recovery Key": "Your Recovery Key", + "Copy to clipboard": "Copy to clipboard", + "Download": "Download", + "I've made a copy": "I've made a copy", + "Your Recovery Key has been copied to your clipboard, paste it to:": "Your Recovery Key has been copied to your clipboard, paste it to:", + "Your Recovery Key is in your Downloads folder.": "Your Recovery Key is in your Downloads folder.", + "Print it and store it somewhere safe": "Print it and store it somewhere safe", + "Save it on a USB key or backup drive": "Save it on a USB key or backup drive", + "Copy it to your personal cloud storage": "Copy it to your personal cloud storage", + "Got it": "Got it", + "Backup created": "Backup created", + "Your encryption keys are now being backed up to your Homeserver.": "Your encryption keys are now being backed up to your Homeserver.", + "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another device.": "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another device.", + "Set up Secure Message Recovery": "Set up Secure Message Recovery", + "Create a Recovery Passphrase": "Create a Recovery Passphrase", + "Confirm Recovery Passphrase": "Confirm Recovery Passphrase", + "Recovery Key": "Recovery Key", + "Keep it safe": "Keep it safe", + "Backing up...": "Backing up...", + "Create Key Backup": "Create Key Backup", + "Unable to create key backup": "Unable to create key backup", + "Retry": "Retry", "Failed to set direct chat tag": "Failed to set direct chat tag", "Failed to remove tag %(tagName)s from room": "Failed to remove tag %(tagName)s from room", "Failed to add tag %(tagName)s to room": "Failed to add tag %(tagName)s to room" diff --git a/src/utils/PasswordScorer.js b/src/utils/PasswordScorer.js new file mode 100644 index 0000000000..e4bbec1637 --- /dev/null +++ b/src/utils/PasswordScorer.js @@ -0,0 +1,84 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import Zxcvbn from 'zxcvbn'; + +import MatrixClientPeg from '../MatrixClientPeg'; +import { _t, _td } from '../languageHandler'; + +const ZXCVBN_USER_INPUTS = [ + 'riot', + 'matrix', +]; + +// Translations for zxcvbn's suggestion strings +_td("Use a few words, avoid common phrases"); +_td("No need for symbols, digits, or uppercase letters"); +_td("Use a longer keyboard pattern with more turns"); +_td("Avoid repeated words and characters"); +_td("Avoid sequences"); +_td("Avoid recent years"); +_td("Avoid years that are associated with you"); +_td("Avoid dates and years that are associated with you"); +_td("Capitalization doesn't help very much"); +_td("All-uppercase is almost as easy to guess as all-lowercase"); +_td("Reversed words aren't much harder to guess"); +_td("Predictable substitutions like '@' instead of 'a' don't help very much"); +_td("Add another word or two. Uncommon words are better."); + +// and warnings +_td("Repeats like \"aaa\" are easy to guess"); +_td("Repeats like \"abcabcabc\" are only slightly harder to guess than \"abc\""); +_td("Sequences like abc or 6543 are easy to guess"); +_td("Recent years are easy to guess"); +_td("Dates are often easy to guess"); +_td("This is a top-10 common password"); +_td("This is a top-100 common password"); +_td("This is a very common password"); +_td("This is similar to a commonly used password"); +_td("A word by itself is easy to guess"); +_td("Names and surnames by themselves are easy to guess"); +_td("Common names and surnames are easy to guess"); + +/** + * Wrapper around zxcvbn password strength estimation + * Include this only from async components: it pulls in zxcvbn + * (obviously) which is large. + */ +export function scorePassword(password) { + if (password.length === 0) return null; + + const userInputs = ZXCVBN_USER_INPUTS.slice(); + userInputs.push(MatrixClientPeg.get().getUserIdLocalpart()); + + let zxcvbnResult = Zxcvbn(password, userInputs); + // Work around https://github.com/dropbox/zxcvbn/issues/216 + if (password.includes(' ')) { + const resultNoSpaces = Zxcvbn(password.replace(/ /g, ''), userInputs); + if (resultNoSpaces.score < zxcvbnResult.score) zxcvbnResult = resultNoSpaces; + } + + for (let i = 0; i < zxcvbnResult.feedback.suggestions.length; ++i) { + // translate suggestions + zxcvbnResult.feedback.suggestions[i] = _t(zxcvbnResult.feedback.suggestions[i]); + } + // and warning, if any + if (zxcvbnResult.feedback.warning) { + zxcvbnResult.feedback.warning = _t(zxcvbnResult.feedback.warning); + } + + return zxcvbnResult; +} From e27f3f0c4bff3f4841cd209e002fbda007b7ac39 Mon Sep 17 00:00:00 2001 From: Osoitz Date: Sat, 24 Nov 2018 09:56:09 +0000 Subject: [PATCH 039/237] Translated using Weblate (Basque) Currently translated at 100.0% (1350 of 1350 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/eu/ --- src/i18n/strings/eu.json | 58 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/eu.json b/src/i18n/strings/eu.json index bfc4d18a62..a520d6eadb 100644 --- a/src/i18n/strings/eu.json +++ b/src/i18n/strings/eu.json @@ -1319,5 +1319,61 @@ "Algorithm: ": "Algoritmoa: ", "Restore backup": "Berreskuratu babes-kopia", "No backup is present": "Ez dago babes-kopiarik", - "Start a new backup": "Hasi babes-kopia berria" + "Start a new backup": "Hasi babes-kopia berria", + "Please review and accept all of the homeserver's policies": "Berrikusi eta onartu hasiera-zerbitzariaren politika guztiak", + "To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of Riot to do this": "Zure txaten historiala ez galtzeko, zure gelako gakoak esportatu behar dituzu saioa amaitu aurretik. Riot-en bertsio berriagora bueltatu behar zara hau egiteko", + "You've previously used a newer version of Riot on %(host)s. To use this version again with end to end encryption, you will need to sign out and back in again. ": "Riot-en bertsio berriago bat erabili duzu %(host)s zerbitzarian. Bertsio hau berriro erabiltzeko muturretik muturrerako zifratzearekin, saioa amaitu eta berriro hasi beharko duzu. ", + "Incompatible Database": "Datu-base bateraezina", + "Continue With Encryption Disabled": "Jarraitu zifratzerik gabe", + "Secure your encrypted message history with a Recovery Passphrase.": "Ziurtatu zure zifratutako mezuen historiala berreskuratze pasa-esaldi batekin.", + "You'll need it if you log out or lose access to this device.": "Saioa amaitzen baduzu edo gailu hau erabiltzeko aukera galtzen baduzu, hau beharko duzu.", + "Enter a passphrase...": "Sartu pasa-esaldi bat...", + "Next": "Hurrengoa", + "If you don't want encrypted message history to be availble on other devices, .": "Ez baduzu zifratutako mezuen historiala beste gailuetan eskuragarri egotea, .", + "Or, if you don't want to create a Recovery Passphrase, skip this step and .": "Edo, ez baduzu berreskuratze pasa-esaldi bat sortu nahi, saltatu urrats hau eta .", + "That matches!": "Bat dator!", + "That doesn't match.": "Ez dator bat.", + "Go back to set it again.": "Joan atzera eta berriro ezarri.", + "Type in your Recovery Passphrase to confirm you remember it. If it helps, add it to your password manager or store it somewhere safe.": "Idatzi zure berreskuratze pasa-esaldia gogoratzen duzula berresteko. lagungarria bazaizu, gehitu ezazu zure pasahitz-kudeatzailera edo gorde toki seguru batean.", + "Repeat your passphrase...": "Errepikatu zure pasa-esaldia...", + "Make a copy of this Recovery Key and keep it safe.": "Egin berreskuratze gako honen kopia eta gorde toki seguruan.", + "As a safety net, you can use it to restore your encrypted message history if you forget your Recovery Passphrase.": "Aukeran, berreskuratze pasa-esaldia ahazten baduzu, zure zifratutako mezuen historiala berreskuratzeko erabili dezakezu.", + "Your Recovery Key": "Zure berreskuratze gakoa", + "Copy to clipboard": "Kopiatu arbelera", + "Download": "Deskargatu", + "I've made a copy": "Kopia bat egin dut", + "Your Recovery Key has been copied to your clipboard, paste it to:": "Zure berreskuratze gakoa zure arbelera kopiatu da, itsatsi hemen:", + "Your Recovery Key is in your Downloads folder.": "Zure berreskuratze gakoa zure Deskargak karpetan dago.", + "Print it and store it somewhere safe": "Inprimatu ezazu eta gorde toki seguruan", + "Save it on a USB key or backup drive": "Gorde ezazu USB giltza batean edo babes-kopien diskoan", + "Copy it to your personal cloud storage": "Kopiatu ezazu zure hodeiko biltegi pertsonalean", + "Got it": "Ulertuta", + "Backup created": "Babes-kopia sortuta", + "Your encryption keys are now being backed up to your Homeserver.": "Zure zifratze gakoak zure hasiera-zerbitzarian gordetzen ari dira.", + "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another device.": "Mezuen berreskuratze segurua ezartzen ez bada, ezin izango duzu zure zifratutako mezuen historiala berreskuratu saioa amaitzen baduzu edo beste gailu bat erabiltzen baduzu.", + "Set up Secure Message Recovery": "Ezarri mezuen berreskuratze segurua", + "Create a Recovery Passphrase": "Sortu berreskuratze pasa-esaldia", + "Confirm Recovery Passphrase": "Berretsi berreskuratze pasa-esaldia", + "Recovery Key": "Berreskuratze gakoa", + "Keep it safe": "Gorde toki seguruan", + "Backing up...": "Babes-kopia egiten...", + "Create Key Backup": "Sortu gakoaren babes-kopia", + "Unable to create key backup": "Ezin izan da gakoaren babes-kopia sortu", + "Retry": "Berriro saiatu", + "Unable to load backup status": "Ezin izan da babes-kopiaren egoera kargatu", + "Unable to restore backup": "Ezin izan da babes-kopia berrezarri", + "No backup found!": "Ez da babes-kopiarik aurkitu!", + "Backup Restored": "Babes-kopia berrezarrita", + "Failed to decrypt %(failedCount)s sessions!": "Ezin izan dira %(failedCount)s saio deszifratu!", + "Restored %(sessionCount)s session keys": "%(sessionCount)s saio gako berrezarrita", + "Enter Recovery Passphrase": "Sartu berreskuratze pasa-esaldia", + "Access your secure message history and set up secure messaging by entering your recovery passphrase.": "Atzitu zure mezu seguruen historiala eta ezarri mezularitza segurua zure berreskuratze pasa-esaldia sartuz.", + "If you've forgotten your recovery passphrase you can use your recovery key or set up new recovery options": "Zure berreskuratze pasa-esaldia ahaztu baduzu berreskuratze gakoa erabili dezakezu edo berreskuratze aukera berriak ezarri ditzakezu", + "Enter Recovery Key": "Sartu berreskuratze gakoa", + "This looks like a valid recovery key!": "Hau baliozko berreskuratze gako bat dirudi!", + "Not a valid recovery key": "Ez da baliozko berreskuratze gako bat", + "Access your secure message history and set up secure messaging by entering your recovery key.": "Atzitu zure mezu seguruen historiala eta ezarri mezularitza segurua zure berreskuratze gakoa sartuz.", + "If you've forgotten your recovery passphrase you can ": "Zure berreskuratze pasa-esaldia ahaztu baduzu ditzakezu", + "Key Backup": "Gakoen babes-kopia", + "Sign in with single sign-on": "Hai saioa urrats batean" } From 015af7d771673074161f7ab64aa413b02b176ebf Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 27 Nov 2018 13:41:34 -0700 Subject: [PATCH 040/237] Use sensible logging --- src/components/structures/login/Login.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index 6002f058f4..bd18699dd1 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -241,8 +241,7 @@ module.exports = React.createClass({ const url = new URL("https://" + serverName); this._tryWellKnownDiscovery(url.hostname); } catch (e) { - console.error("Problem parsing URL or unhandled error doing .well-known discovery"); - console.error(e); + console.error("Problem parsing URL or unhandled error doing .well-known discovery:", e); this.setState({discoveryError: _t("Failed to perform homeserver discovery")}); } } From ec3173c8dda70c3bf570f1b01e9d168df483fe70 Mon Sep 17 00:00:00 2001 From: Akarshan Biswas Date: Tue, 27 Nov 2018 09:57:36 +0000 Subject: [PATCH 041/237] Translated using Weblate (Hindi) Currently translated at 24.0% (325 of 1350 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/hi/ --- src/i18n/strings/hi.json | 211 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 210 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/hi.json b/src/i18n/strings/hi.json index 6f168c1849..b043c9d225 100644 --- a/src/i18n/strings/hi.json +++ b/src/i18n/strings/hi.json @@ -114,5 +114,214 @@ "Operation failed": "कार्रवाई विफल", "Failed to invite": "आमंत्रित करने में विफल", "Failed to invite the following users to the %(roomName)s room:": "निम्नलिखित उपयोगकर्ताओं को %(roomName)s रूम में आमंत्रित करने में विफल:", - "You need to be logged in.": "आपको लॉग इन करने की जरूरत है।" + "You need to be logged in.": "आपको लॉग इन करने की जरूरत है।", + "Unable to load! Check your network connectivity and try again.": "लोड नहीं किया जा सकता! अपनी नेटवर्क कनेक्टिविटी जांचें और पुनः प्रयास करें।", + "You need to be able to invite users to do that.": "आपको उपयोगकर्ताओं को ऐसा करने के लिए आमंत्रित करने में सक्षम होना चाहिए।", + "Unable to create widget.": "विजेट बनाने में असमर्थ।", + "Missing roomId.": "गुमशुदा रूम ID।", + "Failed to send request.": "अनुरोध भेजने में विफल।", + "This room is not recognised.": "यह रूम पहचाना नहीं गया है।", + "Power level must be positive integer.": "पावर स्तर सकारात्मक पूर्णांक होना चाहिए।", + "You are not in this room.": "आप इस रूम में नहीं हैं।", + "You do not have permission to do that in this room.": "आपको इस कमरे में ऐसा करने की अनुमति नहीं है।", + "Missing room_id in request": "अनुरोध में रूम_आईडी गुम है", + "Room %(roomId)s not visible": "%(roomId)s रूम दिखाई नहीं दे रहा है", + "Missing user_id in request": "अनुरोध में user_id गुम है", + "Usage": "प्रयोग", + "Searches DuckDuckGo for results": "परिणामों के लिए DuckDuckGo खोजें", + "/ddg is not a command": "/ddg एक कमांड नहीं है", + "To use it, just wait for autocomplete results to load and tab through them.": "इसका उपयोग करने के लिए, बस स्वत: पूर्ण परिणामों को लोड करने और उनके माध्यम से टैब के लिए प्रतीक्षा करें।", + "Changes your display nickname": "अपना प्रदर्शन उपनाम बदलता है", + "Changes colour scheme of current room": "वर्तमान कमरे की रंग योजना बदलता है", + "Sets the room topic": "कमरे के विषय सेट करता है", + "Invites user with given id to current room": "दिए गए आईडी के साथ उपयोगकर्ता को वर्तमान रूम में आमंत्रित करता है", + "Joins room with given alias": "दिए गए उपनाम के साथ रूम में शामिल हो जाता है", + "Leave room": "रूम छोड़ें", + "Unrecognised room alias:": "अपरिचित रूम उपनाम:", + "Kicks user with given id": "दिए गए आईडी के साथ उपयोगकर्ता को निर्वासन(किक) करता हैं", + "Bans user with given id": "दिए गए आईडी के साथ उपयोगकर्ता को प्रतिबंध लगाता है", + "Unbans user with given id": "दिए गए आईडी के साथ उपयोगकर्ता को अप्रतिबंधित करता हैं", + "Ignores a user, hiding their messages from you": "उपयोगकर्ता को अनदेखा करें और स्वयं से संदेश छुपाएं", + "Ignored user": "अनदेखा उपयोगकर्ता", + "You are now ignoring %(userId)s": "आप %(userId)s को अनदेखा कर रहे हैं", + "Stops ignoring a user, showing their messages going forward": "उपयोगकर्ता को अनदेखा करना बंद करें और एक संदेश प्रदर्शित करें", + "Unignored user": "अनदेखा बंद किया गया उपयोगकर्ता", + "You are no longer ignoring %(userId)s": "अब आप %(userId)s को अनदेखा नहीं कर रहे हैं", + "Define the power level of a user": "उपयोगकर्ता के पावर स्तर को परिभाषित करें", + "Deops user with given id": "दिए गए आईडी के साथ उपयोगकर्ता को देओप्स करना", + "Opens the Developer Tools dialog": "डेवलपर टूल्स संवाद खोलता है", + "Verifies a user, device, and pubkey tuple": "उपयोगकर्ता, डिवाइस और पबकी टुपल को सत्यापित करता है", + "Unknown (user, device) pair:": "अज्ञात (उपयोगकर्ता, डिवाइस) जोड़ी:", + "Device already verified!": "डिवाइस पहले ही सत्यापित है!", + "WARNING: Device already verified, but keys do NOT MATCH!": "चेतावनी: डिवाइस पहले ही सत्यापित है, लेकिन चाबियाँ मेल नहीं खाती हैं!", + "WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!": "चेतावनी: कुंजी सत्यापन विफल! %(userId)s और डिवाइस %(deviceId)s के लिए हस्ताक्षर कुंजी \"%(fprint)s\" है जो प्रदान की गई कुंजी \"%(fingerprint)s\" से मेल नहीं खाती है। इसका मतलब यह हो सकता है कि आपके संचार को अंतरग्रहण किया जा रहा है!", + "Verified key": "सत्यापित कुंजी", + "The signing key you provided matches the signing key you received from %(userId)s's device %(deviceId)s. Device marked as verified.": "आपके द्वारा प्रदान की गई हस्ताक्षर कुंजी %(userId)s के डिवाइस %(deviceId)s से प्राप्त हस्ताक्षर कुंजी से मेल खाती है। डिवाइस सत्यापित के रूप में चिह्नित किया गया है।", + "Displays action": "कार्रवाई प्रदर्शित करता है", + "Forces the current outbound group session in an encrypted room to be discarded": "एक एन्क्रिप्टेड रूम में मौजूदा आउटबाउंड समूह सत्र को त्यागने के लिए मजबूर करता है", + "Unrecognised command:": "अपरिचित आदेश:", + "Reason": "कारण", + "%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s ने %(displayName)s के लिए निमंत्रण को स्वीकार कर लिया है।", + "%(targetName)s accepted an invitation.": "%(targetName)s ने एक निमंत्रण स्वीकार कर लिया।", + "%(senderName)s requested a VoIP conference.": "%(senderName)s ने एक वीओआईपी सम्मेलन का अनुरोध किया।", + "%(senderName)s invited %(targetName)s.": "%(senderName)s ने %(targetName)s को आमंत्रित किया।", + "%(senderName)s banned %(targetName)s.": "%(senderName)s ने %(targetName)s को प्रतिबंधित किया।", + "%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s ने अपना प्रदर्शन नाम %(displayName)s में बदल दिया।", + "%(senderName)s set their display name to %(displayName)s.": "%(senderName)s अपना प्रदर्शन नाम %(displayName)s पर सेट किया।", + "%(senderName)s removed their display name (%(oldDisplayName)s).": "%(senderName)s ने अपना प्रदर्शन नाम हटा दिया (%(oldDisplayName)s)।", + "%(senderName)s removed their profile picture.": "%(senderName)s ने अपनी प्रोफाइल तस्वीर हटा दी।", + "%(senderName)s changed their profile picture.": "%(senderName)s ने अपनी प्रोफाइल तस्वीर बदल दी।", + "%(senderName)s set a profile picture.": "%(senderName)s ने प्रोफाइल तस्वीर सेट कया।", + "VoIP conference started.": "वीओआईपी सम्मेलन शुरू हुआ।", + "%(targetName)s joined the room.": "%(targetName)s रूम में शामिल हो गया।", + "VoIP conference finished.": "वीओआईपी सम्मेलन समाप्त हो गया।", + "%(targetName)s rejected the invitation.": "%(targetName)s ने निमंत्रण को खारिज कर दिया।", + "%(targetName)s left the room.": "%(targetName)s ने रूम छोर दिया।", + "%(senderName)s unbanned %(targetName)s.": "%(senderName)s ने %(targetName)s को अप्रतिबंधित कर दिया।", + "%(senderName)s kicked %(targetName)s.": "%(senderName)s ने %(targetName)s को किक कर दिया।", + "%(senderName)s withdrew %(targetName)s's invitation.": "%(senderName)s ने %(targetName)s की निमंत्रण वापस ले लिया।", + "%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s ने विषय को \"%(topic)s\" में बदल दिया।", + "%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s ने रूम का नाम हटा दिया।", + "%(senderDisplayName)s changed the room name to %(roomName)s.": "%(senderDisplayName)s कमरे का नाम बदलकर %(roomName)s कर दिया।", + "%(senderDisplayName)s sent an image.": "%(senderDisplayName)s ने एक छवि भेजी।", + "%(senderName)s added %(count)s %(addedAddresses)s as addresses for this room.|other": "%(senderName)s ने इस रूम के लिए पते के रूप में %(addedAddresses)s को जोड़ा।", + "%(senderName)s added %(count)s %(addedAddresses)s as addresses for this room.|one": "%(senderName)s ने इस रूम के लिए एक पते के रूप में %(addedAddresses)s को जोड़ा।", + "%(senderName)s removed %(count)s %(removedAddresses)s as addresses for this room.|other": "%(senderName)s ने इस कमरे के लिए पते के रूप में %(removedAddresses)s को हटा दिया।", + "%(senderName)s removed %(count)s %(removedAddresses)s as addresses for this room.|one": "%(senderName)s ने इस कमरे के लिए एक पते के रूप में %(removedAddresses)s को हटा दिया।", + "%(senderName)s added %(addedAddresses)s and removed %(removedAddresses)s as addresses for this room.": "%(senderName)s ने इस कमरे के लिए पते के रूप में %(addedAddresses)s को जोड़ा और %(removedAddresses)s को हटा दिया।", + "%(senderName)s set the main address for this room to %(address)s.": "%(senderName)s ने इस कमरे के लिए मुख्य पता %(address)s पर सेट किया।", + "%(senderName)s removed the main address for this room.": "%(senderName)s ने इस कमरे के लिए मुख्य पता हटा दिया।", + "Someone": "कोई", + "(not supported by this browser)": "(इस ब्राउज़र द्वारा समर्थित नहीं है)", + "%(senderName)s answered the call.": "%(senderName)s ने कॉल का जवाब दिया।", + "(could not connect media)": "(मीडिया कनेक्ट नहीं कर सका)", + "(no answer)": "(कोई जवाब नहीं)", + "(unknown failure: %(reason)s)": "(अज्ञात विफलता: %(reason)s)", + "%(senderName)s ended the call.": "%(senderName)s ने कॉल समाप्त कर दिया।", + "%(senderName)s placed a %(callType)s call.": "%(senderName)s ने %(callType)s कॉल रखा।", + "%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.": "%(senderName)s रूम में शामिल होने के लिए %(targetDisplayName)s को निमंत्रण भेजा।", + "%(senderName)s made future room history visible to all room members, from the point they are invited.": "%(senderName)s ने भविष्य के रूम का इतिहास सभी रूम के सदस्यों के लिए प्रकाशित कर दिया जिस बिंदु से उन्हें आमंत्रित किया गया था।", + "%(senderName)s made future room history visible to all room members, from the point they joined.": "%(senderName)s ने भविष्य के रूम का इतिहास सभी रूम के सदस्यों के लिए दृश्यमान किया, जिस बिंदु में वे शामिल हुए थे।", + "%(senderName)s made future room history visible to all room members.": "%(senderName)s ने भविष्य के रूम का इतिहास सभी रूम के सदस्यों के लिए दृश्यमान बना दिया।", + "%(senderName)s made future room history visible to anyone.": "%(senderName)s ने भविष्य के रूम का इतिहास हर किसी के लिए दृश्यमान बना दिया।", + "%(senderName)s made future room history visible to unknown (%(visibility)s).": "%(senderName)s ने भविष्य के रूम का इतिहास अज्ञात (%(visibility)s) के लिए दृश्यमान बनाया।", + "%(senderName)s turned on end-to-end encryption (algorithm %(algorithm)s).": "%(senderName)s ने एंड-टू-एंड एन्क्रिप्शन (एल्गोरिदम %(algorithm)s) चालू कर दिया।", + "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s का %(fromPowerLevel)s से %(toPowerLevel)s", + "%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s ने %(powerLevelDiffText)s के पावर स्तर को बदल दिया।", + "%(senderName)s changed the pinned messages for the room.": "%(senderName)s ने रूम के लिए पिन किए गए संदेश को बदल दिया।", + "%(widgetName)s widget modified by %(senderName)s": "%(widgetName)s विजेट %(senderName)s द्वारा संशोधित", + "%(widgetName)s widget added by %(senderName)s": "%(widgetName)s विजेट %(senderName)s द्वारा जोड़ा गया", + "%(widgetName)s widget removed by %(senderName)s": "%(widgetName)s विजेट %(senderName)s द्वारा हटा दिया गया", + "%(displayName)s is typing": "%(displayName)s टाइप कर रहा है", + "%(names)s and %(count)s others are typing|other": "%(names)s और %(count)s अन्य टाइप कर रहे हैं", + "%(names)s and %(count)s others are typing|one": "%(names)s और एक दूसरा व्यक्ति टाइप कर रहे हैं", + "%(names)s and %(lastPerson)s are typing": "%(names)s और %(lastPerson)s टाइप कर रहे हैं", + "Failure to create room": "रूम बनाने में विफलता", + "Server may be unavailable, overloaded, or you hit a bug.": "सर्वर अनुपलब्ध, अधिभारित हो सकता है, या अपने एक सॉफ्टवेयर गर्बरी को पाया।", + "Send anyway": "वैसे भी भेजें", + "Send": "भेजें", + "Unnamed Room": "अनाम रूम", + "This homeserver has hit its Monthly Active User limit.": "इस होमसर्वर ने अपनी मासिक सक्रिय उपयोगकर्ता सीमा को प्राप्त कर लिया हैं।", + "This homeserver has exceeded one of its resource limits.": "यह होम सर्वर अपनी संसाधन सीमाओं में से एक से अधिक हो गया है।", + "Please contact your service administrator to continue using the service.": "सेवा का उपयोग जारी रखने के लिए कृपया अपने सेवा व्यवस्थापक से संपर्क करें ।", + "Unable to connect to Homeserver. Retrying...": "होमसर्वर से कनेक्ट करने में असमर्थ। पुनः प्रयास किया जा रहा हैं...", + "Your browser does not support the required cryptography extensions": "आपका ब्राउज़र आवश्यक क्रिप्टोग्राफी एक्सटेंशन का समर्थन नहीं करता है", + "Not a valid Riot keyfile": "यह एक वैध रायट कीकुंजी नहीं है", + "Authentication check failed: incorrect password?": "प्रमाणीकरण जांच विफल: गलत पासवर्ड?", + "Sorry, your homeserver is too old to participate in this room.": "क्षमा करें, इस रूम में भाग लेने के लिए आपका होमसर्वर बहुत पुराना है।", + "Please contact your homeserver administrator.": "कृपया अपने होमसर्वर व्यवस्थापक से संपर्क करें।", + "Failed to join room": "रूम में शामिल होने में विफल", + "Message Pinning": "संदेश पिनिंग", + "Increase performance by only loading room members on first view": "पहले दृश्य पर केवल कमरे के सदस्यों को लोड करके प्रदर्शन बढ़ाएं", + "Backup of encryption keys to server": "सर्वर पर एन्क्रिप्शन कुंजी का बैकअप", + "Disable Emoji suggestions while typing": "टाइप करते समय इमोजी सुझाव अक्षम करें", + "Use compact timeline layout": "कॉम्पैक्ट टाइमलाइन लेआउट का प्रयोग करें", + "Hide removed messages": "हटाए गए संदेशों को छुपाएं", + "Hide join/leave messages (invites/kicks/bans unaffected)": "शामिल होने/छोड़ने के सन्देश छुपाएं (आमंत्रित / किक/ प्रतिबंध अप्रभावित)", + "Hide avatar changes": "अवतार परिवर्तन छुपाएं", + "Hide display name changes": "प्रदर्शन नाम परिवर्तन छुपाएं", + "Hide read receipts": "पढ़ी रसीदें छुपाएं", + "Show timestamps in 12 hour format (e.g. 2:30pm)": "१२ घंटे प्रारूप में टाइमस्टैम्प दिखाएं (उदहारण:२:३० अपराह्न बजे)", + "Always show message timestamps": "हमेशा संदेश टाइमस्टैम्प दिखाएं", + "Autoplay GIFs and videos": "जीआईएफ और वीडियो को स्वत: प्ले करें", + "Always show encryption icons": "हमेशा एन्क्रिप्शन आइकन दिखाएं", + "Enable automatic language detection for syntax highlighting": "वाक्यविन्यास हाइलाइटिंग के लिए स्वत: भाषा का पता प्रणाली सक्षम करें", + "Hide avatars in user and room mentions": "उपयोगकर्ता और रूम के उल्लेखों में अवतार छुपाएं", + "Disable big emoji in chat": "बातचीत में बड़ा इमोजी अक्षम करें", + "Don't send typing notifications": "टाइपिंग नोटिफिकेशन न भेजें", + "Automatically replace plain text Emoji": "स्वचालित रूप से सादा पाठ इमोजी को प्रतिस्थापित करें", + "Mirror local video feed": "स्थानीय वीडियो फ़ीड को आईना करें", + "Disable Community Filter Panel": "सामुदायिक फ़िल्टर पैनल अक्षम करें", + "Disable Peer-to-Peer for 1:1 calls": "१:१ कॉल के लिए पीयर-टू-पीयर अक्षम करें", + "Send analytics data": "विश्लेषण डेटा भेजें", + "Never send encrypted messages to unverified devices from this device": "इस डिवाइस से असत्यापित डिवाइस पर एन्क्रिप्टेड संदेश कभी न भेजें", + "Never send encrypted messages to unverified devices in this room from this device": "इस डिवाइस से असत्यापित डिवाइस पर एन्क्रिप्टेड संदेश कभी न भेजें", + "Enable inline URL previews by default": "डिफ़ॉल्ट रूप से इनलाइन यूआरएल पूर्वावलोकन सक्षम करें", + "Enable URL previews for this room (only affects you)": "इस रूम के लिए यूआरएल पूर्वावलोकन सक्षम करें (केवल आपको प्रभावित करता है)", + "Enable URL previews by default for participants in this room": "इस रूम में प्रतिभागियों के लिए डिफ़ॉल्ट रूप से यूआरएल पूर्वावलोकन सक्षम करें", + "Room Colour": "रूम का रंग", + "Pin rooms I'm mentioned in to the top of the room list": "रूम की सूची के शीर्ष पर पिन रूम का उल्लेख करें", + "Pin unread rooms to the top of the room list": "रूम की सूची के शीर्ष पर अपठित रूम पिन करें", + "Enable widget screenshots on supported widgets": "समर्थित विजेट्स पर विजेट स्क्रीनशॉट सक्षम करें", + "Show empty room list headings": "खाली रूम सूची शीर्षलेख दिखाएं", + "Show developer tools": "डेवलपर टूल दिखाएं", + "Collecting app version information": "ऐप संस्करण जानकारी एकत्रित कर रहा हैं", + "Collecting logs": "लॉग एकत्रित कर रहा हैं", + "Uploading report": "रिपोर्ट अपलोड हो रहा है", + "Waiting for response from server": "सर्वर से प्रतिक्रिया की प्रतीक्षा कर रहा है", + "Messages containing my display name": "मेरे प्रदर्शन नाम वाले संदेश", + "Messages containing my user name": "मेरे उपयोगकर्ता नाम युक्त संदेश", + "Messages in one-to-one chats": "एक-से-एक चैट में संदेश", + "Messages in group chats": "समूह चैट में संदेश", + "When I'm invited to a room": "जब मुझे एक रूम में आमंत्रित किया जाता है", + "Call invitation": "कॉल आमंत्रण", + "Messages sent by bot": "रोबॉट द्वारा भेजे गए संदेश", + "Active call (%(roomName)s)": "सक्रिय कॉल (%(roomName)s)", + "unknown caller": "अज्ञात फ़ोन करने वाला", + "Incoming voice call from %(name)s": "%(name)s से आने वाली ध्वनि कॉल", + "Incoming video call from %(name)s": "%(name)s से आने वाली वीडियो कॉल", + "Incoming call from %(name)s": "%(name)s से आने वाली कॉल", + "Decline": "पतन", + "Accept": "स्वीकार", + "Error": "त्रुटि", + "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains": "एक टेक्स्ट संदेश %(msisdn)s को भेजा गया है। कृपया इसमें सत्यापन कोड दर्ज करें", + "Incorrect verification code": "गलत सत्यापन कोड", + "Enter Code": "कोड दर्ज करें", + "Submit": "जमा करें", + "Phone": "फ़ोन", + "Add phone number": "फोन नंबर डालें", + "Add": "जोड़े", + "Failed to upload profile picture!": "प्रोफाइल तस्वीर अपलोड करने में विफल!", + "Upload new:": "नया अपलोड करें:", + "No display name": "कोई प्रदर्शन नाम नहीं", + "New passwords don't match": "नए पासवर्ड मेल नहीं खाते हैं", + "Passwords can't be empty": "पासवर्ड खाली नहीं हो सकते हैं", + "Warning!": "चेतावनी!", + "Changing password will currently reset any end-to-end encryption keys on all devices, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "पासवर्ड बदलना वर्तमान में सभी उपकरणों पर किसी भी एंड-टू-एंड एन्क्रिप्शन कुंजी को रीसेट कर देगा, एन्क्रिप्टेड चैट इतिहास को अपठनीय बनायेगा, जब तक कि आप पहले अपनी रूम कुंजियां निर्यात न करें और बाद में उन्हें फिर से आयात न करें। भविष्य में यह सुधार होगा।", + "Export E2E room keys": "E2E रूम कुंजी निर्यात करें", + "Do you want to set an email address?": "क्या आप एक ईमेल पता सेट करना चाहते हैं?", + "Current password": "वर्तमान पासवर्ड", + "Password": "पासवर्ड", + "New Password": "नया पासवर्ड", + "Confirm password": "पासवर्ड की पुष्टि कीजिये", + "Change Password": "पासवर्ड बदलें", + "Your home server does not support device management.": "आपका होम सर्वर डिवाइस प्रबंधन का समर्थन नहीं करता है।", + "Unable to load device list": "डिवाइस सूची लोड करने में असमर्थ", + "Authentication": "प्रमाणीकरण", + "Delete %(count)s devices|other": "%(count)s यंत्र हटाएं", + "Delete %(count)s devices|one": "यंत्र हटाएं", + "Device ID": "यंत्र आईडी", + "Device Name": "यंत्र का नाम", + "Last seen": "अंतिम बार देखा गया", + "Select devices": "यंत्रो का चयन करें", + "Failed to set display name": "प्रदर्शन नाम सेट करने में विफल", + "Disable Notifications": "नोटीफिकेशन निष्क्रिय करें", + "Enable Notifications": "सूचनाएं सक्षम करें", + "Delete Backup": "बैकअप हटाएं", + "Delete your backed up encryption keys from the server? You will no longer be able to use your recovery key to read encrypted message history": "सर्वर से अपनी बैक अप एन्क्रिप्शन कुंजी हटाएं? एन्क्रिप्टेड संदेश इतिहास पढ़ने के लिए अब आप अपनी रिकवरी कुंजी का उपयोग नहीं कर पाएंगे", + "Delete backup": "बैकअप हटाएं", + "Unable to load key backup status": "कुंजी बैकअप स्थिति लोड होने में असमर्थ", + "This device is uploading keys to this backup": "यह यंत्र इस बैकअप में कुंजी अपलोड कर रहा है", + "This device is not uploading keys to this backup": "यह यंत्र बैकअप में कुंजी अपलोड नहीं कर रहा है", + "Backup has a valid signature from this device": "इस डिवाइस से बैकअप में वैध हस्ताक्षर है" } From f58cf6ccd12e59c0c7282dfc72a8cd76d428df11 Mon Sep 17 00:00:00 2001 From: Karol Kosek Date: Tue, 27 Nov 2018 20:05:54 +0000 Subject: [PATCH 042/237] Translated using Weblate (Polish) Currently translated at 88.6% (1197 of 1350 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/pl/ --- src/i18n/strings/pl.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/pl.json b/src/i18n/strings/pl.json index b16fdd7889..03a7fe4de0 100644 --- a/src/i18n/strings/pl.json +++ b/src/i18n/strings/pl.json @@ -1209,5 +1209,7 @@ "If the other version of Riot is still open in another tab, please close it as using Riot on the same host with both lazy loading enabled and disabled simultaneously will cause issues.": "Jeśli inna wersja Riot jest nadal otwarta w innej zakładce, proszę zamknij ją, ponieważ używanie Riot na tym samym komputerze z włączonym i wyłączonym jednocześnie leniwym ładowaniem będzie powodować problemy.", "And %(count)s more...|other": "I %(count)s więcej…", "Delete Backup": "Usuń Kopię Zapasową", - "Delete backup": "Usuń Kopię Zapasową" + "Delete backup": "Usuń Kopię Zapasową", + "Unable to load! Check your network connectivity and try again.": "Nie można załadować! Sprawdź połączenie sieciowe i spróbuj ponownie.", + "Algorithm: ": "Algorytm: " } From 2b027b716f1bb43639fac2e46b663a6c92b74296 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 27 Nov 2018 14:23:28 -0700 Subject: [PATCH 043/237] Suppress CORS errors in the 'failed to join room' dialog This isn't pretty, but it does address https://github.com/vector-im/riot-web/issues/7526 Tested with Chrome and Firefox to ensure the message is stable enough to parse. --- src/i18n/strings/en_EN.json | 1 + src/stores/RoomViewStore.js | 3 +++ 2 files changed, 4 insertions(+) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 557ef62edf..c80cbe6c55 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -220,6 +220,7 @@ "Your browser does not support the required cryptography extensions": "Your browser does not support the required cryptography extensions", "Not a valid Riot keyfile": "Not a valid Riot keyfile", "Authentication check failed: incorrect password?": "Authentication check failed: incorrect password?", + "There was an error joining the room": "There was an error joining the room", "Sorry, your homeserver is too old to participate in this room.": "Sorry, your homeserver is too old to participate in this room.", "Please contact your homeserver administrator.": "Please contact your homeserver administrator.", "Failed to join room": "Failed to join room", diff --git a/src/stores/RoomViewStore.js b/src/stores/RoomViewStore.js index f15925f480..5ddd84c07c 100644 --- a/src/stores/RoomViewStore.js +++ b/src/stores/RoomViewStore.js @@ -224,6 +224,9 @@ class RoomViewStore extends Store { err: err, }); let msg = err.message ? err.message : JSON.stringify(err); + if (msg && msg.startsWith("CORS request rejected")) { + msg = _t("There was an error joining the room"); + } if (err.errcode === 'M_INCOMPATIBLE_ROOM_VERSION') { msg =
    {_t("Sorry, your homeserver is too old to participate in this room.")}
    From 2d8a07224c48815375db3608c9abf00cc00376e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20C?= Date: Tue, 27 Nov 2018 23:56:32 +0000 Subject: [PATCH 044/237] Translated using Weblate (French) Currently translated at 100.0% (1353 of 1353 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/fr/ --- src/i18n/strings/fr.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index 7a629a1e78..b9592d516f 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -1377,5 +1377,8 @@ "Not a valid recovery key": "Ce n'est pas une clé de récupération valide", "Access your secure message history and set up secure messaging by entering your recovery key.": "Accédez à l'historique sécurisé de vos messages et configurez la messagerie sécurisée en renseignant votre clé de récupération.", "If you've forgotten your recovery passphrase you can ": "Si vous avez oublié votre clé de récupération vous pouvez ", - "Key Backup": "Sauvegarde de clés" + "Key Backup": "Sauvegarde de clés", + "Failed to perform homeserver discovery": "Échec lors de la découverte du serveur d'accueil", + "Invalid homeserver discovery response": "Réponse de découverte du serveur d'accueil non valide", + "Cannot find homeserver": "Le serveur d'accueil est introuvable" } From 3aa768f776150b703251f558d77f98d287499fdd Mon Sep 17 00:00:00 2001 From: Szimszon Date: Thu, 29 Nov 2018 07:22:46 +0000 Subject: [PATCH 045/237] Translated using Weblate (Hungarian) Currently translated at 100.0% (1353 of 1353 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/hu/ --- src/i18n/strings/hu.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index 3bf4da30cb..b7c8fe17a1 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -1377,5 +1377,8 @@ "Not a valid recovery key": "Nem helyreállítási kulcs", "Access your secure message history and set up secure messaging by entering your recovery key.": "A helyreállítási kulcs megadásával hozzáférhetsz a régi biztonságos üzeneteidhez és beállíthatod a biztonságos üzenetküldést.", "If you've forgotten your recovery passphrase you can ": "Ha elfelejtetted a helyreállítási jelmondatot ", - "Key Backup": "Kulcs mentés" + "Key Backup": "Kulcs mentés", + "Failed to perform homeserver discovery": "A Matrix szerver felderítése sikertelen", + "Invalid homeserver discovery response": "A Matrix szerver felderítésére kapott válasz érvénytelen", + "Cannot find homeserver": "Matrix szerver nem található" } From 987ad0b0db9194dfbe4df00741f413f0baeac27e Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 29 Nov 2018 15:05:53 -0700 Subject: [PATCH 046/237] Check if users exist before inviting them and communicate errors Fixes https://github.com/vector-im/riot-web/issues/3283 Fixes https://github.com/vector-im/riot-web/issues/3968 Fixes https://github.com/vector-im/riot-web/issues/4308 Fixes https://github.com/vector-im/riot-web/issues/1597 Fixes https://github.com/vector-im/riot-web/issues/6790 This does 3 things: * Makes the `MultiInviter` check for a user profile before attempting an invite. This is to prove the user exists. * Use the `MultiInviter` everywhere to avoid duplicating the logic. Although a couple places only invite one user, it is still worthwhile. * Communicate errors from the `MultiInviter` to the user in all cases. This is done through dialogs, where some existed previously but were not invoked. Specifically to the 403 error not working: What was happening was the `MultiInviter` loop was setting the `fatal` flag, but that didn't resolve the promise it stored. This caused a promise to always be open, therefore never hitting a dialog. --- src/RoomInvite.js | 65 +++++++++++++----------- src/SlashCommands.js | 10 +++- src/components/views/rooms/MemberInfo.js | 11 +++- src/i18n/strings/en_EN.json | 4 ++ src/utils/MultiInviter.js | 45 ++++++++++++---- 5 files changed, 90 insertions(+), 45 deletions(-) diff --git a/src/RoomInvite.js b/src/RoomInvite.js index a96d1b2f6b..32c521bb48 100644 --- a/src/RoomInvite.js +++ b/src/RoomInvite.js @@ -1,6 +1,6 @@ /* Copyright 2016 OpenMarket Ltd -Copyright 2017 New Vector Ltd +Copyright 2017, 2018 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,6 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import React from 'react'; import MatrixClientPeg from './MatrixClientPeg'; import MultiInviter from './utils/MultiInviter'; import Modal from './Modal'; @@ -25,18 +26,6 @@ import dis from './dispatcher'; import DMRoomMap from './utils/DMRoomMap'; import { _t } from './languageHandler'; -export function inviteToRoom(roomId, addr) { - const addrType = getAddressType(addr); - - if (addrType == 'email') { - return MatrixClientPeg.get().inviteByEmail(roomId, addr); - } else if (addrType == 'mx-user-id') { - return MatrixClientPeg.get().invite(roomId, addr); - } else { - throw new Error('Unsupported address'); - } -} - /** * Invites multiple addresses to a room * Simpler interface to utils/MultiInviter but with @@ -46,9 +35,9 @@ export function inviteToRoom(roomId, addr) { * @param {string[]} addrs Array of strings of addresses to invite. May be matrix IDs or 3pids. * @returns {Promise} Promise */ -export function inviteMultipleToRoom(roomId, addrs) { +function inviteMultipleToRoom(roomId, addrs) { const inviter = new MultiInviter(roomId); - return inviter.invite(addrs); + return inviter.invite(addrs).then(addrs => Promise.resolve({addrs, inviter})); } export function showStartChatInviteDialog() { @@ -129,8 +118,8 @@ function _onStartChatFinished(shouldInvite, addrs) { createRoom().then((roomId) => { room = MatrixClientPeg.get().getRoom(roomId); return inviteMultipleToRoom(roomId, addrTexts); - }).then((addrs) => { - return _showAnyInviteErrors(addrs, room); + }).then((result) => { + return _showAnyInviteErrors(result.addrs, room, result.inviter); }).catch((err) => { console.error(err.stack); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); @@ -148,9 +137,9 @@ function _onRoomInviteFinished(roomId, shouldInvite, addrs) { const addrTexts = addrs.map((addr) => addr.address); // Invite new users to a room - inviteMultipleToRoom(roomId, addrTexts).then((addrs) => { + inviteMultipleToRoom(roomId, addrTexts).then((result) => { const room = MatrixClientPeg.get().getRoom(roomId); - return _showAnyInviteErrors(addrs, room); + return _showAnyInviteErrors(result.addrs, room, result.inviter); }).catch((err) => { console.error(err.stack); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); @@ -169,22 +158,36 @@ function _isDmChat(addrTexts) { } } -function _showAnyInviteErrors(addrs, room) { +function _showAnyInviteErrors(addrs, room, inviter) { // Show user any errors - const errorList = []; - for (const addr of Object.keys(addrs)) { - if (addrs[addr] === "error") { - errorList.push(addr); + const failedUsers = Object.keys(addrs).filter(a => addrs[a] === 'error'); + if (failedUsers.length === 1 && inviter.fatal) { + // Just get the first message because there was a fatal problem on the first + // user. This usually means that no other users were attempted, making it + // pointless for us to list who failed exactly. + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createTrackedDialog('Failed to invite users to the room', '', ErrorDialog, { + title: _t("Failed to invite users to the room:", {roomName: room.name}), + description: inviter.getErrorText(failedUsers[0]), + }); + } else { + const errorList = []; + for (const addr of failedUsers) { + if (addrs[addr] === "error") { + const reason = inviter.getErrorText(addr); + errorList.push(addr + ": " + reason); + } + } + + if (errorList.length > 0) { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createTrackedDialog('Failed to invite the following users to the room', '', ErrorDialog, { + title: _t("Failed to invite the following users to the %(roomName)s room:", {roomName: room.name}), + description: errorList.join(
    ), + }); } } - if (errorList.length > 0) { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createTrackedDialog('Failed to invite the following users to the room', '', ErrorDialog, { - title: _t("Failed to invite the following users to the %(roomName)s room:", {roomName: room.name}), - description: errorList.join(", "), - }); - } return addrs; } diff --git a/src/SlashCommands.js b/src/SlashCommands.js index 8a34ba7ab1..eca4075fe9 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -26,6 +26,7 @@ import Modal from './Modal'; import SettingsStore, {SettingLevel} from './settings/SettingsStore'; import {MATRIXTO_URL_PATTERN} from "./linkify-matrix"; import * as querystring from "querystring"; +import MultiInviter from './utils/MultiInviter'; class Command { @@ -142,7 +143,14 @@ export const CommandMap = { if (args) { const matches = args.match(/^(\S+)$/); if (matches) { - return success(MatrixClientPeg.get().invite(roomId, matches[1])); + // We use a MultiInviter to re-use the invite logic, even though + // we're only inviting one user. + const userId = matches[1]; + const inviter = new MultiInviter(roomId); + return success(inviter.invite([userId]).then(() => { + if (inviter.getCompletionState(userId) !== "invited") + throw new Error(inviter.getErrorText(userId)); + })); } } return reject(this.getUsage()); diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 7ff52ecbb6..17b1311c4f 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -41,6 +41,7 @@ import withMatrixClient from '../../../wrappers/withMatrixClient'; import AccessibleButton from '../elements/AccessibleButton'; import RoomViewStore from '../../../stores/RoomViewStore'; import SdkConfig from '../../../SdkConfig'; +import MultiInviter from "../../../utils/MultiInviter"; module.exports = withMatrixClient(React.createClass({ displayName: 'MemberInfo', @@ -714,12 +715,18 @@ module.exports = withMatrixClient(React.createClass({ const roomId = member && member.roomId ? member.roomId : RoomViewStore.getRoomId(); const onInviteUserButton = async() => { try { - await cli.invite(roomId, member.userId); + // We use a MultiInviter to re-use the invite logic, even though + // we're only inviting one user. + const inviter = new MultiInviter(roomId); + await inviter.invite([member.userId]).then(() => { + if (inviter.getCompletionState(userId) !== "invited") + throw new Error(inviter.getErrorText(userId)); + }); } catch (err) { const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); Modal.createTrackedDialog('Failed to invite', '', ErrorDialog, { title: _t('Failed to invite'), - description: ((err && err.message) ? err.message : "Operation failed"), + description: ((err && err.message) ? err.message : _t("Operation failed")), }); } }; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index f9980f5645..5d5a65cb63 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -106,6 +106,7 @@ "Failed to invite user": "Failed to invite user", "Operation failed": "Operation failed", "Failed to invite": "Failed to invite", + "Failed to invite users to the room:": "Failed to invite users to the room:", "Failed to invite the following users to the %(roomName)s room:": "Failed to invite the following users to the %(roomName)s room:", "You need to be logged in.": "You need to be logged in.", "You need to be able to invite users to do that.": "You need to be able to invite users to do that.", @@ -220,6 +221,9 @@ "Your browser does not support the required cryptography extensions": "Your browser does not support the required cryptography extensions", "Not a valid Riot keyfile": "Not a valid Riot keyfile", "Authentication check failed: incorrect password?": "Authentication check failed: incorrect password?", + "You do not have permission to invite people to this room.": "You do not have permission to invite people to this room.", + "User %(user_id)s does not exist": "User %(user_id)s does not exist", + "Unknown server error": "Unknown server error", "Sorry, your homeserver is too old to participate in this room.": "Sorry, your homeserver is too old to participate in this room.", "Please contact your homeserver administrator.": "Please contact your homeserver administrator.", "Failed to join room": "Failed to join room", diff --git a/src/utils/MultiInviter.js b/src/utils/MultiInviter.js index b3e7fc495a..ad10f28edf 100644 --- a/src/utils/MultiInviter.js +++ b/src/utils/MultiInviter.js @@ -1,6 +1,6 @@ /* Copyright 2016 OpenMarket Ltd -Copyright 2017 New Vector Ltd +Copyright 2017, 2018 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,9 +17,9 @@ limitations under the License. import MatrixClientPeg from '../MatrixClientPeg'; import {getAddressType} from '../UserAddress'; -import {inviteToRoom} from '../RoomInvite'; import GroupStore from '../stores/GroupStore'; import Promise from 'bluebird'; +import {_t} from "../languageHandler"; /** * Invites multiple addresses to a room or group, handling rate limiting from the server @@ -49,7 +49,7 @@ export default class MultiInviter { * Invite users to this room. This may only be called once per * instance of the class. * - * @param {array} addresses Array of addresses to invite + * @param {array} addrs Array of addresses to invite * @returns {Promise} Resolved when all invitations in the queue are complete */ invite(addrs) { @@ -88,12 +88,30 @@ export default class MultiInviter { return this.errorTexts[addr]; } + async _inviteToRoom(roomId, addr) { + const addrType = getAddressType(addr); + + if (addrType === 'email') { + return MatrixClientPeg.get().inviteByEmail(roomId, addr); + } else if (addrType === 'mx-user-id') { + const profile = await MatrixClientPeg.get().getProfileInfo(addr); + if (!profile) { + return Promise.reject({errcode: "M_NOT_FOUND", error: "User does not have a profile."}); + } + + return MatrixClientPeg.get().invite(roomId, addr); + } else { + throw new Error('Unsupported address'); + } + } + + _inviteMore(nextIndex) { if (this._canceled) { return; } - if (nextIndex == this.addrs.length) { + if (nextIndex === this.addrs.length) { this.busy = false; this.deferred.resolve(this.completionStates); return; @@ -111,7 +129,7 @@ export default class MultiInviter { // don't re-invite (there's no way in the UI to do this, but // for sanity's sake) - if (this.completionStates[addr] == 'invited') { + if (this.completionStates[addr] === 'invited') { this._inviteMore(nextIndex + 1); return; } @@ -120,7 +138,7 @@ export default class MultiInviter { if (this.groupId !== null) { doInvite = GroupStore.inviteUserToGroup(this.groupId, addr); } else { - doInvite = inviteToRoom(this.roomId, addr); + doInvite = this._inviteToRoom(this.roomId, addr); } doInvite.then(() => { @@ -129,29 +147,34 @@ export default class MultiInviter { this.completionStates[addr] = 'invited'; this._inviteMore(nextIndex + 1); - }, (err) => { + }).catch((err) => { if (this._canceled) { return; } let errorText; let fatal = false; - if (err.errcode == 'M_FORBIDDEN') { + if (err.errcode === 'M_FORBIDDEN') { fatal = true; - errorText = 'You do not have permission to invite people to this room.'; - } else if (err.errcode == 'M_LIMIT_EXCEEDED') { + errorText = _t('You do not have permission to invite people to this room.'); + } else if (err.errcode === 'M_LIMIT_EXCEEDED') { // we're being throttled so wait a bit & try again setTimeout(() => { this._inviteMore(nextIndex); }, 5000); return; + } else if(err.errcode === "M_NOT_FOUND") { + errorText = _t("User %(user_id)s does not exist", {user_id: addr}); } else { - errorText = 'Unknown server error'; + errorText = _t('Unknown server error'); } this.completionStates[addr] = 'error'; this.errorTexts[addr] = errorText; this.busy = !fatal; + this.fatal = fatal; if (!fatal) { this._inviteMore(nextIndex + 1); + } else { + this.deferred.resolve(this.completionStates); } }); } From 84b568a2358740b9d72cae33327adb0126b92fc1 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 29 Nov 2018 15:16:45 -0700 Subject: [PATCH 047/237] Appease the linter --- src/SlashCommands.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/SlashCommands.js b/src/SlashCommands.js index eca4075fe9..24328d6372 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -148,8 +148,9 @@ export const CommandMap = { const userId = matches[1]; const inviter = new MultiInviter(roomId); return success(inviter.invite([userId]).then(() => { - if (inviter.getCompletionState(userId) !== "invited") + if (inviter.getCompletionState(userId) !== "invited") { throw new Error(inviter.getErrorText(userId)); + } })); } } From 0834985ff4946a5a331898e7e58a58075d78a0dd Mon Sep 17 00:00:00 2001 From: Osoitz Date: Sat, 1 Dec 2018 16:58:12 +0000 Subject: [PATCH 048/237] Translated using Weblate (Basque) Currently translated at 100.0% (1353 of 1353 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/eu/ --- src/i18n/strings/eu.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/eu.json b/src/i18n/strings/eu.json index a520d6eadb..ce5778b749 100644 --- a/src/i18n/strings/eu.json +++ b/src/i18n/strings/eu.json @@ -1375,5 +1375,8 @@ "Access your secure message history and set up secure messaging by entering your recovery key.": "Atzitu zure mezu seguruen historiala eta ezarri mezularitza segurua zure berreskuratze gakoa sartuz.", "If you've forgotten your recovery passphrase you can ": "Zure berreskuratze pasa-esaldia ahaztu baduzu ditzakezu", "Key Backup": "Gakoen babes-kopia", - "Sign in with single sign-on": "Hai saioa urrats batean" + "Sign in with single sign-on": "Hai saioa urrats batean", + "Failed to perform homeserver discovery": "Huts egin du hasiera-zerbitzarien bilaketak", + "Invalid homeserver discovery response": "Baliogabeko hasiera-zerbitzarien bilaketaren erantzuna", + "Cannot find homeserver": "Ezin izan da hasiera-zerbitzaria aurkitu" } From c163773248ff38ebc95685d36b11fd527a04f426 Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Wed, 28 Nov 2018 08:05:30 +0000 Subject: [PATCH 049/237] Translated using Weblate (Albanian) Currently translated at 99.2% (1343 of 1353 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/sq/ --- src/i18n/strings/sq.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index 24809fb332..98dfdb237a 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -1347,5 +1347,9 @@ "Access your secure message history and set up secure messaging by entering your recovery key.": "Hyni te historiku i mesazheve tuaj të siguruar dhe rregulloni shkëmbim mesazhesh të sigurt duke dhënë kyçin tuaj të rimarrjeve.", "If you've forgotten your recovery passphrase you can ": "Nëse keni harruar frazëkalimin tuaj të rimarrjeve, mund të rregulloni mundësi të reja rimarrjeje", "Key Backup": "Kopjeruajtje Kyçi", - "Sign in with single sign-on": "Bëni hyrjen me hyrje njëshe" + "Sign in with single sign-on": "Bëni hyrjen me hyrje njëshe", + "Disable Peer-to-Peer for 1:1 calls": "Çaktivizoje mekanizmin Peer-to-Peer për thirrje 1 me 1", + "Failed to perform homeserver discovery": "S’u arrit të kryhej zbulim shërbyesi Home", + "Invalid homeserver discovery response": "Përgjigje e pavlefshme zbulimi shërbyesi Home", + "Cannot find homeserver": "S’gjendet dot shërbyesi Home" } From f7a8e144f2066dbff655d0e84e4911cc16464d86 Mon Sep 17 00:00:00 2001 From: Krombel Date: Sun, 2 Dec 2018 00:46:35 +0000 Subject: [PATCH 050/237] Translated using Weblate (German) Currently translated at 98.3% (1330 of 1353 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/de/ --- src/i18n/strings/de_DE.json | 57 ++++++++++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index fffacf786e..0e12104a7d 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -1300,5 +1300,60 @@ "Open Devtools": "Öffne Entwickler-Werkzeuge", "Show developer tools": "Zeige Entwickler-Werkzeuge", "If you would like to create a Matrix account you can register now.": "Wenn du ein Matrix-Konto erstellen möchtest, kannst du dich jetzt registrieren.", - "You are currently using Riot anonymously as a guest.": "Du benutzt aktuell Riot anonym als Gast." + "You are currently using Riot anonymously as a guest.": "Du benutzt aktuell Riot anonym als Gast.", + "Unable to load! Check your network connectivity and try again.": "Konnte nicht geladen werden! Überprüfe deine Netzwerkverbindung und versuche es erneut.", + "Backup of encryption keys to server": "Sichern der Verschlüsselungs-Schlüssel auf dem Server", + "Delete Backup": "Sicherung löschen", + "Delete backup": "Sicherung löschen", + "This device is uploading keys to this backup": "Dieses Gerät lädt Schlüssel zu dieser Sicherung hoch", + "This device is not uploading keys to this backup": "Dieses Gerät lädt keine Schlüssel zu dieser Sicherung hoch", + "Backup has a valid signature from this device": "Sicherung hat eine valide Signatur von diesem Gerät", + "Backup has an invalid signature from verified device ": "Sicherung hat eine invalide Signatur vom verifiziertem Gerät ", + "Backup has an invalid signature from unverified device ": "Sicherung hat eine invalide Signatur vom unverifiziertem Gerät ", + "Backup has a valid signature from verified device x": "Sicherung hat eine valide Signatur vom verifiziertem Gerät x", + "Backup has a valid signature from unverified device ": "Sicherung hat eine valide Signatur vom unverifiziertem Gerät ", + "Backup is not signed by any of your devices": "Sicherung wurde von keinem deiner Geräte signiert", + "Backup version: ": "Sicherungsversion: ", + "Algorithm: ": "Algorithmus: ", + "Restore backup": "Sicherung wiederherstellen", + "No backup is present": "Keine Sicherung verfügbar", + "Start a new backup": "Starte einen neue Sicherung", + "To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of Riot to do this": "Um deinen Chatverlauf nicht zu verlieren, musst du deine Raum-Schlüssel exportieren, bevor du dich abmeldest. Du musst zurück zu einer neueren Riot-Version gehen, um dies zu tun", + "Incompatible Database": "Inkompatible Datenbanken", + "Continue With Encryption Disabled": "Mit deaktivierter Verschlüsselung fortfahren", + "You'll need it if you log out or lose access to this device.": "Du wirst es brauchen, wenn du dich abmeldest oder den Zugang zu diesem Gerät verlierst.", + "Enter a passphrase...": "Passphrase eingeben...", + "Next": "Nächstes", + "That matches!": "Das passt!", + "That doesn't match.": "Das passt nicht.", + "Go back to set it again.": "Gehe zurück und setze es erneut.", + "Repeat your passphrase...": "Wiederhole deine Passphrase...", + "Make a copy of this Recovery Key and keep it safe.": "Mache eine Kopie dieses Wiederherstellungsschlüssels und verwahre ihn sicher.", + "Your Recovery Key": "Dein Wiederherstellungsschlüssel", + "Copy to clipboard": "In Zwischenablage kopieren", + "Download": "Herunterladen", + "I've made a copy": "Ich habe eine Kopie gemacht", + "Print it and store it somewhere safe": "Drucke ihn aus und lagere ihn, wo er sicher ist", + "Save it on a USB key or backup drive": "Speichere ihn auf einem USB-Schlüssel oder Sicherungsslaufwerk", + "Copy it to your personal cloud storage": "Kopiere ihn in deinen persönlichen Cloud-Speicher", + "Got it": "Verstanden", + "Backup created": "Sicherung erstellt", + "Your encryption keys are now being backed up to your Homeserver.": "Deine Verschlüsselungsschlüssel sind nun auf deinem Heimserver gesichert wurden.", + "Create a Recovery Passphrase": "Erstelle eine Wiederherstellungs-Passphrase", + "Confirm Recovery Passphrase": "Bestätige Wiederherstellungs-Passphrase", + "Recovery Key": "Wiederherstellungsschlüssel", + "Keep it safe": "Lager ihn sicher", + "Backing up...": "Am sichern...", + "Create Key Backup": "Erzeuge Schlüsselsicherung", + "Unable to create key backup": "Konnte Schlüsselsicherung nicht erstellen", + "Retry": "Erneut probieren", + "Unable to restore backup": "Konnte Sicherung nicht wiederherstellen", + "No backup found!": "Keine Sicherung gefunden!", + "Backup Restored": "Sicherung wiederhergestellt", + "Enter Recovery Passphrase": "Gebe Wiederherstellungs-Passphrase ein", + "Enter Recovery Key": "Gebe Wiederherstellungsschlüssel ein", + "This looks like a valid recovery key!": "Dies sieht nach einem validen Wiederherstellungsschlüssel aus", + "Not a valid recovery key": "Kein valider Wiederherstellungsschlüssel", + "Key Backup": "Schlüsselsicherung", + "Cannot find homeserver": "Konnte Heimserver nicht finden" } From e25df6afd819df16b74f6869c65df0f393ec505b Mon Sep 17 00:00:00 2001 From: Akarshan Biswas Date: Wed, 28 Nov 2018 06:31:34 +0000 Subject: [PATCH 051/237] Translated using Weblate (Hindi) Currently translated at 26.3% (356 of 1353 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/hi/ --- src/i18n/strings/hi.json | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/hi.json b/src/i18n/strings/hi.json index b043c9d225..4c944f9925 100644 --- a/src/i18n/strings/hi.json +++ b/src/i18n/strings/hi.json @@ -323,5 +323,36 @@ "Unable to load key backup status": "कुंजी बैकअप स्थिति लोड होने में असमर्थ", "This device is uploading keys to this backup": "यह यंत्र इस बैकअप में कुंजी अपलोड कर रहा है", "This device is not uploading keys to this backup": "यह यंत्र बैकअप में कुंजी अपलोड नहीं कर रहा है", - "Backup has a valid signature from this device": "इस डिवाइस से बैकअप में वैध हस्ताक्षर है" + "Backup has a valid signature from this device": "इस डिवाइस से बैकअप में वैध हस्ताक्षर है", + "Backup has a valid signature from verified device x": "सत्यापित डिवाइस x से बैकअप में मान्य हस्ताक्षर है", + "Backup has a valid signature from unverified device ": "असत्यापित डिवाइस से बैकअप में मान्य हस्ताक्षर है", + "Backup has an invalid signature from verified device ": "सत्यापित डिवाइस से बैकअप में अमान्य हस्ताक्षर है", + "Backup has an invalid signature from unverified device ": "असत्यापित डिवाइस से बैकअप में अमान्य हस्ताक्षर है", + "Verify...": "सत्यापित करें ...", + "Backup is not signed by any of your devices": "बैकअप आपके किसी भी डिवाइस द्वारा हस्ताक्षरित नहीं है", + "Backup version: ": "बैकअप संस्करण: ", + "Algorithm: ": "कलन विधि: ", + "Restore backup": "बैकअप बहाल करें", + "No backup is present": "कोई बैकअप प्रस्तुत नहीं है", + "Start a new backup": "एक नया बैकअप शुरू करें", + "Error saving email notification preferences": "ईमेल अधिसूचना प्राथमिकताओं को सहेजने में त्रुटि", + "An error occurred whilst saving your email notification preferences.": "आपकी ईमेल अधिसूचना वरीयताओं को सहेजते समय एक त्रुटि हुई।", + "Keywords": "कीवर्ड", + "Enter keywords separated by a comma:": "अल्पविराम से अलग करके कीवर्ड दर्ज करें:", + "OK": "ठीक", + "Failed to change settings": "सेटिंग्स बदलने में विफल", + "Can't update user notification settings": "उपयोगकर्ता अधिसूचना सेटिंग्स अद्यतन नहीं कर सकते हैं", + "Failed to update keywords": "कीवर्ड अपडेट करने में विफल", + "Messages containing keywords": "कीवर्ड युक्त संदेश", + "Notify for all other messages/rooms": "अन्य सभी संदेशों/रूम के लिए सूचित करें", + "Notify me for anything else": "मुझे किसी और चीज़ के लिए सूचित करें", + "Enable notifications for this account": "इस खाते के लिए अधिसूचनाएं सक्षम करें", + "All notifications are currently disabled for all targets.": "सभी सूचनाएं वर्तमान में सभी लक्ष्यों के लिए अक्षम हैं।", + "Add an email address above to configure email notifications": "ईमेल अधिसूचनाओं को कॉन्फ़िगर करने के लिए उपरोक्त एक ईमेल पता जोड़ें", + "Enable email notifications": "ईमेल अधिसूचनाएं सक्षम करें", + "Notifications on the following keywords follow rules which can’t be displayed here:": "निम्नलिखित कीवर्ड पर अधिसूचनाएं नियमों का पालन करती हैं जिन्हें यहां प्रदर्शित नहीं किया जा सकता है:", + "Unable to fetch notification target list": "अधिसूचना लक्ष्य सूची लाने में असमर्थ", + "Notification targets": "अधिसूचना के लक्ष्य", + "Advanced notification settings": "उन्नत अधिसूचना सेटिंग्स", + "There are advanced notifications which are not shown here": "उन्नत सूचनाएं हैं जो यहां दिखाई नहीं दी गई हैं" } From dbc3e87e532424cf8d681d68bbbe3519d71c298d Mon Sep 17 00:00:00 2001 From: Karol Kosek Date: Mon, 3 Dec 2018 22:59:30 +0000 Subject: [PATCH 052/237] Translated using Weblate (Polish) Currently translated at 88.5% (1198 of 1353 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/pl/ --- src/i18n/strings/pl.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/pl.json b/src/i18n/strings/pl.json index 03a7fe4de0..052823ec59 100644 --- a/src/i18n/strings/pl.json +++ b/src/i18n/strings/pl.json @@ -1211,5 +1211,6 @@ "Delete Backup": "Usuń Kopię Zapasową", "Delete backup": "Usuń Kopię Zapasową", "Unable to load! Check your network connectivity and try again.": "Nie można załadować! Sprawdź połączenie sieciowe i spróbuj ponownie.", - "Algorithm: ": "Algorytm: " + "Algorithm: ": "Algorytm: ", + "Pin unread rooms to the top of the room list": "Przypnij nieprzeczytanie pokoje na górę listy pokojów" } From e131a76e80993cbedf566d2802a087f53345db5e Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Tue, 4 Dec 2018 03:16:25 +0000 Subject: [PATCH 053/237] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (1355 of 1355 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/zh_Hant/ --- src/i18n/strings/zh_Hant.json | 82 ++++++++++++++++++++++++++++++++++- 1 file changed, 81 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index 242990264c..f955e800e2 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -1300,5 +1300,85 @@ "You are currently using Riot anonymously as a guest.": "您目前是以訪客的身份匿名使用 Riot。", "You are an administrator of this community. You will not be able to rejoin without an invite from another administrator.": "您是此社群的管理員。您將無法在沒有其他管理員的邀請下重新加入。", "Open Devtools": "開啟開發者工具", - "Show developer tools": "顯示開發者工具" + "Show developer tools": "顯示開發者工具", + "Unable to load! Check your network connectivity and try again.": "無法載入!請檢查您的網路連線狀態並再試一次。", + "Backup of encryption keys to server": "將加密金鑰備份到伺服器", + "Delete Backup": "刪除備份", + "Delete your backed up encryption keys from the server? You will no longer be able to use your recovery key to read encrypted message history": "從伺服器刪除您已備份的加密金鑰?您將無法再使用您的復原金鑰來讀取加密的訊息歷史", + "Delete backup": "刪除備份", + "Unable to load key backup status": "無法載入金鑰備份狀態", + "This device is uploading keys to this backup": "此裝置正在上傳金鑰到此備份", + "This device is not uploading keys to this backup": "此裝置並未上傳金鑰到此備份", + "Backup has a valid signature from this device": "備份有從此裝置而來的有效簽章", + "Backup has a valid signature from verified device x": "備份有從已驗證的 x 裝置而來的有效簽章", + "Backup has a valid signature from unverified device ": "備份有從未驗證的 裝置而來的有效簽章", + "Backup has an invalid signature from verified device ": "備份有從已驗證的 裝置而來的無效簽章", + "Backup has an invalid signature from unverified device ": "備份有從未驗證的 裝置而來的無效簽章", + "Backup is not signed by any of your devices": "備份未被您的任何裝置簽署", + "Backup version: ": "備份版本: ", + "Algorithm: ": "演算法: ", + "Restore backup": "恢復備份", + "No backup is present": "沒有備份", + "Start a new backup": "開始新備份", + "Please review and accept all of the homeserver's policies": "請審閱並接受家伺服器的所有政策", + "To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of Riot to do this": "為了避免遺失您的聊天歷史,您必須在登出前匯出您的聊天室金鑰。您必須回到較新的 Riot 才能執行此動作", + "You've previously used a newer version of Riot on %(host)s. To use this version again with end to end encryption, you will need to sign out and back in again. ": "您先前在 %(host)s 上使用較新的 Riot 版本。要再次與此版本一同使用端到端加密,您將需要登出並再次登入。 ", + "Incompatible Database": "不相容的資料庫", + "Continue With Encryption Disabled": "在停用加密的情況下繼續", + "Secure your encrypted message history with a Recovery Passphrase.": "以復原密碼保證您的加密訊息歷史安全。", + "You'll need it if you log out or lose access to this device.": "如果您登出或是遺失對此裝置的存取權,您將會需要它。", + "Enter a passphrase...": "輸入密碼……", + "Next": "下一個", + "If you don't want encrypted message history to be availble on other devices, .": "如果您不想要讓加密的訊息歷史在其他裝置上可用,。", + "Or, if you don't want to create a Recovery Passphrase, skip this step and .": "或是,如果您不想建立復原密碼,跳過此步驟並。", + "That matches!": "符合!", + "That doesn't match.": "不符合。", + "Go back to set it again.": "回去重新設定它。", + "Type in your Recovery Passphrase to confirm you remember it. If it helps, add it to your password manager or store it somewhere safe.": "輸入您的復原密碼以確認您記得它。如果可以的話,把它加入到您的密碼管理員或是把它儲存在其他安全的地方。", + "Repeat your passphrase...": "重覆您的密碼……", + "Make a copy of this Recovery Key and keep it safe.": "複製這把復原金鑰並把它放在安全的地方。", + "As a safety net, you can use it to restore your encrypted message history if you forget your Recovery Passphrase.": "做為安全網,您可以在忘記您的復原密碼時使用它來復原您的加密訊息歷史。", + "Your Recovery Key": "您的復原金鑰", + "Copy to clipboard": "複製到剪貼簿", + "Download": "下載", + "I've made a copy": "我已經有副本了", + "Your Recovery Key has been copied to your clipboard, paste it to:": "您的復原金鑰已複製到您的剪貼簿,將它貼上到:", + "Your Recovery Key is in your Downloads folder.": "您的復原金鑰在您的下載資料夾。", + "Print it and store it somewhere safe": "列印它並存放在安全的地方", + "Save it on a USB key or backup drive": "將它儲存到 USB 金鑰或備份磁碟上", + "Copy it to your personal cloud storage": "將它複製 到您的個人雲端儲存", + "Got it": "知道了", + "Backup created": "備份已建立", + "Your encryption keys are now being backed up to your Homeserver.": "您的加密金鑰已經備份到您的家伺服器了。", + "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another device.": "在沒有設定安全訊息復原的狀況下,您將無法在登出或使用其他裝置後復原您的已加密訊息歷史。", + "Set up Secure Message Recovery": "設定安全訊息復原", + "Create a Recovery Passphrase": "建立復原密碼", + "Confirm Recovery Passphrase": "確認復原密碼", + "Recovery Key": "復原金鑰", + "Keep it safe": "保持安全", + "Backing up...": "正在備份……", + "Create Key Backup": "建立金鑰備份", + "Unable to create key backup": "無法建立金鑰備份", + "Retry": "重試", + "Unable to load backup status": "無法載入備份狀態", + "Unable to restore backup": "無法復原備份", + "No backup found!": "找不到備份!", + "Backup Restored": "備份已復原", + "Failed to decrypt %(failedCount)s sessions!": "解密 %(failedCount)s 工作階段失敗!", + "Restored %(sessionCount)s session keys": "%(sessionCount)s 工作階段金鑰已復原", + "Enter Recovery Passphrase": "輸入復原密碼", + "Access your secure message history and set up secure messaging by entering your recovery passphrase.": "存取您的安全訊息歷史並透過輸入您的復原密碼來設定安全訊息。", + "If you've forgotten your recovery passphrase you can use your recovery key or set up new recovery options": "如果您忘記您的復原密碼,您可以使用您的復原金鑰設定新的復原選項", + "Enter Recovery Key": "輸入復原金鑰", + "This looks like a valid recovery key!": "看起來是有效的復原金鑰!", + "Not a valid recovery key": "不是有效的復原金鑰", + "Access your secure message history and set up secure messaging by entering your recovery key.": "存取您的安全訊息歷史並趟過輸入您的復原金鑰來設定安全傳訊。", + "If you've forgotten your recovery passphrase you can ": "如果您忘記您的復原密碼,您可以", + "Key Backup": "金鑰備份", + "Failed to perform homeserver discovery": "執行家伺服器探索失敗", + "Invalid homeserver discovery response": "無效的家伺服器探索回應", + "Cannot find homeserver": "找不到家伺服器", + "Sign in with single sign-on": "以單一登入來登入", + "File is too big. Maximum file size is %(fileSize)s": "檔案太大了。最大的檔案大小為 %(fileSize)s", + "The following files cannot be uploaded:": "下列檔案無法上傳:" } From 8510fe721c0a1760e7c955e5250827b577cc570f Mon Sep 17 00:00:00 2001 From: Szimszon Date: Tue, 4 Dec 2018 07:07:50 +0000 Subject: [PATCH 054/237] Translated using Weblate (Hungarian) Currently translated at 100.0% (1355 of 1355 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/hu/ --- src/i18n/strings/hu.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index b7c8fe17a1..e6c2f2528b 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -1380,5 +1380,7 @@ "Key Backup": "Kulcs mentés", "Failed to perform homeserver discovery": "A Matrix szerver felderítése sikertelen", "Invalid homeserver discovery response": "A Matrix szerver felderítésére kapott válasz érvénytelen", - "Cannot find homeserver": "Matrix szerver nem található" + "Cannot find homeserver": "Matrix szerver nem található", + "File is too big. Maximum file size is %(fileSize)s": "A fájl túl nagy. A maximális fájl méret: %(fileSize)s", + "The following files cannot be uploaded:": "Az alábbi fájlokat nem lehetett feltölteni:" } From c4ad15f9191f28d7bbf8bb72aa2c07a59ad085ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20C?= Date: Tue, 4 Dec 2018 07:44:40 +0000 Subject: [PATCH 055/237] Translated using Weblate (French) Currently translated at 100.0% (1355 of 1355 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/fr/ --- src/i18n/strings/fr.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index b9592d516f..c98e55d1a0 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -1380,5 +1380,7 @@ "Key Backup": "Sauvegarde de clés", "Failed to perform homeserver discovery": "Échec lors de la découverte du serveur d'accueil", "Invalid homeserver discovery response": "Réponse de découverte du serveur d'accueil non valide", - "Cannot find homeserver": "Le serveur d'accueil est introuvable" + "Cannot find homeserver": "Le serveur d'accueil est introuvable", + "File is too big. Maximum file size is %(fileSize)s": "Le fichier est trop gros. La taille maximum est de %(fileSize)s", + "The following files cannot be uploaded:": "Les fichiers suivants n'ont pas pu être envoyés :" } From 5b2c2a0adfe2f95baf2654efe1ed525d4e7b1aa3 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 4 Dec 2018 11:41:04 +0000 Subject: [PATCH 056/237] comment typo --- .../views/dialogs/keybackup/CreateKeyBackupDialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js index 7aa3133874..b0b044613a 100644 --- a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js +++ b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js @@ -178,7 +178,7 @@ export default React.createClass({ this.setState({ passPhrase: e.target.value, // precompute this and keep it in state: zxcvbn is fast but - // we use it in a couple of different places so so point recomputing + // we use it in a couple of different places so no point recomputing // it unnecessarily. zxcvbnResult: scorePassword(e.target.value), }); From 98b239892364fbe11fac84d3255339e9879ade16 Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Tue, 4 Dec 2018 08:10:58 +0000 Subject: [PATCH 057/237] Translated using Weblate (Albanian) Currently translated at 99.3% (1346 of 1355 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/sq/ --- src/i18n/strings/sq.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index 98dfdb237a..1b6b5f3bf3 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -295,7 +295,7 @@ "With your current browser, the look and feel of the application may be completely incorrect, and some or all features may not function. If you want to try it anyway you can continue, but you are on your own in terms of any issues you may encounter!": "Me shfletuesin tuaj të tanishëm, pamja dhe ndjesitë nga aplikacioni mund të jenë plotësisht të pasakta, dhe disa nga ose krejt veçoritë të mos funksionojnë. Nëse doni ta provoni sido qoftë, mund të vazhdoni, por mos u ankoni për çfarëdo problemesh që mund të hasni!", "Checking for an update...": "Po kontrollohet për një përditësim…", "There are advanced notifications which are not shown here": "Ka njoftime të thelluara që nuk shfaqen këtu", - "Show empty room list headings": "", + "Show empty room list headings": "Shfaq krye liste dhomash të zbrazëta", "PM": "PM", "AM": "AM", "Room name or alias": "Emër dhome ose alias", @@ -1351,5 +1351,7 @@ "Disable Peer-to-Peer for 1:1 calls": "Çaktivizoje mekanizmin Peer-to-Peer për thirrje 1 me 1", "Failed to perform homeserver discovery": "S’u arrit të kryhej zbulim shërbyesi Home", "Invalid homeserver discovery response": "Përgjigje e pavlefshme zbulimi shërbyesi Home", - "Cannot find homeserver": "S’gjendet dot shërbyesi Home" + "Cannot find homeserver": "S’gjendet dot shërbyesi Home", + "File is too big. Maximum file size is %(fileSize)s": "Kartela është shumë e madhe. Madhësia maksimum për kartelat është %(fileSize)s", + "The following files cannot be uploaded:": "Kartelat vijuese s’mund të ngarkohen:" } From 3f7a3b8cfb1021147a49bb9f9b94ed08e65c3331 Mon Sep 17 00:00:00 2001 From: Szimszon Date: Tue, 4 Dec 2018 13:42:24 +0000 Subject: [PATCH 058/237] Translated using Weblate (Hungarian) Currently translated at 100.0% (1381 of 1381 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/hu/ --- src/i18n/strings/hu.json | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index e6c2f2528b..a1b521b606 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -1382,5 +1382,31 @@ "Invalid homeserver discovery response": "A Matrix szerver felderítésére kapott válasz érvénytelen", "Cannot find homeserver": "Matrix szerver nem található", "File is too big. Maximum file size is %(fileSize)s": "A fájl túl nagy. A maximális fájl méret: %(fileSize)s", - "The following files cannot be uploaded:": "Az alábbi fájlokat nem lehetett feltölteni:" + "The following files cannot be uploaded:": "Az alábbi fájlokat nem lehetett feltölteni:", + "Use a few words, avoid common phrases": "Néhány szót használj és kerüld el a szokásos szövegeket", + "No need for symbols, digits, or uppercase letters": "Nincs szükség szimbólumokra, számokra vagy nagy betűkre", + "Use a longer keyboard pattern with more turns": "Használj hosszabb billentyűzet mintát több kanyarral", + "Avoid repeated words and characters": "Kerüld a szó-, vagy betűismétlést", + "Avoid sequences": "Kerüld a sorozatokat", + "Avoid recent years": "Kerüld a közeli éveket", + "Avoid years that are associated with you": "Kerüld azokat az éveket amik összefüggésbe hozhatók veled", + "Avoid dates and years that are associated with you": "Kerüld a dátumokat és évszámokat amik összefüggésbe hozhatók veled", + "Capitalization doesn't help very much": "A nagybetűk nem igazán segítenek", + "All-uppercase is almost as easy to guess as all-lowercase": "A csupa nagybetűset majdnem olyan könnyű kitalálni mint a csupa kisbetűset", + "Reversed words aren't much harder to guess": "A megfordított betűrendet sem sokkal nehezebb kitalálni", + "Predictable substitutions like '@' instead of 'a' don't help very much": "Megjósolható helyettesítések mint az „a” helyett a „@” nem sokat segítenek", + "Add another word or two. Uncommon words are better.": "Adj hozzá még egy-két szót. A ritkán használt szavak jobbak.", + "Repeats like \"aaa\" are easy to guess": "Ismétlések mint az „aaa” könnyen kitalálhatók", + "Repeats like \"abcabcabc\" are only slightly harder to guess than \"abc\"": "Az „abcabcabc” sorozatot csak kicsivel nehezebb kitalálni mint az „abc”-t", + "Sequences like abc or 6543 are easy to guess": "Az olyan mint az abc vagy 6543 sorokat könnyű kitalálni", + "Recent years are easy to guess": "A közelmúlt évszámait könnyű kitalálni", + "Dates are often easy to guess": "Általában a dátumokat könnyű kitalálni", + "This is a top-10 common password": "Ez benne van a 10 legelterjedtebb jelszó listájában", + "This is a top-100 common password": "Ez benne van a 100 legelterjedtebb jelszó listájában", + "This is a very common password": "Ez egy nagyon gyakori jelszó", + "This is similar to a commonly used password": "Ez nagyon hasonlít egy gyakori jelszóhoz", + "A word by itself is easy to guess": "Egy szót magában könnyű kitalálni", + "Names and surnames by themselves are easy to guess": "Neveket egymagukban könnyű kitalálni", + "Common names and surnames are easy to guess": "Elterjedt neveket könnyű kitalálni", + "Great! This passphrase looks strong enough.": "Szuper! Ez a jelmondat elég erősnek látszik." } From 6ab9ae7aa5f9ed057253a4ad3fc6a3919a45b8df Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Tue, 4 Dec 2018 15:32:55 +0000 Subject: [PATCH 059/237] Translated using Weblate (Albanian) Currently translated at 99.3% (1372 of 1381 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/sq/ --- src/i18n/strings/sq.json | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index 1b6b5f3bf3..0884a2bee9 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -1075,7 +1075,7 @@ "%(oneUser)shad their invitation withdrawn %(count)s times|one": "U tërhoq mbrapsht ftesa për %(oneUser)s", "What GitHub issue are these logs for?": "Për cilat çështje në GitHub janë këta regjistra?", "Community IDs cannot be empty.": "ID-të e bashkësisë s’mund të jenë të zbrazëta.", - "This will make your account permanently unusable. You will not be able to log in, and no one will be able to re-register the same user ID. This will cause your account to leave all rooms it is participating in, and it will remove your account details from your identity server. This action is irreversible.": "Kjo do ta bëjë llogarinë tuaj përgjithmonë të papërdorshme. S’do të jeni në gjendje të hyni në llogarinë tuaj, dhe askush s’do të jetë në gjendje të riregjistrojë të njëjtën ID përdoruesi. Kjo do të shkaktojë daljen e llogarisë tuaj nga krejt dhomat ku merr pjesë, dhe do të heqë hollësitë e llogarisë tuaj nga shërbyesi juaj i identiteteve. Ky veprim është i paprapakthyeshëm.", + "This will make your account permanently unusable. You will not be able to log in, and no one will be able to re-register the same user ID. This will cause your account to leave all rooms it is participating in, and it will remove your account details from your identity server. This action is irreversible.": "Kjo do ta bëjë llogarinë tuaj përgjithmonë të papërdorshme. S’do të jeni në gjendje të hyni në llogarinë tuaj, dhe askush s’do të jetë në gjendje të riregjistrojë të njëjtën ID përdoruesi. Kjo do të shkaktojë daljen e llogarisë tuaj nga krejt dhomat ku merrni pjesë, dhe do të heqë hollësitë e llogarisë tuaj nga shërbyesi juaj i identiteteve. Ky veprim është i paprapakthyeshëm.", "Deactivating your account does not by default cause us to forget messages you have sent. If you would like us to forget your messages, please tick the box below.": "Çaktivizimi i llogarisë tuaj nuk shkakton, si parazgjedhje, harrimin nga ne të mesazheve që keni dërguar. Nëse do të donit të harrojmë mesazhet tuaja, ju lutemi, i vini shenjë kutizës më poshtë.", "Upgrade this room to version %(version)s": "Përmirësojeni këtë dhomë me versionin %(version)s", "Share Room": "Ndani Dhomë Me të Tjerë", @@ -1353,5 +1353,31 @@ "Invalid homeserver discovery response": "Përgjigje e pavlefshme zbulimi shërbyesi Home", "Cannot find homeserver": "S’gjendet dot shërbyesi Home", "File is too big. Maximum file size is %(fileSize)s": "Kartela është shumë e madhe. Madhësia maksimum për kartelat është %(fileSize)s", - "The following files cannot be uploaded:": "Kartelat vijuese s’mund të ngarkohen:" + "The following files cannot be uploaded:": "Kartelat vijuese s’mund të ngarkohen:", + "Use a few words, avoid common phrases": "Përdorni ca fjalë, shmangni fraza të rëndomta", + "No need for symbols, digits, or uppercase letters": "S’ka nevojë për simbole, shifra apo shkronja të mëdha", + "Use a longer keyboard pattern with more turns": "Përdorni një rregullsi më të gjatë tastiere, me më tepër kthesa", + "Avoid repeated words and characters": "Shmangi përsëritje fjalësh dhe përsëritje shkronjash", + "Avoid sequences": "Shmangi togfjalësha", + "Avoid recent years": "Shmangni vitet e fundit", + "Avoid years that are associated with you": "Shmangni vite që kanë lidhje me ju", + "Avoid dates and years that are associated with you": "Shmangni data dhe vite që kanë lidhje me ju", + "Capitalization doesn't help very much": "Shkrimi i shkronjës së parë me të madhe nuk ndihmon kushedi çë", + "All-uppercase is almost as easy to guess as all-lowercase": "Fjalë shkruar krejt me të mëdha janë thuajse po aq të lehta për t’i hamendësuar sa ato me krejt të vogla", + "Reversed words aren't much harder to guess": "Fjalët së prapthi s’janë të vështira për t’i marrë me mend", + "Predictable substitutions like '@' instead of 'a' don't help very much": "Zëvendësime të parashikueshme, të tilla si '@', në vend të 'a', nuk ndihmojnë kushedi çë", + "Add another word or two. Uncommon words are better.": "Shtoni një a dy fjalë të tjera. Fjalë jo të rëndomta janë më të përshtatshme.", + "Repeats like \"aaa\" are easy to guess": "Përsëritje të tilla si \"aaa\" janë të lehta për t’u hamendësuar", + "Repeats like \"abcabcabc\" are only slightly harder to guess than \"abc\"": "Përsëritje të tilla si \"abcabcabc\" janë vetëm pak më të vështira për t’u hamendësuar se sa \"abc\"", + "Sequences like abc or 6543 are easy to guess": "Sekuenca të tilla si abc ose 6543 janë të lehta për t’u hamendsuar", + "Recent years are easy to guess": "Vitet tani afër janë të lehtë për t’u hamendësuar", + "Dates are often easy to guess": "Datat shpesh janë të lehta për t’i gjetur", + "This is a top-10 common password": "Ky fjalëkalim është nga 10 më të rëndomtët", + "This is a top-100 common password": "Ky fjalëkalim është nga 100 më të rëndomtët", + "This is a very common password": "Ky është një fjalëkalim shumë i rëndomtë", + "This is similar to a commonly used password": "Ky është i ngjashëm me një fjalëkalim të përdorur rëndom", + "A word by itself is easy to guess": "Një fjalë më vete është e lehtë të hamendësohet", + "Names and surnames by themselves are easy to guess": "Emrat dhe mbiemrat në vetvete janë të lehtë për t’i hamendësuar", + "Common names and surnames are easy to guess": "Emra dhe mbiemra të rëndomtë janë të kollajtë për t’u hamendësuar", + "Great! This passphrase looks strong enough.": "Bukur! Ky frazëkalim duket goxha i fuqishëm." } From c849a2389c5751450467cff29acfcbad8590262f Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 4 Dec 2018 16:38:32 +0000 Subject: [PATCH 060/237] Show correct text if passphrase is skipped Also set 'downloaded' state in a couple more places --- .../dialogs/keybackup/CreateKeyBackupDialog.js | 14 +++++++++++++- src/i18n/strings/en_EN.json | 16 ++++++---------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js index b0b044613a..8547add256 100644 --- a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js +++ b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js @@ -56,6 +56,7 @@ export default React.createClass({ copied: false, downloaded: false, zxcvbnResult: null, + setPassPhrase: false, }; }, @@ -132,6 +133,7 @@ export default React.createClass({ this._keyBackupInfo = await MatrixClientPeg.get().prepareKeyBackupVersion(); this.setState({ copied: false, + downloaded: false, phase: PHASE_SHOWKEY, }); }, @@ -149,7 +151,9 @@ export default React.createClass({ _onPassPhraseConfirmNextClick: async function() { this._keyBackupInfo = await MatrixClientPeg.get().prepareKeyBackupVersion(this.state.passPhrase); this.setState({ + setPassPhrase: true, copied: false, + downloaded: false, phase: PHASE_SHOWKEY, }); }, @@ -327,9 +331,17 @@ export default React.createClass({ _renderPhaseShowKey: function() { const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + + let bodyText; + if (this.state.setPassPhrase) { + bodyText = _t("As a safety net, you can use it to restore your encrypted message history if you forget your Recovery Passphrase."); + } else { + bodyText = _t("As a safety net, you can use it to restore your encrypted message history."); + } + return

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

    -

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

    +

    {bodyText}

    {_t("Your Recovery Key")}
    diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index f5a28cc774..5cd3e295bb 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -459,8 +459,9 @@ "numbered-list": "numbered-list", "Attachment": "Attachment", "At this time it is not possible to reply with a file so this will be sent without being a reply.": "At this time it is not possible to reply with a file so this will be sent without being a reply.", - "Upload Files": "Upload Files", "Are you sure you want to upload the following files?": "Are you sure you want to upload the following files?", + "The following files cannot be uploaded:": "The following files cannot be uploaded:", + "Upload Files": "Upload Files", "Encrypted room": "Encrypted room", "Unencrypted room": "Unencrypted room", "Hangup": "Hangup", @@ -1168,6 +1169,7 @@ "more": "more", "You seem to be uploading files, are you sure you want to quit?": "You seem to be uploading files, are you sure you want to quit?", "You seem to be in a call, are you sure you want to quit?": "You seem to be in a call, are you sure you want to quit?", + "File is too big. Maximum file size is %(fileSize)s": "File is too big. Maximum file size is %(fileSize)s", "Failed to upload file": "Failed to upload file", "Server may be unavailable, overloaded, or the file too big": "Server may be unavailable, overloaded, or the file too big", "Search failed": "Search failed", @@ -1351,8 +1353,9 @@ "Go back to set it again.": "Go back to set it again.", "Type in your Recovery Passphrase to confirm you remember it. If it helps, add it to your password manager or store it somewhere safe.": "Type in your Recovery Passphrase to confirm you remember it. If it helps, add it to your password manager or store it somewhere safe.", "Repeat your passphrase...": "Repeat your passphrase...", - "Make a copy of this Recovery Key and keep it safe.": "Make a copy of this Recovery Key and keep it safe.", "As a safety net, you can use it to restore your encrypted message history if you forget your Recovery Passphrase.": "As a safety net, you can use it to restore your encrypted message history if you forget your Recovery Passphrase.", + "As a safety net, you can use it to restore your encrypted message history.": "As a safety net, you can use it to restore your encrypted message history.", + "Make a copy of this Recovery Key and keep it safe.": "Make a copy of this Recovery Key and keep it safe.", "Your Recovery Key": "Your Recovery Key", "Copy to clipboard": "Copy to clipboard", "Download": "Download", @@ -1377,12 +1380,5 @@ "Retry": "Retry", "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", - "File is too big. Maximum file size is %(fileSize)s": "File is too big. Maximum file size is %(fileSize)s", - "Reason": "Reason", - "The following files cannot be uploaded:": "The following files cannot be uploaded:", - "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" + "Failed to add tag %(tagName)s to room": "Failed to add tag %(tagName)s to room" } From cf9fbe041aa9ea8a949d0c7f92dabe8febf67b0c Mon Sep 17 00:00:00 2001 From: Szimszon Date: Tue, 4 Dec 2018 13:42:44 +0000 Subject: [PATCH 061/237] Translated using Weblate (Hungarian) Currently translated at 100.0% (1382 of 1382 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/hu/ --- src/i18n/strings/hu.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index a1b521b606..4c3bf8b6d4 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -1408,5 +1408,6 @@ "A word by itself is easy to guess": "Egy szót magában könnyű kitalálni", "Names and surnames by themselves are easy to guess": "Neveket egymagukban könnyű kitalálni", "Common names and surnames are easy to guess": "Elterjedt neveket könnyű kitalálni", - "Great! This passphrase looks strong enough.": "Szuper! Ez a jelmondat elég erősnek látszik." + "Great! This passphrase looks strong enough.": "Szuper! Ez a jelmondat elég erősnek látszik.", + "As a safety net, you can use it to restore your encrypted message history.": "Használhatod egy biztonsági hálóként a titkosított üzenetek visszaállításához." } From 5fc25fd6baecaf61f55c03bd3320e18021247b31 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 3 Dec 2018 20:38:29 -0600 Subject: [PATCH 062/237] Only mark group as failed to load for summary Currently, any error in the `GroupStore`s several requests can cause the whole `GroupView` component to hide and be mark the group as failed to load. Since it is known that group members may fail to load in some cases, let's only show failed to load for the whole group when the summary fails. This also strengthens the `GroupView` test by ensuring we wait for multiple updates for checking results. Signed-off-by: J. Ryan Stinnett --- src/components/structures/GroupView.js | 17 ++++----- src/stores/GroupStore.js | 6 +--- test/components/structures/GroupView-test.js | 37 +++++++++++++++----- test/test-utils.js | 15 +++++--- 4 files changed, 50 insertions(+), 25 deletions(-) diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index 2c287c1b60..e2fd15aa89 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -470,7 +470,7 @@ export default React.createClass({ GroupStore.registerListener(groupId, this.onGroupStoreUpdated.bind(this, firstInit)); let willDoOnboarding = false; // XXX: This should be more fluxy - let's get the error from GroupStore .getError or something - GroupStore.on('error', (err, errorGroupId) => { + GroupStore.on('error', (err, errorGroupId, stateKey) => { if (this._unmounted || groupId !== errorGroupId) return; if (err.errcode === 'M_GUEST_ACCESS_FORBIDDEN' && !willDoOnboarding) { dis.dispatch({ @@ -483,11 +483,13 @@ export default React.createClass({ dis.dispatch({action: 'require_registration'}); willDoOnboarding = true; } - this.setState({ - summary: null, - error: err, - editing: false, - }); + if (stateKey === GroupStore.STATE_KEY.Summary) { + this.setState({ + summary: null, + error: err, + editing: false, + }); + } }); }, @@ -511,7 +513,6 @@ export default React.createClass({ isUserMember: GroupStore.getGroupMembers(this.props.groupId).some( (m) => m.userId === this._matrixClient.credentials.userId, ), - error: null, }); // XXX: This might not work but this.props.groupIsNew unused anyway if (this.props.groupIsNew && firstInit) { @@ -1157,7 +1158,7 @@ export default React.createClass({ if (this.state.summaryLoading && this.state.error === null || this.state.saving) { return ; - } else if (this.state.summary) { + } else if (this.state.summary && !this.state.error) { const summary = this.state.summary; let avatarNode; diff --git a/src/stores/GroupStore.js b/src/stores/GroupStore.js index bc2be37f51..4ac1e42e2e 100644 --- a/src/stores/GroupStore.js +++ b/src/stores/GroupStore.js @@ -122,10 +122,6 @@ class GroupStore extends EventEmitter { ); }, }; - - this.on('error', (err, groupId) => { - console.error(`GroupStore encountered error whilst fetching data for ${groupId}`, err); - }); } _fetchResource(stateKey, groupId) { @@ -148,7 +144,7 @@ class GroupStore extends EventEmitter { } console.error(`Failed to get resource ${stateKey} for ${groupId}`, err); - this.emit('error', err, groupId); + this.emit('error', err, groupId, stateKey); }).finally(() => { // Indicate finished request, allow for future fetches delete this._fetchResourcePromise[stateKey][groupId]; diff --git a/test/components/structures/GroupView-test.js b/test/components/structures/GroupView-test.js index 3b3510f26e..89632dcc48 100644 --- a/test/components/structures/GroupView-test.js +++ b/test/components/structures/GroupView-test.js @@ -164,7 +164,7 @@ describe('GroupView', function() { it('should indicate failure after failed /summary', function() { const groupView = ReactTestUtils.findRenderedComponentWithType(root, GroupView); - const prom = waitForUpdate(groupView).then(() => { + const prom = waitForUpdate(groupView, 4).then(() => { ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView_error'); }); @@ -179,7 +179,7 @@ describe('GroupView', function() { it('should show a group avatar, name, id and short description after successful /summary', function() { const groupView = ReactTestUtils.findRenderedComponentWithType(root, GroupView); - const prom = waitForUpdate(groupView).then(() => { + const prom = waitForUpdate(groupView, 4).then(() => { ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView'); const avatar = ReactTestUtils.findRenderedComponentWithType(root, sdk.getComponent('avatars.GroupAvatar')); @@ -214,7 +214,7 @@ describe('GroupView', function() { it('should show a simple long description after successful /summary', function() { const groupView = ReactTestUtils.findRenderedComponentWithType(root, GroupView); - const prom = waitForUpdate(groupView).then(() => { + const prom = waitForUpdate(groupView, 4).then(() => { ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView'); const longDesc = ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView_groupDesc'); @@ -235,7 +235,7 @@ describe('GroupView', function() { it('should show a placeholder if a long description is not set', function() { const groupView = ReactTestUtils.findRenderedComponentWithType(root, GroupView); - const prom = waitForUpdate(groupView).then(() => { + const prom = waitForUpdate(groupView, 4).then(() => { const placeholder = ReactTestUtils .findRenderedDOMComponentWithClass(root, 'mx_GroupView_groupDesc_placeholder'); const placeholderElement = ReactDOM.findDOMNode(placeholder); @@ -255,7 +255,7 @@ describe('GroupView', function() { it('should show a complicated long description after successful /summary', function() { const groupView = ReactTestUtils.findRenderedComponentWithType(root, GroupView); - const prom = waitForUpdate(groupView).then(() => { + const prom = waitForUpdate(groupView, 4).then(() => { const longDesc = ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView_groupDesc'); const longDescElement = ReactDOM.findDOMNode(longDesc); expect(longDescElement).toExist(); @@ -282,7 +282,7 @@ describe('GroupView', function() { it('should disallow images with non-mxc URLs', function() { const groupView = ReactTestUtils.findRenderedComponentWithType(root, GroupView); - const prom = waitForUpdate(groupView).then(() => { + const prom = waitForUpdate(groupView, 4).then(() => { const longDesc = ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView_groupDesc'); const longDescElement = ReactDOM.findDOMNode(longDesc); expect(longDescElement).toExist(); @@ -305,7 +305,7 @@ describe('GroupView', function() { it('should show a RoomDetailList after a successful /summary & /rooms (no rooms returned)', function() { const groupView = ReactTestUtils.findRenderedComponentWithType(root, GroupView); - const prom = waitForUpdate(groupView).then(() => { + const prom = waitForUpdate(groupView, 4).then(() => { const roomDetailList = ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_RoomDetailList'); const roomDetailListElement = ReactDOM.findDOMNode(roomDetailList); expect(roomDetailListElement).toExist(); @@ -322,7 +322,7 @@ describe('GroupView', function() { it('should show a RoomDetailList after a successful /summary & /rooms (with a single room)', function() { const groupView = ReactTestUtils.findRenderedComponentWithType(root, GroupView); - const prom = waitForUpdate(groupView).then(() => { + const prom = waitForUpdate(groupView, 4).then(() => { const roomDetailList = ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_RoomDetailList'); const roomDetailListElement = ReactDOM.findDOMNode(roomDetailList); expect(roomDetailListElement).toExist(); @@ -355,4 +355,25 @@ describe('GroupView', function() { httpBackend.flush(undefined, undefined, 0); return prom; }); + + it('should show a summary even if /users fails', function() { + const groupView = ReactTestUtils.findRenderedComponentWithType(root, GroupView); + + // Only wait for 3 updates in this test since we don't change state for + // the /users error case. + const prom = waitForUpdate(groupView, 3).then(() => { + const shortDesc = ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView_header_shortDesc'); + const shortDescElement = ReactDOM.findDOMNode(shortDesc); + expect(shortDescElement).toExist(); + expect(shortDescElement.innerText).toBe('This is a community'); + }); + + httpBackend.when('GET', '/groups/' + groupIdEncoded + '/summary').respond(200, summaryResponse); + httpBackend.when('GET', '/groups/' + groupIdEncoded + '/users').respond(500, {}); + httpBackend.when('GET', '/groups/' + groupIdEncoded + '/invited_users').respond(200, { chunk: [] }); + httpBackend.when('GET', '/groups/' + groupIdEncoded + '/rooms').respond(200, { chunk: [] }); + + httpBackend.flush(undefined, undefined, 0); + return prom; + }); }); diff --git a/test/test-utils.js b/test/test-utils.js index bc4d29210e..d5bcd9397a 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -310,19 +310,26 @@ export function wrapInMatrixClientContext(WrappedComponent) { /** * Call fn before calling componentDidUpdate on a react component instance, inst. * @param {React.Component} inst an instance of a React component. + * @param {integer} updates Number of updates to wait for. (Defaults to 1.) * @returns {Promise} promise that resolves when componentDidUpdate is called on * given component instance. */ -export function waitForUpdate(inst) { +export function waitForUpdate(inst, updates = 1) { return new Promise((resolve, reject) => { const cdu = inst.componentDidUpdate; + console.log(`Waiting for ${updates} update(s)`); + inst.componentDidUpdate = (prevProps, prevState, snapshot) => { - resolve(); + updates--; + console.log(`Got update, ${updates} remaining`); + + if (updates == 0) { + inst.componentDidUpdate = cdu; + resolve(); + } if (cdu) cdu(prevProps, prevState, snapshot); - - inst.componentDidUpdate = cdu; }; }); } From 22ff76e6b73e400f6966da0c59a1d1039164d60e Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 4 Dec 2018 16:42:50 -0600 Subject: [PATCH 063/237] Add error to UI when group member list fails to load Signed-off-by: J. Ryan Stinnett --- .../views/groups/GroupMemberList.js | 40 ++++- src/i18n/strings/en_EN.json | 1 + src/i18n/strings/en_US.json | 2 + .../views/groups/GroupMemberList-test.js | 149 ++++++++++++++++++ 4 files changed, 187 insertions(+), 5 deletions(-) create mode 100644 test/components/views/groups/GroupMemberList-test.js diff --git a/src/components/views/groups/GroupMemberList.js b/src/components/views/groups/GroupMemberList.js index 38c679a5b5..9b96fa33a9 100644 --- a/src/components/views/groups/GroupMemberList.js +++ b/src/components/views/groups/GroupMemberList.js @@ -32,7 +32,9 @@ export default React.createClass({ getInitialState: function() { return { members: null, + membersError: null, invitedMembers: null, + invitedMembersError: null, truncateAt: INITIAL_LOAD_NUM_MEMBERS, }; }, @@ -50,6 +52,19 @@ export default React.createClass({ GroupStore.registerListener(groupId, () => { this._fetchMembers(); }); + GroupStore.on('error', (err, errorGroupId, stateKey) => { + if (this._unmounted || groupId !== errorGroupId) return; + if (stateKey === GroupStore.STATE_KEY.GroupMembers) { + this.setState({ + membersError: err, + }); + } + if (stateKey === GroupStore.STATE_KEY.GroupInvitedMembers) { + this.setState({ + invitedMembersError: err, + }); + } + }); }, _fetchMembers: function() { @@ -83,7 +98,11 @@ export default React.createClass({ this.setState({ searchQuery: ev.target.value }); }, - makeGroupMemberTiles: function(query, memberList) { + makeGroupMemberTiles: function(query, memberList, memberListError) { + if (memberListError) { + return
    { _t("Failed to load group members") }
    ; + } + const GroupMemberTile = sdk.getComponent("groups.GroupMemberTile"); const TruncatedList = sdk.getComponent("elements.TruncatedList"); query = (query || "").toLowerCase(); @@ -153,15 +172,26 @@ export default React.createClass({ ); const joined = this.state.members ?
    - { this.makeGroupMemberTiles(this.state.searchQuery, this.state.members) } + { + this.makeGroupMemberTiles( + this.state.searchQuery, + this.state.members, + this.state.membersError, + ) + }
    :
    ; const invited = (this.state.invitedMembers && this.state.invitedMembers.length > 0) ?
    -

    { _t("Invited") }

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

    {_t("Invited")}

    + { + this.makeGroupMemberTiles( + this.state.searchQuery, + this.state.invitedMembers, + this.state.invitedMembersError, + ) + }
    :
    ; - return (
    { inputBox } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index f9980f5645..8e25f96b64 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1107,6 +1107,7 @@ "Community %(groupId)s not found": "Community %(groupId)s not found", "This Home server does not support communities": "This Home server does not support communities", "Failed to load %(groupId)s": "Failed to load %(groupId)s", + "Failed to load group members": "Failed to load group members", "Couldn't load home page": "Couldn't load home page", "You are currently using Riot anonymously as a guest.": "You are currently using Riot anonymously as a guest.", "If you would like to create a Matrix account you can register now.": "If you would like to create a Matrix account you can register now.", diff --git a/src/i18n/strings/en_US.json b/src/i18n/strings/en_US.json index aa7140aaa8..b96a49eac7 100644 --- a/src/i18n/strings/en_US.json +++ b/src/i18n/strings/en_US.json @@ -134,6 +134,8 @@ "Failed to join room": "Failed to join room", "Failed to kick": "Failed to kick", "Failed to leave room": "Failed to leave room", + "Failed to load %(groupId)s": "Failed to load %(groupId)s", + "Failed to load group members": "Failed to load group members", "Failed to load timeline position": "Failed to load timeline position", "Failed to mute user": "Failed to mute user", "Failed to reject invite": "Failed to reject invite", diff --git a/test/components/views/groups/GroupMemberList-test.js b/test/components/views/groups/GroupMemberList-test.js new file mode 100644 index 0000000000..d71d0377d7 --- /dev/null +++ b/test/components/views/groups/GroupMemberList-test.js @@ -0,0 +1,149 @@ +/* +Copyright 2018 New Vector Ltd. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from "react"; +import ReactDOM from "react-dom"; +import ReactTestUtils from "react-dom/test-utils"; +import expect from "expect"; + +import MockHttpBackend from "matrix-mock-request"; +import MatrixClientPeg from "../../../../src/MatrixClientPeg"; +import sdk from "matrix-react-sdk"; +import Matrix from "matrix-js-sdk"; + +import * as TestUtils from "test-utils"; +const { waitForUpdate } = TestUtils; + +const GroupMemberList = sdk.getComponent("views.groups.GroupMemberList"); +const WrappedGroupMemberList = TestUtils.wrapInMatrixClientContext(GroupMemberList); + +describe("GroupMemberList", function() { + let root; + let rootElement; + let httpBackend; + let summaryResponse; + let groupId; + let groupIdEncoded; + + // Summary response fields + const user = { + is_privileged: true, // can edit the group + is_public: true, // appear as a member to non-members + is_publicised: true, // display flair + }; + const usersSection = { + roles: {}, + total_user_count_estimate: 0, + users: [], + }; + const roomsSection = { + categories: {}, + rooms: [], + total_room_count_estimate: 0, + }; + + // Users response fields + const usersResponse = { + chunk: [ + { + user_id: "@test:matrix.org", + displayname: "Test", + avatar_url: "mxc://matrix.org/oUxxDyzQOHdVDMxgwFzyCWEe", + is_public: true, + is_privileged: true, + attestation: {}, + }, + ], + }; + + beforeEach(function() { + TestUtils.beforeEach(this); + + httpBackend = new MockHttpBackend(); + + Matrix.request(httpBackend.requestFn); + + MatrixClientPeg.get = () => Matrix.createClient({ + baseUrl: "https://my.home.server", + userId: "@me:here", + accessToken: "123456789", + }); + + summaryResponse = { + profile: { + avatar_url: "mxc://someavatarurl", + is_openly_joinable: true, + is_public: true, + long_description: "This is a LONG description.", + name: "The name of a community", + short_description: "This is a community", + }, + user, + users_section: usersSection, + rooms_section: roomsSection, + }; + + groupId = "+" + Math.random().toString(16).slice(2) + ":domain"; + groupIdEncoded = encodeURIComponent(groupId); + + rootElement = document.createElement("div"); + root = ReactDOM.render(, rootElement); + }); + + afterEach(function() { + ReactDOM.unmountComponentAtNode(rootElement); + }); + + it("should show group member list after successful /users", function() { + const groupMemberList = ReactTestUtils.findRenderedComponentWithType(root, GroupMemberList); + const prom = waitForUpdate(groupMemberList, 4).then(() => { + ReactTestUtils.findRenderedDOMComponentWithClass(root, "mx_MemberList"); + + const memberList = ReactTestUtils.findRenderedDOMComponentWithClass(root, "mx_MemberList_joined"); + const memberListElement = ReactDOM.findDOMNode(memberList); + expect(memberListElement).toExist(); + expect(memberListElement.innerText).toBe("Test"); + }); + + httpBackend.when("GET", "/groups/" + groupIdEncoded + "/summary").respond(200, summaryResponse); + httpBackend.when("GET", "/groups/" + groupIdEncoded + "/users").respond(200, usersResponse); + httpBackend.when("GET", "/groups/" + groupIdEncoded + "/invited_users").respond(200, { chunk: [] }); + httpBackend.when("GET", "/groups/" + groupIdEncoded + "/rooms").respond(200, { chunk: [] }); + + httpBackend.flush(undefined, undefined, 0); + return prom; + }); + + it("should show error message after failed /users", function() { + const groupMemberList = ReactTestUtils.findRenderedComponentWithType(root, GroupMemberList); + const prom = waitForUpdate(groupMemberList, 4).then(() => { + ReactTestUtils.findRenderedDOMComponentWithClass(root, "mx_MemberList"); + + const memberList = ReactTestUtils.findRenderedDOMComponentWithClass(root, "mx_MemberList_joined"); + const memberListElement = ReactDOM.findDOMNode(memberList); + expect(memberListElement).toExist(); + expect(memberListElement.innerText).toBe("Failed to load group members"); + }); + + httpBackend.when("GET", "/groups/" + groupIdEncoded + "/summary").respond(200, summaryResponse); + httpBackend.when("GET", "/groups/" + groupIdEncoded + "/users").respond(500, {}); + httpBackend.when("GET", "/groups/" + groupIdEncoded + "/invited_users").respond(200, { chunk: [] }); + httpBackend.when("GET", "/groups/" + groupIdEncoded + "/rooms").respond(200, { chunk: [] }); + + httpBackend.flush(undefined, undefined, 0); + return prom; + }); +}); From 7f4ee2b1cabc6a74478b6c2bbef53852b8e3f735 Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Wed, 5 Dec 2018 03:54:22 +0000 Subject: [PATCH 064/237] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (1382 of 1382 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/zh_Hant/ --- src/i18n/strings/zh_Hant.json | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index f955e800e2..712911064f 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -1380,5 +1380,32 @@ "Cannot find homeserver": "找不到家伺服器", "Sign in with single sign-on": "以單一登入來登入", "File is too big. Maximum file size is %(fileSize)s": "檔案太大了。最大的檔案大小為 %(fileSize)s", - "The following files cannot be uploaded:": "下列檔案無法上傳:" + "The following files cannot be uploaded:": "下列檔案無法上傳:", + "Use a few words, avoid common phrases": "使用數個字,但避免常用片語", + "No need for symbols, digits, or uppercase letters": "不需要符號、數字或大寫字母", + "Use a longer keyboard pattern with more turns": "以更多變化使用較長的鍵盤模式", + "Avoid repeated words and characters": "避免重覆的文字與字母", + "Avoid sequences": "避免序列", + "Avoid recent years": "避免最近的年份", + "Avoid years that are associated with you": "避免關於您的年份", + "Avoid dates and years that are associated with you": "避免關於您的日期與年份", + "Capitalization doesn't help very much": "大寫並沒有太大的協助", + "All-uppercase is almost as easy to guess as all-lowercase": "全大寫通常比全小寫好猜", + "Reversed words aren't much harder to guess": "反向拼字不會比較難猜", + "Predictable substitutions like '@' instead of 'a' don't help very much": "如「@」而非「a」這樣的預期中的替換並沒有太多的協助", + "Add another word or two. Uncommon words are better.": "加入一個或兩個額外的單字。最好是不常用的。", + "Repeats like \"aaa\" are easy to guess": "如「aaa」這樣的重覆易於猜測", + "Repeats like \"abcabcabc\" are only slightly harder to guess than \"abc\"": "如「abcabcabc」這樣的重覆只比「abc」難猜一點", + "Sequences like abc or 6543 are easy to guess": "如 abc 或 6543 這樣的序列易於猜測", + "Recent years are easy to guess": "最近的年份易於猜測", + "Dates are often easy to guess": "日期通常比較好猜", + "This is a top-10 common password": "這是十大最常見的密碼", + "This is a top-100 common password": "這是百大最常見的密碼", + "This is a very common password": "這是非常常見的密碼", + "This is similar to a commonly used password": "這與常見使用的密碼很類似", + "A word by itself is easy to guess": "單字本身很容易猜測", + "Names and surnames by themselves are easy to guess": "姓名與姓氏本身很容易猜測", + "Common names and surnames are easy to guess": "常見的名字與姓氏易於猜測", + "Great! This passphrase looks strong enough.": "很好!這個密碼看起來夠強了。", + "As a safety net, you can use it to restore your encrypted message history.": "做為安全網,您可以使用它來復原您已加密的訊息歷史。" } From 633be5061c0a197b2c8dab1c6d7641d8a37dfe51 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 4 Dec 2018 23:34:57 -0700 Subject: [PATCH 065/237] Introduce a default_server_name for aesthetics and rework .well-known Fixes https://github.com/vector-im/riot-web/issues/7724 The `default_server_name` from the config gets displayed in the "Login with my [server] matrix ID" dropdown when the default server is being used. At this point, we also discourage the use of the `default_hs_url` and `default_is_url` options because we do an implicit .well-known lookup to configure the client based on the `default_server_name`. If the URLs are still present in the config, we'll honour them and won't do a .well-known lookup when the URLs are mixed with the new server_name option. Users will be warned if the `default_server_name` does not match the `default_hs_url` if both are supplied. Users are additionally prevented from logging in, registering, and resetting their password if the implicit .well-known check fails - this is to prevent people from doing actions against the wrong homeserver. This relies on https://github.com/matrix-org/matrix-js-sdk/pull/799 as we now do auto discovery in two places. Instead of bringing the .well-known out to its own utility class in the react-sdk, we might as well drag it out to the js-sdk. --- res/css/structures/login/_Login.scss | 7 + src/components/structures/MatrixChat.js | 46 ++++- .../structures/login/ForgotPassword.js | 23 +++ src/components/structures/login/Login.js | 157 ++++++------------ .../structures/login/Registration.js | 23 ++- src/components/views/login/PasswordLogin.js | 20 ++- src/i18n/strings/en_EN.json | 8 +- 7 files changed, 166 insertions(+), 118 deletions(-) diff --git a/res/css/structures/login/_Login.scss b/res/css/structures/login/_Login.scss index 1264d2a30f..9b19c24b14 100644 --- a/res/css/structures/login/_Login.scss +++ b/res/css/structures/login/_Login.scss @@ -180,6 +180,13 @@ limitations under the License. margin-bottom: 12px; } +.mx_Login_subtext { + display: block; + font-size: 0.8em; + text-align: center; + margin: 10px; +} + .mx_Login_type_container { display: flex; margin-bottom: 14px; diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 4d7c71e3ef..dc3872664b 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -48,6 +48,8 @@ import SettingsStore, {SettingLevel} from "../../settings/SettingsStore"; import { startAnyRegistrationFlow } from "../../Registration.js"; import { messageForSyncError } from '../../utils/ErrorUtils'; +const AutoDiscovery = Matrix.AutoDiscovery; + // Disable warnings for now: we use deprecated bluebird functions // and need to migrate, but they spam the console with warnings. Promise.config({warnings: false}); @@ -181,6 +183,12 @@ export default React.createClass({ register_is_url: null, register_id_sid: null, + // Parameters used for setting up the login/registration views + defaultServerName: this.props.config.default_server_name, + defaultHsUrl: this.props.config.default_hs_url, + defaultIsUrl: this.props.config.default_is_url, + defaultServerDiscoveryError: null, + // When showing Modal dialogs we need to set aria-hidden on the root app element // and disable it when there are no dialogs hideToSRUsers: false, @@ -199,6 +207,10 @@ export default React.createClass({ }; }, + getDefaultServerName: function() { + return this.state.defaultServerName; + }, + getCurrentHsUrl: function() { if (this.state.register_hs_url) { return this.state.register_hs_url; @@ -211,8 +223,10 @@ export default React.createClass({ } }, - getDefaultHsUrl() { - return this.props.config.default_hs_url || "https://matrix.org"; + getDefaultHsUrl(defaultToMatrixDotOrg) { + defaultToMatrixDotOrg = typeof(defaultToMatrixDotOrg) !== 'boolean' ? true : defaultToMatrixDotOrg; + if (!this.state.defaultHsUrl && defaultToMatrixDotOrg) return "https://matrix.org"; + return this.state.defaultHsUrl; }, getFallbackHsUrl: function() { @@ -232,7 +246,7 @@ export default React.createClass({ }, getDefaultIsUrl() { - return this.props.config.default_is_url || "https://vector.im"; + return this.state.defaultIsUrl || "https://vector.im"; }, componentWillMount: function() { @@ -282,6 +296,11 @@ export default React.createClass({ console.info(`Team token set to ${this._teamToken}`); } + // Set up the default URLs (async) + if (this.getDefaultServerName() && !this.getDefaultHsUrl(false)) { + this._tryDiscoverDefaultHomeserver(this.getDefaultServerName()); + } + // Set a default HS with query param `hs_url` const paramHs = this.props.startingFragmentQueryParams.hs_url; if (paramHs) { @@ -1732,6 +1751,21 @@ export default React.createClass({ this.setState(newState); }, + _tryDiscoverDefaultHomeserver: async function(serverName) { + const discovery = await AutoDiscovery.findClientConfig(serverName); + const state = discovery["m.homeserver"].state; + if (state !== AutoDiscovery.SUCCESS) { + console.error("Failed to discover homeserver on startup:", discovery); + this.setState({defaultServerDiscoveryError: discovery["m.homeserver"].error}); + } else { + const hsUrl = discovery["m.homeserver"].base_url; + const isUrl = discovery["m.identity_server"].state === AutoDiscovery.SUCCESS + ? discovery["m.identity_server"].base_url + : "https://vector.im"; + this.setState({defaultHsUrl: hsUrl, defaultIsUrl: isUrl}); + } + }, + _makeRegistrationUrl: function(params) { if (this.props.startingFragmentQueryParams.referrer) { params.referrer = this.props.startingFragmentQueryParams.referrer; @@ -1820,6 +1854,8 @@ export default React.createClass({ idSid={this.state.register_id_sid} email={this.props.startingFragmentQueryParams.email} referrer={this.props.startingFragmentQueryParams.referrer} + defaultServerName={this.getDefaultServerName()} + defaultServerDiscoveryError={this.state.defaultServerDiscoveryError} defaultHsUrl={this.getDefaultHsUrl()} defaultIsUrl={this.getDefaultIsUrl()} brand={this.props.config.brand} @@ -1842,6 +1878,8 @@ export default React.createClass({ const ForgotPassword = sdk.getComponent('structures.login.ForgotPassword'); return ( { err }
    ; + } + const LanguageSelector = sdk.getComponent('structures.login.LanguageSelector'); resetPasswordJsx = ( @@ -230,6 +252,7 @@ module.exports = React.createClass({ { serverConfigSection } + { errorText } { _t('Return to login screen') } diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index bd18699dd1..08e94e413a 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -26,11 +26,17 @@ import Login from '../../../Login'; import SdkConfig from '../../../SdkConfig'; import SettingsStore from "../../../settings/SettingsStore"; import { messageForResourceLimitError } from '../../../utils/ErrorUtils'; -import request from 'browser-request'; +import { AutoDiscovery } from "matrix-js-sdk"; // For validating phone numbers without country codes const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/; +// These are used in several places, and come from the js-sdk's autodiscovery +// stuff. We define them here so that they'll be picked up by i18n. +_td("Invalid homeserver discovery response"); +_td("Invalid identity server discovery response"); +_td("General failure"); + /** * A wire component which glues together login UI components and Login logic */ @@ -51,6 +57,14 @@ module.exports = React.createClass({ // different home server without confusing users. fallbackHsUrl: PropTypes.string, + // The default server name to use when the user hasn't specified + // one. This is used when displaying the defaultHsUrl in the UI. + defaultServerName: PropTypes.string, + + // An error passed along from higher up explaining that something + // went wrong when finding the defaultHsUrl. + defaultServerDiscoveryError: PropTypes.string, + defaultDeviceDisplayName: PropTypes.string, // login shouldn't know or care how registration is done. @@ -113,7 +127,7 @@ module.exports = React.createClass({ onPasswordLogin: function(username, phoneCountry, phoneNumber, password) { // Prevent people from submitting their password when homeserver // discovery went wrong - if (this.state.discoveryError) return; + if (this.state.discoveryError || this.props.defaultServerDiscoveryError) return; this.setState({ busy: true, @@ -290,114 +304,43 @@ module.exports = React.createClass({ } try { - const wellknown = await this._getWellKnownObject(`https://${serverName}/.well-known/matrix/client`); - if (!wellknown["m.homeserver"]) { - console.error("No m.homeserver key in well-known response"); - this.setState({discoveryError: _t("Invalid homeserver discovery response")}); - return; + const discovery = await AutoDiscovery.findClientConfig(serverName); + const state = discovery["m.homeserver"].state; + if (state !== AutoDiscovery.SUCCESS && state !== AutoDiscovery.PROMPT) { + this.setState({ + discoveredHsUrl: "", + discoveredIsUrl: "", + discoveryError: discovery["m.homeserver"].error, + }); + } else if (state === AutoDiscovery.PROMPT) { + this.setState({ + discoveredHsUrl: "", + discoveredIsUrl: "", + discoveryError: "", + }); + } else if (state === AutoDiscovery.SUCCESS) { + this.setState({ + discoveredHsUrl: discovery["m.homeserver"].base_url, + discoveredIsUrl: + discovery["m.identity_server"].state === AutoDiscovery.SUCCESS + ? discovery["m.identity_server"].base_url + : "", + discoveryError: "", + }); + } else { + console.warn("Unknown state for m.homeserver in discovery response: ", discovery); + this.setState({ + discoveredHsUrl: "", + discoveredIsUrl: "", + discoveryError: _t("Unknown failure discovering homeserver"), + }); } - - const hsUrl = this._sanitizeWellKnownUrl(wellknown["m.homeserver"]["base_url"]); - if (!hsUrl) { - console.error("Invalid base_url for m.homeserver"); - this.setState({discoveryError: _t("Invalid homeserver discovery response")}); - return; - } - - console.log("Verifying homeserver URL: " + hsUrl); - const hsVersions = await this._getWellKnownObject(`${hsUrl}/_matrix/client/versions`); - if (!hsVersions["versions"]) { - console.error("Invalid /versions response"); - this.setState({discoveryError: _t("Invalid homeserver discovery response")}); - return; - } - - let isUrl = ""; - if (wellknown["m.identity_server"]) { - isUrl = this._sanitizeWellKnownUrl(wellknown["m.identity_server"]["base_url"]); - if (!isUrl) { - console.error("Invalid base_url for m.identity_server"); - this.setState({discoveryError: _t("Invalid homeserver discovery response")}); - return; - } - - console.log("Verifying identity server URL: " + isUrl); - const isResponse = await this._getWellKnownObject(`${isUrl}/_matrix/identity/api/v1`); - if (!isResponse) { - console.error("Invalid /api/v1 response"); - this.setState({discoveryError: _t("Invalid homeserver discovery response")}); - return; - } - } - - this.setState({discoveredHsUrl: hsUrl, discoveredIsUrl: isUrl, discoveryError: ""}); } catch (e) { console.error(e); - if (e.wkAction) { - if (e.wkAction === "FAIL_ERROR" || e.wkAction === "FAIL_PROMPT") { - // We treat FAIL_ERROR and FAIL_PROMPT the same to avoid having the user - // submit their details to the wrong homeserver. In practice, the custom - // server options will show up to try and guide the user into entering - // the required information. - this.setState({discoveryError: _t("Cannot find homeserver")}); - return; - } else if (e.wkAction === "IGNORE") { - // Nothing to discover - this.setState({discoveryError: "", discoveredHsUrl: "", discoveredIsUrl: ""}); - return; - } - } - throw e; } }, - _sanitizeWellKnownUrl: function(url) { - if (!url) return false; - - const parser = document.createElement('a'); - parser.href = url; - - if (parser.protocol !== "http:" && parser.protocol !== "https:") return false; - if (!parser.hostname) return false; - - const port = parser.port ? `:${parser.port}` : ""; - const path = parser.pathname ? parser.pathname : ""; - let saferUrl = `${parser.protocol}//${parser.hostname}${port}${path}`; - if (saferUrl.endsWith("/")) saferUrl = saferUrl.substring(0, saferUrl.length - 1); - return saferUrl; - }, - - _getWellKnownObject: function(url) { - return new Promise(function(resolve, reject) { - request( - { method: "GET", url: url }, - (err, response, body) => { - if (err || response.status < 200 || response.status >= 300) { - let action = "FAIL_ERROR"; - if (response.status === 404) { - // We could just resolve with an empty object, but that - // causes a different series of branches when the m.homeserver - // bit of the JSON is missing. - action = "IGNORE"; - } - reject({err: err, response: response, wkAction: action}); - return; - } - - try { - resolve(JSON.parse(body)); - } catch (e) { - console.error(e); - if (e.name === "SyntaxError") { - reject({wkAction: "FAIL_PROMPT", wkError: "Invalid JSON"}); - } else throw e; - } - }, - ); - }); - }, - _initLoginLogic: function(hsUrl, isUrl) { const self = this; hsUrl = hsUrl || this.state.enteredHomeserverUrl; @@ -527,6 +470,9 @@ module.exports = React.createClass({ _renderPasswordStep: function() { const PasswordLogin = sdk.getComponent('login.PasswordLogin'); + const hsName = this.state.enteredHomeserverUrl === this.props.defaultHsUrl + ? this.props.defaultServerName + : null; return ( ); }, @@ -559,7 +506,7 @@ module.exports = React.createClass({ const ServerConfig = sdk.getComponent("login.ServerConfig"); const loader = this.state.busy ?
    : null; - const errorText = this.state.discoveryError || this.state.errorText; + const errorText = this.props.defaultServerDiscoveryError || this.state.discoveryError || this.state.errorText; let loginAsGuestJsx; if (this.props.enableGuest) { @@ -576,7 +523,7 @@ module.exports = React.createClass({ serverConfig = { this.state.errorText }
    ; + const err = this.state.errorText || this.props.defaultServerDiscoveryError; + if (theme === 'status' && err) { + header =
    { err }
    ; } else { header =

    { _t('Create an account') }

    ; - if (this.state.errorText) { - errorText =
    { this.state.errorText }
    ; + if (err) { + errorText =
    { err }
    ; } } diff --git a/src/components/views/login/PasswordLogin.js b/src/components/views/login/PasswordLogin.js index 6a5577fb62..582ccf94dd 100644 --- a/src/components/views/login/PasswordLogin.js +++ b/src/components/views/login/PasswordLogin.js @@ -40,6 +40,7 @@ class PasswordLogin extends React.Component { initialPassword: "", loginIncorrect: false, hsDomain: "", + hsName: null, } constructor(props) { @@ -250,13 +251,24 @@ class PasswordLogin extends React.Component { ); } - let matrixIdText = ''; + let matrixIdText = _t('Matrix ID'); + let matrixIdSubtext = null; + if (this.props.hsName) { + matrixIdText = _t('%(serverName)s Matrix ID', {serverName: this.props.hsName}); + } if (this.props.hsUrl) { try { const parsedHsUrl = new URL(this.props.hsUrl); - matrixIdText = _t('%(serverName)s Matrix ID', {serverName: parsedHsUrl.hostname}); + if (!this.props.hsName) { + matrixIdText = _t('%(serverName)s Matrix ID', {serverName: parsedHsUrl.hostname}); + } else if (parsedHsUrl.hostname !== this.props.hsName) { + matrixIdSubtext = _t('%(serverName)s is located at %(homeserverUrl)s', { + serverName: this.props.hsName, + homeserverUrl: this.props.hsUrl, + }); + } } catch (e) { - // pass + // ignore } } @@ -292,6 +304,7 @@ class PasswordLogin extends React.Component {
    { loginType } + { matrixIdSubtext } { loginField } {this._passwordField = e;}} type="password" name="password" @@ -325,6 +338,7 @@ PasswordLogin.propTypes = { onPhoneNumberChanged: PropTypes.func, onPasswordChanged: PropTypes.func, loginIncorrect: PropTypes.bool, + hsName: PropTypes.string, }; module.exports = PasswordLogin; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index e4aad2c55d..22764c2e77 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -686,6 +686,7 @@ "Mobile phone number": "Mobile phone number", "Forgot your password?": "Forgot your password?", "%(serverName)s Matrix ID": "%(serverName)s Matrix ID", + "%(serverName)s is located at %(homeserverUrl)s": "%(serverName)s is located at %(homeserverUrl)s", "Sign in with": "Sign in with", "Email address": "Email address", "Sign in": "Sign in", @@ -831,7 +832,6 @@ "And %(count)s more...|other": "And %(count)s more...", "ex. @bob:example.com": "ex. @bob:example.com", "Add User": "Add User", - "Matrix ID": "Matrix ID", "Matrix Room ID": "Matrix Room ID", "email address": "email address", "You have entered an invalid address.": "You have entered an invalid address.", @@ -1283,6 +1283,9 @@ "Confirm your new password": "Confirm your new password", "Send Reset Email": "Send Reset Email", "Create an account": "Create an account", + "Invalid homeserver discovery response": "Invalid homeserver discovery response", + "Invalid identity server discovery response": "Invalid identity server discovery response", + "General failure": "General failure", "This Home Server does not support login using email address.": "This Home Server does not support login using email address.", "Please contact your service administrator to continue using this service.": "Please contact your service administrator to continue using this service.", "Incorrect username and/or password.": "Incorrect username and/or password.", @@ -1290,8 +1293,7 @@ "Guest access is disabled on this Home Server.": "Guest access is disabled on this Home Server.", "Failed to perform homeserver discovery": "Failed to perform homeserver discovery", "The phone number entered looks invalid": "The phone number entered looks invalid", - "Invalid homeserver discovery response": "Invalid homeserver discovery response", - "Cannot find homeserver": "Cannot find homeserver", + "Unknown failure discovering homeserver": "Unknown failure discovering homeserver", "This homeserver doesn't offer any login flows which are supported by this client.": "This homeserver doesn't offer any login flows which are supported by this client.", "Error: Problem communicating with the given homeserver.": "Error: Problem communicating with the given homeserver.", "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or enable unsafe scripts.": "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or enable unsafe scripts.", From 573cb8a380d7dd74ec217a7e041f4ec6346ca299 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20C?= Date: Wed, 5 Dec 2018 10:39:37 +0000 Subject: [PATCH 066/237] Translated using Weblate (French) Currently translated at 100.0% (1382 of 1382 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/fr/ --- src/i18n/strings/fr.json | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index c98e55d1a0..f2c63310ac 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -1382,5 +1382,32 @@ "Invalid homeserver discovery response": "Réponse de découverte du serveur d'accueil non valide", "Cannot find homeserver": "Le serveur d'accueil est introuvable", "File is too big. Maximum file size is %(fileSize)s": "Le fichier est trop gros. La taille maximum est de %(fileSize)s", - "The following files cannot be uploaded:": "Les fichiers suivants n'ont pas pu être envoyés :" + "The following files cannot be uploaded:": "Les fichiers suivants n'ont pas pu être envoyés :", + "Use a few words, avoid common phrases": "Utilisez quelques mots, évitez les phrases courantes", + "No need for symbols, digits, or uppercase letters": "Il n'y a pas besoin de symboles, de chiffres ou de majuscules", + "Avoid repeated words and characters": "Évitez de répéter des mots et des caractères", + "Avoid sequences": "Évitez les séquences", + "Avoid recent years": "Évitez les années récentes", + "Avoid years that are associated with you": "Évitez les années qui ont un rapport avec vous", + "Avoid dates and years that are associated with you": "Évitez les dates et les années qui ont un rapport avec vous", + "Capitalization doesn't help very much": "Les majuscules n'aident pas vraiment", + "All-uppercase is almost as easy to guess as all-lowercase": "Uniquement des majuscules, c'est presque aussi facile à deviner qu'uniquement des minuscules", + "Reversed words aren't much harder to guess": "Les mots inversés ne sont pas beaucoup plus difficiles à deviner", + "Predictable substitutions like '@' instead of 'a' don't help very much": "Les substitutions prévisibles comme « @ » à la place de « a » n'aident pas vraiment", + "Add another word or two. Uncommon words are better.": "Ajoutez un ou deux mots. Les mots rares sont à privilégier.", + "Repeats like \"aaa\" are easy to guess": "Les répétitions comme « aaa » sont faciles à deviner", + "Repeats like \"abcabcabc\" are only slightly harder to guess than \"abc\"": "Les répétitions comme « abcabcabc » ne sont pas beaucoup plus difficiles à deviner que « abc »", + "Sequences like abc or 6543 are easy to guess": "Les séquences comme abc ou 6543 sont faciles à deviner", + "Recent years are easy to guess": "Les années récentes sont faciles à deviner", + "Dates are often easy to guess": "Les dates sont généralement faciles à deviner", + "This is a top-10 common password": "Cela fait partie des 10 mots de passe les plus répandus", + "This is a top-100 common password": "Cela fait partie des 100 mots de passe les plus répandus", + "This is a very common password": "C'est un mot de passe très répandu", + "This is similar to a commonly used password": "Cela ressemble à un mot de passe répandu", + "A word by itself is easy to guess": "Un mot seul est facile à deviner", + "Names and surnames by themselves are easy to guess": "Les noms et prénoms seuls sont faciles à deviner", + "Common names and surnames are easy to guess": "Les noms et prénoms répandus sont faciles à deviner", + "Use a longer keyboard pattern with more turns": "Utilisez un schéma plus long et avec plus de variations", + "Great! This passphrase looks strong enough.": "Super ! Cette phrase de passe a l'air assez forte.", + "As a safety net, you can use it to restore your encrypted message history.": "En cas de problème, vous pouvez l'utiliser pour récupérer l'historique de vos messages chiffrés." } From 817f78ecd5461d6ad9a35f5df76c66ba93ec7a7e Mon Sep 17 00:00:00 2001 From: Karol Kosek Date: Wed, 5 Dec 2018 06:09:59 +0000 Subject: [PATCH 067/237] Translated using Weblate (Polish) Currently translated at 88.2% (1219 of 1382 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/pl/ --- src/i18n/strings/pl.json | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/pl.json b/src/i18n/strings/pl.json index 052823ec59..705442e7bf 100644 --- a/src/i18n/strings/pl.json +++ b/src/i18n/strings/pl.json @@ -1212,5 +1212,26 @@ "Delete backup": "Usuń Kopię Zapasową", "Unable to load! Check your network connectivity and try again.": "Nie można załadować! Sprawdź połączenie sieciowe i spróbuj ponownie.", "Algorithm: ": "Algorytm: ", - "Pin unread rooms to the top of the room list": "Przypnij nieprzeczytanie pokoje na górę listy pokojów" + "Pin unread rooms to the top of the room list": "Przypnij nieprzeczytanie pokoje na górę listy pokojów", + "Use a few words, avoid common phrases": "Użyj kilku słów, unikaj typowych zwrotów", + "Avoid repeated words and characters": "Unikaj powtarzających się słów i znaków", + "Avoid sequences": "Unikaj sekwencji", + "Avoid recent years": "Unikaj ostatnich lat", + "Avoid years that are associated with you": "Unikaj lat, które są z tobą związane z Tobą", + "Avoid dates and years that are associated with you": "Unikaj dat i lat, które są z tobą związane z Tobą", + "Add another word or two. Uncommon words are better.": "Dodaj kolejne słowo lub dwa. Niezwykłe słowa są lepsze.", + "Recent years are easy to guess": "Ostatnie lata są łatwe do odgadnięcia", + "Dates are often easy to guess": "Daty są często łatwe do odgadnięcia", + "This is a very common password": "To jest bardzo popularne hasło", + "Backup version: ": "Wersja kopii zapasowej: ", + "Restore backup": "Przywróć kopię zapasową", + "Room version number: ": "Numer wersji pokoju: ", + "Reversed words aren't much harder to guess": "Odwrócone słowa nie są trudniejsze do odgadnięcia", + "Predictable substitutions like '@' instead of 'a' don't help very much": "Przewidywalne podstawienia, takie jak \"@\" zamiast \"a\", nie pomagają zbytnio", + "Repeats like \"aaa\" are easy to guess": "Powtórzenia takie jak \"aaa\" są łatwe do odgadnięcia", + "Repeats like \"abcabcabc\" are only slightly harder to guess than \"abc\"": "Powtórzenia takie jak \"abcabcabc\" są tylko trochę trudniejsze do odgadnięcia niż \"abc\"", + "Sequences like abc or 6543 are easy to guess": "Sekwencje takie jak abc lub 6543 są łatwe do odgadnięcia", + "A word by itself is easy to guess": "Samo słowo jest łatwe do odgadnięcia", + "Names and surnames by themselves are easy to guess": "Imiona i nazwiska same w sobie są łatwe do odgadnięcia", + "Common names and surnames are easy to guess": "Popularne imiona i nazwiska są łatwe do odgadnięcia" } From 5764622ebab7aa252654672b09306690d94bfb67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20C?= Date: Wed, 5 Dec 2018 10:40:18 +0000 Subject: [PATCH 068/237] Translated using Weblate (French) Currently translated at 100.0% (1383 of 1383 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/fr/ --- src/i18n/strings/fr.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index f2c63310ac..a6e2942e38 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -1409,5 +1409,6 @@ "Common names and surnames are easy to guess": "Les noms et prénoms répandus sont faciles à deviner", "Use a longer keyboard pattern with more turns": "Utilisez un schéma plus long et avec plus de variations", "Great! This passphrase looks strong enough.": "Super ! Cette phrase de passe a l'air assez forte.", - "As a safety net, you can use it to restore your encrypted message history.": "En cas de problème, vous pouvez l'utiliser pour récupérer l'historique de vos messages chiffrés." + "As a safety net, you can use it to restore your encrypted message history.": "En cas de problème, vous pouvez l'utiliser pour récupérer l'historique de vos messages chiffrés.", + "Failed to load group members": "Échec du chargement des membres du groupe" } From 9f25c39a5316982eb985950d53598704d236006f Mon Sep 17 00:00:00 2001 From: Szimszon Date: Tue, 4 Dec 2018 18:53:44 +0000 Subject: [PATCH 069/237] Translated using Weblate (Hungarian) Currently translated at 100.0% (1383 of 1383 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/hu/ --- src/i18n/strings/hu.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index 4c3bf8b6d4..6b69512d7b 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -1409,5 +1409,6 @@ "Names and surnames by themselves are easy to guess": "Neveket egymagukban könnyű kitalálni", "Common names and surnames are easy to guess": "Elterjedt neveket könnyű kitalálni", "Great! This passphrase looks strong enough.": "Szuper! Ez a jelmondat elég erősnek látszik.", - "As a safety net, you can use it to restore your encrypted message history.": "Használhatod egy biztonsági hálóként a titkosított üzenetek visszaállításához." + "As a safety net, you can use it to restore your encrypted message history.": "Használhatod egy biztonsági hálóként a titkosított üzenetek visszaállításához.", + "Failed to load group members": "A közösség tagságokat nem sikerült betölteni" } From c8aa53cabd21fd2c94681f47b084e82ecfe01cc0 Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Wed, 5 Dec 2018 15:46:04 +0000 Subject: [PATCH 070/237] Translated using Weblate (Albanian) Currently translated at 99.3% (1374 of 1383 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/sq/ --- src/i18n/strings/sq.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index 0884a2bee9..5671184e0a 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -1379,5 +1379,7 @@ "A word by itself is easy to guess": "Një fjalë më vete është e lehtë të hamendësohet", "Names and surnames by themselves are easy to guess": "Emrat dhe mbiemrat në vetvete janë të lehtë për t’i hamendësuar", "Common names and surnames are easy to guess": "Emra dhe mbiemra të rëndomtë janë të kollajtë për t’u hamendësuar", - "Great! This passphrase looks strong enough.": "Bukur! Ky frazëkalim duket goxha i fuqishëm." + "Great! This passphrase looks strong enough.": "Bukur! Ky frazëkalim duket goxha i fuqishëm.", + "Failed to load group members": "S'u arrit të ngarkoheshin anëtarë grupi", + "As a safety net, you can use it to restore your encrypted message history.": "Si një rrjet sigurie, mund ta përdorni për të rikthyer historikun e mesazheve tuaj të fshehtëzuar." } From c553323d5aa93e8a95166ff3ac2db5076f4a2851 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Wed, 5 Dec 2018 17:39:38 +0100 Subject: [PATCH 071/237] Factor out common login code (#2307) Removes the duplication between the various points where we send off a login request and parse the response. --- src/Lifecycle.js | 21 +++------- src/Login.js | 99 ++++++++++++++++++------------------------------ 2 files changed, 42 insertions(+), 78 deletions(-) diff --git a/src/Lifecycle.js b/src/Lifecycle.js index b0912c759e..f781f36fe4 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -32,6 +32,7 @@ import Modal from './Modal'; import sdk from './index'; import ActiveWidgetStore from './stores/ActiveWidgetStore'; import PlatformPeg from "./PlatformPeg"; +import {sendLoginRequest} from "./Login"; /** * Called at startup, to attempt to build a logged-in Matrix session. It tries @@ -129,27 +130,17 @@ export function attemptTokenLogin(queryParams, defaultDeviceDisplayName) { return Promise.resolve(false); } - // create a temporary MatrixClient to do the login - const client = Matrix.createClient({ - baseUrl: queryParams.homeserver, - }); - - return client.login( + return sendLoginRequest( + queryParams.homeserver, + queryParams.identityServer, "m.login.token", { token: queryParams.loginToken, initial_device_display_name: defaultDeviceDisplayName, }, - ).then(function(data) { + ).then(function(creds) { console.log("Logged in with token"); return _clearStorage().then(() => { - _persistCredentialsToLocalStorage({ - userId: data.user_id, - deviceId: data.device_id, - accessToken: data.access_token, - homeserverUrl: queryParams.homeserver, - identityServerUrl: queryParams.identityServer, - guest: false, - }); + _persistCredentialsToLocalStorage(creds); return true; }); }).catch((err) => { diff --git a/src/Login.js b/src/Login.js index ec55a1e8c7..330eb8a8f5 100644 --- a/src/Login.js +++ b/src/Login.js @@ -1,6 +1,7 @@ /* Copyright 2015, 2016 OpenMarket Ltd Copyright 2017 Vector Creations Ltd +Copyright 2018 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,7 +18,6 @@ limitations under the License. import Matrix from "matrix-js-sdk"; -import Promise from 'bluebird'; import url from 'url'; export default class Login { @@ -141,60 +141,20 @@ export default class Login { }; Object.assign(loginParams, legacyParams); - const client = this._createTemporaryClient(); - const tryFallbackHs = (originalError) => { - const fbClient = Matrix.createClient({ - baseUrl: self._fallbackHsUrl, - idBaseUrl: this._isUrl, - }); - - return fbClient.login('m.login.password', loginParams).then(function(data) { - return Promise.resolve({ - homeserverUrl: self._fallbackHsUrl, - identityServerUrl: self._isUrl, - userId: data.user_id, - deviceId: data.device_id, - accessToken: data.access_token, - }); - }).catch((fallback_error) => { + return sendLoginRequest( + self._fallbackHsUrl, this._isUrl, 'm.login.password', loginParams, + ).catch((fallback_error) => { console.log("fallback HS login failed", fallback_error); // throw the original error throw originalError; }); }; - const tryLowercaseUsername = (originalError) => { - const loginParamsLowercase = Object.assign({}, loginParams, { - user: username.toLowerCase(), - identifier: { - user: username.toLowerCase(), - }, - }); - return client.login('m.login.password', loginParamsLowercase).then(function(data) { - return Promise.resolve({ - homeserverUrl: self._hsUrl, - identityServerUrl: self._isUrl, - userId: data.user_id, - deviceId: data.device_id, - accessToken: data.access_token, - }); - }).catch((fallback_error) => { - console.log("Lowercase username login failed", fallback_error); - // throw the original error - throw originalError; - }); - }; let originalLoginError = null; - return client.login('m.login.password', loginParams).then(function(data) { - return Promise.resolve({ - homeserverUrl: self._hsUrl, - identityServerUrl: self._isUrl, - userId: data.user_id, - deviceId: data.device_id, - accessToken: data.access_token, - }); - }).catch((error) => { + return sendLoginRequest( + self._hsUrl, self._isUrl, 'm.login.password', loginParams, + ).catch((error) => { originalLoginError = error; if (error.httpStatus === 403) { if (self._fallbackHsUrl) { @@ -202,22 +162,6 @@ export default class Login { } } throw originalLoginError; - }).catch((error) => { - // We apparently squash case at login serverside these days: - // https://github.com/matrix-org/synapse/blob/1189be43a2479f5adf034613e8d10e3f4f452eb9/synapse/handlers/auth.py#L475 - // so this wasn't needed after all. Keeping the code around in case the - // the situation changes... - - /* - if ( - error.httpStatus === 403 && - loginParams.identifier.type === 'm.id.user' && - username.search(/[A-Z]/) > -1 - ) { - return tryLowercaseUsername(originalLoginError); - } - */ - throw originalLoginError; }).catch((error) => { console.log("Login failed", error); throw error; @@ -239,3 +183,32 @@ export default class Login { return client.getSsoLoginUrl(url.format(parsedUrl), loginType); } } + + +/** + * Send a login request to the given server, and format the response + * as a MatrixClientCreds + * + * @param {string} hsUrl the base url of the Homeserver used to log in. + * @param {string} isUrl the base url of the default identity server + * @param {string} loginType the type of login to do + * @param {object} loginParams the parameters for the login + * + * @returns {MatrixClientCreds} + */ +export async function sendLoginRequest(hsUrl, isUrl, loginType, loginParams) { + const client = Matrix.createClient({ + baseUrl: hsUrl, + idBaseUrl: isUrl, + }); + + const data = await client.login(loginType, loginParams); + + return { + homeserverUrl: hsUrl, + identityServerUrl: isUrl, + userId: data.user_id, + deviceId: data.device_id, + accessToken: data.access_token, + }; +} From 28f4752c5b1672ef124743f30c6a4ac22aa29c91 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 5 Dec 2018 11:27:48 -0700 Subject: [PATCH 072/237] Rename returned completion states to be something sensible --- src/RoomInvite.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/RoomInvite.js b/src/RoomInvite.js index 32c521bb48..3547b9195f 100644 --- a/src/RoomInvite.js +++ b/src/RoomInvite.js @@ -37,7 +37,7 @@ import { _t } from './languageHandler'; */ function inviteMultipleToRoom(roomId, addrs) { const inviter = new MultiInviter(roomId); - return inviter.invite(addrs).then(addrs => Promise.resolve({addrs, inviter})); + return inviter.invite(addrs).then(states => Promise.resolve({states, inviter})); } export function showStartChatInviteDialog() { @@ -119,7 +119,7 @@ function _onStartChatFinished(shouldInvite, addrs) { room = MatrixClientPeg.get().getRoom(roomId); return inviteMultipleToRoom(roomId, addrTexts); }).then((result) => { - return _showAnyInviteErrors(result.addrs, room, result.inviter); + return _showAnyInviteErrors(result.states, room, result.inviter); }).catch((err) => { console.error(err.stack); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); @@ -139,7 +139,7 @@ function _onRoomInviteFinished(roomId, shouldInvite, addrs) { // Invite new users to a room inviteMultipleToRoom(roomId, addrTexts).then((result) => { const room = MatrixClientPeg.get().getRoom(roomId); - return _showAnyInviteErrors(result.addrs, room, result.inviter); + return _showAnyInviteErrors(result.states, room, result.inviter); }).catch((err) => { console.error(err.stack); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); From 7e817f4aa90a739c4a15e9bb91f874651ee529f9 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 5 Dec 2018 11:29:53 -0700 Subject: [PATCH 073/237] Add a helpful comment --- src/stores/RoomViewStore.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/stores/RoomViewStore.js b/src/stores/RoomViewStore.js index 5ddd84c07c..9e048e5d8e 100644 --- a/src/stores/RoomViewStore.js +++ b/src/stores/RoomViewStore.js @@ -224,6 +224,8 @@ class RoomViewStore extends Store { err: err, }); let msg = err.message ? err.message : JSON.stringify(err); + // XXX: We are relying on the error message returned by browsers here. + // This isn't great, but it does generalize the error being shown to users. if (msg && msg.startsWith("CORS request rejected")) { msg = _t("There was an error joining the room"); } From a2b825ba92b8b0751640a23e6573513b02dff97e Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 5 Dec 2018 11:52:10 -0700 Subject: [PATCH 074/237] Sort translations by file name This keeps the strings close together and roughly in the same area as the others, and makes it easier to maintain the translation file. --- scripts/gen-i18n.js | 16 ++++++++++++++-- src/i18n/strings/en_EN.json | 22 +++++++++++----------- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/scripts/gen-i18n.js b/scripts/gen-i18n.js index a1a2e6f7c5..3d3d5af116 100755 --- a/scripts/gen-i18n.js +++ b/scripts/gen-i18n.js @@ -222,10 +222,21 @@ const translatables = new Set(); const walkOpts = { listeners: { + names: function(root, nodeNamesArray) { + // Sort the names case insensitively and alphabetically to + // maintain some sense of order between the different strings. + nodeNamesArray.sort((a, b) => { + a = a.toLowerCase(); + b = b.toLowerCase(); + if (a > b) return 1; + if (a < b) return -1; + return 0; + }); + }, file: function(root, fileStats, next) { const fullPath = path.join(root, fileStats.name); - let ltrs; + let trs; if (fileStats.name.endsWith('.js')) { trs = getTranslationsJs(fullPath); } else if (fileStats.name.endsWith('.html')) { @@ -235,7 +246,8 @@ const walkOpts = { } console.log(`${fullPath} (${trs.size} strings)`); for (const tr of trs.values()) { - translatables.add(tr); + // Convert DOS line endings to unix + translatables.add(tr.replace(/\r\n/g, "\n")); } }, } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index d137d2fff4..846e66bfcd 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -43,6 +43,10 @@ "The file '%(fileName)s' failed to upload": "The file '%(fileName)s' failed to upload", "The file '%(fileName)s' exceeds this home server's size limit for uploads": "The file '%(fileName)s' exceeds this home server's size limit for uploads", "Upload Failed": "Upload Failed", + "Failure to create room": "Failure to create room", + "Server may be unavailable, overloaded, or you hit a bug.": "Server may be unavailable, overloaded, or you hit a bug.", + "Send anyway": "Send anyway", + "Send": "Send", "Sun": "Sun", "Mon": "Mon", "Tue": "Tue", @@ -82,6 +86,7 @@ "Failed to invite users to community": "Failed to invite users to community", "Failed to invite users to %(groupId)s": "Failed to invite users to %(groupId)s", "Failed to add the following rooms to %(groupId)s:": "Failed to add the following rooms to %(groupId)s:", + "Unnamed Room": "Unnamed Room", "Error": "Error", "Unable to load! Check your network connectivity and try again.": "Unable to load! Check your network connectivity and try again.", "Dismiss": "Dismiss", @@ -210,11 +215,6 @@ "%(names)s and %(count)s others are typing|other": "%(names)s and %(count)s others are typing", "%(names)s and %(count)s others are typing|one": "%(names)s and one other is typing", "%(names)s and %(lastPerson)s are typing": "%(names)s and %(lastPerson)s are typing", - "Failure to create room": "Failure to create room", - "Server may be unavailable, overloaded, or you hit a bug.": "Server may be unavailable, overloaded, or you hit a bug.", - "Send anyway": "Send anyway", - "Send": "Send", - "Unnamed Room": "Unnamed Room", "This homeserver has hit its Monthly Active User limit.": "This homeserver has hit its Monthly Active User limit.", "This homeserver has exceeded one of its resource limits.": "This homeserver has exceeded one of its resource limits.", "Please contact your service administrator to continue using the service.": "Please contact your service administrator to continue using the service.", @@ -222,6 +222,9 @@ "Your browser does not support the required cryptography extensions": "Your browser does not support the required cryptography extensions", "Not a valid Riot keyfile": "Not a valid Riot keyfile", "Authentication check failed: incorrect password?": "Authentication check failed: incorrect password?", + "You do not have permission to invite people to this room.": "You do not have permission to invite people to this room.", + "User %(user_id)s does not exist": "User %(user_id)s does not exist", + "Unknown server error": "Unknown server error", "Use a few words, avoid common phrases": "Use a few words, avoid common phrases", "No need for symbols, digits, or uppercase letters": "No need for symbols, digits, or uppercase letters", "Use a longer keyboard pattern with more turns": "Use a longer keyboard pattern with more turns", @@ -247,9 +250,6 @@ "A word by itself is easy to guess": "A word by itself is easy to guess", "Names and surnames by themselves are easy to guess": "Names and surnames by themselves are easy to guess", "Common names and surnames are easy to guess": "Common names and surnames are easy to guess", - "You do not have permission to invite people to this room.": "You do not have permission to invite people to this room.", - "User %(user_id)s does not exist": "User %(user_id)s does not exist", - "Unknown server error": "Unknown server error", "Sorry, your homeserver is too old to participate in this room.": "Sorry, your homeserver is too old to participate in this room.", "Please contact your homeserver administrator.": "Please contact your homeserver administrator.", "Failed to join room": "Failed to join room", @@ -490,11 +490,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", @@ -733,6 +733,7 @@ "Remove this user from community?": "Remove this user from community?", "Failed to withdraw invitation": "Failed to withdraw invitation", "Failed to remove user from community": "Failed to remove user from community", + "Failed to load group members": "Failed to load group members", "Filter community members": "Filter community members", "Flair will appear if enabled in room settings": "Flair will appear if enabled in room settings", "Flair will not appear": "Flair will not appear", @@ -1103,7 +1104,6 @@ "Community %(groupId)s not found": "Community %(groupId)s not found", "This Home server does not support communities": "This Home server does not support communities", "Failed to load %(groupId)s": "Failed to load %(groupId)s", - "Failed to load group members": "Failed to load group members", "Couldn't load home page": "Couldn't load home page", "You are currently using Riot anonymously as a guest.": "You are currently using Riot anonymously as a guest.", "If you would like to create a Matrix account you can register now.": "If you would like to create a Matrix account you can register now.", From 216fc6412a425e7a43163674f14f11064cf2ea1d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 5 Dec 2018 13:52:27 -0700 Subject: [PATCH 075/237] Fix pinning of rooms without badges Fixes https://github.com/vector-im/riot-web/issues/7723 This adds consideration for rooms that are "mentions only" (or "unread-muted" as internally referenced). --- src/stores/RoomListStore.js | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/stores/RoomListStore.js b/src/stores/RoomListStore.js index 0f8e5d7b4d..0d99377180 100644 --- a/src/stores/RoomListStore.js +++ b/src/stores/RoomListStore.js @@ -300,6 +300,10 @@ class RoomListStore extends Store { const ts = this._tsOfNewestEvent(room); this._updateCachedRoomState(roomId, "timestamp", ts); return ts; + } else if (type === "unread-muted") { + const unread = Unread.doesRoomHaveUnreadMessages(room); + this._updateCachedRoomState(roomId, "unread-muted", unread); + return unread; } else if (type === "unread") { const unread = room.getUnreadNotificationCount() > 0; this._updateCachedRoomState(roomId, "unread", unread); @@ -358,8 +362,21 @@ class RoomListStore extends Store { } if (pinUnread) { - const unreadA = this._getRoomState(roomA, "unread"); - const unreadB = this._getRoomState(roomB, "unread"); + let unreadA = this._getRoomState(roomA, "unread"); + let unreadB = this._getRoomState(roomB, "unread"); + if (unreadA && !unreadB) return -1; + if (!unreadA && unreadB) return 1; + + // If they both have unread messages, sort by timestamp + // If nether have unread message (the fourth check not shown + // here), then just sort by timestamp anyways. + if (unreadA && unreadB) return timestampDiff; + + // Unread can also mean "unread without badge", which is + // different from what the above checks for. We're also + // going to sort those here. + unreadA = this._getRoomState(roomA, "unread-muted"); + unreadB = this._getRoomState(roomB, "unread-muted"); if (unreadA && !unreadB) return -1; if (!unreadA && unreadB) return 1; From 93c90896b5d9508b4a4bb82dc2fdef9e3800c3fe Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 5 Dec 2018 14:00:09 -0700 Subject: [PATCH 076/237] Regenerate en_EN.json --- src/i18n/strings/en_EN.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index e7fbdcdd43..7d263f6a4c 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -225,7 +225,6 @@ "You do not have permission to invite people to this room.": "You do not have permission to invite people to this room.", "User %(user_id)s does not exist": "User %(user_id)s does not exist", "Unknown server error": "Unknown server error", - "There was an error joining the room": "There was an error joining the room", "Use a few words, avoid common phrases": "Use a few words, avoid common phrases", "No need for symbols, digits, or uppercase letters": "No need for symbols, digits, or uppercase letters", "Use a longer keyboard pattern with more turns": "Use a longer keyboard pattern with more turns", @@ -251,6 +250,7 @@ "A word by itself is easy to guess": "A word by itself is easy to guess", "Names and surnames by themselves are easy to guess": "Names and surnames by themselves are easy to guess", "Common names and surnames are easy to guess": "Common names and surnames are easy to guess", + "There was an error joining the room": "There was an error joining the room", "Sorry, your homeserver is too old to participate in this room.": "Sorry, your homeserver is too old to participate in this room.", "Please contact your homeserver administrator.": "Please contact your homeserver administrator.", "Failed to join room": "Failed to join room", From f08a54ed1e9b157c10ac05115ed69073305e3021 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 5 Dec 2018 18:00:09 -0700 Subject: [PATCH 077/237] Don't consider ACL'd servers as permalink candidates and fix the bug where it was picking 4 servers instead of 3. This was due to `<=` instead of `<` in the `MAX_SERVER_CANDIDATES` loop. Added tests for all the things. Fixes https://github.com/vector-im/riot-web/issues/7752 Fixes https://github.com/vector-im/riot-web/issues/7682 --- package.json | 1 + src/matrix-to.js | 61 +++++++++++++- test/matrix-to-test.js | 186 ++++++++++++++++++++++++++++++++++++++--- 3 files changed, 232 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index 67d1f3ba1e..d444c15eab 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "gfm.css": "^1.1.1", "glob": "^5.0.14", "highlight.js": "^9.13.0", + "is-ip": "^2.0.0", "isomorphic-fetch": "^2.2.1", "linkifyjs": "^2.1.6", "lodash": "^4.13.1", diff --git a/src/matrix-to.js b/src/matrix-to.js index b5827f671a..fb2c8096d7 100644 --- a/src/matrix-to.js +++ b/src/matrix-to.js @@ -15,6 +15,8 @@ limitations under the License. */ import MatrixClientPeg from "./MatrixClientPeg"; +import isIp from "is-ip"; +import utils from 'matrix-js-sdk/lib/utils'; export const host = "matrix.to"; export const baseUrl = `https://${host}`; @@ -90,7 +92,9 @@ export function pickServerCandidates(roomId) { // Rationale for popular servers: It's hard to get rid of people when // they keep flocking in from a particular server. Sure, the server could // be ACL'd in the future or for some reason be evicted from the room - // however an event like that is unlikely the larger the room gets. + // however an event like that is unlikely the larger the room gets. If + // the server is ACL'd at the time of generating the link however, we + // shouldn't pick them. We also don't pick IP addresses. // Note: we don't pick the server the room was created on because the // homeserver should already be using that server as a last ditch attempt @@ -104,12 +108,29 @@ export function pickServerCandidates(roomId) { // The receiving user can then manually append the known-good server to // the list and magically have the link work. + const bannedHostsRegexps = []; + let allowedHostsRegexps = [new RegExp(".*")]; // default allow everyone + if (room.currentState) { + const aclEvent = room.currentState.getStateEvents("m.room.server_acl", ""); + if (aclEvent && aclEvent.getContent()) { + const getRegex = (hostname) => new RegExp("^" + utils.globToRegexp(hostname, false) + "$"); + + const denied = aclEvent.getContent().deny || []; + denied.forEach(h => bannedHostsRegexps.push(getRegex(h))); + + const allowed = aclEvent.getContent().allow || []; + allowedHostsRegexps = []; // we don't want to use the default rule here + allowed.forEach(h => allowedHostsRegexps.push(getRegex(h))); + } + } + const populationMap: {[server:string]:number} = {}; const highestPlUser = {userId: null, powerLevel: 0, serverName: null}; for (const member of room.getJoinedMembers()) { const serverName = member.userId.split(":").splice(1).join(":"); - if (member.powerLevel > highestPlUser.powerLevel) { + if (member.powerLevel > highestPlUser.powerLevel && !isHostnameIpAddress(serverName) + && !isHostInRegex(serverName, bannedHostsRegexps) && isHostInRegex(serverName, allowedHostsRegexps)) { highestPlUser.userId = member.userId; highestPlUser.powerLevel = member.powerLevel; highestPlUser.serverName = serverName; @@ -125,8 +146,9 @@ export function pickServerCandidates(roomId) { const beforePopulation = candidates.length; const serversByPopulation = Object.keys(populationMap) .sort((a, b) => populationMap[b] - populationMap[a]) - .filter(a => !candidates.includes(a)); - for (let i = beforePopulation; i <= MAX_SERVER_CANDIDATES; i++) { + .filter(a => !candidates.includes(a) && !isHostnameIpAddress(a) + && !isHostInRegex(a, bannedHostsRegexps) && isHostInRegex(a, allowedHostsRegexps)); + for (let i = beforePopulation; i < MAX_SERVER_CANDIDATES; i++) { const idx = i - beforePopulation; if (idx >= serversByPopulation.length) break; candidates.push(serversByPopulation[idx]); @@ -134,3 +156,34 @@ export function pickServerCandidates(roomId) { return candidates; } + +function getHostnameFromMatrixDomain(domain) { + if (!domain) return null; + + // The hostname might have a port, so we convert it to a URL and + // split out the real hostname. + const parser = document.createElement('a'); + parser.href = "https://" + domain; + return parser.hostname; +} + +function isHostInRegex(hostname, regexps) { + hostname = getHostnameFromMatrixDomain(hostname); + if (!hostname) return true; // assumed + if (regexps.length > 0 && !regexps[0].test) throw new Error(regexps[0]); + + return regexps.filter(h => h.test(hostname)).length > 0; +} + +function isHostnameIpAddress(hostname) { + hostname = getHostnameFromMatrixDomain(hostname); + if (!hostname) return false; + + // is-ip doesn't want IPv6 addresses surrounded by brackets, so + // take them off. + if (hostname.startsWith("[") && hostname.endsWith("]")) { + hostname = hostname.substring(1, hostname.length - 1); + } + + return isIp(hostname); +} \ No newline at end of file diff --git a/test/matrix-to-test.js b/test/matrix-to-test.js index 70533575c4..6392e326e9 100644 --- a/test/matrix-to-test.js +++ b/test/matrix-to-test.js @@ -150,7 +150,39 @@ describe('matrix-to', function() { expect(pickedServers[2]).toBe("third"); }); - it('should work with IPv4 hostnames', function() { + it('should pick a maximum of 3 candidate servers', function() { + peg.get().getRoom = () => { + return { + getJoinedMembers: () => [ + { + userId: "@alice:alpha", + powerLevel: 100, + }, + { + userId: "@alice:bravo", + powerLevel: 0, + }, + { + userId: "@alice:charlie", + powerLevel: 0, + }, + { + userId: "@alice:delta", + powerLevel: 0, + }, + { + userId: "@alice:echo", + powerLevel: 0, + }, + ], + }; + }; + const pickedServers = pickServerCandidates("!somewhere:example.org"); + expect(pickedServers).toExist(); + expect(pickedServers.length).toBe(3); + }); + + it('should not consider IPv4 hosts', function() { peg.get().getRoom = () => { return { getJoinedMembers: () => [ @@ -163,11 +195,10 @@ describe('matrix-to', function() { }; const pickedServers = pickServerCandidates("!somewhere:example.org"); expect(pickedServers).toExist(); - expect(pickedServers.length).toBe(1); - expect(pickedServers[0]).toBe("127.0.0.1"); + expect(pickedServers.length).toBe(0); }); - it('should work with IPv6 hostnames', function() { + it('should not consider IPv6 hosts', function() { peg.get().getRoom = () => { return { getJoinedMembers: () => [ @@ -180,11 +211,10 @@ describe('matrix-to', function() { }; const pickedServers = pickServerCandidates("!somewhere:example.org"); expect(pickedServers).toExist(); - expect(pickedServers.length).toBe(1); - expect(pickedServers[0]).toBe("[::1]"); + expect(pickedServers.length).toBe(0); }); - it('should work with IPv4 hostnames with ports', function() { + it('should not consider IPv4 hostnames with ports', function() { peg.get().getRoom = () => { return { getJoinedMembers: () => [ @@ -197,11 +227,10 @@ describe('matrix-to', function() { }; const pickedServers = pickServerCandidates("!somewhere:example.org"); expect(pickedServers).toExist(); - expect(pickedServers.length).toBe(1); - expect(pickedServers[0]).toBe("127.0.0.1:8448"); + expect(pickedServers.length).toBe(0); }); - it('should work with IPv6 hostnames with ports', function() { + it('should not consider IPv6 hostnames with ports', function() { peg.get().getRoom = () => { return { getJoinedMembers: () => [ @@ -214,8 +243,7 @@ describe('matrix-to', function() { }; const pickedServers = pickServerCandidates("!somewhere:example.org"); expect(pickedServers).toExist(); - expect(pickedServers.length).toBe(1); - expect(pickedServers[0]).toBe("[::1]:8448"); + expect(pickedServers.length).toBe(0); }); it('should work with hostnames with ports', function() { @@ -235,6 +263,140 @@ describe('matrix-to', function() { expect(pickedServers[0]).toBe("example.org:8448"); }); + it('should not consider servers explicitly denied by ACLs', function() { + peg.get().getRoom = () => { + return { + getJoinedMembers: () => [ + { + userId: "@alice:evilcorp.com", + powerLevel: 100, + }, + { + userId: "@bob:chat.evilcorp.com", + powerLevel: 0, + }, + ], + currentState: { + getStateEvents: (type, key) => { + if (type !== "m.room.server_acl" || key !== "") return null; + return { + getContent: () => { + return { + deny: ["evilcorp.com", "*.evilcorp.com"], + allow: ["*"], + }; + }, + }; + }, + }, + }; + }; + const pickedServers = pickServerCandidates("!somewhere:example.org"); + expect(pickedServers).toExist(); + expect(pickedServers.length).toBe(0); + }); + + it('should not consider servers not allowed by ACLs', function() { + peg.get().getRoom = () => { + return { + getJoinedMembers: () => [ + { + userId: "@alice:evilcorp.com", + powerLevel: 100, + }, + { + userId: "@bob:chat.evilcorp.com", + powerLevel: 0, + }, + ], + currentState: { + getStateEvents: (type, key) => { + if (type !== "m.room.server_acl" || key !== "") return null; + return { + getContent: () => { + return { + deny: [], + allow: [], // implies "ban everyone" + }; + }, + }; + }, + }, + }; + }; + const pickedServers = pickServerCandidates("!somewhere:example.org"); + expect(pickedServers).toExist(); + expect(pickedServers.length).toBe(0); + }); + + it('should consider servers not explicitly banned by ACLs', function() { + peg.get().getRoom = () => { + return { + getJoinedMembers: () => [ + { + userId: "@alice:evilcorp.com", + powerLevel: 100, + }, + { + userId: "@bob:chat.evilcorp.com", + powerLevel: 0, + }, + ], + currentState: { + getStateEvents: (type, key) => { + if (type !== "m.room.server_acl" || key !== "") return null; + return { + getContent: () => { + return { + deny: ["*.evilcorp.com"], // evilcorp.com is still good though + allow: ["*"], + }; + }, + }; + }, + }, + }; + }; + const pickedServers = pickServerCandidates("!somewhere:example.org"); + expect(pickedServers).toExist(); + expect(pickedServers.length).toBe(1); + expect(pickedServers[0]).toEqual("evilcorp.com"); + }); + + it('should consider servers not disallowed by ACLs', function() { + peg.get().getRoom = () => { + return { + getJoinedMembers: () => [ + { + userId: "@alice:evilcorp.com", + powerLevel: 100, + }, + { + userId: "@bob:chat.evilcorp.com", + powerLevel: 0, + }, + ], + currentState: { + getStateEvents: (type, key) => { + if (type !== "m.room.server_acl" || key !== "") return null; + return { + getContent: () => { + return { + deny: [], + allow: ["evilcorp.com"], // implies "ban everyone else" + }; + }, + }; + }, + }, + }; + }; + const pickedServers = pickServerCandidates("!somewhere:example.org"); + expect(pickedServers).toExist(); + expect(pickedServers.length).toBe(1); + expect(pickedServers[0]).toEqual("evilcorp.com"); + }); + it('should generate an event permalink for room IDs with no candidate servers', function() { peg.get().getRoom = () => null; const result = makeEventPermalink("!somewhere:example.org", "$something:example.com"); From 45bc1f7dbdfe72e969e80890849426bcc5285703 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 5 Dec 2018 18:14:22 -0700 Subject: [PATCH 078/237] Appease the linter --- src/matrix-to.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix-to.js b/src/matrix-to.js index fb2c8096d7..b750dff6d6 100644 --- a/src/matrix-to.js +++ b/src/matrix-to.js @@ -186,4 +186,4 @@ function isHostnameIpAddress(hostname) { } return isIp(hostname); -} \ No newline at end of file +} From 870825b1803a509cc2ae289c20db4589610a128f Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 6 Dec 2018 11:15:36 +0000 Subject: [PATCH 079/237] js-sdk rc.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 67d1f3ba1e..b3c8461efe 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "linkifyjs": "^2.1.6", "lodash": "^4.13.1", "lolex": "2.3.2", - "matrix-js-sdk": "0.14.1", + "matrix-js-sdk": "0.14.2-rc.1", "optimist": "^0.6.1", "pako": "^1.0.5", "prop-types": "^15.5.8", From e2c01445d34eb484dd3c353f9da383e985f7c722 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 6 Dec 2018 11:18:37 +0000 Subject: [PATCH 080/237] Prepare changelog for v0.14.7-rc.1 --- CHANGELOG.md | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index eea47dcb8f..44d05396d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,44 @@ +Changes in [0.14.7-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.14.7-rc.1) (2018-12-06) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.14.6...v0.14.7-rc.1) + + * Suppress CORS errors in the 'failed to join room' dialog + [\#2306](https://github.com/matrix-org/matrix-react-sdk/pull/2306) + * Check if users exist before inviting them and communicate errors + [\#2317](https://github.com/matrix-org/matrix-react-sdk/pull/2317) + * Update from Weblate. + [\#2328](https://github.com/matrix-org/matrix-react-sdk/pull/2328) + * Allow group summary to load when /users fails + [\#2326](https://github.com/matrix-org/matrix-react-sdk/pull/2326) + * Show correct text if passphrase is skipped + [\#2324](https://github.com/matrix-org/matrix-react-sdk/pull/2324) + * Add password strength meter to backup creation UI + [\#2294](https://github.com/matrix-org/matrix-react-sdk/pull/2294) + * Check upload limits before trying to upload large files + [\#1876](https://github.com/matrix-org/matrix-react-sdk/pull/1876) + * Support .well-known discovery + [\#2227](https://github.com/matrix-org/matrix-react-sdk/pull/2227) + * Make create key backup dialog async + [\#2291](https://github.com/matrix-org/matrix-react-sdk/pull/2291) + * Forgot to enable continue button on download + [\#2288](https://github.com/matrix-org/matrix-react-sdk/pull/2288) + * Online incremental megolm backups (v2) + [\#2169](https://github.com/matrix-org/matrix-react-sdk/pull/2169) + * Add recovery key download button + [\#2284](https://github.com/matrix-org/matrix-react-sdk/pull/2284) + * Passphrase Support for e2e backups + [\#2283](https://github.com/matrix-org/matrix-react-sdk/pull/2283) + * Update async dialog interface to use promises + [\#2286](https://github.com/matrix-org/matrix-react-sdk/pull/2286) + * Support for m.login.sso + [\#2279](https://github.com/matrix-org/matrix-react-sdk/pull/2279) + * Added badge to non-autoplay GIFs + [\#2235](https://github.com/matrix-org/matrix-react-sdk/pull/2235) + * Improve terms auth flow + [\#2277](https://github.com/matrix-org/matrix-react-sdk/pull/2277) + * Handle crypto db version upgrade + [\#2282](https://github.com/matrix-org/matrix-react-sdk/pull/2282) + Changes in [0.14.6](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.14.6) (2018-11-22) ===================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.14.5...v0.14.6) From 58ab9a09953c21375aa946b43d726cb8966a239f Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 6 Dec 2018 11:18:37 +0000 Subject: [PATCH 081/237] v0.14.7-rc.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b3c8461efe..ebf3648f7d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "0.14.6", + "version": "0.14.7-rc.1", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { From 77c51aff2d26dd21f9bbd6ea6b1f813ab6c01714 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 6 Dec 2018 11:44:00 +0000 Subject: [PATCH 082/237] Ship the babelrc file to npm We ship the source files, so it probably makes sense to ship the babelrc that tells you how to compile them. --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index ebf3648f7d..09078927c4 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "license": "Apache-2.0", "main": "lib/index.js", "files": [ + ".babelrc", ".eslintrc.js", "CHANGELOG.md", "CONTRIBUTING.rst", From c94d8d6f68120a70f00a9c0f597fd4eed6c5f78a Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 6 Dec 2018 12:39:23 +0000 Subject: [PATCH 083/237] Prepare changelog for v0.14.7-rc.2 --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 44d05396d9..1261ad8d40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +Changes in [0.14.7-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.14.7-rc.2) (2018-12-06) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.14.7-rc.1...v0.14.7-rc.2) + + * Ship the babelrc file to npm + [\#2332](https://github.com/matrix-org/matrix-react-sdk/pull/2332) + Changes in [0.14.7-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.14.7-rc.1) (2018-12-06) =============================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.14.6...v0.14.7-rc.1) From a82b54f25a65e49712e32eb44f32eacf81d73eda Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 6 Dec 2018 12:39:24 +0000 Subject: [PATCH 084/237] v0.14.7-rc.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 09078927c4..58aefff176 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "0.14.7-rc.1", + "version": "0.14.7-rc.2", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { From ca1313099f4db238374f281944710bccd1b2398d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 6 Dec 2018 11:45:58 -0700 Subject: [PATCH 085/237] Show the IncomingCallBox if the call is for the RoomSubList Fixes https://github.com/vector-im/riot-web/issues/4369 Previously the RoomSubList would filter its list of rooms to verify that the incoming call belongs to it. This causes problems when the sub list is being told some rooms don't exist (ie: the list is filtered). It is trivial for the RoomList to instead track which RoomSubList (tag) it should be handing the call off to so we do that instead now. The RoomSubList trusts that the caller has already filtered it and will render the IncomingCallBox if it has an incoming call. --- src/components/structures/RoomSubList.js | 15 +++----- src/components/views/rooms/RoomList.js | 45 +++++++++++++++++++----- 2 files changed, 41 insertions(+), 19 deletions(-) diff --git a/src/components/structures/RoomSubList.js b/src/components/structures/RoomSubList.js index d798070659..cdeb8926c0 100644 --- a/src/components/structures/RoomSubList.js +++ b/src/components/structures/RoomSubList.js @@ -327,17 +327,10 @@ const RoomSubList = React.createClass({ let incomingCall; if (this.props.incomingCall) { - const self = this; - // Check if the incoming call is for this section - const incomingCallRoom = this.props.list.filter(function(room) { - return self.props.incomingCall.roomId === room.roomId; - }); - - if (incomingCallRoom.length === 1) { - const IncomingCallBox = sdk.getComponent("voip.IncomingCallBox"); - incomingCall = - ; - } + // We can assume that if we have an incoming call then it is for this list + const IncomingCallBox = sdk.getComponent("voip.IncomingCallBox"); + incomingCall = + ; } const tabindex = this.props.searchFilter === "" ? "0" : "-1"; diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 3e632ba8ce..3ad35c036d 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -71,6 +71,7 @@ module.exports = React.createClass({ isLoadingLeftRooms: false, totalRoomCount: null, lists: {}, + incomingCallTag: null, incomingCall: null, selectedTags: [], }; @@ -155,11 +156,13 @@ module.exports = React.createClass({ if (call && call.call_state === 'ringing') { this.setState({ incomingCall: call, + incomingCallTag: this.getTagNameForRoomId(payload.room_id), }); this._repositionIncomingCallBox(undefined, true); } else { this.setState({ incomingCall: null, + incomingCallTag: null, }); } break; @@ -328,6 +331,26 @@ module.exports = React.createClass({ // this._lastRefreshRoomListTs = Date.now(); }, + getTagNameForRoomId: function(roomId) { + const lists = RoomListStore.getRoomLists(); + for (const tagName of Object.keys(lists)) { + for (const room of lists[tagName]) { + // Should be impossible, but guard anyways. + if (!room) { + continue; + } + const myUserId = MatrixClientPeg.get().getUserId(); + if (HIDE_CONFERENCE_CHANS && Rooms.isConfCallRoom(room, myUserId, this.props.ConferenceHandler)) { + continue; + } + + if (room.roomId === roomId) return tagName; + } + } + + return null; + }, + getRoomLists: function() { const lists = RoomListStore.getRoomLists(); @@ -621,6 +644,12 @@ module.exports = React.createClass({ // so checking on every render is the sanest thing at this time. const showEmpty = SettingsStore.getValue('RoomSubList.showEmpty'); + const incomingCallIfTaggedAs = (tagName) => { + if (!this.state.incomingCall) return null; + if (this.state.incomingCallTag !== tagName) return null; + return this.state.incomingCall; + }; + const self = this; return ( @@ -750,7 +779,7 @@ module.exports = React.createClass({ tagName="m.lowpriority" editable={false} order="recent" - incomingCall={self.state.incomingCall} + incomingCall={incomingCallIfTaggedAs('m.server_notice')} collapsed={self.props.collapsed} searchFilter={self.props.searchFilter} onHeaderClick={self.onSubListHeaderClick} From 757181c322937c1a9bd650b56e2e892b16231fdf Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 6 Dec 2018 16:18:46 -0600 Subject: [PATCH 086/237] Update React guide in code style This updates React guidance to prefer JS classes and adds additional info about how to handle specific situations when using them. Signed-off-by: J. Ryan Stinnett --- code_style.md | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/code_style.md b/code_style.md index 2cac303e54..96f3879ebc 100644 --- a/code_style.md +++ b/code_style.md @@ -165,7 +165,6 @@ ECMAScript React ----- -- Use React.createClass rather than ES6 classes for components, as the boilerplate is way too heavy on ES6 currently. ES7 might improve it. - Pull out functions in props to the class, generally as specific event handlers: ```jsx @@ -174,11 +173,38 @@ React // Better // Best, if onFooClick would do anything other than directly calling doStuff ``` - - Not doing so is acceptable in a single case; in function-refs: - + + Not doing so is acceptable in a single case: in function-refs: + ```jsx this.component = self}> ``` + +- Prefer classes that extend `React.Component` (or `React.PureComponent`) instead of `React.createClass` + - You can avoid the need to bind handler functions by using [property initializers](https://reactjs.org/docs/react-component.html#constructor): + + ```js + class Widget extends React.Component + onFooClick = () => { + ... + } + } + ``` + - To define `propTypes`, use a static property: + ```js + class Widget extends React.Component + static propTypes = { + ... + } + } + ``` + - If you need to specify initial component state, [assign it](https://reactjs.org/docs/react-component.html#constructor) to `this.state` in the constructor: + ```js + constructor(props) { + super(props); + // Don't call this.setState() here! + this.state = { counter: 0 }; + } + ``` - Think about whether your component really needs state: are you duplicating information in component state that could be derived from the model? From 173669b375aa86fda3303a757db14f4407a5854a Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 6 Dec 2018 16:18:02 -0700 Subject: [PATCH 087/237] Show the number of unread notifications above the bell on the right Fixes https://github.com/vector-im/riot-web/issues/3383 This achieves the result by counting up the number of highlights across all rooms and setting that as the badge above the icon. If there are no highlights, nothing is displayed. The red highlight on the bell is done by abusing how the Tinter works: because it has access to the properties of the SVG that we'd need to override it, we give it a collection of colors it should use instead of the theme/tint it is trying to apply. This results in the Tinter using our warning color instead of whatever it was going to apply. The RightPanel now listens for events to update the count too, otherwise when the user receives a ping they'd have to switch rooms to see the change. --- res/css/structures/_RightPanel.scss | 4 +++ src/Tinter.js | 13 +++++---- src/components/structures/RightPanel.js | 28 ++++++++++++++++++-- src/components/views/elements/TintableSvg.js | 3 ++- 4 files changed, 40 insertions(+), 8 deletions(-) diff --git a/res/css/structures/_RightPanel.scss b/res/css/structures/_RightPanel.scss index b4dff612ed..554aabfcd1 100644 --- a/res/css/structures/_RightPanel.scss +++ b/res/css/structures/_RightPanel.scss @@ -55,6 +55,10 @@ limitations under the License. padding-bottom: 3px; } +.mx_RightPanel_headerButton_badgeHighlight .mx_RightPanel_headerButton_badge { + color: $warning-color; +} + .mx_RightPanel_headerButton_highlight { width: 25px; height: 5px; diff --git a/src/Tinter.js b/src/Tinter.js index d24a4c3e74..1b1ebbcccd 100644 --- a/src/Tinter.js +++ b/src/Tinter.js @@ -390,7 +390,7 @@ class Tinter { // XXX: we could just move this all into TintableSvg, but as it's so similar // to the CSS fixup stuff in Tinter (just that the fixups are stored in TintableSvg) // keeping it here for now. - calcSvgFixups(svgs) { + calcSvgFixups(svgs, forceColors) { // go through manually fixing up SVG colours. // we could do this by stylesheets, but keeping the stylesheets // updated would be a PITA, so just brute-force search for the @@ -418,13 +418,14 @@ class Tinter { const tag = tags[j]; for (let k = 0; k < this.svgAttrs.length; k++) { const attr = this.svgAttrs[k]; - for (let l = 0; l < this.keyHex.length; l++) { + for (let m = 0; m < this.keyHex.length; m++) { // dev note: don't use L please. if (tag.getAttribute(attr) && - tag.getAttribute(attr).toUpperCase() === this.keyHex[l]) { + tag.getAttribute(attr).toUpperCase() === this.keyHex[m]) { fixups.push({ node: tag, attr: attr, - index: l, + index: m, + forceColors: forceColors, }); } } @@ -440,7 +441,9 @@ class Tinter { if (DEBUG) console.log("applySvgFixups start for " + fixups); for (let i = 0; i < fixups.length; i++) { const svgFixup = fixups[i]; - svgFixup.node.setAttribute(svgFixup.attr, this.colors[svgFixup.index]); + const forcedColor = svgFixup.forceColors ? svgFixup.forceColors[svgFixup.index] : null; + if (forcedColor) console.log(forcedColor); + svgFixup.node.setAttribute(svgFixup.attr, forcedColor ? forcedColor : this.colors[svgFixup.index]); } if (DEBUG) console.log("applySvgFixups end"); } diff --git a/src/components/structures/RightPanel.js b/src/components/structures/RightPanel.js index 9017447a34..c21c5f459f 100644 --- a/src/components/structures/RightPanel.js +++ b/src/components/structures/RightPanel.js @@ -30,6 +30,7 @@ import { showGroupInviteDialog, showGroupAddRoomDialog } from '../../GroupAddres import GroupStore from '../../stores/GroupStore'; import { formatCount } from '../../utils/FormattingUtils'; +import MatrixClientPeg from "../../MatrixClientPeg"; class HeaderButton extends React.Component { constructor() { @@ -49,17 +50,26 @@ class HeaderButton extends React.Component { const TintableSvg = sdk.getComponent("elements.TintableSvg"); const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); + // XXX: We really shouldn't be hardcoding colors here, but the way TintableSvg + // works kinda prevents us from using normal CSS tactics. We use $warning-color + // here. + // Note: This array gets passed along to the Tinter's forceColors eventually. + const tintableColors = this.props.badgeHighlight ? ["#ff0064"] : null; + + const classNames = ["mx_RightPanel_headerButton"]; + if (this.props.badgeHighlight) classNames.push("mx_RightPanel_headerButton_badgeHighlight"); + return
    { this.props.badge ? this.props.badge :   }
    - + { this.props.isHighlighted ?
    :
    } ; @@ -76,6 +86,7 @@ HeaderButton.propTypes = { // The badge to display above the icon badge: PropTypes.node, + badgeHighlight: PropTypes.bool, // The parameters to track the click event analytics: PropTypes.arrayOf(PropTypes.string).isRequired, @@ -113,6 +124,7 @@ module.exports = React.createClass({ this.dispatcherRef = dis.register(this.onAction); const cli = this.context.matrixClient; cli.on("RoomState.members", this.onRoomStateMember); + cli.on("Room.notificationCounts", this.onRoomNotifications); this._initGroupStore(this.props.groupId); }, @@ -200,6 +212,10 @@ module.exports = React.createClass({ } }, + onRoomNotifications: function(room, type, count) { + if (type === "highlight") this.forceUpdate(); + }, + _delayedUpdate: new RateLimitedFunc(function() { this.forceUpdate(); // eslint-disable-line babel/no-invalid-this }, 500), @@ -308,6 +324,13 @@ module.exports = React.createClass({ let headerButtons = []; if (this.props.roomId) { + let notifCountBadge; + let notifCount = 0; + MatrixClientPeg.get().getRooms().forEach(r => notifCount += (r.getUnreadNotificationCount('highlight') || 0)); + if (notifCount > 0) { + notifCountBadge =
    { formatCount(notifCount) }
    ; + } + headerButtons = [ 0} analytics={['Right Panel', 'Notification List Button', 'click']} />, ]; diff --git a/src/components/views/elements/TintableSvg.js b/src/components/views/elements/TintableSvg.js index e04bf87793..af9e56377b 100644 --- a/src/components/views/elements/TintableSvg.js +++ b/src/components/views/elements/TintableSvg.js @@ -29,6 +29,7 @@ var TintableSvg = React.createClass({ width: PropTypes.string.isRequired, height: PropTypes.string.isRequired, className: PropTypes.string, + forceColors: PropTypes.arrayOf(PropTypes.string), }, statics: { @@ -58,7 +59,7 @@ var TintableSvg = React.createClass({ onLoad: function(event) { // console.log("TintableSvg.onLoad for " + this.props.src); - this.fixups = Tinter.calcSvgFixups([event.target]); + this.fixups = Tinter.calcSvgFixups([event.target], this.props.forceColors); Tinter.applySvgFixups(this.fixups); }, From 0b65a1ee1a91fd25f8d53f80dae521ce6aaea8b0 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 6 Dec 2018 19:24:59 -0600 Subject: [PATCH 088/237] Remove outdated info about custom skins It has been marked outdated for several years. Since it appears on the repo home page, it makes the project feel unmaintained. Signed-off-by: J. Ryan Stinnett --- README.md | 58 ------------------------------------------------------- 1 file changed, 58 deletions(-) diff --git a/README.md b/README.md index ac45497dd4..ec95fbd132 100644 --- a/README.md +++ b/README.md @@ -127,61 +127,3 @@ Github Issues All issues should be filed under https://github.com/vector-im/riot-web/issues for now. - -OUTDATED: To Create Your Own Skin -================================= - -**This is ALL LIES currently, and needs to be updated** - -Skins are modules are exported from such a package in the `lib` directory. -`lib/skins` contains one directory per-skin, named after the skin, and the -`modules` directory contains modules as their javascript files. - -A basic skin is provided in the matrix-react-skin package. This also contains -a minimal application that instantiates the basic skin making a working matrix -client. - -You can use matrix-react-sdk directly, but to do this you would have to provide -'views' for each UI component. To get started quickly, use matrix-react-skin. - -To actually change the look of a skin, you can create a base skin (which -does not use views from any other skin) or you can make a derived skin. -Note that derived skins are currently experimental: for example, the CSS -from the skins it is based on will not be automatically included. - -To make a skin, create React classes for any custom components you wish to add -in a skin within `src/skins/`. These can be based off the files in -`views` in the `matrix-react-skin` package, modifying the require() statement -appropriately. - -If you make a derived skin, you only need copy the files you wish to customise. - -Once you've made all your view files, you need to make a `skinfo.json`. This -contains all the metadata for a skin. This is a JSON file with, currently, a -single key, 'baseSkin'. Set this to the empty string if your skin is a base skin, -or for a derived skin, set it to the path of your base skin's skinfo.json file, as -you would use in a require call. - -Now you have the basis of a skin, you need to generate a skindex.json file. The -`reskindex.js` tool in matrix-react-sdk does this for you. It is suggested that -you add an npm script to run this, as in matrix-react-skin. - -For more specific detail on any of these steps, look at matrix-react-skin. - -Alternative instructions: - - * Create a new NPM project. Be sure to directly depend on react, (otherwise - you can end up with two copies of react). - * Create an index.js file that sets up react. Add require statements for - React and matrix-react-sdk. Load a skin using the 'loadSkin' method on the - SDK and call Render. This can be a skin provided by a separate package or - a skin in the same package. - * Add a way to build your project: we suggest copying the scripts block - from matrix-react-skin (which uses babel and webpack). You could use - different tools but remember that at least the skins and modules of - your project should end up in plain (ie. non ES6, non JSX) javascript in - the lib directory at the end of the build process, as well as any - packaging that you might do. - * Create an index.html file pulling in your compiled javascript and the - CSS bundle from the skin you use. For now, you'll also need to manually - import CSS from any skins that your skin inherts from. From 95d15b78632bdf770928729fb3f468295cd74925 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 6 Dec 2018 22:26:51 -0700 Subject: [PATCH 089/237] Fix tinting of notification icon and use a more reliable notification source The js-sdk's placement of the notification change was unreliable and could cause stuck notifications. The new location (piggybacking the Notifier) is a lot more reliable. The tinting has been changed fairly invasively in order to support the changing of the `fill` attribute. What was happening before was the `fill` property would happily get set to the forced color value, but when it came time to reset it it wouldn't be part of the colors array and fail the check, therefore never being changed back. By using a second field we can ensure we are checking the not-forced value where possible, falling back to the potentially forced value if needed. In addition to fixing which color the Tinter was checking against, something noticed during development is that `this.colors` might not always be a set of hex color codes. This is problematic when the attribute we're looking to replace is a rgb color code but we're only looking at `keyHex` - the value won't be reset. It appears as though this happens when people use custom tinting in places as `this.colors` often gets set to the rgb values throughout the file. To fix it, we just check against `keyHex` and `keyRgb`. --- src/Notifier.js | 5 +++++ src/Tinter.js | 13 ++++++++++--- src/components/structures/RightPanel.js | 10 ++++------ src/components/views/elements/TintableSvg.js | 16 ++++++++++++++-- 4 files changed, 33 insertions(+), 11 deletions(-) diff --git a/src/Notifier.js b/src/Notifier.js index 80e8be1084..8550f3bf95 100644 --- a/src/Notifier.js +++ b/src/Notifier.js @@ -289,6 +289,11 @@ const Notifier = { const room = MatrixClientPeg.get().getRoom(ev.getRoomId()); const actions = MatrixClientPeg.get().getPushActionsForEvent(ev); if (actions && actions.notify) { + dis.dispatch({ + action: "event_notification", + event: ev, + room: room, + }); if (this.isEnabled()) { this._displayPopupNotification(ev, room); } diff --git a/src/Tinter.js b/src/Tinter.js index 1b1ebbcccd..9c2afd4fab 100644 --- a/src/Tinter.js +++ b/src/Tinter.js @@ -419,11 +419,18 @@ class Tinter { for (let k = 0; k < this.svgAttrs.length; k++) { const attr = this.svgAttrs[k]; for (let m = 0; m < this.keyHex.length; m++) { // dev note: don't use L please. - if (tag.getAttribute(attr) && - tag.getAttribute(attr).toUpperCase() === this.keyHex[m]) { + // We use a different attribute from the one we're setting + // because we may also be using forceColors. If we were to + // check the keyHex against a forceColors value, it may not + // match and therefore not change when we need it to. + const valAttrName = "mx-val-" + attr; + let attribute = tag.getAttribute(valAttrName); + if (!attribute) attribute = tag.getAttribute(attr); // fall back to the original + if (attribute && (attribute.toUpperCase() === this.keyHex[m] || attribute.toLowerCase() === this.keyRgb[m])) { fixups.push({ node: tag, attr: attr, + refAttr: valAttrName, index: m, forceColors: forceColors, }); @@ -442,8 +449,8 @@ class Tinter { for (let i = 0; i < fixups.length; i++) { const svgFixup = fixups[i]; const forcedColor = svgFixup.forceColors ? svgFixup.forceColors[svgFixup.index] : null; - if (forcedColor) console.log(forcedColor); svgFixup.node.setAttribute(svgFixup.attr, forcedColor ? forcedColor : this.colors[svgFixup.index]); + svgFixup.node.setAttribute(svgFixup.refAttr, this.colors[svgFixup.index]); } if (DEBUG) console.log("applySvgFixups end"); } diff --git a/src/components/structures/RightPanel.js b/src/components/structures/RightPanel.js index c21c5f459f..0870f085a5 100644 --- a/src/components/structures/RightPanel.js +++ b/src/components/structures/RightPanel.js @@ -124,7 +124,6 @@ module.exports = React.createClass({ this.dispatcherRef = dis.register(this.onAction); const cli = this.context.matrixClient; cli.on("RoomState.members", this.onRoomStateMember); - cli.on("Room.notificationCounts", this.onRoomNotifications); this._initGroupStore(this.props.groupId); }, @@ -212,16 +211,15 @@ module.exports = React.createClass({ } }, - onRoomNotifications: function(room, type, count) { - if (type === "highlight") this.forceUpdate(); - }, - _delayedUpdate: new RateLimitedFunc(function() { this.forceUpdate(); // eslint-disable-line babel/no-invalid-this }, 500), onAction: function(payload) { - if (payload.action === "view_user") { + if (payload.action === "event_notification") { + // Try and re-caclulate any badge counts we might have + this.forceUpdate(); + } else if (payload.action === "view_user") { dis.dispatch({ action: 'show_right_panel', }); diff --git a/src/components/views/elements/TintableSvg.js b/src/components/views/elements/TintableSvg.js index af9e56377b..08628c8ca9 100644 --- a/src/components/views/elements/TintableSvg.js +++ b/src/components/views/elements/TintableSvg.js @@ -51,6 +51,12 @@ var TintableSvg = React.createClass({ delete TintableSvg.mounts[this.id]; }, + componentDidUpdate: function(prevProps, prevState) { + if (prevProps.forceColors !== this.props.forceColors) { + this.calcAndApplyFixups(this.refs.svgContainer); + } + }, + tint: function() { // TODO: only bother running this if the global tint settings have changed // since we loaded! @@ -58,8 +64,13 @@ var TintableSvg = React.createClass({ }, onLoad: function(event) { - // console.log("TintableSvg.onLoad for " + this.props.src); - this.fixups = Tinter.calcSvgFixups([event.target], this.props.forceColors); + this.calcAndApplyFixups(event.target); + }, + + calcAndApplyFixups: function(target) { + if (!target) return; + // console.log("TintableSvg.calcAndApplyFixups for " + this.props.src); + this.fixups = Tinter.calcSvgFixups([target], this.props.forceColors); Tinter.applySvgFixups(this.fixups); }, @@ -72,6 +83,7 @@ var TintableSvg = React.createClass({ height={this.props.height} onLoad={this.onLoad} tabIndex="-1" + ref="svgContainer" /> ); }, From a92d2902c4c3db3193598723cb0e2646a70b2528 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 6 Dec 2018 15:39:59 -0600 Subject: [PATCH 090/237] Add an in-room reminder to set up key recovery This adds an in-room reminder above the message timeline to set up Secure Message Recovery so that your keys will be backed up. If you try to ignore it, an additional dialog is shown to confirm. Fixes vector-im/riot-web#7783. Signed-off-by: J. Ryan Stinnett --- res/css/_components.scss | 1 + .../views/rooms/_RoomRecoveryReminder.scss | 43 ++++++++++ res/themes/dark/css/_dark.scss | 10 +++ res/themes/light/css/_base.scss | 10 +++ .../keybackup/CreateKeyBackupDialog.js | 2 +- .../keybackup/IgnoreRecoveryReminderDialog.js | 70 +++++++++++++++ src/components/structures/RoomView.js | 25 ++++++ src/components/structures/UserSettings.js | 1 + .../views/rooms/RoomRecoveryReminder.js | 85 +++++++++++++++++++ src/i18n/strings/en_EN.json | 9 +- src/settings/Settings.js | 5 ++ 11 files changed, 259 insertions(+), 2 deletions(-) create mode 100644 res/css/views/rooms/_RoomRecoveryReminder.scss create mode 100644 src/async-components/views/dialogs/keybackup/IgnoreRecoveryReminderDialog.js create mode 100644 src/components/views/rooms/RoomRecoveryReminder.js diff --git a/res/css/_components.scss b/res/css/_components.scss index 083071ef6c..579856f880 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -101,6 +101,7 @@ @import "./views/rooms/_RoomHeader.scss"; @import "./views/rooms/_RoomList.scss"; @import "./views/rooms/_RoomPreviewBar.scss"; +@import "./views/rooms/_RoomRecoveryReminder.scss"; @import "./views/rooms/_RoomSettings.scss"; @import "./views/rooms/_RoomTile.scss"; @import "./views/rooms/_RoomTooltip.scss"; diff --git a/res/css/views/rooms/_RoomRecoveryReminder.scss b/res/css/views/rooms/_RoomRecoveryReminder.scss new file mode 100644 index 0000000000..4bb42ff114 --- /dev/null +++ b/res/css/views/rooms/_RoomRecoveryReminder.scss @@ -0,0 +1,43 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_RoomRecoveryReminder { + display: flex; + flex-direction: column; + text-align: center; + background-color: $room-warning-bg-color; + padding: 20px; + border: 1px solid $primary-hairline-color; + border-bottom: unset; +} + +.mx_RoomRecoveryReminder_header { + font-weight: bold; + margin-bottom: 1em; +} + +.mx_RoomRecoveryReminder_body { + margin-bottom: 1em; +} + +.mx_RoomRecoveryReminder_button { + @mixin mx_DialogButton; + margin: 0 10px; +} + +.mx_RoomRecoveryReminder_button.mx_RoomRecoveryReminder_secondary { + @mixin mx_DialogButton_secondary; +} diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index b773d7c720..5dbc00af4e 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -100,6 +100,8 @@ $voip-accept-color: #80f480; $rte-bg-color: #353535; $rte-code-bg-color: #000; +$room-warning-bg-color: #2d2d2d; + // ******************** $roomtile-name-color: rgba(186, 186, 186, 0.8); @@ -169,6 +171,14 @@ $progressbar-color: #000; outline: none; } +@define-mixin mx_DialogButton_secondary { + // flip colours for the secondary ones + font-weight: 600; + border: 1px solid $accent-color ! important; + color: $accent-color; + background-color: $accent-fg-color; +} + // Nasty hacks to apply a filter to arbitrary monochrome artwork to make it // better match the theme. Typically applied to dark grey 'off' buttons or // light grey 'on' buttons. diff --git a/res/themes/light/css/_base.scss b/res/themes/light/css/_base.scss index 49347492ff..c275b94fb5 100644 --- a/res/themes/light/css/_base.scss +++ b/res/themes/light/css/_base.scss @@ -155,6 +155,8 @@ $imagebody-giflabel-border: rgba(0, 0, 0, 0.2); // unused? $progressbar-color: #000; +$room-warning-bg-color: #fff8e3; + // ***** Mixins! ***** @define-mixin mx_DialogButton { @@ -187,3 +189,11 @@ $progressbar-color: #000; font-size: 15px; padding: 0px 1.5em 0px 1.5em; } + +@define-mixin mx_DialogButton_secondary { + // flip colours for the secondary ones + font-weight: 600; + border: 1px solid $accent-color ! important; + color: $accent-color; + background-color: $accent-fg-color; +} diff --git a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js index 8547add256..6b115b890f 100644 --- a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js +++ b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js @@ -251,7 +251,7 @@ export default React.createClass({ />

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

    +

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

    +

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

    +
    + +
    +
    + + ); + } +} diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 934031e98d..0e0d56647d 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -607,6 +607,20 @@ module.exports = React.createClass({ } }, + async onRoomRecoveryReminderFinished(backupCreated) { + // If the user cancelled the key backup dialog, it suggests they don't + // want to be reminded anymore. + if (!backupCreated) { + await SettingsStore.setValue( + "showRoomRecoveryReminder", + null, + SettingLevel.ACCOUNT, + false, + ); + } + this.forceUpdate(); + }, + canResetTimeline: function() { if (!this.refs.messagePanel) { return true; @@ -1521,6 +1535,7 @@ module.exports = React.createClass({ const Loader = sdk.getComponent("elements.Spinner"); const TimelinePanel = sdk.getComponent("structures.TimelinePanel"); const RoomUpgradeWarningBar = sdk.getComponent("rooms.RoomUpgradeWarningBar"); + const RoomRecoveryReminder = sdk.getComponent("rooms.RoomRecoveryReminder"); if (!this.state.room) { if (this.state.roomLoading || this.state.peekLoading) { @@ -1655,6 +1670,13 @@ module.exports = React.createClass({ this.state.room.userMayUpgradeRoom(MatrixClientPeg.get().credentials.userId) ); + const showRoomRecoveryReminder = ( + SettingsStore.isFeatureEnabled("feature_keybackup") && + SettingsStore.getValue("showRoomRecoveryReminder") && + MatrixClientPeg.get().isRoomEncrypted(this.state.room.roomId) && + !MatrixClientPeg.get().getKeyBackupEnabled() + ); + let aux = null; let hideCancel = false; if (this.state.editingRoomSettings) { @@ -1669,6 +1691,9 @@ module.exports = React.createClass({ } else if (showRoomUpgradeBar) { aux = ; hideCancel = true; + } else if (showRoomRecoveryReminder) { + aux = ; + hideCancel = true; } else if (this.state.showingPinned) { hideCancel = true; // has own cancel aux = ; diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 4c15b4ec27..6f932d71e1 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -64,6 +64,7 @@ const SIMPLE_SETTINGS = [ { id: "urlPreviewsEnabled" }, { id: "autoplayGifsAndVideos" }, { id: "alwaysShowEncryptionIcons" }, + { id: "showRoomRecoveryReminder" }, { id: "hideReadReceipts" }, { id: "dontSendTypingNotifications" }, { id: "alwaysShowTimestamps" }, diff --git a/src/components/views/rooms/RoomRecoveryReminder.js b/src/components/views/rooms/RoomRecoveryReminder.js new file mode 100644 index 0000000000..265bfd3ee3 --- /dev/null +++ b/src/components/views/rooms/RoomRecoveryReminder.js @@ -0,0 +1,85 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from "react"; +import PropTypes from "prop-types"; +import sdk from "../../../index"; +import { _t } from "../../../languageHandler"; +import Modal from "../../../Modal"; + +export default class RoomRecoveryReminder extends React.PureComponent { + static propTypes = { + onFinished: PropTypes.func.isRequired, + } + + showKeyBackupDialog = () => { + Modal.createTrackedDialogAsync("Key Backup", "Key Backup", + import("../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog"), + { + onFinished: this.props.onFinished, + }, + ); + } + + onDontAskAgainClick = () => { + // When you choose "Don't ask again" from the room reminder, we show a + // dialog to confirm the choice. + Modal.createTrackedDialogAsync("Ignore Recovery Reminder", "Ignore Recovery Reminder", + import("../../../async-components/views/dialogs/keybackup/IgnoreRecoveryReminderDialog"), + { + onDontAskAgain: () => { + // Report false to the caller, who should prevent the + // reminder from appearing in the future. + this.props.onFinished(false); + }, + onSetup: () => { + this.showKeyBackupDialog(); + }, + }, + ); + } + + onSetupClick = () => { + this.showKeyBackupDialog(); + } + + render() { + const AccessibleButton = sdk.getComponent("views.elements.AccessibleButton"); + + return ( +
    +
    {_t( + "Secure Message Recovery", + )}
    +
    {_t( + "If you log out or use another device, you'll lose your " + + "secure message history. To prevent this, set up Secure " + + "Message Recovery.", + )}
    +
    + + { _t("Don't ask again") } + + + { _t("Set up") } + +
    +
    + ); + } +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 7d263f6a4c..a4ce5143d7 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -268,6 +268,7 @@ "Always show message timestamps": "Always show message timestamps", "Autoplay GIFs and videos": "Autoplay GIFs and videos", "Always show encryption icons": "Always show encryption icons", + "Show a reminder to enable Secure Message Recovery in encrypted rooms": "Show a reminder to enable Secure Message Recovery in encrypted rooms", "Enable automatic language detection for syntax highlighting": "Enable automatic language detection for syntax highlighting", "Hide avatars in user and room mentions": "Hide avatars in user and room mentions", "Disable big emoji in chat": "Disable big emoji in chat", @@ -562,6 +563,10 @@ "You are trying to access a room.": "You are trying to access a room.", "Click here to join the discussion!": "Click here to join the discussion!", "This is a preview of this room. Room interactions have been disabled": "This is a preview of this room. Room interactions have been disabled", + "Secure Message Recovery": "Secure Message Recovery", + "If you log out or use another device, you'll lose your secure message history. To prevent this, set up Secure Message Recovery.": "If you log out or use another device, you'll lose your secure message history. To prevent this, set up Secure Message Recovery.", + "Don't ask again": "Don't ask again", + "Set up": "Set up", "To change the room's avatar, you must be a": "To change the room's avatar, you must be a", "To change the room's name, you must be a": "To change the room's name, you must be a", "To change the room's main address, you must be a": "To change the room's main address, you must be a", @@ -1352,7 +1357,7 @@ "Secure your encrypted message history with a Recovery Passphrase.": "Secure your encrypted message history with a Recovery Passphrase.", "You'll need it if you log out or lose access to this device.": "You'll need it if you log out or lose access to this device.", "Enter a passphrase...": "Enter a passphrase...", - "If you don't want encrypted message history to be availble on other devices, .": "If you don't want encrypted message history to be availble on other devices, .", + "If you don't want encrypted message history to be available on other devices, .": "If you don't want encrypted message history to be available on other devices, .", "Or, if you don't want to create a Recovery Passphrase, skip this step and .": "Or, if you don't want to create a Recovery Passphrase, skip this step and .", "That matches!": "That matches!", "That doesn't match.": "That doesn't match.", @@ -1384,6 +1389,8 @@ "Create Key Backup": "Create Key Backup", "Unable to create key backup": "Unable to create key backup", "Retry": "Retry", + "Without setting up Secure Message Recovery, you'll lose your secure message history when you log out.": "Without setting up Secure Message Recovery, you'll lose your secure message history when you log out.", + "If you don't want to set this up now, you can later in Settings.": "If you don't want to set this up now, you can later in Settings.", "Failed to set direct chat tag": "Failed to set direct chat tag", "Failed to remove tag %(tagName)s from room": "Failed to remove tag %(tagName)s from room", "Failed to add tag %(tagName)s to room": "Failed to add tag %(tagName)s to room" diff --git a/src/settings/Settings.js b/src/settings/Settings.js index eb702a729c..c9a4ecdebe 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -151,6 +151,11 @@ export const SETTINGS = { displayName: _td('Always show encryption icons'), default: true, }, + "showRoomRecoveryReminder": { + supportedLevels: LEVELS_ACCOUNT_SETTINGS, + displayName: _td('Show a reminder to enable Secure Message Recovery in encrypted rooms'), + default: true, + }, "enableSyntaxHighlightLanguageDetection": { supportedLevels: LEVELS_ACCOUNT_SETTINGS, displayName: _td('Enable automatic language detection for syntax highlighting'), From d062e2c2f4dca6eb52650b34a520e6a6bf31199d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 7 Dec 2018 15:03:58 -0700 Subject: [PATCH 091/237] Check to make sure email addresses look roughly valid before inviting them to room Fixes https://github.com/vector-im/riot-web/issues/6854 --- src/components/views/dialogs/AddressPickerDialog.js | 5 +++++ src/i18n/strings/en_EN.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/views/dialogs/AddressPickerDialog.js b/src/components/views/dialogs/AddressPickerDialog.js index abc52f7b1d..cbe80763a6 100644 --- a/src/components/views/dialogs/AddressPickerDialog.js +++ b/src/components/views/dialogs/AddressPickerDialog.js @@ -23,6 +23,7 @@ import MatrixClientPeg from '../../../MatrixClientPeg'; import Promise from 'bluebird'; import { addressTypes, getAddressType } from '../../../UserAddress.js'; import GroupStore from '../../../stores/GroupStore'; +import * as Email from "../../../email"; const TRUNCATE_QUERY_LIST = 40; const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200; @@ -419,6 +420,10 @@ module.exports = React.createClass({ // a perfectly valid address if there are close matches. const addrType = getAddressType(query); if (this.props.validAddressTypes.includes(addrType)) { + if (addrType === 'email' && !Email.looksValid(query)) { + this.setState({searchError: _t("That doesn't look like a valid email address")}); + return; + } suggestedList.unshift({ addressType: addrType, address: query, diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index a4ce5143d7..ff403e9062 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -871,6 +871,7 @@ "Matrix ID": "Matrix ID", "Matrix Room ID": "Matrix Room ID", "email address": "email address", + "That doesn't look like a valid email address": "That doesn't look like a valid email address", "You have entered an invalid address.": "You have entered an invalid address.", "Try using one of the following valid address types: %(validTypesList)s.": "Try using one of the following valid address types: %(validTypesList)s.", "Preparing to send logs": "Preparing to send logs", @@ -912,7 +913,6 @@ "You've previously used a newer version of Riot on %(host)s. To use this version again with end to end encryption, you will need to sign out and back in again. ": "You've previously used a newer version of Riot on %(host)s. To use this version again with end to end encryption, you will need to sign out and back in again. ", "Incompatible Database": "Incompatible Database", "Continue With Encryption Disabled": "Continue With Encryption Disabled", - "Failed to indicate account erasure": "Failed to indicate account erasure", "Unknown error": "Unknown error", "Incorrect password": "Incorrect password", "Deactivate Account": "Deactivate Account", From 6707186edcec64e314dfeebbe3900100686d484f Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 7 Dec 2018 15:36:49 -0700 Subject: [PATCH 092/237] Change how the default server name and HS URL interact They are now independent of each other. If both are specified in the config, the user will see an error and be prevented from logging in. The expected behaviour is that when a default server name is given, we do a .well-known lookup to find the default homeserver (and block the UI while we do this to prevent it from using matrix.org while we go out and find more information). If the config specifies just a default homeserver URL however, we don't do anything special. --- res/css/structures/login/_Login.scss | 7 ------- src/components/structures/MatrixChat.js | 22 ++++++++++++++++++--- src/components/structures/login/Login.js | 5 +---- src/components/views/login/PasswordLogin.js | 14 ++----------- src/i18n/strings/en_EN.json | 3 ++- 5 files changed, 24 insertions(+), 27 deletions(-) diff --git a/res/css/structures/login/_Login.scss b/res/css/structures/login/_Login.scss index 9b19c24b14..1264d2a30f 100644 --- a/res/css/structures/login/_Login.scss +++ b/res/css/structures/login/_Login.scss @@ -180,13 +180,6 @@ limitations under the License. margin-bottom: 12px; } -.mx_Login_subtext { - display: block; - font-size: 0.8em; - text-align: center; - margin: 10px; -} - .mx_Login_type_container { display: flex; margin-bottom: 14px; diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index dc3872664b..e93234c679 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -298,7 +298,16 @@ export default React.createClass({ // Set up the default URLs (async) if (this.getDefaultServerName() && !this.getDefaultHsUrl(false)) { + this.setState({loadingDefaultHomeserver: true}); this._tryDiscoverDefaultHomeserver(this.getDefaultServerName()); + } else if (this.getDefaultServerName() && this.getDefaultHsUrl(false)) { + // Ideally we would somehow only communicate this to the server admins, but + // given this is at login time we can't really do much besides hope that people + // will check their settings. + this.setState({ + defaultServerName: null, // To un-hide any secrets people might be keeping + defaultServerDiscoveryError: _t("Invalid configuration: Cannot supply a default homeserver URL and a default server name"), + }); } // Set a default HS with query param `hs_url` @@ -1756,13 +1765,20 @@ export default React.createClass({ const state = discovery["m.homeserver"].state; if (state !== AutoDiscovery.SUCCESS) { console.error("Failed to discover homeserver on startup:", discovery); - this.setState({defaultServerDiscoveryError: discovery["m.homeserver"].error}); + this.setState({ + defaultServerDiscoveryError: discovery["m.homeserver"].error, + loadingDefaultHomeserver: false, + }); } else { const hsUrl = discovery["m.homeserver"].base_url; const isUrl = discovery["m.identity_server"].state === AutoDiscovery.SUCCESS ? discovery["m.identity_server"].base_url : "https://vector.im"; - this.setState({defaultHsUrl: hsUrl, defaultIsUrl: isUrl}); + this.setState({ + defaultHsUrl: hsUrl, + defaultIsUrl: isUrl, + loadingDefaultHomeserver: false, + }); } }, @@ -1780,7 +1796,7 @@ export default React.createClass({ render: function() { // console.log(`Rendering MatrixChat with view ${this.state.view}`); - if (this.state.view === VIEWS.LOADING || this.state.view === VIEWS.LOGGING_IN) { + if (this.state.view === VIEWS.LOADING || this.state.view === VIEWS.LOGGING_IN || this.state.loadingDefaultHomeserver) { const Spinner = sdk.getComponent('elements.Spinner'); return (
    diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index 08e94e413a..6dcbfe7e47 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -470,9 +470,6 @@ module.exports = React.createClass({ _renderPasswordStep: function() { const PasswordLogin = sdk.getComponent('login.PasswordLogin'); - const hsName = this.state.enteredHomeserverUrl === this.props.defaultHsUrl - ? this.props.defaultServerName - : null; return ( ); }, diff --git a/src/components/views/login/PasswordLogin.js b/src/components/views/login/PasswordLogin.js index 582ccf94dd..04aaae3630 100644 --- a/src/components/views/login/PasswordLogin.js +++ b/src/components/views/login/PasswordLogin.js @@ -252,21 +252,12 @@ class PasswordLogin extends React.Component { } let matrixIdText = _t('Matrix ID'); - let matrixIdSubtext = null; if (this.props.hsName) { matrixIdText = _t('%(serverName)s Matrix ID', {serverName: this.props.hsName}); - } - if (this.props.hsUrl) { + } else { try { const parsedHsUrl = new URL(this.props.hsUrl); - if (!this.props.hsName) { - matrixIdText = _t('%(serverName)s Matrix ID', {serverName: parsedHsUrl.hostname}); - } else if (parsedHsUrl.hostname !== this.props.hsName) { - matrixIdSubtext = _t('%(serverName)s is located at %(homeserverUrl)s', { - serverName: this.props.hsName, - homeserverUrl: this.props.hsUrl, - }); - } + matrixIdText = _t('%(serverName)s Matrix ID', {serverName: parsedHsUrl.hostname}); } catch (e) { // ignore } @@ -304,7 +295,6 @@ class PasswordLogin extends React.Component {
    { loginType } - { matrixIdSubtext } { loginField } {this._passwordField = e;}} type="password" name="password" diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 87bc05c81c..8c5f3f5351 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -721,8 +721,8 @@ "User name": "User name", "Mobile phone number": "Mobile phone number", "Forgot your password?": "Forgot your password?", + "Matrix ID": "Matrix ID", "%(serverName)s Matrix ID": "%(serverName)s Matrix ID", - "%(serverName)s is located at %(homeserverUrl)s": "%(serverName)s is located at %(homeserverUrl)s", "Sign in with": "Sign in with", "Email address": "Email address", "Sign in": "Sign in", @@ -1114,6 +1114,7 @@ "You are currently using Riot anonymously as a guest.": "You are currently using Riot anonymously as a guest.", "If you would like to create a Matrix account you can register now.": "If you would like to create a Matrix account you can register now.", "Login": "Login", + "Invalid configuration: Cannot supply a default homeserver URL and a default server name": "Invalid configuration: Cannot supply a default homeserver URL and a default server name", "Failed to reject invitation": "Failed to reject invitation", "This room is not public. You will not be able to rejoin without an invite.": "This room is not public. You will not be able to rejoin without an invite.", "Are you sure you want to leave the room '%(roomName)s'?": "Are you sure you want to leave the room '%(roomName)s'?", From a969237dc07a203baca4ef3903dc83d0f3479012 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 7 Dec 2018 15:37:20 -0700 Subject: [PATCH 093/237] Disable the submit button while .well-known is underway To give the user a little feedback about something happening. This definitely needs to be improved in the future though. --- src/components/structures/login/Login.js | 9 ++++++++- src/components/views/login/PasswordLogin.js | 6 +++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index 6dcbfe7e47..bfaab8fdb8 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -94,6 +94,7 @@ module.exports = React.createClass({ discoveredHsUrl: "", discoveredIsUrl: "", discoveryError: "", + findingHomeserver: false, }; }, @@ -299,10 +300,11 @@ module.exports = React.createClass({ _tryWellKnownDiscovery: async function(serverName) { if (!serverName.trim()) { // Nothing to discover - this.setState({discoveryError: "", discoveredHsUrl: "", discoveredIsUrl: ""}); + this.setState({discoveryError: "", discoveredHsUrl: "", discoveredIsUrl: "", findingHomeserver: false}); return; } + this.setState({findingHomeserver: true}); try { const discovery = await AutoDiscovery.findClientConfig(serverName); const state = discovery["m.homeserver"].state; @@ -311,12 +313,14 @@ module.exports = React.createClass({ discoveredHsUrl: "", discoveredIsUrl: "", discoveryError: discovery["m.homeserver"].error, + findingHomeserver: false, }); } else if (state === AutoDiscovery.PROMPT) { this.setState({ discoveredHsUrl: "", discoveredIsUrl: "", discoveryError: "", + findingHomeserver: false, }); } else if (state === AutoDiscovery.SUCCESS) { this.setState({ @@ -326,6 +330,7 @@ module.exports = React.createClass({ ? discovery["m.identity_server"].base_url : "", discoveryError: "", + findingHomeserver: false, }); } else { console.warn("Unknown state for m.homeserver in discovery response: ", discovery); @@ -333,6 +338,7 @@ module.exports = React.createClass({ discoveredHsUrl: "", discoveredIsUrl: "", discoveryError: _t("Unknown failure discovering homeserver"), + findingHomeserver: false, }); } } catch (e) { @@ -485,6 +491,7 @@ module.exports = React.createClass({ loginIncorrect={this.state.loginIncorrect} hsUrl={this.state.enteredHomeserverUrl} hsName={this.props.defaultServerName} + disableSubmit={this.state.findingHomeserver} /> ); }, diff --git a/src/components/views/login/PasswordLogin.js b/src/components/views/login/PasswordLogin.js index 04aaae3630..59d4db379c 100644 --- a/src/components/views/login/PasswordLogin.js +++ b/src/components/views/login/PasswordLogin.js @@ -41,6 +41,7 @@ class PasswordLogin extends React.Component { loginIncorrect: false, hsDomain: "", hsName: null, + disableSubmit: false, } constructor(props) { @@ -291,6 +292,8 @@ class PasswordLogin extends React.Component { ); } + const disableSubmit = this.props.disableSubmit || matrixIdText === ''; + return (
    @@ -304,7 +307,7 @@ class PasswordLogin extends React.Component { />
    { forgotPasswordJsx } - +
    ); @@ -329,6 +332,7 @@ PasswordLogin.propTypes = { onPasswordChanged: PropTypes.func, loginIncorrect: PropTypes.bool, hsName: PropTypes.string, + disableSubmit: PropTypes.bool, }; module.exports = PasswordLogin; From f2468f562d381310585fd7edbca24946c20b5c94 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 7 Dec 2018 20:15:21 -0700 Subject: [PATCH 094/237] Speed up room unread checks by not hitting the SettingsStore so often This was noticed as a problem after `Unread.doesRoomHaveUnreadMessages` started being called a lot more frequently. Down the call stack, `shouldHideEvent` is called which used to call into the `SettingsStore` frequently, causing performance issues in many cases. The `SettingsStore` tries to be as fast as possible, however there's still code paths that make it less than desirable to use as the first condition in an AND condition. By not hitting the `SettingsStore` so often, we can shorten those code paths. As for how much this improves things, I ran some profiling before and after this change. This was done on my massive 1200+ room account. Before it was possible to see nearly 2 seconds spent generating room lists where 20-130ms per room was spent figuring out if the room has unread messages. Afterwards, the room list was generating within ~330ms and each unread check taking 0-2ms. There's still room for improvement on generating the room list, however the significant gains here seem worth it. --- src/shouldHideEvent.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/shouldHideEvent.js b/src/shouldHideEvent.js index 3aad05a976..609f48b128 100644 --- a/src/shouldHideEvent.js +++ b/src/shouldHideEvent.js @@ -42,14 +42,14 @@ export default function shouldHideEvent(ev) { const isEnabled = (name) => SettingsStore.getValue(name, ev.getRoomId()); // Hide redacted events - if (isEnabled('hideRedactions') && ev.isRedacted()) return true; + if (ev.isRedacted() && isEnabled('hideRedactions')) return true; const eventDiff = memberEventDiff(ev); if (eventDiff.isMemberEvent) { - if (isEnabled('hideJoinLeaves') && (eventDiff.isJoin || eventDiff.isPart)) return true; - if (isEnabled('hideAvatarChanges') && eventDiff.isAvatarChange) return true; - if (isEnabled('hideDisplaynameChanges') && eventDiff.isDisplaynameChange) return true; + if ((eventDiff.isJoin || eventDiff.isPart) && isEnabled('hideJoinLeaves')) return true; + if (eventDiff.isAvatarChange && isEnabled('hideAvatarChanges')) return true; + if (eventDiff.isDisplaynameChange && isEnabled('hideDisplaynameChanges')) return true; } return false; From ebdba32393d7b1b3f7f73aab793e39dc2fedb87d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sat, 8 Dec 2018 12:06:37 -0700 Subject: [PATCH 095/237] Add a comment about the SettingsStore being slow --- src/shouldHideEvent.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/shouldHideEvent.js b/src/shouldHideEvent.js index 609f48b128..adc89a126a 100644 --- a/src/shouldHideEvent.js +++ b/src/shouldHideEvent.js @@ -38,7 +38,9 @@ function memberEventDiff(ev) { } export default function shouldHideEvent(ev) { - // Wrap getValue() for readability + // Wrap getValue() for readability. Calling the SettingsStore can be + // fairly resource heavy, so the checks below should avoid hitting it + // where possible. const isEnabled = (name) => SettingsStore.getValue(name, ev.getRoomId()); // Hide redacted events From 5444a61e6fa7ad080ea4a11d40e96437bfb1e911 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 10 Dec 2018 13:39:35 +0000 Subject: [PATCH 096/237] Released js-sdk --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 58aefff176..8b835ec70f 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,7 @@ "linkifyjs": "^2.1.6", "lodash": "^4.13.1", "lolex": "2.3.2", - "matrix-js-sdk": "0.14.2-rc.1", + "matrix-js-sdk": "0.14.2", "optimist": "^0.6.1", "pako": "^1.0.5", "prop-types": "^15.5.8", From 9d456b2d0d6c7e5638a562e08619d73e33a888fc Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 10 Dec 2018 13:43:58 +0000 Subject: [PATCH 097/237] Prepare changelog for v0.14.7 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1261ad8d40..742b8b4529 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +Changes in [0.14.7](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.14.7) (2018-12-10) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.14.7-rc.2...v0.14.7) + + * No changes since rc.2 + Changes in [0.14.7-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.14.7-rc.2) (2018-12-06) =============================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.14.7-rc.1...v0.14.7-rc.2) From 37c984e1955185fc1d915b8620ec506dad177959 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 10 Dec 2018 13:43:59 +0000 Subject: [PATCH 098/237] v0.14.7 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8b835ec70f..9cca57075c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "0.14.7-rc.2", + "version": "0.14.7", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { From 36dd43f73402a37d4b99368cb370d95343c81eb7 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 10 Dec 2018 14:44:12 +0000 Subject: [PATCH 099/237] Avoid preserving HS url at logout When I was talking to Matthew about this the other day, we couldn't think of a good reason why we should preserve the HS URL at logout. It introduces the problem that, if a client is redirected after login as per MSC1730, and then you log out, you'll then get a login screen for the wrong server. So basically there's no reason to have an mx_hs_url/mx_is_url without an access token, and we can remove the stuff which preserves it, and the stuff that attempts to restore it. --- src/Lifecycle.js | 9 --------- src/components/structures/MatrixChat.js | 4 ---- 2 files changed, 13 deletions(-) diff --git a/src/Lifecycle.js b/src/Lifecycle.js index f781f36fe4..ed057eb020 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -497,16 +497,7 @@ function _clearStorage() { Analytics.logout(); if (window.localStorage) { - const hsUrl = window.localStorage.getItem("mx_hs_url"); - const isUrl = window.localStorage.getItem("mx_is_url"); window.localStorage.clear(); - - // preserve our HS & IS URLs for convenience - // N.B. we cache them in hsUrl/isUrl and can't really inline them - // as getCurrentHsUrl() may call through to localStorage. - // NB. We do clear the device ID (as well as all the settings) - if (hsUrl) window.localStorage.setItem("mx_hs_url", hsUrl); - if (isUrl) window.localStorage.setItem("mx_is_url", isUrl); } // create a temporary client to clear out the persistent stores. diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 4d7c71e3ef..7feca4de45 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -204,8 +204,6 @@ export default React.createClass({ return this.state.register_hs_url; } else if (MatrixClientPeg.get()) { return MatrixClientPeg.get().getHomeserverUrl(); - } else if (window.localStorage && window.localStorage.getItem("mx_hs_url")) { - return window.localStorage.getItem("mx_hs_url"); } else { return this.getDefaultHsUrl(); } @@ -224,8 +222,6 @@ export default React.createClass({ return this.state.register_is_url; } else if (MatrixClientPeg.get()) { return MatrixClientPeg.get().getIdentityServerUrl(); - } else if (window.localStorage && window.localStorage.getItem("mx_is_url")) { - return window.localStorage.getItem("mx_is_url"); } else { return this.getDefaultIsUrl(); } From ab6566980f668b96fd510a3d00ba5cc7b2ec94a7 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 11 Dec 2018 19:15:08 +0000 Subject: [PATCH 100/237] Remove unused string left behind after #2259 Signed-off-by: J. Ryan Stinnett --- src/i18n/strings/en_EN.json | 1 - 1 file changed, 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index a4ce5143d7..0df81b8e2a 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -912,7 +912,6 @@ "You've previously used a newer version of Riot on %(host)s. To use this version again with end to end encryption, you will need to sign out and back in again. ": "You've previously used a newer version of Riot on %(host)s. To use this version again with end to end encryption, you will need to sign out and back in again. ", "Incompatible Database": "Incompatible Database", "Continue With Encryption Disabled": "Continue With Encryption Disabled", - "Failed to indicate account erasure": "Failed to indicate account erasure", "Unknown error": "Unknown error", "Incorrect password": "Incorrect password", "Deactivate Account": "Deactivate Account", From 04c30181c63b5f17a775a580d2b7e5f37e256959 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 11 Dec 2018 19:01:27 +0000 Subject: [PATCH 101/237] Expose hidden notification rules in UI Adds UI control for 3 hidden notification rules: * Messages containing @room * Encrypted one-to-one messages * Encrypted group messages This should help to clarify some mysterious notification behavior, as it wasn't obvious that these rules existed. Fixes vector-im/riot-web#7833. Signed-off-by: J. Ryan Stinnett --- .../views/settings/Notifications.js | 6 +++ src/i18n/strings/en_EN.json | 3 ++ src/notifications/StandardActions.js | 1 + .../VectorPushRulesDefinitions.js | 54 ++++++++++++++++--- 4 files changed, 57 insertions(+), 7 deletions(-) diff --git a/src/components/views/settings/Notifications.js b/src/components/views/settings/Notifications.js index 72ad2943aa..40c43e6b2e 100644 --- a/src/components/views/settings/Notifications.js +++ b/src/components/views/settings/Notifications.js @@ -483,8 +483,11 @@ module.exports = React.createClass({ // The default push rules displayed by Vector UI '.m.rule.contains_display_name': 'vector', '.m.rule.contains_user_name': 'vector', + '.m.rule.roomnotif': 'vector', '.m.rule.room_one_to_one': 'vector', + '.m.rule.encrypted_room_one_to_one': 'vector', '.m.rule.message': 'vector', + '.m.rule.encrypted': 'vector', '.m.rule.invite_for_me': 'vector', //'.m.rule.member_event': 'vector', '.m.rule.call': 'vector', @@ -534,9 +537,12 @@ module.exports = React.createClass({ const vectorRuleIds = [ '.m.rule.contains_display_name', '.m.rule.contains_user_name', + '.m.rule.roomnotif', '_keywords', '.m.rule.room_one_to_one', + '.m.rule.encrypted_room_one_to_one', '.m.rule.message', + '.m.rule.encrypted', '.m.rule.invite_for_me', //'im.vector.rule.member_event', '.m.rule.call', diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 0df81b8e2a..7165539347 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -295,8 +295,11 @@ "Waiting for response from server": "Waiting for response from server", "Messages containing my display name": "Messages containing my display name", "Messages containing my user name": "Messages containing my user name", + "Messages containing @room": "Messages containing @room", "Messages in one-to-one chats": "Messages in one-to-one chats", + "Encrypted messages in one-to-one chats": "Encrypted messages in one-to-one chats", "Messages in group chats": "Messages in group chats", + "Encrypted messages in group chats": "Encrypted messages in group chats", "When I'm invited to a room": "When I'm invited to a room", "Call invitation": "Call invitation", "Messages sent by bot": "Messages sent by bot", diff --git a/src/notifications/StandardActions.js b/src/notifications/StandardActions.js index 30d6ea5975..15f645d5f7 100644 --- a/src/notifications/StandardActions.js +++ b/src/notifications/StandardActions.js @@ -24,6 +24,7 @@ module.exports = { ACTION_NOTIFY: encodeActions({notify: true}), ACTION_NOTIFY_DEFAULT_SOUND: encodeActions({notify: true, sound: "default"}), ACTION_NOTIFY_RING_SOUND: encodeActions({notify: true, sound: "ring"}), + ACTION_HIGHLIGHT: encodeActions({notify: true, highlight: true}), ACTION_HIGHLIGHT_DEFAULT_SOUND: encodeActions({notify: true, sound: "default", highlight: true}), ACTION_DONT_NOTIFY: encodeActions({notify: false}), ACTION_DISABLED: null, diff --git a/src/notifications/VectorPushRulesDefinitions.js b/src/notifications/VectorPushRulesDefinitions.js index eeb193cb8a..d763da7e64 100644 --- a/src/notifications/VectorPushRulesDefinitions.js +++ b/src/notifications/VectorPushRulesDefinitions.js @@ -20,6 +20,7 @@ import { _td } from '../languageHandler'; const StandardActions = require('./StandardActions'); const PushRuleVectorState = require('./PushRuleVectorState'); +const { decodeActions } = require('./NotificationUtils'); class VectorPushRuleDefinition { constructor(opts) { @@ -31,13 +32,11 @@ class VectorPushRuleDefinition { // Translate the rule actions and its enabled value into vector state ruleToVectorState(rule) { let enabled = false; - let actions = null; if (rule) { enabled = rule.enabled; - actions = rule.actions; } - for (const stateKey in PushRuleVectorState.states) { + for (const stateKey in PushRuleVectorState.states) { // eslint-disable-line guard-for-in const state = PushRuleVectorState.states[stateKey]; const vectorStateToActions = this.vectorStateToActions[state]; @@ -47,15 +46,21 @@ class VectorPushRuleDefinition { return state; } } else { - // The actions must match to the ones expected by vector state - if (enabled && JSON.stringify(rule.actions) === JSON.stringify(vectorStateToActions)) { + // The actions must match to the ones expected by vector state. + // Use `decodeActions` on both sides to canonicalize things like + // value: true vs. unspecified for highlight (which defaults to + // true, making them equivalent. + if (enabled && + JSON.stringify(decodeActions(rule.actions)) === + JSON.stringify(decodeActions(vectorStateToActions))) { return state; } } } - console.error("Cannot translate rule actions into Vector rule state. Rule: " + - JSON.stringify(rule)); + console.error(`Cannot translate rule actions into Vector rule state. ` + + `Rule: ${JSON.stringify(rule)}, ` + + `Expected: ${JSON.stringify(this.vectorStateToActions)}`); return undefined; } } @@ -86,6 +91,17 @@ module.exports = { }, }), + // Messages containing @room + ".m.rule.roomnotif": new VectorPushRuleDefinition({ + kind: "override", + description: _td("Messages containing @room"), // passed through _t() translation in src/components/views/settings/Notifications.js + vectorStateToActions: { // The actions for each vector state, or null to disable the rule. + on: StandardActions.ACTION_NOTIFY, + loud: StandardActions.ACTION_HIGHLIGHT, + off: StandardActions.ACTION_DISABLED, + }, + }), + // Messages just sent to the user in a 1:1 room ".m.rule.room_one_to_one": new VectorPushRuleDefinition({ kind: "underride", @@ -97,6 +113,17 @@ module.exports = { }, }), + // Encrypted messages just sent to the user in a 1:1 room + ".m.rule.encrypted_room_one_to_one": new VectorPushRuleDefinition({ + kind: "underride", + description: _td("Encrypted messages in one-to-one chats"), // passed through _t() translation in src/components/views/settings/Notifications.js + vectorStateToActions: { + on: StandardActions.ACTION_NOTIFY, + loud: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND, + off: StandardActions.ACTION_DONT_NOTIFY, + }, + }), + // Messages just sent to a group chat room // 1:1 room messages are catched by the .m.rule.room_one_to_one rule if any defined // By opposition, all other room messages are from group chat rooms. @@ -110,6 +137,19 @@ module.exports = { }, }), + // Encrypted messages just sent to a group chat room + // Encrypted 1:1 room messages are catched by the .m.rule.encrypted_room_one_to_one rule if any defined + // By opposition, all other room messages are from group chat rooms. + ".m.rule.encrypted": new VectorPushRuleDefinition({ + kind: "underride", + description: _td("Encrypted messages in group chats"), // passed through _t() translation in src/components/views/settings/Notifications.js + vectorStateToActions: { + on: StandardActions.ACTION_NOTIFY, + loud: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND, + off: StandardActions.ACTION_DONT_NOTIFY, + }, + }), + // Invitation for the user ".m.rule.invite_for_me": new VectorPushRuleDefinition({ kind: "underride", From 2ca747740616c7c0f8e1fc0b071fb5526b1b31de Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 12 Dec 2018 01:46:48 +0000 Subject: [PATCH 102/237] Fix typo in push rules comment Signed-off-by: J. Ryan Stinnett --- src/notifications/VectorPushRulesDefinitions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/notifications/VectorPushRulesDefinitions.js b/src/notifications/VectorPushRulesDefinitions.js index d763da7e64..3df2e70774 100644 --- a/src/notifications/VectorPushRulesDefinitions.js +++ b/src/notifications/VectorPushRulesDefinitions.js @@ -49,7 +49,7 @@ class VectorPushRuleDefinition { // The actions must match to the ones expected by vector state. // Use `decodeActions` on both sides to canonicalize things like // value: true vs. unspecified for highlight (which defaults to - // true, making them equivalent. + // true, making them equivalent). if (enabled && JSON.stringify(decodeActions(rule.actions)) === JSON.stringify(decodeActions(vectorStateToActions))) { From ce7969e3d5a2ed4fb6b98539c1bdeb58e915c655 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 11 Dec 2018 21:40:11 -0700 Subject: [PATCH 103/237] Display custom status messages in the UI Part of https://github.com/vector-im/riot-web/issues/1528 --- res/css/views/rooms/_RoomTile.scss | 19 ++++++++++++++++++- src/components/views/rooms/EntityTile.js | 21 +++++++++++++++++---- src/components/views/rooms/MemberTile.js | 5 ++++- src/components/views/rooms/RoomTile.js | 16 ++++++++++++++++ 4 files changed, 55 insertions(+), 6 deletions(-) diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss index ccd3afe26c..2014bb6404 100644 --- a/res/css/views/rooms/_RoomTile.scss +++ b/res/css/views/rooms/_RoomTile.scss @@ -35,7 +35,20 @@ limitations under the License. .mx_RoomTile_nameContainer { display: inline-block; width: 180px; - height: 24px; + //height: 24px; + vertical-align: middle; +} + +.mx_RoomTile_subtext { + display: inline-block; + font-size: 0.8em; + padding: 0 0 0 7px; + margin: 0; + overflow: hidden; + white-space: nowrap; + text-overflow: clip; + position: relative; + bottom: 4px; } .mx_RoomTile_avatar_container { @@ -76,6 +89,10 @@ limitations under the License. text-overflow: ellipsis; } +.mx_RoomTile_hasSubtext .mx_RoomTile_avatar { + padding-top: 0; +} + .mx_RoomTile_invite { /* color: rgba(69, 69, 69, 0.5); */ } diff --git a/src/components/views/rooms/EntityTile.js b/src/components/views/rooms/EntityTile.js index 6b3264d123..27215430a1 100644 --- a/src/components/views/rooms/EntityTile.js +++ b/src/components/views/rooms/EntityTile.js @@ -70,6 +70,7 @@ const EntityTile = React.createClass({ onClick: PropTypes.func, suppressOnHover: PropTypes.bool, showPresence: PropTypes.bool, + subtextLabel: PropTypes.string, }, getDefaultProps: function() { @@ -125,19 +126,31 @@ const EntityTile = React.createClass({ let nameClasses = 'mx_EntityTile_name'; if (this.props.showPresence) { presenceLabel = ; + currentlyActive={this.props.presenceCurrentlyActive} + presenceState={this.props.presenceState}/>; nameClasses += ' mx_EntityTile_name_hover'; } + if (this.props.subtextLabel) { + presenceLabel = {this.props.subtextLabel}; + } nameEl = (
    - + - { name } + {name} {presenceLabel}
    ); + } else if (this.props.subtextLabel) { + nameEl = ( +
    + + {name} + + {this.props.subtextLabel} +
    + ); } else { nameEl = ( { name } diff --git a/src/components/views/rooms/MemberTile.js b/src/components/views/rooms/MemberTile.js index 2359bc242c..d246b37234 100644 --- a/src/components/views/rooms/MemberTile.js +++ b/src/components/views/rooms/MemberTile.js @@ -84,6 +84,7 @@ module.exports = React.createClass({ const name = this._getDisplayName(); const active = -1; const presenceState = member.user ? member.user.presence : null; + const statusMessage = member.user ? member.user.statusMessage : null; const av = ( @@ -106,7 +107,9 @@ module.exports = React.createClass({ presenceLastTs={member.user ? member.user.lastPresenceTs : 0} presenceCurrentlyActive={member.user ? member.user.currentlyActive : false} avatarJsx={av} title={this.getPowerLabel()} onClick={this.onClick} - name={name} powerStatus={powerStatus} showPresence={this.props.showPresence} /> + name={name} powerStatus={powerStatus} showPresence={this.props.showPresence} + subtextLabel={statusMessage} + /> ); }, }); diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 54044e8d65..2c48862ee9 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -251,6 +251,17 @@ module.exports = React.createClass({ const mentionBadges = this.props.highlight && this._shouldShowMentionBadge(); const badges = notifBadges || mentionBadges; + const isJoined = this.props.room.getMyMembership() === "join"; + const looksLikeDm = this.props.room.currentState.getMembers().length === 2; + let subtext = null; + if (!isInvite && isJoined && looksLikeDm) { + const selfId = MatrixClientPeg.get().getUserId(); + const otherMember = this.props.room.currentState.getMembersExcept([selfId])[0]; + if (otherMember.user && otherMember.user.statusMessage) { + subtext = otherMember.user.statusMessage; + } + } + const classes = classNames({ 'mx_RoomTile': true, 'mx_RoomTile_selected': this.state.selected, @@ -261,6 +272,7 @@ module.exports = React.createClass({ 'mx_RoomTile_menuDisplayed': this.state.menuDisplayed, 'mx_RoomTile_noBadges': !badges, 'mx_RoomTile_transparent': this.props.transparent, + 'mx_RoomTile_hasSubtext': !!subtext && !this.props.isCollapsed, }); const avatarClasses = classNames({ @@ -291,6 +303,7 @@ module.exports = React.createClass({ const EmojiText = sdk.getComponent('elements.EmojiText'); let label; + let subtextLabel; let tooltip; if (!this.props.collapsed) { const nameClasses = classNames({ @@ -299,6 +312,8 @@ module.exports = React.createClass({ 'mx_RoomTile_badgeShown': badges || this.state.badgeHover || this.state.menuDisplayed, }); + subtextLabel = subtext ? { subtext } : null; + if (this.state.selected) { const nameSelected = { name }; @@ -339,6 +354,7 @@ module.exports = React.createClass({
    { label } + { subtextLabel } { badge }
    { /* { incomingCallBox } */ } From cd9ea2b2d79ad01cbd719be69c9e1c185f23d503 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 12 Dec 2018 12:57:48 -0700 Subject: [PATCH 104/237] Fix alignment of avatars and status messages also introduce the status message to the MemberInfo pane Part of https://github.com/vector-im/riot-web/issues/1528 --- res/css/views/rooms/_EntityTile.scss | 8 ++++++++ res/css/views/rooms/_MemberInfo.scss | 7 +++++++ res/css/views/rooms/_RoomTile.scss | 12 ++++++------ src/components/views/rooms/EntityTile.js | 4 ++-- src/components/views/rooms/MemberInfo.js | 8 ++++++++ src/components/views/rooms/RoomTile.js | 2 +- 6 files changed, 32 insertions(+), 9 deletions(-) diff --git a/res/css/views/rooms/_EntityTile.scss b/res/css/views/rooms/_EntityTile.scss index 031894afde..90d5dc9aa5 100644 --- a/res/css/views/rooms/_EntityTile.scss +++ b/res/css/views/rooms/_EntityTile.scss @@ -111,4 +111,12 @@ limitations under the License. opacity: 0.25; } +.mx_EntityTile_subtext { + font-size: 11px; + opacity: 0.5; + overflow: hidden; + white-space: nowrap; + text-overflow: clip; +} + diff --git a/res/css/views/rooms/_MemberInfo.scss b/res/css/views/rooms/_MemberInfo.scss index 5d47275efe..2270e83743 100644 --- a/res/css/views/rooms/_MemberInfo.scss +++ b/res/css/views/rooms/_MemberInfo.scss @@ -110,3 +110,10 @@ limitations under the License. margin-left: 8px; } +.mx_MemberInfo_statusMessage { + font-size: 11px; + opacity: 0.5; + overflow: hidden; + white-space: nowrap; + text-overflow: clip; +} diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss index 2014bb6404..6a89636d15 100644 --- a/res/css/views/rooms/_RoomTile.scss +++ b/res/css/views/rooms/_RoomTile.scss @@ -41,7 +41,7 @@ limitations under the License. .mx_RoomTile_subtext { display: inline-block; - font-size: 0.8em; + font-size: 11px; padding: 0 0 0 7px; margin: 0; overflow: hidden; @@ -62,10 +62,14 @@ limitations under the License. padding-left: 16px; padding-right: 6px; width: 24px; - height: 24px; vertical-align: middle; } +.mx_RoomTile_hasSubtext .mx_RoomTile_avatar { + padding-top: 0; + vertical-align: super; +} + .mx_RoomTile_dm { display: block; position: absolute; @@ -89,10 +93,6 @@ limitations under the License. text-overflow: ellipsis; } -.mx_RoomTile_hasSubtext .mx_RoomTile_avatar { - padding-top: 0; -} - .mx_RoomTile_invite { /* color: rgba(69, 69, 69, 0.5); */ } diff --git a/src/components/views/rooms/EntityTile.js b/src/components/views/rooms/EntityTile.js index 27215430a1..a5b75b89bf 100644 --- a/src/components/views/rooms/EntityTile.js +++ b/src/components/views/rooms/EntityTile.js @@ -131,7 +131,7 @@ const EntityTile = React.createClass({ nameClasses += ' mx_EntityTile_name_hover'; } if (this.props.subtextLabel) { - presenceLabel = {this.props.subtextLabel}; + presenceLabel = {this.props.subtextLabel}; } nameEl = (
    @@ -148,7 +148,7 @@ const EntityTile = React.createClass({ {name} - {this.props.subtextLabel} + {this.props.subtextLabel}
    ); } else { diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 17b1311c4f..4eea33e952 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -889,11 +889,13 @@ module.exports = withMatrixClient(React.createClass({ let presenceState; let presenceLastActiveAgo; let presenceCurrentlyActive; + let statusMessage; if (this.props.member.user) { presenceState = this.props.member.user.presence; presenceLastActiveAgo = this.props.member.user.lastActiveAgo; presenceCurrentlyActive = this.props.member.user.currentlyActive; + statusMessage = this.props.member.user.statusMessage; } const room = this.props.matrixClient.getRoom(this.props.member.roomId); @@ -915,6 +917,11 @@ module.exports = withMatrixClient(React.createClass({ presenceState={presenceState} />; } + let statusLabel = null; + if (statusMessage) { + statusLabel = { statusMessage }; + } + let roomMemberDetails = null; if (this.props.member.roomId) { // is in room const PowerSelector = sdk.getComponent('elements.PowerSelector'); @@ -931,6 +938,7 @@ module.exports = withMatrixClient(React.createClass({
    {presenceLabel} + {statusLabel}
    ; } diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 2c48862ee9..2f95aab97a 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -272,7 +272,7 @@ module.exports = React.createClass({ 'mx_RoomTile_menuDisplayed': this.state.menuDisplayed, 'mx_RoomTile_noBadges': !badges, 'mx_RoomTile_transparent': this.props.transparent, - 'mx_RoomTile_hasSubtext': !!subtext && !this.props.isCollapsed, + 'mx_RoomTile_hasSubtext': subtext && !this.props.collapsed, }); const avatarClasses = classNames({ From dd382ecb05d85cb37f33f3d93ef84f64e69f02f1 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 12 Dec 2018 13:20:10 -0700 Subject: [PATCH 105/237] Fix a bug with determining 1:1 rooms We shouldn't consider rooms where people have left or been banned --- src/components/views/rooms/RoomTile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 2f95aab97a..fa18a0687b 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -252,7 +252,7 @@ module.exports = React.createClass({ const badges = notifBadges || mentionBadges; const isJoined = this.props.room.getMyMembership() === "join"; - const looksLikeDm = this.props.room.currentState.getMembers().length === 2; + const looksLikeDm = this.props.room.getInvitedAndJoinedMemberCount() === 2; let subtext = null; if (!isInvite && isJoined && looksLikeDm) { const selfId = MatrixClientPeg.get().getUserId(); From a91963e5eeaadac4f2f24e50bf1f911ebcd1d7b4 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 12 Dec 2018 18:03:30 -0700 Subject: [PATCH 106/237] Replace the avatar next to the composer with a status entry menu The checkmark might change, and there appears to be some state tracking mishaps that need to be worked out. Part of https://github.com/vector-im/riot-web/issues/1528 --- res/css/_components.scss | 1 + .../avatars/_MemberStatusMessageAvatar.scss | 54 ++++++ res/img/icons-checkmark.svg | 94 ++++++++++ .../avatars/MemberStatusMessageAvatar.js | 165 ++++++++++++++++++ src/components/views/rooms/MessageComposer.js | 4 +- src/i18n/strings/en_EN.json | 2 + 6 files changed, 318 insertions(+), 2 deletions(-) create mode 100644 res/css/views/avatars/_MemberStatusMessageAvatar.scss create mode 100644 res/img/icons-checkmark.svg create mode 100644 src/components/views/avatars/MemberStatusMessageAvatar.js diff --git a/res/css/_components.scss b/res/css/_components.scss index 579856f880..7975a71e4f 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -24,6 +24,7 @@ @import "./structures/_ViewSource.scss"; @import "./structures/login/_Login.scss"; @import "./views/avatars/_BaseAvatar.scss"; +@import "./views/avatars/_MemberStatusMessageAvatar.scss"; @import "./views/context_menus/_MessageContextMenu.scss"; @import "./views/context_menus/_RoomTileContextMenu.scss"; @import "./views/context_menus/_TagTileContextMenu.scss"; diff --git a/res/css/views/avatars/_MemberStatusMessageAvatar.scss b/res/css/views/avatars/_MemberStatusMessageAvatar.scss new file mode 100644 index 0000000000..166dc1a2c7 --- /dev/null +++ b/res/css/views/avatars/_MemberStatusMessageAvatar.scss @@ -0,0 +1,54 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_MemberStatusMessageAvatar { +} + +.mx_MemberStatusMessageAvatar_contextMenu_message { + display: inline-block; + border-radius: 3px 0 0 3px; + border: 1px solid $input-border-color; + font-size: 13px; + padding: 7px 7px 7px 9px; + width: 135px; + background-color: $primary-bg-color !important; +} + +.mx_MemberStatusMessageAvatar_contextMenu_submit { + display: inline-block; +} + +.mx_MemberStatusMessageAvatar_contextMenu_submit img { + vertical-align: middle; + margin-left: 8px; +} + +.mx_MemberStatusMessageAvatar_contextMenu hr { + border: 0.5px solid $menu-border-color; +} + +.mx_MemberStatusMessageAvatar_contextMenu_clearIcon { + margin: 5px 15px 5px 5px; + vertical-align: middle; +} + +.mx_MemberStatusMessageAvatar_contextMenu_clear { + padding: 2px; +} + +.mx_MemberStatusMessageAvatar_contextMenu_hasStatus .mx_MemberStatusMessageAvatar_contextMenu_clear { + color: $warning-color; +} diff --git a/res/img/icons-checkmark.svg b/res/img/icons-checkmark.svg new file mode 100644 index 0000000000..748dc61995 --- /dev/null +++ b/res/img/icons-checkmark.svg @@ -0,0 +1,94 @@ + + + +image/svg+xml + + + +icons_create_room +Created with sketchtool. + + + + + + + + + + + + diff --git a/src/components/views/avatars/MemberStatusMessageAvatar.js b/src/components/views/avatars/MemberStatusMessageAvatar.js new file mode 100644 index 0000000000..66122f9eee --- /dev/null +++ b/src/components/views/avatars/MemberStatusMessageAvatar.js @@ -0,0 +1,165 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import PropTypes from 'prop-types'; +import { _t } from '../../../languageHandler'; +import MatrixClientPeg from '../../../MatrixClientPeg'; +import AccessibleButton from '../elements/AccessibleButton'; +import MemberAvatar from '../avatars/MemberAvatar'; +import classNames from 'classnames'; +import * as ContextualMenu from "../../structures/ContextualMenu"; +import GenericElementContextMenu from "../context_menus/GenericElementContextMenu"; + +export default class MemberStatusMessageAvatar extends React.Component { + constructor(props, context) { + super(props, context); + this._onRoomStateEvents = this._onRoomStateEvents.bind(this); + this._onClick = this._onClick.bind(this); + this._onClearClick = this._onClearClick.bind(this); + this._onSubmit = this._onSubmit.bind(this); + this._onStatusChange = this._onStatusChange.bind(this); + } + + componentWillMount() { + if (this.props.member.userId !== MatrixClientPeg.get().getUserId()) { + throw new Error("Cannot use MemberStatusMessageAvatar on anyone but the logged in user"); + } + } + + componentDidMount() { + MatrixClientPeg.get().on("RoomState.events", this._onRoomStateEvents); + + if (this.props.member.user) { + this.setState({message: this.props.member.user.statusMessage}); + } else { + this.setState({message: ""}); + } + } + + componentWillUnmount() { + if (MatrixClientPeg.get()) { + MatrixClientPeg.get().removeListener("RoomState.events", this._onRoomStateEvents); + } + } + + _onRoomStateEvents(ev, state) { + if (ev.getStateKey() !== MatrixClientPeg.get().getUserId()) return; + if (ev.getType() !== "im.vector.user_status") return; + // TODO: We should be relying on `this.props.member.user.statusMessage` + this.setState({message: ev.getContent()["status"]}); + this.forceUpdate(); + } + + _onClick(e) { + e.stopPropagation(); + + const elementRect = e.target.getBoundingClientRect(); + + // The window X and Y offsets are to adjust position when zoomed in to page + const x = (elementRect.left + window.pageXOffset) - (elementRect.width / 2) + 3; + const chevronOffset = 12; + let y = elementRect.top + (elementRect.height / 2) + window.pageYOffset; + y = y - (chevronOffset + 4); // where 4 is 1/4 the height of the chevron + + const contextMenu = this._renderContextMenu(); + + ContextualMenu.createMenu(GenericElementContextMenu, { + chevronOffset: chevronOffset, + chevronFace: 'bottom', + left: x, + top: y, + menuWidth: 190, + element: contextMenu, + }); + } + + async _onClearClick(e) { + await MatrixClientPeg.get().setStatusMessage(""); + this.setState({message: ""}); + } + + _onSubmit(e) { + e.preventDefault(); + MatrixClientPeg.get().setStatusMessage(this.state.message); + } + + _onStatusChange(e) { + this.setState({message: e.target.value}); + } + + _renderContextMenu() { + const form =
    + + + + +
    ; + + const clearIcon = this.state.message ? "img/cancel-red.svg" : "img/cancel.svg"; + const clearButton = + {_t('Clear + {_t("Clear status")} + ; + + const menuClasses = classNames({ + "mx_MemberStatusMessageAvatar_contextMenu": true, + "mx_MemberStatusMessageAvatar_contextMenu_hasStatus": this.state.message, + }); + + return
    + { form } +
    + { clearButton } +
    ; + } + + render() { + const hasStatus = this.props.member.user ? !!this.props.member.user.statusMessage : false; + + const classes = classNames({ + "mx_MemberStatusMessageAvatar": true, + "mx_MemberStatusMessageAvatar_hasStatus": hasStatus, + }); + + return + + ; + } +} + +MemberStatusMessageAvatar.propTypes = { + member: PropTypes.object.isRequired, + width: PropTypes.number, + height: PropTypes.number, + resizeMethod: PropTypes.string, +}; + +MemberStatusMessageAvatar.defaultProps = { + width: 40, + height: 40, + resizeMethod: 'crop', +}; diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 3fa0f888df..2fc35d80cc 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -291,7 +291,7 @@ export default class MessageComposer extends React.Component { render() { const uploadInputStyle = {display: 'none'}; - const MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); + const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar'); const TintableSvg = sdk.getComponent("elements.TintableSvg"); const MessageComposerInput = sdk.getComponent("rooms.MessageComposerInput"); @@ -300,7 +300,7 @@ export default class MessageComposer extends React.Component { if (this.state.me) { controls.push(
    - +
    , ); } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 0df81b8e2a..e81ee82ca7 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1054,6 +1054,8 @@ "Low Priority": "Low Priority", "Direct Chat": "Direct Chat", "View Community": "View Community", + "Clear status": "Clear status", + "Set a new status...": "Set a new status...", "Sorry, your browser is not able to run Riot.": "Sorry, your browser is not able to run Riot.", "Riot uses many advanced browser features, some of which are not available or experimental in your current browser.": "Riot uses many advanced browser features, some of which are not available or experimental in your current browser.", "Please install Chrome or Firefox for the best experience.": "Please install Chrome or Firefox for the best experience.", From 99f5b9e39b7d81d9f62553e945ce019224464650 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 12 Dec 2018 18:18:43 -0700 Subject: [PATCH 107/237] Misc cleanup of whitespace --- res/css/views/avatars/_MemberStatusMessageAvatar.scss | 3 --- src/components/views/rooms/EntityTile.js | 8 ++++---- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/res/css/views/avatars/_MemberStatusMessageAvatar.scss b/res/css/views/avatars/_MemberStatusMessageAvatar.scss index 166dc1a2c7..4027bfa514 100644 --- a/res/css/views/avatars/_MemberStatusMessageAvatar.scss +++ b/res/css/views/avatars/_MemberStatusMessageAvatar.scss @@ -14,9 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_MemberStatusMessageAvatar { -} - .mx_MemberStatusMessageAvatar_contextMenu_message { display: inline-block; border-radius: 3px 0 0 3px; diff --git a/src/components/views/rooms/EntityTile.js b/src/components/views/rooms/EntityTile.js index a5b75b89bf..46c5502310 100644 --- a/src/components/views/rooms/EntityTile.js +++ b/src/components/views/rooms/EntityTile.js @@ -126,8 +126,8 @@ const EntityTile = React.createClass({ let nameClasses = 'mx_EntityTile_name'; if (this.props.showPresence) { presenceLabel = ; + currentlyActive={this.props.presenceCurrentlyActive} + presenceState={this.props.presenceState} />; nameClasses += ' mx_EntityTile_name_hover'; } if (this.props.subtextLabel) { @@ -135,9 +135,9 @@ const EntityTile = React.createClass({ } nameEl = (
    - + - {name} + { name } {presenceLabel}
    From b0b7932f5fc6960262bdbd3b54bf0d1e119a6677 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 12 Dec 2018 22:26:39 -0700 Subject: [PATCH 108/237] Move status context menu to its own component This fixes a lot of the state bugs such as buttons not updating, etc. This commit also adds the border around the avatar to indicate a status is set. --- res/css/_components.scss | 1 + .../avatars/_MemberStatusMessageAvatar.scss | 37 +------- .../_StatusMessageContextMenu.scss | 51 +++++++++++ .../avatars/MemberStatusMessageAvatar.js | 61 +------------- .../context_menus/StatusMessageContextMenu.js | 84 +++++++++++++++++++ 5 files changed, 143 insertions(+), 91 deletions(-) create mode 100644 res/css/views/context_menus/_StatusMessageContextMenu.scss create mode 100644 src/components/views/context_menus/StatusMessageContextMenu.js diff --git a/res/css/_components.scss b/res/css/_components.scss index 7975a71e4f..7271038444 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -27,6 +27,7 @@ @import "./views/avatars/_MemberStatusMessageAvatar.scss"; @import "./views/context_menus/_MessageContextMenu.scss"; @import "./views/context_menus/_RoomTileContextMenu.scss"; +@import "./views/context_menus/_StatusMessageContextMenu.scss"; @import "./views/context_menus/_TagTileContextMenu.scss"; @import "./views/dialogs/_BugReportDialog.scss"; @import "./views/dialogs/_ChangelogDialog.scss"; diff --git a/res/css/views/avatars/_MemberStatusMessageAvatar.scss b/res/css/views/avatars/_MemberStatusMessageAvatar.scss index 4027bfa514..c857b9807b 100644 --- a/res/css/views/avatars/_MemberStatusMessageAvatar.scss +++ b/res/css/views/avatars/_MemberStatusMessageAvatar.scss @@ -14,38 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_MemberStatusMessageAvatar_contextMenu_message { - display: inline-block; - border-radius: 3px 0 0 3px; - border: 1px solid $input-border-color; - font-size: 13px; - padding: 7px 7px 7px 9px; - width: 135px; - background-color: $primary-bg-color !important; -} - -.mx_MemberStatusMessageAvatar_contextMenu_submit { - display: inline-block; -} - -.mx_MemberStatusMessageAvatar_contextMenu_submit img { - vertical-align: middle; - margin-left: 8px; -} - -.mx_MemberStatusMessageAvatar_contextMenu hr { - border: 0.5px solid $menu-border-color; -} - -.mx_MemberStatusMessageAvatar_contextMenu_clearIcon { - margin: 5px 15px 5px 5px; - vertical-align: middle; -} - -.mx_MemberStatusMessageAvatar_contextMenu_clear { - padding: 2px; -} - -.mx_MemberStatusMessageAvatar_contextMenu_hasStatus .mx_MemberStatusMessageAvatar_contextMenu_clear { - color: $warning-color; +.mx_MemberStatusMessageAvatar_hasStatus { + border: 2px solid $accent-color; + border-radius: 40px; } diff --git a/res/css/views/context_menus/_StatusMessageContextMenu.scss b/res/css/views/context_menus/_StatusMessageContextMenu.scss new file mode 100644 index 0000000000..465f1b53e4 --- /dev/null +++ b/res/css/views/context_menus/_StatusMessageContextMenu.scss @@ -0,0 +1,51 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_StatusMessageContextMenu_message { + display: inline-block; + border-radius: 3px 0 0 3px; + border: 1px solid $input-border-color; + font-size: 13px; + padding: 7px 7px 7px 9px; + width: 135px; + background-color: $primary-bg-color !important; +} + +.mx_StatusMessageContextMenu_submit { + display: inline-block; +} + +.mx_StatusMessageContextMenu_submit img { + vertical-align: middle; + margin-left: 8px; +} + +.mx_StatusMessageContextMenu hr { + border: 0.5px solid $menu-border-color; +} + +.mx_StatusMessageContextMenu_clearIcon { + margin: 5px 15px 5px 5px; + vertical-align: middle; +} + +.mx_StatusMessageContextMenu_clear { + padding: 2px; +} + +.mx_StatusMessageContextMenu_hasStatus .mx_StatusMessageContextMenu_clear { + color: $warning-color; +} diff --git a/src/components/views/avatars/MemberStatusMessageAvatar.js b/src/components/views/avatars/MemberStatusMessageAvatar.js index 66122f9eee..00b25b3c73 100644 --- a/src/components/views/avatars/MemberStatusMessageAvatar.js +++ b/src/components/views/avatars/MemberStatusMessageAvatar.js @@ -16,22 +16,18 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import { _t } from '../../../languageHandler'; import MatrixClientPeg from '../../../MatrixClientPeg'; import AccessibleButton from '../elements/AccessibleButton'; import MemberAvatar from '../avatars/MemberAvatar'; import classNames from 'classnames'; import * as ContextualMenu from "../../structures/ContextualMenu"; -import GenericElementContextMenu from "../context_menus/GenericElementContextMenu"; +import StatusMessageContextMenu from "../context_menus/StatusMessageContextMenu"; export default class MemberStatusMessageAvatar extends React.Component { constructor(props, context) { super(props, context); this._onRoomStateEvents = this._onRoomStateEvents.bind(this); this._onClick = this._onClick.bind(this); - this._onClearClick = this._onClearClick.bind(this); - this._onSubmit = this._onSubmit.bind(this); - this._onStatusChange = this._onStatusChange.bind(this); } componentWillMount() { @@ -75,64 +71,16 @@ export default class MemberStatusMessageAvatar extends React.Component { let y = elementRect.top + (elementRect.height / 2) + window.pageYOffset; y = y - (chevronOffset + 4); // where 4 is 1/4 the height of the chevron - const contextMenu = this._renderContextMenu(); - - ContextualMenu.createMenu(GenericElementContextMenu, { + ContextualMenu.createMenu(StatusMessageContextMenu, { chevronOffset: chevronOffset, chevronFace: 'bottom', left: x, top: y, menuWidth: 190, - element: contextMenu, + user: this.props.member.user, }); } - async _onClearClick(e) { - await MatrixClientPeg.get().setStatusMessage(""); - this.setState({message: ""}); - } - - _onSubmit(e) { - e.preventDefault(); - MatrixClientPeg.get().setStatusMessage(this.state.message); - } - - _onStatusChange(e) { - this.setState({message: e.target.value}); - } - - _renderContextMenu() { - const form =
    - - - - -
    ; - - const clearIcon = this.state.message ? "img/cancel-red.svg" : "img/cancel.svg"; - const clearButton = - {_t('Clear - {_t("Clear status")} - ; - - const menuClasses = classNames({ - "mx_MemberStatusMessageAvatar_contextMenu": true, - "mx_MemberStatusMessageAvatar_contextMenu_hasStatus": this.state.message, - }); - - return
    - { form } -
    - { clearButton } -
    ; - } - render() { const hasStatus = this.props.member.user ? !!this.props.member.user.statusMessage : false; @@ -145,8 +93,7 @@ export default class MemberStatusMessageAvatar extends React.Component { + resizeMethod={this.props.resizeMethod} /> ; } } diff --git a/src/components/views/context_menus/StatusMessageContextMenu.js b/src/components/views/context_menus/StatusMessageContextMenu.js new file mode 100644 index 0000000000..f77669329f --- /dev/null +++ b/src/components/views/context_menus/StatusMessageContextMenu.js @@ -0,0 +1,84 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import PropTypes from 'prop-types'; +import { _t } from '../../../languageHandler'; +import MatrixClientPeg from '../../../MatrixClientPeg'; +import AccessibleButton from '../elements/AccessibleButton'; +import classNames from 'classnames'; + +export default class StatusMessageContextMenu extends React.Component { + constructor(props, context) { + super(props, context); + this._onClearClick = this._onClearClick.bind(this); + this._onSubmit = this._onSubmit.bind(this); + this._onStatusChange = this._onStatusChange.bind(this); + + this.state = { + message: props.user ? props.user.statusMessage : "", + }; + } + + async _onClearClick(e) { + await MatrixClientPeg.get().setStatusMessage(""); + this.setState({message: ""}); + } + + _onSubmit(e) { + e.preventDefault(); + MatrixClientPeg.get().setStatusMessage(this.state.message); + } + + _onStatusChange(e) { + this.setState({message: e.target.value}); + } + + render() { + const form =
    + + + + +
    ; + + const clearIcon = this.state.message ? "img/cancel-red.svg" : "img/cancel.svg"; + const clearButton = + {_t('Clear + {_t("Clear status")} + ; + + const menuClasses = classNames({ + "mx_StatusMessageContextMenu": true, + "mx_StatusMessageContextMenu_hasStatus": this.state.message, + }); + + return
    + { form } +
    + { clearButton } +
    ; + } +} + +StatusMessageContextMenu.propTypes = { + // js-sdk User object. Not required because it might not exist. + user: PropTypes.object, +}; From f2649f7807cd65b787a03e0a78a3f2b77af22987 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 12 Dec 2018 23:07:03 -0700 Subject: [PATCH 109/237] Use the now-prefixed js-sdk status message API See https://github.com/matrix-org/matrix-js-sdk/commit/08b3dfa3b5b5d0b63272f0b80b9fdd88d0795c45 --- src/components/views/avatars/MemberStatusMessageAvatar.js | 6 +++--- .../views/context_menus/StatusMessageContextMenu.js | 6 +++--- src/components/views/rooms/MemberInfo.js | 2 +- src/components/views/rooms/MemberTile.js | 2 +- src/components/views/rooms/RoomTile.js | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/components/views/avatars/MemberStatusMessageAvatar.js b/src/components/views/avatars/MemberStatusMessageAvatar.js index 00b25b3c73..2a2c97ee7c 100644 --- a/src/components/views/avatars/MemberStatusMessageAvatar.js +++ b/src/components/views/avatars/MemberStatusMessageAvatar.js @@ -40,7 +40,7 @@ export default class MemberStatusMessageAvatar extends React.Component { MatrixClientPeg.get().on("RoomState.events", this._onRoomStateEvents); if (this.props.member.user) { - this.setState({message: this.props.member.user.statusMessage}); + this.setState({message: this.props.member.user._unstable_statusMessage}); } else { this.setState({message: ""}); } @@ -55,7 +55,7 @@ export default class MemberStatusMessageAvatar extends React.Component { _onRoomStateEvents(ev, state) { if (ev.getStateKey() !== MatrixClientPeg.get().getUserId()) return; if (ev.getType() !== "im.vector.user_status") return; - // TODO: We should be relying on `this.props.member.user.statusMessage` + // TODO: We should be relying on `this.props.member.user._unstable_statusMessage` this.setState({message: ev.getContent()["status"]}); this.forceUpdate(); } @@ -82,7 +82,7 @@ export default class MemberStatusMessageAvatar extends React.Component { } render() { - const hasStatus = this.props.member.user ? !!this.props.member.user.statusMessage : false; + const hasStatus = this.props.member.user ? !!this.props.member.user._unstable_statusMessage : false; const classes = classNames({ "mx_MemberStatusMessageAvatar": true, diff --git a/src/components/views/context_menus/StatusMessageContextMenu.js b/src/components/views/context_menus/StatusMessageContextMenu.js index f77669329f..a3b31420f6 100644 --- a/src/components/views/context_menus/StatusMessageContextMenu.js +++ b/src/components/views/context_menus/StatusMessageContextMenu.js @@ -29,18 +29,18 @@ export default class StatusMessageContextMenu extends React.Component { this._onStatusChange = this._onStatusChange.bind(this); this.state = { - message: props.user ? props.user.statusMessage : "", + message: props.user ? props.user._unstable_statusMessage : "", }; } async _onClearClick(e) { - await MatrixClientPeg.get().setStatusMessage(""); + await MatrixClientPeg.get()._unstable_setStatusMessage(""); this.setState({message: ""}); } _onSubmit(e) { e.preventDefault(); - MatrixClientPeg.get().setStatusMessage(this.state.message); + MatrixClientPeg.get()._unstable_setStatusMessage(this.state.message); } _onStatusChange(e) { diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 4eea33e952..6bcdc53d4c 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -895,7 +895,7 @@ module.exports = withMatrixClient(React.createClass({ presenceState = this.props.member.user.presence; presenceLastActiveAgo = this.props.member.user.lastActiveAgo; presenceCurrentlyActive = this.props.member.user.currentlyActive; - statusMessage = this.props.member.user.statusMessage; + statusMessage = this.props.member.user._unstable_statusMessage; } const room = this.props.matrixClient.getRoom(this.props.member.roomId); diff --git a/src/components/views/rooms/MemberTile.js b/src/components/views/rooms/MemberTile.js index d246b37234..96a8e0b515 100644 --- a/src/components/views/rooms/MemberTile.js +++ b/src/components/views/rooms/MemberTile.js @@ -84,7 +84,7 @@ module.exports = React.createClass({ const name = this._getDisplayName(); const active = -1; const presenceState = member.user ? member.user.presence : null; - const statusMessage = member.user ? member.user.statusMessage : null; + const statusMessage = member.user ? member.user._unstable_statusMessage : null; const av = ( diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index fa18a0687b..91c5d0321d 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -257,8 +257,8 @@ module.exports = React.createClass({ if (!isInvite && isJoined && looksLikeDm) { const selfId = MatrixClientPeg.get().getUserId(); const otherMember = this.props.room.currentState.getMembersExcept([selfId])[0]; - if (otherMember.user && otherMember.user.statusMessage) { - subtext = otherMember.user.statusMessage; + if (otherMember.user && otherMember.user._unstable_statusMessage) { + subtext = otherMember.user._unstable_statusMessage; } } From 15366fbb0a9434ab06bac7a965cfe09375ef07ab Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 13 Dec 2018 15:59:18 +0000 Subject: [PATCH 110/237] Change room recovery reminder button style Change the button to a transparent background so that it's less prominent and you focus on the primary button instead. Signed-off-by: J. Ryan Stinnett --- res/css/views/rooms/_RoomRecoveryReminder.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/res/css/views/rooms/_RoomRecoveryReminder.scss b/res/css/views/rooms/_RoomRecoveryReminder.scss index 4bb42ff114..e4e2d19b42 100644 --- a/res/css/views/rooms/_RoomRecoveryReminder.scss +++ b/res/css/views/rooms/_RoomRecoveryReminder.scss @@ -40,4 +40,5 @@ limitations under the License. .mx_RoomRecoveryReminder_button.mx_RoomRecoveryReminder_secondary { @mixin mx_DialogButton_secondary; + background-color: transparent; } From c6f35428d751432403a71d2e0e025e92723f73af Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 13 Dec 2018 09:37:54 -0700 Subject: [PATCH 111/237] Update checkmark icon --- res/img/icons-checkmark.svg | 109 ++++++------------------------------ 1 file changed, 16 insertions(+), 93 deletions(-) diff --git a/res/img/icons-checkmark.svg b/res/img/icons-checkmark.svg index 748dc61995..3c5392003d 100644 --- a/res/img/icons-checkmark.svg +++ b/res/img/icons-checkmark.svg @@ -1,94 +1,17 @@ - - - -image/svg+xml - - - -icons_create_room -Created with sketchtool. - - - - - - - - - - - + + + + Tick + Created with Sketch. + + + + + + + + + + + From 63658e0441e9be4ad1b26797e9621cc921041768 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 13 Dec 2018 14:29:12 -0700 Subject: [PATCH 112/237] Add a missing null check --- src/components/views/rooms/RoomTile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 91c5d0321d..676edc1ea2 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -257,7 +257,7 @@ module.exports = React.createClass({ if (!isInvite && isJoined && looksLikeDm) { const selfId = MatrixClientPeg.get().getUserId(); const otherMember = this.props.room.currentState.getMembersExcept([selfId])[0]; - if (otherMember.user && otherMember.user._unstable_statusMessage) { + if (otherMember && otherMember.user && otherMember.user._unstable_statusMessage) { subtext = otherMember.user._unstable_statusMessage; } } From 5f434cd31cda9d64523810777e4be48d3ce293d6 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 13 Dec 2018 14:45:08 -0700 Subject: [PATCH 113/237] Don't break the UI when something goes wrong --- src/components/structures/MatrixChat.js | 38 ++++++++++++++---------- src/components/structures/login/Login.js | 5 +++- src/i18n/strings/en_EN.json | 1 + 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index e93234c679..c8b2737cc9 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1761,22 +1761,30 @@ export default React.createClass({ }, _tryDiscoverDefaultHomeserver: async function(serverName) { - const discovery = await AutoDiscovery.findClientConfig(serverName); - const state = discovery["m.homeserver"].state; - if (state !== AutoDiscovery.SUCCESS) { - console.error("Failed to discover homeserver on startup:", discovery); + try { + const discovery = await AutoDiscovery.findClientConfig(serverName); + const state = discovery["m.homeserver"].state; + if (state !== AutoDiscovery.SUCCESS) { + console.error("Failed to discover homeserver on startup:", discovery); + this.setState({ + defaultServerDiscoveryError: discovery["m.homeserver"].error, + loadingDefaultHomeserver: false, + }); + } else { + const hsUrl = discovery["m.homeserver"].base_url; + const isUrl = discovery["m.identity_server"].state === AutoDiscovery.SUCCESS + ? discovery["m.identity_server"].base_url + : "https://vector.im"; + this.setState({ + defaultHsUrl: hsUrl, + defaultIsUrl: isUrl, + loadingDefaultHomeserver: false, + }); + } + } catch (e) { + console.error(e); this.setState({ - defaultServerDiscoveryError: discovery["m.homeserver"].error, - loadingDefaultHomeserver: false, - }); - } else { - const hsUrl = discovery["m.homeserver"].base_url; - const isUrl = discovery["m.identity_server"].state === AutoDiscovery.SUCCESS - ? discovery["m.identity_server"].base_url - : "https://vector.im"; - this.setState({ - defaultHsUrl: hsUrl, - defaultIsUrl: isUrl, + defaultServerDiscoveryError: _t("Unknown error discovering homeserver"), loadingDefaultHomeserver: false, }); } diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index bfaab8fdb8..b94a1759cf 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -343,7 +343,10 @@ module.exports = React.createClass({ } } catch (e) { console.error(e); - throw e; + this.setState({ + findingHomeserver: false, + discoveryError: _t("Unknown error discovering homeserver"), + }); } }, diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 8c5f3f5351..56109fa8db 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1128,6 +1128,7 @@ "Review terms and conditions": "Review terms and conditions", "Old cryptography data detected": "Old cryptography data detected", "Data from an older version of Riot has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.": "Data from an older version of Riot has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.", + "Unknown error discovering homeserver": "Unknown error discovering homeserver", "Logout": "Logout", "Your Communities": "Your Communities", "Did you know: you can use communities to filter your Riot.im experience!": "Did you know: you can use communities to filter your Riot.im experience!", From 49769a405d70ccfced28dd5f58af6fe0494dba4d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 13 Dec 2018 16:12:56 -0700 Subject: [PATCH 114/237] Fix translation error on notification icon Introduced by https://github.com/matrix-org/matrix-react-sdk/pull/2336 --- src/components/structures/RightPanel.js | 3 ++- src/i18n/strings/en_EN.json | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/structures/RightPanel.js b/src/components/structures/RightPanel.js index 0870f085a5..e8371697f2 100644 --- a/src/components/structures/RightPanel.js +++ b/src/components/structures/RightPanel.js @@ -326,7 +326,8 @@ module.exports = React.createClass({ let notifCount = 0; MatrixClientPeg.get().getRooms().forEach(r => notifCount += (r.getUnreadNotificationCount('highlight') || 0)); if (notifCount > 0) { - notifCountBadge =
    { formatCount(notifCount) }
    ; + const title = _t("%(count)s Notifications", {count: formatCount(notifCount)}); + notifCountBadge =
    { formatCount(notifCount) }
    ; } headerButtons = [ diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 92fb545b4e..a97182520a 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1144,6 +1144,8 @@ "%(count)s Members|other": "%(count)s Members", "%(count)s Members|one": "%(count)s Member", "Invite to this room": "Invite to this room", + "%(count)s Notifications|other": "%(count)s Notifications", + "%(count)s Notifications|one": "%(count)s Notification", "Files": "Files", "Notifications": "Notifications", "Hide panel": "Hide panel", From 8592e76e124c28d69903865e43180c7153bbaee0 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 13 Dec 2018 16:05:34 -0700 Subject: [PATCH 115/237] Standardize errors about localpart structure Fixes https://github.com/vector-im/riot-web/issues/5833 This also includes changing some Jira references that aren't searchable anymore, and a thing to replace the spinner on the SetMxidDialog as per https://github.com/vector-im/riot-web/issues/5833#issuecomment-445323177 --- src/Registration.js | 4 ++++ src/components/structures/login/Registration.js | 2 +- src/components/views/dialogs/SetMxIdDialog.js | 15 ++++++--------- src/components/views/login/RegistrationForm.js | 7 +++---- .../views/room_settings/AliasSettings.js | 2 +- src/i18n/strings/en_EN.json | 3 ++- 6 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/Registration.js b/src/Registration.js index f86c9cc618..8364fc7cdb 100644 --- a/src/Registration.js +++ b/src/Registration.js @@ -26,6 +26,10 @@ import MatrixClientPeg from './MatrixClientPeg'; import Modal from './Modal'; import { _t } from './languageHandler'; +// Regex for what a "safe" or "Matrix-looking" localpart would be. +// TODO: Update as needed for https://github.com/matrix-org/matrix-doc/issues/1514 +export const SAFE_LOCALPART_REGEX = /^[a-z0-9=_\-./]+$/g; + /** * Starts either the ILAG or full registration flow, depending * on what the HS supports diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index 98fd61370d..ad3ea5f19c 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -342,7 +342,7 @@ module.exports = React.createClass({ errMsg = _t('A phone number is required to register on this homeserver.'); break; case "RegistrationForm.ERR_USERNAME_INVALID": - errMsg = _t('User names may only contain letters, numbers, dots, hyphens and underscores.'); + errMsg = _t("Only use lower case letters, numbers and '=_-./'"); break; case "RegistrationForm.ERR_USERNAME_BLANK": errMsg = _t('You need to enter a user name.'); diff --git a/src/components/views/dialogs/SetMxIdDialog.js b/src/components/views/dialogs/SetMxIdDialog.js index fb892c4a0a..222a2c35fe 100644 --- a/src/components/views/dialogs/SetMxIdDialog.js +++ b/src/components/views/dialogs/SetMxIdDialog.js @@ -23,6 +23,7 @@ import MatrixClientPeg from '../../../MatrixClientPeg'; import classnames from 'classnames'; import { KeyCode } from '../../../Keyboard'; import { _t } from '../../../languageHandler'; +import { SAFE_LOCALPART_REGEX } from '../../../Registration'; // The amount of time to wait for further changes to the input username before // sending a request to the server @@ -110,12 +111,11 @@ export default React.createClass({ }, _doUsernameCheck: function() { - // XXX: SPEC-1 - // Check if username is valid - // Naive impl copied from https://github.com/matrix-org/matrix-react-sdk/blob/66c3a6d9ca695780eb6b662e242e88323053ff33/src/components/views/login/RegistrationForm.js#L190 - if (encodeURIComponent(this.state.username) !== this.state.username) { + // We do a quick check ahead of the username availability API to ensure the + // user ID roughly looks okay from a Matrix perspective. + if (!SAFE_LOCALPART_REGEX.test(this.state.username)) { this.setState({ - usernameError: _t('User names may only contain letters, numbers, dots, hyphens and underscores.'), + usernameError: _t("Only use lower case letters, numbers and '=_-./'"), }); return Promise.reject(); } @@ -210,7 +210,6 @@ export default React.createClass({ render: function() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const InteractiveAuth = sdk.getComponent('structures.InteractiveAuth'); - const Spinner = sdk.getComponent('elements.Spinner'); let auth; if (this.state.doingUIAuth) { @@ -230,9 +229,8 @@ export default React.createClass({ }); let usernameIndicator = null; - let usernameBusyIndicator = null; if (this.state.usernameBusy) { - usernameBusyIndicator = ; + usernameIndicator =
    {_t("Checking...")}
    ; } else { const usernameAvailable = this.state.username && this.state.usernameCheckSupport && !this.state.usernameError; @@ -270,7 +268,6 @@ export default React.createClass({ size="30" className={inputClasses} /> - { usernameBusyIndicator }
    { usernameIndicator }

    diff --git a/src/components/views/login/RegistrationForm.js b/src/components/views/login/RegistrationForm.js index fe977025ae..137aeada91 100644 --- a/src/components/views/login/RegistrationForm.js +++ b/src/components/views/login/RegistrationForm.js @@ -25,7 +25,7 @@ import { looksValid as phoneNumberLooksValid } from '../../../phonenumber'; import Modal from '../../../Modal'; import { _t } from '../../../languageHandler'; import SdkConfig from '../../../SdkConfig'; -import SettingsStore from "../../../settings/SettingsStore"; +import { SAFE_LOCALPART_REGEX } from '../../../Registration'; const FIELD_EMAIL = 'field_email'; const FIELD_PHONE_COUNTRY = 'field_phone_country'; @@ -194,9 +194,8 @@ module.exports = React.createClass({ } else this.markFieldValid(field_id, phoneNumberValid, "RegistrationForm.ERR_PHONE_NUMBER_INVALID"); break; case FIELD_USERNAME: - // XXX: SPEC-1 - var username = this.refs.username.value.trim(); - if (encodeURIComponent(username) != username) { + const username = this.refs.username.value.trim(); + if (!SAFE_LOCALPART_REGEX.test(username)) { this.markFieldValid( field_id, false, diff --git a/src/components/views/room_settings/AliasSettings.js b/src/components/views/room_settings/AliasSettings.js index de5d3db625..f68670b2f9 100644 --- a/src/components/views/room_settings/AliasSettings.js +++ b/src/components/views/room_settings/AliasSettings.js @@ -130,7 +130,7 @@ module.exports = React.createClass({ }, isAliasValid: function(alias) { - // XXX: FIXME SPEC-1 + // XXX: FIXME https://github.com/matrix-org/matrix-doc/issues/668 return (alias.match(/^#([^\/:,]+?):(.+)$/) && encodeURI(alias) === alias); }, diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 92fb545b4e..c3282c8bf8 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -985,10 +985,11 @@ "Unable to verify email address.": "Unable to verify email address.", "This will allow you to reset your password and receive notifications.": "This will allow you to reset your password and receive notifications.", "Skip": "Skip", - "User names may only contain letters, numbers, dots, hyphens and underscores.": "User names may only contain letters, numbers, dots, hyphens and underscores.", + "Only use lower case letters, numbers and '=_-./'": "Only use lower case letters, numbers and '=_-./'", "Username not available": "Username not available", "Username invalid: %(errMessage)s": "Username invalid: %(errMessage)s", "An error occurred: %(error_string)s": "An error occurred: %(error_string)s", + "Checking...": "Checking...", "Username available": "Username available", "To get started, please pick a username!": "To get started, please pick a username!", "This will be your account name on the homeserver, or you can pick a different server.": "This will be your account name on the homeserver, or you can pick a different server.", From 576bfedfb54e5625bcda2b976b1ca62aadfb0e11 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 13 Dec 2018 22:00:06 -0700 Subject: [PATCH 116/237] Remove global flag Regular expression objects are stateful, and the global flag interferes badly with the expression. A valid call can cause it to act like a flip flop instead of a stateless test. --- src/Registration.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Registration.js b/src/Registration.js index 8364fc7cdb..98aee3ac83 100644 --- a/src/Registration.js +++ b/src/Registration.js @@ -28,7 +28,7 @@ import { _t } from './languageHandler'; // Regex for what a "safe" or "Matrix-looking" localpart would be. // TODO: Update as needed for https://github.com/matrix-org/matrix-doc/issues/1514 -export const SAFE_LOCALPART_REGEX = /^[a-z0-9=_\-./]+$/g; +export const SAFE_LOCALPART_REGEX = /^[a-z0-9=_\-./]+$/; /** * Starts either the ILAG or full registration flow, depending From 2afdb04db5f3dbc5bd7466b22a5da523432f6d39 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 14 Dec 2018 17:05:01 +0100 Subject: [PATCH 117/237] prevent jumping on when hovering over invited section --- res/css/views/rooms/_RoomTile.scss | 4 ---- 1 file changed, 4 deletions(-) diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss index ff54da7196..a15f9f3186 100644 --- a/res/css/views/rooms/_RoomTile.scss +++ b/res/css/views/rooms/_RoomTile.scss @@ -39,10 +39,6 @@ limitations under the License. .mx_RoomTile_menuButton { display: block; } - - .mx_RoomTile_badge { - display: none; - } } .mx_RoomTile_tooltip { From e4d2b6f2b76db4a700ccbb66875347d6b9a5c5f1 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 14 Dec 2018 17:06:00 +0100 Subject: [PATCH 118/237] fix resize handles being too thick on edge (hopefully) --- res/css/views/elements/_ResizeHandle.scss | 17 +++++++++++++---- src/components/views/elements/ResizeHandle.js | 2 +- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/res/css/views/elements/_ResizeHandle.scss b/res/css/views/elements/_ResizeHandle.scss index 8f533a02bc..42ff6e3825 100644 --- a/res/css/views/elements/_ResizeHandle.scss +++ b/res/css/views/elements/_ResizeHandle.scss @@ -17,21 +17,30 @@ limitations under the License. .mx_ResizeHandle { cursor: row-resize; flex: 0 0 auto; - background: $panel-divider-color; - background-clip: content-box; z-index: 100; } .mx_ResizeHandle.mx_ResizeHandle_horizontal { - width: 1px; margin: 0 -5px; padding: 0 5px; cursor: col-resize; } .mx_ResizeHandle.mx_ResizeHandle_vertical { - height: 1px; margin: -5px 0; padding: 5px 0; cursor: row-resize; } + +.mx_ResizeHandle > div { + background: $panel-divider-color; +} + +.mx_ResizeHandle.mx_ResizeHandle_horizontal > div { + width: 1px; + height: 100%; +} + +.mx_ResizeHandle.mx_ResizeHandle_vertical > div { + height: 1px; +} diff --git a/src/components/views/elements/ResizeHandle.js b/src/components/views/elements/ResizeHandle.js index b5487b1fc1..863dfaaa93 100644 --- a/src/components/views/elements/ResizeHandle.js +++ b/src/components/views/elements/ResizeHandle.js @@ -14,7 +14,7 @@ const ResizeHandle = (props) => { classNames.push('mx_ResizeHandle_reverse'); } return ( -

    +
    ); }; From 3ea0fd1a2f3dbe536c57a50ad2f01262c19392fa Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 14 Dec 2018 17:06:51 +0100 Subject: [PATCH 119/237] fix overflow indicators not being updated when searching --- src/components/structures/RoomSubList.js | 8 +++++++- src/components/views/rooms/RoomList.js | 18 +++++++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/components/structures/RoomSubList.js b/src/components/structures/RoomSubList.js index 91b29d4665..b4fbc5406e 100644 --- a/src/components/structures/RoomSubList.js +++ b/src/components/structures/RoomSubList.js @@ -313,6 +313,12 @@ const RoomSubList = React.createClass({ ); }, + checkOverflow: function() { + if (this.refs.scroller) { + this.refs.scroller.checkOverflow(); + } + }, + render: function() { const len = this.props.list.length + this.props.extraTiles.length; if (len) { @@ -330,7 +336,7 @@ const RoomSubList = React.createClass({ tiles.push(...this.props.extraTiles); return
    {this._getHeaderJsx()} - + { tiles }
    ; diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 9e766bdc15..9fb872cd32 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -71,6 +71,10 @@ module.exports = React.createClass({ getInitialState: function() { + this._subListRefs = { + // key => RoomSubList ref + }; + const sizesJson = window.localStorage.getItem("mx_roomlist_sizes"); const collapsedJson = window.localStorage.getItem("mx_roomlist_collapsed"); this.subListSizes = sizesJson ? JSON.parse(sizesJson) : {}; @@ -174,8 +178,11 @@ module.exports = React.createClass({ this.mounted = true; }, - componentDidUpdate: function() { + componentDidUpdate: function(prevProps) { this._repositionIncomingCallBox(undefined, false); + if (this.props.searchFilter !== prevProps.searchFilter) { + Object.values(this._subListRefs).forEach(l => l.checkOverflow()); + } }, onAction: function(payload) { @@ -483,6 +490,14 @@ module.exports = React.createClass({ window.localStorage.setItem("mx_roomlist_collapsed", JSON.stringify(this.collapsedState)); }, + _subListRef: function(key, ref) { + if (!ref) { + delete this._subListRefs[key]; + } else { + this._subListRefs[key] = ref; + } + }, + _mapSubListProps: function(subListsProps) { const defaultProps = { collapsed: this.props.collapsed, @@ -513,6 +528,7 @@ module.exports = React.createClass({ const startAsHidden = props.startAsHidden || this.collapsedState[chosenKey]; let subList = ( Date: Fri, 14 Dec 2018 13:44:40 -0700 Subject: [PATCH 120/237] Disable password managers on the status form --- src/components/views/context_menus/StatusMessageContextMenu.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/context_menus/StatusMessageContextMenu.js b/src/components/views/context_menus/StatusMessageContextMenu.js index a3b31420f6..5f137a12a5 100644 --- a/src/components/views/context_menus/StatusMessageContextMenu.js +++ b/src/components/views/context_menus/StatusMessageContextMenu.js @@ -48,7 +48,7 @@ export default class StatusMessageContextMenu extends React.Component { } render() { - const form =
    + const form = From 7b0766a30352c0ebf06e91e11ed7c2ab0c993ad5 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 14 Dec 2018 13:49:35 -0700 Subject: [PATCH 121/237] Apply 50% opacity to the checkmark when there is no status --- res/css/views/context_menus/_StatusMessageContextMenu.scss | 4 ++++ .../views/context_menus/StatusMessageContextMenu.js | 7 ++++++- src/i18n/strings/en_EN.json | 4 ++-- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/res/css/views/context_menus/_StatusMessageContextMenu.scss b/res/css/views/context_menus/_StatusMessageContextMenu.scss index 465f1b53e4..873ad99495 100644 --- a/res/css/views/context_menus/_StatusMessageContextMenu.scss +++ b/res/css/views/context_menus/_StatusMessageContextMenu.scss @@ -28,6 +28,10 @@ limitations under the License. display: inline-block; } +.mx_StatusMessageContextMenu_submitFaded { + opacity: 0.5; +} + .mx_StatusMessageContextMenu_submit img { vertical-align: middle; margin-left: 8px; diff --git a/src/components/views/context_menus/StatusMessageContextMenu.js b/src/components/views/context_menus/StatusMessageContextMenu.js index 5f137a12a5..243164301d 100644 --- a/src/components/views/context_menus/StatusMessageContextMenu.js +++ b/src/components/views/context_menus/StatusMessageContextMenu.js @@ -48,11 +48,16 @@ export default class StatusMessageContextMenu extends React.Component { } render() { + const formSubmitClasses = classNames({ + "mx_StatusMessageContextMenu_submit": true, + "mx_StatusMessageContextMenu_submitFaded": !this.state.message, // no message == faded + }); + const form = - + ; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index e81ee82ca7..5c7e067f95 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1053,9 +1053,9 @@ "Forget": "Forget", "Low Priority": "Low Priority", "Direct Chat": "Direct Chat", - "View Community": "View Community", - "Clear status": "Clear status", "Set a new status...": "Set a new status...", + "Clear status": "Clear status", + "View Community": "View Community", "Sorry, your browser is not able to run Riot.": "Sorry, your browser is not able to run Riot.", "Riot uses many advanced browser features, some of which are not available or experimental in your current browser.": "Riot uses many advanced browser features, some of which are not available or experimental in your current browser.", "Please install Chrome or Firefox for the best experience.": "Please install Chrome or Firefox for the best experience.", From c706135c6ed55ce8882c5ed75afeaecb023472e6 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sun, 16 Dec 2018 12:49:41 -0700 Subject: [PATCH 122/237] Force use of dharma theme --- src/settings/Settings.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/settings/Settings.js b/src/settings/Settings.js index eb702a729c..d0b4b9b9d6 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -195,8 +195,8 @@ export const SETTINGS = { default: false, }, "theme": { - supportedLevels: LEVELS_ACCOUNT_SETTINGS, - default: "light", + supportedLevels: ['config'], + default: "dharma", }, "webRtcForceTURN": { supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG, From 649910139aef5850ec2c6136f633c90827355481 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 17 Dec 2018 12:27:42 +0100 Subject: [PATCH 123/237] disable setting theme completely --- src/components/structures/MatrixChat.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 4d7c71e3ef..3f73bf9dd8 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1079,6 +1079,10 @@ export default React.createClass({ * @param {string} theme new theme */ _onSetTheme: function(theme) { + // disable changing the theme for now + // as other themes are not compatible with dharma + return; + if (!theme) { theme = SettingsStore.getValue("theme"); } From 510cec1ebfaadefa25e11399de78474a3e50efa0 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 17 Dec 2018 12:49:38 +0100 Subject: [PATCH 124/237] disabling setting theme without breaking the build --- src/components/structures/MatrixChat.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 3f73bf9dd8..0a062afc43 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -673,9 +673,11 @@ export default React.createClass({ }); break; } - case 'set_theme': - this._onSetTheme(payload.value); - break; + // case 'set_theme': + // disable changing the theme for now + // as other themes are not compatible with dharma + // this._onSetTheme(payload.value); + // break; case 'on_logging_in': // We are now logging in, so set the state to reflect that // NB. This does not touch 'ready' since if our dispatches @@ -1079,10 +1081,6 @@ export default React.createClass({ * @param {string} theme new theme */ _onSetTheme: function(theme) { - // disable changing the theme for now - // as other themes are not compatible with dharma - return; - if (!theme) { theme = SettingsStore.getValue("theme"); } From 4fffb55cc548add3112a8bf94901d3b6d5309fa7 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 17 Dec 2018 13:52:32 +0100 Subject: [PATCH 125/237] ignore any unknown tags --- src/stores/RoomListStore.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/RoomListStore.js b/src/stores/RoomListStore.js index 0f8e5d7b4d..84924232ea 100644 --- a/src/stores/RoomListStore.js +++ b/src/stores/RoomListStore.js @@ -226,7 +226,7 @@ class RoomListStore extends Store { // ignore any m. tag names we don't know about tagNames = tagNames.filter((t) => { - return !t.startsWith('m.') || lists[t] !== undefined; + return lists[t] !== undefined; }); if (tagNames.length) { From 532fb6ea783c85fbce56770c7d4bf8e87d23e96f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 17 Dec 2018 13:58:36 +0100 Subject: [PATCH 126/237] update comment as well --- src/stores/RoomListStore.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/RoomListStore.js b/src/stores/RoomListStore.js index 84924232ea..c636c53631 100644 --- a/src/stores/RoomListStore.js +++ b/src/stores/RoomListStore.js @@ -224,7 +224,7 @@ class RoomListStore extends Store { } } - // ignore any m. tag names we don't know about + // ignore tags we don't know about tagNames = tagNames.filter((t) => { return lists[t] !== undefined; }); From 6c1639b2ce8396f3b2409f7f14a5814862fe993a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 17 Dec 2018 14:03:28 +0100 Subject: [PATCH 127/237] dont hardcode avatar container dimensions --- res/css/views/rooms/_RoomTile.scss | 2 -- 1 file changed, 2 deletions(-) diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss index a15f9f3186..7dd302e1e8 100644 --- a/res/css/views/rooms/_RoomTile.scss +++ b/res/css/views/rooms/_RoomTile.scss @@ -51,8 +51,6 @@ limitations under the License. .mx_RoomTile_avatar { flex: 0; padding: 4px; - width: 32px; - height: 32px; } .mx_RoomTile_avatar_container { From 242b0c21acfde351f48d1b0bb969a58000dc8e61 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 17 Dec 2018 14:03:48 +0100 Subject: [PATCH 128/237] make avatar 24px and tile height 32px --- res/css/views/rooms/_RoomTile.scss | 2 +- src/components/views/rooms/RoomTile.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss index 7dd302e1e8..2a79599606 100644 --- a/res/css/views/rooms/_RoomTile.scss +++ b/res/css/views/rooms/_RoomTile.scss @@ -19,7 +19,7 @@ limitations under the License. flex-direction: row; align-items: center; cursor: pointer; - height: 40px; + height: 32px; margin: 0; padding: 0 8px 0 10px; position: relative; diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index faa08c7001..4e05fbcb8b 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -333,7 +333,7 @@ module.exports = React.createClass({ >
    - + { dmIndicator }
    From 0496ed535c23f764c430393b3e7cd9086d6d1dc6 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 17 Dec 2018 14:35:11 +0100 Subject: [PATCH 129/237] have a bit more space between rooms --- res/css/views/rooms/_RoomTile.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss index 2a79599606..d5ec3d365a 100644 --- a/res/css/views/rooms/_RoomTile.scss +++ b/res/css/views/rooms/_RoomTile.scss @@ -19,7 +19,7 @@ limitations under the License. flex-direction: row; align-items: center; cursor: pointer; - height: 32px; + height: 34px; margin: 0; padding: 0 8px 0 10px; position: relative; From bbafd8c2d33db1779f9c61577f6b8a23fcabfe11 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 17 Dec 2018 15:22:02 +0100 Subject: [PATCH 130/237] toggle right panel when clicking already active header button --- .../views/right_panel/HeaderButton.js | 1 + .../views/right_panel/HeaderButtons.js | 21 ++++++++++++++++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/components/views/right_panel/HeaderButton.js b/src/components/views/right_panel/HeaderButton.js index a01d3444f1..bb9f613607 100644 --- a/src/components/views/right_panel/HeaderButton.js +++ b/src/components/views/right_panel/HeaderButton.js @@ -36,6 +36,7 @@ export default class HeaderButton extends React.Component { dis.dispatch({ action: 'view_right_panel_phase', phase: this.props.clickPhase, + fromHeader: true, }); } diff --git a/src/components/views/right_panel/HeaderButtons.js b/src/components/views/right_panel/HeaderButtons.js index 3c59c52089..16f7f006f8 100644 --- a/src/components/views/right_panel/HeaderButtons.js +++ b/src/components/views/right_panel/HeaderButtons.js @@ -49,9 +49,24 @@ export default class HeaderButtons extends React.Component { onAction(payload) { if (payload.action === "view_right_panel_phase") { - this.setState({ - phase: payload.phase, - }); + // only actions coming from header buttons should collapse the right panel + if (this.state.phase === payload.phase && payload.fromHeader) { + dis.dispatch({ + action: 'hide_right_panel', + }); + this.setState({ + phase: null, + }); + } else { + if (!this.state.phase) { + dis.dispatch({ + action: 'show_right_panel', + }); + } + this.setState({ + phase: payload.phase, + }); + } } } From 65f9bc97540f9f2b49e5f9af8fd77a82a89d3e62 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 17 Dec 2018 15:23:46 +0100 Subject: [PATCH 131/237] remove expand button in room header --- src/components/views/rooms/RoomHeader.js | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 3b85730e11..e92d1499aa 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -394,14 +394,6 @@ module.exports = React.createClass({
    ; } - let rightPanelButtons; - if (this.props.collapsedRhs) { - rightPanelButtons = - - - ; - } - let rightRow; let manageIntegsButton; if (this.props.room && this.props.room.roomId && this.props.inRoom) { @@ -419,7 +411,6 @@ module.exports = React.createClass({ { manageIntegsButton } { forgetButton } { searchButton } - { rightPanelButtons }
    ; } From f7b6e9c6fc90fe02c6a0b5c01b832cca487f236d Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 17 Dec 2018 15:30:39 +0100 Subject: [PATCH 132/237] name collapsedRhs consistently everywhere --- src/components/structures/LoggedInView.js | 4 ++-- src/components/structures/MatrixChat.js | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index 635c5de44e..c36d3fe651 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -438,7 +438,7 @@ const LoggedInView = React.createClass({ eventPixelOffset={this.props.initialEventPixelOffset} key={this.props.currentRoomId || 'roomview'} disabled={this.props.middleDisabled} - collapsedRhs={this.props.collapseRhs} + collapsedRhs={this.props.collapsedRhs} ConferenceHandler={this.props.ConferenceHandler} />; break; @@ -488,7 +488,7 @@ const LoggedInView = React.createClass({ page_element = ; break; } diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 0a062afc43..ebc56dc438 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -161,7 +161,7 @@ export default React.createClass({ viewUserId: null, collapseLhs: false, - collapseRhs: false, + collapsedRhs: false, leftDisabled: false, middleDisabled: false, rightDisabled: false, @@ -555,7 +555,7 @@ export default React.createClass({ break; case 'view_user': // FIXME: ugly hack to expand the RightPanel and then re-dispatch. - if (this.state.collapseRhs) { + if (this.state.collapsedRhs) { setTimeout(()=>{ dis.dispatch({ action: 'show_right_panel', @@ -657,12 +657,12 @@ export default React.createClass({ break; case 'hide_right_panel': this.setState({ - collapseRhs: true, + collapsedRhs: true, }); break; case 'show_right_panel': this.setState({ - collapseRhs: false, + collapsedRhs: false, }); break; case 'panel_disable': { @@ -1217,7 +1217,7 @@ export default React.createClass({ view: VIEWS.LOGIN, ready: false, collapseLhs: false, - collapseRhs: false, + collapsedRhs: false, currentRoomId: null, page_type: PageTypes.RoomDirectory, }); From a734fb9d35a5750c47ce7c7352378d6dd83304f7 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 17 Dec 2018 15:55:26 +0100 Subject: [PATCH 133/237] dont set initial phase, show panel when collapsed --- src/components/structures/GroupView.js | 2 +- src/components/views/right_panel/HeaderButtons.js | 4 ++-- src/components/views/rooms/RoomHeader.js | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index 3f20a6da98..d77837ad63 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -1311,7 +1311,7 @@ export default React.createClass({
    { rightButtons }
    - +
    diff --git a/src/components/views/right_panel/HeaderButtons.js b/src/components/views/right_panel/HeaderButtons.js index 16f7f006f8..e0448c0ae2 100644 --- a/src/components/views/right_panel/HeaderButtons.js +++ b/src/components/views/right_panel/HeaderButtons.js @@ -25,7 +25,7 @@ export default class HeaderButtons extends React.Component { super(props); this.state = { - phase: initialPhase, + phase: props.collapsedRhs ? null : initialPhase, isUserPrivilegedInGroup: null, }; this.onAction = this.onAction.bind(this); @@ -58,7 +58,7 @@ export default class HeaderButtons extends React.Component { phase: null, }); } else { - if (!this.state.phase) { + if (this.props.collapsedRhs) { dis.dispatch({ action: 'show_right_panel', }); diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index e92d1499aa..8aea31f9ed 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -424,7 +424,7 @@ module.exports = React.createClass({ { saveButton } { cancelButton } { rightRow } - +
    ); From f744374d1d5a7c40458a4cca078f50ac0c3d3df1 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 17 Dec 2018 15:56:17 +0100 Subject: [PATCH 134/237] read collapsedRhs from props when mounting main split --- src/components/structures/MainSplit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/MainSplit.js b/src/components/structures/MainSplit.js index 6fd0274f1a..5c69ef6745 100644 --- a/src/components/structures/MainSplit.js +++ b/src/components/structures/MainSplit.js @@ -55,7 +55,7 @@ export default class MainSplit extends React.Component { } componentDidMount() { - if (this.props.panel && !this.collapsedRhs) { + if (this.props.panel && !this.props.collapsedRhs) { this._createResizer(); } } From b7c353d0a6caa096e290f714e27566c6b7df3477 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 17 Dec 2018 15:56:35 +0100 Subject: [PATCH 135/237] persist and load collapsed rhs globally --- src/components/structures/MatrixChat.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index ebc56dc438..187caa69df 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -161,7 +161,7 @@ export default React.createClass({ viewUserId: null, collapseLhs: false, - collapsedRhs: false, + collapsedRhs: window.localStorage.getItem("mx_rhs_collapsed") === "true", leftDisabled: false, middleDisabled: false, rightDisabled: false, @@ -656,11 +656,13 @@ export default React.createClass({ }); break; case 'hide_right_panel': + window.localStorage.setItem("mx_rhs_collapsed", true); this.setState({ collapsedRhs: true, }); break; case 'show_right_panel': + window.localStorage.setItem("mx_rhs_collapsed", false); this.setState({ collapsedRhs: false, }); From d304c35b38e49a3a6b40c5d1ece74c650b51b08f Mon Sep 17 00:00:00 2001 From: Willem Mulder Date: Fri, 14 Dec 2018 14:26:35 +0100 Subject: [PATCH 136/237] Allow widgets to autoplay media This is useful for e.g. webcam streams in widgets. Signed-off-by: Willem Mulder --- src/components/views/elements/AppTile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 23b24adbb4..e36561cc15 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -544,7 +544,7 @@ export default class AppTile extends React.Component { // Additional iframe feature pemissions // (see - https://sites.google.com/a/chromium.org/dev/Home/chromium-security/deprecating-permissions-in-cross-origin-iframes and https://wicg.github.io/feature-policy/) - const iframeFeatures = "microphone; camera; encrypted-media;"; + const iframeFeatures = "microphone; camera; encrypted-media; autoplay;"; const appTileBodyClass = 'mx_AppTileBody' + (this.props.miniMode ? '_mini ' : ' '); From 1c4621c98e7b09b14bc7da6aa0cf4fc4e075f2d5 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 18 Dec 2018 00:26:25 +0000 Subject: [PATCH 137/237] Link to CONTRIBUTING from JS SDK The JS SDK's CONTRIBUTING file is a bit simpler to read. The Synapse version previously used includes mentions of Python lint tools that don't apply here. Signed-off-by: J. Ryan Stinnett --- CONTRIBUTING.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 99025f0e0a..f7c8c8b1c5 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -1,4 +1,4 @@ Contributing code to The React SDK ================================== -matrix-react-sdk follows the same pattern as https://github.com/matrix-org/synapse/blob/master/CONTRIBUTING.rst +matrix-react-sdk follows the same pattern as https://github.com/matrix-org/matrix-js-sdk/blob/master/CONTRIBUTING.rst From c6da61f1de8bd992715a82184eb45cbe80399104 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 17 Dec 2018 18:47:33 -0700 Subject: [PATCH 138/237] Make sure to grab the InlineSpinner object --- src/components/structures/GroupView.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index e2fd15aa89..56e6575793 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -1077,6 +1077,7 @@ export default React.createClass({ }, _getJoinableNode: function() { + const InlineSpinner = sdk.getComponent('elements.InlineSpinner'); return this.state.editing ?

    { _t('Who can join this community?') } From f6727c5724bdadfcfa2d8449282f2c52264ffa85 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 18 Dec 2018 10:34:26 +0100 Subject: [PATCH 139/237] add collapsedRhs to propTypes --- src/components/structures/LoggedInView.js | 2 +- src/components/views/right_panel/HeaderButtons.js | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index c36d3fe651..0433ce25b3 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -62,7 +62,7 @@ const LoggedInView = React.createClass({ // Called with the credentials of a registered user (if they were a ROU that // transitioned to PWLU) onRegistered: PropTypes.func, - + collapsedRhs: PropTypes.bool, teamToken: PropTypes.string, // Used by the RoomView to handle joining rooms diff --git a/src/components/views/right_panel/HeaderButtons.js b/src/components/views/right_panel/HeaderButtons.js index e0448c0ae2..4ba0148652 100644 --- a/src/components/views/right_panel/HeaderButtons.js +++ b/src/components/views/right_panel/HeaderButtons.js @@ -18,6 +18,7 @@ limitations under the License. */ import React from 'react'; +import PropTypes from 'prop-types'; import dis from '../../../dispatcher'; export default class HeaderButtons extends React.Component { @@ -77,3 +78,7 @@ export default class HeaderButtons extends React.Component {

    ; } } + +HeaderButtons.propTypes = { + collapsedRhs: PropTypes.bool, +}; From b359a2edeef727ccc513ffdaf0058c6cae53d0ad Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 18 Dec 2018 10:56:00 +0100 Subject: [PATCH 140/237] call header clicked callback after rerendering, so resizer has DOM nodes --- src/components/structures/RoomSubList.js | 5 +++-- src/components/views/rooms/RoomList.js | 16 ++++++++++++++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/components/structures/RoomSubList.js b/src/components/structures/RoomSubList.js index b4fbc5406e..a4f97e0efd 100644 --- a/src/components/structures/RoomSubList.js +++ b/src/components/structures/RoomSubList.js @@ -110,8 +110,9 @@ const RoomSubList = React.createClass({ if (this.isCollapsableOnClick()) { // The header isCollapsable, so the click is to be interpreted as collapse and truncation logic const isHidden = !this.state.hidden; - this.setState({hidden: isHidden}); - this.props.onHeaderClick(isHidden); + this.setState({hidden: isHidden}, () => { + this.props.onHeaderClick(isHidden); + }); } else { // The header is stuck, so the click is to be interpreted as a scroll to the header this.props.onHeaderClick(this.state.hidden, this.refs.header.dataset.originalPosition); diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 9fb872cd32..ce935ddf32 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -485,9 +485,21 @@ module.exports = React.createClass({ (filter[0] === '#' && room.getAliases().some((alias) => alias.toLowerCase().startsWith(lcFilter)))); }, - _persistCollapsedState: function(key, collapsed) { + _handleCollapsedState: function(key, collapsed) { + // persist collapsed state this.collapsedState[key] = collapsed; window.localStorage.setItem("mx_roomlist_collapsed", JSON.stringify(this.collapsedState)); + // load the persisted size configuration of the expanded sub list + if (!collapsed) { + const size = this.subListSizes[key]; + const handle = this.resizer.forHandleWithId(key); + if (handle) { + handle.resize(size); + } + } + // check overflow, as sub lists sizes have changed + // important this happens after calling resize above + Object.values(this._subListRefs).forEach(l => l.checkOverflow()); }, _subListRef: function(key, ref) { @@ -520,7 +532,7 @@ module.exports = React.createClass({ const {key, label, onHeaderClick, ... otherProps} = props; const chosenKey = key || label; const onSubListHeaderClick = (collapsed) => { - this._persistCollapsedState(chosenKey, collapsed); + this._handleCollapsedState(chosenKey, collapsed); if (onHeaderClick) { onHeaderClick(collapsed); } From 2b14f2af5c3f54b61a743968c696d2ffc989ef73 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 13 Dec 2018 12:10:08 +0000 Subject: [PATCH 141/237] Clean up when new key backup version fails to backup If creating a new key backup version succeeds but backing up to it fails, delete the version to avoid surprises. In addition, this converts the creation of a new key backup to async / await style. Signed-off-by: J. Ryan Stinnett --- .../keybackup/CreateKeyBackupDialog.js | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js index 6b115b890f..0db9d0699b 100644 --- a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js +++ b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js @@ -92,25 +92,33 @@ export default React.createClass({ }); }, - _createBackup: function() { + _createBackup: async function() { this.setState({ phase: PHASE_BACKINGUP, error: null, }); - this._createBackupPromise = MatrixClientPeg.get().createKeyBackupVersion( - this._keyBackupInfo, - ).then((info) => { - return MatrixClientPeg.get().backupAllGroupSessions(info.version); - }).then(() => { + let info; + try { + info = await MatrixClientPeg.get().createKeyBackupVersion( + this._keyBackupInfo, + ); + await MatrixClientPeg.get().backupAllGroupSessions(info.version); this.setState({ phase: PHASE_DONE, }); - }).catch(e => { + } catch (e) { console.log("Error creating key backup", e); + // TODO: If creating a version succeeds, but backup fails, should we + // delete the version, disable backup, or do nothing? If we just + // disable without deleting, we'll enable on next app reload since + // it is trusted. + if (info) { + MatrixClientPeg.get().deleteKeyBackupVersion(info.version); + } this.setState({ error: e, }); - }); + } }, _onCancel: function() { From acc2e98355fcde7b1cfee2b7e49bea088c02af5d Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 13 Dec 2018 15:55:48 +0000 Subject: [PATCH 142/237] Add New Recovery Method dialog Adds a New Recovery Method dialog which is shown when key backup fails because of a version mismatch / version not found error. The set up button in the dialog currently only marks a device as verified (via a verification prompt) instead of the eventual restore and cross-sign flow, since those pieces don't exist yet. Signed-off-by: J. Ryan Stinnett --- res/css/_common.scss | 2 +- res/css/_components.scss | 1 + .../keybackup/_NewRecoveryMethodDialog.scss | 41 +++++++ res/img/e2e/lock-warning.svg | 1 + .../keybackup/NewRecoveryMethodDialog.js | 110 ++++++++++++++++++ src/components/structures/MatrixChat.js | 5 + src/components/views/dialogs/BaseDialog.js | 3 +- .../views/settings/KeyBackupPanel.js | 5 +- src/i18n/strings/en_EN.json | 8 +- 9 files changed, 170 insertions(+), 6 deletions(-) create mode 100644 res/css/views/dialogs/keybackup/_NewRecoveryMethodDialog.scss create mode 100644 res/img/e2e/lock-warning.svg create mode 100644 src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js diff --git a/res/css/_common.scss b/res/css/_common.scss index 11e04f5dc0..97ae5412e1 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -34,7 +34,7 @@ body { -webkit-font-smoothing: subpixel-antialiased; } -div.error, div.warning { +.error, .warning { color: $warning-color; } diff --git a/res/css/_components.scss b/res/css/_components.scss index 579856f880..48aa211fd8 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -47,6 +47,7 @@ @import "./views/dialogs/_ShareDialog.scss"; @import "./views/dialogs/_UnknownDeviceDialog.scss"; @import "./views/dialogs/keybackup/_CreateKeyBackupDialog.scss"; +@import "./views/dialogs/keybackup/_NewRecoveryMethodDialog.scss"; @import "./views/dialogs/keybackup/_RestoreKeyBackupDialog.scss"; @import "./views/directory/_NetworkDropdown.scss"; @import "./views/elements/_AccessibleButton.scss"; diff --git a/res/css/views/dialogs/keybackup/_NewRecoveryMethodDialog.scss b/res/css/views/dialogs/keybackup/_NewRecoveryMethodDialog.scss new file mode 100644 index 0000000000..370f82d9ab --- /dev/null +++ b/res/css/views/dialogs/keybackup/_NewRecoveryMethodDialog.scss @@ -0,0 +1,41 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_NewRecoveryMethodDialog .mx_Dialog_title { + margin-bottom: 32px; +} + +.mx_NewRecoveryMethodDialog_title { + position: relative; + padding-left: 45px; + padding-bottom: 10px; + + &:before { + mask: url("../../../img/e2e/lock-warning.svg"); + mask-repeat: no-repeat; + background-color: $primary-fg-color; + content: ""; + position: absolute; + top: -6px; + right: 0; + bottom: 0; + left: 0; + } +} + +.mx_NewRecoveryMethodDialog .mx_Dialog_buttons { + margin-top: 36px; +} diff --git a/res/img/e2e/lock-warning.svg b/res/img/e2e/lock-warning.svg new file mode 100644 index 0000000000..a984ed85a0 --- /dev/null +++ b/res/img/e2e/lock-warning.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js b/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js new file mode 100644 index 0000000000..e88e0444bc --- /dev/null +++ b/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js @@ -0,0 +1,110 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from "react"; +import PropTypes from "prop-types"; +import sdk from "../../../../index"; +import MatrixClientPeg from '../../../../MatrixClientPeg'; +import dis from "../../../../dispatcher"; +import { _t } from "../../../../languageHandler"; +import Modal from "../../../../Modal"; + +export default class NewRecoveryMethodDialog extends React.PureComponent { + static propTypes = { + onFinished: PropTypes.func.isRequired, + } + + onGoToSettingsClick = () => { + this.props.onFinished(); + dis.dispatch({ action: 'view_user_settings' }); + } + + onSetupClick = async() => { + // TODO: Should change to a restore key backup flow that checks the + // recovery passphrase while at the same time also cross-signing the + // device as well in a single flow. Since we don't have that yet, we'll + // look for an unverified device and verify it. Note that this means + // we won't restore keys yet; instead we'll only trust the backup for + // sending our own new keys to it. + let backupSigStatus; + try { + const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion(); + backupSigStatus = await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo); + } catch (e) { + console.log("Unable to fetch key backup status", e); + return; + } + + let unverifiedDevice; + for (const sig of backupSigStatus.sigs) { + if (!sig.device.isVerified()) { + unverifiedDevice = sig.device; + break; + } + } + if (!unverifiedDevice) { + console.log("Unable to find a device to verify."); + return; + } + + const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog'); + Modal.createTrackedDialog('Device Verify Dialog', '', DeviceVerifyDialog, { + userId: MatrixClientPeg.get().credentials.userId, + device: unverifiedDevice, + onFinished: this.props.onFinished, + }); + } + + render() { + const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog"); + const DialogButtons = sdk.getComponent("views.elements.DialogButtons"); + const title = + {_t("New Recovery Method")} + ; + + return ( + +
    +

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

    +

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

    +

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

    + +
    +
    + ); + } +} diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 4517304453..fd95276445 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1430,6 +1430,11 @@ export default React.createClass({ break; } }); + cli.on("crypto.keyBackupFailed", () => { + Modal.createTrackedDialogAsync('New Recovery Method', 'New Recovery Method', + import('../../async-components/views/dialogs/keybackup/NewRecoveryMethodDialog'), + ); + }); // Fire the tinter right on startup to ensure the default theme is applied // A later sync can/will correct the tint to be the right value for the user diff --git a/src/components/views/dialogs/BaseDialog.js b/src/components/views/dialogs/BaseDialog.js index 8ec417a59b..3e9052cc34 100644 --- a/src/components/views/dialogs/BaseDialog.js +++ b/src/components/views/dialogs/BaseDialog.js @@ -57,8 +57,7 @@ export default React.createClass({ className: PropTypes.string, // Title for the dialog. - // (could probably actually be something more complicated than a string if desired) - title: PropTypes.string.isRequired, + title: PropTypes.node.isRequired, // children should be the content of the dialog children: PropTypes.node, diff --git a/src/components/views/settings/KeyBackupPanel.js b/src/components/views/settings/KeyBackupPanel.js index b08f4d0e78..03b98d28a0 100644 --- a/src/components/views/settings/KeyBackupPanel.js +++ b/src/components/views/settings/KeyBackupPanel.js @@ -154,6 +154,7 @@ export default class KeyBackupPanel extends React.Component { } let backupSigStatuses = this.state.backupSigStatus.sigs.map((sig, i) => { + const deviceName = sig.device.getDisplayName() || sig.device.deviceId; const sigStatusSubstitutions = { validity: sub => @@ -163,7 +164,7 @@ export default class KeyBackupPanel extends React.Component { {sub} , - device: sub => {sig.device.getDisplayName()}, + device: sub => {deviceName}, }; let sigStatus; if (sig.device.getFingerprint() === MatrixClientPeg.get().getDeviceEd25519Key()) { @@ -174,7 +175,7 @@ export default class KeyBackupPanel extends React.Component { } else if (sig.valid && sig.device.isVerified()) { sigStatus = _t( "Backup has a valid signature from " + - "verified device x", + "verified device ", {}, sigStatusSubstitutions, ); } else if (sig.valid && !sig.device.isVerified()) { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 00f781ea5b..b5a5762cc4 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -351,7 +351,7 @@ "This device is uploading keys to this backup": "This device is uploading keys to this backup", "This device is not uploading keys to this backup": "This device is not uploading keys to this backup", "Backup has a valid signature from this device": "Backup has a valid signature from this device", - "Backup has a valid signature from verified device x": "Backup has a valid signature from verified device x", + "Backup has a valid signature from verified device ": "Backup has a valid signature from verified device ", "Backup has a valid signature from unverified device ": "Backup has a valid signature from unverified device ", "Backup has an invalid signature from verified device ": "Backup has an invalid signature from verified device ", "Backup has an invalid signature from unverified device ": "Backup has an invalid signature from unverified device ", @@ -1401,6 +1401,12 @@ "Retry": "Retry", "Without setting up Secure Message Recovery, you'll lose your secure message history when you log out.": "Without setting up Secure Message Recovery, you'll lose your secure message history when you log out.", "If you don't want to set this up now, you can later in Settings.": "If you don't want to set this up now, you can later in Settings.", + "New Recovery Method": "New Recovery Method", + "A new recovery passphrase and key for Secure Messages has been detected.": "A new recovery passphrase and key for Secure Messages has been detected.", + "Setting up Secure Messages on this device will re-encrypt this device's message history with the new recovery method.": "Setting up Secure Messages on this device will re-encrypt this device's message history with the new recovery method.", + "If you didn't set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "If you didn't set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.", + "Set up Secure Messages": "Set up Secure Messages", + "Go to Settings": "Go to Settings", "Failed to set direct chat tag": "Failed to set direct chat tag", "Failed to remove tag %(tagName)s from room": "Failed to remove tag %(tagName)s from room", "Failed to add tag %(tagName)s to room": "Failed to add tag %(tagName)s to room" From cdcb3c1a55dfc1f2affdbe20a5cd8001a6873e03 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 18 Dec 2018 14:26:33 +0100 Subject: [PATCH 143/237] check overflow and restore sizes in more places inside RoomList: check overflow on mount restore size on query change (in case a sublist appeared) check overflow when updating rooms avoid duplicating for restoring size and checking overflow --- src/components/views/rooms/RoomList.js | 41 +++++++++++++++++++------- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index ce935ddf32..8a5c24f2e0 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -152,6 +152,8 @@ module.exports = React.createClass({ } this.subListSizes[id] = newSize; window.localStorage.setItem("mx_roomlist_sizes", JSON.stringify(this.subListSizes)); + // update overflow indicators + this._checkSubListsOverflow(); }, componentDidMount: function() { @@ -167,12 +169,10 @@ module.exports = React.createClass({ }); // load stored sizes - Object.entries(this.subListSizes).forEach(([id, size]) => { - const handle = this.resizer.forHandleWithId(id); - if (handle) { - handle.resize(size); - } + Object.keys(this.subListSizes).forEach((key) => { + this._restoreSubListSize(key); }); + this._checkSubListsOverflow(); this.resizer.attach(); this.mounted = true; @@ -181,7 +181,11 @@ module.exports = React.createClass({ componentDidUpdate: function(prevProps) { this._repositionIncomingCallBox(undefined, false); if (this.props.searchFilter !== prevProps.searchFilter) { - Object.values(this._subListRefs).forEach(l => l.checkOverflow()); + // restore sizes + Object.keys(this.subListSizes).forEach((key) => { + this._restoreSubListSize(key); + }); + this._checkSubListsOverflow(); } }, @@ -354,6 +358,12 @@ module.exports = React.createClass({ // Do this here so as to not render every time the selected tags // themselves change. selectedTags: TagOrderStore.getSelectedTags(), + }, () => { + // we don't need to restore any size here, do we? + // i guess we could have triggered a new group to appear + // that already an explicit size the last time it appeared ... + // + this._checkSubListsOverflow(); }); // this._lastRefreshRoomListTs = Date.now(); @@ -491,14 +501,23 @@ module.exports = React.createClass({ window.localStorage.setItem("mx_roomlist_collapsed", JSON.stringify(this.collapsedState)); // load the persisted size configuration of the expanded sub list if (!collapsed) { - const size = this.subListSizes[key]; - const handle = this.resizer.forHandleWithId(key); - if (handle) { - handle.resize(size); - } + this._restoreSubListSize(key); } // check overflow, as sub lists sizes have changed // important this happens after calling resize above + this._checkSubListsOverflow(); + }, + + _restoreSubListSize(key) { + const size = this.subListSizes[key]; + const handle = this.resizer.forHandleWithId(key); + if (handle) { + handle.resize(size); + } + }, + + // check overflow for scroll indicator gradient + _checkSubListsOverflow() { Object.values(this._subListRefs).forEach(l => l.checkOverflow()); }, From 3ddc8baed138ab23c180b1ec31a8ee91cc0f2188 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 18 Dec 2018 14:27:10 +0100 Subject: [PATCH 144/237] fix resizing sometimes not working (and selecting text) Last friday a child
    was added inside the ResizeHandle component, which made the parentElement/classList checks fail on the event.target here. This would only fail (and select all the text) when dragging exactly on the grey line (the div), not the transparent margin around it. use closest to make sure we have the root element of the handle. --- src/resizer/resizer.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/resizer/resizer.js b/src/resizer/resizer.js index 7ef542a6e1..0e113b3664 100644 --- a/src/resizer/resizer.js +++ b/src/resizer/resizer.js @@ -84,8 +84,10 @@ export class Resizer { } _onMouseDown(event) { - const target = event.target; - if (!this._isResizeHandle(target) || target.parentElement !== this.container) { + // use closest in case the resize handle contains + // child dom nodes that can be the target + const resizeHandle = event.target && event.target.closest(`.${this.classNames.handle}`); + if (!resizeHandle || resizeHandle.parentElement !== this.container) { return; } // prevent starting a drag operation @@ -96,7 +98,7 @@ export class Resizer { this.container.classList.add(this.classNames.resizing); } - const {sizer, distributor} = this._createSizerAndDistributor(target); + const {sizer, distributor} = this._createSizerAndDistributor(resizeHandle); const onMouseMove = (event) => { const offset = sizer.offsetFromEvent(event); From e67d9c6d4f04bc518f18af413bc72252c4b2b3f2 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 18 Dec 2018 14:29:42 +0100 Subject: [PATCH 145/237] forward checkOverflow to AutoHideScrollbar, fix over/underflow detection the overflow/underflow events are not always reliable in nooverlay browsers (FF), so forward the checkOverflow call we need anyway for the scroll indicator gradients to see if we need to do the margin trick for the on-hover scrollbar we use in nooverlay browsers. this fixes on hover jumping in a subroomlist --- .../structures/AutoHideScrollbar.js | 33 ++++++++++++----- .../structures/IndicatorScrollbar.js | 37 ++++++++++++------- 2 files changed, 48 insertions(+), 22 deletions(-) diff --git a/src/components/structures/AutoHideScrollbar.js b/src/components/structures/AutoHideScrollbar.js index a328d478bc..1098f96a76 100644 --- a/src/components/structures/AutoHideScrollbar.js +++ b/src/components/structures/AutoHideScrollbar.js @@ -69,6 +69,7 @@ export default class AutoHideScrollbar extends React.Component { this.onOverflow = this.onOverflow.bind(this); this.onUnderflow = this.onUnderflow.bind(this); this._collectContainerRef = this._collectContainerRef.bind(this); + this._needsOverflowListener = null; } onOverflow() { @@ -81,21 +82,32 @@ export default class AutoHideScrollbar extends React.Component { this.containerRef.classList.add("mx_AutoHideScrollbar_underflow"); } + checkOverflow() { + if (!this._needsOverflowListener) { + return; + } + if (this.containerRef.scrollHeight > this.containerRef.clientHeight) { + this.onOverflow(); + } else { + this.onUnderflow(); + } + } + + componentDidUpdate() { + this.checkOverflow(); + } + + componentDidMount() { + this.checkOverflow(); + } + _collectContainerRef(ref) { if (ref && !this.containerRef) { this.containerRef = ref; - const needsOverflowListener = - document.body.classList.contains("mx_scrollbar_nooverlay"); - - if (needsOverflowListener) { + if (this._needsOverflowListener) { this.containerRef.addEventListener("overflow", this.onOverflow); this.containerRef.addEventListener("underflow", this.onUnderflow); } - if (ref.scrollHeight > ref.clientHeight) { - this.onOverflow(); - } else { - this.onUnderflow(); - } } if (this.props.wrappedRef) { this.props.wrappedRef(ref); @@ -111,6 +123,9 @@ export default class AutoHideScrollbar extends React.Component { render() { installBodyClassesIfNeeded(); + if (this._needsOverflowListener === null) { + this._needsOverflowListener = document.body.classList.contains("mx_scrollbar_nooverlay"); + } return (
    0; - const hasBottomOverflow = this._scroller.scrollHeight > - (this._scroller.scrollTop + this._scroller.clientHeight); + const hasTopOverflow = this._scrollElement.scrollTop > 0; + const hasBottomOverflow = this._scrollElement.scrollHeight > + (this._scrollElement.scrollTop + this._scrollElement.clientHeight); if (hasTopOverflow) { - this._scroller.classList.add("mx_IndicatorScrollbar_topOverflow"); + this._scrollElement.classList.add("mx_IndicatorScrollbar_topOverflow"); } else { - this._scroller.classList.remove("mx_IndicatorScrollbar_topOverflow"); + this._scrollElement.classList.remove("mx_IndicatorScrollbar_topOverflow"); } if (hasBottomOverflow) { - this._scroller.classList.add("mx_IndicatorScrollbar_bottomOverflow"); + this._scrollElement.classList.add("mx_IndicatorScrollbar_bottomOverflow"); } else { - this._scroller.classList.remove("mx_IndicatorScrollbar_bottomOverflow"); + this._scrollElement.classList.remove("mx_IndicatorScrollbar_bottomOverflow"); + } + + if (this._autoHideScrollbar) { + this._autoHideScrollbar.checkOverflow(); } } componentWillUnmount() { - if (this._scroller) { - this._scroller.removeEventListener("scroll", this.checkOverflow); + if (this._scrollElement) { + this._scrollElement.removeEventListener("scroll", this.checkOverflow); } } render() { - return ( + return ( { this.props.children } ); } From 279521cab4ddfab78e57ced2ebffb02b12d2955c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 18 Dec 2018 14:31:38 +0100 Subject: [PATCH 146/237] add id to props for completeness --- src/components/views/elements/ResizeHandle.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/elements/ResizeHandle.js b/src/components/views/elements/ResizeHandle.js index 863dfaaa93..578689b45c 100644 --- a/src/components/views/elements/ResizeHandle.js +++ b/src/components/views/elements/ResizeHandle.js @@ -21,6 +21,7 @@ const ResizeHandle = (props) => { ResizeHandle.propTypes = { vertical: PropTypes.bool, reverse: PropTypes.bool, + id: PropTypes.string, }; export default ResizeHandle; From affe75fd3fab281326086af320dfe3b5fa731425 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 18 Dec 2018 14:31:59 +0100 Subject: [PATCH 147/237] make scroll indicator gradient smaller (40px->30px) --- res/css/structures/_RoomSubList.scss | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/res/css/structures/_RoomSubList.scss b/res/css/structures/_RoomSubList.scss index ce28c168b9..3ab4a22396 100644 --- a/res/css/structures/_RoomSubList.scss +++ b/res/css/structures/_RoomSubList.scss @@ -154,7 +154,7 @@ limitations under the License. position: sticky; left: 0; right: 0; - height: 40px; + height: 30px; content: ""; display: block; z-index: 100; @@ -162,10 +162,10 @@ limitations under the License. } &.mx_IndicatorScrollbar_topOverflow > .mx_AutoHideScrollbar_offset { - margin-top: -40px; + margin-top: -30px; } &.mx_IndicatorScrollbar_bottomOverflow > .mx_AutoHideScrollbar_offset { - margin-bottom: -40px; + margin-bottom: -30px; } &.mx_IndicatorScrollbar_topOverflow::before { From 12a339fe10c7e1e76917328e95a2429e649b2a28 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 18 Dec 2018 14:32:26 +0100 Subject: [PATCH 148/237] change subroomlist min height, as roomtiles are smaller now --- res/css/structures/_RoomSubList.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/structures/_RoomSubList.scss b/res/css/structures/_RoomSubList.scss index 3ab4a22396..2ce79bf5e1 100644 --- a/res/css/structures/_RoomSubList.scss +++ b/res/css/structures/_RoomSubList.scss @@ -39,7 +39,7 @@ limitations under the License. } .mx_RoomSubList_nonEmpty { - min-height: 76px; + min-height: 70px; .mx_AutoHideScrollbar_offset { padding-bottom: 4px; From f0a412e7214f8f8e51a31a7470ea2fc5849059f1 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 18 Dec 2018 14:32:46 +0100 Subject: [PATCH 149/237] fix docs --- res/css/structures/_RoomSubList.scss | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/res/css/structures/_RoomSubList.scss b/res/css/structures/_RoomSubList.scss index 2ce79bf5e1..3a0dd0395b 100644 --- a/res/css/structures/_RoomSubList.scss +++ b/res/css/structures/_RoomSubList.scss @@ -19,14 +19,14 @@ limitations under the License. each with a flex-shrink difference of 4 order of magnitude, so they ideally wouldn't affect each other. lowest category: .mx_RoomSubList - flex:-shrink: 10000000 + flex-shrink: 10000000 distribute size of items within the same categery by their size middle category: .mx_RoomSubList.resized-sized - flex:-shrink: 1000 + flex-shrink: 1000 applied when using the resizer, will have a max-height set to it, to limit the size highest category: .mx_RoomSubList.resized-all - flex:-shrink: 1 + flex-shrink: 1 small flex-shrink value (1), is only added if you can drag the resizer so far so in practice you can only assign this category if there is enough space. */ From 2cda6d8f35cc58a0d59d945b167c9c9b4da14d24 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 18 Dec 2018 15:06:56 +0100 Subject: [PATCH 150/237] fix empty comment line --- src/components/views/rooms/RoomList.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 8a5c24f2e0..7a06cc3da5 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -362,7 +362,6 @@ module.exports = React.createClass({ // we don't need to restore any size here, do we? // i guess we could have triggered a new group to appear // that already an explicit size the last time it appeared ... - // this._checkSubListsOverflow(); }); From 31c13adaba2da09ccb65d5bf6cb6ab1fe2c76b5a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 18 Dec 2018 15:10:57 +0100 Subject: [PATCH 151/237] cleanup: do initialization in componentDidMount instead of render --- src/components/structures/AutoHideScrollbar.js | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/components/structures/AutoHideScrollbar.js b/src/components/structures/AutoHideScrollbar.js index 1098f96a76..47ae24ba0f 100644 --- a/src/components/structures/AutoHideScrollbar.js +++ b/src/components/structures/AutoHideScrollbar.js @@ -98,16 +98,19 @@ export default class AutoHideScrollbar extends React.Component { } componentDidMount() { + installBodyClassesIfNeeded(); + this._needsOverflowListener = + document.body.classList.contains("mx_scrollbar_nooverlay"); + if (this._needsOverflowListener) { + this.containerRef.addEventListener("overflow", this.onOverflow); + this.containerRef.addEventListener("underflow", this.onUnderflow); + } this.checkOverflow(); } _collectContainerRef(ref) { if (ref && !this.containerRef) { this.containerRef = ref; - if (this._needsOverflowListener) { - this.containerRef.addEventListener("overflow", this.onOverflow); - this.containerRef.addEventListener("underflow", this.onUnderflow); - } } if (this.props.wrappedRef) { this.props.wrappedRef(ref); @@ -115,17 +118,13 @@ export default class AutoHideScrollbar extends React.Component { } componentWillUnmount() { - if (this.containerRef) { + if (this._needsOverflowListener && this.containerRef) { this.containerRef.removeEventListener("overflow", this.onOverflow); this.containerRef.removeEventListener("underflow", this.onUnderflow); } } render() { - installBodyClassesIfNeeded(); - if (this._needsOverflowListener === null) { - this._needsOverflowListener = document.body.classList.contains("mx_scrollbar_nooverlay"); - } return (
    Date: Tue, 18 Dec 2018 15:33:28 +0100 Subject: [PATCH 152/237] remove dead code (collapse button was removed) --- src/components/views/rooms/RoomHeader.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 8aea31f9ed..996519445a 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -146,10 +146,6 @@ module.exports = React.createClass({ MatrixClientPeg.get().sendStateEvent(this.props.room.roomId, 'm.room.avatar', {url: null}, ''); }, - onShowRhsClick: function(ev) { - dis.dispatch({ action: 'show_right_panel' }); - }, - onShareRoomClick: function(ev) { const ShareDialog = sdk.getComponent("dialogs.ShareDialog"); Modal.createTrackedDialog('share room dialog', '', ShareDialog, { From a10f0a32672c8d011ae8a4f36bfd3f15329b0eee Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 18 Dec 2018 15:33:45 +0100 Subject: [PATCH 153/237] don't open right panel when switching room again a view_right_panel_phase is dispatched by RoomHeaderButtons on view_room, which was triggering this to show the panel again. Check the fromHeader flag just like when hiding the panel so only room header buttons can hide or show the right panel --- src/components/views/right_panel/HeaderButtons.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/right_panel/HeaderButtons.js b/src/components/views/right_panel/HeaderButtons.js index 4ba0148652..18ec2bd941 100644 --- a/src/components/views/right_panel/HeaderButtons.js +++ b/src/components/views/right_panel/HeaderButtons.js @@ -59,7 +59,7 @@ export default class HeaderButtons extends React.Component { phase: null, }); } else { - if (this.props.collapsedRhs) { + if (this.props.collapsedRhs && payload.fromHeader) { dis.dispatch({ action: 'show_right_panel', }); From dafc54c434fbc24a395a9b2e75ca29f51d529c69 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 18 Dec 2018 15:36:54 +0100 Subject: [PATCH 154/237] don't highlight room header buttons when right panel is collapsed --- .../views/right_panel/GroupHeaderButtons.js | 12 ++++++------ src/components/views/right_panel/HeaderButtons.js | 11 +++++++++++ .../views/right_panel/RoomHeaderButtons.js | 10 +++++----- 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/src/components/views/right_panel/GroupHeaderButtons.js b/src/components/views/right_panel/GroupHeaderButtons.js index af54787b2c..6fcba1d815 100644 --- a/src/components/views/right_panel/GroupHeaderButtons.js +++ b/src/components/views/right_panel/GroupHeaderButtons.js @@ -55,23 +55,23 @@ export default class GroupHeaderButtons extends HeaderButtons { } renderButtons() { - const isPhaseGroup = [ + const groupPhases = [ RightPanel.Phase.GroupMemberInfo, RightPanel.Phase.GroupMemberList, - ].includes(this.state.phase); - const isPhaseRoom = [ + ]; + const roomPhases = [ RightPanel.Phase.GroupRoomList, RightPanel.Phase.GroupRoomInfo, - ].includes(this.state.phase); + ]; return [ , , diff --git a/src/components/views/right_panel/HeaderButtons.js b/src/components/views/right_panel/HeaderButtons.js index 18ec2bd941..3e3bbd3cd2 100644 --- a/src/components/views/right_panel/HeaderButtons.js +++ b/src/components/views/right_panel/HeaderButtons.js @@ -48,6 +48,17 @@ export default class HeaderButtons extends React.Component { }, extras)); } + isPhase(phases) { + if (this.props.collapsedRhs) { + return false; + } + if (Array.isArray(phases)) { + return phases.includes(this.state.phase); + } else { + return phases === this.state.phase; + } + } + onAction(payload) { if (payload.action === "view_right_panel_phase") { // only actions coming from header buttons should collapse the right panel diff --git a/src/components/views/right_panel/RoomHeaderButtons.js b/src/components/views/right_panel/RoomHeaderButtons.js index ba06bd9953..ff97fc5f4f 100644 --- a/src/components/views/right_panel/RoomHeaderButtons.js +++ b/src/components/views/right_panel/RoomHeaderButtons.js @@ -46,24 +46,24 @@ export default class RoomHeaderButtons extends HeaderButtons { } renderButtons() { - const isMembersPhase = [ + const membersPhases = [ RightPanel.Phase.RoomMemberList, RightPanel.Phase.RoomMemberInfo, - ].includes(this.state.phase); + ]; return [ , , , From 92c598dbcf4f287bc290e5a758b42c019391b055 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 18 Dec 2018 15:38:15 +0100 Subject: [PATCH 155/237] remove group header expand right panel button --- src/components/structures/GroupView.js | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index d77837ad63..478126db75 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -1272,15 +1272,6 @@ export default React.createClass({ , ); - if (this.props.collapsedRhs) { - rightButtons.push( - - - , - ); - } } const rightPanel = !this.props.collapsedRhs ? : undefined; From c0ab74d5f1cf32b96e7fc50f64c4c3df2d5968a3 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 18 Dec 2018 16:27:17 +0100 Subject: [PATCH 156/237] reemit view_right_panel_phase when showing rhs, so right tab is active --- src/components/views/right_panel/HeaderButtons.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/views/right_panel/HeaderButtons.js b/src/components/views/right_panel/HeaderButtons.js index 3e3bbd3cd2..f0479eb8be 100644 --- a/src/components/views/right_panel/HeaderButtons.js +++ b/src/components/views/right_panel/HeaderButtons.js @@ -74,6 +74,11 @@ export default class HeaderButtons extends React.Component { dis.dispatch({ action: 'show_right_panel', }); + // emit payload again as the RightPanel didn't exist up + // till show_right_panel, just without the fromHeader flag + // as that would hide the right panel again + dis.dispatch(Object.assign({}, payload, {fromHeader: false})); + } this.setState({ phase: payload.phase, From 4b788fcb7e823b92feda4b6da94d7e29e17e8f8a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 18 Dec 2018 16:29:31 +0100 Subject: [PATCH 157/237] fix lint --- src/components/views/rooms/RoomHeader.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 996519445a..c7695cc4f7 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -23,7 +23,6 @@ import sdk from '../../../index'; import { _t } from '../../../languageHandler'; import MatrixClientPeg from '../../../MatrixClientPeg'; import Modal from "../../../Modal"; -import dis from "../../../dispatcher"; import RateLimitedFunc from '../../../ratelimitedfunc'; import * as linkify from 'linkifyjs'; From dd6dd7a4fc6e19ff2d146477c899c4e524483347 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 18 Dec 2018 16:35:07 +0100 Subject: [PATCH 158/237] select search query on focus --- src/components/structures/SearchBox.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/structures/SearchBox.js b/src/components/structures/SearchBox.js index 4df3e837c7..ea1fa312c1 100644 --- a/src/components/structures/SearchBox.js +++ b/src/components/structures/SearchBox.js @@ -56,7 +56,6 @@ module.exports = React.createClass({ case 'focus_room_filter': if (this.refs.search) { this.refs.search.focus(); - this.refs.search.select(); } break; } @@ -83,6 +82,10 @@ module.exports = React.createClass({ } }, + _onFocus: function(ev) { + ev.target.select(); + }, + _clearSearch: function(source) { this.refs.search.value = ""; this.onChange(); @@ -108,6 +111,7 @@ module.exports = React.createClass({ ref="search" className="mx_textinput_icon mx_textinput_search" value={ this.state.searchTerm } + onFocus={ this._onFocus } onChange={ this.onChange } onKeyDown={ this._onKeyDown } placeholder={ _t('Filter room names') } From c917b4038b14f25eb951c0c8284f065678243662 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 18 Dec 2018 17:02:48 +0100 Subject: [PATCH 159/237] scope default input style rules to MatrixChat --- res/themes/dharma/css/_dharma.scss | 47 +++++++++++++++++------------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/res/themes/dharma/css/_dharma.scss b/res/themes/dharma/css/_dharma.scss index f1badb35ca..bce736e91e 100644 --- a/res/themes/dharma/css/_dharma.scss +++ b/res/themes/dharma/css/_dharma.scss @@ -192,32 +192,37 @@ $progressbar-color: #000; // it has the appearance of a text box so the controls // appear to be part of the input -:not(.mx_textinput) > input[type=text], -:not(.mx_textinput) > input[type=search], -.mx_textinput { - display: block; - margin: 9px; - box-sizing: border-box; - background-color: transparent; - color: $input-darker-fg-color; - border-radius: 4px; - border: 1px solid #c1c1c1; -} +.mx_MatrixChat { -.mx_textinput { - display: flex; - align-items: center; -} + :not(.mx_textinput) > input[type=text], + :not(.mx_textinput) > input[type=search], + .mx_textinput { + display: block; + margin: 9px; + box-sizing: border-box; + background-color: transparent; + color: $input-darker-fg-color; + border-radius: 4px; + border: 1px solid #c1c1c1; + flex: 0 0 auto; + } -.mx_textinput > input[type=text], -.mx_textinput > input[type=search] { - border: none; - flex: 1; - color: inherit; //from .mx_textinput + .mx_textinput { + display: flex; + align-items: center; + + > input[type=text], + > input[type=search] { + border: none; + flex: 1; + color: inherit; //from .mx_textinput + } + } } input[type=text], -input[type=search] { +input[type=search], +input[type=password] { padding: 9px; font-family: $font-family; font-size: 14px; From fd5ad568866e9ee30d3ecfb222d9bbd5791f631c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 18 Dec 2018 17:12:32 +0100 Subject: [PATCH 160/237] give right panel default width of 350px --- src/components/structures/MainSplit.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/structures/MainSplit.js b/src/components/structures/MainSplit.js index 5c69ef6745..0427130eea 100644 --- a/src/components/structures/MainSplit.js +++ b/src/components/structures/MainSplit.js @@ -41,10 +41,13 @@ export default class MainSplit extends React.Component { {onResized: this._onResized}, ); resizer.setClassNames(classNames); - const rhsSize = window.localStorage.getItem("mx_rhs_size"); + let rhsSize = window.localStorage.getItem("mx_rhs_size"); if (rhsSize !== null) { - resizer.forHandleAt(0).resize(parseInt(rhsSize, 10)); + rhsSize = parseInt(rhsSize, 10); + } else { + rhsSize = 350; } + resizer.forHandleAt(0).resize(rhsSize); resizer.attach(); this.resizer = resizer; From 074c96cd3eae0543e44b058ddabb85842f7ec4ba Mon Sep 17 00:00:00 2001 From: Nad Chishtie Date: Tue, 18 Dec 2018 16:48:20 +0000 Subject: [PATCH 161/237] First pass of normalising icons. --- res/css/structures/_RightPanel.scss | 5 +++-- res/css/structures/_TagPanel.scss | 4 ++-- res/css/views/rooms/_MemberList.scss | 2 +- res/css/views/rooms/_RoomHeader.scss | 4 ++-- res/img/feather-icons/face.svg | 14 ++++++++++++++ res/img/feather-icons/files.svg | 11 +++++++++++ res/img/feather-icons/grid.svg | 13 +++++++++++++ res/img/feather-icons/notifications.svg | 10 ++++++++++ res/img/feather-icons/paperclip.svg | 10 ++++++++++ res/img/feather-icons/phone.svg | 10 ++++++++++ res/img/feather-icons/search.svg | 11 +++++++++++ res/img/feather-icons/settings.svg | 11 +++++++++++ res/img/feather-icons/share.svg | 14 ++++++++++++++ res/img/feather-icons/user-add.svg | 13 +++++++++++++ res/img/feather-icons/user.svg | 11 +++++++++++ res/img/feather-icons/users.svg | 15 +++++++++++++++ res/img/feather-icons/video.svg | 11 +++++++++++ res/themes/dharma/css/_dharma.scss | 2 +- .../views/elements/ManageIntegsButton.js | 2 +- .../views/right_panel/RoomHeaderButtons.js | 6 +++--- src/components/views/rooms/MessageComposer.js | 6 +++--- src/components/views/rooms/RoomHeader.js | 6 +++--- src/components/views/rooms/Stickerpicker.js | 4 ++-- 23 files changed, 175 insertions(+), 20 deletions(-) create mode 100644 res/img/feather-icons/face.svg create mode 100644 res/img/feather-icons/files.svg create mode 100644 res/img/feather-icons/grid.svg create mode 100644 res/img/feather-icons/notifications.svg create mode 100644 res/img/feather-icons/paperclip.svg create mode 100644 res/img/feather-icons/phone.svg create mode 100644 res/img/feather-icons/search.svg create mode 100644 res/img/feather-icons/settings.svg create mode 100644 res/img/feather-icons/share.svg create mode 100644 res/img/feather-icons/user-add.svg create mode 100644 res/img/feather-icons/user.svg create mode 100644 res/img/feather-icons/users.svg create mode 100644 res/img/feather-icons/video.svg diff --git a/res/css/structures/_RightPanel.scss b/res/css/structures/_RightPanel.scss index 07fe404749..592eea067e 100644 --- a/res/css/structures/_RightPanel.scss +++ b/res/css/structures/_RightPanel.scss @@ -45,7 +45,8 @@ limitations under the License. cursor: pointer; flex: 0 0 auto; vertical-align: top; - padding-left: 4px; + margin-top: 4px; + padding-left: 5px; padding-right: 5px; text-align: center; position: relative; @@ -57,7 +58,7 @@ limitations under the License. } .mx_RightPanel_headerButton_highlight { - border-color: $accent-color; + border-color: $button-bg-color; } .mx_RightPanel_headerButton_badge { diff --git a/res/css/structures/_TagPanel.scss b/res/css/structures/_TagPanel.scss index 85e5c1742f..1ffb017e28 100644 --- a/res/css/structures/_TagPanel.scss +++ b/res/css/structures/_TagPanel.scss @@ -138,9 +138,9 @@ limitations under the License. &:before { background-color: $tagpanel-bg-color; - mask: url('../../img/icons-groups-nobg.svg'); + mask: url('../../img/feather-icons/users.svg'); mask-repeat: no-repeat; - mask-position: center 8px; + mask-position: center 10px; content: ''; position: absolute; top: 0; diff --git a/res/css/views/rooms/_MemberList.scss b/res/css/views/rooms/_MemberList.scss index 2695ebcf31..6f9491b22f 100644 --- a/res/css/views/rooms/_MemberList.scss +++ b/res/css/views/rooms/_MemberList.scss @@ -83,7 +83,7 @@ limitations under the License. .mx_MemberList_invite span { margin: 0 auto; - background-image: url('../../img/icon-invite-people.svg'); + background-image: url('../../img/feather-icons/user-add.svg'); background-repeat: no-repeat; background-position: center left; padding-left: 25px; diff --git a/res/css/views/rooms/_RoomHeader.scss b/res/css/views/rooms/_RoomHeader.scss index 7d2cbfe863..0697ccf40f 100644 --- a/res/css/views/rooms/_RoomHeader.scss +++ b/res/css/views/rooms/_RoomHeader.scss @@ -59,8 +59,8 @@ limitations under the License. .mx_RoomHeader_buttons { display: flex; align-items: center; - margin-top: 4px; background-color: $primary-bg-color; + padding-right: 5px; } .mx_RoomHeader_info { @@ -197,7 +197,7 @@ limitations under the License. } .mx_RoomHeader_button { - margin-left: 12px; + margin-left: 10px; cursor: pointer; } diff --git a/res/img/feather-icons/face.svg b/res/img/feather-icons/face.svg new file mode 100644 index 0000000000..0a359b2dea --- /dev/null +++ b/res/img/feather-icons/face.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/res/img/feather-icons/files.svg b/res/img/feather-icons/files.svg new file mode 100644 index 0000000000..c66d9ad121 --- /dev/null +++ b/res/img/feather-icons/files.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/res/img/feather-icons/grid.svg b/res/img/feather-icons/grid.svg new file mode 100644 index 0000000000..e6912b0cc7 --- /dev/null +++ b/res/img/feather-icons/grid.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/res/img/feather-icons/notifications.svg b/res/img/feather-icons/notifications.svg new file mode 100644 index 0000000000..2fe85e810c --- /dev/null +++ b/res/img/feather-icons/notifications.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/res/img/feather-icons/paperclip.svg b/res/img/feather-icons/paperclip.svg new file mode 100644 index 0000000000..ed2bb88681 --- /dev/null +++ b/res/img/feather-icons/paperclip.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/res/img/feather-icons/phone.svg b/res/img/feather-icons/phone.svg new file mode 100644 index 0000000000..58b257f113 --- /dev/null +++ b/res/img/feather-icons/phone.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/res/img/feather-icons/search.svg b/res/img/feather-icons/search.svg new file mode 100644 index 0000000000..8b14246f64 --- /dev/null +++ b/res/img/feather-icons/search.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/res/img/feather-icons/settings.svg b/res/img/feather-icons/settings.svg new file mode 100644 index 0000000000..ea7ce5c55b --- /dev/null +++ b/res/img/feather-icons/settings.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/res/img/feather-icons/share.svg b/res/img/feather-icons/share.svg new file mode 100644 index 0000000000..a012e1b7a5 --- /dev/null +++ b/res/img/feather-icons/share.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/res/img/feather-icons/user-add.svg b/res/img/feather-icons/user-add.svg new file mode 100644 index 0000000000..cbb25934c1 --- /dev/null +++ b/res/img/feather-icons/user-add.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/res/img/feather-icons/user.svg b/res/img/feather-icons/user.svg new file mode 100644 index 0000000000..a789e580d5 --- /dev/null +++ b/res/img/feather-icons/user.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/res/img/feather-icons/users.svg b/res/img/feather-icons/users.svg new file mode 100644 index 0000000000..b0deac0a9e --- /dev/null +++ b/res/img/feather-icons/users.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/res/img/feather-icons/video.svg b/res/img/feather-icons/video.svg new file mode 100644 index 0000000000..a4c456832f --- /dev/null +++ b/res/img/feather-icons/video.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/res/themes/dharma/css/_dharma.scss b/res/themes/dharma/css/_dharma.scss index f1badb35ca..ce1b7b2a68 100644 --- a/res/themes/dharma/css/_dharma.scss +++ b/res/themes/dharma/css/_dharma.scss @@ -259,7 +259,7 @@ input[type=search].mx_textinput_icon { input[type=text].mx_textinput_icon.mx_textinput_search, input[type=search].mx_textinput_icon.mx_textinput_search { - background-image: url('../../img/icons-search.svg'); + background-image: url('../../img/feather-icons/search.svg'); } // dont search UI as not all browsers support it, diff --git a/src/components/views/elements/ManageIntegsButton.js b/src/components/views/elements/ManageIntegsButton.js index 024c5feda5..f45053de44 100644 --- a/src/components/views/elements/ManageIntegsButton.js +++ b/src/components/views/elements/ManageIntegsButton.js @@ -91,7 +91,7 @@ export default class ManageIntegsButton extends React.Component { integrationsButton = ( - + { integrationsWarningTriangle } { integrationsErrorPopup } diff --git a/src/components/views/right_panel/RoomHeaderButtons.js b/src/components/views/right_panel/RoomHeaderButtons.js index ba06bd9953..cd4bb1c229 100644 --- a/src/components/views/right_panel/RoomHeaderButtons.js +++ b/src/components/views/right_panel/RoomHeaderButtons.js @@ -52,17 +52,17 @@ export default class RoomHeaderButtons extends HeaderButtons { ].includes(this.state.phase); return [ - , - , - - + ; videoCallButton = - + ; } @@ -320,7 +320,7 @@ export default class MessageComposer extends React.Component { const uploadButton = ( - + - + ; } @@ -382,7 +382,7 @@ module.exports = React.createClass({ if (this.props.onSearchClick && this.props.inRoom) { searchButton = - + ; } @@ -390,7 +390,7 @@ module.exports = React.createClass({ if (this.props.inRoom) { shareRoomButton = - + ; } diff --git a/src/components/views/rooms/Stickerpicker.js b/src/components/views/rooms/Stickerpicker.js index 085303eafb..c7d9f890a7 100644 --- a/src/components/views/rooms/Stickerpicker.js +++ b/src/components/views/rooms/Stickerpicker.js @@ -351,7 +351,7 @@ export default class Stickerpicker extends React.Component { onClick={this._onHideStickersClick} ref='target' title={_t("Hide Stickers")}> - + ; } else { // Show show-stickers button @@ -362,7 +362,7 @@ export default class Stickerpicker extends React.Component { className="mx_MessageComposer_stickers" onClick={this._onShowStickersClick} title={_t("Show Stickers")}> - + ; } return
    From 2ad9f45655a3fb281669ec9df6c197d26a4cefbe Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 18 Dec 2018 17:56:05 +0100 Subject: [PATCH 162/237] add badge with dot to rm button, so it catches your eye better --- res/css/views/rooms/_TopUnreadMessagesBar.scss | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/res/css/views/rooms/_TopUnreadMessagesBar.scss b/res/css/views/rooms/_TopUnreadMessagesBar.scss index c4ca035a2e..67579552c1 100644 --- a/res/css/views/rooms/_TopUnreadMessagesBar.scss +++ b/res/css/views/rooms/_TopUnreadMessagesBar.scss @@ -14,6 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +@charset "utf-8"; + .mx_TopUnreadMessagesBar { z-index: 1000; position: absolute; @@ -22,6 +24,22 @@ limitations under the License. width: 38px; } +.mx_TopUnreadMessagesBar:after { + content: "·"; + position: absolute; + top: -8px; + left: 11px; + width: 16px; + height: 16px; + border-radius: 16px; + font-weight: 600; + font-size: 30px; + line-height: 14px; + text-align: center; + color: $secondary-accent-color; + background-color: $accent-color; +} + .mx_TopUnreadMessagesBar_scrollUp { height: 38px; border-radius: 19px; From b036e59021d95163c1816346f4a1014ae7667927 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 18 Dec 2018 15:14:55 +0000 Subject: [PATCH 163/237] Enable ESLint rule to require defined components in JSX Signed-off-by: J. Ryan Stinnett --- .eslintrc.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.eslintrc.js b/.eslintrc.js index 62d24ea707..971809f851 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -47,6 +47,9 @@ module.exports = { }], "react/jsx-key": ["error"], + // Components in JSX should always be defined. + "react/jsx-no-undef": "error", + // Assert no spacing in JSX curly brackets // // From 37b3644fd91e49a8d39b779bd9c1ade4286ee450 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 18 Dec 2018 17:40:30 +0000 Subject: [PATCH 164/237] React-sdk changes to support sandboxed electron --- src/BasePlatform.js | 6 +-- src/components/structures/UserSettings.js | 45 ++++++++++++----------- src/components/views/elements/AppTile.js | 34 ----------------- 3 files changed, 25 insertions(+), 60 deletions(-) diff --git a/src/BasePlatform.js b/src/BasePlatform.js index abc9aa0bed..79f0d69e2c 100644 --- a/src/BasePlatform.js +++ b/src/BasePlatform.js @@ -3,6 +3,7 @@ /* Copyright 2016 Aviral Dasgupta Copyright 2016 OpenMarket Ltd +Copyright 2018 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -105,11 +106,6 @@ export default class BasePlatform { return "Not implemented"; } - isElectron(): boolean { return false; } - - setupScreenSharingForIframe() { - } - /** * Restarts the application, without neccessarily reloading * any application code diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 6f932d71e1..b9dbe345c5 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -188,9 +188,11 @@ module.exports = React.createClass({ phase: "UserSettings.LOADING", // LOADING, DISPLAY email_add_pending: false, vectorVersion: undefined, + canSelfUpdate: null, rejectingInvites: false, mediaDevices: null, ignoredUsers: [], + autoLaunchEnabled: null, }; }, @@ -209,6 +211,13 @@ module.exports = React.createClass({ }, (e) => { console.log("Failed to fetch app version", e); }); + + PlatformPeg.get().canSelfUpdate().then((canUpdate) => { + if (this._unmounted) return; + this.setState({ + canSelfUpdate: canUpdate, + }); + }); } this._refreshMediaDevices(); @@ -227,11 +236,12 @@ module.exports = React.createClass({ }); this._refreshFromServer(); - if (PlatformPeg.get().isElectron()) { - const {ipcRenderer} = require('electron'); - - ipcRenderer.on('settings', this._electronSettings); - ipcRenderer.send('settings_get'); + if (PlatformPeg.get().supportsAutoLaunch()) { + PlatformPeg.get().getAutoLaunchEnabled().then(enabled => { + this.setState({ + autoLaunchEnabled: enabled, + }); + }); } this.setState({ @@ -262,11 +272,6 @@ module.exports = React.createClass({ if (cli) { cli.removeListener("RoomMember.membership", this._onInviteStateChange); } - - if (PlatformPeg.get().isElectron()) { - const {ipcRenderer} = require('electron'); - ipcRenderer.removeListener('settings', this._electronSettings); - } }, // `UserSettings` assumes that the client peg will not be null, so give it some @@ -285,10 +290,6 @@ module.exports = React.createClass({ }); }, - _electronSettings: function(ev, settings) { - this.setState({ electron_settings: settings }); - }, - _refreshMediaDevices: function(stream) { if (stream) { // kill stream so that we don't leave it lingering around with webcam enabled etc @@ -967,7 +968,7 @@ module.exports = React.createClass({ _renderCheckUpdate: function() { const platform = PlatformPeg.get(); - if ('canSelfUpdate' in platform && platform.canSelfUpdate() && 'startUpdateCheck' in platform) { + if (this.state.canSelfUpdate) { return

    { _t('Updates') }

    @@ -1012,8 +1013,7 @@ module.exports = React.createClass({ }, _renderElectronSettings: function() { - const settings = this.state.electron_settings; - if (!settings) return; + if (!PlatformPeg.get().supportsAutoLaunch()) return; // TODO: This should probably be a granular setting, but it only applies to electron // and ends up being get/set outside of matrix anyways (local system setting). @@ -1023,7 +1023,7 @@ module.exports = React.createClass({
    @@ -1033,8 +1033,11 @@ module.exports = React.createClass({ }, _onAutoLaunchChanged: function(e) { - const {ipcRenderer} = require('electron'); - ipcRenderer.send('settings_set', 'auto-launch', e.target.checked); + PlatformPeg.get().setAutoLaunchEnabled(e.target.checked).then(() => { + this.setState({ + autoLaunchEnabled: e.target.checked, + }); + }); }, _mapWebRtcDevicesToSpans: function(devices) { @@ -1393,7 +1396,7 @@ module.exports = React.createClass({ { this._renderBulkOptions() } { this._renderBugReport() } - { PlatformPeg.get().isElectron() && this._renderElectronSettings() } + { this._renderElectronSettings() } { this._renderAnalyticsControl() } diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 23b24adbb4..7eae17ace8 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -49,7 +49,6 @@ export default class AppTile extends React.Component { this.state = this._getNewState(props); this._onAction = this._onAction.bind(this); - this._onMessage = this._onMessage.bind(this); this._onLoaded = this._onLoaded.bind(this); this._onEditClick = this._onEditClick.bind(this); this._onDeleteClick = this._onDeleteClick.bind(this); @@ -143,10 +142,6 @@ export default class AppTile extends React.Component { } componentDidMount() { - // Legacy Jitsi widget messaging -- TODO replace this with standard widget - // postMessaging API - window.addEventListener('message', this._onMessage, false); - // Widget action listeners this.dispatcherRef = dis.register(this._onAction); } @@ -155,9 +150,6 @@ export default class AppTile extends React.Component { // Widget action listeners dis.unregister(this.dispatcherRef); - // Jitsi listener - window.removeEventListener('message', this._onMessage); - // if it's not remaining on screen, get rid of the PersistedElement container if (!ActiveWidgetStore.getWidgetPersistence(this.props.id)) { ActiveWidgetStore.destroyPersistentWidget(); @@ -233,32 +225,6 @@ export default class AppTile extends React.Component { } } - // Legacy Jitsi widget messaging - // TODO -- This should be replaced with the new widget postMessaging API - _onMessage(event) { - if (this.props.type !== 'jitsi') { - return; - } - if (!event.origin) { - event.origin = event.originalEvent.origin; - } - - const widgetUrlObj = url.parse(this.state.widgetUrl); - const eventOrigin = url.parse(event.origin); - if ( - eventOrigin.protocol !== widgetUrlObj.protocol || - eventOrigin.host !== widgetUrlObj.host - ) { - return; - } - - if (event.data.widgetAction === 'jitsi_iframe_loaded') { - const iframe = this.refs.appFrame.contentWindow - .document.querySelector('iframe[id^="jitsiConferenceFrame"]'); - PlatformPeg.get().setupScreenSharingForIframe(iframe); - } - } - _canUserModify() { // User widgets should always be modifiable by their creator if (this.props.userWidget && MatrixClientPeg.get().credentials.userId === this.props.creatorUserId) { From ef60a34180c973511f4a436cd21faa98f297a845 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 18 Dec 2018 10:53:37 -0700 Subject: [PATCH 165/237] Clean up and follow code style --- res/css/views/rooms/_RoomTile.scss | 1 - .../avatars/MemberStatusMessageAvatar.js | 39 +++++++++---------- .../context_menus/StatusMessageContextMenu.js | 25 ++++++------ 3 files changed, 29 insertions(+), 36 deletions(-) diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss index 6a89636d15..b5ac9aadc6 100644 --- a/res/css/views/rooms/_RoomTile.scss +++ b/res/css/views/rooms/_RoomTile.scss @@ -35,7 +35,6 @@ limitations under the License. .mx_RoomTile_nameContainer { display: inline-block; width: 180px; - //height: 24px; vertical-align: middle; } diff --git a/src/components/views/avatars/MemberStatusMessageAvatar.js b/src/components/views/avatars/MemberStatusMessageAvatar.js index 2a2c97ee7c..189641eb8a 100644 --- a/src/components/views/avatars/MemberStatusMessageAvatar.js +++ b/src/components/views/avatars/MemberStatusMessageAvatar.js @@ -23,11 +23,22 @@ import classNames from 'classnames'; import * as ContextualMenu from "../../structures/ContextualMenu"; import StatusMessageContextMenu from "../context_menus/StatusMessageContextMenu"; -export default class MemberStatusMessageAvatar extends React.Component { +export default class MemberStatusMessageAvatar extends React.PureComponent { + static propTypes = { + member: PropTypes.object.isRequired, + width: PropTypes.number, + height: PropTypes.number, + resizeMethod: PropTypes.string, + }; + + static defaultProps = { + width: 40, + height: 40, + resizeMethod: 'crop', + }; + constructor(props, context) { super(props, context); - this._onRoomStateEvents = this._onRoomStateEvents.bind(this); - this._onClick = this._onClick.bind(this); } componentWillMount() { @@ -52,15 +63,14 @@ export default class MemberStatusMessageAvatar extends React.Component { } } - _onRoomStateEvents(ev, state) { + _onRoomStateEvents = (ev, state) => { if (ev.getStateKey() !== MatrixClientPeg.get().getUserId()) return; if (ev.getType() !== "im.vector.user_status") return; // TODO: We should be relying on `this.props.member.user._unstable_statusMessage` this.setState({message: ev.getContent()["status"]}); - this.forceUpdate(); - } + }; - _onClick(e) { + _onClick = (e) => { e.stopPropagation(); const elementRect = e.target.getBoundingClientRect(); @@ -79,7 +89,7 @@ export default class MemberStatusMessageAvatar extends React.Component { menuWidth: 190, user: this.props.member.user, }); - } + }; render() { const hasStatus = this.props.member.user ? !!this.props.member.user._unstable_statusMessage : false; @@ -97,16 +107,3 @@ export default class MemberStatusMessageAvatar extends React.Component { ; } } - -MemberStatusMessageAvatar.propTypes = { - member: PropTypes.object.isRequired, - width: PropTypes.number, - height: PropTypes.number, - resizeMethod: PropTypes.string, -}; - -MemberStatusMessageAvatar.defaultProps = { - width: 40, - height: 40, - resizeMethod: 'crop', -}; diff --git a/src/components/views/context_menus/StatusMessageContextMenu.js b/src/components/views/context_menus/StatusMessageContextMenu.js index 243164301d..d062fc2a3e 100644 --- a/src/components/views/context_menus/StatusMessageContextMenu.js +++ b/src/components/views/context_menus/StatusMessageContextMenu.js @@ -22,30 +22,32 @@ import AccessibleButton from '../elements/AccessibleButton'; import classNames from 'classnames'; export default class StatusMessageContextMenu extends React.Component { + static propTypes = { + // js-sdk User object. Not required because it might not exist. + user: PropTypes.object, + }; + constructor(props, context) { super(props, context); - this._onClearClick = this._onClearClick.bind(this); - this._onSubmit = this._onSubmit.bind(this); - this._onStatusChange = this._onStatusChange.bind(this); this.state = { message: props.user ? props.user._unstable_statusMessage : "", }; } - async _onClearClick(e) { + _onClearClick = async (e) => { await MatrixClientPeg.get()._unstable_setStatusMessage(""); this.setState({message: ""}); - } + }; - _onSubmit(e) { + _onSubmit = (e) => { e.preventDefault(); MatrixClientPeg.get()._unstable_setStatusMessage(this.state.message); - } + }; - _onStatusChange(e) { + _onStatusChange = (e) => { this.setState({message: e.target.value}); - } + }; render() { const formSubmitClasses = classNames({ @@ -82,8 +84,3 @@ export default class StatusMessageContextMenu extends React.Component {
    ; } } - -StatusMessageContextMenu.propTypes = { - // js-sdk User object. Not required because it might not exist. - user: PropTypes.object, -}; From 3a8b9ab8501aed59808146f11dd802b72f7398de Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 18 Dec 2018 17:57:23 +0000 Subject: [PATCH 166/237] unused import --- src/components/views/elements/AppTile.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 7eae17ace8..1ce32c852c 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -22,7 +22,6 @@ import qs from 'querystring'; import React from 'react'; import PropTypes from 'prop-types'; import MatrixClientPeg from '../../../MatrixClientPeg'; -import PlatformPeg from '../../../PlatformPeg'; import ScalarAuthClient from '../../../ScalarAuthClient'; import WidgetMessaging from '../../../WidgetMessaging'; import TintableSvgButton from './TintableSvgButton'; From a829bc901cf80d3240fc69c13224dbc81db3d7c6 Mon Sep 17 00:00:00 2001 From: Nad Chishtie Date: Tue, 18 Dec 2018 17:59:46 +0000 Subject: [PATCH 167/237] Updated CSS syntax to avoid 'transparent black'. --- res/css/structures/_RoomSubList.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/res/css/structures/_RoomSubList.scss b/res/css/structures/_RoomSubList.scss index 3a0dd0395b..a3319669cb 100644 --- a/res/css/structures/_RoomSubList.scss +++ b/res/css/structures/_RoomSubList.scss @@ -170,12 +170,12 @@ limitations under the License. &.mx_IndicatorScrollbar_topOverflow::before { top: 0; - background: linear-gradient($secondary-accent-color, transparent); + background: linear-gradient(to top, rgba(242,245,248,0), rgba(242,245,248,1)); } &.mx_IndicatorScrollbar_bottomOverflow::after { bottom: 0; - background: linear-gradient(transparent, $secondary-accent-color); + background: linear-gradient(to bottom, rgba(242,245,248,0), rgba(242,245,248,1)); } } From d20a934642a117253ae8b138a4ca2de71bf05866 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 18 Dec 2018 11:04:16 -0700 Subject: [PATCH 168/237] Appease the linter --- src/components/views/context_menus/StatusMessageContextMenu.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/context_menus/StatusMessageContextMenu.js b/src/components/views/context_menus/StatusMessageContextMenu.js index d062fc2a3e..f07220db44 100644 --- a/src/components/views/context_menus/StatusMessageContextMenu.js +++ b/src/components/views/context_menus/StatusMessageContextMenu.js @@ -35,7 +35,7 @@ export default class StatusMessageContextMenu extends React.Component { }; } - _onClearClick = async (e) => { + _onClearClick = async(e) => { await MatrixClientPeg.get()._unstable_setStatusMessage(""); this.setState({message: ""}); }; From 04c9fff6ce9cb4b5ac69adddbac8f709cf6afdb3 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 18 Dec 2018 15:11:08 -0700 Subject: [PATCH 169/237] Add a feature flag for custom status messages --- .../views/avatars/MemberStatusMessageAvatar.js | 10 +++++++++- src/components/views/rooms/MemberInfo.js | 6 +++++- src/components/views/rooms/MemberTile.js | 8 +++++++- src/components/views/rooms/RoomTile.js | 3 ++- src/i18n/strings/en_EN.json | 1 + src/settings/Settings.js | 6 ++++++ 6 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/components/views/avatars/MemberStatusMessageAvatar.js b/src/components/views/avatars/MemberStatusMessageAvatar.js index 189641eb8a..814144b64d 100644 --- a/src/components/views/avatars/MemberStatusMessageAvatar.js +++ b/src/components/views/avatars/MemberStatusMessageAvatar.js @@ -22,8 +22,9 @@ import MemberAvatar from '../avatars/MemberAvatar'; import classNames from 'classnames'; import * as ContextualMenu from "../../structures/ContextualMenu"; import StatusMessageContextMenu from "../context_menus/StatusMessageContextMenu"; +import SettingsStore from "../../../settings/SettingsStore"; -export default class MemberStatusMessageAvatar extends React.PureComponent { +export default class MemberStatusMessageAvatar extends React.Component { static propTypes = { member: PropTypes.object.isRequired, width: PropTypes.number, @@ -92,6 +93,13 @@ export default class MemberStatusMessageAvatar extends React.PureComponent { }; render() { + if (!SettingsStore.isFeatureEnabled("feature_custom_status")) { + return ; + } + const hasStatus = this.props.member.user ? !!this.props.member.user._unstable_statusMessage : false; const classes = classNames({ diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 6bcdc53d4c..1829413dfd 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -42,6 +42,7 @@ import AccessibleButton from '../elements/AccessibleButton'; import RoomViewStore from '../../../stores/RoomViewStore'; import SdkConfig from '../../../SdkConfig'; import MultiInviter from "../../../utils/MultiInviter"; +import SettingsStore from "../../../settings/SettingsStore"; module.exports = withMatrixClient(React.createClass({ displayName: 'MemberInfo', @@ -895,7 +896,10 @@ module.exports = withMatrixClient(React.createClass({ presenceState = this.props.member.user.presence; presenceLastActiveAgo = this.props.member.user.lastActiveAgo; presenceCurrentlyActive = this.props.member.user.currentlyActive; - statusMessage = this.props.member.user._unstable_statusMessage; + + if (SettingsStore.isFeatureEnabled("feature_custom_status")) { + statusMessage = this.props.member.user._unstable_statusMessage; + } } const room = this.props.matrixClient.getRoom(this.props.member.roomId); diff --git a/src/components/views/rooms/MemberTile.js b/src/components/views/rooms/MemberTile.js index 96a8e0b515..ba951792d0 100644 --- a/src/components/views/rooms/MemberTile.js +++ b/src/components/views/rooms/MemberTile.js @@ -16,6 +16,8 @@ limitations under the License. 'use strict'; +import SettingsStore from "../../../settings/SettingsStore"; + const React = require('react'); import PropTypes from 'prop-types'; @@ -84,7 +86,11 @@ module.exports = React.createClass({ const name = this._getDisplayName(); const active = -1; const presenceState = member.user ? member.user.presence : null; - const statusMessage = member.user ? member.user._unstable_statusMessage : null; + + let statusMessage = null; + if (member.user && SettingsStore.isFeatureEnabled("feature_custom_status")) { + statusMessage = member.user._unstable_statusMessage; + } const av = ( diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 676edc1ea2..a054246b4f 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -30,6 +30,7 @@ import * as FormattingUtils from '../../../utils/FormattingUtils'; import AccessibleButton from '../elements/AccessibleButton'; import ActiveRoomObserver from '../../../ActiveRoomObserver'; import RoomViewStore from '../../../stores/RoomViewStore'; +import SettingsStore from "../../../settings/SettingsStore"; module.exports = React.createClass({ displayName: 'RoomTile', @@ -254,7 +255,7 @@ module.exports = React.createClass({ const isJoined = this.props.room.getMyMembership() === "join"; const looksLikeDm = this.props.room.getInvitedAndJoinedMemberCount() === 2; let subtext = null; - if (!isInvite && isJoined && looksLikeDm) { + if (!isInvite && isJoined && looksLikeDm && SettingsStore.isFeatureEnabled("feature_custom_status")) { const selfId = MatrixClientPeg.get().getUserId(); const otherMember = this.props.room.currentState.getMembersExcept([selfId])[0]; if (otherMember && otherMember.user && otherMember.user._unstable_statusMessage) { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 5c7e067f95..0bd1858b90 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -255,6 +255,7 @@ "Please contact your homeserver administrator.": "Please contact your homeserver administrator.", "Failed to join room": "Failed to join room", "Message Pinning": "Message Pinning", + "Custom user status messages": "Custom user status messages", "Increase performance by only loading room members on first view": "Increase performance by only loading room members on first view", "Backup of encryption keys to server": "Backup of encryption keys to server", "Disable Emoji suggestions while typing": "Disable Emoji suggestions while typing", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index c9a4ecdebe..1cac8559d1 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -83,6 +83,12 @@ export const SETTINGS = { supportedLevels: LEVELS_FEATURE, default: false, }, + "feature_custom_status": { + isFeature: true, + displayName: _td("Custom user status messages"), + supportedLevels: LEVELS_FEATURE, + default: false, + }, "feature_lazyloading": { isFeature: true, displayName: _td("Increase performance by only loading room members on first view"), From e09121556d70bdd819a3d736046595de951061d6 Mon Sep 17 00:00:00 2001 From: Nad Chishtie Date: Wed, 19 Dec 2018 09:28:33 +0000 Subject: [PATCH 170/237] Use a more sane SVG. --- res/img/icons-room-add.svg | 78 ++++---------------------------------- 1 file changed, 8 insertions(+), 70 deletions(-) diff --git a/res/img/icons-room-add.svg b/res/img/icons-room-add.svg index 6dd2e21295..f0b7584df9 100644 --- a/res/img/icons-room-add.svg +++ b/res/img/icons-room-add.svg @@ -1,71 +1,9 @@ - - - - - - image/svg+xml - - - - - - - - - - - + + + + + + + + From b826b0d9980a44eab73e22ac354067cf876e6211 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 19 Dec 2018 12:11:21 +0100 Subject: [PATCH 171/237] Add redesign feedback dialog + button in tag panel --- res/css/structures/_TagPanel.scss | 22 ++++++-- res/img/feather-icons/life-buoy.svg | 21 ++++++++ src/components/structures/TagPanel.js | 21 +++++++- .../views/dialogs/RedesignFeedbackDialog.js | 52 +++++++++++++++++++ src/components/views/elements/ActionButton.js | 8 ++- src/components/views/elements/GroupsButton.js | 5 +- src/i18n/strings/en_EN.json | 6 ++- 7 files changed, 123 insertions(+), 12 deletions(-) create mode 100644 res/img/feather-icons/life-buoy.svg create mode 100644 src/components/views/dialogs/RedesignFeedbackDialog.js diff --git a/res/css/structures/_TagPanel.scss b/res/css/structures/_TagPanel.scss index 1ffb017e28..361c8cdf60 100644 --- a/res/css/structures/_TagPanel.scss +++ b/res/css/structures/_TagPanel.scss @@ -124,10 +124,24 @@ limitations under the License. padding-right: 4px; } +.mx_TagPanel_groupsButton { + flex: 0; + margin: 17px 0 3px 0; +} + + +.mx_TagPanel_groupsButton > .mx_GroupsButton:before { + mask: url('../../img/feather-icons/users.svg'); + mask-position: center 10px; +} + +.mx_TagPanel_groupsButton > .mx_TagPanel_report:before { + mask: url('../../img/feather-icons/life-buoy.svg'); + mask-position: center 10px; +} + .mx_TagPanel_groupsButton > .mx_AccessibleButton { - flex: auto; - margin-bottom: 17px; - margin-top: 18px; + margin-bottom: 12px; height: 40px; width: 40px; border-radius: 20px; @@ -138,9 +152,7 @@ limitations under the License. &:before { background-color: $tagpanel-bg-color; - mask: url('../../img/feather-icons/users.svg'); mask-repeat: no-repeat; - mask-position: center 10px; content: ''; position: absolute; top: 0; diff --git a/res/img/feather-icons/life-buoy.svg b/res/img/feather-icons/life-buoy.svg new file mode 100644 index 0000000000..3ba383fa9a --- /dev/null +++ b/res/img/feather-icons/life-buoy.svg @@ -0,0 +1,21 @@ + + + + life-buoy + Created with Sketch. + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/structures/TagPanel.js b/src/components/structures/TagPanel.js index f23ac698ba..7e77a64e62 100644 --- a/src/components/structures/TagPanel.js +++ b/src/components/structures/TagPanel.js @@ -23,6 +23,7 @@ import GroupActions from '../../actions/GroupActions'; import sdk from '../../index'; import dis from '../../dispatcher'; +import Modal from '../../Modal'; import { _t } from '../../languageHandler'; import { Droppable } from 'react-beautiful-dnd'; @@ -47,6 +48,8 @@ const TagPanel = React.createClass({ this.context.matrixClient.on("Group.myMembership", this._onGroupMyMembership); this.context.matrixClient.on("sync", this._onClientSync); + this._dispatcherRef = dis.register(this._onAction); + this._tagOrderStoreToken = TagOrderStore.addListener(() => { if (this.unmounted) { return; @@ -67,6 +70,9 @@ const TagPanel = React.createClass({ if (this._filterStoreToken) { this._filterStoreToken.remove(); } + if (this._dispatcherRef) { + dis.unregister(this._dispatcherRef); + } }, _onGroupMyMembership() { @@ -100,13 +106,21 @@ const TagPanel = React.createClass({ dis.dispatch({action: 'deselect_tags'}); }, + _onAction(payload) { + if (payload.action === "show_redesign_feedback_dialog") { + const RedesignFeedbackDialog = + sdk.getComponent("views.dialogs.RedesignFeedbackDialog"); + Modal.createDialog(RedesignFeedbackDialog); + } + }, + render() { const GroupsButton = sdk.getComponent('elements.GroupsButton'); const DNDTagTile = sdk.getComponent('elements.DNDTagTile'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const TintableSvg = sdk.getComponent('elements.TintableSvg'); const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper"); - + const ActionButton = sdk.getComponent("elements.ActionButton"); const tags = this.state.orderedTags.map((tag, index) => { return
    - + +
    ; }, diff --git a/src/components/views/dialogs/RedesignFeedbackDialog.js b/src/components/views/dialogs/RedesignFeedbackDialog.js new file mode 100644 index 0000000000..29ee233522 --- /dev/null +++ b/src/components/views/dialogs/RedesignFeedbackDialog.js @@ -0,0 +1,52 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import QuestionDialog from './QuestionDialog'; +import { _t } from '../../../languageHandler'; + +export default (props) => { + + const existingIssuesUrl = "https://github.com/vector-im/riot-web/issues" + + "?q=is%3Aopen+is%3Aissue+label%3Aredesign+sort%3Areactions-%2B1-desc"; + const newIssueUrl = "https://github.com/vector-im/riot-web/issues/new" + + "?assignees=&labels=redesign&template=redesign_issue.md&title="; + + const description1 = + _t("Thanks for testing the Riot Redesign. " + + "If you run into any bugs or visual issues, " + + "please let us know on GitHub."); + const description2 = _t("To help avoid duplicate issues, " + + "please view existing issues " + + "first (and add a +1) or create a new issue " + + "if you can't find it.", {}, + { + existingIssuesLink: (sub) => { + return { sub }; + }, + newIssueLink: (sub) => { + return { sub }; + }, + }); + + return (

    {description1}

    {description2}

    } + button={_t("Go back")} + onFinished={props.onFinished} + />); +}; diff --git a/src/components/views/elements/ActionButton.js b/src/components/views/elements/ActionButton.js index a9d95e4a52..1ca5ab9983 100644 --- a/src/components/views/elements/ActionButton.js +++ b/src/components/views/elements/ActionButton.js @@ -31,6 +31,7 @@ export default React.createClass({ mouseOverAction: PropTypes.string, label: PropTypes.string.isRequired, iconPath: PropTypes.string, + className: PropTypes.string, }, getDefaultProps: function() { @@ -76,8 +77,13 @@ export default React.createClass({ () : undefined; + const classNames = ["mx_RoleButton"]; + if (this.props.className) { + classNames.push(this.props.className); + } + return ( - ); }; GroupsButton.propTypes = { size: PropTypes.string, - tooltip: PropTypes.bool, }; export default GroupsButton; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index c137253a43..14e5c14529 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1348,5 +1348,9 @@ "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", + "Report bugs & give feedback": "Report bugs & give feedback", + "Thanks for testing the Riot Redesign. If you run into any bugs or visual issues, please let us know on GitHub.": "Thanks for testing the Riot Redesign. If you run into any bugs or visual issues, please let us know on GitHub.", + "To help avoid duplicate issues, please view existing issues first (and add a +1) or create a new issue if you can't find it.": "To help avoid duplicate issues, please view existing issues first (and add a +1) or create a new issue if you can't find it.", + "Go back": "Go back" } From b3a12867a67b475e1cb1de6d1f098da5f8f73ecd Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 19 Dec 2018 12:56:08 +0100 Subject: [PATCH 172/237] remove extra blank lines --- res/css/structures/_TagPanel.scss | 1 - src/components/views/dialogs/RedesignFeedbackDialog.js | 1 - 2 files changed, 2 deletions(-) diff --git a/res/css/structures/_TagPanel.scss b/res/css/structures/_TagPanel.scss index 361c8cdf60..1275c9c60e 100644 --- a/res/css/structures/_TagPanel.scss +++ b/res/css/structures/_TagPanel.scss @@ -129,7 +129,6 @@ limitations under the License. margin: 17px 0 3px 0; } - .mx_TagPanel_groupsButton > .mx_GroupsButton:before { mask: url('../../img/feather-icons/users.svg'); mask-position: center 10px; diff --git a/src/components/views/dialogs/RedesignFeedbackDialog.js b/src/components/views/dialogs/RedesignFeedbackDialog.js index 29ee233522..c428aca16a 100644 --- a/src/components/views/dialogs/RedesignFeedbackDialog.js +++ b/src/components/views/dialogs/RedesignFeedbackDialog.js @@ -19,7 +19,6 @@ import QuestionDialog from './QuestionDialog'; import { _t } from '../../../languageHandler'; export default (props) => { - const existingIssuesUrl = "https://github.com/vector-im/riot-web/issues" + "?q=is%3Aopen+is%3Aissue+label%3Aredesign+sort%3Areactions-%2B1-desc"; const newIssueUrl = "https://github.com/vector-im/riot-web/issues/new" + From f7ed1ff046894841b23c777295de2207cced1f47 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 19 Dec 2018 12:59:03 +0100 Subject: [PATCH 173/237] avoid buttons being deformed in collapsed state unsure what this was doing before, I suspect the BottomLeftMenu which is gone now. --- res/css/structures/_LeftPanel.scss | 6 ------ 1 file changed, 6 deletions(-) diff --git a/res/css/structures/_LeftPanel.scss b/res/css/structures/_LeftPanel.scss index 1fe7a42678..47cb231b58 100644 --- a/res/css/structures/_LeftPanel.scss +++ b/res/css/structures/_LeftPanel.scss @@ -89,12 +89,6 @@ limitations under the License. pointer-events: none; } -.mx_LeftPanel_container.collapsed .mx_RoleButton { - margin-right: 0px ! important; - padding-top: 3px ! important; - padding-bottom: 3px ! important; -} - .mx_BottomLeftMenu_options > div { display: inline-block; } From 45f05092ed99a15d28ad3a5b9af57ed2fde1a7a9 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 19 Dec 2018 10:21:43 -0700 Subject: [PATCH 174/237] Add a comment to describe why we're not using the property we should be --- src/components/views/avatars/MemberStatusMessageAvatar.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/views/avatars/MemberStatusMessageAvatar.js b/src/components/views/avatars/MemberStatusMessageAvatar.js index 814144b64d..aebd1741b7 100644 --- a/src/components/views/avatars/MemberStatusMessageAvatar.js +++ b/src/components/views/avatars/MemberStatusMessageAvatar.js @@ -68,6 +68,9 @@ export default class MemberStatusMessageAvatar extends React.Component { if (ev.getStateKey() !== MatrixClientPeg.get().getUserId()) return; if (ev.getType() !== "im.vector.user_status") return; // TODO: We should be relying on `this.props.member.user._unstable_statusMessage` + // We don't currently because the js-sdk doesn't emit a specific event for this + // change, and we don't want to race it. This should be improved when we rip out + // the im.vector.user_status stuff and replace it with a complete solution. this.setState({message: ev.getContent()["status"]}); }; From a22a9492a0d1eb74a6cb3b0a6eaca55e998d799a Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 18 Dec 2018 14:34:02 +0000 Subject: [PATCH 175/237] Remove duplicate CSS file for CreateKeyBackupDialog Signed-off-by: J. Ryan Stinnett --- res/css/_components.scss | 1 - .../views/dialogs/_CreateKeyBackupDialog.scss | 25 ------------------- .../keybackup/_CreateKeyBackupDialog.scss | 9 ++++++- 3 files changed, 8 insertions(+), 27 deletions(-) delete mode 100644 res/css/views/dialogs/_CreateKeyBackupDialog.scss diff --git a/res/css/_components.scss b/res/css/_components.scss index 48aa211fd8..9f50856ce0 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -33,7 +33,6 @@ @import "./views/dialogs/_ChatInviteDialog.scss"; @import "./views/dialogs/_ConfirmUserActionDialog.scss"; @import "./views/dialogs/_CreateGroupDialog.scss"; -@import "./views/dialogs/_CreateKeyBackupDialog.scss"; @import "./views/dialogs/_CreateRoomDialog.scss"; @import "./views/dialogs/_DeactivateAccountDialog.scss"; @import "./views/dialogs/_DevtoolsDialog.scss"; diff --git a/res/css/views/dialogs/_CreateKeyBackupDialog.scss b/res/css/views/dialogs/_CreateKeyBackupDialog.scss deleted file mode 100644 index a422cf858c..0000000000 --- a/res/css/views/dialogs/_CreateKeyBackupDialog.scss +++ /dev/null @@ -1,25 +0,0 @@ -/* -Copyright 2018 New Vector Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -.mx_CreateKeyBackupDialog { - padding-right: 40px; -} - -.mx_CreateKeyBackupDialog_recoveryKey { - padding: 20px; - color: $info-plinth-fg-color; - background-color: $info-plinth-bg-color; -} diff --git a/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss b/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss index 2cb6b11c0c..1addb99792 100644 --- a/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss +++ b/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss @@ -13,7 +13,11 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ - + +.mx_CreateKeyBackupDialog { + padding-right: 40px; +} + .mx_CreateKeyBackupDialog_primaryContainer { /*FIXME: plinth colour in new theme(s). background-color: $accent-color;*/ padding: 20px @@ -54,4 +58,7 @@ limitations under the License. .mx_CreateKeyBackupDialog_recoveryKey { width: 300px; + padding: 20px; + color: $info-plinth-fg-color; + background-color: $info-plinth-bg-color; } From 9c4ff4048a596b7bd824fd9b6362a4fb23c1a5ae Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 18 Dec 2018 17:26:11 +0000 Subject: [PATCH 176/237] Convert show recovery key to flexbox This allows the buttons to fit on a single line and flows a bit better at low widths. Signed-off-by: J. Ryan Stinnett --- .../keybackup/_CreateKeyBackupDialog.scss | 17 +++++++++-- .../keybackup/CreateKeyBackupDialog.js | 28 +++++++++---------- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss b/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss index 1addb99792..0b686dbcdc 100644 --- a/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss +++ b/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss @@ -52,13 +52,24 @@ limitations under the License. float: right; } -.mx_CreateKeyBackupDialog_recoveryKeyButtons { - float: right; +.mx_CreateKeyBackupDialog_recoveryKeyHeader { + margin-bottom: 1em; +} + +.mx_CreateKeyBackupDialog_recoveryKeyContainer { + display: flex; } .mx_CreateKeyBackupDialog_recoveryKey { - width: 300px; + width: 275px; padding: 20px; color: $info-plinth-fg-color; background-color: $info-plinth-bg-color; + margin-right: 8px; +} + +.mx_CreateKeyBackupDialog_recoveryKeyButtons { + flex: 1; + display: flex; + align-items: center; } diff --git a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js index 0db9d0699b..6593472670 100644 --- a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js +++ b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js @@ -351,21 +351,21 @@ export default React.createClass({

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

    {bodyText}

    -

    {_t("Your Recovery Key")}
    -
    - - { - // FIXME REDESIGN: buttons should be adjacent but insufficient room in current design - } -

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


    From a597ad10b0a8632688d1da40d6db5cabb0532722 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 19 Dec 2018 10:44:31 +0000 Subject: [PATCH 177/237] Add a few more zxcvbn strings Signed-off-by: J. Ryan Stinnett --- src/i18n/strings/en_EN.json | 2 ++ src/utils/PasswordScorer.js | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index b5a5762cc4..5f436a4610 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -250,6 +250,8 @@ "A word by itself is easy to guess": "A word by itself is easy to guess", "Names and surnames by themselves are easy to guess": "Names and surnames by themselves are easy to guess", "Common names and surnames are easy to guess": "Common names and surnames are easy to guess", + "Straight rows of keys are easy to guess": "Straight rows of keys are easy to guess", + "Short keyboard patterns are easy to guess": "Short keyboard patterns are easy to guess", "There was an error joining the room": "There was an error joining the room", "Sorry, your homeserver is too old to participate in this room.": "Sorry, your homeserver is too old to participate in this room.", "Please contact your homeserver administrator.": "Please contact your homeserver administrator.", diff --git a/src/utils/PasswordScorer.js b/src/utils/PasswordScorer.js index e4bbec1637..545686cdb6 100644 --- a/src/utils/PasswordScorer.js +++ b/src/utils/PasswordScorer.js @@ -52,6 +52,8 @@ _td("This is similar to a commonly used password"); _td("A word by itself is easy to guess"); _td("Names and surnames by themselves are easy to guess"); _td("Common names and surnames are easy to guess"); +_td("Straight rows of keys are easy to guess"); +_td("Short keyboard patterns are easy to guess"); /** * Wrapper around zxcvbn password strength estimation From 24f0123dedb8eb8b6e9ea39efcb8c43fd8ec4465 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 19 Dec 2018 10:56:50 +0000 Subject: [PATCH 178/237] Convert pass phrase entry to flexbox Signed-off-by: J. Ryan Stinnett --- .../keybackup/_CreateKeyBackupDialog.scss | 16 ++++--- .../keybackup/CreateKeyBackupDialog.js | 44 ++++++++++--------- 2 files changed, 35 insertions(+), 25 deletions(-) diff --git a/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss b/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss index 0b686dbcdc..424ffbd0a8 100644 --- a/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss +++ b/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss @@ -29,9 +29,13 @@ limitations under the License. display: block; } +.mx_CreateKeyBackupDialog_passPhraseContainer { + display: flex; + align-items: start; +} + .mx_CreateKeyBackupDialog_passPhraseHelp { - float: right; - width: 230px; + flex: 1; height: 85px; margin-left: 20px; font-size: 80%; @@ -42,14 +46,16 @@ limitations under the License. } .mx_CreateKeyBackupDialog_passPhraseInput { + flex: none; width: 250px; border: 1px solid $accent-color; border-radius: 5px; padding: 10px; + margin-bottom: 1em; } .mx_CreateKeyBackupDialog_passPhraseMatch { - float: right; + margin-left: 20px; } .mx_CreateKeyBackupDialog_recoveryKeyHeader { @@ -61,11 +67,11 @@ limitations under the License. } .mx_CreateKeyBackupDialog_recoveryKey { - width: 275px; + width: 262px; padding: 20px; color: $info-plinth-fg-color; background-color: $info-plinth-bg-color; - margin-right: 8px; + margin-right: 12px; } .mx_CreateKeyBackupDialog_recoveryKeyButtons { diff --git a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js index 6593472670..dda378e792 100644 --- a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js +++ b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js @@ -239,17 +239,19 @@ export default React.createClass({

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

    -
    - {strengthMeter} - {helpText} +
    + +
    + {strengthMeter} + {helpText} +
    -
    - {passPhraseMatch} -
    - +
    +
    + +
    + {passPhraseMatch}
    Date: Wed, 19 Dec 2018 11:06:47 +0000 Subject: [PATCH 179/237] Use primary styling on download / clipboard key actions Signed-off-by: J. Ryan Stinnett --- .../views/dialogs/keybackup/CreateKeyBackupDialog.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js index dda378e792..a097e84cdb 100644 --- a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js +++ b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js @@ -363,10 +363,10 @@ export default React.createClass({ {this._keyBackupInfo.recovery_key}
    - -
    From fd94dc686f66feddc1e336de9cfa83e24fabab8d Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 19 Dec 2018 17:26:40 +0000 Subject: [PATCH 180/237] Handle errors when fetching commits for changelog It's possible to get errors when fetching commits (for example, if the rate limit is exceeded), so this will handle the error case and display it instead of an infinite spinner. Signed-off-by: J. Ryan Stinnett --- .../views/dialogs/ChangelogDialog.js | 23 ++++++++++++++----- src/i18n/strings/en_EN.json | 1 + 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/components/views/dialogs/ChangelogDialog.js b/src/components/views/dialogs/ChangelogDialog.js index b93678b2ab..3c9414fd88 100644 --- a/src/components/views/dialogs/ChangelogDialog.js +++ b/src/components/views/dialogs/ChangelogDialog.js @@ -36,8 +36,12 @@ export default class ChangelogDialog extends React.Component { for (let i=0; i { - if (body == null) return; + const url = `https://api.github.com/repos/${REPOS[i]}/compare/${oldVersion}...${newVersion}`; + request(url, (err, response, body) => { + if (response.statusCode < 200 || response.statusCode >= 300) { + this.setState({ [REPOS[i]]: response.statusText }); + return; + } this.setState({[REPOS[i]]: JSON.parse(body).commits}); }); } @@ -58,13 +62,20 @@ export default class ChangelogDialog extends React.Component { const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog'); const logs = REPOS.map(repo => { - if (this.state[repo] == null) return ; + let content; + if (this.state[repo] == null) { + content = ; + } else if (typeof this.state[repo] === "string") { + content = _t("Unable to load commit detail: %(msg)s", { + msg: this.state[repo], + }); + } else { + content = this.state[repo].map(this._elementsForCommit); + } return (

    {repo}

    -
      - {this.state[repo].map(this._elementsForCommit)} -
    +
      {content}
    ); }); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 5f436a4610..22a869dc6e 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -889,6 +889,7 @@ "What GitHub issue are these logs for?": "What GitHub issue are these logs for?", "Notes:": "Notes:", "Send logs": "Send logs", + "Unable to load commit detail: %(msg)s": "Unable to load commit detail: %(msg)s", "Unavailable": "Unavailable", "Changelog": "Changelog", "Create a new chat or reuse an existing one": "Create a new chat or reuse an existing one", From b61aa33e99bc5535088b87e2a045e0e51c78dbf2 Mon Sep 17 00:00:00 2001 From: Nad Chishtie Date: Thu, 20 Dec 2018 11:33:53 +0000 Subject: [PATCH 181/237] Tint input icon. --- res/img/feather-icons/search-input.svg | 11 +++++++++++ res/themes/dharma/css/_dharma.scss | 4 +++- 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 res/img/feather-icons/search-input.svg diff --git a/res/img/feather-icons/search-input.svg b/res/img/feather-icons/search-input.svg new file mode 100644 index 0000000000..3be5acb32e --- /dev/null +++ b/res/img/feather-icons/search-input.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/res/themes/dharma/css/_dharma.scss b/res/themes/dharma/css/_dharma.scss index 2cc27f3be0..805e13b579 100644 --- a/res/themes/dharma/css/_dharma.scss +++ b/res/themes/dharma/css/_dharma.scss @@ -262,9 +262,11 @@ input[type=search].mx_textinput_icon { background-position: 10px center; } + +// FIXME THEME - Tint by CSS rather than referencing a duplicate asset input[type=text].mx_textinput_icon.mx_textinput_search, input[type=search].mx_textinput_icon.mx_textinput_search { - background-image: url('../../img/feather-icons/search.svg'); + background-image: url('../../img/feather-icons/search-input.svg'); } // dont search UI as not all browsers support it, From 6c2d2f60cf2e225f630c1ed2481aace584a1b915 Mon Sep 17 00:00:00 2001 From: Nad Chishtie Date: Thu, 20 Dec 2018 11:34:20 +0000 Subject: [PATCH 182/237] Polished left left panel. --- res/css/structures/_TagPanel.scss | 4 ++-- res/img/feather-icons/life-buoy.svg | 7 ++----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/res/css/structures/_TagPanel.scss b/res/css/structures/_TagPanel.scss index 1275c9c60e..77eefc7e10 100644 --- a/res/css/structures/_TagPanel.scss +++ b/res/css/structures/_TagPanel.scss @@ -131,12 +131,12 @@ limitations under the License. .mx_TagPanel_groupsButton > .mx_GroupsButton:before { mask: url('../../img/feather-icons/users.svg'); - mask-position: center 10px; + mask-position: center 11px; } .mx_TagPanel_groupsButton > .mx_TagPanel_report:before { mask: url('../../img/feather-icons/life-buoy.svg'); - mask-position: center 10px; + mask-position: center 9px; } .mx_TagPanel_groupsButton > .mx_AccessibleButton { diff --git a/res/img/feather-icons/life-buoy.svg b/res/img/feather-icons/life-buoy.svg index 3ba383fa9a..20bd0f0b5d 100644 --- a/res/img/feather-icons/life-buoy.svg +++ b/res/img/feather-icons/life-buoy.svg @@ -1,8 +1,5 @@ - + - - life-buoy - Created with Sketch. @@ -18,4 +15,4 @@ - \ No newline at end of file + From d7473c4f4dd7046ee8e5ec588a8d4d3a1e5bf72a Mon Sep 17 00:00:00 2001 From: Nad Chishtie Date: Thu, 20 Dec 2018 11:35:21 +0000 Subject: [PATCH 183/237] Various colour, contrast and legibility improvements. --- res/css/structures/_RoomSubList.scss | 4 ++-- res/css/views/rooms/_RoomTile.scss | 4 ++-- res/img/icons-close.svg | 8 ++++---- res/img/topleft-chevron.svg | 2 +- res/themes/dharma/css/_dharma.scss | 9 ++++++--- 5 files changed, 15 insertions(+), 12 deletions(-) diff --git a/res/css/structures/_RoomSubList.scss b/res/css/structures/_RoomSubList.scss index a3319669cb..78208d7d7c 100644 --- a/res/css/structures/_RoomSubList.scss +++ b/res/css/structures/_RoomSubList.scss @@ -94,7 +94,7 @@ limitations under the License. font-weight: 600; font-size: 12px; padding: 0 5px; - background-color: $accent-color; + background-color: $roomtile-name-color; } .mx_RoomSubList_addRoom, .mx_RoomSubList_badge { @@ -114,7 +114,7 @@ limitations under the License. } .mx_RoomSubList_badgeHighlight { - background-color: $warning-color; + background-color: $accent-color; } .mx_RoomSubList_chevron { diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss index d5ec3d365a..19dfbb3c26 100644 --- a/res/css/views/rooms/_RoomTile.scss +++ b/res/css/views/rooms/_RoomTile.scss @@ -112,11 +112,11 @@ limitations under the License. } .mx_RoomTile_unreadNotify .mx_RoomTile_badge { - background-color: $accent-color; + background-color: $roomtile-name-color; } .mx_RoomTile_highlight .mx_RoomTile_badge { - background-color: $warning-color; + background-color: $accent-color; } .mx_RoomTile_unread, .mx_RoomTile_highlight { diff --git a/res/img/icons-close.svg b/res/img/icons-close.svg index e516140dd2..b2dd44fc26 100644 --- a/res/img/icons-close.svg +++ b/res/img/icons-close.svg @@ -75,22 +75,22 @@ - + - \ No newline at end of file + diff --git a/res/img/topleft-chevron.svg b/res/img/topleft-chevron.svg index fa89852874..1cfeaf6352 100644 --- a/res/img/topleft-chevron.svg +++ b/res/img/topleft-chevron.svg @@ -64,7 +64,7 @@ + style="stroke:#61708b;stroke-width:1.6"> diff --git a/res/themes/dharma/css/_dharma.scss b/res/themes/dharma/css/_dharma.scss index 805e13b579..c9930168f0 100644 --- a/res/themes/dharma/css/_dharma.scss +++ b/res/themes/dharma/css/_dharma.scss @@ -121,13 +121,13 @@ $rte-group-pill-color: #aaa; $topleftmenu-color: #212121; $roomheader-color: #45474a; -$roomheader-addroom-color: #929eb4; +$roomheader-addroom-color: #91A1C0; $roomtopic-color: #9fa9ba; $eventtile-meta-color: $roomtopic-color; // ******************** -$roomtile-name-color: #929eb4; +$roomtile-name-color: #61708b; $roomtile-selected-color: #212121; $roomtile-notified-color: #212121; $roomtile-selected-bg-color: #fff; @@ -215,7 +215,10 @@ $progressbar-color: #000; > input[type=search] { border: none; flex: 1; - color: inherit; //from .mx_textinput + color: $primary-fg-color; + }, + input::placeholder { + color: $roomsublist-label-fg-color; } } } From 9b8f07c19f39f57986d4be71f89bd29374e9c692 Mon Sep 17 00:00:00 2001 From: Nad Chishtie Date: Thu, 20 Dec 2018 11:35:43 +0000 Subject: [PATCH 184/237] Don't override text colour of selected room in room list. --- res/css/views/rooms/_RoomTile.scss | 4 ---- 1 file changed, 4 deletions(-) diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss index 19dfbb3c26..d0ef5c332a 100644 --- a/res/css/views/rooms/_RoomTile.scss +++ b/res/css/views/rooms/_RoomTile.scss @@ -130,10 +130,6 @@ limitations under the License. .mx_RoomTile_selected { border-radius: 4px; background-color: $roomtile-selected-bg-color; - - .mx_RoomTile_name { - color: $roomtile-selected-color; - } } .mx_DNDRoomTile { From 42c8d43065d97b8c8df0433702e67f8d21265b21 Mon Sep 17 00:00:00 2001 From: Nad Chishtie Date: Thu, 20 Dec 2018 11:35:58 +0000 Subject: [PATCH 185/237] Lint. --- res/css/views/rooms/_RoomTile.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss index d0ef5c332a..104691430a 100644 --- a/res/css/views/rooms/_RoomTile.scss +++ b/res/css/views/rooms/_RoomTile.scss @@ -158,4 +158,3 @@ limitations under the License. .mx_RoomTile.mx_RoomTile_transparent:focus { background-color: $roomtile-transparent-focused-color; } - From 09bf68b1bdac149be670f5e8b1a7e5070f8566e8 Mon Sep 17 00:00:00 2001 From: Nad Chishtie Date: Thu, 20 Dec 2018 12:46:43 +0000 Subject: [PATCH 186/237] Improved colours. --- res/css/_common.scss | 4 ++-- res/css/structures/_RoomSubList.scss | 2 +- res/css/views/rooms/_EventTile.scss | 2 +- res/css/views/rooms/_RoomTile.scss | 2 +- res/themes/dharma/css/_dharma.scss | 11 ++++++----- 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/res/css/_common.scss b/res/css/_common.scss index 149ec75569..797070d4e2 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -47,7 +47,7 @@ h2 { a:hover, a:link, a:visited { - color: $accent-color; + color: $accent-color-alt; } input[type=text], input[type=password], textarea { @@ -301,7 +301,7 @@ textarea { } .mx_textButton { - @mixin mx_DialogButton_small; + @mixin mx_DialogButton_small; } .mx_textButton:hover { diff --git a/res/css/structures/_RoomSubList.scss b/res/css/structures/_RoomSubList.scss index 78208d7d7c..2d471ee198 100644 --- a/res/css/structures/_RoomSubList.scss +++ b/res/css/structures/_RoomSubList.scss @@ -114,7 +114,7 @@ limitations under the License. } .mx_RoomSubList_badgeHighlight { - background-color: $accent-color; + background-color: $warning-color; } .mx_RoomSubList_chevron { diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 52074563f6..154e5390c8 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -445,7 +445,7 @@ limitations under the License. } .mx_EventTile_content .markdown-body a { - color: $accent-color; + color: $accent-color-alt; } .mx_EventTile_content .markdown-body .hljs { diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss index 104691430a..232b715f7a 100644 --- a/res/css/views/rooms/_RoomTile.scss +++ b/res/css/views/rooms/_RoomTile.scss @@ -116,7 +116,7 @@ limitations under the License. } .mx_RoomTile_highlight .mx_RoomTile_badge { - background-color: $accent-color; + background-color: $warning-color; } .mx_RoomTile_unread, .mx_RoomTile_highlight { diff --git a/res/themes/dharma/css/_dharma.scss b/res/themes/dharma/css/_dharma.scss index c9930168f0..0851762be2 100644 --- a/res/themes/dharma/css/_dharma.scss +++ b/res/themes/dharma/css/_dharma.scss @@ -20,19 +20,20 @@ $focus-bg-color: #dddddd; // button UI (white-on-green in light skin) $accent-fg-color: #ffffff; -$accent-color: #f56679; -$accent-color-50pct: #f56679; +$accent-color: #7ac9a1; +$accent-color-50pct: #92caad; +$accent-color-alt: #238CF5; $selection-fg-color: $primary-bg-color; -$focus-brightness: 125%; +$focus-brightness: 105%; // red warning colour -$warning-color: #ff0064; +$warning-color: #f56679; // background colour for warnings $warning-bg-color: #DF2A8B; $info-bg-color: #2A9EDF; -$mention-user-pill-bg-color: #ff0064; +$mention-user-pill-bg-color: $warning-color; $other-user-pill-bg-color: rgba(0, 0, 0, 0.1); // pinned events indicator From 5723ae60407e851d4b98ca6058625856380c36fb Mon Sep 17 00:00:00 2001 From: Nad Chishtie Date: Thu, 20 Dec 2018 16:06:02 +0000 Subject: [PATCH 187/237] Defined $accent-color-alt in all themes --- res/themes/dark/css/_dark.scss | 1 + res/themes/light/css/_base.scss | 1 + 2 files changed, 2 insertions(+) diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 7bb9fcc053..ed84bde698 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -12,6 +12,7 @@ $light-fg-color: #747474; // button UI (white-on-green in light skin) $accent-fg-color: $primary-bg-color; $accent-color: #76CFA6; +$accent-color-alt: $accent-color; $accent-color-50pct: #76CFA67F; $selection-fg-color: $primary-fg-color; diff --git a/res/themes/light/css/_base.scss b/res/themes/light/css/_base.scss index 3aaa1dde2a..71c1ab5e3c 100644 --- a/res/themes/light/css/_base.scss +++ b/res/themes/light/css/_base.scss @@ -20,6 +20,7 @@ $focus-bg-color: #dddddd; // button UI (white-on-green in light skin) $accent-fg-color: #ffffff; $accent-color: #76CFA6; +$accent-color-alt: $accent-color; $accent-color-50pct: #76CFA67F; $selection-fg-color: $primary-bg-color; From 7affd5fcff5eb76c8852127136f0148bf9246506 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 20 Dec 2018 22:13:40 +0000 Subject: [PATCH 188/237] Try fetching more branches for PRs Attempt both the PR author's branch and the PR's target branch. This resolves issues on experimental where we need riot-web to also be experimental. --- scripts/fetchdep.sh | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/scripts/fetchdep.sh b/scripts/fetchdep.sh index 73c622133b..f20bfe8920 100755 --- a/scripts/fetchdep.sh +++ b/scripts/fetchdep.sh @@ -1,22 +1,24 @@ #!/bin/sh -set -e - org="$1" repo="$2" rm -r "$repo" || true -curbranch="$TRAVIS_PULL_REQUEST_BRANCH" -[ -z "$curbranch" ] && curbranch="$TRAVIS_BRANCH" -[ -z "$curbranch" ] && curbranch=`"echo $GIT_BRANCH" | sed -e 's/^origin\///'` # jenkins +clone() { + branch=$1 + if [ -n "$branch" ] + then + echo "Trying to use the branch $branch" + git clone https://github.com/$org/$repo.git $repo --branch "$branch" && exit 0 + fi +} -if [ -n "$curbranch" ] -then - echo "Determined branch to be $curbranch" - - git clone https://github.com/$org/$repo.git $repo --branch "$curbranch" && exit 0 -fi - -echo "Checking out develop branch" -git clone https://github.com/$org/$repo.git $repo --branch develop +# Try the PR author's branch in case it exists on the deps as well. +clone $TRAVIS_PULL_REQUEST_BRANCH +# Try the target branch of the push or PR. +clone $TRAVIS_BRANCH +# Try the current branch from Jenkins. +clone `"echo $GIT_BRANCH" | sed -e 's/^origin\///'` +# Use develop as the last resort. +clone develop From 097d6fdedeacaee92ebe68fcc252cd1f9ea85710 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 20 Dec 2018 20:23:06 +0000 Subject: [PATCH 189/237] Update room filter focus handling to avoid infinite loop The classes on the search box input were changed without updating the focusing loop in the room filter which used one of these classes as a boundary condition. This led to a case that could loop forever. Regressed by #2267. Fixes vector-im/riot-web#7926. --- src/components/structures/LeftPanel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/LeftPanel.js b/src/components/structures/LeftPanel.js index 77f6f1f948..ba0e97366e 100644 --- a/src/components/structures/LeftPanel.js +++ b/src/components/structures/LeftPanel.js @@ -151,7 +151,7 @@ const LeftPanel = React.createClass({ } } while (element && !( classes.contains("mx_RoomTile") || - classes.contains("mx_SearchBox_search"))); + classes.contains("mx_textinput_search"))); if (element) { element.focus(); From 991104d15666011739abcc4fca6b36e3dc154571 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 28 Nov 2018 00:33:20 +0000 Subject: [PATCH 190/237] Handle well-known data in the login response ... as per [MSC1730](https://github.com/matrix-org/matrix-doc/pull/1730). --- src/Login.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/Login.js b/src/Login.js index 330eb8a8f5..ca045e36cd 100644 --- a/src/Login.js +++ b/src/Login.js @@ -204,6 +204,19 @@ export async function sendLoginRequest(hsUrl, isUrl, loginType, loginParams) { const data = await client.login(loginType, loginParams); + const wellknown = data.well_known; + if (wellknown) { + if (wellknown["m.homeserver"] && wellknown["m.homeserver"]["base_url"]) { + hsUrl = wellknown["m.homeserver"]["base_url"]; + console.log(`Overrode homeserver setting with ${hsUrl} from login response`); + } + if (wellknown["m.identity_server"] && wellknown["m.identity_server"]["base_url"]) { + // TODO: should we prompt here? + isUrl = wellknown["m.identity_server"]["base_url"]; + console.log(`Overrode IS setting with ${isUrl} from login response`); + } + } + return { homeserverUrl: hsUrl, identityServerUrl: isUrl, From f5ff580e3506286be0887baa4cb84f4962355fa9 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 21 Dec 2018 14:51:05 -0700 Subject: [PATCH 191/237] Add some logging for riot-web#7838 See https://github.com/vector-im/riot-web/issues/7838 --- src/components/views/rooms/MessageComposer.js | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 2fc35d80cc..f7a67c2189 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -349,6 +349,34 @@ export default class MessageComposer extends React.Component { const canSendMessages = !this.state.tombstone && this.props.room.maySendMessage(); + // TODO: Remove temporary logging for riot-web#7838 + // Note: we rip apart the power level event ourselves because we don't want to + // log too much data about it - just the bits we care about. Many of the variables + // logged here are to help figure out where in the stack the 'cannot post in room' + // warning is coming from. This means logging various numbers from the PL event to + // verify RoomState._maySendEventOfType is doing the right thing. + const room = this.props.room; + const plEvent = room.currentState.getStateEvents('m.room.power_levels', ''); + let plEventString = ""; + if (plEvent) { + const content = plEvent.getContent(); + if (!content) { + plEventString = ""; + } else { + const stringifyFalsey = (v) => v === null ? '' : (v === undefined ? '' : v); + const actualUserPl = stringifyFalsey(content.users ? content.users[room.myUserId] : ""); + const usersPl = stringifyFalsey(content.users_default); + const actualEventPl = stringifyFalsey(content.events ? content.events['m.room.message'] : ""); + const eventPl = stringifyFalsey(content.events_default); + plEventString = `actualUserPl=${actualUserPl} defaultUserPl=${usersPl} actualEventPl=${actualEventPl} defaultEventPl=${eventPl}`; + } + } + console.log( + `[riot-web#7838] renderComposer() hasTombstone=${!!this.state.tombstone} maySendMessage=${room.maySendMessage()}` + + ` myMembership=${room.getMyMembership()} maySendEvent=${room.currentState.maySendEvent('m.room.message', room.myUserId)}` + + ` myUserId=${room.myUserId} roomId=${room.roomId} hasPlEvent=${!!plEvent} powerLevels='${plEventString}'` + ); + if (canSendMessages) { // This also currently includes the call buttons. Really we should // check separately for whether we can call, but this is slightly @@ -425,6 +453,8 @@ export default class MessageComposer extends React.Component {
    ); } else { + // TODO: Remove temporary logging for riot-web#7838 + console.log("[riot-web#7838] Falling back to showing cannot post in room error"); controls.push(
    { _t('You do not have permission to post to this room') } From df89d973a06a68536cc414c192b6ff247f513def Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 21 Dec 2018 18:24:10 -0700 Subject: [PATCH 192/237] Fetch matching e2e-test branch --- .travis-test-riot.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis-test-riot.sh b/.travis-test-riot.sh index 88b3719b3a..e1eb186ea5 100755 --- a/.travis-test-riot.sh +++ b/.travis-test-riot.sh @@ -30,7 +30,7 @@ popd if [ "$TRAVIS_BRANCH" = "develop" ] then # run end to end tests - git clone https://github.com/matrix-org/matrix-react-end-to-end-tests.git --branch master + scripts/fetchdep.sh matrix-org matrix-react-end-to-end-tests pushd matrix-react-end-to-end-tests ln -s $REACT_SDK_DIR/$RIOT_WEB_DIR riot/riot-web # PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true ./install.sh From 310f634a0d24d5866b96ae387420200a0b8618a4 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 21 Dec 2018 19:16:37 -0700 Subject: [PATCH 193/237] Ensure we install the master branch of the e2e tests as a default --- .travis-test-riot.sh | 2 +- scripts/fetchdep.sh | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.travis-test-riot.sh b/.travis-test-riot.sh index e1eb186ea5..d1c2804b2a 100755 --- a/.travis-test-riot.sh +++ b/.travis-test-riot.sh @@ -30,7 +30,7 @@ popd if [ "$TRAVIS_BRANCH" = "develop" ] then # run end to end tests - scripts/fetchdep.sh matrix-org matrix-react-end-to-end-tests + scripts/fetchdep.sh matrix-org matrix-react-end-to-end-tests master pushd matrix-react-end-to-end-tests ln -s $REACT_SDK_DIR/$RIOT_WEB_DIR riot/riot-web # PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true ./install.sh diff --git a/scripts/fetchdep.sh b/scripts/fetchdep.sh index 73c622133b..f951c5b341 100755 --- a/scripts/fetchdep.sh +++ b/scripts/fetchdep.sh @@ -4,6 +4,9 @@ set -e org="$1" repo="$2" +defbranch="$3" + +[ -z "$defbranch" ] && defbranch="develop" rm -r "$repo" || true @@ -19,4 +22,4 @@ then fi echo "Checking out develop branch" -git clone https://github.com/$org/$repo.git $repo --branch develop +git clone https://github.com/$org/$repo.git $repo --branch $defbranch From 384320e29f84128a345af08380536727a4046ff1 Mon Sep 17 00:00:00 2001 From: Christopher Medlin Date: Mon, 24 Dec 2018 14:46:36 -0800 Subject: [PATCH 194/237] Consistently order flairs based on room configuration. --- src/components/views/messages/SenderProfile.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/messages/SenderProfile.js b/src/components/views/messages/SenderProfile.js index 0f767675e2..bcad4d8c82 100644 --- a/src/components/views/messages/SenderProfile.js +++ b/src/components/views/messages/SenderProfile.js @@ -84,8 +84,8 @@ export default React.createClass({ _getDisplayedGroups(userGroups, relatedGroups) { let displayedGroups = userGroups || []; if (relatedGroups && relatedGroups.length > 0) { - displayedGroups = displayedGroups.filter((groupId) => { - return relatedGroups.includes(groupId); + displayedGroups = relatedGroups.filter((groupId) => { + return displayedGroups.includes(groupId); }); } else { displayedGroups = []; From 135a0884c92373214b3c8bcd0a82bf7e35a3556a Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 28 Dec 2018 19:42:13 -0700 Subject: [PATCH 195/237] Update scripts/fetchdep.sh Co-Authored-By: turt2live --- scripts/fetchdep.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/fetchdep.sh b/scripts/fetchdep.sh index f951c5b341..4f42859439 100755 --- a/scripts/fetchdep.sh +++ b/scripts/fetchdep.sh @@ -21,5 +21,5 @@ then git clone https://github.com/$org/$repo.git $repo --branch "$curbranch" && exit 0 fi -echo "Checking out develop branch" +echo "Checking out default branch $defbranch" git clone https://github.com/$org/$repo.git $repo --branch $defbranch From 00405e7f2220b7a96f66fe6d51074c248fddf98c Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 31 Dec 2018 09:59:08 -0600 Subject: [PATCH 196/237] Use Nunito font from the server only There are various versions of Nunito in circulation, and some have errors in their metrics or smaller supported character sets. To ensure all users get the expected experience, don't allow local copies of Nunito to be used. Fixes vector-im/riot-web#7959. --- res/themes/dharma/css/_fonts.scss | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/res/themes/dharma/css/_fonts.scss b/res/themes/dharma/css/_fonts.scss index 392a10206b..bb45432262 100644 --- a/res/themes/dharma/css/_fonts.scss +++ b/res/themes/dharma/css/_fonts.scss @@ -2,7 +2,7 @@ * Nunito. * Includes extended Latin and Vietnamese character sets * Current URLs are v9, derived from the contents of - * https://fonts.googleapis.com/css?family=Nunito:400,400i,600,600i,700,700i&subset=latin-ext,vietnamese + * https://fonts.googleapis.com/css?family=Nunito:400,400i,600,600i,700,700i&subset=latin-ext,vietnamese */ /* the 'src' links are relative to the bundle.css, which is in a subdirectory. @@ -11,37 +11,37 @@ font-family: 'Nunito'; font-style: italic; font-weight: 400; - src: local('Nunito Italic'), local('Nunito-Italic'), url('../../fonts/Nunito/XRXX3I6Li01BKofIMNaDRss.ttf') format('truetype'); + src: url('../../fonts/Nunito/XRXX3I6Li01BKofIMNaDRss.ttf') format('truetype'); } @font-face { font-family: 'Nunito'; font-style: italic; font-weight: 600; - src: local('Nunito SemiBold Italic'), local('Nunito-SemiBoldItalic'), url('../../fonts/Nunito/XRXQ3I6Li01BKofIMN5cYtvKUTo.ttf') format('truetype'); + src: url('../../fonts/Nunito/XRXQ3I6Li01BKofIMN5cYtvKUTo.ttf') format('truetype'); } @font-face { font-family: 'Nunito'; font-style: italic; font-weight: 700; - src: local('Nunito Bold Italic'), local('Nunito-BoldItalic'), url('../../fonts/Nunito/XRXQ3I6Li01BKofIMN44Y9vKUTo.ttf') format('truetype'); + src: url('../../fonts/Nunito/XRXQ3I6Li01BKofIMN44Y9vKUTo.ttf') format('truetype'); } @font-face { font-family: 'Nunito'; font-style: normal; font-weight: 400; - src: local('Nunito Regular'), local('Nunito-Regular'), url('../../fonts/Nunito/XRXV3I6Li01BKofINeaE.ttf') format('truetype'); + src: url('../../fonts/Nunito/XRXV3I6Li01BKofINeaE.ttf') format('truetype'); } @font-face { font-family: 'Nunito'; font-style: normal; font-weight: 600; - src: local('Nunito SemiBold'), local('Nunito-SemiBold'), url('../../fonts/Nunito/XRXW3I6Li01BKofA6sKUYevN.ttf') format('truetype'); + src: url('../../fonts/Nunito/XRXW3I6Li01BKofA6sKUYevN.ttf') format('truetype'); } @font-face { font-family: 'Nunito'; font-style: normal; font-weight: 700; - src: local('Nunito Bold'), local('Nunito-Bold'), url('../../fonts/Nunito/XRXW3I6Li01BKofAjsOUYevN.ttf') format('truetype'); + src: url('../../fonts/Nunito/XRXW3I6Li01BKofAjsOUYevN.ttf') format('truetype'); } /* From 402f58225f6a87032be71fa0ee854f09a44ecf29 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 2 Jan 2019 15:32:14 -0600 Subject: [PATCH 197/237] Show in-room reminder when key backup creating device unverified If the current device hasn't verified the device that created the account's current key backup version, then the current device is won't use the key backup. This change adjusts an existing in-room reminder to do the right thing for this case by allowing the user to verify the device that created the key backup. Fixes vector-im/riot-web#7902. --- src/components/structures/RoomView.js | 7 ++ .../views/rooms/RoomRecoveryReminder.js | 101 ++++++++++++++++-- src/i18n/strings/en_EN.json | 3 +- 3 files changed, 102 insertions(+), 9 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 0e0d56647d..dbfd6a2775 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -161,6 +161,7 @@ module.exports = React.createClass({ MatrixClientPeg.get().on("RoomState.members", this.onRoomStateMember); MatrixClientPeg.get().on("Room.myMembership", this.onMyMembership); MatrixClientPeg.get().on("accountData", this.onAccountData); + MatrixClientPeg.get().on("crypto.keyBackupStatus", this.onKeyBackupStatus); this._fetchMediaConfig(); // Start listening for RoomViewStore updates this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate); @@ -449,6 +450,7 @@ module.exports = React.createClass({ MatrixClientPeg.get().removeListener("Room.myMembership", this.onMyMembership); MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember); MatrixClientPeg.get().removeListener("accountData", this.onAccountData); + MatrixClientPeg.get().removeListener("crypto.keyBackupStatus", this.onKeyBackupStatus); } window.removeEventListener('beforeunload', this.onPageUnload); @@ -618,6 +620,11 @@ module.exports = React.createClass({ false, ); } + }, + + onKeyBackupStatus() { + // Key backup status changes affect whether the in-room recovery + // reminder is displayed. this.forceUpdate(); }, diff --git a/src/components/views/rooms/RoomRecoveryReminder.js b/src/components/views/rooms/RoomRecoveryReminder.js index 265bfd3ee3..d03c5fc96d 100644 --- a/src/components/views/rooms/RoomRecoveryReminder.js +++ b/src/components/views/rooms/RoomRecoveryReminder.js @@ -19,13 +19,76 @@ import PropTypes from "prop-types"; import sdk from "../../../index"; import { _t } from "../../../languageHandler"; import Modal from "../../../Modal"; +import MatrixClientPeg from "../../../MatrixClientPeg"; export default class RoomRecoveryReminder extends React.PureComponent { static propTypes = { onFinished: PropTypes.func.isRequired, } - showKeyBackupDialog = () => { + constructor(props) { + super(props); + + this.state = { + loading: true, + error: null, + unverifiedDevice: null, + }; + } + + componentWillMount() { + this._loadBackupStatus(); + } + + async _loadBackupStatus() { + let backupSigStatus; + try { + const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion(); + backupSigStatus = await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo); + } catch (e) { + console.log("Unable to fetch key backup status", e); + this.setState({ + loading: false, + error: e, + }); + return; + } + + let unverifiedDevice; + for (const sig of backupSigStatus.sigs) { + if (!sig.device.isVerified()) { + unverifiedDevice = sig.device; + break; + } + } + this.setState({ + loading: false, + unverifiedDevice, + }); + } + + showSetupDialog = () => { + if (this.state.unverifiedDevice) { + // A key backup exists for this account, but the creating device is not + // verified, so we'll show the device verify dialog. + // TODO: Should change to a restore key backup flow that checks the recovery + // passphrase while at the same time also cross-signing the device as well in + // a single flow (for cases where a key backup exists but the backup creating + // device is unverified). Since we don't have that yet, we'll look for an + // unverified device and verify it. Note that this means we won't restore + // keys yet; instead we'll only trust the backup for sending our own new keys + // to it. + const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog'); + Modal.createTrackedDialog('Device Verify Dialog', '', DeviceVerifyDialog, { + userId: MatrixClientPeg.get().credentials.userId, + device: this.state.unverifiedDevice, + onFinished: this.props.onFinished, + }); + return; + } + + // The default case assumes that a key backup doesn't exist for this account, so + // we'll show the create key backup flow. Modal.createTrackedDialogAsync("Key Backup", "Key Backup", import("../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog"), { @@ -46,29 +109,51 @@ export default class RoomRecoveryReminder extends React.PureComponent { this.props.onFinished(false); }, onSetup: () => { - this.showKeyBackupDialog(); + this.showSetupDialog(); }, }, ); } onSetupClick = () => { - this.showKeyBackupDialog(); + this.showSetupDialog(); } render() { + if (this.state.loading) { + return null; + } + const AccessibleButton = sdk.getComponent("views.elements.AccessibleButton"); + let body; + if (this.state.error) { + body =
    + {_t("Unable to load key backup status")} +
    ; + } else if (this.state.unverifiedDevice) { + // A key backup exists for this account, but the creating device is not + // verified. + body = _t( + "To view your secure message history and ensure you can view new " + + "messages on future devices, set up Secure Message Recovery.", + ); + } else { + // The default case assumes that a key backup doesn't exist for this account. + // (This component doesn't currently check that itself.) + body = _t( + "If you log out or use another device, you'll lose your " + + "secure message history. To prevent this, set up Secure " + + "Message Recovery.", + ); + } + return (
    {_t( "Secure Message Recovery", )}
    -
    {_t( - "If you log out or use another device, you'll lose your " + - "secure message history. To prevent this, set up Secure " + - "Message Recovery.", - )}
    +
    {body}
    diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 81fcd963b3..ef659bf566 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -569,8 +569,9 @@ "You are trying to access a room.": "You are trying to access a room.", "Click here to join the discussion!": "Click here to join the discussion!", "This is a preview of this room. Room interactions have been disabled": "This is a preview of this room. Room interactions have been disabled", - "Secure Message Recovery": "Secure Message Recovery", + "To view your secure message history and ensure you can view new messages on future devices, set up Secure Message Recovery.": "To view your secure message history and ensure you can view new messages on future devices, set up Secure Message Recovery.", "If you log out or use another device, you'll lose your secure message history. To prevent this, set up Secure Message Recovery.": "If you log out or use another device, you'll lose your secure message history. To prevent this, set up Secure Message Recovery.", + "Secure Message Recovery": "Secure Message Recovery", "Don't ask again": "Don't ask again", "Set up": "Set up", "To change the room's avatar, you must be a": "To change the room's avatar, you must be a", From 22167f8538ddce5dba81b8d26afd0d30cf0ba9a1 Mon Sep 17 00:00:00 2001 From: Stephen Solka Date: Mon, 24 Dec 2018 21:02:36 -0500 Subject: [PATCH 198/237] upgrade expect to 23.6.0 Signed-off-by: Stephen Solka --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7a63d55415..c902829a7d 100644 --- a/package.json +++ b/package.json @@ -124,7 +124,7 @@ "eslint-plugin-flowtype": "^2.30.0", "eslint-plugin-react": "^7.7.0", "estree-walker": "^0.5.0", - "expect": "^1.16.0", + "expect": "^23.6.0", "flow-parser": "^0.57.3", "karma": "^3.0.0", "karma-chrome-launcher": "^0.2.3", From c3185a4cdbca67764237c771c0ac2245c79bf588 Mon Sep 17 00:00:00 2001 From: Stephen Solka Date: Mon, 24 Dec 2018 21:13:09 -0500 Subject: [PATCH 199/237] breaking changes from expect upgrade Signed-off-by: Stephen Solka --- test/DecryptionFailureTracker-test.js | 2 +- test/components/structures/GroupView-test.js | 40 +++++++++---------- .../dialogs/InteractiveAuthDialog-test.js | 4 +- .../views/groups/GroupMemberList-test.js | 4 +- test/components/views/rooms/RoomList-test.js | 4 +- test/matrix-to-test.js | 30 +++++++------- 6 files changed, 42 insertions(+), 42 deletions(-) diff --git a/test/DecryptionFailureTracker-test.js b/test/DecryptionFailureTracker-test.js index 617c9d5d68..baa0545f77 100644 --- a/test/DecryptionFailureTracker-test.js +++ b/test/DecryptionFailureTracker-test.js @@ -54,7 +54,7 @@ describe('DecryptionFailureTracker', function() { // Immediately track the newest failures tracker.trackFailures(); - expect(count).toNotBe(0, 'should track a failure for an event that failed decryption'); + expect(count).not.toBe(0, 'should track a failure for an event that failed decryption'); done(); }); diff --git a/test/components/structures/GroupView-test.js b/test/components/structures/GroupView-test.js index 89632dcc48..b49c335bdf 100644 --- a/test/components/structures/GroupView-test.js +++ b/test/components/structures/GroupView-test.js @@ -185,21 +185,21 @@ describe('GroupView', function() { const avatar = ReactTestUtils.findRenderedComponentWithType(root, sdk.getComponent('avatars.GroupAvatar')); const img = ReactTestUtils.findRenderedDOMComponentWithTag(avatar, 'img'); const avatarImgElement = ReactDOM.findDOMNode(img); - expect(avatarImgElement).toExist(); - expect(avatarImgElement.src).toInclude( + expect(avatarImgElement).toBeTruthy(); + expect(avatarImgElement.src).toContain( 'https://my.home.server/_matrix/media/v1/thumbnail/' + 'someavatarurl?width=48&height=48&method=crop', ); const name = ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView_header_name'); const nameElement = ReactDOM.findDOMNode(name); - expect(nameElement).toExist(); - expect(nameElement.innerText).toInclude('The name of a community'); - expect(nameElement.innerText).toInclude(groupId); + expect(nameElement).toBeTruthy(); + expect(nameElement.innerText).toContain('The name of a community'); + expect(nameElement.innerText).toContain(groupId); const shortDesc = ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView_header_shortDesc'); const shortDescElement = ReactDOM.findDOMNode(shortDesc); - expect(shortDescElement).toExist(); + expect(shortDescElement).toBeTruthy(); expect(shortDescElement.innerText).toBe('This is a community'); }); @@ -219,7 +219,7 @@ describe('GroupView', function() { const longDesc = ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView_groupDesc'); const longDescElement = ReactDOM.findDOMNode(longDesc); - expect(longDescElement).toExist(); + expect(longDescElement).toBeTruthy(); expect(longDescElement.innerText).toBe('This is a LONG description.'); expect(longDescElement.innerHTML).toBe('
    This is a LONG description.
    '); }); @@ -239,7 +239,7 @@ describe('GroupView', function() { const placeholder = ReactTestUtils .findRenderedDOMComponentWithClass(root, 'mx_GroupView_groupDesc_placeholder'); const placeholderElement = ReactDOM.findDOMNode(placeholder); - expect(placeholderElement).toExist(); + expect(placeholderElement).toBeTruthy(); }); httpBackend @@ -258,15 +258,15 @@ describe('GroupView', function() { const prom = waitForUpdate(groupView, 4).then(() => { const longDesc = ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView_groupDesc'); const longDescElement = ReactDOM.findDOMNode(longDesc); - expect(longDescElement).toExist(); + expect(longDescElement).toBeTruthy(); - expect(longDescElement.innerHTML).toInclude('

    This is a more complicated group page

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

    With paragraphs

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

      This is a more complicated group page

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

      With paragraphs

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

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

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

        +
        + {strengthMeter} + {helpText} +

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

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

        -

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

        +

        {bodyText}

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

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

        +

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

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

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

        +

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

        +

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

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

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

        { _t('Updates') }

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

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

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

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

        { _t('Create an account') }

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

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

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

        { _t("Invited") }

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

        {_t("Invited")}

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

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

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

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

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

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

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

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

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

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

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

        HTML for your community's page

        \n

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

        \n

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

        \n": "

        HTML për faqen e bashkësisë tuaj

        \n

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

        \n

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

        \n", + "Failed to add the following rooms to the summary of %(groupId)s:": "S’u arrit të shtoheshin dhomat vijuese te përmbledhja e %(groupId)s:", + "Failed to remove the room from the summary of %(groupId)s": "S’u arrit të hiqej dhoma prej përmbledhjes së %(groupId)s", + "Failed to remove a user from the summary of %(groupId)s": "S’u arrit të hiqej një përdorues nga përmbledhja e %(groupId)s", + "Changes made to your community name and avatar might not be seen by other users for up to 30 minutes.": "Ndryshimet e bëra te emri dhe avatari i bashkësisë tuaj mund të mos shihen nga përdoruesit e tjera para deri 30 minutash.", + "Can't leave Server Notices room": "Dhoma Njoftime Shërbyesi, s’braktiset dot", + "For security, this session has been signed out. Please sign in again.": "Për hir të sigurisë, është bërë dalja nga ky sesion. Ju lutemi, ribëni hyrjen.", + "Data from an older version of Riot has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.": "Janë pikasur të dhëna nga një version i dikurshëm i Riot-it. Kjo do të bëjë që kriptografia skaj-më-skaj te versioni i dikurshëm të mos punojë si duhet. Mesazhet e fshehtëzuar skaj-më-skaj tani së fundi teksa përdorej versioni i dikurshëm mund të mos jenë të shfshehtëzueshëm në këtë version. Kjo mund bëjë edhe që mesazhet e shkëmbyera me këtë version të dështojnë. Nëse ju dalin probleme, bëni daljen dhe rihyni në llogari. Që të ruhet historiku i mesazheve, eksportoni dhe ri-importoni kyçet tuaj.", + "Did you know: you can use communities to filter your Riot.im experience!": "E dinit se: mund t’i përdorni bashkësitë për të filtruar punimin tuaj në Riot.im?", + "Error whilst fetching joined communities": "Gabim teksa silleshin bashkësitë ku merret pjesë", + "Show devices, send anyway or cancel.": "Shfaq pajisje, dërgoje sido qoftë ose anuloje.", + "%(count)s Resend all or cancel all now. You can also select individual messages to resend or cancel.|other": "Ridërgojini krejt ose anulojini krejt tani. Për ridërgim ose anulim, mundeni edhe të përzgjidhni mesazhe individualë.", + "%(count)s Resend all or cancel all now. You can also select individual messages to resend or cancel.|one": "Ridërgojeni mesazhin ose anulojeni mesazhin tani.", + "For security, logging out will delete any end-to-end encryption keys from this browser. If you want to be able to decrypt your conversation history from future Riot sessions, please export your room keys for safe-keeping.": "Për hir të sigurisë, dalja nga llogaria do të sjellë fshirjen në këtë shfletues të çfarëdo kyçesh fshehtëzimi skaj-më-skaj. Nëse doni të jeni në gjendje të fshehtëzoni historikun e bisedave tuaja që nga sesione të ardhshëm Riot, ju lutemi, eksportoni kyçet tuaj të dhomës, për t’i ruajtur të parrezikuar diku.", + "Audio Output": "Sinjal Audio", + "Error: Problem communicating with the given homeserver.": "Gabimr: Problem komunikimi me shërbyesin e dhënë Home.", + "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or enable unsafe scripts.": "S’lidhet dot te shërbyes Home përmes HTTP-je, kur te shtylla e shfletuesit tuaj jepet një URL HTTPS. Ose përdorni HTTPS-në, ose aktivizoni përdorimin e programtheve jo të sigurt.", + "Can't connect to homeserver - please check your connectivity, ensure your homeserver's SSL certificate is trusted, and that a browser extension is not blocking requests.": "S’lidhet dot te shërbyes Home - ju lutemi, kontrolloni lidhjen tuaj, sigurohuni që dëshmia SSL e shërbyesit tuaj Home besohet, dhe që s’ka ndonjë zgjerim shfletuesi që po bllokon kërkesat tuaja.", + "Failed to remove tag %(tagName)s from room": "S’u arrit të hiqej etiketa %(tagName)s nga dhoma", + "Failed to add tag %(tagName)s to room": "S’u arrit të shtohej në dhomë etiketa %(tagName)s", + "Pin unread rooms to the top of the room list": "Fiksoji dhomat e palexuara në krye të listës së dhomave", + "Pin rooms I'm mentioned in to the top of the room list": "Fiksoji dhomat ku përmendem në krye të listës së dhomave", + "Enable widget screenshots on supported widgets": "Aktivizo foto ekrani widget-esh për widget-e që e mbulojnë", + "Changing password will currently reset any end-to-end encryption keys on all devices, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "Ndryshimi i fjalëkalimit do të sjellë zerimin e çfarëdo kyçesh fshehtëzimi skaj-më-skaj në krejt pajisjet, duke e bërë të palexueshëm historikun e fshehtëzuar të bisedave, hiq rastin kur i eksportoni më parë kyçet tuaj të dhomës dhe i ri-importoni ata më pas. Në të ardhmen kjo do të përmirësohet.", + "Join as voice or video.": "Merrni pjesë me ose me video.", + "Key share requests are sent to your other devices automatically. If you rejected or dismissed the key share request on your other devices, click here to request the keys for this session again.": "Kërkesat për ndarje kyçesh dërgohen automatikisht te pajisjet tuaja të tjera. Nëse s’e pranuat ose e hodhët tej kërkesën për ndarje kyçesh në pajisjet tuaja të tjera, klikoni këtu që të rikërkoni kyçe për këtë sesion.", + "If your other devices do not have the key for this message you will not be able to decrypt them.": "Nëse pajisjet tuaja të tjera nuk kanë kyçin për këtë mesazh, s’do të jeni në gjendje ta shfshehtëzoni.", + "Demote yourself?": "Të zhgradohet vetvetja?", + "Demote": "Zhgradoje", + "Failed to toggle moderator status": "S’u arrit të këmbehet gjendje moderatori", + "Server unavailable, overloaded, or something else went wrong.": "Shërbyesi është i pakapshëm, i mbingarkuar, ose diç tjetër shkoi ters.", + "Drop here to tag %(section)s": "Hidheni këtu që të caktohet etiketë për %(section)s", + "Press to start a chat with someone": "Shtypni që të nisni një bisedë me dikë", + "No users have specific privileges in this room": "S’ka përdorues me privilegje të caktuara në këtë dhomë", + "Guests cannot join this room even if explicitly invited.": "Vizitorët s’mund të marrin pjesë në këtë edhe po të jenë ftuar shprehimisht.", + "Publish this room to the public in %(domain)s's room directory?": "Të bëhet publike kjo dhomë te drejtoria e dhomave %(domain)s?", + "Click here to upgrade to the latest room version and ensure room integrity is protected.": "Klikoni këtu që ta përmirësoni me versionin më të ri të dhomë dhe të garantoni mbrojtjen e paprekshmërisë së dhomës.", + "In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.": "Në dhoma të fshehtëzuara, si kjo, paraparja e URL-ve është e çaktivizuar, si parazgjedhje, për të garantuar që shërbyesi juaj home (ku edhe prodhohen paraparjet) të mos grumbullojë të dhëna rreth lidhjesh që shihni në këtë dhomë.", + "Please review and accept the policies of this homeserver:": "Ju lutemi, shqyrtoni dhe pranoni rregullat e këtij shërbyesi home:", + "If you don't specify an email address, you won't be able to reset your password. Are you sure?": "Nëse nuk përcaktoni një adresë email, s’do të jeni në gjendje të bëni ricaktime të fjalëkalimit tuaj. Jeni i sigurt?", + "Removing a room from the community will also remove it from the community page.": "Heqja e një dhome nga bashkësia do ta heqë atë edhe nga faqja e bashkësisë.", + "Please help improve Riot.im by sending anonymous usage data. This will use a cookie (please see our Cookie Policy).": "Ju lutemi, ndihmoni të përmirësohet Riot.im duke dërguar të dhëna anonime përdorimi. Për këtë do të përdoret një cookie (ju lutemi, shihni Rregullat tona mbi Cookie-t).", + "Please help improve Riot.im by sending anonymous usage data. This will use a cookie.": "Ju lutemi, ndihmoni të përmirësohet Riot.im duke dërguar të dhëna anonime përdorimi. Për këtë do të përdoret një cookie.", + "Please contact your service administrator to get this limit increased.": "Ju lutemi, që të shtohet ky kufi, lidhuni me përgjegjësin e shërbimit.", + "If the other version of Riot is still open in another tab, please close it as using Riot on the same host with both lazy loading enabled and disabled simultaneously will cause issues.": "Nëse versioni tjetër i Riot-it është ende i hapur në një skedë tjetër, ju lutemi, mbylleni, ngaqë përdorimi njëkohësisht i Riot-it në të njëjtën strehë, në njërën anë me lazy loading të aktivizuar dhe në anën tjetër të çaktivizuar do të shkaktojë probleme.", + "Riot now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!": "Riot-i tani përdor 3 deri 5 herë më pak kujtesë, duke ngarkuar të dhëna mbi përdorues të tjerë vetëm kur duhen. Ju lutemi, prisni, teksa njëkohësojmë të dhënat me shërbyesin!", + "Put a link back to the old room at the start of the new room so people can see old messages": "Vendosni në krye të dhomës së re një lidhje për te dhoma e vjetër, që njerëzit të mund të shohin mesazhet e vjetër", + "Log out and remove encryption keys?": "Të dilet dhe të hiqen kyçet e fshehtëzimit?", + "If you have previously used a more recent version of Riot, your session may be incompatible with this version. Close this window and return to the more recent version.": "Nëse më herët keni përdorur një version më të freskët të Riot-it, sesioni juaj mund të jetë i papërputhshëm me këtë version. Mbylleni këtë dritare dhe kthehuni te versioni më i ri.", + "Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.": "Pastrimi i gjërave të depozituara në shfletuesin tuaj mund ta ndreqë problemin, por kjo do të sjellë nxjerrjen tuaj nga llogari dhe do ta bëjë të palexueshëm çfarëdo historiku të fshehtëzuar të bisedës.", + "If you would like to create a Matrix account you can register now.": "Nëse do të donit të krijoni një llogari Matrix, mund të regjistroheni që tani.", + "If you already have a Matrix account you can log in instead.": "Nëse keni tashmë një llogari Matrix, mund të bëni hyrjen.", + "Share message history with new users": "Ndani me përdorues të rinj historik mesazhesh", + "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Krijoni një bashkësi që bëni tok përdorues dhe dhoma! Krijoni një faqe hyrëse vetjake, që të ravijëzoni hapësirën tuaj në universin Matrix.", + "Sent messages will be stored until your connection has returned.": "Mesazhet e dërguar do të depozitohen deri sa lidhja juaj të jetë rikthyer.", + "Server may be unavailable, overloaded, or the file too big": "Shërbyesi mund të jetë i pakapshëm, i mbingarkuar, ose kartela është shumë e madhe", + "Server may be unavailable, overloaded, or search timed out :(": "Shërbyesi mund të jetë i pakapshëm, i mbingarkuar, ose kërkimit i mbaroi koha :(", + "If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Nëse parashtruar një të metë përmes GitHub-it, regjistrat e diagnostikimit mund të na ndihmojnë të ndjekim problemin. Regjistrat e diagnostikimit përmbajnë të dhëna përdorimi, përfshi emrin tuaj të përdoruesit, ID-të ose aliaset e dhomave apo grupeve që keni vizituar dhe emrat e përdoruesve të përdoruesve të tjerë. Në to nuk përmbahen mesazhet.", + "Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.": "Privatësia është e rëndësishme për ne, ndaj nuk grumbullojmë ndonjë të dhënë personale apo të identifikueshme për analizat tona.", + "Learn more about how we use analytics.": "Mësoni më tepër se si i përdorim analizat.", + "Lazy loading is not supported by your current homeserver.": "Lazy loading nuk mbulohet nga shërbyesi juaj i tanishëm Home.", + "Reject all %(invitedRooms)s invites": "Mos prano asnjë ftesë për në %(invitedRooms)s", + "Missing Media Permissions, click here to request.": "Mungojnë Leje Mediash, klikoni këtu që të kërkohen.", + "New passwords must match each other.": "Fjalëkalimet e rinj duhet të përputhen me njëri-tjetrin.", + "Please note you are logging into the %(hs)s server, not matrix.org.": "Ju lutemi, kini parasysh se jeni futur te shërbyesi %(hs)s, jo te matrix.org.", + "Guest access is disabled on this Home Server.": "Në këtë shërbyes Home është çaktivizuar hyrja si vizitor.", + "Password too short (min %(MIN_PASSWORD_LENGTH)s).": "Fjalëkalim shumë i shkurtër (minimumi %(MIN_PASSWORD_LENGTH)s).", + "You need to register to do this. Would you like to register now?": "Për ta bërë këtë, lypset të regjistroheni. Doni të regjistroheni që tani?", + "Stops ignoring a user, showing their messages going forward": "Resht shpërfilljen e një përdoruesi, duke i shfaqur mesazhet e tij të dërgohen", + "Verifies a user, device, and pubkey tuple": "Verifikon një përdorues, pajisje dhe një set kyçesh publikë", + "WARNING: Device already verified, but keys do NOT MATCH!": "KUJDES: Pajisje tashmë e verifikuar, por kyçet NUK PËRPUTHEN!", + "WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!": "KUJDES: VERIFIKIMI I KYÇIT DËSHTOI! Kyçi i nënshkrimit për %(userId)s dhe pajisjen %(deviceId)s është \"%(fprint)s\", që nuk përpythet me kyçin e dhënë \"%(fingerprint)s\". Kjo mund të jetë shenjë se komunikimet tuaja po përgjohen!", + "The signing key you provided matches the signing key you received from %(userId)s's device %(deviceId)s. Device marked as verified.": "Kyçi i nënshkrimit që dhatë përputhet me kyçin e nënshkrimit që morët nga pajisja e %(userId)s %(deviceId)s. Pajisja u shënua si e verifikuar.", + "Your browser does not support the required cryptography extensions": "Shfletuesi juaj nuk mbulon zgjerimet kriptografike të domosdoshme", + "Show timestamps in 12 hour format (e.g. 2:30pm)": "Vulat kohore shfaqi në formatin 12 orësh (p.sh. 2:30pm)", + "Enable inline URL previews by default": "Aktivizo, si parazgjedhje, paraparje URL-sh brendazi", + "Your home server does not support device management.": "Shërbyesi juaj Home nuk mbulon administrim pajisjesh.", + "The maximum permitted number of widgets have already been added to this room.": "Në këtë dhomë është shtuar tashmë numri maksimum i lejuar për widget-et.", + "Your key share request has been sent - please check your other devices for key share requests.": "Kërkesa juaj për shkëmbim kyçesh u dërgua - ju lutemi, kontrolloni pajisjet tuaja të tjera për kërkesa shkëmbimi kyçesh.", + "Undecryptable": "I pafshehtëzueshëm", + "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.": "S’do të jeni në gjendje ta zhbëni këtë, ngaqë po zhgradoni veten, nëse jeni përdoruesi i fundit i privilegjuar te dhoma do të jetë e pamundur të rifitoni privilegjet.", + "Jump to read receipt": "Hidhuni te leximi i faturës", + "Unable to reply": "S’arrihet të përgjigjet", + "Unknown for %(duration)s": "I panjohur për %(duration)s", + "You're not in any rooms yet! Press to make a room or to browse the directory": "S’jeni ende në ndonjë dhomë! Shtypni që të krijoni një dhomë ose që të shfletoni drejtorinë", + "Unable to ascertain that the address this invite was sent to matches one associated with your account.": "S’arrihet të sigurohet që adresa prej nga qe dërguar kjo ftesë përputhet me atë përshoqëruar llogarisë tuaj.", + "This invitation was sent to an email address which is not associated with this account:": "Kjo ftesë qe dërguar për një adresë email e cila nuk i përshoqërohet kësaj llogarie:", + "Would you like to accept or decline this invitation?": "Do të donit ta pranoni apo hidhni tej këtë ftesë?", + "To remove other users' messages, you must be a": "Që të hiqni mesazhe përdoruesish të tjerë, duhet të jeni një", + "This room is not accessible by remote Matrix servers": "Kjo dhomë nuk është e përdorshme nga shërbyes Matrix të largët", + "To send events of type , you must be a": "Që të dërgoni akte të llojit , duhet të jeni", + "This room version is vulnerable to malicious modification of room state.": "Ky version i dhomës është i cenueshëm nga modifikime dashakaqe të gjendjes së dhomës.", + "Stickerpack": "Paketë ngjitësish", + "You have enabled URL previews by default.": "E keni aktivizuar, si parazgjedhje, paraparjen e URL-ve.", + "You have disabled URL previews by default.": "E keni çaktivizuar, si parazgjedhje, paraparjen e URL-ve.", + "URL previews are enabled by default for participants in this room.": "Për pjesëmarrësit në këtë dhomë paraparja e URL-ve është e aktivizuar, si parazgjedhje.", + "URL previews are disabled by default for participants in this room.": "Për pjesëmarrësit në këtë dhomë paraparja e URL-ve është e çaktivizuar, si parazgjedhje.", + "When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.": "Kur dikush vë një URL në mesazh, për të dhënë rreth lidhjes më tepër të dhëna, të tilla si titulli, përshkrimi dhe një figurë e sajtit, do të shfaqet një paraparje e URL-së.", + "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?": "Ju ndan një hap nga shpënia te një sajt palë e tretë, që kështu të mund të mirëfilltësoni llogarinë tuaj me %(integrationsUrl)s. Doni të vazhdohet?", + "This allows you to use this app with an existing Matrix account on a different home server.": "Kjo ju lejon ta përdorni këtë aplikacion me një llogari Matrix ekxistuese në një shërbyes tjetër Home.", + "You can also set a custom identity server but this will typically prevent interaction with users based on email address.": "Mund të ujdisni edhe një shërbyes vetjak identitetesh, por kjo normalisht do të pengojë ndërveprim mes përdoruesish bazuar në adresë email.", + "The visibility of '%(roomName)s' in %(groupId)s could not be updated.": "Dukshmëria e '%(roomName)s' te %(groupId)s s’u përditësua dot.", + "Something went wrong when trying to get your communities.": "Diç shkoi ters teksa provohej të merreshin bashkësitë tuaja.", + "Warning: This widget might use cookies.": "Kujdes: Ky widget mund të përdorë cookies.", + "Unable to load event that was replied to, it either does not exist or you do not have permission to view it.": "S’arrihet të ngarkohet akti të cilit iu përgjigj, ose nuk ekziston, ose s’keni leje ta shihni.", + "Try using one of the following valid address types: %(validTypesList)s.": "Provoni të përdorni një nga llojet e vlefshme të adresave më poshtë: %(validTypesList)s.", + "You already have existing direct chats with this user:": "Keni tashmë fjalosje të drejtpërdrejta me këtë përdorues:", + "Something went wrong whilst creating your community": "Diç shkoi ters teksa krijohej bashkësia juaj", + "You've previously used Riot on %(host)s with lazy loading of members enabled. In this version lazy loading is disabled. As the local cache is not compatible between these two settings, Riot needs to resync your account.": "Më parë përdornit Riot në %(host)s me lazy loading anëtarësh të aktivizuar. Në këtë version lazy loading është çaktivizuar. Ngaqë fshehtina vendore s’është e përputhshme mes këtyre dy rregullimeve, Riot-i lyp të rinjëkohësohet llogaria juaj.", + "Upgrading this room requires closing down the current instance of the room and creating a new room it its place. To give room members the best possible experience, we will:": "Përmirësimi i kësaj dhome lyp mbylljen e instancës së tanishme të dhomës dhe krijimin në vend të saj të një dhome të re. Për t’u dhënë anëtareve të dhomës më të mirën e mundshme, do të:", + "Update any local room aliases to point to the new room": "Përditësoni çfarëdo aliasesh dhomash vendore që të shpien te dhoma e re", + "Stop users from speaking in the old version of the room, and post a message advising users to move to the new room": "Ndalojuni përdoruesve të flasin në versionin e vjetër të dhomës, dhe postoni një mesazh që u këshillon atyre të hidhen te dhoma e re", + "We encountered an error trying to restore your previous session.": "Hasëm një gabim teksa provohej të rikthehej sesioni juaj i dikurshëm.", + "This will allow you to reset your password and receive notifications.": "Kjo do t’ju lejojë të ricaktoni fjalëkalimin tuaj dhe të merrni njoftime.", + "This will be your account name on the homeserver, or you can pick a different server.": "Ky do të jetë emri i llogarisë tuaj te shërbyesi home, ose mund të zgjidhni një shërbyes tjetër.", + "You are currently using Riot anonymously as a guest.": "Hëpërhë po e përdorni Riot-in në mënyrë anonime, si një vizitor.", + "You are currently blacklisting unverified devices; to send messages to these devices you must verify them.": "Po kaloni në listë të zezë pajisje të paverifikuara; që të dërgoni mesazhe te këto pajisje, duhet t’i verifikoni.", + "We recommend you go through the verification process for each device to confirm they belong to their legitimate owner, but you can resend the message without verifying if you prefer.": "Këshillojmë të përshkoni procesin e verifikimit për çdo pajisje, që t’u bindur se u takojnë të zotëve të ligjshëm, por, nëse parapëlqeni, mund ta dërgoni mesazhin pa verifikuar gjë.", + "You must join the room to see its files": "Duhet të hyni në dhomë, pa të shihni kartelat e saj", + "The room '%(roomName)s' could not be removed from the summary.": "Dhoma '%(roomName)s' s’u hoq dot nga përmbledhja.", + "The user '%(displayName)s' could not be removed from the summary.": "Përdoruesi '%(displayName)s' s’u hoq dot nga përmbledhja.", + "You are an administrator of this community. You will not be able to rejoin without an invite from another administrator.": "Jeni një përgjegjës i kësaj bashkësie. S’do të jeni në gjendje të rihyni pa një ftesë nga një tjetër përgjegjës.", + "These rooms are displayed to community members on the community page. Community members can join the rooms by clicking on them.": "Këto dhoma u shfaqen anëtarëve të bashkësisë te faqja e bashkësisë. Anëtarët e bashkësisë mund të marrin pjesë në dhoma duke klikuar mbi to.", + "Your community hasn't got a Long Description, a HTML page to show to community members.
        Click here to open settings and give it one!": "Bashkësia juaj s’ka ndonjë Përshkrim të Gjatë, një faqe HTML për t’ua shfaqur anëtarëve të bashkësisë.
        Klikoni këtu që të hapni rregullimet dhe t’i krijoni një të tillë!", + "This room is not public. You will not be able to rejoin without an invite.": "Kjo dhomë s’është publike. S’do të jeni në gjendje të rihyni në të pa një ftesë.", + "This room is used for important messages from the Homeserver, so you cannot leave it.": "Kjo dhomë përdoret për mesazhe të rëndësishëm nga shërbyesi Home, ndaj s’mund ta braktisni.", + "To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "Që të ndërtoni një filtër, tërhiqeni avatarin e një bashkësie te paneli i filtrimeve në skajin e majtë të ekranit. Për të parë vetëm dhomat dhe personat e përshoqëruar asaj bashkësie, mund të klikoni në çfarëdo kohe mbi një avatar te panelit të filtrimeve.", + "You can't send any messages until you review and agree to our terms and conditions.": "S’mund të dërgoni ndonjë mesazh, përpara se të shqyrtoni dhe pajtoheni me termat dhe kushtet tona.", + "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please contact your service administrator to continue using the service.": "Mesazhi juaj s’u dërgua, ngaqë ky shërbyes Home ka mbërritur në Kufirin Mujor të Përdoruesve Aktivë. Ju lutemi, që të vazhdoni ta përdorni këtë shërbim, lidhuni me përgjegjësin e shërbimit tuaj.", + "Your message wasn't sent because this homeserver has exceeded a resource limit. Please contact your service administrator to continue using the service.": "Mesazhi juaj s’u dërgua, ngaqë ky shërbyes Home ka tejkaluar kufirin e një burimi. Ju lutemi, që të vazhdoni ta përdorni këtë shërbim, lidhuni me përgjegjësin e shërbimit tuaj.", + "There's no one else here! Would you like to invite others or stop warning about the empty room?": "S’ka njeri këtu! Do të donit të ftoni të tjerë apo të reshtet së njoftuari për dhomë të zbrazët?", + "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "U provua të ngarkohej një pikë e caktuar në kronologjinë e kësaj dhome, por nuk keni leje për ta parë mesazhin në fjalë.", + "Your password was successfully changed. You will not receive push notifications on other devices until you log back in to them": "Fjalëkalimi juaj u ndryshua me sukses. Nuk do të merrni njoftime push në pajisjet tuaja të tjera, veç në hyfshi sërish në llogarinë tuaj në to", + "Start automatically after system login": "Nisu vetvetiu pas hyrjes në sistem", + "You may need to manually permit Riot to access your microphone/webcam": "Lypset të lejoni dorazi Riot-in të përdorë mikrofonin/kamerën tuaj web", + "No Audio Outputs detected": "S’u pikasën Sinjale Audio Në Dalje", + "Resetting password will currently reset any end-to-end encryption keys on all devices, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "Ricaktimi i fjalëkalimit do të shkaktojë në fakt edhe zerimin e çfarëdo kyçi fshehtëzimesh skaj-më-skaj në krejt pajisjet, duke e bërë kështu të palexueshëm historikun e bisedës së fshehtëzuar, veç në paçi eksportuar më parë kyçet e dhomës tuaj dhe i rim-importoni më pas. Në të ardhmen kjo punë do të përmirësohet.", + "You have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, sign in again on each device": "Jeni nxjerrë jashtë krejt pajisjeve dhe nuk do të merrni më njoftime push. Që të riaktivizoni njoftimet, bëni sërish hyrjen në çdo pajisje", + "This Home Server does not support login using email address.": "Ky shërbyes Home nuk mbulon hyrje përmes adresash email.", + "This homeserver doesn't offer any login flows which are supported by this client.": "Ky shërbyes home nuk ofron ndonjë mënyrë hyrjesh që mbulohet nga ky klient.", + "Unable to query for supported registration methods": "S’arrihet të kërkohet për metoda regjistrimi që mbulohen", + "The exported file will allow anyone who can read it to decrypt any encrypted messages that you can see, so you should be careful to keep it secure. To help with this, you should enter a passphrase below, which will be used to encrypt the exported data. It will only be possible to import the data by using the same passphrase.": "Kartela e eksportuar do t’i lejojë kujtdo që e lexon të shfshehtëzojë çfarëdo mesazhesh të fshehtëzuar që mund të shihni, ndaj duhet të jeni i kujdesshëm për ta mbajtur të parrezikuar. Si ndihmë për këtë, duhet të jepni më poshtë një frazëkalim, që do të përdoret për të fshehtëzuar të dhënat e eksportuara. Importimi i të dhënave do të jetë i mundur vetëm duke përdorur të njëjtin frazëkalim.", + "Not a valid Riot keyfile": "S’është kartelë kyçesh Riot e vlefshme", + "Revoke Moderator": "Shfuqizoje Si Moderator", + "You have no historical rooms": "S’keni dhoma të dikurshme", + "Historical": "Të dikurshme", + "Flair": "Simbole", + "Showing flair for these communities:": "Shfaqen simbole për këto bashkësi:", + "This room is not showing flair for any communities": "Kjo dhomë nuk shfaq simbole për ndonjë bashkësi", + "Robot check is currently unavailable on desktop - please use a web browser": "Kontrolli për robot hëpërhë s’është i përdorshëm në desktop - ju lutemi, përdorni një shfletues", + "Please review and accept all of the homeserver's policies": "Ju lutemi, shqyrtoni dhe pranoni krejt rregullat e këtij shërbyesi home", + "Flair will appear if enabled in room settings": "Simbolet do të shfaqen nëse aktivizohen te rregullimet e dhomës", + "Flair will not appear": "Simbolet nuk do të shfaqen", + "Display your community flair in rooms configured to show it.": "Shfaqni simbolet e bashkësisë tuaj në dhoma të formësuara për t’i shfaqur ato.", + "Are you sure you wish to remove (delete) this event? Note that if you delete a room name or topic change, it could undo the change.": "Jeni i sigurt se doni të hiqet (fshihet) ky akt? Mbani parasysh se nëse fshini emrin e një dhome ose ndryshimin e temës, kjo mund të sjellë zhbërjen e ndryshimit.", + "To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of Riot to do this": "Që të shmanget humbja e historikut të fjalosjes tuaj, duhet të eksportoni kyçet e dhomës tuaj përpara se të dilni nga llogari. Që ta bëni këtë, duhe të riktheheni te versioni më i ri i Riot-it", + "You've previously used a newer version of Riot on %(host)s. To use this version again with end to end encryption, you will need to sign out and back in again. ": "Më parë përdorët një version më të ri të Riot-it në %(host)s. Që ta përdorni sërish këtë version me fshehtëzim skaj-më-skaj, duhet të dilni dhe rihyni te llogaria juaj. ", + "Incompatible Database": "Bazë të dhënash e Papërputhshme", + "Continue With Encryption Disabled": "Vazhdo Me Fshehtëzimin të Çaktivizuar", + "Unable to load! Check your network connectivity and try again.": "S’arrihet të ngarkohet! Kontrolloni lidhjen tuaj në rrjet dhe riprovoni.", + "Forces the current outbound group session in an encrypted room to be discarded": "Forces the current outbound group session in an encrypted room to be discarded", + "Backup of encryption keys to server": "Kopjeruajtje kyçesh fshehtëzimi në shërbyes", + "Delete Backup": "Fshije Kopjeruajtjen", + "Delete your backed up encryption keys from the server? You will no longer be able to use your recovery key to read encrypted message history": "Të fshihen nga shërbyesi kyçet e kopjeruajtur të fshehtëzimit? S’do të jeni më në gjendje të përdorni kyçin tuaj të rimarrjeve për lexim historiku mesazhesh të fshehtëzuar", + "Delete backup": "Fshije kopjeruajtjen", + "Unable to load key backup status": "S’arrihet të ngarkohet gjendje kopjeruajtjeje kyçesh", + "This device is uploading keys to this backup": "Kjo pajisje po ngarkon kyçe te kjo kopjeruajtje", + "This device is not uploading keys to this backup": "Kjo pajisje nuk po ngarkon kyçe te kjo kopjeruajtje", + "Backup has a valid signature from this device": "Kopjeruajtja ka një nënshkrim të vlefshëm prej kësaj pajisjeje", + "Backup has a valid signature from verified device x": "Kopjeruajtja ka një nënshkrim të vlefshëm prej pajisjes së verifikuar x", + "Backup has a valid signature from unverified device ": "Kopjeruajtja ka një nënshkrim të vlefshëm prej pajisjes së paverifikuar ", + "Backup has an invalid signature from verified device ": "Kopjeruajtja ka një nënshkrim të pavlefshëm prej pajisjes së verifikuar ", + "Backup has an invalid signature from unverified device ": "Kopjeruajtja ka një nënshkrim të pavlefshëm prej pajisjes së paverifikuar ", + "Backup is not signed by any of your devices": "Kopjeruajtja s’është nënshkruar nga ndonjë prej pajisjeve tuaja", + "Backup version: ": "Version kopjeruajtjeje: ", + "Algorithm: ": "Algoritëm: ", + "Restore backup": "Riktheje kopjeruajtjen", + "No backup is present": "S’ka kopjeruajtje të pranishëm", + "Start a new backup": "Filloni një kopjeruajtje të re", + "Secure your encrypted message history with a Recovery Passphrase.": "Sigurojeni historikun e mesazheve tuaj të fshehtëzuar me një Frazëkalim Rimarrjesh.", + "You'll need it if you log out or lose access to this device.": "Do t’ju duhet, nëse dilni nga llogaria ose nëse s’përdorni më dot pajisjen.", + "Enter a passphrase...": "Jepni një frazëkalim…", + "Next": "Pasuesja", + "If you don't want encrypted message history to be availble on other devices, .": "Nëse s’doni që historiku i mesazheve të fshehtëzuara të jetë i përdorshëm në pajisje të tjera, .", + "Or, if you don't want to create a Recovery Passphrase, skip this step and .": "Ose, nëse s’doni të krijohet një Frazëkalim Rimarrjesh, anashkalojeni këtë hap dhe .", + "That matches!": "U përputhën!", + "That doesn't match.": "S’përputhen.", + "Go back to set it again.": "Shkoni mbrapsht që ta ricaktoni.", + "Type in your Recovery Passphrase to confirm you remember it. If it helps, add it to your password manager or store it somewhere safe.": "Shtypeni Frazëkalimin tuaj të Rimarrjeve që të ripohoni se e mbani mend. Nëse bën punë, shtojeni te përgjegjësi juaj i fjalëkalimeve ose depozitojeni diku pa rrezik.", + "Repeat your passphrase...": "Përsëritni frazëkalimin tuaj…", + "Make a copy of this Recovery Key and keep it safe.": "Bëni një kopje të këtij Kyçi RImarrjesh dhe mbajeni të parrezikuar.", + "As a safety net, you can use it to restore your encrypted message history if you forget your Recovery Passphrase.": "Si rrjet i parrezikuar, mund ta përdoreni për të rikthyer historikun e mesazheve tuaj të fshehtëzuar, nëse harroni Frazëkalimin e Rimarrjeve.", + "Your Recovery Key": "Kyçi Juaj i Rimarrjeve", + "Copy to clipboard": "Kopjoje në të papastër", + "Download": "Shkarkoje", + "I've made a copy": "Kam bërë një kopje", + "Your Recovery Key has been copied to your clipboard, paste it to:": "Kyçi juaj i Fshehtëzimeve është kopjuar te e papastra juaj, ngjiteni te:", + "Your Recovery Key is in your Downloads folder.": "Kyçi juaj i Fshehtëzimeve gjendet te dosja juaj Shkarkime.", + "Print it and store it somewhere safe": "Shtypeni dhe ruajeni diku pa rrezik", + "Save it on a USB key or backup drive": "Ruajeni në një diskth USB ose disk kopjeruajtjesh", + "Copy it to your personal cloud storage": "Kopjojeni te depoja juaj personale në re", + "Got it": "E mora vesh", + "Backup created": "Kopjeruajtja u krijua", + "Your encryption keys are now being backed up to your Homeserver.": "Kyçet tuaj të fshehtëzimit tani po kopjeruhen te shërbyesi juaj Home.", + "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another device.": "Pa rregulluar Rimarrje të Siguruar, s’do të jeni në gjendje të riktheni historikun e mesazheve tuaj të fshehtëzuar, nëse bëni daljen ose përdorni një pajisje tjetër.", + "Set up Secure Message Recovery": "Rregulloni Rimarrje të Siguruar Mesazhesh", + "Create a Recovery Passphrase": "Krijoni Frazëkalim Rimarrjeje", + "Confirm Recovery Passphrase": "Ripohoni Frazëkalim Rimarrjeje", + "Recovery Key": "Kyç Rimarrjesh", + "Keep it safe": "Mbajeni të parrezikuar", + "Backing up...": "Po kopjeruhet…", + "Create Key Backup": "Krijo Kopjeruajtje Kyçesh", + "Unable to create key backup": "S’arrihet të krijojhet kopjeruajtje kyçesh", + "Retry": "Riprovo", + "Unable to load backup status": "S’arrihet të ngarkohet gjendje kopjeruajtjeje", + "Unable to restore backup": "S’arrihet të rikthehet kopjeruajtje", + "No backup found!": "S’u gjet kopjeruajtje!", + "Backup Restored": "Kopjeruajtja u Rikthye", + "Failed to decrypt %(failedCount)s sessions!": "S’u arrit të shfshehtëzohet sesioni %(failedCount)s!", + "Restored %(sessionCount)s session keys": "U rikthyen kyçet e sesionit %(sessionCount)s", + "Enter Recovery Passphrase": "Jepni Frazëkalim Rimarrjeje", + "Access your secure message history and set up secure messaging by entering your recovery passphrase.": "Hyni te historiku i mesazheve tuaj të siguruar dhe rregulloni shkëmbim mesazhesh të sigurt duke dhënë frazëkalimin tuaj të rimarrjeve.", + "If you've forgotten your recovery passphrase you can use your recovery key or set up new recovery options": "Nëse keni harruar frazëkalimin tuaj të rimarrjeve, mund të përdorni kyçin tuaj të rimarrjeve ose rregulloni mundësi të reja rimarrjeje", + "Enter Recovery Key": "Jepni Kyç Rimarrjeje", + "This looks like a valid recovery key!": "Ky duket si kyç i vlefshëm rimarrjesh!", + "Not a valid recovery key": "Kyç rimarrjesh jo i vlefshëm", + "Access your secure message history and set up secure messaging by entering your recovery key.": "Hyni te historiku i mesazheve tuaj të siguruar dhe rregulloni shkëmbim mesazhesh të sigurt duke dhënë kyçin tuaj të rimarrjeve.", + "If you've forgotten your recovery passphrase you can ": "Nëse keni harruar frazëkalimin tuaj të rimarrjeve, mund të rregulloni mundësi të reja rimarrjeje", + "Key Backup": "Kopjeruajtje Kyçi", + "Sign in with single sign-on": "Bëni hyrjen me hyrje njëshe", + "Disable Peer-to-Peer for 1:1 calls": "Çaktivizoje mekanizmin Peer-to-Peer për thirrje 1 me 1", + "Failed to perform homeserver discovery": "S’u arrit të kryhej zbulim shërbyesi Home", + "Invalid homeserver discovery response": "Përgjigje e pavlefshme zbulimi shërbyesi Home", + "Cannot find homeserver": "S’gjendet dot shërbyesi Home", + "File is too big. Maximum file size is %(fileSize)s": "Kartela është shumë e madhe. Madhësia maksimum për kartelat është %(fileSize)s", + "The following files cannot be uploaded:": "Kartelat vijuese s’mund të ngarkohen:", + "Use a few words, avoid common phrases": "Përdorni ca fjalë, shmangni fraza të rëndomta", + "No need for symbols, digits, or uppercase letters": "S’ka nevojë për simbole, shifra apo shkronja të mëdha", + "Use a longer keyboard pattern with more turns": "Përdorni një rregullsi më të gjatë tastiere, me më tepër kthesa", + "Avoid repeated words and characters": "Shmangi përsëritje fjalësh dhe përsëritje shkronjash", + "Avoid sequences": "Shmangi togfjalësha", + "Avoid recent years": "Shmangni vitet e fundit", + "Avoid years that are associated with you": "Shmangni vite që kanë lidhje me ju", + "Avoid dates and years that are associated with you": "Shmangni data dhe vite që kanë lidhje me ju", + "Capitalization doesn't help very much": "Shkrimi i shkronjës së parë me të madhe nuk ndihmon kushedi çë", + "All-uppercase is almost as easy to guess as all-lowercase": "Fjalë shkruar krejt me të mëdha janë thuajse po aq të lehta për t’i hamendësuar sa ato me krejt të vogla", + "Reversed words aren't much harder to guess": "Fjalët së prapthi s’janë të vështira për t’i marrë me mend", + "Predictable substitutions like '@' instead of 'a' don't help very much": "Zëvendësime të parashikueshme, të tilla si '@', në vend të 'a', nuk ndihmojnë kushedi çë", + "Add another word or two. Uncommon words are better.": "Shtoni një a dy fjalë të tjera. Fjalë jo të rëndomta janë më të përshtatshme.", + "Repeats like \"aaa\" are easy to guess": "Përsëritje të tilla si \"aaa\" janë të lehta për t’u hamendësuar", + "Repeats like \"abcabcabc\" are only slightly harder to guess than \"abc\"": "Përsëritje të tilla si \"abcabcabc\" janë vetëm pak më të vështira për t’u hamendësuar se sa \"abc\"", + "Sequences like abc or 6543 are easy to guess": "Sekuenca të tilla si abc ose 6543 janë të lehta për t’u hamendsuar", + "Recent years are easy to guess": "Vitet tani afër janë të lehtë për t’u hamendësuar", + "Dates are often easy to guess": "Datat shpesh janë të lehta për t’i gjetur", + "This is a top-10 common password": "Ky fjalëkalim është nga 10 më të rëndomtët", + "This is a top-100 common password": "Ky fjalëkalim është nga 100 më të rëndomtët", + "This is a very common password": "Ky është një fjalëkalim shumë i rëndomtë", + "This is similar to a commonly used password": "Ky është i ngjashëm me një fjalëkalim të përdorur rëndom", + "A word by itself is easy to guess": "Një fjalë më vete është e lehtë të hamendësohet", + "Names and surnames by themselves are easy to guess": "Emrat dhe mbiemrat në vetvete janë të lehtë për t’i hamendësuar", + "Common names and surnames are easy to guess": "Emra dhe mbiemra të rëndomtë janë të kollajtë për t’u hamendësuar", + "Great! This passphrase looks strong enough.": "Bukur! Ky frazëkalim duket goxha i fuqishëm.", + "Failed to load group members": "S'u arrit të ngarkoheshin anëtarë grupi", + "As a safety net, you can use it to restore your encrypted message history.": "Si një rrjet sigurie, mund ta përdorni për të rikthyer historikun e mesazheve tuaj të fshehtëzuar." } diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index 242990264c..712911064f 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -1300,5 +1300,112 @@ "You are currently using Riot anonymously as a guest.": "您目前是以訪客的身份匿名使用 Riot。", "You are an administrator of this community. You will not be able to rejoin without an invite from another administrator.": "您是此社群的管理員。您將無法在沒有其他管理員的邀請下重新加入。", "Open Devtools": "開啟開發者工具", - "Show developer tools": "顯示開發者工具" + "Show developer tools": "顯示開發者工具", + "Unable to load! Check your network connectivity and try again.": "無法載入!請檢查您的網路連線狀態並再試一次。", + "Backup of encryption keys to server": "將加密金鑰備份到伺服器", + "Delete Backup": "刪除備份", + "Delete your backed up encryption keys from the server? You will no longer be able to use your recovery key to read encrypted message history": "從伺服器刪除您已備份的加密金鑰?您將無法再使用您的復原金鑰來讀取加密的訊息歷史", + "Delete backup": "刪除備份", + "Unable to load key backup status": "無法載入金鑰備份狀態", + "This device is uploading keys to this backup": "此裝置正在上傳金鑰到此備份", + "This device is not uploading keys to this backup": "此裝置並未上傳金鑰到此備份", + "Backup has a valid signature from this device": "備份有從此裝置而來的有效簽章", + "Backup has a valid signature from verified device x": "備份有從已驗證的 x 裝置而來的有效簽章", + "Backup has a valid signature from unverified device ": "備份有從未驗證的 裝置而來的有效簽章", + "Backup has an invalid signature from verified device ": "備份有從已驗證的 裝置而來的無效簽章", + "Backup has an invalid signature from unverified device ": "備份有從未驗證的 裝置而來的無效簽章", + "Backup is not signed by any of your devices": "備份未被您的任何裝置簽署", + "Backup version: ": "備份版本: ", + "Algorithm: ": "演算法: ", + "Restore backup": "恢復備份", + "No backup is present": "沒有備份", + "Start a new backup": "開始新備份", + "Please review and accept all of the homeserver's policies": "請審閱並接受家伺服器的所有政策", + "To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of Riot to do this": "為了避免遺失您的聊天歷史,您必須在登出前匯出您的聊天室金鑰。您必須回到較新的 Riot 才能執行此動作", + "You've previously used a newer version of Riot on %(host)s. To use this version again with end to end encryption, you will need to sign out and back in again. ": "您先前在 %(host)s 上使用較新的 Riot 版本。要再次與此版本一同使用端到端加密,您將需要登出並再次登入。 ", + "Incompatible Database": "不相容的資料庫", + "Continue With Encryption Disabled": "在停用加密的情況下繼續", + "Secure your encrypted message history with a Recovery Passphrase.": "以復原密碼保證您的加密訊息歷史安全。", + "You'll need it if you log out or lose access to this device.": "如果您登出或是遺失對此裝置的存取權,您將會需要它。", + "Enter a passphrase...": "輸入密碼……", + "Next": "下一個", + "If you don't want encrypted message history to be availble on other devices, .": "如果您不想要讓加密的訊息歷史在其他裝置上可用,。", + "Or, if you don't want to create a Recovery Passphrase, skip this step and .": "或是,如果您不想建立復原密碼,跳過此步驟並。", + "That matches!": "符合!", + "That doesn't match.": "不符合。", + "Go back to set it again.": "回去重新設定它。", + "Type in your Recovery Passphrase to confirm you remember it. If it helps, add it to your password manager or store it somewhere safe.": "輸入您的復原密碼以確認您記得它。如果可以的話,把它加入到您的密碼管理員或是把它儲存在其他安全的地方。", + "Repeat your passphrase...": "重覆您的密碼……", + "Make a copy of this Recovery Key and keep it safe.": "複製這把復原金鑰並把它放在安全的地方。", + "As a safety net, you can use it to restore your encrypted message history if you forget your Recovery Passphrase.": "做為安全網,您可以在忘記您的復原密碼時使用它來復原您的加密訊息歷史。", + "Your Recovery Key": "您的復原金鑰", + "Copy to clipboard": "複製到剪貼簿", + "Download": "下載", + "I've made a copy": "我已經有副本了", + "Your Recovery Key has been copied to your clipboard, paste it to:": "您的復原金鑰已複製到您的剪貼簿,將它貼上到:", + "Your Recovery Key is in your Downloads folder.": "您的復原金鑰在您的下載資料夾。", + "Print it and store it somewhere safe": "列印它並存放在安全的地方", + "Save it on a USB key or backup drive": "將它儲存到 USB 金鑰或備份磁碟上", + "Copy it to your personal cloud storage": "將它複製 到您的個人雲端儲存", + "Got it": "知道了", + "Backup created": "備份已建立", + "Your encryption keys are now being backed up to your Homeserver.": "您的加密金鑰已經備份到您的家伺服器了。", + "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another device.": "在沒有設定安全訊息復原的狀況下,您將無法在登出或使用其他裝置後復原您的已加密訊息歷史。", + "Set up Secure Message Recovery": "設定安全訊息復原", + "Create a Recovery Passphrase": "建立復原密碼", + "Confirm Recovery Passphrase": "確認復原密碼", + "Recovery Key": "復原金鑰", + "Keep it safe": "保持安全", + "Backing up...": "正在備份……", + "Create Key Backup": "建立金鑰備份", + "Unable to create key backup": "無法建立金鑰備份", + "Retry": "重試", + "Unable to load backup status": "無法載入備份狀態", + "Unable to restore backup": "無法復原備份", + "No backup found!": "找不到備份!", + "Backup Restored": "備份已復原", + "Failed to decrypt %(failedCount)s sessions!": "解密 %(failedCount)s 工作階段失敗!", + "Restored %(sessionCount)s session keys": "%(sessionCount)s 工作階段金鑰已復原", + "Enter Recovery Passphrase": "輸入復原密碼", + "Access your secure message history and set up secure messaging by entering your recovery passphrase.": "存取您的安全訊息歷史並透過輸入您的復原密碼來設定安全訊息。", + "If you've forgotten your recovery passphrase you can use your recovery key or set up new recovery options": "如果您忘記您的復原密碼,您可以使用您的復原金鑰設定新的復原選項", + "Enter Recovery Key": "輸入復原金鑰", + "This looks like a valid recovery key!": "看起來是有效的復原金鑰!", + "Not a valid recovery key": "不是有效的復原金鑰", + "Access your secure message history and set up secure messaging by entering your recovery key.": "存取您的安全訊息歷史並趟過輸入您的復原金鑰來設定安全傳訊。", + "If you've forgotten your recovery passphrase you can ": "如果您忘記您的復原密碼,您可以", + "Key Backup": "金鑰備份", + "Failed to perform homeserver discovery": "執行家伺服器探索失敗", + "Invalid homeserver discovery response": "無效的家伺服器探索回應", + "Cannot find homeserver": "找不到家伺服器", + "Sign in with single sign-on": "以單一登入來登入", + "File is too big. Maximum file size is %(fileSize)s": "檔案太大了。最大的檔案大小為 %(fileSize)s", + "The following files cannot be uploaded:": "下列檔案無法上傳:", + "Use a few words, avoid common phrases": "使用數個字,但避免常用片語", + "No need for symbols, digits, or uppercase letters": "不需要符號、數字或大寫字母", + "Use a longer keyboard pattern with more turns": "以更多變化使用較長的鍵盤模式", + "Avoid repeated words and characters": "避免重覆的文字與字母", + "Avoid sequences": "避免序列", + "Avoid recent years": "避免最近的年份", + "Avoid years that are associated with you": "避免關於您的年份", + "Avoid dates and years that are associated with you": "避免關於您的日期與年份", + "Capitalization doesn't help very much": "大寫並沒有太大的協助", + "All-uppercase is almost as easy to guess as all-lowercase": "全大寫通常比全小寫好猜", + "Reversed words aren't much harder to guess": "反向拼字不會比較難猜", + "Predictable substitutions like '@' instead of 'a' don't help very much": "如「@」而非「a」這樣的預期中的替換並沒有太多的協助", + "Add another word or two. Uncommon words are better.": "加入一個或兩個額外的單字。最好是不常用的。", + "Repeats like \"aaa\" are easy to guess": "如「aaa」這樣的重覆易於猜測", + "Repeats like \"abcabcabc\" are only slightly harder to guess than \"abc\"": "如「abcabcabc」這樣的重覆只比「abc」難猜一點", + "Sequences like abc or 6543 are easy to guess": "如 abc 或 6543 這樣的序列易於猜測", + "Recent years are easy to guess": "最近的年份易於猜測", + "Dates are often easy to guess": "日期通常比較好猜", + "This is a top-10 common password": "這是十大最常見的密碼", + "This is a top-100 common password": "這是百大最常見的密碼", + "This is a very common password": "這是非常常見的密碼", + "This is similar to a commonly used password": "這與常見使用的密碼很類似", + "A word by itself is easy to guess": "單字本身很容易猜測", + "Names and surnames by themselves are easy to guess": "姓名與姓氏本身很容易猜測", + "Common names and surnames are easy to guess": "常見的名字與姓氏易於猜測", + "Great! This passphrase looks strong enough.": "很好!這個密碼看起來夠強了。", + "As a safety net, you can use it to restore your encrypted message history.": "做為安全網,您可以使用它來復原您已加密的訊息歷史。" } diff --git a/src/matrix-to.js b/src/matrix-to.js index b5827f671a..b750dff6d6 100644 --- a/src/matrix-to.js +++ b/src/matrix-to.js @@ -15,6 +15,8 @@ limitations under the License. */ import MatrixClientPeg from "./MatrixClientPeg"; +import isIp from "is-ip"; +import utils from 'matrix-js-sdk/lib/utils'; export const host = "matrix.to"; export const baseUrl = `https://${host}`; @@ -90,7 +92,9 @@ export function pickServerCandidates(roomId) { // Rationale for popular servers: It's hard to get rid of people when // they keep flocking in from a particular server. Sure, the server could // be ACL'd in the future or for some reason be evicted from the room - // however an event like that is unlikely the larger the room gets. + // however an event like that is unlikely the larger the room gets. If + // the server is ACL'd at the time of generating the link however, we + // shouldn't pick them. We also don't pick IP addresses. // Note: we don't pick the server the room was created on because the // homeserver should already be using that server as a last ditch attempt @@ -104,12 +108,29 @@ export function pickServerCandidates(roomId) { // The receiving user can then manually append the known-good server to // the list and magically have the link work. + const bannedHostsRegexps = []; + let allowedHostsRegexps = [new RegExp(".*")]; // default allow everyone + if (room.currentState) { + const aclEvent = room.currentState.getStateEvents("m.room.server_acl", ""); + if (aclEvent && aclEvent.getContent()) { + const getRegex = (hostname) => new RegExp("^" + utils.globToRegexp(hostname, false) + "$"); + + const denied = aclEvent.getContent().deny || []; + denied.forEach(h => bannedHostsRegexps.push(getRegex(h))); + + const allowed = aclEvent.getContent().allow || []; + allowedHostsRegexps = []; // we don't want to use the default rule here + allowed.forEach(h => allowedHostsRegexps.push(getRegex(h))); + } + } + const populationMap: {[server:string]:number} = {}; const highestPlUser = {userId: null, powerLevel: 0, serverName: null}; for (const member of room.getJoinedMembers()) { const serverName = member.userId.split(":").splice(1).join(":"); - if (member.powerLevel > highestPlUser.powerLevel) { + if (member.powerLevel > highestPlUser.powerLevel && !isHostnameIpAddress(serverName) + && !isHostInRegex(serverName, bannedHostsRegexps) && isHostInRegex(serverName, allowedHostsRegexps)) { highestPlUser.userId = member.userId; highestPlUser.powerLevel = member.powerLevel; highestPlUser.serverName = serverName; @@ -125,8 +146,9 @@ export function pickServerCandidates(roomId) { const beforePopulation = candidates.length; const serversByPopulation = Object.keys(populationMap) .sort((a, b) => populationMap[b] - populationMap[a]) - .filter(a => !candidates.includes(a)); - for (let i = beforePopulation; i <= MAX_SERVER_CANDIDATES; i++) { + .filter(a => !candidates.includes(a) && !isHostnameIpAddress(a) + && !isHostInRegex(a, bannedHostsRegexps) && isHostInRegex(a, allowedHostsRegexps)); + for (let i = beforePopulation; i < MAX_SERVER_CANDIDATES; i++) { const idx = i - beforePopulation; if (idx >= serversByPopulation.length) break; candidates.push(serversByPopulation[idx]); @@ -134,3 +156,34 @@ export function pickServerCandidates(roomId) { return candidates; } + +function getHostnameFromMatrixDomain(domain) { + if (!domain) return null; + + // The hostname might have a port, so we convert it to a URL and + // split out the real hostname. + const parser = document.createElement('a'); + parser.href = "https://" + domain; + return parser.hostname; +} + +function isHostInRegex(hostname, regexps) { + hostname = getHostnameFromMatrixDomain(hostname); + if (!hostname) return true; // assumed + if (regexps.length > 0 && !regexps[0].test) throw new Error(regexps[0]); + + return regexps.filter(h => h.test(hostname)).length > 0; +} + +function isHostnameIpAddress(hostname) { + hostname = getHostnameFromMatrixDomain(hostname); + if (!hostname) return false; + + // is-ip doesn't want IPv6 addresses surrounded by brackets, so + // take them off. + if (hostname.startsWith("[") && hostname.endsWith("]")) { + hostname = hostname.substring(1, hostname.length - 1); + } + + return isIp(hostname); +} diff --git a/src/notifications/StandardActions.js b/src/notifications/StandardActions.js index 30d6ea5975..15f645d5f7 100644 --- a/src/notifications/StandardActions.js +++ b/src/notifications/StandardActions.js @@ -24,6 +24,7 @@ module.exports = { ACTION_NOTIFY: encodeActions({notify: true}), ACTION_NOTIFY_DEFAULT_SOUND: encodeActions({notify: true, sound: "default"}), ACTION_NOTIFY_RING_SOUND: encodeActions({notify: true, sound: "ring"}), + ACTION_HIGHLIGHT: encodeActions({notify: true, highlight: true}), ACTION_HIGHLIGHT_DEFAULT_SOUND: encodeActions({notify: true, sound: "default", highlight: true}), ACTION_DONT_NOTIFY: encodeActions({notify: false}), ACTION_DISABLED: null, diff --git a/src/notifications/VectorPushRulesDefinitions.js b/src/notifications/VectorPushRulesDefinitions.js index eeb193cb8a..3df2e70774 100644 --- a/src/notifications/VectorPushRulesDefinitions.js +++ b/src/notifications/VectorPushRulesDefinitions.js @@ -20,6 +20,7 @@ import { _td } from '../languageHandler'; const StandardActions = require('./StandardActions'); const PushRuleVectorState = require('./PushRuleVectorState'); +const { decodeActions } = require('./NotificationUtils'); class VectorPushRuleDefinition { constructor(opts) { @@ -31,13 +32,11 @@ class VectorPushRuleDefinition { // Translate the rule actions and its enabled value into vector state ruleToVectorState(rule) { let enabled = false; - let actions = null; if (rule) { enabled = rule.enabled; - actions = rule.actions; } - for (const stateKey in PushRuleVectorState.states) { + for (const stateKey in PushRuleVectorState.states) { // eslint-disable-line guard-for-in const state = PushRuleVectorState.states[stateKey]; const vectorStateToActions = this.vectorStateToActions[state]; @@ -47,15 +46,21 @@ class VectorPushRuleDefinition { return state; } } else { - // The actions must match to the ones expected by vector state - if (enabled && JSON.stringify(rule.actions) === JSON.stringify(vectorStateToActions)) { + // The actions must match to the ones expected by vector state. + // Use `decodeActions` on both sides to canonicalize things like + // value: true vs. unspecified for highlight (which defaults to + // true, making them equivalent). + if (enabled && + JSON.stringify(decodeActions(rule.actions)) === + JSON.stringify(decodeActions(vectorStateToActions))) { return state; } } } - console.error("Cannot translate rule actions into Vector rule state. Rule: " + - JSON.stringify(rule)); + console.error(`Cannot translate rule actions into Vector rule state. ` + + `Rule: ${JSON.stringify(rule)}, ` + + `Expected: ${JSON.stringify(this.vectorStateToActions)}`); return undefined; } } @@ -86,6 +91,17 @@ module.exports = { }, }), + // Messages containing @room + ".m.rule.roomnotif": new VectorPushRuleDefinition({ + kind: "override", + description: _td("Messages containing @room"), // passed through _t() translation in src/components/views/settings/Notifications.js + vectorStateToActions: { // The actions for each vector state, or null to disable the rule. + on: StandardActions.ACTION_NOTIFY, + loud: StandardActions.ACTION_HIGHLIGHT, + off: StandardActions.ACTION_DISABLED, + }, + }), + // Messages just sent to the user in a 1:1 room ".m.rule.room_one_to_one": new VectorPushRuleDefinition({ kind: "underride", @@ -97,6 +113,17 @@ module.exports = { }, }), + // Encrypted messages just sent to the user in a 1:1 room + ".m.rule.encrypted_room_one_to_one": new VectorPushRuleDefinition({ + kind: "underride", + description: _td("Encrypted messages in one-to-one chats"), // passed through _t() translation in src/components/views/settings/Notifications.js + vectorStateToActions: { + on: StandardActions.ACTION_NOTIFY, + loud: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND, + off: StandardActions.ACTION_DONT_NOTIFY, + }, + }), + // Messages just sent to a group chat room // 1:1 room messages are catched by the .m.rule.room_one_to_one rule if any defined // By opposition, all other room messages are from group chat rooms. @@ -110,6 +137,19 @@ module.exports = { }, }), + // Encrypted messages just sent to a group chat room + // Encrypted 1:1 room messages are catched by the .m.rule.encrypted_room_one_to_one rule if any defined + // By opposition, all other room messages are from group chat rooms. + ".m.rule.encrypted": new VectorPushRuleDefinition({ + kind: "underride", + description: _td("Encrypted messages in group chats"), // passed through _t() translation in src/components/views/settings/Notifications.js + vectorStateToActions: { + on: StandardActions.ACTION_NOTIFY, + loud: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND, + off: StandardActions.ACTION_DONT_NOTIFY, + }, + }), + // Invitation for the user ".m.rule.invite_for_me": new VectorPushRuleDefinition({ kind: "underride", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index d0b4b9b9d6..56e66844dc 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -151,6 +151,11 @@ export const SETTINGS = { displayName: _td('Always show encryption icons'), default: true, }, + "showRoomRecoveryReminder": { + supportedLevels: LEVELS_ACCOUNT_SETTINGS, + displayName: _td('Show a reminder to enable Secure Message Recovery in encrypted rooms'), + default: true, + }, "enableSyntaxHighlightLanguageDetection": { supportedLevels: LEVELS_ACCOUNT_SETTINGS, displayName: _td('Enable automatic language detection for syntax highlighting'), diff --git a/src/shouldHideEvent.js b/src/shouldHideEvent.js index 3aad05a976..adc89a126a 100644 --- a/src/shouldHideEvent.js +++ b/src/shouldHideEvent.js @@ -38,18 +38,20 @@ function memberEventDiff(ev) { } export default function shouldHideEvent(ev) { - // Wrap getValue() for readability + // Wrap getValue() for readability. Calling the SettingsStore can be + // fairly resource heavy, so the checks below should avoid hitting it + // where possible. const isEnabled = (name) => SettingsStore.getValue(name, ev.getRoomId()); // Hide redacted events - if (isEnabled('hideRedactions') && ev.isRedacted()) return true; + if (ev.isRedacted() && isEnabled('hideRedactions')) return true; const eventDiff = memberEventDiff(ev); if (eventDiff.isMemberEvent) { - if (isEnabled('hideJoinLeaves') && (eventDiff.isJoin || eventDiff.isPart)) return true; - if (isEnabled('hideAvatarChanges') && eventDiff.isAvatarChange) return true; - if (isEnabled('hideDisplaynameChanges') && eventDiff.isDisplaynameChange) return true; + if ((eventDiff.isJoin || eventDiff.isPart) && isEnabled('hideJoinLeaves')) return true; + if (eventDiff.isAvatarChange && isEnabled('hideAvatarChanges')) return true; + if (eventDiff.isDisplaynameChange && isEnabled('hideDisplaynameChanges')) return true; } return false; diff --git a/src/stores/GroupStore.js b/src/stores/GroupStore.js index bc2be37f51..4ac1e42e2e 100644 --- a/src/stores/GroupStore.js +++ b/src/stores/GroupStore.js @@ -122,10 +122,6 @@ class GroupStore extends EventEmitter { ); }, }; - - this.on('error', (err, groupId) => { - console.error(`GroupStore encountered error whilst fetching data for ${groupId}`, err); - }); } _fetchResource(stateKey, groupId) { @@ -148,7 +144,7 @@ class GroupStore extends EventEmitter { } console.error(`Failed to get resource ${stateKey} for ${groupId}`, err); - this.emit('error', err, groupId); + this.emit('error', err, groupId, stateKey); }).finally(() => { // Indicate finished request, allow for future fetches delete this._fetchResourcePromise[stateKey][groupId]; diff --git a/src/stores/RoomListStore.js b/src/stores/RoomListStore.js index c636c53631..af6a8cc991 100644 --- a/src/stores/RoomListStore.js +++ b/src/stores/RoomListStore.js @@ -300,6 +300,10 @@ class RoomListStore extends Store { const ts = this._tsOfNewestEvent(room); this._updateCachedRoomState(roomId, "timestamp", ts); return ts; + } else if (type === "unread-muted") { + const unread = Unread.doesRoomHaveUnreadMessages(room); + this._updateCachedRoomState(roomId, "unread-muted", unread); + return unread; } else if (type === "unread") { const unread = room.getUnreadNotificationCount() > 0; this._updateCachedRoomState(roomId, "unread", unread); @@ -358,8 +362,21 @@ class RoomListStore extends Store { } if (pinUnread) { - const unreadA = this._getRoomState(roomA, "unread"); - const unreadB = this._getRoomState(roomB, "unread"); + let unreadA = this._getRoomState(roomA, "unread"); + let unreadB = this._getRoomState(roomB, "unread"); + if (unreadA && !unreadB) return -1; + if (!unreadA && unreadB) return 1; + + // If they both have unread messages, sort by timestamp + // If nether have unread message (the fourth check not shown + // here), then just sort by timestamp anyways. + if (unreadA && unreadB) return timestampDiff; + + // Unread can also mean "unread without badge", which is + // different from what the above checks for. We're also + // going to sort those here. + unreadA = this._getRoomState(roomA, "unread-muted"); + unreadB = this._getRoomState(roomB, "unread-muted"); if (unreadA && !unreadB) return -1; if (!unreadA && unreadB) return 1; diff --git a/src/stores/RoomViewStore.js b/src/stores/RoomViewStore.js index f15925f480..9e048e5d8e 100644 --- a/src/stores/RoomViewStore.js +++ b/src/stores/RoomViewStore.js @@ -224,6 +224,11 @@ class RoomViewStore extends Store { err: err, }); let msg = err.message ? err.message : JSON.stringify(err); + // XXX: We are relying on the error message returned by browsers here. + // This isn't great, but it does generalize the error being shown to users. + if (msg && msg.startsWith("CORS request rejected")) { + msg = _t("There was an error joining the room"); + } if (err.errcode === 'M_INCOMPATIBLE_ROOM_VERSION') { msg =
        {_t("Sorry, your homeserver is too old to participate in this room.")}
        diff --git a/src/utils/MultiInviter.js b/src/utils/MultiInviter.js index b3e7fc495a..ad10f28edf 100644 --- a/src/utils/MultiInviter.js +++ b/src/utils/MultiInviter.js @@ -1,6 +1,6 @@ /* Copyright 2016 OpenMarket Ltd -Copyright 2017 New Vector Ltd +Copyright 2017, 2018 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,9 +17,9 @@ limitations under the License. import MatrixClientPeg from '../MatrixClientPeg'; import {getAddressType} from '../UserAddress'; -import {inviteToRoom} from '../RoomInvite'; import GroupStore from '../stores/GroupStore'; import Promise from 'bluebird'; +import {_t} from "../languageHandler"; /** * Invites multiple addresses to a room or group, handling rate limiting from the server @@ -49,7 +49,7 @@ export default class MultiInviter { * Invite users to this room. This may only be called once per * instance of the class. * - * @param {array} addresses Array of addresses to invite + * @param {array} addrs Array of addresses to invite * @returns {Promise} Resolved when all invitations in the queue are complete */ invite(addrs) { @@ -88,12 +88,30 @@ export default class MultiInviter { return this.errorTexts[addr]; } + async _inviteToRoom(roomId, addr) { + const addrType = getAddressType(addr); + + if (addrType === 'email') { + return MatrixClientPeg.get().inviteByEmail(roomId, addr); + } else if (addrType === 'mx-user-id') { + const profile = await MatrixClientPeg.get().getProfileInfo(addr); + if (!profile) { + return Promise.reject({errcode: "M_NOT_FOUND", error: "User does not have a profile."}); + } + + return MatrixClientPeg.get().invite(roomId, addr); + } else { + throw new Error('Unsupported address'); + } + } + + _inviteMore(nextIndex) { if (this._canceled) { return; } - if (nextIndex == this.addrs.length) { + if (nextIndex === this.addrs.length) { this.busy = false; this.deferred.resolve(this.completionStates); return; @@ -111,7 +129,7 @@ export default class MultiInviter { // don't re-invite (there's no way in the UI to do this, but // for sanity's sake) - if (this.completionStates[addr] == 'invited') { + if (this.completionStates[addr] === 'invited') { this._inviteMore(nextIndex + 1); return; } @@ -120,7 +138,7 @@ export default class MultiInviter { if (this.groupId !== null) { doInvite = GroupStore.inviteUserToGroup(this.groupId, addr); } else { - doInvite = inviteToRoom(this.roomId, addr); + doInvite = this._inviteToRoom(this.roomId, addr); } doInvite.then(() => { @@ -129,29 +147,34 @@ export default class MultiInviter { this.completionStates[addr] = 'invited'; this._inviteMore(nextIndex + 1); - }, (err) => { + }).catch((err) => { if (this._canceled) { return; } let errorText; let fatal = false; - if (err.errcode == 'M_FORBIDDEN') { + if (err.errcode === 'M_FORBIDDEN') { fatal = true; - errorText = 'You do not have permission to invite people to this room.'; - } else if (err.errcode == 'M_LIMIT_EXCEEDED') { + errorText = _t('You do not have permission to invite people to this room.'); + } else if (err.errcode === 'M_LIMIT_EXCEEDED') { // we're being throttled so wait a bit & try again setTimeout(() => { this._inviteMore(nextIndex); }, 5000); return; + } else if(err.errcode === "M_NOT_FOUND") { + errorText = _t("User %(user_id)s does not exist", {user_id: addr}); } else { - errorText = 'Unknown server error'; + errorText = _t('Unknown server error'); } this.completionStates[addr] = 'error'; this.errorTexts[addr] = errorText; this.busy = !fatal; + this.fatal = fatal; if (!fatal) { this._inviteMore(nextIndex + 1); + } else { + this.deferred.resolve(this.completionStates); } }); } diff --git a/src/utils/PasswordScorer.js b/src/utils/PasswordScorer.js new file mode 100644 index 0000000000..e4bbec1637 --- /dev/null +++ b/src/utils/PasswordScorer.js @@ -0,0 +1,84 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import Zxcvbn from 'zxcvbn'; + +import MatrixClientPeg from '../MatrixClientPeg'; +import { _t, _td } from '../languageHandler'; + +const ZXCVBN_USER_INPUTS = [ + 'riot', + 'matrix', +]; + +// Translations for zxcvbn's suggestion strings +_td("Use a few words, avoid common phrases"); +_td("No need for symbols, digits, or uppercase letters"); +_td("Use a longer keyboard pattern with more turns"); +_td("Avoid repeated words and characters"); +_td("Avoid sequences"); +_td("Avoid recent years"); +_td("Avoid years that are associated with you"); +_td("Avoid dates and years that are associated with you"); +_td("Capitalization doesn't help very much"); +_td("All-uppercase is almost as easy to guess as all-lowercase"); +_td("Reversed words aren't much harder to guess"); +_td("Predictable substitutions like '@' instead of 'a' don't help very much"); +_td("Add another word or two. Uncommon words are better."); + +// and warnings +_td("Repeats like \"aaa\" are easy to guess"); +_td("Repeats like \"abcabcabc\" are only slightly harder to guess than \"abc\""); +_td("Sequences like abc or 6543 are easy to guess"); +_td("Recent years are easy to guess"); +_td("Dates are often easy to guess"); +_td("This is a top-10 common password"); +_td("This is a top-100 common password"); +_td("This is a very common password"); +_td("This is similar to a commonly used password"); +_td("A word by itself is easy to guess"); +_td("Names and surnames by themselves are easy to guess"); +_td("Common names and surnames are easy to guess"); + +/** + * Wrapper around zxcvbn password strength estimation + * Include this only from async components: it pulls in zxcvbn + * (obviously) which is large. + */ +export function scorePassword(password) { + if (password.length === 0) return null; + + const userInputs = ZXCVBN_USER_INPUTS.slice(); + userInputs.push(MatrixClientPeg.get().getUserIdLocalpart()); + + let zxcvbnResult = Zxcvbn(password, userInputs); + // Work around https://github.com/dropbox/zxcvbn/issues/216 + if (password.includes(' ')) { + const resultNoSpaces = Zxcvbn(password.replace(/ /g, ''), userInputs); + if (resultNoSpaces.score < zxcvbnResult.score) zxcvbnResult = resultNoSpaces; + } + + for (let i = 0; i < zxcvbnResult.feedback.suggestions.length; ++i) { + // translate suggestions + zxcvbnResult.feedback.suggestions[i] = _t(zxcvbnResult.feedback.suggestions[i]); + } + // and warning, if any + if (zxcvbnResult.feedback.warning) { + zxcvbnResult.feedback.warning = _t(zxcvbnResult.feedback.warning); + } + + return zxcvbnResult; +} diff --git a/test/components/structures/GroupView-test.js b/test/components/structures/GroupView-test.js index 3b3510f26e..89632dcc48 100644 --- a/test/components/structures/GroupView-test.js +++ b/test/components/structures/GroupView-test.js @@ -164,7 +164,7 @@ describe('GroupView', function() { it('should indicate failure after failed /summary', function() { const groupView = ReactTestUtils.findRenderedComponentWithType(root, GroupView); - const prom = waitForUpdate(groupView).then(() => { + const prom = waitForUpdate(groupView, 4).then(() => { ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView_error'); }); @@ -179,7 +179,7 @@ describe('GroupView', function() { it('should show a group avatar, name, id and short description after successful /summary', function() { const groupView = ReactTestUtils.findRenderedComponentWithType(root, GroupView); - const prom = waitForUpdate(groupView).then(() => { + const prom = waitForUpdate(groupView, 4).then(() => { ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView'); const avatar = ReactTestUtils.findRenderedComponentWithType(root, sdk.getComponent('avatars.GroupAvatar')); @@ -214,7 +214,7 @@ describe('GroupView', function() { it('should show a simple long description after successful /summary', function() { const groupView = ReactTestUtils.findRenderedComponentWithType(root, GroupView); - const prom = waitForUpdate(groupView).then(() => { + const prom = waitForUpdate(groupView, 4).then(() => { ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView'); const longDesc = ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView_groupDesc'); @@ -235,7 +235,7 @@ describe('GroupView', function() { it('should show a placeholder if a long description is not set', function() { const groupView = ReactTestUtils.findRenderedComponentWithType(root, GroupView); - const prom = waitForUpdate(groupView).then(() => { + const prom = waitForUpdate(groupView, 4).then(() => { const placeholder = ReactTestUtils .findRenderedDOMComponentWithClass(root, 'mx_GroupView_groupDesc_placeholder'); const placeholderElement = ReactDOM.findDOMNode(placeholder); @@ -255,7 +255,7 @@ describe('GroupView', function() { it('should show a complicated long description after successful /summary', function() { const groupView = ReactTestUtils.findRenderedComponentWithType(root, GroupView); - const prom = waitForUpdate(groupView).then(() => { + const prom = waitForUpdate(groupView, 4).then(() => { const longDesc = ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView_groupDesc'); const longDescElement = ReactDOM.findDOMNode(longDesc); expect(longDescElement).toExist(); @@ -282,7 +282,7 @@ describe('GroupView', function() { it('should disallow images with non-mxc URLs', function() { const groupView = ReactTestUtils.findRenderedComponentWithType(root, GroupView); - const prom = waitForUpdate(groupView).then(() => { + const prom = waitForUpdate(groupView, 4).then(() => { const longDesc = ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView_groupDesc'); const longDescElement = ReactDOM.findDOMNode(longDesc); expect(longDescElement).toExist(); @@ -305,7 +305,7 @@ describe('GroupView', function() { it('should show a RoomDetailList after a successful /summary & /rooms (no rooms returned)', function() { const groupView = ReactTestUtils.findRenderedComponentWithType(root, GroupView); - const prom = waitForUpdate(groupView).then(() => { + const prom = waitForUpdate(groupView, 4).then(() => { const roomDetailList = ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_RoomDetailList'); const roomDetailListElement = ReactDOM.findDOMNode(roomDetailList); expect(roomDetailListElement).toExist(); @@ -322,7 +322,7 @@ describe('GroupView', function() { it('should show a RoomDetailList after a successful /summary & /rooms (with a single room)', function() { const groupView = ReactTestUtils.findRenderedComponentWithType(root, GroupView); - const prom = waitForUpdate(groupView).then(() => { + const prom = waitForUpdate(groupView, 4).then(() => { const roomDetailList = ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_RoomDetailList'); const roomDetailListElement = ReactDOM.findDOMNode(roomDetailList); expect(roomDetailListElement).toExist(); @@ -355,4 +355,25 @@ describe('GroupView', function() { httpBackend.flush(undefined, undefined, 0); return prom; }); + + it('should show a summary even if /users fails', function() { + const groupView = ReactTestUtils.findRenderedComponentWithType(root, GroupView); + + // Only wait for 3 updates in this test since we don't change state for + // the /users error case. + const prom = waitForUpdate(groupView, 3).then(() => { + const shortDesc = ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView_header_shortDesc'); + const shortDescElement = ReactDOM.findDOMNode(shortDesc); + expect(shortDescElement).toExist(); + expect(shortDescElement.innerText).toBe('This is a community'); + }); + + httpBackend.when('GET', '/groups/' + groupIdEncoded + '/summary').respond(200, summaryResponse); + httpBackend.when('GET', '/groups/' + groupIdEncoded + '/users').respond(500, {}); + httpBackend.when('GET', '/groups/' + groupIdEncoded + '/invited_users').respond(200, { chunk: [] }); + httpBackend.when('GET', '/groups/' + groupIdEncoded + '/rooms').respond(200, { chunk: [] }); + + httpBackend.flush(undefined, undefined, 0); + return prom; + }); }); diff --git a/test/components/views/groups/GroupMemberList-test.js b/test/components/views/groups/GroupMemberList-test.js new file mode 100644 index 0000000000..d71d0377d7 --- /dev/null +++ b/test/components/views/groups/GroupMemberList-test.js @@ -0,0 +1,149 @@ +/* +Copyright 2018 New Vector Ltd. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from "react"; +import ReactDOM from "react-dom"; +import ReactTestUtils from "react-dom/test-utils"; +import expect from "expect"; + +import MockHttpBackend from "matrix-mock-request"; +import MatrixClientPeg from "../../../../src/MatrixClientPeg"; +import sdk from "matrix-react-sdk"; +import Matrix from "matrix-js-sdk"; + +import * as TestUtils from "test-utils"; +const { waitForUpdate } = TestUtils; + +const GroupMemberList = sdk.getComponent("views.groups.GroupMemberList"); +const WrappedGroupMemberList = TestUtils.wrapInMatrixClientContext(GroupMemberList); + +describe("GroupMemberList", function() { + let root; + let rootElement; + let httpBackend; + let summaryResponse; + let groupId; + let groupIdEncoded; + + // Summary response fields + const user = { + is_privileged: true, // can edit the group + is_public: true, // appear as a member to non-members + is_publicised: true, // display flair + }; + const usersSection = { + roles: {}, + total_user_count_estimate: 0, + users: [], + }; + const roomsSection = { + categories: {}, + rooms: [], + total_room_count_estimate: 0, + }; + + // Users response fields + const usersResponse = { + chunk: [ + { + user_id: "@test:matrix.org", + displayname: "Test", + avatar_url: "mxc://matrix.org/oUxxDyzQOHdVDMxgwFzyCWEe", + is_public: true, + is_privileged: true, + attestation: {}, + }, + ], + }; + + beforeEach(function() { + TestUtils.beforeEach(this); + + httpBackend = new MockHttpBackend(); + + Matrix.request(httpBackend.requestFn); + + MatrixClientPeg.get = () => Matrix.createClient({ + baseUrl: "https://my.home.server", + userId: "@me:here", + accessToken: "123456789", + }); + + summaryResponse = { + profile: { + avatar_url: "mxc://someavatarurl", + is_openly_joinable: true, + is_public: true, + long_description: "This is a LONG description.", + name: "The name of a community", + short_description: "This is a community", + }, + user, + users_section: usersSection, + rooms_section: roomsSection, + }; + + groupId = "+" + Math.random().toString(16).slice(2) + ":domain"; + groupIdEncoded = encodeURIComponent(groupId); + + rootElement = document.createElement("div"); + root = ReactDOM.render(, rootElement); + }); + + afterEach(function() { + ReactDOM.unmountComponentAtNode(rootElement); + }); + + it("should show group member list after successful /users", function() { + const groupMemberList = ReactTestUtils.findRenderedComponentWithType(root, GroupMemberList); + const prom = waitForUpdate(groupMemberList, 4).then(() => { + ReactTestUtils.findRenderedDOMComponentWithClass(root, "mx_MemberList"); + + const memberList = ReactTestUtils.findRenderedDOMComponentWithClass(root, "mx_MemberList_joined"); + const memberListElement = ReactDOM.findDOMNode(memberList); + expect(memberListElement).toExist(); + expect(memberListElement.innerText).toBe("Test"); + }); + + httpBackend.when("GET", "/groups/" + groupIdEncoded + "/summary").respond(200, summaryResponse); + httpBackend.when("GET", "/groups/" + groupIdEncoded + "/users").respond(200, usersResponse); + httpBackend.when("GET", "/groups/" + groupIdEncoded + "/invited_users").respond(200, { chunk: [] }); + httpBackend.when("GET", "/groups/" + groupIdEncoded + "/rooms").respond(200, { chunk: [] }); + + httpBackend.flush(undefined, undefined, 0); + return prom; + }); + + it("should show error message after failed /users", function() { + const groupMemberList = ReactTestUtils.findRenderedComponentWithType(root, GroupMemberList); + const prom = waitForUpdate(groupMemberList, 4).then(() => { + ReactTestUtils.findRenderedDOMComponentWithClass(root, "mx_MemberList"); + + const memberList = ReactTestUtils.findRenderedDOMComponentWithClass(root, "mx_MemberList_joined"); + const memberListElement = ReactDOM.findDOMNode(memberList); + expect(memberListElement).toExist(); + expect(memberListElement.innerText).toBe("Failed to load group members"); + }); + + httpBackend.when("GET", "/groups/" + groupIdEncoded + "/summary").respond(200, summaryResponse); + httpBackend.when("GET", "/groups/" + groupIdEncoded + "/users").respond(500, {}); + httpBackend.when("GET", "/groups/" + groupIdEncoded + "/invited_users").respond(200, { chunk: [] }); + httpBackend.when("GET", "/groups/" + groupIdEncoded + "/rooms").respond(200, { chunk: [] }); + + httpBackend.flush(undefined, undefined, 0); + return prom; + }); +}); diff --git a/test/matrix-to-test.js b/test/matrix-to-test.js index 70533575c4..6392e326e9 100644 --- a/test/matrix-to-test.js +++ b/test/matrix-to-test.js @@ -150,7 +150,39 @@ describe('matrix-to', function() { expect(pickedServers[2]).toBe("third"); }); - it('should work with IPv4 hostnames', function() { + it('should pick a maximum of 3 candidate servers', function() { + peg.get().getRoom = () => { + return { + getJoinedMembers: () => [ + { + userId: "@alice:alpha", + powerLevel: 100, + }, + { + userId: "@alice:bravo", + powerLevel: 0, + }, + { + userId: "@alice:charlie", + powerLevel: 0, + }, + { + userId: "@alice:delta", + powerLevel: 0, + }, + { + userId: "@alice:echo", + powerLevel: 0, + }, + ], + }; + }; + const pickedServers = pickServerCandidates("!somewhere:example.org"); + expect(pickedServers).toExist(); + expect(pickedServers.length).toBe(3); + }); + + it('should not consider IPv4 hosts', function() { peg.get().getRoom = () => { return { getJoinedMembers: () => [ @@ -163,11 +195,10 @@ describe('matrix-to', function() { }; const pickedServers = pickServerCandidates("!somewhere:example.org"); expect(pickedServers).toExist(); - expect(pickedServers.length).toBe(1); - expect(pickedServers[0]).toBe("127.0.0.1"); + expect(pickedServers.length).toBe(0); }); - it('should work with IPv6 hostnames', function() { + it('should not consider IPv6 hosts', function() { peg.get().getRoom = () => { return { getJoinedMembers: () => [ @@ -180,11 +211,10 @@ describe('matrix-to', function() { }; const pickedServers = pickServerCandidates("!somewhere:example.org"); expect(pickedServers).toExist(); - expect(pickedServers.length).toBe(1); - expect(pickedServers[0]).toBe("[::1]"); + expect(pickedServers.length).toBe(0); }); - it('should work with IPv4 hostnames with ports', function() { + it('should not consider IPv4 hostnames with ports', function() { peg.get().getRoom = () => { return { getJoinedMembers: () => [ @@ -197,11 +227,10 @@ describe('matrix-to', function() { }; const pickedServers = pickServerCandidates("!somewhere:example.org"); expect(pickedServers).toExist(); - expect(pickedServers.length).toBe(1); - expect(pickedServers[0]).toBe("127.0.0.1:8448"); + expect(pickedServers.length).toBe(0); }); - it('should work with IPv6 hostnames with ports', function() { + it('should not consider IPv6 hostnames with ports', function() { peg.get().getRoom = () => { return { getJoinedMembers: () => [ @@ -214,8 +243,7 @@ describe('matrix-to', function() { }; const pickedServers = pickServerCandidates("!somewhere:example.org"); expect(pickedServers).toExist(); - expect(pickedServers.length).toBe(1); - expect(pickedServers[0]).toBe("[::1]:8448"); + expect(pickedServers.length).toBe(0); }); it('should work with hostnames with ports', function() { @@ -235,6 +263,140 @@ describe('matrix-to', function() { expect(pickedServers[0]).toBe("example.org:8448"); }); + it('should not consider servers explicitly denied by ACLs', function() { + peg.get().getRoom = () => { + return { + getJoinedMembers: () => [ + { + userId: "@alice:evilcorp.com", + powerLevel: 100, + }, + { + userId: "@bob:chat.evilcorp.com", + powerLevel: 0, + }, + ], + currentState: { + getStateEvents: (type, key) => { + if (type !== "m.room.server_acl" || key !== "") return null; + return { + getContent: () => { + return { + deny: ["evilcorp.com", "*.evilcorp.com"], + allow: ["*"], + }; + }, + }; + }, + }, + }; + }; + const pickedServers = pickServerCandidates("!somewhere:example.org"); + expect(pickedServers).toExist(); + expect(pickedServers.length).toBe(0); + }); + + it('should not consider servers not allowed by ACLs', function() { + peg.get().getRoom = () => { + return { + getJoinedMembers: () => [ + { + userId: "@alice:evilcorp.com", + powerLevel: 100, + }, + { + userId: "@bob:chat.evilcorp.com", + powerLevel: 0, + }, + ], + currentState: { + getStateEvents: (type, key) => { + if (type !== "m.room.server_acl" || key !== "") return null; + return { + getContent: () => { + return { + deny: [], + allow: [], // implies "ban everyone" + }; + }, + }; + }, + }, + }; + }; + const pickedServers = pickServerCandidates("!somewhere:example.org"); + expect(pickedServers).toExist(); + expect(pickedServers.length).toBe(0); + }); + + it('should consider servers not explicitly banned by ACLs', function() { + peg.get().getRoom = () => { + return { + getJoinedMembers: () => [ + { + userId: "@alice:evilcorp.com", + powerLevel: 100, + }, + { + userId: "@bob:chat.evilcorp.com", + powerLevel: 0, + }, + ], + currentState: { + getStateEvents: (type, key) => { + if (type !== "m.room.server_acl" || key !== "") return null; + return { + getContent: () => { + return { + deny: ["*.evilcorp.com"], // evilcorp.com is still good though + allow: ["*"], + }; + }, + }; + }, + }, + }; + }; + const pickedServers = pickServerCandidates("!somewhere:example.org"); + expect(pickedServers).toExist(); + expect(pickedServers.length).toBe(1); + expect(pickedServers[0]).toEqual("evilcorp.com"); + }); + + it('should consider servers not disallowed by ACLs', function() { + peg.get().getRoom = () => { + return { + getJoinedMembers: () => [ + { + userId: "@alice:evilcorp.com", + powerLevel: 100, + }, + { + userId: "@bob:chat.evilcorp.com", + powerLevel: 0, + }, + ], + currentState: { + getStateEvents: (type, key) => { + if (type !== "m.room.server_acl" || key !== "") return null; + return { + getContent: () => { + return { + deny: [], + allow: ["evilcorp.com"], // implies "ban everyone else" + }; + }, + }; + }, + }, + }; + }; + const pickedServers = pickServerCandidates("!somewhere:example.org"); + expect(pickedServers).toExist(); + expect(pickedServers.length).toBe(1); + expect(pickedServers[0]).toEqual("evilcorp.com"); + }); + it('should generate an event permalink for room IDs with no candidate servers', function() { peg.get().getRoom = () => null; const result = makeEventPermalink("!somewhere:example.org", "$something:example.com"); diff --git a/test/test-utils.js b/test/test-utils.js index bc4d29210e..d5bcd9397a 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -310,19 +310,26 @@ export function wrapInMatrixClientContext(WrappedComponent) { /** * Call fn before calling componentDidUpdate on a react component instance, inst. * @param {React.Component} inst an instance of a React component. + * @param {integer} updates Number of updates to wait for. (Defaults to 1.) * @returns {Promise} promise that resolves when componentDidUpdate is called on * given component instance. */ -export function waitForUpdate(inst) { +export function waitForUpdate(inst, updates = 1) { return new Promise((resolve, reject) => { const cdu = inst.componentDidUpdate; + console.log(`Waiting for ${updates} update(s)`); + inst.componentDidUpdate = (prevProps, prevState, snapshot) => { - resolve(); + updates--; + console.log(`Got update, ${updates} remaining`); + + if (updates == 0) { + inst.componentDidUpdate = cdu; + resolve(); + } if (cdu) cdu(prevProps, prevState, snapshot); - - inst.componentDidUpdate = cdu; }; }); } From 982e037822f7afb0e41fe18e2de5141d3cd4aa49 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 3 Jan 2019 18:05:38 +0000 Subject: [PATCH 202/237] Fix merge --- res/css/structures/_RightPanel.scss | 4 ---- res/themes/dharma/css/_dharma.scss | 10 ++++++++++ src/components/views/rooms/RoomList.js | 16 ++++++++-------- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/res/css/structures/_RightPanel.scss b/res/css/structures/_RightPanel.scss index ae9e7ba981..592eea067e 100644 --- a/res/css/structures/_RightPanel.scss +++ b/res/css/structures/_RightPanel.scss @@ -57,10 +57,6 @@ limitations under the License. pointer-events: none; } -.mx_RightPanel_headerButton_badgeHighlight .mx_RightPanel_headerButton_badge { - color: $warning-color; -} - .mx_RightPanel_headerButton_highlight { border-color: $button-bg-color; } diff --git a/res/themes/dharma/css/_dharma.scss b/res/themes/dharma/css/_dharma.scss index 0851762be2..08a287ad71 100644 --- a/res/themes/dharma/css/_dharma.scss +++ b/res/themes/dharma/css/_dharma.scss @@ -186,6 +186,8 @@ $lightbox-border-color: #ffffff; // unused? $progressbar-color: #000; +$room-warning-bg-color: #fff8e3; + /*** form elements ***/ // .mx_textinput is a container for a text input @@ -320,3 +322,11 @@ input[type=search]::-webkit-search-results-decoration { font-size: 15px; padding: 0px 1.5em 0px 1.5em; } + +@define-mixin mx_DialogButton_secondary { + // flip colours for the secondary ones + font-weight: 600; + border: 1px solid $accent-color ! important; + color: $accent-color; + background-color: $accent-fg-color; +} diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 181df6c613..df2a242852 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -622,7 +622,7 @@ module.exports = React.createClass({ list: this.state.lists['im.vector.fake.invite'], label: _t('Invites'), order: "recent", - incomingCall={incomingCallIfTaggedAs('im.vector.fake.invite')}, + incomingCall: incomingCallIfTaggedAs('im.vector.fake.invite'), isInvite: true, }, { @@ -630,7 +630,7 @@ module.exports = React.createClass({ label: _t('Favourites'), tagName: "m.favourite", order: "manual", - incomingCall={incomingCallIfTaggedAs('m.favourite')}, + incomingCall: incomingCallIfTaggedAs('m.favourite'), }, { list: this.state.lists['im.vector.fake.direct'], @@ -638,7 +638,7 @@ module.exports = React.createClass({ tagName: "im.vector.fake.direct", headerItems: this._getHeaderItems('im.vector.fake.direct'), order: "recent", - incomingCall={incomingCallIfTaggedAs('im.vector.fake.direct')}, + incomingCall: incomingCallIfTaggedAs('im.vector.fake.direct'), onAddRoom: () => {dis.dispatch({action: 'view_create_chat'})}, }, { @@ -646,7 +646,7 @@ module.exports = React.createClass({ label: _t('Rooms'), headerItems: this._getHeaderItems('im.vector.fake.recent'), order: "recent", - incomingCall={incomingCallIfTaggedAs('im.vector.fake.recent')}, + incomingCall: incomingCallIfTaggedAs('im.vector.fake.recent'), onAddRoom: () => {dis.dispatch({action: 'view_create_room'})}, }, ]; @@ -660,7 +660,7 @@ module.exports = React.createClass({ label: labelForTagName(tagName), tagName: tagName, order: "manual", - incomingCallIfTaggedAs(tagName), + incomingCall: incomingCallIfTaggedAs(tagName), }; }); subLists = subLists.concat(tagSubLists); @@ -670,13 +670,13 @@ module.exports = React.createClass({ label: _t('Low priority'), tagName: "m.lowpriority", order: "recent", - incomingCall={incomingCallIfTaggedAs('m.lowpriority')}, + incomingCall: incomingCallIfTaggedAs('m.lowpriority'), }, { list: this.state.lists['im.vector.fake.archived'], label: _t('Historical'), order: "recent", - incomingCall={incomingCallIfTaggedAs('im.vector.fake.archived')}, + incomingCall: incomingCallIfTaggedAs('im.vector.fake.archived'), startAsHidden: true, showSpinner: this.state.isLoadingLeftRooms, onHeaderClick: this.onArchivedHeaderClick, @@ -686,7 +686,7 @@ module.exports = React.createClass({ label: _t('System Alerts'), tagName: "m.lowpriority", order: "recent", - incomingCall={incomingCallIfTaggedAs('m.server_notice')}, + incomingCall: incomingCallIfTaggedAs('m.server_notice'), }, ]); From 35c9dce241e70a6d8807b3a10ff125589ffeb27b Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 20 Dec 2018 17:26:13 -0700 Subject: [PATCH 203/237] Fix browser navigation not working between /home, /login, /register, etc All of the anchors were pointed at `#` which, when clicked, would trigger a hash change in the browser. This change races the change made by the screen handling where the screen handling ends up losing. Because the hash is then tracked as empty rather than `#/login` (for example), the state machine considers future changes as no-ops and doesn't do anything with them. By using `preventDefault` and `stopPropagation` on the anchor click events, we prevent the browser from automatically going to an empty hash, which then means the screen handling isn't racing the browser, and the hash change state machine doesn't no-op. After applying that fix, going between pages worked great unless you were going from /login to /home. This is because the MatrixChat state machine was now out of sync (a `view` of `LOGIN` but a `page` of `HomePage` - an invalid state). All we have to do here is ensure the right view is used when navigating to the homepage. Fixes https://github.com/vector-im/riot-web/issues/4061 Note: the concerns in 4061 about logging out upon entering the view appear to have been solved. Navigating to the login page doesn't obliterate your session, at least in my testing. --- src/components/structures/HomePage.js | 8 ++++++-- src/components/structures/MatrixChat.js | 3 +++ .../structures/login/ForgotPassword.js | 16 ++++++++++++++-- src/components/structures/login/Login.js | 12 ++++++++++-- src/components/structures/login/Registration.js | 8 +++++++- 5 files changed, 40 insertions(+), 7 deletions(-) diff --git a/src/components/structures/HomePage.js b/src/components/structures/HomePage.js index 01aabf6115..8f0c270513 100644 --- a/src/components/structures/HomePage.js +++ b/src/components/structures/HomePage.js @@ -91,11 +91,15 @@ class HomePage extends React.Component { this._unmounted = true; } - onLoginClick() { + onLoginClick(ev) { + ev.preventDefault(); + ev.stopPropagation(); dis.dispatch({ action: 'start_login' }); } - onRegisterClick() { + onRegisterClick(ev) { + ev.preventDefault(); + ev.stopPropagation(); dis.dispatch({ action: 'start_registration' }); } diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index b01174a91c..44689a4d30 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -927,6 +927,9 @@ export default React.createClass({ }, _viewHome: function() { + this.setStateForNewView({ + view: VIEWS.LOGGED_IN, + }); this._setPage(PageTypes.HomePage); this.notifyNewScreen('home'); }, diff --git a/src/components/structures/login/ForgotPassword.js b/src/components/structures/login/ForgotPassword.js index 559136948a..5c0e428339 100644 --- a/src/components/structures/login/ForgotPassword.js +++ b/src/components/structures/login/ForgotPassword.js @@ -162,6 +162,18 @@ module.exports = React.createClass({ this.setState(newState); }, + onLoginClick: function(ev) { + ev.preventDefault(); + ev.stopPropagation(); + this.props.onLoginClick(); + }, + + onRegisterClick: function(ev) { + ev.preventDefault(); + ev.stopPropagation(); + this.props.onRegisterClick(); + }, + showErrorDialog: function(body, title) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createTrackedDialog('Forgot Password Error', '', ErrorDialog, { @@ -253,10 +265,10 @@ module.exports = React.createClass({ { serverConfigSection } { errorText } - + { _t('Return to login screen') } - + { _t('Create an account') } diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index b94a1759cf..11bd3580e5 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -214,7 +214,9 @@ module.exports = React.createClass({ }).done(); }, - _onLoginAsGuestClick: function() { + _onLoginAsGuestClick: function(ev) { + ev.preventDefault(); + const self = this; self.setState({ busy: true, @@ -297,6 +299,12 @@ module.exports = React.createClass({ }); }, + onRegisterClick: function(ev) { + ev.preventDefault(); + ev.stopPropagation(); + this.props.onRegisterClick(); + }, + _tryWellKnownDiscovery: async function(serverName) { if (!serverName.trim()) { // Nothing to discover @@ -567,7 +575,7 @@ module.exports = React.createClass({ { errorTextSection } { this.componentForStep(this.state.currentFlow) } { serverConfig } - + { _t('Create an account') } { loginAsGuestJsx } diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index ad3ea5f19c..fa5a02e881 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -363,6 +363,12 @@ module.exports = React.createClass({ } }, + onLoginClick: function(ev) { + ev.preventDefault(); + ev.stopPropagation(); + this.props.onLoginClick(); + }, + _makeRegisterRequest: function(auth) { // Only send the bind params if we're sending username / pw params // (Since we need to send no params at all to use the ones saved in the @@ -468,7 +474,7 @@ module.exports = React.createClass({ let signIn; if (!this.state.doingUIAuth) { signIn = ( - + { theme === 'status' ? _t('Sign in') : _t('I already have an account') } ); From 63a7b86eac3489f28060e6132f25ed7df2506dd3 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 20 Dec 2018 14:56:18 -0700 Subject: [PATCH 204/237] Flatten and simplify the memberlist sorting algorithm The previous algorithm had a bug where it was getting stuck on the power level comparison, leaving the memberlist incorrectly ordered. The new algorithm uses less branching to try and walk through the different cases instead. Additionally, the steps used to determine the order have changed slightly to better represent an active member list. This commit also includes changes to try and re-sort the member list more often during presence changes. Events are not always emitted, however. This may be a js-sdk bug but appears to happen prior to these changes as well. Fixes https://github.com/vector-im/riot-web/issues/6953 --- src/components/views/rooms/MemberList.js | 94 ++++++++++++++---------- 1 file changed, 55 insertions(+), 39 deletions(-) diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js index 92c486825c..053d3cc1f5 100644 --- a/src/components/views/rooms/MemberList.js +++ b/src/components/views/rooms/MemberList.js @@ -68,7 +68,9 @@ module.exports = React.createClass({ // We listen for changes to the lastPresenceTs which is essentially // listening for all presence events (we display most of not all of // the information contained in presence events). - cli.on("User.lastPresenceTs", this.onUserLastPresenceTs); + cli.on("User.lastPresenceTs", this.onUserPresenceChange); + cli.on("User.presence", this.onUserPresenceChange); + cli.on("User.currentlyActive", this.onUserPresenceChange); // cli.on("Room.timeline", this.onRoomTimeline); }, @@ -81,7 +83,9 @@ module.exports = React.createClass({ cli.removeListener("Room.myMembership", this.onMyMembership); cli.removeListener("RoomState.events", this.onRoomStateEvent); cli.removeListener("Room", this.onRoom); - cli.removeListener("User.lastPresenceTs", this.onUserLastPresenceTs); + cli.removeListener("User.lastPresenceTs", this.onUserPresenceChange); + cli.removeListener("User.presence", this.onUserPresenceChange); + cli.removeListener("User.currentlyActive", this.onUserPresenceChange); } // cancel any pending calls to the rate_limited_funcs @@ -132,12 +136,12 @@ module.exports = React.createClass({ }; }, - onUserLastPresenceTs(event, user) { + onUserPresenceChange(event, user) { // Attach a SINGLE listener for global presence changes then locate the // member tile and re-render it. This is more efficient than every tile - // evar attaching their own listener. - // console.log("explicit presence from " + user.userId); + // ever attaching their own listener. const tile = this.refs[user.userId]; + console.log(`Got presence update for ${user.userId}. hasTile=${!!tile}`); if (tile) { this._updateList(); // reorder the membership list } @@ -267,7 +271,8 @@ module.exports = React.createClass({ if (!member) { return "(null)"; } else { - return "(" + member.name + ", " + member.powerLevel + ", " + member.user.lastActiveAgo + ", " + member.user.currentlyActive + ")"; + const u = member.user; + return "(" + member.name + ", " + member.powerLevel + ", " + (u ? u.lastActiveAgo : "") + ", " + (u ? u.getLastActiveTs() : "") + ", " + (u ? u.currentlyActive : "") + ")"; } }, @@ -275,48 +280,59 @@ module.exports = React.createClass({ // returns 0 if a and b are equivalent in ordering // returns positive if a comes after b. memberSort: function(memberA, memberB) { - // order by last active, with "active now" first. - // ...and then by power - // ...and then alphabetically. - // We could tiebreak instead by "last recently spoken in this room" if we wanted to. + // order by presence, with "active now" first. + // ...and then by power level + // ...and then by last active + // ...and then alphabetically. + // We could tiebreak instead by "last recently spoken in this room" if we wanted to. - const userA = memberA.user; - const userB = memberB.user; + console.log(`Comparing userA=${this.memberString(memberA)} userB=${this.memberString(memberB)}`); - // if (!userA || !userB) { - // console.log("comparing " + memberA.name + " user=" + memberA.user + " with " + memberB.name + " user=" + memberB.user); - // } + const userA = memberA.user; + const userB = memberB.user; - if (!userA && !userB) return 0; - if (userA && !userB) return -1; - if (!userA && userB) return 1; + if (!userA) console.log("!! MISSING USER FOR A-SIDE: " + memberA.name + " !!"); + if (!userB) console.log("!! MISSING USER FOR B-SIDE: " + memberB.name + " !!"); - // console.log("comparing " + this.memberString(memberA) + " and " + this.memberString(memberB)); + if (!userA && !userB) return 0; + if (userA && !userB) return -1; + if (!userA && userB) return 1; - if ((userA.currentlyActive && userB.currentlyActive) || !this._showPresence) { - // console.log(memberA.name + " and " + memberB.name + " are both active"); - if (memberA.powerLevel === memberB.powerLevel) { - // console.log(memberA + " and " + memberB + " have same power level"); - if (memberA.name && memberB.name) { - // console.log("comparing names: " + memberA.name + " and " + memberB.name); - const nameA = memberA.name[0] === '@' ? memberA.name.substr(1) : memberA.name; - const nameB = memberB.name[0] === '@' ? memberB.name.substr(1) : memberB.name; - return nameA.localeCompare(nameB); - } else { - return 0; - } - } else { - // console.log("comparing power: " + memberA.powerLevel + " and " + memberB.powerLevel); - return memberB.powerLevel - memberA.powerLevel; - } + // First by presence + if (this._showPresence) { + const convertPresence = (p) => p === 'unavailable' ? 'online' : p; + const presenceIndex = p => { + const order = ['active', 'online', 'offline']; + const idx = order.indexOf(convertPresence(p)); + return idx === -1 ? order.length : idx; // unknown states at the end + }; + + const idxA = presenceIndex(userA.currentlyActive ? 'active' : userA.presence); + const idxB = presenceIndex(userB.currentlyActive ? 'active' : userB.presence); + console.log(`userA_presenceGroup=${idxA} userB_presenceGroup=${idxB}`); + if (idxA !== idxB) { + console.log("Comparing on presence group - returning"); + return idxA - idxB; } + } - if (userA.currentlyActive && !userB.currentlyActive) return -1; - if (!userA.currentlyActive && userB.currentlyActive) return 1; + // Second by power level + if (memberA.powerLevel !== memberB.powerLevel) { + console.log("Comparing on power level - returning"); + return memberB.powerLevel - memberA.powerLevel; + } - // For now, let's just order things by timestamp. It's really annoying - // that a user disappears from sight just because they temporarily go offline + // Third by last active + if (this._showPresence && userA.getLastActiveTs() !== userB.getLastActiveTs) { + console.log("Comparing on last active timestamp - returning"); return userB.getLastActiveTs() - userA.getLastActiveTs(); + } + + // Fourth by name (alphabetical) + const nameA = memberA.name[0] === '@' ? memberA.name.substr(1) : memberA.name; + const nameB = memberB.name[0] === '@' ? memberB.name.substr(1) : memberB.name; + console.log(`Comparing userA_name=${nameA} against userB_name=${nameB} - returning`); + return nameA.localeCompare(nameB); }, onSearchQueryChanged: function(ev) { From 95844ebf9d795fa64a73ee710c5b0ea160273333 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 20 Dec 2018 14:57:27 -0700 Subject: [PATCH 205/237] Comment out debugging statements They are useful to have around, but not to have enabled all the time. --- src/components/views/rooms/MemberList.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js index 053d3cc1f5..dc97a3689c 100644 --- a/src/components/views/rooms/MemberList.js +++ b/src/components/views/rooms/MemberList.js @@ -141,7 +141,7 @@ module.exports = React.createClass({ // member tile and re-render it. This is more efficient than every tile // ever attaching their own listener. const tile = this.refs[user.userId]; - console.log(`Got presence update for ${user.userId}. hasTile=${!!tile}`); + // console.log(`Got presence update for ${user.userId}. hasTile=${!!tile}`); if (tile) { this._updateList(); // reorder the membership list } @@ -286,13 +286,13 @@ module.exports = React.createClass({ // ...and then alphabetically. // We could tiebreak instead by "last recently spoken in this room" if we wanted to. - console.log(`Comparing userA=${this.memberString(memberA)} userB=${this.memberString(memberB)}`); + // console.log(`Comparing userA=${this.memberString(memberA)} userB=${this.memberString(memberB)}`); const userA = memberA.user; const userB = memberB.user; - if (!userA) console.log("!! MISSING USER FOR A-SIDE: " + memberA.name + " !!"); - if (!userB) console.log("!! MISSING USER FOR B-SIDE: " + memberB.name + " !!"); + // if (!userA) console.log("!! MISSING USER FOR A-SIDE: " + memberA.name + " !!"); + // if (!userB) console.log("!! MISSING USER FOR B-SIDE: " + memberB.name + " !!"); if (!userA && !userB) return 0; if (userA && !userB) return -1; @@ -309,29 +309,29 @@ module.exports = React.createClass({ const idxA = presenceIndex(userA.currentlyActive ? 'active' : userA.presence); const idxB = presenceIndex(userB.currentlyActive ? 'active' : userB.presence); - console.log(`userA_presenceGroup=${idxA} userB_presenceGroup=${idxB}`); + // console.log(`userA_presenceGroup=${idxA} userB_presenceGroup=${idxB}`); if (idxA !== idxB) { - console.log("Comparing on presence group - returning"); + // console.log("Comparing on presence group - returning"); return idxA - idxB; } } // Second by power level if (memberA.powerLevel !== memberB.powerLevel) { - console.log("Comparing on power level - returning"); + // console.log("Comparing on power level - returning"); return memberB.powerLevel - memberA.powerLevel; } // Third by last active if (this._showPresence && userA.getLastActiveTs() !== userB.getLastActiveTs) { - console.log("Comparing on last active timestamp - returning"); + // console.log("Comparing on last active timestamp - returning"); return userB.getLastActiveTs() - userA.getLastActiveTs(); } // Fourth by name (alphabetical) const nameA = memberA.name[0] === '@' ? memberA.name.substr(1) : memberA.name; const nameB = memberB.name[0] === '@' ? memberB.name.substr(1) : memberB.name; - console.log(`Comparing userA_name=${nameA} against userB_name=${nameB} - returning`); + // console.log(`Comparing userA_name=${nameA} against userB_name=${nameB} - returning`); return nameA.localeCompare(nameB); }, From 223a49d81f289630db10763fe684d4eec506ce03 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 3 Jan 2019 13:57:20 -0700 Subject: [PATCH 206/237] Don't re-sort the room list if the user is hovering over it Fixes vector-im/riot-web#5624 --- src/components/views/rooms/RoomList.js | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index df2a242852..af6533296e 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -86,6 +86,7 @@ module.exports = React.createClass({ incomingCallTag: null, incomingCall: null, selectedTags: [], + hover: false, }; }, @@ -294,6 +295,17 @@ module.exports = React.createClass({ this.forceUpdate(); }, + onMouseEnter: function(ev) { + this.setState({hover: true}); + }, + + onMouseLeave: function(ev) { + this.setState({hover: false}); + + // Refresh the room list just in case the user missed something. + this._delayedRefreshRoomList(); + }, + _delayedRefreshRoomList: new rate_limited_func(function() { this.refreshRoomList(); }, 500), @@ -346,6 +358,11 @@ module.exports = React.createClass({ }, refreshRoomList: function() { + if (this.state.hover) { + // Don't re-sort the list if we're hovering over the list + return; + } + // TODO: ideally we'd calculate this once at start, and then maintain // any changes to it incrementally, updating the appropriate sublists // as needed. @@ -693,7 +710,8 @@ module.exports = React.createClass({ const subListComponents = this._mapSubListProps(subLists); return ( -
        +
        { subListComponents }
        ); From 7904b91b58a8b1079447347256e18e6eb7d7d674 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 2 Jan 2019 13:07:10 -0700 Subject: [PATCH 207/237] Use the safer way to set the logged in view state --- src/components/structures/MatrixChat.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 44689a4d30..38f0243444 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1186,10 +1186,7 @@ export default React.createClass({ * @param {string} teamToken */ _onLoggedIn: async function(teamToken) { - this.setState({ - view: VIEWS.LOGGED_IN, - }); - + this.setStateForNewView({view: VIEWS.LOGGED_IN}); if (teamToken) { // A team member has logged in, not a guest this._teamToken = teamToken; From 4c264b87fa6aaf96f2cc1010e2cd1f0a35f396e7 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 2 Jan 2019 13:07:40 -0700 Subject: [PATCH 208/237] Add missing stopPropagation --- src/components/structures/login/Login.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index 11bd3580e5..321084389b 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -216,6 +216,7 @@ module.exports = React.createClass({ _onLoginAsGuestClick: function(ev) { ev.preventDefault(); + ev.stopPropagation(); const self = this; self.setState({ From e5192811a1b3b6da901d0d3543ccd4a1dd4ce2ce Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 3 Jan 2019 14:22:16 -0700 Subject: [PATCH 209/237] Add a comment explaining the reason for setting the LOGGED_IN view --- src/components/structures/MatrixChat.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 38f0243444..a03265da1c 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -927,6 +927,7 @@ export default React.createClass({ }, _viewHome: function() { + // The home page requires the "logged in" view, so we'll set that. this.setStateForNewView({ view: VIEWS.LOGGED_IN, }); From 0978687c65d5d2b9a853af04ce156748b113f70b Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 3 Jan 2019 14:37:04 -0700 Subject: [PATCH 210/237] Revert "Merge pull request #2383 from matrix-org/travis/back-button" This reverts commit 78592286bd0703173baa634627c8873167add862. --- src/components/structures/HomePage.js | 8 ++------ src/components/structures/MatrixChat.js | 9 ++++----- .../structures/login/ForgotPassword.js | 16 ++-------------- src/components/structures/login/Login.js | 13 ++----------- src/components/structures/login/Registration.js | 8 +------- 5 files changed, 11 insertions(+), 43 deletions(-) diff --git a/src/components/structures/HomePage.js b/src/components/structures/HomePage.js index 8f0c270513..01aabf6115 100644 --- a/src/components/structures/HomePage.js +++ b/src/components/structures/HomePage.js @@ -91,15 +91,11 @@ class HomePage extends React.Component { this._unmounted = true; } - onLoginClick(ev) { - ev.preventDefault(); - ev.stopPropagation(); + onLoginClick() { dis.dispatch({ action: 'start_login' }); } - onRegisterClick(ev) { - ev.preventDefault(); - ev.stopPropagation(); + onRegisterClick() { dis.dispatch({ action: 'start_registration' }); } diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index a03265da1c..b01174a91c 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -927,10 +927,6 @@ export default React.createClass({ }, _viewHome: function() { - // The home page requires the "logged in" view, so we'll set that. - this.setStateForNewView({ - view: VIEWS.LOGGED_IN, - }); this._setPage(PageTypes.HomePage); this.notifyNewScreen('home'); }, @@ -1187,7 +1183,10 @@ export default React.createClass({ * @param {string} teamToken */ _onLoggedIn: async function(teamToken) { - this.setStateForNewView({view: VIEWS.LOGGED_IN}); + this.setState({ + view: VIEWS.LOGGED_IN, + }); + if (teamToken) { // A team member has logged in, not a guest this._teamToken = teamToken; diff --git a/src/components/structures/login/ForgotPassword.js b/src/components/structures/login/ForgotPassword.js index 5c0e428339..559136948a 100644 --- a/src/components/structures/login/ForgotPassword.js +++ b/src/components/structures/login/ForgotPassword.js @@ -162,18 +162,6 @@ module.exports = React.createClass({ this.setState(newState); }, - onLoginClick: function(ev) { - ev.preventDefault(); - ev.stopPropagation(); - this.props.onLoginClick(); - }, - - onRegisterClick: function(ev) { - ev.preventDefault(); - ev.stopPropagation(); - this.props.onRegisterClick(); - }, - showErrorDialog: function(body, title) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createTrackedDialog('Forgot Password Error', '', ErrorDialog, { @@ -265,10 +253,10 @@ module.exports = React.createClass({ { serverConfigSection } { errorText } - + { _t('Return to login screen') } - + { _t('Create an account') } diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index 321084389b..b94a1759cf 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -214,10 +214,7 @@ module.exports = React.createClass({ }).done(); }, - _onLoginAsGuestClick: function(ev) { - ev.preventDefault(); - ev.stopPropagation(); - + _onLoginAsGuestClick: function() { const self = this; self.setState({ busy: true, @@ -300,12 +297,6 @@ module.exports = React.createClass({ }); }, - onRegisterClick: function(ev) { - ev.preventDefault(); - ev.stopPropagation(); - this.props.onRegisterClick(); - }, - _tryWellKnownDiscovery: async function(serverName) { if (!serverName.trim()) { // Nothing to discover @@ -576,7 +567,7 @@ module.exports = React.createClass({ { errorTextSection } { this.componentForStep(this.state.currentFlow) } { serverConfig } - + { _t('Create an account') } { loginAsGuestJsx } diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index fa5a02e881..ad3ea5f19c 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -363,12 +363,6 @@ module.exports = React.createClass({ } }, - onLoginClick: function(ev) { - ev.preventDefault(); - ev.stopPropagation(); - this.props.onLoginClick(); - }, - _makeRegisterRequest: function(auth) { // Only send the bind params if we're sending username / pw params // (Since we need to send no params at all to use the ones saved in the @@ -474,7 +468,7 @@ module.exports = React.createClass({ let signIn; if (!this.state.doingUIAuth) { signIn = ( - + { theme === 'status' ? _t('Sign in') : _t('I already have an account') } ); From d111fe27bb85b001c063c116584221c9aed0a1b3 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 3 Jan 2019 14:34:18 -0700 Subject: [PATCH 211/237] Revert "Merge pull request #2396 from matrix-org/travis/room-list-hover" This reverts commit 5098b28672ecd246dce74a7e7f140ad9be5e5ff9. --- src/components/views/rooms/RoomList.js | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index af6533296e..df2a242852 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -86,7 +86,6 @@ module.exports = React.createClass({ incomingCallTag: null, incomingCall: null, selectedTags: [], - hover: false, }; }, @@ -295,17 +294,6 @@ module.exports = React.createClass({ this.forceUpdate(); }, - onMouseEnter: function(ev) { - this.setState({hover: true}); - }, - - onMouseLeave: function(ev) { - this.setState({hover: false}); - - // Refresh the room list just in case the user missed something. - this._delayedRefreshRoomList(); - }, - _delayedRefreshRoomList: new rate_limited_func(function() { this.refreshRoomList(); }, 500), @@ -358,11 +346,6 @@ module.exports = React.createClass({ }, refreshRoomList: function() { - if (this.state.hover) { - // Don't re-sort the list if we're hovering over the list - return; - } - // TODO: ideally we'd calculate this once at start, and then maintain // any changes to it incrementally, updating the appropriate sublists // as needed. @@ -710,8 +693,7 @@ module.exports = React.createClass({ const subListComponents = this._mapSubListProps(subLists); return ( -
        +
        { subListComponents }
        ); From f928be6f5933be8e64f78d4efc7ee80ee5f0aadc Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 3 Jan 2019 18:55:56 +0000 Subject: [PATCH 212/237] Revert "Merge pull request #2395 from matrix-org/dbkr/merge_develop_experimental" This reverts commit ad47144355e221e9a430f09dcba0b9c6d99a74a8. --- .eslintrc.js | 3 - CHANGELOG.md | 54 -- CONTRIBUTING.rst | 2 +- README.md | 58 ++ code_style.md | 34 +- package.json | 9 +- res/css/_common.scss | 2 +- res/css/_components.scss | 2 - .../keybackup/_CreateKeyBackupDialog.scss | 20 +- .../keybackup/_NewRecoveryMethodDialog.scss | 41 - .../views/rooms/_RoomRecoveryReminder.scss | 44 - res/img/e2e/lock-warning.svg | 1 - res/themes/dark/css/_dark.scss | 10 - res/themes/dharma/css/_dharma.scss | 10 - res/themes/light/css/_base.scss | 10 - scripts/gen-i18n.js | 16 +- src/BasePlatform.js | 6 +- src/ContentMessages.js | 4 +- src/Lifecycle.js | 30 +- src/Login.js | 99 ++- src/Notifier.js | 5 - src/Registration.js | 4 - src/RoomInvite.js | 65 +- src/SlashCommands.js | 11 +- src/Tinter.js | 22 +- .../keybackup/CreateKeyBackupDialog.js | 79 +- .../keybackup/IgnoreRecoveryReminderDialog.js | 70 -- .../keybackup/NewRecoveryMethodDialog.js | 110 --- src/components/structures/GroupView.js | 18 +- src/components/structures/MatrixChat.js | 81 +- src/components/structures/RoomSubList.js | 15 +- src/components/structures/RoomView.js | 67 +- src/components/structures/UserSettings.js | 46 +- .../structures/login/ForgotPassword.js | 23 - src/components/structures/login/Login.js | 109 +-- .../structures/login/Registration.js | 25 +- .../views/dialogs/AddressPickerDialog.js | 5 - src/components/views/dialogs/BaseDialog.js | 3 +- .../views/dialogs/DeactivateAccountDialog.js | 41 +- src/components/views/dialogs/SetMxIdDialog.js | 15 +- src/components/views/elements/AppTile.js | 37 +- src/components/views/elements/TintableSvg.js | 17 +- .../views/groups/GroupMemberList.js | 39 +- src/components/views/login/PasswordLogin.js | 24 +- .../views/login/RegistrationForm.js | 7 +- src/components/views/login/ServerConfig.js | 17 - .../views/room_settings/AliasSettings.js | 2 +- src/components/views/rooms/MemberInfo.js | 11 +- src/components/views/rooms/MessageComposer.js | 63 +- src/components/views/rooms/RoomList.js | 37 - .../views/rooms/RoomRecoveryReminder.js | 85 -- .../views/settings/KeyBackupPanel.js | 5 +- .../views/settings/Notifications.js | 6 - src/i18n/strings/de_DE.json | 57 +- src/i18n/strings/en_EN.json | 195 ++--- src/i18n/strings/en_US.json | 2 - src/i18n/strings/eu.json | 80 +- src/i18n/strings/fr.json | 110 +-- src/i18n/strings/hi.json | 341 +------- src/i18n/strings/hu.json | 110 +-- src/i18n/strings/pl.json | 34 +- src/i18n/strings/sq.json | 792 ++---------------- src/i18n/strings/zh_Hant.json | 109 +-- src/matrix-to.js | 61 +- src/notifications/StandardActions.js | 1 - .../VectorPushRulesDefinitions.js | 54 +- src/settings/Settings.js | 5 - src/shouldHideEvent.js | 12 +- src/stores/GroupStore.js | 6 +- src/stores/RoomListStore.js | 21 +- src/stores/RoomViewStore.js | 5 - src/utils/MultiInviter.js | 45 +- src/utils/PasswordScorer.js | 84 -- test/components/structures/GroupView-test.js | 37 +- .../views/groups/GroupMemberList-test.js | 149 ---- test/matrix-to-test.js | 186 +--- test/test-utils.js | 15 +- 77 files changed, 598 insertions(+), 3532 deletions(-) delete mode 100644 res/css/views/dialogs/keybackup/_NewRecoveryMethodDialog.scss delete mode 100644 res/css/views/rooms/_RoomRecoveryReminder.scss delete mode 100644 res/img/e2e/lock-warning.svg delete mode 100644 src/async-components/views/dialogs/keybackup/IgnoreRecoveryReminderDialog.js delete mode 100644 src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js delete mode 100644 src/components/views/rooms/RoomRecoveryReminder.js delete mode 100644 src/utils/PasswordScorer.js delete mode 100644 test/components/views/groups/GroupMemberList-test.js diff --git a/.eslintrc.js b/.eslintrc.js index 971809f851..62d24ea707 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -47,9 +47,6 @@ module.exports = { }], "react/jsx-key": ["error"], - // Components in JSX should always be defined. - "react/jsx-no-undef": "error", - // Assert no spacing in JSX curly brackets // // diff --git a/CHANGELOG.md b/CHANGELOG.md index 742b8b4529..eea47dcb8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,57 +1,3 @@ -Changes in [0.14.7](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.14.7) (2018-12-10) -===================================================================================================== -[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.14.7-rc.2...v0.14.7) - - * No changes since rc.2 - -Changes in [0.14.7-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.14.7-rc.2) (2018-12-06) -=============================================================================================================== -[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.14.7-rc.1...v0.14.7-rc.2) - - * Ship the babelrc file to npm - [\#2332](https://github.com/matrix-org/matrix-react-sdk/pull/2332) - -Changes in [0.14.7-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.14.7-rc.1) (2018-12-06) -=============================================================================================================== -[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.14.6...v0.14.7-rc.1) - - * Suppress CORS errors in the 'failed to join room' dialog - [\#2306](https://github.com/matrix-org/matrix-react-sdk/pull/2306) - * Check if users exist before inviting them and communicate errors - [\#2317](https://github.com/matrix-org/matrix-react-sdk/pull/2317) - * Update from Weblate. - [\#2328](https://github.com/matrix-org/matrix-react-sdk/pull/2328) - * Allow group summary to load when /users fails - [\#2326](https://github.com/matrix-org/matrix-react-sdk/pull/2326) - * Show correct text if passphrase is skipped - [\#2324](https://github.com/matrix-org/matrix-react-sdk/pull/2324) - * Add password strength meter to backup creation UI - [\#2294](https://github.com/matrix-org/matrix-react-sdk/pull/2294) - * Check upload limits before trying to upload large files - [\#1876](https://github.com/matrix-org/matrix-react-sdk/pull/1876) - * Support .well-known discovery - [\#2227](https://github.com/matrix-org/matrix-react-sdk/pull/2227) - * Make create key backup dialog async - [\#2291](https://github.com/matrix-org/matrix-react-sdk/pull/2291) - * Forgot to enable continue button on download - [\#2288](https://github.com/matrix-org/matrix-react-sdk/pull/2288) - * Online incremental megolm backups (v2) - [\#2169](https://github.com/matrix-org/matrix-react-sdk/pull/2169) - * Add recovery key download button - [\#2284](https://github.com/matrix-org/matrix-react-sdk/pull/2284) - * Passphrase Support for e2e backups - [\#2283](https://github.com/matrix-org/matrix-react-sdk/pull/2283) - * Update async dialog interface to use promises - [\#2286](https://github.com/matrix-org/matrix-react-sdk/pull/2286) - * Support for m.login.sso - [\#2279](https://github.com/matrix-org/matrix-react-sdk/pull/2279) - * Added badge to non-autoplay GIFs - [\#2235](https://github.com/matrix-org/matrix-react-sdk/pull/2235) - * Improve terms auth flow - [\#2277](https://github.com/matrix-org/matrix-react-sdk/pull/2277) - * Handle crypto db version upgrade - [\#2282](https://github.com/matrix-org/matrix-react-sdk/pull/2282) - Changes in [0.14.6](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.14.6) (2018-11-22) ===================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.14.5...v0.14.6) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index f7c8c8b1c5..99025f0e0a 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -1,4 +1,4 @@ Contributing code to The React SDK ================================== -matrix-react-sdk follows the same pattern as https://github.com/matrix-org/matrix-js-sdk/blob/master/CONTRIBUTING.rst +matrix-react-sdk follows the same pattern as https://github.com/matrix-org/synapse/blob/master/CONTRIBUTING.rst diff --git a/README.md b/README.md index ec95fbd132..ac45497dd4 100644 --- a/README.md +++ b/README.md @@ -127,3 +127,61 @@ Github Issues All issues should be filed under https://github.com/vector-im/riot-web/issues for now. + +OUTDATED: To Create Your Own Skin +================================= + +**This is ALL LIES currently, and needs to be updated** + +Skins are modules are exported from such a package in the `lib` directory. +`lib/skins` contains one directory per-skin, named after the skin, and the +`modules` directory contains modules as their javascript files. + +A basic skin is provided in the matrix-react-skin package. This also contains +a minimal application that instantiates the basic skin making a working matrix +client. + +You can use matrix-react-sdk directly, but to do this you would have to provide +'views' for each UI component. To get started quickly, use matrix-react-skin. + +To actually change the look of a skin, you can create a base skin (which +does not use views from any other skin) or you can make a derived skin. +Note that derived skins are currently experimental: for example, the CSS +from the skins it is based on will not be automatically included. + +To make a skin, create React classes for any custom components you wish to add +in a skin within `src/skins/`. These can be based off the files in +`views` in the `matrix-react-skin` package, modifying the require() statement +appropriately. + +If you make a derived skin, you only need copy the files you wish to customise. + +Once you've made all your view files, you need to make a `skinfo.json`. This +contains all the metadata for a skin. This is a JSON file with, currently, a +single key, 'baseSkin'. Set this to the empty string if your skin is a base skin, +or for a derived skin, set it to the path of your base skin's skinfo.json file, as +you would use in a require call. + +Now you have the basis of a skin, you need to generate a skindex.json file. The +`reskindex.js` tool in matrix-react-sdk does this for you. It is suggested that +you add an npm script to run this, as in matrix-react-skin. + +For more specific detail on any of these steps, look at matrix-react-skin. + +Alternative instructions: + + * Create a new NPM project. Be sure to directly depend on react, (otherwise + you can end up with two copies of react). + * Create an index.js file that sets up react. Add require statements for + React and matrix-react-sdk. Load a skin using the 'loadSkin' method on the + SDK and call Render. This can be a skin provided by a separate package or + a skin in the same package. + * Add a way to build your project: we suggest copying the scripts block + from matrix-react-skin (which uses babel and webpack). You could use + different tools but remember that at least the skins and modules of + your project should end up in plain (ie. non ES6, non JSX) javascript in + the lib directory at the end of the build process, as well as any + packaging that you might do. + * Create an index.html file pulling in your compiled javascript and the + CSS bundle from the skin you use. For now, you'll also need to manually + import CSS from any skins that your skin inherts from. diff --git a/code_style.md b/code_style.md index 96f3879ebc..2cac303e54 100644 --- a/code_style.md +++ b/code_style.md @@ -165,6 +165,7 @@ ECMAScript React ----- +- Use React.createClass rather than ES6 classes for components, as the boilerplate is way too heavy on ES6 currently. ES7 might improve it. - Pull out functions in props to the class, generally as specific event handlers: ```jsx @@ -173,38 +174,11 @@ React // Better // Best, if onFooClick would do anything other than directly calling doStuff ``` - - Not doing so is acceptable in a single case: in function-refs: - + + Not doing so is acceptable in a single case; in function-refs: + ```jsx this.component = self}> ``` - -- Prefer classes that extend `React.Component` (or `React.PureComponent`) instead of `React.createClass` - - You can avoid the need to bind handler functions by using [property initializers](https://reactjs.org/docs/react-component.html#constructor): - - ```js - class Widget extends React.Component - onFooClick = () => { - ... - } - } - ``` - - To define `propTypes`, use a static property: - ```js - class Widget extends React.Component - static propTypes = { - ... - } - } - ``` - - If you need to specify initial component state, [assign it](https://reactjs.org/docs/react-component.html#constructor) to `this.state` in the constructor: - ```js - constructor(props) { - super(props); - // Don't call this.setState() here! - this.state = { counter: 0 }; - } - ``` - Think about whether your component really needs state: are you duplicating information in component state that could be derived from the model? diff --git a/package.json b/package.json index 7a63d55415..b5cdfdf401 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "0.14.7", + "version": "0.14.6", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -10,7 +10,6 @@ "license": "Apache-2.0", "main": "lib/index.js", "files": [ - ".babelrc", ".eslintrc.js", "CHANGELOG.md", "CONTRIBUTING.rst", @@ -73,12 +72,11 @@ "gfm.css": "^1.1.1", "glob": "^5.0.14", "highlight.js": "^9.13.0", - "is-ip": "^2.0.0", "isomorphic-fetch": "^2.2.1", "linkifyjs": "^2.1.6", "lodash": "^4.13.1", "lolex": "2.3.2", - "matrix-js-sdk": "0.14.2", + "matrix-js-sdk": "0.14.1", "optimist": "^0.6.1", "pako": "^1.0.5", "prop-types": "^15.5.8", @@ -98,8 +96,7 @@ "text-encoding-utf-8": "^1.0.1", "url": "^0.11.0", "velocity-vector": "github:vector-im/velocity#059e3b2", - "whatwg-fetch": "^1.1.1", - "zxcvbn": "^4.4.2" + "whatwg-fetch": "^1.1.1" }, "devDependencies": { "babel-cli": "^6.26.0", diff --git a/res/css/_common.scss b/res/css/_common.scss index bec4c02c18..797070d4e2 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -32,7 +32,7 @@ body { margin: 0px; } -.error, .warning { +div.error, div.warning { color: $warning-color; } diff --git a/res/css/_components.scss b/res/css/_components.scss index 63b1bde2d6..92e243e8d1 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -49,7 +49,6 @@ @import "./views/dialogs/_ShareDialog.scss"; @import "./views/dialogs/_UnknownDeviceDialog.scss"; @import "./views/dialogs/keybackup/_CreateKeyBackupDialog.scss"; -@import "./views/dialogs/keybackup/_NewRecoveryMethodDialog.scss"; @import "./views/dialogs/keybackup/_RestoreKeyBackupDialog.scss"; @import "./views/directory/_NetworkDropdown.scss"; @import "./views/elements/_AccessibleButton.scss"; @@ -105,7 +104,6 @@ @import "./views/rooms/_RoomHeader.scss"; @import "./views/rooms/_RoomList.scss"; @import "./views/rooms/_RoomPreviewBar.scss"; -@import "./views/rooms/_RoomRecoveryReminder.scss"; @import "./views/rooms/_RoomSettings.scss"; @import "./views/rooms/_RoomTile.scss"; @import "./views/rooms/_RoomTooltip.scss"; diff --git a/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss b/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss index 2cb6b11c0c..507c89ace7 100644 --- a/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss +++ b/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss @@ -19,26 +19,8 @@ limitations under the License. padding: 20px } -.mx_CreateKeyBackupDialog_primaryContainer::after { - content: ""; - clear: both; - display: block; -} - -.mx_CreateKeyBackupDialog_passPhraseHelp { - float: right; - width: 230px; - height: 85px; - margin-left: 20px; - font-size: 80%; -} - -.mx_CreateKeyBackupDialog_passPhraseHelp progress { - width: 100%; -} - .mx_CreateKeyBackupDialog_passPhraseInput { - width: 250px; + width: 300px; border: 1px solid $accent-color; border-radius: 5px; padding: 10px; diff --git a/res/css/views/dialogs/keybackup/_NewRecoveryMethodDialog.scss b/res/css/views/dialogs/keybackup/_NewRecoveryMethodDialog.scss deleted file mode 100644 index 370f82d9ab..0000000000 --- a/res/css/views/dialogs/keybackup/_NewRecoveryMethodDialog.scss +++ /dev/null @@ -1,41 +0,0 @@ -/* -Copyright 2018 New Vector Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -.mx_NewRecoveryMethodDialog .mx_Dialog_title { - margin-bottom: 32px; -} - -.mx_NewRecoveryMethodDialog_title { - position: relative; - padding-left: 45px; - padding-bottom: 10px; - - &:before { - mask: url("../../../img/e2e/lock-warning.svg"); - mask-repeat: no-repeat; - background-color: $primary-fg-color; - content: ""; - position: absolute; - top: -6px; - right: 0; - bottom: 0; - left: 0; - } -} - -.mx_NewRecoveryMethodDialog .mx_Dialog_buttons { - margin-top: 36px; -} diff --git a/res/css/views/rooms/_RoomRecoveryReminder.scss b/res/css/views/rooms/_RoomRecoveryReminder.scss deleted file mode 100644 index e4e2d19b42..0000000000 --- a/res/css/views/rooms/_RoomRecoveryReminder.scss +++ /dev/null @@ -1,44 +0,0 @@ -/* -Copyright 2018 New Vector Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -.mx_RoomRecoveryReminder { - display: flex; - flex-direction: column; - text-align: center; - background-color: $room-warning-bg-color; - padding: 20px; - border: 1px solid $primary-hairline-color; - border-bottom: unset; -} - -.mx_RoomRecoveryReminder_header { - font-weight: bold; - margin-bottom: 1em; -} - -.mx_RoomRecoveryReminder_body { - margin-bottom: 1em; -} - -.mx_RoomRecoveryReminder_button { - @mixin mx_DialogButton; - margin: 0 10px; -} - -.mx_RoomRecoveryReminder_button.mx_RoomRecoveryReminder_secondary { - @mixin mx_DialogButton_secondary; - background-color: transparent; -} diff --git a/res/img/e2e/lock-warning.svg b/res/img/e2e/lock-warning.svg deleted file mode 100644 index a984ed85a0..0000000000 --- a/res/img/e2e/lock-warning.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 636db5b39e..ed84bde698 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -107,8 +107,6 @@ $voip-accept-color: #80f480; $rte-bg-color: #353535; $rte-code-bg-color: #000; -$room-warning-bg-color: #2d2d2d; - // ******************** $roomtile-name-color: rgba(186, 186, 186, 0.8); @@ -187,14 +185,6 @@ $progressbar-color: #000; outline: none; } -@define-mixin mx_DialogButton_secondary { - // flip colours for the secondary ones - font-weight: 600; - border: 1px solid $accent-color ! important; - color: $accent-color; - background-color: $accent-fg-color; -} - // Nasty hacks to apply a filter to arbitrary monochrome artwork to make it // better match the theme. Typically applied to dark grey 'off' buttons or // light grey 'on' buttons. diff --git a/res/themes/dharma/css/_dharma.scss b/res/themes/dharma/css/_dharma.scss index 08a287ad71..0851762be2 100644 --- a/res/themes/dharma/css/_dharma.scss +++ b/res/themes/dharma/css/_dharma.scss @@ -186,8 +186,6 @@ $lightbox-border-color: #ffffff; // unused? $progressbar-color: #000; -$room-warning-bg-color: #fff8e3; - /*** form elements ***/ // .mx_textinput is a container for a text input @@ -322,11 +320,3 @@ input[type=search]::-webkit-search-results-decoration { font-size: 15px; padding: 0px 1.5em 0px 1.5em; } - -@define-mixin mx_DialogButton_secondary { - // flip colours for the secondary ones - font-weight: 600; - border: 1px solid $accent-color ! important; - color: $accent-color; - background-color: $accent-fg-color; -} diff --git a/res/themes/light/css/_base.scss b/res/themes/light/css/_base.scss index 9fcb58d7f1..71c1ab5e3c 100644 --- a/res/themes/light/css/_base.scss +++ b/res/themes/light/css/_base.scss @@ -181,8 +181,6 @@ $imagebody-giflabel-border: rgba(0, 0, 0, 0.2); // unused? $progressbar-color: #000; -$room-warning-bg-color: #fff8e3; - // ***** Mixins! ***** @define-mixin mx_DialogButton { @@ -215,11 +213,3 @@ $room-warning-bg-color: #fff8e3; font-size: 15px; padding: 0px 1.5em 0px 1.5em; } - -@define-mixin mx_DialogButton_secondary { - // flip colours for the secondary ones - font-weight: 600; - border: 1px solid $accent-color ! important; - color: $accent-color; - background-color: $accent-fg-color; -} diff --git a/scripts/gen-i18n.js b/scripts/gen-i18n.js index 3d3d5af116..a1a2e6f7c5 100755 --- a/scripts/gen-i18n.js +++ b/scripts/gen-i18n.js @@ -222,21 +222,10 @@ const translatables = new Set(); const walkOpts = { listeners: { - names: function(root, nodeNamesArray) { - // Sort the names case insensitively and alphabetically to - // maintain some sense of order between the different strings. - nodeNamesArray.sort((a, b) => { - a = a.toLowerCase(); - b = b.toLowerCase(); - if (a > b) return 1; - if (a < b) return -1; - return 0; - }); - }, file: function(root, fileStats, next) { const fullPath = path.join(root, fileStats.name); - let trs; + let ltrs; if (fileStats.name.endsWith('.js')) { trs = getTranslationsJs(fullPath); } else if (fileStats.name.endsWith('.html')) { @@ -246,8 +235,7 @@ const walkOpts = { } console.log(`${fullPath} (${trs.size} strings)`); for (const tr of trs.values()) { - // Convert DOS line endings to unix - translatables.add(tr.replace(/\r\n/g, "\n")); + translatables.add(tr); } }, } diff --git a/src/BasePlatform.js b/src/BasePlatform.js index 79f0d69e2c..abc9aa0bed 100644 --- a/src/BasePlatform.js +++ b/src/BasePlatform.js @@ -3,7 +3,6 @@ /* Copyright 2016 Aviral Dasgupta Copyright 2016 OpenMarket Ltd -Copyright 2018 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -106,6 +105,11 @@ export default class BasePlatform { return "Not implemented"; } + isElectron(): boolean { return false; } + + setupScreenSharingForIframe() { + } + /** * Restarts the application, without neccessarily reloading * any application code diff --git a/src/ContentMessages.js b/src/ContentMessages.js index f2bbdfafe5..fd21977108 100644 --- a/src/ContentMessages.js +++ b/src/ContentMessages.js @@ -377,9 +377,9 @@ class ContentMessages { } } if (error) { - dis.dispatch({action: 'upload_failed', upload, error}); + dis.dispatch({action: 'upload_failed', upload: upload}); } else { - dis.dispatch({action: 'upload_finished', upload}); + dis.dispatch({action: 'upload_finished', upload: upload}); } }); } diff --git a/src/Lifecycle.js b/src/Lifecycle.js index ed057eb020..b0912c759e 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -32,7 +32,6 @@ import Modal from './Modal'; import sdk from './index'; import ActiveWidgetStore from './stores/ActiveWidgetStore'; import PlatformPeg from "./PlatformPeg"; -import {sendLoginRequest} from "./Login"; /** * Called at startup, to attempt to build a logged-in Matrix session. It tries @@ -130,17 +129,27 @@ export function attemptTokenLogin(queryParams, defaultDeviceDisplayName) { return Promise.resolve(false); } - return sendLoginRequest( - queryParams.homeserver, - queryParams.identityServer, + // create a temporary MatrixClient to do the login + const client = Matrix.createClient({ + baseUrl: queryParams.homeserver, + }); + + return client.login( "m.login.token", { token: queryParams.loginToken, initial_device_display_name: defaultDeviceDisplayName, }, - ).then(function(creds) { + ).then(function(data) { console.log("Logged in with token"); return _clearStorage().then(() => { - _persistCredentialsToLocalStorage(creds); + _persistCredentialsToLocalStorage({ + userId: data.user_id, + deviceId: data.device_id, + accessToken: data.access_token, + homeserverUrl: queryParams.homeserver, + identityServerUrl: queryParams.identityServer, + guest: false, + }); return true; }); }).catch((err) => { @@ -497,7 +506,16 @@ function _clearStorage() { Analytics.logout(); if (window.localStorage) { + const hsUrl = window.localStorage.getItem("mx_hs_url"); + const isUrl = window.localStorage.getItem("mx_is_url"); window.localStorage.clear(); + + // preserve our HS & IS URLs for convenience + // N.B. we cache them in hsUrl/isUrl and can't really inline them + // as getCurrentHsUrl() may call through to localStorage. + // NB. We do clear the device ID (as well as all the settings) + if (hsUrl) window.localStorage.setItem("mx_hs_url", hsUrl); + if (isUrl) window.localStorage.setItem("mx_is_url", isUrl); } // create a temporary client to clear out the persistent stores. diff --git a/src/Login.js b/src/Login.js index 330eb8a8f5..ec55a1e8c7 100644 --- a/src/Login.js +++ b/src/Login.js @@ -1,7 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd Copyright 2017 Vector Creations Ltd -Copyright 2018 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -18,6 +17,7 @@ limitations under the License. import Matrix from "matrix-js-sdk"; +import Promise from 'bluebird'; import url from 'url'; export default class Login { @@ -141,20 +141,60 @@ export default class Login { }; Object.assign(loginParams, legacyParams); + const client = this._createTemporaryClient(); + const tryFallbackHs = (originalError) => { - return sendLoginRequest( - self._fallbackHsUrl, this._isUrl, 'm.login.password', loginParams, - ).catch((fallback_error) => { + const fbClient = Matrix.createClient({ + baseUrl: self._fallbackHsUrl, + idBaseUrl: this._isUrl, + }); + + return fbClient.login('m.login.password', loginParams).then(function(data) { + return Promise.resolve({ + homeserverUrl: self._fallbackHsUrl, + identityServerUrl: self._isUrl, + userId: data.user_id, + deviceId: data.device_id, + accessToken: data.access_token, + }); + }).catch((fallback_error) => { console.log("fallback HS login failed", fallback_error); // throw the original error throw originalError; }); }; + const tryLowercaseUsername = (originalError) => { + const loginParamsLowercase = Object.assign({}, loginParams, { + user: username.toLowerCase(), + identifier: { + user: username.toLowerCase(), + }, + }); + return client.login('m.login.password', loginParamsLowercase).then(function(data) { + return Promise.resolve({ + homeserverUrl: self._hsUrl, + identityServerUrl: self._isUrl, + userId: data.user_id, + deviceId: data.device_id, + accessToken: data.access_token, + }); + }).catch((fallback_error) => { + console.log("Lowercase username login failed", fallback_error); + // throw the original error + throw originalError; + }); + }; let originalLoginError = null; - return sendLoginRequest( - self._hsUrl, self._isUrl, 'm.login.password', loginParams, - ).catch((error) => { + return client.login('m.login.password', loginParams).then(function(data) { + return Promise.resolve({ + homeserverUrl: self._hsUrl, + identityServerUrl: self._isUrl, + userId: data.user_id, + deviceId: data.device_id, + accessToken: data.access_token, + }); + }).catch((error) => { originalLoginError = error; if (error.httpStatus === 403) { if (self._fallbackHsUrl) { @@ -162,6 +202,22 @@ export default class Login { } } throw originalLoginError; + }).catch((error) => { + // We apparently squash case at login serverside these days: + // https://github.com/matrix-org/synapse/blob/1189be43a2479f5adf034613e8d10e3f4f452eb9/synapse/handlers/auth.py#L475 + // so this wasn't needed after all. Keeping the code around in case the + // the situation changes... + + /* + if ( + error.httpStatus === 403 && + loginParams.identifier.type === 'm.id.user' && + username.search(/[A-Z]/) > -1 + ) { + return tryLowercaseUsername(originalLoginError); + } + */ + throw originalLoginError; }).catch((error) => { console.log("Login failed", error); throw error; @@ -183,32 +239,3 @@ export default class Login { return client.getSsoLoginUrl(url.format(parsedUrl), loginType); } } - - -/** - * Send a login request to the given server, and format the response - * as a MatrixClientCreds - * - * @param {string} hsUrl the base url of the Homeserver used to log in. - * @param {string} isUrl the base url of the default identity server - * @param {string} loginType the type of login to do - * @param {object} loginParams the parameters for the login - * - * @returns {MatrixClientCreds} - */ -export async function sendLoginRequest(hsUrl, isUrl, loginType, loginParams) { - const client = Matrix.createClient({ - baseUrl: hsUrl, - idBaseUrl: isUrl, - }); - - const data = await client.login(loginType, loginParams); - - return { - homeserverUrl: hsUrl, - identityServerUrl: isUrl, - userId: data.user_id, - deviceId: data.device_id, - accessToken: data.access_token, - }; -} diff --git a/src/Notifier.js b/src/Notifier.js index 8550f3bf95..80e8be1084 100644 --- a/src/Notifier.js +++ b/src/Notifier.js @@ -289,11 +289,6 @@ const Notifier = { const room = MatrixClientPeg.get().getRoom(ev.getRoomId()); const actions = MatrixClientPeg.get().getPushActionsForEvent(ev); if (actions && actions.notify) { - dis.dispatch({ - action: "event_notification", - event: ev, - room: room, - }); if (this.isEnabled()) { this._displayPopupNotification(ev, room); } diff --git a/src/Registration.js b/src/Registration.js index 98aee3ac83..f86c9cc618 100644 --- a/src/Registration.js +++ b/src/Registration.js @@ -26,10 +26,6 @@ import MatrixClientPeg from './MatrixClientPeg'; import Modal from './Modal'; import { _t } from './languageHandler'; -// Regex for what a "safe" or "Matrix-looking" localpart would be. -// TODO: Update as needed for https://github.com/matrix-org/matrix-doc/issues/1514 -export const SAFE_LOCALPART_REGEX = /^[a-z0-9=_\-./]+$/; - /** * Starts either the ILAG or full registration flow, depending * on what the HS supports diff --git a/src/RoomInvite.js b/src/RoomInvite.js index 3547b9195f..a96d1b2f6b 100644 --- a/src/RoomInvite.js +++ b/src/RoomInvite.js @@ -1,6 +1,6 @@ /* Copyright 2016 OpenMarket Ltd -Copyright 2017, 2018 New Vector Ltd +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. @@ -15,7 +15,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; import MatrixClientPeg from './MatrixClientPeg'; import MultiInviter from './utils/MultiInviter'; import Modal from './Modal'; @@ -26,6 +25,18 @@ import dis from './dispatcher'; import DMRoomMap from './utils/DMRoomMap'; import { _t } from './languageHandler'; +export function inviteToRoom(roomId, addr) { + const addrType = getAddressType(addr); + + if (addrType == 'email') { + return MatrixClientPeg.get().inviteByEmail(roomId, addr); + } else if (addrType == 'mx-user-id') { + return MatrixClientPeg.get().invite(roomId, addr); + } else { + throw new Error('Unsupported address'); + } +} + /** * Invites multiple addresses to a room * Simpler interface to utils/MultiInviter but with @@ -35,9 +46,9 @@ import { _t } from './languageHandler'; * @param {string[]} addrs Array of strings of addresses to invite. May be matrix IDs or 3pids. * @returns {Promise} Promise */ -function inviteMultipleToRoom(roomId, addrs) { +export function inviteMultipleToRoom(roomId, addrs) { const inviter = new MultiInviter(roomId); - return inviter.invite(addrs).then(states => Promise.resolve({states, inviter})); + return inviter.invite(addrs); } export function showStartChatInviteDialog() { @@ -118,8 +129,8 @@ function _onStartChatFinished(shouldInvite, addrs) { createRoom().then((roomId) => { room = MatrixClientPeg.get().getRoom(roomId); return inviteMultipleToRoom(roomId, addrTexts); - }).then((result) => { - return _showAnyInviteErrors(result.states, room, result.inviter); + }).then((addrs) => { + return _showAnyInviteErrors(addrs, room); }).catch((err) => { console.error(err.stack); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); @@ -137,9 +148,9 @@ function _onRoomInviteFinished(roomId, shouldInvite, addrs) { const addrTexts = addrs.map((addr) => addr.address); // Invite new users to a room - inviteMultipleToRoom(roomId, addrTexts).then((result) => { + inviteMultipleToRoom(roomId, addrTexts).then((addrs) => { const room = MatrixClientPeg.get().getRoom(roomId); - return _showAnyInviteErrors(result.states, room, result.inviter); + return _showAnyInviteErrors(addrs, room); }).catch((err) => { console.error(err.stack); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); @@ -158,36 +169,22 @@ function _isDmChat(addrTexts) { } } -function _showAnyInviteErrors(addrs, room, inviter) { +function _showAnyInviteErrors(addrs, room) { // Show user any errors - const failedUsers = Object.keys(addrs).filter(a => addrs[a] === 'error'); - if (failedUsers.length === 1 && inviter.fatal) { - // Just get the first message because there was a fatal problem on the first - // user. This usually means that no other users were attempted, making it - // pointless for us to list who failed exactly. - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createTrackedDialog('Failed to invite users to the room', '', ErrorDialog, { - title: _t("Failed to invite users to the room:", {roomName: room.name}), - description: inviter.getErrorText(failedUsers[0]), - }); - } else { - const errorList = []; - for (const addr of failedUsers) { - if (addrs[addr] === "error") { - const reason = inviter.getErrorText(addr); - errorList.push(addr + ": " + reason); - } - } - - if (errorList.length > 0) { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createTrackedDialog('Failed to invite the following users to the room', '', ErrorDialog, { - title: _t("Failed to invite the following users to the %(roomName)s room:", {roomName: room.name}), - description: errorList.join(
        ), - }); + const errorList = []; + for (const addr of Object.keys(addrs)) { + if (addrs[addr] === "error") { + errorList.push(addr); } } + if (errorList.length > 0) { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createTrackedDialog('Failed to invite the following users to the room', '', ErrorDialog, { + title: _t("Failed to invite the following users to the %(roomName)s room:", {roomName: room.name}), + description: errorList.join(", "), + }); + } return addrs; } diff --git a/src/SlashCommands.js b/src/SlashCommands.js index 24328d6372..8a34ba7ab1 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -26,7 +26,6 @@ import Modal from './Modal'; import SettingsStore, {SettingLevel} from './settings/SettingsStore'; import {MATRIXTO_URL_PATTERN} from "./linkify-matrix"; import * as querystring from "querystring"; -import MultiInviter from './utils/MultiInviter'; class Command { @@ -143,15 +142,7 @@ export const CommandMap = { if (args) { const matches = args.match(/^(\S+)$/); if (matches) { - // We use a MultiInviter to re-use the invite logic, even though - // we're only inviting one user. - const userId = matches[1]; - const inviter = new MultiInviter(roomId); - return success(inviter.invite([userId]).then(() => { - if (inviter.getCompletionState(userId) !== "invited") { - throw new Error(inviter.getErrorText(userId)); - } - })); + return success(MatrixClientPeg.get().invite(roomId, matches[1])); } } return reject(this.getUsage()); diff --git a/src/Tinter.js b/src/Tinter.js index 80375dead2..de9ae94097 100644 --- a/src/Tinter.js +++ b/src/Tinter.js @@ -392,7 +392,7 @@ class Tinter { // XXX: we could just move this all into TintableSvg, but as it's so similar // to the CSS fixup stuff in Tinter (just that the fixups are stored in TintableSvg) // keeping it here for now. - calcSvgFixups(svgs, forceColors) { + calcSvgFixups(svgs) { // go through manually fixing up SVG colours. // we could do this by stylesheets, but keeping the stylesheets // updated would be a PITA, so just brute-force search for the @@ -420,21 +420,13 @@ class Tinter { const tag = tags[j]; for (let k = 0; k < this.svgAttrs.length; k++) { const attr = this.svgAttrs[k]; - for (let m = 0; m < this.keyHex.length; m++) { // dev note: don't use L please. - // We use a different attribute from the one we're setting - // because we may also be using forceColors. If we were to - // check the keyHex against a forceColors value, it may not - // match and therefore not change when we need it to. - const valAttrName = "mx-val-" + attr; - let attribute = tag.getAttribute(valAttrName); - if (!attribute) attribute = tag.getAttribute(attr); // fall back to the original - if (attribute && (attribute.toUpperCase() === this.keyHex[m] || attribute.toLowerCase() === this.keyRgb[m])) { + for (let l = 0; l < this.keyHex.length; l++) { + if (tag.getAttribute(attr) && + tag.getAttribute(attr).toUpperCase() === this.keyHex[l]) { fixups.push({ node: tag, attr: attr, - refAttr: valAttrName, - index: m, - forceColors: forceColors, + index: l, }); } } @@ -450,9 +442,7 @@ class Tinter { if (DEBUG) console.log("applySvgFixups start for " + fixups); for (let i = 0; i < fixups.length; i++) { const svgFixup = fixups[i]; - const forcedColor = svgFixup.forceColors ? svgFixup.forceColors[svgFixup.index] : null; - svgFixup.node.setAttribute(svgFixup.attr, forcedColor ? forcedColor : this.colors[svgFixup.index]); - svgFixup.node.setAttribute(svgFixup.refAttr, this.colors[svgFixup.index]); + svgFixup.node.setAttribute(svgFixup.attr, this.colors[svgFixup.index]); } if (DEBUG) console.log("applySvgFixups end"); } diff --git a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js index 0db9d0699b..2f43d18072 100644 --- a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js +++ b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js @@ -17,7 +17,6 @@ limitations under the License. import React from 'react'; import sdk from '../../../../index'; import MatrixClientPeg from '../../../../MatrixClientPeg'; -import { scorePassword } from '../../../../utils/PasswordScorer'; import FileSaver from 'file-saver'; @@ -31,8 +30,6 @@ const PHASE_BACKINGUP = 4; const PHASE_DONE = 5; const PHASE_OPTOUT_CONFIRM = 6; -const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc. - // XXX: copied from ShareDialog: factor out into utils function selectText(target) { const range = document.createRange(); @@ -55,8 +52,6 @@ export default React.createClass({ passPhraseConfirm: '', copied: false, downloaded: false, - zxcvbnResult: null, - setPassPhrase: false, }; }, @@ -92,33 +87,25 @@ export default React.createClass({ }); }, - _createBackup: async function() { + _createBackup: function() { this.setState({ phase: PHASE_BACKINGUP, error: null, }); - let info; - try { - info = await MatrixClientPeg.get().createKeyBackupVersion( - this._keyBackupInfo, - ); - await MatrixClientPeg.get().backupAllGroupSessions(info.version); + this._createBackupPromise = MatrixClientPeg.get().createKeyBackupVersion( + this._keyBackupInfo, + ).then((info) => { + return MatrixClientPeg.get().backupAllGroupSessions(info.version); + }).then(() => { this.setState({ phase: PHASE_DONE, }); - } catch (e) { + }).catch(e => { console.log("Error creating key backup", e); - // TODO: If creating a version succeeds, but backup fails, should we - // delete the version, disable backup, or do nothing? If we just - // disable without deleting, we'll enable on next app reload since - // it is trusted. - if (info) { - MatrixClientPeg.get().deleteKeyBackupVersion(info.version); - } this.setState({ error: e, }); - } + }); }, _onCancel: function() { @@ -141,7 +128,6 @@ export default React.createClass({ this._keyBackupInfo = await MatrixClientPeg.get().prepareKeyBackupVersion(); this.setState({ copied: false, - downloaded: false, phase: PHASE_SHOWKEY, }); }, @@ -159,9 +145,7 @@ export default React.createClass({ _onPassPhraseConfirmNextClick: async function() { this._keyBackupInfo = await MatrixClientPeg.get().prepareKeyBackupVersion(this.state.passPhrase); this.setState({ - setPassPhrase: true, copied: false, - downloaded: false, phase: PHASE_SHOWKEY, }); }, @@ -189,10 +173,6 @@ export default React.createClass({ _onPassPhraseChange: function(e) { this.setState({ passPhrase: e.target.value, - // precompute this and keep it in state: zxcvbn is fast but - // we use it in a couple of different places so no point recomputing - // it unnecessarily. - zxcvbnResult: scorePassword(e.target.value), }); }, @@ -203,46 +183,17 @@ export default React.createClass({ }, _passPhraseIsValid: function() { - return this.state.zxcvbnResult && this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE; + return this.state.passPhrase !== ''; }, _renderPhasePassPhrase: function() { const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - - let strengthMeter; - let helpText; - if (this.state.zxcvbnResult) { - if (this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE) { - helpText = _t("Great! This passphrase looks strong enough."); - } else { - const suggestions = []; - for (let i = 0; i < this.state.zxcvbnResult.feedback.suggestions.length; ++i) { - suggestions.push(
        {this.state.zxcvbnResult.feedback.suggestions[i]}
        ); - } - const suggestionBlock = suggestions.length > 0 ?
        - {suggestions} -
        : null; - - helpText =
        - {this.state.zxcvbnResult.feedback.warning} - {suggestionBlock} -
        ; - } - strengthMeter =
        - -
        ; - } - return

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

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

        -
        - {strengthMeter} - {helpText} -

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

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

        -

        {bodyText}

        +

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

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

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

        -

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

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

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

        -

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

        -

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

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

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

        { _t('Updates') }

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

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

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

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

        { _t('Create an account') }

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

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

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

        {_t("Invited")}

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

        { _t("Invited") }

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

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

        -
      • ); - } + fileList.push(
      • + { files[i].name || _t('Attachment') } +
      • ); } const isQuoting = Boolean(RoomViewStore.getQuotingEvent()); @@ -173,47 +161,23 @@ export default class MessageComposer extends React.Component { }

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

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

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

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

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

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

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

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

        HTML for your community's page

        \n

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

        \n

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

        \n": "

        HTML për faqen e bashkësisë tuaj

        \n

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

        \n

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

        \n", - "Failed to add the following rooms to the summary of %(groupId)s:": "S’u arrit të shtoheshin dhomat vijuese te përmbledhja e %(groupId)s:", - "Failed to remove the room from the summary of %(groupId)s": "S’u arrit të hiqej dhoma prej përmbledhjes së %(groupId)s", - "Failed to remove a user from the summary of %(groupId)s": "S’u arrit të hiqej një përdorues nga përmbledhja e %(groupId)s", - "Changes made to your community name and avatar might not be seen by other users for up to 30 minutes.": "Ndryshimet e bëra te emri dhe avatari i bashkësisë tuaj mund të mos shihen nga përdoruesit e tjera para deri 30 minutash.", - "Can't leave Server Notices room": "Dhoma Njoftime Shërbyesi, s’braktiset dot", - "For security, this session has been signed out. Please sign in again.": "Për hir të sigurisë, është bërë dalja nga ky sesion. Ju lutemi, ribëni hyrjen.", - "Data from an older version of Riot has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.": "Janë pikasur të dhëna nga një version i dikurshëm i Riot-it. Kjo do të bëjë që kriptografia skaj-më-skaj te versioni i dikurshëm të mos punojë si duhet. Mesazhet e fshehtëzuar skaj-më-skaj tani së fundi teksa përdorej versioni i dikurshëm mund të mos jenë të shfshehtëzueshëm në këtë version. Kjo mund bëjë edhe që mesazhet e shkëmbyera me këtë version të dështojnë. Nëse ju dalin probleme, bëni daljen dhe rihyni në llogari. Që të ruhet historiku i mesazheve, eksportoni dhe ri-importoni kyçet tuaj.", - "Did you know: you can use communities to filter your Riot.im experience!": "E dinit se: mund t’i përdorni bashkësitë për të filtruar punimin tuaj në Riot.im?", - "Error whilst fetching joined communities": "Gabim teksa silleshin bashkësitë ku merret pjesë", - "Show devices, send anyway or cancel.": "Shfaq pajisje, dërgoje sido qoftë ose anuloje.", - "%(count)s Resend all or cancel all now. You can also select individual messages to resend or cancel.|other": "Ridërgojini krejt ose anulojini krejt tani. Për ridërgim ose anulim, mundeni edhe të përzgjidhni mesazhe individualë.", - "%(count)s Resend all or cancel all now. You can also select individual messages to resend or cancel.|one": "Ridërgojeni mesazhin ose anulojeni mesazhin tani.", - "For security, logging out will delete any end-to-end encryption keys from this browser. If you want to be able to decrypt your conversation history from future Riot sessions, please export your room keys for safe-keeping.": "Për hir të sigurisë, dalja nga llogaria do të sjellë fshirjen në këtë shfletues të çfarëdo kyçesh fshehtëzimi skaj-më-skaj. Nëse doni të jeni në gjendje të fshehtëzoni historikun e bisedave tuaja që nga sesione të ardhshëm Riot, ju lutemi, eksportoni kyçet tuaj të dhomës, për t’i ruajtur të parrezikuar diku.", - "Audio Output": "Sinjal Audio", - "Error: Problem communicating with the given homeserver.": "Gabimr: Problem komunikimi me shërbyesin e dhënë Home.", - "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or enable unsafe scripts.": "S’lidhet dot te shërbyes Home përmes HTTP-je, kur te shtylla e shfletuesit tuaj jepet një URL HTTPS. Ose përdorni HTTPS-në, ose aktivizoni përdorimin e programtheve jo të sigurt.", - "Can't connect to homeserver - please check your connectivity, ensure your homeserver's SSL certificate is trusted, and that a browser extension is not blocking requests.": "S’lidhet dot te shërbyes Home - ju lutemi, kontrolloni lidhjen tuaj, sigurohuni që dëshmia SSL e shërbyesit tuaj Home besohet, dhe që s’ka ndonjë zgjerim shfletuesi që po bllokon kërkesat tuaja.", - "Failed to remove tag %(tagName)s from room": "S’u arrit të hiqej etiketa %(tagName)s nga dhoma", - "Failed to add tag %(tagName)s to room": "S’u arrit të shtohej në dhomë etiketa %(tagName)s", - "Pin unread rooms to the top of the room list": "Fiksoji dhomat e palexuara në krye të listës së dhomave", - "Pin rooms I'm mentioned in to the top of the room list": "Fiksoji dhomat ku përmendem në krye të listës së dhomave", - "Enable widget screenshots on supported widgets": "Aktivizo foto ekrani widget-esh për widget-e që e mbulojnë", - "Changing password will currently reset any end-to-end encryption keys on all devices, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "Ndryshimi i fjalëkalimit do të sjellë zerimin e çfarëdo kyçesh fshehtëzimi skaj-më-skaj në krejt pajisjet, duke e bërë të palexueshëm historikun e fshehtëzuar të bisedave, hiq rastin kur i eksportoni më parë kyçet tuaj të dhomës dhe i ri-importoni ata më pas. Në të ardhmen kjo do të përmirësohet.", - "Join as voice or video.": "Merrni pjesë me ose me video.", - "Key share requests are sent to your other devices automatically. If you rejected or dismissed the key share request on your other devices, click here to request the keys for this session again.": "Kërkesat për ndarje kyçesh dërgohen automatikisht te pajisjet tuaja të tjera. Nëse s’e pranuat ose e hodhët tej kërkesën për ndarje kyçesh në pajisjet tuaja të tjera, klikoni këtu që të rikërkoni kyçe për këtë sesion.", - "If your other devices do not have the key for this message you will not be able to decrypt them.": "Nëse pajisjet tuaja të tjera nuk kanë kyçin për këtë mesazh, s’do të jeni në gjendje ta shfshehtëzoni.", - "Demote yourself?": "Të zhgradohet vetvetja?", - "Demote": "Zhgradoje", - "Failed to toggle moderator status": "S’u arrit të këmbehet gjendje moderatori", - "Server unavailable, overloaded, or something else went wrong.": "Shërbyesi është i pakapshëm, i mbingarkuar, ose diç tjetër shkoi ters.", - "Drop here to tag %(section)s": "Hidheni këtu që të caktohet etiketë për %(section)s", - "Press to start a chat with someone": "Shtypni që të nisni një bisedë me dikë", - "No users have specific privileges in this room": "S’ka përdorues me privilegje të caktuara në këtë dhomë", - "Guests cannot join this room even if explicitly invited.": "Vizitorët s’mund të marrin pjesë në këtë edhe po të jenë ftuar shprehimisht.", - "Publish this room to the public in %(domain)s's room directory?": "Të bëhet publike kjo dhomë te drejtoria e dhomave %(domain)s?", - "Click here to upgrade to the latest room version and ensure room integrity is protected.": "Klikoni këtu që ta përmirësoni me versionin më të ri të dhomë dhe të garantoni mbrojtjen e paprekshmërisë së dhomës.", - "In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.": "Në dhoma të fshehtëzuara, si kjo, paraparja e URL-ve është e çaktivizuar, si parazgjedhje, për të garantuar që shërbyesi juaj home (ku edhe prodhohen paraparjet) të mos grumbullojë të dhëna rreth lidhjesh që shihni në këtë dhomë.", - "Please review and accept the policies of this homeserver:": "Ju lutemi, shqyrtoni dhe pranoni rregullat e këtij shërbyesi home:", - "If you don't specify an email address, you won't be able to reset your password. Are you sure?": "Nëse nuk përcaktoni një adresë email, s’do të jeni në gjendje të bëni ricaktime të fjalëkalimit tuaj. Jeni i sigurt?", - "Removing a room from the community will also remove it from the community page.": "Heqja e një dhome nga bashkësia do ta heqë atë edhe nga faqja e bashkësisë.", - "Please help improve Riot.im by sending anonymous usage data. This will use a cookie (please see our Cookie Policy).": "Ju lutemi, ndihmoni të përmirësohet Riot.im duke dërguar të dhëna anonime përdorimi. Për këtë do të përdoret një cookie (ju lutemi, shihni Rregullat tona mbi Cookie-t).", - "Please help improve Riot.im by sending anonymous usage data. This will use a cookie.": "Ju lutemi, ndihmoni të përmirësohet Riot.im duke dërguar të dhëna anonime përdorimi. Për këtë do të përdoret një cookie.", - "Please contact your service administrator to get this limit increased.": "Ju lutemi, që të shtohet ky kufi, lidhuni me përgjegjësin e shërbimit.", - "If the other version of Riot is still open in another tab, please close it as using Riot on the same host with both lazy loading enabled and disabled simultaneously will cause issues.": "Nëse versioni tjetër i Riot-it është ende i hapur në një skedë tjetër, ju lutemi, mbylleni, ngaqë përdorimi njëkohësisht i Riot-it në të njëjtën strehë, në njërën anë me lazy loading të aktivizuar dhe në anën tjetër të çaktivizuar do të shkaktojë probleme.", - "Riot now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!": "Riot-i tani përdor 3 deri 5 herë më pak kujtesë, duke ngarkuar të dhëna mbi përdorues të tjerë vetëm kur duhen. Ju lutemi, prisni, teksa njëkohësojmë të dhënat me shërbyesin!", - "Put a link back to the old room at the start of the new room so people can see old messages": "Vendosni në krye të dhomës së re një lidhje për te dhoma e vjetër, që njerëzit të mund të shohin mesazhet e vjetër", - "Log out and remove encryption keys?": "Të dilet dhe të hiqen kyçet e fshehtëzimit?", - "If you have previously used a more recent version of Riot, your session may be incompatible with this version. Close this window and return to the more recent version.": "Nëse më herët keni përdorur një version më të freskët të Riot-it, sesioni juaj mund të jetë i papërputhshëm me këtë version. Mbylleni këtë dritare dhe kthehuni te versioni më i ri.", - "Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.": "Pastrimi i gjërave të depozituara në shfletuesin tuaj mund ta ndreqë problemin, por kjo do të sjellë nxjerrjen tuaj nga llogari dhe do ta bëjë të palexueshëm çfarëdo historiku të fshehtëzuar të bisedës.", - "If you would like to create a Matrix account you can register now.": "Nëse do të donit të krijoni një llogari Matrix, mund të regjistroheni që tani.", - "If you already have a Matrix account you can log in instead.": "Nëse keni tashmë një llogari Matrix, mund të bëni hyrjen.", - "Share message history with new users": "Ndani me përdorues të rinj historik mesazhesh", - "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Krijoni një bashkësi që bëni tok përdorues dhe dhoma! Krijoni një faqe hyrëse vetjake, që të ravijëzoni hapësirën tuaj në universin Matrix.", - "Sent messages will be stored until your connection has returned.": "Mesazhet e dërguar do të depozitohen deri sa lidhja juaj të jetë rikthyer.", - "Server may be unavailable, overloaded, or the file too big": "Shërbyesi mund të jetë i pakapshëm, i mbingarkuar, ose kartela është shumë e madhe", - "Server may be unavailable, overloaded, or search timed out :(": "Shërbyesi mund të jetë i pakapshëm, i mbingarkuar, ose kërkimit i mbaroi koha :(", - "If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Nëse parashtruar një të metë përmes GitHub-it, regjistrat e diagnostikimit mund të na ndihmojnë të ndjekim problemin. Regjistrat e diagnostikimit përmbajnë të dhëna përdorimi, përfshi emrin tuaj të përdoruesit, ID-të ose aliaset e dhomave apo grupeve që keni vizituar dhe emrat e përdoruesve të përdoruesve të tjerë. Në to nuk përmbahen mesazhet.", - "Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.": "Privatësia është e rëndësishme për ne, ndaj nuk grumbullojmë ndonjë të dhënë personale apo të identifikueshme për analizat tona.", - "Learn more about how we use analytics.": "Mësoni më tepër se si i përdorim analizat.", - "Lazy loading is not supported by your current homeserver.": "Lazy loading nuk mbulohet nga shërbyesi juaj i tanishëm Home.", - "Reject all %(invitedRooms)s invites": "Mos prano asnjë ftesë për në %(invitedRooms)s", - "Missing Media Permissions, click here to request.": "Mungojnë Leje Mediash, klikoni këtu që të kërkohen.", - "New passwords must match each other.": "Fjalëkalimet e rinj duhet të përputhen me njëri-tjetrin.", - "Please note you are logging into the %(hs)s server, not matrix.org.": "Ju lutemi, kini parasysh se jeni futur te shërbyesi %(hs)s, jo te matrix.org.", - "Guest access is disabled on this Home Server.": "Në këtë shërbyes Home është çaktivizuar hyrja si vizitor.", - "Password too short (min %(MIN_PASSWORD_LENGTH)s).": "Fjalëkalim shumë i shkurtër (minimumi %(MIN_PASSWORD_LENGTH)s).", - "You need to register to do this. Would you like to register now?": "Për ta bërë këtë, lypset të regjistroheni. Doni të regjistroheni që tani?", - "Stops ignoring a user, showing their messages going forward": "Resht shpërfilljen e një përdoruesi, duke i shfaqur mesazhet e tij të dërgohen", - "Verifies a user, device, and pubkey tuple": "Verifikon një përdorues, pajisje dhe një set kyçesh publikë", - "WARNING: Device already verified, but keys do NOT MATCH!": "KUJDES: Pajisje tashmë e verifikuar, por kyçet NUK PËRPUTHEN!", - "WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!": "KUJDES: VERIFIKIMI I KYÇIT DËSHTOI! Kyçi i nënshkrimit për %(userId)s dhe pajisjen %(deviceId)s është \"%(fprint)s\", që nuk përpythet me kyçin e dhënë \"%(fingerprint)s\". Kjo mund të jetë shenjë se komunikimet tuaja po përgjohen!", - "The signing key you provided matches the signing key you received from %(userId)s's device %(deviceId)s. Device marked as verified.": "Kyçi i nënshkrimit që dhatë përputhet me kyçin e nënshkrimit që morët nga pajisja e %(userId)s %(deviceId)s. Pajisja u shënua si e verifikuar.", - "Your browser does not support the required cryptography extensions": "Shfletuesi juaj nuk mbulon zgjerimet kriptografike të domosdoshme", - "Show timestamps in 12 hour format (e.g. 2:30pm)": "Vulat kohore shfaqi në formatin 12 orësh (p.sh. 2:30pm)", - "Enable inline URL previews by default": "Aktivizo, si parazgjedhje, paraparje URL-sh brendazi", - "Your home server does not support device management.": "Shërbyesi juaj Home nuk mbulon administrim pajisjesh.", - "The maximum permitted number of widgets have already been added to this room.": "Në këtë dhomë është shtuar tashmë numri maksimum i lejuar për widget-et.", - "Your key share request has been sent - please check your other devices for key share requests.": "Kërkesa juaj për shkëmbim kyçesh u dërgua - ju lutemi, kontrolloni pajisjet tuaja të tjera për kërkesa shkëmbimi kyçesh.", - "Undecryptable": "I pafshehtëzueshëm", - "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.": "S’do të jeni në gjendje ta zhbëni këtë, ngaqë po zhgradoni veten, nëse jeni përdoruesi i fundit i privilegjuar te dhoma do të jetë e pamundur të rifitoni privilegjet.", - "Jump to read receipt": "Hidhuni te leximi i faturës", - "Unable to reply": "S’arrihet të përgjigjet", - "Unknown for %(duration)s": "I panjohur për %(duration)s", - "You're not in any rooms yet! Press to make a room or to browse the directory": "S’jeni ende në ndonjë dhomë! Shtypni që të krijoni një dhomë ose që të shfletoni drejtorinë", - "Unable to ascertain that the address this invite was sent to matches one associated with your account.": "S’arrihet të sigurohet që adresa prej nga qe dërguar kjo ftesë përputhet me atë përshoqëruar llogarisë tuaj.", - "This invitation was sent to an email address which is not associated with this account:": "Kjo ftesë qe dërguar për një adresë email e cila nuk i përshoqërohet kësaj llogarie:", - "Would you like to accept or decline this invitation?": "Do të donit ta pranoni apo hidhni tej këtë ftesë?", - "To remove other users' messages, you must be a": "Që të hiqni mesazhe përdoruesish të tjerë, duhet të jeni një", - "This room is not accessible by remote Matrix servers": "Kjo dhomë nuk është e përdorshme nga shërbyes Matrix të largët", - "To send events of type , you must be a": "Që të dërgoni akte të llojit , duhet të jeni", - "This room version is vulnerable to malicious modification of room state.": "Ky version i dhomës është i cenueshëm nga modifikime dashakaqe të gjendjes së dhomës.", - "Stickerpack": "Paketë ngjitësish", - "You have enabled URL previews by default.": "E keni aktivizuar, si parazgjedhje, paraparjen e URL-ve.", - "You have disabled URL previews by default.": "E keni çaktivizuar, si parazgjedhje, paraparjen e URL-ve.", - "URL previews are enabled by default for participants in this room.": "Për pjesëmarrësit në këtë dhomë paraparja e URL-ve është e aktivizuar, si parazgjedhje.", - "URL previews are disabled by default for participants in this room.": "Për pjesëmarrësit në këtë dhomë paraparja e URL-ve është e çaktivizuar, si parazgjedhje.", - "When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.": "Kur dikush vë një URL në mesazh, për të dhënë rreth lidhjes më tepër të dhëna, të tilla si titulli, përshkrimi dhe një figurë e sajtit, do të shfaqet një paraparje e URL-së.", - "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?": "Ju ndan një hap nga shpënia te një sajt palë e tretë, që kështu të mund të mirëfilltësoni llogarinë tuaj me %(integrationsUrl)s. Doni të vazhdohet?", - "This allows you to use this app with an existing Matrix account on a different home server.": "Kjo ju lejon ta përdorni këtë aplikacion me një llogari Matrix ekxistuese në një shërbyes tjetër Home.", - "You can also set a custom identity server but this will typically prevent interaction with users based on email address.": "Mund të ujdisni edhe një shërbyes vetjak identitetesh, por kjo normalisht do të pengojë ndërveprim mes përdoruesish bazuar në adresë email.", - "The visibility of '%(roomName)s' in %(groupId)s could not be updated.": "Dukshmëria e '%(roomName)s' te %(groupId)s s’u përditësua dot.", - "Something went wrong when trying to get your communities.": "Diç shkoi ters teksa provohej të merreshin bashkësitë tuaja.", - "Warning: This widget might use cookies.": "Kujdes: Ky widget mund të përdorë cookies.", - "Unable to load event that was replied to, it either does not exist or you do not have permission to view it.": "S’arrihet të ngarkohet akti të cilit iu përgjigj, ose nuk ekziston, ose s’keni leje ta shihni.", - "Try using one of the following valid address types: %(validTypesList)s.": "Provoni të përdorni një nga llojet e vlefshme të adresave më poshtë: %(validTypesList)s.", - "You already have existing direct chats with this user:": "Keni tashmë fjalosje të drejtpërdrejta me këtë përdorues:", - "Something went wrong whilst creating your community": "Diç shkoi ters teksa krijohej bashkësia juaj", - "You've previously used Riot on %(host)s with lazy loading of members enabled. In this version lazy loading is disabled. As the local cache is not compatible between these two settings, Riot needs to resync your account.": "Më parë përdornit Riot në %(host)s me lazy loading anëtarësh të aktivizuar. Në këtë version lazy loading është çaktivizuar. Ngaqë fshehtina vendore s’është e përputhshme mes këtyre dy rregullimeve, Riot-i lyp të rinjëkohësohet llogaria juaj.", - "Upgrading this room requires closing down the current instance of the room and creating a new room it its place. To give room members the best possible experience, we will:": "Përmirësimi i kësaj dhome lyp mbylljen e instancës së tanishme të dhomës dhe krijimin në vend të saj të një dhome të re. Për t’u dhënë anëtareve të dhomës më të mirën e mundshme, do të:", - "Update any local room aliases to point to the new room": "Përditësoni çfarëdo aliasesh dhomash vendore që të shpien te dhoma e re", - "Stop users from speaking in the old version of the room, and post a message advising users to move to the new room": "Ndalojuni përdoruesve të flasin në versionin e vjetër të dhomës, dhe postoni një mesazh që u këshillon atyre të hidhen te dhoma e re", - "We encountered an error trying to restore your previous session.": "Hasëm një gabim teksa provohej të rikthehej sesioni juaj i dikurshëm.", - "This will allow you to reset your password and receive notifications.": "Kjo do t’ju lejojë të ricaktoni fjalëkalimin tuaj dhe të merrni njoftime.", - "This will be your account name on the homeserver, or you can pick a different server.": "Ky do të jetë emri i llogarisë tuaj te shërbyesi home, ose mund të zgjidhni një shërbyes tjetër.", - "You are currently using Riot anonymously as a guest.": "Hëpërhë po e përdorni Riot-in në mënyrë anonime, si një vizitor.", - "You are currently blacklisting unverified devices; to send messages to these devices you must verify them.": "Po kaloni në listë të zezë pajisje të paverifikuara; që të dërgoni mesazhe te këto pajisje, duhet t’i verifikoni.", - "We recommend you go through the verification process for each device to confirm they belong to their legitimate owner, but you can resend the message without verifying if you prefer.": "Këshillojmë të përshkoni procesin e verifikimit për çdo pajisje, që t’u bindur se u takojnë të zotëve të ligjshëm, por, nëse parapëlqeni, mund ta dërgoni mesazhin pa verifikuar gjë.", - "You must join the room to see its files": "Duhet të hyni në dhomë, pa të shihni kartelat e saj", - "The room '%(roomName)s' could not be removed from the summary.": "Dhoma '%(roomName)s' s’u hoq dot nga përmbledhja.", - "The user '%(displayName)s' could not be removed from the summary.": "Përdoruesi '%(displayName)s' s’u hoq dot nga përmbledhja.", - "You are an administrator of this community. You will not be able to rejoin without an invite from another administrator.": "Jeni një përgjegjës i kësaj bashkësie. S’do të jeni në gjendje të rihyni pa një ftesë nga një tjetër përgjegjës.", - "These rooms are displayed to community members on the community page. Community members can join the rooms by clicking on them.": "Këto dhoma u shfaqen anëtarëve të bashkësisë te faqja e bashkësisë. Anëtarët e bashkësisë mund të marrin pjesë në dhoma duke klikuar mbi to.", - "Your community hasn't got a Long Description, a HTML page to show to community members.
        Click here to open settings and give it one!": "Bashkësia juaj s’ka ndonjë Përshkrim të Gjatë, një faqe HTML për t’ua shfaqur anëtarëve të bashkësisë.
        Klikoni këtu që të hapni rregullimet dhe t’i krijoni një të tillë!", - "This room is not public. You will not be able to rejoin without an invite.": "Kjo dhomë s’është publike. S’do të jeni në gjendje të rihyni në të pa një ftesë.", - "This room is used for important messages from the Homeserver, so you cannot leave it.": "Kjo dhomë përdoret për mesazhe të rëndësishëm nga shërbyesi Home, ndaj s’mund ta braktisni.", - "To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "Që të ndërtoni një filtër, tërhiqeni avatarin e një bashkësie te paneli i filtrimeve në skajin e majtë të ekranit. Për të parë vetëm dhomat dhe personat e përshoqëruar asaj bashkësie, mund të klikoni në çfarëdo kohe mbi një avatar te panelit të filtrimeve.", - "You can't send any messages until you review and agree to our terms and conditions.": "S’mund të dërgoni ndonjë mesazh, përpara se të shqyrtoni dhe pajtoheni me termat dhe kushtet tona.", - "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please contact your service administrator to continue using the service.": "Mesazhi juaj s’u dërgua, ngaqë ky shërbyes Home ka mbërritur në Kufirin Mujor të Përdoruesve Aktivë. Ju lutemi, që të vazhdoni ta përdorni këtë shërbim, lidhuni me përgjegjësin e shërbimit tuaj.", - "Your message wasn't sent because this homeserver has exceeded a resource limit. Please contact your service administrator to continue using the service.": "Mesazhi juaj s’u dërgua, ngaqë ky shërbyes Home ka tejkaluar kufirin e një burimi. Ju lutemi, që të vazhdoni ta përdorni këtë shërbim, lidhuni me përgjegjësin e shërbimit tuaj.", - "There's no one else here! Would you like to invite others or stop warning about the empty room?": "S’ka njeri këtu! Do të donit të ftoni të tjerë apo të reshtet së njoftuari për dhomë të zbrazët?", - "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "U provua të ngarkohej një pikë e caktuar në kronologjinë e kësaj dhome, por nuk keni leje për ta parë mesazhin në fjalë.", - "Your password was successfully changed. You will not receive push notifications on other devices until you log back in to them": "Fjalëkalimi juaj u ndryshua me sukses. Nuk do të merrni njoftime push në pajisjet tuaja të tjera, veç në hyfshi sërish në llogarinë tuaj në to", - "Start automatically after system login": "Nisu vetvetiu pas hyrjes në sistem", - "You may need to manually permit Riot to access your microphone/webcam": "Lypset të lejoni dorazi Riot-in të përdorë mikrofonin/kamerën tuaj web", - "No Audio Outputs detected": "S’u pikasën Sinjale Audio Në Dalje", - "Resetting password will currently reset any end-to-end encryption keys on all devices, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "Ricaktimi i fjalëkalimit do të shkaktojë në fakt edhe zerimin e çfarëdo kyçi fshehtëzimesh skaj-më-skaj në krejt pajisjet, duke e bërë kështu të palexueshëm historikun e bisedës së fshehtëzuar, veç në paçi eksportuar më parë kyçet e dhomës tuaj dhe i rim-importoni më pas. Në të ardhmen kjo punë do të përmirësohet.", - "You have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, sign in again on each device": "Jeni nxjerrë jashtë krejt pajisjeve dhe nuk do të merrni më njoftime push. Që të riaktivizoni njoftimet, bëni sërish hyrjen në çdo pajisje", - "This Home Server does not support login using email address.": "Ky shërbyes Home nuk mbulon hyrje përmes adresash email.", - "This homeserver doesn't offer any login flows which are supported by this client.": "Ky shërbyes home nuk ofron ndonjë mënyrë hyrjesh që mbulohet nga ky klient.", - "Unable to query for supported registration methods": "S’arrihet të kërkohet për metoda regjistrimi që mbulohen", - "The exported file will allow anyone who can read it to decrypt any encrypted messages that you can see, so you should be careful to keep it secure. To help with this, you should enter a passphrase below, which will be used to encrypt the exported data. It will only be possible to import the data by using the same passphrase.": "Kartela e eksportuar do t’i lejojë kujtdo që e lexon të shfshehtëzojë çfarëdo mesazhesh të fshehtëzuar që mund të shihni, ndaj duhet të jeni i kujdesshëm për ta mbajtur të parrezikuar. Si ndihmë për këtë, duhet të jepni më poshtë një frazëkalim, që do të përdoret për të fshehtëzuar të dhënat e eksportuara. Importimi i të dhënave do të jetë i mundur vetëm duke përdorur të njëjtin frazëkalim.", - "Not a valid Riot keyfile": "S’është kartelë kyçesh Riot e vlefshme", - "Revoke Moderator": "Shfuqizoje Si Moderator", - "You have no historical rooms": "S’keni dhoma të dikurshme", - "Historical": "Të dikurshme", - "Flair": "Simbole", - "Showing flair for these communities:": "Shfaqen simbole për këto bashkësi:", - "This room is not showing flair for any communities": "Kjo dhomë nuk shfaq simbole për ndonjë bashkësi", - "Robot check is currently unavailable on desktop - please use a web browser": "Kontrolli për robot hëpërhë s’është i përdorshëm në desktop - ju lutemi, përdorni një shfletues", - "Please review and accept all of the homeserver's policies": "Ju lutemi, shqyrtoni dhe pranoni krejt rregullat e këtij shërbyesi home", - "Flair will appear if enabled in room settings": "Simbolet do të shfaqen nëse aktivizohen te rregullimet e dhomës", - "Flair will not appear": "Simbolet nuk do të shfaqen", - "Display your community flair in rooms configured to show it.": "Shfaqni simbolet e bashkësisë tuaj në dhoma të formësuara për t’i shfaqur ato.", - "Are you sure you wish to remove (delete) this event? Note that if you delete a room name or topic change, it could undo the change.": "Jeni i sigurt se doni të hiqet (fshihet) ky akt? Mbani parasysh se nëse fshini emrin e një dhome ose ndryshimin e temës, kjo mund të sjellë zhbërjen e ndryshimit.", - "To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of Riot to do this": "Që të shmanget humbja e historikut të fjalosjes tuaj, duhet të eksportoni kyçet e dhomës tuaj përpara se të dilni nga llogari. Që ta bëni këtë, duhe të riktheheni te versioni më i ri i Riot-it", - "You've previously used a newer version of Riot on %(host)s. To use this version again with end to end encryption, you will need to sign out and back in again. ": "Më parë përdorët një version më të ri të Riot-it në %(host)s. Që ta përdorni sërish këtë version me fshehtëzim skaj-më-skaj, duhet të dilni dhe rihyni te llogaria juaj. ", - "Incompatible Database": "Bazë të dhënash e Papërputhshme", - "Continue With Encryption Disabled": "Vazhdo Me Fshehtëzimin të Çaktivizuar", - "Unable to load! Check your network connectivity and try again.": "S’arrihet të ngarkohet! Kontrolloni lidhjen tuaj në rrjet dhe riprovoni.", - "Forces the current outbound group session in an encrypted room to be discarded": "Forces the current outbound group session in an encrypted room to be discarded", - "Backup of encryption keys to server": "Kopjeruajtje kyçesh fshehtëzimi në shërbyes", - "Delete Backup": "Fshije Kopjeruajtjen", - "Delete your backed up encryption keys from the server? You will no longer be able to use your recovery key to read encrypted message history": "Të fshihen nga shërbyesi kyçet e kopjeruajtur të fshehtëzimit? S’do të jeni më në gjendje të përdorni kyçin tuaj të rimarrjeve për lexim historiku mesazhesh të fshehtëzuar", - "Delete backup": "Fshije kopjeruajtjen", - "Unable to load key backup status": "S’arrihet të ngarkohet gjendje kopjeruajtjeje kyçesh", - "This device is uploading keys to this backup": "Kjo pajisje po ngarkon kyçe te kjo kopjeruajtje", - "This device is not uploading keys to this backup": "Kjo pajisje nuk po ngarkon kyçe te kjo kopjeruajtje", - "Backup has a valid signature from this device": "Kopjeruajtja ka një nënshkrim të vlefshëm prej kësaj pajisjeje", - "Backup has a valid signature from verified device x": "Kopjeruajtja ka një nënshkrim të vlefshëm prej pajisjes së verifikuar x", - "Backup has a valid signature from unverified device ": "Kopjeruajtja ka një nënshkrim të vlefshëm prej pajisjes së paverifikuar ", - "Backup has an invalid signature from verified device ": "Kopjeruajtja ka një nënshkrim të pavlefshëm prej pajisjes së verifikuar ", - "Backup has an invalid signature from unverified device ": "Kopjeruajtja ka një nënshkrim të pavlefshëm prej pajisjes së paverifikuar ", - "Backup is not signed by any of your devices": "Kopjeruajtja s’është nënshkruar nga ndonjë prej pajisjeve tuaja", - "Backup version: ": "Version kopjeruajtjeje: ", - "Algorithm: ": "Algoritëm: ", - "Restore backup": "Riktheje kopjeruajtjen", - "No backup is present": "S’ka kopjeruajtje të pranishëm", - "Start a new backup": "Filloni një kopjeruajtje të re", - "Secure your encrypted message history with a Recovery Passphrase.": "Sigurojeni historikun e mesazheve tuaj të fshehtëzuar me një Frazëkalim Rimarrjesh.", - "You'll need it if you log out or lose access to this device.": "Do t’ju duhet, nëse dilni nga llogaria ose nëse s’përdorni më dot pajisjen.", - "Enter a passphrase...": "Jepni një frazëkalim…", - "Next": "Pasuesja", - "If you don't want encrypted message history to be availble on other devices, .": "Nëse s’doni që historiku i mesazheve të fshehtëzuara të jetë i përdorshëm në pajisje të tjera, .", - "Or, if you don't want to create a Recovery Passphrase, skip this step and .": "Ose, nëse s’doni të krijohet një Frazëkalim Rimarrjesh, anashkalojeni këtë hap dhe .", - "That matches!": "U përputhën!", - "That doesn't match.": "S’përputhen.", - "Go back to set it again.": "Shkoni mbrapsht që ta ricaktoni.", - "Type in your Recovery Passphrase to confirm you remember it. If it helps, add it to your password manager or store it somewhere safe.": "Shtypeni Frazëkalimin tuaj të Rimarrjeve që të ripohoni se e mbani mend. Nëse bën punë, shtojeni te përgjegjësi juaj i fjalëkalimeve ose depozitojeni diku pa rrezik.", - "Repeat your passphrase...": "Përsëritni frazëkalimin tuaj…", - "Make a copy of this Recovery Key and keep it safe.": "Bëni një kopje të këtij Kyçi RImarrjesh dhe mbajeni të parrezikuar.", - "As a safety net, you can use it to restore your encrypted message history if you forget your Recovery Passphrase.": "Si rrjet i parrezikuar, mund ta përdoreni për të rikthyer historikun e mesazheve tuaj të fshehtëzuar, nëse harroni Frazëkalimin e Rimarrjeve.", - "Your Recovery Key": "Kyçi Juaj i Rimarrjeve", - "Copy to clipboard": "Kopjoje në të papastër", - "Download": "Shkarkoje", - "I've made a copy": "Kam bërë një kopje", - "Your Recovery Key has been copied to your clipboard, paste it to:": "Kyçi juaj i Fshehtëzimeve është kopjuar te e papastra juaj, ngjiteni te:", - "Your Recovery Key is in your Downloads folder.": "Kyçi juaj i Fshehtëzimeve gjendet te dosja juaj Shkarkime.", - "Print it and store it somewhere safe": "Shtypeni dhe ruajeni diku pa rrezik", - "Save it on a USB key or backup drive": "Ruajeni në një diskth USB ose disk kopjeruajtjesh", - "Copy it to your personal cloud storage": "Kopjojeni te depoja juaj personale në re", - "Got it": "E mora vesh", - "Backup created": "Kopjeruajtja u krijua", - "Your encryption keys are now being backed up to your Homeserver.": "Kyçet tuaj të fshehtëzimit tani po kopjeruhen te shërbyesi juaj Home.", - "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another device.": "Pa rregulluar Rimarrje të Siguruar, s’do të jeni në gjendje të riktheni historikun e mesazheve tuaj të fshehtëzuar, nëse bëni daljen ose përdorni një pajisje tjetër.", - "Set up Secure Message Recovery": "Rregulloni Rimarrje të Siguruar Mesazhesh", - "Create a Recovery Passphrase": "Krijoni Frazëkalim Rimarrjeje", - "Confirm Recovery Passphrase": "Ripohoni Frazëkalim Rimarrjeje", - "Recovery Key": "Kyç Rimarrjesh", - "Keep it safe": "Mbajeni të parrezikuar", - "Backing up...": "Po kopjeruhet…", - "Create Key Backup": "Krijo Kopjeruajtje Kyçesh", - "Unable to create key backup": "S’arrihet të krijojhet kopjeruajtje kyçesh", - "Retry": "Riprovo", - "Unable to load backup status": "S’arrihet të ngarkohet gjendje kopjeruajtjeje", - "Unable to restore backup": "S’arrihet të rikthehet kopjeruajtje", - "No backup found!": "S’u gjet kopjeruajtje!", - "Backup Restored": "Kopjeruajtja u Rikthye", - "Failed to decrypt %(failedCount)s sessions!": "S’u arrit të shfshehtëzohet sesioni %(failedCount)s!", - "Restored %(sessionCount)s session keys": "U rikthyen kyçet e sesionit %(sessionCount)s", - "Enter Recovery Passphrase": "Jepni Frazëkalim Rimarrjeje", - "Access your secure message history and set up secure messaging by entering your recovery passphrase.": "Hyni te historiku i mesazheve tuaj të siguruar dhe rregulloni shkëmbim mesazhesh të sigurt duke dhënë frazëkalimin tuaj të rimarrjeve.", - "If you've forgotten your recovery passphrase you can use your recovery key or set up new recovery options": "Nëse keni harruar frazëkalimin tuaj të rimarrjeve, mund të përdorni kyçin tuaj të rimarrjeve ose rregulloni mundësi të reja rimarrjeje", - "Enter Recovery Key": "Jepni Kyç Rimarrjeje", - "This looks like a valid recovery key!": "Ky duket si kyç i vlefshëm rimarrjesh!", - "Not a valid recovery key": "Kyç rimarrjesh jo i vlefshëm", - "Access your secure message history and set up secure messaging by entering your recovery key.": "Hyni te historiku i mesazheve tuaj të siguruar dhe rregulloni shkëmbim mesazhesh të sigurt duke dhënë kyçin tuaj të rimarrjeve.", - "If you've forgotten your recovery passphrase you can ": "Nëse keni harruar frazëkalimin tuaj të rimarrjeve, mund të rregulloni mundësi të reja rimarrjeje", - "Key Backup": "Kopjeruajtje Kyçi", - "Sign in with single sign-on": "Bëni hyrjen me hyrje njëshe", - "Disable Peer-to-Peer for 1:1 calls": "Çaktivizoje mekanizmin Peer-to-Peer për thirrje 1 me 1", - "Failed to perform homeserver discovery": "S’u arrit të kryhej zbulim shërbyesi Home", - "Invalid homeserver discovery response": "Përgjigje e pavlefshme zbulimi shërbyesi Home", - "Cannot find homeserver": "S’gjendet dot shërbyesi Home", - "File is too big. Maximum file size is %(fileSize)s": "Kartela është shumë e madhe. Madhësia maksimum për kartelat është %(fileSize)s", - "The following files cannot be uploaded:": "Kartelat vijuese s’mund të ngarkohen:", - "Use a few words, avoid common phrases": "Përdorni ca fjalë, shmangni fraza të rëndomta", - "No need for symbols, digits, or uppercase letters": "S’ka nevojë për simbole, shifra apo shkronja të mëdha", - "Use a longer keyboard pattern with more turns": "Përdorni një rregullsi më të gjatë tastiere, me më tepër kthesa", - "Avoid repeated words and characters": "Shmangi përsëritje fjalësh dhe përsëritje shkronjash", - "Avoid sequences": "Shmangi togfjalësha", - "Avoid recent years": "Shmangni vitet e fundit", - "Avoid years that are associated with you": "Shmangni vite që kanë lidhje me ju", - "Avoid dates and years that are associated with you": "Shmangni data dhe vite që kanë lidhje me ju", - "Capitalization doesn't help very much": "Shkrimi i shkronjës së parë me të madhe nuk ndihmon kushedi çë", - "All-uppercase is almost as easy to guess as all-lowercase": "Fjalë shkruar krejt me të mëdha janë thuajse po aq të lehta për t’i hamendësuar sa ato me krejt të vogla", - "Reversed words aren't much harder to guess": "Fjalët së prapthi s’janë të vështira për t’i marrë me mend", - "Predictable substitutions like '@' instead of 'a' don't help very much": "Zëvendësime të parashikueshme, të tilla si '@', në vend të 'a', nuk ndihmojnë kushedi çë", - "Add another word or two. Uncommon words are better.": "Shtoni një a dy fjalë të tjera. Fjalë jo të rëndomta janë më të përshtatshme.", - "Repeats like \"aaa\" are easy to guess": "Përsëritje të tilla si \"aaa\" janë të lehta për t’u hamendësuar", - "Repeats like \"abcabcabc\" are only slightly harder to guess than \"abc\"": "Përsëritje të tilla si \"abcabcabc\" janë vetëm pak më të vështira për t’u hamendësuar se sa \"abc\"", - "Sequences like abc or 6543 are easy to guess": "Sekuenca të tilla si abc ose 6543 janë të lehta për t’u hamendsuar", - "Recent years are easy to guess": "Vitet tani afër janë të lehtë për t’u hamendësuar", - "Dates are often easy to guess": "Datat shpesh janë të lehta për t’i gjetur", - "This is a top-10 common password": "Ky fjalëkalim është nga 10 më të rëndomtët", - "This is a top-100 common password": "Ky fjalëkalim është nga 100 më të rëndomtët", - "This is a very common password": "Ky është një fjalëkalim shumë i rëndomtë", - "This is similar to a commonly used password": "Ky është i ngjashëm me një fjalëkalim të përdorur rëndom", - "A word by itself is easy to guess": "Një fjalë më vete është e lehtë të hamendësohet", - "Names and surnames by themselves are easy to guess": "Emrat dhe mbiemrat në vetvete janë të lehtë për t’i hamendësuar", - "Common names and surnames are easy to guess": "Emra dhe mbiemra të rëndomtë janë të kollajtë për t’u hamendësuar", - "Great! This passphrase looks strong enough.": "Bukur! Ky frazëkalim duket goxha i fuqishëm.", - "Failed to load group members": "S'u arrit të ngarkoheshin anëtarë grupi", - "As a safety net, you can use it to restore your encrypted message history.": "Si një rrjet sigurie, mund ta përdorni për të rikthyer historikun e mesazheve tuaj të fshehtëzuar." + "The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.": "Kartela e eksportit është e mbrojtur me një frazëkalim. Që të shfshehtëzoni kartelën, duhet ta jepni frazëkalimin këtu." } diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index 712911064f..242990264c 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -1300,112 +1300,5 @@ "You are currently using Riot anonymously as a guest.": "您目前是以訪客的身份匿名使用 Riot。", "You are an administrator of this community. You will not be able to rejoin without an invite from another administrator.": "您是此社群的管理員。您將無法在沒有其他管理員的邀請下重新加入。", "Open Devtools": "開啟開發者工具", - "Show developer tools": "顯示開發者工具", - "Unable to load! Check your network connectivity and try again.": "無法載入!請檢查您的網路連線狀態並再試一次。", - "Backup of encryption keys to server": "將加密金鑰備份到伺服器", - "Delete Backup": "刪除備份", - "Delete your backed up encryption keys from the server? You will no longer be able to use your recovery key to read encrypted message history": "從伺服器刪除您已備份的加密金鑰?您將無法再使用您的復原金鑰來讀取加密的訊息歷史", - "Delete backup": "刪除備份", - "Unable to load key backup status": "無法載入金鑰備份狀態", - "This device is uploading keys to this backup": "此裝置正在上傳金鑰到此備份", - "This device is not uploading keys to this backup": "此裝置並未上傳金鑰到此備份", - "Backup has a valid signature from this device": "備份有從此裝置而來的有效簽章", - "Backup has a valid signature from verified device x": "備份有從已驗證的 x 裝置而來的有效簽章", - "Backup has a valid signature from unverified device ": "備份有從未驗證的 裝置而來的有效簽章", - "Backup has an invalid signature from verified device ": "備份有從已驗證的 裝置而來的無效簽章", - "Backup has an invalid signature from unverified device ": "備份有從未驗證的 裝置而來的無效簽章", - "Backup is not signed by any of your devices": "備份未被您的任何裝置簽署", - "Backup version: ": "備份版本: ", - "Algorithm: ": "演算法: ", - "Restore backup": "恢復備份", - "No backup is present": "沒有備份", - "Start a new backup": "開始新備份", - "Please review and accept all of the homeserver's policies": "請審閱並接受家伺服器的所有政策", - "To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of Riot to do this": "為了避免遺失您的聊天歷史,您必須在登出前匯出您的聊天室金鑰。您必須回到較新的 Riot 才能執行此動作", - "You've previously used a newer version of Riot on %(host)s. To use this version again with end to end encryption, you will need to sign out and back in again. ": "您先前在 %(host)s 上使用較新的 Riot 版本。要再次與此版本一同使用端到端加密,您將需要登出並再次登入。 ", - "Incompatible Database": "不相容的資料庫", - "Continue With Encryption Disabled": "在停用加密的情況下繼續", - "Secure your encrypted message history with a Recovery Passphrase.": "以復原密碼保證您的加密訊息歷史安全。", - "You'll need it if you log out or lose access to this device.": "如果您登出或是遺失對此裝置的存取權,您將會需要它。", - "Enter a passphrase...": "輸入密碼……", - "Next": "下一個", - "If you don't want encrypted message history to be availble on other devices, .": "如果您不想要讓加密的訊息歷史在其他裝置上可用,。", - "Or, if you don't want to create a Recovery Passphrase, skip this step and .": "或是,如果您不想建立復原密碼,跳過此步驟並。", - "That matches!": "符合!", - "That doesn't match.": "不符合。", - "Go back to set it again.": "回去重新設定它。", - "Type in your Recovery Passphrase to confirm you remember it. If it helps, add it to your password manager or store it somewhere safe.": "輸入您的復原密碼以確認您記得它。如果可以的話,把它加入到您的密碼管理員或是把它儲存在其他安全的地方。", - "Repeat your passphrase...": "重覆您的密碼……", - "Make a copy of this Recovery Key and keep it safe.": "複製這把復原金鑰並把它放在安全的地方。", - "As a safety net, you can use it to restore your encrypted message history if you forget your Recovery Passphrase.": "做為安全網,您可以在忘記您的復原密碼時使用它來復原您的加密訊息歷史。", - "Your Recovery Key": "您的復原金鑰", - "Copy to clipboard": "複製到剪貼簿", - "Download": "下載", - "I've made a copy": "我已經有副本了", - "Your Recovery Key has been copied to your clipboard, paste it to:": "您的復原金鑰已複製到您的剪貼簿,將它貼上到:", - "Your Recovery Key is in your Downloads folder.": "您的復原金鑰在您的下載資料夾。", - "Print it and store it somewhere safe": "列印它並存放在安全的地方", - "Save it on a USB key or backup drive": "將它儲存到 USB 金鑰或備份磁碟上", - "Copy it to your personal cloud storage": "將它複製 到您的個人雲端儲存", - "Got it": "知道了", - "Backup created": "備份已建立", - "Your encryption keys are now being backed up to your Homeserver.": "您的加密金鑰已經備份到您的家伺服器了。", - "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another device.": "在沒有設定安全訊息復原的狀況下,您將無法在登出或使用其他裝置後復原您的已加密訊息歷史。", - "Set up Secure Message Recovery": "設定安全訊息復原", - "Create a Recovery Passphrase": "建立復原密碼", - "Confirm Recovery Passphrase": "確認復原密碼", - "Recovery Key": "復原金鑰", - "Keep it safe": "保持安全", - "Backing up...": "正在備份……", - "Create Key Backup": "建立金鑰備份", - "Unable to create key backup": "無法建立金鑰備份", - "Retry": "重試", - "Unable to load backup status": "無法載入備份狀態", - "Unable to restore backup": "無法復原備份", - "No backup found!": "找不到備份!", - "Backup Restored": "備份已復原", - "Failed to decrypt %(failedCount)s sessions!": "解密 %(failedCount)s 工作階段失敗!", - "Restored %(sessionCount)s session keys": "%(sessionCount)s 工作階段金鑰已復原", - "Enter Recovery Passphrase": "輸入復原密碼", - "Access your secure message history and set up secure messaging by entering your recovery passphrase.": "存取您的安全訊息歷史並透過輸入您的復原密碼來設定安全訊息。", - "If you've forgotten your recovery passphrase you can use your recovery key or set up new recovery options": "如果您忘記您的復原密碼,您可以使用您的復原金鑰設定新的復原選項", - "Enter Recovery Key": "輸入復原金鑰", - "This looks like a valid recovery key!": "看起來是有效的復原金鑰!", - "Not a valid recovery key": "不是有效的復原金鑰", - "Access your secure message history and set up secure messaging by entering your recovery key.": "存取您的安全訊息歷史並趟過輸入您的復原金鑰來設定安全傳訊。", - "If you've forgotten your recovery passphrase you can ": "如果您忘記您的復原密碼,您可以", - "Key Backup": "金鑰備份", - "Failed to perform homeserver discovery": "執行家伺服器探索失敗", - "Invalid homeserver discovery response": "無效的家伺服器探索回應", - "Cannot find homeserver": "找不到家伺服器", - "Sign in with single sign-on": "以單一登入來登入", - "File is too big. Maximum file size is %(fileSize)s": "檔案太大了。最大的檔案大小為 %(fileSize)s", - "The following files cannot be uploaded:": "下列檔案無法上傳:", - "Use a few words, avoid common phrases": "使用數個字,但避免常用片語", - "No need for symbols, digits, or uppercase letters": "不需要符號、數字或大寫字母", - "Use a longer keyboard pattern with more turns": "以更多變化使用較長的鍵盤模式", - "Avoid repeated words and characters": "避免重覆的文字與字母", - "Avoid sequences": "避免序列", - "Avoid recent years": "避免最近的年份", - "Avoid years that are associated with you": "避免關於您的年份", - "Avoid dates and years that are associated with you": "避免關於您的日期與年份", - "Capitalization doesn't help very much": "大寫並沒有太大的協助", - "All-uppercase is almost as easy to guess as all-lowercase": "全大寫通常比全小寫好猜", - "Reversed words aren't much harder to guess": "反向拼字不會比較難猜", - "Predictable substitutions like '@' instead of 'a' don't help very much": "如「@」而非「a」這樣的預期中的替換並沒有太多的協助", - "Add another word or two. Uncommon words are better.": "加入一個或兩個額外的單字。最好是不常用的。", - "Repeats like \"aaa\" are easy to guess": "如「aaa」這樣的重覆易於猜測", - "Repeats like \"abcabcabc\" are only slightly harder to guess than \"abc\"": "如「abcabcabc」這樣的重覆只比「abc」難猜一點", - "Sequences like abc or 6543 are easy to guess": "如 abc 或 6543 這樣的序列易於猜測", - "Recent years are easy to guess": "最近的年份易於猜測", - "Dates are often easy to guess": "日期通常比較好猜", - "This is a top-10 common password": "這是十大最常見的密碼", - "This is a top-100 common password": "這是百大最常見的密碼", - "This is a very common password": "這是非常常見的密碼", - "This is similar to a commonly used password": "這與常見使用的密碼很類似", - "A word by itself is easy to guess": "單字本身很容易猜測", - "Names and surnames by themselves are easy to guess": "姓名與姓氏本身很容易猜測", - "Common names and surnames are easy to guess": "常見的名字與姓氏易於猜測", - "Great! This passphrase looks strong enough.": "很好!這個密碼看起來夠強了。", - "As a safety net, you can use it to restore your encrypted message history.": "做為安全網,您可以使用它來復原您已加密的訊息歷史。" + "Show developer tools": "顯示開發者工具" } diff --git a/src/matrix-to.js b/src/matrix-to.js index b750dff6d6..b5827f671a 100644 --- a/src/matrix-to.js +++ b/src/matrix-to.js @@ -15,8 +15,6 @@ limitations under the License. */ import MatrixClientPeg from "./MatrixClientPeg"; -import isIp from "is-ip"; -import utils from 'matrix-js-sdk/lib/utils'; export const host = "matrix.to"; export const baseUrl = `https://${host}`; @@ -92,9 +90,7 @@ export function pickServerCandidates(roomId) { // Rationale for popular servers: It's hard to get rid of people when // they keep flocking in from a particular server. Sure, the server could // be ACL'd in the future or for some reason be evicted from the room - // however an event like that is unlikely the larger the room gets. If - // the server is ACL'd at the time of generating the link however, we - // shouldn't pick them. We also don't pick IP addresses. + // however an event like that is unlikely the larger the room gets. // Note: we don't pick the server the room was created on because the // homeserver should already be using that server as a last ditch attempt @@ -108,29 +104,12 @@ export function pickServerCandidates(roomId) { // The receiving user can then manually append the known-good server to // the list and magically have the link work. - const bannedHostsRegexps = []; - let allowedHostsRegexps = [new RegExp(".*")]; // default allow everyone - if (room.currentState) { - const aclEvent = room.currentState.getStateEvents("m.room.server_acl", ""); - if (aclEvent && aclEvent.getContent()) { - const getRegex = (hostname) => new RegExp("^" + utils.globToRegexp(hostname, false) + "$"); - - const denied = aclEvent.getContent().deny || []; - denied.forEach(h => bannedHostsRegexps.push(getRegex(h))); - - const allowed = aclEvent.getContent().allow || []; - allowedHostsRegexps = []; // we don't want to use the default rule here - allowed.forEach(h => allowedHostsRegexps.push(getRegex(h))); - } - } - const populationMap: {[server:string]:number} = {}; const highestPlUser = {userId: null, powerLevel: 0, serverName: null}; for (const member of room.getJoinedMembers()) { const serverName = member.userId.split(":").splice(1).join(":"); - if (member.powerLevel > highestPlUser.powerLevel && !isHostnameIpAddress(serverName) - && !isHostInRegex(serverName, bannedHostsRegexps) && isHostInRegex(serverName, allowedHostsRegexps)) { + if (member.powerLevel > highestPlUser.powerLevel) { highestPlUser.userId = member.userId; highestPlUser.powerLevel = member.powerLevel; highestPlUser.serverName = serverName; @@ -146,9 +125,8 @@ export function pickServerCandidates(roomId) { const beforePopulation = candidates.length; const serversByPopulation = Object.keys(populationMap) .sort((a, b) => populationMap[b] - populationMap[a]) - .filter(a => !candidates.includes(a) && !isHostnameIpAddress(a) - && !isHostInRegex(a, bannedHostsRegexps) && isHostInRegex(a, allowedHostsRegexps)); - for (let i = beforePopulation; i < MAX_SERVER_CANDIDATES; i++) { + .filter(a => !candidates.includes(a)); + for (let i = beforePopulation; i <= MAX_SERVER_CANDIDATES; i++) { const idx = i - beforePopulation; if (idx >= serversByPopulation.length) break; candidates.push(serversByPopulation[idx]); @@ -156,34 +134,3 @@ export function pickServerCandidates(roomId) { return candidates; } - -function getHostnameFromMatrixDomain(domain) { - if (!domain) return null; - - // The hostname might have a port, so we convert it to a URL and - // split out the real hostname. - const parser = document.createElement('a'); - parser.href = "https://" + domain; - return parser.hostname; -} - -function isHostInRegex(hostname, regexps) { - hostname = getHostnameFromMatrixDomain(hostname); - if (!hostname) return true; // assumed - if (regexps.length > 0 && !regexps[0].test) throw new Error(regexps[0]); - - return regexps.filter(h => h.test(hostname)).length > 0; -} - -function isHostnameIpAddress(hostname) { - hostname = getHostnameFromMatrixDomain(hostname); - if (!hostname) return false; - - // is-ip doesn't want IPv6 addresses surrounded by brackets, so - // take them off. - if (hostname.startsWith("[") && hostname.endsWith("]")) { - hostname = hostname.substring(1, hostname.length - 1); - } - - return isIp(hostname); -} diff --git a/src/notifications/StandardActions.js b/src/notifications/StandardActions.js index 15f645d5f7..30d6ea5975 100644 --- a/src/notifications/StandardActions.js +++ b/src/notifications/StandardActions.js @@ -24,7 +24,6 @@ module.exports = { ACTION_NOTIFY: encodeActions({notify: true}), ACTION_NOTIFY_DEFAULT_SOUND: encodeActions({notify: true, sound: "default"}), ACTION_NOTIFY_RING_SOUND: encodeActions({notify: true, sound: "ring"}), - ACTION_HIGHLIGHT: encodeActions({notify: true, highlight: true}), ACTION_HIGHLIGHT_DEFAULT_SOUND: encodeActions({notify: true, sound: "default", highlight: true}), ACTION_DONT_NOTIFY: encodeActions({notify: false}), ACTION_DISABLED: null, diff --git a/src/notifications/VectorPushRulesDefinitions.js b/src/notifications/VectorPushRulesDefinitions.js index 3df2e70774..eeb193cb8a 100644 --- a/src/notifications/VectorPushRulesDefinitions.js +++ b/src/notifications/VectorPushRulesDefinitions.js @@ -20,7 +20,6 @@ import { _td } from '../languageHandler'; const StandardActions = require('./StandardActions'); const PushRuleVectorState = require('./PushRuleVectorState'); -const { decodeActions } = require('./NotificationUtils'); class VectorPushRuleDefinition { constructor(opts) { @@ -32,11 +31,13 @@ class VectorPushRuleDefinition { // Translate the rule actions and its enabled value into vector state ruleToVectorState(rule) { let enabled = false; + let actions = null; if (rule) { enabled = rule.enabled; + actions = rule.actions; } - for (const stateKey in PushRuleVectorState.states) { // eslint-disable-line guard-for-in + for (const stateKey in PushRuleVectorState.states) { const state = PushRuleVectorState.states[stateKey]; const vectorStateToActions = this.vectorStateToActions[state]; @@ -46,21 +47,15 @@ class VectorPushRuleDefinition { return state; } } else { - // The actions must match to the ones expected by vector state. - // Use `decodeActions` on both sides to canonicalize things like - // value: true vs. unspecified for highlight (which defaults to - // true, making them equivalent). - if (enabled && - JSON.stringify(decodeActions(rule.actions)) === - JSON.stringify(decodeActions(vectorStateToActions))) { + // The actions must match to the ones expected by vector state + if (enabled && JSON.stringify(rule.actions) === JSON.stringify(vectorStateToActions)) { return state; } } } - console.error(`Cannot translate rule actions into Vector rule state. ` + - `Rule: ${JSON.stringify(rule)}, ` + - `Expected: ${JSON.stringify(this.vectorStateToActions)}`); + console.error("Cannot translate rule actions into Vector rule state. Rule: " + + JSON.stringify(rule)); return undefined; } } @@ -91,17 +86,6 @@ module.exports = { }, }), - // Messages containing @room - ".m.rule.roomnotif": new VectorPushRuleDefinition({ - kind: "override", - description: _td("Messages containing @room"), // passed through _t() translation in src/components/views/settings/Notifications.js - vectorStateToActions: { // The actions for each vector state, or null to disable the rule. - on: StandardActions.ACTION_NOTIFY, - loud: StandardActions.ACTION_HIGHLIGHT, - off: StandardActions.ACTION_DISABLED, - }, - }), - // Messages just sent to the user in a 1:1 room ".m.rule.room_one_to_one": new VectorPushRuleDefinition({ kind: "underride", @@ -113,17 +97,6 @@ module.exports = { }, }), - // Encrypted messages just sent to the user in a 1:1 room - ".m.rule.encrypted_room_one_to_one": new VectorPushRuleDefinition({ - kind: "underride", - description: _td("Encrypted messages in one-to-one chats"), // passed through _t() translation in src/components/views/settings/Notifications.js - vectorStateToActions: { - on: StandardActions.ACTION_NOTIFY, - loud: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND, - off: StandardActions.ACTION_DONT_NOTIFY, - }, - }), - // Messages just sent to a group chat room // 1:1 room messages are catched by the .m.rule.room_one_to_one rule if any defined // By opposition, all other room messages are from group chat rooms. @@ -137,19 +110,6 @@ module.exports = { }, }), - // Encrypted messages just sent to a group chat room - // Encrypted 1:1 room messages are catched by the .m.rule.encrypted_room_one_to_one rule if any defined - // By opposition, all other room messages are from group chat rooms. - ".m.rule.encrypted": new VectorPushRuleDefinition({ - kind: "underride", - description: _td("Encrypted messages in group chats"), // passed through _t() translation in src/components/views/settings/Notifications.js - vectorStateToActions: { - on: StandardActions.ACTION_NOTIFY, - loud: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND, - off: StandardActions.ACTION_DONT_NOTIFY, - }, - }), - // Invitation for the user ".m.rule.invite_for_me": new VectorPushRuleDefinition({ kind: "underride", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 56e66844dc..d0b4b9b9d6 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -151,11 +151,6 @@ export const SETTINGS = { displayName: _td('Always show encryption icons'), default: true, }, - "showRoomRecoveryReminder": { - supportedLevels: LEVELS_ACCOUNT_SETTINGS, - displayName: _td('Show a reminder to enable Secure Message Recovery in encrypted rooms'), - default: true, - }, "enableSyntaxHighlightLanguageDetection": { supportedLevels: LEVELS_ACCOUNT_SETTINGS, displayName: _td('Enable automatic language detection for syntax highlighting'), diff --git a/src/shouldHideEvent.js b/src/shouldHideEvent.js index adc89a126a..3aad05a976 100644 --- a/src/shouldHideEvent.js +++ b/src/shouldHideEvent.js @@ -38,20 +38,18 @@ function memberEventDiff(ev) { } export default function shouldHideEvent(ev) { - // Wrap getValue() for readability. Calling the SettingsStore can be - // fairly resource heavy, so the checks below should avoid hitting it - // where possible. + // Wrap getValue() for readability const isEnabled = (name) => SettingsStore.getValue(name, ev.getRoomId()); // Hide redacted events - if (ev.isRedacted() && isEnabled('hideRedactions')) return true; + if (isEnabled('hideRedactions') && ev.isRedacted()) return true; const eventDiff = memberEventDiff(ev); if (eventDiff.isMemberEvent) { - if ((eventDiff.isJoin || eventDiff.isPart) && isEnabled('hideJoinLeaves')) return true; - if (eventDiff.isAvatarChange && isEnabled('hideAvatarChanges')) return true; - if (eventDiff.isDisplaynameChange && isEnabled('hideDisplaynameChanges')) return true; + if (isEnabled('hideJoinLeaves') && (eventDiff.isJoin || eventDiff.isPart)) return true; + if (isEnabled('hideAvatarChanges') && eventDiff.isAvatarChange) return true; + if (isEnabled('hideDisplaynameChanges') && eventDiff.isDisplaynameChange) return true; } return false; diff --git a/src/stores/GroupStore.js b/src/stores/GroupStore.js index 4ac1e42e2e..bc2be37f51 100644 --- a/src/stores/GroupStore.js +++ b/src/stores/GroupStore.js @@ -122,6 +122,10 @@ class GroupStore extends EventEmitter { ); }, }; + + this.on('error', (err, groupId) => { + console.error(`GroupStore encountered error whilst fetching data for ${groupId}`, err); + }); } _fetchResource(stateKey, groupId) { @@ -144,7 +148,7 @@ class GroupStore extends EventEmitter { } console.error(`Failed to get resource ${stateKey} for ${groupId}`, err); - this.emit('error', err, groupId, stateKey); + this.emit('error', err, groupId); }).finally(() => { // Indicate finished request, allow for future fetches delete this._fetchResourcePromise[stateKey][groupId]; diff --git a/src/stores/RoomListStore.js b/src/stores/RoomListStore.js index af6a8cc991..c636c53631 100644 --- a/src/stores/RoomListStore.js +++ b/src/stores/RoomListStore.js @@ -300,10 +300,6 @@ class RoomListStore extends Store { const ts = this._tsOfNewestEvent(room); this._updateCachedRoomState(roomId, "timestamp", ts); return ts; - } else if (type === "unread-muted") { - const unread = Unread.doesRoomHaveUnreadMessages(room); - this._updateCachedRoomState(roomId, "unread-muted", unread); - return unread; } else if (type === "unread") { const unread = room.getUnreadNotificationCount() > 0; this._updateCachedRoomState(roomId, "unread", unread); @@ -362,21 +358,8 @@ class RoomListStore extends Store { } if (pinUnread) { - let unreadA = this._getRoomState(roomA, "unread"); - let unreadB = this._getRoomState(roomB, "unread"); - if (unreadA && !unreadB) return -1; - if (!unreadA && unreadB) return 1; - - // If they both have unread messages, sort by timestamp - // If nether have unread message (the fourth check not shown - // here), then just sort by timestamp anyways. - if (unreadA && unreadB) return timestampDiff; - - // Unread can also mean "unread without badge", which is - // different from what the above checks for. We're also - // going to sort those here. - unreadA = this._getRoomState(roomA, "unread-muted"); - unreadB = this._getRoomState(roomB, "unread-muted"); + const unreadA = this._getRoomState(roomA, "unread"); + const unreadB = this._getRoomState(roomB, "unread"); if (unreadA && !unreadB) return -1; if (!unreadA && unreadB) return 1; diff --git a/src/stores/RoomViewStore.js b/src/stores/RoomViewStore.js index 9e048e5d8e..f15925f480 100644 --- a/src/stores/RoomViewStore.js +++ b/src/stores/RoomViewStore.js @@ -224,11 +224,6 @@ class RoomViewStore extends Store { err: err, }); let msg = err.message ? err.message : JSON.stringify(err); - // XXX: We are relying on the error message returned by browsers here. - // This isn't great, but it does generalize the error being shown to users. - if (msg && msg.startsWith("CORS request rejected")) { - msg = _t("There was an error joining the room"); - } if (err.errcode === 'M_INCOMPATIBLE_ROOM_VERSION') { msg =
        {_t("Sorry, your homeserver is too old to participate in this room.")}
        diff --git a/src/utils/MultiInviter.js b/src/utils/MultiInviter.js index ad10f28edf..b3e7fc495a 100644 --- a/src/utils/MultiInviter.js +++ b/src/utils/MultiInviter.js @@ -1,6 +1,6 @@ /* Copyright 2016 OpenMarket Ltd -Copyright 2017, 2018 New Vector Ltd +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. @@ -17,9 +17,9 @@ limitations under the License. import MatrixClientPeg from '../MatrixClientPeg'; import {getAddressType} from '../UserAddress'; +import {inviteToRoom} from '../RoomInvite'; import GroupStore from '../stores/GroupStore'; import Promise from 'bluebird'; -import {_t} from "../languageHandler"; /** * Invites multiple addresses to a room or group, handling rate limiting from the server @@ -49,7 +49,7 @@ export default class MultiInviter { * Invite users to this room. This may only be called once per * instance of the class. * - * @param {array} addrs Array of addresses to invite + * @param {array} addresses Array of addresses to invite * @returns {Promise} Resolved when all invitations in the queue are complete */ invite(addrs) { @@ -88,30 +88,12 @@ export default class MultiInviter { return this.errorTexts[addr]; } - async _inviteToRoom(roomId, addr) { - const addrType = getAddressType(addr); - - if (addrType === 'email') { - return MatrixClientPeg.get().inviteByEmail(roomId, addr); - } else if (addrType === 'mx-user-id') { - const profile = await MatrixClientPeg.get().getProfileInfo(addr); - if (!profile) { - return Promise.reject({errcode: "M_NOT_FOUND", error: "User does not have a profile."}); - } - - return MatrixClientPeg.get().invite(roomId, addr); - } else { - throw new Error('Unsupported address'); - } - } - - _inviteMore(nextIndex) { if (this._canceled) { return; } - if (nextIndex === this.addrs.length) { + if (nextIndex == this.addrs.length) { this.busy = false; this.deferred.resolve(this.completionStates); return; @@ -129,7 +111,7 @@ export default class MultiInviter { // don't re-invite (there's no way in the UI to do this, but // for sanity's sake) - if (this.completionStates[addr] === 'invited') { + if (this.completionStates[addr] == 'invited') { this._inviteMore(nextIndex + 1); return; } @@ -138,7 +120,7 @@ export default class MultiInviter { if (this.groupId !== null) { doInvite = GroupStore.inviteUserToGroup(this.groupId, addr); } else { - doInvite = this._inviteToRoom(this.roomId, addr); + doInvite = inviteToRoom(this.roomId, addr); } doInvite.then(() => { @@ -147,34 +129,29 @@ export default class MultiInviter { this.completionStates[addr] = 'invited'; this._inviteMore(nextIndex + 1); - }).catch((err) => { + }, (err) => { if (this._canceled) { return; } let errorText; let fatal = false; - if (err.errcode === 'M_FORBIDDEN') { + if (err.errcode == 'M_FORBIDDEN') { fatal = true; - errorText = _t('You do not have permission to invite people to this room.'); - } else if (err.errcode === 'M_LIMIT_EXCEEDED') { + errorText = 'You do not have permission to invite people to this room.'; + } else if (err.errcode == 'M_LIMIT_EXCEEDED') { // we're being throttled so wait a bit & try again setTimeout(() => { this._inviteMore(nextIndex); }, 5000); return; - } else if(err.errcode === "M_NOT_FOUND") { - errorText = _t("User %(user_id)s does not exist", {user_id: addr}); } else { - errorText = _t('Unknown server error'); + errorText = 'Unknown server error'; } this.completionStates[addr] = 'error'; this.errorTexts[addr] = errorText; this.busy = !fatal; - this.fatal = fatal; if (!fatal) { this._inviteMore(nextIndex + 1); - } else { - this.deferred.resolve(this.completionStates); } }); } diff --git a/src/utils/PasswordScorer.js b/src/utils/PasswordScorer.js deleted file mode 100644 index e4bbec1637..0000000000 --- a/src/utils/PasswordScorer.js +++ /dev/null @@ -1,84 +0,0 @@ -/* -Copyright 2018 New Vector Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import Zxcvbn from 'zxcvbn'; - -import MatrixClientPeg from '../MatrixClientPeg'; -import { _t, _td } from '../languageHandler'; - -const ZXCVBN_USER_INPUTS = [ - 'riot', - 'matrix', -]; - -// Translations for zxcvbn's suggestion strings -_td("Use a few words, avoid common phrases"); -_td("No need for symbols, digits, or uppercase letters"); -_td("Use a longer keyboard pattern with more turns"); -_td("Avoid repeated words and characters"); -_td("Avoid sequences"); -_td("Avoid recent years"); -_td("Avoid years that are associated with you"); -_td("Avoid dates and years that are associated with you"); -_td("Capitalization doesn't help very much"); -_td("All-uppercase is almost as easy to guess as all-lowercase"); -_td("Reversed words aren't much harder to guess"); -_td("Predictable substitutions like '@' instead of 'a' don't help very much"); -_td("Add another word or two. Uncommon words are better."); - -// and warnings -_td("Repeats like \"aaa\" are easy to guess"); -_td("Repeats like \"abcabcabc\" are only slightly harder to guess than \"abc\""); -_td("Sequences like abc or 6543 are easy to guess"); -_td("Recent years are easy to guess"); -_td("Dates are often easy to guess"); -_td("This is a top-10 common password"); -_td("This is a top-100 common password"); -_td("This is a very common password"); -_td("This is similar to a commonly used password"); -_td("A word by itself is easy to guess"); -_td("Names and surnames by themselves are easy to guess"); -_td("Common names and surnames are easy to guess"); - -/** - * Wrapper around zxcvbn password strength estimation - * Include this only from async components: it pulls in zxcvbn - * (obviously) which is large. - */ -export function scorePassword(password) { - if (password.length === 0) return null; - - const userInputs = ZXCVBN_USER_INPUTS.slice(); - userInputs.push(MatrixClientPeg.get().getUserIdLocalpart()); - - let zxcvbnResult = Zxcvbn(password, userInputs); - // Work around https://github.com/dropbox/zxcvbn/issues/216 - if (password.includes(' ')) { - const resultNoSpaces = Zxcvbn(password.replace(/ /g, ''), userInputs); - if (resultNoSpaces.score < zxcvbnResult.score) zxcvbnResult = resultNoSpaces; - } - - for (let i = 0; i < zxcvbnResult.feedback.suggestions.length; ++i) { - // translate suggestions - zxcvbnResult.feedback.suggestions[i] = _t(zxcvbnResult.feedback.suggestions[i]); - } - // and warning, if any - if (zxcvbnResult.feedback.warning) { - zxcvbnResult.feedback.warning = _t(zxcvbnResult.feedback.warning); - } - - return zxcvbnResult; -} diff --git a/test/components/structures/GroupView-test.js b/test/components/structures/GroupView-test.js index 89632dcc48..3b3510f26e 100644 --- a/test/components/structures/GroupView-test.js +++ b/test/components/structures/GroupView-test.js @@ -164,7 +164,7 @@ describe('GroupView', function() { it('should indicate failure after failed /summary', function() { const groupView = ReactTestUtils.findRenderedComponentWithType(root, GroupView); - const prom = waitForUpdate(groupView, 4).then(() => { + const prom = waitForUpdate(groupView).then(() => { ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView_error'); }); @@ -179,7 +179,7 @@ describe('GroupView', function() { it('should show a group avatar, name, id and short description after successful /summary', function() { const groupView = ReactTestUtils.findRenderedComponentWithType(root, GroupView); - const prom = waitForUpdate(groupView, 4).then(() => { + const prom = waitForUpdate(groupView).then(() => { ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView'); const avatar = ReactTestUtils.findRenderedComponentWithType(root, sdk.getComponent('avatars.GroupAvatar')); @@ -214,7 +214,7 @@ describe('GroupView', function() { it('should show a simple long description after successful /summary', function() { const groupView = ReactTestUtils.findRenderedComponentWithType(root, GroupView); - const prom = waitForUpdate(groupView, 4).then(() => { + const prom = waitForUpdate(groupView).then(() => { ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView'); const longDesc = ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView_groupDesc'); @@ -235,7 +235,7 @@ describe('GroupView', function() { it('should show a placeholder if a long description is not set', function() { const groupView = ReactTestUtils.findRenderedComponentWithType(root, GroupView); - const prom = waitForUpdate(groupView, 4).then(() => { + const prom = waitForUpdate(groupView).then(() => { const placeholder = ReactTestUtils .findRenderedDOMComponentWithClass(root, 'mx_GroupView_groupDesc_placeholder'); const placeholderElement = ReactDOM.findDOMNode(placeholder); @@ -255,7 +255,7 @@ describe('GroupView', function() { it('should show a complicated long description after successful /summary', function() { const groupView = ReactTestUtils.findRenderedComponentWithType(root, GroupView); - const prom = waitForUpdate(groupView, 4).then(() => { + const prom = waitForUpdate(groupView).then(() => { const longDesc = ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView_groupDesc'); const longDescElement = ReactDOM.findDOMNode(longDesc); expect(longDescElement).toExist(); @@ -282,7 +282,7 @@ describe('GroupView', function() { it('should disallow images with non-mxc URLs', function() { const groupView = ReactTestUtils.findRenderedComponentWithType(root, GroupView); - const prom = waitForUpdate(groupView, 4).then(() => { + const prom = waitForUpdate(groupView).then(() => { const longDesc = ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView_groupDesc'); const longDescElement = ReactDOM.findDOMNode(longDesc); expect(longDescElement).toExist(); @@ -305,7 +305,7 @@ describe('GroupView', function() { it('should show a RoomDetailList after a successful /summary & /rooms (no rooms returned)', function() { const groupView = ReactTestUtils.findRenderedComponentWithType(root, GroupView); - const prom = waitForUpdate(groupView, 4).then(() => { + const prom = waitForUpdate(groupView).then(() => { const roomDetailList = ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_RoomDetailList'); const roomDetailListElement = ReactDOM.findDOMNode(roomDetailList); expect(roomDetailListElement).toExist(); @@ -322,7 +322,7 @@ describe('GroupView', function() { it('should show a RoomDetailList after a successful /summary & /rooms (with a single room)', function() { const groupView = ReactTestUtils.findRenderedComponentWithType(root, GroupView); - const prom = waitForUpdate(groupView, 4).then(() => { + const prom = waitForUpdate(groupView).then(() => { const roomDetailList = ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_RoomDetailList'); const roomDetailListElement = ReactDOM.findDOMNode(roomDetailList); expect(roomDetailListElement).toExist(); @@ -355,25 +355,4 @@ describe('GroupView', function() { httpBackend.flush(undefined, undefined, 0); return prom; }); - - it('should show a summary even if /users fails', function() { - const groupView = ReactTestUtils.findRenderedComponentWithType(root, GroupView); - - // Only wait for 3 updates in this test since we don't change state for - // the /users error case. - const prom = waitForUpdate(groupView, 3).then(() => { - const shortDesc = ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_GroupView_header_shortDesc'); - const shortDescElement = ReactDOM.findDOMNode(shortDesc); - expect(shortDescElement).toExist(); - expect(shortDescElement.innerText).toBe('This is a community'); - }); - - httpBackend.when('GET', '/groups/' + groupIdEncoded + '/summary').respond(200, summaryResponse); - httpBackend.when('GET', '/groups/' + groupIdEncoded + '/users').respond(500, {}); - httpBackend.when('GET', '/groups/' + groupIdEncoded + '/invited_users').respond(200, { chunk: [] }); - httpBackend.when('GET', '/groups/' + groupIdEncoded + '/rooms').respond(200, { chunk: [] }); - - httpBackend.flush(undefined, undefined, 0); - return prom; - }); }); diff --git a/test/components/views/groups/GroupMemberList-test.js b/test/components/views/groups/GroupMemberList-test.js deleted file mode 100644 index d71d0377d7..0000000000 --- a/test/components/views/groups/GroupMemberList-test.js +++ /dev/null @@ -1,149 +0,0 @@ -/* -Copyright 2018 New Vector Ltd. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React from "react"; -import ReactDOM from "react-dom"; -import ReactTestUtils from "react-dom/test-utils"; -import expect from "expect"; - -import MockHttpBackend from "matrix-mock-request"; -import MatrixClientPeg from "../../../../src/MatrixClientPeg"; -import sdk from "matrix-react-sdk"; -import Matrix from "matrix-js-sdk"; - -import * as TestUtils from "test-utils"; -const { waitForUpdate } = TestUtils; - -const GroupMemberList = sdk.getComponent("views.groups.GroupMemberList"); -const WrappedGroupMemberList = TestUtils.wrapInMatrixClientContext(GroupMemberList); - -describe("GroupMemberList", function() { - let root; - let rootElement; - let httpBackend; - let summaryResponse; - let groupId; - let groupIdEncoded; - - // Summary response fields - const user = { - is_privileged: true, // can edit the group - is_public: true, // appear as a member to non-members - is_publicised: true, // display flair - }; - const usersSection = { - roles: {}, - total_user_count_estimate: 0, - users: [], - }; - const roomsSection = { - categories: {}, - rooms: [], - total_room_count_estimate: 0, - }; - - // Users response fields - const usersResponse = { - chunk: [ - { - user_id: "@test:matrix.org", - displayname: "Test", - avatar_url: "mxc://matrix.org/oUxxDyzQOHdVDMxgwFzyCWEe", - is_public: true, - is_privileged: true, - attestation: {}, - }, - ], - }; - - beforeEach(function() { - TestUtils.beforeEach(this); - - httpBackend = new MockHttpBackend(); - - Matrix.request(httpBackend.requestFn); - - MatrixClientPeg.get = () => Matrix.createClient({ - baseUrl: "https://my.home.server", - userId: "@me:here", - accessToken: "123456789", - }); - - summaryResponse = { - profile: { - avatar_url: "mxc://someavatarurl", - is_openly_joinable: true, - is_public: true, - long_description: "This is a LONG description.", - name: "The name of a community", - short_description: "This is a community", - }, - user, - users_section: usersSection, - rooms_section: roomsSection, - }; - - groupId = "+" + Math.random().toString(16).slice(2) + ":domain"; - groupIdEncoded = encodeURIComponent(groupId); - - rootElement = document.createElement("div"); - root = ReactDOM.render(, rootElement); - }); - - afterEach(function() { - ReactDOM.unmountComponentAtNode(rootElement); - }); - - it("should show group member list after successful /users", function() { - const groupMemberList = ReactTestUtils.findRenderedComponentWithType(root, GroupMemberList); - const prom = waitForUpdate(groupMemberList, 4).then(() => { - ReactTestUtils.findRenderedDOMComponentWithClass(root, "mx_MemberList"); - - const memberList = ReactTestUtils.findRenderedDOMComponentWithClass(root, "mx_MemberList_joined"); - const memberListElement = ReactDOM.findDOMNode(memberList); - expect(memberListElement).toExist(); - expect(memberListElement.innerText).toBe("Test"); - }); - - httpBackend.when("GET", "/groups/" + groupIdEncoded + "/summary").respond(200, summaryResponse); - httpBackend.when("GET", "/groups/" + groupIdEncoded + "/users").respond(200, usersResponse); - httpBackend.when("GET", "/groups/" + groupIdEncoded + "/invited_users").respond(200, { chunk: [] }); - httpBackend.when("GET", "/groups/" + groupIdEncoded + "/rooms").respond(200, { chunk: [] }); - - httpBackend.flush(undefined, undefined, 0); - return prom; - }); - - it("should show error message after failed /users", function() { - const groupMemberList = ReactTestUtils.findRenderedComponentWithType(root, GroupMemberList); - const prom = waitForUpdate(groupMemberList, 4).then(() => { - ReactTestUtils.findRenderedDOMComponentWithClass(root, "mx_MemberList"); - - const memberList = ReactTestUtils.findRenderedDOMComponentWithClass(root, "mx_MemberList_joined"); - const memberListElement = ReactDOM.findDOMNode(memberList); - expect(memberListElement).toExist(); - expect(memberListElement.innerText).toBe("Failed to load group members"); - }); - - httpBackend.when("GET", "/groups/" + groupIdEncoded + "/summary").respond(200, summaryResponse); - httpBackend.when("GET", "/groups/" + groupIdEncoded + "/users").respond(500, {}); - httpBackend.when("GET", "/groups/" + groupIdEncoded + "/invited_users").respond(200, { chunk: [] }); - httpBackend.when("GET", "/groups/" + groupIdEncoded + "/rooms").respond(200, { chunk: [] }); - - httpBackend.flush(undefined, undefined, 0); - return prom; - }); -}); diff --git a/test/matrix-to-test.js b/test/matrix-to-test.js index 6392e326e9..70533575c4 100644 --- a/test/matrix-to-test.js +++ b/test/matrix-to-test.js @@ -150,39 +150,7 @@ describe('matrix-to', function() { expect(pickedServers[2]).toBe("third"); }); - it('should pick a maximum of 3 candidate servers', function() { - peg.get().getRoom = () => { - return { - getJoinedMembers: () => [ - { - userId: "@alice:alpha", - powerLevel: 100, - }, - { - userId: "@alice:bravo", - powerLevel: 0, - }, - { - userId: "@alice:charlie", - powerLevel: 0, - }, - { - userId: "@alice:delta", - powerLevel: 0, - }, - { - userId: "@alice:echo", - powerLevel: 0, - }, - ], - }; - }; - const pickedServers = pickServerCandidates("!somewhere:example.org"); - expect(pickedServers).toExist(); - expect(pickedServers.length).toBe(3); - }); - - it('should not consider IPv4 hosts', function() { + it('should work with IPv4 hostnames', function() { peg.get().getRoom = () => { return { getJoinedMembers: () => [ @@ -195,10 +163,11 @@ describe('matrix-to', function() { }; const pickedServers = pickServerCandidates("!somewhere:example.org"); expect(pickedServers).toExist(); - expect(pickedServers.length).toBe(0); + expect(pickedServers.length).toBe(1); + expect(pickedServers[0]).toBe("127.0.0.1"); }); - it('should not consider IPv6 hosts', function() { + it('should work with IPv6 hostnames', function() { peg.get().getRoom = () => { return { getJoinedMembers: () => [ @@ -211,10 +180,11 @@ describe('matrix-to', function() { }; const pickedServers = pickServerCandidates("!somewhere:example.org"); expect(pickedServers).toExist(); - expect(pickedServers.length).toBe(0); + expect(pickedServers.length).toBe(1); + expect(pickedServers[0]).toBe("[::1]"); }); - it('should not consider IPv4 hostnames with ports', function() { + it('should work with IPv4 hostnames with ports', function() { peg.get().getRoom = () => { return { getJoinedMembers: () => [ @@ -227,10 +197,11 @@ describe('matrix-to', function() { }; const pickedServers = pickServerCandidates("!somewhere:example.org"); expect(pickedServers).toExist(); - expect(pickedServers.length).toBe(0); + expect(pickedServers.length).toBe(1); + expect(pickedServers[0]).toBe("127.0.0.1:8448"); }); - it('should not consider IPv6 hostnames with ports', function() { + it('should work with IPv6 hostnames with ports', function() { peg.get().getRoom = () => { return { getJoinedMembers: () => [ @@ -243,7 +214,8 @@ describe('matrix-to', function() { }; const pickedServers = pickServerCandidates("!somewhere:example.org"); expect(pickedServers).toExist(); - expect(pickedServers.length).toBe(0); + expect(pickedServers.length).toBe(1); + expect(pickedServers[0]).toBe("[::1]:8448"); }); it('should work with hostnames with ports', function() { @@ -263,140 +235,6 @@ describe('matrix-to', function() { expect(pickedServers[0]).toBe("example.org:8448"); }); - it('should not consider servers explicitly denied by ACLs', function() { - peg.get().getRoom = () => { - return { - getJoinedMembers: () => [ - { - userId: "@alice:evilcorp.com", - powerLevel: 100, - }, - { - userId: "@bob:chat.evilcorp.com", - powerLevel: 0, - }, - ], - currentState: { - getStateEvents: (type, key) => { - if (type !== "m.room.server_acl" || key !== "") return null; - return { - getContent: () => { - return { - deny: ["evilcorp.com", "*.evilcorp.com"], - allow: ["*"], - }; - }, - }; - }, - }, - }; - }; - const pickedServers = pickServerCandidates("!somewhere:example.org"); - expect(pickedServers).toExist(); - expect(pickedServers.length).toBe(0); - }); - - it('should not consider servers not allowed by ACLs', function() { - peg.get().getRoom = () => { - return { - getJoinedMembers: () => [ - { - userId: "@alice:evilcorp.com", - powerLevel: 100, - }, - { - userId: "@bob:chat.evilcorp.com", - powerLevel: 0, - }, - ], - currentState: { - getStateEvents: (type, key) => { - if (type !== "m.room.server_acl" || key !== "") return null; - return { - getContent: () => { - return { - deny: [], - allow: [], // implies "ban everyone" - }; - }, - }; - }, - }, - }; - }; - const pickedServers = pickServerCandidates("!somewhere:example.org"); - expect(pickedServers).toExist(); - expect(pickedServers.length).toBe(0); - }); - - it('should consider servers not explicitly banned by ACLs', function() { - peg.get().getRoom = () => { - return { - getJoinedMembers: () => [ - { - userId: "@alice:evilcorp.com", - powerLevel: 100, - }, - { - userId: "@bob:chat.evilcorp.com", - powerLevel: 0, - }, - ], - currentState: { - getStateEvents: (type, key) => { - if (type !== "m.room.server_acl" || key !== "") return null; - return { - getContent: () => { - return { - deny: ["*.evilcorp.com"], // evilcorp.com is still good though - allow: ["*"], - }; - }, - }; - }, - }, - }; - }; - const pickedServers = pickServerCandidates("!somewhere:example.org"); - expect(pickedServers).toExist(); - expect(pickedServers.length).toBe(1); - expect(pickedServers[0]).toEqual("evilcorp.com"); - }); - - it('should consider servers not disallowed by ACLs', function() { - peg.get().getRoom = () => { - return { - getJoinedMembers: () => [ - { - userId: "@alice:evilcorp.com", - powerLevel: 100, - }, - { - userId: "@bob:chat.evilcorp.com", - powerLevel: 0, - }, - ], - currentState: { - getStateEvents: (type, key) => { - if (type !== "m.room.server_acl" || key !== "") return null; - return { - getContent: () => { - return { - deny: [], - allow: ["evilcorp.com"], // implies "ban everyone else" - }; - }, - }; - }, - }, - }; - }; - const pickedServers = pickServerCandidates("!somewhere:example.org"); - expect(pickedServers).toExist(); - expect(pickedServers.length).toBe(1); - expect(pickedServers[0]).toEqual("evilcorp.com"); - }); - it('should generate an event permalink for room IDs with no candidate servers', function() { peg.get().getRoom = () => null; const result = makeEventPermalink("!somewhere:example.org", "$something:example.com"); diff --git a/test/test-utils.js b/test/test-utils.js index d5bcd9397a..bc4d29210e 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -310,26 +310,19 @@ export function wrapInMatrixClientContext(WrappedComponent) { /** * Call fn before calling componentDidUpdate on a react component instance, inst. * @param {React.Component} inst an instance of a React component. - * @param {integer} updates Number of updates to wait for. (Defaults to 1.) * @returns {Promise} promise that resolves when componentDidUpdate is called on * given component instance. */ -export function waitForUpdate(inst, updates = 1) { +export function waitForUpdate(inst) { return new Promise((resolve, reject) => { const cdu = inst.componentDidUpdate; - console.log(`Waiting for ${updates} update(s)`); - inst.componentDidUpdate = (prevProps, prevState, snapshot) => { - updates--; - console.log(`Got update, ${updates} remaining`); - - if (updates == 0) { - inst.componentDidUpdate = cdu; - resolve(); - } + resolve(); if (cdu) cdu(prevProps, prevState, snapshot); + + inst.componentDidUpdate = cdu; }; }); } From 6447a60e1fd1d81eea977ca6ff520869e5f016cc Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 3 Jan 2019 15:44:07 -0700 Subject: [PATCH 213/237] Revert "Merge pull request #2336 from matrix-org/travis/notif-button" This reverts commit 96300b45b7bd594e19b503bb27fdbf9bebf1508e. --- res/css/structures/_RightPanel.scss | 4 --- src/Notifier.js | 5 ---- src/Tinter.js | 22 ++++----------- src/components/structures/RightPanel.js | 29 ++------------------ src/components/views/elements/TintableSvg.js | 17 ++---------- 5 files changed, 11 insertions(+), 66 deletions(-) diff --git a/res/css/structures/_RightPanel.scss b/res/css/structures/_RightPanel.scss index 554aabfcd1..b4dff612ed 100644 --- a/res/css/structures/_RightPanel.scss +++ b/res/css/structures/_RightPanel.scss @@ -55,10 +55,6 @@ limitations under the License. padding-bottom: 3px; } -.mx_RightPanel_headerButton_badgeHighlight .mx_RightPanel_headerButton_badge { - color: $warning-color; -} - .mx_RightPanel_headerButton_highlight { width: 25px; height: 5px; diff --git a/src/Notifier.js b/src/Notifier.js index 8550f3bf95..80e8be1084 100644 --- a/src/Notifier.js +++ b/src/Notifier.js @@ -289,11 +289,6 @@ const Notifier = { const room = MatrixClientPeg.get().getRoom(ev.getRoomId()); const actions = MatrixClientPeg.get().getPushActionsForEvent(ev); if (actions && actions.notify) { - dis.dispatch({ - action: "event_notification", - event: ev, - room: room, - }); if (this.isEnabled()) { this._displayPopupNotification(ev, room); } diff --git a/src/Tinter.js b/src/Tinter.js index 9c2afd4fab..d24a4c3e74 100644 --- a/src/Tinter.js +++ b/src/Tinter.js @@ -390,7 +390,7 @@ class Tinter { // XXX: we could just move this all into TintableSvg, but as it's so similar // to the CSS fixup stuff in Tinter (just that the fixups are stored in TintableSvg) // keeping it here for now. - calcSvgFixups(svgs, forceColors) { + calcSvgFixups(svgs) { // go through manually fixing up SVG colours. // we could do this by stylesheets, but keeping the stylesheets // updated would be a PITA, so just brute-force search for the @@ -418,21 +418,13 @@ class Tinter { const tag = tags[j]; for (let k = 0; k < this.svgAttrs.length; k++) { const attr = this.svgAttrs[k]; - for (let m = 0; m < this.keyHex.length; m++) { // dev note: don't use L please. - // We use a different attribute from the one we're setting - // because we may also be using forceColors. If we were to - // check the keyHex against a forceColors value, it may not - // match and therefore not change when we need it to. - const valAttrName = "mx-val-" + attr; - let attribute = tag.getAttribute(valAttrName); - if (!attribute) attribute = tag.getAttribute(attr); // fall back to the original - if (attribute && (attribute.toUpperCase() === this.keyHex[m] || attribute.toLowerCase() === this.keyRgb[m])) { + for (let l = 0; l < this.keyHex.length; l++) { + if (tag.getAttribute(attr) && + tag.getAttribute(attr).toUpperCase() === this.keyHex[l]) { fixups.push({ node: tag, attr: attr, - refAttr: valAttrName, - index: m, - forceColors: forceColors, + index: l, }); } } @@ -448,9 +440,7 @@ class Tinter { if (DEBUG) console.log("applySvgFixups start for " + fixups); for (let i = 0; i < fixups.length; i++) { const svgFixup = fixups[i]; - const forcedColor = svgFixup.forceColors ? svgFixup.forceColors[svgFixup.index] : null; - svgFixup.node.setAttribute(svgFixup.attr, forcedColor ? forcedColor : this.colors[svgFixup.index]); - svgFixup.node.setAttribute(svgFixup.refAttr, this.colors[svgFixup.index]); + svgFixup.node.setAttribute(svgFixup.attr, this.colors[svgFixup.index]); } if (DEBUG) console.log("applySvgFixups end"); } diff --git a/src/components/structures/RightPanel.js b/src/components/structures/RightPanel.js index e8371697f2..9017447a34 100644 --- a/src/components/structures/RightPanel.js +++ b/src/components/structures/RightPanel.js @@ -30,7 +30,6 @@ import { showGroupInviteDialog, showGroupAddRoomDialog } from '../../GroupAddres import GroupStore from '../../stores/GroupStore'; import { formatCount } from '../../utils/FormattingUtils'; -import MatrixClientPeg from "../../MatrixClientPeg"; class HeaderButton extends React.Component { constructor() { @@ -50,26 +49,17 @@ class HeaderButton extends React.Component { const TintableSvg = sdk.getComponent("elements.TintableSvg"); const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); - // XXX: We really shouldn't be hardcoding colors here, but the way TintableSvg - // works kinda prevents us from using normal CSS tactics. We use $warning-color - // here. - // Note: This array gets passed along to the Tinter's forceColors eventually. - const tintableColors = this.props.badgeHighlight ? ["#ff0064"] : null; - - const classNames = ["mx_RightPanel_headerButton"]; - if (this.props.badgeHighlight) classNames.push("mx_RightPanel_headerButton_badgeHighlight"); - return
        { this.props.badge ? this.props.badge :   }
        - + { this.props.isHighlighted ?
        :
        } ; @@ -86,7 +76,6 @@ HeaderButton.propTypes = { // The badge to display above the icon badge: PropTypes.node, - badgeHighlight: PropTypes.bool, // The parameters to track the click event analytics: PropTypes.arrayOf(PropTypes.string).isRequired, @@ -216,10 +205,7 @@ module.exports = React.createClass({ }, 500), onAction: function(payload) { - if (payload.action === "event_notification") { - // Try and re-caclulate any badge counts we might have - this.forceUpdate(); - } else if (payload.action === "view_user") { + if (payload.action === "view_user") { dis.dispatch({ action: 'show_right_panel', }); @@ -322,14 +308,6 @@ module.exports = React.createClass({ let headerButtons = []; if (this.props.roomId) { - let notifCountBadge; - let notifCount = 0; - MatrixClientPeg.get().getRooms().forEach(r => notifCount += (r.getUnreadNotificationCount('highlight') || 0)); - if (notifCount > 0) { - const title = _t("%(count)s Notifications", {count: formatCount(notifCount)}); - notifCountBadge =
        { formatCount(notifCount) }
        ; - } - headerButtons = [ 0} analytics={['Right Panel', 'Notification List Button', 'click']} />, ]; diff --git a/src/components/views/elements/TintableSvg.js b/src/components/views/elements/TintableSvg.js index 08628c8ca9..e04bf87793 100644 --- a/src/components/views/elements/TintableSvg.js +++ b/src/components/views/elements/TintableSvg.js @@ -29,7 +29,6 @@ var TintableSvg = React.createClass({ width: PropTypes.string.isRequired, height: PropTypes.string.isRequired, className: PropTypes.string, - forceColors: PropTypes.arrayOf(PropTypes.string), }, statics: { @@ -51,12 +50,6 @@ var TintableSvg = React.createClass({ delete TintableSvg.mounts[this.id]; }, - componentDidUpdate: function(prevProps, prevState) { - if (prevProps.forceColors !== this.props.forceColors) { - this.calcAndApplyFixups(this.refs.svgContainer); - } - }, - tint: function() { // TODO: only bother running this if the global tint settings have changed // since we loaded! @@ -64,13 +57,8 @@ var TintableSvg = React.createClass({ }, onLoad: function(event) { - this.calcAndApplyFixups(event.target); - }, - - calcAndApplyFixups: function(target) { - if (!target) return; - // console.log("TintableSvg.calcAndApplyFixups for " + this.props.src); - this.fixups = Tinter.calcSvgFixups([target], this.props.forceColors); + // console.log("TintableSvg.onLoad for " + this.props.src); + this.fixups = Tinter.calcSvgFixups([event.target]); Tinter.applySvgFixups(this.fixups); }, @@ -83,7 +71,6 @@ var TintableSvg = React.createClass({ height={this.props.height} onLoad={this.onLoad} tabIndex="-1" - ref="svgContainer" /> ); }, From 01eacfa26a019bd8918cb92d8dcfa3c82ceb63d4 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 3 Jan 2019 16:02:36 -0700 Subject: [PATCH 214/237] Regenerate en_EN.json --- src/i18n/strings/en_EN.json | 51 ++++++++++--------------------------- 1 file changed, 13 insertions(+), 38 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index f5509cf428..be2f950156 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -215,15 +215,6 @@ "%(names)s and %(count)s others are typing …|other": "%(names)s and %(count)s others are typing …", "%(names)s and %(count)s others are typing …|one": "%(names)s and one other is typing …", "%(names)s and %(lastPerson)s are typing …": "%(names)s and %(lastPerson)s are typing …", - "Failure to create room": "Failure to create room", - "Server may be unavailable, overloaded, or you hit a bug.": "Server may be unavailable, overloaded, or you hit a bug.", - "Send anyway": "Send anyway", - "Send": "Send", - "Unnamed Room": "Unnamed Room", - "%(displayName)s is typing": "%(displayName)s is typing", - "%(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", "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.", @@ -467,6 +458,7 @@ "Close": "Close", "and %(count)s others...|other": "and %(count)s others...", "and %(count)s others...|one": "and one other...", + "Invite to this room": "Invite to this room", "Invited": "Invited", "Filter room members": "Filter room members", "%(userName)s (power %(powerLevelNumber)s)": "%(userName)s (power %(powerLevelNumber)s)", @@ -542,21 +534,17 @@ "Forget room": "Forget room", "Search": "Search", "Share room": "Share room", - "Show panel": "Show panel", "Drop here to favourite": "Drop here to favourite", "Drop here to tag direct chat": "Drop here to tag direct chat", "Drop here to restore": "Drop here to restore", "Drop here to demote": "Drop here to demote", "Drop here to tag %(section)s": "Drop here to tag %(section)s", - "Press to start a chat with someone": "Press to start a chat with someone", - "You're not in any rooms yet! Press to make a room or to browse the directory": "You're not in any rooms yet! Press to make a room or to browse the directory", "Community Invites": "Community Invites", "Invites": "Invites", "Favourites": "Favourites", "People": "People", "Rooms": "Rooms", "Low priority": "Low priority", - "You have no historical rooms": "You have no historical rooms", "Historical": "Historical", "System Alerts": "System Alerts", "Joining room...": "Joining room...", @@ -682,6 +670,9 @@ "In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.": "In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.", "URL Previews": "URL Previews", "When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.": "When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.", + "Members": "Members", + "Files": "Files", + "Notifications": "Notifications", "Sunday": "Sunday", "Monday": "Monday", "Tuesday": "Tuesday", @@ -757,6 +748,7 @@ "Failed to remove user from community": "Failed to remove user from community", "Failed to load group members": "Failed to load group members", "Filter community members": "Filter community members", + "Invite to this community": "Invite to this community", "Flair will appear if enabled in room settings": "Flair will appear if enabled in room settings", "Flair will not appear": "Flair will not appear", "Are you sure you want to remove '%(roomName)s' from %(groupId)s?": "Are you sure you want to remove '%(roomName)s' from %(groupId)s?", @@ -769,6 +761,7 @@ "Visibility in Room List": "Visibility in Room List", "Visible to everyone": "Visible to everyone", "Only visible to community members": "Only visible to community members", + "Add rooms to this community": "Add rooms to this community", "Filter community rooms": "Filter community rooms", "Something went wrong when trying to get your communities.": "Something went wrong when trying to get your communities.", "Display your community flair in rooms configured to show it.": "Display your community flair in rooms configured to show it.", @@ -973,6 +966,11 @@ "Clear cache and resync": "Clear cache and resync", "Riot now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!": "Riot now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!", "Updating Riot": "Updating Riot", + "For security, logging out will delete any end-to-end encryption keys from this browser. If you want to be able to decrypt your conversation history from future Riot sessions, please export your room keys for safe-keeping.": "For security, logging out will delete any end-to-end encryption keys from this browser. If you want to be able to decrypt your conversation history from future Riot sessions, please export your room keys for safe-keeping.", + "Thanks for testing the Riot Redesign. If you run into any bugs or visual issues, please let us know on GitHub.": "Thanks for testing the Riot Redesign. If you run into any bugs or visual issues, please let us know on GitHub.", + "To help avoid duplicate issues, please view existing issues first (and add a +1) or create a new issue if you can't find it.": "To help avoid duplicate issues, please view existing issues first (and add a +1) or create a new issue if you can't find it.", + "Report bugs & give feedback": "Report bugs & give feedback", + "Go back": "Go back", "Failed to upgrade room": "Failed to upgrade room", "The room upgrade could not be completed": "The room upgrade could not be completed", "Upgrade this room to version %(version)s": "Upgrade this room to version %(version)s", @@ -1080,11 +1078,6 @@ "Safari and Opera work too.": "Safari and Opera work too.", "With your current browser, the look and feel of the application may be completely incorrect, and some or all features may not function. If you want to try it anyway you can continue, but you are on your own in terms of any issues you may encounter!": "With your current browser, the look and feel of the application may be completely incorrect, and some or all features may not function. If you want to try it anyway you can continue, but you are on your own in terms of any issues you may encounter!", "I understand the risks and wish to continue": "I understand the risks and wish to continue", - "Name": "Name", - "Topic": "Topic", - "Make this room private": "Make this room private", - "Share message history with new users": "Share message history with new users", - "Encrypt room": "Encrypt room", "You must register to use this functionality": "You must register to use this functionality", "You must join the room to see its files": "You must join the room to see its files", "There are no visible files in this room": "There are no visible files in this room", @@ -1113,7 +1106,6 @@ "Community Settings": "Community Settings", "Changes made to your community name and avatar might not be seen by other users for up to 30 minutes.": "Changes made to your community name and avatar might not be seen by other users for up to 30 minutes.", "These rooms are displayed to community members on the community page. Community members can join the rooms by clicking on them.": "These rooms are displayed to community members on the community page. Community members can join the rooms by clicking on them.", - "Add rooms to this community": "Add rooms to this community", "Featured Rooms:": "Featured Rooms:", "Featured Users:": "Featured Users:", "%(inviter)s has invited you to join this community": "%(inviter)s has invited you to join this community", @@ -1156,16 +1148,6 @@ "Create a new community": "Create a new community", "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.", "You have no visible notifications": "You have no visible notifications", - "Members": "Members", - "%(count)s Members|other": "%(count)s Members", - "%(count)s Members|one": "%(count)s Member", - "Invite to this room": "Invite to this room", - "%(count)s Notifications|other": "%(count)s Notifications", - "%(count)s Notifications|one": "%(count)s Notification", - "Files": "Files", - "Notifications": "Notifications", - "Hide panel": "Hide panel", - "Invite to this community": "Invite to this community", "Failed to get protocol list from Home Server": "Failed to get protocol list from Home Server", "The Home Server may be too old to support third party networks": "The Home Server may be too old to support third party networks", "Failed to get public room list": "Failed to get public room list", @@ -1200,7 +1182,6 @@ "%(count)s new messages|one": "%(count)s new message", "Active call": "Active call", "There's no one else here! Would you like to invite others or stop warning about the empty room?": "There's no one else here! Would you like to invite others or stop warning about the empty room?", - "more": "more", "You seem to be uploading files, are you sure you want to quit?": "You seem to be uploading files, are you sure you want to quit?", "You seem to be in a call, are you sure you want to quit?": "You seem to be in a call, are you sure you want to quit?", "File is too big. Maximum file size is %(fileSize)s": "File is too big. Maximum file size is %(fileSize)s", @@ -1218,8 +1199,6 @@ "Click to mute video": "Click to mute video", "Click to unmute audio": "Click to unmute audio", "Click to mute audio": "Click to mute audio", - "Expand panel": "Expand panel", - "Collapse panel": "Collapse panel", "Filter room names": "Filter room names", "Clear filter": "Clear filter", "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.", @@ -1234,7 +1213,6 @@ "Status.im theme": "Status.im theme", "Can't load user settings": "Can't load user settings", "Server may be unavailable or overloaded": "Server may be unavailable or overloaded", - "For security, logging out will delete any end-to-end encryption keys from this browser. If you want to be able to decrypt your conversation history from future Riot sessions, please export your room keys for safe-keeping.": "For security, logging out will delete any end-to-end encryption keys from this browser. If you want to be able to decrypt your conversation history from future Riot sessions, please export your room keys for safe-keeping.", "Success": "Success", "Your password was successfully changed. You will not receive push notifications on other devices until you log back in to them": "Your password was successfully changed. You will not receive push notifications on other devices until you log back in to them", "Remove Contact Information?": "Remove Contact Information?", @@ -1353,6 +1331,7 @@ "unknown device": "unknown device", "NOT verified": "NOT verified", "verified": "verified", + "Name": "Name", "Verification": "Verification", "Ed25519 fingerprint": "Ed25519 fingerprint", "User ID": "User ID", @@ -1425,9 +1404,5 @@ "Go to Settings": "Go to Settings", "Failed to set direct chat tag": "Failed to set direct chat tag", "Failed to remove tag %(tagName)s from room": "Failed to remove tag %(tagName)s from room", - "Failed to add tag %(tagName)s to room": "Failed to add tag %(tagName)s to room", - "Report bugs & give feedback": "Report bugs & give feedback", - "Thanks for testing the Riot Redesign. If you run into any bugs or visual issues, please let us know on GitHub.": "Thanks for testing the Riot Redesign. If you run into any bugs or visual issues, please let us know on GitHub.", - "To help avoid duplicate issues, please view existing issues first (and add a +1) or create a new issue if you can't find it.": "To help avoid duplicate issues, please view existing issues first (and add a +1) or create a new issue if you can't find it.", - "Go back": "Go back" + "Failed to add tag %(tagName)s to room": "Failed to add tag %(tagName)s to room" } From 8f5e825cd50ee045cd70532d7938bd2cef5d1765 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 20 Dec 2018 17:26:13 -0700 Subject: [PATCH 215/237] Fix browser navigation not working between /home, /login, /register, etc All of the anchors were pointed at `#` which, when clicked, would trigger a hash change in the browser. This change races the change made by the screen handling where the screen handling ends up losing. Because the hash is then tracked as empty rather than `#/login` (for example), the state machine considers future changes as no-ops and doesn't do anything with them. By using `preventDefault` and `stopPropagation` on the anchor click events, we prevent the browser from automatically going to an empty hash, which then means the screen handling isn't racing the browser, and the hash change state machine doesn't no-op. After applying that fix, going between pages worked great unless you were going from /login to /home. This is because the MatrixChat state machine was now out of sync (a `view` of `LOGIN` but a `page` of `HomePage` - an invalid state). All we have to do here is ensure the right view is used when navigating to the homepage. Fixes https://github.com/vector-im/riot-web/issues/4061 Note: the concerns in 4061 about logging out upon entering the view appear to have been solved. Navigating to the login page doesn't obliterate your session, at least in my testing. --- src/components/structures/HomePage.js | 8 ++++++-- src/components/structures/MatrixChat.js | 3 +++ .../structures/login/ForgotPassword.js | 16 ++++++++++++++-- src/components/structures/login/Login.js | 12 ++++++++++-- src/components/structures/login/Registration.js | 8 +++++++- 5 files changed, 40 insertions(+), 7 deletions(-) diff --git a/src/components/structures/HomePage.js b/src/components/structures/HomePage.js index 01aabf6115..8f0c270513 100644 --- a/src/components/structures/HomePage.js +++ b/src/components/structures/HomePage.js @@ -91,11 +91,15 @@ class HomePage extends React.Component { this._unmounted = true; } - onLoginClick() { + onLoginClick(ev) { + ev.preventDefault(); + ev.stopPropagation(); dis.dispatch({ action: 'start_login' }); } - onRegisterClick() { + onRegisterClick(ev) { + ev.preventDefault(); + ev.stopPropagation(); dis.dispatch({ action: 'start_registration' }); } diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index b01174a91c..44689a4d30 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -927,6 +927,9 @@ export default React.createClass({ }, _viewHome: function() { + this.setStateForNewView({ + view: VIEWS.LOGGED_IN, + }); this._setPage(PageTypes.HomePage); this.notifyNewScreen('home'); }, diff --git a/src/components/structures/login/ForgotPassword.js b/src/components/structures/login/ForgotPassword.js index 559136948a..5c0e428339 100644 --- a/src/components/structures/login/ForgotPassword.js +++ b/src/components/structures/login/ForgotPassword.js @@ -162,6 +162,18 @@ module.exports = React.createClass({ this.setState(newState); }, + onLoginClick: function(ev) { + ev.preventDefault(); + ev.stopPropagation(); + this.props.onLoginClick(); + }, + + onRegisterClick: function(ev) { + ev.preventDefault(); + ev.stopPropagation(); + this.props.onRegisterClick(); + }, + showErrorDialog: function(body, title) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createTrackedDialog('Forgot Password Error', '', ErrorDialog, { @@ -253,10 +265,10 @@ module.exports = React.createClass({ { serverConfigSection } { errorText } - + { _t('Return to login screen') } - + { _t('Create an account') } diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index b94a1759cf..11bd3580e5 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -214,7 +214,9 @@ module.exports = React.createClass({ }).done(); }, - _onLoginAsGuestClick: function() { + _onLoginAsGuestClick: function(ev) { + ev.preventDefault(); + const self = this; self.setState({ busy: true, @@ -297,6 +299,12 @@ module.exports = React.createClass({ }); }, + onRegisterClick: function(ev) { + ev.preventDefault(); + ev.stopPropagation(); + this.props.onRegisterClick(); + }, + _tryWellKnownDiscovery: async function(serverName) { if (!serverName.trim()) { // Nothing to discover @@ -567,7 +575,7 @@ module.exports = React.createClass({ { errorTextSection } { this.componentForStep(this.state.currentFlow) } { serverConfig } - + { _t('Create an account') } { loginAsGuestJsx } diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index ad3ea5f19c..fa5a02e881 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -363,6 +363,12 @@ module.exports = React.createClass({ } }, + onLoginClick: function(ev) { + ev.preventDefault(); + ev.stopPropagation(); + this.props.onLoginClick(); + }, + _makeRegisterRequest: function(auth) { // Only send the bind params if we're sending username / pw params // (Since we need to send no params at all to use the ones saved in the @@ -468,7 +474,7 @@ module.exports = React.createClass({ let signIn; if (!this.state.doingUIAuth) { signIn = ( - + { theme === 'status' ? _t('Sign in') : _t('I already have an account') } ); From 2e5b3ec0f1bc55b36379c9416fe3c04cffb17425 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 2 Jan 2019 13:07:10 -0700 Subject: [PATCH 216/237] Use the safer way to set the logged in view state --- src/components/structures/MatrixChat.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 44689a4d30..38f0243444 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1186,10 +1186,7 @@ export default React.createClass({ * @param {string} teamToken */ _onLoggedIn: async function(teamToken) { - this.setState({ - view: VIEWS.LOGGED_IN, - }); - + this.setStateForNewView({view: VIEWS.LOGGED_IN}); if (teamToken) { // A team member has logged in, not a guest this._teamToken = teamToken; From d8f6a7c684c0e1fd68d02ddc38ccd902387670f9 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 2 Jan 2019 13:07:40 -0700 Subject: [PATCH 217/237] Add missing stopPropagation --- src/components/structures/login/Login.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index 11bd3580e5..321084389b 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -216,6 +216,7 @@ module.exports = React.createClass({ _onLoginAsGuestClick: function(ev) { ev.preventDefault(); + ev.stopPropagation(); const self = this; self.setState({ From 643dd5ca471e3e9bc1db07aae50402aa08493d66 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 3 Jan 2019 14:22:16 -0700 Subject: [PATCH 218/237] Add a comment explaining the reason for setting the LOGGED_IN view --- src/components/structures/MatrixChat.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 38f0243444..a03265da1c 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -927,6 +927,7 @@ export default React.createClass({ }, _viewHome: function() { + // The home page requires the "logged in" view, so we'll set that. this.setStateForNewView({ view: VIEWS.LOGGED_IN, }); From 1481dcf9d7ce609200a8d8ff47b0b01dc6bdca19 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 3 Jan 2019 13:57:20 -0700 Subject: [PATCH 219/237] Don't re-sort the room list if the user is hovering over it Fixes vector-im/riot-web#5624 --- src/components/views/rooms/RoomList.js | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index c1cd7e5d57..dbfe95dadf 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -86,6 +86,7 @@ module.exports = React.createClass({ incomingCallTag: null, incomingCall: null, selectedTags: [], + hover: false, }; }, @@ -294,6 +295,17 @@ module.exports = React.createClass({ this.forceUpdate(); }, + onMouseEnter: function(ev) { + this.setState({hover: true}); + }, + + onMouseLeave: function(ev) { + this.setState({hover: false}); + + // Refresh the room list just in case the user missed something. + this._delayedRefreshRoomList(); + }, + _delayedRefreshRoomList: new rate_limited_func(function() { this.refreshRoomList(); }, 500), @@ -346,6 +358,11 @@ module.exports = React.createClass({ }, refreshRoomList: function() { + if (this.state.hover) { + // Don't re-sort the list if we're hovering over the list + return; + } + // TODO: ideally we'd calculate this once at start, and then maintain // any changes to it incrementally, updating the appropriate sublists // as needed. @@ -693,7 +710,8 @@ module.exports = React.createClass({ const subListComponents = this._mapSubListProps(subLists); return ( -
        +
        { subListComponents }
        ); From ff2faf7996af9790d6c22a73719367596698e206 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 3 Jan 2019 16:32:52 -0700 Subject: [PATCH 220/237] Add missing CSS to dharma theme --- res/themes/dharma/css/_dharma.scss | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/res/themes/dharma/css/_dharma.scss b/res/themes/dharma/css/_dharma.scss index 0851762be2..08a287ad71 100644 --- a/res/themes/dharma/css/_dharma.scss +++ b/res/themes/dharma/css/_dharma.scss @@ -186,6 +186,8 @@ $lightbox-border-color: #ffffff; // unused? $progressbar-color: #000; +$room-warning-bg-color: #fff8e3; + /*** form elements ***/ // .mx_textinput is a container for a text input @@ -320,3 +322,11 @@ input[type=search]::-webkit-search-results-decoration { font-size: 15px; padding: 0px 1.5em 0px 1.5em; } + +@define-mixin mx_DialogButton_secondary { + // flip colours for the secondary ones + font-weight: 600; + border: 1px solid $accent-color ! important; + color: $accent-color; + background-color: $accent-fg-color; +} From c89b6e1e27c0b4fc3c597864bc695dfac8a5af4f Mon Sep 17 00:00:00 2001 From: Christopher Medlin Date: Thu, 3 Jan 2019 15:42:17 -0800 Subject: [PATCH 221/237] Add slash command for changing room name Signed-off-by: Christopher Medlin --- src/SlashCommands.js | 12 ++++++++++++ src/i18n/strings/en_EN.json | 1 + src/i18n/strings/en_US.json | 1 + 3 files changed, 14 insertions(+) diff --git a/src/SlashCommands.js b/src/SlashCommands.js index 24328d6372..f4a9f9d5ff 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -135,6 +135,18 @@ export const CommandMap = { }, }), + name: new Command({ + name: 'name', + args: '', + description: _td('Sets the room name'), + runFn: function(roomId, args) { + if (args) { + return success(MatrixClientPeg.get().setRoomName(roomId, args)); + } + return reject(this.getUsage()); + }, + }), + invite: new Command({ name: 'invite', args: '', diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index ef659bf566..1271934b63 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -133,6 +133,7 @@ "Changes your display nickname": "Changes your display nickname", "Changes colour scheme of current room": "Changes colour scheme of current room", "Sets the room topic": "Sets the room topic", + "Sets the room name": "Sets the room name", "Invites user with given id to current room": "Invites user with given id to current room", "Joins room with given alias": "Joins room with given alias", "Leave room": "Leave room", diff --git a/src/i18n/strings/en_US.json b/src/i18n/strings/en_US.json index b96a49eac7..5e09f1d860 100644 --- a/src/i18n/strings/en_US.json +++ b/src/i18n/strings/en_US.json @@ -677,6 +677,7 @@ "Integrations Error": "Integrations Error", "NOTE: Apps are not end-to-end encrypted": "NOTE: Apps are not end-to-end encrypted", "Sets the room topic": "Sets the room topic", + "Sets the room name": "Sets the room name", "The maximum permitted number of widgets have already been added to this room.": "The maximum permitted number of widgets have already been added to this room.", "To get started, please pick a username!": "To get started, please pick a username!", "Unable to create widget.": "Unable to create widget.", From 920911c0a1f1cf9f41f130311a372b455518f510 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 3 Jan 2019 18:36:57 -0600 Subject: [PATCH 222/237] Room list fixes for custom status --- res/css/views/rooms/_RoomTile.scss | 11 +++++++++-- src/components/views/rooms/RoomTile.js | 6 ++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss index ab33f4e2d4..99a904cebb 100644 --- a/res/css/views/rooms/_RoomTile.scss +++ b/res/css/views/rooms/_RoomTile.scss @@ -50,11 +50,18 @@ limitations under the License. .mx_RoomTile_nameContainer { - display: inline-block; + display: flex; + align-items: center; width: 180px; vertical-align: middle; } +.mx_RoomTile_labelContainer { + display: flex; + flex-direction: column; + flex: 1; +} + .mx_RoomTile_avatar { flex: 0; padding: 4px; @@ -103,7 +110,7 @@ limitations under the License. flex: 1 5 auto; font-size: 14px; font-weight: 600; - padding: 6px; + padding: 0 6px; color: $roomtile-name-color; white-space: nowrap; overflow-x: hidden; diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index ae83c70fcd..bce4d15f16 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -354,8 +354,10 @@ module.exports = React.createClass({
        - { label } - { subtextLabel } +
        + { label } + { subtextLabel } +
        { contextMenuButton } { badge }
        From 51dc1fe9bf98095136b33ad3623319daa84ccb95 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 3 Jan 2019 18:05:32 -0700 Subject: [PATCH 223/237] Fix badges on room tiles not being right aligned Thanks to Ryan for fixing this! Problem: https://t2l.io/_matrix/media/v1/download/t2l.io/da6fccb60969da1c455630faae990881 --- res/css/views/rooms/_RoomTile.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss index 99a904cebb..c74cec25e3 100644 --- a/res/css/views/rooms/_RoomTile.scss +++ b/res/css/views/rooms/_RoomTile.scss @@ -52,7 +52,7 @@ limitations under the License. .mx_RoomTile_nameContainer { display: flex; align-items: center; - width: 180px; + flex: 1; vertical-align: middle; } From 852d6df580e010e3981dc1ee0f29f860e689d3dc Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 3 Jan 2019 18:06:37 -0700 Subject: [PATCH 224/237] Fix composer avatar being an oval when a custom status is set Fixes this: https://t2l.io/_matrix/media/v1/download/t2l.io/0d5c7fa9a4b5bf226a020def8480a887 --- res/css/views/avatars/_MemberStatusMessageAvatar.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/res/css/views/avatars/_MemberStatusMessageAvatar.scss b/res/css/views/avatars/_MemberStatusMessageAvatar.scss index c857b9807b..29cae9df34 100644 --- a/res/css/views/avatars/_MemberStatusMessageAvatar.scss +++ b/res/css/views/avatars/_MemberStatusMessageAvatar.scss @@ -17,4 +17,5 @@ limitations under the License. .mx_MemberStatusMessageAvatar_hasStatus { border: 2px solid $accent-color; border-radius: 40px; + padding-right: 0 !important; /* Override AccessibleButton styling */ } From 93ea9d3617ae2273b19805b048d47718ef9932ba Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 3 Jan 2019 18:07:34 -0700 Subject: [PATCH 225/237] Condense .mx_RoomTile_avatar properties Looks like the develop->experimental merge duplicated the definition. We'll take the best of both and make it work. --- res/css/views/rooms/_RoomTile.scss | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss index c74cec25e3..70d505e4ea 100644 --- a/res/css/views/rooms/_RoomTile.scss +++ b/res/css/views/rooms/_RoomTile.scss @@ -62,11 +62,6 @@ limitations under the License. flex: 1; } -.mx_RoomTile_avatar { - flex: 0; - padding: 4px; -} - .mx_RoomTile_subtext { display: inline-block; font-size: 11px; @@ -84,11 +79,8 @@ limitations under the License. } .mx_RoomTile_avatar { - display: inline-block; - padding-top: 5px; - padding-bottom: 5px; - padding-left: 16px; - padding-right: 6px; + flex: 0; + padding: 4px; width: 24px; vertical-align: middle; } From 77dfba8a225b9d74e1a366373a3d61af07d69b41 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 3 Jan 2019 21:55:52 -0700 Subject: [PATCH 226/237] Add unit tests for MemberList Particularly the ordering of the tiles. --- src/components/views/rooms/MemberList.js | 8 +- .../components/views/rooms/MemberList-test.js | 295 ++++++++++++++++++ 2 files changed, 301 insertions(+), 2 deletions(-) create mode 100644 test/components/views/rooms/MemberList-test.js diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js index dc97a3689c..52201b31be 100644 --- a/src/components/views/rooms/MemberList.js +++ b/src/components/views/rooms/MemberList.js @@ -185,6 +185,10 @@ module.exports = React.createClass({ }, _updateList: new rate_limited_func(function() { + this._updateListNow(); + }, 500), + + _updateListNow: function() { // console.log("Updating memberlist"); const newState = { loading: false, @@ -193,7 +197,7 @@ module.exports = React.createClass({ newState.filteredJoinedMembers = this._filterMembers(newState.members, 'join', this.state.searchQuery); newState.filteredInvitedMembers = this._filterMembers(newState.members, 'invite', this.state.searchQuery); this.setState(newState); - }, 500), + }, getMembersWithUser: function() { if (!this.props.roomId) return []; @@ -272,7 +276,7 @@ module.exports = React.createClass({ return "(null)"; } else { const u = member.user; - return "(" + member.name + ", " + member.powerLevel + ", " + (u ? u.lastActiveAgo : "") + ", " + (u ? u.getLastActiveTs() : "") + ", " + (u ? u.currentlyActive : "") + ")"; + return "(" + member.name + ", " + member.powerLevel + ", " + (u ? u.lastActiveAgo : "") + ", " + (u ? u.getLastActiveTs() : "") + ", " + (u ? u.currentlyActive : "") + ", " + (u ? u.presence : "") + ")"; } }, diff --git a/test/components/views/rooms/MemberList-test.js b/test/components/views/rooms/MemberList-test.js new file mode 100644 index 0000000000..01d351c688 --- /dev/null +++ b/test/components/views/rooms/MemberList-test.js @@ -0,0 +1,295 @@ +import React from 'react'; +import ReactTestUtils from 'react-addons-test-utils'; +import ReactDOM from 'react-dom'; +import expect from 'expect'; +import lolex from 'lolex'; + +import * as TestUtils from 'test-utils'; + +import sdk from '../../../../src/index'; +import MatrixClientPeg from '../../../../src/MatrixClientPeg'; + +import {MatrixClient, Room, RoomMember, User} from 'matrix-js-sdk'; + +function generateRoomId() { + return '!' + Math.random().toString().slice(2, 10) + ':domain'; +} + + +describe('MemberList', () => { + function createRoom(opts) { + const room = new Room(generateRoomId(), null, client.getUserId()); + if (opts) { + Object.assign(room, opts); + } + return room; + } + + let parentDiv = null; + let sandbox = null; + let client = null; + let root = null; + let clock = null; + let memberListRoom; + let memberList = null; + + let adminUsers = []; + let moderatorUsers = []; + let defaultUsers = []; + + beforeEach(function () { + TestUtils.beforeEach(this); + sandbox = TestUtils.stubClient(sandbox); + client = MatrixClientPeg.get(); + client.hasLazyLoadMembersEnabled = () => false; + + clock = lolex.install(); + + parentDiv = document.createElement('div'); + document.body.appendChild(parentDiv); + + // Make room + memberListRoom = createRoom(); + expect(memberListRoom.roomId).toBeTruthy(); + + // Make users + const usersPerLevel = 2; + for (let i = 0; i < usersPerLevel; i++) { + const adminUser = new RoomMember(memberListRoom.roomId, `@admin${i}:localhost`); + adminUser.membership = "join"; + adminUser.powerLevel = 100; + adminUser.user = new User(adminUser.userId); + adminUser.user.currentlyActive = true; + adminUser.user.presence = 'online'; + adminUser.user.lastPresenceTs = 1000; + adminUser.user.lastActiveAgo = 10; + adminUsers.push(adminUser); + + const moderatorUser = new RoomMember(memberListRoom.roomId, `@moderator${i}:localhost`); + moderatorUser.membership = "join"; + moderatorUser.powerLevel = 50; + moderatorUser.user = new User(moderatorUser.userId); + moderatorUser.user.currentlyActive = true; + moderatorUser.user.presence = 'online'; + moderatorUser.user.lastPresenceTs = 1000; + moderatorUser.user.lastActiveAgo = 10; + moderatorUsers.push(moderatorUser); + + const defaultUser = new RoomMember(memberListRoom.roomId, `@default${i}:localhost`); + defaultUser.membership = "join"; + defaultUser.powerLevel = 0; + defaultUser.user = new User(defaultUser.userId); + defaultUser.user.currentlyActive = true; + defaultUser.user.presence = 'online'; + defaultUser.user.lastPresenceTs = 1000; + defaultUser.user.lastActiveAgo = 10; + defaultUsers.push(defaultUser); + } + + client.getRoom = (roomId) => { + if (roomId === memberListRoom.roomId) return memberListRoom; + else return null; + }; + memberListRoom.currentState = { + members: {}, + getStateEvents: () => [], // ignore 3pid invites + }; + for (const member of [...adminUsers, ...moderatorUsers, ...defaultUsers]) { + memberListRoom.currentState.members[member.userId] = member; + } + + const MemberList = sdk.getComponent('views.rooms.MemberList'); + const WrappedMemberList = TestUtils.wrapInMatrixClientContext(MemberList); + const gatherWrappedRef = (r) => { + memberList = r; + }; + root = ReactDOM.render(, parentDiv); + }); + + afterEach((done) => { + if (parentDiv) { + ReactDOM.unmountComponentAtNode(parentDiv); + parentDiv.remove(); + parentDiv = null; + } + sandbox.restore(); + + clock.uninstall(); + + done(); + }); + + function expectOrderedByPresenceAndPowerLevel(memberTiles, isPresenceEnabled) { + let prevMember = null; + for (const tile of memberTiles) { + const memberA = prevMember; + const memberB = tile.props.member; + prevMember = memberB; // just in case an expect fails, set this early + if (!memberA) { + continue; + } + + console.log("COMPARING A VS B:"); + console.log(memberList.memberString(memberA)); + console.log(memberList.memberString(memberB)); + + const userA = memberA.user; + const userB = memberB.user; + + let groupChange = false; + + if (isPresenceEnabled) { + const convertPresence = (p) => p === 'unavailable' ? 'online' : p; + const presenceIndex = p => { + const order = ['active', 'online', 'offline']; + const idx = order.indexOf(convertPresence(p)); + return idx === -1 ? order.length : idx; // unknown states at the end + }; + + const idxA = presenceIndex(userA.currentlyActive ? 'active' : userA.presence); + const idxB = presenceIndex(userB.currentlyActive ? 'active' : userB.presence); + console.log("Comparing presence groups..."); + expect(idxB).toBeGreaterThanOrEqual(idxA); + groupChange = idxA !== idxB; + } else { + console.log("Skipped presence groups"); + } + + if (!groupChange) { + console.log("Comparing power levels..."); + expect(memberA.powerLevel).toBeGreaterThanOrEqual(memberB.powerLevel); + groupChange = memberA.powerLevel !== memberB.powerLevel; + } else { + console.log("Skipping power level check due to group change"); + } + + if (!groupChange) { + if (isPresenceEnabled) { + console.log("Comparing last active timestamp..."); + expect(userB.getLastActiveTs()).toBeGreaterThanOrEqual(userA.getLastActiveTs()); + groupChange = userA.getLastActiveTs() !== userB.getLastActiveTs(); + } else { + console.log("Skipping last active timestamp"); + } + } else { + console.log("Skipping last active timestamp check due to group change"); + } + + if (!groupChange) { + const nameA = memberA.name[0] === '@' ? memberA.name.substr(1) : memberA.name; + const nameB = memberB.name[0] === '@' ? memberB.name.substr(1) : memberB.name; + const nameCompare = nameB.localeCompare(nameA); + console.log("Comparing name"); + expect(nameCompare).toBeGreaterThanOrEqual(0); + } else { + console.log("Skipping name check due to group change"); + } + } + } + + function itDoesOrderMembersCorrectly(enablePresence) { + const MemberTile = sdk.getComponent("rooms.MemberTile"); + describe('does order members correctly', () => { + // Note: even if presence is disabled, we still expect that the presence + // tests will pass. All expectOrderedByPresenceAndPowerLevel does is ensure + // the order is perceived correctly, regardless of what we did to the members. + + // Each of the 4 tests here is done to prove that the member list can meet + // all 4 criteria independently. Together, they should work. + + it('by presence state', () => { + // Intentionally pick users that will confuse the power level sorting + const activeUsers = [defaultUsers[0]]; + const onlineUsers = [adminUsers[0]]; + const offlineUsers = [...moderatorUsers, ...adminUsers.slice(1), ...defaultUsers.slice(1)]; + activeUsers.forEach((u) => { + u.user.currentlyActive = true; + u.user.presence = 'online'; + }); + onlineUsers.forEach((u) => { + u.user.currentlyActive = false; + u.user.presence = 'online'; + }); + offlineUsers.forEach((u) => { + u.user.currentlyActive = false; + u.user.presence = 'offline'; + }); + + // Bypass all the event listeners and skip to the good part + memberList._showPresence = enablePresence; + memberList._updateListNow(); + + const tiles = ReactTestUtils.scryRenderedComponentsWithType(root, MemberTile); + expectOrderedByPresenceAndPowerLevel(tiles, enablePresence); + }); + + it('by power level', () => { + // We already have admin, moderator, and default users so leave them alone + + // Bypass all the event listeners and skip to the good part + memberList._showPresence = enablePresence; + memberList._updateListNow(); + + const tiles = ReactTestUtils.scryRenderedComponentsWithType(root, MemberTile); + expectOrderedByPresenceAndPowerLevel(tiles, enablePresence); + }); + + it('by last active timestamp', () => { + // Intentionally pick users that will confuse the power level sorting + // lastActiveAgoTs == lastPresenceTs - lastActiveAgo + const activeUsers = [defaultUsers[0]]; + const semiActiveUsers = [adminUsers[0]]; + const inactiveUsers = [...moderatorUsers, ...adminUsers.slice(1), ...defaultUsers.slice(1)]; + activeUsers.forEach((u) => { + u.user.lastPresenceTs = 1000; + u.user.lastActiveAgo = 0; + }); + semiActiveUsers.forEach((u) => { + u.user.lastPresenceTs = 1000; + u.user.lastActiveAgo = 50; + }); + inactiveUsers.forEach((u) => { + u.user.lastPresenceTs = 1000; + u.user.lastActiveAgo = 100; + }); + + // Bypass all the event listeners and skip to the good part + memberList._showPresence = enablePresence; + memberList._updateListNow(); + + const tiles = ReactTestUtils.scryRenderedComponentsWithType(root, MemberTile); + expectOrderedByPresenceAndPowerLevel(tiles, enablePresence); + }); + + it('by name', () => { + // Intentionally put everyone on the same level to force a name comparison + const allUsers = [...adminUsers, ...moderatorUsers, ...defaultUsers]; + allUsers.forEach((u) => { + u.user.currentlyActive = true; + u.user.presence = "online"; + u.user.lastPresenceTs = 1000; + u.user.lastActiveAgo = 0; + u.powerLevel = 100; + }); + + // Bypass all the event listeners and skip to the good part + memberList._showPresence = enablePresence; + memberList._updateListNow(); + + const tiles = ReactTestUtils.scryRenderedComponentsWithType(root, MemberTile); + expectOrderedByPresenceAndPowerLevel(tiles, enablePresence); + }); + }); + } + + describe('when presence is enabled', () => { + itDoesOrderMembersCorrectly(true); + }); + + describe('when presence is not enabled', () => { + itDoesOrderMembersCorrectly(false); + }); +}); + + From 5a2113e1a9458cbbb346660f54725baeab70f4bd Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 3 Jan 2019 21:56:10 -0700 Subject: [PATCH 227/237] Fix a comparison bug in ordering the MemberList --- src/components/views/rooms/MemberList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js index 52201b31be..0924a5ec38 100644 --- a/src/components/views/rooms/MemberList.js +++ b/src/components/views/rooms/MemberList.js @@ -327,7 +327,7 @@ module.exports = React.createClass({ } // Third by last active - if (this._showPresence && userA.getLastActiveTs() !== userB.getLastActiveTs) { + if (this._showPresence && userA.getLastActiveTs() !== userB.getLastActiveTs()) { // console.log("Comparing on last active timestamp - returning"); return userB.getLastActiveTs() - userA.getLastActiveTs(); } From 34d5870a03e3149c77bedfa6bb7567f6ea4cea63 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 3 Jan 2019 22:07:09 -0700 Subject: [PATCH 228/237] Appease the linter --- test/components/views/rooms/MemberList-test.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/test/components/views/rooms/MemberList-test.js b/test/components/views/rooms/MemberList-test.js index 01d351c688..57adde1717 100644 --- a/test/components/views/rooms/MemberList-test.js +++ b/test/components/views/rooms/MemberList-test.js @@ -9,7 +9,7 @@ import * as TestUtils from 'test-utils'; import sdk from '../../../../src/index'; import MatrixClientPeg from '../../../../src/MatrixClientPeg'; -import {MatrixClient, Room, RoomMember, User} from 'matrix-js-sdk'; +import {Room, RoomMember, User} from 'matrix-js-sdk'; function generateRoomId() { return '!' + Math.random().toString().slice(2, 10) + ':domain'; @@ -37,7 +37,7 @@ describe('MemberList', () => { let moderatorUsers = []; let defaultUsers = []; - beforeEach(function () { + beforeEach(function (){ TestUtils.beforeEach(this); sandbox = TestUtils.stubClient(sandbox); client = MatrixClientPeg.get(); @@ -53,6 +53,9 @@ describe('MemberList', () => { expect(memberListRoom.roomId).toBeTruthy(); // Make users + adminUsers = []; + moderatorUsers = []; + defaultUsers = []; const usersPerLevel = 2; for (let i = 0; i < usersPerLevel; i++) { const adminUser = new RoomMember(memberListRoom.roomId, `@admin${i}:localhost`); @@ -104,7 +107,7 @@ describe('MemberList', () => { memberList = r; }; root = ReactDOM.render(, parentDiv); + wrappedRef={gatherWrappedRef} />, parentDiv); }); afterEach((done) => { From f59625f7bdfaef32a9cf740edc97d2532b362e74 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 3 Jan 2019 22:24:24 -0700 Subject: [PATCH 229/237] Fix last active test Time is backwards from all the other tests: larger is older, so we want LessThanOrEqual. Also ensure all the power levels are the same to prevent the sort algorithm from running a PL ordering. --- test/components/views/rooms/MemberList-test.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/components/views/rooms/MemberList-test.js b/test/components/views/rooms/MemberList-test.js index 57adde1717..ae54ed42ea 100644 --- a/test/components/views/rooms/MemberList-test.js +++ b/test/components/views/rooms/MemberList-test.js @@ -170,7 +170,7 @@ describe('MemberList', () => { if (!groupChange) { if (isPresenceEnabled) { console.log("Comparing last active timestamp..."); - expect(userB.getLastActiveTs()).toBeGreaterThanOrEqual(userA.getLastActiveTs()); + expect(userB.getLastActiveTs()).toBeLessThanOrEqual(userA.getLastActiveTs()); groupChange = userA.getLastActiveTs() !== userB.getLastActiveTs(); } else { console.log("Skipping last active timestamp"); @@ -245,14 +245,17 @@ describe('MemberList', () => { const semiActiveUsers = [adminUsers[0]]; const inactiveUsers = [...moderatorUsers, ...adminUsers.slice(1), ...defaultUsers.slice(1)]; activeUsers.forEach((u) => { + u.powerLevel = 100; // set everyone to the same PL to avoid running that check u.user.lastPresenceTs = 1000; u.user.lastActiveAgo = 0; }); semiActiveUsers.forEach((u) => { + u.powerLevel = 100; u.user.lastPresenceTs = 1000; u.user.lastActiveAgo = 50; }); inactiveUsers.forEach((u) => { + u.powerLevel = 100; u.user.lastPresenceTs = 1000; u.user.lastActiveAgo = 100; }); From 0ebde5266ebbe63e3a1b74123e9388872cf66cf7 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 3 Jan 2019 22:33:01 -0700 Subject: [PATCH 230/237] Appease the linter again --- test/components/views/rooms/MemberList-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/components/views/rooms/MemberList-test.js b/test/components/views/rooms/MemberList-test.js index ae54ed42ea..b9d96635a2 100644 --- a/test/components/views/rooms/MemberList-test.js +++ b/test/components/views/rooms/MemberList-test.js @@ -37,7 +37,7 @@ describe('MemberList', () => { let moderatorUsers = []; let defaultUsers = []; - beforeEach(function (){ + beforeEach(function() { TestUtils.beforeEach(this); sandbox = TestUtils.stubClient(sandbox); client = MatrixClientPeg.get(); From e876edcbc398b2518a26d4fe201ff9943830cef7 Mon Sep 17 00:00:00 2001 From: Jason Robinson Date: Fri, 4 Jan 2019 10:51:39 +0200 Subject: [PATCH 231/237] Clarify readme instructions for developers Setting up the SDK for development, I noticed that 3 tests failed on develop branch. Linking the JS SDK made the tests pass. It looks like the assumption is that developers link against the develop branch of JS SDK to develop the React SDK. Clarify this in the readme for completeness sake. Signed-off-by: Jason Robinson --- README.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/README.md b/README.md index ec95fbd132..8852fc9bf0 100644 --- a/README.md +++ b/README.md @@ -127,3 +127,36 @@ Github Issues All issues should be filed under https://github.com/vector-im/riot-web/issues for now. + +Development +=========== + +Ensure you have the latest stable Node JS runtime installed. Then check out +the code and pull in dependencies: + +```bash +git clone https://github.com/matrix-org/matrix-react-sdk.git +cd matrix-react-sdk +git checkout develop +npm install +``` + +`matrix-react-sdk` depends on `matrix-js-sdk`. To make use of changes in the +latter and to ensure tests run against the develop branch of `matrix-js-sdk`, +you should run the following which will sync changes from the JS sdk here. + +```bash +npm link ../matrix-js-sdk +``` + +Command assumes a checked out and installed `matrix-js-sdk` folder in parent +folder. + +Running tests +============= + +Ensure you've followed the above development instructions and then: + +```bash +npm run test +``` From e102000e583fbaa63a4e44c3f8e414f5309e2638 Mon Sep 17 00:00:00 2001 From: Christopher Medlin Date: Fri, 4 Jan 2019 11:29:51 -0800 Subject: [PATCH 232/237] Change to 'roomname' instead of 'name'. --- src/SlashCommands.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/SlashCommands.js b/src/SlashCommands.js index f4a9f9d5ff..47c6c26b45 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -135,8 +135,8 @@ export const CommandMap = { }, }), - name: new Command({ - name: 'name', + roomname: new Command({ + name: 'roomname', args: '', description: _td('Sets the room name'), runFn: function(roomId, args) { From 97747640bb000881b6922ae33017bdba9f154743 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 4 Jan 2019 09:51:20 -0600 Subject: [PATCH 233/237] Record keys button styling tweak --- res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss b/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss index 424ffbd0a8..25077f8005 100644 --- a/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss +++ b/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss @@ -79,3 +79,8 @@ limitations under the License. display: flex; align-items: center; } + +.mx_CreateKeyBackupDialog_recoveryKeyButtons button { + flex: 1; + white-space: nowrap; +} From 2769e681698ef96b067ceaeca0c5ca31b6543f62 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 4 Jan 2019 10:03:14 -0600 Subject: [PATCH 234/237] Give the create key backup title more space --- res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss b/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss index 25077f8005..615b61f842 100644 --- a/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss +++ b/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss @@ -18,6 +18,11 @@ limitations under the License. padding-right: 40px; } +.mx_CreateKeyBackupDialog .mx_Dialog_title { + /* TODO: Consider setting this for all dialog titles. */ + margin-bottom: 1em; +} + .mx_CreateKeyBackupDialog_primaryContainer { /*FIXME: plinth colour in new theme(s). background-color: $accent-color;*/ padding: 20px From 5419b4279b1cc3eaae16cd6e7aaab1822f4da412 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 4 Jan 2019 11:16:06 -0600 Subject: [PATCH 235/237] Simplify create key backup maze The download / copy actions to store the new recovery key now send you forward (the most likely case) with a Back button in case you wanted to also do the other storing type. --- .../keybackup/CreateKeyBackupDialog.js | 19 ++++++------------- src/i18n/strings/en_EN.json | 1 - 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js index a097e84cdb..c593a9b3ea 100644 --- a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js +++ b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js @@ -180,7 +180,7 @@ export default React.createClass({ }); }, - _onKeepItSafeGotItClick: function() { + _onKeepItSafeBackClick: function() { this.setState({ phase: PHASE_SHOWKEY, }); @@ -342,8 +342,6 @@ export default React.createClass({ }, _renderPhaseShowKey: function() { - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); - let bodyText; if (this.state.setPassPhrase) { bodyText = _t("As a safety net, you can use it to restore your encrypted message history if you forget your Recovery Passphrase."); @@ -372,12 +370,6 @@ export default React.createClass({

        -
        -
        ; }, @@ -402,10 +394,11 @@ export default React.createClass({
      • {_t("Save it on a USB key or backup drive", {}, {b: s => {s}})}
      • {_t("Copy it to your personal cloud storage", {}, {b: s => {s}})}
      - + + +
    ; }, diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 8b05f5c9ce..85cb8c9868 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1382,7 +1382,6 @@ "Print it and store it somewhere safe": "Print it and store it somewhere safe", "Save it on a USB key or backup drive": "Save it on a USB key or backup drive", "Copy it to your personal cloud storage": "Copy it to your personal cloud storage", - "Got it": "Got it", "Backup created": "Backup created", "Your encryption keys are now being backed up to your Homeserver.": "Your encryption keys are now being backed up to your Homeserver.", "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another device.": "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another device.", From a7c9d4ea1f6e9686d3800d540e357ee916e2c875 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 4 Jan 2019 18:14:24 -0600 Subject: [PATCH 236/237] Use same link style in HTML messages Links in HTML messages were missing the usual underline style, making them look different from links in text messages (which already do this). Fixes vector-im/riot-web#4655. --- res/css/views/rooms/_EventTile.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 154e5390c8..6b22c4fe66 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -446,6 +446,7 @@ limitations under the License. .mx_EventTile_content .markdown-body a { color: $accent-color-alt; + text-decoration: underline; } .mx_EventTile_content .markdown-body .hljs { From 633b8d419f4155ead488a9248c52fea1238af093 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sun, 6 Jan 2019 15:13:07 +0200 Subject: [PATCH 237/237] Mention node 8.x as recommended in development docs Co-Authored-By: jaywink --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8852fc9bf0..c70f68902b 100644 --- a/README.md +++ b/README.md @@ -131,7 +131,7 @@ for now. Development =========== -Ensure you have the latest stable Node JS runtime installed. Then check out +Ensure you have the latest stable Node JS runtime installed (v8.x is the best choice). Then check out the code and pull in dependencies: ```bash