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

This commit is contained in:
Weblate 2017-06-15 16:35:46 +00:00
commit 81d0388021
16 changed files with 250 additions and 122 deletions

View file

@ -146,7 +146,6 @@ src/Roles.js
src/RoomListSorter.js src/RoomListSorter.js
src/RoomNotifs.js src/RoomNotifs.js
src/Rooms.js src/Rooms.js
src/RtsClient.js
src/ScalarAuthClient.js src/ScalarAuthClient.js
src/ScalarMessaging.js src/ScalarMessaging.js
src/SdkConfig.js src/SdkConfig.js

View file

@ -1,3 +1,13 @@
Changes in [0.9.4](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.9.4) (2017-06-14)
===================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.9.3...v0.9.4)
* Ask for email address after setting password for the first time
[\#1090](https://github.com/matrix-org/matrix-react-sdk/pull/1090)
* DM guessing: prefer oldest joined member
[\#1087](https://github.com/matrix-org/matrix-react-sdk/pull/1087)
* More translations
Changes in [0.9.3](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.9.3) (2017-06-12) Changes in [0.9.3](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.9.3) (2017-06-12)
=================================================================================================== ===================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.9.3-rc.2...v0.9.3) [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.9.3-rc.2...v0.9.3)

View file

@ -1,6 +1,6 @@
{ {
"name": "matrix-react-sdk", "name": "matrix-react-sdk",
"version": "0.9.3", "version": "0.9.4",
"description": "SDK for matrix.org using React", "description": "SDK for matrix.org using React",
"author": "matrix.org", "author": "matrix.org",
"repository": { "repository": {

View file

@ -19,6 +19,7 @@ import q from 'q';
import Matrix from 'matrix-js-sdk'; import Matrix from 'matrix-js-sdk';
import MatrixClientPeg from './MatrixClientPeg'; import MatrixClientPeg from './MatrixClientPeg';
import createMatrixClient from './utils/createMatrixClient';
import Analytics from './Analytics'; import Analytics from './Analytics';
import Notifier from './Notifier'; import Notifier from './Notifier';
import UserActivity from './UserActivity'; import UserActivity from './UserActivity';
@ -34,9 +35,6 @@ 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:
* *
* 0. if it looks like we are in the middle of a registration process, it does
* nothing.
*
* 1. if we have a loginToken in the (real) query params, it uses that to log * 1. if we have a loginToken in the (real) query params, it uses that to log
* in. * in.
* *
@ -48,7 +46,7 @@ import { _t } from './languageHandler';
* *
* 4. it attempts to auto-register as a guest user. * 4. it attempts to auto-register as a guest user.
* *
* If any of steps 1-4 are successful, it will call {setLoggedIn}, 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
@ -79,14 +77,6 @@ export function loadSession(opts) {
const guestIsUrl = opts.guestIsUrl; const guestIsUrl = opts.guestIsUrl;
const defaultDeviceDisplayName = opts.defaultDeviceDisplayName; const defaultDeviceDisplayName = opts.defaultDeviceDisplayName;
if (fragmentQueryParams.client_secret && fragmentQueryParams.sid) {
// this happens during email validation: the email contains a link to the
// IS, which in turn redirects back to vector. We let MatrixChat create a
// Registration component which completes the next stage of registration.
console.log("Not registering as guest: registration already in progress.");
return q();
}
if (!guestHsUrl) { if (!guestHsUrl) {
console.warn("Cannot enable guest access: can't determine HS URL to use"); console.warn("Cannot enable guest access: can't determine HS URL to use");
enableGuest = false; enableGuest = false;
@ -105,14 +95,13 @@ export function loadSession(opts) {
fragmentQueryParams.guest_access_token fragmentQueryParams.guest_access_token
) { ) {
console.log("Using guest access credentials"); console.log("Using guest access credentials");
setLoggedIn({ return _doSetLoggedIn({
userId: fragmentQueryParams.guest_user_id, userId: fragmentQueryParams.guest_user_id,
accessToken: fragmentQueryParams.guest_access_token, accessToken: fragmentQueryParams.guest_access_token,
homeserverUrl: guestHsUrl, homeserverUrl: guestHsUrl,
identityServerUrl: guestIsUrl, identityServerUrl: guestIsUrl,
guest: true, guest: true,
}); }, true);
return q();
} }
return _restoreFromLocalStorage().then((success) => { return _restoreFromLocalStorage().then((success) => {
@ -141,14 +130,14 @@ function _loginWithToken(queryParams, defaultDeviceDisplayName) {
}, },
).then(function(data) { ).then(function(data) {
console.log("Logged in with token"); console.log("Logged in with token");
setLoggedIn({ return _doSetLoggedIn({
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) => { }, (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);
@ -172,14 +161,14 @@ function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) {
}, },
}).then((creds) => { }).then((creds) => {
console.log("Registered as guest: %s", creds.user_id); console.log("Registered as guest: %s", creds.user_id);
setLoggedIn({ return _doSetLoggedIn({
userId: creds.user_id, userId: creds.user_id,
deviceId: creds.device_id, deviceId: creds.device_id,
accessToken: creds.access_token, accessToken: creds.access_token,
homeserverUrl: hsUrl, homeserverUrl: hsUrl,
identityServerUrl: isUrl, identityServerUrl: isUrl,
guest: true, guest: true,
}); }, true);
}, (err) => { }, (err) => {
console.error("Failed to register as guest: " + err + " " + err.data); console.error("Failed to register as guest: " + err + " " + err.data);
}); });
@ -216,15 +205,14 @@ function _restoreFromLocalStorage() {
if (accessToken && userId && hsUrl) { if (accessToken && userId && hsUrl) {
console.log("Restoring session for %s", userId); console.log("Restoring session for %s", userId);
try { try {
setLoggedIn({ return _doSetLoggedIn({
userId: userId, userId: userId,
deviceId: deviceId, deviceId: deviceId,
accessToken: accessToken, accessToken: accessToken,
homeserverUrl: hsUrl, homeserverUrl: hsUrl,
identityServerUrl: isUrl, identityServerUrl: isUrl,
guest: isGuest, guest: isGuest,
}); }, false).then(() => true);
return q(true);
} catch (e) { } catch (e) {
return _handleRestoreFailure(e); return _handleRestoreFailure(e);
} }
@ -245,7 +233,7 @@ function _handleRestoreFailure(e) {
+ ' This is a once off; sorry for the inconvenience.', + ' This is a once off; sorry for the inconvenience.',
); );
_clearLocalStorage(); _clearStorage();
return q.reject(new Error( return q.reject(new Error(
_t('Unable to restore previous session') + ': ' + msg, _t('Unable to restore previous session') + ': ' + msg,
@ -266,7 +254,7 @@ function _handleRestoreFailure(e) {
return def.promise.then((success) => { return def.promise.then((success) => {
if (success) { if (success) {
// user clicked continue. // user clicked continue.
_clearLocalStorage(); _clearStorage();
return false; return false;
} }
@ -277,17 +265,40 @@ function _handleRestoreFailure(e) {
let rtsClient = null; let rtsClient = null;
export function initRtsClient(url) { export function initRtsClient(url) {
rtsClient = new RtsClient(url); if (url) {
rtsClient = new RtsClient(url);
} else {
rtsClient = null;
}
} }
/** /**
* Transitions to a logged-in state using the given credentials * Transitions to a logged-in state using the given credentials.
*
* Starts the matrix client and all other react-sdk services that
* listen for events while a session is logged in.
*
* Also stops the old MatrixClient and clears old credentials/etc out of
* storage before starting the new client.
*
* @param {MatrixClientCreds} credentials The credentials to use * @param {MatrixClientCreds} credentials The credentials to use
*/ */
export function setLoggedIn(credentials) { export function setLoggedIn(credentials) {
credentials.guest = Boolean(credentials.guest); stopMatrixClient();
_doSetLoggedIn(credentials, true);
}
Analytics.setGuest(credentials.guest); /**
* fires on_logging_in, optionally clears localstorage, persists new credentials
* to localstorage, starts the new client.
*
* @param {MatrixClientCreds} credentials
* @param {Boolean} clearStorage
*
* returns a Promise which resolves once the client has been started
*/
async function _doSetLoggedIn(credentials, clearStorage) {
credentials.guest = Boolean(credentials.guest);
console.log( console.log(
"setLoggedIn: mxid:", credentials.userId, "setLoggedIn: mxid:", credentials.userId,
@ -295,12 +306,19 @@ export function setLoggedIn(credentials) {
"guest:", credentials.guest, "guest:", credentials.guest,
"hs:", credentials.homeserverUrl, "hs:", credentials.homeserverUrl,
); );
// This is dispatched to indicate that the user is still in the process of logging in // This is dispatched to indicate that the user is still in the process of logging in
// because `teamPromise` may take some time to resolve, breaking the assumption that // because `teamPromise` may take some time to resolve, breaking the assumption that
// `setLoggedIn` takes an "instant" to complete, and dispatch `on_logged_in` a few ms // `setLoggedIn` takes an "instant" to complete, and dispatch `on_logged_in` a few ms
// later than MatrixChat might assume. // later than MatrixChat might assume.
dis.dispatch({action: 'on_logging_in'}); dis.dispatch({action: 'on_logging_in'});
if (clearStorage) {
await _clearStorage();
}
Analytics.setGuest(credentials.guest);
// Resolves by default // Resolves by default
let teamPromise = Promise.resolve(null); let teamPromise = Promise.resolve(null);
@ -349,9 +367,6 @@ export function setLoggedIn(credentials) {
console.warn("No local storage available: can't persist session!"); console.warn("No local storage available: can't persist session!");
} }
// stop any running clients before we create a new one with these new credentials
stopMatrixClient();
MatrixClientPeg.replaceUsingCreds(credentials); MatrixClientPeg.replaceUsingCreds(credentials);
teamPromise.then((teamToken) => { teamPromise.then((teamToken) => {
@ -400,7 +415,7 @@ export function logout() {
* Starts the matrix client and all other react-sdk services that * Starts the matrix client and all other react-sdk services that
* listen for events while a session is logged in. * listen for events while a session is logged in.
*/ */
export function startMatrixClient() { function startMatrixClient() {
// dispatch this before starting the matrix client: it's used // dispatch this before starting the matrix client: it's used
// to add listeners for the 'sync' event so otherwise we'd have // to add listeners for the 'sync' event so otherwise we'd have
// a race condition (and we need to dispatch synchronously for this // a race condition (and we need to dispatch synchronously for this
@ -416,34 +431,44 @@ export function startMatrixClient() {
} }
/* /*
* Stops a running client and all related services, used after * Stops a running client and all related services, and clears persistent
* a session has been logged out / ended. * storage. Used after a session has been logged out.
*/ */
export function onLoggedOut() { export function onLoggedOut() {
_clearLocalStorage();
stopMatrixClient(); stopMatrixClient();
_clearStorage().done();
dis.dispatch({action: 'on_logged_out'}); dis.dispatch({action: 'on_logged_out'});
} }
function _clearLocalStorage() { /**
* @returns {Promise} promise which resolves once the stores have been cleared
*/
function _clearStorage() {
Analytics.logout(); Analytics.logout();
if (!window.localStorage) {
return;
}
const hsUrl = window.localStorage.getItem("mx_hs_url");
const isUrl = window.localStorage.getItem("mx_is_url");
window.localStorage.clear();
// preserve our HS & IS URLs for convenience if (window.localStorage) {
// N.B. we cache them in hsUrl/isUrl and can't really inline them const hsUrl = window.localStorage.getItem("mx_hs_url");
// as getCurrentHsUrl() may call through to localStorage. const isUrl = window.localStorage.getItem("mx_is_url");
// NB. We do clear the device ID (as well as all the settings) window.localStorage.clear();
if (hsUrl) window.localStorage.setItem("mx_hs_url", hsUrl);
if (isUrl) window.localStorage.setItem("mx_is_url", isUrl); // preserve our HS & IS URLs for convenience
// N.B. we cache them in hsUrl/isUrl and can't really inline them
// as getCurrentHsUrl() may call through to localStorage.
// NB. We do clear the device ID (as well as all the settings)
if (hsUrl) window.localStorage.setItem("mx_hs_url", hsUrl);
if (isUrl) window.localStorage.setItem("mx_is_url", isUrl);
}
// create a temporary client to clear out the persistent stores.
const cli = createMatrixClient({
// we'll never make any requests, so can pass a bogus HS URL
baseUrl: "",
});
return cli.clearStores();
} }
/** /**
* Stop all the background processes related to the current client * Stop all the background processes related to the current client.
*/ */
export function stopMatrixClient() { export function stopMatrixClient() {
Notifier.stop(); Notifier.stop();
@ -454,7 +479,6 @@ export function stopMatrixClient() {
if (cli) { if (cli) {
cli.stopClient(); cli.stopClient();
cli.removeAllListeners(); cli.removeAllListeners();
cli.store.deleteAllData();
MatrixClientPeg.unset(); MatrixClientPeg.unset();
} }
} }

View file

@ -16,12 +16,10 @@ limitations under the License.
'use strict'; 'use strict';
import Matrix from 'matrix-js-sdk';
import utils from 'matrix-js-sdk/lib/utils'; import utils from 'matrix-js-sdk/lib/utils';
import EventTimeline from 'matrix-js-sdk/lib/models/event-timeline'; import EventTimeline from 'matrix-js-sdk/lib/models/event-timeline';
import EventTimelineSet from 'matrix-js-sdk/lib/models/event-timeline-set'; import EventTimelineSet from 'matrix-js-sdk/lib/models/event-timeline-set';
import createMatrixClient from './utils/createMatrixClient';
const localStorage = window.localStorage;
interface MatrixClientCreds { interface MatrixClientCreds {
homeserverUrl: string, homeserverUrl: string,
@ -129,22 +127,7 @@ class MatrixClientPeg {
timelineSupport: true, timelineSupport: true,
}; };
if (localStorage) { this.matrixClient = createMatrixClient(opts);
opts.sessionStore = new Matrix.WebStorageSessionStore(localStorage);
}
if (window.indexedDB && localStorage) {
// FIXME: bodge to remove old database. Remove this after a few weeks.
window.indexedDB.deleteDatabase("matrix-js-sdk:default");
opts.store = new Matrix.IndexedDBStore({
indexedDB: window.indexedDB,
dbName: "riot-web-sync",
localStorage: localStorage,
workerScript: this.indexedDbWorkerScript,
});
}
this.matrixClient = Matrix.createClient(opts);
// we're going to add eventlisteners for each matrix event tile, so the // we're going to add eventlisteners for each matrix event tile, so the
// potential number of event listeners is quite high. // potential number of event listeners is quite high.

View file

@ -144,7 +144,18 @@ export function guessDMRoomTarget(room, me) {
let oldestTs; let oldestTs;
let oldestUser; let oldestUser;
// Pick the user who's been here longest (and isn't us) // Pick the joined user who's been here longest (and isn't us),
for (const user of room.getJoinedMembers()) {
if (user.userId == me.userId) continue;
if (oldestTs === undefined || user.events.member.getTs() < oldestTs) {
oldestUser = user;
oldestTs = user.events.member.getTs();
}
}
if (oldestUser) return oldestUser;
// if there are no joined members other than us, use the oldest member
for (const user of room.currentState.getMembers()) { for (const user of room.currentState.getMembers()) {
if (user.userId == me.userId) continue; if (user.userId == me.userId) continue;

View file

@ -1,5 +1,7 @@
import 'whatwg-fetch'; import 'whatwg-fetch';
let fetchFunction = fetch;
function checkStatus(response) { function checkStatus(response) {
if (!response.ok) { if (!response.ok) {
return response.text().then((text) => { return response.text().then((text) => {
@ -31,7 +33,7 @@ const request = (url, opts) => {
opts.body = JSON.stringify(opts.body); opts.body = JSON.stringify(opts.body);
opts.headers['Content-Type'] = 'application/json'; opts.headers['Content-Type'] = 'application/json';
} }
return fetch(url, opts) return fetchFunction(url, opts)
.then(checkStatus) .then(checkStatus)
.then(parseJson); .then(parseJson);
}; };
@ -64,7 +66,7 @@ export default class RtsClient {
client_secret: clientSecret, client_secret: clientSecret,
}, },
method: 'POST', method: 'POST',
} },
); );
} }
@ -74,7 +76,7 @@ export default class RtsClient {
qs: { qs: {
team_token: teamToken, team_token: teamToken,
}, },
} },
); );
} }
@ -91,7 +93,12 @@ export default class RtsClient {
qs: { qs: {
user_id: userId, user_id: userId,
}, },
} },
); );
} }
// allow fetch to be replaced, for testing.
static setFetch(fn) {
fetchFunction = fn;
}
} }

View file

@ -263,10 +263,22 @@ module.exports = React.createClass({
window.addEventListener('resize', this.handleResize); window.addEventListener('resize', this.handleResize);
this.handleResize(); this.handleResize();
if (this.props.config.teamServerConfig && const teamServerConfig = this.props.config.teamServerConfig || {};
this.props.config.teamServerConfig.teamServerURL Lifecycle.initRtsClient(teamServerConfig.teamServerURL);
) {
Lifecycle.initRtsClient(this.props.config.teamServerConfig.teamServerURL); // if the user has followed a login or register link, don't reanimate
// the old creds, but rather go straight to the relevant page
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;
} }
// the extra q() ensures that synchronous exceptions hit the same codepath as // the extra q() ensures that synchronous exceptions hit the same codepath as
@ -840,14 +852,6 @@ module.exports = React.createClass({
_onLoadCompleted: function() { _onLoadCompleted: function() {
this.props.onLoadCompleted(); this.props.onLoadCompleted();
this.setState({loading: false}); 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();
}
}, },
/** /**
@ -946,6 +950,7 @@ module.exports = React.createClass({
this.state.screenAfterLogin.screen, this.state.screenAfterLogin.screen,
this.state.screenAfterLogin.params, this.state.screenAfterLogin.params,
); );
// XXX: is this necessary? `showScreen` should do it for us.
this.notifyNewScreen(this.state.screenAfterLogin.screen); this.notifyNewScreen(this.state.screenAfterLogin.screen);
this.setState({screenAfterLogin: null}); this.setState({screenAfterLogin: null});
} else if (localStorage && localStorage.getItem('mx_last_room_id')) { } else if (localStorage && localStorage.getItem('mx_last_room_id')) {
@ -1229,6 +1234,8 @@ module.exports = React.createClass({
onReturnToGuestClick: function() { onReturnToGuestClick: function() {
// reanimate our guest login // reanimate our guest login
if (this.state.guestCreds) { if (this.state.guestCreds) {
// TODO: this is probably a bit broken - we don't want to be
// clearing storage when we reanimate the guest creds.
Lifecycle.setLoggedIn(this.state.guestCreds); Lifecycle.setLoggedIn(this.state.guestCreds);
this.setState({guestCreds: null}); this.setState({guestCreds: null});
} }

View file

@ -170,6 +170,25 @@ module.exports = React.createClass({
isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(), isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(),
}; };
// 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,
);
// finished joining, start waiting for a room and show a spinner. See onRoom.
newState.waitingForRoom = this.state.joining && !newState.joining &&
!RoomViewStore.getJoinError();
// 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 // Clear the search results when clicking a search result (which changes the
// currently scrolled to event, this.state.initialEventId). // currently scrolled to event, this.state.initialEventId).
if (this.state.initialEventId !== newState.initialEventId) { if (this.state.initialEventId !== newState.initialEventId) {
@ -185,8 +204,9 @@ module.exports = React.createClass({
this.setState(newState, () => { this.setState(newState, () => {
// At this point, this.state.roomId could be null (e.g. the alias might not // 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. // have been resolved yet) so anything called here must handle this case.
this._onHaveRoom(); if (initial) {
this.onRoom(MatrixClientPeg.get().getRoom(this.state.roomId)); this._onHaveRoom();
}
}); });
}, },
@ -202,25 +222,20 @@ module.exports = React.createClass({
// which must be by alias or invite wherever possible (peeking currently does // which must be by alias or invite wherever possible (peeking currently does
// not work over federation). // not work over federation).
// NB. We peek if we are not in the room, although if we try to peek into // NB. We peek if we have never seen the room before (i.e. js-sdk does not know
// a room in which we have a member event (ie. we've left) synapse will just // about it). We don't peek in the historical case where we were joined but are
// send us the same data as we get in the sync (ie. the last events we saw). // now not joined because the js-sdk peeking API will clobber our historical room,
const room = MatrixClientPeg.get().getRoom(this.state.roomId); // making it impossible to indicate a newly joined room.
let isUserJoined = null; const room = this.state.room;
if (room) { if (room) {
isUserJoined = room.hasMembershipState(
MatrixClientPeg.get().credentials.userId, 'join',
);
this._updateAutoComplete(room); this._updateAutoComplete(room);
this.tabComplete.loadEntries(room); this.tabComplete.loadEntries(room);
} }
if (!isUserJoined && !this.state.joining && this.state.roomId) { if (!this.state.joining && this.state.roomId) {
if (this.props.autoJoin) { if (this.props.autoJoin) {
this.onJoinButtonClicked(); this.onJoinButtonClicked();
} else if (this.state.roomId) { } else if (!room) {
console.log("Attempting to peek into room %s", this.state.roomId); console.log("Attempting to peek into room %s", this.state.roomId);
this.setState({ this.setState({
peekLoading: true, peekLoading: true,
}); });
@ -244,7 +259,8 @@ module.exports = React.createClass({
} }
}).done(); }).done();
} }
} else if (isUserJoined) { } else if (room) {
// Stop peeking because we have joined this room previously
MatrixClientPeg.get().stopPeeking(); MatrixClientPeg.get().stopPeeking();
this.setState({ this.setState({
unsentMessageError: this._getUnsentMessageError(room), unsentMessageError: this._getUnsentMessageError(room),
@ -607,6 +623,7 @@ module.exports = React.createClass({
} }
this.setState({ this.setState({
room: room, room: room,
waitingForRoom: false,
}, () => { }, () => {
this._onRoomLoaded(room); this._onRoomLoaded(room);
}); });
@ -662,7 +679,14 @@ module.exports = React.createClass({
onRoomMemberMembership: function(ev, member, oldMembership) { onRoomMemberMembership: function(ev, member, oldMembership) {
if (member.userId == MatrixClientPeg.get().credentials.userId) { if (member.userId == MatrixClientPeg.get().credentials.userId) {
this.forceUpdate();
if (member.membership === 'join') {
this.setState({
waitingForRoom: false,
});
} else {
this.forceUpdate();
}
} }
}, },
@ -1416,6 +1440,10 @@ module.exports = React.createClass({
const Loader = sdk.getComponent("elements.Spinner"); const Loader = sdk.getComponent("elements.Spinner");
const TimelinePanel = sdk.getComponent("structures.TimelinePanel"); 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.room) {
if (this.state.roomLoading || this.state.peekLoading) { if (this.state.roomLoading || this.state.peekLoading) {
return ( return (
@ -1449,7 +1477,7 @@ module.exports = React.createClass({
onRejectClick={ this.onRejectThreepidInviteButtonClicked } onRejectClick={ this.onRejectThreepidInviteButtonClicked }
canPreview={ false } error={ this.state.roomLoadError } canPreview={ false } error={ this.state.roomLoadError }
roomAlias={room_alias} roomAlias={room_alias}
spinner={this.state.joining} spinner={previewBarSpinner}
inviterName={inviterName} inviterName={inviterName}
invitedEmail={invitedEmail} invitedEmail={invitedEmail}
room={this.state.room} room={this.state.room}
@ -1492,7 +1520,7 @@ module.exports = React.createClass({
onRejectClick={ this.onRejectButtonClicked } onRejectClick={ this.onRejectButtonClicked }
inviterName={ inviterName } inviterName={ inviterName }
canPreview={ false } canPreview={ false }
spinner={this.state.joining} spinner={previewBarSpinner}
room={this.state.room} room={this.state.room}
/> />
</div> </div>
@ -1568,7 +1596,7 @@ module.exports = React.createClass({
<RoomPreviewBar onJoinClick={this.onJoinButtonClicked} <RoomPreviewBar onJoinClick={this.onJoinButtonClicked}
onForgetClick={ this.onForgetClick } onForgetClick={ this.onForgetClick }
onRejectClick={this.onRejectThreepidInviteButtonClicked} onRejectClick={this.onRejectThreepidInviteButtonClicked}
spinner={this.state.joining} spinner={previewBarSpinner}
inviterName={inviterName} inviterName={inviterName}
invitedEmail={invitedEmail} invitedEmail={invitedEmail}
canPreview={this.state.canPeek} canPreview={this.state.canPeek}

View file

@ -352,13 +352,14 @@ module.exports = React.createClass({
const tile = tiles[backwards ? i : tiles.length - 1 - i]; const tile = tiles[backwards ? i : tiles.length - 1 - i];
// Subtract height of tile as if it were unpaginated // Subtract height of tile as if it were unpaginated
excessHeight -= tile.clientHeight; 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 // The tile may not have a scroll token, so guard it
if (tile.dataset.scrollTokens) { if (tile.dataset.scrollTokens) {
markerScrollToken = tile.dataset.scrollTokens.split(',')[0]; markerScrollToken = tile.dataset.scrollTokens.split(',')[0];
} }
if (tile.clientHeight > excessHeight) {
break;
}
} }
if (markerScrollToken) { if (markerScrollToken) {

View file

@ -14,15 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import q from 'q';
import React from 'react'; import React from 'react';
import sdk from '../../../index'; import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg';
import classnames from 'classnames';
import KeyCode from '../../../KeyCode';
import Email from '../../../email'; import Email from '../../../email';
import AddThreepid from '../../../AddThreepid'; import AddThreepid from '../../../AddThreepid';
import { _t, _tJsx } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
@ -89,6 +85,10 @@ export default React.createClass({
this.setState({emailBusy: true}); this.setState({emailBusy: true});
}, },
onCancelled: function() {
this.props.onFinished(false);
},
onEmailDialogFinished: function(ok) { onEmailDialogFinished: function(ok) {
if (ok) { if (ok) {
this.verifyEmailAddress(); this.verifyEmailAddress();
@ -137,7 +137,7 @@ export default React.createClass({
return ( return (
<BaseDialog className="mx_SetEmailDialog" <BaseDialog className="mx_SetEmailDialog"
onFinished={this.props.onFinished} onFinished={this.onCancelled}
title={this.props.title} title={this.props.title}
> >
<div className="mx_Dialog_content"> <div className="mx_Dialog_content">
@ -155,7 +155,7 @@ export default React.createClass({
<input <input
type="submit" type="submit"
value={_t("Cancel")} value={_t("Cancel")}
onClick={this.props.onFinished} onClick={this.onCancelled}
/> />
</div> </div>
</BaseDialog> </BaseDialog>

View file

@ -223,7 +223,7 @@ module.exports = React.createClass({
const passwordLabel = this.state.cachedPassword ? const passwordLabel = this.state.cachedPassword ?
_t('Password') : _t('New Password'); _t('Password') : _t('New Password');
return ( return (
<div className={this.props.className}> <form className={this.props.className} onSubmit={this.onClickChange}>
{ currentPassword } { currentPassword }
<div className={rowClassName}> <div className={rowClassName}>
<div className={rowLabelClassName}> <div className={rowLabelClassName}>
@ -246,7 +246,7 @@ module.exports = React.createClass({
element="button"> element="button">
{ _t('Change Password') } { _t('Change Password') }
</AccessibleButton> </AccessibleButton>
</div> </form>
); );
case this.Phases.Uploading: case this.Phases.Uploading:
var Loader = sdk.getComponent("elements.Spinner"); var Loader = sdk.getComponent("elements.Spinner");

View file

@ -41,9 +41,6 @@ class MatrixDispatcher extends flux.Dispatcher {
} }
} }
// XXX this is a big anti-pattern, and makes testing hard. Because dispatches
// happen asynchronously, it is possible for actions dispatched in one thread
// to arrive in another, with *hilarious* consequences.
if (global.mxDispatcher === undefined) { if (global.mxDispatcher === undefined) {
global.mxDispatcher = new MatrixDispatcher(); global.mxDispatcher = new MatrixDispatcher();
} }

View file

@ -15,6 +15,7 @@ limitations under the License.
*/ */
import Skinner from './Skinner'; import Skinner from './Skinner';
import RtsClient from './RtsClient';
module.exports.loadSkin = function(skinObject) { module.exports.loadSkin = function(skinObject) {
Skinner.load(skinObject); Skinner.load(skinObject);
@ -27,3 +28,7 @@ module.exports.resetSkin = function() {
module.exports.getComponent = function(componentName) { module.exports.getComponent = function(componentName) {
return Skinner.getComponent(componentName); return Skinner.getComponent(componentName);
}; };
module.exports.setFetch = function(fetchFunction) {
RtsClient.setFetch(fetchFunction);
};

View file

@ -123,6 +123,7 @@ class RoomViewStore extends Store {
if (payload.room_id) { if (payload.room_id) {
const newState = { const newState = {
roomId: payload.room_id, roomId: payload.room_id,
roomAlias: payload.room_alias,
initialEventId: payload.event_id, initialEventId: payload.event_id,
initialEventPixelOffset: undefined, initialEventPixelOffset: undefined,
isInitialEventHighlighted: payload.highlighted, isInitialEventHighlighted: payload.highlighted,

View file

@ -0,0 +1,55 @@
/*
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import Matrix from 'matrix-js-sdk';
const localStorage = window.localStorage;
/**
* Create a new matrix client, with the persistent stores set up appropriately
* (using localstorage/indexeddb, etc)
*
* @param {Object} opts options to pass to Matrix.createClient. This will be
* extended with `sessionStore` and `store` members.
*
* @param {string} indexedDbWorkerScript Optional URL for a web worker script
* for IndexedDB store operations. If not given, indexeddb ops are done on
* the main thread.
*
* @returns {MatrixClient} the newly-created MatrixClient
*/
export default function createMatrixClient(opts, indexedDbWorkerScript) {
const storeOpts = {};
if (localStorage) {
storeOpts.sessionStore = new Matrix.WebStorageSessionStore(localStorage);
}
if (window.indexedDB && localStorage) {
// FIXME: bodge to remove old database. Remove this after a few weeks.
window.indexedDB.deleteDatabase("matrix-js-sdk:default");
storeOpts.store = new Matrix.IndexedDBStore({
indexedDB: window.indexedDB,
dbName: "riot-web-sync",
localStorage: localStorage,
workerScript: indexedDbWorkerScript,
});
}
opts = Object.assign(storeOpts, opts);
return Matrix.createClient(opts);
}