diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 39a159869c..59580e7cb6 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -35,26 +35,20 @@ import { _t } from './languageHandler'; * Called at startup, to attempt to build a logged-in Matrix session. It tries * a number of things: * - * 1. if we have a loginToken in the (real) query params, it uses that to log - * in. * - * 2. if we have a guest access token in the fragment query params, it uses + * 1. if we have a guest access token in the fragment query params, it uses * that. * - * 3. if an access token is stored in local storage (from a previous session), + * 2. if an access token is stored in local storage (from a previous session), * it uses that. * - * 4. it attempts to auto-register as a guest user. + * 3. it attempts to auto-register as a guest user. * * If any of steps 1-4 are successful, it will call {_doSetLoggedIn}, which in * turn will raise on_logged_in and will_start_client events. * * @param {object} opts * - * @param {object} opts.realQueryParams: string->string map of the - * query-parameters extracted from the real query-string of the starting - * URI. - * * @param {object} opts.fragmentQueryParams: string->string map of the * query-parameters extracted from the #-fragment of the starting URI. * @@ -68,9 +62,10 @@ import { _t } from './languageHandler'; * true; defines the IS to use. * * @returns {Promise} a promise which resolves when the above process completes. + * Resolves to `true` if we ended up starting a session, or `false` if we + * failed. */ export function loadSession(opts) { - const realQueryParams = opts.realQueryParams || {}; const fragmentQueryParams = opts.fragmentQueryParams || {}; let enableGuest = opts.enableGuest || false; const guestHsUrl = opts.guestHsUrl; @@ -82,14 +77,6 @@ export function loadSession(opts) { enableGuest = false; } - if (realQueryParams.loginToken) { - if (!realQueryParams.homeserver) { - console.warn("Cannot log in with token: can't determine HS URL to use"); - } else { - return _loginWithToken(realQueryParams, defaultDeviceDisplayName); - } - } - if (enableGuest && fragmentQueryParams.guest_user_id && fragmentQueryParams.guest_access_token @@ -101,12 +88,12 @@ export function loadSession(opts) { homeserverUrl: guestHsUrl, identityServerUrl: guestIsUrl, guest: true, - }, true); + }, true).then(() => true); } return _restoreFromLocalStorage().then((success) => { if (success) { - return; + return true; } if (enableGuest) { @@ -114,10 +101,30 @@ export function loadSession(opts) { } // fall back to login screen + return false; }); } -function _loginWithToken(queryParams, defaultDeviceDisplayName) { +/** + * @param {Object} queryParams string->string map of the + * query-parameters extracted from the real query-string of the starting + * URI. + * + * @param {String} defaultDeviceDisplayName + * + * @returns {Promise} promise which resolves to true if we completed the token + * login, else false + */ +export function attemptTokenLogin(queryParams, defaultDeviceDisplayName) { + if (!queryParams.loginToken) { + return q(false); + } + + if (!queryParams.homeserver) { + console.warn("Cannot log in with token: can't determine HS URL to use"); + return q(false); + } + // create a temporary MatrixClient to do the login const client = Matrix.createClient({ baseUrl: queryParams.homeserver, @@ -130,17 +137,21 @@ function _loginWithToken(queryParams, defaultDeviceDisplayName) { }, ).then(function(data) { console.log("Logged in with token"); - return _doSetLoggedIn({ - userId: data.user_id, - deviceId: data.device_id, - accessToken: data.access_token, - homeserverUrl: queryParams.homeserver, - identityServerUrl: queryParams.identityServer, - guest: false, - }, true); - }, (err) => { + return _clearStorage().then(() => { + _persistCredentialsToLocalStorage({ + userId: data.user_id, + deviceId: data.device_id, + accessToken: data.access_token, + homeserverUrl: queryParams.homeserver, + identityServerUrl: queryParams.identityServer, + guest: false, + }); + return true; + }); + }).catch((err) => { console.error("Failed to log in with login token: " + err + " " + err.data); + return false; }); } @@ -168,9 +179,10 @@ function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) { homeserverUrl: hsUrl, identityServerUrl: isUrl, guest: true, - }, true); + }, true).then(() => true); }, (err) => { console.error("Failed to register as guest: " + err + " " + err.data); + return false; }); } @@ -282,10 +294,12 @@ export function initRtsClient(url) { * storage before starting the new client. * * @param {MatrixClientCreds} credentials The credentials to use + * + * @returns {Promise} promise which resolves to the new MatrixClient once it has been started */ export function setLoggedIn(credentials) { stopMatrixClient(); - _doSetLoggedIn(credentials, true); + return _doSetLoggedIn(credentials, true); } /** @@ -295,7 +309,7 @@ export function setLoggedIn(credentials) { * @param {MatrixClientCreds} credentials * @param {Boolean} clearStorage * - * returns a Promise which resolves once the client has been started + * @returns {Promise} promise which resolves to the new MatrixClient once it has been started */ async function _doSetLoggedIn(credentials, clearStorage) { credentials.guest = Boolean(credentials.guest); @@ -322,23 +336,10 @@ async function _doSetLoggedIn(credentials, clearStorage) { // Resolves by default let teamPromise = Promise.resolve(null); - // persist the session + if (localStorage) { try { - localStorage.setItem("mx_hs_url", credentials.homeserverUrl); - localStorage.setItem("mx_is_url", credentials.identityServerUrl); - localStorage.setItem("mx_user_id", credentials.userId); - localStorage.setItem("mx_access_token", credentials.accessToken); - localStorage.setItem("mx_is_guest", JSON.stringify(credentials.guest)); - - // if we didn't get a deviceId from the login, leave mx_device_id unset, - // rather than setting it to "undefined". - // - // (in this case MatrixClient doesn't bother with the crypto stuff - // - that's fine for us). - if (credentials.deviceId) { - localStorage.setItem("mx_device_id", credentials.deviceId); - } + _persistCredentialsToLocalStorage(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. @@ -349,8 +350,6 @@ async function _doSetLoggedIn(credentials, clearStorage) { cachedPassword: credentials.password, }); } - - console.log("Session persisted for %s", credentials.userId); } catch (e) { console.warn("Error using local storage: can't persist session!", e); } @@ -377,6 +376,26 @@ async function _doSetLoggedIn(credentials, clearStorage) { }); startMatrixClient(); + return MatrixClientPeg.get(); +} + +function _persistCredentialsToLocalStorage(credentials) { + localStorage.setItem("mx_hs_url", credentials.homeserverUrl); + localStorage.setItem("mx_is_url", credentials.identityServerUrl); + localStorage.setItem("mx_user_id", credentials.userId); + localStorage.setItem("mx_access_token", credentials.accessToken); + localStorage.setItem("mx_is_guest", JSON.stringify(credentials.guest)); + + // if we didn't get a deviceId from the login, leave mx_device_id unset, + // rather than setting it to "undefined". + // + // (in this case MatrixClient doesn't bother with the crypto stuff + // - that's fine for us). + if (credentials.deviceId) { + localStorage.setItem("mx_device_id", credentials.deviceId); + } + + console.log("Session persisted for %s", credentials.userId); } /** diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index ab937c07ac..e2bb5764cc 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -43,7 +43,41 @@ import createRoom from "../../createRoom"; import * as UDEHandler from '../../UnknownDeviceErrorHandler'; import { _t, getCurrentLanguage } from '../../languageHandler'; +/** constants for MatrixChat.state.view */ +const VIEWS = { + // a special initial state which is only used at startup, while we are + // trying to re-animate a matrix client or register as a guest. + LOADING: 0, + + // we are showing the login view + LOGIN: 1, + + // we are showing the registration view + REGISTER: 2, + + // completeing the registration flow + POST_REGISTRATION: 3, + + // showing the 'forgot password' view + FORGOT_PASSWORD: 4, + + // we have valid matrix credentials (either via an explicit login, via the + // initial re-animation/guest registration, or via a registration), and are + // now setting up a matrixclient to talk to it. This isn't an instant + // process because (a) we need to clear out indexeddb, and (b) we need to + // talk to the team server; while it is going on we show a big spinner. + LOGGING_IN: 5, + + // we are logged in with an active matrix client. + LOGGED_IN: 6, +}; + module.exports = React.createClass({ + // we export this so that the integration tests can use it :-S + statics: { + VIEWS: VIEWS, + }, + displayName: 'MatrixChat', propTypes: { @@ -59,8 +93,8 @@ module.exports = React.createClass({ // the initial queryParams extracted from the hash-fragment of the URI startingFragmentQueryParams: React.PropTypes.object, - // called when the session load completes - onLoadCompleted: React.PropTypes.func, + // called when we have completed a token login + onTokenLoginCompleted: React.PropTypes.func, // Represents the screen to display as a result of parsing the initial // window.location @@ -93,8 +127,10 @@ module.exports = React.createClass({ getInitialState: function() { const s = { - loading: true, - screen: undefined, + // the master view we are showing. + view: VIEWS.LOADING, + + // a thing to call showScreen with once login completes. screenAfterLogin: this.props.initialScreenAfterLogin, // Stashed guest credentials if the user logs out @@ -113,8 +149,6 @@ module.exports = React.createClass({ // If we're trying to just view a user ID (i.e. /user URL), this is it viewUserId: null, - loggedIn: false, - loggingIn: false, collapse_lhs: false, collapse_rhs: false, ready: false, @@ -143,7 +177,7 @@ module.exports = React.createClass({ realQueryParams: {}, startingFragmentQueryParams: {}, config: {}, - onLoadCompleted: () => {}, + onTokenLoginCompleted: () => {}, }; }, @@ -266,39 +300,49 @@ module.exports = React.createClass({ const teamServerConfig = this.props.config.teamServerConfig || {}; Lifecycle.initRtsClient(teamServerConfig.teamServerURL); - // if the user has followed a login or register link, don't reanimate - // the old creds, but rather go straight to the relevant page + // the first thing to do is to try the token params in the query-string + Lifecycle.attemptTokenLogin(this.props.realQueryParams).then((loggedIn) => { + if(loggedIn) { + this.props.onTokenLoginCompleted(); - const firstScreen = this.state.screenAfterLogin ? - this.state.screenAfterLogin.screen : null; + // don't do anything else until the page reloads - just stay in + // the 'loading' state. + return; + } - if (firstScreen === 'login' || - firstScreen === 'register' || - firstScreen === 'forgot_password') { - this.props.onLoadCompleted(); - this.setState({loading: false}); - this._showScreenAfterLogin(); - return; - } + // if the user has followed a login or register link, don't reanimate + // the old creds, but rather go straight to the relevant page + const firstScreen = this.state.screenAfterLogin ? + this.state.screenAfterLogin.screen : null; - // the extra q() ensures that synchronous exceptions hit the same codepath as - // asynchronous ones. - q().then(() => { - return Lifecycle.loadSession({ - realQueryParams: this.props.realQueryParams, - fragmentQueryParams: this.props.startingFragmentQueryParams, - enableGuest: this.props.enableGuest, - guestHsUrl: this.getCurrentHsUrl(), - guestIsUrl: this.getCurrentIsUrl(), - defaultDeviceDisplayName: this.props.defaultDeviceDisplayName, + if (firstScreen === 'login' || + firstScreen === 'register' || + firstScreen === 'forgot_password') { + this.setState({loading: false}); + this._showScreenAfterLogin(); + return; + } + + // the extra q() ensures that synchronous exceptions hit the same codepath as + // asynchronous ones. + return q().then(() => { + return Lifecycle.loadSession({ + fragmentQueryParams: this.props.startingFragmentQueryParams, + enableGuest: this.props.enableGuest, + guestHsUrl: this.getCurrentHsUrl(), + guestIsUrl: this.getCurrentIsUrl(), + defaultDeviceDisplayName: this.props.defaultDeviceDisplayName, + }); + }).catch((e) => { + console.error("Unable to load session", e); + return false; + }).then((loadedSession) => { + if (!loadedSession) { + // fall back to showing the login screen + dis.dispatch({action: "start_login"}); + } }); - }).catch((e) => { - console.error("Unable to load session", e); - }).done(()=>{ - // stuff this through the dispatcher so that it happens - // after the on_logged_in action. - dis.dispatch({action: 'load_completed'}); - }); + }).done(); }, componentWillUnmount: function() { @@ -317,18 +361,19 @@ module.exports = React.createClass({ } }, - setStateForNewScreen: function(state) { + setStateForNewView: function(state) { + if (state.view === undefined) { + throw new Error("setStateForNewView with no view!"); + } const newState = { - screen: undefined, viewUserId: null, - loggedIn: false, - ready: false, }; Object.assign(newState, state); this.setState(newState); }, onAction: function(payload) { + // console.log(`MatrixClientPeg.onAction: ${payload.action}`); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); @@ -347,19 +392,19 @@ module.exports = React.createClass({ guestCreds: MatrixClientPeg.getCredentials(), }); } - this.setStateForNewScreen({ - screen: 'login', + this.setStateForNewView({ + view: VIEWS.LOGIN, }); this.notifyNewScreen('login'); break; case 'start_post_registration': - this.setState({ // don't clobber loggedIn status - screen: 'post_registration', + this.setState({ + view: VIEWS.POST_REGISTRATION, }); break; case 'start_password_recovery': - this.setStateForNewScreen({ - screen: 'forgot_password', + this.setStateForNewView({ + view: VIEWS.FORGOT_PASSWORD, }); this.notifyNewScreen('forgot_password'); break; @@ -503,7 +548,10 @@ module.exports = React.createClass({ // and also that we're not ready (we'll be marked as logged // in once the login completes, then ready once the sync // completes). - this.setState({loggingIn: true, ready: false}); + this.setStateForNewView({ + view: VIEWS.LOGGING_IN, + ready: false, + }); break; case 'on_logged_in': this._onLoggedIn(payload.teamToken); @@ -514,15 +562,15 @@ module.exports = React.createClass({ case 'will_start_client': this._onWillStartClient(); break; - case 'load_completed': - this._onLoadCompleted(); - break; case 'new_version': this.onVersion( payload.currentVersion, payload.newVersion, payload.releaseNotes, ); break; + case 'send_event': + this.onSendEvent(payload.room_id, payload.event); + break; } }, @@ -537,8 +585,8 @@ module.exports = React.createClass({ }, _startRegistration: function(params) { - this.setStateForNewScreen({ - screen: 'register', + this.setStateForNewView({ + view: VIEWS.REGISTER, // these params may be undefined, but if they are, // unset them from our state: we don't want to // resume a previous registration session if the @@ -846,14 +894,6 @@ module.exports = React.createClass({ }); }, - /** - * Called when the sessionloader has finished - */ - _onLoadCompleted: function() { - this.props.onLoadCompleted(); - this.setState({loading: false}); - }, - /** * Called whenever someone changes the theme * @@ -906,9 +946,8 @@ module.exports = React.createClass({ */ _onLoggedIn: function(teamToken) { this.setState({ + view: VIEWS.LOGGED_IN, guestCreds: null, - loggedIn: true, - loggingIn: false, }); if (teamToken) { @@ -969,8 +1008,8 @@ module.exports = React.createClass({ */ _onLoggedOut: function() { this.notifyNewScreen('login'); - this.setStateForNewScreen({ - loggedIn: false, + this.setStateForNewView({ + view: VIEWS.LOGIN, ready: false, collapse_lhs: false, collapse_rhs: false, @@ -1133,7 +1172,7 @@ module.exports = React.createClass({ // we can't view a room unless we're logged in // (a guest account is fine) - if (this.state.loggedIn) { + if (this.state.view === VIEWS.LOGGED_IN) { dis.dispatch(payload); } } else if (screen.indexOf('user/') == 0) { @@ -1241,19 +1280,20 @@ module.exports = React.createClass({ } }, + // returns a promise which resolves to the new MatrixClient 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; - Lifecycle.setLoggedIn(credentials); + return Lifecycle.setLoggedIn(credentials); }, onFinishPostRegistration: function() { // Don't confuse this with "PageType" which is the middle window to show this.setState({ - screen: undefined, + view: VIEWS.LOGGED_IN, }); this.showScreen("settings"); }, @@ -1267,6 +1307,27 @@ module.exports = React.createClass({ }); }, + onSendEvent: function(roomId, event) { + const cli = MatrixClientPeg.get(); + if (!cli) { + dis.dispatch({action: 'message_send_failed'}); + return; + } + + cli.sendEvent(roomId, event.getType(), event.getContent()).done(() => { + dis.dispatch({action: 'message_sent'}); + }, (err) => { + if (err.name === 'UnknownDeviceError') { + dis.dispatch({ + action: 'unknown_device_error', + err: err, + room: cli.getRoom(roomId), + }); + } + dis.dispatch({action: 'message_send_failed'}); + }); + }, + updateStatusIndicator: function(state, prevState) { let notifCount = 0; @@ -1321,11 +1382,9 @@ module.exports = React.createClass({ }, render: function() { - // `loading` might be set to false before `loggedIn = true`, causing the default - // (``) to be visible for a few MS (say, whilst a request is in-flight to - // the RTS). So in the meantime, use `loggingIn`, which is true between - // actions `on_logging_in` and `on_logged_in`. - if (this.state.loading || this.state.loggingIn) { + // console.log(`Rendering MatrixChat with view ${this.state.view}`); + + if (this.state.view === VIEWS.LOADING || this.state.view === VIEWS.LOGGING_IN) { const Spinner = sdk.getComponent('elements.Spinner'); return (
@@ -1335,7 +1394,7 @@ module.exports = React.createClass({ } // needs to be before normal PageTypes as you are logged in technically - if (this.state.screen == 'post_registration') { + if (this.state.view === VIEWS.POST_REGISTRATION) { const PostRegistration = sdk.getComponent('structures.login.PostRegistration'); return ( - ); - } else if (this.state.loggedIn) { - // we think we are logged in, but are still waiting for the /sync to complete - const Spinner = sdk.getComponent('elements.Spinner'); - return ( -
- - - { _t('Logout') } - -
- ); - } else if (this.state.screen == 'register') { + if (this.state.view === VIEWS.LOGGED_IN) { + // `ready` and `view==LOGGED_IN` may be set before `page_type` (because the + // latter is set via the dispatcher). If we don't yet have a `page_type`, + // keep showing the spinner for now. + if (this.state.ready && this.state.page_type) { + /* for now, we stuff the entirety of our props and state into the LoggedInView. + * we should go through and figure out what we actually need to pass down, as well + * as using something like redux to avoid having a billion bits of state kicking around. + */ + const LoggedInView = sdk.getComponent('structures.LoggedInView'); + return ( + + ); + } else { + // we think we are logged in, but are still waiting for the /sync to complete + const Spinner = sdk.getComponent('elements.Spinner'); + return ( +
+ + + { _t('Logout') } + +
+ ); + } + } + + if (this.state.view === VIEWS.REGISTER) { const Registration = sdk.getComponent('structures.login.Registration'); return ( ); - } else if (this.state.screen == 'forgot_password') { + } + + + if (this.state.view === VIEWS.FORGOT_PASSWORD) { const ForgotPassword = sdk.getComponent('structures.login.ForgotPassword'); return ( ); - } else { + } + + if (this.state.view === VIEWS.LOGIN) { const Login = sdk.getComponent('structures.login.Login'); return ( ); } + + console.error(`Unknown view ${this.state.view}`); }, }); diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 25500d7739..b29b3579f0 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -169,6 +169,7 @@ module.exports = React.createClass({ initialEventId: RoomViewStore.getInitialEventId(), initialEventPixelOffset: RoomViewStore.getInitialEventPixelOffset(), isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(), + forwardingEvent: RoomViewStore.getForwardingEvent(), shouldPeek: RoomViewStore.shouldPeek(), }; @@ -457,11 +458,6 @@ module.exports = React.createClass({ callState: callState }); - break; - case 'forward_event': - this.setState({ - forwardingEvent: payload.content, - }); break; } }, @@ -1169,8 +1165,13 @@ module.exports = React.createClass({ this.updateTint(); this.setState({ editingRoomSettings: false, - forwardingEvent: null, }); + if (this.state.forwardingEvent) { + dis.dispatch({ + action: 'forward_event', + event: null, + }); + } dis.dispatch({action: 'focus_composer'}); }, @@ -1581,7 +1582,7 @@ module.exports = React.createClass({ } else if (this.state.uploadingRoomSettings) { aux = ; } else if (this.state.forwardingEvent !== null) { - aux = ; + aux = ; } else if (this.state.searching) { hideCancel = true; // has own cancel aux = ; diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js index f035efee92..2daa6d2152 100644 --- a/src/components/structures/ScrollPanel.js +++ b/src/components/structures/ScrollPanel.js @@ -160,6 +160,10 @@ module.exports = React.createClass({ this.checkFillState(); }, + componentWillUpdate: function(nextProps, nextState) { + this._saveScrollState(); + }, + componentDidUpdate: function() { // after adding event tiles, we may need to tweak the scroll (either to // keep at the bottom of the timeline, or to maintain the view after diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index 17fbf445b2..388198bb02 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -218,29 +218,29 @@ module.exports = React.createClass({ } trackPromise.then((teamToken) => { - this.props.onLoggedIn({ + return this.props.onLoggedIn({ userId: response.user_id, deviceId: response.device_id, homeserverUrl: this._matrixClient.getHomeserverUrl(), identityServerUrl: this._matrixClient.getIdentityServerUrl(), accessToken: response.access_token }, teamToken); - }).then(() => { - return this._setupPushers(); + }).then((cli) => { + return this._setupPushers(cli); }); }, - _setupPushers: function() { + _setupPushers: function(matrixClient) { if (!this.props.brand) { return q(); } - return MatrixClientPeg.get().getPushers().then((resp)=>{ + return matrixClient.getPushers().then((resp)=>{ const pushers = resp.pushers; for (let i = 0; i < pushers.length; ++i) { if (pushers[i].kind == 'email') { const emailPusher = pushers[i]; emailPusher.data = { brand: this.props.brand }; - MatrixClientPeg.get().setPusher(emailPusher).done(() => { + matrixClient.setPusher(emailPusher).done(() => { console.log("Set email branding to " + this.props.brand); }, (error) => { console.error("Couldn't set email branding: " + error); diff --git a/src/components/views/rooms/ForwardMessage.js b/src/components/views/rooms/ForwardMessage.js index 33df201d7c..3c97128a02 100644 --- a/src/components/views/rooms/ForwardMessage.js +++ b/src/components/views/rooms/ForwardMessage.js @@ -17,7 +17,6 @@ import React from 'react'; import { _t } from '../../../languageHandler'; -import MatrixClientPeg from '../../../MatrixClientPeg'; import dis from '../../../dispatcher'; import KeyCode from '../../../KeyCode'; @@ -26,11 +25,6 @@ module.exports = React.createClass({ displayName: 'ForwardMessage', propTypes: { - currentRoomId: React.PropTypes.string.isRequired, - - /* the MatrixEvent to be forwarded */ - mxEvent: React.PropTypes.object.isRequired, - onCancelClick: React.PropTypes.func.isRequired, }, @@ -44,7 +38,6 @@ module.exports = React.createClass({ }, componentDidMount: function() { - this.dispatcherRef = dis.register(this.onAction); document.addEventListener('keydown', this._onKeyDown); }, @@ -54,30 +47,9 @@ module.exports = React.createClass({ sideOpacity: 1.0, middleOpacity: 1.0, }); - dis.unregister(this.dispatcherRef); document.removeEventListener('keydown', this._onKeyDown); }, - onAction: function(payload) { - if (payload.action === 'view_room') { - const event = this.props.mxEvent; - const Client = MatrixClientPeg.get(); - Client.sendEvent(payload.room_id, event.getType(), event.getContent()).done(() => { - dis.dispatch({action: 'message_sent'}); - }, (err) => { - if (err.name === "UnknownDeviceError") { - dis.dispatch({ - action: 'unknown_device_error', - err: err, - room: Client.getRoom(payload.room_id), - }); - } - dis.dispatch({action: 'message_send_failed'}); - }); - if (this.props.currentRoomId === payload.room_id) this.props.onCancelClick(); - } - }, - _onKeyDown: function(ev) { switch (ev.keyCode) { case KeyCode.ESCAPE: diff --git a/src/i18n/strings/el.json b/src/i18n/strings/el.json index eb25cd9e16..cdfb558f3a 100644 --- a/src/i18n/strings/el.json +++ b/src/i18n/strings/el.json @@ -839,13 +839,13 @@ "Share message history with new users": "Διαμοιρασμός ιστορικού μηνυμάτων με τους νέους χρήστες", "numbullet": "απαρίθμηση", "%(severalUsers)sleft and rejoined %(repeats)s times": "%(severalUsers)s έφυγαν και ξανασυνδέθηκαν %(repeats)s φορές", - "%(oneUser)sleft and rejoined %(repeats)s times": "%(severalUsers)s έφυγε και ξανασυνδέθηκε %(repeats)s φορές", + "%(oneUser)sleft and rejoined %(repeats)s times": "%(oneUser)s έφυγε και ξανασυνδέθηκε %(repeats)s φορές", "%(severalUsers)sleft and rejoined": "%(severalUsers)s έφυγαν και ξανασυνδέθηκαν", - "%(oneUser)sleft and rejoined": "%(severalUsers)s έφυγε και ξανασυνδέθηκε", + "%(oneUser)sleft and rejoined": "%(oneUser)s έφυγε και ξανασυνδέθηκε", "%(severalUsers)shad their invitations withdrawn %(repeats)s times": "Οι %(severalUsers)s απέσυραν τις προσκλήσεις τους %(repeats)s φορές", - "%(oneUser)shad their invitation 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": "Ο %(severalUsers)s απέσυρε την πρόσκληση του", + "%(oneUser)shad their invitation withdrawn": "Ο %(oneUser)s απέσυρε την πρόσκληση του", "You must join the room to see its files": "Πρέπει να συνδεθείτε στο δωμάτιο για να δείτε τα αρχεία του", "Reject all %(invitedRooms)s invites": "Απόρριψη όλων των προσκλήσεων %(invitedRooms)s", "Failed to invite the following users to the %(roomName)s room:": "Δεν ήταν δυνατή η πρόσκληση των χρηστών στο δωμάτιο %(roomName)s:", diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index 3cc449b1fc..696a548b4a 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -204,7 +204,7 @@ "Anyone who knows the room's link, apart from guests": "A vendégeken kívül bárki aki ismeri a szoba link-jét", "Anyone who knows the room's link, including guests": "Bárki aki tudja a szoba link-jét, még a vendégek is", "Are you sure?": "Biztos?", - "Are you sure you want to leave the room '%(roomName)s'?": "Biztos elhagyod a szobát?", + "Are you sure you want to leave the room '%(roomName)s'?": "Biztos elhagyod a szobát '%(roomName)s'?", "Are you sure you want to reject the invitation?": "Biztos elutasítod a meghívást?", "Are you sure you want to upload the following files?": "Biztos feltöltöd ezeket a fájlokat?", "Attachment": "Csatolmány", diff --git a/src/i18n/strings/nl.json b/src/i18n/strings/nl.json index dee54d9c97..e7a9f6e4b6 100644 --- a/src/i18n/strings/nl.json +++ b/src/i18n/strings/nl.json @@ -333,7 +333,7 @@ "Create an account": "Open een account", "Cryptography": "Cryptografie", "Current password": "Huidig wachtwoord", - "%(senderDisplayName)s removed the room name.": "%(senderDisplayName) heeft de naam van de kamer verwijderd.", + "%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s heeft de naam van de kamer verwijderd.", "Create a new chat or reuse an existing one": "Maak een nieuwe chat aan of gebruik een reeds bestaande", "Create Room": "Maak een kamer", "Curve25519 identity key": "Curve25519 identiteitssleutel", diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json index 8712d310e7..d69282a07e 100644 --- a/src/i18n/strings/ru.json +++ b/src/i18n/strings/ru.json @@ -246,7 +246,7 @@ "Failed to set up conference call": "Не удалось установить конференц-вызов", "Failed to verify email address: make sure you clicked the link in the email": "Не удалось подтвердить email-адрес: убедитесь что вы щелкнули по ссылке электронной почты", "Failure to create room": "Не удалось создать комнату", - "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId) изменил %(fromPowerLevel) на %(toPowerLevel)", + "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s изменил %(fromPowerLevel)s на %(toPowerLevel)s", "Guest users can't create new rooms. Please register to create room and start a chat.": "Гостевые пользователи не могут создавать новые комнаты. Зарегистрируйтесь для создания комнаты и чата.", "click to reveal": "нажать для открытия", "%(senderName)s invited %(targetName)s.": "%(senderName)s приглашает %(targetName)s.", @@ -355,7 +355,7 @@ "Friday": "Пятница", "Saturday": "Суббота", "Sunday": "Воскресенье", - "%(weekDayName)s %(time)s": "%(weekDayName) %(time)", + "%(weekDayName)s %(time)s": "%(weekDayName)s %(time)s", "Upload an avatar:": "Загрузите аватар:", "You need to be logged in.": "Вы должны быть авторизованы.", "You need to be able to invite users to do that.": "Вам необходимо пригласить пользователей чтобы сделать это.", @@ -524,7 +524,7 @@ "OK": "ОК", "Only people who have been invited": "Только приглашённые люди", "Passwords can't be empty": "Поля паролей не могут быть пустыми", - "%(senderName)s placed a %(callType)s call.": "%(senderName) выполнил %(callType) вызов.", + "%(senderName)s placed a %(callType)s call.": "%(senderName)s выполнил %(callType)s вызов.", "Please check your email and click on the link it contains. Once this is done, click continue.": "Пожалуйста, проверьте вашу электронную почту и нажмите в ней ссылку. По завершении нажмите продолжить.", "Power level must be positive integer.": "Уровень силы должен быть положительным числом.", "Press": "Нажать", @@ -946,8 +946,8 @@ "Would you like to accept or decline this invitation?": "Хотели бы вы подтвердить это приглашение или отклонить?", "(~%(count)s results).one": "(~%(count)s Результат)", "Can't connect to homeserver - please check your connectivity, ensure your homeserver's SSL certificate is trusted, and that a browser extension is not blocking requests.": "Не удается подключиться к домашнему серверу - проверьте подключение, убедитесь, что ваш сертификат SSL homeserver's SSL certificate действителен, и расширение браузера не блокирует запросы.", - "You have been banned from %(roomName)s by %(userName)s.": "%(userName) забанил Вас в % (roomName).", - "You have been kicked from %(roomName)s by %(userName)s.": "%(userName) выгнал Вас из %(roomName).", + "You have been banned from %(roomName)s by %(userName)s.": "%(userName)s забанил Вас в %(roomName)s.", + "You have been kicked from %(roomName)s by %(userName)s.": "%(userName)s выгнал Вас из %(roomName)s.", "You may wish to login with a different account, or add this email to this account.": "Вы можете войти в систему с другой учетной записью или добавить этот адрес email в эту учетную запись.", "Your home server does not support device management.": "Ваш домашний сервер не поддерживает управление устройствами.", "(could not connect media)": "(не удается подключиться к медиа)", diff --git a/src/i18n/strings/sv.json b/src/i18n/strings/sv.json index fe8b1ffee1..21bda3b741 100644 --- a/src/i18n/strings/sv.json +++ b/src/i18n/strings/sv.json @@ -296,7 +296,7 @@ "Active call (%(roomName)s)": "Aktiv samtal (%(roomName)s)", "Add": "Lägg till", "Admin tools": "Admin verktyg", - "And %(count)s more...": "Och %(count) till...", + "And %(count)s more...": "Och %(count)s till...", "Alias (optional)": "Alias (valfri)", "Can't connect to homeserver - please check your connectivity, ensure your homeserver's SSL certificate is trusted, and that a browser extension is not blocking requests.": "Det gick inte att ansluta till servern - kontrollera anslutningen, försäkra att din hemservers TLS-certifikat är betrott, och att inget webbläsartillägg blockerar förfrågningar.", "%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s ändrade maktnivån av %(powerLevelDiffText)s.", diff --git a/src/stores/RoomViewStore.js b/src/stores/RoomViewStore.js index 38f16f945b..2f7d55b71f 100644 --- a/src/stores/RoomViewStore.js +++ b/src/stores/RoomViewStore.js @@ -55,6 +55,8 @@ const INITIAL_STATE = { // pixelOffset: the number of pixels the window is scrolled down // from the focussedEvent. scrollStateMap: {}, + + forwardingEvent: null, }; /** @@ -116,6 +118,11 @@ class RoomViewStore extends Store { case 'update_scroll_state': this._updateScrollState(payload); break; + case 'forward_event': + this._setState({ + forwardingEvent: payload.event, + }); + break; } } @@ -127,6 +134,7 @@ class RoomViewStore extends Store { initialEventId: payload.event_id, initialEventPixelOffset: undefined, isInitialEventHighlighted: payload.highlighted, + forwardingEvent: null, roomLoading: false, roomLoadError: null, // should peek by default @@ -143,6 +151,14 @@ class RoomViewStore extends Store { } } + if (this._state.forwardingEvent) { + dis.dispatch({ + action: 'send_event', + room_id: newState.roomId, + event: this._state.forwardingEvent, + }); + } + this._setState(newState); } else if (payload.room_alias) { // Resolve the alias and then do a second dispatch with the room ID acquired @@ -279,6 +295,11 @@ class RoomViewStore extends Store { return this._state.joinError; } + // The mxEvent if one is about to be forwarded + getForwardingEvent() { + return this._state.forwardingEvent; + } + shouldPeek() { return this._state.shouldPeek; }