Merge in changes from develop

This commit is contained in:
Richard Lewis 2017-06-27 11:44:36 +01:00
commit ddc0da396d
75 changed files with 5789 additions and 955 deletions

View file

@ -212,6 +212,7 @@ export default React.createClass({
const HomePage = sdk.getComponent('structures.HomePage');
const MatrixToolbar = sdk.getComponent('globals.MatrixToolbar');
const NewVersionBar = sdk.getComponent('globals.NewVersionBar');
const UpdateCheckBar = sdk.getComponent('globals.UpdateCheckBar');
const PasswordNagBar = sdk.getComponent('globals.PasswordNagBar');
let page_element;
@ -239,7 +240,6 @@ export default React.createClass({
page_element = <UserSettings
onClose={this.props.onUserSettingsClose}
brand={this.props.config.brand}
collapsedRhs={this.props.collapse_rhs}
enableLabs={this.props.config.enableLabs}
referralBaseUrl={this.props.config.referralBaseUrl}
teamToken={this.props.teamToken}
@ -270,7 +270,6 @@ export default React.createClass({
this.props.config.teamServerConfig.teamServerURL : null;
page_element = <HomePage
collapsedRhs={this.props.collapse_rhs}
teamServerUrl={teamServerUrl}
teamToken={this.props.teamToken}
homePageUrl={this.props.config.welcomePageUrl}
@ -283,12 +282,14 @@ export default React.createClass({
break;
}
let topBar;
const isGuest = this.props.matrixClient.isGuest();
var topBar;
if (this.props.hasNewVersion) {
topBar = <NewVersionBar version={this.props.version} newVersion={this.props.newVersion}
releaseNotes={this.props.newVersionReleaseNotes}
releaseNotes={this.props.newVersionReleaseNotes}
/>;
} else if (this.props.checkingForUpdate) {
topBar = <UpdateCheckBar {...this.props.checkingForUpdate} />;
} else if (this.state.userHasGeneratedPassword) {
topBar = <PasswordNagBar />;
} else if (!isGuest && Notifier.supportsDesktopNotifications() && !Notifier.isEnabled() && !Notifier.isToolbarHidden()) {

View file

@ -41,9 +41,44 @@ import PageTypes from '../../PageTypes';
import createRoom from "../../createRoom";
import * as UDEHandler from '../../UnknownDeviceErrorHandler';
import KeyRequestHandler from '../../KeyRequestHandler';
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 +94,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,14 +128,11 @@ module.exports = React.createClass({
getInitialState: function() {
const s = {
loading: true,
screen: undefined,
screenAfterLogin: this.props.initialScreenAfterLogin,
// the master view we are showing.
view: VIEWS.LOADING,
// Stashed guest credentials if the user logs out
// whilst logged in as a guest user (so they can change
// their mind & log back in)
guestCreds: null,
// a thing to call showScreen with once login completes.
screenAfterLogin: this.props.initialScreenAfterLogin,
// What the LoggedInView would be showing if visible
page_type: null,
@ -113,8 +145,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,
@ -127,6 +157,7 @@ module.exports = React.createClass({
newVersion: null,
hasNewVersion: false,
newVersionReleaseNotes: null,
checkingForUpdate: null,
// Parameters used in the registration dance with the IS
register_client_secret: null,
@ -143,7 +174,7 @@ module.exports = React.createClass({
realQueryParams: {},
startingFragmentQueryParams: {},
config: {},
onLoadCompleted: () => {},
onTokenLoginCompleted: () => {},
};
},
@ -263,30 +294,52 @@ module.exports = React.createClass({
window.addEventListener('resize', this.handleResize);
this.handleResize();
if (this.props.config.teamServerConfig &&
this.props.config.teamServerConfig.teamServerURL
) {
Lifecycle.initRtsClient(this.props.config.teamServerConfig.teamServerURL);
}
const teamServerConfig = this.props.config.teamServerConfig || {};
Lifecycle.initRtsClient(teamServerConfig.teamServerURL);
// 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,
// 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.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(`Error attempting 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() {
@ -305,18 +358,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");
@ -328,26 +382,19 @@ module.exports = React.createClass({
this._startRegistration(payload.params || {});
break;
case 'start_login':
if (MatrixClientPeg.get() &&
MatrixClientPeg.get().isGuest()
) {
this.setState({
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;
@ -371,7 +418,7 @@ module.exports = React.createClass({
MatrixClientPeg.get().leave(payload.room_id).done(() => {
modal.close();
if (this.currentRoomId === payload.room_id) {
if (this.state.currentRoomId === payload.room_id) {
dis.dispatch({action: 'view_next_room'});
}
}, (err) => {
@ -488,10 +535,11 @@ module.exports = React.createClass({
break;
case 'on_logging_in':
// We are now logging in, so set the state to reflect that
// 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});
// NB. This does not touch 'ready' since if our dispatches
// are delayed, the sync could already have completed
this.setStateForNewView({
view: VIEWS.LOGGING_IN,
});
break;
case 'on_logged_in':
this._onLoggedIn(payload.teamToken);
@ -500,10 +548,12 @@ module.exports = React.createClass({
this._onLoggedOut();
break;
case 'will_start_client':
this._onWillStartClient();
break;
case 'load_completed':
this._onLoadCompleted();
this.setState({ready: false}, () => {
// if the client is about to start, we are, by definition, not ready.
// Set ready to false now, then it'll be set to true when the sync
// listener we set below fires.
this._onWillStartClient();
});
break;
case 'new_version':
this.onVersion(
@ -511,6 +561,12 @@ module.exports = React.createClass({
payload.releaseNotes,
);
break;
case 'check_updates':
this.setState({ checkingForUpdate: payload.value });
break;
case 'send_event':
this.onSendEvent(payload.room_id, payload.event);
break;
}
},
@ -525,8 +581,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
@ -834,22 +890,6 @@ module.exports = React.createClass({
});
},
/**
* Called when the sessionloader has finished
*/
_onLoadCompleted: function() {
this.props.onLoadCompleted();
this.setState({loading: false});
// Show screens (like 'register') that need to be shown without _onLoggedIn
// being called. 'register' needs to be routed here when the email confirmation
// link is clicked on.
if (this.state.screenAfterLogin &&
['register'].indexOf(this.state.screenAfterLogin.screen) !== -1) {
this._showScreenAfterLogin();
}
},
/**
* Called whenever someone changes the theme
*
@ -902,9 +942,7 @@ module.exports = React.createClass({
*/
_onLoggedIn: function(teamToken) {
this.setState({
guestCreds: null,
loggedIn: true,
loggingIn: false,
view: VIEWS.LOGGED_IN,
});
if (teamToken) {
@ -913,10 +951,6 @@ module.exports = React.createClass({
dis.dispatch({action: 'view_home_page'});
} else if (this._is_registered) {
this._is_registered = false;
// reset the 'have completed first sync' flag,
// since we've just logged in and will be about to sync
this.firstSyncComplete = false;
this.firstSyncPromise = q.defer();
// Set the display name = user ID localpart
MatrixClientPeg.get().setDisplayName(
@ -946,6 +980,7 @@ module.exports = React.createClass({
this.state.screenAfterLogin.screen,
this.state.screenAfterLogin.params,
);
// XXX: is this necessary? `showScreen` should do it for us.
this.notifyNewScreen(this.state.screenAfterLogin.screen);
this.setState({screenAfterLogin: null});
} else if (localStorage && localStorage.getItem('mx_last_room_id')) {
@ -964,8 +999,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,
@ -973,6 +1008,7 @@ module.exports = React.createClass({
page_type: PageTypes.RoomDirectory,
});
this._teamToken = null;
this._setPageSubtitle();
},
/**
@ -981,6 +1017,12 @@ module.exports = React.createClass({
*/
_onWillStartClient() {
const self = this;
// reset the 'have completed first sync' flag,
// since we're about to start the client and therefore about
// to do the first sync
this.firstSyncComplete = false;
this.firstSyncPromise = q.defer();
const cli = MatrixClientPeg.get();
// Allow the JS SDK to reap timeline events. This reduces the amount of
@ -1051,6 +1093,14 @@ module.exports = React.createClass({
}
}
});
const krh = new KeyRequestHandler(cli);
cli.on("crypto.roomKeyRequest", (req) => {
krh.handleKeyRequest(req);
});
cli.on("crypto.roomKeyRequestCancellation", (req) => {
krh.handleKeyRequestCancellation(req);
});
},
showScreen: function(screen, params) {
@ -1128,7 +1178,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) {
@ -1226,27 +1276,25 @@ module.exports = React.createClass({
this.showScreen("forgot_password");
},
onReturnToGuestClick: function() {
// reanimate our guest login
if (this.state.guestCreds) {
Lifecycle.setLoggedIn(this.state.guestCreds);
this.setState({guestCreds: null});
}
onReturnToAppClick: function() {
// treat it the same as if the user had completed the login
this._onLoggedIn(null);
},
// 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");
},
@ -1257,9 +1305,35 @@ module.exports = React.createClass({
newVersion: latest,
hasNewVersion: current !== latest,
newVersionReleaseNotes: releaseNotes,
checkingForUpdate: null,
});
},
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'});
});
},
_setPageSubtitle: function(subtitle='') {
document.title = `Riot ${subtitle}`;
},
updateStatusIndicator: function(state, prevState) {
let notifCount = 0;
@ -1280,15 +1354,15 @@ module.exports = React.createClass({
PlatformPeg.get().setNotificationCount(notifCount);
}
let title = "Riot ";
let subtitle = '';
if (state === "ERROR") {
title += `[${_t("Offline")}] `;
subtitle += `[${_t("Offline")}] `;
}
if (notifCount > 0) {
title += `[${notifCount}]`;
subtitle += `[${notifCount}]`;
}
document.title = title;
this._setPageSubtitle(subtitle);
},
onUserSettingsClose: function() {
@ -1314,11 +1388,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">
@ -1328,7 +1400,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
@ -1336,38 +1408,42 @@ module.exports = React.createClass({
);
}
// `ready` and `loggedIn` 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) {
/* 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 (
<LoggedInView ref="loggedInView" matrixClient={MatrixClientPeg.get()}
onRoomCreated={this.onRoomCreated}
onUserSettingsClose={this.onUserSettingsClose}
onRegistered={this.onRegistered}
currentRoomId={this.state.currentRoomId}
teamToken={this._teamToken}
{...this.props}
{...this.state}
/>
);
} 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 (
<div className="mx_MatrixChat_splash">
<Spinner />
<a href="#" className="mx_MatrixChat_splashButtons" onClick={ this.onLogoutClick }>
{ _t('Logout') }
</a>
</div>
);
} 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 (
<LoggedInView ref="loggedInView" matrixClient={MatrixClientPeg.get()}
onRoomCreated={this.onRoomCreated}
onUserSettingsClose={this.onUserSettingsClose}
onRegistered={this.onRegistered}
currentRoomId={this.state.currentRoomId}
teamToken={this._teamToken}
{...this.props}
{...this.state}
/>
);
} else {
// we think we are logged in, but are still waiting for the /sync to complete
const Spinner = sdk.getComponent('elements.Spinner');
return (
<div className="mx_MatrixChat_splash">
<Spinner />
<a href="#" className="mx_MatrixChat_splashButtons" onClick={ this.onLogoutClick }>
{ _t('Logout') }
</a>
</div>
);
}
}
if (this.state.view === VIEWS.REGISTER) {
const Registration = sdk.getComponent('structures.login.Registration');
return (
<Registration
@ -1387,10 +1463,13 @@ module.exports = React.createClass({
onLoggedIn={this.onRegistered}
onLoginClick={this.onLoginClick}
onRegisterClick={this.onRegisterClick}
onCancelClick={this.state.guestCreds ? this.onReturnToGuestClick : null}
onCancelClick={MatrixClientPeg.get() ? this.onReturnToAppClick : null}
/>
);
} else if (this.state.screen == 'forgot_password') {
}
if (this.state.view === VIEWS.FORGOT_PASSWORD) {
const ForgotPassword = sdk.getComponent('structures.login.ForgotPassword');
return (
<ForgotPassword
@ -1402,7 +1481,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
@ -1416,9 +1497,11 @@ module.exports = React.createClass({
defaultDeviceDisplayName={this.props.defaultDeviceDisplayName}
onForgotPasswordClick={this.onForgotPasswordClick}
enableGuest={this.props.enableGuest}
onCancelClick={this.state.guestCreds ? this.onReturnToGuestClick : null}
onCancelClick={MatrixClientPeg.get() ? this.onReturnToAppClick : null}
/>
);
}
console.error(`Unknown view ${this.state.view}`);
},
});

View file

@ -94,6 +94,7 @@ module.exports = React.createClass({
userId: null,
roomLoading: true,
peekLoading: false,
shouldPeek: true,
// The event to be scrolled to initially
initialEventId: null,
@ -170,8 +171,32 @@ module.exports = React.createClass({
initialEventId: RoomViewStore.getInitialEventId(),
initialEventPixelOffset: RoomViewStore.getInitialEventPixelOffset(),
isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(),
forwardingEvent: RoomViewStore.getForwardingEvent(),
shouldPeek: RoomViewStore.shouldPeek(),
};
// finished joining, start waiting for a room and show a spinner. See onRoom.
newState.waitingForRoom = this.state.joining && !newState.joining &&
!RoomViewStore.getJoinError();
// Temporary logging to diagnose https://github.com/vector-im/riot-web/issues/4307
console.log(
'RVS update:',
newState.roomId,
newState.roomAlias,
'loading?', newState.roomLoading,
'joining?', newState.joining,
'initial?', initial,
'waiting?', newState.waitingForRoom,
'shouldPeek?', newState.shouldPeek,
);
// NB: This does assume that the roomID will not change for the lifetime of
// the RoomView instance
if (initial) {
newState.room = MatrixClientPeg.get().getRoom(newState.roomId);
}
// Clear the search results when clicking a search result (which changes the
// currently scrolled to event, this.state.initialEventId).
if (this.state.initialEventId !== newState.initialEventId) {
@ -187,8 +212,9 @@ module.exports = React.createClass({
this.setState(newState, () => {
// At this point, this.state.roomId could be null (e.g. the alias might not
// have been resolved yet) so anything called here must handle this case.
this._onHaveRoom();
this.onRoom(MatrixClientPeg.get().getRoom(this.state.roomId));
if (initial) {
this._onHaveRoom();
}
});
},
@ -204,25 +230,22 @@ module.exports = React.createClass({
// which must be by alias or invite wherever possible (peeking currently does
// not work over federation).
// NB. We peek if we are not in the room, although if we try to peek into
// a room in which we have a member event (ie. we've left) synapse will just
// send us the same data as we get in the sync (ie. the last events we saw).
const room = MatrixClientPeg.get().getRoom(this.state.roomId);
let isUserJoined = null;
// NB. We peek if we have never seen the room before (i.e. js-sdk does not know
// about it). We don't peek in the historical case where we were joined but are
// now not joined because the js-sdk peeking API will clobber our historical room,
// making it impossible to indicate a newly joined room.
const room = this.state.room;
if (room) {
isUserJoined = room.hasMembershipState(
MatrixClientPeg.get().credentials.userId, 'join',
);
this._updateAutoComplete(room);
this.tabComplete.loadEntries(room);
this.setState({
unsentMessageError: this._getUnsentMessageError(room),
});
this._onRoomLoaded(room);
}
if (!isUserJoined && !this.state.joining && this.state.roomId) {
if (!this.state.joining && this.state.roomId) {
if (this.props.autoJoin) {
this.onJoinButtonClicked();
} else if (this.state.roomId) {
} else if (!room && this.state.shouldPeek) {
console.log("Attempting to peek into room %s", this.state.roomId);
this.setState({
peekLoading: true,
});
@ -246,7 +269,8 @@ module.exports = React.createClass({
}
}).done();
}
} else if (isUserJoined) {
} else if (room) {
// Stop peeking because we have joined this room previously
MatrixClientPeg.get().stopPeeking();
this.setState({
showApps: this._shouldShowApps(room),
@ -496,8 +520,7 @@ module.exports = React.createClass({
// and that has probably just changed
if (ev.sender) {
this.tabComplete.onMemberSpoke(ev.sender);
// nb. we don't need to update the new autocomplete here since
// its results are currently ordered purely by search score.
UserProvider.getInstance().onUserSpoke(ev.sender);
}
},
@ -520,6 +543,8 @@ module.exports = React.createClass({
this._warnAboutEncryption(room);
this._calculatePeekRules(room);
this._updatePreviewUrlVisibility(room);
this.tabComplete.loadEntries(room);
UserProvider.getInstance().setUserListFromRoom(room);
},
_warnAboutEncryption: function(room) {
@ -620,6 +645,7 @@ module.exports = React.createClass({
}
this.setState({
room: room,
waitingForRoom: false,
}, () => {
this._onRoomLoaded(room);
});
@ -675,7 +701,14 @@ module.exports = React.createClass({
onRoomMemberMembership: function(ev, member, oldMembership) {
if (member.userId == MatrixClientPeg.get().credentials.userId) {
this.forceUpdate();
if (member.membership === 'join') {
this.setState({
waitingForRoom: false,
});
} else {
this.forceUpdate();
}
}
},
@ -688,7 +721,7 @@ module.exports = React.createClass({
// refresh the tab complete list
this.tabComplete.loadEntries(this.state.room);
this._updateAutoComplete(this.state.room);
UserProvider.getInstance().setUserListFromRoom(this.state.room);
// if we are now a member of the room, where we were not before, that
// means we have finished joining a room we were previously peeking
@ -1153,8 +1186,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'});
},
@ -1408,14 +1446,6 @@ module.exports = React.createClass({
}
},
_updateAutoComplete: function(room) {
const myUserId = MatrixClientPeg.get().credentials.userId;
const members = room.getJoinedMembers().filter(function(member) {
if (member.userId !== myUserId) return true;
});
UserProvider.getInstance().setUserList(members);
},
render: function() {
const RoomHeader = sdk.getComponent('rooms.RoomHeader');
const MessageComposer = sdk.getComponent('rooms.MessageComposer');
@ -1429,6 +1459,10 @@ module.exports = React.createClass({
const Loader = sdk.getComponent("elements.Spinner");
const TimelinePanel = sdk.getComponent("structures.TimelinePanel");
// Whether the preview bar spinner should be shown. We do this when joining or
// when waiting for a room to be returned by js-sdk when joining
const previewBarSpinner = this.state.joining || this.state.waitingForRoom;
if (!this.state.room) {
if (this.state.roomLoading || this.state.peekLoading) {
return (
@ -1448,7 +1482,7 @@ module.exports = React.createClass({
// We have no room object for this room, only the ID.
// We've got to this room by following a link, possibly a third party invite.
var room_alias = this.state.room_alias;
const roomAlias = this.state.roomAlias;
return (
<div className="mx_RoomView">
<RoomHeader ref="header"
@ -1461,8 +1495,8 @@ module.exports = React.createClass({
onForgetClick={ this.onForgetClick }
onRejectClick={ this.onRejectThreepidInviteButtonClicked }
canPreview={ false } error={ this.state.roomLoadError }
roomAlias={room_alias}
spinner={this.state.joining}
roomAlias={roomAlias}
spinner={previewBarSpinner}
inviterName={inviterName}
invitedEmail={invitedEmail}
room={this.state.room}
@ -1505,7 +1539,7 @@ module.exports = React.createClass({
onRejectClick={ this.onRejectButtonClicked }
inviterName={ inviterName }
canPreview={ false }
spinner={this.state.joining}
spinner={previewBarSpinner}
room={this.state.room}
/>
</div>
@ -1561,7 +1595,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}/>;
@ -1581,7 +1615,7 @@ module.exports = React.createClass({
<RoomPreviewBar onJoinClick={this.onJoinButtonClicked}
onForgetClick={ this.onForgetClick }
onRejectClick={this.onRejectThreepidInviteButtonClicked}
spinner={this.state.joining}
spinner={previewBarSpinner}
inviterName={inviterName}
invitedEmail={invitedEmail}
canPreview={this.state.canPeek}

View file

@ -352,13 +352,14 @@ module.exports = React.createClass({
const tile = tiles[backwards ? i : tiles.length - 1 - i];
// Subtract height of tile as if it were unpaginated
excessHeight -= tile.clientHeight;
//If removing the tile would lead to future pagination, break before setting scroll token
if (tile.clientHeight > excessHeight) {
break;
}
// The tile may not have a scroll token, so guard it
if (tile.dataset.scrollTokens) {
markerScrollToken = tile.dataset.scrollTokens.split(',')[0];
}
if (tile.clientHeight > excessHeight) {
break;
}
}
if (markerScrollToken) {

View file

@ -21,6 +21,7 @@ const MatrixClientPeg = require("../../MatrixClientPeg");
const PlatformPeg = require("../../PlatformPeg");
const Modal = require('../../Modal');
const dis = require("../../dispatcher");
import sessionStore from '../../stores/SessionStore';
const q = require('q');
const packageJson = require('../../../package.json');
const UserSettingsStore = require('../../UserSettingsStore');
@ -92,6 +93,10 @@ const SETTINGS_LABELS = [
id: 'disableMarkdown',
label: 'Disable markdown formatting',
},
{
id: 'enableSyntaxHighlightLanguageDetection',
label: 'Enable automatic language detection for syntax highlighting',
},
/*
{
id: 'useFixedWidthFont',
@ -169,9 +174,6 @@ module.exports = React.createClass({
// The base URL to use in the referral link. Defaults to window.location.origin.
referralBaseUrl: React.PropTypes.string,
// true if RightPanel is collapsed
collapsedRhs: React.PropTypes.bool,
// Team token for the referral link. If falsy, the referral section will
// not appear
teamToken: React.PropTypes.string,
@ -246,6 +248,12 @@ module.exports = React.createClass({
this.setState({
language: languageHandler.getCurrentLanguage(),
});
this._sessionStore = sessionStore;
this._sessionStoreToken = this._sessionStore.addListener(
this._setStateFromSessionStore,
);
this._setStateFromSessionStore();
},
componentDidMount: function() {
@ -272,6 +280,22 @@ module.exports = React.createClass({
}
},
// `UserSettings` assumes that the client peg will not be null, so give it some
// sort of assurance here by only allowing a re-render if the client is truthy.
//
// This is required because `UserSettings` maintains its own state and if this state
// updates (e.g. during _setStateFromSessionStore) after the client peg has been made
// null (during logout), then it will attempt to re-render and throw errors.
shouldComponentUpdate: function() {
return Boolean(MatrixClientPeg.get());
},
_setStateFromSessionStore: function() {
this.setState({
userHasGeneratedPassword: Boolean(this._sessionStore.getCachedPassword()),
});
},
_electronSettings: function(ev, settings) {
this.setState({ electron_settings: settings });
},
@ -622,6 +646,10 @@ module.exports = React.createClass({
},
_renderUserInterfaceSettings: function() {
// TODO: this ought to be a separate component so that we don't need
// to rebind the onChange each time we render
const onChange = (e) =>
UserSettingsStore.setLocalSetting('autocompleteDelay', + e.target.value);
return (
<div>
<h3>{ _t("User Interface") }</h3>
@ -629,8 +657,21 @@ module.exports = React.createClass({
{ this._renderUrlPreviewSelector() }
{ SETTINGS_LABELS.map( this._renderSyncedSetting ) }
{ THEMES.map( this._renderThemeSelector ) }
<table>
<tbody>
<tr>
<td><strong>{_t('Autocomplete Delay (ms):')}</strong></td>
<td>
<input
type="number"
defaultValue={UserSettingsStore.getLocalSetting('autocompleteDelay', 200)}
onChange={onChange}
/>
</td>
</tr>
</tbody>
</table>
{ this._renderLanguageSetting() }
</div>
</div>
);
@ -864,6 +905,21 @@ module.exports = React.createClass({
</div>;
},
_renderCheckUpdate: function() {
const platform = PlatformPeg.get();
if ('canSelfUpdate' in platform && platform.canSelfUpdate() && 'startUpdateCheck' in platform) {
return <div>
<h3>{_t('Updates')}</h3>
<div className="mx_UserSettings_section">
<AccessibleButton className="mx_UserSettings_button" onClick={platform.startUpdateCheck}>
{_t('Check for update')}
</AccessibleButton>
</div>
</div>;
}
return <div />;
},
_renderBulkOptions: function() {
const invitedRooms = MatrixClientPeg.get().getRooms().filter((r) => {
return r.hasMembershipState(this._me, "invite");
@ -1164,7 +1220,6 @@ module.exports = React.createClass({
<div className="mx_UserSettings">
<SimpleRoomHeader
title={ _t("Settings") }
collapsedRhs={ this.props.collapsedRhs }
onCancelClick={ this.props.onClose }
/>
@ -1205,10 +1260,14 @@ module.exports = React.createClass({
<h3>{ _t("Account") }</h3>
<div className="mx_UserSettings_section cadcampoHide">
<AccessibleButton className="mx_UserSettings_logout mx_UserSettings_button" onClick={this.onLogoutClicked}>
{ _t("Sign out") }
</AccessibleButton>
{ this.state.userHasGeneratedPassword ?
<div className="mx_UserSettings_passwordWarning">
{ _t("To return to your account in future you need to set a password") }
</div> : null
}
{accountJsx}
</div>
@ -1262,6 +1321,8 @@ module.exports = React.createClass({
</div>
</div>
{this._renderCheckUpdate()}
{this._renderClearCache()}
{this._renderDeactivateAccount()}

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