Merge remote-tracking branch 'origin/develop' into develop

This commit is contained in:
Weblate 2017-06-19 10:25:04 +00:00
commit a2764a0c1f
12 changed files with 297 additions and 210 deletions

View file

@ -35,26 +35,20 @@ import { _t } from './languageHandler';
* Called at startup, to attempt to build a logged-in Matrix session. It tries * Called at startup, to attempt to build a logged-in Matrix session. It tries
* a number of things: * 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. * 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. * 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 * 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. * turn will raise on_logged_in and will_start_client events.
* *
* @param {object} opts * @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 * @param {object} opts.fragmentQueryParams: string->string map of the
* query-parameters extracted from the #-fragment of the starting URI. * query-parameters extracted from the #-fragment of the starting URI.
* *
@ -68,9 +62,10 @@ import { _t } from './languageHandler';
* true; defines the IS to use. * true; defines the IS to use.
* *
* @returns {Promise} a promise which resolves when the above process completes. * @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) { export function loadSession(opts) {
const realQueryParams = opts.realQueryParams || {};
const fragmentQueryParams = opts.fragmentQueryParams || {}; const fragmentQueryParams = opts.fragmentQueryParams || {};
let enableGuest = opts.enableGuest || false; let enableGuest = opts.enableGuest || false;
const guestHsUrl = opts.guestHsUrl; const guestHsUrl = opts.guestHsUrl;
@ -82,14 +77,6 @@ export function loadSession(opts) {
enableGuest = false; 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 && if (enableGuest &&
fragmentQueryParams.guest_user_id && fragmentQueryParams.guest_user_id &&
fragmentQueryParams.guest_access_token fragmentQueryParams.guest_access_token
@ -101,12 +88,12 @@ export function loadSession(opts) {
homeserverUrl: guestHsUrl, homeserverUrl: guestHsUrl,
identityServerUrl: guestIsUrl, identityServerUrl: guestIsUrl,
guest: true, guest: true,
}, true); }, true).then(() => true);
} }
return _restoreFromLocalStorage().then((success) => { return _restoreFromLocalStorage().then((success) => {
if (success) { if (success) {
return; return true;
} }
if (enableGuest) { if (enableGuest) {
@ -114,10 +101,30 @@ export function loadSession(opts) {
} }
// fall back to login screen // 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 // create a temporary MatrixClient to do the login
const client = Matrix.createClient({ const client = Matrix.createClient({
baseUrl: queryParams.homeserver, baseUrl: queryParams.homeserver,
@ -130,17 +137,21 @@ function _loginWithToken(queryParams, defaultDeviceDisplayName) {
}, },
).then(function(data) { ).then(function(data) {
console.log("Logged in with token"); console.log("Logged in with token");
return _doSetLoggedIn({ return _clearStorage().then(() => {
_persistCredentialsToLocalStorage({
userId: data.user_id, userId: data.user_id,
deviceId: data.device_id, deviceId: data.device_id,
accessToken: data.access_token, accessToken: data.access_token,
homeserverUrl: queryParams.homeserver, homeserverUrl: queryParams.homeserver,
identityServerUrl: queryParams.identityServer, identityServerUrl: queryParams.identityServer,
guest: false, guest: false,
}, true); });
}, (err) => { return true;
});
}).catch((err) => {
console.error("Failed to log in with login token: " + err + " " + console.error("Failed to log in with login token: " + err + " " +
err.data); err.data);
return false;
}); });
} }
@ -168,9 +179,10 @@ function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) {
homeserverUrl: hsUrl, homeserverUrl: hsUrl,
identityServerUrl: isUrl, identityServerUrl: isUrl,
guest: true, guest: true,
}, true); }, true).then(() => true);
}, (err) => { }, (err) => {
console.error("Failed to register as guest: " + err + " " + err.data); 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. * storage before starting the new client.
* *
* @param {MatrixClientCreds} credentials The credentials to use * @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) { export function setLoggedIn(credentials) {
stopMatrixClient(); stopMatrixClient();
_doSetLoggedIn(credentials, true); return _doSetLoggedIn(credentials, true);
} }
/** /**
@ -295,7 +309,7 @@ export function setLoggedIn(credentials) {
* @param {MatrixClientCreds} credentials * @param {MatrixClientCreds} credentials
* @param {Boolean} clearStorage * @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) { async function _doSetLoggedIn(credentials, clearStorage) {
credentials.guest = Boolean(credentials.guest); credentials.guest = Boolean(credentials.guest);
@ -322,23 +336,10 @@ async function _doSetLoggedIn(credentials, clearStorage) {
// Resolves by default // Resolves by default
let teamPromise = Promise.resolve(null); let teamPromise = Promise.resolve(null);
// persist the session
if (localStorage) { if (localStorage) {
try { try {
localStorage.setItem("mx_hs_url", credentials.homeserverUrl); _persistCredentialsToLocalStorage(credentials);
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);
}
// The user registered as a PWLU (PassWord-Less User), the generated password // 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. // 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, cachedPassword: credentials.password,
}); });
} }
console.log("Session persisted for %s", credentials.userId);
} catch (e) { } catch (e) {
console.warn("Error using local storage: can't persist session!", e); console.warn("Error using local storage: can't persist session!", e);
} }
@ -377,6 +376,26 @@ async function _doSetLoggedIn(credentials, clearStorage) {
}); });
startMatrixClient(); 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);
} }
/** /**

View file

@ -43,7 +43,41 @@ import createRoom from "../../createRoom";
import * as UDEHandler from '../../UnknownDeviceErrorHandler'; import * as UDEHandler from '../../UnknownDeviceErrorHandler';
import { _t, getCurrentLanguage } from '../../languageHandler'; 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({ module.exports = React.createClass({
// we export this so that the integration tests can use it :-S
statics: {
VIEWS: VIEWS,
},
displayName: 'MatrixChat', displayName: 'MatrixChat',
propTypes: { propTypes: {
@ -59,8 +93,8 @@ module.exports = React.createClass({
// the initial queryParams extracted from the hash-fragment of the URI // the initial queryParams extracted from the hash-fragment of the URI
startingFragmentQueryParams: React.PropTypes.object, startingFragmentQueryParams: React.PropTypes.object,
// called when the session load completes // called when we have completed a token login
onLoadCompleted: React.PropTypes.func, onTokenLoginCompleted: React.PropTypes.func,
// Represents the screen to display as a result of parsing the initial // Represents the screen to display as a result of parsing the initial
// window.location // window.location
@ -93,8 +127,10 @@ module.exports = React.createClass({
getInitialState: function() { getInitialState: function() {
const s = { const s = {
loading: true, // the master view we are showing.
screen: undefined, view: VIEWS.LOADING,
// a thing to call showScreen with once login completes.
screenAfterLogin: this.props.initialScreenAfterLogin, screenAfterLogin: this.props.initialScreenAfterLogin,
// Stashed guest credentials if the user logs out // 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 // If we're trying to just view a user ID (i.e. /user URL), this is it
viewUserId: null, viewUserId: null,
loggedIn: false,
loggingIn: false,
collapse_lhs: false, collapse_lhs: false,
collapse_rhs: false, collapse_rhs: false,
ready: false, ready: false,
@ -143,7 +177,7 @@ module.exports = React.createClass({
realQueryParams: {}, realQueryParams: {},
startingFragmentQueryParams: {}, startingFragmentQueryParams: {},
config: {}, config: {},
onLoadCompleted: () => {}, onTokenLoginCompleted: () => {},
}; };
}, },
@ -266,16 +300,24 @@ module.exports = React.createClass({
const teamServerConfig = this.props.config.teamServerConfig || {}; const teamServerConfig = this.props.config.teamServerConfig || {};
Lifecycle.initRtsClient(teamServerConfig.teamServerURL); Lifecycle.initRtsClient(teamServerConfig.teamServerURL);
// 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();
// don't do anything else until the page reloads - just stay in
// the 'loading' state.
return;
}
// if the user has followed a login or register link, don't reanimate // 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 old creds, but rather go straight to the relevant page
const firstScreen = this.state.screenAfterLogin ? const firstScreen = this.state.screenAfterLogin ?
this.state.screenAfterLogin.screen : null; this.state.screenAfterLogin.screen : null;
if (firstScreen === 'login' || if (firstScreen === 'login' ||
firstScreen === 'register' || firstScreen === 'register' ||
firstScreen === 'forgot_password') { firstScreen === 'forgot_password') {
this.props.onLoadCompleted();
this.setState({loading: false}); this.setState({loading: false});
this._showScreenAfterLogin(); this._showScreenAfterLogin();
return; return;
@ -283,9 +325,8 @@ module.exports = React.createClass({
// the extra q() ensures that synchronous exceptions hit the same codepath as // the extra q() ensures that synchronous exceptions hit the same codepath as
// asynchronous ones. // asynchronous ones.
q().then(() => { return q().then(() => {
return Lifecycle.loadSession({ return Lifecycle.loadSession({
realQueryParams: this.props.realQueryParams,
fragmentQueryParams: this.props.startingFragmentQueryParams, fragmentQueryParams: this.props.startingFragmentQueryParams,
enableGuest: this.props.enableGuest, enableGuest: this.props.enableGuest,
guestHsUrl: this.getCurrentHsUrl(), guestHsUrl: this.getCurrentHsUrl(),
@ -294,11 +335,14 @@ module.exports = React.createClass({
}); });
}).catch((e) => { }).catch((e) => {
console.error("Unable to load session", e); console.error("Unable to load session", e);
}).done(()=>{ return false;
// stuff this through the dispatcher so that it happens }).then((loadedSession) => {
// after the on_logged_in action. if (!loadedSession) {
dis.dispatch({action: 'load_completed'}); // fall back to showing the login screen
dis.dispatch({action: "start_login"});
}
}); });
}).done();
}, },
componentWillUnmount: function() { 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 = { const newState = {
screen: undefined,
viewUserId: null, viewUserId: null,
loggedIn: false,
ready: false,
}; };
Object.assign(newState, state); Object.assign(newState, state);
this.setState(newState); this.setState(newState);
}, },
onAction: function(payload) { onAction: function(payload) {
// console.log(`MatrixClientPeg.onAction: ${payload.action}`);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
@ -347,19 +392,19 @@ module.exports = React.createClass({
guestCreds: MatrixClientPeg.getCredentials(), guestCreds: MatrixClientPeg.getCredentials(),
}); });
} }
this.setStateForNewScreen({ this.setStateForNewView({
screen: 'login', view: VIEWS.LOGIN,
}); });
this.notifyNewScreen('login'); this.notifyNewScreen('login');
break; break;
case 'start_post_registration': case 'start_post_registration':
this.setState({ // don't clobber loggedIn status this.setState({
screen: 'post_registration', view: VIEWS.POST_REGISTRATION,
}); });
break; break;
case 'start_password_recovery': case 'start_password_recovery':
this.setStateForNewScreen({ this.setStateForNewView({
screen: 'forgot_password', view: VIEWS.FORGOT_PASSWORD,
}); });
this.notifyNewScreen('forgot_password'); this.notifyNewScreen('forgot_password');
break; break;
@ -503,7 +548,10 @@ module.exports = React.createClass({
// and also that we're not ready (we'll be marked as logged // and also that we're not ready (we'll be marked as logged
// in once the login completes, then ready once the sync // in once the login completes, then ready once the sync
// completes). // completes).
this.setState({loggingIn: true, ready: false}); this.setStateForNewView({
view: VIEWS.LOGGING_IN,
ready: false,
});
break; break;
case 'on_logged_in': case 'on_logged_in':
this._onLoggedIn(payload.teamToken); this._onLoggedIn(payload.teamToken);
@ -514,15 +562,15 @@ module.exports = React.createClass({
case 'will_start_client': case 'will_start_client':
this._onWillStartClient(); this._onWillStartClient();
break; break;
case 'load_completed':
this._onLoadCompleted();
break;
case 'new_version': case 'new_version':
this.onVersion( this.onVersion(
payload.currentVersion, payload.newVersion, payload.currentVersion, payload.newVersion,
payload.releaseNotes, payload.releaseNotes,
); );
break; break;
case 'send_event':
this.onSendEvent(payload.room_id, payload.event);
break;
} }
}, },
@ -537,8 +585,8 @@ module.exports = React.createClass({
}, },
_startRegistration: function(params) { _startRegistration: function(params) {
this.setStateForNewScreen({ this.setStateForNewView({
screen: 'register', view: VIEWS.REGISTER,
// these params may be undefined, but if they are, // these params may be undefined, but if they are,
// unset them from our state: we don't want to // unset them from our state: we don't want to
// resume a previous registration session if the // 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 * Called whenever someone changes the theme
* *
@ -906,9 +946,8 @@ module.exports = React.createClass({
*/ */
_onLoggedIn: function(teamToken) { _onLoggedIn: function(teamToken) {
this.setState({ this.setState({
view: VIEWS.LOGGED_IN,
guestCreds: null, guestCreds: null,
loggedIn: true,
loggingIn: false,
}); });
if (teamToken) { if (teamToken) {
@ -969,8 +1008,8 @@ module.exports = React.createClass({
*/ */
_onLoggedOut: function() { _onLoggedOut: function() {
this.notifyNewScreen('login'); this.notifyNewScreen('login');
this.setStateForNewScreen({ this.setStateForNewView({
loggedIn: false, view: VIEWS.LOGIN,
ready: false, ready: false,
collapse_lhs: false, collapse_lhs: false,
collapse_rhs: false, collapse_rhs: false,
@ -1133,7 +1172,7 @@ module.exports = React.createClass({
// we can't view a room unless we're logged in // we can't view a room unless we're logged in
// (a guest account is fine) // (a guest account is fine)
if (this.state.loggedIn) { if (this.state.view === VIEWS.LOGGED_IN) {
dis.dispatch(payload); dis.dispatch(payload);
} }
} else if (screen.indexOf('user/') == 0) { } 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) { onRegistered: function(credentials, teamToken) {
// XXX: These both should be in state or ideally store(s) because we risk not // 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. // rendering the most up-to-date view of state otherwise.
// teamToken may not be truthy // teamToken may not be truthy
this._teamToken = teamToken; this._teamToken = teamToken;
this._is_registered = true; this._is_registered = true;
Lifecycle.setLoggedIn(credentials); return Lifecycle.setLoggedIn(credentials);
}, },
onFinishPostRegistration: function() { onFinishPostRegistration: function() {
// Don't confuse this with "PageType" which is the middle window to show // Don't confuse this with "PageType" which is the middle window to show
this.setState({ this.setState({
screen: undefined, view: VIEWS.LOGGED_IN,
}); });
this.showScreen("settings"); 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) { updateStatusIndicator: function(state, prevState) {
let notifCount = 0; let notifCount = 0;
@ -1321,11 +1382,9 @@ module.exports = React.createClass({
}, },
render: function() { render: function() {
// `loading` might be set to false before `loggedIn = true`, causing the default // console.log(`Rendering MatrixChat with view ${this.state.view}`);
// (`<Login>`) 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 if (this.state.view === VIEWS.LOADING || this.state.view === VIEWS.LOGGING_IN) {
// actions `on_logging_in` and `on_logged_in`.
if (this.state.loading || this.state.loggingIn) {
const Spinner = sdk.getComponent('elements.Spinner'); const Spinner = sdk.getComponent('elements.Spinner');
return ( return (
<div className="mx_MatrixChat_splash"> <div className="mx_MatrixChat_splash">
@ -1335,7 +1394,7 @@ module.exports = React.createClass({
} }
// needs to be before normal PageTypes as you are logged in technically // 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'); const PostRegistration = sdk.getComponent('structures.login.PostRegistration');
return ( return (
<PostRegistration <PostRegistration
@ -1343,10 +1402,11 @@ module.exports = React.createClass({
); );
} }
// `ready` and `loggedIn` may be set before `page_type` (because the 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`, // latter is set via the dispatcher). If we don't yet have a `page_type`,
// keep showing the spinner for now. // keep showing the spinner for now.
if (this.state.loggedIn && this.state.ready && this.state.page_type) { if (this.state.ready && this.state.page_type) {
/* for now, we stuff the entirety of our props and state into the LoggedInView. /* 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 * 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. * as using something like redux to avoid having a billion bits of state kicking around.
@ -1363,7 +1423,7 @@ module.exports = React.createClass({
{...this.state} {...this.state}
/> />
); );
} else if (this.state.loggedIn) { } else {
// we think we are logged in, but are still waiting for the /sync to complete // we think we are logged in, but are still waiting for the /sync to complete
const Spinner = sdk.getComponent('elements.Spinner'); const Spinner = sdk.getComponent('elements.Spinner');
return ( return (
@ -1374,7 +1434,10 @@ module.exports = React.createClass({
</a> </a>
</div> </div>
); );
} else if (this.state.screen == 'register') { }
}
if (this.state.view === VIEWS.REGISTER) {
const Registration = sdk.getComponent('structures.login.Registration'); const Registration = sdk.getComponent('structures.login.Registration');
return ( return (
<Registration <Registration
@ -1397,7 +1460,10 @@ module.exports = React.createClass({
onCancelClick={this.state.guestCreds ? this.onReturnToGuestClick : null} onCancelClick={this.state.guestCreds ? this.onReturnToGuestClick : null}
/> />
); );
} else if (this.state.screen == 'forgot_password') { }
if (this.state.view === VIEWS.FORGOT_PASSWORD) {
const ForgotPassword = sdk.getComponent('structures.login.ForgotPassword'); const ForgotPassword = sdk.getComponent('structures.login.ForgotPassword');
return ( return (
<ForgotPassword <ForgotPassword
@ -1409,7 +1475,9 @@ module.exports = React.createClass({
onRegisterClick={this.onRegisterClick} onRegisterClick={this.onRegisterClick}
onLoginClick={this.onLoginClick} /> onLoginClick={this.onLoginClick} />
); );
} else { }
if (this.state.view === VIEWS.LOGIN) {
const Login = sdk.getComponent('structures.login.Login'); const Login = sdk.getComponent('structures.login.Login');
return ( return (
<Login <Login
@ -1427,5 +1495,7 @@ module.exports = React.createClass({
/> />
); );
} }
console.error(`Unknown view ${this.state.view}`);
}, },
}); });

View file

@ -169,6 +169,7 @@ module.exports = React.createClass({
initialEventId: RoomViewStore.getInitialEventId(), initialEventId: RoomViewStore.getInitialEventId(),
initialEventPixelOffset: RoomViewStore.getInitialEventPixelOffset(), initialEventPixelOffset: RoomViewStore.getInitialEventPixelOffset(),
isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(), isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(),
forwardingEvent: RoomViewStore.getForwardingEvent(),
shouldPeek: RoomViewStore.shouldPeek(), shouldPeek: RoomViewStore.shouldPeek(),
}; };
@ -457,11 +458,6 @@ module.exports = React.createClass({
callState: callState callState: callState
}); });
break;
case 'forward_event':
this.setState({
forwardingEvent: payload.content,
});
break; break;
} }
}, },
@ -1169,8 +1165,13 @@ module.exports = React.createClass({
this.updateTint(); this.updateTint();
this.setState({ this.setState({
editingRoomSettings: false, editingRoomSettings: false,
forwardingEvent: null,
}); });
if (this.state.forwardingEvent) {
dis.dispatch({
action: 'forward_event',
event: null,
});
}
dis.dispatch({action: 'focus_composer'}); dis.dispatch({action: 'focus_composer'});
}, },
@ -1581,7 +1582,7 @@ module.exports = React.createClass({
} else if (this.state.uploadingRoomSettings) { } else if (this.state.uploadingRoomSettings) {
aux = <Loader/>; aux = <Loader/>;
} else if (this.state.forwardingEvent !== null) { } else if (this.state.forwardingEvent !== null) {
aux = <ForwardMessage onCancelClick={this.onCancelClick} currentRoomId={this.state.room.roomId} mxEvent={this.state.forwardingEvent} />; aux = <ForwardMessage onCancelClick={this.onCancelClick} />;
} else if (this.state.searching) { } else if (this.state.searching) {
hideCancel = true; // has own cancel hideCancel = true; // has own cancel
aux = <SearchBar ref="search_bar" searchInProgress={this.state.searchInProgress } onCancelClick={this.onCancelSearchClick} onSearch={this.onSearch}/>; aux = <SearchBar ref="search_bar" searchInProgress={this.state.searchInProgress } onCancelClick={this.onCancelSearchClick} onSearch={this.onSearch}/>;

View file

@ -160,6 +160,10 @@ module.exports = React.createClass({
this.checkFillState(); this.checkFillState();
}, },
componentWillUpdate: function(nextProps, nextState) {
this._saveScrollState();
},
componentDidUpdate: function() { componentDidUpdate: function() {
// after adding event tiles, we may need to tweak the scroll (either to // 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 // keep at the bottom of the timeline, or to maintain the view after

View file

@ -218,29 +218,29 @@ module.exports = React.createClass({
} }
trackPromise.then((teamToken) => { trackPromise.then((teamToken) => {
this.props.onLoggedIn({ return this.props.onLoggedIn({
userId: response.user_id, userId: response.user_id,
deviceId: response.device_id, deviceId: response.device_id,
homeserverUrl: this._matrixClient.getHomeserverUrl(), homeserverUrl: this._matrixClient.getHomeserverUrl(),
identityServerUrl: this._matrixClient.getIdentityServerUrl(), identityServerUrl: this._matrixClient.getIdentityServerUrl(),
accessToken: response.access_token accessToken: response.access_token
}, teamToken); }, teamToken);
}).then(() => { }).then((cli) => {
return this._setupPushers(); return this._setupPushers(cli);
}); });
}, },
_setupPushers: function() { _setupPushers: function(matrixClient) {
if (!this.props.brand) { if (!this.props.brand) {
return q(); return q();
} }
return MatrixClientPeg.get().getPushers().then((resp)=>{ return matrixClient.getPushers().then((resp)=>{
const pushers = resp.pushers; const pushers = resp.pushers;
for (let i = 0; i < pushers.length; ++i) { for (let i = 0; i < pushers.length; ++i) {
if (pushers[i].kind == 'email') { if (pushers[i].kind == 'email') {
const emailPusher = pushers[i]; const emailPusher = pushers[i];
emailPusher.data = { brand: this.props.brand }; emailPusher.data = { brand: this.props.brand };
MatrixClientPeg.get().setPusher(emailPusher).done(() => { matrixClient.setPusher(emailPusher).done(() => {
console.log("Set email branding to " + this.props.brand); console.log("Set email branding to " + this.props.brand);
}, (error) => { }, (error) => {
console.error("Couldn't set email branding: " + error); console.error("Couldn't set email branding: " + error);

View file

@ -17,7 +17,6 @@
import React from 'react'; import React from 'react';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import MatrixClientPeg from '../../../MatrixClientPeg';
import dis from '../../../dispatcher'; import dis from '../../../dispatcher';
import KeyCode from '../../../KeyCode'; import KeyCode from '../../../KeyCode';
@ -26,11 +25,6 @@ module.exports = React.createClass({
displayName: 'ForwardMessage', displayName: 'ForwardMessage',
propTypes: { propTypes: {
currentRoomId: React.PropTypes.string.isRequired,
/* the MatrixEvent to be forwarded */
mxEvent: React.PropTypes.object.isRequired,
onCancelClick: React.PropTypes.func.isRequired, onCancelClick: React.PropTypes.func.isRequired,
}, },
@ -44,7 +38,6 @@ module.exports = React.createClass({
}, },
componentDidMount: function() { componentDidMount: function() {
this.dispatcherRef = dis.register(this.onAction);
document.addEventListener('keydown', this._onKeyDown); document.addEventListener('keydown', this._onKeyDown);
}, },
@ -54,30 +47,9 @@ module.exports = React.createClass({
sideOpacity: 1.0, sideOpacity: 1.0,
middleOpacity: 1.0, middleOpacity: 1.0,
}); });
dis.unregister(this.dispatcherRef);
document.removeEventListener('keydown', this._onKeyDown); 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) { _onKeyDown: function(ev) {
switch (ev.keyCode) { switch (ev.keyCode) {
case KeyCode.ESCAPE: case KeyCode.ESCAPE:

View file

@ -839,13 +839,13 @@
"Share message history with new users": "Διαμοιρασμός ιστορικού μηνυμάτων με τους νέους χρήστες", "Share message history with new users": "Διαμοιρασμός ιστορικού μηνυμάτων με τους νέους χρήστες",
"numbullet": "απαρίθμηση", "numbullet": "απαρίθμηση",
"%(severalUsers)sleft and rejoined %(repeats)s times": "%(severalUsers)s έφυγαν και ξανασυνδέθηκαν %(repeats)s φορές", "%(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 έφυγαν και ξανασυνδέθηκαν", "%(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 φορές", "%(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 απέσυραν τις προσκλήσεις τους", "%(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": "Πρέπει να συνδεθείτε στο δωμάτιο για να δείτε τα αρχεία του", "You must join the room to see its files": "Πρέπει να συνδεθείτε στο δωμάτιο για να δείτε τα αρχεία του",
"Reject all %(invitedRooms)s invites": "Απόρριψη όλων των προσκλήσεων %(invitedRooms)s", "Reject all %(invitedRooms)s invites": "Απόρριψη όλων των προσκλήσεων %(invitedRooms)s",
"Failed to invite the following users to the %(roomName)s room:": "Δεν ήταν δυνατή η πρόσκληση των χρηστών στο δωμάτιο %(roomName)s:", "Failed to invite the following users to the %(roomName)s room:": "Δεν ήταν δυνατή η πρόσκληση των χρηστών στο δωμάτιο %(roomName)s:",

View file

@ -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, 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", "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?": "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 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?", "Are you sure you want to upload the following files?": "Biztos feltöltöd ezeket a fájlokat?",
"Attachment": "Csatolmány", "Attachment": "Csatolmány",

View file

@ -333,7 +333,7 @@
"Create an account": "Open een account", "Create an account": "Open een account",
"Cryptography": "Cryptografie", "Cryptography": "Cryptografie",
"Current password": "Huidig wachtwoord", "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 a new chat or reuse an existing one": "Maak een nieuwe chat aan of gebruik een reeds bestaande",
"Create Room": "Maak een kamer", "Create Room": "Maak een kamer",
"Curve25519 identity key": "Curve25519 identiteitssleutel", "Curve25519 identity key": "Curve25519 identiteitssleutel",

View file

@ -246,7 +246,7 @@
"Failed to set up conference call": "Не удалось установить конференц-вызов", "Failed to set up conference call": "Не удалось установить конференц-вызов",
"Failed to verify email address: make sure you clicked the link in the email": "Не удалось подтвердить email-адрес: убедитесь что вы щелкнули по ссылке электронной почты", "Failed to verify email address: make sure you clicked the link in the email": "Не удалось подтвердить email-адрес: убедитесь что вы щелкнули по ссылке электронной почты",
"Failure to create room": "Не удалось создать комнату", "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.": "Гостевые пользователи не могут создавать новые комнаты. Зарегистрируйтесь для создания комнаты и чата.", "Guest users can't create new rooms. Please register to create room and start a chat.": "Гостевые пользователи не могут создавать новые комнаты. Зарегистрируйтесь для создания комнаты и чата.",
"click to reveal": "нажать для открытия", "click to reveal": "нажать для открытия",
"%(senderName)s invited %(targetName)s.": "%(senderName)s приглашает %(targetName)s.", "%(senderName)s invited %(targetName)s.": "%(senderName)s приглашает %(targetName)s.",
@ -355,7 +355,7 @@
"Friday": "Пятница", "Friday": "Пятница",
"Saturday": "Суббота", "Saturday": "Суббота",
"Sunday": "Воскресенье", "Sunday": "Воскресенье",
"%(weekDayName)s %(time)s": "%(weekDayName) %(time)", "%(weekDayName)s %(time)s": "%(weekDayName)s %(time)s",
"Upload an avatar:": "Загрузите аватар:", "Upload an avatar:": "Загрузите аватар:",
"You need to be logged in.": "Вы должны быть авторизованы.", "You need to be logged in.": "Вы должны быть авторизованы.",
"You need to be able to invite users to do that.": "Вам необходимо пригласить пользователей чтобы сделать это.", "You need to be able to invite users to do that.": "Вам необходимо пригласить пользователей чтобы сделать это.",
@ -524,7 +524,7 @@
"OK": "ОК", "OK": "ОК",
"Only people who have been invited": "Только приглашённые люди", "Only people who have been invited": "Только приглашённые люди",
"Passwords can't be empty": "Поля паролей не могут быть пустыми", "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.": "Пожалуйста, проверьте вашу электронную почту и нажмите в ней ссылку. По завершении нажмите продолжить.", "Please check your email and click on the link it contains. Once this is done, click continue.": "Пожалуйста, проверьте вашу электронную почту и нажмите в ней ссылку. По завершении нажмите продолжить.",
"Power level must be positive integer.": "Уровень силы должен быть положительным числом.", "Power level must be positive integer.": "Уровень силы должен быть положительным числом.",
"Press": "Нажать", "Press": "Нажать",
@ -946,8 +946,8 @@
"Would you like to <acceptText>accept</acceptText> or <declineText>decline</declineText> this invitation?": "Хотели бы вы <acceptText>подтвердить</acceptText> это приглашение или <declineText>отклонить</declineText>?", "Would you like to <acceptText>accept</acceptText> or <declineText>decline</declineText> this invitation?": "Хотели бы вы <acceptText>подтвердить</acceptText> это приглашение или <declineText>отклонить</declineText>?",
"(~%(count)s results).one": "(~%(count)s Результат)", "(~%(count)s results).one": "(~%(count)s Результат)",
"Can't connect to homeserver - please check your connectivity, ensure your <a>homeserver's SSL certificate</a> is trusted, and that a browser extension is not blocking requests.": "Не удается подключиться к домашнему серверу - проверьте подключение, убедитесь, что ваш сертификат SSL <a>homeserver's SSL certificate</a> действителен, и расширение браузера не блокирует запросы.", "Can't connect to homeserver - please check your connectivity, ensure your <a>homeserver's SSL certificate</a> is trusted, and that a browser extension is not blocking requests.": "Не удается подключиться к домашнему серверу - проверьте подключение, убедитесь, что ваш сертификат SSL <a>homeserver's SSL certificate</a> действителен, и расширение браузера не блокирует запросы.",
"You have been banned 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) выгнал Вас из %(roomName).", "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 в эту учетную запись.", "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.": "Ваш домашний сервер не поддерживает управление устройствами.", "Your home server does not support device management.": "Ваш домашний сервер не поддерживает управление устройствами.",
"(could not connect media)": "(не удается подключиться к медиа)", "(could not connect media)": "(не удается подключиться к медиа)",

View file

@ -296,7 +296,7 @@
"Active call (%(roomName)s)": "Aktiv samtal (%(roomName)s)", "Active call (%(roomName)s)": "Aktiv samtal (%(roomName)s)",
"Add": "Lägg till", "Add": "Lägg till",
"Admin tools": "Admin verktyg", "Admin tools": "Admin verktyg",
"And %(count)s more...": "Och %(count) till...", "And %(count)s more...": "Och %(count)s till...",
"Alias (optional)": "Alias (valfri)", "Alias (optional)": "Alias (valfri)",
"Can't connect to homeserver - please check your connectivity, ensure your <a>homeserver's SSL certificate</a> 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 <a>hemservers TLS-certifikat</a> är betrott, och att inget webbläsartillägg blockerar förfrågningar.", "Can't connect to homeserver - please check your connectivity, ensure your <a>homeserver's SSL certificate</a> 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 <a>hemservers TLS-certifikat</a> ä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.", "%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s ändrade maktnivån av %(powerLevelDiffText)s.",

View file

@ -55,6 +55,8 @@ const INITIAL_STATE = {
// pixelOffset: the number of pixels the window is scrolled down // pixelOffset: the number of pixels the window is scrolled down
// from the focussedEvent. // from the focussedEvent.
scrollStateMap: {}, scrollStateMap: {},
forwardingEvent: null,
}; };
/** /**
@ -116,6 +118,11 @@ class RoomViewStore extends Store {
case 'update_scroll_state': case 'update_scroll_state':
this._updateScrollState(payload); this._updateScrollState(payload);
break; break;
case 'forward_event':
this._setState({
forwardingEvent: payload.event,
});
break;
} }
} }
@ -127,6 +134,7 @@ class RoomViewStore extends Store {
initialEventId: payload.event_id, initialEventId: payload.event_id,
initialEventPixelOffset: undefined, initialEventPixelOffset: undefined,
isInitialEventHighlighted: payload.highlighted, isInitialEventHighlighted: payload.highlighted,
forwardingEvent: null,
roomLoading: false, roomLoading: false,
roomLoadError: null, roomLoadError: null,
// should peek by default // 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); this._setState(newState);
} else if (payload.room_alias) { } else if (payload.room_alias) {
// Resolve the alias and then do a second dispatch with the room ID acquired // 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; return this._state.joinError;
} }
// The mxEvent if one is about to be forwarded
getForwardingEvent() {
return this._state.forwardingEvent;
}
shouldPeek() { shouldPeek() {
return this._state.shouldPeek; return this._state.shouldPeek;
} }