Merge pull request #42 from matrix-org/kegan/controller-merging4
Phase 4 controller merging
This commit is contained in:
commit
4fe2cc54d6
29 changed files with 3168 additions and 989 deletions
290
src/components/structures/CreateRoom.js
Normal file
290
src/components/structures/CreateRoom.js
Normal file
|
@ -0,0 +1,290 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var React = require("react");
|
||||
var MatrixClientPeg = require("../../MatrixClientPeg");
|
||||
var PresetValues = {
|
||||
PrivateChat: "private_chat",
|
||||
PublicChat: "public_chat",
|
||||
Custom: "custom",
|
||||
};
|
||||
var q = require('q');
|
||||
var encryption = require("../../encryption");
|
||||
var sdk = require('../../index');
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'CreateRoom',
|
||||
|
||||
propTypes: {
|
||||
onRoomCreated: React.PropTypes.func,
|
||||
},
|
||||
|
||||
phases: {
|
||||
CONFIG: "CONFIG", // We're waiting for user to configure and hit create.
|
||||
CREATING: "CREATING", // We're sending the request.
|
||||
CREATED: "CREATED", // We successfully created the room.
|
||||
ERROR: "ERROR", // There was an error while trying to create room.
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
onRoomCreated: function() {},
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
phase: this.phases.CONFIG,
|
||||
error_string: "",
|
||||
is_private: true,
|
||||
share_history: false,
|
||||
default_preset: PresetValues.PrivateChat,
|
||||
topic: '',
|
||||
room_name: '',
|
||||
invited_users: [],
|
||||
};
|
||||
},
|
||||
|
||||
onCreateRoom: function() {
|
||||
var options = {};
|
||||
|
||||
if (this.state.room_name) {
|
||||
options.name = this.state.room_name;
|
||||
}
|
||||
|
||||
if (this.state.topic) {
|
||||
options.topic = this.state.topic;
|
||||
}
|
||||
|
||||
if (this.state.preset) {
|
||||
if (this.state.preset != PresetValues.Custom) {
|
||||
options.preset = this.state.preset;
|
||||
} else {
|
||||
options.initial_state = [
|
||||
{
|
||||
type: "m.room.join_rules",
|
||||
content: {
|
||||
"join_rule": this.state.is_private ? "invite" : "public"
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "m.room.history_visibility",
|
||||
content: {
|
||||
"history_visibility": this.state.share_history ? "shared" : "invited"
|
||||
}
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
options.invite = this.state.invited_users;
|
||||
|
||||
var alias = this.getAliasLocalpart();
|
||||
if (alias) {
|
||||
options.room_alias_name = alias;
|
||||
}
|
||||
|
||||
var cli = MatrixClientPeg.get();
|
||||
if (!cli) {
|
||||
// TODO: Error.
|
||||
console.error("Cannot create room: No matrix client.");
|
||||
return;
|
||||
}
|
||||
|
||||
var deferred = cli.createRoom(options);
|
||||
|
||||
var response;
|
||||
|
||||
if (this.state.encrypt) {
|
||||
deferred = deferred.then(function(res) {
|
||||
response = res;
|
||||
return encryption.enableEncryption(
|
||||
cli, response.room_id, options.invite
|
||||
);
|
||||
}).then(function() {
|
||||
return q(response) }
|
||||
);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
phase: this.phases.CREATING,
|
||||
});
|
||||
|
||||
var self = this;
|
||||
|
||||
deferred.then(function (resp) {
|
||||
self.setState({
|
||||
phase: self.phases.CREATED,
|
||||
});
|
||||
self.props.onRoomCreated(resp.room_id);
|
||||
}, function(err) {
|
||||
self.setState({
|
||||
phase: self.phases.ERROR,
|
||||
error_string: err.toString(),
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
getPreset: function() {
|
||||
return this.refs.presets.getPreset();
|
||||
},
|
||||
|
||||
getName: function() {
|
||||
return this.refs.name_textbox.getName();
|
||||
},
|
||||
|
||||
getTopic: function() {
|
||||
return this.refs.topic.getTopic();
|
||||
},
|
||||
|
||||
getAliasLocalpart: function() {
|
||||
return this.refs.alias.getAliasLocalpart();
|
||||
},
|
||||
|
||||
getInvitedUsers: function() {
|
||||
return this.refs.user_selector.getUserIds();
|
||||
},
|
||||
|
||||
onPresetChanged: function(preset) {
|
||||
switch (preset) {
|
||||
case PresetValues.PrivateChat:
|
||||
this.setState({
|
||||
preset: preset,
|
||||
is_private: true,
|
||||
share_history: false,
|
||||
});
|
||||
break;
|
||||
case PresetValues.PublicChat:
|
||||
this.setState({
|
||||
preset: preset,
|
||||
is_private: false,
|
||||
share_history: true,
|
||||
});
|
||||
break;
|
||||
case PresetValues.Custom:
|
||||
this.setState({
|
||||
preset: preset,
|
||||
});
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
onPrivateChanged: function(ev) {
|
||||
this.setState({
|
||||
preset: PresetValues.Custom,
|
||||
is_private: ev.target.checked,
|
||||
});
|
||||
},
|
||||
|
||||
onShareHistoryChanged: function(ev) {
|
||||
this.setState({
|
||||
preset: PresetValues.Custom,
|
||||
share_history: ev.target.checked,
|
||||
});
|
||||
},
|
||||
|
||||
onTopicChange: function(ev) {
|
||||
this.setState({
|
||||
topic: ev.target.value,
|
||||
});
|
||||
},
|
||||
|
||||
onNameChange: function(ev) {
|
||||
this.setState({
|
||||
room_name: ev.target.value,
|
||||
});
|
||||
},
|
||||
|
||||
onInviteChanged: function(invited_users) {
|
||||
this.setState({
|
||||
invited_users: invited_users,
|
||||
});
|
||||
},
|
||||
|
||||
onAliasChanged: function(alias) {
|
||||
this.setState({
|
||||
alias: alias
|
||||
})
|
||||
},
|
||||
|
||||
onEncryptChanged: function(ev) {
|
||||
this.setState({
|
||||
encrypt: ev.target.checked,
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var curr_phase = this.state.phase;
|
||||
if (curr_phase == this.phases.CREATING) {
|
||||
var Loader = sdk.getComponent("elements.Spinner");
|
||||
return (
|
||||
<Loader/>
|
||||
);
|
||||
} else {
|
||||
var error_box = "";
|
||||
if (curr_phase == this.phases.ERROR) {
|
||||
error_box = (
|
||||
<div className="mx_Error">
|
||||
An error occured: {this.state.error_string}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
var CreateRoomButton = sdk.getComponent("create_room.CreateRoomButton");
|
||||
var RoomAlias = sdk.getComponent("create_room.RoomAlias");
|
||||
var Presets = sdk.getComponent("create_room.Presets");
|
||||
var UserSelector = sdk.getComponent("elements.UserSelector");
|
||||
var RoomHeader = sdk.getComponent("rooms.RoomHeader");
|
||||
|
||||
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 />
|
||||
<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>
|
||||
<label>
|
||||
<input type="checkbox" ref="is_private" checked={this.state.is_private} onChange={this.onPrivateChanged}/>
|
||||
Make this room private
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label>
|
||||
<input type="checkbox" ref="share_history" checked={this.state.share_history} onChange={this.onShareHistoryChanged}/>
|
||||
Share message history with new users
|
||||
</label>
|
||||
</div>
|
||||
<div className="mx_CreateRoom_encrypt">
|
||||
<label>
|
||||
<input type="checkbox" ref="encrypt" checked={this.state.encrypt} onChange={this.onEncryptChanged}/>
|
||||
Encrypt room
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<CreateRoomButton onCreateRoom={this.onCreateRoom} /> <br />
|
||||
</div>
|
||||
{error_box}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
667
src/components/structures/MatrixChat.js
Normal file
667
src/components/structures/MatrixChat.js
Normal file
|
@ -0,0 +1,667 @@
|
|||
/*
|
||||
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 Matrix = require("matrix-js-sdk");
|
||||
var url = require('url');
|
||||
|
||||
var MatrixClientPeg = require("../../MatrixClientPeg");
|
||||
var Notifier = require("../../Notifier");
|
||||
var ContextualMenu = require("../../ContextualMenu");
|
||||
var RoomListSorter = require("../../RoomListSorter");
|
||||
var UserActivity = require("../../UserActivity");
|
||||
var Presence = require("../../Presence");
|
||||
var dis = require("../../dispatcher");
|
||||
|
||||
var Login = require("./login/Login");
|
||||
var Registration = require("./login/Registration");
|
||||
var PostRegistration = require("./login/PostRegistration");
|
||||
|
||||
var sdk = require('../../index');
|
||||
var MatrixTools = require('../../MatrixTools');
|
||||
var linkifyMatrix = require("../../linkify-matrix");
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'MatrixChat',
|
||||
|
||||
propTypes: {
|
||||
config: React.PropTypes.object.isRequired,
|
||||
ConferenceHandler: React.PropTypes.any,
|
||||
onNewScreen: React.PropTypes.func,
|
||||
registrationUrl: React.PropTypes.string
|
||||
},
|
||||
|
||||
PageTypes: {
|
||||
RoomView: "room_view",
|
||||
UserSettings: "user_settings",
|
||||
CreateRoom: "create_room",
|
||||
RoomDirectory: "room_directory",
|
||||
},
|
||||
|
||||
AuxPanel: {
|
||||
RoomSettings: "room_settings",
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
var s = {
|
||||
logged_in: !!(MatrixClientPeg.get() && MatrixClientPeg.get().credentials),
|
||||
collapse_lhs: false,
|
||||
collapse_rhs: false,
|
||||
ready: false,
|
||||
width: 10000
|
||||
};
|
||||
if (s.logged_in) {
|
||||
if (MatrixClientPeg.get().getRooms().length) {
|
||||
s.page_type = this.PageTypes.RoomView;
|
||||
} else {
|
||||
// we don't need to default to the directoy here
|
||||
// as we'll go there anyway after syncing
|
||||
// s.page_type = this.PageTypes.RoomDirectory;
|
||||
}
|
||||
}
|
||||
return s;
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
if (this.state.logged_in) {
|
||||
this.startMatrixClient();
|
||||
}
|
||||
this.focusComposer = false;
|
||||
document.addEventListener("keydown", this.onKeyDown);
|
||||
window.addEventListener("focus", this.onFocus);
|
||||
if (this.state.logged_in) {
|
||||
this.notifyNewScreen('');
|
||||
} else {
|
||||
this.notifyNewScreen('login');
|
||||
}
|
||||
|
||||
// this can technically be done anywhere but doing this here keeps all
|
||||
// the routing url path logic together.
|
||||
if (this.onAliasClick) {
|
||||
linkifyMatrix.onAliasClick = this.onAliasClick;
|
||||
}
|
||||
if (this.onUserClick) {
|
||||
linkifyMatrix.onUserClick = this.onUserClick;
|
||||
}
|
||||
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
this.handleResize();
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
dis.unregister(this.dispatcherRef);
|
||||
document.removeEventListener("keydown", this.onKeyDown);
|
||||
window.removeEventListener("focus", this.onFocus);
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
},
|
||||
|
||||
componentDidUpdate: function() {
|
||||
if (this.focusComposer) {
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
this.focusComposer = false;
|
||||
}
|
||||
},
|
||||
|
||||
onAction: function(payload) {
|
||||
var roomIndexDelta = 1;
|
||||
|
||||
var self = this;
|
||||
switch (payload.action) {
|
||||
case 'logout':
|
||||
if (window.localStorage) {
|
||||
window.localStorage.clear();
|
||||
}
|
||||
Notifier.stop();
|
||||
UserActivity.stop();
|
||||
Presence.stop();
|
||||
MatrixClientPeg.get().stopClient();
|
||||
MatrixClientPeg.get().removeAllListeners();
|
||||
MatrixClientPeg.unset();
|
||||
this.notifyNewScreen('login');
|
||||
this.replaceState({
|
||||
logged_in: false,
|
||||
ready: false
|
||||
});
|
||||
break;
|
||||
case 'start_registration':
|
||||
if (this.state.logged_in) return;
|
||||
var newState = payload.params || {};
|
||||
newState.screen = 'register';
|
||||
if (
|
||||
payload.params &&
|
||||
payload.params.client_secret &&
|
||||
payload.params.session_id &&
|
||||
payload.params.hs_url &&
|
||||
payload.params.is_url &&
|
||||
payload.params.sid
|
||||
) {
|
||||
newState.register_client_secret = payload.params.client_secret;
|
||||
newState.register_session_id = payload.params.session_id;
|
||||
newState.register_hs_url = payload.params.hs_url;
|
||||
newState.register_is_url = payload.params.is_url;
|
||||
newState.register_id_sid = payload.params.sid;
|
||||
}
|
||||
this.replaceState(newState);
|
||||
this.notifyNewScreen('register');
|
||||
break;
|
||||
case 'start_login':
|
||||
if (this.state.logged_in) return;
|
||||
this.replaceState({
|
||||
screen: 'login'
|
||||
});
|
||||
this.notifyNewScreen('login');
|
||||
break;
|
||||
case 'start_post_registration':
|
||||
this.setState({ // don't clobber logged_in status
|
||||
screen: 'post_registration'
|
||||
});
|
||||
break;
|
||||
case 'token_login':
|
||||
if (this.state.logged_in) return;
|
||||
|
||||
var self = this;
|
||||
MatrixClientPeg.replaceUsingUrls(
|
||||
payload.params.homeserver,
|
||||
payload.params.identityServer
|
||||
);
|
||||
|
||||
var client = MatrixClientPeg.get();
|
||||
client.loginWithToken(payload.params.loginToken).done(function(data) {
|
||||
MatrixClientPeg.replaceUsingAccessToken(
|
||||
client.getHomeserverUrl(), client.getIdentityServerUrl(),
|
||||
data.user_id, data.access_token
|
||||
);
|
||||
self.setState({
|
||||
screen: undefined,
|
||||
logged_in: true
|
||||
});
|
||||
|
||||
// We're left with the login token, hs and is url as query params
|
||||
// in the url, a little nasty but let's redirect to clear them
|
||||
var parsedUrl = url.parse(window.location.href);
|
||||
parsedUrl.search = "";
|
||||
window.location.href = url.format(parsedUrl);
|
||||
|
||||
}, function(error) {
|
||||
self.notifyNewScreen('login');
|
||||
self.setState({errorText: 'Login failed.'});
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
this.notifyNewScreen('room/'+presentedId);
|
||||
newState.ready = true;
|
||||
}
|
||||
this.setState(newState);
|
||||
break;
|
||||
case 'view_prev_room':
|
||||
roomIndexDelta = -1;
|
||||
case 'view_next_room':
|
||||
var allRooms = RoomListSorter.mostRecentActivityFirst(
|
||||
MatrixClientPeg.get().getRooms()
|
||||
);
|
||||
var roomIndex = -1;
|
||||
for (var i = 0; i < allRooms.length; ++i) {
|
||||
if (allRooms[i].roomId == this.state.currentRoom) {
|
||||
roomIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
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);
|
||||
break;
|
||||
case 'view_indexed_room':
|
||||
var allRooms = RoomListSorter.mostRecentActivityFirst(
|
||||
MatrixClientPeg.get().getRooms()
|
||||
);
|
||||
var roomIndex = payload.roomIndex;
|
||||
if (allRooms[roomIndex]) {
|
||||
this.focusComposer = true;
|
||||
this.setState({
|
||||
currentRoom: allRooms[roomIndex].roomId
|
||||
});
|
||||
this.notifyNewScreen('room/'+allRooms[roomIndex].roomId);
|
||||
}
|
||||
break;
|
||||
case 'view_room_alias':
|
||||
var foundRoom = MatrixTools.getRoomForAlias(
|
||||
MatrixClientPeg.get().getRooms(), payload.room_alias
|
||||
);
|
||||
if (foundRoom) {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: foundRoom.roomId
|
||||
});
|
||||
return;
|
||||
}
|
||||
// resolve the alias and *then* view it
|
||||
MatrixClientPeg.get().getRoomIdForAlias(payload.room_alias).done(
|
||||
function(result) {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: result.room_id
|
||||
});
|
||||
});
|
||||
break;
|
||||
case 'view_user_settings':
|
||||
this.setState({
|
||||
page_type: this.PageTypes.UserSettings,
|
||||
});
|
||||
this.notifyNewScreen('settings');
|
||||
break;
|
||||
case 'view_create_room':
|
||||
this.setState({
|
||||
page_type: this.PageTypes.CreateRoom,
|
||||
});
|
||||
this.notifyNewScreen('new');
|
||||
break;
|
||||
case 'view_room_directory':
|
||||
this.setState({
|
||||
page_type: this.PageTypes.RoomDirectory,
|
||||
});
|
||||
this.notifyNewScreen('directory');
|
||||
break;
|
||||
case 'notifier_enabled':
|
||||
this.forceUpdate();
|
||||
break;
|
||||
case 'hide_left_panel':
|
||||
this.setState({
|
||||
collapse_lhs: true,
|
||||
});
|
||||
break;
|
||||
case 'show_left_panel':
|
||||
this.setState({
|
||||
collapse_lhs: false,
|
||||
});
|
||||
break;
|
||||
case 'hide_right_panel':
|
||||
this.setState({
|
||||
collapse_rhs: true,
|
||||
});
|
||||
break;
|
||||
case 'show_right_panel':
|
||||
this.setState({
|
||||
collapse_rhs: false,
|
||||
});
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
onLoggedIn: function(credentials) {
|
||||
console.log("onLoggedIn => %s", credentials.userId);
|
||||
MatrixClientPeg.replaceUsingAccessToken(
|
||||
credentials.homeserverUrl, credentials.identityServerUrl,
|
||||
credentials.userId, credentials.accessToken
|
||||
);
|
||||
this.setState({
|
||||
screen: undefined,
|
||||
logged_in: true
|
||||
});
|
||||
this.startMatrixClient();
|
||||
this.notifyNewScreen('');
|
||||
},
|
||||
|
||||
startMatrixClient: function() {
|
||||
var cli = MatrixClientPeg.get();
|
||||
var self = this;
|
||||
cli.on('sync', function(state) {
|
||||
console.log("MatrixClient sync state => %s", state);
|
||||
if (state !== "PREPARED") { return; }
|
||||
self.sdkReady = true;
|
||||
|
||||
if (self.starting_room_alias) {
|
||||
dis.dispatch({
|
||||
action: 'view_room_alias',
|
||||
room_alias: self.starting_room_alias
|
||||
});
|
||||
delete self.starting_room_alias;
|
||||
} else if (!self.state.page_type) {
|
||||
if (!self.state.currentRoom) {
|
||||
var firstRoom = null;
|
||||
if (cli.getRooms() && cli.getRooms().length) {
|
||||
firstRoom = RoomListSorter.mostRecentActivityFirst(
|
||||
cli.getRooms()
|
||||
)[0].roomId;
|
||||
self.setState({ready: true, currentRoom: firstRoom, page_type: self.PageTypes.RoomView});
|
||||
} else {
|
||||
self.setState({ready: true, page_type: self.PageTypes.RoomDirectory});
|
||||
}
|
||||
} else {
|
||||
self.setState({ready: true, page_type: self.PageTypes.RoomView});
|
||||
}
|
||||
|
||||
// we notifyNewScreen now because now the room will actually be displayed,
|
||||
// and (mostly) now we can get the correct alias.
|
||||
var presentedId = self.state.currentRoom;
|
||||
var room = MatrixClientPeg.get().getRoom(self.state.currentRoom);
|
||||
if (room) {
|
||||
var theAlias = MatrixTools.getCanonicalAliasForRoom(room);
|
||||
if (theAlias) presentedId = theAlias;
|
||||
}
|
||||
self.notifyNewScreen('room/'+presentedId);
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
} else {
|
||||
self.setState({ready: true});
|
||||
}
|
||||
});
|
||||
cli.on('Call.incoming', function(call) {
|
||||
dis.dispatch({
|
||||
action: 'incoming_call',
|
||||
call: call
|
||||
});
|
||||
});
|
||||
Notifier.start();
|
||||
UserActivity.start();
|
||||
Presence.start();
|
||||
cli.startClient();
|
||||
},
|
||||
|
||||
onKeyDown: function(ev) {
|
||||
if (ev.altKey) {
|
||||
/*
|
||||
// Remove this for now as ctrl+alt = alt-gr so this breaks keyboards which rely on alt-gr for numbers
|
||||
// Will need to find a better meta key if anyone actually cares about using this.
|
||||
if (ev.ctrlKey && ev.keyCode > 48 && ev.keyCode < 58) {
|
||||
dis.dispatch({
|
||||
action: 'view_indexed_room',
|
||||
roomIndex: ev.keyCode - 49,
|
||||
});
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
return;
|
||||
}
|
||||
*/
|
||||
switch (ev.keyCode) {
|
||||
case 38:
|
||||
dis.dispatch({action: 'view_prev_room'});
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
break;
|
||||
case 40:
|
||||
dis.dispatch({action: 'view_next_room'});
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onFocus: function(ev) {
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
},
|
||||
|
||||
showScreen: function(screen, params) {
|
||||
if (screen == 'register') {
|
||||
dis.dispatch({
|
||||
action: 'start_registration',
|
||||
params: params
|
||||
});
|
||||
} else if (screen == 'login') {
|
||||
dis.dispatch({
|
||||
action: 'start_login',
|
||||
params: params
|
||||
});
|
||||
} else if (screen == 'token_login') {
|
||||
dis.dispatch({
|
||||
action: 'token_login',
|
||||
params: params
|
||||
});
|
||||
} else if (screen == 'new') {
|
||||
dis.dispatch({
|
||||
action: 'view_create_room',
|
||||
});
|
||||
} else if (screen == 'settings') {
|
||||
dis.dispatch({
|
||||
action: 'view_user_settings',
|
||||
});
|
||||
} else if (screen == 'directory') {
|
||||
dis.dispatch({
|
||||
action: 'view_room_directory',
|
||||
});
|
||||
} else if (screen == 'post_registration') {
|
||||
dis.dispatch({
|
||||
action: 'start_post_registration',
|
||||
});
|
||||
} else if (screen.indexOf('room/') == 0) {
|
||||
var roomString = screen.split('/')[1];
|
||||
if (roomString[0] == '#') {
|
||||
if (this.state.logged_in) {
|
||||
dis.dispatch({
|
||||
action: 'view_room_alias',
|
||||
room_alias: roomString
|
||||
});
|
||||
} else {
|
||||
// Okay, we'll take you here soon...
|
||||
this.starting_room_alias = roomString;
|
||||
// ...but you're still going to have to log in.
|
||||
this.notifyNewScreen('login');
|
||||
}
|
||||
} else {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: roomString
|
||||
});
|
||||
}
|
||||
}
|
||||
else {
|
||||
console.error("Unknown screen : %s", screen);
|
||||
}
|
||||
},
|
||||
|
||||
notifyNewScreen: function(screen) {
|
||||
if (this.props.onNewScreen) {
|
||||
this.props.onNewScreen(screen);
|
||||
}
|
||||
},
|
||||
|
||||
onAliasClick: function(event, alias) {
|
||||
event.preventDefault();
|
||||
dis.dispatch({action: 'view_room_alias', room_alias: alias});
|
||||
},
|
||||
|
||||
onUserClick: function(event, userId) {
|
||||
event.preventDefault();
|
||||
var MemberInfo = sdk.getComponent('rooms.MemberInfo');
|
||||
var member = new Matrix.RoomMember(null, userId);
|
||||
ContextualMenu.createMenu(MemberInfo, {
|
||||
member: member,
|
||||
right: window.innerWidth - event.pageX,
|
||||
top: event.pageY
|
||||
});
|
||||
},
|
||||
|
||||
onLogoutClick: function(event) {
|
||||
dis.dispatch({
|
||||
action: 'logout'
|
||||
});
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
},
|
||||
|
||||
handleResize: function(e) {
|
||||
var hideLhsThreshold = 1000;
|
||||
var showLhsThreshold = 1000;
|
||||
var hideRhsThreshold = 820;
|
||||
var showRhsThreshold = 820;
|
||||
|
||||
if (this.state.width > hideLhsThreshold && window.innerWidth <= hideLhsThreshold) {
|
||||
dis.dispatch({ action: 'hide_left_panel' });
|
||||
}
|
||||
if (this.state.width <= showLhsThreshold && window.innerWidth > showLhsThreshold) {
|
||||
dis.dispatch({ action: 'show_left_panel' });
|
||||
}
|
||||
if (this.state.width > hideRhsThreshold && window.innerWidth <= hideRhsThreshold) {
|
||||
dis.dispatch({ action: 'hide_right_panel' });
|
||||
}
|
||||
if (this.state.width <= showRhsThreshold && window.innerWidth > showRhsThreshold) {
|
||||
dis.dispatch({ action: 'show_right_panel' });
|
||||
}
|
||||
|
||||
this.setState({width: window.innerWidth});
|
||||
},
|
||||
|
||||
onRoomCreated: function(room_id) {
|
||||
dis.dispatch({
|
||||
action: "view_room",
|
||||
room_id: room_id,
|
||||
});
|
||||
},
|
||||
|
||||
onRegisterClick: function() {
|
||||
this.showScreen("register");
|
||||
},
|
||||
|
||||
onLoginClick: function() {
|
||||
this.showScreen("login");
|
||||
},
|
||||
|
||||
onRegistered: function(credentials) {
|
||||
this.onLoggedIn(credentials);
|
||||
// do post-registration stuff
|
||||
this.showScreen("post_registration");
|
||||
},
|
||||
|
||||
onFinishPostRegistration: function() {
|
||||
// Don't confuse this with "PageType" which is the middle window to show
|
||||
this.setState({
|
||||
screen: undefined
|
||||
});
|
||||
this.showScreen("settings");
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var LeftPanel = sdk.getComponent('organisms.LeftPanel');
|
||||
var RoomView = sdk.getComponent('structures.RoomView');
|
||||
var RightPanel = sdk.getComponent('organisms.RightPanel');
|
||||
var UserSettings = sdk.getComponent('structures.UserSettings');
|
||||
var CreateRoom = sdk.getComponent('structures.CreateRoom');
|
||||
var RoomDirectory = sdk.getComponent('organisms.RoomDirectory');
|
||||
var MatrixToolbar = sdk.getComponent('molecules.MatrixToolbar');
|
||||
|
||||
// needs to be before normal PageTypes as you are logged in technically
|
||||
if (this.state.screen == 'post_registration') {
|
||||
return (
|
||||
<PostRegistration
|
||||
onComplete={this.onFinishPostRegistration} />
|
||||
);
|
||||
}
|
||||
else if (this.state.logged_in && this.state.ready) {
|
||||
var page_element;
|
||||
var right_panel = "";
|
||||
|
||||
switch (this.state.page_type) {
|
||||
case this.PageTypes.RoomView:
|
||||
page_element = (
|
||||
<RoomView
|
||||
roomId={this.state.currentRoom}
|
||||
key={this.state.currentRoom}
|
||||
ConferenceHandler={this.props.ConferenceHandler} />
|
||||
);
|
||||
right_panel = <RightPanel roomId={this.state.currentRoom} collapsed={this.state.collapse_rhs} />
|
||||
break;
|
||||
case this.PageTypes.UserSettings:
|
||||
page_element = <UserSettings />
|
||||
right_panel = <RightPanel collapsed={this.state.collapse_rhs}/>
|
||||
break;
|
||||
case this.PageTypes.CreateRoom:
|
||||
page_element = <CreateRoom onRoomCreated={this.onRoomCreated}/>
|
||||
right_panel = <RightPanel collapsed={this.state.collapse_rhs}/>
|
||||
break;
|
||||
case this.PageTypes.RoomDirectory:
|
||||
page_element = <RoomDirectory />
|
||||
right_panel = <RightPanel collapsed={this.state.collapse_rhs}/>
|
||||
break;
|
||||
}
|
||||
|
||||
// TODO: Fix duplication here and do conditionals like we do above
|
||||
if (Notifier.supportsDesktopNotifications() && !Notifier.isEnabled() && !Notifier.isToolbarHidden()) {
|
||||
return (
|
||||
<div className="mx_MatrixChat_wrapper">
|
||||
<MatrixToolbar />
|
||||
<div className="mx_MatrixChat mx_MatrixChat_toolbarShowing">
|
||||
<LeftPanel selectedRoom={this.state.currentRoom} collapsed={this.state.collapse_lhs} />
|
||||
<main className="mx_MatrixChat_middlePanel">
|
||||
{page_element}
|
||||
</main>
|
||||
{right_panel}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
else {
|
||||
return (
|
||||
<div className="mx_MatrixChat">
|
||||
<LeftPanel selectedRoom={this.state.currentRoom} collapsed={this.state.collapse_lhs} />
|
||||
<main className="mx_MatrixChat_middlePanel">
|
||||
{page_element}
|
||||
</main>
|
||||
{right_panel}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
} else if (this.state.logged_in) {
|
||||
var Spinner = sdk.getComponent('elements.Spinner');
|
||||
return (
|
||||
<div className="mx_MatrixChat_splash">
|
||||
<Spinner />
|
||||
<a href="#" className="mx_MatrixChat_splashButtons" onClick={ this.onLogoutClick }>Logout</a>
|
||||
</div>
|
||||
);
|
||||
} else if (this.state.screen == 'register') {
|
||||
return (
|
||||
<Registration
|
||||
clientSecret={this.state.register_client_secret}
|
||||
sessionId={this.state.register_session_id}
|
||||
idSid={this.state.register_id_sid}
|
||||
hsUrl={config.default_hs_url}
|
||||
isUrl={config.default_is_url}
|
||||
registrationUrl={this.props.registrationUrl}
|
||||
onLoggedIn={this.onRegistered}
|
||||
onLoginClick={this.onLoginClick} />
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Login
|
||||
onLoggedIn={this.onLoggedIn}
|
||||
onRegisterClick={this.onRegisterClick}
|
||||
homeserverUrl={config.default_hs_url}
|
||||
identityServerUrl={config.default_is_url} />
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
1051
src/components/structures/RoomView.js
Normal file
1051
src/components/structures/RoomView.js
Normal file
File diff suppressed because it is too large
Load diff
162
src/components/structures/UserSettings.js
Normal file
162
src/components/structures/UserSettings.js
Normal file
|
@ -0,0 +1,162 @@
|
|||
/*
|
||||
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 sdk = require('../../index');
|
||||
var MatrixClientPeg = require("../../MatrixClientPeg");
|
||||
var Modal = require('../../Modal');
|
||||
var q = require('q');
|
||||
var version = require('../../../package.json').version;
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'UserSettings',
|
||||
Phases: {
|
||||
Loading: "loading",
|
||||
Display: "display",
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
avatarUrl: null,
|
||||
threePids: [],
|
||||
clientVersion: version,
|
||||
phase: this.Phases.Loading,
|
||||
};
|
||||
},
|
||||
|
||||
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); }
|
||||
);
|
||||
},
|
||||
|
||||
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);
|
||||
},
|
||||
|
||||
addEmail: function() {
|
||||
|
||||
},
|
||||
|
||||
editDisplayName: function() {
|
||||
this.refs.displayname.edit();
|
||||
},
|
||||
|
||||
changePassword: function() {
|
||||
var ChangePassword = sdk.getComponent('settings.ChangePassword');
|
||||
Modal.createDialog(ChangePassword);
|
||||
},
|
||||
|
||||
onLogoutClicked: function(ev) {
|
||||
var LogoutPrompt = sdk.getComponent('dialogs.LogoutPrompt');
|
||||
this.logoutModal = Modal.createDialog(LogoutPrompt, {onCancel: this.onLogoutPromptCancel});
|
||||
},
|
||||
|
||||
onLogoutPromptCancel: function() {
|
||||
this.logoutModal.closeDialog();
|
||||
},
|
||||
|
||||
onAvatarDialogCancel: function() {
|
||||
this.avatarDialog.close();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var Loader = sdk.getComponent("elements.Spinner");
|
||||
if (this.state.phase === this.Phases.Loading) {
|
||||
return <Loader />
|
||||
}
|
||||
else if (this.state.phase === this.Phases.Display) {
|
||||
var ChangeDisplayName = sdk.getComponent('settings.ChangeDisplayName');
|
||||
var EnableNotificationsButton = sdk.getComponent('settings.EnableNotificationsButton');
|
||||
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
|
||||
</div>
|
||||
<div className="mx_UserSettings_Avatar_Edit" onClick={this.editAvatar}>
|
||||
Edit
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mx_UserSettings_DisplayName">
|
||||
<ChangeDisplayName ref="displayname" />
|
||||
<div className="mx_UserSettings_DisplayName_Edit" onClick={this.editDisplayName}>
|
||||
Edit
|
||||
</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>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
199
src/components/structures/login/Login.js
Normal file
199
src/components/structures/login/Login.js
Normal file
|
@ -0,0 +1,199 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
var ReactDOM = require('react-dom');
|
||||
var sdk = require('../../../index');
|
||||
var Signup = require("../../../Signup");
|
||||
var PasswordLogin = require("../../views/login/PasswordLogin");
|
||||
var CasLogin = require("../../views/login/CasLogin");
|
||||
var ServerConfig = require("../../views/login/ServerConfig");
|
||||
|
||||
/**
|
||||
* A wire component which glues together login UI components and Signup logic
|
||||
*/
|
||||
module.exports = React.createClass({displayName: 'Login',
|
||||
propTypes: {
|
||||
onLoggedIn: React.PropTypes.func.isRequired,
|
||||
homeserverUrl: React.PropTypes.string,
|
||||
identityServerUrl: React.PropTypes.string,
|
||||
// login shouldn't know or care how registration is done.
|
||||
onRegisterClick: React.PropTypes.func.isRequired
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
homeserverUrl: 'https://matrix.org/',
|
||||
identityServerUrl: 'https://matrix.org'
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
busy: false,
|
||||
errorText: null,
|
||||
enteredHomeserverUrl: this.props.homeserverUrl,
|
||||
enteredIdentityServerUrl: this.props.identityServerUrl
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._initLoginLogic();
|
||||
},
|
||||
|
||||
onPasswordLogin: function(username, password) {
|
||||
var self = this;
|
||||
self.setState({
|
||||
busy: true
|
||||
});
|
||||
|
||||
this._loginLogic.loginViaPassword(username, password).then(function(data) {
|
||||
self.props.onLoggedIn(data);
|
||||
}, function(error) {
|
||||
self._setErrorTextFromError(error);
|
||||
}).finally(function() {
|
||||
self.setState({
|
||||
busy: false
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
onHsUrlChanged: function(newHsUrl) {
|
||||
this._initLoginLogic(newHsUrl);
|
||||
},
|
||||
|
||||
onIsUrlChanged: function(newIsUrl) {
|
||||
this._initLoginLogic(null, newIsUrl);
|
||||
},
|
||||
|
||||
_initLoginLogic: function(hsUrl, isUrl) {
|
||||
var self = this;
|
||||
hsUrl = hsUrl || this.state.enteredHomeserverUrl;
|
||||
isUrl = isUrl || this.state.enteredIdentityServerUrl;
|
||||
|
||||
var loginLogic = new Signup.Login(hsUrl, isUrl);
|
||||
this._loginLogic = loginLogic;
|
||||
|
||||
loginLogic.getFlows().then(function(flows) {
|
||||
// old behaviour was to always use the first flow without presenting
|
||||
// options. This works in most cases (we don't have a UI for multiple
|
||||
// logins so let's skip that for now).
|
||||
loginLogic.chooseFlow(0);
|
||||
}, function(err) {
|
||||
self._setErrorTextFromError(err);
|
||||
}).finally(function() {
|
||||
self.setState({
|
||||
busy: false
|
||||
});
|
||||
});
|
||||
|
||||
this.setState({
|
||||
enteredHomeserverUrl: hsUrl,
|
||||
enteredIdentityServerUrl: isUrl,
|
||||
busy: true,
|
||||
errorText: null // reset err messages
|
||||
});
|
||||
},
|
||||
|
||||
_getCurrentFlowStep: function() {
|
||||
return this._loginLogic ? this._loginLogic.getCurrentFlowStep() : null
|
||||
},
|
||||
|
||||
_setErrorTextFromError: function(err) {
|
||||
if (err.friendlyText) {
|
||||
this.setState({
|
||||
errorText: err.friendlyText
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var errCode = err.errcode;
|
||||
if (!errCode && err.httpStatus) {
|
||||
errCode = "HTTP " + err.httpStatus;
|
||||
}
|
||||
this.setState({
|
||||
errorText: (
|
||||
"Error: Problem communicating with the given homeserver " +
|
||||
(errCode ? "(" + errCode + ")" : "")
|
||||
)
|
||||
});
|
||||
},
|
||||
|
||||
componentForStep: function(step) {
|
||||
switch (step) {
|
||||
case 'm.login.password':
|
||||
return (
|
||||
<PasswordLogin onSubmit={this.onPasswordLogin} />
|
||||
);
|
||||
case 'm.login.cas':
|
||||
return (
|
||||
<CasLogin />
|
||||
);
|
||||
default:
|
||||
if (!step) {
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
Sorry, this homeserver is using a login which is not
|
||||
recognised by Vector ({step})
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var Loader = sdk.getComponent("elements.Spinner");
|
||||
var loader = this.state.busy ? <div className="mx_Login_loader"><Loader /></div> : null;
|
||||
|
||||
return (
|
||||
<div className="mx_Login">
|
||||
<div className="mx_Login_box">
|
||||
<div className="mx_Login_logo">
|
||||
<img src="img/logo.png" width="249" height="78" alt="vector"/>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Sign in</h2>
|
||||
{ this.componentForStep(this._getCurrentFlowStep()) }
|
||||
<ServerConfig ref="serverConfig"
|
||||
withToggleButton={true}
|
||||
defaultHsUrl={this.props.homeserverUrl}
|
||||
defaultIsUrl={this.props.identityServerUrl}
|
||||
onHsUrlChanged={this.onHsUrlChanged}
|
||||
onIsUrlChanged={this.onIsUrlChanged}
|
||||
delayTimeMs={1000}/>
|
||||
<div className="mx_Login_error">
|
||||
{ loader }
|
||||
{ this.state.errorText }
|
||||
</div>
|
||||
<a className="mx_Login_create" onClick={this.props.onRegisterClick} href="#">
|
||||
Create a new account
|
||||
</a>
|
||||
<br/>
|
||||
<div className="mx_Login_links">
|
||||
<a href="https://medium.com/@Vector">blog</a> ·
|
||||
<a href="https://twitter.com/@VectorCo">twitter</a> ·
|
||||
<a href="https://github.com/vector-im/vector-web">github</a> ·
|
||||
<a href="https://matrix.org">powered by Matrix</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
80
src/components/structures/login/PostRegistration.js
Normal file
80
src/components/structures/login/PostRegistration.js
Normal file
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
var sdk = require('../../../index');
|
||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'PostRegistration',
|
||||
|
||||
propTypes: {
|
||||
onComplete: React.PropTypes.func.isRequired
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
avatarUrl: null,
|
||||
errorString: null,
|
||||
busy: false
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
// There is some assymetry between ChangeDisplayName and ChangeAvatar,
|
||||
// as ChangeDisplayName will auto-get the name but ChangeAvatar expects
|
||||
// the URL to be passed to you (because it's also used for room avatars).
|
||||
var cli = MatrixClientPeg.get();
|
||||
this.setState({busy: true});
|
||||
var self = this;
|
||||
cli.getProfileInfo(cli.credentials.userId).done(function(result) {
|
||||
self.setState({
|
||||
avatarUrl: MatrixClientPeg.get().mxcUrlToHttp(result.avatar_url),
|
||||
busy: false
|
||||
});
|
||||
}, function(error) {
|
||||
self.setState({
|
||||
errorString: "Failed to fetch avatar URL",
|
||||
busy: false
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var ChangeDisplayName = sdk.getComponent('settings.ChangeDisplayName');
|
||||
var ChangeAvatar = sdk.getComponent('settings.ChangeAvatar');
|
||||
return (
|
||||
<div className="mx_Login">
|
||||
<div className="mx_Login_box">
|
||||
<div className="mx_Login_logo">
|
||||
<img src="img/logo.png" width="249" height="78" alt="vector"/>
|
||||
</div>
|
||||
<div className="mx_Login_profile">
|
||||
Set a display name:
|
||||
<ChangeDisplayName />
|
||||
Upload an avatar:
|
||||
<ChangeAvatar
|
||||
initialAvatarUrl={this.state.avatarUrl} />
|
||||
<button onClick={this.props.onComplete}>Continue</button>
|
||||
{this.state.errorString}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
248
src/components/structures/login/Registration.js
Normal file
248
src/components/structures/login/Registration.js
Normal file
|
@ -0,0 +1,248 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
|
||||
var sdk = require('../../../index');
|
||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
var dis = require('../../../dispatcher');
|
||||
var Signup = require("../../../Signup");
|
||||
var ServerConfig = require("../../views/login/ServerConfig");
|
||||
var RegistrationForm = require("../../views/login/RegistrationForm");
|
||||
var CaptchaForm = require("../../views/login/CaptchaForm");
|
||||
|
||||
var MIN_PASSWORD_LENGTH = 6;
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'Registration',
|
||||
|
||||
propTypes: {
|
||||
onLoggedIn: React.PropTypes.func.isRequired,
|
||||
clientSecret: React.PropTypes.string,
|
||||
sessionId: React.PropTypes.string,
|
||||
registrationUrl: React.PropTypes.string,
|
||||
idSid: React.PropTypes.string,
|
||||
hsUrl: React.PropTypes.string,
|
||||
isUrl: React.PropTypes.string,
|
||||
// registration shouldn't know or care how login is done.
|
||||
onLoginClick: React.PropTypes.func.isRequired
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
busy: false,
|
||||
errorText: null,
|
||||
enteredHomeserverUrl: this.props.hsUrl,
|
||||
enteredIdentityServerUrl: this.props.isUrl
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
// attach this to the instance rather than this.state since it isn't UI
|
||||
this.registerLogic = new Signup.Register(
|
||||
this.props.hsUrl, this.props.isUrl
|
||||
);
|
||||
this.registerLogic.setClientSecret(this.props.clientSecret);
|
||||
this.registerLogic.setSessionId(this.props.sessionId);
|
||||
this.registerLogic.setRegistrationUrl(this.props.registrationUrl);
|
||||
this.registerLogic.setIdSid(this.props.idSid);
|
||||
this.registerLogic.recheckState();
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
dis.unregister(this.dispatcherRef);
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
// may have already done an HTTP hit (e.g. redirect from an email) so
|
||||
// check for any pending response
|
||||
var promise = this.registerLogic.getPromise();
|
||||
if (promise) {
|
||||
this.onProcessingRegistration(promise);
|
||||
}
|
||||
},
|
||||
|
||||
onHsUrlChanged: function(newHsUrl) {
|
||||
this.registerLogic.setHomeserverUrl(newHsUrl);
|
||||
},
|
||||
|
||||
onIsUrlChanged: function(newIsUrl) {
|
||||
this.registerLogic.setIdentityServerUrl(newIsUrl);
|
||||
},
|
||||
|
||||
onAction: function(payload) {
|
||||
if (payload.action !== "registration_step_update") {
|
||||
return;
|
||||
}
|
||||
this.forceUpdate(); // registration state has changed.
|
||||
},
|
||||
|
||||
onFormSubmit: function(formVals) {
|
||||
var self = this;
|
||||
this.setState({
|
||||
errorText: "",
|
||||
busy: true
|
||||
});
|
||||
this.onProcessingRegistration(this.registerLogic.register(formVals));
|
||||
},
|
||||
|
||||
// Promise is resolved when the registration process is FULLY COMPLETE
|
||||
onProcessingRegistration: function(promise) {
|
||||
var self = this;
|
||||
promise.done(function(response) {
|
||||
if (!response || !response.access_token) {
|
||||
console.warn(
|
||||
"FIXME: Register fulfilled without a final response, " +
|
||||
"did you break the promise chain?"
|
||||
);
|
||||
// no matter, we'll grab it direct
|
||||
response = self.registerLogic.getCredentials();
|
||||
}
|
||||
if (!response || !response.user_id || !response.access_token) {
|
||||
console.error("Final response is missing keys.");
|
||||
self.setState({
|
||||
errorText: "There was a problem processing the response."
|
||||
});
|
||||
return;
|
||||
}
|
||||
self.props.onLoggedIn({
|
||||
userId: response.user_id,
|
||||
homeserverUrl: self.registerLogic.getHomeserverUrl(),
|
||||
identityServerUrl: self.registerLogic.getIdentityServerUrl(),
|
||||
accessToken: response.access_token
|
||||
});
|
||||
self.setState({
|
||||
busy: false
|
||||
});
|
||||
}, function(err) {
|
||||
if (err.message) {
|
||||
self.setState({
|
||||
errorText: err.message
|
||||
});
|
||||
}
|
||||
self.setState({
|
||||
busy: false
|
||||
});
|
||||
console.log(err);
|
||||
});
|
||||
},
|
||||
|
||||
onFormValidationFailed: function(errCode) {
|
||||
var errMsg;
|
||||
switch (errCode) {
|
||||
case "RegistrationForm.ERR_PASSWORD_MISSING":
|
||||
errMsg = "Missing password.";
|
||||
break;
|
||||
case "RegistrationForm.ERR_PASSWORD_MISMATCH":
|
||||
errMsg = "Passwords don't match.";
|
||||
break;
|
||||
case "RegistrationForm.ERR_PASSWORD_LENGTH":
|
||||
errMsg = `Password too short (min ${MIN_PASSWORD_LENGTH}).`;
|
||||
break;
|
||||
default:
|
||||
console.error("Unknown error code: %s", errCode);
|
||||
errMsg = "An unknown error occurred.";
|
||||
break;
|
||||
}
|
||||
this.setState({
|
||||
errorText: errMsg
|
||||
});
|
||||
},
|
||||
|
||||
onCaptchaLoaded: function(divIdName) {
|
||||
this.registerLogic.tellStage("m.login.recaptcha", {
|
||||
divId: divIdName
|
||||
});
|
||||
this.setState({
|
||||
busy: false // requires user input
|
||||
});
|
||||
},
|
||||
|
||||
_getRegisterContentJsx: function() {
|
||||
var currStep = this.registerLogic.getStep();
|
||||
var registerStep;
|
||||
switch (currStep) {
|
||||
case "Register.COMPLETE":
|
||||
break; // NOP
|
||||
case "Register.START":
|
||||
case "Register.STEP_m.login.dummy":
|
||||
registerStep = (
|
||||
<RegistrationForm
|
||||
showEmail={true}
|
||||
minPasswordLength={MIN_PASSWORD_LENGTH}
|
||||
onError={this.onFormValidationFailed}
|
||||
onRegisterClick={this.onFormSubmit} />
|
||||
);
|
||||
break;
|
||||
case "Register.STEP_m.login.email.identity":
|
||||
registerStep = (
|
||||
<div>
|
||||
Please check your email to continue registration.
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
case "Register.STEP_m.login.recaptcha":
|
||||
registerStep = (
|
||||
<CaptchaForm onCaptchaLoaded={this.onCaptchaLoaded} />
|
||||
);
|
||||
break;
|
||||
default:
|
||||
console.error("Unknown register state: %s", currStep);
|
||||
break;
|
||||
}
|
||||
var busySpinner;
|
||||
if (this.state.busy) {
|
||||
var Spinner = sdk.getComponent("elements.Spinner");
|
||||
busySpinner = (
|
||||
<Spinner />
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<h2>Create an account</h2>
|
||||
{registerStep}
|
||||
<div className="mx_Login_error">{this.state.errorText}</div>
|
||||
{busySpinner}
|
||||
<ServerConfig ref="serverConfig"
|
||||
withToggleButton={true}
|
||||
defaultHsUrl={this.state.enteredHomeserverUrl}
|
||||
defaultIsUrl={this.state.enteredIdentityServerUrl}
|
||||
onHsUrlChanged={this.onHsUrlChanged}
|
||||
onIsUrlChanged={this.onIsUrlChanged}
|
||||
delayTimeMs={1000} />
|
||||
<a className="mx_Login_create" onClick={this.props.onLoginClick} href="#">
|
||||
I already have an account
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<div className="mx_Login">
|
||||
<div className="mx_Login_box">
|
||||
<div className="mx_Login_logo">
|
||||
<img src="img/logo.png" width="249" height="78" alt="vector"/>
|
||||
</div>
|
||||
{this._getRegisterContentJsx()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
65
src/components/views/dialogs/ErrorDialog.js
Normal file
65
src/components/views/dialogs/ErrorDialog.js
Normal file
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Usage:
|
||||
* Modal.createDialog(ErrorDialog, {
|
||||
* title: "some text", (default: "Error")
|
||||
* description: "some more text",
|
||||
* button: "Button Text",
|
||||
* onClose: someFunction,
|
||||
* focus: true|false (default: true)
|
||||
* });
|
||||
*/
|
||||
|
||||
var React = require("react");
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'ErrorDialog',
|
||||
propTypes: {
|
||||
title: React.PropTypes.string,
|
||||
button: React.PropTypes.string,
|
||||
focus: React.PropTypes.bool,
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
title: "Error",
|
||||
description: "An error has occurred.",
|
||||
button: "OK",
|
||||
focus: true,
|
||||
};
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<div className="mx_ErrorDialog">
|
||||
<div className="mx_ErrorDialogTitle">
|
||||
{this.props.title}
|
||||
</div>
|
||||
<div className="mx_Dialog_content">
|
||||
{this.props.description}
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this.props.onFinished} autoFocus={this.props.focus}>
|
||||
{this.props.button}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
48
src/components/views/dialogs/LogoutPrompt.js
Normal file
48
src/components/views/dialogs/LogoutPrompt.js
Normal file
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
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 dis = require("../../../dispatcher");
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'LogoutPrompt',
|
||||
logOut: function() {
|
||||
dis.dispatch({action: 'logout'});
|
||||
if (this.props.onFinished) {
|
||||
this.props.onFinished();
|
||||
}
|
||||
},
|
||||
|
||||
cancelPrompt: function() {
|
||||
if (this.props.onFinished) {
|
||||
this.props.onFinished();
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<div>
|
||||
<div className="mx_Dialog_content">
|
||||
Sign out?
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this.logOut}>Sign Out</button>
|
||||
<button onClick={this.cancelPrompt}>Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
67
src/components/views/dialogs/QuestionDialog.js
Normal file
67
src/components/views/dialogs/QuestionDialog.js
Normal file
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
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");
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'QuestionDialog',
|
||||
propTypes: {
|
||||
title: React.PropTypes.string,
|
||||
description: React.PropTypes.string,
|
||||
button: React.PropTypes.string,
|
||||
focus: React.PropTypes.bool,
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
title: "",
|
||||
description: "",
|
||||
button: "OK",
|
||||
focus: true,
|
||||
};
|
||||
},
|
||||
|
||||
onOk: function() {
|
||||
this.props.onFinished(true);
|
||||
},
|
||||
|
||||
onCancel: function() {
|
||||
this.props.onFinished(false);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<div className="mx_QuestionDialog">
|
||||
<div className="mx_QuestionDialogTitle">
|
||||
{this.props.title}
|
||||
</div>
|
||||
<div className="mx_Dialog_content">
|
||||
{this.props.description}
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this.onOk} autoFocus={this.props.focus}>
|
||||
{this.props.button}
|
||||
</button>
|
||||
|
||||
<button onClick={this.onCancel}>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
126
src/components/views/login/RegistrationForm.js
Normal file
126
src/components/views/login/RegistrationForm.js
Normal file
|
@ -0,0 +1,126 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
var sdk = require('../../../index');
|
||||
|
||||
/**
|
||||
* A pure UI component which displays a registration form.
|
||||
*/
|
||||
module.exports = React.createClass({
|
||||
displayName: 'RegistrationForm',
|
||||
|
||||
propTypes: {
|
||||
defaultEmail: React.PropTypes.string,
|
||||
defaultUsername: React.PropTypes.string,
|
||||
showEmail: React.PropTypes.bool,
|
||||
minPasswordLength: React.PropTypes.number,
|
||||
onError: React.PropTypes.func,
|
||||
onRegisterClick: React.PropTypes.func // onRegisterClick(Object) => ?Promise
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
showEmail: false,
|
||||
minPasswordLength: 6,
|
||||
onError: function(e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
email: this.props.defaultEmail,
|
||||
username: this.props.defaultUsername,
|
||||
password: null,
|
||||
passwordConfirm: null
|
||||
};
|
||||
},
|
||||
|
||||
onSubmit: function(ev) {
|
||||
ev.preventDefault();
|
||||
|
||||
var pwd1 = this.refs.password.value.trim();
|
||||
var pwd2 = this.refs.passwordConfirm.value.trim()
|
||||
|
||||
var errCode;
|
||||
if (!pwd1 || !pwd2) {
|
||||
errCode = "RegistrationForm.ERR_PASSWORD_MISSING";
|
||||
}
|
||||
else if (pwd1 !== pwd2) {
|
||||
errCode = "RegistrationForm.ERR_PASSWORD_MISMATCH";
|
||||
}
|
||||
else if (pwd1.length < this.props.minPasswordLength) {
|
||||
errCode = "RegistrationForm.ERR_PASSWORD_LENGTH";
|
||||
}
|
||||
if (errCode) {
|
||||
this.props.onError(errCode);
|
||||
return;
|
||||
}
|
||||
|
||||
var promise = this.props.onRegisterClick({
|
||||
username: this.refs.username.value.trim(),
|
||||
password: pwd1,
|
||||
email: this.refs.email.value.trim()
|
||||
});
|
||||
|
||||
if (promise) {
|
||||
ev.target.disabled = true;
|
||||
promise.finally(function() {
|
||||
ev.target.disabled = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var emailSection, registerButton;
|
||||
if (this.props.showEmail) {
|
||||
emailSection = (
|
||||
<input className="mx_Login_field" type="text" ref="email"
|
||||
autoFocus={true} placeholder="Email address"
|
||||
defaultValue={this.state.email} />
|
||||
);
|
||||
}
|
||||
if (this.props.onRegisterClick) {
|
||||
registerButton = (
|
||||
<input className="mx_Login_submit" type="submit" value="Register" />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form onSubmit={this.onSubmit}>
|
||||
{emailSection}
|
||||
<br />
|
||||
<input className="mx_Login_field" type="text" ref="username"
|
||||
placeholder="User name" defaultValue={this.state.username} />
|
||||
<br />
|
||||
<input className="mx_Login_field" type="password" ref="password"
|
||||
placeholder="Password" defaultValue={this.state.password} />
|
||||
<br />
|
||||
<input className="mx_Login_field" type="password" ref="passwordConfirm"
|
||||
placeholder="Confirm password"
|
||||
defaultValue={this.state.passwordConfirm} />
|
||||
<br />
|
||||
{registerButton}
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
161
src/components/views/login/ServerConfig.js
Normal file
161
src/components/views/login/ServerConfig.js
Normal file
|
@ -0,0 +1,161 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
var Modal = require('../../../Modal');
|
||||
var sdk = require('../../../index');
|
||||
|
||||
/**
|
||||
* A pure UI component which displays the HS and IS to use.
|
||||
*/
|
||||
module.exports = React.createClass({
|
||||
displayName: 'ServerConfig',
|
||||
|
||||
propTypes: {
|
||||
onHsUrlChanged: React.PropTypes.func,
|
||||
onIsUrlChanged: React.PropTypes.func,
|
||||
defaultHsUrl: React.PropTypes.string,
|
||||
defaultIsUrl: React.PropTypes.string,
|
||||
withToggleButton: React.PropTypes.bool,
|
||||
delayTimeMs: React.PropTypes.number // time to wait before invoking onChanged
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
onHsUrlChanged: function() {},
|
||||
onIsUrlChanged: function() {},
|
||||
withToggleButton: false,
|
||||
delayTimeMs: 0
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
hs_url: this.props.defaultHsUrl,
|
||||
is_url: this.props.defaultIsUrl,
|
||||
original_hs_url: this.props.defaultHsUrl,
|
||||
original_is_url: this.props.defaultIsUrl,
|
||||
// no toggle button = show, toggle button = hide
|
||||
configVisible: !this.props.withToggleButton
|
||||
}
|
||||
},
|
||||
|
||||
onHomeserverChanged: function(ev) {
|
||||
this.setState({hs_url: ev.target.value}, function() {
|
||||
this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, function() {
|
||||
this.props.onHsUrlChanged(this.state.hs_url);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
onIdentityServerChanged: function(ev) {
|
||||
this.setState({is_url: ev.target.value}, function() {
|
||||
this._isTimeoutId = this._waitThenInvoke(this._isTimeoutId, function() {
|
||||
this.props.onIsUrlChanged(this.state.is_url);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
_waitThenInvoke: function(existingTimeoutId, fn) {
|
||||
if (existingTimeoutId) {
|
||||
clearTimeout(existingTimeoutId);
|
||||
}
|
||||
return setTimeout(fn.bind(this), this.props.delayTimeMs);
|
||||
},
|
||||
|
||||
getHsUrl: function() {
|
||||
return this.state.hs_url;
|
||||
},
|
||||
|
||||
getIsUrl: function() {
|
||||
return this.state.is_url;
|
||||
},
|
||||
|
||||
onServerConfigVisibleChange: function(ev) {
|
||||
this.setState({
|
||||
configVisible: ev.target.checked
|
||||
});
|
||||
},
|
||||
|
||||
showHelpPopup: function() {
|
||||
var ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: 'Custom Server Options',
|
||||
description: <span>
|
||||
You can use the custom server options to log into other Matrix
|
||||
servers by specifying a different Home server URL.
|
||||
<br/>
|
||||
This allows you to use Vector with an existing Matrix account on
|
||||
a different Home server.
|
||||
<br/>
|
||||
<br/>
|
||||
You can also set a custom Identity server but this will affect
|
||||
people's ability to find you if you use a server in a group other
|
||||
than the main Matrix.org group.
|
||||
</span>,
|
||||
button: "Dismiss",
|
||||
focus: true
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var serverConfigStyle = {};
|
||||
serverConfigStyle.display = this.state.configVisible ? 'block' : 'none';
|
||||
|
||||
var toggleButton;
|
||||
if (this.props.withToggleButton) {
|
||||
toggleButton = (
|
||||
<div>
|
||||
<input className="mx_Login_checkbox" id="advanced" type="checkbox"
|
||||
checked={this.state.configVisible}
|
||||
onChange={this.onServerConfigVisibleChange} />
|
||||
<label className="mx_Login_label" htmlFor="advanced">
|
||||
Use custom server options (advanced)
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{toggleButton}
|
||||
<div style={serverConfigStyle}>
|
||||
<div className="mx_ServerConfig">
|
||||
<label className="mx_Login_label mx_ServerConfig_hslabel" htmlFor="hsurl">
|
||||
Home server URL
|
||||
</label>
|
||||
<input className="mx_Login_field" id="hsurl" type="text"
|
||||
placeholder={this.state.original_hs_url}
|
||||
value={this.state.hs_url}
|
||||
onChange={this.onHomeserverChanged} />
|
||||
<label className="mx_Login_label mx_ServerConfig_islabel" htmlFor="isurl">
|
||||
Identity server URL
|
||||
</label>
|
||||
<input className="mx_Login_field" id="isurl" type="text"
|
||||
placeholder={this.state.original_is_url}
|
||||
value={this.state.is_url}
|
||||
onChange={this.onIdentityServerChanged} />
|
||||
<a className="mx_ServerConfig_help" href="#" onClick={this.showHelpPopup}>
|
||||
What does this mean?
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
|
@ -49,7 +49,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onKick: function() {
|
||||
var ErrorDialog = sdk.getComponent("organisms.ErrorDialog");
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
var roomId = this.props.member.roomId;
|
||||
var target = this.props.member.userId;
|
||||
MatrixClientPeg.get().kick(roomId, target).done(function() {
|
||||
|
@ -66,7 +66,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onBan: function() {
|
||||
var ErrorDialog = sdk.getComponent("organisms.ErrorDialog");
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
var roomId = this.props.member.roomId;
|
||||
var target = this.props.member.userId;
|
||||
MatrixClientPeg.get().ban(roomId, target).done(function() {
|
||||
|
@ -83,7 +83,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onMuteToggle: function() {
|
||||
var ErrorDialog = sdk.getComponent("organisms.ErrorDialog");
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
var roomId = this.props.member.roomId;
|
||||
var target = this.props.member.userId;
|
||||
var room = MatrixClientPeg.get().getRoom(roomId);
|
||||
|
@ -127,7 +127,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onModToggle: function() {
|
||||
var ErrorDialog = sdk.getComponent("organisms.ErrorDialog");
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
var roomId = this.props.member.roomId;
|
||||
var target = this.props.member.userId;
|
||||
var room = MatrixClientPeg.get().getRoom(roomId);
|
||||
|
@ -224,8 +224,8 @@ module.exports = React.createClass({
|
|||
// FIXME: this is horribly duplicated with MemberTile's onLeaveClick.
|
||||
// Not sure what the right solution to this is.
|
||||
onLeaveClick: function() {
|
||||
var ErrorDialog = sdk.getComponent("organisms.ErrorDialog");
|
||||
var QuestionDialog = sdk.getComponent("organisms.QuestionDialog");
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
|
||||
var roomId = this.props.member.roomId;
|
||||
Modal.createDialog(QuestionDialog, {
|
||||
|
|
291
src/components/views/rooms/MemberList.js
Normal file
291
src/components/views/rooms/MemberList.js
Normal file
|
@ -0,0 +1,291 @@
|
|||
/*
|
||||
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 classNames = require('classnames');
|
||||
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||
var Modal = require("../../../Modal");
|
||||
var sdk = require('../../../index');
|
||||
var GeminiScrollbar = require('react-gemini-scrollbar');
|
||||
|
||||
var INITIAL_LOAD_NUM_MEMBERS = 50;
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'MemberList',
|
||||
getInitialState: function() {
|
||||
if (!this.props.roomId) return { members: [] };
|
||||
var cli = MatrixClientPeg.get();
|
||||
var room = cli.getRoom(this.props.roomId);
|
||||
if (!room) return { members: [] };
|
||||
|
||||
this.memberDict = this.getMemberDict();
|
||||
|
||||
var members = this.roomMembers(INITIAL_LOAD_NUM_MEMBERS);
|
||||
return {
|
||||
members: members
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
var cli = MatrixClientPeg.get();
|
||||
cli.on("RoomState.members", this.onRoomStateMember);
|
||||
cli.on("RoomMember.name", this.onRoomMemberName);
|
||||
cli.on("Room", this.onRoom); // invites
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
if (MatrixClientPeg.get()) {
|
||||
MatrixClientPeg.get().removeListener("Room", this.onRoom);
|
||||
MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember);
|
||||
MatrixClientPeg.get().removeListener("RoomMember.name", this.onRoomMemberName);
|
||||
MatrixClientPeg.get().removeListener("User.presence", this.userPresenceFn);
|
||||
}
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
var self = this;
|
||||
|
||||
// Lazy-load in more than the first N members
|
||||
setTimeout(function() {
|
||||
if (!self.isMounted()) return;
|
||||
self.setState({
|
||||
members: self.roomMembers()
|
||||
});
|
||||
}, 50);
|
||||
|
||||
// Attach a SINGLE listener for global presence changes then locate the
|
||||
// member tile and re-render it. This is more efficient than every tile
|
||||
// evar attaching their own listener.
|
||||
function updateUserState(event, user) {
|
||||
// XXX: evil hack to track the age of this presence info.
|
||||
// this should be removed once syjs-28 is resolved in the JS SDK itself.
|
||||
user.lastPresenceTs = Date.now();
|
||||
|
||||
var tile = self.refs[user.userId];
|
||||
|
||||
if (tile) {
|
||||
self._updateList(); // reorder the membership list
|
||||
}
|
||||
}
|
||||
// FIXME: we should probably also reset 'lastActiveAgo' to zero whenever
|
||||
// we see a typing notif from a user, as we don't get presence updates for those.
|
||||
MatrixClientPeg.get().on("User.presence", updateUserState);
|
||||
this.userPresenceFn = updateUserState;
|
||||
},
|
||||
// Remember to set 'key' on a MemberList to the ID of the room it's for
|
||||
/*componentWillReceiveProps: function(newProps) {
|
||||
},*/
|
||||
|
||||
onRoom: function(room) {
|
||||
if (room.roomId !== this.props.roomId) {
|
||||
return;
|
||||
}
|
||||
// We listen for room events because when we accept an invite
|
||||
// we need to wait till the room is fully populated with state
|
||||
// before refreshing the member list else we get a stale list.
|
||||
this._updateList();
|
||||
},
|
||||
|
||||
onRoomStateMember: function(ev, state, member) {
|
||||
this._updateList();
|
||||
},
|
||||
|
||||
onRoomMemberName: function(ev, member) {
|
||||
this._updateList();
|
||||
},
|
||||
|
||||
_updateList: function() {
|
||||
this.memberDict = this.getMemberDict();
|
||||
|
||||
var self = this;
|
||||
this.setState({
|
||||
members: self.roomMembers()
|
||||
});
|
||||
},
|
||||
|
||||
onInvite: function(inputText) {
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
var self = this;
|
||||
inputText = inputText.trim(); // react requires es5-shim so we know trim() exists
|
||||
var isEmailAddress = /^\S+@\S+\.\S+$/.test(inputText);
|
||||
|
||||
// sanity check the input for user IDs
|
||||
if (!isEmailAddress && (inputText[0] !== '@' || inputText.indexOf(":") === -1)) {
|
||||
console.error("Bad user ID to invite: %s", inputText);
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Invite Error",
|
||||
description: "Malformed user ID. Should look like '@localpart:domain'"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var promise;
|
||||
if (isEmailAddress) {
|
||||
promise = MatrixClientPeg.get().inviteByEmail(this.props.roomId, inputText);
|
||||
}
|
||||
else {
|
||||
promise = MatrixClientPeg.get().invite(this.props.roomId, inputText);
|
||||
}
|
||||
|
||||
self.setState({
|
||||
inviting: true
|
||||
});
|
||||
console.log(
|
||||
"Invite %s to %s - isEmail=%s", inputText, this.props.roomId, isEmailAddress
|
||||
);
|
||||
promise.done(function(res) {
|
||||
console.log("Invited");
|
||||
self.setState({
|
||||
inviting: false
|
||||
});
|
||||
}, function(err) {
|
||||
console.error("Failed to invite: %s", JSON.stringify(err));
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Server error whilst inviting",
|
||||
description: err.message
|
||||
});
|
||||
self.setState({
|
||||
inviting: false
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
getMemberDict: function() {
|
||||
if (!this.props.roomId) return {};
|
||||
var cli = MatrixClientPeg.get();
|
||||
var room = cli.getRoom(this.props.roomId);
|
||||
if (!room) return {};
|
||||
|
||||
var all_members = room.currentState.members;
|
||||
|
||||
// XXX: evil hack until SYJS-28 is fixed
|
||||
Object.keys(all_members).map(function(userId) {
|
||||
if (all_members[userId].user && !all_members[userId].user.lastPresenceTs) {
|
||||
all_members[userId].user.lastPresenceTs = Date.now();
|
||||
}
|
||||
});
|
||||
|
||||
return all_members;
|
||||
},
|
||||
|
||||
roomMembers: function(limit) {
|
||||
var all_members = this.memberDict || {};
|
||||
var all_user_ids = Object.keys(all_members);
|
||||
|
||||
if (this.memberSort) all_user_ids.sort(this.memberSort);
|
||||
|
||||
var to_display = [];
|
||||
var count = 0;
|
||||
for (var i = 0; i < all_user_ids.length && (limit === undefined || count < limit); ++i) {
|
||||
var user_id = all_user_ids[i];
|
||||
var m = all_members[user_id];
|
||||
|
||||
if (m.membership == 'join' || m.membership == 'invite') {
|
||||
to_display.push(user_id);
|
||||
++count;
|
||||
}
|
||||
}
|
||||
return to_display;
|
||||
},
|
||||
|
||||
|
||||
|
||||
memberSort: function(userIdA, userIdB) {
|
||||
var userA = this.memberDict[userIdA].user;
|
||||
var userB = this.memberDict[userIdB].user;
|
||||
|
||||
var presenceMap = {
|
||||
online: 3,
|
||||
unavailable: 2,
|
||||
offline: 1
|
||||
};
|
||||
|
||||
var presenceOrdA = userA ? presenceMap[userA.presence] : 0;
|
||||
var presenceOrdB = userB ? presenceMap[userB.presence] : 0;
|
||||
|
||||
if (presenceOrdA != presenceOrdB) {
|
||||
return presenceOrdB - presenceOrdA;
|
||||
}
|
||||
|
||||
var latA = userA ? (userA.lastPresenceTs - (userA.lastActiveAgo || userA.lastPresenceTs)) : 0;
|
||||
var latB = userB ? (userB.lastPresenceTs - (userB.lastActiveAgo || userB.lastPresenceTs)) : 0;
|
||||
|
||||
return latB - latA;
|
||||
},
|
||||
|
||||
makeMemberTiles: function(membership) {
|
||||
var MemberTile = sdk.getComponent("rooms.MemberTile");
|
||||
|
||||
var self = this;
|
||||
return self.state.members.filter(function(userId) {
|
||||
var m = self.memberDict[userId];
|
||||
return m.membership == membership;
|
||||
}).map(function(userId) {
|
||||
var m = self.memberDict[userId];
|
||||
return (
|
||||
<MemberTile key={userId} member={m} ref={userId} />
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
onPopulateInvite: function(e) {
|
||||
this.onInvite(this.refs.invite.value);
|
||||
e.preventDefault();
|
||||
},
|
||||
|
||||
inviteTile: function() {
|
||||
if (this.state.inviting) {
|
||||
var Loader = sdk.getComponent("elements.Spinner");
|
||||
return (
|
||||
<Loader />
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<form onSubmit={this.onPopulateInvite}>
|
||||
<input className="mx_MemberList_invite" ref="invite" placeholder="Invite another user"/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var invitedSection = null;
|
||||
var invitedMemberTiles = this.makeMemberTiles('invite');
|
||||
if (invitedMemberTiles.length > 0) {
|
||||
invitedSection = (
|
||||
<div className="mx_MemberList_invited">
|
||||
<h2>Invited</h2>
|
||||
<div className="mx_MemberList_wrapper">
|
||||
{invitedMemberTiles}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="mx_MemberList">
|
||||
<GeminiScrollbar autoshow={true} className="mx_MemberList_border">
|
||||
{this.inviteTile()}
|
||||
<div>
|
||||
<div className="mx_MemberList_wrapper">
|
||||
{this.makeMemberTiles('join')}
|
||||
</div>
|
||||
</div>
|
||||
{invitedSection}
|
||||
</GeminiScrollbar>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
@ -31,7 +31,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onLeaveClick: function() {
|
||||
var QuestionDialog = sdk.getComponent("organisms.QuestionDialog");
|
||||
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
|
||||
var roomId = this.props.member.roomId;
|
||||
Modal.createDialog(QuestionDialog, {
|
||||
|
|
|
@ -272,7 +272,7 @@ module.exports = React.createClass({
|
|||
this.markdownEnabled = false;
|
||||
}
|
||||
else {
|
||||
var ErrorDialog = sdk.getComponent("organisms.ErrorDialog");
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Unknown command",
|
||||
description: "Usage: /markdown on|off"
|
||||
|
@ -292,7 +292,7 @@ module.exports = React.createClass({
|
|||
console.log("Command success.");
|
||||
}, function(err) {
|
||||
console.error("Command failure: %s", err);
|
||||
var ErrorDialog = sdk.getComponent("organisms.ErrorDialog");
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Server error",
|
||||
description: err.message
|
||||
|
@ -301,7 +301,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
else if (cmd.error) {
|
||||
console.error(cmd.error);
|
||||
var ErrorDialog = sdk.getComponent("organisms.ErrorDialog");
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Command error",
|
||||
description: cmd.error
|
||||
|
|
303
src/components/views/rooms/RoomList.js
Normal file
303
src/components/views/rooms/RoomList.js
Normal file
|
@ -0,0 +1,303 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
var React = require("react");
|
||||
var ReactDOM = require("react-dom");
|
||||
var GeminiScrollbar = require('react-gemini-scrollbar');
|
||||
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||
var RoomListSorter = require("../../../RoomListSorter");
|
||||
var dis = require("../../../dispatcher");
|
||||
var sdk = require('../../../index');
|
||||
|
||||
var HIDE_CONFERENCE_CHANS = true;
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'RoomList',
|
||||
|
||||
propTypes: {
|
||||
ConferenceHandler: React.PropTypes.any, // e.g. VectorConferenceHandler
|
||||
collapsed: React.PropTypes.bool,
|
||||
currentRoom: React.PropTypes.string
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
activityMap: null,
|
||||
lists: {},
|
||||
}
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
var cli = MatrixClientPeg.get();
|
||||
cli.on("Room", this.onRoom);
|
||||
cli.on("Room.timeline", this.onRoomTimeline);
|
||||
cli.on("Room.name", this.onRoomName);
|
||||
cli.on("Room.tags", this.onRoomTags);
|
||||
cli.on("RoomState.events", this.onRoomStateEvents);
|
||||
cli.on("RoomMember.name", this.onRoomMemberName);
|
||||
|
||||
var s = this.getRoomLists();
|
||||
s.activityMap = {};
|
||||
this.setState(s);
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
},
|
||||
|
||||
onAction: function(payload) {
|
||||
switch (payload.action) {
|
||||
case 'view_tooltip':
|
||||
this.tooltip = payload.tooltip;
|
||||
this._repositionTooltip();
|
||||
if (this.tooltip) this.tooltip.style.display = 'block';
|
||||
break
|
||||
}
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
dis.unregister(this.dispatcherRef);
|
||||
if (MatrixClientPeg.get()) {
|
||||
MatrixClientPeg.get().removeListener("Room", this.onRoom);
|
||||
MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline);
|
||||
MatrixClientPeg.get().removeListener("Room.name", this.onRoomName);
|
||||
MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents);
|
||||
}
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(newProps) {
|
||||
this.state.activityMap[newProps.selectedRoom] = undefined;
|
||||
this.setState({
|
||||
activityMap: this.state.activityMap
|
||||
});
|
||||
},
|
||||
|
||||
onRoom: function(room) {
|
||||
this.refreshRoomList();
|
||||
},
|
||||
|
||||
onRoomTimeline: function(ev, room, toStartOfTimeline) {
|
||||
if (toStartOfTimeline) return;
|
||||
|
||||
var hl = 0;
|
||||
if (
|
||||
room.roomId != this.props.selectedRoom &&
|
||||
ev.getSender() != MatrixClientPeg.get().credentials.userId)
|
||||
{
|
||||
// don't mark rooms as unread for just member changes
|
||||
if (ev.getType() != "m.room.member") {
|
||||
hl = 1;
|
||||
}
|
||||
|
||||
var actions = MatrixClientPeg.get().getPushActionsForEvent(ev);
|
||||
if (actions && actions.tweaks && actions.tweaks.highlight) {
|
||||
hl = 2;
|
||||
}
|
||||
}
|
||||
|
||||
if (hl > 0) {
|
||||
var newState = this.getRoomLists();
|
||||
|
||||
// obviously this won't deep copy but this shouldn't be necessary
|
||||
var amap = this.state.activityMap;
|
||||
amap[room.roomId] = Math.max(amap[room.roomId] || 0, hl);
|
||||
|
||||
newState.activityMap = amap;
|
||||
|
||||
this.setState(newState);
|
||||
}
|
||||
},
|
||||
|
||||
onRoomName: function(room) {
|
||||
this.refreshRoomList();
|
||||
},
|
||||
|
||||
onRoomTags: function(event, room) {
|
||||
this.refreshRoomList();
|
||||
},
|
||||
|
||||
onRoomStateEvents: function(ev, state) {
|
||||
setTimeout(this.refreshRoomList, 0);
|
||||
},
|
||||
|
||||
onRoomMemberName: function(ev, member) {
|
||||
setTimeout(this.refreshRoomList, 0);
|
||||
},
|
||||
|
||||
refreshRoomList: function() {
|
||||
// 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());
|
||||
},
|
||||
|
||||
getRoomLists: function() {
|
||||
var self = this;
|
||||
var s = { lists: {} };
|
||||
|
||||
s.lists["m.invite"] = [];
|
||||
s.lists["m.favourite"] = [];
|
||||
s.lists["m.recent"] = [];
|
||||
s.lists["m.lowpriority"] = [];
|
||||
s.lists["m.archived"] = [];
|
||||
|
||||
MatrixClientPeg.get().getRooms().forEach(function(room) {
|
||||
var me = room.getMember(MatrixClientPeg.get().credentials.userId);
|
||||
|
||||
if (me && me.membership == "invite") {
|
||||
s.lists["m.invite"].push(room);
|
||||
}
|
||||
else {
|
||||
var shouldShowRoom = (
|
||||
me && (me.membership == "join")
|
||||
);
|
||||
|
||||
// hiding conf rooms only ever toggles shouldShowRoom to false
|
||||
if (shouldShowRoom && HIDE_CONFERENCE_CHANS) {
|
||||
// we want to hide the 1:1 conf<->user room and not the group chat
|
||||
var joinedMembers = room.getJoinedMembers();
|
||||
if (joinedMembers.length === 2) {
|
||||
var otherMember = joinedMembers.filter(function(m) {
|
||||
return m.userId !== me.userId
|
||||
})[0];
|
||||
var ConfHandler = self.props.ConferenceHandler;
|
||||
if (ConfHandler && ConfHandler.isConferenceUser(otherMember)) {
|
||||
// console.log("Hiding conference 1:1 room %s", room.roomId);
|
||||
shouldShowRoom = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldShowRoom) {
|
||||
var tagNames = Object.keys(room.tags);
|
||||
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);
|
||||
}
|
||||
}
|
||||
else {
|
||||
s.lists["m.recent"].push(room);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
//console.log("calculated new roomLists; m.recent = " + s.lists["m.recent"]);
|
||||
|
||||
// we actually apply the sorting to this when receiving the prop in RoomSubLists.
|
||||
|
||||
return s;
|
||||
},
|
||||
|
||||
_repositionTooltip: function(e) {
|
||||
if (this.tooltip && this.tooltip.parentElement) {
|
||||
var scroll = ReactDOM.findDOMNode(this);
|
||||
this.tooltip.style.top = (scroll.parentElement.offsetTop + this.tooltip.parentElement.offsetTop - scroll.children[2].scrollTop) + "px";
|
||||
}
|
||||
},
|
||||
|
||||
onShowClick: function() {
|
||||
dis.dispatch({
|
||||
action: 'show_left_panel',
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var expandButton = this.props.collapsed ?
|
||||
<img className="mx_RoomList_expandButton" onClick={ this.onShowClick } src="img/menu.png" width="20" alt=">"/> :
|
||||
null;
|
||||
|
||||
var RoomSubList = sdk.getComponent('organisms.RoomSubList');
|
||||
var self = this;
|
||||
|
||||
return (
|
||||
<GeminiScrollbar className="mx_RoomList_scrollbar" autoshow={true} onScroll={self._repositionTooltip}>
|
||||
<div className="mx_RoomList">
|
||||
{ expandButton }
|
||||
|
||||
<RoomSubList list={ self.state.lists['m.invite'] }
|
||||
label="Invites"
|
||||
editable={ false }
|
||||
order="recent"
|
||||
activityMap={ self.state.activityMap }
|
||||
selectedRoom={ self.props.selectedRoom }
|
||||
collapsed={ self.props.collapsed } />
|
||||
|
||||
<RoomSubList list={ self.state.lists['m.favourite'] }
|
||||
label="Favourites"
|
||||
tagName="m.favourite"
|
||||
verb="favourite"
|
||||
editable={ true }
|
||||
order="manual"
|
||||
activityMap={ self.state.activityMap }
|
||||
selectedRoom={ self.props.selectedRoom }
|
||||
collapsed={ self.props.collapsed } />
|
||||
|
||||
<RoomSubList list={ self.state.lists['m.recent'] }
|
||||
label="Conversations"
|
||||
editable={ true }
|
||||
verb="restore"
|
||||
order="recent"
|
||||
activityMap={ self.state.activityMap }
|
||||
selectedRoom={ self.props.selectedRoom }
|
||||
collapsed={ self.props.collapsed } />
|
||||
|
||||
{ Object.keys(self.state.lists).map(function(tagName) {
|
||||
if (!tagName.match(/^m\.(invite|favourite|recent|lowpriority|archived)$/)) {
|
||||
return <RoomSubList list={ self.state.lists[tagName] }
|
||||
key={ tagName }
|
||||
label={ tagName }
|
||||
tagName={ tagName }
|
||||
verb={ "tag as " + tagName }
|
||||
editable={ true }
|
||||
order="manual"
|
||||
activityMap={ self.state.activityMap }
|
||||
selectedRoom={ self.props.selectedRoom }
|
||||
collapsed={ self.props.collapsed } />
|
||||
|
||||
}
|
||||
}) }
|
||||
|
||||
<RoomSubList list={ self.state.lists['m.lowpriority'] }
|
||||
label="Low priority"
|
||||
tagName="m.lowpriority"
|
||||
verb="demote"
|
||||
editable={ true }
|
||||
order="recent"
|
||||
bottommost={ self.state.lists['m.archived'].length === 0 }
|
||||
activityMap={ self.state.activityMap }
|
||||
selectedRoom={ self.props.selectedRoom }
|
||||
collapsed={ self.props.collapsed } />
|
||||
|
||||
<RoomSubList list={ self.state.lists['m.archived'] }
|
||||
label="Historical"
|
||||
editable={ false }
|
||||
order="recent"
|
||||
bottommost={ true }
|
||||
activityMap={ self.state.activityMap }
|
||||
selectedRoom={ self.props.selectedRoom }
|
||||
collapsed={ self.props.collapsed } />
|
||||
</div>
|
||||
</GeminiScrollbar>
|
||||
);
|
||||
}
|
||||
});
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
var React = require("react");
|
||||
var Notifier = require("../../../Notifier");
|
||||
var sdk = require('../../../index');
|
||||
var dis = require("../../../dispatcher");
|
||||
|
||||
|
@ -38,12 +39,10 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
enabled: function() {
|
||||
var Notifier = sdk.getComponent('organisms.Notifier');
|
||||
return Notifier.isEnabled();
|
||||
},
|
||||
|
||||
onClick: function() {
|
||||
var Notifier = sdk.getComponent('organisms.Notifier');
|
||||
var self = this;
|
||||
if (!Notifier.supportsDesktopNotifications()) {
|
||||
return;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue