+ { fileDropTarget }
{ conferenceCallNotification }
diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js
index ddf4229170..fa932b6757 100644
--- a/src/components/structures/UserSettings.js
+++ b/src/components/structures/UserSettings.js
@@ -21,6 +21,7 @@ var dis = require("../../dispatcher");
var q = require('q');
var version = require('../../../package.json').version;
var UserSettingsStore = require('../../UserSettingsStore');
+var GeminiScrollbar = require('react-gemini-scrollbar');
module.exports = React.createClass({
displayName: 'UserSettings',
@@ -83,6 +84,12 @@ module.exports = React.createClass({
}
},
+ onAvatarPickerClick: function(ev) {
+ if (this.refs.file_label) {
+ this.refs.file_label.click();
+ }
+ },
+
onAvatarSelected: function(ev) {
var self = this;
var changeAvatar = this.refs.changeAvatar;
@@ -175,7 +182,7 @@ module.exports = React.createClass({
if (MatrixClientPeg.get().isGuest()) {
accountJsx = (
- Upgrade (It's free!)
+ Create an account
);
}
@@ -196,6 +203,8 @@ module.exports = React.createClass({
+
+
Profile
@@ -225,13 +234,15 @@ module.exports = React.createClass({
-
+
+
+
@@ -241,13 +252,12 @@ module.exports = React.createClass({
Account
- {accountJsx}
-
-
-
-
+
+
Log out
+
+ {accountJsx}
Notifications
@@ -281,6 +291,8 @@ module.exports = React.createClass({
Version {this.state.clientVersion}
+
+
);
}
diff --git a/src/components/views/elements/EditableText.js b/src/components/views/elements/EditableText.js
index beedfc35c8..683cfe4fc8 100644
--- a/src/components/views/elements/EditableText.js
+++ b/src/components/views/elements/EditableText.js
@@ -18,13 +18,22 @@ limitations under the License.
var React = require('react');
+const KEY_TAB = 9;
+const KEY_SHIFT = 16;
+const KEY_WINDOWS = 91;
+
module.exports = React.createClass({
displayName: 'EditableText',
propTypes: {
onValueChanged: React.PropTypes.func,
initialValue: React.PropTypes.string,
label: React.PropTypes.string,
- placeHolder: React.PropTypes.string,
+ placeholder: React.PropTypes.string,
+ className: React.PropTypes.string,
+ labelClassName: React.PropTypes.string,
+ placeholderClassName: React.PropTypes.string,
+ blurToCancel: React.PropTypes.bool,
+ editable: React.PropTypes.bool,
},
Phases: {
@@ -36,38 +45,62 @@ module.exports = React.createClass({
return {
onValueChanged: function() {},
initialValue: '',
- label: 'Click to set',
+ label: '',
placeholder: '',
+ editable: true,
};
},
getInitialState: function() {
return {
- value: this.props.initialValue,
phase: this.Phases.Display,
}
},
componentWillReceiveProps: function(nextProps) {
- this.setState({
- value: nextProps.initialValue
- });
+ if (nextProps.initialValue !== this.props.initialValue) {
+ this.value = nextProps.initialValue;
+ if (this.refs.editable_div) {
+ this.showPlaceholder(!this.value);
+ }
+ }
+ },
+
+ componentWillMount: function() {
+ // we track value as an JS object field rather than in React state
+ // as React doesn't play nice with contentEditable.
+ this.value = '';
+ this.placeholder = false;
+ },
+
+ componentDidMount: function() {
+ 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;
+ this.refs.editable_div.setAttribute("class", this.props.className + " " + this.props.placeholderClassName);
+ this.placeholder = true;
+ this.value = '';
+ }
+ else {
+ this.refs.editable_div.textContent = this.value;
+ this.refs.editable_div.setAttribute("class", this.props.className);
+ this.placeholder = false;
+ }
},
getValue: function() {
- return this.state.value;
+ return this.value;
},
- setValue: function(val, shouldSubmit, suppressListener) {
- var self = this;
- this.setState({
- value: val,
- phase: this.Phases.Display,
- }, function() {
- if (!suppressListener) {
- self.onValueChanged(shouldSubmit);
- }
- });
+ setValue: function(value) {
+ this.value = value;
+ this.showPlaceholder(!this.value);
},
edit: function() {
@@ -80,65 +113,106 @@ module.exports = React.createClass({
this.setState({
phase: this.Phases.Display,
});
+ this.value = this.props.initialValue;
+ this.showPlaceholder(!this.value);
this.onValueChanged(false);
},
onValueChanged: function(shouldSubmit) {
- this.props.onValueChanged(this.state.value, shouldSubmit);
+ this.props.onValueChanged(this.value, shouldSubmit);
+ },
+
+ onKeyDown: function(ev) {
+ // console.log("keyDown: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder);
+
+ if (this.placeholder) {
+ this.showPlaceholder(false);
+ }
+
+ if (ev.key == "Enter") {
+ ev.stopPropagation();
+ ev.preventDefault();
+ }
+
+ // console.log("keyDown: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder);
},
onKeyUp: function(ev) {
+ // console.log("keyUp: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder);
+
+ if (!ev.target.textContent) {
+ this.showPlaceholder(true);
+ }
+ else if (!this.placeholder) {
+ this.value = ev.target.textContent;
+ }
+
if (ev.key == "Enter") {
this.onFinish(ev);
} else if (ev.key == "Escape") {
this.cancelEdit();
}
+
+ // console.log("keyUp: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder);
},
- onClickDiv: function() {
+ onClickDiv: function(ev) {
+ if (!this.props.editable) return;
+
this.setState({
phase: this.Phases.Edit,
})
},
onFocus: function(ev) {
- ev.target.setSelectionRange(0, ev.target.value.length);
- },
+ //ev.target.setSelectionRange(0, ev.target.textContent.length);
- onFinish: function(ev) {
- if (ev.target.value) {
- this.setValue(ev.target.value, ev.key === "Enter");
- } else {
- this.cancelEdit();
+ var node = ev.target.childNodes[0];
+ if (node) {
+ var range = document.createRange();
+ range.setStart(node, 0);
+ range.setEnd(node, node.length);
+
+ var sel = window.getSelection();
+ sel.removeAllRanges();
+ sel.addRange(range);
}
},
- onBlur: function() {
- this.cancelEdit();
+ onFinish: function(ev) {
+ var self = this;
+ var submit = (ev.key === "Enter");
+ this.setState({
+ phase: this.Phases.Display,
+ }, function() {
+ self.onValueChanged(submit);
+ });
+ },
+
+ onBlur: function(ev) {
+ var sel = window.getSelection();
+ sel.removeAllRanges();
+
+ if (this.props.blurToCancel)
+ this.cancelEdit();
+ else
+ this.onFinish(ev);
+
+ this.showPlaceholder(!this.value);
},
render: function() {
var editable_el;
- if (this.state.phase == this.Phases.Display) {
- if (this.state.value) {
- editable_el =
{this.state.value}
;
- } else {
- editable_el =
{this.props.label}
;
- }
- } else if (this.state.phase == this.Phases.Edit) {
- editable_el = (
-
-
-
- );
+ if (!this.props.editable || (this.state.phase == this.Phases.Display && (this.props.label || this.props.labelClassName) && !this.value)) {
+ // show the label
+ editable_el =
{ this.props.label || this.props.initialValue }
;
+ } else {
+ // show the content editable div, but manually manage its contents as react and contentEditable don't play nice together
+ editable_el =
;
}
- return (
-
- {editable_el}
-
- );
+ return editable_el;
}
});
diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js
index fe763d06bf..e3613ef9a3 100644
--- a/src/components/views/messages/TextualBody.js
+++ b/src/components/views/messages/TextualBody.js
@@ -36,6 +36,9 @@ module.exports = React.createClass({
},
componentDidUpdate: function() {
+ // XXX: why don't we linkify here?
+ // XXX: why do we bother doing this on update at all, given events are immutable?
+
if (this.props.mxEvent.getContent().format === "org.matrix.custom.html")
HtmlUtils.highlightDom(ReactDOM.findDOMNode(this));
},
diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js
index f4073035af..3e3221992e 100644
--- a/src/components/views/rooms/MemberList.js
+++ b/src/components/views/rooms/MemberList.js
@@ -318,7 +318,7 @@ module.exports = React.createClass({
} else {
return (
);
}
diff --git a/src/components/views/rooms/MemberTile.js b/src/components/views/rooms/MemberTile.js
index 84d8edb2b5..0f40180274 100644
--- a/src/components/views/rooms/MemberTile.js
+++ b/src/components/views/rooms/MemberTile.js
@@ -137,7 +137,7 @@ module.exports = React.createClass({
}
var nameEl;
- if (this.state.hover) {
+ if (this.state.hover && this.props.member) {
var presenceState = (member && member.user) ? member.user.presence : null;
var PresenceLabel = sdk.getComponent("rooms.PresenceLabel");
nameEl = (
diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js
index a05c6c30ac..5340798875 100644
--- a/src/components/views/rooms/RoomHeader.js
+++ b/src/components/views/rooms/RoomHeader.js
@@ -21,6 +21,12 @@ var sdk = require('../../../index');
var dis = require("../../../dispatcher");
var MatrixClientPeg = require('../../../MatrixClientPeg');
+var linkify = require('linkifyjs');
+var linkifyElement = require('linkifyjs/element');
+var linkifyMatrix = require('../../../linkify-matrix');
+
+linkifyMatrix(linkify);
+
module.exports = React.createClass({
displayName: 'RoomHeader',
@@ -41,6 +47,25 @@ module.exports = React.createClass({
};
},
+ componentWillReceiveProps: function(newProps) {
+ if (newProps.editing) {
+ var topic = this.props.room.currentState.getStateEvents('m.room.topic', '');
+ var name = this.props.room.currentState.getStateEvents('m.room.name', '');
+
+ this.setState({
+ name: name ? name.getContent().name : '',
+ defaultName: this.props.room.getDefaultRoomName(MatrixClientPeg.get().credentials.userId),
+ topic: topic ? topic.getContent().topic : '',
+ });
+ }
+ },
+
+ componentDidUpdate: function() {
+ if (this.refs.topic) {
+ linkifyElement(this.refs.topic, linkifyMatrix.options);
+ }
+ },
+
onVideoClick: function(e) {
dis.dispatch({
action: 'place_call',
@@ -57,26 +82,59 @@ module.exports = React.createClass({
});
},
- onNameChange: function(new_name) {
- if (this.props.room.name != new_name && new_name) {
- MatrixClientPeg.get().setRoomName(this.props.room.roomId, new_name);
+ onNameChanged: function(value) {
+ this.setState({ name : value });
+ },
+
+ onTopicChanged: function(value) {
+ this.setState({ topic : value });
+ },
+
+ onAvatarPickerClick: function(ev) {
+ if (this.refs.file_label) {
+ this.refs.file_label.click();
}
},
+ onAvatarSelected: function(ev) {
+ var self = this;
+ var changeAvatar = this.refs.changeAvatar;
+ if (!changeAvatar) {
+ console.error("No ChangeAvatar found to upload image to!");
+ return;
+ }
+ changeAvatar.onFileSelected(ev).done(function() {
+ // dunno if the avatar changed, re-check it.
+ self._refreshFromServer();
+ }, function(err) {
+ var errMsg = (typeof err === "string") ? err : (err.error || "");
+ var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+ Modal.createDialog(ErrorDialog, {
+ title: "Error",
+ description: "Failed to set avatar. " + errMsg
+ });
+ });
+ },
+
getRoomName: function() {
- return this.refs.name_edit.value;
+ return this.state.name;
+ },
+
+ getTopic: function() {
+ return this.state.topic;
},
render: function() {
var EditableText = sdk.getComponent("elements.EditableText");
- var RoomAvatar = sdk.getComponent('avatars.RoomAvatar');
+ var RoomAvatar = sdk.getComponent("avatars.RoomAvatar");
+ var ChangeAvatar = sdk.getComponent("settings.ChangeAvatar");
var TintableSvg = sdk.getComponent("elements.TintableSvg");
var header;
if (this.props.simpleHeader) {
var cancel;
if (this.props.onCancelClick) {
- cancel =
+ cancel =
}
header =
@@ -87,27 +145,72 @@ module.exports = React.createClass({
}
else {
- var topic = this.props.room.currentState.getStateEvents('m.room.topic', '');
-
var name = null;
var searchStatus = null;
var topic_el = null;
var cancel_button = 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 =
-
-
-
- // if (topic) topic_el =
- cancel_button =
Cancel
- save_button =
Save Changes
- } else {
- //
+ // calculate permissions. XXX: this should be done on mount or something, and factored out with RoomSettings
+ var power_levels = this.props.room.currentState.getStateEvents('m.room.power_levels', '');
+ var events_levels = (power_levels ? power_levels.events : {}) || {};
+ var user_id = MatrixClientPeg.get().credentials.userId;
+
+ if (power_levels) {
+ power_levels = power_levels.getContent();
+ var default_user_level = parseInt(power_levels.users_default || 0);
+ var user_levels = power_levels.users || {};
+ var current_user_level = user_levels[user_id];
+ if (current_user_level == undefined) current_user_level = default_user_level;
+ } else {
+ var default_user_level = 0;
+ var user_levels = [];
+ var current_user_level = 0;
+ }
+ var state_default = parseInt((power_levels ? power_levels.state_default : 0) || 0);
+
+ var room_avatar_level = state_default;
+ if (events_levels['m.room.avatar'] !== undefined) {
+ room_avatar_level = events_levels['m.room.avatar'];
+ }
+ var can_set_room_avatar = current_user_level >= room_avatar_level;
+
+ var room_name_level = state_default;
+ if (events_levels['m.room.name'] !== undefined) {
+ room_name_level = events_levels['m.room.name'];
+ }
+ var can_set_room_name = current_user_level >= room_name_level;
+
+ var room_topic_level = state_default;
+ if (events_levels['m.room.topic'] !== undefined) {
+ room_topic_level = events_levels['m.room.topic'];
+ }
+ var can_set_room_topic = current_user_level >= room_topic_level;
+
+ var placeholderName = "Unnamed Room";
+ if (this.state.defaultName && this.state.defaultName !== '?') {
+ placeholderName += " (" + this.state.defaultName + ")";
+ }
+
+ save_button =
Save
+ cancel_button =
+ }
+
+ if (can_set_room_name) {
+ name =
+
+
+
+ }
+ else {
var searchStatus;
// don't display the search count until the search completes and
// gives us a valid (possibly zero) searchCount.
@@ -116,21 +219,55 @@ module.exports = React.createClass({
}
name =
-
+
{ this.props.room.name }
{ searchStatus }
- if (topic) topic_el =
{ topic.getContent().topic }
;
+ }
+
+ if (can_set_room_topic) {
+ topic_el =
+
+ } else {
+ var topic = this.props.room.currentState.getStateEvents('m.room.topic', '');
+ if (topic) topic_el =
{ topic.getContent().topic }
;
}
var roomAvatar = null;
if (this.props.room) {
- roomAvatar = (
-
- );
+ if (can_set_room_avatar) {
+ roomAvatar = (
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+ else {
+ roomAvatar = (
+
+
+
+ );
+ }
}
var leave_button;
@@ -149,9 +286,21 @@ module.exports = React.createClass({
;
}
+ var right_row;
+ if (!this.props.editing) {
+ right_row =
+
+ { forget_button }
+ { leave_button }
+
+
+
+
;
+ }
+
header =
-
+
{ roomAvatar }
@@ -160,20 +309,14 @@ module.exports = React.createClass({
{ topic_el }
- {cancel_button}
{save_button}
-
- { forget_button }
- { leave_button }
-
-
-
-
+ {cancel_button}
+ {right_row}
}
return (
-
+
{ header }
);
diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js
index c5e37521c5..0ff15a4f43 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
@@ -38,6 +39,16 @@ module.exports = React.createClass({
propTypes: {
room: React.PropTypes.object.isRequired,
+ onSaveClick: React.PropTypes.func,
+ onCancelClick: React.PropTypes.func,
+ },
+
+ componentDidMount: function() {
+ // XXX: dirty hack to gutwrench to focus on the invite box
+ if (this.props.room.getJoinedMembers().length == 1) {
+ var inviteBox = document.getElementById("mx_MemberList_invite");
+ if (inviteBox) setTimeout(function() { inviteBox.focus(); }, 0);
+ }
},
getInitialState: function() {
@@ -68,13 +79,35 @@ 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] = {};
+ });
+
return {
power_levels_changed: false,
color_scheme_changed: false,
color_scheme_index: room_color_index,
+ aliases_changed: false,
+ aliases: aliases,
+ tags_changed: false,
+ tags: tags,
};
},
+ resetState: function() {
+ this.set.state(this.getInitialState());
+ },
+
canGuestsJoin: function() {
return this.refs.guests_join.checked;
},
@@ -84,7 +117,7 @@ module.exports = React.createClass({
},
getTopic: function() {
- return this.refs.topic.value;
+ return this.refs.topic ? this.refs.topic.value : "";
},
getJoinRules: function() {
@@ -102,13 +135,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,
};
@@ -116,6 +149,112 @@ 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;
+
+ var ops = [];
+
+ var delta = {};
+ Object.keys(this.props.room.tags).forEach(function(oldTag) {
+ delta[oldTag] = delta[oldTag] || 0;
+ delta[oldTag]--;
+ });
+ Object.keys(this.state.tags).forEach(function(newTag) {
+ delta[newTag] = delta[newTag] || 0;
+ delta[newTag]++;
+ });
+ Object.keys(delta).forEach(function(tag) {
+ if (delta[tag] == 1) {
+ ops.push({ type: "put", tag: tag });
+ } else if (delta[tag] == -1) {
+ ops.push({ type: "delete", tag: tag });
+ } else {
+ console.error("Calculated tag delta of " + delta[tag] +
+ " - this should never happen!");
+ }
+ });
+
+ return ops;
+ },
+
onPowerLevelsChanged: function() {
this.setState({
power_levels_changed: true
@@ -141,11 +280,100 @@ module.exports = React.createClass({
});
},
- render: function() {
- var ChangeAvatar = sdk.getComponent('settings.ChangeAvatar');
+ 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",
+ });
+ }
+ },
- var topic = this.props.room.currentState.getStateEvents('m.room.topic', '');
- if (topic) topic = topic.getContent().topic;
+ 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') {
+ delete this.state.tags['m.lowpriority'];
+ }
+ else if (tagName === 'm.lowpriority') {
+ delete this.state.tags['m.favourite'];
+ }
+
+ this.state.tags[tagName] = this.state.tags[tagName] || {};
+ }
+ else {
+ delete this.state.tags[tagName];
+ }
+
+ // XXX: hacky say to deep-edit state
+ this.setState({
+ tags: this.state.tags,
+ tags_changed: true
+ });
+ },
+
+ render: function() {
+ // TODO: go through greying out things you don't have permission to change
+ // (or turning them into informative stuff)
+
+ var EditableText = sdk.getComponent('elements.EditableText');
+ var PowerSelector = sdk.getComponent('elements.PowerSelector');
var join_rule = this.props.room.currentState.getStateEvents('m.room.join_rules', '');
if (join_rule) join_rule = join_rule.getContent().join_rule;
@@ -159,7 +387,9 @@ module.exports = React.createClass({
guest_access = guest_access.getContent().guest_access;
}
- var events_levels = power_levels.events || {};
+ var events_levels = (power_levels ? power_levels.events : {}) || {};
+
+ var user_id = MatrixClientPeg.get().credentials.userId;
if (power_levels) {
power_levels = power_levels.getContent();
@@ -178,8 +408,6 @@ module.exports = React.createClass({
var user_levels = power_levels.users || {};
- 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;
@@ -208,14 +436,127 @@ 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 state_default = (parseInt(power_levels ? power_levels.state_default : 0) || 0);
+
+ var room_aliases_level = state_default;
+ 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 = state_default;
+ 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 tag_level = state_default;
+ if (events_levels['m.tag'] !== undefined) {
+ tag_level = events_levels['m.tag'];
+ }
+ var can_set_tag = current_user_level >= tag_level;
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 =
+
+
+ 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 =
+
+ { Object.keys(self.state.aliases).map(function(stateKey, i) {
+ return self.state.aliases[stateKey].map(function(alias, j) {
+ return { alias }
+ });
+ })}
+ not set
+
+ }
+ else {
+ canonical_alias_section =
{ canonical_alias || "not set" } ;
+ }
+
+ var aliases_section =
+
+
Directory
+
+ { this.state.aliases[domain].length
+ ? "This room can be found on " + domain + " as:"
+ : "This room is not findable on " + domain }
+
+
+ { this.state.aliases[domain].map(function(alias, i) {
+ var deleteButton;
+ if (can_set_room_aliases) {
+ deleteButton =
;
+ }
+ return (
+
+
+
+ { deleteButton }
+
+
+ );
+ })}
+
+
+
+
+
+
+
+
+
+ { remote_aliases_section }
+
+
The official way to refer to this room is: { canonical_alias_section }
+
;
+
var room_colors_section =
Room Colour
@@ -242,35 +583,30 @@ module.exports = React.createClass({
;
- var change_avatar;
- if (can_set_room_avatar) {
- change_avatar =
+ var user_levels_section;
+ if (user_levels.length) {
+ user_levels_section =
-
Room Icon
-
- ;
- }
-
- var banned = this.props.room.getMembersWithMembership("ban");
-
- var events_levels_section;
- if (events_levels.length) {
- events_levels_section =
-
-
Event levels
-
- {Object.keys(events_levels).map(function(event_type, i) {
+
+ Users with specific roles are:
+
+
+ {Object.keys(user_levels).map(function(user, i) {
return (
-
-
{event_type}
-
+
);
})}
;
}
+ else {
+ user_levels_section =
No users have specific privileges in this room.
+ }
+ var banned = this.props.room.getMembersWithMembership("ban");
var banned_users_section;
if (banned.length) {
banned_users_section =
@@ -288,79 +624,115 @@ module.exports = React.createClass({
;
}
+ var create_event = this.props.room.currentState.getStateEvents('m.room.create', '');
+ var unfederatable_section;
+ if (create_event.getContent()["m.federate"] === false) {
+ unfederatable_section =
Ths room is not accessible by remote Matrix servers.
+ }
+
+ // TODO: support editing custom events_levels
+ // TODO: support editing custom user_levels
+
+ var tags = [
+ { name: "m.favourite", label: "Favourite", ref: "tag_favourite" },
+ { name: "m.lowpriority", label: "Low priority", ref: "tag_lowpriority" },
+ ];
+
+ Object.keys(this.state.tags).sort().forEach(function(tagName) {
+ if (tagName !== 'm.favourite' && tagName !== 'm.lowpriority') {
+ tags.push({ name: tagName, label: tagName });
+ }
+ });
+
+ var tags_section =
+
+ This room is tagged as
+ { can_set_tag ?
+ tags.map(function(tag, i) {
+ return (
+
+ { tag.label }
+ );
+ }) : tags.map(function(tag) { return tag.label; }).join(", ")
+ }
+
+
return (
-
Make this room private
Share message history with new users
-
-
- Allow guests to read messages in this room
-
-
-
- Allow guests to join this room
-
+
Allow guests to read messages in this room
+
Allow guests to join this room
Encrypt room
+ { tags_section }
+
{ room_colors_section }
+ { aliases_section }
-
Power levels
+
Permissions
-
-
Ban level
-
+
+
The default role for new room members is
+
-
-
Kick level
-
+
+
To send messages, you must be a
+
-
-
Redact level
-
+
+
To invite users into the room, you must be a
+
-
-
Invite level
-
+
+
To configure the room, you must be a
+
-
-
Send event level
-
+
+
To kick users, you must be a
+
-
-
Set state level
-
+
+
To ban users, you must be a
+
-
-
Default user level
-
+
+
To redact messages, you must be a
+
-
-
User levels
-
- {Object.keys(user_levels).map(function(user, i) {
+ {Object.keys(events_levels).map(function(event_type, i) {
return (
-
-
{user}
-
+
+
To send events of type { event_type }
, you must be a
+
);
})}
+
+ { unfederatable_section }
+
+
+
Users
+
+
+ Your role in this room is currently
.
+
+
+ { user_levels_section }
- { events_levels_section }
{ banned_users_section }
- { change_avatar }
+
+
Advanced
+
+ This room's internal ID is { this.props.room.roomId }
+
+
);
}
diff --git a/src/components/views/settings/ChangeAvatar.js b/src/components/views/settings/ChangeAvatar.js
index 979b4b3c1b..7bd3d7754b 100644
--- a/src/components/views/settings/ChangeAvatar.js
+++ b/src/components/views/settings/ChangeAvatar.js
@@ -25,6 +25,8 @@ module.exports = React.createClass({
room: React.PropTypes.object,
// if false, you need to call changeAvatar.onFileSelected yourself.
showUploadSection: React.PropTypes.bool,
+ width: React.PropTypes.number,
+ height: React.PropTypes.number,
className: React.PropTypes.string
},
@@ -37,7 +39,9 @@ module.exports = React.createClass({
getDefaultProps: function() {
return {
showUploadSection: true,
- className: "mx_Dialog_content" // FIXME - shouldn't be this by default
+ className: "",
+ width: 80,
+ height: 80,
};
},
@@ -111,13 +115,14 @@ module.exports = React.createClass({
// Having just set an avatar we just display that since it will take a little
// time to propagate through to the RoomAvatar.
if (this.props.room && !this.avatarSet) {
- avatarImg =
;
+ avatarImg =
;
} else {
var style = {
- maxWidth: 240,
- maxHeight: 240,
+ width: this.props.width,
+ height: this.props.height,
objectFit: 'cover',
};
+ // FIXME: surely we should be using MemberAvatar or UserAvatar or something here...
avatarImg =
;
}
diff --git a/src/components/views/settings/ChangeDisplayName.js b/src/components/views/settings/ChangeDisplayName.js
index 8b31fbf1e3..ed5eb3fa42 100644
--- a/src/components/views/settings/ChangeDisplayName.js
+++ b/src/components/views/settings/ChangeDisplayName.js
@@ -99,7 +99,9 @@ module.exports = React.createClass({
var EditableText = sdk.getComponent('elements.EditableText');
return (
);
}