);
}
diff --git a/skins/base/views/molecules/MessageComposer.js b/skins/base/views/molecules/MessageComposer.js
index 89c426cb..639c5bff 100644
--- a/skins/base/views/molecules/MessageComposer.js
+++ b/skins/base/views/molecules/MessageComposer.js
@@ -18,16 +18,46 @@ limitations under the License.
var React = require('react');
+var MatrixClientPeg = require("../../../../src/MatrixClientPeg");
var MessageComposerController = require("../../../../src/controllers/molecules/MessageComposer");
+var ContentMessages = require("../../../../src/ContentMessages");
module.exports = React.createClass({
displayName: 'MessageComposer',
mixins: [MessageComposerController],
+ onUploadClick: function(ev) {
+ this.refs.uploadInput.getDOMNode().click();
+ },
+
+ onUploadFileSelected: function(ev) {
+ var files = ev.target.files;
+ // MessageComposer shouldn't have to rely on it's parent passing in a callback to upload a file
+ if (files && files.length > 0) {
+ this.props.uploadFile(files[0]);
+ }
+ this.refs.uploadInput.getDOMNode().value = null;
+ },
+
render: function() {
+ var me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId);
+ var uploadInputStyle = {display: 'none'};
return (
);
},
diff --git a/skins/base/views/molecules/MessageTile.js b/skins/base/views/molecules/MessageTile.js
index 6808ecd9..572ee0d5 100644
--- a/skins/base/views/molecules/MessageTile.js
+++ b/skins/base/views/molecules/MessageTile.js
@@ -20,6 +20,7 @@ var React = require('react');
var classNames = require("classnames");
+var MatrixClientPeg = require("../../../../src/MatrixClientPeg");
var ComponentBroker = require('../../../../src/ComponentBroker');
var MessageTimestamp = ComponentBroker.get('atoms/MessageTimestamp');
@@ -52,12 +53,30 @@ module.exports = React.createClass({
mx_MessageTile: true,
mx_MessageTile_sending: this.props.mxEvent.status == 'sending',
mx_MessageTile_notSent: this.props.mxEvent.status == 'not_sent',
- mx_MessageTile_highlight: this.shouldHighlight()
+ mx_MessageTile_highlight: this.shouldHighlight(),
+ mx_MessageTile_continuation: this.props.continuation,
});
+ var timestamp = this.props.last ?
: null;
+ var avatar, sender, resend;
+ if (!this.props.continuation) {
+ avatar = (
+
;
+ }
+ if (this.props.mxEvent.status === "not_sent" && !this.state.resending) {
+ resend =
);
diff --git a/skins/base/views/molecules/RoomCreate.js b/skins/base/views/molecules/RoomCreate.js
new file mode 100644
index 00000000..9ad4f428
--- /dev/null
+++ b/skins/base/views/molecules/RoomCreate.js
@@ -0,0 +1,43 @@
+/*
+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 classNames = require('classnames');
+
+//var RoomCreateController = require("../../../../src/controllers/molecules/RoomCreateController");
+
+var MatrixClientPeg = require("../../../../src/MatrixClientPeg");
+
+module.exports = React.createClass({
+ displayName: 'RoomCreate',
+ // mixins: [RoomCreateController],
+ render: function() {
+ return (
+
+ );
+ }
+});
diff --git a/skins/base/views/molecules/RoomDropTarget.js b/skins/base/views/molecules/RoomDropTarget.js
new file mode 100644
index 00000000..0a076949
--- /dev/null
+++ b/skins/base/views/molecules/RoomDropTarget.js
@@ -0,0 +1,36 @@
+/*
+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 classNames = require('classnames');
+
+//var RoomDropTargetController = require("../../../../src/controllers/molecules/RoomDropTargetController");
+
+var MatrixClientPeg = require("../../../../src/MatrixClientPeg");
+
+module.exports = React.createClass({
+ displayName: 'RoomDropTarget',
+ // mixins: [RoomDropTargetController],
+ render: function() {
+ return (
+
+ );
+ }
+});
diff --git a/skins/base/views/molecules/RoomHeader.js b/skins/base/views/molecules/RoomHeader.js
index b5296f4e..7ad001d6 100644
--- a/skins/base/views/molecules/RoomHeader.js
+++ b/skins/base/views/molecules/RoomHeader.js
@@ -17,19 +17,112 @@ limitations under the License.
'use strict';
var React = require('react');
+var ComponentBroker = require('../../../../src/ComponentBroker');
+var MatrixClientPeg = require("../../../../src/MatrixClientPeg");
var RoomHeaderController = require("../../../../src/controllers/molecules/RoomHeader");
+var EditableText = ComponentBroker.get("atoms/EditableText");
module.exports = React.createClass({
displayName: 'RoomHeader',
mixins: [RoomHeaderController],
+ onNameChange: function(new_name) {
+ if (this.props.room.name != new_name && new_name) {
+ MatrixClientPeg.get().setRoomName(this.props.room.roomId, new_name);
+ }
+ },
+
+ getRoomName: function() {
+ return this.refs.name_edit.getDOMNode().value;
+ },
+
render: function() {
+
+ var header;
+ if (this.props.simpleHeader) {
+ header =
+
+ }
+ else {
+ var topic = this.props.room.currentState.getStateEvents('m.room.topic', '');
+
+ var callButtons;
+ if (this.state) {
+ switch (this.state.call_state) {
+ case "ringback":
+ case "connected":
+ callButtons = (
+
+ );
+ break;
+ }
+ }
+
+ var name = null;
+ var topic_el = null;
+ var save_button = null;
+ var settings_button = null;
+ var actual_name = this.props.room.currentState.getStateEvents('m.room.name', '');
+ if (actual_name) actual_name = actual_name.getContent().name;
+ if (this.props.editing) {
+ name =
- {this.props.room.name}
+ { header }
);
},
});
-
diff --git a/skins/base/views/molecules/RoomSettings.js b/skins/base/views/molecules/RoomSettings.js
new file mode 100644
index 00000000..36c1f4c2
--- /dev/null
+++ b/skins/base/views/molecules/RoomSettings.js
@@ -0,0 +1,213 @@
+/*
+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("../../../../src/MatrixClientPeg");
+
+var RoomSettingsController = require("../../../../src/controllers/molecules/RoomSettings");
+
+module.exports = React.createClass({
+ displayName: 'RoomSettings',
+ mixins: [RoomSettingsController],
+
+ getTopic: function() {
+ return this.refs.topic.getDOMNode().value;
+ },
+
+ getJoinRules: function() {
+ return this.refs.is_private.getDOMNode().checked ? "invite" : "public";
+ },
+
+ getHistoryVisibility: function() {
+ return this.refs.share_history.getDOMNode().checked ? "shared" : "invited";
+ },
+
+ getPowerLevels: function() {
+ if (!this.state.power_levels_changed) return undefined;
+
+ var power_levels = this.props.room.currentState.getStateEvents('m.room.power_levels', '');
+ power_levels = power_levels.getContent();
+
+ var new_power_levels = {
+ ban: parseInt(this.refs.ban.getDOMNode().value),
+ kick: parseInt(this.refs.kick.getDOMNode().value),
+ redact: parseInt(this.refs.redact.getDOMNode().value),
+ invite: parseInt(this.refs.invite.getDOMNode().value),
+ events_default: parseInt(this.refs.events_default.getDOMNode().value),
+ state_default: parseInt(this.refs.state_default.getDOMNode().value),
+ users_default: parseInt(this.refs.users_default.getDOMNode().value),
+ users: power_levels.users,
+ events: power_levels.events,
+ };
+
+ return new_power_levels;
+ },
+
+ onPowerLevelsChanged: function() {
+ this.setState({
+ power_levels_changed: true
+ });
+ },
+
+ render: function() {
+ var topic = this.props.room.currentState.getStateEvents('m.room.topic', '');
+ if (topic) topic = topic.getContent().topic;
+
+ var join_rule = this.props.room.currentState.getStateEvents('m.room.join_rules', '');
+ if (join_rule) join_rule = join_rule.getContent().join_rule;
+
+ var history_visibility = this.props.room.currentState.getStateEvents('m.room.history_visibility', '');
+ if (history_visibility) history_visibility = history_visibility.getContent().history_visibility;
+
+ var power_levels = this.props.room.currentState.getStateEvents('m.room.power_levels', '');
+
+ if (power_levels) {
+ power_levels = power_levels.getContent();
+
+ var ban_level = parseInt(power_levels.ban);
+ var kick_level = parseInt(power_levels.kick);
+ var redact_level = parseInt(power_levels.redact);
+ var invite_level = parseInt(power_levels.invite || 0);
+ var send_level = parseInt(power_levels.events_default || 0);
+ var state_level = parseInt(power_levels.state_default || 0);
+ var default_user_level = parseInt(power_levels.users_default || 0);
+
+ if (power_levels.ban == undefined) ban_level = 50;
+ if (power_levels.kick == undefined) kick_level = 50;
+ if (power_levels.redact == undefined) redact_level = 50;
+
+ var user_levels = power_levels.users || [];
+ var events_levels = power_levels.events || [];
+
+ var user_id = MatrixClientPeg.get().credentials.userId;
+
+ var current_user_level = user_levels[user_id];
+ if (current_user_level == undefined) current_user_level = default_user_level;
+
+ var power_level_level = events_levels["m.room.power_levels"];
+ if (power_level_level == undefined) {
+ power_level_level = state_level;
+ }
+
+ var can_change_levels = current_user_level >= power_level_level;
+ } else {
+ var ban_level = 50;
+ var kick_level = 50;
+ var redact_level = 50;
+ var invite_level = 0;
+ var send_level = 0;
+ var state_level = 0;
+ var default_user_level = 0;
+
+ var user_levels = [];
+ var events_levels = [];
+
+ var current_user_level = 0;
+
+ var power_level_level = 0;
+
+ var can_change_levels = false;
+ }
+
+ var banned = this.props.room.getMembersWithMemership("ban");
+
+ return (
+
+ );
+ }
+});
diff --git a/skins/base/views/molecules/RoomTile.js b/skins/base/views/molecules/RoomTile.js
index 0e80fc20..6b80fc8a 100644
--- a/skins/base/views/molecules/RoomTile.js
+++ b/skins/base/views/molecules/RoomTile.js
@@ -30,14 +30,35 @@ module.exports = React.createClass({
var myUserId = MatrixClientPeg.get().credentials.userId;
var classes = classNames({
'mx_RoomTile': true,
- 'selected': this.props.selected,
- 'unread': this.props.unread,
- 'highlight': this.props.highlight,
- 'invited': this.props.room.currentState.members[myUserId].membership == 'invite'
+ 'mx_RoomTile_selected': this.props.selected,
+ 'mx_RoomTile_unread': this.props.unread,
+ 'mx_RoomTile_highlight': this.props.highlight,
+ 'mx_RoomTile_invited': this.props.room.currentState.members[myUserId].membership == 'invite'
});
+ var name = this.props.room.name.replace(":", ":\u200b");
+ var badge;
+ if (this.props.highlight) {
+ badge =
;
+ }
+ /*
+ if (this.props.highlight) {
+ badge =
;
+ }
+ var nameCell;
+ if (badge) {
+ nameCell =
);
}
diff --git a/skins/base/views/molecules/ServerConfig.js b/skins/base/views/molecules/ServerConfig.js
index e06536c5..c533cf94 100644
--- a/skins/base/views/molecules/ServerConfig.js
+++ b/skins/base/views/molecules/ServerConfig.js
@@ -26,17 +26,12 @@ module.exports = React.createClass({
render: function() {
return (
-
-
+
);
}
diff --git a/skins/base/views/molecules/UnknownMessageTile.js b/skins/base/views/molecules/UnknownMessageTile.js
index 27e801c9..b965a4a1 100644
--- a/skins/base/views/molecules/UnknownMessageTile.js
+++ b/skins/base/views/molecules/UnknownMessageTile.js
@@ -25,9 +25,10 @@ module.exports = React.createClass({
mixins: [UnknownMessageTileController],
render: function() {
+ var content = this.props.mxEvent.getContent();
return (
- ?
+ {content.body}
);
},
diff --git a/skins/base/views/molecules/UserSelector.js b/skins/base/views/molecules/UserSelector.js
index 7517e29d..8ec00866 100644
--- a/skins/base/views/molecules/UserSelector.js
+++ b/skins/base/views/molecules/UserSelector.js
@@ -26,17 +26,19 @@ module.exports = React.createClass({
onAddUserId: function() {
this.addUser(this.refs.user_id_input.getDOMNode().value);
+ this.refs.user_id_input.getDOMNode().value = "";
},
render: function() {
+ var self = this;
return (
);
diff --git a/skins/base/views/molecules/voip/CallView.js b/skins/base/views/molecules/voip/CallView.js
new file mode 100644
index 00000000..3642e6b5
--- /dev/null
+++ b/skins/base/views/molecules/voip/CallView.js
@@ -0,0 +1,41 @@
+/*
+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("../../../../../src/MatrixClientPeg");
+var ComponentBroker = require('../../../../../src/ComponentBroker');
+var CallViewController = require(
+ "../../../../../src/controllers/molecules/voip/CallView"
+);
+var VideoView = ComponentBroker.get('molecules/voip/VideoView');
+
+module.exports = React.createClass({
+ displayName: 'CallView',
+ mixins: [CallViewController],
+
+ getVideoView: function() {
+ return this.refs.video;
+ },
+
+ render: function(){
+ return (
+
+ );
+ }
+});
\ No newline at end of file
diff --git a/skins/base/views/molecules/voip/IncomingCallBox.js b/skins/base/views/molecules/voip/IncomingCallBox.js
new file mode 100644
index 00000000..5becedb1
--- /dev/null
+++ b/skins/base/views/molecules/voip/IncomingCallBox.js
@@ -0,0 +1,70 @@
+/*
+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("../../../../../src/MatrixClientPeg");
+var IncomingCallBoxController = require(
+ "../../../../../src/controllers/molecules/voip/IncomingCallBox"
+);
+
+module.exports = React.createClass({
+ displayName: 'IncomingCallBox',
+ mixins: [IncomingCallBoxController],
+
+ getRingAudio: function() {
+ return this.refs.ringAudio.getDOMNode();
+ },
+
+ render: function() {
+ if (!this.state.incomingCall || !this.state.incomingCall.roomId) {
+ return (
+
+ );
+ }
+ var caller = MatrixClientPeg.get().getRoom(this.state.incomingCall.roomId).name;
+ return (
+
+
+
+
+ Incoming { this.state.incomingCall ? this.state.incomingCall.type : '' } call from { caller }
+
+
+
+ );
+ }
+});
diff --git a/skins/base/views/molecules/voip/MCallAnswerTile.js b/skins/base/views/molecules/voip/MCallAnswerTile.js
new file mode 100644
index 00000000..b7ea3cad
--- /dev/null
+++ b/skins/base/views/molecules/voip/MCallAnswerTile.js
@@ -0,0 +1,50 @@
+/*
+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("../../../../../src/MatrixClientPeg");
+var ComponentBroker = require('../../../../../src/ComponentBroker');
+var MCallAnswerTileController = require("../../../../../src/controllers/molecules/voip/MCallAnswerTile");
+var MessageTimestamp = ComponentBroker.get('atoms/MessageTimestamp');
+
+module.exports = React.createClass({
+ displayName: 'MCallAnswerTile',
+ mixins: [MCallAnswerTileController],
+
+ getAnswerText: function(event) {
+ var senderName = event.sender ? event.sender.name : "Someone";
+ return senderName + " answered the call.";
+ },
+
+ render: function() {
+ // XXX: for now, just cheekily borrow the css from message tile...
+ return (
+
+
+
+
+
+
+
+ {this.getAnswerText(this.props.mxEvent)}
+
+
+ );
+ },
+});
+
diff --git a/skins/base/views/molecules/voip/MCallHangupTile.js b/skins/base/views/molecules/voip/MCallHangupTile.js
new file mode 100644
index 00000000..261bd8d1
--- /dev/null
+++ b/skins/base/views/molecules/voip/MCallHangupTile.js
@@ -0,0 +1,50 @@
+/*
+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("../../../../../src/MatrixClientPeg");
+var ComponentBroker = require('../../../../../src/ComponentBroker');
+var MCallHangupTileController = require("../../../../../src/controllers/molecules/voip/MCallHangupTile");
+var MessageTimestamp = ComponentBroker.get('atoms/MessageTimestamp');
+
+module.exports = React.createClass({
+ displayName: 'MCallHangupTile',
+ mixins: [MCallHangupTileController],
+
+ getHangupText: function(event) {
+ var senderName = event.sender ? event.sender.name : "Someone";
+ return senderName + " ended the call.";
+ },
+
+ render: function() {
+ // XXX: for now, just cheekily borrow the css from message tile...
+ return (
+
+
+
+
+
+
+
+ {this.getHangupText(this.props.mxEvent)}
+
+
+ );
+ },
+});
+
diff --git a/skins/base/views/molecules/voip/MCallInviteTile.js b/skins/base/views/molecules/voip/MCallInviteTile.js
new file mode 100644
index 00000000..4e8f6baf
--- /dev/null
+++ b/skins/base/views/molecules/voip/MCallInviteTile.js
@@ -0,0 +1,56 @@
+/*
+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("../../../../../src/MatrixClientPeg");
+var ComponentBroker = require('../../../../../src/ComponentBroker');
+var MCallInviteTileController = require("../../../../../src/controllers/molecules/voip/MCallInviteTile");
+var MessageTimestamp = ComponentBroker.get('atoms/MessageTimestamp');
+
+module.exports = React.createClass({
+ displayName: 'MCallInviteTile',
+ mixins: [MCallInviteTileController],
+
+ getInviteText: function(event) {
+ var senderName = event.sender ? event.sender.name : "Someone";
+ // FIXME: Find a better way to determine this from the event?
+ var type = "voice";
+ if (event.getContent().offer &&
+ event.getContent().offer.sdp.indexOf('m=video') !== -1) {
+ type = "video";
+ }
+ return senderName + " placed a " + type + " call.";
+ },
+
+ render: function() {
+ // XXX: for now, just cheekily borrow the css from message tile...
+ return (
+
+
+
+
+
+
+
+ {this.getInviteText(this.props.mxEvent)}
+
+
+ );
+ },
+});
+
diff --git a/skins/base/views/molecules/voip/VideoView.js b/skins/base/views/molecules/voip/VideoView.js
new file mode 100644
index 00000000..19ad17a7
--- /dev/null
+++ b/skins/base/views/molecules/voip/VideoView.js
@@ -0,0 +1,50 @@
+/*
+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("../../../../../src/MatrixClientPeg");
+var ComponentBroker = require('../../../../../src/ComponentBroker');
+var VideoViewController = require("../../../../../src/controllers/molecules/voip/VideoView");
+var VideoFeed = ComponentBroker.get('atoms/voip/VideoFeed');
+
+module.exports = React.createClass({
+ displayName: 'VideoView',
+ mixins: [VideoViewController],
+
+ getRemoteVideoElement: function() {
+ return this.refs.remote.getDOMNode();
+ },
+
+ getLocalVideoElement: function() {
+ return this.refs.local.getDOMNode();
+ },
+
+ render: function() {
+ return (
+
+ );
+ }
+});
\ No newline at end of file
diff --git a/skins/base/views/organisms/CreateRoom.js b/skins/base/views/organisms/CreateRoom.js
index 36f6e466..21769621 100644
--- a/skins/base/views/organisms/CreateRoom.js
+++ b/skins/base/views/organisms/CreateRoom.js
@@ -22,11 +22,15 @@ var CreateRoomController = require("../../../../src/controllers/organisms/Create
var ComponentBroker = require('../../../../src/ComponentBroker');
+var PresetValues = require('../../../../src/controllers/atoms/create_room/Presets').Presets;
+
var CreateRoomButton = ComponentBroker.get("atoms/create_room/CreateRoomButton");
-var RoomNameTextbox = ComponentBroker.get("atoms/create_room/RoomNameTextbox");
+var RoomAlias = ComponentBroker.get("atoms/create_room/RoomAlias");
var Presets = ComponentBroker.get("atoms/create_room/Presets");
var UserSelector = ComponentBroker.get("molecules/UserSelector");
+var Loader = require("react-loader");
+
module.exports = React.createClass({
displayName: 'CreateRoom',
@@ -40,15 +44,91 @@ module.exports = React.createClass({
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) {
return (
-
Creating...
+
);
} else {
var error_box = "";
@@ -61,10 +141,15 @@ module.exports = React.createClass({
}
return (
);
diff --git a/skins/base/views/organisms/ErrorDialog.js b/skins/base/views/organisms/ErrorDialog.js
new file mode 100644
index 00000000..68d597cb
--- /dev/null
+++ b/skins/base/views/organisms/ErrorDialog.js
@@ -0,0 +1,54 @@
+/*
+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';
+
+/*
+ * 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');
+var ErrorDialogController = require("../../../../src/controllers/organisms/ErrorDialog");
+
+module.exports = React.createClass({
+ displayName: 'ErrorDialog',
+ mixins: [ErrorDialogController],
+
+ render: function() {
+ return (
+
+
+ {this.props.title}
+
+
+ {this.props.description}
+
+
+
+
+
+ );
+ }
+});
diff --git a/skins/base/views/organisms/LeftPanel.js b/skins/base/views/organisms/LeftPanel.js
new file mode 100644
index 00000000..16575910
--- /dev/null
+++ b/skins/base/views/organisms/LeftPanel.js
@@ -0,0 +1,41 @@
+/*
+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 ComponentBroker = require('../../../../src/ComponentBroker');
+
+var RoomList = ComponentBroker.get('organisms/RoomList');
+var BottomLeftMenu = ComponentBroker.get('molecules/BottomLeftMenu');
+var IncomingCallBox = ComponentBroker.get('molecules/voip/IncomingCallBox');
+var RoomCreate = ComponentBroker.get('molecules/RoomCreate');
+
+module.exports = React.createClass({
+ displayName: 'LeftPanel',
+
+ render: function() {
+ return (
+
+
+
+
+
+
+ );
+ }
+});
+
diff --git a/skins/base/views/organisms/LogoutPrompt.js b/skins/base/views/organisms/LogoutPrompt.js
new file mode 100644
index 00000000..10ed07ed
--- /dev/null
+++ b/skins/base/views/organisms/LogoutPrompt.js
@@ -0,0 +1,41 @@
+/*
+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 LogoutPromptController = require("../../../../src/controllers/organisms/LogoutPrompt");
+
+module.exports = React.createClass({
+ displayName: 'LogoutPrompt',
+ mixins: [LogoutPromptController],
+
+ render: function() {
+ return (
+
+
+ Sign out?
+
+
+
+
+
+
+ );
+ },
+});
+
diff --git a/skins/base/views/organisms/MemberList.js b/skins/base/views/organisms/MemberList.js
index 5d1b2fd0..dfeecb03 100644
--- a/skins/base/views/organisms/MemberList.js
+++ b/skins/base/views/organisms/MemberList.js
@@ -17,38 +17,101 @@ limitations under the License.
'use strict';
var React = require('react');
+var classNames = require('classnames');
var MemberListController = require("../../../../src/controllers/organisms/MemberList");
var ComponentBroker = require('../../../../src/ComponentBroker');
var MemberTile = ComponentBroker.get("molecules/MemberTile");
+var EditableText = ComponentBroker.get("atoms/EditableText");
module.exports = React.createClass({
displayName: 'MemberList',
mixins: [MemberListController],
+ getInitialState: function() {
+ return { editing: false };
+ },
+
+ // FIXME: combine this more nicely with the MemberInfo positioning stuff...
+ onMemberListScroll: function(ev) {
+ if (this.refs.memberListScroll) {
+ var memberListScroll = this.refs.memberListScroll.getDOMNode();
+ // offset the current MemberInfo bubble
+ var memberInfo = document.getElementsByClassName("mx_MemberInfo")[0];
+ if (memberInfo) {
+ memberInfo.style.top = (memberInfo.parentElement.offsetTop - memberListScroll.scrollTop) + "px";
+ }
+ }
+ },
+
makeMemberTiles: function() {
- var that = this;
- return Object.keys(that.state.memberDict).map(function(userId) {
- var m = that.state.memberDict[userId];
+ var self = this;
+ return Object.keys(self.state.memberDict).map(function(userId) {
+ var m = self.state.memberDict[userId];
return (
-
-
-
+
);
});
},
+ onPopulateInvite: function(inputText, shouldSubmit) {
+ // reset back to placeholder
+ this.refs.invite.setValue("Invite", false, true);
+ this.setState({ editing: false });
+ if (!shouldSubmit) {
+ return; // enter key wasn't pressed
+ }
+ this.onInvite(inputText);
+ },
+
+ onClickInvite: function(ev) {
+ this.setState({ editing: true });
+ this.refs.invite.onClickDiv();
+ console.log("forcing update on memberlist after having clicked invite");
+ ev.stopPropagation();
+ ev.preventDefault();
+ },
+
+ inviteTile: function() {
+ // if (this.state.inviting) {
+ // return (
+ //
+ // );
+ // }
+
+ var classes = classNames({
+ mx_MemberTile: true,
+ mx_MemberTile_inviteEditing: this.state.editing,
+ });
+
+ console.log("rendering inviteTile, with phase as " + (this.refs.invite ? this.refs.invite.state.phase : "unknown"));
+
+ return (
+
+ );
+ },
+
render: function() {
return (
-
- {this.makeMemberTiles()}
-
+
+
+
+
+
Members
+
+ {this.makeMemberTiles()}
+ {this.inviteTile()}
+
+
);
}
diff --git a/skins/base/views/organisms/Notifier.js b/skins/base/views/organisms/Notifier.js
index 09f1921a..7df76e29 100644
--- a/skins/base/views/organisms/Notifier.js
+++ b/skins/base/views/organisms/Notifier.js
@@ -19,32 +19,14 @@ limitations under the License.
var NotifierController = require("../../../../src/controllers/organisms/Notifier");
var MatrixClientPeg = require("../../../../src/MatrixClientPeg");
+var TextForEvent = require("../../../../src/TextForEvent");
var extend = require("../../../../src/extend");
var dis = require("../../../../src/dispatcher");
var NotifierView = {
notificationMessageForEvent: function(ev) {
- var senderDisplayName = ev.sender ? ev.sender.name : '';
- var message = null;
-
- if (ev.event.type === "m.room.message") {
- message = ev.getContent().body;
- if (ev.getContent().msgtype === "m.emote") {
- message = "* " + senderDisplayName + " " + message;
- } else if (ev.getContent().msgtype === "m.image") {
- message = senderDisplayName + " sent an image.";
- }
- } else if (ev.event.type == "m.room.member") {
- if (ev.event.state_key !== MatrixClientPeg.get().credentials.userId && "join" === ev.getContent().membership) {
- // Notify when another user joins
- message = senderDisplayName + " joined";
- } else if (ev.event.state_key === MatrixClientPeg.get().credentials.userId && "invite" === ev.getContent().membership) {
- // notify when you are invited
- message = senderDisplayName + " invited you to a room";
- }
- }
- return message;
+ return TextForEvent.textForEvent(ev);
},
displayNotification: function(ev, room) {
@@ -61,8 +43,18 @@ var NotifierView = {
var title;
if (!ev.sender || room.name == ev.sender.name) {
title = room.name;
+ // notificationMessageForEvent includes sender,
+ // but we already have the sender here
+ if (ev.getContent().body) msg = ev.getContent().body;
+ } else if (ev.getType() == 'm.room.member') {
+ // context is all in the message here, we don't need
+ // to display sender info
+ title = room.name;
} else if (ev.sender) {
title = ev.sender.name + " (" + room.name + ")";
+ // notificationMessageForEvent includes sender,
+ // but we've just out sender in the title
+ if (ev.getContent().body) msg = ev.getContent().body;
}
var notification = new global.Notification(
diff --git a/skins/base/views/organisms/RightPanel.js b/skins/base/views/organisms/RightPanel.js
new file mode 100644
index 00000000..e5ca89c9
--- /dev/null
+++ b/skins/base/views/organisms/RightPanel.js
@@ -0,0 +1,78 @@
+/*
+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 ComponentBroker = require('../../../../src/ComponentBroker');
+
+var MemberList = ComponentBroker.get('organisms/MemberList');
+
+module.exports = React.createClass({
+ displayName: 'RightPanel',
+
+ Phase : {
+ Blank: 'Blank',
+ None: 'None',
+ MemberList: 'MemberList',
+ FileList: 'FileList',
+ },
+
+ getInitialState: function() {
+ return {
+ phase : this.Phase.MemberList
+ }
+ },
+
+ onMemberListButtonClick: function() {
+ if (this.state.phase == this.Phase.None) {
+ this.setState({ phase: this.Phase.MemberList });
+ }
+ else {
+ this.setState({ phase: this.Phase.None });
+ }
+ },
+
+ render: function() {
+ var buttonGroup;
+ var panel;
+ if (this.props.roomId) {
+ buttonGroup =
+
+
+
+
+
+
+
+
;
+
+ if (this.state.phase == this.Phase.MemberList) {
+ panel =
+ }
+ }
+
+ return (
+
+
+ { buttonGroup }
+
+ { panel }
+
+ );
+ }
+});
+
diff --git a/skins/base/views/organisms/RoomDirectory.js b/skins/base/views/organisms/RoomDirectory.js
new file mode 100644
index 00000000..eb5ea3de
--- /dev/null
+++ b/skins/base/views/organisms/RoomDirectory.js
@@ -0,0 +1,130 @@
+/*
+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("../../../../src/MatrixClientPeg");
+var Modal = require("../../../../src/Modal");
+var ComponentBroker = require('../../../../src/ComponentBroker');
+var ErrorDialog = ComponentBroker.get("organisms/ErrorDialog");
+var RoomHeader = ComponentBroker.get('molecules/RoomHeader');
+var dis = require("../../../../src/dispatcher");
+
+
+module.exports = React.createClass({
+ displayName: 'RoomDirectory',
+
+ getInitialState: function() {
+ return {
+ publicRooms: [],
+ roomAlias: '',
+ }
+ },
+
+ componentDidMount: function() {
+ var self = this;
+ MatrixClientPeg.get().publicRooms(function (err, data) {
+ if (err) {
+ console.error("Failed to get publicRooms: %s", JSON.stringify(err));
+ Modal.createDialog(ErrorDialog, {
+ title: "Failed to get public room list",
+ description: err.message
+ });
+ }
+ else {
+ self.setState({
+ publicRooms: data.chunk
+ });
+ self.forceUpdate();
+ }
+ });
+ },
+
+ joinRoom: function(roomId) {
+ // XXX: check that JS SDK suppresses duplicate attempts to join the same room
+ MatrixClientPeg.get().joinRoom(roomId).done(function() {
+ dis.dispatch({
+ action: 'view_room',
+ room_id: roomId
+ });
+ }, function(err) {
+ console.error("Failed to join room: %s", JSON.stringify(err));
+ Modal.createDialog(ErrorDialog, {
+ title: "Failed to join room",
+ description: err.message
+ });
+ });
+ },
+
+ getRows: function(filter) {
+ if (!this.state.publicRooms) return [];
+
+ var rooms = this.state.publicRooms.filter(function(a) {
+ // FIXME: if incrementally typing, keep narrowing down the search set
+ return (a.aliases[0].search(filter) >= 0);
+ }).sort(function(a,b) {
+ return a.num_joined_members > b.num_joined_members;
+ });
+ var rows = [];
+ var self = this;
+ for (var i = 0; i < rooms.length; i++) {
+ var name = rooms[i].name;
+ if (!name) {
+ if (rooms[i].aliases[0]) name = rooms[i].aliases[0]
+ }
+ else {
+ if (rooms[i].aliases[0]) name += " (" + rooms[i].aliases[0] + ")";
+ }
+ rows.unshift(
+
+ { name } |
+ { rooms[i].topic } |
+ { rooms[i].num_joined_members } |
+
+ );
+ }
+ return rows;
+ },
+
+ onKeyUp: function(ev) {
+ this.forceUpdate();
+ this.setState({ roomAlias : this.refs.roomAlias.getDOMNode().value })
+ if (ev.key == "Enter") {
+ this.joinRoom(this.refs.roomAlias.getDOMNode().value);
+ }
+ if (ev.key == "Down") {
+
+ }
+ },
+
+ render: function() {
+ return (
+
+
+
+
+
+ Room | Topic | Users |
+ { this.getRows(this.state.roomAlias) }
+
+
+
+ );
+ }
+});
+
diff --git a/skins/base/views/organisms/RoomList.js b/skins/base/views/organisms/RoomList.js
index f8be66f7..7b3ab075 100644
--- a/skins/base/views/organisms/RoomList.js
+++ b/skins/base/views/organisms/RoomList.js
@@ -17,10 +17,12 @@ limitations under the License.
'use strict';
var React = require('react');
+var ComponentBroker = require('../../../../src/ComponentBroker');
+
+var RoomDropTarget = ComponentBroker.get('molecules/RoomDropTarget');
var RoomListController = require("../../../../src/controllers/organisms/RoomList");
-
module.exports = React.createClass({
displayName: 'RoomList',
mixins: [RoomListController],
@@ -28,7 +30,16 @@ module.exports = React.createClass({
render: function() {
return (
- {this.makeRoomTiles()}
+
Favourites
+
+
+
Recents
+
+ {this.makeRoomTiles()}
+
+
+
Archive
+
);
}
diff --git a/skins/base/views/organisms/RoomView.js b/skins/base/views/organisms/RoomView.js
index 8de03a84..25841b91 100644
--- a/skins/base/views/organisms/RoomView.js
+++ b/skins/base/views/organisms/RoomView.js
@@ -22,12 +22,14 @@ var MatrixClientPeg = require("../../../../src/MatrixClientPeg");
var ComponentBroker = require('../../../../src/ComponentBroker');
var classNames = require("classnames");
+var filesize = require('filesize');
+var q = require('q');
var MessageTile = ComponentBroker.get('molecules/MessageTile');
var RoomHeader = ComponentBroker.get('molecules/RoomHeader');
-var MemberList = ComponentBroker.get('organisms/MemberList');
var MessageComposer = ComponentBroker.get('molecules/MessageComposer');
-
+var CallView = ComponentBroker.get("molecules/voip/CallView");
+var RoomSettings = ComponentBroker.get("molecules/RoomSettings");
var RoomViewController = require("../../../../src/controllers/organisms/RoomView");
var Loader = require("react-loader");
@@ -37,6 +39,31 @@ module.exports = React.createClass({
displayName: 'RoomView',
mixins: [RoomViewController],
+ onSettingsClick: function() {
+ this.setState({editingRoomSettings: true});
+ },
+
+ onSaveClick: function() {
+ this.setState({
+ editingRoomSettings: false,
+ uploadingRoomSettings: true,
+ });
+
+ var new_name = this.refs.header.getRoomName();
+ var new_topic = this.refs.room_settings.getTopic();
+ var new_join_rule = this.refs.room_settings.getJoinRules();
+ var new_history_visibility = this.refs.room_settings.getHistoryVisibility();
+ var new_power_levels = this.refs.room_settings.getPowerLevels();
+
+ this.uploadNewState(
+ new_name,
+ new_topic,
+ new_join_rule,
+ new_history_visibility,
+ new_power_levels
+ );
+ },
+
render: function() {
if (!this.state.room) {
return (
@@ -71,25 +98,73 @@ module.exports = React.createClass({
mx_RoomView_scrollheader: true,
loading: this.state.paginating
});
+
+ var statusBar = (
+
+ );
+
+ if (this.state.upload) {
+ var innerProgressStyle = {
+ width: ((this.state.upload.uploadedBytes / this.state.upload.totalBytes) * 100) + '%'
+ };
+ statusBar = (
+
+
Uploading {this.state.upload.fileName}
+
+ {filesize(this.state.upload.uploadedBytes)} / {filesize(this.state.upload.totalBytes)}
+
+
+
+ );
+ } else {
+ var typingString = this.getWhoIsTypingString();
+ if (typingString) {
+ statusBar = (
+
+
+ {typingString}
+
+ );
+ }
+ }
+
+ var roomEdit = null;
+
+ if (this.state.editingRoomSettings) {
+ roomEdit =
;
+ }
+
+ if (this.state.uploadingRoomSettings) {
+ roomEdit =
;
+ }
+
return (
-
-
-
-
-
-
-
- {this.getEventTiles()}
-
-
-
-
-
+
+
+
+ { roomEdit }
+
+
+
+
+
+ {this.getEventTiles()}
+
+
+
+
+
);
}
},
});
-
diff --git a/skins/base/views/organisms/UserSettings.js b/skins/base/views/organisms/UserSettings.js
new file mode 100644
index 00000000..58a82487
--- /dev/null
+++ b/skins/base/views/organisms/UserSettings.js
@@ -0,0 +1,113 @@
+/*
+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 ComponentBroker = require('../../../../src/ComponentBroker');
+var MatrixClientPeg = require("../../../../src/MatrixClientPeg");
+
+var UserSettingsController = require("../../../../src/controllers/organisms/UserSettings");
+
+var EditableText = ComponentBroker.get('atoms/EditableText');
+var EnableNotificationsButton = ComponentBroker.get('atoms/EnableNotificationsButton');
+var ChangeAvatar = ComponentBroker.get('molecules/ChangeAvatar');
+var ChangePassword = ComponentBroker.get('molecules/ChangePassword');
+var LogoutPrompt = ComponentBroker.get('organisms/LogoutPrompt');
+var Loader = require("react-loader");
+
+var Modal = require("../../../../src/Modal");
+
+module.exports = React.createClass({
+ displayName: 'UserSettings',
+ mixins: [UserSettingsController],
+
+ editAvatar: function() {
+ var url = MatrixClientPeg.get().mxcUrlToHttp(this.state.avatarUrl);
+ Modal.createDialog(ChangeAvatar, {initialAvatarUrl: url});
+ },
+
+ addEmail: function() {
+
+ },
+
+ editDisplayName: function() {
+ this.refs.displayname.edit();
+ },
+
+ changePassword: function() {
+ Modal.createDialog(ChangePassword);
+ },
+
+ onLogoutClicked: function(ev) {
+ this.logoutModal = Modal.createDialog(LogoutPrompt, {onCancel: this.onLogoutPromptCancel});
+ },
+
+ onLogoutPromptCancel: function() {
+ this.logoutModal.closeDialog();
+ },
+
+ render: function() {
+ switch (this.state.phase) {
+ case this.Phases.Loading:
+ return
+ case this.Phases.Display:
+ return (
+
+
+
User Settings
+
+
+
+
+
+
+
+ {this.state.threepids.map(function(val) {
+ return
{val.address}
;
+ })}
+
+
+
Add email
+
+
+
+
+
Global Settings
+
+
+
+ Change Password
+
+
+ Version {this.state.clientVersion}
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+ }
+});
diff --git a/skins/base/views/pages/MatrixChat.js b/skins/base/views/pages/MatrixChat.js
index 0231af44..eb2f2a52 100644
--- a/skins/base/views/pages/MatrixChat.js
+++ b/skins/base/views/pages/MatrixChat.js
@@ -19,35 +19,87 @@ limitations under the License.
var React = require('react');
var ComponentBroker = require('../../../../src/ComponentBroker');
-var RoomList = ComponentBroker.get('organisms/RoomList');
+var LeftPanel = ComponentBroker.get('organisms/LeftPanel');
var RoomView = ComponentBroker.get('organisms/RoomView');
-var MatrixToolbar = ComponentBroker.get('molecules/MatrixToolbar');
+var RightPanel = ComponentBroker.get('organisms/RightPanel');
var Login = ComponentBroker.get('templates/Login');
+var UserSettings = ComponentBroker.get('organisms/UserSettings');
var Register = ComponentBroker.get('templates/Register');
+var CreateRoom = ComponentBroker.get('organisms/CreateRoom');
+var RoomDirectory = ComponentBroker.get('organisms/RoomDirectory');
+var MatrixToolbar = ComponentBroker.get('molecules/MatrixToolbar');
+var Notifier = ComponentBroker.get('organisms/Notifier');
var MatrixChatController = require("../../../../src/controllers/pages/MatrixChat");
// should be atomised
var Loader = require("react-loader");
+var classNames = require("classnames");
+
+var dis = require("../../../../src/dispatcher");
module.exports = React.createClass({
displayName: 'MatrixChat',
mixins: [MatrixChatController],
+ onRoomCreated: function(room_id) {
+ dis.dispatch({
+ action: "view_room",
+ room_id: room_id,
+ });
+ },
+
render: function() {
if (this.state.logged_in && this.state.ready) {
- return (
-
-
-
-
+
+ var page_element;
+ var right_panel = "";
+
+ switch (this.state.page_type) {
+ case this.PageTypes.RoomView:
+ page_element =
+ right_panel =
+ break;
+ case this.PageTypes.UserSettings:
+ page_element =
+ right_panel =
+ break;
+ case this.PageTypes.CreateRoom:
+ page_element =
+ right_panel =
+ break;
+ case this.PageTypes.RoomDirectory:
+ page_element =
+ right_panel =
+ break;
+ }
+
+ if (!Notifier.isEnabled()) {
+ return (
+
+
+
+
+ {page_element}
+
+ {right_panel}
+
-
-
-
- );
+ );
+ }
+ else {
+ return (
+
+
+
+ {page_element}
+
+ {right_panel}
+
+ );
+ }
} else if (this.state.logged_in) {
return (
@@ -67,4 +119,3 @@ module.exports = React.createClass({
}
}
});
-
diff --git a/skins/base/views/templates/Login.js b/skins/base/views/templates/Login.js
index f71e3070..08cd060b 100644
--- a/skins/base/views/templates/Login.js
+++ b/skins/base/views/templates/Login.js
@@ -19,6 +19,7 @@ limitations under the License.
var React = require('react');
var ComponentBroker = require("../../../../src/ComponentBroker");
+var MatrixClientPeg = require("../../../../src/MatrixClientPeg");
var ProgressBar = ComponentBroker.get("molecules/ProgressBar");
var Loader = require("react-loader");
@@ -28,15 +29,44 @@ var LoginController = require("../../../../src/controllers/templates/Login");
var ServerConfig = ComponentBroker.get("molecules/ServerConfig");
module.exports = React.createClass({
+ DEFAULT_HS_URL: 'https://matrix.org',
+ DEFAULT_IS_URL: 'https://matrix.org',
+
displayName: 'Login',
mixins: [LoginController],
+ getInitialState: function() {
+ return {
+ serverConfigVisible: false
+ };
+ },
+
+ componentWillMount: function() {
+ this.onHSChosen();
+ this.customHsUrl = this.DEFAULT_HS_URL;
+ this.customIsUrl = this.DEFAULT_IS_URL;
+ },
+
getHsUrl: function() {
- return this.refs.serverConfig.getHsUrl();
+ if (this.state.serverConfigVisible) {
+ return this.customHsUrl;
+ } else {
+ return this.DEFAULT_HS_URL;
+ }
},
getIsUrl: function() {
- return this.refs.serverConfig.getIsUrl();
+ if (this.state.serverConfigVisible) {
+ return this.customIsUrl;
+ } else {
+ return this.DEFAULT_IS_URL;
+ }
+ },
+
+ onServerConfigVisibleChange: function(ev) {
+ this.setState({
+ serverConfigVisible: ev.target.checked
+ }, this.onHsUrlChanged);
},
/**
@@ -49,15 +79,44 @@ module.exports = React.createClass({
};
},
+ onHsUrlChanged: function() {
+ this.customHsUrl = this.refs.serverConfig.getHsUrl();
+ this.customIsUrl = this.refs.serverConfig.getIsUrl();
+ MatrixClientPeg.replaceUsingUrls(
+ this.getHsUrl(),
+ this.getIsUrl()
+ );
+ this.setState({
+ hs_url: this.getHsUrl(),
+ is_url: this.getIsUrl()
+ });
+ // XXX: HSes do not have to offer password auth, so we
+ // need to update and maybe show a different component
+ // when a new HS is entered.
+ /*if (this.updateHsTimeout) {
+ clearTimeout(this.updateHsTimeout);
+ }
+ var self = this;
+ this.updateHsTimeout = setTimeout(function() {
+ self.onHSChosen();
+ }, 500);*/
+ },
+
componentForStep: function(step) {
switch (step) {
case 'choose_hs':
+ var serverConfigStyle = {};
+ serverConfigStyle.display = this.state.serverConfigVisible ? 'block' : 'none';
return (
-
+
+
+
+
+
);
// XXX: clearly these should be separate organisms
@@ -65,15 +124,24 @@ module.exports = React.createClass({
return (
);
}
},
+ onUsernameChanged: function(ev) {
+ this.setState({username: ev.target.value});
+ },
+
+ onPasswordChanged: function(ev) {
+ this.setState({password: ev.target.value});
+ },
+
loginContent: function() {
if (this.state.busy) {
return (
@@ -82,10 +150,10 @@ module.exports = React.createClass({
} else {
return (
);
}
@@ -94,8 +162,12 @@ module.exports = React.createClass({
render: function() {
return (
-
- {this.loginContent()}
+
+
+
+
+ {this.loginContent()}
+
);
}
diff --git a/skins/base/views/templates/Register.js b/skins/base/views/templates/Register.js
index 94f3b969..784db4cf 100644
--- a/skins/base/views/templates/Register.js
+++ b/skins/base/views/templates/Register.js
@@ -27,9 +27,23 @@ var RegisterController = require("../../../../src/controllers/templates/Register
var ServerConfig = ComponentBroker.get("molecules/ServerConfig");
module.exports = React.createClass({
+ DEFAULT_HS_URL: 'https://matrix.org',
+ DEFAULT_IS_URL: 'https://matrix.org',
+
displayName: 'Register',
mixins: [RegisterController],
+ getInitialState: function() {
+ return {
+ serverConfigVisible: false
+ };
+ },
+
+ componentWillMount: function() {
+ this.customHsUrl = this.DEFAULT_HS_URL;
+ this.customIsUrl = this.DEFAULT_IS_URL;
+ },
+
getRegFormVals: function() {
return {
email: this.refs.email.getDOMNode().value,
@@ -40,25 +54,67 @@ module.exports = React.createClass({
},
getHsUrl: function() {
- return this.refs.serverConfig.getHsUrl();
+ if (this.state.serverConfigVisible) {
+ return this.customHsUrl;
+ } else {
+ return this.DEFAULT_HS_URL;
+ }
},
getIsUrl: function() {
- return this.refs.serverConfig.getIsUrl();
+ if (this.state.serverConfigVisible) {
+ return this.customIsUrl;
+ } else {
+ return this.DEFAULT_IS_URL;
+ }
+ },
+
+ onServerConfigVisibleChange: function(ev) {
+ this.setState({
+ serverConfigVisible: ev.target.checked
+ });
+ },
+
+ getUserIdSuffix: function() {
+ return '';
+ var actualHsUrl = document.createElement('a');
+ actualHsUrl.href = this.getHsUrl();
+ var defaultHsUrl = document.createElement('a');
+ defaultHsUrl.href = this.DEFAULT_HS_URL;
+ if (actualHsUrl.host == defaultHsUrl.host) {
+ return ':matrix.org';
+ }
+ return '';
+ },
+
+ onServerUrlChanged: function(newUrl) {
+ this.customHsUrl = this.refs.serverConfig.getHsUrl();
+ this.customIsUrl = this.refs.serverConfig.getIsUrl();
+ this.forceUpdate();
},
componentForStep: function(step) {
switch (step) {
case 'initial':
+ var serverConfigStyle = {};
+ serverConfigStyle.display = this.state.serverConfigVisible ? 'block' : 'none';
return (
);
@@ -72,7 +128,7 @@ module.exports = React.createClass({
case 'stage_m.login.recaptcha':
return (
- This Home Server would like to make sure you're not a robot
+ This Home Server would like to make sure you are not a robot
);
@@ -87,10 +143,10 @@ module.exports = React.createClass({
} else {
return (
);
}
@@ -123,8 +179,13 @@ module.exports = React.createClass({
render: function() {
return (
-
- {this.registerContent()}
+
+
+
+
+
+ {this.registerContent()}
+
);
}
diff --git a/src/CallHandler.js b/src/CallHandler.js
new file mode 100644
index 00000000..0915a65a
--- /dev/null
+++ b/src/CallHandler.js
@@ -0,0 +1,230 @@
+/*
+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';
+
+/*
+ * Manages a list of all the currently active calls.
+ *
+ * This handler dispatches when voip calls are added/updated/removed from this list:
+ * {
+ * action: 'call_state'
+ * room_id:
+ * }
+ *
+ * To know the state of the call, this handler exposes a getter to
+ * obtain the call for a room:
+ * var call = CallHandler.getCall(roomId)
+ * var state = call.call_state; // ringing|ringback|connected|ended|busy|stop_ringback|stop_ringing
+ *
+ * This handler listens for and handles the following actions:
+ * {
+ * action: 'place_call',
+ * type: 'voice|video',
+ * room_id:
+ * }
+ *
+ * {
+ * action: 'incoming_call'
+ * call: MatrixCall
+ * }
+ *
+ * {
+ * action: 'hangup'
+ * room_id:
+ * }
+ *
+ * {
+ * action: 'answer'
+ * room_id:
+ * }
+ */
+
+var MatrixClientPeg = require("./MatrixClientPeg");
+var Modal = require("./Modal");
+var ComponentBroker = require('./ComponentBroker');
+var ErrorDialog = ComponentBroker.get("organisms/ErrorDialog");
+var Matrix = require("matrix-js-sdk");
+var dis = require("./dispatcher");
+
+var calls = {
+ //room_id: MatrixCall
+};
+
+function play(audioId) {
+ // TODO: Attach an invisible element for this instead
+ // which listens?
+ var audio = document.getElementById(audioId);
+ if (audio) {
+ audio.load();
+ audio.play();
+ }
+}
+
+function pause(audioId) {
+ // TODO: Attach an invisible element for this instead
+ // which listens?
+ var audio = document.getElementById(audioId);
+ if (audio) {
+ audio.pause();
+ }
+}
+
+function _setCallListeners(call) {
+ call.on("error", function(err) {
+ console.error("Call error: %s", err);
+ console.error(err.stack);
+ call.hangup();
+ _setCallState(undefined, call.roomId, "ended");
+ });
+ call.on("hangup", function() {
+ _setCallState(undefined, call.roomId, "ended");
+ });
+ // map web rtc states to dummy UI state
+ // ringing|ringback|connected|ended|busy|stop_ringback|stop_ringing
+ call.on("state", function(newState, oldState) {
+ if (newState === "ringing") {
+ _setCallState(call, call.roomId, "ringing");
+ pause("ringbackAudio");
+ }
+ else if (newState === "invite_sent") {
+ _setCallState(call, call.roomId, "ringback");
+ play("ringbackAudio");
+ }
+ else if (newState === "ended" && oldState === "connected") {
+ _setCallState(call, call.roomId, "ended");
+ pause("ringbackAudio");
+ play("callendAudio");
+ }
+ else if (newState === "ended" && oldState === "invite_sent" &&
+ (call.hangupParty === "remote" ||
+ (call.hangupParty === "local" && call.hangupReason === "invite_timeout")
+ )) {
+ _setCallState(call, call.roomId, "busy");
+ pause("ringbackAudio");
+ play("busyAudio");
+ Modal.createDialog(ErrorDialog, {
+ title: "Call Timeout",
+ description: "The remote side failed to pick up."
+ });
+ }
+ else if (oldState === "invite_sent") {
+ _setCallState(call, call.roomId, "stop_ringback");
+ pause("ringbackAudio");
+ }
+ else if (oldState === "ringing") {
+ _setCallState(call, call.roomId, "stop_ringing");
+ pause("ringbackAudio");
+ }
+ else if (newState === "connected") {
+ _setCallState(call, call.roomId, "connected");
+ pause("ringbackAudio");
+ }
+ });
+}
+
+function _setCallState(call, roomId, status) {
+ console.log(
+ "Call state in %s changed to %s (%s)", roomId, status, (call ? call.state : "-")
+ );
+ calls[roomId] = call;
+ if (call) {
+ call.call_state = status;
+ }
+ dis.dispatch({
+ action: 'call_state',
+ room_id: roomId
+ });
+}
+
+dis.register(function(payload) {
+ switch (payload.action) {
+ case 'place_call':
+ if (calls[payload.room_id]) {
+ return; // don't allow >1 call to be placed.
+ }
+ var room = MatrixClientPeg.get().getRoom(payload.room_id);
+ if (!room) {
+ console.error("Room %s does not exist.", payload.room_id);
+ return;
+ }
+ var members = room.getJoinedMembers();
+ if (members.length !== 2) {
+ var text = members.length === 1 ? "yourself." : "more than 2 people.";
+ Modal.createDialog(ErrorDialog, {
+ description: "You cannot place a call with " + text
+ });
+ console.error(
+ "Fail: There are %s joined members in this room, not 2.",
+ room.getJoinedMembers().length
+ );
+ return;
+ }
+ console.log("Place %s call in %s", payload.type, payload.room_id);
+ var call = Matrix.createNewMatrixCall(
+ MatrixClientPeg.get(), payload.room_id
+ );
+ _setCallListeners(call);
+ _setCallState(call, call.roomId, "ringback");
+ if (payload.type === 'voice') {
+ call.placeVoiceCall();
+ }
+ else if (payload.type === 'video') {
+ call.placeVideoCall(
+ payload.remote_element,
+ payload.local_element
+ );
+ }
+ else {
+ console.error("Unknown call type: %s", payload.type);
+ }
+
+ break;
+ case 'incoming_call':
+ if (calls[payload.call.roomId]) {
+ payload.call.hangup("busy");
+ return; // don't allow >1 call to be received, hangup newer one.
+ }
+ var call = payload.call;
+ _setCallListeners(call);
+ _setCallState(call, call.roomId, "ringing");
+ break;
+ case 'hangup':
+ if (!calls[payload.room_id]) {
+ return; // no call to hangup
+ }
+ calls[payload.room_id].hangup();
+ _setCallState(null, payload.room_id, "ended");
+ break;
+ case 'answer':
+ if (!calls[payload.room_id]) {
+ return; // no call to answer
+ }
+ calls[payload.room_id].answer();
+ _setCallState(calls[payload.room_id], payload.room_id, "connected");
+ dis.dispatch({
+ action: "view_room",
+ room_id: payload.room_id
+ });
+ break;
+ }
+});
+
+module.exports = {
+ getCall: function(roomId) {
+ return calls[roomId] || null;
+ }
+};
\ No newline at end of file
diff --git a/src/ComponentBroker.js b/src/ComponentBroker.js
index 6445e947..6e5d1e11 100644
--- a/src/ComponentBroker.js
+++ b/src/ComponentBroker.js
@@ -62,7 +62,7 @@ require('../skins/base/views/atoms/LogoutButton');
require('../skins/base/views/atoms/EnableNotificationsButton');
require('../skins/base/views/atoms/MessageTimestamp');
require('../skins/base/views/atoms/create_room/CreateRoomButton');
-require('../skins/base/views/atoms/create_room/RoomNameTextbox');
+require('../skins/base/views/atoms/create_room/RoomAlias');
require('../skins/base/views/atoms/create_room/Presets');
require('../skins/base/views/atoms/EditableText');
require('../skins/base/views/molecules/MatrixToolbar');
@@ -89,4 +89,27 @@ require('../skins/base/views/templates/Register');
require('../skins/base/views/organisms/Notifier');
require('../skins/base/views/organisms/CreateRoom');
require('../skins/base/views/molecules/UserSelector');
+require('../skins/base/views/organisms/UserSettings');
+require('../skins/base/views/molecules/ChangeAvatar');
+require('../skins/base/views/molecules/ChangePassword');
+require('../skins/base/views/molecules/RoomSettings');
+// new for vector
+require('../skins/base/views/organisms/LeftPanel');
+require('../skins/base/views/organisms/RightPanel');
+require('../skins/base/views/organisms/LogoutPrompt');
+require('../skins/base/views/organisms/RoomDirectory');
+require('../skins/base/views/molecules/RoomCreate');
+require('../skins/base/views/molecules/RoomDropTarget');
+require('../skins/base/views/molecules/BottomLeftMenu');
+require('../skins/base/views/molecules/DateSeparator');
+require('../skins/base/views/atoms/voip/VideoFeed');
+require('../skins/base/views/molecules/voip/VideoView');
+require('../skins/base/views/molecules/voip/CallView');
+require('../skins/base/views/molecules/voip/IncomingCallBox');
+require('../skins/base/views/molecules/voip/MCallInviteTile');
+require('../skins/base/views/molecules/voip/MCallAnswerTile');
+require('../skins/base/views/molecules/voip/MCallHangupTile');
+require('../skins/base/views/molecules/EventAsTextTile');
+require('../skins/base/views/molecules/MemberInfo');
+require('../skins/base/views/organisms/ErrorDialog');
}
diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js
index 6b36e67e..322b70f4 100644
--- a/src/MatrixClientPeg.js
+++ b/src/MatrixClientPeg.js
@@ -23,6 +23,16 @@ var matrixClient = null;
var localStorage = window.localStorage;
+function deviceId() {
+ var id = Math.floor(Math.random()*16777215).toString(16);
+ id = "W" + "000000".substring(id.length) + id;
+ if (localStorage) {
+ id = localStorage.getItem("mx_device_id") || id;
+ localStorage.setItem("mx_device_id", id);
+ }
+ return id;
+}
+
function createClient(hs_url, is_url, user_id, access_token) {
var opts = {
baseUrl: hs_url,
@@ -31,6 +41,11 @@ function createClient(hs_url, is_url, user_id, access_token) {
userId: user_id
};
+ if (localStorage) {
+ opts.sessionStore = new Matrix.WebStorageSessionStore(localStorage);
+ opts.deviceId = deviceId();
+ }
+
matrixClient = Matrix.createClient(opts);
}
@@ -49,6 +64,10 @@ module.exports = {
return matrixClient;
},
+ unset: function() {
+ matrixClient = null;
+ },
+
replaceUsingUrls: function(hs_url, is_url) {
matrixClient = Matrix.createClient({
baseUrl: hs_url,
diff --git a/src/Modal.js b/src/Modal.js
new file mode 100644
index 00000000..a8331e55
--- /dev/null
+++ b/src/Modal.js
@@ -0,0 +1,60 @@
+/*
+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 q = require('q');
+
+module.exports = {
+ DialogContainerId: "mx_Dialog_Container",
+
+ getOrCreateContainer: function() {
+ var container = document.getElementById(this.DialogContainerId);
+
+ if (!container) {
+ container = document.createElement("div");
+ container.id = this.DialogContainerId;
+ document.body.appendChild(container);
+ }
+
+ return container;
+ },
+
+ createDialog: function (Element, props) {
+ var self = this;
+
+ var closeDialog = function() {
+ React.unmountComponentAtNode(self.getOrCreateContainer());
+
+ if (props && props.onFinished) props.onFinished.apply(arguments);
+ };
+
+ // FIXME: If a dialog uses getDefaultProps it clobbers the onFinished
+ // property set here so you can't close the dialog from a button click!
+ var dialog = (
+
+ );
+
+ React.render(dialog, this.getOrCreateContainer());
+ },
+};
diff --git a/src/Presence.js b/src/Presence.js
new file mode 100644
index 00000000..558c7e99
--- /dev/null
+++ b/src/Presence.js
@@ -0,0 +1,107 @@
+/*
+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 MatrixClientPeg = require("./MatrixClientPeg");
+
+ // Time in ms after that a user is considered as unavailable/away
+var UNAVAILABLE_TIME_MS = 3 * 60 * 1000; // 3 mins
+var PRESENCE_STATES = ["online", "offline", "unavailable"];
+
+// The current presence state
+var state, timer;
+
+module.exports = {
+
+ /**
+ * Start listening the user activity to evaluate his presence state.
+ * Any state change will be sent to the Home Server.
+ */
+ start: function() {
+ var self = this;
+ this.running = true;
+ if (undefined === state) {
+ // The user is online if they move the mouse or press a key
+ document.onmousemove = function() { self._resetTimer(); };
+ document.onkeypress = function() { self._resetTimer(); };
+ this._resetTimer();
+ }
+ },
+
+ /**
+ * Stop tracking user activity
+ */
+ stop: function() {
+ this.running = false;
+ if (timer) {
+ clearTimeout(timer);
+ timer = undefined;
+ }
+ state = undefined;
+ },
+
+ /**
+ * Get the current presence state.
+ * @returns {string} the presence state (see PRESENCE enum)
+ */
+ getState: function() {
+ return state;
+ },
+
+ /**
+ * Set the presence state.
+ * If the state has changed, the Home Server will be notified.
+ * @param {string} newState the new presence state (see PRESENCE enum)
+ */
+ setState: function(newState) {
+ if (newState === state) {
+ return;
+ }
+ if (PRESENCE_STATES.indexOf(newState) === -1) {
+ throw new Error("Bad presence state: " + newState);
+ }
+ if (!this.running) {
+ return;
+ }
+ state = newState;
+ MatrixClientPeg.get().setPresence(state).done(function() {
+ console.log("Presence: %s", newState);
+ }, function(err) {
+ console.error("Failed to set presence: %s", err);
+ });
+ },
+
+ /**
+ * Callback called when the user made no action on the page for UNAVAILABLE_TIME ms.
+ * @private
+ */
+ _onUnavailableTimerFire: function() {
+ this.setState("unavailable");
+ },
+
+ /**
+ * Callback called when the user made an action on the page
+ * @private
+ */
+ _resetTimer: function() {
+ var self = this;
+ this.setState("online");
+ // Re-arm the timer
+ clearTimeout(timer);
+ timer = setTimeout(function() {
+ self._onUnavailableTimerFire();
+ }, UNAVAILABLE_TIME_MS);
+ }
+};
\ No newline at end of file
diff --git a/src/RoomListSorter.js b/src/RoomListSorter.js
index bc7a0016..730a0de1 100644
--- a/src/RoomListSorter.js
+++ b/src/RoomListSorter.js
@@ -17,7 +17,12 @@ limitations under the License.
'use strict';
function tsOfNewestEvent(room) {
- return room.timeline[room.timeline.length - 1].getTs();
+ if (room.timeline.length) {
+ return room.timeline[room.timeline.length - 1].getTs();
+ }
+ else {
+ return Number.MAX_SAFE_INTEGER;
+ }
}
function mostRecentActivityFirst(roomList) {
diff --git a/src/SlashCommands.js b/src/SlashCommands.js
new file mode 100644
index 00000000..907aa26d
--- /dev/null
+++ b/src/SlashCommands.js
@@ -0,0 +1,253 @@
+/*
+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 MatrixClientPeg = require("./MatrixClientPeg");
+var dis = require("./dispatcher");
+var encryption = require("./encryption");
+
+var reject = function(msg) {
+ return {
+ error: msg
+ };
+};
+
+var success = function(promise) {
+ return {
+ promise: promise
+ };
+};
+
+var commands = {
+ // Change your nickname
+ nick: function(room_id, args) {
+ if (args) {
+ return success(
+ MatrixClientPeg.get().setDisplayName(args)
+ );
+ }
+ return reject("Usage: /nick ");
+ },
+
+ encrypt: function(room_id, args) {
+ if (args == "on") {
+ var client = MatrixClientPeg.get();
+ var members = client.getRoom(room_id).currentState.members;
+ var user_ids = Object.keys(members);
+ return success(
+ encryption.enableEncryption(client, room_id, user_ids)
+ );
+ }
+ if (args == "off") {
+ var client = MatrixClientPeg.get();
+ return success(
+ encryption.disableEncryption(client, room_id)
+ );
+
+ }
+ return reject("Usage: encrypt ");
+ },
+
+ // Change the room topic
+ topic: function(room_id, args) {
+ if (args) {
+ return success(
+ MatrixClientPeg.get().setRoomTopic(room_id, args)
+ );
+ }
+ return reject("Usage: /topic ");
+ },
+
+ // Invite a user
+ invite: function(room_id, args) {
+ if (args) {
+ var matches = args.match(/^(\S+)$/);
+ if (matches) {
+ return success(
+ MatrixClientPeg.get().invite(room_id, matches[1])
+ );
+ }
+ }
+ return reject("Usage: /invite ");
+ },
+
+ // Join a room
+ join: function(room_id, args) {
+ if (args) {
+ var matches = args.match(/^(\S+)$/);
+ if (matches) {
+ var room_alias = matches[1];
+ // Try to find a room with this alias
+ var rooms = MatrixClientPeg.get().getRooms();
+ var roomId;
+ for (var i = 0; i < rooms.length; i++) {
+ var aliasEvents = rooms[i].currentState.getStateEvents(
+ "m.room.aliases"
+ );
+ for (var j = 0; j < aliasEvents.length; j++) {
+ var aliases = aliasEvents[j].getContent().aliases || [];
+ for (var k = 0; k < aliases.length; k++) {
+ if (aliases[k] === room_alias) {
+ roomId = rooms[i].roomId;
+ break;
+ }
+ }
+ if (roomId) { break; }
+ }
+ if (roomId) { break; }
+ }
+ if (roomId) { // we've already joined this room, view it.
+ dis.dispatch({
+ action: 'view_room',
+ room_id: roomId
+ });
+ return success();
+ }
+ else {
+ // attempt to join this alias.
+ return success(
+ MatrixClientPeg.get().joinRoom(room_alias).then(
+ function(room) {
+ dis.dispatch({
+ action: 'view_room',
+ room_id: room.roomId
+ });
+ })
+ );
+ }
+ }
+ }
+ return reject("Usage: /join ");
+ },
+
+ // Kick a user from the room with an optional reason
+ kick: function(room_id, args) {
+ if (args) {
+ var matches = args.match(/^(\S+?)( +(.*))?$/);
+ if (matches) {
+ return success(
+ MatrixClientPeg.get().kick(room_id, matches[1], matches[3])
+ );
+ }
+ }
+ return reject("Usage: /kick []");
+ },
+
+ // Ban a user from the room with an optional reason
+ ban: function(room_id, args) {
+ if (args) {
+ var matches = args.match(/^(\S+?)( +(.*))?$/);
+ if (matches) {
+ return success(
+ MatrixClientPeg.get().ban(room_id, matches[1], matches[3])
+ );
+ }
+ }
+ return reject("Usage: /ban []");
+ },
+
+ // Unban a user from the room
+ unban: function(room_id, args) {
+ if (args) {
+ var matches = args.match(/^(\S+)$/);
+ if (matches) {
+ // Reset the user membership to "leave" to unban him
+ return success(
+ MatrixClientPeg.get().unban(room_id, matches[1])
+ );
+ }
+ }
+ return reject("Usage: /unban ");
+ },
+
+ // Define the power level of a user
+ op: function(room_id, args) {
+ if (args) {
+ var matches = args.match(/^(\S+?)( +(\d+))?$/);
+ var powerLevel = 50; // default power level for op
+ if (matches) {
+ var user_id = matches[1];
+ if (matches.length === 4 && undefined !== matches[3]) {
+ powerLevel = parseInt(matches[3]);
+ }
+ if (powerLevel !== NaN) {
+ var room = MatrixClientPeg.get().getRoom(room_id);
+ if (!room) {
+ return reject("Bad room ID: " + room_id);
+ }
+ var powerLevelEvent = room.currentState.getStateEvents(
+ "m.room.power_levels", ""
+ );
+ return success(
+ MatrixClientPeg.get().setPowerLevel(
+ room_id, user_id, powerLevel, powerLevelEvent
+ )
+ );
+ }
+ }
+ }
+ return reject("Usage: /op []");
+ },
+
+ // Reset the power level of a user
+ deop: function(room_id, args) {
+ if (args) {
+ var matches = args.match(/^(\S+)$/);
+ if (matches) {
+ var room = MatrixClientPeg.get().getRoom(room_id);
+ if (!room) {
+ return reject("Bad room ID: " + room_id);
+ }
+
+ var powerLevelEvent = room.currentState.getStateEvents(
+ "m.room.power_levels", ""
+ );
+ return success(
+ MatrixClientPeg.get().setPowerLevel(
+ room_id, args, undefined, powerLevelEvent
+ )
+ );
+ }
+ }
+ return reject("Usage: /deop ");
+ }
+};
+
+module.exports = {
+ /**
+ * Process the given text for /commands and perform them.
+ * @param {string} roomId The room in which the command was performed.
+ * @param {string} input The raw text input by the user.
+ * @return {Object|null} An object with the property 'error' if there was an error
+ * processing the command, or 'promise' if a request was sent out.
+ * Returns null if the input didn't match a command.
+ */
+ processInput: function(roomId, input) {
+ // trim any trailing whitespace, as it can confuse the parser for
+ // IRC-style commands
+ input = input.replace(/\s+$/, "");
+ if (input[0] === "/" && input[1] !== "/") {
+ var bits = input.match(/^(\S+?)( +(.*))?$/);
+ var cmd = bits[1].substring(1).toLowerCase();
+ var args = bits[3];
+ if (commands[cmd]) {
+ return commands[cmd](roomId, args);
+ }
+ }
+ return null; // not a command
+ }
+};
diff --git a/src/TextForEvent.js b/src/TextForEvent.js
new file mode 100644
index 00000000..c8f2f71b
--- /dev/null
+++ b/src/TextForEvent.js
@@ -0,0 +1,82 @@
+
+function textForMemberEvent(ev) {
+ // XXX: SYJS-16
+ var senderName = ev.sender ? ev.sender.name : ev.getSender();
+ var targetName = ev.target ? ev.target.name : ev.getStateKey();
+ var reason = ev.getContent().reason ? (
+ " Reason: " + ev.getContent().reason
+ ) : "";
+ switch (ev.getContent().membership) {
+ case 'invite':
+ return senderName + " invited " + targetName + ".";
+ case 'ban':
+ return senderName + " banned " + targetName + "." + reason;
+ case 'join':
+ if (ev.getPrevContent() && ev.getPrevContent().membership == 'join') {
+ if (ev.getPrevContent().displayname && ev.getContent().displayname && ev.getPrevContent().displayname != ev.getContent().displayname) {
+ return ev.getSender() + " changed their display name from " +
+ ev.getPrevContent().displayname + " to " +
+ ev.getContent().displayname;
+ } else if (!ev.getPrevContent().displayname && ev.getContent().displayname) {
+ return ev.getSender() + " set their display name to " + ev.getContent().displayname;
+ } else if (ev.getPrevContent().displayname && !ev.getContent().displayname) {
+ return ev.getSender() + " removed their display name";
+ } else if (ev.getPrevContent().avatar_url && !ev.getContent().avatar_url) {
+ return ev.getSender() + " removed their profile picture";
+ } else if (ev.getPrevContent().avatar_url && ev.getContent().avatar_url && ev.getPrevContent().avatar_url != ev.getContent().avatar_url) {
+ return ev.getSender() + " changed their profile picture";
+ } else if (!ev.getPrevContent().avatar_url && ev.getContent().avatar_url) {
+ return ev.getSender() + " set a profile picture";
+ }
+ } else {
+ if (!ev.target) console.warn("Join message has no target! -- " + ev.getContent().state_key);
+ return targetName + " joined the room.";
+ }
+ return '';
+ case 'leave':
+ if (ev.getSender() === ev.getStateKey()) {
+ return targetName + " left the room.";
+ }
+ else if (ev.getPrevContent().membership === "ban") {
+ return senderName + " unbanned " + targetName + ".";
+ }
+ else if (ev.getPrevContent().membership === "join") {
+ return senderName + " kicked " + targetName + "." + reason;
+ }
+ else {
+ return targetName + " left the room.";
+ }
+ }
+};
+
+function textForTopicEvent(ev) {
+ var senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
+
+ return senderDisplayName + ' changed the topic to, "' + ev.getContent().topic + '"';
+};
+
+function textForMessageEvent(ev) {
+ var senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
+
+ var message = senderDisplayName + ': ' + ev.getContent().body;
+ if (ev.getContent().msgtype === "m.emote") {
+ message = "* " + senderDisplayName + " " + message;
+ } else if (ev.getContent().msgtype === "m.image") {
+ message = senderDisplayName + " sent an image.";
+ }
+ return message;
+};
+
+var handlers = {
+ 'm.room.message': textForMessageEvent,
+ 'm.room.topic': textForTopicEvent,
+ 'm.room.member': textForMemberEvent
+};
+
+module.exports = {
+ textForEvent: function(ev) {
+ var hdlr = handlers[ev.getType()];
+ if (!hdlr) return "";
+ return hdlr(ev);
+ }
+}
diff --git a/src/WhoIsTyping.js b/src/WhoIsTyping.js
new file mode 100644
index 00000000..4fb53990
--- /dev/null
+++ b/src/WhoIsTyping.js
@@ -0,0 +1,49 @@
+var MatrixClientPeg = require("./MatrixClientPeg");
+
+module.exports = {
+ usersTypingApartFromMe: function(room) {
+ return this.usersTyping(
+ room, [MatrixClientPeg.get().credentials.userId]
+ );
+ },
+
+ /**
+ * Given a Room object and, optionally, a list of userID strings
+ * to exclude, return a list of user objects who are typing.
+ */
+ usersTyping: function(room, exclude) {
+ var whoIsTyping = [];
+
+ if (exclude === undefined) {
+ exclude = [];
+ }
+
+ var memberKeys = Object.keys(room.currentState.members);
+ for (var i = 0; i < memberKeys.length; ++i) {
+ var userId = memberKeys[i];
+
+ if (room.currentState.members[userId].typing) {
+ if (exclude.indexOf(userId) == -1) {
+ whoIsTyping.push(room.currentState.members[userId]);
+ }
+ }
+ }
+
+ return whoIsTyping;
+ },
+
+ whoIsTypingString: function(room) {
+ var whoIsTyping = this.usersTypingApartFromMe(room);
+ if (whoIsTyping.length == 0) {
+ return null;
+ } else if (whoIsTyping.length == 1) {
+ return whoIsTyping[0].name + ' is typing';
+ } else {
+ var names = whoIsTyping.map(function(m) {
+ return m.name;
+ });
+ var lastPerson = names.shift();
+ return names.join(', ') + ' and ' + lastPerson + ' are typing';
+ }
+ }
+}
diff --git a/src/controllers/atoms/EditableText.js b/src/controllers/atoms/EditableText.js
index ac469736..5ea4ce8c 100644
--- a/src/controllers/atoms/EditableText.js
+++ b/src/controllers/atoms/EditableText.js
@@ -21,7 +21,9 @@ var React = require('react');
module.exports = {
propTypes: {
onValueChanged: React.PropTypes.func,
- initalValue: React.PropTypes.string,
+ initialValue: React.PropTypes.string,
+ label: React.PropTypes.string,
+ placeHolder: React.PropTypes.string,
},
Phases: {
@@ -32,37 +34,55 @@ module.exports = {
getDefaultProps: function() {
return {
onValueChanged: function() {},
- initalValue: '',
+ initialValue: '',
+ label: 'Click to set',
+ placeholder: '',
};
},
getInitialState: function() {
return {
- value: this.props.initalValue,
+ value: this.props.initialValue,
phase: this.Phases.Display,
}
},
+ componentWillReceiveProps: function(nextProps) {
+ this.setState({
+ value: nextProps.initialValue
+ });
+ },
+
getValue: function() {
return this.state.value;
},
- setValue: function(val) {
+ setValue: function(val, shouldSubmit, suppressListener) {
+ var self = this;
this.setState({
value: val,
phase: this.Phases.Display,
+ }, function() {
+ if (!suppressListener) {
+ self.onValueChanged(shouldSubmit);
+ }
});
+ },
- this.onValueChanged();
+ edit: function() {
+ this.setState({
+ phase: this.Phases.Edit,
+ });
},
cancelEdit: function() {
this.setState({
phase: this.Phases.Display,
});
+ this.onValueChanged(false);
},
- onValueChanged: function() {
- this.props.onValueChanged(this.state.value);
+ onValueChanged: function(shouldSubmit) {
+ this.props.onValueChanged(this.state.value, shouldSubmit);
},
};
diff --git a/src/controllers/atoms/EnableNotificationsButton.js b/src/controllers/atoms/EnableNotificationsButton.js
index c600f330..d6638b27 100644
--- a/src/controllers/atoms/EnableNotificationsButton.js
+++ b/src/controllers/atoms/EnableNotificationsButton.js
@@ -15,53 +15,43 @@ limitations under the License.
*/
'use strict';
+var ComponentBroker = require("../../ComponentBroker");
+var Notifier = ComponentBroker.get('organisms/Notifier');
+var dis = require("../../dispatcher");
module.exports = {
- notificationsAvailable: function() {
- return !!global.Notification;
+
+ componentDidMount: function() {
+ this.dispatcherRef = dis.register(this.onAction);
},
- havePermission: function() {
- return global.Notification.permission == 'granted';
+ componentWillUnmount: function() {
+ dis.unregister(this.dispatcherRef);
+ },
+
+ onAction: function(payload) {
+ if (payload.action !== "notifier_enabled") {
+ return;
+ }
+ this.forceUpdate();
},
enabled: function() {
- if (!this.havePermission()) return false;
-
- if (!global.localStorage) return true;
-
- var enabled = global.localStorage.getItem('notifications_enabled');
- if (enabled === null) return true;
- return enabled === 'true';
- },
-
- disable: function() {
- if (!global.localStorage) return;
- global.localStorage.setItem('notifications_enabled', 'false');
- this.forceUpdate();
- },
-
- enable: function() {
- if (!this.havePermission()) {
- var that = this;
- global.Notification.requestPermission(function() {
- that.forceUpdate();
- });
- }
-
- if (!global.localStorage) return;
- global.localStorage.setItem('notifications_enabled', 'true');
- this.forceUpdate();
+ return Notifier.isEnabled();
},
onClick: function() {
- if (!this.notificationsAvailable()) {
+ var self = this;
+ if (!Notifier.supportsDesktopNotifications()) {
return;
}
- if (!this.enabled()) {
- this.enable();
+ if (!Notifier.isEnabled()) {
+ Notifier.setEnabled(true, function() {
+ self.forceUpdate();
+ });
} else {
- this.disable();
+ Notifier.setEnabled(false);
}
+ this.forceUpdate();
},
};
diff --git a/src/controllers/atoms/create_room/Presets.js b/src/controllers/atoms/create_room/Presets.js
index 5ff7327e..bcc2f514 100644
--- a/src/controllers/atoms/create_room/Presets.js
+++ b/src/controllers/atoms/create_room/Presets.js
@@ -18,24 +18,23 @@ limitations under the License.
var React = require('react');
+var Presets = {
+ PrivateChat: "private_chat",
+ PublicChat: "public_chat",
+ Custom: "custom",
+};
+
module.exports = {
propTypes: {
- default_preset: React.PropTypes.string
+ onChange: React.PropTypes.func,
+ preset: React.PropTypes.string
},
+ Presets: Presets,
+
getDefaultProps: function() {
return {
- default_preset: 'private_chat',
+ onChange: function() {},
};
},
-
- getInitialState: function() {
- return {
- preset: this.props.default_preset,
- }
- },
-
- getPreset: function() {
- return this.state.preset;
- },
};
diff --git a/src/controllers/atoms/create_room/RoomAlias.js b/src/controllers/atoms/create_room/RoomAlias.js
new file mode 100644
index 00000000..4b268e90
--- /dev/null
+++ b/src/controllers/atoms/create_room/RoomAlias.js
@@ -0,0 +1,49 @@
+/*
+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');
+
+module.exports = {
+ propTypes: {
+ // Specifying a homeserver will make magical things happen when you,
+ // e.g. start typing in the room alias box.
+ homeserver: React.PropTypes.string,
+ alias: React.PropTypes.string,
+ onChange: React.PropTypes.func,
+ },
+
+ getDefaultProps: function() {
+ return {
+ onChange: function() {},
+ alias: '',
+ };
+ },
+
+ getAliasLocalpart: function() {
+ var room_alias = this.props.alias;
+
+ if (room_alias && this.props.homeserver) {
+ var suffix = ":" + this.props.homeserver;
+ if (room_alias.startsWith("#") && room_alias.endsWith(suffix)) {
+ room_alias = room_alias.slice(1, -suffix.length);
+ }
+ }
+
+ return room_alias;
+ },
+};
diff --git a/src/controllers/atoms/voip/VideoFeed.js b/src/controllers/atoms/voip/VideoFeed.js
new file mode 100644
index 00000000..8aa688b2
--- /dev/null
+++ b/src/controllers/atoms/voip/VideoFeed.js
@@ -0,0 +1,21 @@
+/*
+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';
+
+module.exports = {
+};
+
diff --git a/src/controllers/molecules/ChangeAvatar.js b/src/controllers/molecules/ChangeAvatar.js
new file mode 100644
index 00000000..72a541b1
--- /dev/null
+++ b/src/controllers/molecules/ChangeAvatar.js
@@ -0,0 +1,71 @@
+/*
+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 dis = require("../../dispatcher");
+
+module.exports = {
+ propTypes: {
+ onFinished: React.PropTypes.func,
+ initialAvatarUrl: React.PropTypes.string.isRequired,
+ },
+
+ Phases: {
+ Display: "display",
+ Uploading: "uploading",
+ Error: "error",
+ },
+
+ getDefaultProps: function() {
+ return {
+ onFinished: function() {},
+ };
+ },
+
+ getInitialState: function() {
+ return {
+ avatarUrl: this.props.initialAvatarUrl,
+ phase: this.Phases.Display,
+ }
+ },
+
+ setAvatarFromFile: function(file) {
+ var newUrl = null;
+
+ this.setState({
+ phase: this.Phases.Uploading
+ });
+ var self = this;
+ MatrixClientPeg.get().uploadContent(file).then(function(url) {
+ newUrl = url;
+ return MatrixClientPeg.get().setAvatarUrl(url);
+ }).done(function() {
+ self.setState({
+ phase: self.Phases.Display,
+ avatarUrl: MatrixClientPeg.get().mxcUrlToHttp(newUrl)
+ });
+ }, function(error) {
+ self.setState({
+ phase: this.Phases.Error
+ });
+ self.onError(error);
+ });
+ },
+}
diff --git a/src/controllers/molecules/ChangePassword.js b/src/controllers/molecules/ChangePassword.js
new file mode 100644
index 00000000..5cc73c5d
--- /dev/null
+++ b/src/controllers/molecules/ChangePassword.js
@@ -0,0 +1,78 @@
+/*
+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 dis = require("../../dispatcher");
+
+module.exports = {
+ propTypes: {
+ onFinished: React.PropTypes.func,
+ },
+
+ Phases: {
+ Edit: "edit",
+ Uploading: "uploading",
+ Error: "error",
+ Success: "Success"
+ },
+
+ getDefaultProps: function() {
+ return {
+ onFinished: function() {},
+ };
+ },
+
+ getInitialState: function() {
+ return {
+ phase: this.Phases.Edit,
+ errorString: ''
+ }
+ },
+
+ changePassword: function(old_password, new_password) {
+ var cli = MatrixClientPeg.get();
+
+ var authDict = {
+ type: 'm.login.password',
+ user: cli.credentials.userId,
+ password: old_password
+ };
+
+ this.setState({
+ phase: this.Phases.Uploading,
+ errorString: '',
+ })
+
+ var d = cli.setPassword(authDict, new_password);
+
+ var self = this;
+ d.then(function() {
+ self.setState({
+ phase: self.Phases.Success,
+ errorString: '',
+ })
+ }, function(err) {
+ self.setState({
+ phase: self.Phases.Error,
+ errorString: err.toString()
+ })
+ });
+ },
+}
diff --git a/src/controllers/molecules/EventAsTextTile.js b/src/controllers/molecules/EventAsTextTile.js
new file mode 100644
index 00000000..8aa688b2
--- /dev/null
+++ b/src/controllers/molecules/EventAsTextTile.js
@@ -0,0 +1,21 @@
+/*
+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';
+
+module.exports = {
+};
+
diff --git a/src/controllers/molecules/MEmoteTile.js b/src/controllers/molecules/MEmoteTile.js
index 8aa688b2..1fb117ce 100644
--- a/src/controllers/molecules/MEmoteTile.js
+++ b/src/controllers/molecules/MEmoteTile.js
@@ -16,6 +16,15 @@ limitations under the License.
'use strict';
+var linkify = require('linkifyjs');
+var linkifyElement = require('linkifyjs/element');
+var linkifyMatrix = require('../../linkify-matrix');
+
+linkifyMatrix(linkify);
+
module.exports = {
+ componentDidMount: function() {
+ linkifyElement(this.refs.content.getDOMNode(), linkifyMatrix.options);
+ }
};
diff --git a/src/controllers/molecules/MemberInfo.js b/src/controllers/molecules/MemberInfo.js
new file mode 100644
index 00000000..4f222c5d
--- /dev/null
+++ b/src/controllers/molecules/MemberInfo.js
@@ -0,0 +1,321 @@
+/*
+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.
+*/
+
+/*
+ * State vars:
+ * 'presence' : string (online|offline|unavailable etc)
+ * 'active' : number (ms ago; can be -1)
+ * 'can': {
+ * kick: boolean,
+ * ban: boolean,
+ * mute: boolean,
+ * modifyLevel: boolean
+ * },
+ * 'muted': boolean,
+ * 'isTargetMod': boolean
+ */
+
+'use strict';
+var MatrixClientPeg = require("../../MatrixClientPeg");
+var dis = require("../../dispatcher");
+var Modal = require("../../Modal");
+var ComponentBroker = require('../../ComponentBroker');
+var ErrorDialog = ComponentBroker.get("organisms/ErrorDialog");
+
+module.exports = {
+ componentDidMount: function() {
+ var self = this;
+ // listen for presence changes
+ function updateUserState(event, user) {
+ if (!self.props.member) { return; }
+
+ if (user.userId === self.props.member.userId) {
+ self.setState({
+ presence: user.presence,
+ active: user.lastActiveAgo
+ });
+ }
+ }
+ MatrixClientPeg.get().on("User.presence", updateUserState);
+ this.userPresenceFn = updateUserState;
+
+ // listen for power level changes
+ function updatePowerLevel(event, member) {
+ if (!self.props.member) { return; }
+
+ if (member.roomId !== self.props.member.roomId) {
+ return;
+ }
+ // only interested in changes to us or them
+ var myUserId = MatrixClientPeg.get().credentials.userId;
+ if ([myUserId, self.props.member.userId].indexOf(member.userId) === -1) {
+ return;
+ }
+ self.setState(self._calculateOpsPermissions());
+ }
+ MatrixClientPeg.get().on("RoomMember.powerLevel", updatePowerLevel);
+ this.updatePowerLevelFn = updatePowerLevel;
+
+ // work out the current state
+ if (this.props.member) {
+ var usr = MatrixClientPeg.get().getUser(this.props.member.userId) || {};
+ var memberState = this._calculateOpsPermissions();
+ memberState.presence = usr.presence || "offline";
+ memberState.active = usr.lastActiveAgo || -1;
+ this.setState(memberState);
+ }
+ },
+
+ componentWillUnmount: function() {
+ MatrixClientPeg.get().removeListener("User.presence", this.userPresenceFn);
+ MatrixClientPeg.get().removeListener(
+ "RoomMember.powerLevel", this.updatePowerLevelFn
+ );
+ },
+
+ onKick: function() {
+ var roomId = this.props.member.roomId;
+ var target = this.props.member.userId;
+ var self = this;
+ MatrixClientPeg.get().kick(roomId, target).done(function() {
+ // NO-OP; rely on the m.room.member event coming down else we could
+ // get out of sync if we force setState here!
+ console.log("Kick success");
+ }, function(err) {
+ Modal.createDialog(ErrorDialog, {
+ title: "Kick error",
+ description: err.message
+ });
+ });
+ },
+
+ onBan: function() {
+ var roomId = this.props.member.roomId;
+ var target = this.props.member.userId;
+ var self = this;
+ MatrixClientPeg.get().ban(roomId, target).done(function() {
+ // NO-OP; rely on the m.room.member event coming down else we could
+ // get out of sync if we force setState here!
+ console.log("Ban success");
+ }, function(err) {
+ Modal.createDialog(ErrorDialog, {
+ title: "Ban error",
+ description: err.message
+ });
+ });
+ },
+
+ onMuteToggle: function() {
+ var roomId = this.props.member.roomId;
+ var target = this.props.member.userId;
+ var self = this;
+ var room = MatrixClientPeg.get().getRoom(roomId);
+ if (!room) {
+ return;
+ }
+ var powerLevelEvent = room.currentState.getStateEvents(
+ "m.room.power_levels", ""
+ );
+ if (!powerLevelEvent) {
+ return;
+ }
+ var isMuted = this.state.muted;
+ var powerLevels = powerLevelEvent.getContent();
+ var levelToSend = (
+ (powerLevels.events ? powerLevels.events["m.room.message"] : null) ||
+ powerLevels.events_default
+ );
+ var level;
+ if (isMuted) { // unmute
+ level = levelToSend;
+ }
+ else { // mute
+ level = levelToSend - 1;
+ }
+
+ MatrixClientPeg.get().setPowerLevel(roomId, target, level, powerLevelEvent).done(
+ function() {
+ // NO-OP; rely on the m.room.member event coming down else we could
+ // get out of sync if we force setState here!
+ console.log("Mute toggle success");
+ }, function(err) {
+ Modal.createDialog(ErrorDialog, {
+ title: "Mute error",
+ description: err.message
+ });
+ });
+ },
+
+ onModToggle: function() {
+ var roomId = this.props.member.roomId;
+ var target = this.props.member.userId;
+ var room = MatrixClientPeg.get().getRoom(roomId);
+ if (!room) {
+ return;
+ }
+ var powerLevelEvent = room.currentState.getStateEvents(
+ "m.room.power_levels", ""
+ );
+ if (!powerLevelEvent) {
+ return;
+ }
+ var me = room.getMember(MatrixClientPeg.get().credentials.userId);
+ if (!me) {
+ return;
+ }
+ var defaultLevel = powerLevelEvent.getContent().users_default;
+ var modLevel = me.powerLevel - 1;
+ // toggle the level
+ var newLevel = this.state.isTargetMod ? defaultLevel : modLevel;
+ MatrixClientPeg.get().setPowerLevel(roomId, target, newLevel, powerLevelEvent).done(
+ function() {
+ // NO-OP; rely on the m.room.member event coming down else we could
+ // get out of sync if we force setState here!
+ console.log("Mod toggle success");
+ }, function(err) {
+ Modal.createDialog(ErrorDialog, {
+ title: "Mod error",
+ description: err.message
+ });
+ });
+ },
+
+ onChatClick: function() {
+ // check if there are any existing rooms with just us and them (1:1)
+ // If so, just view that room. If not, create a private room with them.
+ var rooms = MatrixClientPeg.get().getRooms();
+ var userIds = [
+ this.props.member.userId,
+ MatrixClientPeg.get().credentials.userId
+ ];
+ var existingRoomId = null;
+ for (var i = 0; i < rooms.length; i++) {
+ var members = rooms[i].getJoinedMembers();
+ if (members.length === 2) {
+ var hasTargetUsers = true;
+ for (var j = 0; j < members.length; j++) {
+ if (userIds.indexOf(members[j].userId) === -1) {
+ hasTargetUsers = false;
+ break;
+ }
+ }
+ if (hasTargetUsers) {
+ existingRoomId = rooms[i].roomId;
+ break;
+ }
+ }
+ }
+
+ if (existingRoomId) {
+ dis.dispatch({
+ action: 'view_room',
+ room_id: existingRoomId
+ });
+ }
+ else {
+ MatrixClientPeg.get().createRoom({
+ invite: [this.props.member.userId],
+ preset: "private_chat"
+ }).done(function(res) {
+ dis.dispatch({
+ action: 'view_room',
+ room_id: res.room_id
+ });
+ }, function(err) {
+ console.error(
+ "Failed to create room: %s", JSON.stringify(err)
+ );
+ });
+ }
+ },
+
+ getInitialState: function() {
+ return {
+ presence: "offline",
+ active: -1,
+ can: {
+ kick: false,
+ ban: false,
+ mute: false,
+ modifyLevel: false
+ },
+ muted: false,
+ isTargetMod: false
+ }
+ },
+
+ _calculateOpsPermissions: function() {
+ var defaultPerms = {
+ can: {},
+ muted: false,
+ modifyLevel: false
+ };
+ var room = MatrixClientPeg.get().getRoom(this.props.member.roomId);
+ if (!room) {
+ return defaultPerms;
+ }
+ var powerLevels = room.currentState.getStateEvents(
+ "m.room.power_levels", ""
+ );
+ if (!powerLevels) {
+ return defaultPerms;
+ }
+ var me = room.getMember(MatrixClientPeg.get().credentials.userId);
+ var them = this.props.member;
+ return {
+ can: this._calculateCanPermissions(
+ me, them, powerLevels.getContent()
+ ),
+ muted: this._isMuted(them, powerLevels.getContent()),
+ isTargetMod: them.powerLevel > powerLevels.getContent().users_default
+ };
+ },
+
+ _calculateCanPermissions: function(me, them, powerLevels) {
+ var can = {
+ kick: false,
+ ban: false,
+ mute: false,
+ modifyLevel: false
+ };
+ var canAffectUser = them.powerLevel < me.powerLevel;
+ if (!canAffectUser) {
+ console.log("Cannot affect user: %s >= %s", them.powerLevel, me.powerLevel);
+ return can;
+ }
+ var editPowerLevel = (
+ (powerLevels.events ? powerLevels.events["m.room.power_levels"] : null) ||
+ powerLevels.state_default
+ );
+ can.kick = me.powerLevel >= powerLevels.kick;
+ can.ban = me.powerLevel >= powerLevels.ban;
+ can.mute = me.powerLevel >= editPowerLevel;
+ can.modifyLevel = me.powerLevel > them.powerLevel;
+ return can;
+ },
+
+ _isMuted: function(member, powerLevelContent) {
+ if (!powerLevelContent || !member) {
+ return false;
+ }
+ var levelToSend = (
+ (powerLevelContent.events ? powerLevelContent.events["m.room.message"] : null) ||
+ powerLevelContent.events_default
+ );
+ return member.powerLevel < levelToSend;
+ }
+};
+
diff --git a/src/controllers/molecules/MessageComposer.js b/src/controllers/molecules/MessageComposer.js
index f55546ae..c73e9f25 100644
--- a/src/controllers/molecules/MessageComposer.js
+++ b/src/controllers/molecules/MessageComposer.js
@@ -17,16 +17,130 @@ limitations under the License.
'use strict';
var MatrixClientPeg = require("../../MatrixClientPeg");
+var SlashCommands = require("../../SlashCommands");
+var Modal = require("../../Modal");
+var ComponentBroker = require('../../ComponentBroker');
+var ErrorDialog = ComponentBroker.get("organisms/ErrorDialog");
var dis = require("../../dispatcher");
+var KeyCode = {
+ ENTER: 13,
+ TAB: 9,
+ SHIFT: 16,
+ UP: 38,
+ DOWN: 40
+};
+
+var TYPING_USER_TIMEOUT = 10000;
+var TYPING_SERVER_TIMEOUT = 30000;
module.exports = {
+ componentWillMount: function() {
+ this.tabStruct = {
+ completing: false,
+ original: null,
+ index: 0
+ };
+ this.sentHistory = {
+ // The list of typed messages. Index 0 is more recent
+ data: [],
+ // The position in data currently displayed
+ position: -1,
+ // The room the history is for.
+ roomId: null,
+ // The original text before they hit UP
+ originalText: null,
+ // The textarea element to set text to.
+ element: null,
+
+ init: function(element, roomId) {
+ this.roomId = roomId;
+ this.element = element;
+ this.position = -1;
+ var storedData = window.sessionStorage.getItem(
+ "history_" + roomId
+ );
+ if (storedData) {
+ this.data = JSON.parse(storedData);
+ }
+ if (this.roomId) {
+ this.setLastTextEntry();
+ }
+ },
+
+ push: function(text) {
+ // store a message in the sent history
+ this.data.unshift(text);
+ window.sessionStorage.setItem(
+ "history_" + this.roomId,
+ JSON.stringify(this.data)
+ );
+ // reset history position
+ this.position = -1;
+ this.originalText = null;
+ },
+
+ // move in the history. Returns true if we managed to move.
+ next: function(offset) {
+ if (this.position === -1) {
+ // user is going into the history, save the current line.
+ this.originalText = this.element.value;
+ }
+ else {
+ // user may have modified this line in the history; remember it.
+ this.data[this.position] = this.element.value;
+ }
+
+ if (offset > 0 && this.position === (this.data.length - 1)) {
+ // we've run out of history
+ return false;
+ }
+
+ // retrieve the next item (bounded).
+ var newPosition = this.position + offset;
+ newPosition = Math.max(-1, newPosition);
+ newPosition = Math.min(newPosition, this.data.length - 1);
+ this.position = newPosition;
+
+ if (this.position !== -1) {
+ // show the message
+ this.element.value = this.data[this.position];
+ }
+ else if (this.originalText !== undefined) {
+ // restore the original text the user was typing.
+ this.element.value = this.originalText;
+ }
+ return true;
+ },
+
+ saveLastTextEntry: function() {
+ // save the currently entered text in order to restore it later.
+ // NB: This isn't 'originalText' because we want to restore
+ // sent history items too!
+ var text = this.element.value;
+ window.sessionStorage.setItem("input_" + this.roomId, text);
+ },
+
+ setLastTextEntry: function() {
+ var text = window.sessionStorage.getItem("input_" + this.roomId);
+ if (text) {
+ this.element.value = text;
+ }
+ }
+ };
+ },
+
componentDidMount: function() {
this.dispatcherRef = dis.register(this.onAction);
+ this.sentHistory.init(
+ this.refs.textarea.getDOMNode(),
+ this.props.room.roomId
+ );
},
componentWillUnmount: function() {
dis.unregister(this.dispatcherRef);
+ this.sentHistory.saveLastTextEntry();
},
onAction: function(payload) {
@@ -38,30 +152,258 @@ module.exports = {
},
onKeyDown: function (ev) {
- if (ev.keyCode == 13) {
- var contentText = this.refs.textarea.getDOMNode().value;
-
- var content = null;
- if (/^\/me /i.test(contentText)) {
- content = {
- msgtype: 'm.emote',
- body: contentText.substring(4)
- };
- } else {
- content = {
- msgtype: 'm.text',
- body: contentText
- };
+ if (ev.keyCode === KeyCode.ENTER) {
+ this.sentHistory.push(this.refs.textarea.getDOMNode().value);
+ this.onEnter(ev);
+ }
+ else if (ev.keyCode === KeyCode.TAB) {
+ var members = [];
+ if (this.props.room) {
+ members = this.props.room.getJoinedMembers();
}
-
- MatrixClientPeg.get().sendMessage(this.props.roomId, content).then(function() {
- dis.dispatch({
- action: 'message_sent'
- });
- });
- this.refs.textarea.getDOMNode().value = '';
+ this.onTab(ev, members);
+ }
+ else if (ev.keyCode === KeyCode.UP || ev.keyCode === KeyCode.DOWN) {
+ this.sentHistory.next(
+ ev.keyCode === KeyCode.UP ? 1 : -1
+ );
ev.preventDefault();
}
+ else if (ev.keyCode !== KeyCode.SHIFT && this.tabStruct.completing) {
+ // they're resuming typing; reset tab complete state vars.
+ this.tabStruct.completing = false;
+ this.tabStruct.index = 0;
+ }
+
+ var self = this;
+ setTimeout(function() {
+ if (self.refs.textarea.getDOMNode().value != '') {
+ self.onTypingActivity();
+ } else {
+ self.onFinishedTyping();
+ }
+ }, 10);
},
+
+ onEnter: function(ev) {
+ var contentText = this.refs.textarea.getDOMNode().value;
+
+ var cmd = SlashCommands.processInput(this.props.room.roomId, contentText);
+ if (cmd) {
+ ev.preventDefault();
+ if (!cmd.error) {
+ this.refs.textarea.getDOMNode().value = '';
+ }
+ if (cmd.promise) {
+ cmd.promise.done(function() {
+ console.log("Command success.");
+ }, function(err) {
+ console.error("Command failure: %s", err);
+ Modal.createDialog(ErrorDialog, {
+ title: "Server error",
+ description: err.message
+ });
+ });
+ }
+ else if (cmd.error) {
+ console.error(cmd.error);
+ Modal.createDialog(ErrorDialog, {
+ title: "Command error",
+ description: cmd.error
+ });
+ }
+ return;
+ }
+
+ var content = null;
+ if (/^\/me /i.test(contentText)) {
+ content = {
+ msgtype: 'm.emote',
+ body: contentText.substring(4)
+ };
+ } else {
+ content = {
+ msgtype: 'm.text',
+ body: contentText
+ };
+ }
+
+ MatrixClientPeg.get().sendMessage(this.props.room.roomId, content).then(function() {
+ dis.dispatch({
+ action: 'message_sent'
+ });
+ }, function() {
+ dis.dispatch({
+ action: 'message_send_failed'
+ });
+ });
+ this.refs.textarea.getDOMNode().value = '';
+ ev.preventDefault();
+ },
+
+ onTab: function(ev, sortedMembers) {
+ var textArea = this.refs.textarea.getDOMNode();
+ if (!this.tabStruct.completing) {
+ this.tabStruct.completing = true;
+ this.tabStruct.index = 0;
+ // cache starting text
+ this.tabStruct.original = textArea.value;
+ }
+
+ // loop in the right direction
+ if (ev.shiftKey) {
+ this.tabStruct.index --;
+ if (this.tabStruct.index < 0) {
+ // wrap to the last search match, and fix up to a real index
+ // value after we've matched.
+ this.tabStruct.index = Number.MAX_VALUE;
+ }
+ }
+ else {
+ this.tabStruct.index++;
+ }
+
+ var searchIndex = 0;
+ var targetIndex = this.tabStruct.index;
+ var text = this.tabStruct.original;
+
+ var search = /@?([a-zA-Z0-9_\-:\.]+)$/.exec(text);
+ // console.log("Searched in '%s' - got %s", text, search);
+ if (targetIndex === 0) { // 0 is always the original text
+ textArea.value = text;
+ }
+ else if (search && search[1]) {
+ // console.log("search found: " + search+" from "+text);
+ var expansion;
+
+ // FIXME: could do better than linear search here
+ for (var i=0; i= targetIndex) {
+ break;
+ }
+ var userId = sortedMembers[i].userId;
+ // === 1 because mxids are @username
+ if (userId.toLowerCase().indexOf(search[1].toLowerCase()) === 1) {
+ expansion = userId;
+ searchIndex++;
+ }
+ }
+ }
+
+ if (searchIndex === targetIndex ||
+ targetIndex === Number.MAX_VALUE) {
+ // xchat-style tab complete, add a colon if tab
+ // completing at the start of the text
+ if (search[0].length === text.length) {
+ expansion += ": ";
+ }
+ else {
+ expansion += " ";
+ }
+ textArea.value = text.replace(
+ /@?([a-zA-Z0-9_\-:\.]+)$/, expansion
+ );
+ // cancel blink
+ textArea.style["background-color"] = "";
+ if (targetIndex === Number.MAX_VALUE) {
+ // wrap the index around to the last index found
+ this.tabStruct.index = searchIndex;
+ targetIndex = searchIndex;
+ }
+ }
+ else {
+ // console.log("wrapped!");
+ textArea.style["background-color"] = "#faa";
+ setTimeout(function() {
+ textArea.style["background-color"] = "";
+ }, 150);
+ textArea.value = text;
+ this.tabStruct.index = 0;
+ }
+ }
+ else {
+ this.tabStruct.index = 0;
+ }
+ // prevent the default TAB operation (typically focus shifting)
+ ev.preventDefault();
+ },
+
+ onTypingActivity: function() {
+ this.isTyping = true;
+ if (!this.userTypingTimer) {
+ this.sendTyping(true);
+ }
+ this.startUserTypingTimer();
+ this.startServerTypingTimer();
+ },
+
+ onFinishedTyping: function() {
+ this.isTyping = false;
+ this.sendTyping(false);
+ this.stopUserTypingTimer();
+ this.stopServerTypingTimer();
+ },
+
+ startUserTypingTimer: function() {
+ this.stopUserTypingTimer();
+ var self = this;
+ this.userTypingTimer = setTimeout(function() {
+ self.isTyping = false;
+ self.sendTyping(self.isTyping);
+ self.userTypingTimer = null;
+ }, TYPING_USER_TIMEOUT);
+ },
+
+ stopUserTypingTimer: function() {
+ if (this.userTypingTimer) {
+ clearTimeout(this.userTypingTimer);
+ this.userTypingTimer = null;
+ }
+ },
+
+ startServerTypingTimer: function() {
+ if (!this.serverTypingTimer) {
+ var self = this;
+ this.serverTypingTimer = setTimeout(function() {
+ if (self.isTyping) {
+ self.sendTyping(self.isTyping);
+ self.startServerTypingTimer();
+ }
+ }, TYPING_SERVER_TIMEOUT / 2);
+ }
+ },
+
+ stopServerTypingTimer: function() {
+ if (this.serverTypingTimer) {
+ clearTimeout(this.servrTypingTimer);
+ this.serverTypingTimer = null;
+ }
+ },
+
+ sendTyping: function(isTyping) {
+ MatrixClientPeg.get().sendTyping(
+ this.props.room.roomId,
+ this.isTyping, TYPING_SERVER_TIMEOUT
+ ).done();
+ },
+
+ refreshTyping: function() {
+ if (this.typingTimeout) {
+ clearTimeout(this.typingTimeout);
+ this.typingTimeout = null;
+ }
+
+ }
};
diff --git a/src/controllers/molecules/MessageTile.js b/src/controllers/molecules/MessageTile.js
index 953e33b5..47b616e7 100644
--- a/src/controllers/molecules/MessageTile.js
+++ b/src/controllers/molecules/MessageTile.js
@@ -23,6 +23,28 @@ module.exports = {
var actions = MatrixClientPeg.get().getPushActionsForEvent(this.props.mxEvent);
if (!actions || !actions.tweaks) { return false; }
return actions.tweaks.highlight;
+ },
+
+ getInitialState: function() {
+ return {
+ resending: false
+ };
+ },
+
+ onResend: function() {
+ var self = this;
+ self.setState({
+ resending: true
+ });
+ MatrixClientPeg.get().resendEvent(
+ this.props.mxEvent, MatrixClientPeg.get().getRoom(
+ this.props.mxEvent.getRoomId()
+ )
+ ).finally(function() {
+ self.setState({
+ resending: false
+ });
+ })
}
};
diff --git a/src/controllers/molecules/RoomHeader.js b/src/controllers/molecules/RoomHeader.js
index 8aa688b2..2ef99953 100644
--- a/src/controllers/molecules/RoomHeader.js
+++ b/src/controllers/molecules/RoomHeader.js
@@ -16,6 +16,80 @@ limitations under the License.
'use strict';
-module.exports = {
-};
+/*
+ * State vars:
+ * this.state.call_state = the UI state of the call (see CallHandler)
+ */
+var React = require('react');
+var dis = require("../../dispatcher");
+var CallHandler = require("../../CallHandler");
+
+module.exports = {
+ propTypes: {
+ room: React.PropTypes.object.isRequired,
+ editing: React.PropTypes.bool,
+ onSettingsClick: React.PropTypes.func,
+ onSaveClick: React.PropTypes.func,
+ },
+
+ getDefaultProps: function() {
+ return {
+ editing: false,
+ onSettingsClick: function() {},
+ onSaveClick: function() {},
+ };
+ },
+
+ componentDidMount: function() {
+ this.dispatcherRef = dis.register(this.onAction);
+ if (this.props.room) {
+ var call = CallHandler.getCall(this.props.room.roomId);
+ var callState = call ? call.call_state : "ended";
+ this.setState({
+ call_state: callState
+ });
+ }
+ },
+
+ componentWillUnmount: function() {
+ dis.unregister(this.dispatcherRef);
+ },
+
+ onAction: function(payload) {
+ // if we were given a room_id to track, don't handle anything else.
+ if (payload.room_id && this.props.room &&
+ this.props.room.roomId !== payload.room_id) {
+ return;
+ }
+ if (payload.action !== 'call_state') {
+ return;
+ }
+ var call = CallHandler.getCall(payload.room_id);
+ var callState = call ? call.call_state : "ended";
+ this.setState({
+ call_state: callState
+ });
+ },
+
+ onVideoClick: function() {
+ dis.dispatch({
+ action: 'place_call',
+ type: "video",
+ room_id: this.props.room.roomId
+ });
+ },
+ onVoiceClick: function() {
+ dis.dispatch({
+ action: 'place_call',
+ type: "voice",
+ room_id: this.props.room.roomId
+ });
+ },
+ onHangupClick: function() {
+ dis.dispatch({
+ action: 'hangup',
+ room_id: this.props.room.roomId
+ });
+ }
+};
diff --git a/src/controllers/atoms/create_room/RoomNameTextbox.js b/src/controllers/molecules/RoomSettings.js
similarity index 72%
rename from src/controllers/atoms/create_room/RoomNameTextbox.js
rename to src/controllers/molecules/RoomSettings.js
index e78692d9..fe7cd634 100644
--- a/src/controllers/atoms/create_room/RoomNameTextbox.js
+++ b/src/controllers/molecules/RoomSettings.js
@@ -20,22 +20,12 @@ var React = require('react');
module.exports = {
propTypes: {
- default_name: React.PropTypes.string
- },
-
- getDefaultProps: function() {
- return {
- default_name: '',
- };
+ room: React.PropTypes.object.isRequired,
},
getInitialState: function() {
return {
- room_name: this.props.default_name,
- }
- },
-
- getName: function() {
- return this.state.room_name;
- },
+ power_levels_changed: false
+ };
+ }
};
diff --git a/src/controllers/molecules/ServerConfig.js b/src/controllers/molecules/ServerConfig.js
index 3cd5156b..3f5dd99b 100644
--- a/src/controllers/molecules/ServerConfig.js
+++ b/src/controllers/molecules/ServerConfig.js
@@ -30,26 +30,28 @@ module.exports = {
return {
onHsUrlChanged: function() {},
onIsUrlChanged: function() {},
- default_hs_url: 'https://matrix.org/',
- default_is_url: 'https://matrix.org/'
+ defaultHsUrl: 'https://matrix.org/',
+ defaultIsUrl: 'https://matrix.org/'
};
},
getInitialState: function() {
return {
- hs_url: this.props.default_hs_url,
- is_url: this.props.default_is_url,
+ hs_url: this.props.defaultHsUrl,
+ is_url: this.props.defaultIsUrl,
}
},
hsChanged: function(ev) {
- this.setState({hs_url: ev.target.value});
- this.props.onHsUrlChanged(this.state.hs_url);
+ this.setState({hs_url: ev.target.value}, function() {
+ this.props.onHsUrlChanged(this.state.hs_url);
+ });
},
isChanged: function(ev) {
- this.setState({is_url: ev.target.value});
- this.props.onIsUrlChanged(this.state.is_url);
+ this.setState({is_url: ev.target.value}, function() {
+ this.props.onIsUrlChanged(this.state.is_url);
+ });
},
getHsUrl: function() {
diff --git a/src/controllers/molecules/UserSelector.js b/src/controllers/molecules/UserSelector.js
index e7e05096..67a56163 100644
--- a/src/controllers/molecules/UserSelector.js
+++ b/src/controllers/molecules/UserSelector.js
@@ -20,38 +20,26 @@ var React = require('react');
module.exports = {
propTypes: {
- initially_selected: React.PropTypes.arrayOf(React.PropTypes.string),
+ onChange: React.PropTypes.func,
+ selected_users: React.PropTypes.arrayOf(React.PropTypes.string),
},
getDefaultProps: function() {
return {
- initially_selected: [],
+ onChange: function() {},
+ selected: [],
};
},
- getInitialState: function() {
- return {
- selected_users: this.props.initially_selected,
- }
- },
-
addUser: function(user_id) {
- if (this.state.selected_users.indexOf(user_id == -1)) {
- this.setState({
- selected_users: this.state.selected_users.concat([user_id]),
- });
+ if (this.props.selected_users.indexOf(user_id == -1)) {
+ this.props.onChange(this.props.selected_users.concat([user_id]));
}
},
removeUser: function(user_id) {
- this.setState({
- selected_users: this.state.selected_users.filter(function(e) {
- return e != user_id;
- }),
- });
+ this.props.onChange(this.props.selected_users.filter(function(e) {
+ return e != user_id;
+ }));
},
-
- getUserIds: function() {
- return this.state.selected_users;
- }
};
diff --git a/src/controllers/molecules/voip/CallView.js b/src/controllers/molecules/voip/CallView.js
new file mode 100644
index 00000000..e43046a5
--- /dev/null
+++ b/src/controllers/molecules/voip/CallView.js
@@ -0,0 +1,71 @@
+/*
+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 dis = require("../../../dispatcher");
+var CallHandler = require("../../../CallHandler");
+
+/*
+ * State vars:
+ * this.state.call = MatrixCall|null
+ *
+ * Props:
+ * this.props.room = Room (JS SDK)
+ */
+
+module.exports = {
+
+ componentDidMount: function() {
+ this.dispatcherRef = dis.register(this.onAction);
+ if (this.props.room) {
+ this.showCall(this.props.room.roomId);
+ }
+ },
+
+ componentWillUnmount: function() {
+ dis.unregister(this.dispatcherRef);
+ },
+
+ onAction: function(payload) {
+ // if we were given a room_id to track, don't handle anything else.
+ if (payload.room_id && this.props.room &&
+ this.props.room.roomId !== payload.room_id) {
+ return;
+ }
+ if (payload.action !== 'call_state') {
+ return;
+ }
+ this.showCall(payload.room_id);
+ },
+
+ showCall: function(roomId) {
+ var call = CallHandler.getCall(roomId);
+ if (call) {
+ call.setLocalVideoElement(this.getVideoView().getLocalVideoElement());
+ // N.B. the remote video element is used for playback for audio for voice calls
+ call.setRemoteVideoElement(this.getVideoView().getRemoteVideoElement());
+ }
+ if (call && call.type === "video" && call.state !== 'ended') {
+ this.getVideoView().getLocalVideoElement().style.display = "initial";
+ this.getVideoView().getRemoteVideoElement().style.display = "initial";
+ }
+ else {
+ this.getVideoView().getLocalVideoElement().style.display = "none";
+ this.getVideoView().getRemoteVideoElement().style.display = "none";
+ }
+ }
+};
+
diff --git a/src/controllers/molecules/voip/IncomingCallBox.js b/src/controllers/molecules/voip/IncomingCallBox.js
new file mode 100644
index 00000000..809c0833
--- /dev/null
+++ b/src/controllers/molecules/voip/IncomingCallBox.js
@@ -0,0 +1,75 @@
+/*
+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 dis = require("../../../dispatcher");
+var CallHandler = require("../../../CallHandler");
+
+module.exports = {
+ componentDidMount: function() {
+ this.dispatcherRef = dis.register(this.onAction);
+ },
+
+ componentWillUnmount: function() {
+ dis.unregister(this.dispatcherRef);
+ },
+
+ getInitialState: function() {
+ return {
+ incomingCall: null
+ }
+ },
+
+ onAction: function(payload) {
+ if (payload.action !== 'call_state') {
+ return;
+ }
+ var call = CallHandler.getCall(payload.room_id);
+ if (!call || call.call_state !== 'ringing') {
+ this.setState({
+ incomingCall: null,
+ });
+ this.getRingAudio().pause();
+ return;
+ }
+ if (call.call_state === "ringing") {
+ this.getRingAudio().load();
+ this.getRingAudio().play();
+ }
+ else {
+ this.getRingAudio().pause();
+ }
+
+ this.setState({
+ incomingCall: call
+ });
+ },
+
+ onAnswerClick: function() {
+ dis.dispatch({
+ action: 'answer',
+ room_id: this.state.incomingCall.roomId
+ });
+ },
+ onRejectClick: function() {
+ dis.dispatch({
+ action: 'hangup',
+ room_id: this.state.incomingCall.roomId
+ });
+ }
+};
+
diff --git a/src/controllers/molecules/voip/MCallAnswerTile.js b/src/controllers/molecules/voip/MCallAnswerTile.js
new file mode 100644
index 00000000..d0977e00
--- /dev/null
+++ b/src/controllers/molecules/voip/MCallAnswerTile.js
@@ -0,0 +1,20 @@
+/*
+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';
+
+module.exports = {
+};
diff --git a/src/controllers/molecules/voip/MCallHangupTile.js b/src/controllers/molecules/voip/MCallHangupTile.js
new file mode 100644
index 00000000..d0977e00
--- /dev/null
+++ b/src/controllers/molecules/voip/MCallHangupTile.js
@@ -0,0 +1,20 @@
+/*
+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';
+
+module.exports = {
+};
diff --git a/src/controllers/molecules/voip/MCallInviteTile.js b/src/controllers/molecules/voip/MCallInviteTile.js
new file mode 100644
index 00000000..d0977e00
--- /dev/null
+++ b/src/controllers/molecules/voip/MCallInviteTile.js
@@ -0,0 +1,20 @@
+/*
+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';
+
+module.exports = {
+};
diff --git a/src/controllers/molecules/voip/VideoView.js b/src/controllers/molecules/voip/VideoView.js
new file mode 100644
index 00000000..8aa688b2
--- /dev/null
+++ b/src/controllers/molecules/voip/VideoView.js
@@ -0,0 +1,21 @@
+/*
+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';
+
+module.exports = {
+};
+
diff --git a/src/controllers/organisms/CreateRoom.js b/src/controllers/organisms/CreateRoom.js
index c2112ce5..f6404eb2 100644
--- a/src/controllers/organisms/CreateRoom.js
+++ b/src/controllers/organisms/CreateRoom.js
@@ -18,6 +18,9 @@ limitations under the License.
var React = require("react");
var MatrixClientPeg = require("../../MatrixClientPeg");
+var PresetValues = require('../atoms/create_room/Presets').Presets;
+var q = require('q');
+var encryption = require("../../encryption");
module.exports = {
propTypes: {
@@ -41,25 +44,52 @@ module.exports = {
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 = {};
- var room_name = this.getName();
- if (room_name) {
- options.name = room_name;
+ if (this.state.room_name) {
+ options.name = this.state.room_name;
}
- var preset = this.getPreset();
- if (preset) {
- options.preset = preset;
+ if (this.state.topic) {
+ options.topic = this.state.topic;
}
- var invited_users = this.getInvitedUsers();
- if (invited_users) {
- options.invite = invited_users;
+ 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_rules": 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();
@@ -69,7 +99,20 @@ module.exports = {
return;
}
- var deferred = MatrixClientPeg.get().createRoom(options);
+ var deferred = cli.createRoom(options);
+
+ var response;
+
+ if (this.state.encrypt) {
+ deferred = deferred.then(function(res) {
+ response = res;
+ return encryption.enableEncryption(
+ cli, response.roomId, options.invite
+ );
+ }).then(function() {
+ return q(response) }
+ );
+ }
this.setState({
phase: this.phases.CREATING,
@@ -77,11 +120,11 @@ module.exports = {
var self = this;
- deferred.then(function () {
+ deferred.then(function (resp) {
self.setState({
phase: self.phases.CREATED,
});
- self.props.onRoomCreated();
+ self.props.onRoomCreated(resp.room_id);
}, function(err) {
self.setState({
phase: self.phases.ERROR,
diff --git a/src/controllers/organisms/ErrorDialog.js b/src/controllers/organisms/ErrorDialog.js
new file mode 100644
index 00000000..73f66c87
--- /dev/null
+++ b/src/controllers/organisms/ErrorDialog.js
@@ -0,0 +1,39 @@
+/*
+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");
+
+module.exports = {
+ propTypes: {
+ title: React.PropTypes.string,
+ description: React.PropTypes.string,
+ button: React.PropTypes.string,
+ focus: React.PropTypes.bool,
+ onFinished: React.PropTypes.func.isRequired,
+ },
+
+ getDefaultProps: function() {
+ var self = this;
+ return {
+ title: "Error",
+ description: "An error has occurred.",
+ button: "OK",
+ focus: true,
+ };
+ },
+};
diff --git a/src/controllers/organisms/LogoutPrompt.js b/src/controllers/organisms/LogoutPrompt.js
new file mode 100644
index 00000000..8875a55c
--- /dev/null
+++ b/src/controllers/organisms/LogoutPrompt.js
@@ -0,0 +1,35 @@
+/*
+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 dis = require("../../dispatcher");
+
+module.exports = {
+ logOut: function() {
+ dis.dispatch({action: 'logout'});
+ if (this.props.onFinished) {
+ this.props.onFinished();
+ }
+ },
+
+ cancelPrompt: function() {
+ if (this.props.onFinished) {
+ this.props.onFinished();
+ }
+ }
+};
+
diff --git a/src/controllers/organisms/MemberList.js b/src/controllers/organisms/MemberList.js
index a511816d..6962863c 100644
--- a/src/controllers/organisms/MemberList.js
+++ b/src/controllers/organisms/MemberList.js
@@ -18,6 +18,9 @@ limitations under the License.
var React = require("react");
var MatrixClientPeg = require("../../MatrixClientPeg");
+var Modal = require("../../Modal");
+var ComponentBroker = require('../../ComponentBroker');
+var ErrorDialog = ComponentBroker.get("organisms/ErrorDialog");
var INITIAL_LOAD_NUM_MEMBERS = 50;
@@ -37,19 +40,31 @@ module.exports = {
componentWillUnmount: function() {
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember);
+ MatrixClientPeg.get().removeListener("User.presence", this.userPresenceFn);
}
},
componentDidMount: function() {
- var that = this;
+ var self = this;
setTimeout(function() {
- if (!that.isMounted()) return;
- that.setState({
- memberDict: that.roomMembers()
+ if (!self.isMounted()) return;
+ self.setState({
+ memberDict: 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) {
+ var tile = self.refs[user.userId];
+ if (tile) {
+ tile.forceUpdate();
+ }
+ }
+ 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) {
},*/
@@ -61,10 +76,59 @@ module.exports = {
});
},
+ onInvite: function(inputText) {
+ var self = this;
+ // sanity check the input
+ inputText = inputText.trim(); // react requires es5-shim so we know trim() exists
+ if (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;
+ }
+ self.setState({
+ inviting: true
+ });
+ console.log("Invite %s to %s", inputText, this.props.roomId);
+ MatrixClientPeg.get().invite(this.props.roomId, inputText).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
+ });
+ });
+ },
+
roomMembers: function(limit) {
+ if (!this.props.roomId) return {};
var cli = MatrixClientPeg.get();
- var all_members = cli.getRoom(this.props.roomId).currentState.members;
+ var room = cli.getRoom(this.props.roomId);
+ if (!room) return {};
+ var all_members = room.currentState.members;
var all_user_ids = Object.keys(all_members);
+
+ all_user_ids.sort(function(userIdA, userIdB) {
+ var userA = all_members[userIdA].user;
+ var userB = all_members[userIdB].user;
+
+ var latA = userA ? userA.lastActiveAgo || Number.MAX_VALUE : Number.MAX_VALUE;
+ var latB = userB ? userB.lastActiveAgo || Number.MAX_VALUE : Number.MAX_VALUE;
+
+ return latA - latB;
+ });
+
+
var to_display = {};
var count = 0;
for (var i = 0; i < all_user_ids.length && (limit === undefined || count < limit); ++i) {
diff --git a/src/controllers/organisms/Notifier.js b/src/controllers/organisms/Notifier.js
index 63e93778..29618d8d 100644
--- a/src/controllers/organisms/Notifier.js
+++ b/src/controllers/organisms/Notifier.js
@@ -17,6 +17,15 @@ limitations under the License.
'use strict';
var MatrixClientPeg = require("../../MatrixClientPeg");
+var dis = require("../../dispatcher");
+
+/*
+ * Dispatches:
+ * {
+ * action: "notifier_enabled",
+ * value: boolean
+ * }
+ */
module.exports = {
start: function() {
@@ -30,12 +39,67 @@ module.exports = {
}
},
+ supportsDesktopNotifications: function() {
+ return !!global.Notification;
+ },
+
+ havePermission: function() {
+ return global.Notification.permission == 'granted';
+ },
+
+ setEnabled: function(enable, callback) {
+ console.log("Notifier.setEnabled => %s", enable);
+ if(enable) {
+ if (!this.havePermission()) {
+ var self = this;
+ global.Notification.requestPermission(function() {
+ if (callback) {
+ callback();
+ dis.dispatch({
+ action: "notifier_enabled",
+ value: true
+ });
+ }
+ });
+ }
+
+ if (!global.localStorage) return;
+ global.localStorage.setItem('notifications_enabled', 'true');
+
+ if (this.havePermission) {
+ dis.dispatch({
+ action: "notifier_enabled",
+ value: true
+ });
+ }
+ }
+ else {
+ if (!global.localStorage) return;
+ global.localStorage.setItem('notifications_enabled', 'false');
+ dis.dispatch({
+ action: "notifier_enabled",
+ value: false
+ });
+ }
+ },
+
+ isEnabled: function() {
+ if (!this.havePermission()) return false;
+
+ if (!global.localStorage) return true;
+
+ var enabled = global.localStorage.getItem('notifications_enabled');
+ if (enabled === null) return true;
+ return enabled === 'true';
+ },
+
onRoomTimeline: function(ev, room, toStartOfTimeline) {
if (toStartOfTimeline) return;
if (ev.sender && ev.sender.userId == MatrixClientPeg.get().credentials.userId) return;
- var enabled = global.localStorage.getItem('notifications_enabled');
- if (enabled === 'false') return;
+ if (!this.isEnabled()) {
+ return;
+ }
var actions = MatrixClientPeg.get().getPushActionsForEvent(ev);
if (actions && actions.notify) {
diff --git a/src/controllers/organisms/RoomList.js b/src/controllers/organisms/RoomList.js
index 03e18547..4adeaf2e 100644
--- a/src/controllers/organisms/RoomList.js
+++ b/src/controllers/organisms/RoomList.js
@@ -100,16 +100,16 @@ module.exports = {
},
makeRoomTiles: function() {
- var that = this;
+ var self = this;
return this.state.roomList.map(function(room) {
- var selected = room.roomId == that.props.selectedRoom;
+ var selected = room.roomId == self.props.selectedRoom;
return (
);
});
diff --git a/src/controllers/organisms/RoomView.js b/src/controllers/organisms/RoomView.js
index 10b375cd..c6a0735b 100644
--- a/src/controllers/organisms/RoomView.js
+++ b/src/controllers/organisms/RoomView.js
@@ -20,6 +20,11 @@ var MatrixClientPeg = require("../../MatrixClientPeg");
var React = require("react");
var q = require("q");
var ContentMessages = require("../../ContentMessages");
+var WhoIsTyping = require("../../WhoIsTyping");
+var Modal = require("../../Modal");
+var ComponentBroker = require('../../ComponentBroker');
+
+var ErrorDialog = ComponentBroker.get("organisms/ErrorDialog");
var dis = require("../../dispatcher");
@@ -27,23 +32,34 @@ var PAGINATE_SIZE = 20;
var INITIAL_SIZE = 100;
var ComponentBroker = require('../../ComponentBroker');
+var Notifier = ComponentBroker.get('organisms/Notifier');
var tileTypes = {
'm.room.message': ComponentBroker.get('molecules/MessageTile'),
- 'm.room.member': ComponentBroker.get('molecules/MRoomMemberTile')
+ 'm.room.member': ComponentBroker.get('molecules/MRoomMemberTile'),
+ 'm.call.invite': ComponentBroker.get('molecules/voip/MCallInviteTile'),
+ 'm.call.answer': ComponentBroker.get('molecules/voip/MCallAnswerTile'),
+ 'm.call.hangup': ComponentBroker.get('molecules/voip/MCallHangupTile'),
+ 'm.room.topic': ComponentBroker.get('molecules/EventAsTextTile'),
};
+var DateSeparator = ComponentBroker.get('molecules/DateSeparator');
+
module.exports = {
getInitialState: function() {
return {
room: this.props.roomId ? MatrixClientPeg.get().getRoom(this.props.roomId) : null,
- messageCap: INITIAL_SIZE
+ messageCap: INITIAL_SIZE,
+ editingRoomSettings: false,
+ uploadingRoomSettings: false,
}
},
componentWillMount: function() {
this.dispatcherRef = dis.register(this.onAction);
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
+ MatrixClientPeg.get().on("Room.name", this.onRoomName);
+ MatrixClientPeg.get().on("RoomMember.typing", this.onRoomMemberTyping);
this.atBottom = true;
},
@@ -51,19 +67,27 @@ module.exports = {
if (this.refs.messageWrapper) {
var messageWrapper = this.refs.messageWrapper.getDOMNode();
messageWrapper.removeEventListener('drop', this.onDrop);
+ messageWrapper.removeEventListener('dragover', this.onDragOver);
}
dis.unregister(this.dispatcherRef);
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline);
+ MatrixClientPeg.get().removeListener("Room.name", this.onRoomName);
+ MatrixClientPeg.get().removeListener("RoomMember.typing", this.onRoomMemberTyping);
}
},
onAction: function(payload) {
switch (payload.action) {
+ case 'message_send_failed':
case 'message_sent':
this.setState({
room: MatrixClientPeg.get().getRoom(this.props.roomId)
});
+ this.forceUpdate();
+ break;
+ case 'notifier_enabled':
+ this.forceUpdate();
break;
}
},
@@ -87,7 +111,7 @@ module.exports = {
// we'll only be showing a spinner.
if (this.state.joining) return;
if (room.roomId != this.props.roomId) return;
-
+
if (this.refs.messageWrapper) {
var messageWrapper = this.refs.messageWrapper.getDOMNode();
this.atBottom = messageWrapper.scrollHeight - messageWrapper.scrollTop <= messageWrapper.clientHeight;
@@ -101,6 +125,18 @@ module.exports = {
}
},
+ onRoomName: function(room) {
+ if (room.roomId == this.props.roomId) {
+ this.setState({
+ room: room
+ });
+ }
+ },
+
+ onRoomMemberTyping: function(ev, member) {
+ this.forceUpdate();
+ },
+
componentDidMount: function() {
if (this.refs.messageWrapper) {
var messageWrapper = this.refs.messageWrapper.getDOMNode();
@@ -146,12 +182,12 @@ module.exports = {
this.waiting_for_paginate = true;
var cap = this.state.messageCap + PAGINATE_SIZE;
this.setState({messageCap: cap, paginating: true});
- var that = this;
+ var self = this;
MatrixClientPeg.get().scrollback(this.state.room, PAGINATE_SIZE).finally(function() {
- that.waiting_for_paginate = false;
- if (that.isMounted()) {
- that.setState({
- room: MatrixClientPeg.get().getRoom(that.props.roomId)
+ self.waiting_for_paginate = false;
+ if (self.isMounted()) {
+ self.setState({
+ room: MatrixClientPeg.get().getRoom(self.props.roomId)
});
}
// wait and set paginating to false when the component updates
@@ -164,14 +200,14 @@ module.exports = {
},
onJoinButtonClicked: function(ev) {
- var that = this;
+ var self = this;
MatrixClientPeg.get().joinRoom(this.props.roomId).then(function() {
- that.setState({
+ self.setState({
joining: false,
- room: MatrixClientPeg.get().getRoom(that.props.roomId)
+ room: MatrixClientPeg.get().getRoom(self.props.roomId)
});
}, function(error) {
- that.setState({
+ self.setState({
joining: false,
joinError: error
});
@@ -207,18 +243,44 @@ module.exports = {
ev.stopPropagation();
ev.preventDefault();
var files = ev.dataTransfer.files;
-
if (files.length == 1) {
- ContentMessages.sendContentToRoom(
- files[0], this.props.roomId, MatrixClientPeg.get()
- ).progress(function(ev) {
- //console.log("Upload: "+ev.loaded+" / "+ev.total);
- }).done(undefined, function() {
- // display error message
- });
+ this.uploadFile(files[0]);
}
},
+ uploadFile: function(file) {
+ this.setState({
+ upload: {
+ fileName: file.name,
+ uploadedBytes: 0,
+ totalBytes: file.size
+ }
+ });
+ var self = this;
+ ContentMessages.sendContentToRoom(
+ file, this.props.roomId, MatrixClientPeg.get()
+ ).progress(function(ev) {
+ //console.log("Upload: "+ev.loaded+" / "+ev.total);
+ self.setState({
+ upload: {
+ fileName: file.name,
+ uploadedBytes: ev.loaded,
+ totalBytes: ev.total
+ }
+ });
+ }).finally(function() {
+ self.setState({
+ upload: undefined
+ });
+ }).done(undefined, function() {
+ // display error message
+ });
+ },
+
+ getWhoIsTypingString: function() {
+ return WhoIsTyping.whoIsTypingString(this.state.room);
+ },
+
getEventTiles: function() {
var ret = [];
var count = 0;
@@ -226,13 +288,126 @@ module.exports = {
for (var i = this.state.room.timeline.length-1; i >= 0 && count < this.state.messageCap; --i) {
var mxEv = this.state.room.timeline[i];
var TileType = tileTypes[mxEv.getType()];
+ var continuation = false;
+ var last = false;
+ var dateSeparator = null;
+ if (i == this.state.room.timeline.length - 1) {
+ last = true;
+ }
+ if (i > 0 && count < this.state.messageCap - 1) {
+ if (this.state.room.timeline[i].sender &&
+ this.state.room.timeline[i - 1].sender &&
+ (this.state.room.timeline[i].sender.userId ===
+ this.state.room.timeline[i - 1].sender.userId) &&
+ (this.state.room.timeline[i].getType() ==
+ this.state.room.timeline[i - 1].getType())
+ )
+ {
+ continuation = true;
+ }
+
+ var ts0 = this.state.room.timeline[i - 1].getTs();
+ var ts1 = this.state.room.timeline[i].getTs();
+ if (new Date(ts0).toDateString() !== new Date(ts1).toDateString()) {
+ dateSeparator = ;
+ continuation = false;
+ }
+ }
if (!TileType) continue;
ret.unshift(
-
+
);
+ if (dateSeparator) {
+ ret.unshift(dateSeparator);
+ }
++count;
}
return ret;
+ },
+
+ uploadNewState: function(new_name, new_topic, new_join_rule, new_history_visibility, new_power_levels) {
+ var old_name = this.state.room.name;
+
+ var old_topic = this.state.room.currentState.getStateEvents('m.room.topic', '');
+ if (old_topic) {
+ old_topic = old_topic.getContent().topic;
+ } else {
+ old_topic = "";
+ }
+
+ var old_join_rule = this.state.room.currentState.getStateEvents('m.room.join_rules', '');
+ if (old_join_rule) {
+ old_join_rule = old_join_rule.getContent().join_rule;
+ } else {
+ old_join_rule = "invite";
+ }
+
+ var old_history_visibility = this.state.room.currentState.getStateEvents('m.room.history_visibility', '');
+ if (old_history_visibility) {
+ old_history_visibility = old_history_visibility.getContent().history_visibility;
+ } else {
+ old_history_visibility = "shared";
+ }
+
+ var deferreds = [];
+
+ if (old_name != new_name && new_name != undefined && new_name) {
+ deferreds.push(
+ MatrixClientPeg.get().setRoomName(this.state.room.roomId, new_name)
+ );
+ }
+
+ if (old_topic != new_topic && new_topic != undefined) {
+ deferreds.push(
+ MatrixClientPeg.get().setRoomTopic(this.state.room.roomId, new_topic)
+ );
+ }
+
+ if (old_join_rule != new_join_rule && new_join_rule != undefined) {
+ deferreds.push(
+ MatrixClientPeg.get().sendStateEvent(
+ this.state.room.roomId, "m.room.join_rules", {
+ join_rule: new_join_rule,
+ }, ""
+ )
+ );
+ }
+
+ if (old_history_visibility != new_history_visibility && new_history_visibility != undefined) {
+ deferreds.push(
+ MatrixClientPeg.get().sendStateEvent(
+ this.state.room.roomId, "m.room.history_visibility", {
+ history_visibility: new_history_visibility,
+ }, ""
+ )
+ );
+ }
+
+ if (new_power_levels) {
+ deferreds.push(
+ MatrixClientPeg.get().sendStateEvent(
+ this.state.room.roomId, "m.room.power_levels", new_power_levels, ""
+ )
+ );
+ }
+
+ if (deferreds.length) {
+ var self = this;
+ q.all(deferreds).fail(function(err) {
+ Modal.createDialog(ErrorDialog, {
+ title: "Failed to set state",
+ description: err.toString()
+ });
+ }).finally(function() {
+ self.setState({
+ uploadingRoomSettings: false,
+ });
+ });
+ } else {
+ this.setState({
+ editingRoomSettings: false,
+ uploadingRoomSettings: false,
+ });
+ }
}
};
-
diff --git a/src/controllers/organisms/UserSettings.js b/src/controllers/organisms/UserSettings.js
new file mode 100644
index 00000000..4eb1fd59
--- /dev/null
+++ b/src/controllers/organisms/UserSettings.js
@@ -0,0 +1,72 @@
+/*
+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 MatrixClientPeg = require("../../MatrixClientPeg");
+var React = require("react");
+var q = require('q');
+var dis = require("../../dispatcher");
+var version = require('../../../package.json').version;
+
+var ComponentBroker = require('../../ComponentBroker');
+
+module.exports = {
+ Phases: {
+ Loading: "loading",
+ Display: "display",
+ },
+
+ getInitialState: function() {
+ return {
+ displayName: null,
+ avatarUrl: null,
+ threePids: [],
+ clientVersion: version,
+ phase: this.Phases.Loading,
+ };
+ },
+
+ changeDisplayname: function(new_displayname) {
+ if (this.state.displayName == new_displayname) return;
+
+ var self = this;
+ return MatrixClientPeg.get().setDisplayName(new_displayname).then(
+ function() { self.setState({displayName: new_displayname}); },
+ function(err) { console.err(err); }
+ );
+ },
+
+ 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({
+ displayName: resps[0].displayname,
+ avatarUrl: resps[0].avatar_url,
+ threepids: resps[1].threepids,
+ phase: self.Phases.Display,
+ });
+ },
+ function(err) { console.err(err); }
+ );
+ }
+}
diff --git a/src/controllers/pages/MatrixChat.js b/src/controllers/pages/MatrixChat.js
index 7c4e35c5..73f526c2 100644
--- a/src/controllers/pages/MatrixChat.js
+++ b/src/controllers/pages/MatrixChat.js
@@ -21,19 +21,38 @@ var Loader = require("react-loader");
var MatrixClientPeg = require("../../MatrixClientPeg");
var RoomListSorter = require("../../RoomListSorter");
-
+var Presence = require("../../Presence");
var dis = require("../../dispatcher");
var ComponentBroker = require('../../ComponentBroker');
-
var Notifier = ComponentBroker.get('organisms/Notifier');
module.exports = {
+ PageTypes: {
+ RoomView: "room_view",
+ UserSettings: "user_settings",
+ CreateRoom: "create_room",
+ RoomDirectory: "room_directory",
+ },
+
+ AuxPanel: {
+ RoomSettings: "room_settings",
+ },
+
getInitialState: function() {
- return {
+ var s = {
logged_in: !!(MatrixClientPeg.get() && MatrixClientPeg.get().credentials),
- ready: false
+ ready: false,
+ aux_panel: null,
};
+ if (s.logged_in) {
+ if (MatrixClientPeg.get().getRooms().length) {
+ s.page_type = this.PageTypes.RoomView;
+ } else {
+ s.page_type = this.PageTypes.RoomDirectory;
+ }
+ }
+ return s;
},
componentDidMount: function() {
@@ -54,6 +73,7 @@ module.exports = {
componentWillUnmount: function() {
dis.unregister(this.dispatcherRef);
document.removeEventListener("keydown", this.onKeyDown);
+ window.removeEventListener("focus", this.onFocus);
},
componentDidUpdate: function() {
@@ -76,8 +96,9 @@ module.exports = {
window.localStorage.clear();
}
Notifier.stop();
+ Presence.stop();
MatrixClientPeg.get().removeAllListeners();
- MatrixClientPeg.replace(null);
+ MatrixClientPeg.unset();
break;
case 'start_registration':
if (this.state.logged_in) return;
@@ -110,8 +131,10 @@ module.exports = {
case 'view_room':
this.focusComposer = true;
this.setState({
- currentRoom: payload.room_id
+ currentRoom: payload.room_id,
+ page_type: this.PageTypes.RoomView,
});
+ this.notifyNewScreen('room/'+payload.room_id);
break;
case 'view_prev_room':
roomIndexDelta = -1;
@@ -131,6 +154,24 @@ module.exports = {
currentRoom: allRooms[roomIndex].roomId
});
break;
+ case 'view_user_settings':
+ this.setState({
+ page_type: this.PageTypes.UserSettings,
+ });
+ break;
+ case 'view_create_room':
+ this.setState({
+ page_type: this.PageTypes.CreateRoom,
+ });
+ break;
+ case 'view_room_directory':
+ this.setState({
+ page_type: this.PageTypes.RoomDirectory,
+ });
+ break;
+ case 'notifier_enabled':
+ this.forceUpdate();
+ break;
}
},
@@ -145,18 +186,30 @@ module.exports = {
startMatrixClient: function() {
var cli = MatrixClientPeg.get();
- var that = this;
+ var self = this;
cli.on('syncComplete', function() {
- var firstRoom = null;
- if (cli.getRooms() && cli.getRooms().length) {
- firstRoom = RoomListSorter.mostRecentActivityFirst(
- cli.getRooms()
- )[0].roomId;
+ 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});
+ self.notifyNewScreen('room/'+firstRoom);
+ } else {
+ self.setState({ready: true});
}
- that.setState({ready: true, currentRoom: firstRoom});
dis.dispatch({action: 'focus_composer'});
});
+ cli.on('Call.incoming', function(call) {
+ dis.dispatch({
+ action: 'incoming_call',
+ call: call
+ });
+ });
Notifier.start();
+ Presence.start();
cli.startClient();
},
@@ -190,6 +243,12 @@ module.exports = {
action: 'start_login',
params: params
});
+ } else if (screen.indexOf('room/') == 0) {
+ var roomId = screen.split('/')[1];
+ dis.dispatch({
+ action: 'view_room',
+ room_id: roomId
+ });
}
},
@@ -199,4 +258,3 @@ module.exports = {
}
}
};
-
diff --git a/src/controllers/templates/Login.js b/src/controllers/templates/Login.js
index 51c2543b..37a95785 100644
--- a/src/controllers/templates/Login.js
+++ b/src/controllers/templates/Login.js
@@ -38,8 +38,7 @@ module.exports = {
this.setState({ step: step, errorText: '', busy: false });
},
- onHSChosen: function(ev) {
- ev.preventDefault();
+ onHSChosen: function() {
MatrixClientPeg.replaceUsingUrls(
this.getHsUrl(),
this.getIsUrl()
@@ -51,24 +50,24 @@ module.exports = {
this.setStep("fetch_stages");
var cli = MatrixClientPeg.get();
this.setState({busy: true});
- var that = this;
+ var self = this;
cli.loginFlows().done(function(result) {
- that.setState({
+ self.setState({
flows: result.flows,
currentStep: 1,
totalSteps: result.flows.length+1
});
- that.setStep('stage_'+result.flows[0].type);
+ self.setStep('stage_'+result.flows[0].type);
}, function(error) {
- that.setStep("choose_hs");
- that.setState({errorText: 'Unable to contact the given Home Server'});
+ self.setStep("choose_hs");
+ self.setState({errorText: 'Unable to contact the given Home Server'});
});
},
onUserPassEntered: function(ev) {
ev.preventDefault();
this.setState({busy: true});
- var that = this;
+ var self = this;
var formVals = this.getFormVals();
@@ -77,15 +76,15 @@ module.exports = {
'password': formVals.password
}).done(function(data) {
MatrixClientPeg.replaceUsingAccessToken(
- that.state.hs_url, that.state.is_url,
+ self.state.hs_url, self.state.is_url,
data.user_id, data.access_token
);
- if (that.props.onLoggedIn) {
- that.props.onLoggedIn();
+ if (self.props.onLoggedIn) {
+ self.props.onLoggedIn();
}
}, function(error) {
- that.setStep("stage_m.login.password");
- that.setState({errorText: 'Login failed.'});
+ self.setStep("stage_m.login.password");
+ self.setState({errorText: 'Login failed.'});
});
},
diff --git a/src/controllers/templates/Register.js b/src/controllers/templates/Register.js
index 29063fb6..faff4c66 100644
--- a/src/controllers/templates/Register.js
+++ b/src/controllers/templates/Register.js
@@ -326,6 +326,14 @@ module.exports = {
});
} else if (error.httpStatus == 401) {
newState.errorText = "Authorisation failed!";
+ } else if (error.httpStatus >= 400 && error.httpStatus < 500) {
+ newState.errorText = "Registration failed!";
+ } else if (error.httpStatus >= 500 && error.httpStatus < 600) {
+ newState.errorText = "Server error during registration!";
+ } else if (error.name == "M_MISSING_PARAM") {
+ // The HS hasn't remembered the login params from
+ // the first try when the login email was sent.
+ newState.errorText = "This home server does not support resuming registration.";
}
self.setState(newState);
}
diff --git a/src/encryption.js b/src/encryption.js
new file mode 100644
index 00000000..dea454a3
--- /dev/null
+++ b/src/encryption.js
@@ -0,0 +1,40 @@
+/*
+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';
+
+function enableEncyption(client, roomId, members) {
+ members = members.slice(0);
+ members.push(client.credentials.userId);
+ // TODO: Check the keys actually match what keys the user has.
+ // TODO: Don't redownload keys each time.
+ return client.downloadKeys(members, "forceDownload").then(function(res) {
+ return client.setRoomEncryption(roomId, {
+ algorithm: "m.olm.v1.curve25519-aes-sha2",
+ members: members,
+ });
+ })
+}
+
+function disableEncryption(client, roomId) {
+ return client.disableRoomEncryption(roomId);
+}
+
+
+module.exports = {
+ enableEncryption: enableEncyption,
+ disableEncryption: disableEncryption,
+}
diff --git a/src/linkify-matrix.js b/src/linkify-matrix.js
index 73a6296c..273fe123 100644
--- a/src/linkify-matrix.js
+++ b/src/linkify-matrix.js
@@ -20,11 +20,11 @@ var extend = require('./extend');
function matrixLinkify(linkify) {
// Text tokens
- var TT = linkify.scanner.TOKENS;
+ var TT = linkify.scanner.TOKENS;
var TextToken = TT.Base;
// Multi tokens
- var MT = linkify.parser.TOKENS;
- var MultiToken = MT.Base;
+ var MT = linkify.parser.TOKENS;
+ var MultiToken = MT.Base;
var S_START = linkify.parser.start;
@@ -35,12 +35,12 @@ function matrixLinkify(linkify) {
};
ROOMALIAS.prototype = new MultiToken();
- var S_HASH = new linkify.parser.State();
- var S_HASH_NAME = new linkify.parser.State();
- var S_HASH_NAME_COLON = new linkify.parser.State();
- var S_HASH_NAME_COLON_DOMAIN = new linkify.parser.State();
- var S_HASH_NAME_COLON_DOMAIN_DOT = new linkify.parser.State();
- var S_ROOMALIAS = new linkify.parser.State(ROOMALIAS);
+ var S_HASH = new linkify.parser.State();
+ var S_HASH_NAME = new linkify.parser.State();
+ var S_HASH_NAME_COLON = new linkify.parser.State();
+ var S_HASH_NAME_COLON_DOMAIN = new linkify.parser.State();
+ var S_HASH_NAME_COLON_DOMAIN_DOT = new linkify.parser.State();
+ var S_ROOMALIAS = new linkify.parser.State(ROOMALIAS);
var roomname_tokens = [
TT.DOT,
@@ -50,10 +50,10 @@ function matrixLinkify(linkify) {
TT.TLD
];
- S_START.on(TT.POUND, S_HASH);
+ S_START.on(TT.POUND, S_HASH);
- S_HASH.on(roomname_tokens, S_HASH_NAME);
- S_HASH_NAME.on(roomname_tokens, S_HASH_NAME);
+ S_HASH.on(roomname_tokens, S_HASH_NAME);
+ S_HASH_NAME.on(roomname_tokens, S_HASH_NAME);
S_HASH_NAME.on(TT.DOMAIN, S_HASH_NAME);
S_HASH_NAME.on(TT.COLON, S_HASH_NAME_COLON);
@@ -71,12 +71,12 @@ function matrixLinkify(linkify) {
};
USERID.prototype = new MultiToken();
- var S_AT = new linkify.parser.State();
- var S_AT_NAME = new linkify.parser.State();
- var S_AT_NAME_COLON = new linkify.parser.State();
- var S_AT_NAME_COLON_DOMAIN = new linkify.parser.State();
- var S_AT_NAME_COLON_DOMAIN_DOT = new linkify.parser.State();
- var S_USERID = new linkify.parser.State(USERID);
+ var S_AT = new linkify.parser.State();
+ var S_AT_NAME = new linkify.parser.State();
+ var S_AT_NAME_COLON = new linkify.parser.State();
+ var S_AT_NAME_COLON_DOMAIN = new linkify.parser.State();
+ var S_AT_NAME_COLON_DOMAIN_DOT = new linkify.parser.State();
+ var S_USERID = new linkify.parser.State(USERID);
var username_tokens = [
TT.DOT,
@@ -86,10 +86,10 @@ function matrixLinkify(linkify) {
TT.TLD
];
- S_START.on(TT.AT, S_AT);
+ S_START.on(TT.AT, S_AT);
- S_AT.on(username_tokens, S_AT_NAME);
- S_AT_NAME.on(username_tokens, S_AT_NAME);
+ S_AT.on(username_tokens, S_AT_NAME);
+ S_AT_NAME.on(username_tokens, S_AT_NAME);
S_AT_NAME.on(TT.DOMAIN, S_AT_NAME);
S_AT_NAME.on(TT.COLON, S_AT_NAME_COLON);