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
* 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({
return _clearStorage().then(() => {
_persistCredentialsToLocalStorage({
userId: data.user_id,
deviceId: data.device_id,
accessToken: data.access_token,
homeserverUrl: queryParams.homeserver,
identityServerUrl: queryParams.identityServer,
guest: false,
}, true);
}, (err) => {
});
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);
}
/**

View File

@ -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,16 +300,24 @@ module.exports = React.createClass({
const teamServerConfig = this.props.config.teamServerConfig || {};
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
// the old creds, but rather go straight to the relevant page
const firstScreen = this.state.screenAfterLogin ?
this.state.screenAfterLogin.screen : null;
if (firstScreen === 'login' ||
firstScreen === 'register' ||
firstScreen === 'forgot_password') {
this.props.onLoadCompleted();
this.setState({loading: false});
this._showScreenAfterLogin();
return;
@ -283,9 +325,8 @@ module.exports = React.createClass({
// the extra q() ensures that synchronous exceptions hit the same codepath as
// asynchronous ones.
q().then(() => {
return q().then(() => {
return Lifecycle.loadSession({
realQueryParams: this.props.realQueryParams,
fragmentQueryParams: this.props.startingFragmentQueryParams,
enableGuest: this.props.enableGuest,
guestHsUrl: this.getCurrentHsUrl(),
@ -294,11 +335,14 @@ module.exports = React.createClass({
});
}).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'});
return false;
}).then((loadedSession) => {
if (!loadedSession) {
// fall back to showing the login screen
dis.dispatch({action: "start_login"});
}
});
}).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
// (`<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
// 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 (
<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
if (this.state.screen == 'post_registration') {
if (this.state.view === VIEWS.POST_REGISTRATION) {
const PostRegistration = sdk.getComponent('structures.login.PostRegistration');
return (
<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`,
// 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.
* 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.
@ -1363,7 +1423,7 @@ module.exports = React.createClass({
{...this.state}
/>
);
} else if (this.state.loggedIn) {
} else {
// we think we are logged in, but are still waiting for the /sync to complete
const Spinner = sdk.getComponent('elements.Spinner');
return (
@ -1374,7 +1434,10 @@ module.exports = React.createClass({
</a>
</div>
);
} else if (this.state.screen == 'register') {
}
}
if (this.state.view === VIEWS.REGISTER) {
const Registration = sdk.getComponent('structures.login.Registration');
return (
<Registration
@ -1397,7 +1460,10 @@ module.exports = React.createClass({
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');
return (
<ForgotPassword
@ -1409,7 +1475,9 @@ module.exports = React.createClass({
onRegisterClick={this.onRegisterClick}
onLoginClick={this.onLoginClick} />
);
} else {
}
if (this.state.view === VIEWS.LOGIN) {
const Login = sdk.getComponent('structures.login.Login');
return (
<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(),
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 = <Loader/>;
} 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) {
hideCancel = true; // has own cancel
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();
},
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

View File

@ -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);

View File

@ -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:

View File

@ -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:",

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, 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",

View File

@ -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",

View File

@ -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 <acceptText>accept</acceptText> or <declineText>decline</declineText> this invitation?": "Хотели бы вы <acceptText>подтвердить</acceptText> это приглашение или <declineText>отклонить</declineText>?",
"(~%(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> действителен, и расширение браузера не блокирует запросы.",
"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)": "(не удается подключиться к медиа)",

View File

@ -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 <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.",

View File

@ -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;
}