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 = ( +