Merge branch 'develop' into kegan/guest-access

This commit is contained in:
Kegan Dougal
2016-01-05 11:39:36 +00:00
44 changed files with 2654 additions and 1040 deletions

View File

@@ -251,13 +251,15 @@ module.exports = React.createClass({
var UserSelector = sdk.getComponent("elements.UserSelector");
var RoomHeader = sdk.getComponent("rooms.RoomHeader");
var domain = MatrixClientPeg.get().credentials.userId.replace(/^.*:/, '');
return (
<div className="mx_CreateRoom">
<RoomHeader simpleHeader="Create room" />
<div className="mx_CreateRoom_body">
<input type="text" ref="room_name" value={this.state.room_name} onChange={this.onNameChange} placeholder="Name"/> <br />
<textarea className="mx_CreateRoom_description" ref="topic" value={this.state.topic} onChange={this.onTopicChange} placeholder="Topic"/> <br />
<RoomAlias ref="alias" alias={this.state.alias} onChange={this.onAliasChanged}/> <br />
<RoomAlias ref="alias" alias={this.state.alias} homeserver={ domain } onChange={this.onAliasChanged}/> <br />
<UserSelector ref="user_selector" selected_users={this.state.invited_users} onChange={this.onInviteChanged}/> <br />
<Presets ref="presets" onChange={this.onPresetChanged} preset={this.state.preset}/> <br />
<div>

View File

@@ -29,6 +29,7 @@ var Login = require("./login/Login");
var Registration = require("./login/Registration");
var PostRegistration = require("./login/PostRegistration");
var Modal = require("../../Modal");
var sdk = require('../../index');
var MatrixTools = require('../../MatrixTools');
var linkifyMatrix = require("../../linkify-matrix");
@@ -41,7 +42,8 @@ module.exports = React.createClass({
ConferenceHandler: React.PropTypes.any,
onNewScreen: React.PropTypes.func,
registrationUrl: React.PropTypes.string,
enableGuest: React.PropTypes.bool
enableGuest: React.PropTypes.bool,
startingQueryParams: React.PropTypes.object
},
PageTypes: {
@@ -75,6 +77,12 @@ module.exports = React.createClass({
return s;
},
getDefaultProps: function() {
return {
startingQueryParams: {}
};
},
componentDidMount: function() {
this._autoRegisterAsGuest = false;
if (this.props.enableGuest) {
@@ -94,6 +102,9 @@ module.exports = React.createClass({
this.startMatrixClient();
}
this.focusComposer = false;
// scrollStateMap is a map from room id to the scroll state returned by
// RoomView.getScrollState()
this.scrollStateMap = {};
document.addEventListener("keydown", this.onKeyDown);
window.addEventListener("focus", this.onFocus);
@@ -246,28 +257,38 @@ module.exports = React.createClass({
});
break;
case 'view_room':
this.focusComposer = true;
var newState = {
currentRoom: payload.room_id,
page_type: this.PageTypes.RoomView,
};
if (this.sdkReady) {
// if the SDK is not ready yet, remember what room
// we're supposed to be on but don't notify about
// the new screen yet (we won't be showing it yet)
// The normal case where this happens is navigating
// to the room in the URL bar on page load.
var presentedId = payload.room_id;
var room = MatrixClientPeg.get().getRoom(payload.room_id);
if (room) {
var theAlias = MatrixTools.getCanonicalAliasForRoom(room);
if (theAlias) presentedId = theAlias;
case 'leave_room':
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
var roomId = payload.room_id;
Modal.createDialog(QuestionDialog, {
title: "Leave room",
description: "Are you sure you want to leave the room?",
onFinished: function(should_leave) {
if (should_leave) {
var d = MatrixClientPeg.get().leave(roomId);
// FIXME: controller shouldn't be loading a view :(
var Loader = sdk.getComponent("elements.Spinner");
var modal = Modal.createDialog(Loader);
d.then(function() {
modal.close();
dis.dispatch({action: 'view_next_room'});
}, function(err) {
modal.close();
Modal.createDialog(ErrorDialog, {
title: "Failed to leave room",
description: err.toString()
});
});
}
}
this.notifyNewScreen('room/'+presentedId);
newState.ready = true;
}
this.setState(newState);
});
break;
case 'view_room':
this._viewRoom(payload.room_id);
break;
case 'view_prev_room':
roomIndexDelta = -1;
@@ -284,11 +305,7 @@ module.exports = React.createClass({
}
roomIndex = (roomIndex + roomIndexDelta) % allRooms.length;
if (roomIndex < 0) roomIndex = allRooms.length - 1;
this.focusComposer = true;
this.setState({
currentRoom: allRooms[roomIndex].roomId
});
this.notifyNewScreen('room/'+allRooms[roomIndex].roomId);
this._viewRoom(allRooms[roomIndex].roomId);
break;
case 'view_indexed_room':
var allRooms = RoomListSorter.mostRecentActivityFirst(
@@ -296,11 +313,7 @@ module.exports = React.createClass({
);
var roomIndex = payload.roomIndex;
if (allRooms[roomIndex]) {
this.focusComposer = true;
this.setState({
currentRoom: allRooms[roomIndex].roomId
});
this.notifyNewScreen('room/'+allRooms[roomIndex].roomId);
this._viewRoom(allRooms[roomIndex].roomId);
}
break;
case 'view_room_alias':
@@ -324,21 +337,15 @@ module.exports = React.createClass({
});
break;
case 'view_user_settings':
this.setState({
page_type: this.PageTypes.UserSettings,
});
this._setPage(this.PageTypes.UserSettings);
this.notifyNewScreen('settings');
break;
case 'view_create_room':
this.setState({
page_type: this.PageTypes.CreateRoom,
});
this._setPage(this.PageTypes.CreateRoom);
this.notifyNewScreen('new');
break;
case 'view_room_directory':
this.setState({
page_type: this.PageTypes.RoomDirectory,
});
this._setPage(this.PageTypes.RoomDirectory);
this.notifyNewScreen('directory');
break;
case 'notifier_enabled':
@@ -367,6 +374,58 @@ module.exports = React.createClass({
}
},
_setPage: function(pageType) {
// record the scroll state if we're in a room view.
this._updateScrollMap();
this.setState({
page_type: pageType,
});
},
_viewRoom: function(roomId) {
// before we switch room, record the scroll state of the current room
this._updateScrollMap();
this.focusComposer = true;
var newState = {
currentRoom: roomId,
page_type: this.PageTypes.RoomView,
};
if (this.sdkReady) {
// if the SDK is not ready yet, remember what room
// we're supposed to be on but don't notify about
// the new screen yet (we won't be showing it yet)
// The normal case where this happens is navigating
// to the room in the URL bar on page load.
var presentedId = roomId;
var room = MatrixClientPeg.get().getRoom(roomId);
if (room) {
var theAlias = MatrixTools.getCanonicalAliasForRoom(room);
if (theAlias) presentedId = theAlias;
}
this.notifyNewScreen('room/'+presentedId);
newState.ready = true;
}
this.setState(newState);
if (this.scrollStateMap[roomId]) {
var scrollState = this.scrollStateMap[roomId];
this.refs.roomView.restoreScrollState(scrollState);
}
},
// update scrollStateMap according to the current scroll state of the
// room view.
_updateScrollMap: function() {
if (!this.refs.roomView) {
return;
}
var roomview = this.refs.roomView;
var state = roomview.getScrollState();
this.scrollStateMap[roomview.props.roomId] = state;
},
onLoggedIn: function(credentials) {
credentials.guest = Boolean(credentials.guest);
console.log("onLoggedIn => %s (guest=%s)", credentials.userId, credentials.guest);
@@ -385,7 +444,10 @@ module.exports = React.createClass({
startMatrixClient: function() {
var cli = MatrixClientPeg.get();
var self = this;
cli.on('sync', function(state) {
cli.on('sync', function(state, prevState) {
if (state === "SYNCING" && prevState === "SYNCING") {
return;
}
console.log("MatrixClient sync state => %s", state);
if (state !== "PREPARED") { return; }
self.sdkReady = true;
@@ -434,7 +496,9 @@ module.exports = React.createClass({
Notifier.start();
UserActivity.start();
Presence.start();
cli.startClient();
cli.startClient({
pendingEventOrdering: "end"
});
},
onKeyDown: function(ev) {
@@ -610,6 +674,22 @@ module.exports = React.createClass({
this.showScreen("settings");
},
onUserSettingsClose: function() {
// XXX: use browser history instead to find the previous room?
if (this.state.currentRoom) {
dis.dispatch({
action: 'view_room',
room_id: this.state.currentRoom,
});
}
else {
dis.dispatch({
action: 'view_indexed_room',
roomIndex: 0,
});
}
},
render: function() {
var LeftPanel = sdk.getComponent('structures.LeftPanel');
var RoomView = sdk.getComponent('structures.RoomView');
@@ -634,6 +714,7 @@ module.exports = React.createClass({
case this.PageTypes.RoomView:
page_element = (
<RoomView
ref="roomView"
roomId={this.state.currentRoom}
key={this.state.currentRoom}
ConferenceHandler={this.props.ConferenceHandler} />
@@ -641,7 +722,7 @@ module.exports = React.createClass({
right_panel = <RightPanel roomId={this.state.currentRoom} collapsed={this.state.collapse_rhs} />
break;
case this.PageTypes.UserSettings:
page_element = <UserSettings />
page_element = <UserSettings onClose={this.onUserSettingsClose} />
right_panel = <RightPanel collapsed={this.state.collapse_rhs}/>
break;
case this.PageTypes.CreateRoom:
@@ -702,6 +783,7 @@ module.exports = React.createClass({
clientSecret={this.state.register_client_secret}
sessionId={this.state.register_session_id}
idSid={this.state.register_id_sid}
email={this.props.startingQueryParams.email}
hsUrl={this.props.config.default_hs_url}
isUrl={this.props.config.default_is_url}
registrationUrl={this.props.registrationUrl}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,288 @@
/*
Copyright 2015 OpenMarket 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.
*/
var React = require("react");
var ReactDOM = require("react-dom");
var GeminiScrollbar = require('react-gemini-scrollbar');
var DEBUG_SCROLL = false;
/* This component implements an intelligent scrolling list.
*
* It wraps a list of <li> children; when items are added to the start or end
* of the list, the scroll position is updated so that the user still sees the
* same position in the list.
*
* It also provides a hook which allows parents to provide more list elements
* when we get close to the start or end of the list.
*
* We don't save the absolute scroll offset, because that would be affected by
* window width, zoom level, amount of scrollback, etc. Instead we save an
* identifier for the last fully-visible message, and the number of pixels the
* window was scrolled below it - which is hopefully be near enough.
*
* Each child element should have a 'data-scroll-token'. This token is used to
* serialise the scroll state, and returned as the 'lastDisplayedScrollToken'
* attribute by getScrollState().
*/
module.exports = React.createClass({
displayName: 'ScrollPanel',
propTypes: {
/* stickyBottom: if set to true, then once the user hits the bottom of
* the list, any new children added to the list will cause the list to
* scroll down to show the new element, rather than preserving the
* existing view.
*/
stickyBottom: React.PropTypes.bool,
/* onFillRequest(backwards): a callback which is called on scroll when
* the user nears the start (backwards = true) or end (backwards =
* false) of the list
*/
onFillRequest: React.PropTypes.func,
/* onScroll: a callback which is called whenever any scroll happens.
*/
onScroll: React.PropTypes.func,
/* className: classnames to add to the top-level div
*/
className: React.PropTypes.string,
/* style: styles to add to the top-level div
*/
style: React.PropTypes.object,
},
getDefaultProps: function() {
return {
stickyBottom: true,
onFillRequest: function(backwards) {},
onScroll: function() {},
};
},
componentWillMount: function() {
this.resetScrollState();
},
componentDidUpdate: function() {
// after adding event tiles, we may need to tweak the scroll (either to
// keep at the bottom of the timeline, or to maintain the view after
// adding events to the top).
this._restoreSavedScrollState();
},
onScroll: function(ev) {
var sn = this._getScrollNode();
if (DEBUG_SCROLL) console.log("Scroll event: offset now:", sn.scrollTop, "recentEventScroll:", this.recentEventScroll);
// Sometimes we see attempts to write to scrollTop essentially being
// ignored. (Or rather, it is successfully written, but on the next
// scroll event, it's been reset again).
//
// This was observed on Chrome 47, when scrolling using the trackpad in OS
// X Yosemite. Can't reproduce on El Capitan. Our theory is that this is
// due to Chrome not being able to cope with the scroll offset being reset
// while a two-finger drag is in progress.
//
// By way of a workaround, we detect this situation and just keep
// resetting scrollTop until we see the scroll node have the right
// value.
if (this.recentEventScroll !== undefined) {
if(sn.scrollTop < this.recentEventScroll-200) {
console.log("Working around vector-im/vector-web#528");
this._restoreSavedScrollState();
return;
}
this.recentEventScroll = undefined;
}
this.scrollState = this._calculateScrollState();
if (DEBUG_SCROLL) console.log("Saved scroll state", this.scrollState);
this.props.onScroll(ev);
this.checkFillState();
},
// return true if the content is fully scrolled down right now; else false.
//
// Note that if the content hasn't yet been fully populated, this may
// spuriously return true even if the user wanted to be looking at earlier
// content. So don't call it in render() cycles.
isAtBottom: function() {
var sn = this._getScrollNode();
// + 1 here to avoid fractional pixel rounding errors
return sn.scrollHeight - sn.scrollTop <= sn.clientHeight + 1;
},
// check the scroll state and send out backfill requests if necessary.
checkFillState: function() {
var sn = this._getScrollNode();
if (sn.scrollTop < sn.clientHeight) {
// there's less than a screenful of messages left - try to get some
// more messages.
this.props.onFillRequest(true);
}
},
// get the current scroll position of the room, so that it can be
// restored later
getScrollState: function() {
return this.scrollState;
},
/* reset the saved scroll state.
*
* This will cause the scroll to be reinitialised on the next update of the
* child list.
*
* This is useful if the list is being replaced, and you don't want to
* preserve scroll even if new children happen to have the same scroll
* tokens as old ones.
*/
resetScrollState: function() {
this.scrollState = null;
},
scrollToTop: function() {
this._getScrollNode().scrollTop = 0;
if (DEBUG_SCROLL) console.log("Scrolled to top");
},
scrollToBottom: function() {
var scrollNode = this._getScrollNode();
scrollNode.scrollTop = scrollNode.scrollHeight;
if (DEBUG_SCROLL) console.log("Scrolled to bottom; offset now", scrollNode.scrollTop);
},
// scroll the message list to the node with the given scrollToken. See
// notes in _calculateScrollState on how this works.
//
// pixel_offset gives the number of pixels between the bottom of the node
// and the bottom of the container.
scrollToToken: function(scrollToken, pixelOffset) {
/* find the dom node with the right scrolltoken */
var node;
var messages = this.refs.itemlist.children;
for (var i = messages.length-1; i >= 0; --i) {
var m = messages[i];
if (!m.dataset.scrollToken) continue;
if (m.dataset.scrollToken == scrollToken) {
node = m;
break;
}
}
if (!node) {
console.error("No node with scrollToken '"+scrollToken+"'");
return;
}
var scrollNode = this._getScrollNode();
var wrapperRect = ReactDOM.findDOMNode(this).getBoundingClientRect();
var boundingRect = node.getBoundingClientRect();
var scrollDelta = boundingRect.bottom + pixelOffset - wrapperRect.bottom;
if(scrollDelta != 0) {
scrollNode.scrollTop += scrollDelta;
// see the comments in onMessageListScroll regarding recentEventScroll
this.recentEventScroll = scrollNode.scrollTop;
}
if (DEBUG_SCROLL) {
console.log("Scrolled to token", node.dataset.scrollToken, "+", pixelOffset+":", scrollNode.scrollTop, "(delta: "+scrollDelta+")");
console.log("recentEventScroll now "+this.recentEventScroll);
}
},
_calculateScrollState: function() {
// Our scroll implementation is agnostic of the precise contents of the
// message list (since it needs to work with both search results and
// timelines). 'refs.messageList' is expected to be a DOM node with a
// number of children, each of which may have a 'data-scroll-token'
// attribute. It is this token which is stored as the
// 'lastDisplayedScrollToken'.
var atBottom = this.isAtBottom();
var itemlist = this.refs.itemlist;
var wrapperRect = ReactDOM.findDOMNode(this).getBoundingClientRect();
var messages = itemlist.children;
for (var i = messages.length-1; i >= 0; --i) {
var node = messages[i];
if (!node.dataset.scrollToken) continue;
var boundingRect = node.getBoundingClientRect();
if (boundingRect.bottom < wrapperRect.bottom) {
return {
atBottom: atBottom,
lastDisplayedScrollToken: node.dataset.scrollToken,
pixelOffset: wrapperRect.bottom - boundingRect.bottom,
}
}
}
// apparently the entire timeline is below the viewport. Give up.
return { atBottom: true };
},
_restoreSavedScrollState: function() {
var scrollState = this.scrollState;
if (!scrollState || (this.props.stickyBottom && scrollState.atBottom)) {
this.scrollToBottom();
} else if (scrollState.lastDisplayedScrollToken) {
this.scrollToToken(scrollState.lastDisplayedScrollToken,
scrollState.pixelOffset);
}
},
/* get the DOM node which has the scrollTop property we care about for our
* message panel.
*/
_getScrollNode: function() {
var panel = ReactDOM.findDOMNode(this.refs.geminiPanel);
// If the gemini scrollbar is doing its thing, this will be a div within
// the message panel (ie, the gemini container); otherwise it will be the
// message panel itself.
if (panel.classList.contains('gm-prevented')) {
return panel;
} else {
return panel.children[2]; // XXX: Fragile!
}
},
render: function() {
// TODO: the classnames on the div and ol could do with being updated to
// reflect the fact that we don't necessarily contain a list of messages.
// it's not obvious why we have a separate div and ol anyway.
return (<GeminiScrollbar autoshow={true} ref="geminiPanel" onScroll={ this.onScroll }
className={this.props.className} style={this.props.style}>
<div className="mx_RoomView_messageListWrapper">
<ol ref="itemlist" className="mx_RoomView_MessageList" aria-live="polite">
{this.props.children}
</ol>
</div>
</GeminiScrollbar>
);
},
});

View File

@@ -0,0 +1,93 @@
/*
Copyright 2015 OpenMarket 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.
*/
var React = require('react');
var ContentMessages = require('../../ContentMessages');
var dis = require('../../dispatcher');
var filesize = require('filesize');
module.exports = React.createClass({displayName: 'UploadBar',
propTypes: {
room: React.PropTypes.object
},
componentDidMount: function() {
dis.register(this.onAction);
this.mounted = true;
},
componentWillUnmount: function() {
this.mounted = false;
},
onAction: function(payload) {
switch (payload.action) {
case 'upload_progress':
case 'upload_finished':
case 'upload_failed':
if (this.mounted) this.forceUpdate();
break;
}
},
render: function() {
var uploads = ContentMessages.getCurrentUploads();
if (uploads.length == 0) {
return <div />
}
var upload;
for (var i = 0; i < uploads.length; ++i) {
if (uploads[i].roomId == this.props.room.roomId) {
upload = uploads[i];
break;
}
}
if (!upload) {
upload = uploads[0];
}
var innerProgressStyle = {
width: ((upload.loaded / (upload.total || 1)) * 100) + '%'
};
var uploadedSize = filesize(upload.loaded);
var totalSize = filesize(upload.total);
if (uploadedSize.replace(/^.* /,'') === totalSize.replace(/^.* /,'')) {
uploadedSize = uploadedSize.replace(/ .*/, '');
}
var others;
if (uploads.length > 1) {
others = 'and '+(uploads.length - 1) + ' other' + (uploads.length > 2 ? 's' : '');
}
return (
<div className="mx_UploadBar">
<div className="mx_UploadBar_uploadProgressOuter">
<div className="mx_UploadBar_uploadProgressInner" style={innerProgressStyle}></div>
</div>
<img className="mx_UploadBar_uploadIcon" src="img/fileicon.png" width="17" height="22"/>
<img className="mx_UploadBar_uploadCancel" src="img/cancel.svg" width="18" height="18"
onClick={function() { ContentMessages.cancelUpload(upload.promise); }}
/>
<div className="mx_UploadBar_uploadBytes">
{ uploadedSize } / { totalSize }
</div>
<div className="mx_UploadBar_uploadFilename">Uploading {upload.fileName}{others}</div>
</div>
);
}
});

View File

@@ -17,14 +17,22 @@ var React = require('react');
var sdk = require('../../index');
var MatrixClientPeg = require("../../MatrixClientPeg");
var Modal = require('../../Modal');
var dis = require("../../dispatcher");
var q = require('q');
var version = require('../../../package.json').version;
var UserSettingsStore = require('../../UserSettingsStore');
module.exports = React.createClass({
displayName: 'UserSettings',
Phases: {
Loading: "loading",
Display: "display",
propTypes: {
onClose: React.PropTypes.func
},
getDefaultProps: function() {
return {
onClose: function() {}
};
},
getInitialState: function() {
@@ -32,131 +40,227 @@ module.exports = React.createClass({
avatarUrl: null,
threePids: [],
clientVersion: version,
phase: this.Phases.Loading,
phase: "UserSettings.LOADING", // LOADING, DISPLAY
};
},
componentWillMount: function() {
var self = this;
var cli = MatrixClientPeg.get();
var profile_d = cli.getProfileInfo(cli.credentials.userId);
var threepid_d = cli.getThreePids();
q.all([profile_d, threepid_d]).then(
function(resps) {
self.setState({
avatarUrl: resps[0].avatar_url,
threepids: resps[1].threepids,
phase: self.Phases.Display,
});
},
function(err) { console.err(err); }
);
this._refreshFromServer();
},
editAvatar: function() {
var url = MatrixClientPeg.get().mxcUrlToHttp(this.state.avatarUrl);
var ChangeAvatar = sdk.getComponent('settings.ChangeAvatar');
var avatarDialog = (
<div>
<ChangeAvatar initialAvatarUrl={url} />
<div className="mx_Dialog_buttons">
<button onClick={this.onAvatarDialogCancel}>Cancel</button>
</div>
</div>
);
this.avatarDialog = Modal.createDialogWithElement(avatarDialog);
componentDidMount: function() {
this.dispatcherRef = dis.register(this.onAction);
this._me = MatrixClientPeg.get().credentials.userId;
},
addEmail: function() {
componentWillUnmount: function() {
dis.unregister(this.dispatcherRef);
},
editDisplayName: function() {
this.refs.displayname.edit();
_refreshFromServer: function() {
var self = this;
q.all([
UserSettingsStore.loadProfileInfo(), UserSettingsStore.loadThreePids()
]).done(function(resps) {
self.setState({
avatarUrl: resps[0].avatar_url,
threepids: resps[1].threepids,
phase: "UserSettings.DISPLAY",
});
}, function(error) {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Can't load user settings",
description: error.toString()
});
});
},
changePassword: function() {
var ChangePassword = sdk.getComponent('settings.ChangePassword');
Modal.createDialog(ChangePassword);
onAction: function(payload) {
if (payload.action === "notifier_enabled") {
this.forceUpdate();
}
},
onAvatarSelected: function(ev) {
var self = this;
var changeAvatar = this.refs.changeAvatar;
if (!changeAvatar) {
console.error("No ChangeAvatar found to upload image to!");
return;
}
changeAvatar.onFileSelected(ev).done(function() {
// dunno if the avatar changed, re-check it.
self._refreshFromServer();
}, function(err) {
var errMsg = (typeof err === "string") ? err : (err.error || "");
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Error",
description: "Failed to set avatar. " + errMsg
});
});
},
onLogoutClicked: function(ev) {
var LogoutPrompt = sdk.getComponent('dialogs.LogoutPrompt');
this.logoutModal = Modal.createDialog(LogoutPrompt, {onCancel: this.onLogoutPromptCancel});
this.logoutModal = Modal.createDialog(
LogoutPrompt, {onCancel: this.onLogoutPromptCancel}
);
},
onPasswordChangeError: function(err) {
var errMsg = err.error || "";
if (err.httpStatus === 403) {
errMsg = "Failed to change password. Is your password correct?";
}
else if (err.httpStatus) {
errMsg += ` (HTTP status ${err.httpStatus})`;
}
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Error",
description: errMsg
});
},
onPasswordChanged: function() {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Success",
description: `Your password was successfully changed. You will not
receive push notifications on other devices until you
log back in to them.`
});
},
onLogoutPromptCancel: function() {
this.logoutModal.closeDialog();
},
onAvatarDialogCancel: function() {
this.avatarDialog.close();
onEnableNotificationsChange: function(event) {
UserSettingsStore.setEnableNotifications(event.target.checked);
},
render: function() {
var Loader = sdk.getComponent("elements.Spinner");
if (this.state.phase === this.Phases.Loading) {
return <Loader />
switch (this.state.phase) {
case "UserSettings.LOADING":
var Loader = sdk.getComponent("elements.Spinner");
return (
<Loader />
);
case "UserSettings.DISPLAY":
break; // quit the switch to return the common state
default:
throw new Error("Unknown state.phase => " + this.state.phase);
}
else if (this.state.phase === this.Phases.Display) {
var ChangeDisplayName = sdk.getComponent('settings.ChangeDisplayName');
var EnableNotificationsButton = sdk.getComponent('settings.EnableNotificationsButton');
return (
// can only get here if phase is UserSettings.DISPLAY
var RoomHeader = sdk.getComponent('rooms.RoomHeader');
var ChangeDisplayName = sdk.getComponent("views.settings.ChangeDisplayName");
var ChangePassword = sdk.getComponent("views.settings.ChangePassword");
var ChangeAvatar = sdk.getComponent('settings.ChangeAvatar');
var avatarUrl = (
this.state.avatarUrl ? MatrixClientPeg.get().mxcUrlToHttp(this.state.avatarUrl) : null
);
return (
<div className="mx_UserSettings">
<div className="mx_UserSettings_User">
<h1>User Settings</h1>
<hr/>
<div className="mx_UserSettings_User_Inner">
<div className="mx_UserSettings_Avatar">
<div className="mx_UserSettings_Avatar_Text">
Profile Photo
<RoomHeader simpleHeader="Settings" />
<h2>Profile</h2>
<div className="mx_UserSettings_section">
<div className="mx_UserSettings_profileTable">
<div className="mx_UserSettings_profileTableRow">
<div className="mx_UserSettings_profileLabelCell">
<label htmlFor="displayName">Display name</label>
</div>
<div className="mx_UserSettings_Avatar_Edit" onClick={this.editAvatar}>
Edit
<div className="mx_UserSettings_profileInputCell">
<ChangeDisplayName />
</div>
</div>
<div className="mx_UserSettings_DisplayName">
<ChangeDisplayName ref="displayname" />
<div className="mx_UserSettings_DisplayName_Edit" onClick={this.editDisplayName}>
Edit
</div>
</div>
{this.state.threepids.map(function(val, pidIndex) {
var id = "email-" + val.address;
return (
<div className="mx_UserSettings_profileTableRow" key={pidIndex}>
<div className="mx_UserSettings_profileLabelCell">
<label htmlFor={id}>Email</label>
</div>
<div className="mx_UserSettings_profileInputCell">
<input key={val.address} id={id} value={val.address} disabled />
</div>
</div>
);
})}
</div>
<div className="mx_UserSettings_3pids">
{this.state.threepids.map(function(val) {
return <div key={val.address}>{val.address}</div>;
})}
</div>
<div className="mx_UserSettings_Add3pid" onClick={this.addEmail}>
Add email
<div className="mx_UserSettings_avatarPicker">
<ChangeAvatar ref="changeAvatar" initialAvatarUrl={avatarUrl}
showUploadSection={false} className="mx_UserSettings_avatarPicker_img"/>
<div className="mx_UserSettings_avatarPicker_edit">
<label htmlFor="avatarInput">
<img src="img/upload.svg"
alt="Upload avatar" title="Upload avatar"
width="19" height="24" />
</label>
<input id="avatarInput" type="file" onChange={this.onAvatarSelected}/>
</div>
</div>
</div>
<div className="mx_UserSettings_Global">
<h1>Global Settings</h1>
<hr/>
<div className="mx_UserSettings_Global_Inner">
<div className="mx_UserSettings_ChangePassword" onClick={this.changePassword}>
Change Password
</div>
<div className="mx_UserSettings_ClientVersion">
Version {this.state.clientVersion}
</div>
<div className="mx_UserSettings_EnableNotifications">
<EnableNotificationsButton />
</div>
<div className="mx_UserSettings_Logout">
<button onClick={this.onLogoutClicked}>Sign Out</button>
<h2>Account</h2>
<div className="mx_UserSettings_section">
<ChangePassword
className="mx_UserSettings_accountTable"
rowClassName="mx_UserSettings_profileTableRow"
rowLabelClassName="mx_UserSettings_profileLabelCell"
rowInputClassName="mx_UserSettings_profileInputCell"
buttonClassName="mx_UserSettings_button"
onError={this.onPasswordChangeError}
onFinished={this.onPasswordChanged} />
</div>
<div className="mx_UserSettings_logout">
<div className="mx_UserSettings_button" onClick={this.onLogoutClicked}>
Log out
</div>
</div>
<h2>Notifications</h2>
<div className="mx_UserSettings_section">
<div className="mx_UserSettings_notifTable">
<div className="mx_UserSettings_notifTableRow">
<div className="mx_UserSettings_notifInputCell">
<input id="enableNotifications"
ref="enableNotifications"
type="checkbox"
checked={ UserSettingsStore.getEnableNotifications() }
onChange={ this.onEnableNotificationsChange } />
</div>
<div className="mx_UserSettings_notifLabelCell">
<label htmlFor="enableNotifications">
Enable desktop notifications
</label>
</div>
</div>
</div>
</div>
<h2>Advanced</h2>
<div className="mx_UserSettings_section">
<div className="mx_UserSettings_advanced">
Logged in as {this._me}
</div>
<div className="mx_UserSettings_advanced">
Version {this.state.clientVersion}
</div>
</div>
</div>
);
}
);
}
});

View File

@@ -39,6 +39,7 @@ module.exports = React.createClass({
idSid: React.PropTypes.string,
hsUrl: React.PropTypes.string,
isUrl: React.PropTypes.string,
email: React.PropTypes.string,
// registration shouldn't know or care how login is done.
onLoginClick: React.PropTypes.func.isRequired
},
@@ -185,6 +186,7 @@ module.exports = React.createClass({
registerStep = (
<RegistrationForm
showEmail={true}
defaultEmail={this.props.email}
minPasswordLength={MIN_PASSWORD_LENGTH}
onError={this.onFormValidationFailed}
onRegisterClick={this.onFormSubmit} />