From ce789ba96289bb9c95bd88cd4831d5d718c4b15c Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Tue, 2 Feb 2016 12:46:14 +0000 Subject: [PATCH] Split out alias settings into its own component: AliasSettings --- src/ObjectUtils.js | 79 +++++ src/component-index.js | 1 + src/components/structures/RoomView.js | 56 +--- .../views/room_settings/AliasSettings.js | 313 ++++++++++++++++++ src/components/views/rooms/RoomSettings.js | 274 +-------------- 5 files changed, 411 insertions(+), 312 deletions(-) create mode 100644 src/ObjectUtils.js create mode 100644 src/components/views/room_settings/AliasSettings.js diff --git a/src/ObjectUtils.js b/src/ObjectUtils.js new file mode 100644 index 0000000000..41dc34cca7 --- /dev/null +++ b/src/ObjectUtils.js @@ -0,0 +1,79 @@ +/* +Copyright 2016 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. +*/ + +/** + * For two objects of the form { key: [val1, val2, val3] }, work out the added/removed + * values. Entirely new keys will result in the entire value array being added. + * @param {Object} before + * @param {Object} after + * @return {Object[]} An array of objects with the form: + * { key: $KEY, val: $VALUE, place: "add|del" } + */ +module.exports.getKeyValueArrayDiffs = function(before, after) { + var results = []; + var delta = {}; + Object.keys(before).forEach(function(beforeKey) { + delta[beforeKey] = delta[beforeKey] || 0; // init to 0 initially + delta[beforeKey]--; // keys present in the past have -ve values + }); + Object.keys(after).forEach(function(afterKey) { + delta[afterKey] = delta[afterKey] || 0; // init to 0 initially + delta[afterKey]++; // keys present in the future have +ve values + }); + + Object.keys(delta).forEach(function(muxedKey) { + switch (delta[muxedKey]) { + case 1: // A new key in after + after[muxedKey].forEach(function(afterVal) { + results.push({ place: "add", key: muxedKey, val: afterVal }); + }); + break; + case -1: // A before key was removed + before[muxedKey].forEach(function(beforeVal) { + results.push({ place: "del", key: muxedKey, val: beforeVal }); + }); + break; + case 0: // A mix of added/removed keys + // compare old & new vals + var itemDelta = {}; + before[muxedKey].forEach(function(beforeVal) { + itemDelta[beforeVal] = itemDelta[beforeVal] || 0; + itemDelta[beforeVal]--; + }); + after[muxedKey].forEach(function(afterVal) { + itemDelta[afterVal] = itemDelta[afterVal] || 0; + itemDelta[afterVal]++; + }); + + Object.keys(itemDelta).forEach(function(item) { + if (itemDelta[item] === 1) { + results.push({ place: "add", key: muxedKey, val: item }); + } else if (itemDelta[item] === -1) { + results.push({ place: "del", key: muxedKey, val: item }); + } else { + // itemDelta of 0 means it was unchanged between before/after + } + }); + break; + default: + console.error("Calculated key delta of " + delta[muxedKey] + + " - this should never happen!"); + break; + } + }); + + return results; +}; diff --git a/src/component-index.js b/src/component-index.js index 50803c045e..0eec385b60 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -63,6 +63,7 @@ module.exports.components['views.messages.MVideoBody'] = require('./components/v module.exports.components['views.messages.TextualBody'] = require('./components/views/messages/TextualBody'); module.exports.components['views.messages.TextualEvent'] = require('./components/views/messages/TextualEvent'); module.exports.components['views.messages.UnknownBody'] = require('./components/views/messages/UnknownBody'); +module.exports.components['views.room_settings.AliasSettings'] = require('./components/views/room_settings/AliasSettings'); module.exports.components['views.rooms.EntityTile'] = require('./components/views/rooms/EntityTile'); module.exports.components['views.rooms.EventTile'] = require('./components/views/rooms/EventTile'); module.exports.components['views.rooms.MemberInfo'] = require('./components/views/rooms/MemberInfo'); diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index e2ed8231ef..6818d1c91c 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -960,14 +960,6 @@ module.exports = React.createClass({ old_guest_join = false; } - 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) { @@ -1046,41 +1038,7 @@ 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 (newVals.tag_operations) { - // FIXME: should probably be factored out with alias_operations above var oplist = []; for (var i = 0; i < newVals.tag_operations.length; i++) { var tag_operation = newVals.tag_operations[i]; @@ -1113,16 +1071,6 @@ module.exports = React.createClass({ } } - 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( @@ -1131,6 +1079,8 @@ module.exports = React.createClass({ ); } + deferreds.push(this.refs.room_settings.saveAliases()); + if (deferreds.length) { var self = this; q.allSettled(deferreds).then( @@ -1241,9 +1191,7 @@ module.exports = React.createClass({ history_visibility: this.refs.room_settings.getHistoryVisibility(), are_notifications_muted: this.refs.room_settings.areNotificationsMuted(), power_levels: this.refs.room_settings.getPowerLevels(), - alias_operations: this.refs.room_settings.getAliasOperations(), tag_operations: this.refs.room_settings.getTagOperations(), - 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/room_settings/AliasSettings.js b/src/components/views/room_settings/AliasSettings.js new file mode 100644 index 0000000000..0dd00852fa --- /dev/null +++ b/src/components/views/room_settings/AliasSettings.js @@ -0,0 +1,313 @@ +/* +Copyright 2016 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +var q = require("q"); +var React = require('react'); +var ObjectUtils = require("../../../ObjectUtils"); +var MatrixClientPeg = require('../../../MatrixClientPeg'); +var sdk = require("../../../index"); +var Modal = require("../../../Modal"); + +module.exports = React.createClass({ + displayName: 'AliasSettings', + + propTypes: { + roomId: React.PropTypes.string.isRequired, + canSetCanonicalAlias: React.PropTypes.bool.isRequired, + canSetAliases: React.PropTypes.bool.isRequired, + aliasEvents: React.PropTypes.array, // [MatrixEvent] + canonicalAliasEvent: React.PropTypes.object // MatrixEvent + }, + + getDefaultProps: function() { + return { + canSetAliases: false, + canSetCanonicalAlias: false, + aliasEvents: [] + }; + }, + + getInitialState: function() { + return this.recalculateState(this.props.aliasEvents, this.props.canonicalAliasEvent); + }, + + recalculateState: function(aliasEvents, canonicalAliasEvent) { + aliasEvents = aliasEvents || []; + + var state = { + domainToAliases: {}, // { domain.com => [#alias1:domain.com, #alias2:domain.com] } + remoteDomains: [], // [ domain.com, foobar.com ] + canonicalAlias: null // #canonical:domain.com + }; + var localDomain = MatrixClientPeg.get().getDomain(); + + state.domainToAliases = this.aliasEventsToDictionary(aliasEvents); + + state.remoteDomains = Object.keys(state.domainToAliases).filter((alias) => { + return alias !== localDomain; + }); + + if (canonicalAliasEvent) { + state.canonicalAlias = canonicalAliasEvent.getContent().alias; + } + + return state; + }, + + saveSettings: function() { + console.log("AliasSettings.saveSettings room=%s", this.props.roomId); + var promises = []; + + // save new canonical alias + var oldCanonicalAlias = null; + if (this.props.canonicalAliasEvent) { + oldCanonicalAlias = this.props.canonicalAliasEvent.getContent().alias; + } + console.log("canon old=%s new=%s", oldCanonicalAlias, this.state.canonicalAlias); + if (oldCanonicalAlias !== this.state.canonicalAlias) { + promises.push( + MatrixClientPeg.get().sendStateEvent( + this.props.roomId, "m.room.canonical_alias", { + alias: this.state.canonicalAlias + }, "" + ) + ); + } + + + // save new aliases for m.room.aliases + var aliasOperations = this.getAliasOperations(); + var promises = []; + for (var i = 0; i < aliasOperations.length; i++) { + var alias_operation = aliasOperations[i]; + console.log("alias %s %s", alias_operation.place, alias_operation.val); + switch (alias_operation.place) { + case 'add': + promises.push( + MatrixClientPeg.get().createAlias( + alias_operation.val, this.props.roomId + ) + ); + break; + case 'del': + promises.push( + MatrixClientPeg.get().deleteAlias( + alias_operation.val + ) + ); + break; + default: + console.log("Unknown alias operation, ignoring: " + alias_operation.place); + } + } + + return q.allSettled(promises); + }, + + aliasEventsToDictionary: function(aliasEvents) { // m.room.alias events + var dict = {}; + aliasEvents.forEach((event) => { + dict[event.getStateKey()] = ( + (event.getContent().aliases || []).slice() // shallow-copy + ); + }); + return dict; + }, + + isAliasValid: function(alias) { + // XXX: FIXME SPEC-1 + return (alias.match(/^#([^\/:,]+?):(.+)$/) && encodeURI(alias) === alias); + }, + + getAliasOperations: function() { + var oldAliases = this.aliasEventsToDictionary(this.props.aliasEvents); + return ObjectUtils.getKeyValueArrayDiffs(oldAliases, this.state.domainToAliases); + }, + + onAliasAdded: function(alias) { + if (!alias || alias.length === 0) return; // ignore attempts to create blank aliases + + if (this.isAliasValid(alias)) { + // add this alias to the domain to aliases dict + var domain = alias.replace(/^.*?:/, ''); + // XXX: do we need to deep copy aliases before editing it? + this.state.domainToAliases[domain] = this.state.domainToAliases[domain] || []; + this.state.domainToAliases[domain].push(alias); + this.setState({ + domainToAliases: this.state.domainToAliases + }); + + // reset the add field + this.refs.add_alias.setValue(''); // FIXME + } + else { + var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createDialog(ErrorDialog, { + title: "Invalid alias format", + description: "'" + alias + "' is not a valid format for an alias", + }); + } + }, + + onAliasChanged: function(domain, index, alias) { + if (alias === "") return; // hit the delete button to delete please + var oldAlias; + if (this.isAliasValid(alias)) { + oldAlias = this.state.domainToAliases[domain][index]; + this.state.domainToAliases[domain][index] = alias; + } + else { + var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createDialog(ErrorDialog, { + title: "Invalid address format", + description: "'" + alias + "' is not a valid format for an address", + }); + } + }, + + onAliasDeleted: function(domain, index) { + // It's a bit naughty to directly manipulate this.state, and React would + // normally whine at you, but it can't see us doing the splice. Given we + // promptly setState anyway, it's just about acceptable. The alternative + // would be to arbitrarily deepcopy to a temp variable and then setState + // that, but why bother when we can cut this corner. + var alias = this.state.domainToAliases[domain].splice(index, 1); + this.setState({ + domainToAliases: this.state.domainToAliases + }); + }, + + onCanonicalAliasChange: function(event) { + this.setState({ + canonicalAlias: event.target.value + }); + }, + + render: function() { + var self = this; + var EditableText = sdk.getComponent("elements.EditableText"); + var localDomain = MatrixClientPeg.get().getDomain(); + + var canonical_alias_section; + if (this.props.canSetCanonicalAlias) { + canonical_alias_section = ( + + ); + } + else { + canonical_alias_section = ( + { this.state.canonicalAlias || "not set" } + ); + } + + var remote_aliases_section; + if (this.state.remoteDomains.length) { + remote_aliases_section = ( +
+
+ Remote addresses for this room: +
+
+ { this.state.remoteDomains.map((domain, i) => { + return this.state.domainToAliases[domain].map(function(alias, j) { + return ( +
+ +
+ ); + }); + })} +
+
+ ); + } + + return ( +
+

Addresses

+
+ The main address for this room is: { canonical_alias_section } +
+
+ { (this.state.domainToAliases[localDomain] && + this.state.domainToAliases[localDomain].length > 0) + ? "Local addresses for this room:" + : "This room has no local addresses" } +
+
+ { (this.state.domainToAliases[localDomain] || []).map((alias, i) => { + var deleteButton; + if (this.props.canSetAliases) { + deleteButton = ( + Delete + ); + } + return ( +
+ +
+ { deleteButton } +
+
+ ); + })} + +
+ +
+ Add +
+
+
+ + { remote_aliases_section } + +
+ ); + } +}); \ No newline at end of file diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index 80218b31c9..b716b2e9df 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +var q = require("q"); var React = require('react'); var MatrixClientPeg = require('../../../MatrixClientPeg'); var Tinter = require('../../../Tinter'); @@ -71,15 +72,6 @@ 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] || []; - var tags = {}; Object.keys(this.props.room.tags).forEach(function(tagName) { tags[tagName] = {}; @@ -89,15 +81,18 @@ module.exports = React.createClass({ power_levels_changed: false, color_scheme_changed: false, color_scheme_index: room_color_index, - aliases_changed: false, - aliases: aliases, tags_changed: false, tags: tags, }; }, + saveAliases: function() { + if (!this.refs.alias_settings) { return q(); } + return this.refs.alias_settings.saveSettings(); + }, + resetState: function() { - this.set.state(this.getInitialState()); + this.setState(this.getInitialState()); }, canGuestsJoin: function() { @@ -145,84 +140,6 @@ 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 - } - - // FIXME: this whole delta-based set comparison function used for domains, aliases & tags - // should be factored out asap rather than duplicated like this. - - // 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 }); - } else { - console.error("Calculated alias delta of " + delta[alias] + - " - this should never happen!"); - } - }); - break; - default: - console.error("Calculated domain delta of " + domainDelta[domain] + - " - this should never happen!"); - break; - } - }); - - return ops; - }, - getTagOperations: function() { if (!this.state.tags_changed) return undefined; @@ -276,72 +193,6 @@ module.exports = React.createClass({ }); }, - 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 address format", - description: "'" + alias + "' is not a valid format for an address", - }); - } - }, - - onAliasDeleted: function(domain, index) { - // It's a bit naughty to directly manipulate this.state, and React would - // normally whine at you, but it can't see us doing the splice. Given we - // promptly setState anyway, it's just about acceptable. The alternative - // would be to arbitrarily deepcopy to a temp variable and then setState - // that, but why bother when we can cut this corner. - var alias = this.state.aliases[domain].splice(index, 1); - this.setState({ - aliases: this.state.aliases - }); - - this.setState({ aliases_changed : true }); - }, - - 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); - }, - onTagChange: function(tagName, event) { if (event.target.checked) { if (tagName === 'm.favourite') { @@ -368,6 +219,7 @@ module.exports = React.createClass({ // TODO: go through greying out things you don't have permission to change // (or turning them into informative stuff) + var AliasSettings = sdk.getComponent("room_settings.AliasSettings"); var EditableText = sdk.getComponent('elements.EditableText'); var PowerSelector = sdk.getComponent('elements.PowerSelector'); @@ -445,13 +297,13 @@ module.exports = React.createClass({ var room_aliases_level = state_default; if (events_levels['m.room.aliases'] !== undefined) { - room_avatar_level = events_levels['m.room.aliases']; + room_aliases_level = events_levels['m.room.aliases']; } var can_set_room_aliases = current_user_level >= room_aliases_level; var canonical_alias_level = state_default; if (events_levels['m.room.canonical_alias'] !== undefined) { - room_avatar_level = events_levels['m.room.canonical_alias']; + canonical_alias_level = events_levels['m.room.canonical_alias']; } var can_set_canonical_alias = current_user_level >= canonical_alias_level; @@ -459,105 +311,6 @@ module.exports = React.createClass({ var self = this; - 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 = -
-
- Remote addresses for this room: -
-
- { remote_domains.map(function(state_key, i) { - return 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 = -
-

Addresses

-
The main address for this room is: { canonical_alias_section }
-
- { this.state.aliases[domain].length - ? "Local addresses for this room:" - : "This room has no local addresses" } -
-
- { this.state.aliases[domain].map(function(alias, i) { - var deleteButton; - if (can_set_room_aliases) { - deleteButton = Delete; - } - return ( -
- -
- { deleteButton } -
-
- ); - })} - -
- -
- Add -
-
-
- - { remote_aliases_section } - -
; - var room_colors_section =

Room Colour

@@ -676,7 +429,12 @@ module.exports = React.createClass({ { room_colors_section } - { aliases_section } +

Permissions