diff --git a/src/TextForEvent.js b/src/TextForEvent.js index 5296ef833e..f2ae22a1bb 100644 --- a/src/TextForEvent.js +++ b/src/TextForEvent.js @@ -66,7 +66,7 @@ function textForMemberEvent(ev) { function textForTopicEvent(ev) { var senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); - return senderDisplayName + ' changed the topic to, "' + ev.getContent().topic + '"'; + return senderDisplayName + ' changed the topic to "' + ev.getContent().topic + '"'; }; function textForRoomNameEvent(ev) { diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 6a19a55bfd..0688b98ca5 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -887,6 +887,24 @@ module.exports = React.createClass({ old_history_visibility = "shared"; } + var old_guest_read = (old_history_visibility === "world_readable"); + + var old_guest_join = this.state.room.currentState.getStateEvents('m.room.guest_access', ''); + if (old_guest_join) { + old_guest_join = (old_guest_join.getContent().guest_access === "can_join"); + } + else { + old_guest_join = (old_guest_join.getContent().guest_access === "forbidden"); + } + + var old_canonical_alias = this.state.room.currentState.getStateEvents('m.room.canonical_alias', ''); + if (old_canonical_alias) { + old_canonical_alias = old_canonical_alias.getContent().alias; + } + else { + old_canonical_alias = ""; + } + var deferreds = []; if (old_name != newVals.name && newVals.name != undefined) { @@ -930,6 +948,49 @@ module.exports = React.createClass({ ); } + if (newVals.alias_operations) { + var oplist = []; + for (var i = 0; i < newVals.alias_operations.length; i++) { + var alias_operation = newVals.alias_operations[i]; + switch (alias_operation.type) { + case 'put': + oplist.push( + MatrixClientPeg.get().createAlias( + alias_operation.alias, this.state.room.roomId + ) + ); + break; + case 'delete': + oplist.push( + MatrixClientPeg.get().deleteAlias( + alias_operation.alias + ) + ); + break; + default: + console.log("Unknown alias operation, ignoring: " + alias_operation.type); + } + } + + if (oplist.length) { + var deferred = oplist[0]; + oplist.splice(1).forEach(function (f) { + deferred = deferred.then(f); + }); + deferreds.push(deferred); + } + } + + if (old_canonical_alias !== newVals.canonical_alias) { + deferreds.push( + MatrixClientPeg.get().sendStateEvent( + this.state.room.roomId, "m.room.canonical_alias", { + alias: newVals.canonical_alias + }, "" + ) + ); + } + if (newVals.color_scheme) { deferreds.push( MatrixClientPeg.get().setRoomAccountData( @@ -938,26 +999,43 @@ module.exports = React.createClass({ ); } - deferreds.push( - MatrixClientPeg.get().setGuestAccess(this.state.room.roomId, { - allowRead: newVals.guest_read, - allowJoin: newVals.guest_join - }) - ); + if (old_guest_read != newVals.guest_read || + old_guest_join != newVals.guest_join) + { + deferreds.push( + MatrixClientPeg.get().setGuestAccess(this.state.room.roomId, { + allowRead: newVals.guest_read, + allowJoin: newVals.guest_join + }) + ); + } if (deferreds.length) { var self = this; - q.all(deferreds).fail(function(err) { - var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createDialog(ErrorDialog, { - title: "Failed to set state", - description: err.toString() + q.allSettled(deferreds).then( + function(results) { + var fails = results.filter(function(result) { return result.state !== "fulfilled" }); + if (fails.length) { + fails.forEach(function(result) { + console.error(result.reason); + }); + var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createDialog(ErrorDialog, { + title: "Failed to set state", + description: fails.map(function(result) { return result.reason }).join("\n"), + }); + self.refs.room_settings.resetState(); + } + else { + self.setState({ + editingRoomSettings: false + }); + } + }).finally(function() { + self.setState({ + uploadingRoomSettings: false, + }); }); - }).finally(function() { - self.setState({ - uploadingRoomSettings: false, - }); - }); } else { this.setState({ editingRoomSettings: false, @@ -1025,16 +1103,17 @@ module.exports = React.createClass({ onSaveClick: function() { this.setState({ - editingRoomSettings: false, uploadingRoomSettings: true, }); this.uploadNewState({ name: this.refs.header.getRoomName(), - topic: this.refs.room_settings.getTopic(), + topic: this.refs.header.getTopic(), join_rule: this.refs.room_settings.getJoinRules(), history_visibility: this.refs.room_settings.getHistoryVisibility(), power_levels: this.refs.room_settings.getPowerLevels(), + alias_operations: this.refs.room_settings.getAliasOperations(), + canonical_alias: this.refs.room_settings.getCanonicalAlias(), guest_join: this.refs.room_settings.canGuestsJoin(), guest_read: this.refs.room_settings.canGuestsRead(), color_scheme: this.refs.room_settings.getColorScheme(), diff --git a/src/components/views/elements/EditableText.js b/src/components/views/elements/EditableText.js index 7f64496393..32a90de7c7 100644 --- a/src/components/views/elements/EditableText.js +++ b/src/components/views/elements/EditableText.js @@ -71,6 +71,13 @@ module.exports = React.createClass({ } }, + componentDidUpdate: function(newProps) { + this.value = this.props.initialValue; + if (this.refs.editable_div) { + this.showPlaceholder(!this.value); + } + }, + showPlaceholder: function(show) { if (show) { this.refs.editable_div.textContent = this.props.placeholder; @@ -89,6 +96,11 @@ module.exports = React.createClass({ return this.value; }, + setValue: function(value) { + this.value = value; + this.showPlaceholder(!this.value); + }, + edit: function() { this.setState({ phase: this.Phases.Edit, @@ -99,6 +111,8 @@ module.exports = React.createClass({ this.setState({ phase: this.Phases.Display, }); + this.value = this.props.initialValue; + this.showPlaceholder(!this.value); this.onValueChanged(false); }, @@ -110,9 +124,7 @@ module.exports = React.createClass({ // console.log("keyDown: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder); if (this.placeholder) { - if (ev.keyCode !== KEY_SHIFT && !ev.metaKey && !ev.ctrlKey && !ev.altKey && ev.keyCode !== KEY_WINDOWS && ev.keyCode !== KEY_TAB) { - this.showPlaceholder(false); - } + this.showPlaceholder(false); } if (ev.key == "Enter") { @@ -182,7 +194,9 @@ module.exports = React.createClass({ if (this.props.blurToCancel) this.cancelEdit(); else - this.onFinish(ev) + this.onFinish(ev); + + this.showPlaceholder(!this.value); }, render: function() { diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index 0313f2c65c..b14c103efd 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -18,6 +18,7 @@ var React = require('react'); var MatrixClientPeg = require('../../../MatrixClientPeg'); var Tinter = require('../../../Tinter'); var sdk = require('../../../index'); +var Modal = require('../../../Modal'); var room_colors = [ // magic room default values courtesy of Ribot @@ -40,6 +41,9 @@ module.exports = React.createClass({ room: React.PropTypes.object.isRequired, }, + componentDidMount: function() { + }, + getInitialState: function() { // work out the initial color index var room_color_index = undefined; @@ -68,15 +72,28 @@ module.exports = React.createClass({ room_color_index = 0; } + // get the aliases + var aliases = {}; + var domain = MatrixClientPeg.get().getDomain(); + var alias_events = this.props.room.currentState.getStateEvents('m.room.aliases'); + for (var i = 0; i < alias_events.length; i++) { + aliases[alias_events[i].getStateKey()] = alias_events[i].getContent().aliases.slice(); // shallow copy + } + aliases[domain] = aliases[domain] || []; + return { power_levels_changed: false, color_scheme_changed: false, color_scheme_index: room_color_index, aliases_changed: false, - aliases: [], + aliases: aliases, }; }, + resetState: function() { + this.set.state(this.getInitialState()); + }, + canGuestsJoin: function() { return this.refs.guests_join.checked; }, @@ -104,13 +121,13 @@ module.exports = React.createClass({ power_levels = power_levels.getContent(); var new_power_levels = { - ban: parseInt(this.refs.ban.value), - kick: parseInt(this.refs.kick.value), - redact: parseInt(this.refs.redact.value), - invite: parseInt(this.refs.invite.value), - events_default: parseInt(this.refs.events_default.value), - state_default: parseInt(this.refs.state_default.value), - users_default: parseInt(this.refs.users_default.value), + ban: parseInt(this.refs.ban.getValue()), + kick: parseInt(this.refs.kick.getValue()), + redact: parseInt(this.refs.redact.getValue()), + invite: parseInt(this.refs.invite.getValue()), + events_default: parseInt(this.refs.events_default.getValue()), + state_default: parseInt(this.refs.state_default.getValue()), + users_default: parseInt(this.refs.users_default.getValue()), users: power_levels.users, events: power_levels.events, }; @@ -118,6 +135,75 @@ module.exports = React.createClass({ return new_power_levels; }, + getCanonicalAlias: function() { + return this.refs.canonical_alias ? this.refs.canonical_alias.value : ""; + }, + + getAliasOperations: function() { + if (!this.state.aliases_changed) return undefined; + + // work out the delta from room state to UI state + var ops = []; + + // calculate original ("old") aliases + var oldAliases = {}; + var aliases = this.state.aliases; + var alias_events = this.props.room.currentState.getStateEvents('m.room.aliases'); + for (var i = 0; i < alias_events.length; i++) { + var domain = alias_events[i].getStateKey(); + oldAliases[domain] = alias_events[i].getContent().aliases.slice(); // shallow copy + } + + // work out whether any domains have entirely disappeared or appeared + var domainDelta = {} + Object.keys(oldAliases).forEach(function(domain) { + domainDelta[domain] = domainDelta[domain] || 0; + domainDelta[domain]--; + }); + Object.keys(aliases).forEach(function(domain) { + domainDelta[domain] = domainDelta[domain] || 0; + domainDelta[domain]++; + }); + + Object.keys(domainDelta).forEach(function(domain) { + switch (domainDelta[domain]) { + case 1: // entirely new domain + aliases[domain].forEach(function(alias) { + ops.push({ type: "put", alias : alias }); + }); + break; + case -1: // entirely removed domain + oldAliases[domain].forEach(function(alias) { + ops.push({ type: "delete", alias : alias }); + }); + break; + case 0: // mix of aliases in this domain. + // compare old & new aliases for this domain + var delta = {}; + oldAliases[domain].forEach(function(item) { + delta[item] = delta[item] || 0; + delta[item]--; + }); + aliases[domain].forEach(function(item) { + delta[item] = delta[item] || 0; + delta[item]++; + }); + + Object.keys(delta).forEach(function(alias) { + if (delta[alias] == 1) { + ops.push({ type: "put", alias: alias }); + } else if (delta[alias] == -1) { + ops.push({ type: "delete", alias: alias }); + } + }); + + break; + } + }); + + return ops; + }, + onPowerLevelsChanged: function() { this.setState({ power_levels_changed: true @@ -143,16 +229,66 @@ module.exports = React.createClass({ }); }, - onAliasChanged: function(i, j) { - + onAliasChanged: function(domain, index, alias) { + if (alias === "") return; // hit the delete button to delete please + var oldAlias; + if (this.isAliasValid(alias)) { + oldAlias = this.state.aliases[domain][index]; + this.state.aliases[domain][index] = alias; + this.setState({ aliases_changed : true }); + } + else { + var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createDialog(ErrorDialog, { + title: "Invalid alias format", + description: "'" + alias + "' is not a valid format for an alias", + }); + } }, - onAliasDeleted: function(i, j) { + onAliasDeleted: function(domain, index) { + // XXX: can we edit state directly and then set, or should we copy it first? + var alias = this.state.aliases[domain].splice(index, 1); + this.setState({ + aliases: this.state.aliases + }); + this.setState({ aliases_changed : true }); }, - onAliasAdded: function(i, j) { + onAliasAdded: function(alias) { + if (alias === "") return; // ignore attempts to create blank aliases + if (alias === undefined) { + alias = this.refs.add_alias ? this.refs.add_alias.getValue() : undefined; + if (alias === undefined || alias === "") return; + } + if (this.isAliasValid(alias)) { + var domain = alias.replace(/^.*?:/, ''); + // XXX: do we need to deep copy aliases before editing it? + this.state.aliases[domain] = this.state.aliases[domain] || []; + this.state.aliases[domain].push(alias); + this.setState({ + aliases: this.state.aliases + }); + + // reset the add field + this.refs.add_alias.setValue(''); + + this.setState({ aliases_changed : true }); + } + else { + var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createDialog(ErrorDialog, { + title: "Invalid alias format", + description: "'" + alias + "' is not a valid format for an alias", + }); + } + }, + + isAliasValid: function(alias) { + // XXX: FIXME SPEC-1 + return (alias.match(/^#([^\/:,]+?):(.+)$/) && encodeURI(alias) === alias); }, render: function() { @@ -223,73 +359,117 @@ module.exports = React.createClass({ var can_change_levels = false; } - var room_avatar_level = parseInt(power_levels.state_default || 0); - if (events_levels['m.room.avatar'] !== undefined) { - room_avatar_level = events_levels['m.room.avatar']; + var room_aliases_level = parseInt(power_levels.state_default || 0); + if (events_levels['m.room.aliases'] !== undefined) { + room_avatar_level = events_levels['m.room.aliases']; } - var can_set_room_avatar = current_user_level >= room_avatar_level; + var can_set_room_aliases = current_user_level >= room_aliases_level; + + var canonical_alias_level = parseInt(power_levels.state_default || 0); + if (events_levels['m.room.canonical_alias'] !== undefined) { + room_avatar_level = events_levels['m.room.canonical_alias']; + } + var can_set_canonical_alias = current_user_level >= canonical_alias_level; var self = this; - var alias_events = this.props.room.currentState.getStateEvents('m.room.aliases'); var canonical_alias_event = this.props.room.currentState.getStateEvents('m.room.canonical_alias', ''); var canonical_alias = canonical_alias_event ? canonical_alias_event.getContent().alias : ""; var domain = MatrixClientPeg.get().getDomain(); + var remote_domains = Object.keys(this.state.aliases).filter(function(alias) { return alias !== domain }); + + var remote_aliases_section; + if (remote_domains.length) { + remote_aliases_section = +
+
+ This room can be found elsewhere as: +
+
+ { remote_domains.map(function(state_key, i) { + self.state.aliases[state_key].map(function(alias, j) { + return ( +
+ +
+
+
+ ); + }); + })} +
+
+ } + + var canonical_alias_section; + if (can_set_canonical_alias) { + canonical_alias_section = + + } + else { + canonical_alias_section = { canonical_alias || "not set" }; + } + var aliases_section =

Directory

- { alias_events.length ? "This room is accessible via:" : "This room has no aliases." } + { this.state.aliases[domain].length + ? "This room can be found on " + domain + " as:" + : "This room is not findable on " + domain }
- { alias_events.map(function(alias_event, i) { - return alias_event.getContent().aliases.map(function(alias, j) { - var deleteButton; - if (alias_event && alias_event.getStateKey() === domain) { - deleteButton = Delete; - } - return ( -
- -
- { deleteButton } -
+ { this.state.aliases[domain].map(function(alias, i) { + var deleteButton; + if (can_set_room_aliases) { + deleteButton = Delete; + } + return ( +
+ +
+ { deleteButton }
- ); - }); +
+ ); })}
+ ref="add_alias" + className="mx_RoomSettings_alias mx_RoomSettings_editable" + placeholderClassName="mx_RoomSettings_aliasPlaceholder" + placeholder={ "New alias (e.g. #foo:" + domain + ")" } + blurToCancel={ false } + onValueChanged={ self.onAliasAdded } />
- Add + Add
-
-
-
The canonical entry is  - +
+ + { remote_aliases_section } + +
The official way to refer to this room is: { canonical_alias_section }
; var room_colors_section = @@ -337,6 +517,9 @@ module.exports = React.createClass({ ; } + else { + user_levels_section =
No users have specific privileges in this room.
+ } var banned = this.props.room.getMembersWithMembership("ban"); var banned_users_section; @@ -381,38 +564,38 @@ module.exports = React.createClass({
The default role for new room members is - +
To send messages, you must be a - +
To invite users into the room, you must be a - +
To configure the room, you must be a - +
To kick users, you must be a - +
To ban users, you must be a - +
To redact messages, you must be a - +
{Object.keys(events_levels).map(function(event_type, i) { return (
To send events of type { event_type }, you must be a - +
); })} @@ -431,6 +614,11 @@ module.exports = React.createClass({ { banned_users_section } +

Advanced

+
+ This room's internal ID is { this.props.room.roomId } +
+
); }