From 8774100508cc36b5b0b96a4d78001543cdbe2f9f Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 28 Apr 2017 13:22:55 +0100 Subject: [PATCH 001/127] Initial implementation: SetDisplayName -> SetMxIdDialog - Replaces SetDisplayNameDialog with SetMxIdDialog. This new dialog will use InteractiveAuth to authenticate a user with their chosen mxid. De-scoped: - style tweaks for the InteractiveAuth in the dialog (capcha) and error message. - checking for mxid availability --- src/component-index.js | 4 +- src/components/structures/LoggedInView.js | 5 + src/components/structures/MatrixChat.js | 1 + src/components/structures/RoomView.js | 53 +++--- .../views/dialogs/SetDisplayNameDialog.js | 87 ---------- src/components/views/dialogs/SetMxIdDialog.js | 153 ++++++++++++++++++ 6 files changed, 182 insertions(+), 121 deletions(-) delete mode 100644 src/components/views/dialogs/SetDisplayNameDialog.js create mode 100644 src/components/views/dialogs/SetMxIdDialog.js diff --git a/src/component-index.js b/src/component-index.js index d6873c6dfd..2e55ad14cc 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -95,8 +95,8 @@ import views$dialogs$QuestionDialog from './components/views/dialogs/QuestionDia views$dialogs$QuestionDialog && (module.exports.components['views.dialogs.QuestionDialog'] = views$dialogs$QuestionDialog); import views$dialogs$SessionRestoreErrorDialog from './components/views/dialogs/SessionRestoreErrorDialog'; views$dialogs$SessionRestoreErrorDialog && (module.exports.components['views.dialogs.SessionRestoreErrorDialog'] = views$dialogs$SessionRestoreErrorDialog); -import views$dialogs$SetDisplayNameDialog from './components/views/dialogs/SetDisplayNameDialog'; -views$dialogs$SetDisplayNameDialog && (module.exports.components['views.dialogs.SetDisplayNameDialog'] = views$dialogs$SetDisplayNameDialog); +import views$dialogs$SetMxIdDialog from './components/views/dialogs/SetMxIdDialog'; +views$dialogs$SetMxIdDialog && (module.exports.components['views.dialogs.SetMxIdDialog'] = views$dialogs$SetMxIdDialog); import views$dialogs$TextInputDialog from './components/views/dialogs/TextInputDialog'; views$dialogs$TextInputDialog && (module.exports.components['views.dialogs.TextInputDialog'] = views$dialogs$TextInputDialog); import views$dialogs$UnknownDeviceDialog from './components/views/dialogs/UnknownDeviceDialog'; diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index c4eeb03d5f..6514b32f79 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -43,6 +43,10 @@ export default React.createClass({ onRoomCreated: React.PropTypes.func, onUserSettingsClose: React.PropTypes.func, + // Called with the credentials of a registered user (if they were a ROU that + // transitioned to PWLU) + onRegistered: React.PropTypes.func, + teamToken: React.PropTypes.string, // and lots and lots of other stuff. @@ -184,6 +188,7 @@ export default React.createClass({ roomAddress={this.props.currentRoomAlias || this.props.currentRoomId} autoJoin={this.props.autoJoin} onRoomIdResolved={this.props.onRoomIdResolved} + onRegistered={this.props.onRegistered} eventId={this.props.initialEventId} thirdPartyInvite={this.props.thirdPartyInvite} oobData={this.props.roomOobData} diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 9b8aa3426a..0fe2d6a52f 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1166,6 +1166,7 @@ module.exports = React.createClass({ onRoomIdResolved={this.onRoomIdResolved} onRoomCreated={this.onRoomCreated} onUserSettingsClose={this.onUserSettingsClose} + onRegistered={this.onRegistered} teamToken={this._teamToken} {...this.props} {...this.state} diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index a0c36374b6..83b446597c 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -69,6 +69,10 @@ module.exports = React.createClass({ // once it has been resolved. onRoomIdResolved: React.PropTypes.func, + // Called with the credentials of a registered user (if they were a ROU that + // transitioned to PWLU) + onRegistered: React.PropTypes.func, + // An object representing a third party invite to join this room // Fields: // * inviteSignUrl (string) The URL used to join this room from an email invite @@ -764,38 +768,27 @@ module.exports = React.createClass({ var self = this; var cli = MatrixClientPeg.get(); - var display_name_promise = q(); - // if this is the first room we're joining, check the user has a display name - // and if they don't, prompt them to set one. - // NB. This unfortunately does not re-use the ChangeDisplayName component because - // it doesn't behave quite as desired here (we want an input field here rather than - // content-editable, and we want a default). - if (cli.getRooms().filter((r) => { - return r.hasMembershipState(cli.credentials.userId, "join"); - })) { - display_name_promise = cli.getProfileInfo(cli.credentials.userId).then((result) => { - if (!result.displayname) { - var SetDisplayNameDialog = sdk.getComponent('views.dialogs.SetDisplayNameDialog'); - var dialog_defer = q.defer(); - Modal.createDialog(SetDisplayNameDialog, { - currentDisplayName: result.displayname, - onFinished: (submitted, newDisplayName) => { - if (submitted) { - cli.setDisplayName(newDisplayName).done(() => { - dialog_defer.resolve(); - }); - } - else { - dialog_defer.reject(); - } - } - }); - return dialog_defer.promise; + var mxIdPromise = q(); + + // If the user is a ROU, allow them to transition to a PWLU + if (cli.isGuest()) { + const SetMxIdDialog = sdk.getComponent('views.dialogs.SetMxIdDialog'); + mxIdPromise = q.defer(); + Modal.createDialog(SetMxIdDialog, { + onFinished: (submitted, credentials) => { + if (!submitted) { + mxIdPromise.reject(); + } + this.props.onRegistered(credentials); + mxIdPromise.resolve(); } }); } - display_name_promise.then(() => { + mxIdPromise.then(() => { + this.setState({ + joining: true + }); // if this is an invite and has the 'direct' hint set, mark it as a DM room now. if (this.state.room) { const me = this.state.room.getMember(MatrixClientPeg.get().credentials.userId); @@ -870,10 +863,6 @@ module.exports = React.createClass({ }); } }).done(); - - this.setState({ - joining: true - }); }, onMessageListScroll: function(ev) { diff --git a/src/components/views/dialogs/SetDisplayNameDialog.js b/src/components/views/dialogs/SetDisplayNameDialog.js deleted file mode 100644 index 1047e05c26..0000000000 --- a/src/components/views/dialogs/SetDisplayNameDialog.js +++ /dev/null @@ -1,87 +0,0 @@ -/* -Copyright 2016 OpenMarket 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 sdk from '../../../index'; -import MatrixClientPeg from '../../../MatrixClientPeg'; - -/** - * Prompt the user to set a display name. - * - * On success, `onFinished(true, newDisplayName)` is called. - */ -export default React.createClass({ - displayName: 'SetDisplayNameDialog', - propTypes: { - onFinished: React.PropTypes.func.isRequired, - currentDisplayName: React.PropTypes.string, - }, - - getInitialState: function() { - if (this.props.currentDisplayName) { - return { value: this.props.currentDisplayName }; - } - - if (MatrixClientPeg.get().isGuest()) { - return { value : "Guest " + MatrixClientPeg.get().getUserIdLocalpart() }; - } - else { - return { value : MatrixClientPeg.get().getUserIdLocalpart() }; - } - }, - - componentDidMount: function() { - this.refs.input_value.select(); - }, - - onValueChange: function(ev) { - this.setState({ - value: ev.target.value - }); - }, - - onFormSubmit: function(ev) { - ev.preventDefault(); - this.props.onFinished(true, this.state.value); - return false; - }, - - render: function() { - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - return ( - -
- Your display name is how you'll appear to others when you speak in rooms.
- What would you like it to be? -
-
-
- -
-
- -
-
-
- ); - }, -}); diff --git a/src/components/views/dialogs/SetMxIdDialog.js b/src/components/views/dialogs/SetMxIdDialog.js new file mode 100644 index 0000000000..78737e4ac4 --- /dev/null +++ b/src/components/views/dialogs/SetMxIdDialog.js @@ -0,0 +1,153 @@ +/* +Copyright 2016 OpenMarket 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 q from 'q'; +import React from 'react'; +import sdk from '../../../index'; +import MatrixClientPeg from '../../../MatrixClientPeg'; + +/** + * Prompt the user to set a display name. + * + * On success, `onFinished(true, newDisplayName)` is called. + */ +export default React.createClass({ + displayName: 'SetMxIdDialog', + propTypes: { + onFinished: React.PropTypes.func.isRequired, + }, + + getInitialState: function() { + return { + username : '', + doingUIAuth: false, + } + }, + + componentDidMount: function() { + this.refs.input_value.select(); + + this._matrixClient = MatrixClientPeg.get(); + }, + + onValueChange: function(ev) { + this.setState({ + username: ev.target.value + }); + }, + + onSubmit: function(ev) { + this.setState({ + doingUIAuth: true, + }); + }, + + _generateAndCachePassword: function() { + const pass = Math.random().toString(36).slice(2); + if (localStorage) { + localStorage.setItem('mx_pass', pass); + } + return pass; + }, + + _makeRegisterRequest: function(auth) { + // Not upgrading - changing mxids + const guestAccessToken = null; + + return this._matrixClient.register( + this.state.username, + this._generateAndCachePassword(), + undefined, // session id: included in the auth dict already + auth, + {}, + guestAccessToken, + ); + }, + + _onUIAuthFinished: function(success, response) { + this.setState({ + doingUIAuth: false, + }); + console.info('Auth Finsihed', arguments); + + if (!success) { + this.setState({ errorText : response.message }); + return; + } + + // XXX Implement RTS /register here + const teamToken = null; + + this.props.onFinished(true, { + userId: response.user_id, + deviceId: response.device_id, + homeserverUrl: this._matrixClient.getHomeserverUrl(), + identityServerUrl: this._matrixClient.getIdentityServerUrl(), + accessToken: response.access_token, + teamToken: teamToken, + }); + }, + + 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) { + auth = ; + } + return ( + +
+

+ Beyond this point you're going to need to pick a username - your + unique identifire in Riot. +

+

+ + You can't change your username, but you can always choose how you + appear to other people in Riot by changing your display name. + +

+ + { auth } +
+ { this.state.errorText } +
+
+
+ +
+
+ ); + }, +}); From 6dff4a44157bd8006a4ee6ccc97efa534bbf5b36 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 28 Apr 2017 13:28:34 +0100 Subject: [PATCH 002/127] Return early after cancelled mxid dialog --- src/components/structures/RoomView.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 83b446597c..d06c2c2226 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -778,6 +778,7 @@ module.exports = React.createClass({ onFinished: (submitted, credentials) => { if (!submitted) { mxIdPromise.reject(); + return; } this.props.onRegistered(credentials); mxIdPromise.resolve(); From d12b1903f2b528426931271c6256aa016082c00d Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 28 Apr 2017 13:29:30 +0100 Subject: [PATCH 003/127] Fix defer promise logic --- src/components/structures/RoomView.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index d06c2c2226..569c4e46f0 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -773,15 +773,16 @@ module.exports = React.createClass({ // If the user is a ROU, allow them to transition to a PWLU if (cli.isGuest()) { const SetMxIdDialog = sdk.getComponent('views.dialogs.SetMxIdDialog'); - mxIdPromise = q.defer(); + const defered = q.defer(); + mxIdPromise = defered.promise; Modal.createDialog(SetMxIdDialog, { onFinished: (submitted, credentials) => { if (!submitted) { - mxIdPromise.reject(); + defered.reject(); return; } this.props.onRegistered(credentials); - mxIdPromise.resolve(); + defered.resolve(); } }); } From 5a5768a4ecc9033f64e98e146ad8628a61cf079c Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 28 Apr 2017 13:38:35 +0100 Subject: [PATCH 004/127] Try to fix tests --- src/components/structures/RoomView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 569c4e46f0..848a8cc7ba 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -771,7 +771,7 @@ module.exports = React.createClass({ var mxIdPromise = q(); // If the user is a ROU, allow them to transition to a PWLU - if (cli.isGuest()) { + if (cli && cli.isGuest()) { const SetMxIdDialog = sdk.getComponent('views.dialogs.SetMxIdDialog'); const defered = q.defer(); mxIdPromise = defered.promise; From a887af9f92c8f9c9b58bcff8190eb42f1e7f6bb3 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 2 May 2017 09:56:14 +0100 Subject: [PATCH 005/127] copyright --- src/component-index.js | 1 + src/components/views/dialogs/SetMxIdDialog.js | 1 + 2 files changed, 2 insertions(+) diff --git a/src/component-index.js b/src/component-index.js index 2e55ad14cc..aa65cabbec 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017 Vector Creations Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/components/views/dialogs/SetMxIdDialog.js b/src/components/views/dialogs/SetMxIdDialog.js index 78737e4ac4..c8083371de 100644 --- a/src/components/views/dialogs/SetMxIdDialog.js +++ b/src/components/views/dialogs/SetMxIdDialog.js @@ -1,5 +1,6 @@ /* Copyright 2016 OpenMarket Ltd +Copyright 2017 Vector Creations Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From 4f71f4c331bb36d19ccdd92d2d92425bc7cf3138 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 2 May 2017 10:07:06 +0100 Subject: [PATCH 006/127] Store mx_pass at the same point as access_token So that we don't overwrite the existing one every time we try to register. --- src/Lifecycle.js | 6 ++++++ src/components/views/dialogs/SetMxIdDialog.js | 12 +++++------- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/Lifecycle.js b/src/Lifecycle.js index f20716cae6..7fba0a0298 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -303,6 +303,12 @@ export function setLoggedIn(credentials) { localStorage.setItem("mx_device_id", credentials.deviceId); } + // The user registered as a PWLU (PassWord-Less User), the generated password + // is cached here such that the user can change it at a later time. + if (credentials.password) { + localStorage.setItem("mx_pass", credentials.password); + } + console.log("Session persisted for %s", credentials.userId); } catch (e) { console.warn("Error using local storage: can't persist session!", e); diff --git a/src/components/views/dialogs/SetMxIdDialog.js b/src/components/views/dialogs/SetMxIdDialog.js index c8083371de..db88c3304b 100644 --- a/src/components/views/dialogs/SetMxIdDialog.js +++ b/src/components/views/dialogs/SetMxIdDialog.js @@ -56,21 +56,18 @@ export default React.createClass({ }); }, - _generateAndCachePassword: function() { - const pass = Math.random().toString(36).slice(2); - if (localStorage) { - localStorage.setItem('mx_pass', pass); - } - return pass; + _generatePassword: function() { + return Math.random().toString(36).slice(2); }, _makeRegisterRequest: function(auth) { // Not upgrading - changing mxids const guestAccessToken = null; + this._generatedPassword = this._generatePassword(); return this._matrixClient.register( this.state.username, - this._generateAndCachePassword(), + this._generatedPassword, undefined, // session id: included in the auth dict already auth, {}, @@ -98,6 +95,7 @@ export default React.createClass({ homeserverUrl: this._matrixClient.getHomeserverUrl(), identityServerUrl: this._matrixClient.getIdentityServerUrl(), accessToken: response.access_token, + password: this._generatedPassword, teamToken: teamToken, }); }, From 13d37e43ff85605ba74a06c146a0ab11086421d8 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 2 May 2017 10:14:54 +0100 Subject: [PATCH 007/127] Mock isGuest --- test/test-utils.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/test-utils.js b/test/test-utils.js index 5209465362..9f404f98eb 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -133,6 +133,7 @@ export function createTestClient() { sendHtmlMessage: () => q({}), getSyncState: () => "SYNCING", generateClientSecret: () => "t35tcl1Ent5ECr3T", + isGuest: () => false, }; } From 18ba5d3e49319c4c47d70a8c0ec1ec9ca84a5fd9 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@googlemail.com> Date: Wed, 3 May 2017 12:39:24 +0100 Subject: [PATCH 008/127] fix typo made in #849 --- src/components/views/dialogs/SetMxIdDialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/dialogs/SetMxIdDialog.js b/src/components/views/dialogs/SetMxIdDialog.js index db88c3304b..d139a4ae3b 100644 --- a/src/components/views/dialogs/SetMxIdDialog.js +++ b/src/components/views/dialogs/SetMxIdDialog.js @@ -122,7 +122,7 @@ export default React.createClass({

Beyond this point you're going to need to pick a username - your - unique identifire in Riot. + unique identifier in Riot.

From 6f4eb9d8b1809c6419945f472d0455978274751f Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 5 May 2017 16:31:33 +0100 Subject: [PATCH 009/127] Show password nag bar when user is PWLU --- src/Lifecycle.js | 6 ++++-- src/components/structures/LoggedInView.js | 13 +++++++++---- src/components/structures/MatrixChat.js | 12 ++++++++++-- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 7fba0a0298..43a46e2d7d 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -284,6 +284,7 @@ export function setLoggedIn(credentials) { // Resolves by default let teamPromise = Promise.resolve(null); + let isPasswordStored = false; // persist the session if (localStorage) { @@ -307,6 +308,7 @@ export function setLoggedIn(credentials) { // is cached here such that the user can change it at a later time. if (credentials.password) { localStorage.setItem("mx_pass", credentials.password); + isPasswordStored = true; } console.log("Session persisted for %s", credentials.userId); @@ -332,10 +334,10 @@ export function setLoggedIn(credentials) { MatrixClientPeg.replaceUsingCreds(credentials); teamPromise.then((teamToken) => { - dis.dispatch({action: 'on_logged_in', teamToken: teamToken}); + dis.dispatch({action: 'on_logged_in', teamToken: teamToken, isPasswordStored}); }, (err) => { console.warn("Failed to get team token on login", err); - dis.dispatch({action: 'on_logged_in', teamToken: null}); + dis.dispatch({action: 'on_logged_in', teamToken: null, isPasswordStored}); }); startMatrixClient(); diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index 6514b32f79..4001227355 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -49,6 +49,10 @@ export default React.createClass({ teamToken: React.PropTypes.string, + // Has the user generated a password that is stored in local storage? + // (are they a PWLU?) + userHasGeneratedPassword: React.PropTypes.boolean, + // and lots and lots of other stuff. }, @@ -177,6 +181,7 @@ export default React.createClass({ const MatrixToolbar = sdk.getComponent('globals.MatrixToolbar'); const GuestWarningBar = sdk.getComponent('globals.GuestWarningBar'); const NewVersionBar = sdk.getComponent('globals.NewVersionBar'); + const PasswordNagBar = sdk.getComponent('globals.PasswordNagBar'); let page_element; let right_panel = ''; @@ -250,11 +255,11 @@ export default React.createClass({ topBar = ; - } - else if (this.props.matrixClient.isGuest()) { + } else if (this.props.matrixClient.isGuest()) { topBar = ; - } - else if (Notifier.supportsDesktopNotifications() && !Notifier.isEnabled() && !Notifier.isToolbarHidden()) { + } else if (this.props.userHasGeneratedPassword) { + topBar = ; + } else if (Notifier.supportsDesktopNotifications() && !Notifier.isEnabled() && !Notifier.isToolbarHidden()) { topBar = ; } diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 0fe2d6a52f..26f89079f7 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -138,6 +138,9 @@ module.exports = React.createClass({ register_hs_url: null, register_is_url: null, register_id_sid: null, + + // Initially, use localStorage as source of truth + userHasGeneratedPassword: localStorage && localStorage.getItem('mx_pass'), }; return s; }, @@ -569,7 +572,7 @@ module.exports = React.createClass({ this.setState({loggingIn: true}); break; case 'on_logged_in': - this._onLoggedIn(payload.teamToken); + this._onLoggedIn(payload.teamToken, payload.isPasswordStored); break; case 'on_logged_out': this._onLoggedOut(); @@ -755,11 +758,15 @@ module.exports = React.createClass({ /** * Called when a new logged in session has started */ - _onLoggedIn: function(teamToken) { + _onLoggedIn: function(teamToken, isPasswordStored) { this.setState({ guestCreds: null, loggedIn: true, loggingIn: false, + // isPasswordStored only true when ROU sets a username and becomes PWLU. + // (the password was randomly generated and stored in localStorage). + userHasGeneratedPassword: + this.state.userHasGeneratedPassword || isPasswordStored, }); if (teamToken) { @@ -1168,6 +1175,7 @@ module.exports = React.createClass({ onUserSettingsClose={this.onUserSettingsClose} onRegistered={this.onRegistered} teamToken={this._teamToken} + userHasGeneratedPassword={this.state.userHasGeneratedPassword} {...this.props} {...this.state} /> From ad2ed129800bbcc79544c66426f2fd70a5708dfa Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 10 May 2017 14:22:17 +0100 Subject: [PATCH 010/127] Redesign mxID chooser, add availability checking Requires https://github.com/matrix-org/matrix-js-sdk/pull/432 for availability checking. Changes: - Redesign the dialog to look more like https://github.com/vector-im/riot-web/issues/3604#issuecomment-299226875 - Attempt to fix wrong password being stored by generating one per SetMxIdDialog (there's no issue tracking this for now, I shall open one if it persists) - Backwards compatible with servers that don't support register/availability - a spinner will appear the first time a username is checked because server support can only be determined after a request. - Rate-limited by a 2s debounce - General style improvements --- src/components/structures/RoomView.js | 11 +- src/components/views/dialogs/SetMxIdDialog.js | 154 +++++++++++++++--- 2 files changed, 137 insertions(+), 28 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 848a8cc7ba..710f333322 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -775,7 +775,8 @@ module.exports = React.createClass({ const SetMxIdDialog = sdk.getComponent('views.dialogs.SetMxIdDialog'); const defered = q.defer(); mxIdPromise = defered.promise; - Modal.createDialog(SetMxIdDialog, { + const close = Modal.createDialog(SetMxIdDialog, { + homeserverUrl: cli.getHomeserverUrl(), onFinished: (submitted, credentials) => { if (!submitted) { defered.reject(); @@ -783,8 +784,12 @@ module.exports = React.createClass({ } this.props.onRegistered(credentials); defered.resolve(); - } - }); + }, + onDifferentServerClicked: (ev) => { + dis.dispatch({action: 'start_registration'}); + close(); + }, + }).close; } mxIdPromise.then(() => { diff --git a/src/components/views/dialogs/SetMxIdDialog.js b/src/components/views/dialogs/SetMxIdDialog.js index d139a4ae3b..445b7eb77f 100644 --- a/src/components/views/dialogs/SetMxIdDialog.js +++ b/src/components/views/dialogs/SetMxIdDialog.js @@ -19,6 +19,11 @@ import q from 'q'; import React from 'react'; import sdk from '../../../index'; import MatrixClientPeg from '../../../MatrixClientPeg'; +import classnames from 'classnames'; + +// The amount of time to wait for further changes to the input username before +// sending a request to the server +const USERNAME_CHECK_DEBOUNCE_MS = 2000; /** * Prompt the user to set a display name. @@ -33,9 +38,20 @@ export default React.createClass({ getInitialState: function() { return { - username : '', + // The entered username + username: '', + // Indicate ongoing work on the username + usernameBusy: false, + // Indicate error with username + usernameError: '', + // Assume the homeserver supports username checking until "M_UNRECOGNIZED" + usernameCheckSupport: true, + + // Whether the auth UI is currently being used doingUIAuth: false, - } + // Indicate error with auth + authError: '', + }; }, componentDidMount: function() { @@ -46,7 +62,28 @@ export default React.createClass({ onValueChange: function(ev) { this.setState({ - username: ev.target.value + username: ev.target.value, + usernameBusy: true, + usernameError: '', + }, () => { + if (!this.state.username || !this.state.usernameCheckSupport) { + this.setState({ + usernameBusy: false, + }); + return; + } + + // Debounce the username check to limit number of requests sent + if (this._usernameCheckTimeout) { + clearTimeout(this._usernameCheckTimeout); + } + this._usernameCheckTimeout = setTimeout(() => { + this._doUsernameCheck().finally(() => { + this.setState({ + usernameBusy: false, + }); + }); + }, USERNAME_CHECK_DEBOUNCE_MS); }); }, @@ -56,6 +93,40 @@ export default React.createClass({ }); }, + _doUsernameCheck: function() { + // Check if username is available + return this._matrixClient.isUsernameAvailable(this.state.username).then( + (isAvailable) => { + if (isAvailable) { + this.setState({usernameError: ''}); + } + }, + (err) => { + // Indicate whether the homeserver supports username checking + const newState = { + usernameCheckSupport: err.errcode !== "M_UNRECOGNIZED", + }; + switch (err.errcode) { + case "M_USER_IN_USE": + newState.usernameError = 'Username not available'; + break; + case "M_INVALID_USERNAME": + newState.usernameError = 'Username invalid: ' + err.message; + break; + case "M_UNRECOGNIZED": + // This homeserver doesn't support username checking, assume it's + // fine and rely on the error appearing in registration step. + newState.usernameError = ''; + break; + default: + newState.usernameError = 'An error occurred' + err.message; + break; + } + this.setState(newState); + }, + ); + }, + _generatePassword: function() { return Math.random().toString(36).slice(2); }, @@ -63,8 +134,9 @@ export default React.createClass({ _makeRegisterRequest: function(auth) { // Not upgrading - changing mxids const guestAccessToken = null; - this._generatedPassword = this._generatePassword(); - + if (!this._generatedPassword) { + this._generatedPassword = this._generatePassword(); + } return this._matrixClient.register( this.state.username, this._generatedPassword, @@ -79,10 +151,9 @@ export default React.createClass({ this.setState({ doingUIAuth: false, }); - console.info('Auth Finsihed', arguments); if (!success) { - this.setState({ errorText : response.message }); + this.setState({ authError: response.message }); return; } @@ -104,6 +175,7 @@ export default React.createClass({ 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) { auth = ; } + const inputClasses = classnames({ + "mx_SetMxIdDialog_input": true, + "error": Boolean(this.state.usernameError), + }); + + let usernameIndicator = null; + let usernameBusyIndicator = null; + if (this.state.usernameBusy) { + usernameBusyIndicator = ; + } else { + const usernameAvailable = this.state.username && + this.state.usernameCheckSupport && !this.state.usernameError; + const usernameIndicatorClasses = classnames({ + "error": Boolean(this.state.usernameError), + "success": usernameAvailable, + }); + usernameIndicator =

+ { usernameAvailable ? 'Username available' : this.state.usernameError } +
; + } + + let authErrorIndicator = null; + if (this.state.authError) { + authErrorIndicator =
+ { this.state.authError } +
; + } + const canContinue = this.state.username && + !this.state.usernameError && + !this.state.usernameBusy; + return (
-

- Beyond this point you're going to need to pick a username - your - unique identifier in Riot. -

-

- - You can't change your username, but you can always choose how you - appear to other people in Riot by changing your display name. - -

- - { auth } -
- { this.state.errorText } +
+ + { usernameBusyIndicator }
+ { usernameIndicator } +

+ This will be your account name on + the {this.props.homeserverUrl} homeserver, + or you can pick a  + + different server + . +

+ { auth } + { authErrorIndicator }
From 6257bfcd87828610159d07c29cf7c277aafcd640 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 10 May 2017 14:28:48 +0100 Subject: [PATCH 011/127] Add prop type for onDifferentServerClicked --- src/components/views/dialogs/SetMxIdDialog.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/views/dialogs/SetMxIdDialog.js b/src/components/views/dialogs/SetMxIdDialog.js index 445b7eb77f..86b5fccbc2 100644 --- a/src/components/views/dialogs/SetMxIdDialog.js +++ b/src/components/views/dialogs/SetMxIdDialog.js @@ -34,6 +34,8 @@ export default React.createClass({ displayName: 'SetMxIdDialog', propTypes: { onFinished: React.PropTypes.func.isRequired, + // Called when the user requests to register with a different homeserver + onDifferentServerClicked: React.PropTypes.func.isRequired, }, getInitialState: function() { From 6326a95b39f335d3da3b66bb48a53a9d456c208c Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 11 May 2017 17:04:11 +0100 Subject: [PATCH 012/127] Prevent ROUs from creating new chats/new rooms Spawn a SetMxIdDialog instead and do nothing. --- src/components/structures/MatrixChat.js | 38 +++++++++++++++++++++---- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 0c8c60ba5c..eeab10b326 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -502,21 +502,23 @@ module.exports = React.createClass({ this.notifyNewScreen('settings'); break; case 'view_create_room': - //this._setPage(PageTypes.CreateRoom); - //this.notifyNewScreen('new'); + if (MatrixClientPeg.get().isGuest()) { + dis.dispatch({action: 'view_set_mxid'}); + break; + } var TextInputDialog = sdk.getComponent("dialogs.TextInputDialog"); Modal.createDialog(TextInputDialog, { title: "Create Room", description: "Room name (optional)", button: "Create Room", - onFinished: (should_create, name) => { - if (should_create) { + onFinished: (shouldCreate, name) => { + if (shouldCreate) { const createOpts = {}; if (name) createOpts.name = name; createRoom({createOpts}).done(); } - } + }, }); break; case 'view_room_directory': @@ -531,6 +533,9 @@ module.exports = React.createClass({ this._setPage(PageTypes.HomePage); this.notifyNewScreen('home'); break; + case 'view_set_mxid': + this._setMxId(); + break; case 'view_create_chat': this._createChat(); break; @@ -679,8 +684,29 @@ module.exports = React.createClass({ }); }, + _setMxId: function() { + const SetMxIdDialog = sdk.getComponent('views.dialogs.SetMxIdDialog'); + const close = Modal.createDialog(SetMxIdDialog, { + homeserverUrl: MatrixClientPeg.get().getHomeserverUrl(), + onFinished: (submitted, credentials) => { + if (!submitted) { + return; + } + this.onRegistered(credentials); + }, + onDifferentServerClicked: (ev) => { + dis.dispatch({action: 'start_registration'}); + close(); + }, + }).close; + }, + _createChat: function() { - var ChatInviteDialog = sdk.getComponent("dialogs.ChatInviteDialog"); + if (MatrixClientPeg.get().isGuest()) { + dis.dispatch({action: 'view_set_mxid'}); + return; + } + const ChatInviteDialog = sdk.getComponent("dialogs.ChatInviteDialog"); Modal.createDialog(ChatInviteDialog, { title: "Start a new chat", }); From cfa108a28c80a8f68311c00cdc8183b5be3b750c Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 11 May 2017 17:07:03 +0100 Subject: [PATCH 013/127] No need to dispatch, just call setMxId --- src/components/structures/MatrixChat.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index eeab10b326..d63bf897c9 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -703,7 +703,7 @@ module.exports = React.createClass({ _createChat: function() { if (MatrixClientPeg.get().isGuest()) { - dis.dispatch({action: 'view_set_mxid'}); + this._setMxId(); return; } const ChatInviteDialog = sdk.getComponent("dialogs.ChatInviteDialog"); From 8725ef38639573bf6f0b36d6ee6cac305e5ef70a Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 11 May 2017 17:47:45 +0100 Subject: [PATCH 014/127] Remove "Current Password" input if mx_pass exists If the user is PWLU, do not show "Current Password" field in ChangePassword and when setting a new password, use the cached password. --- src/components/structures/LoggedInView.js | 1 + src/components/structures/MatrixChat.js | 9 ++++- src/components/structures/UserSettings.js | 5 +++ .../views/settings/ChangePassword.js | 38 +++++++++++-------- 4 files changed, 37 insertions(+), 16 deletions(-) diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index 4001227355..2afb43bf47 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -216,6 +216,7 @@ export default React.createClass({ enableLabs={this.props.config.enableLabs} referralBaseUrl={this.props.config.referralBaseUrl} teamToken={this.props.teamToken} + cachedPassword={this.props.cachedPassword} />; if (!this.props.collapse_rhs) right_panel = ; break; diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 0c8c60ba5c..b3fa6e9040 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -590,6 +590,12 @@ module.exports = React.createClass({ payload.releaseNotes ); break; + case 'password_changed': + this.setState({ + userHasGeneratedPassword: false, + }); + localStorage.removeItem("mx_pass"); + break; } }, @@ -1176,7 +1182,8 @@ module.exports = React.createClass({ onUserSettingsClose={this.onUserSettingsClose} onRegistered={this.onRegistered} teamToken={this._teamToken} - userHasGeneratedPassword={this.state.userHasGeneratedPassword} + cachedPassword={this.state.userHasGeneratedPassword ? + localStorage.getItem('mx_pass') : null} {...this.props} {...this.state} /> diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 46dce8bd2e..fa0fcadf0e 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -139,6 +139,9 @@ module.exports = React.createClass({ // Team token for the referral link. If falsy, the referral section will // not appear teamToken: React.PropTypes.string, + + // the user is a PWLU (/w password stashed in localStorage 'mx_pass') + cachedPassword: React.PropTypes.string, }, getDefaultProps: function() { @@ -331,6 +334,7 @@ module.exports = React.createClass({ receive push notifications on other devices until you log back in to them.`, }); + dis.dispatch({action: 'password_changed'}); }, onUpgradeClicked: function() { @@ -894,6 +898,7 @@ module.exports = React.createClass({ rowLabelClassName="mx_UserSettings_profileLabelCell" rowInputClassName="mx_UserSettings_profileInputCell" buttonClassName="mx_UserSettings_button mx_UserSettings_changePasswordButton" + cachedPassword={this.props.cachedPassword} onError={this.onPasswordChangeError} onFinished={this.onPasswordChanged} /> ); diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js index 20ce45e5dd..bbb5d14219 100644 --- a/src/components/views/settings/ChangePassword.js +++ b/src/components/views/settings/ChangePassword.js @@ -31,7 +31,10 @@ module.exports = React.createClass({ rowClassName: React.PropTypes.string, rowLabelClassName: React.PropTypes.string, rowInputClassName: React.PropTypes.string, - buttonClassName: React.PropTypes.string + buttonClassName: React.PropTypes.string, + + // user is a PWLU (/w password stashed in localStorage 'mx_pass') + cachedPassword: React.PropTypes.string, }, Phases: { @@ -121,10 +124,10 @@ module.exports = React.createClass({ matrixClient: MatrixClientPeg.get(), } ); - }, + }, onClickChange: function() { - var old_password = this.refs.old_input.value; + var old_password = this.props.cachedPassword || this.refs.old_input.value; var new_password = this.refs.new_input.value; var confirm_password = this.refs.confirm_input.value; var err = this.props.onCheckPassword( @@ -139,23 +142,28 @@ module.exports = React.createClass({ }, render: function() { - var rowClassName = this.props.rowClassName; - var rowLabelClassName = this.props.rowLabelClassName; - var rowInputClassName = this.props.rowInputClassName; - var buttonClassName = this.props.buttonClassName; + const rowClassName = this.props.rowClassName; + const rowLabelClassName = this.props.rowLabelClassName; + const rowInputClassName = this.props.rowInputClassName; + const buttonClassName = this.props.buttonClassName; + + let currentPassword = null; + if (!this.props.cachedPassword) { + currentPassword =
+
+ +
+
+ +
+
; + } switch (this.state.phase) { case this.Phases.Edit: return (
-
-
- -
-
- -
-
+ { currentPassword }
From 1176573f39b3c2531887e36a2d6c79b3136c5ab5 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 12 May 2017 12:02:45 +0100 Subject: [PATCH 015/127] Implement SessionStore This wraps session-related state into a basic flux store. The localStorage item 'mx_pass' is the only thing managed by this store for now but it could easily be extended to track other items (like the teamToken which is passed around through props a lot) --- src/Lifecycle.js | 12 ++-- src/components/structures/LoggedInView.js | 3 +- src/components/structures/MatrixChat.js | 29 +++++---- src/components/structures/UserSettings.js | 4 -- .../views/settings/ChangePassword.js | 25 ++++++-- src/stores/SessionStore.js | 63 +++++++++++++++++++ 6 files changed, 104 insertions(+), 32 deletions(-) create mode 100644 src/stores/SessionStore.js diff --git a/src/Lifecycle.js b/src/Lifecycle.js index a7a06401da..decb544b3c 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -289,7 +289,6 @@ export function setLoggedIn(credentials) { // Resolves by default let teamPromise = Promise.resolve(null); - let isPasswordStored = false; // persist the session if (localStorage) { @@ -312,8 +311,11 @@ export function setLoggedIn(credentials) { // The user registered as a PWLU (PassWord-Less User), the generated password // is cached here such that the user can change it at a later time. if (credentials.password) { - localStorage.setItem("mx_pass", credentials.password); - isPasswordStored = true; + // Update SessionStore + dis.dispatch({ + action: 'cached_password', + cachedPassword: credentials.password, + }); } console.log("Session persisted for %s", credentials.userId); @@ -339,10 +341,10 @@ export function setLoggedIn(credentials) { MatrixClientPeg.replaceUsingCreds(credentials); teamPromise.then((teamToken) => { - dis.dispatch({action: 'on_logged_in', teamToken: teamToken, isPasswordStored}); + dis.dispatch({action: 'on_logged_in', teamToken: teamToken}); }, (err) => { console.warn("Failed to get team token on login", err); - dis.dispatch({action: 'on_logged_in', teamToken: null, isPasswordStored}); + dis.dispatch({action: 'on_logged_in', teamToken: null}); }); startMatrixClient(); diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index 2afb43bf47..0851c01a18 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -51,7 +51,7 @@ export default React.createClass({ // Has the user generated a password that is stored in local storage? // (are they a PWLU?) - userHasGeneratedPassword: React.PropTypes.boolean, + userHasGeneratedPassword: React.PropTypes.bool, // and lots and lots of other stuff. }, @@ -216,7 +216,6 @@ export default React.createClass({ enableLabs={this.props.config.enableLabs} referralBaseUrl={this.props.config.referralBaseUrl} teamToken={this.props.teamToken} - cachedPassword={this.props.cachedPassword} />; if (!this.props.collapse_rhs) right_panel = ; break; diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index b3fa6e9040..d7e24c019a 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -40,6 +40,8 @@ var PageTypes = require('../../PageTypes'); var createRoom = require("../../createRoom"); import * as UDEHandler from '../../UnknownDeviceErrorHandler'; +import getSessionStore from '../../stores/SessionStore'; + module.exports = React.createClass({ displayName: 'MatrixChat', @@ -139,8 +141,7 @@ module.exports = React.createClass({ register_is_url: null, register_id_sid: null, - // Initially, use localStorage as source of truth - userHasGeneratedPassword: localStorage && localStorage.getItem('mx_pass'), + userHasGeneratedPassword: false, }; return s; }, @@ -249,6 +250,10 @@ module.exports = React.createClass({ register_hs_url: paramHs, }); } + + this._sessionStore = getSessionStore(); + this._sessionStore.on('update', this._setStateFromSessionStore); + this._setStateFromSessionStore(); }, componentDidMount: function() { @@ -590,12 +595,6 @@ module.exports = React.createClass({ payload.releaseNotes ); break; - case 'password_changed': - this.setState({ - userHasGeneratedPassword: false, - }); - localStorage.removeItem("mx_pass"); - break; } }, @@ -765,15 +764,11 @@ module.exports = React.createClass({ /** * Called when a new logged in session has started */ - _onLoggedIn: function(teamToken, isPasswordStored) { + _onLoggedIn: function(teamToken) { this.setState({ guestCreds: null, loggedIn: true, loggingIn: false, - // isPasswordStored only true when ROU sets a username and becomes PWLU. - // (the password was randomly generated and stored in localStorage). - userHasGeneratedPassword: - this.state.userHasGeneratedPassword || isPasswordStored, }); if (teamToken) { @@ -902,6 +897,12 @@ module.exports = React.createClass({ }); }, + _setStateFromSessionStore() { + this.setState({ + userHasGeneratedPassword: Boolean(this._sessionStore.getCachedPassword()), + }); + }, + onFocus: function(ev) { dis.dispatch({action: 'focus_composer'}); }, @@ -1182,8 +1183,6 @@ module.exports = React.createClass({ onUserSettingsClose={this.onUserSettingsClose} onRegistered={this.onRegistered} teamToken={this._teamToken} - cachedPassword={this.state.userHasGeneratedPassword ? - localStorage.getItem('mx_pass') : null} {...this.props} {...this.state} /> diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index fa0fcadf0e..d352d5cae8 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -139,9 +139,6 @@ module.exports = React.createClass({ // Team token for the referral link. If falsy, the referral section will // not appear teamToken: React.PropTypes.string, - - // the user is a PWLU (/w password stashed in localStorage 'mx_pass') - cachedPassword: React.PropTypes.string, }, getDefaultProps: function() { @@ -898,7 +895,6 @@ module.exports = React.createClass({ rowLabelClassName="mx_UserSettings_profileLabelCell" rowInputClassName="mx_UserSettings_profileInputCell" buttonClassName="mx_UserSettings_button mx_UserSettings_changePasswordButton" - cachedPassword={this.props.cachedPassword} onError={this.onPasswordChangeError} onFinished={this.onPasswordChanged} /> ); diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js index bbb5d14219..3a1c777cd9 100644 --- a/src/components/views/settings/ChangePassword.js +++ b/src/components/views/settings/ChangePassword.js @@ -22,6 +22,8 @@ var Modal = require("../../../Modal"); var sdk = require("../../../index"); import AccessibleButton from '../elements/AccessibleButton'; +import getSessionStore from '../../../stores/SessionStore'; + module.exports = React.createClass({ displayName: 'ChangePassword', propTypes: { @@ -32,9 +34,6 @@ module.exports = React.createClass({ rowLabelClassName: React.PropTypes.string, rowInputClassName: React.PropTypes.string, buttonClassName: React.PropTypes.string, - - // user is a PWLU (/w password stashed in localStorage 'mx_pass') - cachedPassword: React.PropTypes.string, }, Phases: { @@ -63,10 +62,24 @@ module.exports = React.createClass({ getInitialState: function() { return { - phase: this.Phases.Edit + phase: this.Phases.Edit, + cachedPassword: null, }; }, + componentWillMount: function() { + this.sessionStore = getSessionStore(); + this.sessionStore.on('update', this.setStateFromSessionStore); + + this.setStateFromSessionStore(); + }, + + setStateFromSessionStore: function() { + this.setState({ + cachedPassword: this.sessionStore.getCachedPassword(), + }); + }, + changePassword: function(old_password, new_password) { var cli = MatrixClientPeg.get(); @@ -127,7 +140,7 @@ module.exports = React.createClass({ }, onClickChange: function() { - var old_password = this.props.cachedPassword || this.refs.old_input.value; + var old_password = this.state.cachedPassword || this.refs.old_input.value; var new_password = this.refs.new_input.value; var confirm_password = this.refs.confirm_input.value; var err = this.props.onCheckPassword( @@ -148,7 +161,7 @@ module.exports = React.createClass({ const buttonClassName = this.props.buttonClassName; let currentPassword = null; - if (!this.props.cachedPassword) { + if (!this.state.cachedPassword) { currentPassword =
diff --git a/src/stores/SessionStore.js b/src/stores/SessionStore.js new file mode 100644 index 0000000000..1c19494e23 --- /dev/null +++ b/src/stores/SessionStore.js @@ -0,0 +1,63 @@ +import dis from '../dispatcher'; +import EventEmitter from 'events'; + +/** + * A class for storing application state to do with the session. This is a simple flux + * store that listens for actions and updates its state accordingly, informing any + * listeners (views) of state changes via the 'update' event. + */ +function SessionStore() { + // Initialise state + this._state = { + cachedPassword: localStorage.getItem('mx_pass'), + }; + + dis.register(this._onAction.bind(this)); +} + +// Inherit from EventEmitter +SessionStore.prototype = EventEmitter.prototype; + +SessionStore.prototype._update = function() { + // Persist state to localStorage + if (this._state.cachedPassword) { + localStorage.setItem('mx_pass', this._state.cachedPassword); + } else { + localStorage.removeItem('mx_pass', this._state.cachedPassword); + } + + this.emit('update'); +}; + +SessionStore.prototype._setState = function(newState) { + this._state = Object.assign(this._state, newState); + this._update(); +}; + +SessionStore.prototype._onAction = function(payload) { + switch (payload.action) { + case 'cached_password': + this._setState({ + cachedPassword: payload.cachedPassword, + }); + break; + case 'password_changed': + this._setState({ + cachedPassword: null, + }); + break; + } +}; + +SessionStore.prototype.getCachedPassword = function() { + return this._state.cachedPassword; +}; + +// Export singleton getter +let singletonSessionStore = null; +export default function getSessionStore() { + if (!singletonSessionStore) { + singletonSessionStore = new SessionStore(); + } + return singletonSessionStore; +} From 5c8187dc8f9804e01ad4d01af27d07801caebc2c Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 12 May 2017 15:47:37 +0100 Subject: [PATCH 016/127] Explicitly pass thru userHasGeneratedPassword --- 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 d7e24c019a..5975d6cf5f 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1183,6 +1183,7 @@ module.exports = React.createClass({ onUserSettingsClose={this.onUserSettingsClose} onRegistered={this.onRegistered} teamToken={this._teamToken} + userHasGeneratedPassword={this.state.userHasGeneratedPassword} {...this.props} {...this.state} /> From 6ffe7ef9b2aabcb41c10043fe762c529f80617dc Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 12 May 2017 15:50:01 +0100 Subject: [PATCH 017/127] Use same singleton impl as MatrixClientPeg for SessionStore --- src/stores/SessionStore.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/stores/SessionStore.js b/src/stores/SessionStore.js index 1c19494e23..7be3885fde 100644 --- a/src/stores/SessionStore.js +++ b/src/stores/SessionStore.js @@ -55,9 +55,7 @@ SessionStore.prototype.getCachedPassword = function() { // Export singleton getter let singletonSessionStore = null; -export default function getSessionStore() { - if (!singletonSessionStore) { - singletonSessionStore = new SessionStore(); - } - return singletonSessionStore; +if (!singletonSessionStore) { + singletonSessionStore = new SessionStore(); } +module.exports = singletonSessionStore; From 536724e7c5b6f55352311ad2856e95ece60ab5cb Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 12 May 2017 15:58:44 +0100 Subject: [PATCH 018/127] ES6 SessionStore --- src/components/structures/MatrixChat.js | 4 +- .../views/settings/ChangePassword.js | 4 +- src/stores/SessionStore.js | 89 ++++++++++--------- 3 files changed, 49 insertions(+), 48 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 5975d6cf5f..b8b3f51422 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -40,7 +40,7 @@ var PageTypes = require('../../PageTypes'); var createRoom = require("../../createRoom"); import * as UDEHandler from '../../UnknownDeviceErrorHandler'; -import getSessionStore from '../../stores/SessionStore'; +import sessionStore from '../../stores/SessionStore'; module.exports = React.createClass({ displayName: 'MatrixChat', @@ -251,7 +251,7 @@ module.exports = React.createClass({ }); } - this._sessionStore = getSessionStore(); + this._sessionStore = sessionStore; this._sessionStore.on('update', this._setStateFromSessionStore); this._setStateFromSessionStore(); }, diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js index 3a1c777cd9..c20bc47152 100644 --- a/src/components/views/settings/ChangePassword.js +++ b/src/components/views/settings/ChangePassword.js @@ -22,7 +22,7 @@ var Modal = require("../../../Modal"); var sdk = require("../../../index"); import AccessibleButton from '../elements/AccessibleButton'; -import getSessionStore from '../../../stores/SessionStore'; +import sessionStore from '../../../stores/SessionStore'; module.exports = React.createClass({ displayName: 'ChangePassword', @@ -68,7 +68,7 @@ module.exports = React.createClass({ }, componentWillMount: function() { - this.sessionStore = getSessionStore(); + this.sessionStore = sessionStore; this.sessionStore.on('update', this.setStateFromSessionStore); this.setStateFromSessionStore(); diff --git a/src/stores/SessionStore.js b/src/stores/SessionStore.js index 7be3885fde..bf605d7f07 100644 --- a/src/stores/SessionStore.js +++ b/src/stores/SessionStore.js @@ -6,53 +6,54 @@ import EventEmitter from 'events'; * store that listens for actions and updates its state accordingly, informing any * listeners (views) of state changes via the 'update' event. */ -function SessionStore() { - // Initialise state - this._state = { - cachedPassword: localStorage.getItem('mx_pass'), - }; +class SessionStore extends EventEmitter { + constructor() { + super(); - dis.register(this._onAction.bind(this)); + // Initialise state + this._state = { + cachedPassword: localStorage.getItem('mx_pass'), + }; + + dis.register(this._onAction.bind(this)); + } + + _update() { + // Persist state to localStorage + if (this._state.cachedPassword) { + localStorage.setItem('mx_pass', this._state.cachedPassword); + } else { + localStorage.removeItem('mx_pass', this._state.cachedPassword); + } + + this.emit('update'); + } + + _setState(newState) { + this._state = Object.assign(this._state, newState); + this._update(); + } + + _onAction(payload) { + switch (payload.action) { + case 'cached_password': + this._setState({ + cachedPassword: payload.cachedPassword, + }); + break; + case 'password_changed': + this._setState({ + cachedPassword: null, + }); + break; + } + } + + getCachedPassword() { + return this._state.cachedPassword; + } } -// Inherit from EventEmitter -SessionStore.prototype = EventEmitter.prototype; - -SessionStore.prototype._update = function() { - // Persist state to localStorage - if (this._state.cachedPassword) { - localStorage.setItem('mx_pass', this._state.cachedPassword); - } else { - localStorage.removeItem('mx_pass', this._state.cachedPassword); - } - - this.emit('update'); -}; - -SessionStore.prototype._setState = function(newState) { - this._state = Object.assign(this._state, newState); - this._update(); -}; - -SessionStore.prototype._onAction = function(payload) { - switch (payload.action) { - case 'cached_password': - this._setState({ - cachedPassword: payload.cachedPassword, - }); - break; - case 'password_changed': - this._setState({ - cachedPassword: null, - }); - break; - } -}; - -SessionStore.prototype.getCachedPassword = function() { - return this._state.cachedPassword; -}; - // Export singleton getter let singletonSessionStore = null; if (!singletonSessionStore) { From 2b4c87aca6eac0d32081624093a1f25fd0683621 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 12 May 2017 16:02:38 +0100 Subject: [PATCH 019/127] Remove useless comment --- src/stores/SessionStore.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/stores/SessionStore.js b/src/stores/SessionStore.js index bf605d7f07..d3370d2df3 100644 --- a/src/stores/SessionStore.js +++ b/src/stores/SessionStore.js @@ -54,7 +54,6 @@ class SessionStore extends EventEmitter { } } -// Export singleton getter let singletonSessionStore = null; if (!singletonSessionStore) { singletonSessionStore = new SessionStore(); From 683f1b8a1ac2cd108d61413414431776d3f10517 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 12 May 2017 17:39:38 +0100 Subject: [PATCH 020/127] Invite the welcome user after registration if configured This will shift focus to the welcome user DM. We probably don't want to do this for teams, but I shall leave that for another PR that fixes teams WRT to new-guest-access. --- src/components/structures/MatrixChat.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index d63bf897c9..ee3c601146 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -578,7 +578,7 @@ module.exports = React.createClass({ this.setState({loggingIn: true}); break; case 'on_logged_in': - this._onLoggedIn(payload.teamToken, payload.isPasswordStored); + this._onLoggedIn(payload.teamToken); break; case 'on_logged_out': this._onLoggedOut(); @@ -801,8 +801,12 @@ module.exports = React.createClass({ this._teamToken = teamToken; dis.dispatch({action: 'view_home_page'}); } else if (this._is_registered) { + if (this.props.config.welcomeUserId) { + createRoom({dmUserId: this.props.config.welcomeUserId}); + return; + } // The user has just logged in after registering - dis.dispatch({action: 'view_user_settings'}); + dis.dispatch({action: 'view_room_directory'}); } else { this._showScreenAfterLogin(); } From da3cb0ee48aaea420a31fd9e060e159dfd7e910f Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 15 May 2017 14:52:19 +0100 Subject: [PATCH 021/127] SessionStore extends flux.Store --- src/components/structures/MatrixChat.js | 2 +- .../views/settings/ChangePassword.js | 10 ++++----- src/stores/SessionStore.js | 21 ++++++++++++------- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index b8b3f51422..4556148986 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -252,7 +252,7 @@ module.exports = React.createClass({ } this._sessionStore = sessionStore; - this._sessionStore.on('update', this._setStateFromSessionStore); + this._sessionStore.addListener(this._setStateFromSessionStore); this._setStateFromSessionStore(); }, diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js index c20bc47152..4d8373bc52 100644 --- a/src/components/views/settings/ChangePassword.js +++ b/src/components/views/settings/ChangePassword.js @@ -68,15 +68,15 @@ module.exports = React.createClass({ }, componentWillMount: function() { - this.sessionStore = sessionStore; - this.sessionStore.on('update', this.setStateFromSessionStore); + this._sessionStore = sessionStore; + this._sessionStore.addListener(this._setStateFromSessionStore); - this.setStateFromSessionStore(); + this._setStateFromSessionStore(); }, - setStateFromSessionStore: function() { + _setStateFromSessionStore: function() { this.setState({ - cachedPassword: this.sessionStore.getCachedPassword(), + cachedPassword: this._sessionStore.getCachedPassword(), }); }, diff --git a/src/stores/SessionStore.js b/src/stores/SessionStore.js index d3370d2df3..1570f58688 100644 --- a/src/stores/SessionStore.js +++ b/src/stores/SessionStore.js @@ -1,21 +1,26 @@ import dis from '../dispatcher'; -import EventEmitter from 'events'; +import {Store} from 'flux/utils'; /** * A class for storing application state to do with the session. This is a simple flux * store that listens for actions and updates its state accordingly, informing any - * listeners (views) of state changes via the 'update' event. + * listeners (views) of state changes. + * + * Usage: + * ``` + * sessionStore.addListener(() => { + * this.setState({ cachedPassword: sessionStore.getCachedPassword() }) + * }) + * ``` */ -class SessionStore extends EventEmitter { +class SessionStore extends Store { constructor() { - super(); + super(dis); // Initialise state this._state = { cachedPassword: localStorage.getItem('mx_pass'), }; - - dis.register(this._onAction.bind(this)); } _update() { @@ -26,7 +31,7 @@ class SessionStore extends EventEmitter { localStorage.removeItem('mx_pass', this._state.cachedPassword); } - this.emit('update'); + this.__emitChange(); } _setState(newState) { @@ -34,7 +39,7 @@ class SessionStore extends EventEmitter { this._update(); } - _onAction(payload) { + __onDispatch(payload) { switch (payload.action) { case 'cached_password': this._setState({ From f73cf772fb83db136cd44fd3cc40e50aec83905e Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 15 May 2017 14:56:05 +0100 Subject: [PATCH 022/127] Move sessionStore ref from MatrixChat to LoggedInView MatrixChat didn't actually use the sessionStore, so this is one less prop to pass. --- src/components/structures/LoggedInView.js | 17 ++++++++++++----- src/components/structures/MatrixChat.js | 12 ------------ 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index 0851c01a18..240a3499a2 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -23,6 +23,7 @@ import Notifier from '../../Notifier'; import PageTypes from '../../PageTypes'; import sdk from '../../index'; import dis from '../../dispatcher'; +import sessionStore from '../../stores/SessionStore'; /** * This is what our MatrixChat shows when we are logged in. The precise view is @@ -49,10 +50,6 @@ export default React.createClass({ teamToken: React.PropTypes.string, - // Has the user generated a password that is stored in local storage? - // (are they a PWLU?) - userHasGeneratedPassword: React.PropTypes.bool, - // and lots and lots of other stuff. }, @@ -80,6 +77,10 @@ export default React.createClass({ this._scrollStateMap = {}; document.addEventListener('keydown', this._onKeyDown); + + this._sessionStore = sessionStore; + this._sessionStore.addListener(this._setStateFromSessionStore); + this._setStateFromSessionStore(); }, componentWillUnmount: function() { @@ -97,6 +98,12 @@ export default React.createClass({ return this.refs.roomView.canResetTimeline(); }, + _setStateFromSessionStore() { + this.setState({ + userHasGeneratedPassword: Boolean(this._sessionStore.getCachedPassword()), + }); + }, + _onKeyDown: function(ev) { /* // Remove this for now as ctrl+alt = alt-gr so this breaks keyboards which rely on alt-gr for numbers @@ -257,7 +264,7 @@ export default React.createClass({ />; } else if (this.props.matrixClient.isGuest()) { topBar = ; - } else if (this.props.userHasGeneratedPassword) { + } else if (this.state.userHasGeneratedPassword) { topBar = ; } else if (Notifier.supportsDesktopNotifications() && !Notifier.isEnabled() && !Notifier.isToolbarHidden()) { topBar = ; diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 4556148986..45b4d07055 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -40,8 +40,6 @@ var PageTypes = require('../../PageTypes'); var createRoom = require("../../createRoom"); import * as UDEHandler from '../../UnknownDeviceErrorHandler'; -import sessionStore from '../../stores/SessionStore'; - module.exports = React.createClass({ displayName: 'MatrixChat', @@ -250,10 +248,6 @@ module.exports = React.createClass({ register_hs_url: paramHs, }); } - - this._sessionStore = sessionStore; - this._sessionStore.addListener(this._setStateFromSessionStore); - this._setStateFromSessionStore(); }, componentDidMount: function() { @@ -897,12 +891,6 @@ module.exports = React.createClass({ }); }, - _setStateFromSessionStore() { - this.setState({ - userHasGeneratedPassword: Boolean(this._sessionStore.getCachedPassword()), - }); - }, - onFocus: function(ev) { dis.dispatch({action: 'focus_composer'}); }, From eb0041d21ab603e3b0d1f0474068ded68f09dba7 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 15 May 2017 17:03:54 +0100 Subject: [PATCH 023/127] Remove redundant state --- src/components/structures/MatrixChat.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 45b4d07055..c5b58c3285 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -138,8 +138,6 @@ module.exports = React.createClass({ register_hs_url: null, register_is_url: null, register_id_sid: null, - - userHasGeneratedPassword: false, }; return s; }, @@ -1171,7 +1169,6 @@ module.exports = React.createClass({ onUserSettingsClose={this.onUserSettingsClose} onRegistered={this.onRegistered} teamToken={this._teamToken} - userHasGeneratedPassword={this.state.userHasGeneratedPassword} {...this.props} {...this.state} /> From 269fd511300bc001604d70a02e96e6e8bef1c283 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 15 May 2017 17:17:32 +0100 Subject: [PATCH 024/127] Remove SessionStore listener on unmount --- src/components/structures/LoggedInView.js | 7 ++++++- src/components/views/settings/ChangePassword.js | 10 +++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index 240a3499a2..bbbf6dff0e 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -79,12 +79,17 @@ export default React.createClass({ document.addEventListener('keydown', this._onKeyDown); this._sessionStore = sessionStore; - this._sessionStore.addListener(this._setStateFromSessionStore); + this._removeSSListener = this._sessionStore.addListener( + this._setStateFromSessionStore, + ).remove; this._setStateFromSessionStore(); }, componentWillUnmount: function() { document.removeEventListener('keydown', this._onKeyDown); + if (this._removeSSListener) { + this._removeSSListener(); + } }, getScrollStateForRoom: function(roomId) { diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js index 4d8373bc52..07680818df 100644 --- a/src/components/views/settings/ChangePassword.js +++ b/src/components/views/settings/ChangePassword.js @@ -69,11 +69,19 @@ module.exports = React.createClass({ componentWillMount: function() { this._sessionStore = sessionStore; - this._sessionStore.addListener(this._setStateFromSessionStore); + this._removeSSListener = this._sessionStore.addListener( + this._setStateFromSessionStore, + ).remove; this._setStateFromSessionStore(); }, + componentWillUnmount: function() { + if (this._removeSSListener) { + this._removeSSListener(); + } + }, + _setStateFromSessionStore: function() { this.setState({ cachedPassword: this._sessionStore.getCachedPassword(), From f199f3599ea8231a3d68ef1dd1b6adeac28e7329 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 15 May 2017 17:31:26 +0100 Subject: [PATCH 025/127] Replace NeedToRegisterDialog /w SetMxIdDialog This uses MatrixChat's `view_set_mxid` --- src/components/structures/RoomView.js | 12 +-- src/components/structures/UserSettings.js | 12 +-- .../views/dialogs/ChatInviteDialog.js | 6 +- .../views/dialogs/NeedToRegisterDialog.js | 78 ------------------- .../views/room_settings/ColorSettings.js | 8 +- src/components/views/rooms/MemberInfo.js | 6 +- src/components/views/rooms/MessageComposer.js | 6 +- src/createRoom.js | 7 +- 8 files changed, 11 insertions(+), 124 deletions(-) delete mode 100644 src/components/views/dialogs/NeedToRegisterDialog.js diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 3cbe76b289..92049bb113 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -869,11 +869,7 @@ module.exports = React.createClass({ MatrixClientPeg.get().isGuest() ) ) { - var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog"); - Modal.createDialog(NeedToRegisterDialog, { - title: "Failed to join the room", - description: "This room is private or inaccessible to guests. You may be able to join if you register." - }); + dis.dispatch({action: 'view_set_mxid'}); } else { var msg = error.message ? error.message : JSON.stringify(error); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); @@ -933,11 +929,7 @@ module.exports = React.createClass({ uploadFile: function(file) { if (MatrixClientPeg.get().isGuest()) { - var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog"); - Modal.createDialog(NeedToRegisterDialog, { - title: "Please Register", - description: "Guest users can't upload files. Please register to upload." - }); + dis.dispatch({action: 'view_set_mxid'}); return; } diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 46dce8bd2e..96c60d7cd8 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -245,11 +245,7 @@ module.exports = React.createClass({ onAvatarPickerClick: function(ev) { if (MatrixClientPeg.get().isGuest()) { - const NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog"); - Modal.createDialog(NeedToRegisterDialog, { - title: "Please Register", - description: "Guests can't set avatars. Please register.", - }); + dis.dispatch({action: 'view_set_mxid'}); return; } @@ -700,11 +696,7 @@ module.exports = React.createClass({ onChange={(e) => { if (MatrixClientPeg.get().isGuest()) { e.target.checked = false; - const NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog"); - Modal.createDialog(NeedToRegisterDialog, { - title: "Please Register", - description: "Guests can't use labs features. Please register.", - }); + dis.dispatch({action: 'view_set_mxid'}); return; } diff --git a/src/components/views/dialogs/ChatInviteDialog.js b/src/components/views/dialogs/ChatInviteDialog.js index 7ba503099a..06c029287f 100644 --- a/src/components/views/dialogs/ChatInviteDialog.js +++ b/src/components/views/dialogs/ChatInviteDialog.js @@ -284,11 +284,7 @@ module.exports = React.createClass({ _startChat: function(addrs) { if (MatrixClientPeg.get().isGuest()) { - var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog"); - Modal.createDialog(NeedToRegisterDialog, { - title: "Please Register", - description: "Guest users can't invite users. Please register." - }); + dis.dispatch({action: 'view_set_mxid'}); return; } diff --git a/src/components/views/dialogs/NeedToRegisterDialog.js b/src/components/views/dialogs/NeedToRegisterDialog.js deleted file mode 100644 index f4df5913d5..0000000000 --- a/src/components/views/dialogs/NeedToRegisterDialog.js +++ /dev/null @@ -1,78 +0,0 @@ -/* -Copyright 2016 OpenMarket 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. -*/ - -/* - * Usage: - * Modal.createDialog(NeedToRegisterDialog, { - * title: "some text", (default: "Registration required") - * description: "some more text", - * onFinished: someFunction, - * }); - */ - -import React from 'react'; -import dis from '../../../dispatcher'; -import sdk from '../../../index'; - -module.exports = React.createClass({ - displayName: 'NeedToRegisterDialog', - propTypes: { - title: React.PropTypes.string, - description: React.PropTypes.oneOfType([ - React.PropTypes.element, - React.PropTypes.string, - ]), - onFinished: React.PropTypes.func.isRequired, - }, - - getDefaultProps: function() { - return { - title: "Registration required", - description: "A registered account is required for this action", - }; - }, - - onRegisterClicked: function() { - dis.dispatch({ - action: "start_upgrade_registration", - }); - if (this.props.onFinished) { - this.props.onFinished(); - } - }, - - render: function() { - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - return ( - -
- {this.props.description} -
-
- - -
-
- ); - }, -}); diff --git a/src/components/views/room_settings/ColorSettings.js b/src/components/views/room_settings/ColorSettings.js index 6a455d9c3c..5fc845a541 100644 --- a/src/components/views/room_settings/ColorSettings.js +++ b/src/components/views/room_settings/ColorSettings.js @@ -21,6 +21,8 @@ var Tinter = require('../../../Tinter'); var MatrixClientPeg = require("../../../MatrixClientPeg"); var Modal = require("../../../Modal"); +import dis from '../../../dispatcher'; + var ROOM_COLORS = [ // magic room default values courtesy of Ribot ["#76cfa6", "#eaf5f0"], @@ -86,11 +88,7 @@ module.exports = React.createClass({ } ).catch(function(err) { if (err.errcode == 'M_GUEST_ACCESS_FORBIDDEN') { - var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog"); - Modal.createDialog(NeedToRegisterDialog, { - title: "Please Register", - description: "Saving room color settings is only available to registered users" - }); + dis.dispatch({action: 'view_set_mxid'}); } }); } diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 1a9a8d5e0f..1f286e9e12 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -374,11 +374,7 @@ module.exports = WithMatrixClient(React.createClass({ console.log("Mod toggle success"); }, function(err) { if (err.errcode == 'M_GUEST_ACCESS_FORBIDDEN') { - var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog"); - Modal.createDialog(NeedToRegisterDialog, { - title: "Please Register", - description: "This action cannot be performed by a guest user. Please register to be able to do this." - }); + dis.dispatch({action: 'view_set_mxid'}); } else { console.error("Toggle moderator error:" + err); Modal.createDialog(ErrorDialog, { diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 0ee3c2082d..df7d0c3640 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -90,11 +90,7 @@ export default class MessageComposer extends React.Component { onUploadClick(ev) { if (MatrixClientPeg.get().isGuest()) { - let NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog"); - Modal.createDialog(NeedToRegisterDialog, { - title: "Please Register", - description: "Guest users can't upload files. Please register to upload.", - }); + dis.dispatch({action: 'view_set_mxid'}); return; } diff --git a/src/createRoom.js b/src/createRoom.js index 674fe23d28..72f4016502 100644 --- a/src/createRoom.js +++ b/src/createRoom.js @@ -41,12 +41,7 @@ function createRoom(opts) { const client = MatrixClientPeg.get(); if (client.isGuest()) { - setTimeout(()=>{ - Modal.createDialog(NeedToRegisterDialog, { - title: "Please Register", - description: "Guest users can't create new rooms. Please register to create room and start a chat." - }); - }, 0); + dis.dispatch({action: 'view_set_mxid'}); return q(null); } From 93ecdc90a9741dbdc5ad9aa6e70ed4d3743217fb Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 16 May 2017 11:45:01 +0100 Subject: [PATCH 026/127] Make confirmation optional on ChangePassword Add option to disable password change confirmation (`disabledConfirmation`). Style fixes, use `
+ onClick={this.onClickChange} + element="button"> Change Password
From f7e6a996c5e6720eadfc7065ef81e9e959d774d3 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 16 May 2017 11:51:09 +0100 Subject: [PATCH 027/127] Add proptype --- src/components/views/settings/ChangePassword.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js index a5e695a1ff..e8a07fd225 100644 --- a/src/components/views/settings/ChangePassword.js +++ b/src/components/views/settings/ChangePassword.js @@ -31,7 +31,8 @@ module.exports = React.createClass({ rowClassName: React.PropTypes.string, rowLabelClassName: React.PropTypes.string, rowInputClassName: React.PropTypes.string, - buttonClassName: React.PropTypes.string + buttonClassName: React.PropTypes.string, + disableConfirmation: React.PropTypes.bool, }, Phases: { From eb36e979c2591f252fb007bc2460b9d1b2dcbd6a Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 16 May 2017 11:52:51 +0100 Subject: [PATCH 028/127] Reference store token, call .remove on it on unmount --- src/components/structures/LoggedInView.js | 8 ++++---- src/components/views/settings/ChangePassword.js | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index bbbf6dff0e..a64ae0a25c 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -79,16 +79,16 @@ export default React.createClass({ document.addEventListener('keydown', this._onKeyDown); this._sessionStore = sessionStore; - this._removeSSListener = this._sessionStore.addListener( + this._sessionStoreToken = this._sessionStore.addListener( this._setStateFromSessionStore, - ).remove; + ); this._setStateFromSessionStore(); }, componentWillUnmount: function() { document.removeEventListener('keydown', this._onKeyDown); - if (this._removeSSListener) { - this._removeSSListener(); + if (this._sessionStoreToken) { + this._sessionStoreToken.remove(); } }, diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js index 07680818df..e3845390de 100644 --- a/src/components/views/settings/ChangePassword.js +++ b/src/components/views/settings/ChangePassword.js @@ -69,16 +69,16 @@ module.exports = React.createClass({ componentWillMount: function() { this._sessionStore = sessionStore; - this._removeSSListener = this._sessionStore.addListener( + this._sessionStoreToken = this._sessionStore.addListener( this._setStateFromSessionStore, - ).remove; + ); this._setStateFromSessionStore(); }, componentWillUnmount: function() { - if (this._removeSSListener) { - this._removeSSListener(); + if (this._sessionStoreToken) { + this._sessionStoreToken.remove(); } }, From 633c6b39f6a1dd98613898287499ec7696a95099 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 16 May 2017 11:58:37 +0100 Subject: [PATCH 029/127] Add comment to Lifecycle --- src/Lifecycle.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Lifecycle.js b/src/Lifecycle.js index decb544b3c..20d5836dae 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -185,6 +185,14 @@ function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) { // returns a promise which resolves to true if a session is found in // localstorage +// +// N.B. Lifecycle.js should not maintain any further localStorage state, we +// are moving towards using SessionStore to keep track of state related +// to the current session (which is typically backed by localStorage). +// +// The plan is to gradually move the localStorage access done here into +// SessionStore to avoid bugs where the view becomes out-of-sync with +// localStorage (e.g. teamToken, isGuest etc.) function _restoreFromLocalStorage() { if (!localStorage) { return q(false); From 5a3c32044e764bb56ec4d4bf1dd413e4bd9bd2f9 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 16 May 2017 12:45:14 +0100 Subject: [PATCH 030/127] disableConfirmation -> confirm --- src/components/views/settings/ChangePassword.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js index e8a07fd225..257e0ac056 100644 --- a/src/components/views/settings/ChangePassword.js +++ b/src/components/views/settings/ChangePassword.js @@ -32,7 +32,7 @@ module.exports = React.createClass({ rowLabelClassName: React.PropTypes.string, rowInputClassName: React.PropTypes.string, buttonClassName: React.PropTypes.string, - disableConfirmation: React.PropTypes.bool, + confirm: React.PropTypes.bool, }, Phases: { @@ -55,7 +55,8 @@ module.exports = React.createClass({ error: "Passwords can't be empty" }; } - } + }, + confirm: true, }; }, @@ -68,7 +69,7 @@ module.exports = React.createClass({ changePassword: function(oldPassword, newPassword) { const cli = MatrixClientPeg.get(); - if (this.props.disableConfirmation) { + if (!this.props.confirm) { this._changePassword(cli, oldPassword, newPassword); return; } From 2c5fb01f03b9ac5e98f2e488c1a6acb26c7e5d22 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 16 May 2017 14:13:22 +0100 Subject: [PATCH 031/127] Fix bugs introduced by dodgy merge --- src/components/views/settings/ChangePassword.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js index 422761601d..601b774932 100644 --- a/src/components/views/settings/ChangePassword.js +++ b/src/components/views/settings/ChangePassword.js @@ -90,7 +90,7 @@ module.exports = React.createClass({ }); }, - changePassword: function(old_password, new_password) { + changePassword: function(oldPassword, newPassword) { const cli = MatrixClientPeg.get(); if (!this.props.confirm) { @@ -158,7 +158,7 @@ module.exports = React.createClass({ }, onClickChange: function() { - const oldPassword = this.refs.old_input.value; + const oldPassword = this.state.cachedPassword || this.refs.old_input.value; const newPassword = this.refs.new_input.value; const confirmPassword = this.refs.confirm_input.value; const err = this.props.onCheckPassword( From ca907f42dc980e97c764b2f535dd26e8615ec066 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 16 May 2017 14:24:24 +0100 Subject: [PATCH 032/127] Fix redundant getComponent --- src/createRoom.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/createRoom.js b/src/createRoom.js index 72f4016502..e18f9cf032 100644 --- a/src/createRoom.js +++ b/src/createRoom.js @@ -36,7 +36,6 @@ function createRoom(opts) { opts = opts || {}; const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - const NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog"); const Loader = sdk.getComponent("elements.Spinner"); const client = MatrixClientPeg.get(); From e1089574ae02bee6036cb745513c026381558f43 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 17 May 2017 09:46:17 +0100 Subject: [PATCH 033/127] Write some tests for the RTS UI Add tests that make assertions about the UI during registration when registration is done with a user recognised as a team member (by the mock rtsClient). --- .../structures/login/Registration.js | 3 +- .../structures/login/Registration-test.js | 105 ++++++++++++++++++ .../views/login/RegistrationForm-test.js | 86 ++++++++++++++ test/test-utils.js | 14 +++ 4 files changed, 206 insertions(+), 2 deletions(-) create mode 100644 test/components/structures/login/Registration-test.js create mode 100644 test/components/views/login/RegistrationForm-test.js diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index 5501a39b58..5eecfa5ff6 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -98,7 +98,7 @@ module.exports = React.createClass({ this.props.teamServerConfig.teamServerURL && !this._rtsClient ) { - this._rtsClient = new RtsClient(this.props.teamServerConfig.teamServerURL); + this._rtsClient = this.props.rtsClient || new RtsClient(this.props.teamServerConfig.teamServerURL); this.setState({ teamServerBusy: true, @@ -221,7 +221,6 @@ module.exports = React.createClass({ } trackPromise.then((teamToken) => { - console.info('Team token promise',teamToken); this.props.onLoggedIn({ userId: response.user_id, deviceId: response.device_id, diff --git a/test/components/structures/login/Registration-test.js b/test/components/structures/login/Registration-test.js new file mode 100644 index 0000000000..b4b54a6315 --- /dev/null +++ b/test/components/structures/login/Registration-test.js @@ -0,0 +1,105 @@ +/* +Copyright 2017 Vector Creations 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. +*/ + +const React = require('react'); +const ReactDOM = require('react-dom'); +const ReactTestUtils = require('react-addons-test-utils'); +const expect = require('expect'); + +const testUtils = require('test-utils'); + +const sdk = require('matrix-react-sdk'); +const Registration = sdk.getComponent('structures.login.Registration'); + +let rtsClient; +let client; + +const TEAM_CONFIG = { + supportEmail: 'support@some.domain', + teamServerURL: 'http://someteamserver.bla', +}; + +const CREDENTIALS = {userId: '@me:here'}; +const MOCK_REG_RESPONSE = { + user_id: CREDENTIALS.userId, + device_id: 'mydevice', + access_token: '2234569864534231', +}; + +describe('Registration', function() { + beforeEach(function() { + testUtils.beforeEach(this); + client = testUtils.createTestClient(); + client.credentials = CREDENTIALS; + + // Mock an RTS client that supports one team and naively returns team tokens when + // tracking by mapping email SIDs to team tokens. This is fine because we only + // want to assert the client behaviour such that a user recognised by the + // rtsClient (which would normally talk to the RTS server) as a team member is + // correctly logged in as one (and other such assertions). + rtsClient = testUtils.createTestRtsClient( + { + 'myawesometeam123': { + name: 'Team Awesome', + domain: 'team.awesome.net', + }, + }, + {'someEmailSid1234': 'myawesometeam123'}, + ); + }); + + it('should track a referral following successful registration of a team member', function(done) { + const expectedCreds = { + userId: MOCK_REG_RESPONSE.user_id, + deviceId: MOCK_REG_RESPONSE.device_id, + homeserverUrl: client.getHomeserverUrl(), + identityServerUrl: client.getIdentityServerUrl(), + accessToken: MOCK_REG_RESPONSE.access_token, + }; + const onLoggedIn = function(creds, teamToken) { + expect(creds).toEqual(expectedCreds); + expect(teamToken).toBe('myawesometeam123'); + done(); + }; + + const res = ReactTestUtils.renderIntoDocument( + , + ); + + res._onUIAuthFinished(true, MOCK_REG_RESPONSE, {emailSid: 'someEmailSid1234'}); + }); + + 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(); + done(); + }); + + const res = ReactTestUtils.renderIntoDocument( + , + ); + + res._onUIAuthFinished(true, MOCK_REG_RESPONSE, {emailSid: 'someOtherEmailSid11'}); + }); +}); diff --git a/test/components/views/login/RegistrationForm-test.js b/test/components/views/login/RegistrationForm-test.js new file mode 100644 index 0000000000..81db5b487b --- /dev/null +++ b/test/components/views/login/RegistrationForm-test.js @@ -0,0 +1,86 @@ +/* +Copyright 2017 Vector Creations 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. +*/ + +const React = require('react'); +const ReactDOM = require("react-dom"); +const ReactTestUtils = require('react-addons-test-utils'); +const expect = require('expect'); + +const testUtils = require('test-utils'); + +const sdk = require('matrix-react-sdk'); +const RegistrationForm = sdk.getComponent('views.login.RegistrationForm'); + +const TEAM_CONFIG = { + supportEmail: "support@some.domain", + teams: [ + { name: "The Team Org.", domain: "team.ac.uk" }, + { name: "The Super Team", domain: "superteam.ac.uk" }, + ], +}; + +function doInputEmail(inputEmail, onTeamSelected) { + const res = ReactTestUtils.renderIntoDocument( + , + ); + + const teamInput = res.refs.email; + teamInput.value = inputEmail; + + ReactTestUtils.Simulate.change(teamInput); + ReactTestUtils.Simulate.blur(teamInput); + + return res; +} + +function expectTeamSelectedFromEmailInput(inputEmail, expectedTeam) { + const onTeamSelected = expect.createSpy(); + doInputEmail(inputEmail, onTeamSelected); + + expect(onTeamSelected).toHaveBeenCalledWith(expectedTeam); +} + +function expectSupportFromEmailInput(inputEmail, isSupportShown) { + const onTeamSelected = expect.createSpy(); + const res = doInputEmail(inputEmail, onTeamSelected); + + expect(res.state.showSupportEmail).toBe(isSupportShown); +} + +describe('RegistrationForm', function() { + beforeEach(function() { + testUtils.beforeEach(this); + }); + + it('should select a team when a team email is entered', function() { + expectTeamSelectedFromEmailInput("member@team.ac.uk", TEAM_CONFIG.teams[0]); + }); + + it('should not select a team when an unrecognised team email is entered', function() { + expectTeamSelectedFromEmailInput("member@someunknownteam.ac.uk", null); + }); + + it('should show support when an unrecognised team email is entered', function() { + expectSupportFromEmailInput("member@someunknownteam.ac.uk", true); + }); + + it('should NOT show support when an unrecognised non-team email is entered', function() { + expectSupportFromEmailInput("someone@yahoo.com", false); + }); +}); diff --git a/test/test-utils.js b/test/test-utils.js index 9f404f98eb..2c866d345c 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -137,6 +137,20 @@ export function createTestClient() { }; } +export function createTestRtsClient(teamMap, sidMap) { + return { + getTeamsConfig() { + return q(Object.keys(teamMap).map((token) => teamMap[token])); + }, + trackReferral(referrer, emailSid, clientSecret) { + return q({team_token: sidMap[emailSid]}); + }, + getTeam(teamToken) { + return q(teamMap[teamToken]); + }, + }; +} + /** * Create an Event. * @param {Object} opts Values for the event. From 96c3bf56f8dbbbcd242bee9292ef87085414c63e Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 19 May 2017 09:43:56 +0100 Subject: [PATCH 034/127] Implement warm-fuzzy success dialog for SetMxIdDialog --- src/components/views/dialogs/SetMxIdDialog.js | 38 ++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/src/components/views/dialogs/SetMxIdDialog.js b/src/components/views/dialogs/SetMxIdDialog.js index 86b5fccbc2..d9d07d517b 100644 --- a/src/components/views/dialogs/SetMxIdDialog.js +++ b/src/components/views/dialogs/SetMxIdDialog.js @@ -53,6 +53,9 @@ export default React.createClass({ doingUIAuth: false, // Indicate error with auth authError: '', + + // Indicate success of setting mxid + success: false, }; }, @@ -95,6 +98,10 @@ export default React.createClass({ }); }, + onSuccessContinue: function() { + this.props.onFinished(true, this._registeredCreds); + }, + _doUsernameCheck: function() { // Check if username is available return this._matrixClient.isUsernameAvailable(this.state.username).then( @@ -162,7 +169,7 @@ export default React.createClass({ // XXX Implement RTS /register here const teamToken = null; - this.props.onFinished(true, { + this._registeredCreds = { userId: response.user_id, deviceId: response.device_id, homeserverUrl: this._matrixClient.getHomeserverUrl(), @@ -170,6 +177,11 @@ export default React.createClass({ accessToken: response.access_token, password: this._generatedPassword, teamToken: teamToken, + }; + + // Before continuing, show a warm-fuzzy success and only submit onSuccessContinue + this.setState({ + success: true, }); }, @@ -219,6 +231,30 @@ export default React.createClass({ !this.state.usernameError && !this.state.usernameBusy; + if (this.state.success) { + return ( + +
+

+ You have successfully + picked { this.state.username } as your + username and you now have access to the full + set of features on Riot. +

+
+
+ +
+
+ ); + } + return ( Date: Mon, 22 May 2017 14:46:49 +0100 Subject: [PATCH 035/127] Add prop to toggle whether new password input is autoFocused --- src/components/views/settings/ChangePassword.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js index 601b774932..bfc9ac264e 100644 --- a/src/components/views/settings/ChangePassword.js +++ b/src/components/views/settings/ChangePassword.js @@ -35,6 +35,8 @@ module.exports = React.createClass({ rowInputClassName: React.PropTypes.string, buttonClassName: React.PropTypes.string, confirm: React.PropTypes.bool, + // Whether to autoFocus the new password input + autoFocusNewPasswordInput: React.PropTypes.bool, }, Phases: { @@ -199,7 +201,7 @@ module.exports = React.createClass({
- +
From b0a824c94190c317f6c18a238d6f3e04727d58a6 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 22 May 2017 16:28:23 +0100 Subject: [PATCH 036/127] Remove double declaration of TextInputDialog --- src/components/structures/MatrixChat.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 85c12979f6..59ce1b622d 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -439,7 +439,6 @@ module.exports = React.createClass({ break; } - var TextInputDialog = sdk.getComponent("dialogs.TextInputDialog"); Modal.createDialog(TextInputDialog, { title: "Create Room", description: "Room name (optional)", From 298c5e4df32d87b0707b9f71ff489e12b800fe15 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 24 May 2017 16:56:13 +0100 Subject: [PATCH 037/127] Implement a store for RoomView This allows for a truely flux-y way of storing the currently viewed room, making some callbacks (like onRoomIdResolved) redundant and making sure that the currently viewed room (ID) is only stored in one place as opposed to the previous many places. This was required for the `join_room` action which can be dispatched to join the currently viewed room. Another change was to introduce `LifeCycleStore` which is a start at encorporating state related to the lifecycle of the app into a flux store. Currently it only contains an action which will be dispatched when the sync state has become PREPARED. This was necessary to do a deferred dispatch of `join_room` following the registration of a PWLU (PassWord-Less User). The following actions are introduced: - RoomViewStore: - `view_room`: dispatch to change the currently viewed room ID - `join_room`: dispatch to join the currently viewed room - LifecycleStore: - `do_after_sync_prepared`: dispatch to store an action which will be dispatched when `sync_state` is dispatched with `state = 'PREPARED'` - MatrixChat: - `sync_state`: dispatched when the sync state changes. Ideally there'd be a SyncStateStore that emitted an `update` upon receiving this, but for now the `LifecycleStore` will listen for `sync_state` directly. --- src/components/structures/LoggedInView.js | 5 +- src/components/structures/MatrixChat.js | 38 ++-- src/components/structures/RoomView.js | 212 ++++++------------ src/components/views/dialogs/SetMxIdDialog.js | 1 + src/createRoom.js | 15 +- src/stores/LifecycleStore.js | 73 ++++++ src/stores/RoomViewStore.js | 145 ++++++++++++ src/stores/SessionStore.js | 15 ++ test/components/structures/RoomView-test.js | 67 ------ test/stores/RoomViewStore-test.js | 56 +++++ test/test-utils.js | 13 +- 11 files changed, 399 insertions(+), 241 deletions(-) create mode 100644 src/stores/LifecycleStore.js create mode 100644 src/stores/RoomViewStore.js delete mode 100644 test/components/structures/RoomView-test.js create mode 100644 test/stores/RoomViewStore-test.js diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index e559a21e1a..df24fbb33b 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -40,7 +40,6 @@ export default React.createClass({ propTypes: { matrixClient: React.PropTypes.instanceOf(Matrix.MatrixClient).isRequired, page_type: React.PropTypes.string.isRequired, - onRoomIdResolved: React.PropTypes.func, onRoomCreated: React.PropTypes.func, onUserSettingsClose: React.PropTypes.func, @@ -190,16 +189,14 @@ export default React.createClass({ case PageTypes.RoomView: page_element = { modal.close(); - if (this.currentRoomId === roomId) { + if (this.state.currentRoomId === roomId) { dis.dispatch({action: 'view_next_room'}); } }, (err) => { @@ -807,8 +808,12 @@ module.exports = React.createClass({ this._teamToken = teamToken; dis.dispatch({action: 'view_home_page'}); } else if (this._is_registered) { + this._is_registered = false; if (this.props.config.welcomeUserId) { - createRoom({dmUserId: this.props.config.welcomeUserId}); + createRoom({ + dmUserId: this.props.config.welcomeUserId, + andView: false, + }); return; } // The user has just logged in after registering @@ -853,7 +858,6 @@ module.exports = React.createClass({ ready: false, collapse_lhs: false, collapse_rhs: false, - currentRoomAlias: null, currentRoomId: null, page_type: PageTypes.RoomDirectory, }); @@ -891,6 +895,7 @@ module.exports = React.createClass({ }); cli.on('sync', function(state, prevState) { + dis.dispatch({action: 'sync_state', prevState, state}); self.updateStatusIndicator(state, prevState); if (state === "SYNCING" && prevState === "SYNCING") { return; @@ -1102,6 +1107,8 @@ module.exports = React.createClass({ }, onRegistered: function(credentials, teamToken) { + // XXX: These both should be in state or ideally store(s) because we risk not + // rendering the most up-to-date view of state otherwise. // teamToken may not be truthy this._teamToken = teamToken; this._is_registered = true; @@ -1163,13 +1170,6 @@ module.exports = React.createClass({ } }, - onRoomIdResolved: function(roomId) { - // It's the RoomView's resposibility to look up room aliases, but we need the - // ID to pass into things like the Member List, so the Room View tells us when - // its done that resolution so we can display things that take a room ID. - this.setState({currentRoomId: roomId}); - }, - _makeRegistrationUrl: function(params) { if (this.props.startingFragmentQueryParams.referrer) { params.referrer = this.props.startingFragmentQueryParams.referrer; @@ -1211,10 +1211,10 @@ module.exports = React.createClass({ const LoggedInView = sdk.getComponent('structures.LoggedInView'); return ( { this.forceUpdate(); - } + }, }); - if (this.props.roomAddress[0] == '#') { - // we always look up the alias from the directory server: - // we want the room that the given alias is pointing to - // right now. We may have joined that alias before but there's - // no guarantee the alias hasn't subsequently been remapped. - MatrixClientPeg.get().getRoomIdForAlias(this.props.roomAddress).done((result) => { - if (this.props.onRoomIdResolved) { - this.props.onRoomIdResolved(result.room_id); - } - var room = MatrixClientPeg.get().getRoom(result.room_id); - this.setState({ - room: room, - roomId: result.room_id, - roomLoading: !room, - unsentMessageError: this._getUnsentMessageError(room), - }, this._onHaveRoom); - }, (err) => { - this.setState({ - roomLoading: false, - roomLoadError: err, - }); - }); - } else { - var room = MatrixClientPeg.get().getRoom(this.props.roomAddress); - this.setState({ - roomId: this.props.roomAddress, - room: room, - roomLoading: !room, - unsentMessageError: this._getUnsentMessageError(room), - }, this._onHaveRoom); + // Start listening for RoomViewStore updates + RoomViewStore.addListener(this._onRoomViewStoreUpdate); + this._onRoomViewStoreUpdate(true); + }, + + _onRoomViewStoreUpdate: function(initial) { + if (this.unmounted) { + return; } + this.setState({ + roomId: RoomViewStore.getRoomId(), + roomAlias: RoomViewStore.getRoomAlias(), + joining: RoomViewStore.isJoining(), + joinError: RoomViewStore.getJoinError(), + }, () => { + this._onHaveRoom(); + this.onRoom(MatrixClientPeg.get().getRoom(this.state.roomId)); + }); }, _onHaveRoom: function() { @@ -224,17 +202,17 @@ module.exports = React.createClass({ // NB. We peek if we are not in the room, although if we try to peek into // a room in which we have a member event (ie. we've left) synapse will just // send us the same data as we get in the sync (ie. the last events we saw). - var user_is_in_room = null; - if (this.state.room) { - user_is_in_room = this.state.room.hasMembershipState( - MatrixClientPeg.get().credentials.userId, 'join' + const room = MatrixClientPeg.get().getRoom(this.state.roomId); + let isUserJoined = null; + if (room) { + isUserJoined = room.hasMembershipState( + MatrixClientPeg.get().credentials.userId, 'join', ); - this._updateAutoComplete(); - this.tabComplete.loadEntries(this.state.room); + this._updateAutoComplete(room); + this.tabComplete.loadEntries(room); } - - if (!user_is_in_room && this.state.roomId) { + if (!isUserJoined && !this.state.joining && this.state.roomId) { if (this.props.autoJoin) { this.onJoinButtonClicked(); } else if (this.state.roomId) { @@ -260,9 +238,12 @@ module.exports = React.createClass({ } }).done(); } - } else if (user_is_in_room) { + } else if (isUserJoined) { MatrixClientPeg.get().stopPeeking(); - this._onRoomLoaded(this.state.room); + this.setState({ + unsentMessageError: this._getUnsentMessageError(room), + }); + this._onRoomLoaded(room); } }, @@ -299,10 +280,6 @@ module.exports = React.createClass({ }, componentWillReceiveProps: function(newProps) { - if (newProps.roomAddress != this.props.roomAddress) { - throw new Error("changing room on a RoomView is not supported"); - } - if (newProps.eventId != this.props.eventId) { // when we change focussed event id, hide the search results. this.setState({searchResults: null}); @@ -523,7 +500,7 @@ module.exports = React.createClass({ this._updatePreviewUrlVisibility(room); }, - _warnAboutEncryption: function (room) { + _warnAboutEncryption: function(room) { if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) { return; } @@ -604,20 +581,14 @@ module.exports = React.createClass({ }, onRoom: function(room) { - // This event is fired when the room is 'stored' by the JS SDK, which - // means it's now a fully-fledged room object ready to be used, so - // set it in our state and start using it (ie. init the timeline) - // This will happen if we start off viewing a room we're not joined, - // then join it whilst RoomView is looking at that room. - if (!this.state.room && room.roomId == this._joiningRoomId) { - this._joiningRoomId = undefined; - this.setState({ - room: room, - joining: false, - }); - - this._onRoomLoaded(room); + if (!room || room.roomId !== this.state.roomId) { + return; } + this.setState({ + room: room, + }, () => { + this._onRoomLoaded(room); + }); }, updateTint: function() { @@ -683,7 +654,7 @@ module.exports = React.createClass({ // refresh the tab complete list this.tabComplete.loadEntries(this.state.room); - this._updateAutoComplete(); + this._updateAutoComplete(this.state.room); // if we are now a member of the room, where we were not before, that // means we have finished joining a room we were previously peeking @@ -778,37 +749,43 @@ module.exports = React.createClass({ }, onJoinButtonClicked: function(ev) { - var self = this; - - var cli = MatrixClientPeg.get(); - var mxIdPromise = q(); + const cli = MatrixClientPeg.get(); // If the user is a ROU, allow them to transition to a PWLU if (cli && cli.isGuest()) { + // Join this room once the user has registered and logged in + dis.dispatch({ + action: 'do_after_sync_prepared', + deferred_action: { + action: 'join_room', + room_id: this.state.roomId, + }, + }); + const SetMxIdDialog = sdk.getComponent('views.dialogs.SetMxIdDialog'); - const defered = q.defer(); - mxIdPromise = defered.promise; const close = Modal.createDialog(SetMxIdDialog, { homeserverUrl: cli.getHomeserverUrl(), onFinished: (submitted, credentials) => { - if (!submitted) { - defered.reject(); - return; + if (submitted) { + this.props.onRegistered(credentials); } - this.props.onRegistered(credentials); - defered.resolve(); }, onDifferentServerClicked: (ev) => { dis.dispatch({action: 'start_registration'}); close(); }, }).close; + return; } - mxIdPromise.then(() => { - this.setState({ - joining: true + q().then(() => { + const signUrl = this.props.thirdPartyInvite ? + this.props.thirdPartyInvite.inviteSignUrl : undefined; + dis.dispatch({ + action: 'join_room', + opts: { inviteSignUrl: signUrl }, }); + // if this is an invite and has the 'direct' hint set, mark it as a DM room now. if (this.state.room) { const me = this.state.room.getMember(MatrixClientPeg.get().credentials.userId); @@ -820,65 +797,8 @@ module.exports = React.createClass({ } } } - return q(); - }).then(() => { - var sign_url = this.props.thirdPartyInvite ? this.props.thirdPartyInvite.inviteSignUrl : undefined; - return MatrixClientPeg.get().joinRoom(this.props.roomAddress, - { inviteSignUrl: sign_url } ); - }).then(function(resp) { - var roomId = resp.roomId; - - // It is possible that there is no Room yet if state hasn't come down - // from /sync - joinRoom will resolve when the HTTP request to join succeeds, - // NOT when it comes down /sync. If there is no room, we'll keep the - // joining flag set until we see it. - - // We'll need to initialise the timeline when joining, but due to - // the above, we can't do it here: we do it in onRoom instead, - // once we have a useable room object. - var room = MatrixClientPeg.get().getRoom(roomId); - if (!room) { - // wait for the room to turn up in onRoom. - self._joiningRoomId = roomId; - } else { - // we've got a valid room, but that might also just mean that - // it was peekable (so we had one before anyway). If we are - // not yet a member of the room, we will need to wait for that - // to happen, in onRoomStateMember. - var me = MatrixClientPeg.get().credentials.userId; - self.setState({ - joining: !room.hasMembershipState(me, "join"), - room: room - }); - } - }).catch(function(error) { - self.setState({ - joining: false, - joinError: error - }); - - if (!error) return; - - // https://matrix.org/jira/browse/SYN-659 - // Need specific error message if joining a room is refused because the user is a guest and guest access is not allowed - if ( - error.errcode == 'M_GUEST_ACCESS_FORBIDDEN' || - ( - error.errcode == 'M_FORBIDDEN' && - MatrixClientPeg.get().isGuest() - ) - ) { - dis.dispatch({action: 'view_set_mxid'}); - } else { - var msg = error.message ? error.message : JSON.stringify(error); - var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createDialog(ErrorDialog, { - title: "Failed to join room", - description: msg - }); - } - }).done(); + }); }, onMessageListScroll: function(ev) { @@ -1451,9 +1371,9 @@ module.exports = React.createClass({ } }, - _updateAutoComplete: function() { + _updateAutoComplete: function(room) { const myUserId = MatrixClientPeg.get().credentials.userId; - const members = this.state.room.getJoinedMembers().filter(function(member) { + const members = room.getJoinedMembers().filter(function(member) { if (member.userId !== myUserId) return true; }); UserProvider.getInstance().setUserList(members); @@ -1491,7 +1411,7 @@ module.exports = React.createClass({ // We have no room object for this room, only the ID. // We've got to this room by following a link, possibly a third party invite. - var room_alias = this.props.roomAddress[0] == '#' ? this.props.roomAddress : null; + var room_alias = this.state.room_alias; return (
{ + * this.setState({ cachedPassword: lifecycleStore.getCachedPassword() }) + * }) + * ``` + */ +class LifecycleStore extends Store { + constructor() { + super(dis); + + // Initialise state + this._state = { + deferred_action: null, + }; + } + + _setState(newState) { + this._state = Object.assign(this._state, newState); + this.__emitChange(); + } + + __onDispatch(payload) { + switch (payload.action) { + case 'do_after_sync_prepared': + this._setState({ + deferred_action: payload.deferred_action, + }); + break; + case 'sync_state': + if (payload.state !== 'PREPARED') { + break; + } + console.warn(this._state); + if (!this._state.deferred_action) break; + const deferredAction = Object.assign({}, this._state.deferred_action); + this._setState({ + deferred_action: null, + }); + dis.dispatch(deferredAction); + break; + } + } +} + +let singletonLifecycleStore = null; +if (!singletonLifecycleStore) { + singletonLifecycleStore = new LifecycleStore(); +} +module.exports = singletonLifecycleStore; diff --git a/src/stores/RoomViewStore.js b/src/stores/RoomViewStore.js new file mode 100644 index 0000000000..fe57079859 --- /dev/null +++ b/src/stores/RoomViewStore.js @@ -0,0 +1,145 @@ +/* +Copyright 2017 Vector Creations 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 dis from '../dispatcher'; +import {Store} from 'flux/utils'; +import MatrixClientPeg from '../MatrixClientPeg'; + +const INITIAL_STATE = { + // Whether we're joining the currently viewed room + joining: false, + // Any error occurred during joining + joinError: null, + // The room ID of the room + roomId: null, + // The room alias of the room (or null if not originally specified in view_room) + roomAlias: null, + // Whether the current room is loading + roomLoading: false, + // Any error that has occurred during loading + roomLoadError: null, +}; + +/** + * A class for storing application state for RoomView. This is the RoomView's interface +* with a subset of the js-sdk. + * ``` + */ +class RoomViewStore extends Store { + constructor() { + super(dis); + + // Initialise state + this._state = INITIAL_STATE; + } + + _setState(newState) { + this._state = Object.assign(this._state, newState); + this.__emitChange(); + } + + __onDispatch(payload) { + switch (payload.action) { + // view_room: + // - room_alias: '#somealias:matrix.org' + // - room_id: '!roomid123:matrix.org' + case 'view_room': + this._viewRoom(payload); + break; + + // join_room: + // - opts: options for joinRoom + case 'join_room': + this._joinRoom(payload); + break; + } + } + + _viewRoom(payload) { + const address = payload.room_alias || payload.room_id; + if (address[0] == '#') { + this._setState({ + roomLoading: true, + }); + MatrixClientPeg.get().getRoomIdForAlias(address).then( + (result) => { + this._setState({ + roomId: result.room_id, + roomAlias: address, + roomLoading: false, + roomLoadError: null, + }); + }, (err) => { + console.error(err); + this._setState({ + roomLoading: false, + roomLoadError: err, + }); + }); + } else { + this._setState({ + roomId: address, + }); + } + } + + _joinRoom(payload) { + this._setState({ + joining: true, + }); + MatrixClientPeg.get().joinRoom(this._state.roomId, payload.opts).then( + () => { + this._setState({ + joining: false, + }); + }, (err) => { + this._setState({ + joining: false, + joinError: err, + }); + }); + } + + reset() { + this._state = Object.assign({}, INITIAL_STATE); + } + + getRoomId() { + return this._state.roomId; + } + + getRoomAlias() { + return this._state.roomAlias; + } + + isRoomLoading() { + return this._state.roomLoading; + } + + isJoining() { + return this._state.joining; + } + + getJoinError() { + return this._state.joinError; + } + +} + +let singletonRoomViewStore = null; +if (!singletonRoomViewStore) { + singletonRoomViewStore = new RoomViewStore(); +} +module.exports = singletonRoomViewStore; diff --git a/src/stores/SessionStore.js b/src/stores/SessionStore.js index 1570f58688..2fd35ce40a 100644 --- a/src/stores/SessionStore.js +++ b/src/stores/SessionStore.js @@ -1,3 +1,18 @@ +/* +Copyright 2017 Vector Creations 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 dis from '../dispatcher'; import {Store} from 'flux/utils'; diff --git a/test/components/structures/RoomView-test.js b/test/components/structures/RoomView-test.js deleted file mode 100644 index 8e7c8160b8..0000000000 --- a/test/components/structures/RoomView-test.js +++ /dev/null @@ -1,67 +0,0 @@ -var React = require('react'); -var expect = require('expect'); -var sinon = require('sinon'); -var ReactDOM = require("react-dom"); - -var sdk = require('matrix-react-sdk'); -var RoomView = sdk.getComponent('structures.RoomView'); -var peg = require('../../../src/MatrixClientPeg'); - -var test_utils = require('../../test-utils'); -var q = require('q'); - -var Skinner = require("../../../src/Skinner"); -var stubComponent = require('../../components/stub-component.js'); - -describe('RoomView', function () { - var sandbox; - var parentDiv; - - beforeEach(function() { - test_utils.beforeEach(this); - sandbox = test_utils.stubClient(); - parentDiv = document.createElement('div'); - - this.oldTimelinePanel = Skinner.getComponent('structures.TimelinePanel'); - this.oldRoomHeader = Skinner.getComponent('views.rooms.RoomHeader'); - Skinner.addComponent('structures.TimelinePanel', stubComponent()); - Skinner.addComponent('views.rooms.RoomHeader', stubComponent()); - - peg.get().credentials = { userId: "@test:example.com" }; - }); - - afterEach(function() { - sandbox.restore(); - - ReactDOM.unmountComponentAtNode(parentDiv); - - Skinner.addComponent('structures.TimelinePanel', this.oldTimelinePanel); - Skinner.addComponent('views.rooms.RoomHeader', this.oldRoomHeader); - }); - - it('resolves a room alias to a room id', function (done) { - peg.get().getRoomIdForAlias.returns(q({room_id: "!randomcharacters:aser.ver"})); - - function onRoomIdResolved(room_id) { - expect(room_id).toEqual("!randomcharacters:aser.ver"); - done(); - } - - ReactDOM.render(, parentDiv); - }); - - it('joins by alias if given an alias', function (done) { - peg.get().getRoomIdForAlias.returns(q({room_id: "!randomcharacters:aser.ver"})); - peg.get().getProfileInfo.returns(q({displayname: "foo"})); - var roomView = ReactDOM.render(, parentDiv); - - peg.get().joinRoom = function(x) { - expect(x).toEqual('#alias:ser.ver'); - done(); - }; - - process.nextTick(function() { - roomView.onJoinButtonClicked(); - }); - }); -}); diff --git a/test/stores/RoomViewStore-test.js b/test/stores/RoomViewStore-test.js new file mode 100644 index 0000000000..7100dced19 --- /dev/null +++ b/test/stores/RoomViewStore-test.js @@ -0,0 +1,56 @@ +import expect from 'expect'; + +import dis from '../../src/dispatcher'; +import RoomViewStore from '../../src/stores/RoomViewStore'; + + +import peg from '../../src/MatrixClientPeg'; + +import * as testUtils from '../test-utils'; +import q from 'q'; + +const dispatch = testUtils.getDispatchForStore(RoomViewStore); + +describe('RoomViewStore', function() { + let sandbox; + + beforeEach(function() { + testUtils.beforeEach(this); + sandbox = testUtils.stubClient(); + peg.get().credentials = { userId: "@test:example.com" }; + + // Reset the state of the store + RoomViewStore.reset(); + }); + + afterEach(function() { + sandbox.restore(); + }); + + it('can be used to view a room by ID and join', function(done) { + peg.get().joinRoom = (roomId) => { + expect(roomId).toBe("!randomcharacters:aser.ver"); + done(); + }; + + dispatch({ action: 'view_room', room_id: '!randomcharacters:aser.ver' }); + dispatch({ action: 'join_room' }); + expect(RoomViewStore.isJoining()).toBe(true); + }); + + it('can be used to view a room by alias and join', function(done) { + peg.get().getRoomIdForAlias.returns(q({room_id: "!randomcharacters:aser.ver"})); + peg.get().joinRoom = (roomId) => { + expect(roomId).toBe("!randomcharacters:aser.ver"); + done(); + }; + + dispatch({ action: 'view_room', room_alias: '#somealias2:aser.ver' }); + + // Wait for the next event loop to allow for room alias resolution + setTimeout(() => { + dispatch({ action: 'join_room' }); + expect(RoomViewStore.isJoining()).toBe(true); + }, 0); + }); +}); diff --git a/test/test-utils.js b/test/test-utils.js index 2c866d345c..569208b355 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -4,7 +4,8 @@ import sinon from 'sinon'; import q from 'q'; import ReactTestUtils from 'react-addons-test-utils'; -import peg from '../src/MatrixClientPeg.js'; +import peg from '../src/MatrixClientPeg'; +import dis from '../src/dispatcher'; import jssdk from 'matrix-js-sdk'; const MatrixEvent = jssdk.MatrixEvent; @@ -290,3 +291,13 @@ export function mkStubRoom(roomId = null) { }, }; } + +export function getDispatchForStore(store) { + // Mock the dispatcher by gut-wrenching. Stores can only __emitChange whilst a + // dispatcher `_isDispatching` is true. + return (payload) => { + dis._isDispatching = true; + dis._callbacks[store._dispatchToken](payload); + dis._isDispatching = false; + }; +} From 5f36f797da0b1a195c31b0cf2f45f2d4b8dd5ae6 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 24 May 2017 17:55:36 +0100 Subject: [PATCH 038/127] Implement default welcome page and allow custom URL /w config This changes the default behaviour of displaying the room directory to instead displaying the default homepage. If specified, the config "welcomePageUrl" can be used to override the default '/home.html'. --- src/components/structures/LoggedInView.js | 4 ++-- src/components/structures/MatrixChat.js | 12 ++---------- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index e559a21e1a..994e6504cf 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -240,7 +240,8 @@ export default React.createClass({ collapsedRhs={this.props.collapse_rhs} teamServerUrl={this.props.config.teamServerConfig.teamServerURL} teamToken={this.props.teamToken} - /> + homePageUrl={this.props.config.welcomePageUrl} + />; if (!this.props.collapse_rhs) right_panel = break; @@ -276,7 +277,6 @@ export default React.createClass({ selectedRoom={this.props.currentRoomId} collapsed={this.props.collapse_lhs || false} opacity={this.props.sideOpacity} - teamToken={this.props.teamToken} />
{page_element} diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 59ce1b622d..1882831bdc 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -457,10 +457,6 @@ module.exports = React.createClass({ this.notifyNewScreen('directory'); break; case 'view_home_page': - if (!this._teamToken) { - dis.dispatch({action: 'view_room_directory'}); - return; - } this._setPage(PageTypes.HomePage); this.notifyNewScreen('home'); break; @@ -812,7 +808,7 @@ module.exports = React.createClass({ return; } // The user has just logged in after registering - dis.dispatch({action: 'view_room_directory'}); + dis.dispatch({action: 'view_home_page'}); } else { this._showScreenAfterLogin(); } @@ -834,12 +830,8 @@ module.exports = React.createClass({ action: 'view_room', room_id: localStorage.getItem('mx_last_room_id'), }); - } else if (this._teamToken) { - // Team token might be set if we're a guest. - // Guests do not call _onLoggedIn with a teamToken - dis.dispatch({action: 'view_home_page'}); } else { - dis.dispatch({action: 'view_room_directory'}); + dis.dispatch({action: 'view_home_page'}); } }, From dcf2fb68aecd5d1eb64a649eac3bba1912dd96d3 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 24 May 2017 18:02:17 +0100 Subject: [PATCH 039/127] Remove console log --- src/stores/LifecycleStore.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/stores/LifecycleStore.js b/src/stores/LifecycleStore.js index 82a3b1b584..43e2de9d52 100644 --- a/src/stores/LifecycleStore.js +++ b/src/stores/LifecycleStore.js @@ -54,7 +54,6 @@ class LifecycleStore extends Store { if (payload.state !== 'PREPARED') { break; } - console.warn(this._state); if (!this._state.deferred_action) break; const deferredAction = Object.assign({}, this._state.deferred_action); this._setState({ From fffe425730688b1d1adea59c813bdc6b6b695273 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 24 May 2017 18:04:04 +0100 Subject: [PATCH 040/127] Add non-null RoomView key --- src/components/structures/LoggedInView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index df24fbb33b..5022b983f0 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -196,7 +196,7 @@ export default React.createClass({ oobData={this.props.roomOobData} highlightedEventId={this.props.highlightedEventId} eventPixelOffset={this.props.initialEventPixelOffset} - key={this.props.currentRoomId} + key={this.props.currentRoomId || 'roomview'} opacity={this.props.middleOpacity} collapsedRhs={this.props.collapse_rhs} ConferenceHandler={this.props.ConferenceHandler} From 8fc44a9b6661a6a5dab303d6895e903a10d2aed1 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 25 May 2017 09:30:57 +0100 Subject: [PATCH 041/127] Add comment to explain sync_state dispatch --- src/components/structures/MatrixChat.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index b1814bc322..dca73a4601 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -895,6 +895,11 @@ module.exports = React.createClass({ }); cli.on('sync', function(state, prevState) { + // LifecycleStore and others cannot directly subscribe to matrix client for + // events because flux only allows store state changes during flux dispatches. + // So dispatch directly from here. Ideally we'd use a SyncStateStore that + // would do this dispatch and expose the sync state itself (by listening to + // its own dispatch). dis.dispatch({action: 'sync_state', prevState, state}); self.updateStatusIndicator(state, prevState); if (state === "SYNCING" && prevState === "SYNCING") { From c894c83fbe2eaced0c2313e405008c62e9484914 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 25 May 2017 11:02:48 +0100 Subject: [PATCH 042/127] Remove GuestWarningBar --- src/components/structures/LoggedInView.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index d2ae57cda4..4687afbee1 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -178,7 +178,6 @@ export default React.createClass({ const RoomDirectory = sdk.getComponent('structures.RoomDirectory'); const HomePage = sdk.getComponent('structures.HomePage'); const MatrixToolbar = sdk.getComponent('globals.MatrixToolbar'); - const GuestWarningBar = sdk.getComponent('globals.GuestWarningBar'); const NewVersionBar = sdk.getComponent('globals.NewVersionBar'); const PasswordNagBar = sdk.getComponent('globals.PasswordNagBar'); @@ -253,8 +252,6 @@ export default React.createClass({ topBar = ; - } else if (this.props.matrixClient.isGuest()) { - topBar = ; } else if (this.state.userHasGeneratedPassword) { topBar = ; } else if (Notifier.supportsDesktopNotifications() && !Notifier.isEnabled() && !Notifier.isToolbarHidden()) { From 5531f274354df79980152d05521a23114cb5d25b Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 26 Apr 2017 18:59:16 +0100 Subject: [PATCH 043/127] Make the left panel more friendly to new users https://github.com/vector-im/riot-web/issues/3609 Conflicts: src/components/views/rooms/RoomList.js cherry-picking commit f5f35e3. --- src/components/views/rooms/RoomList.js | 163 +++++++++++++++++++------ 1 file changed, 124 insertions(+), 39 deletions(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 760b0543c6..9dfa99fb44 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -29,7 +29,14 @@ var Rooms = require('../../../Rooms'); import DMRoomMap from '../../../utils/DMRoomMap'; var Receipt = require('../../../utils/Receipt'); -var HIDE_CONFERENCE_CHANS = true; +const HIDE_CONFERENCE_CHANS = true; + +const VERBS = { + 'm.favourite': 'favourite', + 'im.vector.fake.direct': 'tag direct chat', + 'im.vector.fake.recent': 'restore', + 'm.lowpriority': 'demote', +}; module.exports = React.createClass({ displayName: 'RoomList', @@ -44,6 +51,7 @@ module.exports = React.createClass({ getInitialState: function() { return { isLoadingLeftRooms: false, + totalRoomCount: null, lists: {}, incomingCall: null, }; @@ -63,8 +71,17 @@ module.exports = React.createClass({ cli.on("RoomMember.name", this.onRoomMemberName); cli.on("accountData", this.onAccountData); - var s = this.getRoomLists(); - this.setState(s); + // lookup for which lists a given roomId is currently in. + this.listsForRoomId = {}; + + this.refreshRoomList(); + + // order of the sublists + //this.listOrder = []; + + // loop count to stop a stack overflow if the user keeps waggling the + // mouse for >30s in a row, or if running under mocha + this._delayedRefreshRoomListLoopCount = 0 }, componentDidMount: function() { @@ -202,31 +219,33 @@ module.exports = React.createClass({ }, 500), refreshRoomList: function() { - // console.log("DEBUG: Refresh room list delta=%s ms", - // (!this._lastRefreshRoomListTs ? "-" : (Date.now() - this._lastRefreshRoomListTs)) - // ); - - // TODO: rather than bluntly regenerating and re-sorting everything - // every time we see any kind of room change from the JS SDK - // we could do incremental updates on our copy of the state - // based on the room which has actually changed. This would stop - // us re-rendering all the sublists every time anything changes anywhere - // in the state of the client. - this.setState(this.getRoomLists()); + // TODO: ideally we'd calculate this once at start, and then maintain + // any changes to it incrementally, updating the appropriate sublists + // as needed. + // Alternatively we'd do something magical with Immutable.js or similar. + const lists = this.getRoomLists(); + let totalRooms = 0; + for (const l of Object.values(lists)) { + totalRooms += l.length; + } + this.setState({ + lists: this.getRoomLists(), + totalRoomCount: totalRooms, + }); // this._lastRefreshRoomListTs = Date.now(); }, getRoomLists: function() { var self = this; - var s = { lists: {} }; + const lists = {}; - s.lists["im.vector.fake.invite"] = []; - s.lists["m.favourite"] = []; - s.lists["im.vector.fake.recent"] = []; - s.lists["im.vector.fake.direct"] = []; - s.lists["m.lowpriority"] = []; - s.lists["im.vector.fake.archived"] = []; + lists["im.vector.fake.invite"] = []; + lists["m.favourite"] = []; + lists["im.vector.fake.recent"] = []; + lists["im.vector.fake.direct"] = []; + lists["m.lowpriority"] = []; + lists["im.vector.fake.archived"] = []; const dmRoomMap = new DMRoomMap(MatrixClientPeg.get()); @@ -240,7 +259,8 @@ module.exports = React.createClass({ // ", prevMembership = " + me.events.member.getPrevContent().membership); if (me.membership == "invite") { - s.lists["im.vector.fake.invite"].push(room); + self.listsForRoomId[room.roomId].push("im.vector.fake.invite"); + lists["im.vector.fake.invite"].push(room); } else if (HIDE_CONFERENCE_CHANS && Rooms.isConfCallRoom(room, me, self.props.ConferenceHandler)) { // skip past this room & don't put it in any lists @@ -254,48 +274,55 @@ module.exports = React.createClass({ if (tagNames.length) { for (var i = 0; i < tagNames.length; i++) { var tagName = tagNames[i]; - s.lists[tagName] = s.lists[tagName] || []; - s.lists[tagNames[i]].push(room); + lists[tagName] = lists[tagName] || []; + lists[tagName].push(room); + self.listsForRoomId[room.roomId].push(tagName); + otherTagNames[tagName] = 1; } } else if (dmRoomMap.getUserIdForRoomId(room.roomId)) { // "Direct Message" rooms (that we're still in and that aren't otherwise tagged) - s.lists["im.vector.fake.direct"].push(room); + self.listsForRoomId[room.roomId].push("im.vector.fake.direct"); + lists["im.vector.fake.direct"].push(room); } else { - s.lists["im.vector.fake.recent"].push(room); + self.listsForRoomId[room.roomId].push("im.vector.fake.recent"); + lists["im.vector.fake.recent"].push(room); } } else if (me.membership === "leave") { - s.lists["im.vector.fake.archived"].push(room); + self.listsForRoomId[room.roomId].push("im.vector.fake.archived"); + lists["im.vector.fake.archived"].push(room); } else { console.error("unrecognised membership: " + me.membership + " - this should never happen"); } }); - if (s.lists["im.vector.fake.direct"].length == 0 && + if (lists["im.vector.fake.direct"].length == 0 && MatrixClientPeg.get().getAccountData('m.direct') === undefined && !MatrixClientPeg.get().isGuest()) { // scan through the 'recents' list for any rooms which look like DM rooms // and make them DM rooms - const oldRecents = s.lists["im.vector.fake.recent"]; - s.lists["im.vector.fake.recent"] = []; + const oldRecents = lists["im.vector.fake.recent"]; + lists["im.vector.fake.recent"] = []; for (const room of oldRecents) { const me = room.getMember(MatrixClientPeg.get().credentials.userId); if (me && Rooms.looksLikeDirectMessageRoom(room, me)) { - s.lists["im.vector.fake.direct"].push(room); + self.listsForRoomId[room.roomId].push("im.vector.fake.direct"); + lists["im.vector.fake.direct"].push(room); } else { - s.lists["im.vector.fake.recent"].push(room); + self.listsForRoomId[room.roomId].push("im.vector.fake.recent"); + lists["im.vector.fake.recent"].push(room); } } // save these new guessed DM rooms into the account data const newMDirectEvent = {}; - for (const room of s.lists["im.vector.fake.direct"]) { + for (const room of lists["im.vector.fake.direct"]) { const me = room.getMember(MatrixClientPeg.get().credentials.userId); const otherPerson = Rooms.getOnlyOtherMember(room, me); if (!otherPerson) continue; @@ -313,7 +340,22 @@ module.exports = React.createClass({ // we actually apply the sorting to this when receiving the prop in RoomSubLists. - return s; + // we'll need this when we get to iterating through lists programatically - e.g. ctrl-shift-up/down +/* + this.listOrder = [ + "im.vector.fake.invite", + "m.favourite", + "im.vector.fake.recent", + "im.vector.fake.direct", + Object.keys(otherTagNames).filter(tagName=>{ + return (!tagName.match(/^m\.(favourite|lowpriority)$/)); + }).sort(), + "m.lowpriority", + "im.vector.fake.archived" + ]; +*/ + + return lists; }, _getScrollNode: function() { @@ -467,6 +509,49 @@ module.exports = React.createClass({ this.refs.gemscroll.forceUpdate(); }, + _getEmptyContent: function(section) { + let greyed = false; + if (this.state.totalRoomCount === 0) { + const TintableSvg = sdk.getComponent('elements.TintableSvg'); + switch (section) { + case 'm.favourite': + case 'm.lowpriority': + greyed = true; + break; + case 'im.vector.fake.direct': + return
+
+ +
+ Use the button below to chat with someone! +
; + case 'im.vector.fake.recent': + return
+
+ +
+ Use the button below to browse the room directory +

+
+ +
+ or this button to start a new one! +
; + } + } + const RoomDropTarget = sdk.getComponent('rooms.RoomDropTarget'); + + const labelText = 'Drop here to ' + (VERBS[section] || 'tag ' + section); + + let label; + if (greyed) { + label = {labelText}; + } else { + label = labelText; + } + return ; + }, + render: function() { var RoomSubList = sdk.getComponent('structures.RoomSubList'); var self = this; @@ -489,7 +574,7 @@ module.exports = React.createClass({ Date: Fri, 28 Apr 2017 11:20:29 +0100 Subject: [PATCH 044/127] Other empty sections no longer need to be greyed --- src/components/views/rooms/RoomList.js | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 9dfa99fb44..e285c1841e 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -510,14 +510,9 @@ module.exports = React.createClass({ }, _getEmptyContent: function(section) { - let greyed = false; if (this.state.totalRoomCount === 0) { const TintableSvg = sdk.getComponent('elements.TintableSvg'); switch (section) { - case 'm.favourite': - case 'm.lowpriority': - greyed = true; - break; case 'im.vector.fake.direct': return
@@ -543,13 +538,7 @@ module.exports = React.createClass({ const labelText = 'Drop here to ' + (VERBS[section] || 'tag ' + section); - let label; - if (greyed) { - label = {labelText}; - } else { - label = labelText; - } - return ; + return ; }, render: function() { From bff0577cb61bfb8095c23254f618d9bb9a42a131 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 4 May 2017 13:55:52 +0100 Subject: [PATCH 045/127] Add buttons to room sub list headers Conflicts: src/component-index.js src/components/views/rooms/RoomList.js cherry-picking commit ce119a6. --- src/components/views/elements/RoleButton.js | 75 +++++++++++++++++++++ src/components/views/rooms/RoomList.js | 40 +++++++---- 2 files changed, 102 insertions(+), 13 deletions(-) create mode 100644 src/components/views/elements/RoleButton.js diff --git a/src/components/views/elements/RoleButton.js b/src/components/views/elements/RoleButton.js new file mode 100644 index 0000000000..06006a5779 --- /dev/null +++ b/src/components/views/elements/RoleButton.js @@ -0,0 +1,75 @@ +/* +Copyright Vector Creations 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 AccessibleButton from './AccessibleButton'; +import dis from '../../../dispatcher'; +import sdk from '../../../index'; + +export default React.createClass({ + displayName: 'RoleButton', + + propTypes: { + role: PropTypes.string.isRequired, + size: PropTypes.string, + }, + + getDefaultProps: function() { + return { + size: 25, + }; + }, + + _onClick: function(ev) { + ev.stopPropagation(); + + let action; + switch(this.props.role) { + case 'start_chat': + action = 'view_create_chat'; + break; + case 'room_directory': + action = 'view_room_directory'; + break; + case 'create_room': + action = 'view_create_room'; + break; + } + if (action) dis.dispatch({action: action}); + }, + + _getIconPath() { + switch(this.props.role) { + case 'start_chat': + return 'img/icons-people.svg'; + case 'room_directory': + return 'img/icons-directory.svg'; + case 'create_room': + return 'img/icons-create-room.svg'; + } + }, + + render: function() { + const TintableSvg = sdk.getComponent("elements.TintableSvg"); + + return ( + + + + ); + } +}); diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index e285c1841e..9a64c16239 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +Copyright Vector Creations Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -510,27 +511,23 @@ module.exports = React.createClass({ }, _getEmptyContent: function(section) { + const RoleButton = sdk.getComponent('elements.RoleButton'); if (this.state.totalRoomCount === 0) { const TintableSvg = sdk.getComponent('elements.TintableSvg'); switch (section) { case 'im.vector.fake.direct': return
-
- -
- Use the button below to chat with someone! + Press + + to start a chat with someone
; case 'im.vector.fake.recent': return
-
- -
- Use the button below to browse the room directory -

-
- -
- or this button to start a new one! + You're not in any rooms yet! Press + + to make a room or + + to browse the directory
; } } @@ -541,6 +538,21 @@ module.exports = React.createClass({ return ; }, + _getHeaderItems: function(section) { + const RoleButton = sdk.getComponent('elements.RoleButton'); + switch (section) { + case 'im.vector.fake.direct': + return + + ; + case 'im.vector.fake.recent': + return + + + ; + } + }, + render: function() { var RoomSubList = sdk.getComponent('structures.RoomSubList'); var self = this; @@ -577,6 +589,7 @@ module.exports = React.createClass({ label="People" tagName="im.vector.fake.direct" emptyContent={this._getEmptyContent('im.vector.fake.direct')} + headerItems={this._getHeaderItems('im.vector.fake.direct')} editable={ true } order="recent" selectedRoom={ self.props.selectedRoom } @@ -591,6 +604,7 @@ module.exports = React.createClass({ label="Rooms" editable={ true } emptyContent={this._getEmptyContent('im.vector.fake.recent')} + headerItems={this._getHeaderItems('im.vector.fake.recent')} order="recent" selectedRoom={ self.props.selectedRoom } incomingCall={ self.state.incomingCall } From 54af06e8e12bfe86f11b4a3131ea155b8e161f3c Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 4 May 2017 15:02:21 +0100 Subject: [PATCH 046/127] What year is it? Who's the president? --- src/components/views/elements/RoleButton.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/RoleButton.js b/src/components/views/elements/RoleButton.js index 06006a5779..f20b4c6b88 100644 --- a/src/components/views/elements/RoleButton.js +++ b/src/components/views/elements/RoleButton.js @@ -1,5 +1,5 @@ /* -Copyright Vector Creations Ltd +Copyright 2017 Vector Creations Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From a996f52ea34c0d4c7cc072c7cf068baf3b9cde1b Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 4 May 2017 15:38:09 +0100 Subject: [PATCH 047/127] Make bottom left menu buttons use RoleButton too --- src/components/views/elements/RoleButton.js | 53 ++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/src/components/views/elements/RoleButton.js b/src/components/views/elements/RoleButton.js index f20b4c6b88..60f227a067 100644 --- a/src/components/views/elements/RoleButton.js +++ b/src/components/views/elements/RoleButton.js @@ -31,6 +31,13 @@ export default React.createClass({ getDefaultProps: function() { return { size: 25, + tooltip: false, + }; + }, + + getInitialState: function() { + return { + showTooltip: false, }; }, @@ -48,10 +55,39 @@ export default React.createClass({ case 'create_room': action = 'view_create_room'; break; + case 'home_page': + action = 'view_home_page'; + break; + case 'settings': + action = 'view_user_settings'; + break; } if (action) dis.dispatch({action: action}); }, + _onMouseEnter: function() { + if (this.props.tooltip) this.setState({showTooltip: true}); + }, + + _onMouseLeave: function() { + this.setState({showTooltip: false}); + }, + + _getLabel() { + switch(this.props.role) { + case 'start_chat': + return 'Start chat'; + case 'room_directory': + return 'Room directory'; + case 'create_room': + return 'Create new room'; + case 'home_page': + return 'Welcome page'; + case 'settings': + return 'Settings'; + } + }, + _getIconPath() { switch(this.props.role) { case 'start_chat': @@ -60,15 +96,30 @@ export default React.createClass({ return 'img/icons-directory.svg'; case 'create_room': return 'img/icons-create-room.svg'; + case 'home_page': + return 'img/icons-home.svg'; + case 'settings': + return 'img/icons-settings.svg'; } }, render: function() { const TintableSvg = sdk.getComponent("elements.TintableSvg"); + let tooltip; + if (this.state.showTooltip) { + const RoomTooltip = sdk.getComponent("rooms.RoomTooltip"); + tooltip = ; + } + return ( - + + {tooltip} ); } From 3d3d89202e01100f1162d30542b59c8826c19e29 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 4 May 2017 15:46:24 +0100 Subject: [PATCH 048/127] Year --- src/components/views/rooms/RoomList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 9a64c16239..ecb178d145 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -1,6 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd -Copyright Vector Creations Ltd +Copyright 2017 Vector Creations Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From 99efbbee5e23913221d95bc9d8457bed24a4e0ae Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 4 May 2017 16:22:06 +0100 Subject: [PATCH 049/127] Depend on prop-types module So we can start writing code compatible with new React --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 059fdd390f..572dcddeb5 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "lodash": "^4.13.1", "matrix-js-sdk": "0.7.8", "optimist": "^0.6.1", + "prop-types": "^15.5.8", "q": "^1.4.1", "react": "^15.4.0", "react-addons-css-transition-group": "15.3.2", From dc2274df54896b48f836854cf46cd10b525d41c8 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 4 May 2017 18:08:04 +0100 Subject: [PATCH 050/127] Hide empty tips if collapsed --- src/components/views/rooms/RoomList.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index ecb178d145..2dce02cc78 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -511,6 +511,12 @@ module.exports = React.createClass({ }, _getEmptyContent: function(section) { + const RoomDropTarget = sdk.getComponent('rooms.RoomDropTarget'); + + if (this.props.collapsed) { + return ; + } + const RoleButton = sdk.getComponent('elements.RoleButton'); if (this.state.totalRoomCount === 0) { const TintableSvg = sdk.getComponent('elements.TintableSvg'); @@ -531,7 +537,6 @@ module.exports = React.createClass({
; } } - const RoomDropTarget = sdk.getComponent('rooms.RoomDropTarget'); const labelText = 'Drop here to ' + (VERBS[section] || 'tag ' + section); From 9337158a470e2d23d16cce0054a931e5f97a3b0d Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 5 May 2017 14:25:18 +0100 Subject: [PATCH 051/127] Separate classes for the different buttons Also rename RoleButton to ActionButton because it's not being given a Role any more. Conflicts: src/component-index.js cherry-picking commit 4a5821e. --- .../{RoleButton.js => ActionButton.js} | 60 +++---------------- .../views/elements/CreateRoomButton.js | 37 ++++++++++++ src/components/views/elements/HomeButton.js | 37 ++++++++++++ .../views/elements/RoomDirectoryButton.js | 37 ++++++++++++ .../views/elements/SettingsButton.js | 37 ++++++++++++ .../views/elements/StartChatButton.js | 37 ++++++++++++ src/components/views/rooms/RoomList.js | 20 ++++--- 7 files changed, 204 insertions(+), 61 deletions(-) rename src/components/views/elements/{RoleButton.js => ActionButton.js} (54%) create mode 100644 src/components/views/elements/CreateRoomButton.js create mode 100644 src/components/views/elements/HomeButton.js create mode 100644 src/components/views/elements/RoomDirectoryButton.js create mode 100644 src/components/views/elements/SettingsButton.js create mode 100644 src/components/views/elements/StartChatButton.js diff --git a/src/components/views/elements/RoleButton.js b/src/components/views/elements/ActionButton.js similarity index 54% rename from src/components/views/elements/RoleButton.js rename to src/components/views/elements/ActionButton.js index 60f227a067..6d6289ddab 100644 --- a/src/components/views/elements/RoleButton.js +++ b/src/components/views/elements/ActionButton.js @@ -24,8 +24,11 @@ export default React.createClass({ displayName: 'RoleButton', propTypes: { - role: PropTypes.string.isRequired, size: PropTypes.string, + tooltip: PropTypes.bool, + action: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + iconPath: PropTypes.string.isRequired, }, getDefaultProps: function() { @@ -43,26 +46,7 @@ export default React.createClass({ _onClick: function(ev) { ev.stopPropagation(); - - let action; - switch(this.props.role) { - case 'start_chat': - action = 'view_create_chat'; - break; - case 'room_directory': - action = 'view_room_directory'; - break; - case 'create_room': - action = 'view_create_room'; - break; - case 'home_page': - action = 'view_home_page'; - break; - case 'settings': - action = 'view_user_settings'; - break; - } - if (action) dis.dispatch({action: action}); + dis.dispatch({action: this.props.action}); }, _onMouseEnter: function() { @@ -73,43 +57,13 @@ export default React.createClass({ this.setState({showTooltip: false}); }, - _getLabel() { - switch(this.props.role) { - case 'start_chat': - return 'Start chat'; - case 'room_directory': - return 'Room directory'; - case 'create_room': - return 'Create new room'; - case 'home_page': - return 'Welcome page'; - case 'settings': - return 'Settings'; - } - }, - - _getIconPath() { - switch(this.props.role) { - case 'start_chat': - return 'img/icons-people.svg'; - case 'room_directory': - return 'img/icons-directory.svg'; - case 'create_room': - return 'img/icons-create-room.svg'; - case 'home_page': - return 'img/icons-home.svg'; - case 'settings': - return 'img/icons-settings.svg'; - } - }, - render: function() { const TintableSvg = sdk.getComponent("elements.TintableSvg"); let tooltip; if (this.state.showTooltip) { const RoomTooltip = sdk.getComponent("rooms.RoomTooltip"); - tooltip = ; + tooltip = ; } return ( @@ -118,7 +72,7 @@ export default React.createClass({ onMouseEnter={this._onMouseEnter} onMouseLeave={this._onMouseLeave} > - + {tooltip} ); diff --git a/src/components/views/elements/CreateRoomButton.js b/src/components/views/elements/CreateRoomButton.js new file mode 100644 index 0000000000..d6b6526d6c --- /dev/null +++ b/src/components/views/elements/CreateRoomButton.js @@ -0,0 +1,37 @@ +/* +Copyright 2017 Vector Creations 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 sdk from '../../../index'; +import PropTypes from 'prop-types'; + +const CreateRoomButton = function(props) { + const ActionButton = sdk.getComponent('elements.ActionButton'); + return ( + + ); +}; + +CreateRoomButton.propTypes = { + size: PropTypes.string, + tooltip: PropTypes.bool, +}; + +export default CreateRoomButton; diff --git a/src/components/views/elements/HomeButton.js b/src/components/views/elements/HomeButton.js new file mode 100644 index 0000000000..4c7f295c87 --- /dev/null +++ b/src/components/views/elements/HomeButton.js @@ -0,0 +1,37 @@ +/* +Copyright 2017 Vector Creations 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 sdk from '../../../index'; +import PropTypes from 'prop-types'; + +const HomeButton = function(props) { + const ActionButton = sdk.getComponent('elements.ActionButton'); + return ( + + ); +}; + +HomeButton.propTypes = { + size: PropTypes.string, + tooltip: PropTypes.bool, +}; + +export default HomeButton; diff --git a/src/components/views/elements/RoomDirectoryButton.js b/src/components/views/elements/RoomDirectoryButton.js new file mode 100644 index 0000000000..651dd8edd0 --- /dev/null +++ b/src/components/views/elements/RoomDirectoryButton.js @@ -0,0 +1,37 @@ +/* +Copyright 2017 Vector Creations 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 sdk from '../../../index'; +import PropTypes from 'prop-types'; + +const RoomDirectoryButton = function(props) { + const ActionButton = sdk.getComponent('elements.ActionButton'); + return ( + + ); +}; + +RoomDirectoryButton.propTypes = { + size: PropTypes.string, + tooltip: PropTypes.bool, +}; + +export default RoomDirectoryButton; diff --git a/src/components/views/elements/SettingsButton.js b/src/components/views/elements/SettingsButton.js new file mode 100644 index 0000000000..51da6e3fd1 --- /dev/null +++ b/src/components/views/elements/SettingsButton.js @@ -0,0 +1,37 @@ +/* +Copyright 2017 Vector Creations 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 sdk from '../../../index'; +import PropTypes from 'prop-types'; + +const SettingsButton = function(props) { + const ActionButton = sdk.getComponent('elements.ActionButton'); + return ( + + ); +}; + +SettingsButton.propTypes = { + size: PropTypes.string, + tooltip: PropTypes.bool, +}; + +export default SettingsButton; diff --git a/src/components/views/elements/StartChatButton.js b/src/components/views/elements/StartChatButton.js new file mode 100644 index 0000000000..66cd911754 --- /dev/null +++ b/src/components/views/elements/StartChatButton.js @@ -0,0 +1,37 @@ +/* +Copyright 2017 Vector Creations 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 sdk from '../../../index'; +import PropTypes from 'prop-types'; + +const StartChatButton = function(props) { + const ActionButton = sdk.getComponent('elements.ActionButton'); + return ( + + ); +}; + +StartChatButton.propTypes = { + size: PropTypes.string, + tooltip: PropTypes.bool, +}; + +export default StartChatButton; diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 2dce02cc78..8c8fd3ea86 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -517,22 +517,24 @@ module.exports = React.createClass({ return ; } - const RoleButton = sdk.getComponent('elements.RoleButton'); + const StartChatButton = sdk.getComponent('elements.StartChatButton'); + const RoomDirectoryButton = sdk.getComponent('elements.RoomDirectoryButton'); + const CreateRoomButton = sdk.getComponent('elements.CreateRoomButton'); if (this.state.totalRoomCount === 0) { const TintableSvg = sdk.getComponent('elements.TintableSvg'); switch (section) { case 'im.vector.fake.direct': return
Press - + to start a chat with someone
; case 'im.vector.fake.recent': return
You're not in any rooms yet! Press - + to make a room or - + to browse the directory
; } @@ -544,16 +546,18 @@ module.exports = React.createClass({ }, _getHeaderItems: function(section) { - const RoleButton = sdk.getComponent('elements.RoleButton'); + const StartChatButton = sdk.getComponent('elements.StartChatButton'); + const RoomDirectoryButton = sdk.getComponent('elements.RoomDirectoryButton'); + const CreateRoomButton = sdk.getComponent('elements.CreateRoomButton'); switch (section) { case 'im.vector.fake.direct': return - + ; case 'im.vector.fake.recent': return - - + + ; } }, From 5e855e6fee3c5cf068263967baa6d0c544c2a32c Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 5 May 2017 14:56:26 +0100 Subject: [PATCH 052/127] Size is a string, import react React gets put in by the JSX transpile --- src/components/views/elements/ActionButton.js | 2 +- src/components/views/elements/CreateRoomButton.js | 1 + src/components/views/elements/HomeButton.js | 1 + src/components/views/elements/RoomDirectoryButton.js | 1 + src/components/views/elements/SettingsButton.js | 1 + src/components/views/elements/StartChatButton.js | 1 + 6 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/views/elements/ActionButton.js b/src/components/views/elements/ActionButton.js index 6d6289ddab..267388daf6 100644 --- a/src/components/views/elements/ActionButton.js +++ b/src/components/views/elements/ActionButton.js @@ -33,7 +33,7 @@ export default React.createClass({ getDefaultProps: function() { return { - size: 25, + size: "25", tooltip: false, }; }, diff --git a/src/components/views/elements/CreateRoomButton.js b/src/components/views/elements/CreateRoomButton.js index d6b6526d6c..e7e526d36b 100644 --- a/src/components/views/elements/CreateRoomButton.js +++ b/src/components/views/elements/CreateRoomButton.js @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import React from 'react'; import sdk from '../../../index'; import PropTypes from 'prop-types'; diff --git a/src/components/views/elements/HomeButton.js b/src/components/views/elements/HomeButton.js index 4c7f295c87..5c446f24c9 100644 --- a/src/components/views/elements/HomeButton.js +++ b/src/components/views/elements/HomeButton.js @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import React from 'react'; import sdk from '../../../index'; import PropTypes from 'prop-types'; diff --git a/src/components/views/elements/RoomDirectoryButton.js b/src/components/views/elements/RoomDirectoryButton.js index 651dd8edd0..5e68776a15 100644 --- a/src/components/views/elements/RoomDirectoryButton.js +++ b/src/components/views/elements/RoomDirectoryButton.js @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import React from 'react'; import sdk from '../../../index'; import PropTypes from 'prop-types'; diff --git a/src/components/views/elements/SettingsButton.js b/src/components/views/elements/SettingsButton.js index 51da6e3fd1..c6438da277 100644 --- a/src/components/views/elements/SettingsButton.js +++ b/src/components/views/elements/SettingsButton.js @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import React from 'react'; import sdk from '../../../index'; import PropTypes from 'prop-types'; diff --git a/src/components/views/elements/StartChatButton.js b/src/components/views/elements/StartChatButton.js index 66cd911754..02d5677a7c 100644 --- a/src/components/views/elements/StartChatButton.js +++ b/src/components/views/elements/StartChatButton.js @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import React from 'react'; import sdk from '../../../index'; import PropTypes from 'prop-types'; From 548f319816d94955fc45fc68f51da5f6dcc2787e Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 5 May 2017 17:51:14 +0100 Subject: [PATCH 053/127] Remove redundant role elements --- src/components/views/rooms/RoomList.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 8c8fd3ea86..cde2bec7da 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -552,12 +552,12 @@ module.exports = React.createClass({ switch (section) { case 'im.vector.fake.direct': return - + ; case 'im.vector.fake.recent': return - - + + ; } }, From 3185d3ed41d376d52d65583247a08a74a12f1983 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 25 May 2017 13:54:59 +0100 Subject: [PATCH 054/127] Re-add bouncing/callout animation to action buttons --- src/components/views/elements/ActionButton.js | 4 ++++ src/components/views/elements/CreateRoomButton.js | 1 + src/components/views/elements/RoomDirectoryButton.js | 1 + src/components/views/elements/StartChatButton.js | 3 ++- src/components/views/rooms/RoomList.js | 6 +++--- 5 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/components/views/elements/ActionButton.js b/src/components/views/elements/ActionButton.js index 267388daf6..08fb6faa1d 100644 --- a/src/components/views/elements/ActionButton.js +++ b/src/components/views/elements/ActionButton.js @@ -27,6 +27,7 @@ export default React.createClass({ size: PropTypes.string, tooltip: PropTypes.bool, action: PropTypes.string.isRequired, + mouseOverAction: PropTypes.string, label: PropTypes.string.isRequired, iconPath: PropTypes.string.isRequired, }, @@ -51,6 +52,9 @@ export default React.createClass({ _onMouseEnter: function() { if (this.props.tooltip) this.setState({showTooltip: true}); + if (this.props.mouseOverAction) { + dis.dispatch({action: this.props.mouseOverAction}); + } }, _onMouseLeave: function() { diff --git a/src/components/views/elements/CreateRoomButton.js b/src/components/views/elements/CreateRoomButton.js index e7e526d36b..82643559b3 100644 --- a/src/components/views/elements/CreateRoomButton.js +++ b/src/components/views/elements/CreateRoomButton.js @@ -22,6 +22,7 @@ const CreateRoomButton = function(props) { const ActionButton = sdk.getComponent('elements.ActionButton'); return ( Press - + to start a chat with someone
; case 'im.vector.fake.recent': return
You're not in any rooms yet! Press - + to make a room or - + to browse the directory
; } From 7900bf1c7de0d170b3ca055332d4bebb3f10f4ef Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 25 May 2017 13:55:37 +0100 Subject: [PATCH 055/127] Don't show "Drop to ..." if total rooms = 0 --- src/components/views/rooms/RoomList.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 98a5229d6a..216dd972cf 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -540,6 +540,10 @@ module.exports = React.createClass({ } } + if (this.state.totalRoomCount === 0) { + return null; + } + const labelText = 'Drop here to ' + (VERBS[section] || 'tag ' + section); return ; From 51c8ee6db23f5eacaa348a58324df69704b06a15 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 25 May 2017 14:38:12 +0100 Subject: [PATCH 056/127] Allow teamServerConfig to be missing --- src/components/structures/LoggedInView.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index 4687afbee1..8cd2bf8a71 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -232,9 +232,15 @@ export default React.createClass({ break; case PageTypes.HomePage: + // If team server config is present, pass the teamServerURL. props.teamToken + // must also be set for the team page to be displayed, otherwise the + // welcomePageUrl is used (which might be undefined). + const teamServerUrl = this.props.config.teamServerConfig ? + this.props.config.teamServerConfig.teamServerURL : null; + page_element = ; From 2265b59287b44c0ad8d473c98e59a84209141df2 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 25 May 2017 14:54:28 +0100 Subject: [PATCH 057/127] Remove warm-fuzzy after setting mxid --- src/components/views/dialogs/SetMxIdDialog.js | 39 +------------------ 1 file changed, 1 insertion(+), 38 deletions(-) diff --git a/src/components/views/dialogs/SetMxIdDialog.js b/src/components/views/dialogs/SetMxIdDialog.js index c371fdd35a..86b5fccbc2 100644 --- a/src/components/views/dialogs/SetMxIdDialog.js +++ b/src/components/views/dialogs/SetMxIdDialog.js @@ -53,9 +53,6 @@ export default React.createClass({ doingUIAuth: false, // Indicate error with auth authError: '', - - // Indicate success of setting mxid - success: false, }; }, @@ -98,10 +95,6 @@ export default React.createClass({ }); }, - onSuccessContinue: function() { - this.props.onFinished(true, this._registeredCreds); - }, - _doUsernameCheck: function() { // Check if username is available return this._matrixClient.isUsernameAvailable(this.state.username).then( @@ -169,7 +162,7 @@ export default React.createClass({ // XXX Implement RTS /register here const teamToken = null; - this._registeredCreds = { + this.props.onFinished(true, { userId: response.user_id, deviceId: response.device_id, homeserverUrl: this._matrixClient.getHomeserverUrl(), @@ -177,11 +170,6 @@ export default React.createClass({ accessToken: response.access_token, password: this._generatedPassword, teamToken: teamToken, - }; - - // Before continuing, show a warm-fuzzy success and only submit onSuccessContinue - this.setState({ - success: true, }); }, @@ -231,31 +219,6 @@ export default React.createClass({ !this.state.usernameError && !this.state.usernameBusy; - if (this.state.success) { - // XXX BaseDialog needs an onFinished - return ( - -
-

- You have successfully - picked { this.state.username } as your - username and you now have access to the full - set of features on Riot. -

-
-
- -
-
- ); - } - return ( Date: Thu, 25 May 2017 15:20:02 +0100 Subject: [PATCH 058/127] Unbreak the roomlist --- src/components/views/rooms/RoomList.js | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 216dd972cf..efadda08ac 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -72,9 +72,6 @@ module.exports = React.createClass({ cli.on("RoomMember.name", this.onRoomMemberName); cli.on("accountData", this.onAccountData); - // lookup for which lists a given roomId is currently in. - this.listsForRoomId = {}; - this.refreshRoomList(); // order of the sublists @@ -260,7 +257,6 @@ module.exports = React.createClass({ // ", prevMembership = " + me.events.member.getPrevContent().membership); if (me.membership == "invite") { - self.listsForRoomId[room.roomId].push("im.vector.fake.invite"); lists["im.vector.fake.invite"].push(room); } else if (HIDE_CONFERENCE_CHANS && Rooms.isConfCallRoom(room, me, self.props.ConferenceHandler)) { @@ -277,22 +273,18 @@ module.exports = React.createClass({ var tagName = tagNames[i]; lists[tagName] = lists[tagName] || []; lists[tagName].push(room); - self.listsForRoomId[room.roomId].push(tagName); otherTagNames[tagName] = 1; } } else if (dmRoomMap.getUserIdForRoomId(room.roomId)) { // "Direct Message" rooms (that we're still in and that aren't otherwise tagged) - self.listsForRoomId[room.roomId].push("im.vector.fake.direct"); lists["im.vector.fake.direct"].push(room); } else { - self.listsForRoomId[room.roomId].push("im.vector.fake.recent"); lists["im.vector.fake.recent"].push(room); } } else if (me.membership === "leave") { - self.listsForRoomId[room.roomId].push("im.vector.fake.archived"); lists["im.vector.fake.archived"].push(room); } else { @@ -313,10 +305,8 @@ module.exports = React.createClass({ const me = room.getMember(MatrixClientPeg.get().credentials.userId); if (me && Rooms.looksLikeDirectMessageRoom(room, me)) { - self.listsForRoomId[room.roomId].push("im.vector.fake.direct"); lists["im.vector.fake.direct"].push(room); } else { - self.listsForRoomId[room.roomId].push("im.vector.fake.recent"); lists["im.vector.fake.recent"].push(room); } } From 11799b4c71330f8dd0636a386e7e6c2782e20f52 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 25 May 2017 15:27:54 +0100 Subject: [PATCH 059/127] Show "Password" instead of "New Password" when the existing password has been cached --- src/components/views/settings/ChangePassword.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js index bfc9ac264e..25af0e389f 100644 --- a/src/components/views/settings/ChangePassword.js +++ b/src/components/views/settings/ChangePassword.js @@ -193,12 +193,14 @@ module.exports = React.createClass({ switch (this.state.phase) { case this.Phases.Edit: + const passwordLabel = this.state.cachedPassword ? + 'Password' : 'New Password'; return (
{ currentPassword }
- +
From 91edc064416c202de31bf18ccfcc14b499ce6ee0 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 25 May 2017 17:04:42 +0100 Subject: [PATCH 060/127] Use RVS to indicate "joining" when setting a mxid This prevents RoomView from doing any peeking whilst the join/registration is in progress, causing weirdness with TimelinePanel getPendingEventList (which throws an error if called when peeking). --- src/components/structures/RoomView.js | 14 ++++++++++---- src/stores/RoomViewStore.js | 11 ++++++++++- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index e5e38a33d8..6e2a7df5ac 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -671,10 +671,6 @@ module.exports = React.createClass({ // compatability workaround, let's not bother. Rooms.setDMRoom(this.state.room.roomId, me.events.member.getSender()).done(); } - - this.setState({ - joining: false - }); } }, 500), @@ -762,12 +758,22 @@ module.exports = React.createClass({ }, }); + // Don't peek whilst registering otherwise getPendingEventList complains + // Do this by indicating our intention to join + dis.dispatch({ + action: 'will_join', + }); + const SetMxIdDialog = sdk.getComponent('views.dialogs.SetMxIdDialog'); const close = Modal.createDialog(SetMxIdDialog, { homeserverUrl: cli.getHomeserverUrl(), onFinished: (submitted, credentials) => { if (submitted) { this.props.onRegistered(credentials); + } else { + dis.dispatch({ + action: 'cancel_join', + }); } }, onDifferentServerClicked: (ev) => { diff --git a/src/stores/RoomViewStore.js b/src/stores/RoomViewStore.js index fe57079859..1ceef551a8 100644 --- a/src/stores/RoomViewStore.js +++ b/src/stores/RoomViewStore.js @@ -58,7 +58,16 @@ class RoomViewStore extends Store { case 'view_room': this._viewRoom(payload); break; - + case 'will_join': + this._setState({ + joining: true, + }); + break; + case 'cancel_join': + this._setState({ + joining: false, + }); + break; // join_room: // - opts: options for joinRoom case 'join_room': From 0849b0e20527ae3c2abf563a0232609ae78550cc Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 25 May 2017 17:10:49 +0100 Subject: [PATCH 061/127] Fix view_next_room, view_previous_room and view_indexed_room These must now make a dispatch to RoomViewStore instead of calling `viewRoom` directly on MatrixChat. This will call both `viewRoom` of MatrixChat _and_ the logic in RVS so there is some redundancy here. It'd be best to move as much as possible of viewRoom out to the RVS itself. But for now, this fixes a bug that occures when leaving (the viewed room would not change). --- src/components/structures/MatrixChat.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index a6f2ee820f..2e4a3b90ad 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -553,6 +553,7 @@ module.exports = React.createClass({ this.notifyNewScreen('register'); }, + // TODO: Move to RoomViewStore _viewNextRoom: function(roomIndexDelta) { const allRooms = RoomListSorter.mostRecentActivityFirst( MatrixClientPeg.get().getRooms(), @@ -566,15 +567,22 @@ module.exports = React.createClass({ } roomIndex = (roomIndex + roomIndexDelta) % allRooms.length; if (roomIndex < 0) roomIndex = allRooms.length - 1; - this._viewRoom({ room_id: allRooms[roomIndex].roomId }); + dis.dispatch({ + action: 'view_room', + room_id: allRooms[roomIndex].roomId, + }); }, + // TODO: Move to RoomViewStore _viewIndexedRoom: function(roomIndex) { const allRooms = RoomListSorter.mostRecentActivityFirst( MatrixClientPeg.get().getRooms(), ); if (allRooms[roomIndex]) { - this._viewRoom({ room_id: allRooms[roomIndex].roomId }); + dis.dispatch({ + action: 'view_room', + room_id: allRooms[roomIndex].roomId, + }); } }, From 263a51938d894896306fab155737aecc7c74b04a Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 25 May 2017 17:16:16 +0100 Subject: [PATCH 062/127] Reset store state when logging out This prevents leaking of state that we do not want to share with the next user --- src/stores/LifecycleStore.js | 5 +++++ src/stores/RoomViewStore.js | 3 +++ src/stores/SessionStore.js | 5 +++++ 3 files changed, 13 insertions(+) diff --git a/src/stores/LifecycleStore.js b/src/stores/LifecycleStore.js index 43e2de9d52..5dfe82500a 100644 --- a/src/stores/LifecycleStore.js +++ b/src/stores/LifecycleStore.js @@ -61,6 +61,11 @@ class LifecycleStore extends Store { }); dis.dispatch(deferredAction); break; + case 'on_logged_out': + this._state = { + deferred_action: null, + }; + break; } } } diff --git a/src/stores/RoomViewStore.js b/src/stores/RoomViewStore.js index 1ceef551a8..d893318af7 100644 --- a/src/stores/RoomViewStore.js +++ b/src/stores/RoomViewStore.js @@ -73,6 +73,9 @@ class RoomViewStore extends Store { case 'join_room': this._joinRoom(payload); break; + case 'on_logged_out': + this.reset(); + break; } } diff --git a/src/stores/SessionStore.js b/src/stores/SessionStore.js index 2fd35ce40a..5713e4d321 100644 --- a/src/stores/SessionStore.js +++ b/src/stores/SessionStore.js @@ -66,6 +66,11 @@ class SessionStore extends Store { cachedPassword: null, }); break; + case 'on_logged_out': + this._state = { + cachedPassword: null, + }; + break; } } From b5b157a0fb0bae097fc0cb62af5169b59eae31f4 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 26 May 2017 10:34:36 +0100 Subject: [PATCH 063/127] Don't show notif nag bar if guest --- src/components/structures/LoggedInView.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index 8cd2bf8a71..e7c1e00008 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -253,6 +253,7 @@ export default React.createClass({ break; } + const isGuest = this.props.matrixClient.isGuest(); var topBar; if (this.props.hasNewVersion) { topBar = ; } else if (this.state.userHasGeneratedPassword) { topBar = ; - } else if (Notifier.supportsDesktopNotifications() && !Notifier.isEnabled() && !Notifier.isToolbarHidden()) { + } else if (!isGuest && Notifier.supportsDesktopNotifications() && !Notifier.isEnabled() && !Notifier.isToolbarHidden()) { topBar = ; } From 2dcc03960a5b9fb362e3fd6a140cc28a61c85344 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 26 May 2017 11:46:33 +0100 Subject: [PATCH 064/127] Set the displayname to the mxid once PWLU --- src/components/structures/MatrixChat.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index a6f2ee820f..6d827b3ef0 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -805,6 +805,12 @@ module.exports = React.createClass({ dis.dispatch({action: 'view_home_page'}); } else if (this._is_registered) { this._is_registered = false; + + // Set the display name = entered username + MatrixClientPeg.get().setDisplayName( + MatrixClientPeg.get().getUserIdLocalpart() + ); + if (this.props.config.welcomeUserId) { createRoom({ dmUserId: this.props.config.welcomeUserId, From c0f43a14fd4c5b61bfe05e820a0091d4afa951d8 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 26 May 2017 11:47:55 +0100 Subject: [PATCH 065/127] Improve comment --- src/components/structures/MatrixChat.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 6d827b3ef0..3c23f649e3 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -806,7 +806,7 @@ module.exports = React.createClass({ } else if (this._is_registered) { this._is_registered = false; - // Set the display name = entered username + // Set the display name = user ID localpart MatrixClientPeg.get().setDisplayName( MatrixClientPeg.get().getUserIdLocalpart() ); From 2400efa92bc2f3622f02a0492102182ff935b613 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 26 May 2017 11:48:38 +0100 Subject: [PATCH 066/127] Correct LifecycleStore docs --- src/stores/LifecycleStore.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/stores/LifecycleStore.js b/src/stores/LifecycleStore.js index 43e2de9d52..3ed2e5c045 100644 --- a/src/stores/LifecycleStore.js +++ b/src/stores/LifecycleStore.js @@ -20,13 +20,6 @@ import {Store} from 'flux/utils'; * A class for storing application state to do with login/registration. This is a simple * flux store that listens for actions and updates its state accordingly, informing any * listeners (views) of state changes. - * - * Usage: - * ``` - * lifecycleStore.addListener(() => { - * this.setState({ cachedPassword: lifecycleStore.getCachedPassword() }) - * }) - * ``` */ class LifecycleStore extends Store { constructor() { From ad3373789fe094d5790119aaf5002205f1d92eb1 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 26 May 2017 11:50:32 +0100 Subject: [PATCH 067/127] Warn about LifecycleStore not explicitly being used --- 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 a6f2ee820f..ef2882fe13 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -32,6 +32,7 @@ import sdk from '../../index'; import * as Rooms from '../../Rooms'; import linkifyMatrix from "../../linkify-matrix"; import * as Lifecycle from '../../Lifecycle'; +// LifecycleStore is not used but does listen to and dispatch actions import LifecycleStore from '../../stores/LifecycleStore'; import RoomViewStore from '../../stores/RoomViewStore'; import PageTypes from '../../PageTypes'; From 28094a9a6634e5ec827bcf8481448b733bf44430 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 26 May 2017 13:13:57 +0100 Subject: [PATCH 068/127] Show "Something went wrong!" when errcode undefined --- src/components/views/dialogs/SetMxIdDialog.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/views/dialogs/SetMxIdDialog.js b/src/components/views/dialogs/SetMxIdDialog.js index 86b5fccbc2..72599a6b21 100644 --- a/src/components/views/dialogs/SetMxIdDialog.js +++ b/src/components/views/dialogs/SetMxIdDialog.js @@ -108,6 +108,7 @@ export default React.createClass({ const newState = { usernameCheckSupport: err.errcode !== "M_UNRECOGNIZED", }; + console.error('Error whilst checking username availability: ', err); switch (err.errcode) { case "M_USER_IN_USE": newState.usernameError = 'Username not available'; @@ -120,8 +121,11 @@ export default React.createClass({ // fine and rely on the error appearing in registration step. newState.usernameError = ''; break; + case undefined: + newState.usernameError = 'Something went wrong!'; + break; default: - newState.usernameError = 'An error occurred' + err.message; + newState.usernameError = 'An error occurred: ' + err.message; break; } this.setState(newState); From 5e136863b0558d1edc83b1413abbef56b4a19742 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 26 May 2017 13:18:44 +0100 Subject: [PATCH 069/127] Block user settings with view_set_mxid --- src/components/structures/MatrixChat.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index ef2882fe13..a03273c1c6 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -433,6 +433,10 @@ module.exports = React.createClass({ this._viewIndexedRoom(payload.roomIndex); break; case 'view_user_settings': + if (MatrixClientPeg.get().isGuest()) { + dis.dispatch({action: 'view_set_mxid'}); + break; + } this._setPage(PageTypes.UserSettings); this.notifyNewScreen('settings'); break; @@ -441,7 +445,6 @@ module.exports = React.createClass({ dis.dispatch({action: 'view_set_mxid'}); break; } - Modal.createDialog(TextInputDialog, { title: "Create Room", description: "Room name (optional)", From 9311b9012a4177c501a61c34a5d7d08b48f054f5 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 26 May 2017 17:23:02 +0100 Subject: [PATCH 070/127] Use the same `.reset` as RoomViewStore --- src/stores/LifecycleStore.js | 16 ++++++++++------ src/stores/SessionStore.js | 16 ++++++++++------ 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/stores/LifecycleStore.js b/src/stores/LifecycleStore.js index 5dfe82500a..f7e3ff9dbb 100644 --- a/src/stores/LifecycleStore.js +++ b/src/stores/LifecycleStore.js @@ -16,6 +16,10 @@ limitations under the License. import dis from '../dispatcher'; import {Store} from 'flux/utils'; +const INITIAL_STATE = { + deferred_action: null, +}; + /** * A class for storing application state to do with login/registration. This is a simple * flux store that listens for actions and updates its state accordingly, informing any @@ -33,9 +37,7 @@ class LifecycleStore extends Store { super(dis); // Initialise state - this._state = { - deferred_action: null, - }; + this._state = INITIAL_STATE; } _setState(newState) { @@ -62,12 +64,14 @@ class LifecycleStore extends Store { dis.dispatch(deferredAction); break; case 'on_logged_out': - this._state = { - deferred_action: null, - }; + this.reset(); break; } } + + reset() { + this._state = Object.assign({}, INITIAL_STATE); + } } let singletonLifecycleStore = null; diff --git a/src/stores/SessionStore.js b/src/stores/SessionStore.js index 5713e4d321..a4b49d9cea 100644 --- a/src/stores/SessionStore.js +++ b/src/stores/SessionStore.js @@ -16,6 +16,10 @@ limitations under the License. import dis from '../dispatcher'; import {Store} from 'flux/utils'; +const INITIAL_STATE = { + cachedPassword: localStorage.getItem('mx_pass'), +}; + /** * A class for storing application state to do with the session. This is a simple flux * store that listens for actions and updates its state accordingly, informing any @@ -33,9 +37,7 @@ class SessionStore extends Store { super(dis); // Initialise state - this._state = { - cachedPassword: localStorage.getItem('mx_pass'), - }; + this._state = INITIAL_STATE; } _update() { @@ -67,11 +69,13 @@ class SessionStore extends Store { }); break; case 'on_logged_out': - this._state = { - cachedPassword: null, - }; + this.reset(); break; } + + reset() { + this._state = Object.assign({}, INITIAL_STATE); + } } getCachedPassword() { From ac44151e2a2d8c9b158d8b8315ddb939430291f6 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 26 May 2017 17:27:31 +0100 Subject: [PATCH 071/127] Put the reset method in the right scope... --- src/stores/SessionStore.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/stores/SessionStore.js b/src/stores/SessionStore.js index a4b49d9cea..62868e4fe4 100644 --- a/src/stores/SessionStore.js +++ b/src/stores/SessionStore.js @@ -72,10 +72,10 @@ class SessionStore extends Store { this.reset(); break; } + } - reset() { - this._state = Object.assign({}, INITIAL_STATE); - } + reset() { + this._state = Object.assign({}, INITIAL_STATE); } getCachedPassword() { From 1efc5c2b2599120849ef7c01ebdef5d8941d4c15 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 28 May 2017 23:28:29 +0100 Subject: [PATCH 072/127] speed up SetMxIdDialog user check to 250ms as it was driving me MAD i18nize new bottomleftmenu buttons --- src/components/views/dialogs/SetMxIdDialog.js | 2 +- src/components/views/elements/CreateRoomButton.js | 3 ++- src/components/views/elements/HomeButton.js | 3 ++- src/components/views/elements/RoomDirectoryButton.js | 3 ++- src/components/views/elements/SettingsButton.js | 3 ++- src/components/views/elements/StartChatButton.js | 3 ++- src/i18n/strings/en_EN.json | 7 ++++++- 7 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/components/views/dialogs/SetMxIdDialog.js b/src/components/views/dialogs/SetMxIdDialog.js index 72599a6b21..d0aa067a70 100644 --- a/src/components/views/dialogs/SetMxIdDialog.js +++ b/src/components/views/dialogs/SetMxIdDialog.js @@ -23,7 +23,7 @@ import classnames from 'classnames'; // The amount of time to wait for further changes to the input username before // sending a request to the server -const USERNAME_CHECK_DEBOUNCE_MS = 2000; +const USERNAME_CHECK_DEBOUNCE_MS = 250; /** * Prompt the user to set a display name. diff --git a/src/components/views/elements/CreateRoomButton.js b/src/components/views/elements/CreateRoomButton.js index 82643559b3..f98974b489 100644 --- a/src/components/views/elements/CreateRoomButton.js +++ b/src/components/views/elements/CreateRoomButton.js @@ -17,13 +17,14 @@ limitations under the License. import React from 'react'; import sdk from '../../../index'; import PropTypes from 'prop-types'; +import { _t } from '../../../languageHandler'; const CreateRoomButton = function(props) { const ActionButton = sdk.getComponent('elements.ActionButton'); return ( Date: Mon, 29 May 2017 01:32:31 +0100 Subject: [PATCH 073/127] add login link to SetMxIdDialog --- src/components/structures/MatrixChat.js | 4 ++++ src/components/structures/RoomView.js | 4 ++++ src/components/views/dialogs/SetMxIdDialog.js | 5 +++++ 3 files changed, 13 insertions(+) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index c48a3731ae..1adb67887c 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -682,6 +682,10 @@ module.exports = React.createClass({ dis.dispatch({action: 'start_registration'}); close(); }, + onLoginClick: (ev) => { + dis.dispatch({action: 'start_login'}); + close(); + }, }).close; }, diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 7ed5ea6eab..6e853c135a 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -788,6 +788,10 @@ module.exports = React.createClass({ dis.dispatch({action: 'start_registration'}); close(); }, + onLoginClick: (ev) => { + dis.dispatch({action: 'start_login'}); + close(); + }, }).close; return; } diff --git a/src/components/views/dialogs/SetMxIdDialog.js b/src/components/views/dialogs/SetMxIdDialog.js index d0aa067a70..19924093de 100644 --- a/src/components/views/dialogs/SetMxIdDialog.js +++ b/src/components/views/dialogs/SetMxIdDialog.js @@ -36,6 +36,8 @@ export default React.createClass({ onFinished: React.PropTypes.func.isRequired, // Called when the user requests to register with a different homeserver onDifferentServerClicked: React.PropTypes.func.isRequired, + // Called if the user wants to switch to login instead + onLoginClick: React.PropTypes.func.isRequired, }, getInitialState: function() { @@ -245,6 +247,9 @@ export default React.createClass({ different server .

+

+ If you already have a Matrix account you can log in instead. +

{ auth } { authErrorIndicator }
From ad1b14967bfb06614c0b04e7a3dfe1483380225b Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 30 May 2017 03:59:06 +0100 Subject: [PATCH 074/127] hide rightpanel on welcome page --- src/components/structures/LoggedInView.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index f630503500..a297952a2c 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -244,7 +244,6 @@ export default React.createClass({ teamToken={this.props.teamToken} homePageUrl={this.props.config.welcomePageUrl} />; - if (!this.props.collapse_rhs) right_panel = break; case PageTypes.UserView: From 952651c6858b40f425c5efad86fcdb7558ab70d0 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 30 May 2017 13:02:35 +0100 Subject: [PATCH 075/127] Allow pressing Enter to submit setMxId --- src/components/views/dialogs/SetMxIdDialog.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/components/views/dialogs/SetMxIdDialog.js b/src/components/views/dialogs/SetMxIdDialog.js index 19924093de..61acbf2192 100644 --- a/src/components/views/dialogs/SetMxIdDialog.js +++ b/src/components/views/dialogs/SetMxIdDialog.js @@ -91,6 +91,12 @@ export default React.createClass({ }); }, + onKeyUp: function(ev) { + if (ev.keyCode === 13) { + this.onSubmit(); + } + }, + onSubmit: function(ev) { this.setState({ doingUIAuth: true, @@ -233,7 +239,10 @@ export default React.createClass({
{ usernameBusyIndicator } From 47bf5401fa06cc074bb6818b7b5c98c11c8de54a Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 30 May 2017 13:14:14 +0100 Subject: [PATCH 076/127] Use KeyCode.ENTER instead of 13 --- src/components/views/dialogs/SetMxIdDialog.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/dialogs/SetMxIdDialog.js b/src/components/views/dialogs/SetMxIdDialog.js index 61acbf2192..efca192ec1 100644 --- a/src/components/views/dialogs/SetMxIdDialog.js +++ b/src/components/views/dialogs/SetMxIdDialog.js @@ -20,6 +20,7 @@ import React from 'react'; import sdk from '../../../index'; import MatrixClientPeg from '../../../MatrixClientPeg'; import classnames from 'classnames'; +import KeyCode from '../../../KeyCode'; // The amount of time to wait for further changes to the input username before // sending a request to the server @@ -92,7 +93,7 @@ export default React.createClass({ }, onKeyUp: function(ev) { - if (ev.keyCode === 13) { + if (ev.keyCode === KeyCode.ENTER) { this.onSubmit(); } }, From 2baef643e3ee1a2a9e8a21f1778ab7aa8d237d93 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 30 May 2017 14:27:02 +0100 Subject: [PATCH 077/127] Add /start to show the setMxId above HomePage --- src/components/structures/MatrixChat.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 1adb67887c..e5fab4dde7 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -685,7 +685,7 @@ module.exports = React.createClass({ onLoginClick: (ev) => { dis.dispatch({action: 'start_login'}); close(); - }, + }, }).close; }, @@ -991,6 +991,11 @@ module.exports = React.createClass({ dis.dispatch({ action: 'view_home_page', }); + } else if (screen == 'start') { + this.showScreen('home'); + dis.dispatch({ + action: 'view_set_mxid', + }); } else if (screen == 'directory') { dis.dispatch({ action: 'view_room_directory', From 6f8d5b1db3dc088767640e6996fa29aac6cc4864 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 30 May 2017 16:10:19 +0100 Subject: [PATCH 078/127] Remove spurious reference to `otherTagNames` --- 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 d62ce4d460..0bca41e9e4 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -274,7 +274,6 @@ module.exports = React.createClass({ var tagName = tagNames[i]; lists[tagName] = lists[tagName] || []; lists[tagName].push(room); - otherTagNames[tagName] = 1; } } else if (dmRoomMap.getUserIdForRoomId(room.roomId)) { From 40154df9304877daccf6da1b46cd2e6db09d1d6a Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 30 May 2017 16:14:27 +0100 Subject: [PATCH 079/127] Show People/Rooms emptySubListTip even when total rooms !== 0 --- src/components/views/rooms/RoomList.js | 35 +++++++++++++------------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 0bca41e9e4..d672dea504 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -510,24 +510,23 @@ module.exports = React.createClass({ const StartChatButton = sdk.getComponent('elements.StartChatButton'); const RoomDirectoryButton = sdk.getComponent('elements.RoomDirectoryButton'); const CreateRoomButton = sdk.getComponent('elements.CreateRoomButton'); - if (this.state.totalRoomCount === 0) { - const TintableSvg = sdk.getComponent('elements.TintableSvg'); - switch (section) { - case 'im.vector.fake.direct': - return
- Press - - to start a chat with someone -
; - case 'im.vector.fake.recent': - return
- You're not in any rooms yet! Press - - to make a room or - - to browse the directory -
; - } + + const TintableSvg = sdk.getComponent('elements.TintableSvg'); + switch (section) { + case 'im.vector.fake.direct': + return
+ Press + + to start a chat with someone +
; + case 'im.vector.fake.recent': + return
+ You're not in any rooms yet! Press + + to make a room or + + to browse the directory +
; } if (this.state.totalRoomCount === 0) { From c15568e003a9a32c37bf6600b796698cea66cd41 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 30 May 2017 21:05:26 +0100 Subject: [PATCH 080/127] remove spurious string --- 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 15879f80ee..11727223e3 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -583,7 +583,6 @@ "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s", "%(weekDayName)s %(time)s": "%(weekDayName)s %(time)s", "Set a display name:": "Set a display name:", - "Set a Display Name": "Set a Display Name", "Upload an avatar:": "Upload an avatar:", "This server does not support authentication with a phone number.": "This server does not support authentication with a phone number.", "Missing password.": "Missing password.", From d83f18ab4669ae2d9b80ef06c3b16c698e5df4b0 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 31 May 2017 10:03:16 +0100 Subject: [PATCH 081/127] Remove cachedPassword from localStorage on_logged_out Fixes https://github.com/vector-im/riot-web/issues/4101 --- src/stores/SessionStore.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/stores/SessionStore.js b/src/stores/SessionStore.js index 62868e4fe4..6ec8ae7697 100644 --- a/src/stores/SessionStore.js +++ b/src/stores/SessionStore.js @@ -69,7 +69,9 @@ class SessionStore extends Store { }); break; case 'on_logged_out': - this.reset(); + this._setState({ + cachedPassword: null, + }); break; } } From d0e270bd1c24244a5dc7cd09801dac60fd792432 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 31 May 2017 15:13:27 +0100 Subject: [PATCH 082/127] Only re-render LoggedInView if MatrixClientPeg.get() is truthy --- src/components/structures/LoggedInView.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index a297952a2c..67e2fe8856 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -24,6 +24,7 @@ import PageTypes from '../../PageTypes'; import sdk from '../../index'; import dis from '../../dispatcher'; import sessionStore from '../../stores/SessionStore'; +import MatrixClientPeg from '../../MatrixClientPeg'; /** * This is what our MatrixChat shows when we are logged in. The precise view is @@ -91,6 +92,16 @@ export default React.createClass({ } }, + // Child components assume that the client peg will not be null, so give them some + // sort of assurance here by only allowing a re-render if the client is truthy. + // + // This is required because `LoggedInView` maintains its own state and if this state + // updates after the client peg has been made null (during logout), then it will + // attempt to re-render and the children will throw errors. + shouldComponentUpdate: function() { + return Boolean(MatrixClientPeg.get()); + }, + getScrollStateForRoom: function(roomId) { return this._scrollStateMap[roomId]; }, From b3a862c2c2f0fe2c0381ea12936775d46c86d894 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 31 May 2017 15:32:55 +0100 Subject: [PATCH 083/127] Remove redundant `reset` --- src/stores/SessionStore.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/stores/SessionStore.js b/src/stores/SessionStore.js index 6ec8ae7697..c4bd39b72c 100644 --- a/src/stores/SessionStore.js +++ b/src/stores/SessionStore.js @@ -76,10 +76,6 @@ class SessionStore extends Store { } } - reset() { - this._state = Object.assign({}, INITIAL_STATE); - } - getCachedPassword() { return this._state.cachedPassword; } From 8192374481935d79a27e61a967a965f2918e4f59 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 31 May 2017 16:02:42 +0100 Subject: [PATCH 084/127] Add missing _t import --- src/components/views/login/LoginFooter.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/login/LoginFooter.js b/src/components/views/login/LoginFooter.js index a5183ffff3..8bdec71685 100644 --- a/src/components/views/login/LoginFooter.js +++ b/src/components/views/login/LoginFooter.js @@ -16,6 +16,7 @@ limitations under the License. 'use strict'; +import { _t } from '../../../languageHandler'; import React from 'react'; module.exports = React.createClass({ @@ -27,5 +28,5 @@ module.exports = React.createClass({ {_t("powered by Matrix")}
); - } + }, }); From dd48b3f055c6197efb28d802b9b8eb29944229a5 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 1 Jun 2017 10:08:02 +0100 Subject: [PATCH 085/127] Add comment --- src/components/views/rooms/RoomList.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index d672dea504..15f8b34312 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -529,6 +529,7 @@ module.exports = React.createClass({
; } + // We don't want to display drop targets if there are no room tiles to drag'n'drop if (this.state.totalRoomCount === 0) { return null; } From 16c4c14a16acf40650f61f338e241c4e6c67fbdf Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 1 Jun 2017 18:01:30 +0100 Subject: [PATCH 086/127] Fix to show the correct room --- src/components/structures/RoomView.js | 9 +++- src/stores/RoomViewStore.js | 65 ++++++++++++++++++--------- 2 files changed, 51 insertions(+), 23 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 2189fa3856..9a930d3d06 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -171,7 +171,7 @@ module.exports = React.createClass({ }); // Start listening for RoomViewStore updates - RoomViewStore.addListener(this._onRoomViewStoreUpdate); + this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate); this._onRoomViewStoreUpdate(true); }, @@ -182,6 +182,8 @@ module.exports = React.createClass({ this.setState({ roomId: RoomViewStore.getRoomId(), roomAlias: RoomViewStore.getRoomAlias(), + roomLoading: RoomViewStore.isRoomLoading(), + roomLoadError: RoomViewStore.getRoomLoadError(), joining: RoomViewStore.isJoining(), joinError: RoomViewStore.getJoinError(), }, () => { @@ -343,6 +345,11 @@ module.exports = React.createClass({ document.removeEventListener("keydown", this.onKeyDown); + // Remove RoomStore listener + if (this._roomStoreToken) { + this._roomStoreToken.remove(); + } + // cancel any pending calls to the rate_limited_funcs this._updateRoomMembers.cancelPendingCall(); diff --git a/src/stores/RoomViewStore.js b/src/stores/RoomViewStore.js index d893318af7..a74652a39d 100644 --- a/src/stores/RoomViewStore.js +++ b/src/stores/RoomViewStore.js @@ -58,6 +58,9 @@ class RoomViewStore extends Store { case 'view_room': this._viewRoom(payload); break; + case 'view_room_error': + this._viewRoomError(payload); + break; case 'will_join': this._setState({ joining: true, @@ -80,31 +83,45 @@ class RoomViewStore extends Store { } _viewRoom(payload) { - const address = payload.room_alias || payload.room_id; - if (address[0] == '#') { + // Always set the room ID if present + if (payload.room_id) { this._setState({ - roomLoading: true, - }); - MatrixClientPeg.get().getRoomIdForAlias(address).then( - (result) => { - this._setState({ - roomId: result.room_id, - roomAlias: address, - roomLoading: false, - roomLoadError: null, - }); - }, (err) => { - console.error(err); - this._setState({ - roomLoading: false, - roomLoadError: err, - }); - }); - } else { - this._setState({ - roomId: address, + roomId: payload.room_id, }); } + + if (payload.room_alias && !payload.room_id) { + this._setState({ + roomId: null, + roomAlias: payload.room_alias, + roomLoading: true, + roomLoadError: null, + }); + MatrixClientPeg.get().getRoomIdForAlias(payload.room_alias).done( + (result) => { + dis.dispatch({ + action: 'view_room', + room_id: result.room_id, + room_alias: payload.room_alias, + }); + }, (err) => { + dis.dispatch({ + action: 'view_room_error', + room_id: null, + room_alias: payload.room_alias, + err: err, + }); + }); + } + } + + _viewRoomError(payload) { + this._setState({ + roomId: payload.room_id, + roomAlias: payload.room_alias, + roomLoading: false, + roomLoadError: payload.err, + }); } _joinRoom(payload) { @@ -140,6 +157,10 @@ class RoomViewStore extends Store { return this._state.roomLoading; } + getRoomLoadError() { + return this._state.roomLoadError; + } + isJoining() { return this._state.joining; } From 7808994b719ef4cce19c9bcca3aba52315c1fc9a Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 2 Jun 2017 09:22:48 +0100 Subject: [PATCH 087/127] Modify RVS test to wait until room loaded This allows for the alias resolution to occur before a join is attempted. In theory, join_room could in future do an optional view_room-esque thing before attemping a join which would be less fragile than dispatching things in the right order. Also, make sure the store indicates that it is not loading when a room ID has been used - no alias resolution need take place. --- src/stores/RoomViewStore.js | 6 +++--- test/stores/RoomViewStore-test.js | 15 +++++++++------ 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/stores/RoomViewStore.js b/src/stores/RoomViewStore.js index a74652a39d..b94e772b02 100644 --- a/src/stores/RoomViewStore.js +++ b/src/stores/RoomViewStore.js @@ -87,10 +87,10 @@ class RoomViewStore extends Store { if (payload.room_id) { this._setState({ roomId: payload.room_id, + roomLoading: false, + roomLoadError: null, }); - } - - if (payload.room_alias && !payload.room_id) { + } else if (payload.room_alias) { this._setState({ roomId: null, roomAlias: payload.room_alias, diff --git a/test/stores/RoomViewStore-test.js b/test/stores/RoomViewStore-test.js index 7100dced19..2f545ffd74 100644 --- a/test/stores/RoomViewStore-test.js +++ b/test/stores/RoomViewStore-test.js @@ -45,12 +45,15 @@ describe('RoomViewStore', function() { done(); }; - dispatch({ action: 'view_room', room_alias: '#somealias2:aser.ver' }); + RoomViewStore.addListener(() => { + // Wait until the room alias has resolved and the room ID is + if (!RoomViewStore.isRoomLoading()) { + expect(RoomViewStore.getRoomId()).toBe("!randomcharacters:aser.ver"); + dispatch({ action: 'join_room' }); + expect(RoomViewStore.isJoining()).toBe(true); + } + }); - // Wait for the next event loop to allow for room alias resolution - setTimeout(() => { - dispatch({ action: 'join_room' }); - expect(RoomViewStore.isJoining()).toBe(true); - }, 0); + dispatch({ action: 'view_room', room_alias: '#somealias2:aser.ver' }); }); }); From defecb1b147bb2525af9d9fe20325be2311cec02 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 1 Jun 2017 14:16:25 +0100 Subject: [PATCH 088/127] Implement /user/@userid:domain?action=chat This is a URL that can be used to start a chat with a user. - If the user is a guest, setMxId dialog will appear before anything and a defered action will cause `ChatCreateOrReuseDialog` to appear once they've logged in. - If the user is registered, they will not see the setMxId dialog. fixes https://github.com/vector-im/riot-web/issues/4034 --- src/components/structures/MatrixChat.js | 64 +++++++- src/components/views/avatars/BaseAvatar.js | 1 + .../views/dialogs/ChatCreateOrReuseDialog.js | 152 +++++++++++++----- .../views/dialogs/ChatInviteDialog.js | 21 ++- src/createRoom.js | 1 + src/i18n/strings/en_EN.json | 8 +- src/stores/LifecycleStore.js | 2 + 7 files changed, 202 insertions(+), 47 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index ff864379fa..a081ce6fe7 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -139,6 +139,10 @@ module.exports = React.createClass({ register_hs_url: null, register_is_url: null, register_id_sid: null, + + // Whether a DM should be created with welcomeUserId (prop) on registration + // see _onLoggedIn + shouldCreateWelcomeDm: true, }; return s; }, @@ -378,6 +382,11 @@ module.exports = React.createClass({ }); this.notifyNewScreen('forgot_password'); break; + case 'start_chat': + createRoom({ + dmUserId: payload.user_id, + }); + break; case 'leave_room': this._leaveRoom(payload.room_id); break; @@ -474,6 +483,9 @@ module.exports = React.createClass({ case 'view_set_mxid': this._setMxId(); break; + case 'view_start_chat_or_reuse': + this._chatCreateOrReuse(payload.user_id); + break; case 'view_create_chat': this._createChat(); break; @@ -707,6 +719,50 @@ module.exports = React.createClass({ }); }, + _chatCreateOrReuse: function(userId) { + const ChatCreateOrReuseDialog = sdk.getComponent( + 'views.dialogs.ChatCreateOrReuseDialog', + ); + if (MatrixClientPeg.get().isGuest()) { + dis.dispatch({ + action: 'do_after_sync_prepared', + deferred_action: { + action: 'view_start_chat_or_reuse', + user_id: userId, + }, + }); + dis.dispatch({ + action: 'view_set_mxid', + }); + return; + } + + const close = Modal.createDialog(ChatCreateOrReuseDialog, { + userId: userId, + onFinished: (success) => { + if (!success) { + // Dialog cancelled, default to home + dis.dispatch({ action: 'view_home_page' }); + } + }, + onNewDMClick: () => { + dis.dispatch({ + action: 'start_chat', + user_id: userId, + }); + // Close the dialog, indicate success (calls onFinished(true)) + close(true); + }, + onExistingRoomSelected: (roomId) => { + dis.dispatch({ + action: 'view_room', + room_id: roomId, + }); + close(true); + }, + }).close; + }, + _invite: function(roomId) { const ChatInviteDialog = sdk.getComponent("dialogs.ChatInviteDialog"); Modal.createDialog(ChatInviteDialog, { @@ -838,7 +894,7 @@ module.exports = React.createClass({ MatrixClientPeg.get().getUserIdLocalpart() ); - if (this.props.config.welcomeUserId) { + if (this.props.config.welcomeUserId && this.state.shouldCreateWelcomeDm) { createRoom({ dmUserId: this.props.config.welcomeUserId, andView: false, @@ -1043,6 +1099,12 @@ module.exports = React.createClass({ } } else if (screen.indexOf('user/') == 0) { const userId = screen.substring(5); + + if (params.action === 'chat') { + this._chatCreateOrReuse(userId); + return; + } + this.setState({ viewUserId: userId }); this._setPage(PageTypes.UserView); this.notifyNewScreen('user/' + userId); diff --git a/src/components/views/avatars/BaseAvatar.js b/src/components/views/avatars/BaseAvatar.js index 65730be40b..a4443430f4 100644 --- a/src/components/views/avatars/BaseAvatar.js +++ b/src/components/views/avatars/BaseAvatar.js @@ -32,6 +32,7 @@ module.exports = React.createClass({ urls: React.PropTypes.array, // [highest_priority, ... , lowest_priority] width: React.PropTypes.number, height: React.PropTypes.number, + // XXX resizeMethod not actually used. resizeMethod: React.PropTypes.string, defaultToInitialLetter: React.PropTypes.bool // true to add default url }, diff --git a/src/components/views/dialogs/ChatCreateOrReuseDialog.js b/src/components/views/dialogs/ChatCreateOrReuseDialog.js index f563af6691..93ebf8cc2f 100644 --- a/src/components/views/dialogs/ChatCreateOrReuseDialog.js +++ b/src/components/views/dialogs/ChatCreateOrReuseDialog.js @@ -18,34 +18,30 @@ import React from 'react'; import sdk from '../../../index'; import dis from '../../../dispatcher'; import MatrixClientPeg from '../../../MatrixClientPeg'; +import { _t } from '../../../languageHandler'; import DMRoomMap from '../../../utils/DMRoomMap'; import AccessibleButton from '../elements/AccessibleButton'; import Unread from '../../../Unread'; import classNames from 'classnames'; import createRoom from '../../../createRoom'; +import { RoomMember } from "matrix-js-sdk"; export default class ChatCreateOrReuseDialog extends React.Component { constructor(props) { super(props); - this.onNewDMClick = this.onNewDMClick.bind(this); this.onRoomTileClick = this.onRoomTileClick.bind(this); + + this.state = { + tiles: [], + profile: { + displayName: null, + avatarUrl: null, + }, + }; } - onNewDMClick() { - createRoom({dmUserId: this.props.userId}); - this.props.onFinished(true); - } - - onRoomTileClick(roomId) { - dis.dispatch({ - action: 'view_room', - room_id: roomId, - }); - this.props.onFinished(true); - } - - render() { + componentWillMount() { const client = MatrixClientPeg.get(); const dmRoomMap = new DMRoomMap(client); @@ -70,40 +66,115 @@ export default class ChatCreateOrReuseDialog extends React.Component { highlight={highlight} isInvite={me.membership == "invite"} onClick={this.onRoomTileClick} - /> + />, ); } } - const labelClasses = classNames({ - mx_MemberInfo_createRoom_label: true, - mx_RoomTile_name: true, + this.setState({ + tiles: tiles, }); - const startNewChat = -
- -
-
{_("Start new chat")}
-
; + + if (tiles.length === 0) { + this.setState({ + busyProfile: true, + }); + MatrixClientPeg.get().getProfileInfo(this.props.userId).then((resp) => { + const profile = { + displayName: resp.displayname, + avatarUrl: null, + }; + if (resp.avatar_url) { + profile.avatarUrl = MatrixClientPeg.get().mxcUrlToHttp( + resp.avatar_url, 48, 48, "crop", + ); + } + this.setState({ + profile: profile, + }); + }, (err) => { + console.error('Unable to get profile for user', this.props.userId, err); + }).finally(() => { + this.setState({ + busyProfile: false, + }); + }); + } + } + + onRoomTileClick(roomId) { + this.props.onExistingRoomSelected(roomId); + } + + render() { + let title = ''; + let content = null; + if (this.state.tiles.length > 0) { + // Show the existing rooms with a "+" to add a new dm + title = _t('Create a new chat or reuse an existing one'); + const labelClasses = classNames({ + mx_MemberInfo_createRoom_label: true, + mx_RoomTile_name: true, + }); + const startNewChat = +
+ +
+
{ _t("Start new chat") }
+
; + content =
+ { _t('You already have existing direct chats with this user:') } +
+ { this.state.tiles } + { startNewChat } +
+
; + } else { + // Show the avatar, name and a button to confirm that a new chat is requested + const BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); + const Spinner = sdk.getComponent('elements.Spinner'); + title = _t('Start chatting'); + + let profile = null; + if (this.state.busyProfile) { + profile = ; + } else { + profile =
+ +
+ {this.state.profile.displayName || this.props.userId} +
+
; + } + content =
+
+

+ { _t('Click on the button below to start chatting!') } +

+ { profile } +
+
+ +
+
; + } const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); return ( { - this.props.onFinished(false) - }} - title='Create a new chat or reuse an existing one' + onFinished={ this.props.onFinished.bind(false) } + title={title} > -
- You already have existing direct chats with this user: -
- {tiles} - {startNewChat} -
-
+ { content }
); } @@ -111,5 +182,8 @@ export default class ChatCreateOrReuseDialog extends React.Component { ChatCreateOrReuseDialog.propTyps = { userId: React.PropTypes.string.isRequired, + // Called when clicking outside of the dialog onFinished: React.PropTypes.func.isRequired, + onNewDMClick: React.PropTypes.func.isRequired, + onExistingRoomSelected: React.PropTypes.func.isRequired, }; diff --git a/src/components/views/dialogs/ChatInviteDialog.js b/src/components/views/dialogs/ChatInviteDialog.js index b50a782073..f475635212 100644 --- a/src/components/views/dialogs/ChatInviteDialog.js +++ b/src/components/views/dialogs/ChatInviteDialog.js @@ -95,16 +95,25 @@ module.exports = React.createClass({ // A Direct Message room already exists for this user, so select a // room from a list that is similar to the one in MemberInfo panel const ChatCreateOrReuseDialog = sdk.getComponent( - "views.dialogs.ChatCreateOrReuseDialog" + "views.dialogs.ChatCreateOrReuseDialog", ); Modal.createDialog(ChatCreateOrReuseDialog, { userId: userId, onFinished: (success) => { - if (success) { - this.props.onFinished(true, inviteList[0]); - } - // else show this ChatInviteDialog again - } + this.props.onFinished(success); + }, + onNewDMClick: () => { + dis.dispatch({ + action: 'start_chat', + user_id: userId, + }); + }, + onExistingRoomSelected: (roomId) => { + dis.dispatch({ + action: 'view_room', + user_id: roomId, + }); + }, }); } else { this._startChat(inviteList); diff --git a/src/createRoom.js b/src/createRoom.js index f476a86dcc..1522dbc9a8 100644 --- a/src/createRoom.js +++ b/src/createRoom.js @@ -97,6 +97,7 @@ function createRoom(opts) { // the room exists, causing things like // https://github.com/vector-im/vector-web/issues/1813 if (opts.andView) { + console.info('And viewing'); dis.dispatch({ action: 'view_room', room_id: roomId, diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 11727223e3..c8d9614961 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -771,5 +771,11 @@ "Idle": "Idle", "Offline": "Offline", "disabled": "disabled", - "enabled": "enabled" + "enabled": "enabled", + "Start chatting": "Start chatting", + "Start Chatting": "Start Chatting", + "Click on the button below to start chatting!": "Click on the button below to start chatting!", + "Create a new chat or reuse an existing one": "Create a new chat or reuse an existing one", + "You already have existing direct chats with this user:": "You already have existing direct chats with this user:", + "Start new chat": "Start new chat" } diff --git a/src/stores/LifecycleStore.js b/src/stores/LifecycleStore.js index d38138b3ef..b06c03b778 100644 --- a/src/stores/LifecycleStore.js +++ b/src/stores/LifecycleStore.js @@ -41,6 +41,7 @@ class LifecycleStore extends Store { __onDispatch(payload) { switch (payload.action) { case 'do_after_sync_prepared': + console.info('Will do after sync', payload.deferred_action); this._setState({ deferred_action: payload.deferred_action, }); @@ -49,6 +50,7 @@ class LifecycleStore extends Store { if (payload.state !== 'PREPARED') { break; } + console.info('Doing', payload.deferred_action); if (!this._state.deferred_action) break; const deferredAction = Object.assign({}, this._state.deferred_action); this._setState({ From 6a9781f0231c532fde87f99e78ce84a5b3cfbdc2 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 2 Jun 2017 11:41:09 +0100 Subject: [PATCH 089/127] Remove redundant state --- src/components/structures/MatrixChat.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index a081ce6fe7..7b51fdb524 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -139,10 +139,6 @@ module.exports = React.createClass({ register_hs_url: null, register_is_url: null, register_id_sid: null, - - // Whether a DM should be created with welcomeUserId (prop) on registration - // see _onLoggedIn - shouldCreateWelcomeDm: true, }; return s; }, @@ -894,7 +890,7 @@ module.exports = React.createClass({ MatrixClientPeg.get().getUserIdLocalpart() ); - if (this.props.config.welcomeUserId && this.state.shouldCreateWelcomeDm) { + if (this.props.config.welcomeUserId) { createRoom({ dmUserId: this.props.config.welcomeUserId, andView: false, From e88b52fa8fd49cfca3517338c2a1da4558875fff Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 2 Jun 2017 11:42:47 +0100 Subject: [PATCH 090/127] Add comment --- 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 7b51fdb524..5791ffe49a 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -719,6 +719,7 @@ module.exports = React.createClass({ const ChatCreateOrReuseDialog = sdk.getComponent( 'views.dialogs.ChatCreateOrReuseDialog', ); + // Use a deferred action to reshow the dialog once the user has registered if (MatrixClientPeg.get().isGuest()) { dis.dispatch({ action: 'do_after_sync_prepared', From 6e84b6e996db49511a9ec5315ce210ef07275cbc Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 2 Jun 2017 11:50:58 +0100 Subject: [PATCH 091/127] Display profile errors better --- .../views/dialogs/ChatCreateOrReuseDialog.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/components/views/dialogs/ChatCreateOrReuseDialog.js b/src/components/views/dialogs/ChatCreateOrReuseDialog.js index 93ebf8cc2f..fdb4e994ae 100644 --- a/src/components/views/dialogs/ChatCreateOrReuseDialog.js +++ b/src/components/views/dialogs/ChatCreateOrReuseDialog.js @@ -38,6 +38,7 @@ export default class ChatCreateOrReuseDialog extends React.Component { displayName: null, avatarUrl: null, }, + profileError: null, }; } @@ -79,7 +80,7 @@ export default class ChatCreateOrReuseDialog extends React.Component { this.setState({ busyProfile: true, }); - MatrixClientPeg.get().getProfileInfo(this.props.userId).then((resp) => { + MatrixClientPeg.get().getProfileInfo(this.props.userId).done((resp) => { const profile = { displayName: resp.displayname, avatarUrl: null, @@ -90,13 +91,17 @@ export default class ChatCreateOrReuseDialog extends React.Component { ); } this.setState({ + busyProfile: false, profile: profile, }); }, (err) => { - console.error('Unable to get profile for user', this.props.userId, err); - }).finally(() => { + console.error( + 'Unable to get profile for user ' + this.props.userId + ':', + err, + ); this.setState({ busyProfile: false, + profileError: err, }); }); } @@ -141,6 +146,10 @@ export default class ChatCreateOrReuseDialog extends React.Component { let profile = null; if (this.state.busyProfile) { profile = ; + } else if (this.state.profileError) { + profile =
+ Unable to load profile information for { this.props.userId } +
; } else { profile =
Date: Fri, 2 Jun 2017 11:53:10 +0100 Subject: [PATCH 092/127] Propagate room join errors to the UI Dispatch so we can set the state in RoomViewStore. Show the error when the room join fails (unsure if it's better to do this from the component or the store). Remove unused joinError from roomview. --- src/components/structures/RoomView.js | 1 - src/stores/RoomViewStore.js | 47 +++++++++++++++++++++++---- 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 9a930d3d06..3151fd28f9 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -185,7 +185,6 @@ module.exports = React.createClass({ roomLoading: RoomViewStore.isRoomLoading(), roomLoadError: RoomViewStore.getRoomLoadError(), joining: RoomViewStore.isJoining(), - joinError: RoomViewStore.getJoinError(), }, () => { this._onHaveRoom(); this.onRoom(MatrixClientPeg.get().getRoom(this.state.roomId)); diff --git a/src/stores/RoomViewStore.js b/src/stores/RoomViewStore.js index b94e772b02..b0957ec3df 100644 --- a/src/stores/RoomViewStore.js +++ b/src/stores/RoomViewStore.js @@ -16,6 +16,9 @@ limitations under the License. import dis from '../dispatcher'; import {Store} from 'flux/utils'; import MatrixClientPeg from '../MatrixClientPeg'; +import sdk from '../index'; +import Modal from '../Modal'; +import { _t } from '../languageHandler'; const INITIAL_STATE = { // Whether we're joining the currently viewed room @@ -76,6 +79,12 @@ class RoomViewStore extends Store { case 'join_room': this._joinRoom(payload); break; + case 'joined_room': + this._joinedRoom(payload); + break; + case 'join_room_error': + this._joinRoomError(payload); + break; case 'on_logged_out': this.reset(); break; @@ -128,19 +137,43 @@ class RoomViewStore extends Store { this._setState({ joining: true, }); - MatrixClientPeg.get().joinRoom(this._state.roomId, payload.opts).then( - () => { - this._setState({ - joining: false, + MatrixClientPeg.get().joinRoom(this._state.roomId, payload.opts).done(() => { + dis.dispatch({ + action: 'joined_room', + room_id: this._state.roomId, }); }, (err) => { - this._setState({ - joining: false, - joinError: err, + dis.dispatch({ + action: 'join_room_error', + room_id: this._state.roomId, + err: err, + }); + const msg = err.message ? err.message : JSON.stringify(err); + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createDialog(ErrorDialog, { + title: _t("Failed to join room"), + description: msg, }); }); } + _joinedRoom(payload) { + if (payload.room_id === this._state.roomId) { + this._setState({ + joining: false, + }); + } + } + + _joinRoomError(payload) { + if (payload.room_id === this._state.roomId) { + this._setState({ + joining: false, + joinError: payload.err, + }); + } + } + reset() { this._state = Object.assign({}, INITIAL_STATE); } From 95e38eb9c458fb0cc8e5a56a8277e0510c966957 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 2 Jun 2017 12:02:37 +0100 Subject: [PATCH 093/127] Remove cryptic log --- src/createRoom.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/createRoom.js b/src/createRoom.js index 1522dbc9a8..f476a86dcc 100644 --- a/src/createRoom.js +++ b/src/createRoom.js @@ -97,7 +97,6 @@ function createRoom(opts) { // the room exists, causing things like // https://github.com/vector-im/vector-web/issues/1813 if (opts.andView) { - console.info('And viewing'); dis.dispatch({ action: 'view_room', room_id: roomId, From f52035f3cd7aa63db1a3c56ef23af16092dce868 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 2 Jun 2017 13:41:41 +0100 Subject: [PATCH 094/127] Set state from dispatch payload unconditionally As apparently doing it confitionally is bad --- src/stores/RoomViewStore.js | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/stores/RoomViewStore.js b/src/stores/RoomViewStore.js index b0957ec3df..6ba0e45f68 100644 --- a/src/stores/RoomViewStore.js +++ b/src/stores/RoomViewStore.js @@ -158,20 +158,16 @@ class RoomViewStore extends Store { } _joinedRoom(payload) { - if (payload.room_id === this._state.roomId) { - this._setState({ - joining: false, - }); - } + this._setState({ + joining: false, + }); } _joinRoomError(payload) { - if (payload.room_id === this._state.roomId) { - this._setState({ - joining: false, - joinError: payload.err, - }); - } + this._setState({ + joining: false, + joinError: payload.err, + }); } reset() { From ac0f2f79d18d6b9e2ec2a98b85f4d9150acc24d6 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 2 Jun 2017 16:02:20 +0100 Subject: [PATCH 095/127] Remove redundant room_id --- src/stores/RoomViewStore.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/stores/RoomViewStore.js b/src/stores/RoomViewStore.js index 6ba0e45f68..cc8959af7a 100644 --- a/src/stores/RoomViewStore.js +++ b/src/stores/RoomViewStore.js @@ -140,12 +140,10 @@ class RoomViewStore extends Store { MatrixClientPeg.get().joinRoom(this._state.roomId, payload.opts).done(() => { dis.dispatch({ action: 'joined_room', - room_id: this._state.roomId, }); }, (err) => { dis.dispatch({ action: 'join_room_error', - room_id: this._state.roomId, err: err, }); const msg = err.message ? err.message : JSON.stringify(err); From 2cc9f9c4030fb81f844ee4c3aeaee020e5b790ee Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 2 Jun 2017 16:25:14 +0100 Subject: [PATCH 096/127] Fix accepting a 3pid invite Fixes https://github.com/vector-im/riot-web/issues/4123 --- src/components/structures/RoomView.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 3151fd28f9..3be7073a4f 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -764,11 +764,13 @@ module.exports = React.createClass({ // If the user is a ROU, allow them to transition to a PWLU if (cli && cli.isGuest()) { // Join this room once the user has registered and logged in + const signUrl = this.props.thirdPartyInvite ? + this.props.thirdPartyInvite.inviteSignUrl : undefined; dis.dispatch({ action: 'do_after_sync_prepared', deferred_action: { action: 'join_room', - room_id: this.state.roomId, + opts: { inviteSignUrl: signUrl }, }, }); From 239874ccced02705aa706363bfa183735b9a4760 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 5 Jun 2017 09:52:39 +0100 Subject: [PATCH 097/127] Introduce state `peekLoading` to avoid collision with `roomLoading` The room loading spinner will now be displayed if the alias is being resolved (roomLoading) or if the peek is being loaded for the room `peekLoading`. --- src/components/structures/RoomView.js | 10 +++++++--- src/stores/LifecycleStore.js | 2 -- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 3be7073a4f..d3e0f6ee2d 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -120,6 +120,7 @@ module.exports = React.createClass({ room: null, roomId: null, roomLoading: true, + peekLoading: false, forwardingEvent: null, editingRoomSettings: false, @@ -222,10 +223,13 @@ module.exports = React.createClass({ } else if (this.state.roomId) { console.log("Attempting to peek into room %s", this.state.roomId); + this.setState({ + peekLoading: true, + }); MatrixClientPeg.get().peekInRoom(this.state.roomId).then((room) => { this.setState({ room: room, - roomLoading: false, + peekLoading: false, }); this._onRoomLoaded(room); }, (err) => { @@ -235,7 +239,7 @@ module.exports = React.createClass({ if (err.errcode == "M_GUEST_ACCESS_FORBIDDEN") { // This is fine: the room just isn't peekable (we assume). this.setState({ - roomLoading: false, + peekLoading: false, }); } else { throw err; @@ -1422,7 +1426,7 @@ module.exports = React.createClass({ const TimelinePanel = sdk.getComponent("structures.TimelinePanel"); if (!this.state.room) { - if (this.state.roomLoading) { + if (this.state.roomLoading || this.state.peekLoading) { return (
diff --git a/src/stores/LifecycleStore.js b/src/stores/LifecycleStore.js index b06c03b778..d38138b3ef 100644 --- a/src/stores/LifecycleStore.js +++ b/src/stores/LifecycleStore.js @@ -41,7 +41,6 @@ class LifecycleStore extends Store { __onDispatch(payload) { switch (payload.action) { case 'do_after_sync_prepared': - console.info('Will do after sync', payload.deferred_action); this._setState({ deferred_action: payload.deferred_action, }); @@ -50,7 +49,6 @@ class LifecycleStore extends Store { if (payload.state !== 'PREPARED') { break; } - console.info('Doing', payload.deferred_action); if (!this._state.deferred_action) break; const deferredAction = Object.assign({}, this._state.deferred_action); this._setState({ From beafb68538932380fd2c0d351d8213cd32f57b8b Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 5 Jun 2017 13:14:55 +0100 Subject: [PATCH 098/127] Don't do a deferred start chat if user is welcome user There's no point in deferring creating a new DM with the welcome user because the setMxId dialog will do so anyway. --- src/components/structures/MatrixChat.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 5791ffe49a..c376244d2a 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -721,13 +721,17 @@ module.exports = React.createClass({ ); // Use a deferred action to reshow the dialog once the user has registered if (MatrixClientPeg.get().isGuest()) { - dis.dispatch({ - action: 'do_after_sync_prepared', - deferred_action: { - action: 'view_start_chat_or_reuse', - user_id: userId, - }, - }); + // No point in making 2 DMs with welcome bot. This assumes view_set_mxid will + // result in a new DM with the welcome user. + if (userId !== this.props.config.welcomeUserId) { + dis.dispatch({ + action: 'do_after_sync_prepared', + deferred_action: { + action: 'view_start_chat_or_reuse', + user_id: userId, + }, + }); + } dis.dispatch({ action: 'view_set_mxid', }); From 3195b9f964998ae61c94d8e96c60dd6aa61d6db5 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 5 Jun 2017 13:41:52 +0100 Subject: [PATCH 099/127] Keep deferred actions for view_user_settings and view_create_chat This will bring up the correct UI as intended instead of defaulting to the home page with welcome user in the room list. Fixes https://github.com/vector-im/riot-web/issues/4162 --- src/components/structures/MatrixChat.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 5791ffe49a..e5912f58d5 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -444,6 +444,12 @@ module.exports = React.createClass({ break; case 'view_user_settings': if (MatrixClientPeg.get().isGuest()) { + dis.dispatch({ + action: 'do_after_sync_prepared', + deferred_action: { + action: 'view_user_settings', + }, + }); dis.dispatch({action: 'view_set_mxid'}); break; } @@ -703,7 +709,13 @@ module.exports = React.createClass({ _createChat: function() { if (MatrixClientPeg.get().isGuest()) { - this._setMxId(); + dis.dispatch({ + action: 'do_after_sync_prepared', + deferred_action: { + action: 'view_create_chat', + }, + }); + dis.dispatch({action: 'view_set_mxid'}); return; } const ChatInviteDialog = sdk.getComponent("dialogs.ChatInviteDialog"); From c82e79ab5f760ca2dbd9dcac7e7af27db365c8ab Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 5 Jun 2017 14:07:24 +0100 Subject: [PATCH 100/127] Only view welcome user if we are not looking at a room --- src/components/structures/MatrixChat.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index c376244d2a..4d79a73d46 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -892,13 +892,14 @@ module.exports = React.createClass({ // Set the display name = user ID localpart MatrixClientPeg.get().setDisplayName( - MatrixClientPeg.get().getUserIdLocalpart() + MatrixClientPeg.get().getUserIdLocalpart(), ); if (this.props.config.welcomeUserId) { createRoom({ dmUserId: this.props.config.welcomeUserId, - andView: false, + // Only view the welcome user if we're NOT looking at a room + andView: !this.state.currentRoomId, }); return; } From 612539567503de27a22f17212c807191b4a8f6f9 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 5 Jun 2017 15:36:10 +0100 Subject: [PATCH 101/127] _t for SetMxIdDialog --- src/components/views/dialogs/SetMxIdDialog.js | 43 +++++++++++++------ 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/src/components/views/dialogs/SetMxIdDialog.js b/src/components/views/dialogs/SetMxIdDialog.js index efca192ec1..78576eb1e4 100644 --- a/src/components/views/dialogs/SetMxIdDialog.js +++ b/src/components/views/dialogs/SetMxIdDialog.js @@ -21,6 +21,7 @@ import sdk from '../../../index'; import MatrixClientPeg from '../../../MatrixClientPeg'; import classnames from 'classnames'; import KeyCode from '../../../KeyCode'; +import { _t, _tJsx } from '../../../languageHandler'; // The amount of time to wait for further changes to the input username before // sending a request to the server @@ -120,10 +121,13 @@ export default React.createClass({ console.error('Error whilst checking username availability: ', err); switch (err.errcode) { case "M_USER_IN_USE": - newState.usernameError = 'Username not available'; + newState.usernameError = _t('Username not available'); break; case "M_INVALID_USERNAME": - newState.usernameError = 'Username invalid: ' + err.message; + newState.usernameError = _t( + 'Username invalid: %(errMessage)', + { errMessage: err.message}, + ); break; case "M_UNRECOGNIZED": // This homeserver doesn't support username checking, assume it's @@ -131,10 +135,13 @@ export default React.createClass({ newState.usernameError = ''; break; case undefined: - newState.usernameError = 'Something went wrong!'; + newState.usernameError = _t('Something went wrong!'); break; default: - newState.usernameError = 'An error occurred: ' + err.message; + newState.usernameError = _t( + 'An error occurred: %(errMessage)', + { errMessage: err.message }, + ); break; } this.setState(newState); @@ -218,7 +225,7 @@ export default React.createClass({ "success": usernameAvailable, }); usernameIndicator =
- { usernameAvailable ? 'Username available' : this.state.usernameError } + { usernameAvailable ? _t('Username available') : this.state.usernameError }
; } @@ -250,15 +257,25 @@ export default React.createClass({
{ usernameIndicator }

- This will be your account name on - the {this.props.homeserverUrl} homeserver, - or you can pick a  - - different server - . + { _tJsx( + 'This will be your account name on the ' + + 'homeserver, or you can pick a different server.', + [ + /<\/span>/, + /(.*?)<\/a>/, + ], + [ + (sub) => {this.props.homeserverUrl}, + (sub) => {sub}, + ], + )}

- If you already have a Matrix account you can log in instead. + { _tJsx( + 'If you already have a Matrix account you can log in instead.', + /(.*?)<\/a>/, + [(sub) => {sub}], + )}

{ auth } { authErrorIndicator } @@ -266,7 +283,7 @@ export default React.createClass({
From 9f6ab4a31bd3d5be1b588b8742976dae421aefa3 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 5 Jun 2017 16:12:00 +0100 Subject: [PATCH 102/127] Add en_EN i18n strings for setMxIdDialog --- src/i18n/strings/en_EN.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 48cb3b8ac9..386414a52e 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -839,5 +839,10 @@ "Disable URL previews for this room (affects only you)": "Disable URL previews for this room (affects only you)", "$senderDisplayName changed the room avatar to ": "$senderDisplayName changed the room avatar to ", "%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s removed the room avatar.", - "%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s changed the avatar for %(roomName)s" + "%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s changed the avatar for %(roomName)s", + "Username available": "Username available", + "Username not available": "Username not available", + "Something went wrong!": "Something went wrong!", + "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.", + "If you already have a Matrix account you can log in instead.": "If you already have a Matrix account you can log in instead." } From ad87e9bd50b46857ce028780d367bfe4258a6608 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 5 Jun 2017 17:28:36 +0100 Subject: [PATCH 103/127] Apply https://github.com/matrix-org/matrix-react-sdk/commit/9cae667c063e67b32e60b89e7256d714a056559b to ilag branch --- src/components/views/elements/CreateRoomButton.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/CreateRoomButton.js b/src/components/views/elements/CreateRoomButton.js index f98974b489..22d222c6f1 100644 --- a/src/components/views/elements/CreateRoomButton.js +++ b/src/components/views/elements/CreateRoomButton.js @@ -22,7 +22,7 @@ import { _t } from '../../../languageHandler'; const CreateRoomButton = function(props) { const ActionButton = sdk.getComponent('elements.ActionButton'); return ( - Date: Mon, 5 Jun 2017 17:40:00 +0100 Subject: [PATCH 104/127] Add missing command --- 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 386414a52e..8c069537cb 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -835,7 +835,7 @@ "Click on the button below to start chatting!": "Click on the button below to start chatting!", "Create a new chat or reuse an existing one": "Create a new chat or reuse an existing one", "You already have existing direct chats with this user:": "You already have existing direct chats with this user:", - "Start new chat": "Start new chat" + "Start new chat": "Start new chat", "Disable URL previews for this room (affects only you)": "Disable URL previews for this room (affects only you)", "$senderDisplayName changed the room avatar to ": "$senderDisplayName changed the room avatar to ", "%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s removed the room avatar.", From 5924654f9d33325f15354faa59ad33227a78a5a9 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 5 Jun 2017 17:45:01 +0100 Subject: [PATCH 105/127] Defer an intention for creating a room --- src/components/structures/MatrixChat.js | 44 +++++++++++++++---------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 3c0552b84b..3fbb18aeda 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -325,7 +325,6 @@ module.exports = React.createClass({ onAction: function(payload) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - const TextInputDialog = sdk.getComponent("dialogs.TextInputDialog"); switch (payload.action) { case 'logout': @@ -457,22 +456,7 @@ module.exports = React.createClass({ this.notifyNewScreen('settings'); break; case 'view_create_room': - if (MatrixClientPeg.get().isGuest()) { - dis.dispatch({action: 'view_set_mxid'}); - break; - } - Modal.createDialog(TextInputDialog, { - title: _t('Create Room'), - description: _t('Room name (optional)'), - button: _t('Create Room'), - onFinished: (should_create, name) => { - if (should_create) { - const createOpts = {}; - if (name) createOpts.name = name; - createRoom({createOpts}).done(); - } - }, - }); + this._createRoom(); break; case 'view_room_directory': this._setPage(PageTypes.RoomDirectory); @@ -727,6 +711,32 @@ module.exports = React.createClass({ }); }, + _createRoom: function() { + if (MatrixClientPeg.get().isGuest()) { + dis.dispatch({ + action: 'do_after_sync_prepared', + deferred_action: { + action: 'view_create_room', + }, + }); + dis.dispatch({action: 'view_set_mxid'}); + return; + } + const TextInputDialog = sdk.getComponent("dialogs.TextInputDialog"); + Modal.createDialog(TextInputDialog, { + title: _t('Create Room'), + description: _t('Room name (optional)'), + button: _t('Create Room'), + onFinished: (should_create, name) => { + if (should_create) { + const createOpts = {}; + if (name) createOpts.name = name; + createRoom({createOpts}).done(); + } + }, + }); + }, + _chatCreateOrReuse: function(userId) { const ChatCreateOrReuseDialog = sdk.getComponent( 'views.dialogs.ChatCreateOrReuseDialog', From f6cfff909855b80ab21764279c729058651b27a7 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 5 Jun 2017 18:37:38 +0100 Subject: [PATCH 106/127] Cancel deferred actions if the set mxid dialog is canceled --- src/components/structures/MatrixChat.js | 3 +++ src/stores/LifecycleStore.js | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 3fbb18aeda..2d917c5241 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -676,6 +676,9 @@ module.exports = React.createClass({ homeserverUrl: MatrixClientPeg.get().getHomeserverUrl(), onFinished: (submitted, credentials) => { if (!submitted) { + dis.dispatch({ + action: 'cancel_after_sync_prepared', + }); return; } this.onRegistered(credentials); diff --git a/src/stores/LifecycleStore.js b/src/stores/LifecycleStore.js index d38138b3ef..d954ef16b6 100644 --- a/src/stores/LifecycleStore.js +++ b/src/stores/LifecycleStore.js @@ -45,6 +45,11 @@ class LifecycleStore extends Store { deferred_action: payload.deferred_action, }); break; + case 'cancel_after_sync_prepared': + this._setState({ + deferred_action: null, + }); + break; case 'sync_state': if (payload.state !== 'PREPARED') { break; From 12d24809160e6bae9ee8173f880ba921991efd03 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 6 Jun 2017 11:36:33 +0100 Subject: [PATCH 107/127] Reset 'first sync' flag / promise on log in Otherwise on any logins after the first,we always think the first sync has completed. --- src/components/structures/MatrixChat.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index a1a6a50fd2..b863bf9cf2 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -927,6 +927,8 @@ module.exports = React.createClass({ dis.dispatch({action: 'view_home_page'}); } else if (this._is_registered) { this._is_registered = false; + this.firstSyncComplete = false; + this.firstSyncPromise = q.defer(); // Set the display name = user ID localpart MatrixClientPeg.get().setDisplayName( From c27f50207df3375556c079312b73076c646f8132 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 6 Jun 2017 11:43:07 +0100 Subject: [PATCH 108/127] comment --- src/components/structures/MatrixChat.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index b863bf9cf2..c31912d798 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -927,6 +927,8 @@ module.exports = React.createClass({ dis.dispatch({action: 'view_home_page'}); } else if (this._is_registered) { this._is_registered = false; + // reset the 'have completed first sync' flag, + // since we've just logged in and will be about to sync this.firstSyncComplete = false; this.firstSyncPromise = q.defer(); From 56df54f9460393b5c8551c1c5432f141199a36d6 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 7 Jun 2017 11:17:57 +0100 Subject: [PATCH 109/127] hardcode logged url to riot.im/app so piwik has a base to work with, it gets confused when a CustomURL isn't actually a URL (like it wasn't after the latest redaction fixes. Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/Analytics.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Analytics.js b/src/Analytics.js index c079011db7..fcedf43ab9 100644 --- a/src/Analytics.js +++ b/src/Analytics.js @@ -20,9 +20,9 @@ import PlatformPeg from './PlatformPeg'; import SdkConfig from './SdkConfig'; function getRedactedUrl() { - const base = window.location.pathname.split('/').slice(-2).join('/'); const redactedHash = window.location.hash.replace(/#\/(room|user)\/(.+)/, "#/$1/"); - return base + redactedHash; + // hardcoded url to make piwik happy + return 'https://riot.im/app/' + redactedHash; } const customVariables = { From bd948ef91534f8e62714fae65a6a967190dcea6d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 7 Jun 2017 11:25:27 +0100 Subject: [PATCH 110/127] for riot.im instances, track location.pathname Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/Analytics.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Analytics.js b/src/Analytics.js index fcedf43ab9..b9f969973e 100644 --- a/src/Analytics.js +++ b/src/Analytics.js @@ -30,6 +30,7 @@ const customVariables = { 'App Version': 2, 'User Type': 3, 'Chosen Language': 4, + 'Instance': 5, }; @@ -86,6 +87,10 @@ class Analytics { this._setVisitVariable('Chosen Language', getCurrentLanguage()); + if (window.location.hostname === 'riot.im') { + this._setVisitVariable('Instance', window.location.pathname); + } + (function() { const g = document.createElement('script'); const s = document.getElementsByTagName('script')[0]; From 0a2a782d63032ff3cdf2916c4aa084b2950e75ee Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 7 Jun 2017 11:26:52 +0100 Subject: [PATCH 111/127] track the action of opt-out Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/Analytics.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Analytics.js b/src/Analytics.js index b9f969973e..92691da1ea 100644 --- a/src/Analytics.js +++ b/src/Analytics.js @@ -56,6 +56,7 @@ class Analytics { * but this is second best, Piwik should not pull anything implicitly. */ disable() { + this.trackEvent('Analytics', 'opt-out'); this.disabled = true; } From 1dbdbc163b93e422a71bd3570c27c958f56895ff Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 7 Jun 2017 11:55:24 +0100 Subject: [PATCH 112/127] Cancel 'join room' action if 'log in' is clicked or 'choose different server' We canceled the deferred action in the MatrixChat SetMxId dialog but not the one in roomview. Fixes https://github.com/vector-im/riot-web/issues/4217 --- src/components/structures/RoomView.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index d3e0f6ee2d..d58b017f4d 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -791,6 +791,9 @@ module.exports = React.createClass({ if (submitted) { this.props.onRegistered(credentials); } else { + dis.dispatch({ + action: 'cancel_after_sync_prepared', + }); dis.dispatch({ action: 'cancel_join', }); From 3b6599dcdc31fdd387ac4479751091d7a7c22ac8 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 7 Jun 2017 15:58:21 +0100 Subject: [PATCH 113/127] Use user_directory endpoint to populate ChatInviteDialog Also attempt to slightly improve the feedback given when searching. There is still room for improvement - it's not totally obvious whether to display a spinner because it's quite jarring were it to replace the dd. This does mean that "No results" will flash awkardly between two queries, neither of which return any results. --- .../views/dialogs/ChatInviteDialog.js | 157 +++++++++--------- src/i18n/strings/en_EN.json | 1 + 2 files changed, 83 insertions(+), 75 deletions(-) diff --git a/src/components/views/dialogs/ChatInviteDialog.js b/src/components/views/dialogs/ChatInviteDialog.js index 3633670f25..867a3b423c 100644 --- a/src/components/views/dialogs/ChatInviteDialog.js +++ b/src/components/views/dialogs/ChatInviteDialog.js @@ -26,6 +26,7 @@ import AccessibleButton from '../elements/AccessibleButton'; import q from 'q'; const TRUNCATE_QUERY_LIST = 40; +const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200; module.exports = React.createClass({ displayName: "ChatInviteDialog", @@ -40,13 +41,13 @@ module.exports = React.createClass({ roomId: React.PropTypes.string, button: React.PropTypes.string, focus: React.PropTypes.bool, - onFinished: React.PropTypes.func.isRequired + onFinished: React.PropTypes.func.isRequired, }, getDefaultProps: function() { return { value: "", - focus: true + focus: true, }; }, @@ -54,12 +55,16 @@ module.exports = React.createClass({ return { error: false, - // List of AddressTile.InviteAddressType objects represeting + // List of AddressTile.InviteAddressType objects representing // the list of addresses we're going to invite inviteList: [], - // List of AddressTile.InviteAddressType objects represeting - // the set of autocompletion results for the current search + // Whether a search is ongoing + busy: false, + // The query being searched for + query: "", + // List of AddressTile.InviteAddressType objects representing + // the set of auto-completion results for the current search // query. queryList: [], }; @@ -70,7 +75,6 @@ module.exports = React.createClass({ // Set the cursor at the end of the text input this.refs.textinput.value = this.props.value; } - this._updateUserList(); }, onButtonClick: function() { @@ -137,15 +141,15 @@ module.exports = React.createClass({ } else if (e.keyCode === 38) { // up arrow e.stopPropagation(); e.preventDefault(); - this.addressSelector.moveSelectionUp(); + if (this.addressSelector) this.addressSelector.moveSelectionUp(); } else if (e.keyCode === 40) { // down arrow e.stopPropagation(); e.preventDefault(); - this.addressSelector.moveSelectionDown(); + if (this.addressSelector) this.addressSelector.moveSelectionDown(); } else if (this.state.queryList.length > 0 && (e.keyCode === 188 || e.keyCode === 13 || e.keyCode === 9)) { // comma or enter or tab e.stopPropagation(); e.preventDefault(); - this.addressSelector.chooseSelection(); + if (this.addressSelector) this.addressSelector.chooseSelection(); } else if (this.refs.textinput.value.length === 0 && this.state.inviteList.length && e.keyCode === 8) { // backspace e.stopPropagation(); e.preventDefault(); @@ -168,74 +172,79 @@ module.exports = React.createClass({ onQueryChanged: function(ev) { const query = ev.target.value.toLowerCase(); - let queryList = []; - - if (query.length < 2) { - return; - } - if (this.queryChangedDebouncer) { clearTimeout(this.queryChangedDebouncer); } - this.queryChangedDebouncer = setTimeout(() => { - // Only do search if there is something to search - if (query.length > 0 && query != '@') { - this._userList.forEach((user) => { - if (user.userId.toLowerCase().indexOf(query) === -1 && - user.displayName.toLowerCase().indexOf(query) === -1 - ) { - return; - } + // Only do search if there is something to search + if (query.length > 0 && query != '@' && query.length >= 2) { + this.queryChangedDebouncer = setTimeout(() => { + this.setState({ + busy: true, + query, + }); + MatrixClientPeg.get().searchUserDirectory({ + term: query, + }).then((resp) => { + const queryList = []; + resp.results.forEach((user) => { + if (user.user_id === MatrixClientPeg.get().credentials.userId) { + return; + } + // Return objects, structure of which is defined + // by InviteAddressType + queryList.push({ + addressType: 'mx', + address: user.user_id, + displayName: user.display_name, + avatarMxc: user.avatar_url, + isKnown: true, + }); + }); - // Return objects, structure of which is defined - // by InviteAddressType - queryList.push({ - addressType: 'mx', - address: user.userId, - displayName: user.displayName, - avatarMxc: user.avatarUrl, - isKnown: true, - order: user.getLastActiveTs(), + // If the query is a valid address, add an entry for that + // This is important, otherwise there's no way to invite + // a perfectly valid address if there are close matches. + const addrType = getAddressType(query); + if (addrType !== null) { + queryList.unshift({ + addressType: addrType, + address: query, + isKnown: false, + }); + if (this._cancelThreepidLookup) this._cancelThreepidLookup(); + if (addrType == 'email') { + this._lookupThreepid(addrType, query).done(); + } + } + this.setState({ + queryList, + error: false, + }, () => { + this.addressSelector.moveSelectionTop(); + }); + }).finally(() => { + this.setState({ + busy: false, }); }); - - queryList = queryList.sort((a,b) => { - return a.order < b.order; - }); - - // If the query is a valid address, add an entry for that - // This is important, otherwise there's no way to invite - // a perfectly valid address if there are close matches. - const addrType = getAddressType(query); - if (addrType !== null) { - queryList.unshift({ - addressType: addrType, - address: query, - isKnown: false, - }); - if (this._cancelThreepidLookup) this._cancelThreepidLookup(); - if (addrType == 'email') { - this._lookupThreepid(addrType, query).done(); - } - } - } + }, QUERY_USER_DIRECTORY_DEBOUNCE_MS); + } else { this.setState({ - queryList: queryList, - error: false, - }, () => { - this.addressSelector.moveSelectionTop(); + queryList: [], + query: "", }); - }, 200); + } }, onDismissed: function(index) { var self = this; - return function() { + return () => { var inviteList = self.state.inviteList.slice(); inviteList.splice(index, 1); self.setState({ inviteList: inviteList, queryList: [], + query: "", }); if (this._cancelThreepidLookup) this._cancelThreepidLookup(); }; @@ -254,6 +263,7 @@ module.exports = React.createClass({ this.setState({ inviteList: inviteList, queryList: [], + query: "", }); if (this._cancelThreepidLookup) this._cancelThreepidLookup(); }, @@ -342,16 +352,6 @@ module.exports = React.createClass({ this.props.onFinished(true, addrTexts); }, - _updateUserList: function() { - // Get all the users - this._userList = MatrixClientPeg.get().getUsers(); - // Remove current user - const meIx = this._userList.findIndex((u) => { - return u.userId === MatrixClientPeg.get().credentials.userId; - }); - this._userList.splice(meIx, 1); - }, - _isOnInviteList: function(uid) { for (let i = 0; i < this.state.inviteList.length; i++) { if ( @@ -419,6 +419,7 @@ module.exports = React.createClass({ this.setState({ inviteList: inviteList, queryList: [], + query: "", }); if (this._cancelThreepidLookup) this._cancelThreepidLookup(); return inviteList; @@ -454,7 +455,7 @@ module.exports = React.createClass({ displayName: res.displayname, avatarMxc: res.avatar_url, isKnown: true, - }] + }], }); }); }, @@ -486,16 +487,22 @@ module.exports = React.createClass({ placeholder={this.props.placeholder} defaultValue={this.props.value} autoFocus={this.props.focus}> - + , ); - var error; - var addressSelector; + let error; + let addressSelector; if (this.state.error) { error =
{_t("You have entered an invalid contact. Try using their Matrix ID or email address.")}
; + } else if ( + this.state.query.length > 0 && + this.state.queryList.length === 0 && + !this.state.busy + ) { + error =
{_t("No results")}
; } else { const addressSelectorHeader =
- Searching known users + { _t("Searching known users") }
; addressSelector = ( {this.addressSelector = ref;}} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index c87ffb8cdf..eb331c24a9 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -599,6 +599,7 @@ "Who can read history?": "Who can read history?", "Who would you like to add to this room?": "Who would you like to add to this room?", "Who would you like to communicate with?": "Who would you like to communicate with?", + "Searching known users": "Searching known users", "%(senderName)s withdrew %(targetName)s's invitation.": "%(senderName)s withdrew %(targetName)s's invitation.", "Would you like to": "Would you like to", "You are already in a call.": "You are already in a call.", From 49b22fb6c6f42fd9366cb19c261b1b03359a6aed Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 7 Jun 2017 16:13:40 +0100 Subject: [PATCH 114/127] Use an arrow function to allow `this` This was causing `TypeError: Cannot read property '_getEmptyContent' of undefined` for those with custom tags. --- src/components/views/rooms/RoomList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index bf270b1148..f90d89c8c1 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -578,7 +578,7 @@ module.exports = React.createClass({ onHeaderClick={ self.onSubListHeaderClick } onShowMoreRooms={ self.onShowMoreRooms } /> - { Object.keys(self.state.lists).map(function(tagName) { + { Object.keys(self.state.lists).map((tagName) => { if (!tagName.match(/^(m\.(favourite|lowpriority)|im\.vector\.fake\.(invite|recent|direct|archived))$/)) { return Date: Wed, 7 Jun 2017 17:33:15 +0100 Subject: [PATCH 115/127] Use then, catch, done and display any errors that occur during search --- .../views/dialogs/ChatInviteDialog.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/components/views/dialogs/ChatInviteDialog.js b/src/components/views/dialogs/ChatInviteDialog.js index 867a3b423c..0c65f02525 100644 --- a/src/components/views/dialogs/ChatInviteDialog.js +++ b/src/components/views/dialogs/ChatInviteDialog.js @@ -61,6 +61,8 @@ module.exports = React.createClass({ // Whether a search is ongoing busy: false, + // An error message generated during the user directory search + searchError: null, // The query being searched for query: "", // List of AddressTile.InviteAddressType objects representing @@ -181,6 +183,7 @@ module.exports = React.createClass({ this.setState({ busy: true, query, + searchError: null, }); MatrixClientPeg.get().searchUserDirectory({ term: query, @@ -222,7 +225,12 @@ module.exports = React.createClass({ }, () => { this.addressSelector.moveSelectionTop(); }); - }).finally(() => { + }).catch((err) => { + console.error('Error whilst searching user directory: ', err); + this.setState({ + searchError: err.errcode ? err.message : _t('Something went wrong!'), + }); + }).done(() => { this.setState({ busy: false, }); @@ -232,6 +240,7 @@ module.exports = React.createClass({ this.setState({ queryList: [], query: "", + searchError: null, }); } }, @@ -494,6 +503,8 @@ module.exports = React.createClass({ let addressSelector; if (this.state.error) { error =
{_t("You have entered an invalid contact. Try using their Matrix ID or email address.")}
; + } else if (this.state.searchError) { + error =
{this.state.searchError}
; } else if ( this.state.query.length > 0 && this.state.queryList.length === 0 && @@ -501,15 +512,11 @@ module.exports = React.createClass({ ) { error =
{_t("No results")}
; } else { - const addressSelectorHeader =
- { _t("Searching known users") } -
; addressSelector = ( {this.addressSelector = ref;}} addressList={ this.state.queryList } onSelected={ this.onSelected } truncateAt={ TRUNCATE_QUERY_LIST } - header={ addressSelectorHeader } /> ); } From 29da25529c903a5d621032271cf17979c36a27cc Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 7 Jun 2017 17:40:09 +0100 Subject: [PATCH 116/127] Make it backwards compatible Hit the user_directory API and if M_UNRECOGNIZED, do a local search and continue to search locally therein. --- .../views/dialogs/ChatInviteDialog.js | 154 +++++++++++------- 1 file changed, 99 insertions(+), 55 deletions(-) diff --git a/src/components/views/dialogs/ChatInviteDialog.js b/src/components/views/dialogs/ChatInviteDialog.js index 0c65f02525..2f635fd670 100644 --- a/src/components/views/dialogs/ChatInviteDialog.js +++ b/src/components/views/dialogs/ChatInviteDialog.js @@ -63,6 +63,8 @@ module.exports = React.createClass({ busy: false, // An error message generated during the user directory search searchError: null, + // Whether the server supports the user_directory API + serverSupportsUserDirectory: true, // The query being searched for query: "", // List of AddressTile.InviteAddressType objects representing @@ -180,61 +182,11 @@ module.exports = React.createClass({ // Only do search if there is something to search if (query.length > 0 && query != '@' && query.length >= 2) { this.queryChangedDebouncer = setTimeout(() => { - this.setState({ - busy: true, - query, - searchError: null, - }); - MatrixClientPeg.get().searchUserDirectory({ - term: query, - }).then((resp) => { - const queryList = []; - resp.results.forEach((user) => { - if (user.user_id === MatrixClientPeg.get().credentials.userId) { - return; - } - // Return objects, structure of which is defined - // by InviteAddressType - queryList.push({ - addressType: 'mx', - address: user.user_id, - displayName: user.display_name, - avatarMxc: user.avatar_url, - isKnown: true, - }); - }); - - // If the query is a valid address, add an entry for that - // This is important, otherwise there's no way to invite - // a perfectly valid address if there are close matches. - const addrType = getAddressType(query); - if (addrType !== null) { - queryList.unshift({ - addressType: addrType, - address: query, - isKnown: false, - }); - if (this._cancelThreepidLookup) this._cancelThreepidLookup(); - if (addrType == 'email') { - this._lookupThreepid(addrType, query).done(); - } - } - this.setState({ - queryList, - error: false, - }, () => { - this.addressSelector.moveSelectionTop(); - }); - }).catch((err) => { - console.error('Error whilst searching user directory: ', err); - this.setState({ - searchError: err.errcode ? err.message : _t('Something went wrong!'), - }); - }).done(() => { - this.setState({ - busy: false, - }); - }); + if (this.state.serverSupportsUserDirectory) { + this._doUserDirectorySearch(query); + } else { + this._doLocalSearch(query); + } }, QUERY_USER_DIRECTORY_DEBOUNCE_MS); } else { this.setState({ @@ -277,6 +229,98 @@ module.exports = React.createClass({ if (this._cancelThreepidLookup) this._cancelThreepidLookup(); }, + _doUserDirectorySearch: function(query) { + this.setState({ + busy: true, + query, + searchError: null, + }); + MatrixClientPeg.get().searchUserDirectory({ + term: query, + }).then((resp) => { + this._processResults(resp.results, query); + }).catch((err) => { + console.error('Error whilst searching user directory: ', err); + this.setState({ + searchError: err.errcode ? err.message : _t('Something went wrong!'), + }); + if (err.errcode === 'M_UNRECOGNIZED') { + this.setState({ + serverSupportsUserDirectory: false, + }); + // Do a local search immediately + this._doLocalSearch(query); + } + }).done(() => { + this.setState({ + busy: false, + }); + }); + }, + + _doLocalSearch: function(query) { + this.setState({ + query, + searchError: null, + }); + const results = []; + MatrixClientPeg.get().getUsers().forEach((user) => { + if (user.userId.toLowerCase().indexOf(query) === -1 && + user.displayName.toLowerCase().indexOf(query) === -1 + ) { + return; + } + + // Put results in the format of the new API + results.push({ + user_id: user.userId, + display_name: user.displayName, + avatar_url: user.avatarUrl, + }); + }); + this._processResults(results, query); + }, + + _processResults: function(results, query) { + const queryList = []; + results.forEach((user) => { + if (user.user_id === MatrixClientPeg.get().credentials.userId) { + return; + } + // Return objects, structure of which is defined + // by InviteAddressType + queryList.push({ + addressType: 'mx', + address: user.user_id, + displayName: user.display_name, + avatarMxc: user.avatar_url, + isKnown: true, + }); + }); + + // If the query is a valid address, add an entry for that + // This is important, otherwise there's no way to invite + // a perfectly valid address if there are close matches. + const addrType = getAddressType(query); + if (addrType !== null) { + queryList.unshift({ + addressType: addrType, + address: query, + isKnown: false, + }); + if (this._cancelThreepidLookup) this._cancelThreepidLookup(); + if (addrType == 'email') { + this._lookupThreepid(addrType, query).done(); + } + } + this.setState({ + queryList, + error: false, + }, () => { + if (this.addressSelector) this.addressSelector.moveSelectionTop(); + }); + }, + _getDirectMessageRooms: function(addr) { const dmRoomMap = new DMRoomMap(MatrixClientPeg.get()); const dmRooms = dmRoomMap.getDMRoomsForUserId(addr); From 74d680a9524aa1eb73d0b4f276c78ce80b5985f9 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 7 Jun 2017 18:15:13 +0100 Subject: [PATCH 117/127] sync fullstops everywhere --- src/i18n/strings/de_DE.json | 28 ++++++++++++++-------------- src/i18n/strings/el.json | 4 ++-- src/i18n/strings/en_US.json | 10 +++++----- src/i18n/strings/es.json | 2 +- src/i18n/strings/fr.json | 10 +++++----- src/i18n/strings/pt.json | 10 +++++----- src/i18n/strings/pt_BR.json | 10 +++++----- src/i18n/strings/ru.json | 6 +++--- 8 files changed, 40 insertions(+), 40 deletions(-) diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index 15375b8f90..513faea017 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -227,7 +227,7 @@ "to join the discussion": "um an der Diskussion teilzunehmen", "To kick users": "Um Nutzer zu entfernen", "Admin": "Administrator", - "Server may be unavailable, overloaded, or you hit a bug": "Server könnte nicht verfügbar oder überlastet sein oder du bist auf einen Fehler gestoßen", + "Server may be unavailable, overloaded, or you hit a bug.": "Server könnte nicht verfügbar oder überlastet sein oder du bist auf einen Fehler gestoßen.", "Could not connect to the integration server": "Konnte keine Verbindung zum Integrations-Server herstellen", "Disable inline URL previews by default": "URL-Vorschau im Chat standardmäßig deaktivieren", "Guests can't use labs features. Please register.": "Gäste können keine Labor-Funktionen nutzen. Bitte registrieren.", @@ -280,15 +280,15 @@ "times": "mal", "Bulk Options": "Bulk-Optionen", "Call Timeout": "Anruf-Timeout", - "Conference call failed": "Konferenzgespräch fehlgeschlagen", - "Conference calling is in development and may not be reliable": "Konferenzgespräche sind in Entwicklung und evtl. nicht zuverlässig", + "Conference call failed.": "Konferenzgespräch fehlgeschlagen.", + "Conference calling is in development and may not be reliable.": "Konferenzgespräche sind in Entwicklung und evtl. nicht zuverlässig.", "Conference calls are not supported in encrypted rooms": "Konferenzgespräche werden in verschlüsselten Räumen nicht unterstützt", "Conference calls are not supported in this client": "Konferenzgespräche werden von diesem Client nicht unterstützt", "Existing Call": "Bereits bestehender Anruf", "Failed to set up conference call": "Konferenzgespräch konnte nicht gestartet werden", "Failed to verify email address: make sure you clicked the link in the email": "Verifizierung der E-Mail-Adresse fehlgeschlagen: Bitte stelle sicher, dass du den Link in der E-Mail angeklickt hast", "Failure to create room": "Raumerstellung fehlgeschlagen", - "Guest users can't create new rooms. Please register to create room and start a chat": "Gäste können keine neuen Räume erstellen. Bitte registrieren um einen Raum zu erstellen und einen Chat zu starten", + "Guest users can't create new rooms. Please register to create room and start a chat.": "Gäste können keine neuen Räume erstellen. Bitte registrieren um einen Raum zu erstellen und einen Chat zu starten.", "Riot does not have permission to send you notifications - please check your browser settings": "Riot hat keine Berechtigung Benachrichtigungen zu senden - bitte prüfe deine Browser-Einstellungen", "Riot was not given permission to send notifications - please try again": "Riot hat das Recht nicht bekommen Benachrichtigungen zu senden. Bitte erneut probieren", "This email address is already in use": "Diese E-Mail-Adresse wird bereits verwendet", @@ -302,11 +302,11 @@ "Unable to enable Notifications": "Benachrichtigungen konnten nicht aktiviert werden", "Upload Failed": "Upload fehlgeschlagen", "VoIP is unsupported": "VoIP wird nicht unterstützt", - "You are already in a call": "Du bist bereits bei einem Anruf", - "You cannot place a call with yourself": "Du kannst keinen Anruf mit dir selbst starten", - "You cannot place VoIP calls in this browser": "Du kannst kein VoIP-Gespräch in diesem Browser starten", + "You are already in a call.": "Du bist bereits bei einem Anruf.", + "You cannot place a call with yourself.": "Du kannst keinen Anruf mit dir selbst starten.", + "You cannot place VoIP calls in this browser.": "Du kannst kein VoIP-Gespräch in diesem Browser starten.", "You need to log back in to generate end-to-end encryption keys for this device and submit the public key to your homeserver. This is a once off; sorry for the inconvenience.": "Du musst dich erneut anmelden, um Ende-zu-Ende-Verschlüsselungs-Schlüssel für dieses Gerät zu generieren und um den öffentlichen Schlüssel auf deinem Homeserver zu hinterlegen. Dies muss nur einmal durchgeführt werden, bitte entschuldige die Unannehmlichkeiten.", - "Your email address does not appear to be associated with a Matrix ID on this Homeserver": "Deine E-Mail-Adresse scheint nicht mit einer Matrix-ID auf diesem Homeserver verknüpft zu sein", + "Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "Deine E-Mail-Adresse scheint nicht mit einer Matrix-ID auf diesem Homeserver verknüpft zu sein.", "Sun": "So", "Mon": "Mo", "Tue": "Di", @@ -581,7 +581,7 @@ "Failed to save settings": "Einstellungen konnten nicht gespeichert werden", "Failed to set display name": "Anzeigename konnte nicht gesetzt werden", "Fill screen": "Fülle Bildschirm", - "Guest users can't upload files. Please register to upload": "Gäste können keine Dateien hochladen. Bitte zunächst registrieren", + "Guest users can't upload files. Please register to upload.": "Gäste können keine Dateien hochladen. Bitte zunächst registrieren.", "Hide Text Formatting Toolbar": "Verberge Text-Formatierungs-Toolbar", "Incorrect verification code": "Falscher Verifizierungscode", "Invalid alias format": "Ungültiges Alias-Format", @@ -608,16 +608,16 @@ "Server error": "Server-Fehler", "Server may be unavailable, overloaded, or search timed out :(": "Der Server ist entweder nicht verfügbar, überlastet oder die Suche wurde wegen Zeitüberschreitung abgebrochen :(", "Server may be unavailable, overloaded, or the file too big": "Server ist entweder nicht verfügbar, überlastet oder die Datei ist zu groß", - "Server unavailable, overloaded, or something else went wrong": "Der Server ist entweder nicht verfügbar, überlastet oder es liegt ein anderweitiger Fehler vor", - "Some of your messages have not been sent": "Einige deiner Nachrichten wurden noch nicht gesendet", + "Server unavailable, overloaded, or something else went wrong.": "Der Server ist entweder nicht verfügbar, überlastet oder es liegt ein anderweitiger Fehler vor.", + "Some of your messages have not been sent.": "Einige deiner Nachrichten wurden noch nicht gesendet.", "Submit": "Absenden", "The main address for this room is: %(canonical_alias_section)s": "Die Hauptadresse für diesen Raum ist: %(canonical_alias_section)s", "This action cannot be performed by a guest user. Please register to be able to do this": "Diese Aktion kann nicht von einem Gast ausgeführt werden. Bitte registriere dich um dies zu tun", "%(actionVerb)s this person?": "Diese Person %(actionVerb)s?", "This room has no local addresses": "Dieser Raum hat keine lokale Adresse", - "This room is private or inaccessible to guests. You may be able to join if you register": "Dieser Raum ist privat oder für Gäste nicht zugänglich. Du kannst jedoch eventuell beitreten, wenn du dich registrierst", - "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question": "Versuchte einen spezifischen Punkt in der Raum-Chronik zu laden, aber du hast keine Berechtigung die angeforderte Nachricht anzuzeigen", - "Tried to load a specific point in this room's timeline, but was unable to find it": "Der Versuch, einen spezifischen Punkt im Chatverlauf zu laden, ist fehlgeschlagen. Der Punkt konnte nicht gefunden werden", + "This room is private or inaccessible to guests. You may be able to join if you register.": "Dieser Raum ist privat oder für Gäste nicht zugänglich. Du kannst jedoch eventuell beitreten, wenn du dich registrierst.", + "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Versuchte einen spezifischen Punkt in der Raum-Chronik zu laden, aber du hast keine Berechtigung die angeforderte Nachricht anzuzeigen.", + "Tried to load a specific point in this room's timeline, but was unable to find it.": "Der Versuch, einen spezifischen Punkt im Chatverlauf zu laden, ist fehlgeschlagen. Der Punkt konnte nicht gefunden werden.", "Turn Markdown off": "Markdown abschalten", "Turn Markdown on": "Markdown einschalten", "Unable to load device list": "Geräteliste konnte nicht geladen werden", diff --git a/src/i18n/strings/el.json b/src/i18n/strings/el.json index ab2129dc81..d8729202d5 100644 --- a/src/i18n/strings/el.json +++ b/src/i18n/strings/el.json @@ -48,7 +48,7 @@ "Authentication": "Πιστοποίηση", "and": "και", "An email has been sent to": "Ένα email στάλθηκε σε", - "A new password must be entered.": "Ο νέος κωδικός πρέπει να εισαχθεί", + "A new password must be entered.": "Ο νέος κωδικός πρέπει να εισαχθεί.", "%(senderName)s answered the call.": "Ο χρήστης %(senderName)s απάντησε.", "An error has occurred.": "Ένα σφάλμα προέκυψε", "Anyone": "Oποιοσδήποτε", @@ -265,7 +265,7 @@ "For security, this session has been signed out. Please sign in again.": "Για λόγους ασφαλείας, αυτή η συνεδρία έχει τερματιστεί. Παρακαλώ συνδεθείτε ξανά.", "For security, logging out will delete any end-to-end encryption keys from this browser. If you want to be able to decrypt your conversation history from future Riot sessions, please export your room keys for safe-keeping.": "Για λόγους ασφαλείας, τα κλειδιά κρυπτογράφησης θα διαγράφονται από τον φυλλομετρητή κατά την αποσύνδεση σας. Εάν επιθυμείτε να αποκρυπτογραφήσετε τις συνομιλίες σας στο μέλλον, εξάγετε τα κλειδιά σας και κρατήστε τα ασφαλή.", "Found a bug?": "Βρήκατε κάποιο πρόβλημα;", - "Guest users can't upload files. Please register to upload": "Οι επισκέπτες δεν μπορούν να ανεβάσουν αρχεία. Παρακαλώ εγγραφείτε πρώτα", + "Guest users can't upload files. Please register to upload.": "Οι επισκέπτες δεν μπορούν να ανεβάσουν αρχεία. Παρακαλώ εγγραφείτε πρώτα.", "had": "είχε", "Hangup": "Κλείσε", "Historical": "Ιστορικό", diff --git a/src/i18n/strings/en_US.json b/src/i18n/strings/en_US.json index 2ad228254b..19b854e936 100644 --- a/src/i18n/strings/en_US.json +++ b/src/i18n/strings/en_US.json @@ -308,7 +308,7 @@ "Guest access is disabled on this Home Server.": "Guest access is disabled on this Home Server.", "Guests can't set avatars. Please register.": "Guests can't set avatars. Please register.", "Guest users can't create new rooms. Please register to create room and start a chat.": "Guest users can't create new rooms. Please register to create room and start a chat.", - "Guest users can't upload files. Please register to upload": "Guest users can't upload files. Please register to upload", + "Guest users can't upload files. Please register to upload.": "Guest users can't upload files. Please register to upload.", "Guests can't use labs features. Please register.": "Guests can't use labs features. Please register.", "Guests cannot join this room even if explicitly invited.": "Guests cannot join this room even if explicitly invited.", "had": "had", @@ -476,7 +476,7 @@ "since the point in time of selecting this option": "since the point in time of selecting this option", "since they joined": "since they joined", "since they were invited": "since they were invited", - "Some of your messages have not been sent": "Some of your messages have not been sent", + "Some of your messages have not been sent.": "Some of your messages have not been sent.", "Someone": "Someone", "Sorry, this homeserver is using a login which is not recognised ": "Sorry, this homeserver is using a login which is not recognized ", "Start a chat": "Start a chat", @@ -501,7 +501,7 @@ "There was a problem logging in.": "There was a problem logging in.", "This room has no local addresses": "This room has no local addresses", "This room is not recognised.": "This room is not recognized.", - "This room is private or inaccessible to guests. You may be able to join if you register": "This room is private or inaccessible to guests. You may be able to join if you register", + "This room is private or inaccessible to guests. You may be able to join if you register.": "This room is private or inaccessible to guests. You may be able to join if you register.", "These are experimental features that may break in unexpected ways": "These are experimental features that may break in unexpected ways", "The visibility of existing history will be unchanged": "The visibility of existing history will be unchanged", "This doesn't appear to be a valid email address": "This doesn't appear to be a valid email address", @@ -530,8 +530,8 @@ "to tag as %(tagName)s": "to tag as %(tagName)s", "to tag direct chat": "to tag direct chat", "To use it, just wait for autocomplete results to load and tab through them.": "To use it, just wait for autocomplete results to load and tab through them.", - "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", - "Tried to load a specific point in this room's timeline, but was unable to find it": "Tried to load a specific point in this room's timeline, but was unable to find it", + "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.", + "Tried to load a specific point in this room's timeline, but was unable to find it.": "Tried to load a specific point in this room's timeline, but was unable to find it.", "Turn Markdown off": "Turn Markdown off", "Turn Markdown on": "Turn Markdown on", "%(senderName)s turned on end-to-end encryption (algorithm %(algorithm)s).": "%(senderName)s turned on end-to-end encryption (algorithm %(algorithm)s).", diff --git a/src/i18n/strings/es.json b/src/i18n/strings/es.json index 9a07710570..125a929b9f 100644 --- a/src/i18n/strings/es.json +++ b/src/i18n/strings/es.json @@ -282,7 +282,7 @@ "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s de %(fromPowerLevel)s a %(toPowerLevel)s", "Guests can't set avatars. Please register.": "Invitados no puedes establecer avatares. Por favor regístrate.", "Guest users can't create new rooms. Please register to create room and start a chat.": "Usuarios invitados no pueden crear nuevas salas. Por favor regístrate para crear la sala y iniciar la conversación.", - "Guest users can't upload files. Please register to upload": "Usuarios invitados no puedes subir archivos. Por favor regístrate para subir tus archivos", + "Guest users can't upload files. Please register to upload.": "Usuarios invitados no puedes subir archivos. Por favor regístrate para subir tus archivos.", "Guests can't use labs features. Please register.": "Invitados no puedes usar las características en desarrollo. Por favor regístrate.", "Guests cannot join this room even if explicitly invited.": "Invitados no pueden unirse a esta sala aun cuando han sido invitados explícitamente.", "had": "tuvo", diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index 0ae02be82f..80d2479c48 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -294,7 +294,7 @@ "Found a bug?": "Trouvé un problème ?", "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s de %(fromPowerLevel)s à %(toPowerLevel)s", "Guest users can't create new rooms. Please register to create room and start a chat.": "Les visiteurs ne peuvent créer de nouveaux salons. Merci de vous enregistrer pour commencer une discussion.", - "Guest users can't upload files. Please register to upload": "Les visiteurs ne peuvent telécharger de fichiers. Merci de vous enregistrer pour télécharger", + "Guest users can't upload files. Please register to upload.": "Les visiteurs ne peuvent telécharger de fichiers. Merci de vous enregistrer pour télécharger.", "had": "avait", "Hangup": "Raccrocher", "Hide read receipts": "Cacher les accusés de réception", @@ -457,7 +457,7 @@ "since the point in time of selecting this option": "depuis le moment où cette option a été sélectionnée", "since they joined": "depuis qu’ils ont rejoint le salon", "since they were invited": "depuis qu’ils ont été invités", - "Some of your messages have not been sent": "Certains de vos messages n’ont pas été envoyés", + "Some of your messages have not been sent.": "Certains de vos messages n’ont pas été envoyés.", "Someone": "Quelqu'un", "Sorry, this homeserver is using a login which is not recognised ": "Désolé, ce homeserver utilise un identifiant qui n’est pas reconnu ", "Start a chat": "Démarrer une conversation", @@ -478,7 +478,7 @@ "The remote side failed to pick up": "Le correspondant n’a pas décroché", "This room has no local addresses": "Ce salon n'a pas d'adresse locale", "This room is not recognised.": "Ce salon n'a pas été reconnu.", - "This room is private or inaccessible to guests. You may be able to join if you register": "Ce salon est privé ou non autorisé aux visiteurs. Vous devriez pouvoir le rejoindre si vous vous enregistrez", + "This room is private or inaccessible to guests. You may be able to join if you register.": "Ce salon est privé ou non autorisé aux visiteurs. Vous devriez pouvoir le rejoindre si vous vous enregistrez.", "These are experimental features that may break in unexpected ways": "Ces fonctionnalités sont expérimentales et risquent de mal fonctionner", "The visibility of existing history will be unchanged": "La visibilité de l’historique existant sera inchangée", "This doesn't appear to be a valid email address": "Cette adresse n’a pas l’air d’être valide", @@ -507,8 +507,8 @@ "to tag as %(tagName)s": "pour marquer comme %(tagName)s", "to tag direct chat": "pour marquer comme conversation directe", "To use it, just wait for autocomplete results to load and tab through them.": "Pour l’utiliser, attendez simplement que les résultats de l’auto-complétion s’affichent et défilez avec la touche Tab.", - "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question": "Une tentative de chargement d’un point donné dans la chronologie de ce salon a été effectuée, mais vous n’avez pas la permission de voir le message en question", - "Tried to load a specific point in this room's timeline, but was unable to find it": "Une tentative de chargement d’un point donné dans la chronologie de ce salon a été effectuée, mais il n’a pas été trouvé", + "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Une tentative de chargement d’un point donné dans la chronologie de ce salon a été effectuée, mais vous n’avez pas la permission de voir le message en question.", + "Tried to load a specific point in this room's timeline, but was unable to find it.": "Une tentative de chargement d’un point donné dans la chronologie de ce salon a été effectuée, mais il n’a pas été trouvé.", "Turn Markdown off": "Désactiver le formatage ’Markdown’", "Turn Markdown on": "Activer le formatage ’Markdown’", "%(senderName)s turned on end-to-end encryption (algorithm %(algorithm)s).": "%(senderName)s a activé l’encryption bout-en-bout (algorithme %(algorithm)s).", diff --git a/src/i18n/strings/pt.json b/src/i18n/strings/pt.json index fb90423c3a..47260b4909 100644 --- a/src/i18n/strings/pt.json +++ b/src/i18n/strings/pt.json @@ -101,7 +101,7 @@ "Guests cannot join this room even if explicitly invited.": "Visitantes não podem entrar nesta sala, mesmo se forem explicitamente convidadas/os.", "Guests can't set avatars. Please register.": "Convidados não podem definir uma foto do perfil. Por favor, registre-se.", "Guests can't use labs features. Please register.": "Convidados não podem usar as funcionalidades de laboratório (lab), por gentileza se registre.", - "Guest users can't upload files. Please register to upload": "Usuários não podem fazer envio de arquivos. Por favor se cadastre para enviar arquivos", + "Guest users can't upload files. Please register to upload.": "Usuários não podem fazer envio de arquivos. Por favor se cadastre para enviar arquivos.", "had": "teve", "Hangup": "Desligar", "Historical": "Histórico", @@ -627,15 +627,15 @@ "Server may be unavailable, overloaded, or search timed out :(": "O servidor pode estar indisponível, sobrecarregado, ou a busca ultrapassou o tempo limite :(", "Server may be unavailable, overloaded, or the file too big": "O servidor pode estar indisponível, sobrecarregado, ou o arquivo é muito grande", "Server unavailable, overloaded, or something else went wrong.": "O servidor pode estar indisponível, sobrecarregado, ou alguma outra coisa não funcionou.", - "Some of your messages have not been sent": "Algumas das suas mensagens não foram enviadas", + "Some of your messages have not been sent.": "Algumas das suas mensagens não foram enviadas.", "Submit": "Enviar", "The main address for this room is": "O endereço principal desta sala é", "This action cannot be performed by a guest user. Please register to be able to do this": "Esta ação não pode ser realizada por um/a usuário/a visitante. Por favor, registre-se para poder fazer isso", "%(actionVerb)s this person?": "%(actionVerb)s esta pessoa?", "This room has no local addresses": "Esta sala não tem endereços locais", - "This room is private or inaccessible to guests. You may be able to join if you register": "Esta sala é privada ou inacessível para visitantes. Você poderá ingressar nela se registrar-se", - "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question": "Tentei carregar um ponto específico na linha do tempo desta sala, mas parece que você não tem permissões para ver a mensagem em questão", - "Tried to load a specific point in this room's timeline, but was unable to find it": "Tentei carregar um ponto específico na linha do tempo desta sala, mas não o encontrei", + "This room is private or inaccessible to guests. You may be able to join if you register.": "Esta sala é privada ou inacessível para visitantes. Você poderá ingressar nela se registrar-se.", + "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Tentei carregar um ponto específico na linha do tempo desta sala, mas parece que você não tem permissões para ver a mensagem em questão.", + "Tried to load a specific point in this room's timeline, but was unable to find it.": "Tentei carregar um ponto específico na linha do tempo desta sala, mas não o encontrei.", "Turn Markdown off": "Desabilitar a formatação 'Markdown'", "Turn Markdown on": "Habilitar a marcação 'Markdown'", "Unable to load device list": "Não foi possível carregar a lista de dispositivos", diff --git a/src/i18n/strings/pt_BR.json b/src/i18n/strings/pt_BR.json index fc942c2ed8..13c9ec0737 100644 --- a/src/i18n/strings/pt_BR.json +++ b/src/i18n/strings/pt_BR.json @@ -101,7 +101,7 @@ "Guests cannot join this room even if explicitly invited.": "Visitantes não podem entrar nesta sala, mesmo se forem explicitamente convidadas/os.", "Guests can't set avatars. Please register.": "Convidados não podem definir uma foto do perfil. Por favor, registre-se.", "Guests can't use labs features. Please register.": "Convidados não podem usar as funcionalidades de laboratório (lab), por gentileza se registre.", - "Guest users can't upload files. Please register to upload": "Usuários não podem fazer envio de arquivos. Por favor se cadastre para enviar arquivos", + "Guest users can't upload files. Please register to upload.": "Usuários não podem fazer envio de arquivos. Por favor se cadastre para enviar arquivos.", "had": "teve", "Hangup": "Desligar", "Historical": "Histórico", @@ -627,15 +627,15 @@ "Server may be unavailable, overloaded, or search timed out :(": "O servidor pode estar indisponível, sobrecarregado, ou a busca ultrapassou o tempo limite :(", "Server may be unavailable, overloaded, or the file too big": "O servidor pode estar indisponível, sobrecarregado, ou o arquivo é muito grande", "Server unavailable, overloaded, or something else went wrong.": "O servidor pode estar indisponível, sobrecarregado, ou alguma outra coisa não funcionou.", - "Some of your messages have not been sent": "Algumas das suas mensagens não foram enviadas", + "Some of your messages have not been sent.": "Algumas das suas mensagens não foram enviadas.", "Submit": "Enviar", "The main address for this room is": "O endereço principal desta sala é", "This action cannot be performed by a guest user. Please register to be able to do this": "Esta ação não pode ser realizada por um/a usuário/a visitante. Por favor, registre-se para poder fazer isso", "%(actionVerb)s this person?": "%(actionVerb)s esta pessoa?", "This room has no local addresses": "Esta sala não tem endereços locais", - "This room is private or inaccessible to guests. You may be able to join if you register": "Esta sala é privada ou inacessível para visitantes. Você poderá ingressar nela se registrar-se", - "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question": "Tentei carregar um ponto específico na linha do tempo desta sala, mas parece que você não tem permissões para ver a mensagem em questão", - "Tried to load a specific point in this room's timeline, but was unable to find it": "Tentei carregar um ponto específico na linha do tempo desta sala, mas não o encontrei", + "This room is private or inaccessible to guests. You may be able to join if you register.": "Esta sala é privada ou inacessível para visitantes. Você poderá ingressar nela se registrar-se.", + "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Tentei carregar um ponto específico na linha do tempo desta sala, mas parece que você não tem permissões para ver a mensagem em questão.", + "Tried to load a specific point in this room's timeline, but was unable to find it.": "Tentei carregar um ponto específico na linha do tempo desta sala, mas não o encontrei.", "Turn Markdown off": "Desabilitar a formatação 'Markdown'", "Turn Markdown on": "Habilitar a marcação 'Markdown'", "Unable to load device list": "Não foi possível carregar a lista de dispositivos", diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json index a0788a15dc..cf9bd78ffd 100644 --- a/src/i18n/strings/ru.json +++ b/src/i18n/strings/ru.json @@ -498,7 +498,7 @@ "Failed to set display name": "Не удалось установить отображаемое имя", "Failed to toggle moderator status": "Не удалось изменить статус модератора", "Fill screen": "Заполнить экран", - "Guest users can't upload files. Please register to upload": "Гости не могут посылать файлы. Пожалуйста, зарегистрируйтесь для отправки", + "Guest users can't upload files. Please register to upload.": "Гости не могут посылать файлы. Пожалуйста, зарегистрируйтесь для отправки.", "Hide read receipts": "Скрыть отметки о прочтении", "Hide Text Formatting Toolbar": "Скрыть панель форматирования текста", "Incorrect verification code": "Неверный код подтверждения", @@ -567,7 +567,7 @@ "since the point in time of selecting this option": "с момента выбора этой настройки", "since they joined": "с момента входа", "since they were invited": "с момента приглашения", - "Some of your messages have not been sent": "Некоторые из ваших сообщений не были отправлены", + "Some of your messages have not been sent.": "Некоторые из ваших сообщений не были отправлены.", "Someone": "Кто-то", "Submit": "Отправить", "Success": "Успех", @@ -583,7 +583,7 @@ "The remote side failed to pick up": "Удалённая сторона не смогла ответить", "This room has no local addresses": "Эта комната не имеет местного адреса", "This room is not recognised.": "Эта комната не опознана.", - "This room is private or inaccessible to guests. You may be able to join if you register": "Эта комната личная или недоступна для гостей. Мы может быть войдёте, если зарегистрируйтесь", + "This room is private or inaccessible to guests. You may be able to join if you register.": "Эта комната личная или недоступна для гостей. Мы может быть войдёте, если зарегистрируйтесь.", "These are experimental features that may break in unexpected ways": "Это экспериментальные функции, которые могут неожиданным образом вызывать ошибки", "This doesn't appear to be a valid email address": "Не похоже, что это правильный адрес электронной почты", "This is a preview of this room. Room interactions have been disabled": "Это просмотр данной комнаты. Взаимодействия с ней были отключены.", From fe487232ade5ebe30e3e1b7b60b08574d7729f0e Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 7 Jun 2017 18:15:48 +0100 Subject: [PATCH 118/127] sync fullstops everywhere --- scripts/fix-i18n.pl | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/scripts/fix-i18n.pl b/scripts/fix-i18n.pl index 247b2b663f..ea19d710df 100755 --- a/scripts/fix-i18n.pl +++ b/scripts/fix-i18n.pl @@ -61,6 +61,11 @@ You are already in a call. You cannot place VoIP calls in this browser. You cannot place a call with yourself. Your email address does not appear to be associated with a Matrix ID on this Homeserver. +Guest users can't upload files. Please register to upload. +Some of your messages have not been sent. +This room is private or inaccessible to guests. You may be able to join if you register. +Tried to load a specific point in this room's timeline, but was unable to find it. +Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question. EOT )]; } From ef2fedc3a9ead6c38888276ad93aed361ef6d6eb Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 7 Jun 2017 18:24:35 +0100 Subject: [PATCH 119/127] fix missing translations and typos in i18n --- src/components/structures/CreateRoom.js | 2 +- src/components/views/dialogs/SetMxIdDialog.js | 6 +++--- src/i18n/strings/de_DE.json | 2 +- src/i18n/strings/en_EN.json | 4 +++- src/i18n/strings/en_US.json | 2 +- src/i18n/strings/fr.json | 2 +- src/i18n/strings/pt.json | 2 +- src/i18n/strings/pt_BR.json | 2 +- src/i18n/strings/ru.json | 2 +- 9 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/components/structures/CreateRoom.js b/src/components/structures/CreateRoom.js index 3e291dfd94..7ecc315ba7 100644 --- a/src/components/structures/CreateRoom.js +++ b/src/components/structures/CreateRoom.js @@ -231,7 +231,7 @@ module.exports = React.createClass({ if (curr_phase == this.phases.ERROR) { error_box = (
- {_t('An error occured: %(error_string)s', {error_string: this.state.error_string})} + {_t('An error occurred: %(error_string)s', {error_string: this.state.error_string})}
); } diff --git a/src/components/views/dialogs/SetMxIdDialog.js b/src/components/views/dialogs/SetMxIdDialog.js index 78576eb1e4..2081701c6b 100644 --- a/src/components/views/dialogs/SetMxIdDialog.js +++ b/src/components/views/dialogs/SetMxIdDialog.js @@ -125,8 +125,8 @@ export default React.createClass({ break; case "M_INVALID_USERNAME": newState.usernameError = _t( - 'Username invalid: %(errMessage)', - { errMessage: err.message}, + 'Username invalid: %(error_message)s', + { error_message: err.message}, ); break; case "M_UNRECOGNIZED": @@ -139,7 +139,7 @@ export default React.createClass({ break; default: newState.usernameError = _t( - 'An error occurred: %(errMessage)', + 'An error occurred: %(errMessage)s', { errMessage: err.message }, ); break; diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index 513faea017..798540900c 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -339,7 +339,7 @@ "User names may only contain letters, numbers, dots, hyphens and underscores.": "Benutzernamen sollen nur Buchstaben, Nummern, Binde- und Unterstriche enthalten.", "An unknown error occurred.": "Ein unbekannter Fehler trat auf.", "I already have an account": "Ich habe bereits einen Account", - "An error occured: %(error_string)s": "Ein Fehler trat auf: %(error_string)s", + "An error occurred: %(error_string)s": "Ein Fehler trat auf: %(error_string)s", "Topic": "Thema", "Make this room private": "Mache diesen Raum privat", "Share message history with new users": "Bisherigen Chatverlauf mit neuen Nutzern teilen", diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index eb331c24a9..11af1c03df 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -268,6 +268,7 @@ "End-to-end encryption information": "End-to-end encryption information", "End-to-end encryption is in beta and may not be reliable": "End-to-end encryption is in beta and may not be reliable", "Enter Code": "Enter Code", + "Enter passphrase": "Enter passphrase", "Error": "Error", "Error decrypting attachment": "Error decrypting attachment", "Error: Problem communicating with the given homeserver.": "Error: Problem communicating with the given homeserver.", @@ -580,6 +581,7 @@ "User ID": "User ID", "User Interface": "User Interface", "User name": "User name", + "Username invalid: %(errMessage)s": "Username invalid: %(errMessage)s", "Users": "Users", "User": "User", "Verification Pending": "Verification Pending", @@ -661,7 +663,7 @@ "User names may only contain letters, numbers, dots, hyphens and underscores.": "User names may only contain letters, numbers, dots, hyphens and underscores.", "An unknown error occurred.": "An unknown error occurred.", "I already have an account": "I already have an account", - "An error occured: %(error_string)s": "An error occured: %(error_string)s", + "An error occurred: %(error_string)s": "An error occurred: %(error_string)s", "Topic": "Topic", "Make Moderator": "Make Moderator", "Make this room private": "Make this room private", diff --git a/src/i18n/strings/en_US.json b/src/i18n/strings/en_US.json index 19b854e936..96f2243f63 100644 --- a/src/i18n/strings/en_US.json +++ b/src/i18n/strings/en_US.json @@ -645,7 +645,7 @@ "User names may only contain letters, numbers, dots, hyphens and underscores.": "User names may only contain letters, numbers, dots, hyphens and underscores.", "An unknown error occurred.": "An unknown error occurred.", "I already have an account": "I already have an account", - "An error occured: %(error_string)s": "An error occured: %(error_string)s", + "An error occurred: %(error_string)s": "An error occurred: %(error_string)s", "Topic": "Topic", "Make Moderator": "Make Moderator", "Make this room private": "Make this room private", diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index 80d2479c48..ff6cb57ab5 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -611,7 +611,7 @@ "User names may only contain letters, numbers, dots, hyphens and underscores.": "Les noms d’utilisateurs ne peuvent contenir que des lettres, chiffres, points et tirets hauts ou bas.", "An unknown error occurred.": "Une erreur inconnue est survenue.", "I already have an account": "J’ai déjà un compte", - "An error occured: %(error_string)s": "Une erreur est survenue : %(error_string)s", + "An error occurred: %(error_string)s": "Une erreur est survenue : %(error_string)s", "Topic": "Sujet", "Make Moderator": "Nommer modérateur", "Make this room private": "Rendre ce salon privé", diff --git a/src/i18n/strings/pt.json b/src/i18n/strings/pt.json index 47260b4909..b76cf6a274 100644 --- a/src/i18n/strings/pt.json +++ b/src/i18n/strings/pt.json @@ -417,7 +417,7 @@ "User names may only contain letters, numbers, dots, hyphens and underscores.": "Nomes de usuária/o podem conter apenas letras, números, pontos, hífens e linha inferior (_).", "An unknown error occurred.": "Um erro desconhecido ocorreu.", "I already have an account": "Eu já tenho uma conta", - "An error occured: %(error_string)s": "Um erro ocorreu: %(error_string)s", + "An error occurred: %(error_string)s": "Um erro ocorreu: %(error_string)s", "Topic": "Tópico", "Make this room private": "Tornar esta sala privada", "Share message history with new users": "Compartilhar histórico de mensagens com novas/os usuárias/os", diff --git a/src/i18n/strings/pt_BR.json b/src/i18n/strings/pt_BR.json index 13c9ec0737..f77ef74799 100644 --- a/src/i18n/strings/pt_BR.json +++ b/src/i18n/strings/pt_BR.json @@ -417,7 +417,7 @@ "User names may only contain letters, numbers, dots, hyphens and underscores.": "Nomes de usuária/o podem conter apenas letras, números, pontos, hífens e linha inferior (_).", "An unknown error occurred.": "Um erro desconhecido ocorreu.", "I already have an account": "Eu já tenho uma conta", - "An error occured: %(error_string)s": "Um erro ocorreu: %(error_string)s", + "An error occurred: %(error_string)s": "Um erro ocorreu: %(error_string)s", "Topic": "Tópico", "Make this room private": "Tornar esta sala privada", "Share message history with new users": "Compartilhar histórico de mensagens com novas/os usuárias/os", diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json index cf9bd78ffd..61c82e6448 100644 --- a/src/i18n/strings/ru.json +++ b/src/i18n/strings/ru.json @@ -336,7 +336,7 @@ "User names may only contain letters, numbers, dots, hyphens and underscores.": "Имена пользователей могут только содержать буквы, числа, точки, дефисы и подчеркивания.", "An unknown error occurred.": "Произошла неизвестная ошибка.", "I already have an account": "У меня уже есть учетная запись", - "An error occured: %(error_string)s": "Произошла ошибка: %(error_string)s", + "An error occurred: %(error_string)s": "Произошла ошибка: %(error_string)s", "Topic": "Тема", "Make this room private": "Сделать эту комнату частной", "Share message history with new users": "Поделись историей сообщений с новыми учасниками", From 8228db88d2f8ea3f2acca0709dd431976c59192c Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 7 Jun 2017 18:28:49 +0100 Subject: [PATCH 120/127] oops, fix more i18n var name typos --- src/components/views/dialogs/SetMxIdDialog.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/views/dialogs/SetMxIdDialog.js b/src/components/views/dialogs/SetMxIdDialog.js index 2081701c6b..d428223ad6 100644 --- a/src/components/views/dialogs/SetMxIdDialog.js +++ b/src/components/views/dialogs/SetMxIdDialog.js @@ -125,8 +125,8 @@ export default React.createClass({ break; case "M_INVALID_USERNAME": newState.usernameError = _t( - 'Username invalid: %(error_message)s', - { error_message: err.message}, + 'Username invalid: %(errMessage)s', + { errMessage: err.message}, ); break; case "M_UNRECOGNIZED": @@ -139,8 +139,8 @@ export default React.createClass({ break; default: newState.usernameError = _t( - 'An error occurred: %(errMessage)s', - { errMessage: err.message }, + 'An error occurred: %(error_string)s', + { error_string: err.message }, ); break; } From f7ab6a574ccda7e6469ae1ec962a6aa0520c9991 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 7 Jun 2017 18:32:32 +0100 Subject: [PATCH 121/127] Remove unused string --- src/i18n/strings/de_DE.json | 1 - src/i18n/strings/en_EN.json | 1 - src/i18n/strings/en_US.json | 1 - src/i18n/strings/fr.json | 1 - src/i18n/strings/pt.json | 1 - src/i18n/strings/pt_BR.json | 1 - src/i18n/strings/ru.json | 1 - src/i18n/strings/th.json | 1 - 8 files changed, 8 deletions(-) diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index 798540900c..6d3abef3e4 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -615,7 +615,6 @@ "This action cannot be performed by a guest user. Please register to be able to do this": "Diese Aktion kann nicht von einem Gast ausgeführt werden. Bitte registriere dich um dies zu tun", "%(actionVerb)s this person?": "Diese Person %(actionVerb)s?", "This room has no local addresses": "Dieser Raum hat keine lokale Adresse", - "This room is private or inaccessible to guests. You may be able to join if you register.": "Dieser Raum ist privat oder für Gäste nicht zugänglich. Du kannst jedoch eventuell beitreten, wenn du dich registrierst.", "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Versuchte einen spezifischen Punkt in der Raum-Chronik zu laden, aber du hast keine Berechtigung die angeforderte Nachricht anzuzeigen.", "Tried to load a specific point in this room's timeline, but was unable to find it.": "Der Versuch, einen spezifischen Punkt im Chatverlauf zu laden, ist fehlgeschlagen. Der Punkt konnte nicht gefunden werden.", "Turn Markdown off": "Markdown abschalten", diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 11af1c03df..6a7f01e9df 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -511,7 +511,6 @@ "There was a problem logging in.": "There was a problem logging in.", "This room has no local addresses": "This room has no local addresses", "This room is not recognised.": "This room is not recognised.", - "This room is private or inaccessible to guests. You may be able to join if you register.": "This room is private or inaccessible to guests. You may be able to join if you register.", "These are experimental features that may break in unexpected ways": "These are experimental features that may break in unexpected ways", "The visibility of existing history will be unchanged": "The visibility of existing history will be unchanged", "This doesn't appear to be a valid email address": "This doesn't appear to be a valid email address", diff --git a/src/i18n/strings/en_US.json b/src/i18n/strings/en_US.json index 96f2243f63..6736e39276 100644 --- a/src/i18n/strings/en_US.json +++ b/src/i18n/strings/en_US.json @@ -501,7 +501,6 @@ "There was a problem logging in.": "There was a problem logging in.", "This room has no local addresses": "This room has no local addresses", "This room is not recognised.": "This room is not recognized.", - "This room is private or inaccessible to guests. You may be able to join if you register.": "This room is private or inaccessible to guests. You may be able to join if you register.", "These are experimental features that may break in unexpected ways": "These are experimental features that may break in unexpected ways", "The visibility of existing history will be unchanged": "The visibility of existing history will be unchanged", "This doesn't appear to be a valid email address": "This doesn't appear to be a valid email address", diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index ff6cb57ab5..bf05173356 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -478,7 +478,6 @@ "The remote side failed to pick up": "Le correspondant n’a pas décroché", "This room has no local addresses": "Ce salon n'a pas d'adresse locale", "This room is not recognised.": "Ce salon n'a pas été reconnu.", - "This room is private or inaccessible to guests. You may be able to join if you register.": "Ce salon est privé ou non autorisé aux visiteurs. Vous devriez pouvoir le rejoindre si vous vous enregistrez.", "These are experimental features that may break in unexpected ways": "Ces fonctionnalités sont expérimentales et risquent de mal fonctionner", "The visibility of existing history will be unchanged": "La visibilité de l’historique existant sera inchangée", "This doesn't appear to be a valid email address": "Cette adresse n’a pas l’air d’être valide", diff --git a/src/i18n/strings/pt.json b/src/i18n/strings/pt.json index b76cf6a274..27d257de9e 100644 --- a/src/i18n/strings/pt.json +++ b/src/i18n/strings/pt.json @@ -633,7 +633,6 @@ "This action cannot be performed by a guest user. Please register to be able to do this": "Esta ação não pode ser realizada por um/a usuário/a visitante. Por favor, registre-se para poder fazer isso", "%(actionVerb)s this person?": "%(actionVerb)s esta pessoa?", "This room has no local addresses": "Esta sala não tem endereços locais", - "This room is private or inaccessible to guests. You may be able to join if you register.": "Esta sala é privada ou inacessível para visitantes. Você poderá ingressar nela se registrar-se.", "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Tentei carregar um ponto específico na linha do tempo desta sala, mas parece que você não tem permissões para ver a mensagem em questão.", "Tried to load a specific point in this room's timeline, but was unable to find it.": "Tentei carregar um ponto específico na linha do tempo desta sala, mas não o encontrei.", "Turn Markdown off": "Desabilitar a formatação 'Markdown'", diff --git a/src/i18n/strings/pt_BR.json b/src/i18n/strings/pt_BR.json index f77ef74799..99175c874e 100644 --- a/src/i18n/strings/pt_BR.json +++ b/src/i18n/strings/pt_BR.json @@ -633,7 +633,6 @@ "This action cannot be performed by a guest user. Please register to be able to do this": "Esta ação não pode ser realizada por um/a usuário/a visitante. Por favor, registre-se para poder fazer isso", "%(actionVerb)s this person?": "%(actionVerb)s esta pessoa?", "This room has no local addresses": "Esta sala não tem endereços locais", - "This room is private or inaccessible to guests. You may be able to join if you register.": "Esta sala é privada ou inacessível para visitantes. Você poderá ingressar nela se registrar-se.", "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Tentei carregar um ponto específico na linha do tempo desta sala, mas parece que você não tem permissões para ver a mensagem em questão.", "Tried to load a specific point in this room's timeline, but was unable to find it.": "Tentei carregar um ponto específico na linha do tempo desta sala, mas não o encontrei.", "Turn Markdown off": "Desabilitar a formatação 'Markdown'", diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json index 61c82e6448..58f8e0291c 100644 --- a/src/i18n/strings/ru.json +++ b/src/i18n/strings/ru.json @@ -583,7 +583,6 @@ "The remote side failed to pick up": "Удалённая сторона не смогла ответить", "This room has no local addresses": "Эта комната не имеет местного адреса", "This room is not recognised.": "Эта комната не опознана.", - "This room is private or inaccessible to guests. You may be able to join if you register.": "Эта комната личная или недоступна для гостей. Мы может быть войдёте, если зарегистрируйтесь.", "These are experimental features that may break in unexpected ways": "Это экспериментальные функции, которые могут неожиданным образом вызывать ошибки", "This doesn't appear to be a valid email address": "Не похоже, что это правильный адрес электронной почты", "This is a preview of this room. Room interactions have been disabled": "Это просмотр данной комнаты. Взаимодействия с ней были отключены.", diff --git a/src/i18n/strings/th.json b/src/i18n/strings/th.json index db67539b38..5bafdc97cd 100644 --- a/src/i18n/strings/th.json +++ b/src/i18n/strings/th.json @@ -318,7 +318,6 @@ "The file '%(fileName)s' failed to upload": "การอัปโหลดไฟล์ '%(fileName)s' ล้มเหลว", "This Home Server does not support login using email address.": "เซิร์ฟเวอร์บ้านนี้ไม่รองรับการลงชื่อเข้าใช้ด้วยที่อยู่อีเมล", "There was a problem logging in.": "มีปัญหาในการลงชื่อเข้าใช้", - "This room is private or inaccessible to guests. You may be able to join if you register": "ห้องนี้เป็นส่วนตัวหรือไม่อนุญาตให้แขกเข้าถึง คุณอาจเข้าร่วมได้หากคุณลงทะเบียน", "this invitation?": "คำเชิญนี้?", "This is a preview of this room. Room interactions have been disabled": "นี่คือตัวอย่างของห้อง การตอบสนองภายในห้องถูกปิดใช้งาน", "This phone number is already in use": "หมายเลขโทรศัพท์นี้ถูกใช้งานแล้ว", From 5cf44454d2b463fe5543a439992a270398aec3f1 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 7 Jun 2017 18:41:18 +0100 Subject: [PATCH 122/127] fix more punctuation fails --- src/i18n/strings/th.json | 1 + src/i18n/strings/zh_Hans.json | 4 ++-- src/i18n/strings/zh_Hant.json | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/i18n/strings/th.json b/src/i18n/strings/th.json index 5bafdc97cd..f5564415fc 100644 --- a/src/i18n/strings/th.json +++ b/src/i18n/strings/th.json @@ -318,6 +318,7 @@ "The file '%(fileName)s' failed to upload": "การอัปโหลดไฟล์ '%(fileName)s' ล้มเหลว", "This Home Server does not support login using email address.": "เซิร์ฟเวอร์บ้านนี้ไม่รองรับการลงชื่อเข้าใช้ด้วยที่อยู่อีเมล", "There was a problem logging in.": "มีปัญหาในการลงชื่อเข้าใช้", + "This room is private or inaccessible to guests. You may be able to join if you register.": "ห้องนี้เป็นส่วนตัวหรือไม่อนุญาตให้แขกเข้าถึง คุณอาจเข้าร่วมได้หากคุณลงทะเบียน", "this invitation?": "คำเชิญนี้?", "This is a preview of this room. Room interactions have been disabled": "นี่คือตัวอย่างของห้อง การตอบสนองภายในห้องถูกปิดใช้งาน", "This phone number is already in use": "หมายเลขโทรศัพท์นี้ถูกใช้งานแล้ว", diff --git a/src/i18n/strings/zh_Hans.json b/src/i18n/strings/zh_Hans.json index 54a6886b35..e0db2b525b 100644 --- a/src/i18n/strings/zh_Hans.json +++ b/src/i18n/strings/zh_Hans.json @@ -76,7 +76,7 @@ "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s 从 %(fromPowerLevel)s 变为 %(toPowerLevel)s", "Guests can't set avatars. Please register.": "游客不能设置头像。请注册。.", "Guest users can't create new rooms. Please register to create room and start a chat.": "游客不能创建聊天室。请注册以创建聊天室和聊天.", - "Guest users can't upload files. Please register to upload": "游客不能上传文件。请注册以上传文件", + "Guest users can't upload files. Please register to upload.": "游客不能上传文件。请注册以上传文件", "Guests can't use labs features. Please register.": "游客不能使用实验性功能。请注册。.", "Guests cannot join this room even if explicitly invited.": "游客不能加入此聊天室,即使有人主动邀请。.", "had": "已经", @@ -138,7 +138,7 @@ "since the point in time of selecting this option": "从选择此选项起", "since they joined": "从他们加入时起", "since they were invited": "从他们被邀请时起", - "Some of your messages have not been sent": "部分消息发送失败", + "Some of your messages have not been sent.": "部分消息发送失败", "Someone": "某个用户", "Sorry, this homeserver is using a login which is not recognised ": "很抱歉,无法识别此主服务器使用的登录方式 ", "Start a chat": "创建聊天", diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index 250dc23ef3..17901789e9 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -194,7 +194,7 @@ "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s 從 %(fromPowerLevel)s 變為 %(toPowerLevel)s", "Guests can't set avatars. Please register.": "游客不能設置頭像。請注冊。.", "Guest users can't create new rooms. Please register to create room and start a chat.": "游客不能創建聊天室。請注冊以創建聊天室和聊天.", - "Guest users can't upload files. Please register to upload": "游客不能上傳文件。請注冊以上傳文件", + "Guest users can't upload files. Please register to upload.": "游客不能上傳文件。請注冊以上傳文件", "Guests can't use labs features. Please register.": "游客不能使用實驗性功能。請注冊。.", "Guests cannot join this room even if explicitly invited.": "游客不能加入此聊天室,即使有人主動邀請。.", "had": "已經", @@ -265,7 +265,7 @@ "since the point in time of selecting this option": "從選擇此選項起", "since they joined": "從他們加入時起", "since they were invited": "從他們被邀請時起", - "Some of your messages have not been sent": "部分消息發送失敗", + "Some of your messages have not been sent.": "部分消息發送失敗", "Someone": "某個用戶", "Sorry, this homeserver is using a login which is not recognised ": "很抱歉,無法識別此主伺服器使用的登錄方式 ", "Start a chat": "創建聊天", From ca1c0e42b0559b2095b2a99783a0f946f020dc89 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 7 Jun 2017 18:42:34 +0100 Subject: [PATCH 123/127] oops, merge correctly --- src/i18n/strings/th.json | 1 - 1 file changed, 1 deletion(-) diff --git a/src/i18n/strings/th.json b/src/i18n/strings/th.json index f5564415fc..5bafdc97cd 100644 --- a/src/i18n/strings/th.json +++ b/src/i18n/strings/th.json @@ -318,7 +318,6 @@ "The file '%(fileName)s' failed to upload": "การอัปโหลดไฟล์ '%(fileName)s' ล้มเหลว", "This Home Server does not support login using email address.": "เซิร์ฟเวอร์บ้านนี้ไม่รองรับการลงชื่อเข้าใช้ด้วยที่อยู่อีเมล", "There was a problem logging in.": "มีปัญหาในการลงชื่อเข้าใช้", - "This room is private or inaccessible to guests. You may be able to join if you register.": "ห้องนี้เป็นส่วนตัวหรือไม่อนุญาตให้แขกเข้าถึง คุณอาจเข้าร่วมได้หากคุณลงทะเบียน", "this invitation?": "คำเชิญนี้?", "This is a preview of this room. Room interactions have been disabled": "นี่คือตัวอย่างของห้อง การตอบสนองภายในห้องถูกปิดใช้งาน", "This phone number is already in use": "หมายเลขโทรศัพท์นี้ถูกใช้งานแล้ว", From ea02d8841c991e1b595a8e87f166170ddc61b2bb Mon Sep 17 00:00:00 2001 From: RiotTranslate Date: Wed, 7 Jun 2017 19:48:48 +0200 Subject: [PATCH 124/127] Update from Weblate. (#1052) * Added translation using Weblate (Thai) * Translated using Weblate (Thai) Currently translated at 2.6% (22 of 827 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.nordgedanken.de/projects/riot-web/matrix-react-sdk/th/ * Translated using Weblate (Thai) Currently translated at 8.9% (74 of 827 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.nordgedanken.de/projects/riot-web/matrix-react-sdk/th/ * Translated using Weblate (German) Currently translated at 99.8% (826 of 827 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.nordgedanken.de/projects/riot-web/matrix-react-sdk/de/ * Translated using Weblate (Swedish) Currently translated at 35.4% (293 of 827 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.nordgedanken.de/projects/riot-web/matrix-react-sdk/sv/ * Translated using Weblate (Thai) Currently translated at 9.7% (81 of 827 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.nordgedanken.de/projects/riot-web/matrix-react-sdk/th/ * Translated using Weblate (German) Currently translated at 100.0% (827 of 827 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.nordgedanken.de/projects/riot-web/matrix-react-sdk/de/ * Translated using Weblate (Russian) Currently translated at 76.9% (636 of 827 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.nordgedanken.de/projects/riot-web/matrix-react-sdk/ru/ * Translated using Weblate (Hungarian) Currently translated at 0.1% (1 of 827 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.nordgedanken.de/projects/riot-web/matrix-react-sdk/hu/ * Translated using Weblate (Thai) Currently translated at 39.2% (325 of 827 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.nordgedanken.de/projects/riot-web/matrix-react-sdk/th/ * Translated using Weblate (German) Currently translated at 99.8% (827 of 828 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.nordgedanken.de/projects/riot-web/matrix-react-sdk/de/ * Translated using Weblate (German) Currently translated at 99.8% (827 of 828 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.nordgedanken.de/projects/riot-web/matrix-react-sdk/de/ * Translated using Weblate (Thai) Currently translated at 39.7% (329 of 828 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.nordgedanken.de/projects/riot-web/matrix-react-sdk/th/ * Translated using Weblate (German) Currently translated at 99.8% (828 of 828 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.nordgedanken.de/projects/riot-web/matrix-react-sdk/de/ * Translated using Weblate (Hungarian) Currently translated at 3.1% (26 of 828 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.nordgedanken.de/projects/riot-web/matrix-react-sdk/hu/ * Translated using Weblate (Russian) Currently translated at 77.4% (641 of 828 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.nordgedanken.de/projects/riot-web/matrix-react-sdk/ru/ * Translated using Weblate (Thai) Currently translated at 39.7% (329 of 828 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.nordgedanken.de/projects/riot-web/matrix-react-sdk/th/ * Translated using Weblate (Russian) Currently translated at 100.0% (828 of 828 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.nordgedanken.de/projects/riot-web/matrix-react-sdk/ru/ * Translated using Weblate (German) Currently translated at 100.0% (828 of 828 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.nordgedanken.de/projects/riot-web/matrix-react-sdk/de/ * Translated using Weblate (Russian) Currently translated at 100.0% (828 of 828 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.nordgedanken.de/projects/riot-web/matrix-react-sdk/ru/ * Translated using Weblate (German) Currently translated at 100.0% (850 of 850 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.nordgedanken.de/projects/riot-web/matrix-react-sdk/de/ * Translated using Weblate (French) Currently translated at 96.5% (821 of 850 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.nordgedanken.de/projects/riot-web/matrix-react-sdk/fr/ * Translated using Weblate (Thai) Currently translated at 39.0% (332 of 850 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.nordgedanken.de/projects/riot-web/matrix-react-sdk/th/ --- src/i18n/strings/de_DE.json | 109 +++++++++++------ src/i18n/strings/fr.json | 5 +- src/i18n/strings/hu.json | 27 ++++- src/i18n/strings/ru.json | 228 +++++++++++++++++++++++++++++++++--- src/i18n/strings/th.json | 16 ++- 5 files changed, 322 insertions(+), 63 deletions(-) diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index 6d3abef3e4..f7b3d16ebc 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -6,7 +6,7 @@ "People": "Direkt-Chats", "Rooms": "Räume", "Low priority": "Niedrige Priorität", - "Historical": "Historisch", + "Historical": "Archiv", "New passwords must match each other.": "Die neuen Passwörter müssen identisch sein.", "A new password must be entered.": "Es muss ein neues Passwort eingegeben werden.", "The email address linked to your account must be entered.": "Es muss die Email-Adresse eingeben werden, welche zum Account gehört.", @@ -42,7 +42,7 @@ "Commands": "Kommandos", "Emoji": "Emoji", "Sorry, this homeserver is using a login which is not recognised ": "Entschuldigung, dieser Homeserver nutzt eine Anmeldetechnik, die nicht bekannt ist ", - "Login as guest": "Anmelden als Gast", + "Login as guest": "Als Gast anmelden", "Return to app": "Zurück zur Anwendung", "Sign in": "Anmelden", "Create a new account": "Erstelle einen neuen Benutzer", @@ -75,7 +75,7 @@ "changed the topic to": "änderte das Thema zu", "Changes to who can read history will only apply to future messages in this room": "Änderungen, die bestimmen, wer den Chatverlauf lesen kann, gelten nur für zukünftige Nachrichten in diesem Raum", "Clear Cache and Reload": "Cache leeren und neu laden", - "Click here": "Klicke hier", + "Click here": "Hier klicken,", "Confirm your new password": "Neues Passwort bestätigen", "Continue": "Fortfahren", "Create an account": "Erstelle einen Account", @@ -96,7 +96,7 @@ "End-to-end encryption is in beta and may not be reliable": "Die Ende-zu-Ende-Verschlüsselung befindet sich aktuell im Beta-Stadium und ist eventuell noch nicht hundertprozentig zuverlässig", "Failed to send email": "Fehler beim Senden der E-Mail", "Account": "Konto", - "Add phone number": "Füge Telefonnummer hinzu", + "Add phone number": "Telefonnummer hinzufügen", "an address": "an Adresse", "Your password was successfully changed. You will not receive push notifications on other devices until you log back in to them": "Dein Passwort wurde erfolgreich geändert. Du wirst erst Benachrichtigungen auf anderen Geräten empfangen können, wenn du dich dort erneut anmeldest", "all room members": "Alle Raum-Mitglieder", @@ -111,7 +111,7 @@ "Default": "Standard", "demote": "Berechtigungslevel herabstufen", "Export E2E room keys": "E2E-Raum-Schlüssel exportieren", - "Failed to change password. Is your password correct?": "Passwort-Änderung schlug fehl. Ist dein Passwort korrekt?", + "Failed to change password. Is your password correct?": "Passwortänderung fehlgeschlagen. Ist dein Passwort richtig?", "Failed to forget room": "Vergessen des Raums schlug fehl", "Failed to leave room": "Verlassen des Raums fehlgeschlagen", "Failed to reject invitation": "Fehler beim Abweisen der Einladung", @@ -140,12 +140,12 @@ "is a": "ist ein", "is trusted": "wird vertraut", "Sign in with": "Ich möchte mich anmelden mit", - "joined and left": "trat bei und ging", - "joined": "trat bei", + "joined and left": "hat den Raum betreten und wieder verlassen", + "joined": "hat den Raum betreten", "joined the room": "trat dem Raum bei", "Leave room": "Verlasse Raum", "left and rejoined": "ging(en) und trat(en) erneut bei", - "left": "ging", + "left": "hat den Raum verlassen", "left the room": "verließ den Raum", "Logged in as": "Angemeldet als", "Logout": "Abmelden", @@ -204,7 +204,7 @@ "Signed Out": "Abgemeldet", "Sign out": "Abmelden", "since the point in time of selecting this option": "ab dem Zeitpunkt, an dem diese Option gewählt wird", - "since they joined": "seitdem sie beitraten", + "since they joined": "ab dem Zeitpunkt, an dem sie beigetreten sind", "since they were invited": "seitdem sie eingeladen wurden", "Someone": "Jemand", "Start a chat": "Starte einen Chat", @@ -336,8 +336,8 @@ "Password too short (min %(MIN_PASSWORD_LENGTH)s).": "Passwort zu kurz (min. %(MIN_PASSWORD_LENGTH)s).", "This doesn't look like a valid email address.": "Dies scheint keine gültige E-Mail-Adresse zu sein.", "This doesn't look like a valid phone number.": "Dies scheint keine gültige Telefonnummer zu sein.", - "User names may only contain letters, numbers, dots, hyphens and underscores.": "Benutzernamen sollen nur Buchstaben, Nummern, Binde- und Unterstriche enthalten.", - "An unknown error occurred.": "Ein unbekannter Fehler trat auf.", + "User names may only contain letters, numbers, dots, hyphens and underscores.": "Benutzernamen dürfen nur Buchstaben, Nummern, Punkte, Binde- und Unterstriche enthalten.", + "An unknown error occurred.": "Ein unbekannter Fehler ist aufgetreten.", "I already have an account": "Ich habe bereits einen Account", "An error occurred: %(error_string)s": "Ein Fehler trat auf: %(error_string)s", "Topic": "Thema", @@ -352,7 +352,7 @@ "%(names)s and %(count)s others are typing": "%(names)s und %(count)s weitere Personen schreiben", "%(senderName)s answered the call.": "%(senderName)s hat den Anruf angenommen.", "%(senderName)s banned %(targetName)s.": "%(senderName)s hat %(targetName)s aus dem Raum verbannt.", - "%(senderName)s changed their display name from %(oldDisplayName)s to %(displayName)s.": "%(senderName)s hat den Anzeigenamen von %(oldDisplayName)s auf %(displayName)s geändert.", + "%(senderName)s changed their display name from %(oldDisplayName)s to %(displayName)s.": "%(senderName)s hat den Anzeigenamen von \"%(oldDisplayName)s\" auf \"%(displayName)s\" geändert.", "%(senderName)s changed their profile picture.": "%(senderName)s hat das Profilbild geändert.", "%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s hat das Berechtigungslevel von %(powerLevelDiffText)s geändert.", "%(senderDisplayName)s changed the room name to %(roomName)s.": "%(senderDisplayName)s änderte den Raumnamen zu %(roomName)s.", @@ -364,7 +364,7 @@ "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s von %(fromPowerLevel)s zu %(toPowerLevel)s", "%(senderName)s invited %(targetName)s.": "%(senderName)s hat %(targetName)s eingeladen.", "%(displayName)s is typing": "%(displayName)s schreibt", - "%(targetName)s joined the room.": "%(targetName)s trat dem Raum bei.", + "%(targetName)s joined the room.": "%(targetName)s hat den Raum betreten.", "%(senderName)s kicked %(targetName)s.": "%(senderName)s kickte %(targetName)s.", "%(targetName)s left the room.": "%(targetName)s hat den Raum verlassen.", "%(senderName)s made future room history visible to": "%(senderName)s machte die zukünftige Raumhistorie sichtbar für", @@ -378,12 +378,12 @@ "Reason": "Grund", "%(targetName)s rejected the invitation.": "%(targetName)s hat die Einladung abgelehnt.", "%(senderName)s removed their display name (%(oldDisplayName)s).": "%(senderName)s löschte den Anzeigenamen (%(oldDisplayName)s).", - "%(senderName)s removed their profile picture.": "%(senderName)s löschte das Profilbild.", + "%(senderName)s removed their profile picture.": "%(senderName)s hat das Profilbild gelöscht.", "%(senderName)s requested a VoIP conference.": "%(senderName)s möchte eine VoIP-Konferenz beginnen.", "Room %(roomId)s not visible": "Raum %(roomId)s ist nicht sichtbar", "%(senderDisplayName)s sent an image.": "%(senderDisplayName)s hat ein Bild gesendet.", "%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.": "%(senderName)s hat %(targetDisplayName)s in diesen Raum eingeladen.", - "%(senderName)s set a profile picture.": "%(senderName)s setzte ein Profilbild.", + "%(senderName)s set a profile picture.": "%(senderName)s hat ein Profilbild gesetzt.", "%(senderName)s set their display name to %(displayName)s.": "%(senderName)s hat den Anzeigenamen geändert in %(displayName)s.", "This room is not recognised.": "Dieser Raum wurde nicht erkannt.", "These are experimental features that may break in unexpected ways": "Dies sind experimentelle Funktionen, die in unerwarteter Weise Fehler verursachen können", @@ -398,7 +398,7 @@ "There are no visible files in this room": "Es gibt keine sichtbaren Dateien in diesem Raum", "Error changing language": "Fehler beim Ändern der Sprache", "Riot was unable to find the correct Data for the selected Language.": "Riot war nicht in der Lage die korrekten Daten für die ausgewählte Sprache zu finden.", - "Connectivity to the server has been lost.": "Verbindung zum Server untergebrochen.", + "Connectivity to the server has been lost.": "Verbindung zum Server wurde unterbrochen.", "Sent messages will be stored until your connection has returned.": "Gesendete Nachrichten werden gespeichert, bis die Internetverbindung wiederhergestellt wurde.", "Auto-complete": "Autovervollständigung", "Resend all": "Alle erneut senden", @@ -427,7 +427,7 @@ "You're not in any rooms yet! Press": "Du bist noch keinem Raum beigetreten! Drücke", "click to reveal": "Klicke zum anzeigen", "To remove other users' messages": "Um Nachrichten anderer Nutzer zu verbergen", - "You are trying to access %(roomName)s": "Du versuchst auf %(roomName)s zuzugreifen", + "You are trying to access %(roomName)s": "Du versuchst, auf den Raum \"%(roomName)s\" zuzugreifen", "af": "Afrikaans", "ar-ae": "Arabisch (VAE)", "ar-bh": "Arabisch (Bahrain)", @@ -558,7 +558,7 @@ "Are you sure?": "Bist du sicher?", "Attachment": "Anhang", "Ban": "Verbannen", - "Can't connect to homeserver - please check your connectivity and ensure your homeserver's SSL certificate is trusted.": "Kann nicht zum Heimserver verbinden - bitte checke eine Verbindung und stelle sicher, dass dem SSL-Zertifikat deines Heimservers vertraut wird.", + "Can't connect to homeserver - please check your connectivity and ensure your homeserver's SSL certificate is trusted.": "Verbindungsaufbau zum Heimserver nicht möglich - bitte Internetverbindung überprüfen und sicherstellen, ob das SSL-Zertifikat des Heimservers vertrauenswürdig ist.", "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or enable unsafe scripts.": "Kann nicht zum Heimserver via HTTP verbinden, wenn eine HTTPS-Url in deiner Adresszeile steht. Nutzer HTTPS oder aktiviere unsichere Skripte.", "changing room on a RoomView is not supported": "Das Ändern eines Raumes in einer RaumAnsicht wird nicht unterstützt", "Click to mute audio": "Klicke um den Ton stumm zu stellen", @@ -590,7 +590,7 @@ "'%(alias)s' is not a valid format for an alias": "'%(alias)s' hat kein valides Aliasformat", "Join Room": "Dem Raum beitreten", "Kick": "Kicke", - "Level": "Level", + "Level": "Berechtigungslevel", "Local addresses for this room:": "Lokale Adressen dieses Raumes:", "Markdown is disabled": "Markdown ist deaktiviert", "Markdown is enabled": "Markdown ist aktiviert", @@ -652,25 +652,25 @@ "%(items)s and one other": "%(items)s und ein(e) weitere(r)", "%(items)s and %(lastItem)s": "%(items)s und %(lastItem)s", "%(severalUsers)sjoined %(repeats)s times": "%(severalUsers)ssind dem Raum %(repeats)s mal beigetreten", - "%(oneUser)sjoined %(repeats)s times": "%(oneUser)strat %(repeats)s mal bei", - "%(severalUsers)sjoined": "%(severalUsers)straten bei", - "%(oneUser)sjoined": "%(oneUser)strat bei", + "%(oneUser)sjoined %(repeats)s times": "%(oneUser)shat den Raum %(repeats)s mal betreten", + "%(severalUsers)sjoined": "%(severalUsers)shaben den Raum betreten", + "%(oneUser)sjoined": "%(oneUser)shat den Raum betreten", "%(severalUsers)sleft %(repeats)s times": "%(severalUsers)sverließen %(repeats)s mal den Raum", "%(oneUser)sleft %(repeats)s times": "%(oneUser)sging %(repeats)s mal", "%(severalUsers)sleft": "%(severalUsers)shaben den Raum verlassen", "%(oneUser)sleft": "%(oneUser)sging", - "%(severalUsers)sjoined and left %(repeats)s times": "%(severalUsers)straten bei und gingen %(repeats)s mal", - "%(oneUser)sjoined and left %(repeats)s times": "%(oneUser)strat bei und ging %(repeats)s mal", - "%(severalUsers)sjoined and left": "%(severalUsers)straten bei und gingen", - "%(oneUser)sjoined and left": "%(oneUser)strat bei und ging", + "%(severalUsers)sjoined and left %(repeats)s times": "%(severalUsers)shaben den Raum %(repeats)s mal betreten und wieder verlassen", + "%(oneUser)sjoined and left %(repeats)s times": "%(oneUser)shat den Raum %(repeats)s mal betreten und wieder verlassen", + "%(severalUsers)sjoined and left": "%(severalUsers)shaben den Raum betreten und wieder verlassen", + "%(oneUser)sjoined and left": "%(oneUser)shat den Raum betreten und wieder verlassen", "%(severalUsers)sleft and rejoined %(repeats)s times": "%(severalUsers)shaben den Raum verlassen und %(repeats)s mal neu betreten", - "%(oneUser)sleft and rejoined %(repeats)s times": "%(oneUser)sging und trat %(repeats)s mal erneut bei", - "%(severalUsers)sleft and rejoined": "%(severalUsers)s gingen und traten erneut bei", + "%(oneUser)sleft and rejoined %(repeats)s times": "%(oneUser)shat den Raum %(repeats)s mal verlassen und wieder neu betreten", + "%(severalUsers)sleft and rejoined": "%(severalUsers)shaben den Raum verlassen und wieder neu betreten", "%(oneUser)sleft left and rejoined": "%(oneUser)sging und trat erneut bei", - "%(severalUsers)srejected their invitations %(repeats)s times": "%(severalUsers)s lehnten %(repeats)s mal ihre Einladung ab", + "%(severalUsers)srejected their invitations %(repeats)s times": "%(severalUsers)shaben ihre Einladung %(repeats)s mal abgelehnt", "%(oneUser)srejected their invitation %(repeats)s times": "%(oneUser)shat die Einladung %(repeats)s mal abgelehnt", - "%(severalUsers)srejected their invitations": "%(severalUsers)slehnten ihre Einladung ab", - "%(oneUser)srejected their invitation": "%(oneUser)slehnte seine/ihre Einladung ab", + "%(severalUsers)srejected their invitations": "%(severalUsers)shaben ihre Einladung abgelehnt", + "%(oneUser)srejected their invitation": "%(oneUser)shat die Einladung abgelehnt", "%(severalUsers)shad their invitations withdrawn %(repeats)s times": "%(severalUsers)szogen ihre Einladungen %(repeats)s mal zurück", "%(oneUser)shad their invitation withdrawn %(repeats)s times": "%(oneUser)szog seine/ihre Einladung %(repeats)s mal zurück", "%(severalUsers)shad their invitations withdrawn": "%(severalUsers)szogen ihre Einladungen zurück", @@ -687,16 +687,16 @@ "were kicked %(repeats)s times": "wurden %(repeats)s mal gekickt", "was kicked %(repeats)s times": "wurde %(repeats)s mal gekickt", "were kicked": "wurden aus dem Raum entfernt", - "%(severalUsers)schanged their name %(repeats)s times": "%(severalUsers)sänderten %(repeats)s mal ihre Namen", - "%(oneUser)schanged their name %(repeats)s times": "%(oneUser)sänderte %(repeats)s mal seinen/ihren Namen", + "%(severalUsers)schanged their name %(repeats)s times": "%(severalUsers)shaben ihren Namen %(repeats)s mal geändert", + "%(oneUser)schanged their name %(repeats)s times": "%(oneUser)shat den Namen %(repeats)s mal geändert", "%(severalUsers)schanged their name": "%(severalUsers)shaben ihre Namen geändert", - "%(oneUser)schanged their name": "%(oneUser)sänderte seinen/ihren Namen", + "%(oneUser)schanged their name": "%(oneUser)shat den Namen geändert", "%(severalUsers)schanged their avatar %(repeats)s times": "%(severalUsers)shaben %(repeats)s mal ihr Profilbild geändert", "%(oneUser)schanged their avatar %(repeats)s times": "%(oneUser)shat %(repeats)s mal das Profilbild geändert", "%(severalUsers)schanged their avatar": "%(severalUsers)shaben ihr Profilbild geändert", "%(oneUser)schanged their avatar": "%(oneUser)shat das Profilbild geändert", "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s, %(day)s. %(monthName)s %(fullYear)s %(time)s", - "%(oneUser)sleft and rejoined": "%(oneUser)sverließ den Raum und trat erneut bei", + "%(oneUser)sleft and rejoined": "%(oneUser)shat den Raum verlassen und wieder neu betreten", "A registered account is required for this action": "Für diese Aktion ist ein registrierter Account notwendig", "Access Token:": "Zugangs-Token:", "Always show message timestamps": "Nachrichten-Zeitstempel immer anzeigen", @@ -727,7 +727,7 @@ "Invalid file%(extra)s": "Ungültige Datei%(extra)s", "Remove %(threePid)s?": "Entferne %(threePid)s?", "Please select the destination room for this message": "Bitte den Raum auswählen, an den diese Nachricht gesendet werden soll", - "%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s löschte den Raumnamen.", + "%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s hat den Raum-Namen gelöscht.", "Passphrases must match": "Passphrase muss übereinstimmen", "Passphrase must not be empty": "Passphrase darf nicht leer sein", "Export room keys": "Raum-Schlüssel exportieren", @@ -861,8 +861,8 @@ "device id: ": "Geräte-ID: ", "Device key:": "Geräte-Schlüssel:", "Email address (optional)": "E-Mail-Adresse (optional)", - "List this room in %(domain)s's room directory?": "Liste diesen Raum in %(domain)s's Raumverzeichnis?", - "Mobile phone number (optional)": "Handynummer (optional)", + "List this room in %(domain)s's room directory?": "Diesen Raum zum Raum-Verzeichnis von %(domain)s hinzufügen?", + "Mobile phone number (optional)": "Mobilfunknummer (optional)", "Password:": "Passwort:", "Register": "Registrieren", "Save": "Speichern", @@ -888,5 +888,36 @@ "$senderDisplayName changed the room avatar to ": "$senderDisplayName hat das Raum-Bild geändert zu ", "%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s hat das Raum-Bild für %(roomName)s geändert", "Hide removed messages": "Gelöschte Nachrichten verbergen", - "Start new chat": "Neuen Chat starten" + "Start new chat": "Neuen Chat starten", + "Disable markdown formatting": "Deaktiviere Markdown-Formatierung", + "Add": "Hinzufügen", + "%(count)s new messages.one": "%(count)s neue Nachricht", + "%(count)s new messages.other": "%(count)s neue Nachrichten", + "Error: Problem communicating with the given homeserver.": "Fehler: Problem beim kommunizieren mit dem angegebenen Heimserver.", + "Failed to fetch avatar URL": "Fehler beim holen der Avatar-URL", + "Some of your messages have not been sent.": "Einige deiner Nachrichten wurden nicht gesendet.", + "The phone number entered looks invalid": "Die Telefonnummer, die eingegeben wurde, sieht ungültig aus", + "This room is private or inaccessible to guests. You may be able to join if you register.": "Dieser Raum ist privat oder für Gäste nicht betretbar. Du kannst evtl. beitreten wenn du dich registrierst.", + "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Es wurde versucht einen spezifischen Punkt in der Chat-Historie zu laden, aber du hast keine Berechtigung diese Nachricht zu sehen.", + "Tried to load a specific point in this room's timeline, but was unable to find it.": "Es wurde versucht einen spezifischen Punkt in der Chat-Historie zu laden, aber er konnte nicht gefunden werden.", + "Uploading %(filename)s and %(count)s others.zero": "%(filename)s wird hochgeladen", + "Uploading %(filename)s and %(count)s others.one": "%(filename)s und %(count)s weitere werden hochgeladen", + "Uploading %(filename)s and %(count)s others.other": "%(filename)s und %(count)s weitere werden hochgeladen", + "You must register to use this functionality": "Du musst dich registrieren um diese Funktionalität zu nutzen", + "Resend all or cancel all now. You can also select individual messages to resend or cancel.": "Sende erneut oder breche alles ab. Du kannst auch auch individuelle Nachrichten erneut senden or abbrechen.", + "Create new room": "Erstelle neuen Raum", + "Welcome page": "Willkommensseite", + "Room directory": "Raum-Verzeichnis", + "Start chat": "Starte Chat", + "New Password": "Neues Passwort", + "Start chatting": "Starte plaudern", + "Start Chatting": "Starte Gespräche", + "Click on the button below to start chatting!": "Klicke den Button unten um das Plaudern zu beginnen!", + "Create a new chat or reuse an existing one": "Erstelle einen neuen Chat oder nutze einen existierenden", + "You already have existing direct chats with this user:": "Du hast bereits direkte Chats mit diesem Nutzer:", + "Username available": "Nutzername verfügbar", + "Username not available": "Nutzername nicht verfügbar", + "Something went wrong!": "Etwas ging schief!", + "This will be your account name on the homeserver, or you can pick a different server.": "Dies wird dein Konto-Name auf dem Heimserver, oder du kannst einen anderen Server auswählen.", + "If you already have a Matrix account you can log in instead.": "Wenn du bereits ein Matrix-Konto hast, kannst du ansonsten auch anmelden." } diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index bf05173356..ecbb20fbb7 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -826,5 +826,8 @@ "You have disabled URL previews by default.": "Vous avez désactivé les aperçus d’URL par défaut.", "You have enabled URL previews by default.": "Vous avez activé les aperçus d’URL par défaut.", "You have entered an invalid contact. Try using their Matrix ID or email address.": "Vous avez entré un contact invalide. Essayez d’utiliser leur identifiant Matrix ou leur adresse email.", - "Hide removed messages": "Cacher les messages supprimés" + "Hide removed messages": "Cacher les messages supprimés", + "Add": "Ajouter", + "%(count)s new messages.one": "%(count)s nouveau message", + "%(count)s new messages.other": "%(count)s nouveaux messages" } diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index 5430737f15..748810f76e 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -1,3 +1,28 @@ { - "Cancel": "Mégse" + "Cancel": "Mégse", + "Search": "Keresés", + "OK": "Rendben", + "Custom Server Options": "Egyedi szerver beállítások", + "Direct Chat": "Közvetlen csevegés", + "Dismiss": "Eltűntet", + "Drop here %(toAction)s": "%(toAction)s -t húzd ide", + "Error": "Hiba", + "Failed to forget room %(errCode)s": "Nem lehet eltávolítani a szobát: %(errCode)s", + "Failed to join the room": "Nem lehet csatlakozni a szobához", + "Favourite": "Kedvenc", + "Mute": "Elnémít", + "Notifications": "Értesítések", + "Operation failed": "Művelet sikertelen", + "Please Register": "Regisztrálj", + "powered by Matrix": "Matrixon alapul", + "Remove": "Töröl", + "Settings": "Beállítások", + "unknown error code": "ismeretlen hiba kód", + "Sunday": "Vasárnap", + "Monday": "Hétfő", + "Tuesday": "Kedd", + "Wednesday": "Szerda", + "Thursday": "Csütörtök", + "Friday": "Péntek", + "Saturday": "Szombat" } diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json index 58f8e0291c..85b7a6a7f5 100644 --- a/src/i18n/strings/ru.json +++ b/src/i18n/strings/ru.json @@ -84,11 +84,11 @@ "Failed to upload file": "Не удалось закачать файл", "Favourite": "Избранное", "favourite": "фаворит", - "Favourites": "Фавориты", + "Favourites": "Избранное", "Filter room members": "Фильтр участников комнаты", "Forget room": "Забыть комнату", "Forgot your password?": "Вы забыли пароль?", - "For security, this session has been signed out. Please sign in again.": "Для обеспечения безопасности, эта сессия была подписана. Войдите в систему еще раз.", + "For security, this session has been signed out. Please sign in again.": "Для обеспечения безопасности эта сессия была завершена. Войдите в систему еще раз.", "Found a bug?": "Нашли ошибку?", "had": "имеет", "Hangup": "Отключение", @@ -153,7 +153,7 @@ "requested a VoIP conference": "requested a VoIP conference", "Return to login screen": "Return to login screen", "Send Reset Email": "Send Reset Email", - "sent an image": "sent an image", + "sent an image": "отправил изображение", "sent an invitation to": "sent an invitation to", "set a profile picture": "set a profile picture", "set their display name to": "set their display name to", @@ -228,7 +228,7 @@ "%(senderName)s banned %(targetName)s.": "%(senderName)s запрещенный %(targetName)s.", "Call Timeout": "Время ожидания вызова", "%(senderName)s changed their display name from %(oldDisplayName)s to %(displayName)s.": "%(senderName)s их имя измененное с %(oldDisplayName)s на %(displayName)s.", - "%(senderName)s changed their profile picture.": "%(senderName)s измененное ихнее фото профиля.", + "%(senderName)s changed their profile picture.": "%(senderName)s изменил фото профиля.", "%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s уровень мощности изменен на %(powerLevelDiffText)s.", "%(senderDisplayName)s changed the room name to %(roomName)s.": "%(senderDisplayName)s имя комнаты измененно на %(roomName)s.", "%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s измененная тема на %(topic)s.", @@ -254,7 +254,7 @@ "%(targetName)s joined the room.": "%(targetName)s вошёл в комнату.", "%(senderName)s kicked %(targetName)s.": "%(senderName)s выкинул %(targetName)s.", "%(targetName)s left the room.": "%(targetName)s покинул комнату.", - "%(senderName)s made future room history visible to": "%(senderName)s история сделаной будущей комнаты, видимая для", + "%(senderName)s made future room history visible to": "%(senderName)s сделал видимой для всех будущую историю комнаты", "Missing room_id in request": "Отсутствует room_id в запросе", "Missing user_id in request": "Отсутствует user_id в запросе", "Must be viewing a room": "Комната должна быть посищена", @@ -356,8 +356,8 @@ "Saturday": "Суббота", "Sunday": "Воскресенье", "%(weekDayName)s %(time)s": "%(weekDayName)s %(time)s", - "Upload an avatar:": "Загрузить аватар", - "You need to be logged in.": "Вы должны быть зарегистрированы", + "Upload an avatar:": "Загрузите аватар:", + "You need to be logged in.": "Вы должны быть зарегистрированы.", "You need to be able to invite users to do that.": "Вам необходимо пригласить пользователей чтобы сделать это.", "You cannot place VoIP calls in this browser.": "Вы не можете сделать вызовы VoIP с этим браузером.", "You are already in a call.": "Вы уже находитесь в разговоре.", @@ -377,7 +377,7 @@ "Oct": "Окт.", "Nov": "Ноя.", "Dec": "Дек.", - "%(weekDayName)s, %(monthName)s %(day)s %(time)s": "%(weekDayName)s, %(monthName)s %(day)s %(time)s", + "%(weekDayName)s, %(monthName)s %(day)s %(time)s": "%(weekDayName)s, %(day)s %(monthName)s %(time)s", "Mon": "Пн", "Sun": "Вс", "Tue": "Вт", @@ -388,8 +388,8 @@ "You need to log back in to generate end-to-end encryption keys for this device and submit the public key to your homeserver. This is a once off; sorry for the inconvenience.": "Вам необходимо снова войти в генерировать сквозное шифрование (е2е) ключей для этого устройства и предоставить публичный ключ Вашему домашнему серверу. Это после выключения; приносим извинения за причиненные неудобства.", "Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "Ваш адрес электронной почты, кажется, не связан с Matrix ID на этом Homeserver.", "to start a chat with someone": "Начать чат с кем-то", - "to tag direct chat": "отометить прямой чат", - "To use it, just wait for autocomplete results to load and tab through them.": "Для его использования, просто подождите результатов автозаполнения для загрузки на вкладке и через них.", + "to tag direct chat": "отметить прямой чат", + "To use it, just wait for autocomplete results to load and tab through them.": "Для его использования просто подождите загрузки результатов автозаполнения и нажимайте Tab для навигации.", "%(senderName)s turned on end-to-end encryption (algorithm %(algorithm)s).": "%(senderName)s включил сквозное шифрование (algorithm %(algorithm)s).", "Unable to restore previous session": "Невозможно востановить предыдущий сеанс", "%(senderName)s unbanned %(targetName)s.": "%(senderName)s запрет отменен %(targetName)s.", @@ -478,7 +478,7 @@ "Always show message timestamps": "Всегда показывать время сообщения", "Authentication": "Авторизация", "olm version:": "версия olm:", - "%(items)s and %(remaining)s others": "%(items)s и %(remaining)s другие", + "%(items)s and %(remaining)s others": "%(items)s и другие %(remaining)s", "%(items)s and one other": "%(items)s и ещё один", "%(items)s and %(lastItem)s": "%(items)s и %(lastItem)s", "and one other...": "и ещё один...", @@ -491,7 +491,7 @@ "Current password": "Текущий пароль", "Email": "Электронная почта", "Failed to kick": "Не удалось выгнать", - "Failed to load timeline position": "Не удалось узнать место во времени", + "Failed to load timeline position": "Не удалось загрузить позицию таймлайна", "Failed to mute user": "Не удалось заглушить", "Failed to reject invite": "Не удалось отклонить приглашение", "Failed to save settings": "Не удалось сохранить настройки", @@ -519,7 +519,7 @@ "New passwords don't match": "Пароли не совпадают", "not set": "не установлено", "not specified": "не указано", - "No devices with registered encryption keys": "Нет устройств с записанными ключами шифрования", + "No devices with registered encryption keys": "Нет устройств с зарегистрированными ключами шифрования", "No more results": "Нет больше результатов", "No results": "Нет результатов", "OK": "ОК", @@ -535,7 +535,7 @@ "rejected": "отклонено", "%(targetName)s rejected the invitation.": "%(targetName)s отклонил приглашение.", "Reject invitation": "Отклонить приглашение", - "Remove Contact Information?": "Убрать контактную информацию?", + "Remove Contact Information?": "Удалить контактную информацию?", "%(senderName)s removed their display name (%(oldDisplayName)s).": "%(senderName)s убрал своё отображаемое имя (%(oldDisplayName)s).", "%(senderName)s removed their profile picture.": "%(senderName)s убрал своё изображение.", "%(senderName)s requested a VoIP conference.": "%(senderName)s запросил голосовую конференц-связь.", @@ -585,7 +585,7 @@ "This room is not recognised.": "Эта комната не опознана.", "These are experimental features that may break in unexpected ways": "Это экспериментальные функции, которые могут неожиданным образом вызывать ошибки", "This doesn't appear to be a valid email address": "Не похоже, что это правильный адрес электронной почты", - "This is a preview of this room. Room interactions have been disabled": "Это просмотр данной комнаты. Взаимодействия с ней были отключены.", + "This is a preview of this room. Room interactions have been disabled": "Это просмотр данной комнаты. Взаимодействия с ней были отключены", "This phone number is already in use": "Этот телефонный номер уже используется", "This room's internal ID is": "Внутренний ID этой комнаты", "times": "раз", @@ -598,7 +598,7 @@ "Unknown room %(roomId)s": "Неизвестная комната %(roomId)s", "You have been invited to join this room by %(inviterName)s": "Вы были приглашены войти в эту комнату от %(inviterName)s", "You seem to be uploading files, are you sure you want to quit?": "Похоже вы передаёте файлы, вы уверены, что хотите выйти?", - "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s", + "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s, %(day)s %(monthName)s %(fullYear)s %(time)s", "Make Moderator": "Сделать модератором", "Room": "Комната", "Cancel": "Отмена", @@ -606,7 +606,7 @@ "italic": "наклонный", "strike": "перечёркнутый", "underline": "подчёркнутый", - "code": "текст", + "code": "код", "quote": "цитата", "bullet": "пункт", "numbullet": "нумерация", @@ -672,5 +672,197 @@ "Logged in as:": "Зарегестрирован как:", "Default Device": "Стандартное устройство", "No Webcams detected": "Веб-камера не обнаружена", - "VoIP": "VoIP" + "VoIP": "VoIP", + "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.": "Для обеспечения безопасности выход из системы удалит все ключи шифроования из этого браузера. Если вы хотите иметь возможность расшифровать переписку в будущем - вы должны экспортирвать ключи вручную.", + "Guest access is disabled on this Home Server.": "Гостевой доступ отключен на этом сервере.", + "Guests can't set avatars. Please register.": "Гости не могут устанавливать аватар. Пожалуйста, зарегистрируйтесь.", + "Guests can't use labs features. Please register.": "Гости не могут использовать экспериментальные возможности. Пожалуйста, зарегистрируйтесь.", + "Guests cannot join this room even if explicitly invited.": "Гости не могут заходить в эту комнату если не были приглашены.", + "Missing Media Permissions, click here to request.": "Отсутствуют разрешения, нажмите для запроса.", + "No media permissions": "Нет медиа разрешений", + "You may need to manually permit Riot to access your microphone/webcam": "Вам необходимо предоставить Riot доступ к микрофону или веб-камере вручную", + "Anyone": "Все", + "Are you sure you want to leave the room '%(roomName)s'?": "Вы уверены, что хотите покинуть '%(roomName)s'?", + "%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s удалил имя комнаты.", + "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.": "Смена пароля также сбросит все ключи шифрования на всех устройствах, сделав зашифрованную историю недоступной, если только вы сначала не экспортируете ключи шифрования и не импортируете их потом. В будущем это будет исправлено.", + "Custom level": "Пользовательский уровень", + "(default: %(userName)s)": "(по-умолчанию: %(userName)s)", + "Device already verified!": "Устройство уже верифицировано!", + "Device ID:": "ID устройства:", + "device id: ": "id устройства: ", + "Device key:": "Ключ устройства:", + "disabled": "отключено", + "Disable markdown formatting": "Отключить форматирование Markdown", + "Email address": "Адрес email", + "Email address (optional)": "Адрем email (не обязательно)", + "enabled": "включено", + "Error decrypting attachment": "Ошибка расшифровки файла", + "Export": "Экспорт", + "Failed to register as guest:": "Ошибка регистрации как гостя:", + "Failed to set avatar.": "Не удалось установить аватар.", + "Import": "Импорт", + "Incorrect username and/or password.": "Неверное имя пользователя и/или пароль.", + "Invalid file%(extra)s": "Неправильный файл%(extra)s", + "Invited": "Приглашен", + "Jump to first unread message.": "Перейти к первому непрочитанному сообщению.", + "List this room in %(domain)s's room directory?": "Показывать эту комнату в списке комнат %(domain)s?", + "Message not sent due to unknown devices being present": "Сообщение не было отправлено из-за присутствия неизвестного устройства", + "Mobile phone number (optional)": "Номер мобильного телефона (не обязательно)", + "Once you've followed the link it contains, click below": "Как только вы пройдете по ссылке, нажмите на кнопку ниже", + "Password:": "Пароль:", + "Privacy warning": "Предупреждение приватности", + "Privileged Users": "Привилегированные пользователи", + "Revoke Moderator": "Снять модераторские права", + "Refer a friend to Riot:": "Расскажите другу о Riot:", + "Register": "Регистрация", + "Remote addresses for this room:": "Удаленные адреса для этой комнаты:", + "Remove %(threePid)s?": "Удалить %(threePid)s?", + "Results from DuckDuckGo": "Результаты от DuckDuckGo", + "Save": "Сохранить", + "Searches DuckDuckGo for results": "Ищет результаты через DuckDuckGo", + "Server error": "Ошибка сервера", + "Server may be unavailable or overloaded": "Сервер может быть недоступен или перегружен", + "Server may be unavailable, overloaded, or search timed out :(": "Сервер может быть недоступен, перегружен или поиск прекращен по тайм-ауту :(", + "Server may be unavailable, overloaded, or the file too big": "Сервер может быть недоступен, перегружен или размер файла слишком большой", + "Server may be unavailable, overloaded, or you hit a bug.": "Сервер может быть недоступен, перегружен или вы нашли баг.", + "Server unavailable, overloaded, or something else went wrong.": "Сервер может быть недоступен, перегружен или произошло что-то страшное.", + "Session ID": "ID сессии", + "%(senderName)s set a profile picture.": "%(senderName)s установил картинку профиля.", + "%(senderName)s set their display name to %(displayName)s.": "%(senderName)s установил отображаемое имя %(displayName)s.", + "Setting a user name will create a fresh account": "Установка имени пользователя создаст новую учетную запись", + "Signed Out": "Вышли", + "Sorry, this homeserver is using a login which is not recognised ": "Извините, этот Home Server использует логин, который не удалось распознать ", + "Tagged as: ": "Теги: ", + "The signing key you provided matches the signing key you received from %(userId)s's device %(deviceId)s. Device marked as verified.": "Ключ, предоставленный вами, совпадает с ключем, полученным от устройства %(userId)s с ID %(deviceId)s. Устройство помечено как верифицированное.", + "%(actionVerb)s this person?": "%(actionVerb)s этого пользователя?", + "The file '%(fileName)s' exceeds this home server's size limit for uploads": "Файл '%(fileName)s' превышает ограничение размера загрузок на этом Home Server'е", + "This Home Server does not support login using email address.": "Этот Home Server не поддерживает вход по адресу email.", + "There was a problem logging in.": "Возникла проблема входа в учетную запись.", + "The visibility of existing history will be unchanged": "Видимость текущей истории не будет изменена", + "this invitation?": "это приглашение?", + "This room is not accessible by remote Matrix servers": "Это комната закрыта для других серверов Matrix", + "To ban users": "Забанить пользователей", + "to browse the directory": "просматривать директорию", + "To configure the room": "Конфигурировать комнату", + "To invite users into the room": "Приглашать пользователей в комнату", + "to join the discussion": "присоединиться к дискуссии", + "To kick users": "Выгонять пользователей", + "To link to a room it must have": "Для создания ссылки на комнату она должна иметь", + "to make a room or": "создать комнату или", + "To remove other users' messages": "Удалять сообщения других пользователей", + "To reset your password, enter the email address linked to your account": "Чтобы сбросить ваш пароль введите адрес email, который используется аккаунтом", + "to tag as %(tagName)s": "отметить как %(tagName)s", + "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 was unable to find it": "Вы попытались загрузить указанное сообщение в комнате, однако сервер не смог его найти", + "Unable to load device list": "Невозможно загрузить список устройств", + "Unknown (user, device) pair:": "Неизвестная пара пользователь-устройство:", + "Unmute": "Разглушить", + "Unrecognised command:": "Неизвестная команда:", + "Unrecognised room alias:": "Неизвестный псевдоним комнаты:", + "Verified key": "Верифицированный ключ", + "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!": "ВНИМАНИЕ: ОШИБКА ВЕРИФИКАЦИИ КЛЮЧЕЙ! Ключ для подписки устройства %(deviceId)s пользователя %(userId)s: \"%(fprint)s\", однако он не совпадает с предоставленным ключем \"%(fingerprint)s\". Это может означать перехват вашего канала коммуникации!", + "You have disabled URL previews by default.": "Предпросмотр ссылок отключен по-умолчанию.", + "You have enabled URL previews by default.": "Предпросмотр ссылок включен по-умолчанию.", + "You have entered an invalid contact. Try using their Matrix ID or email address.": "Вы ввели неправильный адрес. Попробуйте использовать Matrix ID или адрес email.", + "You need to enter a user name.": "Необходимо ввести имя пользователя.", + "You seem to be in a call, are you sure you want to quit?": "Вы учавствуете в звонке, вы уверены, что хотите выйти?", + "You will not be able to undo this change as you are promoting the user to have the same power level as yourself": "Вы не сможете отменить это действие так как даете пользователю такой же уровень доступа как и у вас", + "Set a Display Name": "Установить отображаемое имя", + "(~%(searchCount)s results)": "(~%(searchCount)s результатов)", + "%(severalUsers)shad their invitations withdrawn %(repeats)s times": "%(severalUsers)s отозвали свои приглашения %(repeats)s раз", + "%(oneUser)shad their invitation withdrawn %(repeats)s times": "%(oneUser)s отозвал свои приглашения %(repeats)s раз", + "%(severalUsers)shad their invitations withdrawn": "%(severalUsers)s отозвали свои приглашения", + "%(oneUser)shad their invitation withdrawn": "%(oneUser)s отозвал свое приглашение", + "Please select the destination room for this message": "Выберите комнату назначения для этого сообщения", + "Options": "Настройки", + "Passphrases must match": "Пароли должны совпадать", + "Passphrase must not be empty": "Пароль не должен быть пустым", + "Export room keys": "Экспортировать ключи", + "Enter passphrase": "Введите пароль", + "Confirm passphrase": "Подтвердите пароль", + "Import room keys": "Импортировать ключи", + "File to import": "Файл для импорта", + "This process allows you to export the keys for messages you have received in encrypted rooms to a local file. You will then be able to import the file into another Matrix client in the future, so that client will also be able to decrypt these messages.": "Этот процесс позволяет вам экспортировать ключи для сообщений, которые вы получили в комнатах с шифрованием, в локальный файл. Вы сможете импортировать эти ключи в другой клиент Matrix чтобы расшифровать эти сообщения.", + "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.": "Экспортированный файл позволит любому пользователю расшифровать и зашифровать сообщения, которые вы видите, поэтому вы должны быть крайне осторожны и держать файл в надежном месте. Чтобы поспособствовать этому вы должны ввести пароль, который будет использоваться для шифрования ключей. Вы сможете импоортировать ключи только зная этот пароль.", + "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.": "Этот процесс позволяем вам импортировать ключи шифрования, которые вы экспортировали ранее из клиента Matrix. После импорта вы сможете читать зашифрованную переписку и отправлять шифрованные сообщения.", + "The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.": "Экспортированный файл защищен паролем. Вы должны ввести этот пароль для расшифровки.", + "You must join the room to see its files": "Вы должны зайти в комнату для просмотра файлов", + "Reject all %(invitedRooms)s invites": "Отклонить все %(invitedRooms)s приглашения", + "Start new chat": "Начать новый чат", + "Guest users can't invite users. Please register.": "Гости не могут приглашать пользователей. Пожалуйста, зарегистрируйтесь.", + "Failed to invite": "Ошибка приглашения", + "Failed to invite user": "Ошибка приглашения пользователя", + "Failed to invite the following users to the %(roomName)s room:": "Ошибка приглашения следующих пользователей в %(roomName)s:", + "Confirm Removal": "Подтвердите удаление", + "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.": "Вы уверены, что хотите удалить этот эвент? Обратите внимание, что если это смена имени комнаты или топика, то удаление отменит это изменение.", + "Unknown error": "Неизвестная ошибка", + "Incorrect password": "Неправильный пароль", + "This will make your account permanently unusable. You will not be able to re-register the same user ID.": "Это сделает вашу учетную запись нерабочей. Вы не сможете зарегистрироваться снова с тем же ID.", + "This action is irreversible.": "Это действие необратимо.", + "To continue, please enter your password.": "Для продолжения введите ваш пароль.", + "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:": "Для верификации устройства, пожалуйста, свяжитесь с владельцем используя другие методы коммуникации (например, лично или по телефону) и попросите его подтвердить, что он видит такой же ключ как написанный ниже:", + "Device name": "Имя устройства", + "Device key": "Ключ устройства", + "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.": "Если совпадают, то нажмите кнопку верификации ниже. Если нет, то кто-то перехватил это устройство или ключ и вы, скорее всего, захотите внести его в черный список.", + "In future this verification process will be more sophisticated.": "В будущем процесс верификации будет усложнен.", + "Verify device": "Верифицировать устройство", + "I verify that the keys match": "Я верифицирую - ключи совпадают", + "We encountered an error trying to restore your previous session. If you continue, you will need to log in again, and encrypted chat history will be unreadable.": "Обнаружена ошибка при восстановлении вашей предыдущей сессии. Вам необходимо зайти снова, шифрованные сообщения будут нечитаемы.", + "Unable to restore session": "Невозможно восстановить сессию", + "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.": "Если вы использовали более новую версию Riot, то ваша сессия может быть несовместима с текущей. Закройте это окно и вернитесь к использованию более новой версии.", + "Continue anyway": "Все равно продолжить", + "Your display name is how you'll appear to others when you speak in rooms. What would you like it to be?": "Отображаемое имя - это то, как вы отображаетесь в чате. Какое имя вы хотите?", + "You are currently blacklisting unverified devices; to send messages to these devices you must verify them.": "Пока что вы вносите неверифицированные устройства в черный список автоматически. Для отправки сообщений на эти устройства вам необходимо их верифицировать.", + "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.": "Рекомендуется сначала верифицировать устройства для подтверждения их владения правильным пользователем, но вы можете отправить сообщение без верификации, если хотите.", + "\"%(RoomName)s\" contains devices that you haven't seen before.": "\"%(RoomName)s\" содержит неизвестные прежде устройства.", + "Unknown Address": "Неизвестный адрес", + "Unblacklist": "Удалить из черного списка", + "Blacklist": "Добавить в черный список", + "Unverify": "Убрать верификацию", + "Verify...": "Верифицировать...", + "ex. @bob:example.com": "например @bob:example.com", + "Add User": "Добавить пользователя", + "This Home Server would like to make sure you are not a robot": "Этот Home Server хочет удостовериться что вы не робот", + "Sign in with CAS": "Войти с помощью CAS", + "You can use the custom server options to sign into other Matrix servers by specifying a different Home server URL.": "Вы можете использовать пользовательские настройки сервера для использования другого Home Server'а при помощи указания его URL.", + "This allows you to use this app with an existing Matrix account on a different home server.": "Это позволяет использовать это приложение с существующей учетной записью на другом Home Server'е.", + "You can also set a custom identity server but this will typically prevent interaction with users based on email address.": "Вы также можете указать пользовательский сервер идентификации, но это обычно ломает возможность общаться с пользователями с помощью адреса email.", + "Please check your email to continue registration.": "Проверьте свою почту для продолжения регистрации.", + "Token incorrect": "Неправильный токен", + "A text message has been sent to": "Текстовое сообщение было отправлено", + "Please enter the code it contains:": "Введите содержащийся код:", + "If you don't specify an email address, you won't be able to reset your password. Are you sure?": "Если вы не укажете адрес email, то вы не сможете сбросить свой пароль в будущем. Вы уверены?", + "You are registering with %(SelectedTeamName)s": "Вы регистрируетесь на %(SelectedTeamName)s", + "Default server": "Сервер по-умолчанию", + "Custom server": "Пользовательский сервер", + "Home server URL": "URL Home Server'а", + "Identity server URL": "URL сервера идентификации", + "What does this mean?": "Что это значит?", + "Error decrypting audio": "Ошибка расшифровки аудио", + "Error decrypting image": "Ошибка расшифровки изображения", + "Image '%(Body)s' cannot be displayed.": "Изображение '%(Body)s' не может быть отображено.", + "This image cannot be displayed.": "Это изображение не может быть отображено.", + "Error decrypting video": "Ошибка расшифровки видео", + "Add an Integration": "Добавить интеграцию", + "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?": "Вы будете перенаправлены на внешний сайт, где вы сможете аутентифицировать свою учетную запись для использования с %(integrationsUrl)s. Вы хотите продолжить?", + "Removed or unknown message type": "Удаленный или неизвестный тип сообщения", + "Disable URL previews by default for participants in this room": "Отключить предпросмотр URL для участников этой комнаты по-умолчанию", + "URL previews are %(globalDisableUrlPreview)s by default for participants in this room.": "Предпросмотр URL %(globalDisableUrlPreview)s по-умолчанию для участников этой комнаты.", + "URL Previews": "Предпросмотр URL", + "Enable URL previews for this room (affects only you)": "Включить предпросмотр URL в этой комнате (только для вас)", + "Drop file here to upload": "Перетащите файл сюда для загрузки", + " (unsupported)": " (не поддерживается)", + "Ongoing conference call%(supportedText)s. %(joinText)s": "Идет конференц-звонок%(supportedText)s. %(joinText)s", + "for %(amount)ss": "для %(amount)s", + "for %(amount)sm": "для %(amount)s", + "for %(amount)sh": "для %(amount)s", + "for %(amount)sd": "для %(amount)s", + "Online": "В сети", + "Idle": "Отошел", + "Offline": "Не в сети", + "Disable URL previews for this room (affects only you)": "Отключить предпросмотр URL в этой комнате (только для вас)", + "$senderDisplayName changed the room avatar to ": "$senderDisplayName сменил аватар комнаты на ", + "%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s удалил аватар комнаты.", + "%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s сменил аватар для %(roomName)s" } diff --git a/src/i18n/strings/th.json b/src/i18n/strings/th.json index 5bafdc97cd..6608219faa 100644 --- a/src/i18n/strings/th.json +++ b/src/i18n/strings/th.json @@ -25,7 +25,7 @@ "(default: %(userName)s)": "(ค่าเริ่มต้น: %(userName)s)", "Default Device": "อุปกรณ์เริ่มต้น", "%(senderName)s banned %(targetName)s.": "%(senderName)s แบน %(targetName)s แล้ว", - "%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s เปลี่ยนหัวข้อเป็น \"%(topic)s\" แล้ว", + "%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s เปลี่ยนหัวข้อเป็น \"%(topic)s\"", "Decrypt %(text)s": "ถอดรหัส %(text)s", "Device ID": "ID อุปกรณ์", "Device ID:": "ID อุปกรณ์:", @@ -96,8 +96,8 @@ "all room members, from the point they are invited": "สมาชิกทั้งหมด นับตั้งแต่เมื่อได้รับคำเชิญ", "all room members, from the point they joined": "สมาชิกทั้งหมด นับตั้งแต่เมื่อเข้าร่วมห้อง", "an address": "ที่อยู่", - "%(items)s and %(remaining)s others": "%(items)s และอีก %(remaining)s อย่าง", - "%(items)s and one other": "%(items)s และอีกหนึ่งอย่าง", + "%(items)s and %(remaining)s others": "%(items)s และอีก %(remaining)s ผู้ใช้", + "%(items)s and one other": "%(items)s และอีกหนึ่งผู้ใช้", "%(items)s and %(lastItem)s": "%(items)s และ %(lastItem)s", "and %(overflowCount)s others...": "และอีก %(overflowCount)s ผู้ใช้...", "and one other...": "และอีกหนึ่งผู้ใช้...", @@ -322,5 +322,13 @@ "This is a preview of this room. Room interactions have been disabled": "นี่คือตัวอย่างของห้อง การตอบสนองภายในห้องถูกปิดใช้งาน", "This phone number is already in use": "หมายเลขโทรศัพท์นี้ถูกใช้งานแล้ว", "This room's internal ID is": "ID ภายในของห้องนี้คือ", - "times": "เวลา" + "times": "เวลา", + "%(oneUser)schanged their name": "%(oneUser)sเปลี่ยนชื่อของเขาแล้ว", + "%(severalUsers)schanged their name %(repeats)s times": "%(severalUsers)sเปลี่ยนชื่อของพวกเขา %(repeats)s ครั้ง", + "%(oneUser)schanged their name %(repeats)s times": "%(oneUser)sเปลี่ยนชื่อของเขา %(repeats)s ครั้ง", + "%(severalUsers)schanged their name": "%(severalUsers)sเปลี่ยนชื่อของพวกเขาแล้ว", + "Create new room": "สร้างห้องใหม่", + "Room directory": "ไดเรกทอรีห้อง", + "Start chat": "เริ่มแชท", + "Welcome page": "หน้าต้อนรับ" } From 566fb2caebc604c90c3073a97482fa6873043e7b Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 7 Jun 2017 22:41:02 +0100 Subject: [PATCH 125/127] Add some logging around switching rooms ... which I wish had been there when I was staring at a rageshake. --- src/components/structures/MatrixChat.js | 11 +++++++++++ src/components/structures/TimelinePanel.js | 3 +++ 2 files changed, 14 insertions(+) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index efb2b38d6e..d11e100f26 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -644,6 +644,17 @@ module.exports = React.createClass({ } } + if (roomInfo.room_alias) { + console.log( + `Switching to room alias ${roomInfo.room_alias} at event ` + + newState.initialEventId, + ); + } else { + console.log(`Switching to room id ${roomInfo.room_id} at event ` + + newState.initialEventId, + ); + } + // Wait for the first sync to complete so that if a room does have an alias, // it would have been retrieved. let waitFor = q(null); diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index f0e6810b6b..928e2405aa 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -902,6 +902,9 @@ var TimelinePanel = React.createClass({ var onError = (error) => { this.setState({timelineLoading: false}); + console.error( + `Error loading timeline panel at ${eventId}: ${error}`, + ); var msg = error.message ? error.message : JSON.stringify(error); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); From bc0eb4294c7f4c45754ef02cc19d1444743653a4 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 8 Jun 2017 10:27:35 +0100 Subject: [PATCH 126/127] rename welcome page to home --- src/components/views/elements/HomeButton.js | 2 +- src/i18n/strings/en_EN.json | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/elements/HomeButton.js b/src/components/views/elements/HomeButton.js index 80c64c6a60..4877f5dd43 100644 --- a/src/components/views/elements/HomeButton.js +++ b/src/components/views/elements/HomeButton.js @@ -23,7 +23,7 @@ const HomeButton = function(props) { const ActionButton = sdk.getComponent('elements.ActionButton'); return ( Date: Thu, 8 Jun 2017 13:05:02 +0200 Subject: [PATCH 127/127] Update from Weblate. (#1053) * Added translation using Weblate (Thai) * Translated using Weblate (Thai) Currently translated at 2.6% (22 of 827 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.nordgedanken.de/projects/riot-web/matrix-react-sdk/th/ * Translated using Weblate (Thai) Currently translated at 8.9% (74 of 827 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.nordgedanken.de/projects/riot-web/matrix-react-sdk/th/ * Translated using Weblate (German) Currently translated at 99.8% (826 of 827 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.nordgedanken.de/projects/riot-web/matrix-react-sdk/de/ * Translated using Weblate (Swedish) Currently translated at 35.4% (293 of 827 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.nordgedanken.de/projects/riot-web/matrix-react-sdk/sv/ * Translated using Weblate (Thai) Currently translated at 9.7% (81 of 827 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.nordgedanken.de/projects/riot-web/matrix-react-sdk/th/ * Translated using Weblate (German) Currently translated at 100.0% (827 of 827 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.nordgedanken.de/projects/riot-web/matrix-react-sdk/de/ * Translated using Weblate (Russian) Currently translated at 76.9% (636 of 827 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.nordgedanken.de/projects/riot-web/matrix-react-sdk/ru/ * Translated using Weblate (Hungarian) Currently translated at 0.1% (1 of 827 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.nordgedanken.de/projects/riot-web/matrix-react-sdk/hu/ * Translated using Weblate (Thai) Currently translated at 39.2% (325 of 827 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.nordgedanken.de/projects/riot-web/matrix-react-sdk/th/ * Translated using Weblate (German) Currently translated at 99.8% (827 of 828 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.nordgedanken.de/projects/riot-web/matrix-react-sdk/de/ * Translated using Weblate (German) Currently translated at 99.8% (827 of 828 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.nordgedanken.de/projects/riot-web/matrix-react-sdk/de/ * Translated using Weblate (Thai) Currently translated at 39.7% (329 of 828 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.nordgedanken.de/projects/riot-web/matrix-react-sdk/th/ * Translated using Weblate (German) Currently translated at 99.8% (828 of 828 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.nordgedanken.de/projects/riot-web/matrix-react-sdk/de/ * Translated using Weblate (Hungarian) Currently translated at 3.1% (26 of 828 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.nordgedanken.de/projects/riot-web/matrix-react-sdk/hu/ * Translated using Weblate (Russian) Currently translated at 77.4% (641 of 828 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.nordgedanken.de/projects/riot-web/matrix-react-sdk/ru/ * Translated using Weblate (Thai) Currently translated at 39.7% (329 of 828 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.nordgedanken.de/projects/riot-web/matrix-react-sdk/th/ * Translated using Weblate (Russian) Currently translated at 100.0% (828 of 828 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.nordgedanken.de/projects/riot-web/matrix-react-sdk/ru/ * Translated using Weblate (German) Currently translated at 100.0% (828 of 828 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.nordgedanken.de/projects/riot-web/matrix-react-sdk/de/ * Translated using Weblate (Russian) Currently translated at 100.0% (828 of 828 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.nordgedanken.de/projects/riot-web/matrix-react-sdk/ru/ * Translated using Weblate (German) Currently translated at 100.0% (850 of 850 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.nordgedanken.de/projects/riot-web/matrix-react-sdk/de/ * Translated using Weblate (French) Currently translated at 96.5% (821 of 850 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.nordgedanken.de/projects/riot-web/matrix-react-sdk/fr/ * Translated using Weblate (Thai) Currently translated at 39.0% (332 of 850 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.nordgedanken.de/projects/riot-web/matrix-react-sdk/th/ * Translated using Weblate (French) Currently translated at 97.0% (825 of 850 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.nordgedanken.de/projects/riot-web/matrix-react-sdk/fr/ * Translated using Weblate (German) Currently translated at 100.0% (850 of 850 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.nordgedanken.de/projects/riot-web/matrix-react-sdk/de/ * Translated using Weblate (Thai) Currently translated at 40.8% (347 of 850 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.nordgedanken.de/projects/riot-web/matrix-react-sdk/th/ * Translated using Weblate (French) Currently translated at 100.0% (850 of 850 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.nordgedanken.de/projects/riot-web/matrix-react-sdk/fr/ * Translated using Weblate (German) Currently translated at 100.0% (850 of 850 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.nordgedanken.de/projects/riot-web/matrix-react-sdk/de/ * Translated using Weblate (Thai) Currently translated at 49.5% (421 of 850 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.nordgedanken.de/projects/riot-web/matrix-react-sdk/th/ * Translated using Weblate (Thai) Currently translated at 56.0% (476 of 850 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.nordgedanken.de/projects/riot-web/matrix-react-sdk/th/ * Translated using Weblate (Russian) Currently translated at 100.0% (852 of 852 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.nordgedanken.de/projects/riot-web/matrix-react-sdk/ru/ * Translated using Weblate (German) Currently translated at 99.7% (850 of 852 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.nordgedanken.de/projects/riot-web/matrix-react-sdk/de/ * Translated using Weblate (Thai) Currently translated at 56.2% (479 of 852 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.nordgedanken.de/projects/riot-web/matrix-react-sdk/th/ * Translated using Weblate (German) Currently translated at 99.7% (851 of 853 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.nordgedanken.de/projects/riot-web/matrix-react-sdk/de/ * Translated using Weblate (German) Currently translated at 99.7% (851 of 853 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.nordgedanken.de/projects/riot-web/matrix-react-sdk/de/ * Translated using Weblate (German) Currently translated at 100.0% (853 of 853 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.nordgedanken.de/projects/riot-web/matrix-react-sdk/de/ --- src/i18n/strings/de_DE.json | 62 +++++++------- src/i18n/strings/fr.json | 31 ++++++- src/i18n/strings/ru.json | 46 +++++++++-- src/i18n/strings/th.json | 156 +++++++++++++++++++++++++++++++++++- 4 files changed, 247 insertions(+), 48 deletions(-) diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index f7b3d16ebc..4102d7be56 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -114,7 +114,7 @@ "Failed to change password. Is your password correct?": "Passwortänderung fehlgeschlagen. Ist dein Passwort richtig?", "Failed to forget room": "Vergessen des Raums schlug fehl", "Failed to leave room": "Verlassen des Raums fehlgeschlagen", - "Failed to reject invitation": "Fehler beim Abweisen der Einladung", + "Failed to reject invitation": "Einladung konnte nicht abgelehnt werden", "Failed to set avatar.": "Fehler beim Setzen des Profilbilds.", "Failed to unban": "Entbannen fehlgeschlagen", "Failed to upload file": "Datei-Upload fehlgeschlagen", @@ -127,7 +127,7 @@ "Found a bug?": "Fehler gefunden?", "Guests cannot join this room even if explicitly invited.": "Gäste können diesem Raum nicht beitreten, auch wenn sie explizit eingeladen wurden.", "Guests can't set avatars. Please register.": "Gäste können kein Profilbild setzen. Bitte registrieren.", - "Guest users can't upload files. Please register to upload.": "Gäste können keine Dateien hochladen. Bitte registrieren um hochzuladen.", + "Guest users can't upload files. Please register to upload.": "Gäste können keine Dateien hochladen. Bitte zunächst registrieren.", "had": "hatte", "Hangup": "Auflegen", "Homeserver is": "Der Homeserver ist", @@ -182,7 +182,7 @@ "Once you've followed the link it contains, click below": "Nachdem du dem darin enthaltenen Link gefolgt bist, klicke unten", "rejected the invitation.": "lehnte die Einladung ab.", "Reject invitation": "Einladung ablehnen", - "Remove Contact Information?": "Kontakt-Informationen löschen?", + "Remove Contact Information?": "Kontakt-Informationen entfernen?", "removed their display name": "löschte den eigenen Anzeigenamen", "Remove": "Entfernen", "requested a VoIP conference": "hat eine VoIP-Konferenz angefordert", @@ -227,7 +227,7 @@ "to join the discussion": "um an der Diskussion teilzunehmen", "To kick users": "Um Nutzer zu entfernen", "Admin": "Administrator", - "Server may be unavailable, overloaded, or you hit a bug.": "Server könnte nicht verfügbar oder überlastet sein oder du bist auf einen Fehler gestoßen.", + "Server may be unavailable, overloaded, or you hit a bug.": "Server ist nicht verfügbar, überlastet oder du bist auf einen Fehler gestoßen.", "Could not connect to the integration server": "Konnte keine Verbindung zum Integrations-Server herstellen", "Disable inline URL previews by default": "URL-Vorschau im Chat standardmäßig deaktivieren", "Guests can't use labs features. Please register.": "Gäste können keine Labor-Funktionen nutzen. Bitte registrieren.", @@ -271,7 +271,7 @@ "Who would you like to communicate with?": "Mit wem möchtest du kommunizieren?", "Would you like to": "Möchtest du", "You do not have permission to post to this room": "Du hast keine Berechtigung an diesen Raum etwas zu senden", - "You have been invited to join this room by %(inviterName)s": "Du wurdest von %(inviterName)s in diesen Raum eingeladen", + "You have been invited to join this room by %(inviterName)s": "%(inviterName)s hat dich in diesen Raum eingeladen", "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": "Du wurdest auf allen Geräten abgemeldet und wirst keine Push-Benachrichtigungen mehr erhalten. Um die Benachrichtigungen zu reaktivieren, musst du dich auf jedem Gerät neu anmelden", "you must be a": "nötige Rolle", "Your password has been reset": "Dein Passwort wurde zurückgesetzt", @@ -281,14 +281,14 @@ "Bulk Options": "Bulk-Optionen", "Call Timeout": "Anruf-Timeout", "Conference call failed.": "Konferenzgespräch fehlgeschlagen.", - "Conference calling is in development and may not be reliable.": "Konferenzgespräche sind in Entwicklung und evtl. nicht zuverlässig.", + "Conference calling is in development and may not be reliable.": "Konferenzgespräche befinden sich noch in der Entwicklungsphase und sind möglicherweise nicht zuverlässig nutzbar.", "Conference calls are not supported in encrypted rooms": "Konferenzgespräche werden in verschlüsselten Räumen nicht unterstützt", "Conference calls are not supported in this client": "Konferenzgespräche werden von diesem Client nicht unterstützt", "Existing Call": "Bereits bestehender Anruf", "Failed to set up conference call": "Konferenzgespräch konnte nicht gestartet werden", "Failed to verify email address: make sure you clicked the link in the email": "Verifizierung der E-Mail-Adresse fehlgeschlagen: Bitte stelle sicher, dass du den Link in der E-Mail angeklickt hast", "Failure to create room": "Raumerstellung fehlgeschlagen", - "Guest users can't create new rooms. Please register to create room and start a chat.": "Gäste können keine neuen Räume erstellen. Bitte registrieren um einen Raum zu erstellen und einen Chat zu starten.", + "Guest users can't create new rooms. Please register to create room and start a chat.": "Gastnutzer können keine neuen Räume erstellen. Bitte registriere dich um Räume zu erstellen und Chats zu starten.", "Riot does not have permission to send you notifications - please check your browser settings": "Riot hat keine Berechtigung Benachrichtigungen zu senden - bitte prüfe deine Browser-Einstellungen", "Riot was not given permission to send notifications - please try again": "Riot hat das Recht nicht bekommen Benachrichtigungen zu senden. Bitte erneut probieren", "This email address is already in use": "Diese E-Mail-Adresse wird bereits verwendet", @@ -302,11 +302,11 @@ "Unable to enable Notifications": "Benachrichtigungen konnten nicht aktiviert werden", "Upload Failed": "Upload fehlgeschlagen", "VoIP is unsupported": "VoIP wird nicht unterstützt", - "You are already in a call.": "Du bist bereits bei einem Anruf.", + "You are already in a call.": "Du bist bereits in einem Gespräch.", "You cannot place a call with yourself.": "Du kannst keinen Anruf mit dir selbst starten.", - "You cannot place VoIP calls in this browser.": "Du kannst kein VoIP-Gespräch in diesem Browser starten.", + "You cannot place VoIP calls in this browser.": "Du kannst keine VoIP-Gespräche in diesem Browser starten.", "You need to log back in to generate end-to-end encryption keys for this device and submit the public key to your homeserver. This is a once off; sorry for the inconvenience.": "Du musst dich erneut anmelden, um Ende-zu-Ende-Verschlüsselungs-Schlüssel für dieses Gerät zu generieren und um den öffentlichen Schlüssel auf deinem Homeserver zu hinterlegen. Dies muss nur einmal durchgeführt werden, bitte entschuldige die Unannehmlichkeiten.", - "Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "Deine E-Mail-Adresse scheint nicht mit einer Matrix-ID auf diesem Homeserver verknüpft zu sein.", + "Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "Deine E-Mail-Adresse scheint nicht mit einer Matrix-ID auf diesem Heimserver verbunden zu sein.", "Sun": "So", "Mon": "Mo", "Tue": "Di", @@ -377,7 +377,7 @@ "Power level must be positive integer.": "Berechtigungslevel muss eine positive ganze Zahl sein.", "Reason": "Grund", "%(targetName)s rejected the invitation.": "%(targetName)s hat die Einladung abgelehnt.", - "%(senderName)s removed their display name (%(oldDisplayName)s).": "%(senderName)s löschte den Anzeigenamen (%(oldDisplayName)s).", + "%(senderName)s removed their display name (%(oldDisplayName)s).": "%(senderName)s hat den Anzeigenamen entfernt (%(oldDisplayName)s).", "%(senderName)s removed their profile picture.": "%(senderName)s hat das Profilbild gelöscht.", "%(senderName)s requested a VoIP conference.": "%(senderName)s möchte eine VoIP-Konferenz beginnen.", "Room %(roomId)s not visible": "Raum %(roomId)s ist nicht sichtbar", @@ -418,7 +418,7 @@ "Press": "Drücke", "tag as %(tagName)s": "als %(tagName)s taggen", "to browse the directory": "um das Raum-Verzeichnis zu durchsuchen", - "to demote": "um die Priorität herabzusetzen", + "to demote": "um das Berechtigungslevel herabzusetzen", "to favourite": "zum Favorisieren", "to make a room or": "um einen Raum zu erstellen, oder", "to restore": "zum wiederherstellen", @@ -581,7 +581,6 @@ "Failed to save settings": "Einstellungen konnten nicht gespeichert werden", "Failed to set display name": "Anzeigename konnte nicht gesetzt werden", "Fill screen": "Fülle Bildschirm", - "Guest users can't upload files. Please register to upload.": "Gäste können keine Dateien hochladen. Bitte zunächst registrieren.", "Hide Text Formatting Toolbar": "Verberge Text-Formatierungs-Toolbar", "Incorrect verification code": "Falscher Verifizierungscode", "Invalid alias format": "Ungültiges Alias-Format", @@ -608,16 +607,17 @@ "Server error": "Server-Fehler", "Server may be unavailable, overloaded, or search timed out :(": "Der Server ist entweder nicht verfügbar, überlastet oder die Suche wurde wegen Zeitüberschreitung abgebrochen :(", "Server may be unavailable, overloaded, or the file too big": "Server ist entweder nicht verfügbar, überlastet oder die Datei ist zu groß", - "Server unavailable, overloaded, or something else went wrong.": "Der Server ist entweder nicht verfügbar, überlastet oder es liegt ein anderweitiger Fehler vor.", - "Some of your messages have not been sent.": "Einige deiner Nachrichten wurden noch nicht gesendet.", + "Server unavailable, overloaded, or something else went wrong.": "Server nicht verfügbar, überlastet oder etwas anderes lief falsch.", + "Some of your messages have not been sent.": "Einige deiner Nachrichten wurden nicht gesendet.", "Submit": "Absenden", "The main address for this room is: %(canonical_alias_section)s": "Die Hauptadresse für diesen Raum ist: %(canonical_alias_section)s", "This action cannot be performed by a guest user. Please register to be able to do this": "Diese Aktion kann nicht von einem Gast ausgeführt werden. Bitte registriere dich um dies zu tun", "%(actionVerb)s this person?": "Diese Person %(actionVerb)s?", "This room has no local addresses": "Dieser Raum hat keine lokale Adresse", - "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Versuchte einen spezifischen Punkt in der Raum-Chronik zu laden, aber du hast keine Berechtigung die angeforderte Nachricht anzuzeigen.", - "Tried to load a specific point in this room's timeline, but was unable to find it.": "Der Versuch, einen spezifischen Punkt im Chatverlauf zu laden, ist fehlgeschlagen. Der Punkt konnte nicht gefunden werden.", - "Turn Markdown off": "Markdown abschalten", + "This room is private or inaccessible to guests. You may be able to join if you register": "Dieser Raum ist privat oder für Gäste nicht zugänglich. Du kannst jedoch eventuell beitreten, wenn du dich registrierst", + "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question": "Versuchte einen spezifischen Punkt in der Raum-Chronik zu laden, aber du hast keine Berechtigung die angeforderte Nachricht anzuzeigen", + "Tried to load a specific point in this room's timeline, but was unable to find it": "Der Versuch, einen spezifischen Punkt im Chatverlauf zu laden, ist fehlgeschlagen. Der Punkt konnte nicht gefunden werden", + "Turn Markdown off": "Markdown deaktiveren", "Turn Markdown on": "Markdown einschalten", "Unable to load device list": "Geräteliste konnte nicht geladen werden", "Unknown command": "Unbekannter Befehl", @@ -725,7 +725,7 @@ "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.": "Eine Änderung des Passworts setzt derzeit alle Schlüssel für die E2E-Verschlüsselung auf allen verwendeten Geräten zurück. Bereits verschlüsselte Chat-Inhalte sind somit nur noch lesbar, wenn du zunächst alle Schlüssel exportierst und später wieder importierst. Wir arbeiten an einer Verbesserung dieser momentan noch notwendigen Vorgehensweise.", "Unmute": "Stummschalten aufheben", "Invalid file%(extra)s": "Ungültige Datei%(extra)s", - "Remove %(threePid)s?": "Entferne %(threePid)s?", + "Remove %(threePid)s?": "%(threePid)s entfernen?", "Please select the destination room for this message": "Bitte den Raum auswählen, an den diese Nachricht gesendet werden soll", "%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s hat den Raum-Namen gelöscht.", "Passphrases must match": "Passphrase muss übereinstimmen", @@ -735,7 +735,6 @@ "Confirm passphrase": "Bestätige Passphrase", "The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.": "Die Export-Datei wird mit einer Passphrase geschützt sein. Du solltest die Passphrase hier eingeben um die Datei zu entschlüsseln.", "You must join the room to see its files": "Du musst dem Raum beitreten, um die Raum-Dateien sehen zu können", - "Server may be unavailable, overloaded, or you hit a bug.": "Server ist nicht verfügbar, überlastet oder du bist auf einen Fehler gestoßen.", "Reject all %(invitedRooms)s invites": "Alle %(invitedRooms)s Einladungen ablehnen", "Start new Chat": "Starte neuen Chat", "Guest users can't invite users. Please register.": "Gäste können keine Nutzer einladen. Bitte registrieren.", @@ -785,7 +784,7 @@ "This image cannot be displayed.": "Dieses Bild kann nicht angezeigt werden.", "Error decrypting video": "Video-Entschlüsselung fehlgeschlagen", "Import room keys": "Importiere Raum-Schlüssel", - "File to import": "Datei zum Importieren", + "File to import": "Zu importierende Datei", "Failed to invite the following users to the %(roomName)s room:": "Das Einladen der folgenden Nutzer in den Raum \"%(roomName)s\" ist fehlgeschlagen:", "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.": "Bist du sicher, dass du dieses Ereignis entfernen (löschen) möchtest? Wenn du die Änderung eines Raum-Namens oder eines Raum-Themas löscht, kann dies dazu führen, dass die ursprüngliche Änderung rückgängig gemacht wird.", "This process allows you to export the keys for messages you have received in encrypted rooms to a local file. You will then be able to import the file into another Matrix client in the future, so that client will also be able to decrypt these messages.": "Dieser Prozess erlaubt es dir, die Schlüssel für in verschlüsselten Räumen empfangene Nachrichten in eine lokale Datei zu exportieren. In Zukunft wird es möglich sein, diese Datei in einen anderen Matrix-Client zu importieren, sodass dieser Client ebenfalls diese Nachrichten entschlüsseln kann.", @@ -827,7 +826,7 @@ "Invited": "Eingeladen", "Set a Display Name": "Setze einen Anzeigenamen", "for %(amount)ss": "für %(amount)ss", - "for %(amount)sm": "für %(amount)sm", + "for %(amount)sm": "seit %(amount)smin", "for %(amount)sh": "für %(amount)sh", "for %(amount)sd": "für %(amount)sd", "%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s hat das Raum-Bild entfernt.", @@ -840,17 +839,13 @@ "Default Device": "Standard-Gerät", "Microphone": "Mikrofon", "Camera": "Kamera", - "Conference call failed.": "Konferenzgespräch fehlgeschlagen.", - "Conference calling is in development and may not be reliable.": "Konferenzgespräche befinden sich noch in der Entwicklungsphase und sind möglicherweise nicht zuverlässig nutzbar.", "Device already verified!": "Gerät bereits verifiziert!", "Export": "Export", "Failed to register as guest:": "Registrieren als Gast schlug fehl:", "Guest access is disabled on this Home Server.": "Gastzugang ist auf diesem Heimserver deaktivert.", - "Guest users can't create new rooms. Please register to create room and start a chat.": "Gastnutzer können keine neuen Räume erstellen. Bitte registriere dich um Räume zu erstellen und Chats zu starten.", "Import": "Import", "Incorrect username and/or password.": "Inkorrekter Nutzername und/oder Passwort.", "Results from DuckDuckGo": "Ergebnisse von DuckDuckGo", - "Server unavailable, overloaded, or something else went wrong.": "Server nicht verfügbar, überlastet oder etwas anderes lief falsch.", "The signing key you provided matches the signing key you received from %(userId)s's device %(deviceId)s. Device marked as verified.": "Den Signaturschlüssel den du bereitstellst stimmt mit dem Schlüssel den du von %(userId)s's Gerät %(deviceId)s empfangen hast überein. Gerät als verifiziert markiert.", "Add a topic": "Thema hinzufügen", "Anyone": "Jeder", @@ -878,13 +873,9 @@ "Verified key": "Verifizierter Schlüssel", "WARNING: Device already verified, but keys do NOT MATCH!": "WARNUNG: Gerät bereits verifiziert, aber Schlüssel sind NICHT GLEICH!", "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!": "WARNUNG: SCHLÜSSEL-VERIFIZIERUNG FEHLGESCHLAGEN! Der Signatur-Schlüssel für %(userId)s und Gerät %(deviceId)s ist \"%(fprint)s\" welche nicht dem bereitgestellten Schlüssel \"%(fingerprint)s\" übereinstimmen. Dies kann bedeuten, dass deine Kommunikation abgefangen wird!", - "You are already in a call.": "Du bist bereits in einem Gespräch.", - "You cannot place a call with yourself.": "Du kannst keinen Anruf mit dir selbst starten.", - "You cannot place VoIP calls in this browser.": "Du kannst keine VoIP-Gespräche in diesem Browser starten.", "You have disabled URL previews by default.": "Du hast die URL-Vorschau standardmäßig deaktiviert.", "You have enabled URL previews by default.": "Du hast die URL-Vorschau standardmäßig aktiviert.", "You have entered an invalid contact. Try using their Matrix ID or email address.": "Du hast einen ungültigen Kontakt eingegeben. Versuche es mit der Matrix-ID oder der E-Mail-Adresse.", - "Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "Deine E-Mail-Adresse scheint nicht mit einer Matrix-ID auf diesem Heimserver verbunden zu sein.", "$senderDisplayName changed the room avatar to ": "$senderDisplayName hat das Raum-Bild geändert zu ", "%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s hat das Raum-Bild für %(roomName)s geändert", "Hide removed messages": "Gelöschte Nachrichten verbergen", @@ -895,17 +886,16 @@ "%(count)s new messages.other": "%(count)s neue Nachrichten", "Error: Problem communicating with the given homeserver.": "Fehler: Problem beim kommunizieren mit dem angegebenen Heimserver.", "Failed to fetch avatar URL": "Fehler beim holen der Avatar-URL", - "Some of your messages have not been sent.": "Einige deiner Nachrichten wurden nicht gesendet.", "The phone number entered looks invalid": "Die Telefonnummer, die eingegeben wurde, sieht ungültig aus", "This room is private or inaccessible to guests. You may be able to join if you register.": "Dieser Raum ist privat oder für Gäste nicht betretbar. Du kannst evtl. beitreten wenn du dich registrierst.", "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Es wurde versucht einen spezifischen Punkt in der Chat-Historie zu laden, aber du hast keine Berechtigung diese Nachricht zu sehen.", "Tried to load a specific point in this room's timeline, but was unable to find it.": "Es wurde versucht einen spezifischen Punkt in der Chat-Historie zu laden, aber er konnte nicht gefunden werden.", "Uploading %(filename)s and %(count)s others.zero": "%(filename)s wird hochgeladen", - "Uploading %(filename)s and %(count)s others.one": "%(filename)s und %(count)s weitere werden hochgeladen", - "Uploading %(filename)s and %(count)s others.other": "%(filename)s und %(count)s weitere werden hochgeladen", + "Uploading %(filename)s and %(count)s others.one": "%(filename)s und %(count)s weitere Dateien werden hochgeladen", + "Uploading %(filename)s and %(count)s others.other": "%(filename)s und %(count)s weitere Dateien werden hochgeladen", "You must register to use this functionality": "Du musst dich registrieren um diese Funktionalität zu nutzen", "Resend all or cancel all now. You can also select individual messages to resend or cancel.": "Sende erneut oder breche alles ab. Du kannst auch auch individuelle Nachrichten erneut senden or abbrechen.", - "Create new room": "Erstelle neuen Raum", + "Create new room": "Neuen Raum erstellen", "Welcome page": "Willkommensseite", "Room directory": "Raum-Verzeichnis", "Start chat": "Starte Chat", @@ -919,5 +909,7 @@ "Username not available": "Nutzername nicht verfügbar", "Something went wrong!": "Etwas ging schief!", "This will be your account name on the homeserver, or you can pick a different server.": "Dies wird dein Konto-Name auf dem Heimserver, oder du kannst einen anderen Server auswählen.", - "If you already have a Matrix account you can log in instead.": "Wenn du bereits ein Matrix-Konto hast, kannst du ansonsten auch anmelden." + "If you already have a Matrix account you can log in instead.": "Wenn du bereits ein Matrix-Benutzerkonto hast, kannst du dich stattdessen auch direkt anmelden.", + "Home": "Start", + "Username invalid: %(errMessage)s": "Nutzername falsch: %(errMessage)s" } diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index ecbb20fbb7..ffd1ef5885 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -829,5 +829,34 @@ "Hide removed messages": "Cacher les messages supprimés", "Add": "Ajouter", "%(count)s new messages.one": "%(count)s nouveau message", - "%(count)s new messages.other": "%(count)s nouveaux messages" + "%(count)s new messages.other": "%(count)s nouveaux messages", + "Disable markdown formatting": "Désactiver le formattage markdown", + "Error: Problem communicating with the given homeserver.": "Erreur: Problème de communication avec le homeserveur.", + "Failed to fetch avatar URL": "Échec lors de la récupération de l’URL de l’avatar", + "The phone number entered looks invalid": "Le numéro de téléphone entré semble être invalide", + "Guest users can't upload files. Please register to upload.": "Les visiteurs ne peuvent pas télécharger de fichier. Veuillez vous enregistrer pour télécharger.", + "Some of your messages have not been sent.": "Certains de vos messages n’ont pas été envoyés.", + "This room is private or inaccessible to guests. You may be able to join if you register.": "Ce salon est privé ou interdits aux visiteurs. Vous pourrez peut-être le joindre si vous vous enregistrez.", + "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Un instant donné de la chronologie n’a pu être chargé car vous n’avez pas la permission de le visualiser.", + "Tried to load a specific point in this room's timeline, but was unable to find it.": "Un instant donné de la chronologie n’a pu être chargé car il n’a pas pu être trouvé.", + "Uploading %(filename)s and %(count)s others.zero": "Téléchargement de %(filename)s", + "Uploading %(filename)s and %(count)s others.one": "Téléchargement de %(filename)s et %(count)s autre", + "Uploading %(filename)s and %(count)s others.other": "Téléchargement de %(filename)s et %(count)s autres", + "You must register to use this functionality": "Vous devez vous inscrire pour utiliser cette fonctionnalité", + "Resend all or cancel all now. You can also select individual messages to resend or cancel.": "Tout renvoyer or tout annuler maintenant. Vous pouvez aussi sélectionner des messages individuels à envoyer ou annuler.", + "Create new room": "Créer un nouveau salon", + "Welcome page": "Page d'accueil", + "Room directory": "Répertoire des salons", + "Start chat": "Démarrer une discussion", + "New Password": "Nouveau mot de passe", + "Start chatting": "Démarrer une discussion", + "Start Chatting": "Démarrer une discussion", + "Click on the button below to start chatting!": "Cliquer sur le bouton ci-dessous pour commencer une discussion !", + "Create a new chat or reuse an existing one": "Démarrer une nouvelle discussion ou en réutiliser une existante", + "You already have existing direct chats with this user:": "Vous avez déjà des discussions en cours avec cet utilisateur :", + "Username available": "Nom d'utilisateur disponible", + "Username not available": "Nom d'utilisateur indisponible", + "Something went wrong!": "Quelque chose s’est mal passé !", + "This will be your account name on the homeserver, or you can pick a different server.": "Cela sera le nom de votre compte sur le serveur , ou vous pouvez sélectionner un autre serveur.", + "If you already have a Matrix account you can log in instead.": "Si vous avez déjà un compte Matrix vous pouvez vous identifier à la place." } diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json index 85b7a6a7f5..647fbfe1d5 100644 --- a/src/i18n/strings/ru.json +++ b/src/i18n/strings/ru.json @@ -92,14 +92,14 @@ "Found a bug?": "Нашли ошибку?", "had": "имеет", "Hangup": "Отключение", - "Historical": "Исторический", + "Historical": "История", "Homeserver is": "Домашний сервер является", "Identity Server is": "Регистрационный сервер", "I have verified my email address": "Я проверил мой адрес электронной почты", "Import E2E room keys": "Импортировать E2E ключ комнаты", "Invalid Email Address": "Недействительный адрес электронной почты", "invited": "invited", - "Invite new room members": "Прегласить новых учасников комнаты", + "Invite new room members": "Пригласить новых учасников в комнату", "Invites": "Приглашать", "Invites user with given id to current room": "Пригласить пользователя с данным id в текущую комнату", "is a": "является", @@ -119,13 +119,13 @@ "Logout": "Выход из системы", "Low priority": "Низкий приоритет", "made future room history visible to": "made future room history visible to", - "Manage Integrations": "Управление интеграций", + "Manage Integrations": "Управление интеграциями", "Members only": "Только участники", "Mobile phone number": "Номер мобильного телефона", "Moderator": "Ведущий", "my Matrix ID": "мой Matrix ID", "Name": "Имя", - "Never send encrypted messages to unverified devices from this device": "Никогда не отправляйте зашифрованные сообщения в непроверенные устройства с этого устройства", + "Never send encrypted messages to unverified devices from this device": "Никогда не отправлять зашифрованные сообщения на неверифицированные устроства с этого устройства", "Never send encrypted messages to unverified devices in this room from this device": "Никогда не отправляйте зашифрованные сообщения в непроверенные устройства в этой комнате из этого устройства", "New password": "Новый пароль", "New passwords must match each other.": "Новые пароли должны соответствовать друг другу.", @@ -190,7 +190,7 @@ "Voice call": "Голосовой вызов", "VoIP conference finished.": "VoIP конференция закончилась.", "VoIP conference started.": "VoIP Конференция стартовала.", - "(warning: cannot be disabled again!)": "(предупреждение: не может быть снова отключен!)", + "(warning: cannot be disabled again!)": "(предупреждение: не может быть отключено!)", "Warning!": "Предупреждение!", "was banned": "запрещен", "was invited": "приглашенный", @@ -200,7 +200,7 @@ "were": "быть", "Who can access this room?": "Кто может получить доступ к этой комнате?", "Who can read history?": "Кто может читать историю?", - "Who would you like to add to this room?": "Кого хотели бы Вы добавлять к этой комнате?", + "Who would you like to add to this room?": "Кого бы вы хотели пригласить в эту комнату?", "Who would you like to communicate with?": "С кем хотели бы Вы связываться?", "withdrawn": "уходить", "Would you like to": "Хотели бы Вы", @@ -658,7 +658,7 @@ "powered by Matrix": "управляемый с Matrix", "Add a topic": "Добавить тему", "Show timestamps in 12 hour format (e.g. 2:30pm)": "Времея отображать в 12 часовом формате (напр. 2:30pm)", - "Use compact timeline layout": "Используйте компактным указанием времени", + "Use compact timeline layout": "Компактное отображение", "Hide removed messages": "Скрыть удаленное сообщение", "No Microphones detected": "Микрофоны не обнаружены", "Unknown devices": "Незнакомое устройство", @@ -864,5 +864,35 @@ "Disable URL previews for this room (affects only you)": "Отключить предпросмотр URL в этой комнате (только для вас)", "$senderDisplayName changed the room avatar to ": "$senderDisplayName сменил аватар комнаты на ", "%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s удалил аватар комнаты.", - "%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s сменил аватар для %(roomName)s" + "%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s сменил аватар для %(roomName)s", + "Create new room": "Создать комнату", + "Room directory": "Каталог комнат", + "Start chat": "Начать чат", + "Welcome page": "Домашняя страница", + "Add": "Добавить", + "%(count)s new messages.one": "%(count)s новое сообщение", + "%(count)s new messages.other": "%(count)s новых сообщений", + "Error: Problem communicating with the given homeserver.": "Ошибка: проблема коммуникаций с указанным Home Server'ом.", + "Failed to fetch avatar URL": "Ошибка получения аватара", + "The phone number entered looks invalid": "Введенный номер телефона выглядит неправильным", + "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 was unable to find it.": "Ошибка загрузки истории комнаты: запрошенный элемент не найден.", + "Uploading %(filename)s and %(count)s others.zero": "Загрузка %(filename)s", + "Uploading %(filename)s and %(count)s others.one": "Загрузка %(filename)s и %(count)s другой файл", + "Uploading %(filename)s and %(count)s others.other": "Загрузка %(filename)s и %(count)s других файлов", + "Username invalid: %(errMessage)s": "Неверное имя пользователя: %(errMessage)s", + "Searching known users": "Искать известных пользователей", + "You must register to use this functionality": "Вы должны зарегистрироваться для использования этой функции", + "Resend all or cancel all now. You can also select individual messages to resend or cancel.": "Отослать снова или отменить отправку. Вы также можете выбрать на отправку или отмену отдельные сообщения.", + "New Password": "Новый пароль", + "Start chatting": "Начать общение", + "Start Chatting": "Начать общение", + "Click on the button below to start chatting!": "Нажмите на кнопку ниже для того, чтобы начать общение!", + "Create a new chat or reuse an existing one": "Создать новый чат или использовать уже существующий", + "You already have existing direct chats with this user:": "У вас уже есть существующие приватные чаты с этим пользователем:", + "Username available": "Имя пользователя доступно", + "Username not available": "Имя пользователя недоступно", + "Something went wrong!": "Что-то пошло не так!", + "This will be your account name on the homeserver, or you can pick a different server.": "Это будет ваше имя пользователя на , или вы можете выбрать другой сервер.", + "If you already have a Matrix account you can log in instead.": "Если вы уже имеете учетную запись Matrix, то вы можете войти." } diff --git a/src/i18n/strings/th.json b/src/i18n/strings/th.json index 6608219faa..f97b6b8d50 100644 --- a/src/i18n/strings/th.json +++ b/src/i18n/strings/th.json @@ -201,7 +201,7 @@ "Hangup": "วางสาย", "Historical": "ประวัติแชทเก่า", "Homeserver is": "เซิร์ฟเวอร์บ้านคือ", - "Identity Server is": "เซิร์ฟเวอร์ยืนยันตัวตนคือ", + "Identity Server is": "เซิร์ฟเวอร์ระบุตัวตนคือ", "I have verified my email address": "ฉันยืนยันที่อยู่อีเมลแล้ว", "Import": "นำเข้า", "Incorrect username and/or password.": "ชื่อผู้ใช้และ/หรือรหัสผ่านไม่ถูกต้อง", @@ -229,7 +229,7 @@ "Leave room": "ออกจากห้อง", "left and rejoined": "ออกแล้วกลับเข้าร่วมอีกครั้ง", "left": "ออกไปแล้ว", - "%(targetName)s left the room.": "%(targetName)s ออกจากห้องไปแล้ว", + "%(targetName)s left the room.": "%(targetName)s ออกจากห้องแล้ว", "List this room in %(domain)s's room directory?": "แสดงห้องนี้ในไดเรกทอรีห้องของ %(domain)s?", "Logged in as:": "เข้าสู่ระบบในชื่อ:", "Login as guest": "เข้าสู่ระบบในฐานะแขก", @@ -322,7 +322,7 @@ "This is a preview of this room. Room interactions have been disabled": "นี่คือตัวอย่างของห้อง การตอบสนองภายในห้องถูกปิดใช้งาน", "This phone number is already in use": "หมายเลขโทรศัพท์นี้ถูกใช้งานแล้ว", "This room's internal ID is": "ID ภายในของห้องนี้คือ", - "times": "เวลา", + "times": "ครั้ง", "%(oneUser)schanged their name": "%(oneUser)sเปลี่ยนชื่อของเขาแล้ว", "%(severalUsers)schanged their name %(repeats)s times": "%(severalUsers)sเปลี่ยนชื่อของพวกเขา %(repeats)s ครั้ง", "%(oneUser)schanged their name %(repeats)s times": "%(oneUser)sเปลี่ยนชื่อของเขา %(repeats)s ครั้ง", @@ -330,5 +330,153 @@ "Create new room": "สร้างห้องใหม่", "Room directory": "ไดเรกทอรีห้อง", "Start chat": "เริ่มแชท", - "Welcome page": "หน้าต้อนรับ" + "Welcome page": "หน้าต้อนรับ", + "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or enable unsafe scripts.": "ไม่สามารถเชื่อมต่อไปยังเซิร์ฟเวอร์บ้านผ่านทาง HTTP ได้เนื่องจาก URL ที่อยู่บนเบราว์เซอร์เป็น HTTPS กรุณาใช้ HTTPS หรือเปิดใช้งานสคริปต์ที่ไม่ปลอดภัย.", + "%(count)s new messages.one": "มี %(count)s ข้อความใหม่", + "%(count)s new messages.other": "มี %(count)s ข้อความใหม่", + "Disable inline URL previews by default": "ตั้งค่าเริ่มต้นให้ไม่แสดงตัวอย่าง URL ในแชท", + "Disable markdown formatting": "ปิดใช้งานการจัดรูปแบบ markdown", + "End-to-end encryption information": "ข้อมูลการเข้ารหัสจากปลายทางถึงปลายทาง", + "End-to-end encryption is in beta and may not be reliable": "การเข้ารหัสจากปลายทางถึงปลายทางยังอยู่ในเบต้า และอาจพึ่งพาไม่ได้", + "Error: Problem communicating with the given homeserver.": "ข้อผิดพลาด: มีปัญหาในการติดต่อกับเซิร์ฟเวอร์บ้านที่กำหนด", + "Export E2E room keys": "ส่งออกกุญแจถอดรหัส E2E", + "Failed to change power level": "การเปลี่ยนระดับอำนาจล้มเหลว", + "Import E2E room keys": "นำเข้ากุญแจถอดรหัส E2E", + "to favourite": "ไปยังรายการโปรด", + "to demote": "เพื่อลดขั้น", + "The default role for new room members is": "บทบาทเริ่มต้นของสมาชิกใหม่คือ", + "The phone number entered looks invalid": "ดูเหมือนว่าหมายเลขโทรศัพท์ที่กรอกรมาไม่ถูกต้อง", + "The email address linked to your account must be entered.": "กรุณากรอกที่อยู่อีเมลที่เชื่อมกับบัญชีของคุณ", + "The file '%(fileName)s' exceeds this home server's size limit for uploads": "ไฟล์ '%(fileName)s' มีขนาดใหญ่เกินจำกัดของเซิร์ฟเวอร์บ้าน", + "To send messages": "เพื่อส่งข้อความ", + "to start a chat with someone": "เพื่อเริ่มแชทกับผู้อื่น", + "to tag as %(tagName)s": "เพื่อแท็กว่า %(tagName)s", + "to tag direct chat": "เพื่อแทกว่าแชทตรง", + "Turn Markdown off": "ปิด markdown", + "Turn Markdown on": "เปิด markdown", + "%(senderName)s turned on end-to-end encryption (algorithm %(algorithm)s).": "%(senderName)s ได้เปิดใช้งานการเข้ารหัสจากปลายทางถึงปลายทาง (อัลกอริทึม%(algorithm)s).", + "Unable to add email address": "ไมาสามารถเพิ่มที่อยู่อีเมล", + "Unable to verify email address.": "ไม่สามารถยืนยันที่อยู่อีเมล", + "Unban": "ปลดแบน", + "%(senderName)s unbanned %(targetName)s.": "%(senderName)s ปลดแบน %(targetName)s แล้ว", + "Unable to capture screen": "ไม่สามารถจับภาพหน้าจอ", + "Unable to enable Notifications": "ไม่สามารถเปิดใช้งานการแจ้งเตือน", + "Unable to load device list": "ไม่สามารถโหลดรายชื่ออุปกรณ์", + "Unencrypted room": "ห้องที่ไม่เข้ารหัส", + "unencrypted": "ยังไม่ได้เข้ารหัส", + "Unknown command": "คำสั่งที่ไม่รู้จัก", + "unknown device": "อุปกรณ์ที่ไม่รู้จัก", + "Unknown room %(roomId)s": "ห้องที่ไม่รู้จัก %(roomId)s", + "Unknown (user, device) pair:": "คู่ (ผู้ใช้, อุปกรณ์) ที่ไม่รู้จัก:", + "unknown": "ไม่รู้จัก", + "Unrecognised command:": "คำสั่งที่ไม่รู้จัก:", + "Unrecognised room alias:": "นามแฝงห้องที่ไม่รู้จัก:", + "Uploading %(filename)s and %(count)s others.zero": "กำลังอัปโหลด %(filename)s", + "Uploading %(filename)s and %(count)s others.one": "กำลังอัปโหลด %(filename)s และอีก %(count)s ไฟล์", + "Uploading %(filename)s and %(count)s others.other": "กำลังอัปโหลด %(filename)s และอีก %(count)s ไฟล์", + "uploaded a file": "อัปโหลดไฟล์", + "Upload Failed": "การอัปโหลดล้มเหลว", + "Upload Files": "อัปโหลดไฟล์", + "Upload file": "อัปโหลดไฟล์", + "Usage": "การใช้งาน", + "User ID": "ID ผู้ใช้", + "User Interface": "อินเตอร์เฟสผู้ใช้", + "User name": "ชื่อผู้ใช้", + "User": "ผู้ใช้", + "Warning!": "คำเตือน!", + "Who can access this room?": "ใครสามารถเข้าถึงห้องนี้ได้?", + "Who can read history?": "ใครสามารถอ่านประวัติแชทได้?", + "Who would you like to add to this room?": "คุณต้องการเพิ่มใครเข้าห้องนี้?", + "Who would you like to communicate with?": "คุณต้องการสื่อสารกับใคร?", + "You're not in any rooms yet! Press": "คุณยังไม่ได้อยู่ในห้องใดเลย! กด", + "You are trying to access %(roomName)s": "คุณกำลังพยายามเข้าสู่ %(roomName)s", + "You have disabled URL previews by default.": "ค่าเริ่มต้นของคุณปิดใช้งานตัวอย่าง URL เอาไว้", + "You have enabled URL previews by default.": "ค่าเริ่มต้นของคุณเปิดใช้งานตัวอย่าง URL เอาไว้", + "you must be a": "คุณต้องเป็น", + "You must register to use this functionality": "คุณต้องลงทะเบียนเพื่อใช้ฟังก์ชันนี้", + "You need to be logged in.": "คุณต้องเข้าสู่ระบบก่อน", + "You need to enter a user name.": "คุณต้องกรอกชื่อผู้ใช้ก่อน", + "Your password has been reset": "รหัสผ่านถูกรีเซ็ตแล้ว", + "Your password was successfully changed. You will not receive push notifications on other devices until you log back in to them": "การเปลี่ยนรหัสผ่านเสร็จสมบูณณ์ คุณจะไม่ได้รับการแจ้งเตือนบนอุปกรณ์อื่น ๆ จนกว่าคุณจะกลับเข้าสู่ระบบในอุปกรณ์เหล่านั้น", + "Sun": "อา.", + "Mon": "จ.", + "Tue": "อ.", + "Wed": "พ.", + "Thu": "พฤ.", + "Fri": "ศ.", + "Sat": "ส.", + "Jan": "ม.ค.", + "Feb": "ก.พ.", + "Mar": "มี.ค.", + "Apr": "เม.ย.", + "May": "พ.ค.", + "Jun": "มิ.ย.", + "Jul": "ก.ค.", + "Aug": "ส.ค.", + "Sep": "ก.ย.", + "Oct": "ต.ค.", + "Nov": "พ.ย.", + "Dec": "ธ.ค.", + "%(weekDayName)s, %(monthName)s %(day)s %(time)s": "%(weekDayName)s %(day)s %(monthName)s %(time)s", + "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s %(day)s %(monthName)s %(fullYear)s %(time)s", + "%(weekDayName)s %(time)s": "%(weekDayName)s %(time)s", + "Set a display name:": "ตั้งชื่อที่แสดง:", + "Set a Display Name": "ตั้งชื่อที่แสดง", + "Passwords don't match.": "รหัสผ่านไม่ตรงกัน", + "Password too short (min %(MIN_PASSWORD_LENGTH)s).": "รหัสผ่านสั้นเกินไป (ขึ้นต่ำ %(MIN_PASSWORD_LENGTH)s ตัวอักษร)", + "An unknown error occurred.": "เกิดข้อผิดพลาดที่ไม่รู้จัก", + "I already have an account": "ฉันมีบัญชีอยู่แล้ว", + "An error occured: %(error_string)s": "เกิดข้อผิดพลาด: %(error_string)s", + "Topic": "หัวข้อ", + "Make Moderator": "เลื่อนขั้นเป็นผู้ช่วยดูแล", + "Make this room private": "ทำให้ห้องนี้เป็นส่วนตัว", + "Share message history with new users": "แบ่งประวัติแชทให้ผู้ใช้ใหม่", + "Encrypt room": "เข้ารหัสห้อง", + "Room": "ห้อง", + "(~%(searchCount)s results)": "(~%(searchCount)s ผลลัพธ์)", + "or": "หรือ", + "bold": "หนา", + "italic": "เอียง", + "strike": "ขีดทับ", + "underline": "ขีดเส้นใต้", + "code": "โค๊ด", + "quote": "อ้างอิง", + "were kicked %(repeats)s times": "ถูกเตะ %(repeats)s ครั้ง", + "was kicked %(repeats)s times": "ถูกเตะ %(repeats)s ครั้ง", + "were kicked": "ถูกเตะ", + "was kicked": "ถูกเตะ", + "New Password": "รหัสผ่านใหม่", + "Options": "ตัวเลือก", + "Export room keys": "ส่งออกกุณแจห้อง", + "Confirm passphrase": "ยืนยันรหัสผ่าน", + "Import room keys": "นำเข้ากุณแจห้อง", + "File to import": "ไฟล์ที่จะนำเข้า", + "Start new chat": "เริ่มแชทใหม่", + "Failed to invite": "การเชิญล้มเหลว", + "Failed to invite user": "การเชิญผู้ใช้ล้มเหลว", + "Failed to invite the following users to the %(roomName)s room:": "การเชิญผู้ใช้เหล่านี้เข้าสู่ห้อง %(roomName)s ล้มเหลว:", + "Confirm Removal": "ยืนยันการลบ", + "Unknown error": "ข้อผิดพลาดที่ไม่รู้จัก", + "Incorrect password": "รหัสผ่านไม่ถูกต้อง", + "Device name": "ชื่ออุปกรณ์", + "Device key": "Key อุปกรณ์", + "Unknown devices": "อุปกรณ์ที่ไม่รู้จัก", + "Unknown Address": "ที่อยู่ที่ไม่รู้จัก", + "Unblacklist": "ถอดบัญชีดำ", + "Blacklist": "ขึ้นบัญชีดำ", + "ex. @bob:example.com": "เช่น @bob:example.com", + "Add User": "เพิ่มผู้ใช้", + "This Home Server would like to make sure you are not a robot": "เซิร์ฟเวอร์บ้านต้องการยืนยันว่าคุณไม่ใช่หุ่นยนต์", + "Sign in with CAS": "เข้าสู่ระบบด้วย CAS", + "You can use the custom server options to sign into other Matrix servers by specifying a different Home server URL.": "คุณสามารถกำหนดเซิร์ฟเวอร์บ้านเองได้โดยใส่ URL ของเซิร์ฟเวอร์นั้น เพื่อเข้าสู่ระบบของเซิร์ฟเวอร์ Matrix อื่น", + "This allows you to use this app with an existing Matrix account on a different home server.": "ทั้งนี่เพื่อให้คุณสามารถใช้ Riot กับบัญชี Matrix ที่มีอยู่แล้วบนเซิร์ฟเวอร์บ้านอื่น ๆ ได้", + "You can also set a custom identity server but this will typically prevent interaction with users based on email address.": "คุณอาจเลือกเซิร์ฟเวอร์ระบุตัวตนเองด้วยก็ได้ แต่คุณจะไม่สามารถเชิญผู้ใช้อื่นด้วยที่อยู่อีเมล หรือรับคำเชิญจากผู้ใช้อื่นทางที่อยู่อีเมลได้", + "Default server": "เซิร์ฟเวอร์เริ่มต้น", + "Custom server": "เซิร์ฟเวอร์ที่กำหนดเอง", + "Home server URL": "URL เซิร์ฟเวอร์บ้าน", + "Identity server URL": "URL เซิร์ฟเวอร์ระบุตัวตน", + "%(severalUsers)sleft %(repeats)s times": "%(targetName)sออกจากห้อง %(repeats)s ครั้ง", + "%(oneUser)sleft %(repeats)s times": "%(oneUser)sออกจากห้อง %(repeats)s ครั้ง", + "%(severalUsers)sleft": "%(severalUsers)sออกจากห้องแล้ว", + "%(oneUser)sleft": "%(oneUser)sออกจากห้องแล้ว" }