diff --git a/.gitignore b/.gitignore
index b99c9f1145..f828c37393 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,3 +12,5 @@ npm-debug.log
/.idea
/src/component-index.js
+
+.DS_Store
diff --git a/package.json b/package.json
index e4f7d82984..15a903c25a 100644
--- a/package.json
+++ b/package.json
@@ -66,6 +66,7 @@
"lodash": "^4.13.1",
"matrix-js-sdk": "0.7.10",
"optimist": "^0.6.1",
+ "prop-types": "^15.5.8",
"q": "^1.4.1",
"react": "^15.4.0",
"react-addons-css-transition-group": "15.3.2",
diff --git a/src/Lifecycle.js b/src/Lifecycle.js
index bf7b25fd2b..54014a0166 100644
--- a/src/Lifecycle.js
+++ b/src/Lifecycle.js
@@ -187,6 +187,14 @@ function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) {
// returns a promise which resolves to true if a session is found in
// localstorage
+//
+// N.B. Lifecycle.js should not maintain any further localStorage state, we
+// are moving towards using SessionStore to keep track of state related
+// to the current session (which is typically backed by localStorage).
+//
+// The plan is to gradually move the localStorage access done here into
+// SessionStore to avoid bugs where the view becomes out-of-sync with
+// localStorage (e.g. teamToken, isGuest etc.)
function _restoreFromLocalStorage() {
if (!localStorage) {
return q(false);
@@ -314,6 +322,16 @@ export function setLoggedIn(credentials) {
localStorage.setItem("mx_device_id", credentials.deviceId);
}
+ // 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.
+ if (credentials.password) {
+ // Update SessionStore
+ dis.dispatch({
+ action: 'cached_password',
+ cachedPassword: credentials.password,
+ });
+ }
+
console.log("Session persisted for %s", credentials.userId);
} catch (e) {
console.warn("Error using local storage: can't persist session!", e);
diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js
index e2fdeb4687..a201a0bea7 100644
--- a/src/components/structures/LoggedInView.js
+++ b/src/components/structures/LoggedInView.js
@@ -25,6 +25,8 @@ import PageTypes from '../../PageTypes';
import CallMediaHandler from '../../CallMediaHandler';
import sdk from '../../index';
import dis from '../../dispatcher';
+import sessionStore from '../../stores/SessionStore';
+import MatrixClientPeg from '../../MatrixClientPeg';
/**
* This is what our MatrixChat shows when we are logged in. The precise view is
@@ -41,10 +43,13 @@ export default React.createClass({
propTypes: {
matrixClient: React.PropTypes.instanceOf(Matrix.MatrixClient).isRequired,
page_type: React.PropTypes.string.isRequired,
- onRoomIdResolved: React.PropTypes.func,
onRoomCreated: React.PropTypes.func,
onUserSettingsClose: React.PropTypes.func,
+ // Called with the credentials of a registered user (if they were a ROU that
+ // transitioned to PWLU)
+ onRegistered: React.PropTypes.func,
+
teamToken: React.PropTypes.string,
// and lots and lots of other stuff.
@@ -83,12 +88,32 @@ export default React.createClass({
CallMediaHandler.loadDevices();
document.addEventListener('keydown', this._onKeyDown);
+
+ this._sessionStore = sessionStore;
+ this._sessionStoreToken = this._sessionStore.addListener(
+ this._setStateFromSessionStore,
+ );
+ this._setStateFromSessionStore();
+
this._matrixClient.on("accountData", this.onAccountData);
},
componentWillUnmount: function() {
document.removeEventListener('keydown', this._onKeyDown);
this._matrixClient.removeListener("accountData", this.onAccountData);
+ if (this._sessionStoreToken) {
+ this._sessionStoreToken.remove();
+ }
+ },
+
+ // Child components assume that the client peg will not be null, so give them some
+ // sort of assurance here by only allowing a re-render if the client is truthy.
+ //
+ // This is required because `LoggedInView` maintains its own state and if this state
+ // updates after the client peg has been made null (during logout), then it will
+ // attempt to re-render and the children will throw errors.
+ shouldComponentUpdate: function() {
+ return Boolean(MatrixClientPeg.get());
},
getScrollStateForRoom: function(roomId) {
@@ -102,10 +127,16 @@ export default React.createClass({
return this.refs.roomView.canResetTimeline();
},
+ _setStateFromSessionStore() {
+ this.setState({
+ userHasGeneratedPassword: Boolean(this._sessionStore.getCachedPassword()),
+ });
+ },
+
onAccountData: function(event) {
if (event.getType() === "im.vector.web.settings") {
this.setState({
- useCompactLayout: event.getContent().useCompactLayout
+ useCompactLayout: event.getContent().useCompactLayout,
});
}
},
@@ -180,8 +211,8 @@ export default React.createClass({
const RoomDirectory = sdk.getComponent('structures.RoomDirectory');
const HomePage = sdk.getComponent('structures.HomePage');
const MatrixToolbar = sdk.getComponent('globals.MatrixToolbar');
- const GuestWarningBar = sdk.getComponent('globals.GuestWarningBar');
const NewVersionBar = sdk.getComponent('globals.NewVersionBar');
+ const PasswordNagBar = sdk.getComponent('globals.PasswordNagBar');
let page_element;
let right_panel = '';
@@ -190,15 +221,14 @@ export default React.createClass({
case PageTypes.RoomView:
page_element =
- if (!this.props.collapse_rhs) right_panel =
+ homePageUrl={this.props.config.welcomePageUrl}
+ />;
break;
case PageTypes.UserView:
@@ -249,16 +285,15 @@ export default React.createClass({
break;
}
+ const isGuest = this.props.matrixClient.isGuest();
var topBar;
if (this.props.hasNewVersion) {
topBar = ;
- }
- else if (this.props.matrixClient.isGuest()) {
- topBar = ;
- }
- else if (Notifier.supportsDesktopNotifications() && !Notifier.isEnabled() && !Notifier.isToolbarHidden()) {
+ } else if (this.state.userHasGeneratedPassword) {
+ topBar = ;
+ } else if (!isGuest && Notifier.supportsDesktopNotifications() && !Notifier.isEnabled() && !Notifier.isToolbarHidden()) {
topBar = ;
}
@@ -278,7 +313,6 @@ export default React.createClass({
selectedRoom={this.props.currentRoomId}
collapsed={this.props.collapse_lhs || false}
opacity={this.props.leftOpacity}
- teamToken={this.props.teamToken}
/>
{page_element}
diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js
index ac0e3f715b..efb2b38d6e 100644
--- a/src/components/structures/MatrixChat.js
+++ b/src/components/structures/MatrixChat.js
@@ -34,6 +34,9 @@ import sdk from '../../index';
import * as Rooms from '../../Rooms';
import linkifyMatrix from "../../linkify-matrix";
import * as Lifecycle from '../../Lifecycle';
+// LifecycleStore is not used but does listen to and dispatch actions
+import LifecycleStore from '../../stores/LifecycleStore';
+import RoomViewStore from '../../stores/RoomViewStore';
import PageTypes from '../../PageTypes';
import createRoom from "../../createRoom";
@@ -102,9 +105,6 @@ module.exports = React.createClass({
// What the LoggedInView would be showing if visible
page_type: null,
- // If we are viewing a room by alias, this contains the alias
- currentRoomAlias: null,
-
// The ID of the room we're viewing. This is either populated directly
// in the case where we view a room by ID or by RoomView when it resolves
// what ID an alias points at.
@@ -191,6 +191,9 @@ module.exports = React.createClass({
componentWillMount: function() {
SdkConfig.put(this.props.config);
+ RoomViewStore.addListener(this._onRoomViewStoreUpdated);
+ this._onRoomViewStoreUpdated();
+
if (!UserSettingsStore.getLocalSetting('analyticsOptOut', false)) Analytics.enable();
// Used by _viewRoom before getting state from sync
@@ -322,7 +325,6 @@ module.exports = React.createClass({
onAction: function(payload) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
- const TextInputDialog = sdk.getComponent("dialogs.TextInputDialog");
switch (payload.action) {
case 'logout':
@@ -374,6 +376,11 @@ module.exports = React.createClass({
});
this.notifyNewScreen('forgot_password');
break;
+ case 'start_chat':
+ createRoom({
+ dmUserId: payload.user_id,
+ });
+ break;
case 'leave_room':
this._leaveRoom(payload.room_id);
break;
@@ -434,37 +441,36 @@ module.exports = React.createClass({
this._viewIndexedRoom(payload.roomIndex);
break;
case 'view_user_settings':
+ if (MatrixClientPeg.get().isGuest()) {
+ dis.dispatch({
+ action: 'do_after_sync_prepared',
+ deferred_action: {
+ action: 'view_user_settings',
+ },
+ });
+ dis.dispatch({action: 'view_set_mxid'});
+ break;
+ }
this._setPage(PageTypes.UserSettings);
this.notifyNewScreen('settings');
break;
case 'view_create_room':
- //this._setPage(PageTypes.CreateRoom);
- //this.notifyNewScreen('new');
- Modal.createDialog(TextInputDialog, {
- title: _t('Create Room'),
- description: _t('Room name (optional)'),
- button: _t('Create Room'),
- onFinished: (shouldCreate, name) => {
- if (shouldCreate) {
- const createOpts = {};
- if (name) createOpts.name = name;
- createRoom({createOpts}).done();
- }
- },
- });
+ this._createRoom();
break;
case 'view_room_directory':
this._setPage(PageTypes.RoomDirectory);
this.notifyNewScreen('directory');
break;
case 'view_home_page':
- if (!this._teamToken) {
- dis.dispatch({action: 'view_room_directory'});
- return;
- }
this._setPage(PageTypes.HomePage);
this.notifyNewScreen('home');
break;
+ case 'view_set_mxid':
+ this._setMxId();
+ break;
+ case 'view_start_chat_or_reuse':
+ this._chatCreateOrReuse(payload.user_id);
+ break;
case 'view_create_chat':
this._createChat();
break;
@@ -533,6 +539,10 @@ module.exports = React.createClass({
}
},
+ _onRoomViewStoreUpdated: function() {
+ this.setState({ currentRoomId: RoomViewStore.getRoomId() });
+ },
+
_setPage: function(pageType) {
this.setState({
page_type: pageType,
@@ -555,6 +565,7 @@ module.exports = React.createClass({
this.notifyNewScreen('register');
},
+ // TODO: Move to RoomViewStore
_viewNextRoom: function(roomIndexDelta) {
const allRooms = RoomListSorter.mostRecentActivityFirst(
MatrixClientPeg.get().getRooms(),
@@ -568,15 +579,22 @@ module.exports = React.createClass({
}
roomIndex = (roomIndex + roomIndexDelta) % allRooms.length;
if (roomIndex < 0) roomIndex = allRooms.length - 1;
- this._viewRoom({ room_id: allRooms[roomIndex].roomId });
+ dis.dispatch({
+ action: 'view_room',
+ room_id: allRooms[roomIndex].roomId,
+ });
},
+ // TODO: Move to RoomViewStore
_viewIndexedRoom: function(roomIndex) {
const allRooms = RoomListSorter.mostRecentActivityFirst(
MatrixClientPeg.get().getRooms(),
);
if (allRooms[roomIndex]) {
- this._viewRoom({ room_id: allRooms[roomIndex].roomId });
+ dis.dispatch({
+ action: 'view_room',
+ room_id: allRooms[roomIndex].roomId,
+ });
}
},
@@ -660,7 +678,41 @@ module.exports = React.createClass({
});
},
+ _setMxId: function() {
+ const SetMxIdDialog = sdk.getComponent('views.dialogs.SetMxIdDialog');
+ const close = Modal.createDialog(SetMxIdDialog, {
+ homeserverUrl: MatrixClientPeg.get().getHomeserverUrl(),
+ onFinished: (submitted, credentials) => {
+ if (!submitted) {
+ dis.dispatch({
+ action: 'cancel_after_sync_prepared',
+ });
+ return;
+ }
+ this.onRegistered(credentials);
+ },
+ onDifferentServerClicked: (ev) => {
+ dis.dispatch({action: 'start_registration'});
+ close();
+ },
+ onLoginClick: (ev) => {
+ dis.dispatch({action: 'start_login'});
+ close();
+ },
+ }).close;
+ },
+
_createChat: function() {
+ if (MatrixClientPeg.get().isGuest()) {
+ dis.dispatch({
+ action: 'do_after_sync_prepared',
+ deferred_action: {
+ action: 'view_create_chat',
+ },
+ });
+ dis.dispatch({action: 'view_set_mxid'});
+ return;
+ }
const ChatInviteDialog = sdk.getComponent("dialogs.ChatInviteDialog");
Modal.createDialog(ChatInviteDialog, {
title: _t('Start a chat'),
@@ -670,6 +722,81 @@ module.exports = React.createClass({
});
},
+ _createRoom: function() {
+ if (MatrixClientPeg.get().isGuest()) {
+ dis.dispatch({
+ action: 'do_after_sync_prepared',
+ deferred_action: {
+ action: 'view_create_room',
+ },
+ });
+ dis.dispatch({action: 'view_set_mxid'});
+ return;
+ }
+ const TextInputDialog = sdk.getComponent("dialogs.TextInputDialog");
+ Modal.createDialog(TextInputDialog, {
+ title: _t('Create Room'),
+ description: _t('Room name (optional)'),
+ button: _t('Create Room'),
+ onFinished: (should_create, name) => {
+ if (should_create) {
+ const createOpts = {};
+ if (name) createOpts.name = name;
+ createRoom({createOpts}).done();
+ }
+ },
+ });
+ },
+
+ _chatCreateOrReuse: function(userId) {
+ const ChatCreateOrReuseDialog = sdk.getComponent(
+ 'views.dialogs.ChatCreateOrReuseDialog',
+ );
+ // Use a deferred action to reshow the dialog once the user has registered
+ if (MatrixClientPeg.get().isGuest()) {
+ // No point in making 2 DMs with welcome bot. This assumes view_set_mxid will
+ // result in a new DM with the welcome user.
+ if (userId !== this.props.config.welcomeUserId) {
+ dis.dispatch({
+ action: 'do_after_sync_prepared',
+ deferred_action: {
+ action: 'view_start_chat_or_reuse',
+ user_id: userId,
+ },
+ });
+ }
+ dis.dispatch({
+ action: 'view_set_mxid',
+ });
+ return;
+ }
+
+ const close = Modal.createDialog(ChatCreateOrReuseDialog, {
+ userId: userId,
+ onFinished: (success) => {
+ if (!success) {
+ // Dialog cancelled, default to home
+ dis.dispatch({ action: 'view_home_page' });
+ }
+ },
+ onNewDMClick: () => {
+ dis.dispatch({
+ action: 'start_chat',
+ user_id: userId,
+ });
+ // Close the dialog, indicate success (calls onFinished(true))
+ close(true);
+ },
+ onExistingRoomSelected: (roomId) => {
+ dis.dispatch({
+ action: 'view_room',
+ room_id: roomId,
+ });
+ close(true);
+ },
+ }).close;
+ },
+
_invite: function(roomId) {
const ChatInviteDialog = sdk.getComponent("dialogs.ChatInviteDialog");
Modal.createDialog(ChatInviteDialog, {
@@ -703,7 +830,7 @@ module.exports = React.createClass({
d.then(() => {
modal.close();
- if (this.currentRoomId === roomId) {
+ if (this.state.currentRoomId === roomId) {
dis.dispatch({action: 'view_next_room'});
}
}, (err) => {
@@ -798,12 +925,27 @@ module.exports = React.createClass({
this._teamToken = teamToken;
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(
+ MatrixClientPeg.get().getUserIdLocalpart(),
+ );
+
if (this.props.config.welcomeUserId && getCurrentLanguage().startsWith("en")) {
- createRoom({dmUserId: this.props.config.welcomeUserId});
+ createRoom({
+ dmUserId: this.props.config.welcomeUserId,
+ // Only view the welcome user if we're NOT looking at a room
+ andView: !this.state.currentRoomId,
+ });
return;
}
// The user has just logged in after registering
- dis.dispatch({action: 'view_room_directory'});
+ dis.dispatch({action: 'view_home_page'});
} else {
this._showScreenAfterLogin();
}
@@ -825,12 +967,8 @@ module.exports = React.createClass({
action: 'view_room',
room_id: localStorage.getItem('mx_last_room_id'),
});
- } else if (this._teamToken) {
- // Team token might be set if we're a guest.
- // Guests do not call _onLoggedIn with a teamToken
- dis.dispatch({action: 'view_home_page'});
} else {
- dis.dispatch({action: 'view_room_directory'});
+ dis.dispatch({action: 'view_home_page'});
}
},
@@ -844,7 +982,6 @@ module.exports = React.createClass({
ready: false,
collapse_lhs: false,
collapse_rhs: false,
- currentRoomAlias: null,
currentRoomId: null,
page_type: PageTypes.RoomDirectory,
});
@@ -882,6 +1019,12 @@ module.exports = React.createClass({
});
cli.on('sync', function(state, prevState) {
+ // LifecycleStore and others cannot directly subscribe to matrix client for
+ // events because flux only allows store state changes during flux dispatches.
+ // So dispatch directly from here. Ideally we'd use a SyncStateStore that
+ // would do this dispatch and expose the sync state itself (by listening to
+ // its own dispatch).
+ dis.dispatch({action: 'sync_state', prevState, state});
self.updateStatusIndicator(state, prevState);
if (state === "SYNCING" && prevState === "SYNCING") {
return;
@@ -951,6 +1094,11 @@ module.exports = React.createClass({
dis.dispatch({
action: 'view_home_page',
});
+ } else if (screen == 'start') {
+ this.showScreen('home');
+ dis.dispatch({
+ action: 'view_set_mxid',
+ });
} else if (screen == 'directory') {
dis.dispatch({
action: 'view_room_directory',
@@ -994,6 +1142,12 @@ module.exports = React.createClass({
}
} else if (screen.indexOf('user/') == 0) {
const userId = screen.substring(5);
+
+ if (params.action === 'chat') {
+ this._chatCreateOrReuse(userId);
+ return;
+ }
+
this.setState({ viewUserId: userId });
this._setPage(PageTypes.UserView);
this.notifyNewScreen('user/' + userId);
@@ -1090,6 +1244,8 @@ module.exports = React.createClass({
},
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;
@@ -1159,13 +1315,6 @@ module.exports = React.createClass({
}
},
- onRoomIdResolved: function(roomId) {
- // It's the RoomView's resposibility to look up room aliases, but we need the
- // ID to pass into things like the Member List, so the Room View tells us when
- // its done that resolution so we can display things that take a room ID.
- this.setState({currentRoomId: roomId});
- },
-
_makeRegistrationUrl: function(params) {
if (this.props.startingFragmentQueryParams.referrer) {
params.referrer = this.props.startingFragmentQueryParams.referrer;
@@ -1207,9 +1356,10 @@ module.exports = React.createClass({
const LoggedInView = sdk.getComponent('structures.LoggedInView');
return (
{
this.forceUpdate();
- }
+ },
});
- if (this.props.roomAddress[0] == '#') {
- // we always look up the alias from the directory server:
- // we want the room that the given alias is pointing to
- // right now. We may have joined that alias before but there's
- // no guarantee the alias hasn't subsequently been remapped.
- MatrixClientPeg.get().getRoomIdForAlias(this.props.roomAddress).done((result) => {
- if (this.props.onRoomIdResolved) {
- this.props.onRoomIdResolved(result.room_id);
- }
- var room = MatrixClientPeg.get().getRoom(result.room_id);
- this.setState({
- room: room,
- roomId: result.room_id,
- roomLoading: !room,
- unsentMessageError: this._getUnsentMessageError(room),
- }, this._onHaveRoom);
- }, (err) => {
- this.setState({
- roomLoading: false,
- roomLoadError: err,
- });
- });
- } else {
- var room = MatrixClientPeg.get().getRoom(this.props.roomAddress);
- this.setState({
- roomId: this.props.roomAddress,
- room: room,
- roomLoading: !room,
- unsentMessageError: this._getUnsentMessageError(room),
- }, this._onHaveRoom);
+ // Start listening for RoomViewStore updates
+ this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
+ this._onRoomViewStoreUpdate(true);
+ },
+
+ _onRoomViewStoreUpdate: function(initial) {
+ if (this.unmounted) {
+ return;
}
+ this.setState({
+ roomId: RoomViewStore.getRoomId(),
+ roomAlias: RoomViewStore.getRoomAlias(),
+ roomLoading: RoomViewStore.isRoomLoading(),
+ roomLoadError: RoomViewStore.getRoomLoadError(),
+ joining: RoomViewStore.isJoining(),
+ }, () => {
+ this._onHaveRoom();
+ this.onRoom(MatrixClientPeg.get().getRoom(this.state.roomId));
+ });
},
_onHaveRoom: function() {
@@ -223,26 +207,29 @@ module.exports = React.createClass({
// 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).
- var user_is_in_room = null;
- if (this.state.room) {
- user_is_in_room = this.state.room.hasMembershipState(
- MatrixClientPeg.get().credentials.userId, 'join'
+ const room = MatrixClientPeg.get().getRoom(this.state.roomId);
+ let isUserJoined = null;
+ if (room) {
+ isUserJoined = room.hasMembershipState(
+ MatrixClientPeg.get().credentials.userId, 'join',
);
- this._updateAutoComplete();
- this.tabComplete.loadEntries(this.state.room);
+ this._updateAutoComplete(room);
+ this.tabComplete.loadEntries(room);
}
-
- if (!user_is_in_room && this.state.roomId) {
+ if (!isUserJoined && !this.state.joining && this.state.roomId) {
if (this.props.autoJoin) {
this.onJoinButtonClicked();
} else if (this.state.roomId) {
console.log("Attempting to peek into room %s", this.state.roomId);
+ this.setState({
+ peekLoading: true,
+ });
MatrixClientPeg.get().peekInRoom(this.state.roomId).then((room) => {
this.setState({
room: room,
- roomLoading: false,
+ peekLoading: false,
});
this._onRoomLoaded(room);
}, (err) => {
@@ -252,16 +239,19 @@ module.exports = React.createClass({
if (err.errcode == "M_GUEST_ACCESS_FORBIDDEN") {
// This is fine: the room just isn't peekable (we assume).
this.setState({
- roomLoading: false,
+ peekLoading: false,
});
} else {
throw err;
}
}).done();
}
- } else if (user_is_in_room) {
+ } else if (isUserJoined) {
MatrixClientPeg.get().stopPeeking();
- this._onRoomLoaded(this.state.room);
+ this.setState({
+ unsentMessageError: this._getUnsentMessageError(room),
+ });
+ this._onRoomLoaded(room);
}
},
@@ -298,10 +288,6 @@ module.exports = React.createClass({
},
componentWillReceiveProps: function(newProps) {
- if (newProps.roomAddress != this.props.roomAddress) {
- throw new Error(_t("changing room on a RoomView is not supported"));
- }
-
if (newProps.eventId != this.props.eventId) {
// when we change focussed event id, hide the search results.
this.setState({searchResults: null});
@@ -362,6 +348,11 @@ module.exports = React.createClass({
document.removeEventListener("keydown", this.onKeyDown);
+ // Remove RoomStore listener
+ if (this._roomStoreToken) {
+ this._roomStoreToken.remove();
+ }
+
// cancel any pending calls to the rate_limited_funcs
this._updateRoomMembers.cancelPendingCall();
@@ -527,7 +518,7 @@ module.exports = React.createClass({
this._updatePreviewUrlVisibility(room);
},
- _warnAboutEncryption: function (room) {
+ _warnAboutEncryption: function(room) {
if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) {
return;
}
@@ -608,20 +599,14 @@ module.exports = React.createClass({
},
onRoom: function(room) {
- // This event is fired when the room is 'stored' by the JS SDK, which
- // means it's now a fully-fledged room object ready to be used, so
- // set it in our state and start using it (ie. init the timeline)
- // This will happen if we start off viewing a room we're not joined,
- // then join it whilst RoomView is looking at that room.
- if (!this.state.room && room.roomId == this._joiningRoomId) {
- this._joiningRoomId = undefined;
- this.setState({
- room: room,
- joining: false,
- });
-
- this._onRoomLoaded(room);
+ if (!room || room.roomId !== this.state.roomId) {
+ return;
}
+ this.setState({
+ room: room,
+ }, () => {
+ this._onRoomLoaded(room);
+ });
},
updateTint: function() {
@@ -687,7 +672,7 @@ module.exports = React.createClass({
// refresh the tab complete list
this.tabComplete.loadEntries(this.state.room);
- this._updateAutoComplete();
+ this._updateAutoComplete(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
@@ -704,10 +689,6 @@ module.exports = React.createClass({
// compatability workaround, let's not bother.
Rooms.setDMRoom(this.state.room.roomId, me.events.member.getSender()).done();
}
-
- this.setState({
- joining: false
- });
}
}, 500),
@@ -782,41 +763,62 @@ module.exports = React.createClass({
},
onJoinButtonClicked: function(ev) {
- var self = this;
+ const cli = MatrixClientPeg.get();
- var cli = MatrixClientPeg.get();
- var display_name_promise = q();
- // if this is the first room we're joining, check the user has a display name
- // and if they don't, prompt them to set one.
- // NB. This unfortunately does not re-use the ChangeDisplayName component because
- // it doesn't behave quite as desired here (we want an input field here rather than
- // content-editable, and we want a default).
- if (cli.getRooms().filter((r) => {
- return r.hasMembershipState(cli.credentials.userId, "join");
- })) {
- display_name_promise = cli.getProfileInfo(cli.credentials.userId).then((result) => {
- if (!result.displayname) {
- var SetDisplayNameDialog = sdk.getComponent('views.dialogs.SetDisplayNameDialog');
- var dialog_defer = q.defer();
- Modal.createDialog(SetDisplayNameDialog, {
- currentDisplayName: result.displayname,
- onFinished: (submitted, newDisplayName) => {
- if (submitted) {
- cli.setDisplayName(newDisplayName).done(() => {
- dialog_defer.resolve();
- });
- }
- else {
- dialog_defer.reject();
- }
- }
- });
- return dialog_defer.promise;
- }
+ // If the user is a ROU, allow them to transition to a PWLU
+ if (cli && cli.isGuest()) {
+ // Join this room once the user has registered and logged in
+ const signUrl = this.props.thirdPartyInvite ?
+ this.props.thirdPartyInvite.inviteSignUrl : undefined;
+ dis.dispatch({
+ action: 'do_after_sync_prepared',
+ deferred_action: {
+ action: 'join_room',
+ opts: { inviteSignUrl: signUrl },
+ },
});
+
+ // Don't peek whilst registering otherwise getPendingEventList complains
+ // Do this by indicating our intention to join
+ dis.dispatch({
+ action: 'will_join',
+ });
+
+ const SetMxIdDialog = sdk.getComponent('views.dialogs.SetMxIdDialog');
+ const close = Modal.createDialog(SetMxIdDialog, {
+ homeserverUrl: cli.getHomeserverUrl(),
+ onFinished: (submitted, credentials) => {
+ if (submitted) {
+ this.props.onRegistered(credentials);
+ } else {
+ dis.dispatch({
+ action: 'cancel_after_sync_prepared',
+ });
+ dis.dispatch({
+ action: 'cancel_join',
+ });
+ }
+ },
+ onDifferentServerClicked: (ev) => {
+ dis.dispatch({action: 'start_registration'});
+ close();
+ },
+ onLoginClick: (ev) => {
+ dis.dispatch({action: 'start_login'});
+ close();
+ },
+ }).close;
+ return;
}
- display_name_promise.then(() => {
+ q().then(() => {
+ const signUrl = this.props.thirdPartyInvite ?
+ this.props.thirdPartyInvite.inviteSignUrl : undefined;
+ dis.dispatch({
+ action: 'join_room',
+ opts: { inviteSignUrl: signUrl },
+ });
+
// if this is an invite and has the 'direct' hint set, mark it as a DM room now.
if (this.state.room) {
const me = this.state.room.getMember(MatrixClientPeg.get().credentials.userId);
@@ -828,72 +830,7 @@ module.exports = React.createClass({
}
}
}
-
return q();
- }).then(() => {
- var sign_url = this.props.thirdPartyInvite ? this.props.thirdPartyInvite.inviteSignUrl : undefined;
- return MatrixClientPeg.get().joinRoom(this.props.roomAddress,
- { inviteSignUrl: sign_url } );
- }).then(function(resp) {
- var roomId = resp.roomId;
-
- // It is possible that there is no Room yet if state hasn't come down
- // from /sync - joinRoom will resolve when the HTTP request to join succeeds,
- // NOT when it comes down /sync. If there is no room, we'll keep the
- // joining flag set until we see it.
-
- // We'll need to initialise the timeline when joining, but due to
- // the above, we can't do it here: we do it in onRoom instead,
- // once we have a useable room object.
- var room = MatrixClientPeg.get().getRoom(roomId);
- if (!room) {
- // wait for the room to turn up in onRoom.
- self._joiningRoomId = roomId;
- } else {
- // we've got a valid room, but that might also just mean that
- // it was peekable (so we had one before anyway). If we are
- // not yet a member of the room, we will need to wait for that
- // to happen, in onRoomStateMember.
- var me = MatrixClientPeg.get().credentials.userId;
- self.setState({
- joining: !room.hasMembershipState(me, "join"),
- room: room
- });
- }
- }).catch(function(error) {
- self.setState({
- joining: false,
- joinError: error
- });
-
- if (!error) return;
-
- // https://matrix.org/jira/browse/SYN-659
- // Need specific error message if joining a room is refused because the user is a guest and guest access is not allowed
- if (
- error.errcode == 'M_GUEST_ACCESS_FORBIDDEN' ||
- (
- error.errcode == 'M_FORBIDDEN' &&
- MatrixClientPeg.get().isGuest()
- )
- ) {
- var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
- Modal.createDialog(NeedToRegisterDialog, {
- title: _t("Failed to join the room"),
- description: _t("This room is private or inaccessible to guests. You may be able to join if you register.")
- });
- } else {
- var msg = error.message ? error.message : JSON.stringify(error);
- var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
- Modal.createDialog(ErrorDialog, {
- title: _t("Failed to join room"),
- description: msg,
- });
- }
- }).done();
-
- this.setState({
- joining: true
});
},
@@ -945,11 +882,7 @@ module.exports = React.createClass({
uploadFile: function(file) {
if (MatrixClientPeg.get().isGuest()) {
- var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
- Modal.createDialog(NeedToRegisterDialog, {
- title: _t("Please Register"),
- description: _t("Guest users can't upload files. Please register to upload.")
- });
+ dis.dispatch({action: 'view_set_mxid'});
return;
}
@@ -1474,9 +1407,9 @@ module.exports = React.createClass({
}
},
- _updateAutoComplete: function() {
+ _updateAutoComplete: function(room) {
const myUserId = MatrixClientPeg.get().credentials.userId;
- const members = this.state.room.getJoinedMembers().filter(function(member) {
+ const members = room.getJoinedMembers().filter(function(member) {
if (member.userId !== myUserId) return true;
});
UserProvider.getInstance().setUserList(members);
@@ -1496,7 +1429,7 @@ module.exports = React.createClass({
const TimelinePanel = sdk.getComponent("structures.TimelinePanel");
if (!this.state.room) {
- if (this.state.roomLoading) {
+ if (this.state.roomLoading || this.state.peekLoading) {
return (
@@ -1514,7 +1447,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.props.roomAddress[0] == '#' ? this.props.roomAddress : null;
+ var room_alias = this.state.room_alias;
return (
- ;
+
+ if (tiles.length === 0) {
+ this.setState({
+ busyProfile: true,
+ });
+ MatrixClientPeg.get().getProfileInfo(this.props.userId).done((resp) => {
+ const profile = {
+ displayName: resp.displayname,
+ avatarUrl: null,
+ };
+ if (resp.avatar_url) {
+ profile.avatarUrl = MatrixClientPeg.get().mxcUrlToHttp(
+ resp.avatar_url, 48, 48, "crop",
+ );
+ }
+ this.setState({
+ busyProfile: false,
+ profile: profile,
+ });
+ }, (err) => {
+ console.error(
+ 'Unable to get profile for user ' + this.props.userId + ':',
+ err,
+ );
+ this.setState({
+ busyProfile: false,
+ profileError: err,
+ });
+ });
+ }
+ }
+
+ onRoomTileClick(roomId) {
+ this.props.onExistingRoomSelected(roomId);
+ }
+
+ render() {
+ let title = '';
+ let content = null;
+ if (this.state.tiles.length > 0) {
+ // Show the existing rooms with a "+" to add a new dm
+ title = _t('Create a new chat or reuse an existing one');
+ const labelClasses = classNames({
+ mx_MemberInfo_createRoom_label: true,
+ mx_RoomTile_name: true,
+ });
+ const startNewChat =
+
+
+
+
{ _t("Start new chat") }
+ ;
+ content =
+ { _t('You already have existing direct chats with this user:') }
+
+ { this.state.tiles }
+ { startNewChat }
+
+
;
+ } else {
+ // Show the avatar, name and a button to confirm that a new chat is requested
+ const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
+ const Spinner = sdk.getComponent('elements.Spinner');
+ title = _t('Start chatting');
+
+ let profile = null;
+ if (this.state.busyProfile) {
+ profile = ;
+ } else if (this.state.profileError) {
+ profile =
+ Unable to load profile information for { this.props.userId }
+
+
+ );
+ },
+});
diff --git a/src/components/views/elements/ActionButton.js b/src/components/views/elements/ActionButton.js
new file mode 100644
index 0000000000..08fb6faa1d
--- /dev/null
+++ b/src/components/views/elements/ActionButton.js
@@ -0,0 +1,84 @@
+/*
+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 React from 'react';
+import PropTypes from 'prop-types';
+import AccessibleButton from './AccessibleButton';
+import dis from '../../../dispatcher';
+import sdk from '../../../index';
+
+export default React.createClass({
+ displayName: 'RoleButton',
+
+ propTypes: {
+ size: PropTypes.string,
+ tooltip: PropTypes.bool,
+ action: PropTypes.string.isRequired,
+ mouseOverAction: PropTypes.string,
+ label: PropTypes.string.isRequired,
+ iconPath: PropTypes.string.isRequired,
+ },
+
+ getDefaultProps: function() {
+ return {
+ size: "25",
+ tooltip: false,
+ };
+ },
+
+ getInitialState: function() {
+ return {
+ showTooltip: false,
+ };
+ },
+
+ _onClick: function(ev) {
+ ev.stopPropagation();
+ dis.dispatch({action: this.props.action});
+ },
+
+ _onMouseEnter: function() {
+ if (this.props.tooltip) this.setState({showTooltip: true});
+ if (this.props.mouseOverAction) {
+ dis.dispatch({action: this.props.mouseOverAction});
+ }
+ },
+
+ _onMouseLeave: function() {
+ this.setState({showTooltip: false});
+ },
+
+ render: function() {
+ const TintableSvg = sdk.getComponent("elements.TintableSvg");
+
+ let tooltip;
+ if (this.state.showTooltip) {
+ const RoomTooltip = sdk.getComponent("rooms.RoomTooltip");
+ tooltip = ;
+ }
+
+ return (
+
+
+ {tooltip}
+
+ );
+ }
+});
diff --git a/src/components/views/elements/CreateRoomButton.js b/src/components/views/elements/CreateRoomButton.js
new file mode 100644
index 0000000000..22d222c6f1
--- /dev/null
+++ b/src/components/views/elements/CreateRoomButton.js
@@ -0,0 +1,40 @@
+/*
+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 React from 'react';
+import sdk from '../../../index';
+import PropTypes from 'prop-types';
+import { _t } from '../../../languageHandler';
+
+const CreateRoomButton = function(props) {
+ const ActionButton = sdk.getComponent('elements.ActionButton');
+ return (
+
+ );
+};
+
+CreateRoomButton.propTypes = {
+ size: PropTypes.string,
+ tooltip: PropTypes.bool,
+};
+
+export default CreateRoomButton;
diff --git a/src/components/views/elements/HomeButton.js b/src/components/views/elements/HomeButton.js
new file mode 100644
index 0000000000..80c64c6a60
--- /dev/null
+++ b/src/components/views/elements/HomeButton.js
@@ -0,0 +1,39 @@
+/*
+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 React from 'react';
+import sdk from '../../../index';
+import PropTypes from 'prop-types';
+import { _t } from '../../../languageHandler';
+
+const HomeButton = function(props) {
+ const ActionButton = sdk.getComponent('elements.ActionButton');
+ return (
+
+ );
+};
+
+HomeButton.propTypes = {
+ size: PropTypes.string,
+ tooltip: PropTypes.bool,
+};
+
+export default HomeButton;
diff --git a/src/components/views/elements/RoomDirectoryButton.js b/src/components/views/elements/RoomDirectoryButton.js
new file mode 100644
index 0000000000..d964d9e5bc
--- /dev/null
+++ b/src/components/views/elements/RoomDirectoryButton.js
@@ -0,0 +1,40 @@
+/*
+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 React from 'react';
+import sdk from '../../../index';
+import PropTypes from 'prop-types';
+import { _t } from '../../../languageHandler';
+
+const RoomDirectoryButton = function(props) {
+ const ActionButton = sdk.getComponent('elements.ActionButton');
+ return (
+
+ );
+};
+
+RoomDirectoryButton.propTypes = {
+ size: PropTypes.string,
+ tooltip: PropTypes.bool,
+};
+
+export default RoomDirectoryButton;
diff --git a/src/components/views/elements/SettingsButton.js b/src/components/views/elements/SettingsButton.js
new file mode 100644
index 0000000000..ad09971689
--- /dev/null
+++ b/src/components/views/elements/SettingsButton.js
@@ -0,0 +1,39 @@
+/*
+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 React from 'react';
+import sdk from '../../../index';
+import PropTypes from 'prop-types';
+import { _t } from '../../../languageHandler';
+
+const SettingsButton = function(props) {
+ const ActionButton = sdk.getComponent('elements.ActionButton');
+ return (
+
+ );
+};
+
+SettingsButton.propTypes = {
+ size: PropTypes.string,
+ tooltip: PropTypes.bool,
+};
+
+export default SettingsButton;
diff --git a/src/components/views/elements/StartChatButton.js b/src/components/views/elements/StartChatButton.js
new file mode 100644
index 0000000000..05a7db0b9c
--- /dev/null
+++ b/src/components/views/elements/StartChatButton.js
@@ -0,0 +1,40 @@
+/*
+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 React from 'react';
+import sdk from '../../../index';
+import PropTypes from 'prop-types';
+import { _t } from '../../../languageHandler';
+
+const StartChatButton = function(props) {
+ const ActionButton = sdk.getComponent('elements.ActionButton');
+ return (
+
+ );
+};
+
+StartChatButton.propTypes = {
+ size: PropTypes.string,
+ tooltip: PropTypes.bool,
+};
+
+export default StartChatButton;
diff --git a/src/components/views/login/LoginFooter.js b/src/components/views/login/LoginFooter.js
index a5183ffff3..8bdec71685 100644
--- a/src/components/views/login/LoginFooter.js
+++ b/src/components/views/login/LoginFooter.js
@@ -16,6 +16,7 @@ limitations under the License.
'use strict';
+import { _t } from '../../../languageHandler';
import React from 'react';
module.exports = React.createClass({
@@ -27,5 +28,5 @@ module.exports = React.createClass({
{_t("powered by Matrix")}
);
- }
+ },
});
diff --git a/src/components/views/room_settings/ColorSettings.js b/src/components/views/room_settings/ColorSettings.js
index 6a455d9c3c..5fc845a541 100644
--- a/src/components/views/room_settings/ColorSettings.js
+++ b/src/components/views/room_settings/ColorSettings.js
@@ -21,6 +21,8 @@ var Tinter = require('../../../Tinter');
var MatrixClientPeg = require("../../../MatrixClientPeg");
var Modal = require("../../../Modal");
+import dis from '../../../dispatcher';
+
var ROOM_COLORS = [
// magic room default values courtesy of Ribot
["#76cfa6", "#eaf5f0"],
@@ -86,11 +88,7 @@ module.exports = React.createClass({
}
).catch(function(err) {
if (err.errcode == 'M_GUEST_ACCESS_FORBIDDEN') {
- var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
- Modal.createDialog(NeedToRegisterDialog, {
- title: "Please Register",
- description: "Saving room color settings is only available to registered users"
- });
+ dis.dispatch({action: 'view_set_mxid'});
}
});
}
diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js
index 86d39f91ec..ece97c7778 100644
--- a/src/components/views/rooms/MemberInfo.js
+++ b/src/components/views/rooms/MemberInfo.js
@@ -375,11 +375,7 @@ module.exports = WithMatrixClient(React.createClass({
console.log("Mod toggle success");
}, function(err) {
if (err.errcode == 'M_GUEST_ACCESS_FORBIDDEN') {
- var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
- Modal.createDialog(NeedToRegisterDialog, {
- title: _t("Please Register"),
- description: _t("This action cannot be performed by a guest user. Please register to be able to do this") + ".",
- });
+ dis.dispatch({action: 'view_set_mxid'});
} else {
console.error("Toggle moderator error:" + err);
Modal.createDialog(ErrorDialog, {
diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js
index 4b812263ce..1fc377c11e 100644
--- a/src/components/views/rooms/MessageComposer.js
+++ b/src/components/views/rooms/MessageComposer.js
@@ -91,11 +91,7 @@ export default class MessageComposer extends React.Component {
onUploadClick(ev) {
if (MatrixClientPeg.get().isGuest()) {
- let NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
- Modal.createDialog(NeedToRegisterDialog, {
- title: _t('Please Register'),
- description: _t('Guest users can\'t upload files. Please register to upload') + '.',
- });
+ dis.dispatch({action: 'view_set_mxid'});
return;
}
diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js
index 3b43ea2b39..bf270b1148 100644
--- a/src/components/views/rooms/RoomList.js
+++ b/src/components/views/rooms/RoomList.js
@@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
+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.
@@ -30,7 +31,14 @@ var Rooms = require('../../../Rooms');
import DMRoomMap from '../../../utils/DMRoomMap';
var Receipt = require('../../../utils/Receipt');
-var HIDE_CONFERENCE_CHANS = true;
+const HIDE_CONFERENCE_CHANS = true;
+
+const VERBS = {
+ 'm.favourite': 'favourite',
+ 'im.vector.fake.direct': 'tag direct chat',
+ 'im.vector.fake.recent': 'restore',
+ 'm.lowpriority': 'demote',
+};
module.exports = React.createClass({
displayName: 'RoomList',
@@ -45,6 +53,7 @@ module.exports = React.createClass({
getInitialState: function() {
return {
isLoadingLeftRooms: false,
+ totalRoomCount: null,
lists: {},
incomingCall: null,
};
@@ -64,8 +73,14 @@ module.exports = React.createClass({
cli.on("RoomMember.name", this.onRoomMemberName);
cli.on("accountData", this.onAccountData);
- var s = this.getRoomLists();
- this.setState(s);
+ this.refreshRoomList();
+
+ // order of the sublists
+ //this.listOrder = [];
+
+ // loop count to stop a stack overflow if the user keeps waggling the
+ // mouse for >30s in a row, or if running under mocha
+ this._delayedRefreshRoomListLoopCount = 0
},
componentDidMount: function() {
@@ -203,31 +218,33 @@ module.exports = React.createClass({
}, 500),
refreshRoomList: function() {
- // console.log("DEBUG: Refresh room list delta=%s ms",
- // (!this._lastRefreshRoomListTs ? "-" : (Date.now() - this._lastRefreshRoomListTs))
- // );
-
- // TODO: rather than bluntly regenerating and re-sorting everything
- // every time we see any kind of room change from the JS SDK
- // we could do incremental updates on our copy of the state
- // based on the room which has actually changed. This would stop
- // us re-rendering all the sublists every time anything changes anywhere
- // in the state of the client.
- this.setState(this.getRoomLists());
+ // TODO: ideally we'd calculate this once at start, and then maintain
+ // any changes to it incrementally, updating the appropriate sublists
+ // as needed.
+ // Alternatively we'd do something magical with Immutable.js or similar.
+ const lists = this.getRoomLists();
+ let totalRooms = 0;
+ for (const l of Object.values(lists)) {
+ totalRooms += l.length;
+ }
+ this.setState({
+ lists: this.getRoomLists(),
+ totalRoomCount: totalRooms,
+ });
// this._lastRefreshRoomListTs = Date.now();
},
getRoomLists: function() {
var self = this;
- var s = { lists: {} };
+ const lists = {};
- s.lists["im.vector.fake.invite"] = [];
- s.lists["m.favourite"] = [];
- s.lists["im.vector.fake.recent"] = [];
- s.lists["im.vector.fake.direct"] = [];
- s.lists["m.lowpriority"] = [];
- s.lists["im.vector.fake.archived"] = [];
+ lists["im.vector.fake.invite"] = [];
+ lists["m.favourite"] = [];
+ lists["im.vector.fake.recent"] = [];
+ lists["im.vector.fake.direct"] = [];
+ lists["m.lowpriority"] = [];
+ lists["im.vector.fake.archived"] = [];
const dmRoomMap = new DMRoomMap(MatrixClientPeg.get());
@@ -241,7 +258,7 @@ module.exports = React.createClass({
// ", prevMembership = " + me.events.member.getPrevContent().membership);
if (me.membership == "invite") {
- s.lists["im.vector.fake.invite"].push(room);
+ lists["im.vector.fake.invite"].push(room);
}
else if (HIDE_CONFERENCE_CHANS && Rooms.isConfCallRoom(room, me, self.props.ConferenceHandler)) {
// skip past this room & don't put it in any lists
@@ -255,20 +272,20 @@ module.exports = React.createClass({
if (tagNames.length) {
for (var i = 0; i < tagNames.length; i++) {
var tagName = tagNames[i];
- s.lists[tagName] = s.lists[tagName] || [];
- s.lists[tagNames[i]].push(room);
+ lists[tagName] = lists[tagName] || [];
+ lists[tagName].push(room);
}
}
else if (dmRoomMap.getUserIdForRoomId(room.roomId)) {
// "Direct Message" rooms (that we're still in and that aren't otherwise tagged)
- s.lists["im.vector.fake.direct"].push(room);
+ lists["im.vector.fake.direct"].push(room);
}
else {
- s.lists["im.vector.fake.recent"].push(room);
+ lists["im.vector.fake.recent"].push(room);
}
}
else if (me.membership === "leave") {
- s.lists["im.vector.fake.archived"].push(room);
+ lists["im.vector.fake.archived"].push(room);
}
else {
console.error("unrecognised membership: " + me.membership + " - this should never happen");
@@ -277,7 +294,22 @@ module.exports = React.createClass({
// we actually apply the sorting to this when receiving the prop in RoomSubLists.
- return s;
+ // we'll need this when we get to iterating through lists programatically - e.g. ctrl-shift-up/down
+/*
+ this.listOrder = [
+ "im.vector.fake.invite",
+ "m.favourite",
+ "im.vector.fake.recent",
+ "im.vector.fake.direct",
+ Object.keys(otherTagNames).filter(tagName=>{
+ return (!tagName.match(/^m\.(favourite|lowpriority)$/));
+ }).sort(),
+ "m.lowpriority",
+ "im.vector.fake.archived"
+ ];
+*/
+
+ return lists;
},
_getScrollNode: function() {
@@ -431,6 +463,62 @@ module.exports = React.createClass({
this.refs.gemscroll.forceUpdate();
},
+ _getEmptyContent: function(section) {
+ const RoomDropTarget = sdk.getComponent('rooms.RoomDropTarget');
+
+ if (this.props.collapsed) {
+ return ;
+ }
+
+ const StartChatButton = sdk.getComponent('elements.StartChatButton');
+ const RoomDirectoryButton = sdk.getComponent('elements.RoomDirectoryButton');
+ const CreateRoomButton = sdk.getComponent('elements.CreateRoomButton');
+
+ const TintableSvg = sdk.getComponent('elements.TintableSvg');
+ switch (section) {
+ case 'im.vector.fake.direct':
+ return
+ Press
+
+ to start a chat with someone
+
;
+ case 'im.vector.fake.recent':
+ return
+ You're not in any rooms yet! Press
+
+ to make a room or
+
+ to browse the directory
+
diff --git a/src/createRoom.js b/src/createRoom.js
index cd8cd7bac3..4d7f5792f3 100644
--- a/src/createRoom.js
+++ b/src/createRoom.js
@@ -37,17 +37,11 @@ function createRoom(opts) {
opts = opts || {};
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
- const NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
const Loader = sdk.getComponent("elements.Spinner");
const client = MatrixClientPeg.get();
if (client.isGuest()) {
- setTimeout(()=>{
- Modal.createDialog(NeedToRegisterDialog, {
- title: _t('Please Register'),
- description: _t('Guest users can\'t create new rooms. Please register to create room and start a chat.')
- });
- }, 0);
+ dis.dispatch({action: 'view_set_mxid'});
return q(null);
}
@@ -64,6 +58,11 @@ function createRoom(opts) {
createOpts.is_direct = true;
}
+ // By default, view the room after creating it
+ if (opts.andView === undefined) {
+ opts.andView = true;
+ }
+
// Allow guests by default since the room is private and they'd
// need an invite. This means clicking on a 3pid invite email can
// actually drop you right in to a chat.
@@ -97,10 +96,12 @@ function createRoom(opts) {
// room has been created, so we race here with the client knowing that
// the room exists, causing things like
// https://github.com/vector-im/vector-web/issues/1813
- dis.dispatch({
- action: 'view_room',
- room_id: roomId
- });
+ if (opts.andView) {
+ dis.dispatch({
+ action: 'view_room',
+ room_id: roomId,
+ });
+ }
return roomId;
}, function(err) {
console.error("Failed to create room " + roomId + " " + err);
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 56f38cc87c..c87ffb8cdf 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -740,6 +740,11 @@
"%(severalUsers)schanged their avatar": "%(severalUsers)schanged their avatar",
"%(oneUser)schanged their avatar": "%(oneUser)schanged their avatar",
"Please select the destination room for this message": "Please select the destination room for this message",
+ "Create new room": "Create new room",
+ "Welcome page": "Welcome page",
+ "Room directory": "Room directory",
+ "Start chat": "Start chat",
+ "New Password": "New Password",
"Start automatically after system login": "Start automatically after system login",
"Desktop specific": "Desktop specific",
"Analytics": "Analytics",
@@ -749,7 +754,6 @@
"Passphrases must match": "Passphrases must match",
"Passphrase must not be empty": "Passphrase must not be empty",
"Export room keys": "Export room keys",
- "Enter passphrase": "Enter passphrase",
"Confirm passphrase": "Confirm passphrase",
"Import room keys": "Import room keys",
"File to import": "File to import",
@@ -758,6 +762,7 @@
"This process allows you to import encryption keys that you had previously exported from another Matrix client. You will then be able to decrypt any messages that the other client could decrypt.": "This process allows you to import encryption keys that you had previously exported from another Matrix client. You will then be able to decrypt any messages that the other client could decrypt.",
"The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.": "The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.",
"You must join the room to see its files": "You must join the room to see its files",
+ "Server may be unavailable, overloaded, or you hit a bug.": "Server may be unavailable, overloaded, or you hit a bug.",
"Reject all %(invitedRooms)s invites": "Reject all %(invitedRooms)s invites",
"Start new chat": "Start new chat",
"Guest users can't invite users. Please register.": "Guest users can't invite users. Please register.",
@@ -822,6 +827,7 @@
"You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?": "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?",
"Removed or unknown message type": "Removed or unknown message type",
"Disable URL previews by default for participants in this room": "Disable URL previews by default for participants in this room",
+ "Disable URL previews for this room (affects only you)": "Disable URL previews for this room (affects only you)",
"URL previews are %(globalDisableUrlPreview)s by default for participants in this room.": "URL previews are %(globalDisableUrlPreview)s by default for participants in this room.",
"URL Previews": "URL Previews",
"Enable URL previews for this room (affects only you)": "Enable URL previews for this room (affects only you)",
@@ -835,8 +841,21 @@
"Online": "Online",
"Idle": "Idle",
"Offline": "Offline",
+ "disabled": "disabled",
+ "enabled": "enabled",
+ "Start chatting": "Start chatting",
+ "Start Chatting": "Start Chatting",
+ "Click on the button below to start chatting!": "Click on the button below to start chatting!",
+ "Create a new chat or reuse an existing one": "Create a new chat or reuse an existing one",
+ "You already have existing direct chats with this user:": "You already have existing direct chats with this user:",
+ "Start new chat": "Start new chat",
"Disable URL previews for this room (affects only you)": "Disable URL previews for this room (affects only you)",
"$senderDisplayName changed the room avatar to ": "$senderDisplayName changed the room avatar to ",
"%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s removed the room avatar.",
- "%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s changed the avatar for %(roomName)s"
+ "%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s changed the avatar for %(roomName)s",
+ "Username available": "Username available",
+ "Username not available": "Username not available",
+ "Something went wrong!": "Something went wrong!",
+ "This will be your account name on the homeserver, or you can pick a different server.": "This will be your account name on the homeserver, or you can pick a different server.",
+ "If you already have a Matrix account you can log in instead.": "If you already have a Matrix account you can log in instead."
}
diff --git a/src/stores/LifecycleStore.js b/src/stores/LifecycleStore.js
new file mode 100644
index 0000000000..d954ef16b6
--- /dev/null
+++ b/src/stores/LifecycleStore.js
@@ -0,0 +1,79 @@
+/*
+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 dis from '../dispatcher';
+import {Store} from 'flux/utils';
+
+const INITIAL_STATE = {
+ deferred_action: null,
+};
+
+/**
+ * A class for storing application state to do with login/registration. This is a simple
+ * flux store that listens for actions and updates its state accordingly, informing any
+ * listeners (views) of state changes.
+ */
+class LifecycleStore extends Store {
+ constructor() {
+ super(dis);
+
+ // Initialise state
+ this._state = INITIAL_STATE;
+ }
+
+ _setState(newState) {
+ this._state = Object.assign(this._state, newState);
+ this.__emitChange();
+ }
+
+ __onDispatch(payload) {
+ switch (payload.action) {
+ case 'do_after_sync_prepared':
+ this._setState({
+ deferred_action: payload.deferred_action,
+ });
+ break;
+ case 'cancel_after_sync_prepared':
+ this._setState({
+ deferred_action: null,
+ });
+ break;
+ case 'sync_state':
+ if (payload.state !== 'PREPARED') {
+ break;
+ }
+ if (!this._state.deferred_action) break;
+ const deferredAction = Object.assign({}, this._state.deferred_action);
+ this._setState({
+ deferred_action: null,
+ });
+ dis.dispatch(deferredAction);
+ break;
+ case 'on_logged_out':
+ this.reset();
+ break;
+ }
+ }
+
+ reset() {
+ this._state = Object.assign({}, INITIAL_STATE);
+ }
+}
+
+let singletonLifecycleStore = null;
+if (!singletonLifecycleStore) {
+ singletonLifecycleStore = new LifecycleStore();
+}
+module.exports = singletonLifecycleStore;
diff --git a/src/stores/RoomViewStore.js b/src/stores/RoomViewStore.js
new file mode 100644
index 0000000000..cc8959af7a
--- /dev/null
+++ b/src/stores/RoomViewStore.js
@@ -0,0 +1,205 @@
+/*
+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 dis from '../dispatcher';
+import {Store} from 'flux/utils';
+import MatrixClientPeg from '../MatrixClientPeg';
+import sdk from '../index';
+import Modal from '../Modal';
+import { _t } from '../languageHandler';
+
+const INITIAL_STATE = {
+ // Whether we're joining the currently viewed room
+ joining: false,
+ // Any error occurred during joining
+ joinError: null,
+ // The room ID of the room
+ roomId: null,
+ // The room alias of the room (or null if not originally specified in view_room)
+ roomAlias: null,
+ // Whether the current room is loading
+ roomLoading: false,
+ // Any error that has occurred during loading
+ roomLoadError: null,
+};
+
+/**
+ * A class for storing application state for RoomView. This is the RoomView's interface
+* with a subset of the js-sdk.
+ * ```
+ */
+class RoomViewStore extends Store {
+ constructor() {
+ super(dis);
+
+ // Initialise state
+ this._state = INITIAL_STATE;
+ }
+
+ _setState(newState) {
+ this._state = Object.assign(this._state, newState);
+ this.__emitChange();
+ }
+
+ __onDispatch(payload) {
+ switch (payload.action) {
+ // view_room:
+ // - room_alias: '#somealias:matrix.org'
+ // - room_id: '!roomid123:matrix.org'
+ case 'view_room':
+ this._viewRoom(payload);
+ break;
+ case 'view_room_error':
+ this._viewRoomError(payload);
+ break;
+ case 'will_join':
+ this._setState({
+ joining: true,
+ });
+ break;
+ case 'cancel_join':
+ this._setState({
+ joining: false,
+ });
+ break;
+ // join_room:
+ // - opts: options for joinRoom
+ case 'join_room':
+ this._joinRoom(payload);
+ break;
+ case 'joined_room':
+ this._joinedRoom(payload);
+ break;
+ case 'join_room_error':
+ this._joinRoomError(payload);
+ break;
+ case 'on_logged_out':
+ this.reset();
+ break;
+ }
+ }
+
+ _viewRoom(payload) {
+ // Always set the room ID if present
+ if (payload.room_id) {
+ this._setState({
+ roomId: payload.room_id,
+ roomLoading: false,
+ roomLoadError: null,
+ });
+ } else if (payload.room_alias) {
+ this._setState({
+ roomId: null,
+ roomAlias: payload.room_alias,
+ roomLoading: true,
+ roomLoadError: null,
+ });
+ MatrixClientPeg.get().getRoomIdForAlias(payload.room_alias).done(
+ (result) => {
+ dis.dispatch({
+ action: 'view_room',
+ room_id: result.room_id,
+ room_alias: payload.room_alias,
+ });
+ }, (err) => {
+ dis.dispatch({
+ action: 'view_room_error',
+ room_id: null,
+ room_alias: payload.room_alias,
+ err: err,
+ });
+ });
+ }
+ }
+
+ _viewRoomError(payload) {
+ this._setState({
+ roomId: payload.room_id,
+ roomAlias: payload.room_alias,
+ roomLoading: false,
+ roomLoadError: payload.err,
+ });
+ }
+
+ _joinRoom(payload) {
+ this._setState({
+ joining: true,
+ });
+ MatrixClientPeg.get().joinRoom(this._state.roomId, payload.opts).done(() => {
+ dis.dispatch({
+ action: 'joined_room',
+ });
+ }, (err) => {
+ dis.dispatch({
+ action: 'join_room_error',
+ err: err,
+ });
+ const msg = err.message ? err.message : JSON.stringify(err);
+ const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+ Modal.createDialog(ErrorDialog, {
+ title: _t("Failed to join room"),
+ description: msg,
+ });
+ });
+ }
+
+ _joinedRoom(payload) {
+ this._setState({
+ joining: false,
+ });
+ }
+
+ _joinRoomError(payload) {
+ this._setState({
+ joining: false,
+ joinError: payload.err,
+ });
+ }
+
+ reset() {
+ this._state = Object.assign({}, INITIAL_STATE);
+ }
+
+ getRoomId() {
+ return this._state.roomId;
+ }
+
+ getRoomAlias() {
+ return this._state.roomAlias;
+ }
+
+ isRoomLoading() {
+ return this._state.roomLoading;
+ }
+
+ getRoomLoadError() {
+ return this._state.roomLoadError;
+ }
+
+ isJoining() {
+ return this._state.joining;
+ }
+
+ getJoinError() {
+ return this._state.joinError;
+ }
+
+}
+
+let singletonRoomViewStore = null;
+if (!singletonRoomViewStore) {
+ singletonRoomViewStore = new RoomViewStore();
+}
+module.exports = singletonRoomViewStore;
diff --git a/src/stores/SessionStore.js b/src/stores/SessionStore.js
new file mode 100644
index 0000000000..c4bd39b72c
--- /dev/null
+++ b/src/stores/SessionStore.js
@@ -0,0 +1,88 @@
+/*
+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 dis from '../dispatcher';
+import {Store} from 'flux/utils';
+
+const INITIAL_STATE = {
+ cachedPassword: localStorage.getItem('mx_pass'),
+};
+
+/**
+ * A class for storing application state to do with the session. This is a simple flux
+ * store that listens for actions and updates its state accordingly, informing any
+ * listeners (views) of state changes.
+ *
+ * Usage:
+ * ```
+ * sessionStore.addListener(() => {
+ * this.setState({ cachedPassword: sessionStore.getCachedPassword() })
+ * })
+ * ```
+ */
+class SessionStore extends Store {
+ constructor() {
+ super(dis);
+
+ // Initialise state
+ this._state = INITIAL_STATE;
+ }
+
+ _update() {
+ // Persist state to localStorage
+ if (this._state.cachedPassword) {
+ localStorage.setItem('mx_pass', this._state.cachedPassword);
+ } else {
+ localStorage.removeItem('mx_pass', this._state.cachedPassword);
+ }
+
+ this.__emitChange();
+ }
+
+ _setState(newState) {
+ this._state = Object.assign(this._state, newState);
+ this._update();
+ }
+
+ __onDispatch(payload) {
+ switch (payload.action) {
+ case 'cached_password':
+ this._setState({
+ cachedPassword: payload.cachedPassword,
+ });
+ break;
+ case 'password_changed':
+ this._setState({
+ cachedPassword: null,
+ });
+ break;
+ case 'on_logged_out':
+ this._setState({
+ cachedPassword: null,
+ });
+ break;
+ }
+ }
+
+ getCachedPassword() {
+ return this._state.cachedPassword;
+ }
+}
+
+let singletonSessionStore = null;
+if (!singletonSessionStore) {
+ singletonSessionStore = new SessionStore();
+}
+module.exports = singletonSessionStore;
diff --git a/test/components/structures/RoomView-test.js b/test/components/structures/RoomView-test.js
deleted file mode 100644
index 8e7c8160b8..0000000000
--- a/test/components/structures/RoomView-test.js
+++ /dev/null
@@ -1,67 +0,0 @@
-var React = require('react');
-var expect = require('expect');
-var sinon = require('sinon');
-var ReactDOM = require("react-dom");
-
-var sdk = require('matrix-react-sdk');
-var RoomView = sdk.getComponent('structures.RoomView');
-var peg = require('../../../src/MatrixClientPeg');
-
-var test_utils = require('../../test-utils');
-var q = require('q');
-
-var Skinner = require("../../../src/Skinner");
-var stubComponent = require('../../components/stub-component.js');
-
-describe('RoomView', function () {
- var sandbox;
- var parentDiv;
-
- beforeEach(function() {
- test_utils.beforeEach(this);
- sandbox = test_utils.stubClient();
- parentDiv = document.createElement('div');
-
- this.oldTimelinePanel = Skinner.getComponent('structures.TimelinePanel');
- this.oldRoomHeader = Skinner.getComponent('views.rooms.RoomHeader');
- Skinner.addComponent('structures.TimelinePanel', stubComponent());
- Skinner.addComponent('views.rooms.RoomHeader', stubComponent());
-
- peg.get().credentials = { userId: "@test:example.com" };
- });
-
- afterEach(function() {
- sandbox.restore();
-
- ReactDOM.unmountComponentAtNode(parentDiv);
-
- Skinner.addComponent('structures.TimelinePanel', this.oldTimelinePanel);
- Skinner.addComponent('views.rooms.RoomHeader', this.oldRoomHeader);
- });
-
- it('resolves a room alias to a room id', function (done) {
- peg.get().getRoomIdForAlias.returns(q({room_id: "!randomcharacters:aser.ver"}));
-
- function onRoomIdResolved(room_id) {
- expect(room_id).toEqual("!randomcharacters:aser.ver");
- done();
- }
-
- ReactDOM.render(, parentDiv);
- });
-
- it('joins by alias if given an alias', function (done) {
- peg.get().getRoomIdForAlias.returns(q({room_id: "!randomcharacters:aser.ver"}));
- peg.get().getProfileInfo.returns(q({displayname: "foo"}));
- var roomView = ReactDOM.render(, parentDiv);
-
- peg.get().joinRoom = function(x) {
- expect(x).toEqual('#alias:ser.ver');
- done();
- };
-
- process.nextTick(function() {
- roomView.onJoinButtonClicked();
- });
- });
-});
diff --git a/test/components/structures/login/Registration-test.js b/test/components/structures/login/Registration-test.js
new file mode 100644
index 0000000000..b4b54a6315
--- /dev/null
+++ b/test/components/structures/login/Registration-test.js
@@ -0,0 +1,105 @@
+/*
+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.
+*/
+
+const React = require('react');
+const ReactDOM = require('react-dom');
+const ReactTestUtils = require('react-addons-test-utils');
+const expect = require('expect');
+
+const testUtils = require('test-utils');
+
+const sdk = require('matrix-react-sdk');
+const Registration = sdk.getComponent('structures.login.Registration');
+
+let rtsClient;
+let client;
+
+const TEAM_CONFIG = {
+ supportEmail: 'support@some.domain',
+ teamServerURL: 'http://someteamserver.bla',
+};
+
+const CREDENTIALS = {userId: '@me:here'};
+const MOCK_REG_RESPONSE = {
+ user_id: CREDENTIALS.userId,
+ device_id: 'mydevice',
+ access_token: '2234569864534231',
+};
+
+describe('Registration', function() {
+ beforeEach(function() {
+ testUtils.beforeEach(this);
+ client = testUtils.createTestClient();
+ client.credentials = CREDENTIALS;
+
+ // Mock an RTS client that supports one team and naively returns team tokens when
+ // tracking by mapping email SIDs to team tokens. This is fine because we only
+ // want to assert the client behaviour such that a user recognised by the
+ // rtsClient (which would normally talk to the RTS server) as a team member is
+ // correctly logged in as one (and other such assertions).
+ rtsClient = testUtils.createTestRtsClient(
+ {
+ 'myawesometeam123': {
+ name: 'Team Awesome',
+ domain: 'team.awesome.net',
+ },
+ },
+ {'someEmailSid1234': 'myawesometeam123'},
+ );
+ });
+
+ it('should track a referral following successful registration of a team member', function(done) {
+ const expectedCreds = {
+ userId: MOCK_REG_RESPONSE.user_id,
+ deviceId: MOCK_REG_RESPONSE.device_id,
+ homeserverUrl: client.getHomeserverUrl(),
+ identityServerUrl: client.getIdentityServerUrl(),
+ accessToken: MOCK_REG_RESPONSE.access_token,
+ };
+ const onLoggedIn = function(creds, teamToken) {
+ expect(creds).toEqual(expectedCreds);
+ expect(teamToken).toBe('myawesometeam123');
+ done();
+ };
+
+ const res = ReactTestUtils.renderIntoDocument(
+ ,
+ );
+
+ res._onUIAuthFinished(true, MOCK_REG_RESPONSE, {emailSid: 'someEmailSid1234'});
+ });
+
+ it('should NOT track a referral following successful registration of a non-team member', function(done) {
+ const onLoggedIn = expect.createSpy().andCall(function(creds, teamToken) {
+ expect(teamToken).toNotExist();
+ done();
+ });
+
+ const res = ReactTestUtils.renderIntoDocument(
+ ,
+ );
+
+ res._onUIAuthFinished(true, MOCK_REG_RESPONSE, {emailSid: 'someOtherEmailSid11'});
+ });
+});
diff --git a/test/components/views/login/RegistrationForm-test.js b/test/components/views/login/RegistrationForm-test.js
new file mode 100644
index 0000000000..81db5b487b
--- /dev/null
+++ b/test/components/views/login/RegistrationForm-test.js
@@ -0,0 +1,86 @@
+/*
+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.
+*/
+
+const React = require('react');
+const ReactDOM = require("react-dom");
+const ReactTestUtils = require('react-addons-test-utils');
+const expect = require('expect');
+
+const testUtils = require('test-utils');
+
+const sdk = require('matrix-react-sdk');
+const RegistrationForm = sdk.getComponent('views.login.RegistrationForm');
+
+const TEAM_CONFIG = {
+ supportEmail: "support@some.domain",
+ teams: [
+ { name: "The Team Org.", domain: "team.ac.uk" },
+ { name: "The Super Team", domain: "superteam.ac.uk" },
+ ],
+};
+
+function doInputEmail(inputEmail, onTeamSelected) {
+ const res = ReactTestUtils.renderIntoDocument(
+ ,
+ );
+
+ const teamInput = res.refs.email;
+ teamInput.value = inputEmail;
+
+ ReactTestUtils.Simulate.change(teamInput);
+ ReactTestUtils.Simulate.blur(teamInput);
+
+ return res;
+}
+
+function expectTeamSelectedFromEmailInput(inputEmail, expectedTeam) {
+ const onTeamSelected = expect.createSpy();
+ doInputEmail(inputEmail, onTeamSelected);
+
+ expect(onTeamSelected).toHaveBeenCalledWith(expectedTeam);
+}
+
+function expectSupportFromEmailInput(inputEmail, isSupportShown) {
+ const onTeamSelected = expect.createSpy();
+ const res = doInputEmail(inputEmail, onTeamSelected);
+
+ expect(res.state.showSupportEmail).toBe(isSupportShown);
+}
+
+describe('RegistrationForm', function() {
+ beforeEach(function() {
+ testUtils.beforeEach(this);
+ });
+
+ it('should select a team when a team email is entered', function() {
+ expectTeamSelectedFromEmailInput("member@team.ac.uk", TEAM_CONFIG.teams[0]);
+ });
+
+ it('should not select a team when an unrecognised team email is entered', function() {
+ expectTeamSelectedFromEmailInput("member@someunknownteam.ac.uk", null);
+ });
+
+ it('should show support when an unrecognised team email is entered', function() {
+ expectSupportFromEmailInput("member@someunknownteam.ac.uk", true);
+ });
+
+ it('should NOT show support when an unrecognised non-team email is entered', function() {
+ expectSupportFromEmailInput("someone@yahoo.com", false);
+ });
+});
diff --git a/test/stores/RoomViewStore-test.js b/test/stores/RoomViewStore-test.js
new file mode 100644
index 0000000000..2f545ffd74
--- /dev/null
+++ b/test/stores/RoomViewStore-test.js
@@ -0,0 +1,59 @@
+import expect from 'expect';
+
+import dis from '../../src/dispatcher';
+import RoomViewStore from '../../src/stores/RoomViewStore';
+
+
+import peg from '../../src/MatrixClientPeg';
+
+import * as testUtils from '../test-utils';
+import q from 'q';
+
+const dispatch = testUtils.getDispatchForStore(RoomViewStore);
+
+describe('RoomViewStore', function() {
+ let sandbox;
+
+ beforeEach(function() {
+ testUtils.beforeEach(this);
+ sandbox = testUtils.stubClient();
+ peg.get().credentials = { userId: "@test:example.com" };
+
+ // Reset the state of the store
+ RoomViewStore.reset();
+ });
+
+ afterEach(function() {
+ sandbox.restore();
+ });
+
+ it('can be used to view a room by ID and join', function(done) {
+ peg.get().joinRoom = (roomId) => {
+ expect(roomId).toBe("!randomcharacters:aser.ver");
+ done();
+ };
+
+ dispatch({ action: 'view_room', room_id: '!randomcharacters:aser.ver' });
+ dispatch({ action: 'join_room' });
+ expect(RoomViewStore.isJoining()).toBe(true);
+ });
+
+ it('can be used to view a room by alias and join', function(done) {
+ peg.get().getRoomIdForAlias.returns(q({room_id: "!randomcharacters:aser.ver"}));
+ peg.get().joinRoom = (roomId) => {
+ expect(roomId).toBe("!randomcharacters:aser.ver");
+ done();
+ };
+
+ RoomViewStore.addListener(() => {
+ // Wait until the room alias has resolved and the room ID is
+ if (!RoomViewStore.isRoomLoading()) {
+ expect(RoomViewStore.getRoomId()).toBe("!randomcharacters:aser.ver");
+ dispatch({ action: 'join_room' });
+ expect(RoomViewStore.isJoining()).toBe(true);
+ }
+ });
+
+ dispatch({ action: 'view_room', room_alias: '#somealias2:aser.ver' });
+ });
+});
diff --git a/test/test-utils.js b/test/test-utils.js
index 5209465362..569208b355 100644
--- a/test/test-utils.js
+++ b/test/test-utils.js
@@ -4,7 +4,8 @@ import sinon from 'sinon';
import q from 'q';
import ReactTestUtils from 'react-addons-test-utils';
-import peg from '../src/MatrixClientPeg.js';
+import peg from '../src/MatrixClientPeg';
+import dis from '../src/dispatcher';
import jssdk from 'matrix-js-sdk';
const MatrixEvent = jssdk.MatrixEvent;
@@ -133,6 +134,21 @@ export function createTestClient() {
sendHtmlMessage: () => q({}),
getSyncState: () => "SYNCING",
generateClientSecret: () => "t35tcl1Ent5ECr3T",
+ isGuest: () => false,
+ };
+}
+
+export function createTestRtsClient(teamMap, sidMap) {
+ return {
+ getTeamsConfig() {
+ return q(Object.keys(teamMap).map((token) => teamMap[token]));
+ },
+ trackReferral(referrer, emailSid, clientSecret) {
+ return q({team_token: sidMap[emailSid]});
+ },
+ getTeam(teamToken) {
+ return q(teamMap[teamToken]);
+ },
};
}
@@ -275,3 +291,13 @@ export function mkStubRoom(roomId = null) {
},
};
}
+
+export function getDispatchForStore(store) {
+ // Mock the dispatcher by gut-wrenching. Stores can only __emitChange whilst a
+ // dispatcher `_isDispatching` is true.
+ return (payload) => {
+ dis._isDispatching = true;
+ dis._callbacks[store._dispatchToken](payload);
+ dis._isDispatching = false;
+ };
+}