Merge pull request #14 from matrix-org/erikj/room_editing

Room Settings.
pull/1/head
David Baker 2015-07-21 07:38:10 -07:00
commit 4679e005bf
9 changed files with 450 additions and 18 deletions

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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>

View File

@ -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>
);
}
});

View File

@ -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({
}
},
});

View File

@ -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');

View File

@ -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 = {
});
}
};

View File

@ -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
};
}
};

View File

@ -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,
});
}
}
};