diff --git a/skins/base/css/molecules/RoomSettings.css b/skins/base/css/molecules/RoomSettings.css new file mode 100644 index 00000000..4100b9e3 --- /dev/null +++ b/skins/base/css/molecules/RoomSettings.css @@ -0,0 +1,30 @@ +/* +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. +*/ + +.mx_RoomSettings_settings { + display: table; + margin: 5px 0; +} + +.mx_RoomSettings_settings > div { + display: table-row; +} + +.mx_RoomSettings_settings > div > * { + display: table-cell; + + margin: 0 10px; +} diff --git a/skins/base/css/organisms/RoomView.css b/skins/base/css/organisms/RoomView.css index 07eeebf1..77893741 100644 --- a/skins/base/css/organisms/RoomView.css +++ b/skins/base/css/organisms/RoomView.css @@ -60,7 +60,7 @@ limitations under the License. order: 3; width: 100%; - height: 100%; + flex: 1; margin-top: 18px; margin-bottom: 18px; @@ -101,7 +101,7 @@ limitations under the License. .mx_RoomView_statusAreaBox { max-width: 720px; - margin: auto; + margin: auto; border-top: 1px solid #a8dbf3; } diff --git a/skins/base/views/molecules/RoomHeader.js b/skins/base/views/molecules/RoomHeader.js index 22e05868..7ad001d6 100644 --- a/skins/base/views/molecules/RoomHeader.js +++ b/skins/base/views/molecules/RoomHeader.js @@ -28,11 +28,15 @@ module.exports = React.createClass({ mixins: [RoomHeaderController], onNameChange: function(new_name) { - if (this.props.room.name != 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; @@ -46,7 +50,6 @@ module.exports = React.createClass({ } else { var topic = this.props.room.currentState.getStateEvents('m.room.topic', ''); - topic = topic ? <div className="mx_RoomHeader_topic">{ topic.getContent().topic }</div> : null; var callButtons; if (this.state) { @@ -62,7 +65,31 @@ module.exports = React.createClass({ } } - header = + 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 = <input type="text" defaultValue={actual_name} placeholder="Name" ref="name_edit"/>; + // if (topic) topic_el = <div className="mx_RoomHeader_topic"><textarea>{ topic.getContent().topic }</textarea></div> + save_button = ( + <div className="mx_RoomHeader_button"onClick={this.props.onSaveClick}> + Save + </div> + ); + } else { + name = <EditableText label={this.props.room.name} initialValue={actual_name} placeHolder="Name" onValueChanged={this.onNameChange} />; + if (topic) topic_el = <div className="mx_RoomHeader_topic">{ topic.getContent().topic }</div>; + settings_button = ( + <div className="mx_RoomHeader_button" onClick={this.props.onSettingsClick}> + <img src="img/settings.png" width="32" height="32"/> + </div> + ); + } + + header = <div className="mx_RoomHeader_wrapper"> <div className="mx_RoomHeader_leftRow"> <div className="mx_RoomHeader_avatar"> @@ -70,16 +97,15 @@ module.exports = React.createClass({ </div> <div className="mx_RoomHeader_info"> <div className="mx_RoomHeader_name"> - <EditableText initialValue={this.props.room.name} onValueChanged={this.onNameChange} /> + { name } </div> - { topic } + { topic_el } </div> </div> {callButtons} <div className="mx_RoomHeader_rightRow"> - <div className="mx_RoomHeader_button"> - <img src="img/settings.png" width="32" height="32"/> - </div> + { save_button } + { settings_button } <div className="mx_RoomHeader_button"> <img src="img/search.png" width="32" height="32"/> </div> diff --git a/skins/base/views/molecules/RoomSettings.js b/skins/base/views/molecules/RoomSettings.js new file mode 100644 index 00000000..2eecd096 --- /dev/null +++ b/skins/base/views/molecules/RoomSettings.js @@ -0,0 +1,200 @@ +/* +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; + } + + return ( + <div className="mx_RoomSettings"> + <textarea placeholder="Description" defaultValue={topic} ref="topic"/> <br/> + <label><input type="checkbox" ref="is_private" defaultChecked={join_rule != "public"}/> Make this room private</label> <br/> + <label><input type="checkbox" ref="share_history" defaultChecked={history_visibility == "shared"}/> Share message history with new users</label> <br/> + <label><input type="checkbox" /> Encrypt room</label> <br/> + + Power levels: + <div className="mx_RoomSettings_power_levels mx_RoomSettings_settings"> + <div> + <label htmlFor="mx_RoomSettings_ban_level">Ban level</label> + <input type="text" defaultValue={ban_level} size="3" ref="ban" id="mx_RoomSettings_ban_level" + disabled={!can_change_levels || current_user_level < ban_level} onChange={this.onPowerLevelsChanged}/> + </div> + <div> + <label htmlFor="mx_RoomSettings_kick_level">Kick level</label> + <input type="text" defaultValue={kick_level} size="3" ref="kick" id="mx_RoomSettings_kick_level" + disabled={!can_change_levels || current_user_level < kick_level} onChange={this.onPowerLevelsChanged}/> + </div> + <div> + <label htmlFor="mx_RoomSettings_redact_level">Redact level</label> + <input type="text" defaultValue={redact_level} size="3" ref="redact" id="mx_RoomSettings_redact_level" + disabled={!can_change_levels || current_user_level < redact_level} onChange={this.onPowerLevelsChanged}/> + </div> + <div> + <label htmlFor="mx_RoomSettings_invite_level">Invite level</label> + <input type="text" defaultValue={invite_level} size="3" ref="invite" id="mx_RoomSettings_invite_level" + disabled={!can_change_levels || current_user_level < invite_level} onChange={this.onPowerLevelsChanged}/> + </div> + <div> + <label htmlFor="mx_RoomSettings_event_level">Send event level</label> + <input type="text" defaultValue={send_level} size="3" ref="events_default" id="mx_RoomSettings_event_level" + disabled={!can_change_levels || current_user_level < send_level} onChange={this.onPowerLevelsChanged}/> + </div> + <div> + <label htmlFor="mx_RoomSettings_state_level">Set state level</label> + <input type="text" defaultValue={state_level} size="3" ref="state_default" id="mx_RoomSettings_state_level" + disabled={!can_change_levels || current_user_level < state_level} onChange={this.onPowerLevelsChanged}/> + </div> + <div> + <label htmlFor="mx_RoomSettings_user_level">Default user level</label> + <input type="text" defaultValue={default_user_level} size="3" ref="users_default" + id="mx_RoomSettings_user_level" disabled={!can_change_levels || current_user_level < default_user_level} + onChange={this.onPowerLevelsChanged}/> + </div> + </div> + + User levels: + <div className="mx_RoomSettings_user_levels mx_RoomSettings_settings"> + {Object.keys(user_levels).map(function(user, i) { + return ( + <div key={user}> + <label htmlFor={"mx_RoomSettings_user_"+i}>{user}</label> + <input type="text" defaultValue={user_levels[user]} size="3" id={"mx_RoomSettings_user_"+i} disabled/> + </div> + ); + })} + </div> + + Event levels: + <div className="mx_RoomSettings_event_lvels mx_RoomSettings_settings"> + {Object.keys(events_levels).map(function(event_type, i) { + return ( + <div key={event_type}> + <label htmlFor={"mx_RoomSettings_event_"+i}>{event_type}</label> + <input type="text" defaultValue={events_levels[event_type]} size="3" id={"mx_RoomSettings_event_"+i} disabled/> + </div> + ); + })} + </div> + </div> + ); + } +}); diff --git a/skins/base/views/organisms/RoomView.js b/skins/base/views/organisms/RoomView.js index c20f9a8c..6ecd02d7 100644 --- a/skins/base/views/organisms/RoomView.js +++ b/skins/base/views/organisms/RoomView.js @@ -21,13 +21,16 @@ var React = require('react'); var MatrixClientPeg = require("../../../../src/MatrixClientPeg"); var ComponentBroker = require('../../../../src/ComponentBroker'); +var Modal = require("../../../../src/Modal"); 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 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"); @@ -38,6 +41,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 ( @@ -104,11 +132,23 @@ module.exports = React.createClass({ } } + var roomEdit = null; + + if (this.state.editingRoomSettings) { + roomEdit = <RoomSettings ref="room_settings" room={this.state.room} />; + } + + if (this.state.uploadingRoomSettings) { + roomEdit = <Loader/>; + } + return ( <div className="mx_RoomView"> - <RoomHeader room={this.state.room} /> + <RoomHeader ref="header" room={this.state.room} editing={this.state.editingRoomSettings} + onSettingsClick={this.onSettingsClick} onSaveClick={this.onSaveClick}/> <div className="mx_RoomView_auxPanel"> <CallView room={this.state.room}/> + { roomEdit } </div> <div ref="messageWrapper" className="mx_RoomView_messagePanel" onScroll={ this.onMessageListScroll }> <div className="mx_RoomView_messageListWrapper"> @@ -130,4 +170,3 @@ module.exports = React.createClass({ } }, }); - diff --git a/src/ComponentBroker.js b/src/ComponentBroker.js index 49fb487b..6e5d1e11 100644 --- a/src/ComponentBroker.js +++ b/src/ComponentBroker.js @@ -92,6 +92,7 @@ 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'); diff --git a/src/controllers/molecules/RoomHeader.js b/src/controllers/molecules/RoomHeader.js index 5bd51e44..2ef99953 100644 --- a/src/controllers/molecules/RoomHeader.js +++ b/src/controllers/molecules/RoomHeader.js @@ -21,10 +21,25 @@ limitations under the License. * 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); @@ -43,7 +58,7 @@ module.exports = { onAction: function(payload) { // if we were given a room_id to track, don't handle anything else. - if (payload.room_id && this.props.room && + if (payload.room_id && this.props.room && this.props.room.roomId !== payload.room_id) { return; } @@ -78,4 +93,3 @@ module.exports = { }); } }; - diff --git a/src/controllers/molecules/RoomSettings.js b/src/controllers/molecules/RoomSettings.js new file mode 100644 index 00000000..fe7cd634 --- /dev/null +++ b/src/controllers/molecules/RoomSettings.js @@ -0,0 +1,31 @@ +/* +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: { + room: React.PropTypes.object.isRequired, + }, + + getInitialState: function() { + return { + power_levels_changed: false + }; + } +}; diff --git a/src/controllers/organisms/RoomView.js b/src/controllers/organisms/RoomView.js index a5bae754..311a3e1e 100644 --- a/src/controllers/organisms/RoomView.js +++ b/src/controllers/organisms/RoomView.js @@ -21,6 +21,10 @@ 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"); @@ -44,7 +48,9 @@ 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, } }, @@ -99,7 +105,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; @@ -300,7 +306,7 @@ module.exports = { dateSeparator = <DateSeparator key={ts1} ts={ts1}/>; continuation = false; } - } + } if (!TileType) continue; ret.unshift( <TileType key={mxEv.getId()} mxEvent={mxEv} continuation={continuation} last={last}/> @@ -311,6 +317,91 @@ module.exports = { ++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, + }); + } } }; -