Merge pull request #2679 from matrix-org/travis/settings/field-editable-list

Fix AliasSettings and RelatedGroups UX
This commit is contained in:
Travis Ralston 2019-02-22 09:40:34 -07:00 committed by GitHub
commit d6f89f422b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 370 additions and 423 deletions

View file

@ -31,7 +31,6 @@ src/components/views/globals/UpdateCheckBar.js
src/components/views/messages/MFileBody.js src/components/views/messages/MFileBody.js
src/components/views/messages/RoomAvatarEvent.js src/components/views/messages/RoomAvatarEvent.js
src/components/views/messages/TextualBody.js src/components/views/messages/TextualBody.js
src/components/views/room_settings/AliasSettings.js
src/components/views/room_settings/ColorSettings.js src/components/views/room_settings/ColorSettings.js
src/components/views/rooms/Autocomplete.js src/components/views/rooms/Autocomplete.js
src/components/views/rooms/AuxPanel.js src/components/views/rooms/AuxPanel.js

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2017 New Vector Ltd. Copyright 2017, 2019 New Vector Ltd.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -16,47 +16,38 @@ limitations under the License.
.mx_EditableItemList { .mx_EditableItemList {
margin-top: 12px; margin-top: 12px;
margin-bottom: 0px; margin-bottom: 10px;
} }
.mx_EditableItem { .mx_EditableItem {
display: flex; margin-bottom: 5px;
margin-left: 56px; margin-left: 15px;
} }
.mx_EditableItem .mx_EditableItem_editable { .mx_EditableItem_delete {
border: 0px; margin-right: 5px;
border-bottom: 1px solid $strong-input-border-color;
padding: 0px;
min-width: 240px;
max-width: 400px;
margin-bottom: 16px;
}
.mx_EditableItem .mx_EditableItem_editable:focus {
border-bottom: 1px solid $accent-color;
outline: none;
box-shadow: none;
}
.mx_EditableItem .mx_EditableItem_editablePlaceholder {
color: $settings-grey-fg-color;
}
.mx_EditableItem .mx_EditableItem_addButton,
.mx_EditableItem .mx_EditableItem_removeButton {
padding-left: 0.5em;
position: relative;
cursor: pointer; cursor: pointer;
vertical-align: middle;
visibility: hidden;
} }
.mx_EditableItem:hover .mx_EditableItem_addButton, .mx_EditableItem_email {
.mx_EditableItem:hover .mx_EditableItem_removeButton { vertical-align: middle;
visibility: visible; }
.mx_EditableItem_promptText {
margin-right: 10px;
}
.mx_EditableItem_confirmBtn {
margin-right: 5px;
}
.mx_EditableItemList_newItem .mx_Field input {
// Use 100% of the space available for the input, but don't let the 10px
// padding on either side of the input to push it out of alignment.
width: calc(100% - 20px);
} }
.mx_EditableItemList_label { .mx_EditableItemList_label {
margin-bottom: 8px; margin-bottom: 5px;
} }

View file

@ -166,6 +166,36 @@ function textForGuestAccessEvent(ev) {
} }
} }
function textForRelatedGroupsEvent(ev) {
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
const groups = ev.getContent().groups || [];
const prevGroups = ev.getPrevContent().groups || [];
const added = groups.filter((g) => !prevGroups.includes(g));
const removed = prevGroups.filter((g) => !groups.includes(g));
if (added.length && !removed.length) {
return _t('%(senderDisplayName)s enabled flair for %(groups)s in this room.', {
senderDisplayName,
groups: added.join(', '),
});
} else if (!added.length && removed.length) {
return _t('%(senderDisplayName)s disabled flair for %(groups)s in this room.', {
senderDisplayName,
groups: removed.join(', '),
});
} else if (added.length && removed.length) {
return _t('%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for ' +
'%(oldGroups)s in this room.', {
senderDisplayName,
newGroups: added.join(', '),
oldGroups: removed.join(', '),
});
} else {
// Don't bother rendering this change (because there were no changes)
return '';
}
}
function textForServerACLEvent(ev) { function textForServerACLEvent(ev) {
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
const prevContent = ev.getPrevContent(); const prevContent = ev.getPrevContent();
@ -473,6 +503,7 @@ const stateHandlers = {
'm.room.tombstone': textForTombstoneEvent, 'm.room.tombstone': textForTombstoneEvent,
'm.room.join_rules': textForJoinRulesEvent, 'm.room.join_rules': textForJoinRulesEvent,
'm.room.guest_access': textForGuestAccessEvent, 'm.room.guest_access': textForGuestAccessEvent,
'm.room.related_groups': textForRelatedGroupsEvent,
'im.vector.modular.widgets': textForWidgetEvent, 'im.vector.modular.widgets': textForWidgetEvent,
}; };

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2017 New Vector Ltd. Copyright 2017, 2019 New Vector Ltd.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -16,142 +16,145 @@ limitations under the License.
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import sdk from '../../../index';
import {_t} from '../../../languageHandler.js'; import {_t} from '../../../languageHandler.js';
import Field from "./Field";
import AccessibleButton from "./AccessibleButton";
const EditableItem = React.createClass({ export class EditableItem extends React.Component {
displayName: 'EditableItem', static propTypes = {
propTypes: {
initialValue: PropTypes.string,
index: PropTypes.number, index: PropTypes.number,
placeholder: PropTypes.string, value: PropTypes.string,
onChange: PropTypes.func,
onRemove: PropTypes.func, onRemove: PropTypes.func,
onAdd: PropTypes.func, };
addOnChange: PropTypes.bool, constructor() {
}, super();
onChange: function(value) { this.state = {
this.setState({ value }); verifyRemove: false,
if (this.props.onChange) this.props.onChange(value, this.props.index); };
if (this.props.addOnChange && this.props.onAdd) this.props.onAdd(value); }
},
_onRemove = (e) => {
e.stopPropagation();
e.preventDefault();
this.setState({verifyRemove: true});
};
_onDontRemove = (e) => {
e.stopPropagation();
e.preventDefault();
this.setState({verifyRemove: false});
};
_onActuallyRemove = (e) => {
e.stopPropagation();
e.preventDefault();
onRemove: function() {
if (this.props.onRemove) this.props.onRemove(this.props.index); if (this.props.onRemove) this.props.onRemove(this.props.index);
}, this.setState({verifyRemove: false});
};
onAdd: function() { render() {
if (this.props.onAdd) this.props.onAdd(this.state.value); if (this.state.verifyRemove) {
}, return (
<div className="mx_EditableItem">
render: function() { <span className="mx_EditableItem_promptText">
const EditableText = sdk.getComponent('elements.EditableText'); {_t("Are you sure?")}
return <div className="mx_EditableItem"> </span>
<EditableText <AccessibleButton onClick={this._onActuallyRemove} kind="primary_sm"
className="mx_EditableItem_editable" className="mx_EditableItem_confirmBtn">
placeholderClassName="mx_EditableItem_editablePlaceholder" {_t("Yes")}
placeholder={this.props.placeholder} </AccessibleButton>
blurToCancel={false} <AccessibleButton onClick={this._onDontRemove} kind="danger_sm"
editable={true} className="mx_EditableItem_confirmBtn">
initialValue={this.props.initialValue} {_t("No")}
onValueChanged={this.onChange} /> </AccessibleButton>
{ this.props.onAdd ?
<div className="mx_EditableItem_addButton">
<img className="mx_filterFlipColor"
src={require("../../../../res/img/plus.svg")} width="14" height="14"
alt={_t("Add")} onClick={this.onAdd} />
</div> </div>
: );
<div className="mx_EditableItem_removeButton"> }
<img className="mx_filterFlipColor"
src={require("../../../../res/img/cancel-small.svg")} width="14" height="14"
alt={_t("Delete")} onClick={this.onRemove} />
</div>
}
</div>;
},
});
// TODO: Make this use the new Field element return (
module.exports = React.createClass({ <div className="mx_EditableItem">
displayName: 'EditableItemList', <img src={require("../../../../res/img/feather-icons/cancel.svg")} width={14} height={14}
onClick={this._onRemove} className="mx_EditableItem_delete" alt={_t("Remove")} />
<span className="mx_EditableItem_item">{this.props.value}</span>
</div>
);
}
}
propTypes: { export default class EditableItemList extends React.Component {
static propTypes = {
items: PropTypes.arrayOf(PropTypes.string).isRequired, items: PropTypes.arrayOf(PropTypes.string).isRequired,
onNewItemChanged: PropTypes.func, itemsLabel: PropTypes.string,
noItemsLabel: PropTypes.string,
placeholder: PropTypes.string,
newItem: PropTypes.string,
onItemAdded: PropTypes.func, onItemAdded: PropTypes.func,
onItemEdited: PropTypes.func,
onItemRemoved: PropTypes.func, onItemRemoved: PropTypes.func,
onNewItemChanged: PropTypes.func,
canEdit: PropTypes.bool, canEdit: PropTypes.bool,
}, canRemove: PropTypes.bool,
};
getDefaultProps: function() { _onItemAdded = (e) => {
return { e.stopPropagation();
onItemAdded: () => {}, e.preventDefault();
onItemEdited: () => {},
onItemRemoved: () => {},
onNewItemChanged: () => {},
};
},
onItemAdded: function(value) { if (this.props.onItemAdded) this.props.onItemAdded(this.props.newItem);
this.props.onItemAdded(value); };
},
onItemEdited: function(value, index) { _onItemRemoved = (index) => {
if (value.length === 0) { if (this.props.onItemRemoved) this.props.onItemRemoved(index);
this.onItemRemoved(index); };
} else {
this.props.onItemEdited(value, index);
}
},
onItemRemoved: function(index) { _onNewItemChanged = (e) => {
this.props.onItemRemoved(index); if (this.props.onNewItemChanged) this.props.onNewItemChanged(e.target.value);
}, };
onNewItemChanged: function(value) { _renderNewItemField() {
this.props.onNewItemChanged(value); return (
}, <form onSubmit={this._onItemAdded} autoComplete={false}
noValidate={true} className="mx_EditableItemList_newItem">
<Field id="newEmailAddress" label={this.props.placeholder}
type="text" autoComplete="off" value={this.props.newItem}
onChange={this._onNewItemChanged}
/>
<AccessibleButton onClick={this._onItemAdded} kind="primary">
{_t("Add")}
</AccessibleButton>
</form>
);
}
render: function() { render() {
const editableItems = this.props.items.map((item, index) => { const editableItems = this.props.items.map((item, index) => {
if (!this.props.canRemove) {
return <li>{item}</li>;
}
return <EditableItem return <EditableItem
key={index} key={index}
index={index} index={index}
initialValue={item} value={item}
onChange={this.onItemEdited} onRemove={this._onItemRemoved}
onRemove={this.onItemRemoved}
placeholder={this.props.placeholder}
/>; />;
}); });
const label = this.props.items.length > 0 ? const editableItemsSection = this.props.canRemove ? editableItems : <ul>{editableItems}</ul>;
this.props.itemsLabel : this.props.noItemsLabel; const label = this.props.items.length > 0 ? this.props.itemsLabel : this.props.noItemsLabel;
return (<div className="mx_EditableItemList"> return (<div className="mx_EditableItemList">
<div className="mx_EditableItemList_label"> <div className="mx_EditableItemList_label">
{ label } { label }
</div> </div>
{ editableItems } { editableItemsSection }
{ this.props.canEdit ? { this.props.canEdit ? this._renderNewItemField() : <div /> }
// This is slightly evil; we want a new instance of
// EditableItem when the list grows. To make sure it's
// reset to its initial state.
<EditableItem
key={editableItems.length}
initialValue={this.props.newItem}
onAdd={this.onItemAdded}
onChange={this.onNewItemChanged}
addOnChange={true}
placeholder={this.props.placeholder}
/> : <div />
}
</div>); </div>);
}, }
}); }

View file

@ -1,6 +1,6 @@
/* /*
Copyright 2016 OpenMarket Ltd Copyright 2016 OpenMarket Ltd
Copyright 2018 New Vector Ltd Copyright 2018, 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -15,112 +15,55 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import Promise from 'bluebird';
const React = require('react'); const React = require('react');
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
const ObjectUtils = require("../../../ObjectUtils");
const MatrixClientPeg = require('../../../MatrixClientPeg'); const MatrixClientPeg = require('../../../MatrixClientPeg');
const sdk = require("../../../index"); const sdk = require("../../../index");
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import Field from "../elements/Field"; import Field from "../elements/Field";
import ErrorDialog from "../dialogs/ErrorDialog";
const Modal = require("../../../Modal"); const Modal = require("../../../Modal");
module.exports = React.createClass({ export default class AliasSettings extends React.Component {
displayName: 'AliasSettings', static propTypes = {
propTypes: {
roomId: PropTypes.string.isRequired, roomId: PropTypes.string.isRequired,
canSetCanonicalAlias: PropTypes.bool.isRequired, canSetCanonicalAlias: PropTypes.bool.isRequired,
canSetAliases: PropTypes.bool.isRequired, canSetAliases: PropTypes.bool.isRequired,
aliasEvents: PropTypes.array, // [MatrixEvent] aliasEvents: PropTypes.array, // [MatrixEvent]
canonicalAliasEvent: PropTypes.object, // MatrixEvent canonicalAliasEvent: PropTypes.object, // MatrixEvent
}, };
getDefaultProps: function() { static defaultProps = {
return { canSetAliases: false,
canSetAliases: false, canSetCanonicalAlias: false,
canSetCanonicalAlias: false, aliasEvents: [],
aliasEvents: [], };
};
},
getInitialState: function() { constructor(props) {
return this.recalculateState(this.props.aliasEvents, this.props.canonicalAliasEvent); super(props);
},
recalculateState: function(aliasEvents, canonicalAliasEvent) {
aliasEvents = aliasEvents || [];
const state = { const state = {
domainToAliases: {}, // { domain.com => [#alias1:domain.com, #alias2:domain.com] } domainToAliases: {}, // { domain.com => [#alias1:domain.com, #alias2:domain.com] }
remoteDomains: [], // [ domain.com, foobar.com ] remoteDomains: [], // [ domain.com, foobar.com ]
canonicalAlias: null, // #canonical:domain.com canonicalAlias: null, // #canonical:domain.com
updatingCanonicalAlias: false,
newItem: "",
}; };
const localDomain = MatrixClientPeg.get().getDomain(); const localDomain = MatrixClientPeg.get().getDomain();
state.domainToAliases = this.aliasEventsToDictionary(props.aliasEvents || []);
state.domainToAliases = this.aliasEventsToDictionary(aliasEvents);
state.remoteDomains = Object.keys(state.domainToAliases).filter((domain) => { state.remoteDomains = Object.keys(state.domainToAliases).filter((domain) => {
return domain !== localDomain && state.domainToAliases[domain].length > 0; return domain !== localDomain && state.domainToAliases[domain].length > 0;
}); });
if (canonicalAliasEvent) { if (props.canonicalAliasEvent) {
state.canonicalAlias = canonicalAliasEvent.getContent().alias; state.canonicalAlias = props.canonicalAliasEvent.getContent().alias;
} }
return state; this.state = state;
}, }
saveSettings: function() { aliasEventsToDictionary(aliasEvents) { // m.room.alias events
let promises = [];
// save new aliases for m.room.aliases
const aliasOperations = this.getAliasOperations();
for (let i = 0; i < aliasOperations.length; i++) {
const 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);
}
}
let oldCanonicalAlias = null;
if (this.props.canonicalAliasEvent) {
oldCanonicalAlias = this.props.canonicalAliasEvent.getContent().alias;
}
const newCanonicalAlias = this.state.canonicalAlias;
if (this.props.canSetCanonicalAlias && oldCanonicalAlias !== newCanonicalAlias) {
console.log("AliasSettings: Updating canonical alias");
promises = [Promise.all(promises).then(
MatrixClientPeg.get().sendStateEvent(
this.props.roomId, "m.room.canonical_alias", {
alias: newCanonicalAlias,
}, "",
),
)];
}
return promises;
},
aliasEventsToDictionary: function(aliasEvents) { // m.room.alias events
const dict = {}; const dict = {};
aliasEvents.forEach((event) => { aliasEvents.forEach((event) => {
dict[event.getStateKey()] = ( dict[event.getStateKey()] = (
@ -128,35 +71,72 @@ module.exports = React.createClass({
); );
}); });
return dict; return dict;
}, }
isAliasValid: function(alias) { isAliasValid(alias) {
// XXX: FIXME https://github.com/matrix-org/matrix-doc/issues/668 // XXX: FIXME https://github.com/matrix-org/matrix-doc/issues/668
return (alias.match(/^#([^\/:,]+?):(.+)$/) && encodeURI(alias) === alias); return (alias.match(/^#([^/:,]+?):(.+)$/) && encodeURI(alias) === alias);
}, }
getAliasOperations: function() { changeCanonicalAlias(alias) {
const oldAliases = this.aliasEventsToDictionary(this.props.aliasEvents); if (!this.props.canSetCanonicalAlias) return;
return ObjectUtils.getKeyValueArrayDiffs(oldAliases, this.state.domainToAliases);
},
onNewAliasChanged: function(value) { this.setState({
canonicalAlias: alias,
updatingCanonicalAlias: true,
});
const eventContent = {};
if (alias) eventContent["alias"] = alias;
MatrixClientPeg.get().sendStateEvent(this.props.roomId, "m.room.canonical_alias",
eventContent, "").catch((err) => {
console.error(err);
Modal.createTrackedDialog('Error updating main address', '', ErrorDialog, {
title: _t("Error updating main address"),
description: _t(
"There was an error updating the room's main address. It may not be allowed by the server " +
"or a temporary failure occurred.",
),
});
}).finally(() => {
this.setState({updatingCanonicalAlias: false});
});
}
onNewAliasChanged = (value) => {
this.setState({newAlias: value}); this.setState({newAlias: value});
}, };
onLocalAliasAdded: function(alias) { onLocalAliasAdded = (alias) => {
if (!alias || alias.length === 0) return; // ignore attempts to create blank aliases if (!alias || alias.length === 0) return; // ignore attempts to create blank aliases
const localDomain = MatrixClientPeg.get().getDomain(); const localDomain = MatrixClientPeg.get().getDomain();
if (!alias.includes(':')) alias += ':' + localDomain; if (!alias.includes(':')) alias += ':' + localDomain;
if (this.isAliasValid(alias) && alias.endsWith(localDomain)) { if (this.isAliasValid(alias) && alias.endsWith(localDomain)) {
this.state.domainToAliases[localDomain] = this.state.domainToAliases[localDomain] || []; MatrixClientPeg.get().createAlias(alias, this.props.roomId).then(() => {
this.state.domainToAliases[localDomain].push(alias); const localAliases = this.state.domainToAliases[localDomain] || [];
const domainAliases = Object.assign({}, this.state.domainToAliases);
domainAliases[localDomain] = [...localAliases, alias];
this.setState({ this.setState({
domainToAliases: this.state.domainToAliases, domainToAliases: domainAliases,
// Reset the add field // Reset the add field
newAlias: "", newAlias: "",
});
if (!this.state.canonicalAlias) {
this.changeCanonicalAlias(alias);
}
}).catch((err) => {
console.error(err);
Modal.createTrackedDialog('Error creating alias', '', ErrorDialog, {
title: _t("Error creating alias"),
description: _t(
"There was an error creating that alias. It may not be allowed by the server " +
"or a temporary failure occurred.",
),
});
}); });
} else { } else {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
@ -165,130 +145,102 @@ module.exports = React.createClass({
description: _t('\'%(alias)s\' is not a valid format for an alias', { alias: alias }), description: _t('\'%(alias)s\' is not a valid format for an alias', { alias: alias }),
}); });
} }
};
if (!this.props.canonicalAlias) { onLocalAliasDeleted = (index) => {
this.setState({
canonicalAlias: alias,
});
}
},
onLocalAliasChanged: function(alias, index) {
if (alias === "") return; // hit the delete button to delete please
const localDomain = MatrixClientPeg.get().getDomain(); const localDomain = MatrixClientPeg.get().getDomain();
if (!alias.includes(':')) alias += ':' + localDomain;
if (this.isAliasValid(alias) && alias.endsWith(localDomain)) { const alias = this.state.domainToAliases[localDomain][index];
this.state.domainToAliases[localDomain][index] = alias;
} else { // TODO: In future, we should probably be making sure that the alias actually belongs
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); // to this room. See https://github.com/vector-im/riot-web/issues/7353
Modal.createTrackedDialog('Invalid address format', '', ErrorDialog, { MatrixClientPeg.get().deleteAlias(alias).then(() => {
title: _t('Invalid address format'), const localAliases = this.state.domainToAliases[localDomain].filter((a) => a !== alias);
description: _t('\'%(alias)s\' is not a valid format for an address', { alias: alias }), const domainAliases = Object.assign({}, this.state.domainToAliases);
domainAliases[localDomain] = localAliases;
this.setState({domainToAliases: domainAliases});
if (this.state.canonicalAlias === alias) {
this.changeCanonicalAlias(null);
}
}).catch((err) => {
console.error(err);
Modal.createTrackedDialog('Error removing alias', '', ErrorDialog, {
title: _t("Error removing alias"),
description: _t(
"There was an error removing that alias. It may no longer exist or a temporary " +
"error occurred.",
),
}); });
}
},
onLocalAliasDeleted: function(index) {
const localDomain = MatrixClientPeg.get().getDomain();
// 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.
const alias = this.state.domainToAliases[localDomain].splice(index, 1);
this.setState({
domainToAliases: this.state.domainToAliases,
}); });
if (this.props.canonicalAlias === alias) { };
this.setState({
canonicalAlias: null,
});
}
},
onCanonicalAliasChange: function(event) { onCanonicalAliasChange = (event) => {
this.setState({ this.changeCanonicalAlias(event.target.value);
canonicalAlias: event.target.value, };
});
},
render: function() { render() {
const self = this;
const EditableText = sdk.getComponent("elements.EditableText");
const EditableItemList = sdk.getComponent("elements.EditableItemList"); const EditableItemList = sdk.getComponent("elements.EditableItemList");
const localDomain = MatrixClientPeg.get().getDomain(); const localDomain = MatrixClientPeg.get().getDomain();
let canonical_alias_section; let found = false;
if (this.props.canSetCanonicalAlias) { const canonicalValue = this.state.canonicalAlias || "";
let found = false; const canonicalAliasSection = (
const canonicalValue = this.state.canonicalAlias || ""; <Field onChange={this.onCanonicalAliasChange} value={canonicalValue}
canonical_alias_section = ( disabled={this.state.updatingCanonicalAlias || !this.props.canSetCanonicalAlias}
<Field onChange={this.onCanonicalAliasChange} value={canonicalValue} element='select' id='canonicalAlias' label={_t('Main address')}>
element='select' id='canonicalAlias' label={_t('Main address')}> <option value="" key="unset">{ _t('not specified') }</option>
<option value="" key="unset">{ _t('not specified') }</option> {
{ Object.keys(this.state.domainToAliases).map((domain, i) => {
Object.keys(self.state.domainToAliases).map((domain, i) => { return this.state.domainToAliases[domain].map((alias, j) => {
return self.state.domainToAliases[domain].map((alias, j) => { if (alias === this.state.canonicalAlias) found = true;
if (alias === this.state.canonicalAlias) found = true; return (
return ( <option value={alias} key={i + "_" + j}>
<option value={alias} key={i + "_" + j}> { alias }
{ alias } </option>
</option> );
); });
}); })
}) }
} {
{ found || !this.state.canonicalAlias ? '' :
found || !this.stateCanonicalAlias ? '' : <option value={ this.state.canonicalAlias } key='arbitrary'>
<option value={ this.state.canonicalAlias } key='arbitrary'> { this.state.canonicalAlias }
{ this.state.canonicalAlias } </option>
</option> }
} </Field>
</Field> );
);
} else {
canonical_alias_section = (
<b>{ this.state.canonicalAlias || _t('not set') }</b>
);
}
let remote_aliases_section; let remoteAliasesSection;
if (this.state.remoteDomains.length) { if (this.state.remoteDomains.length) {
remote_aliases_section = ( remoteAliasesSection = (
<div> <div>
<div> <div>
{ _t("Remote addresses for this room:") } { _t("Remote addresses for this room:") }
</div> </div>
<div> <ul>
{ this.state.remoteDomains.map((domain, i) => { { this.state.remoteDomains.map((domain, i) => {
return this.state.domainToAliases[domain].map(function(alias, j) { return this.state.domainToAliases[domain].map((alias, j) => {
return ( return <li key={i + "_" + j}>{alias}</li>;
<div key={i + "_" + j}>
<EditableText
className="mx_AliasSettings_alias mx_AliasSettings_editable"
blurToCancel={false}
editable={false}
initialValue={alias} />
</div>
);
}); });
}) } }) }
</div> </ul>
</div> </div>
); );
} }
return ( return (
<div className='mx_AliasSettings'> <div className='mx_AliasSettings'>
{canonical_alias_section} {canonicalAliasSection}
<EditableItemList <EditableItemList
className={"mx_RoomSettings_localAliases"} className={"mx_RoomSettings_localAliases"}
items={this.state.domainToAliases[localDomain] || []} items={this.state.domainToAliases[localDomain] || []}
newItem={this.state.newAlias} newItem={this.state.newAlias}
onNewItemChanged={this.onNewAliasChanged} onNewItemChanged={this.onNewAliasChanged}
canRemove={this.props.canSetAliases}
canEdit={this.props.canSetAliases} canEdit={this.props.canSetAliases}
onItemAdded={this.onLocalAliasAdded} onItemAdded={this.onLocalAliasAdded}
onItemEdited={this.onLocalAliasChanged}
onItemRemoved={this.onLocalAliasDeleted} onItemRemoved={this.onLocalAliasDeleted}
itemsLabel={_t('Local addresses for this room:')} itemsLabel={_t('Local addresses for this room:')}
noItemsLabel={_t('This room has no local addresses')} noItemsLabel={_t('This room has no local addresses')}
@ -296,10 +248,8 @@ module.exports = React.createClass({
'New address (e.g. #foo:%(localDomain)s)', {localDomain: localDomain}, 'New address (e.g. #foo:%(localDomain)s)', {localDomain: localDomain},
)} )}
/> />
{remoteAliasesSection}
{ remote_aliases_section }
</div> </div>
); );
}, }
}); }

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2017 New Vector Ltd. Copyright 2017, 2019 New Vector Ltd.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -20,61 +20,50 @@ import {MatrixEvent, MatrixClient} from 'matrix-js-sdk';
import sdk from '../../../index'; import sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import isEqual from 'lodash/isEqual'; import ErrorDialog from "../dialogs/ErrorDialog";
const GROUP_ID_REGEX = /\+\S+:\S+/; const GROUP_ID_REGEX = /\+\S+:\S+/;
module.exports = React.createClass({ export default class RelatedGroupSettings extends React.Component {
displayName: 'RelatedGroupSettings', static propTypes = {
propTypes: {
roomId: PropTypes.string.isRequired, roomId: PropTypes.string.isRequired,
canSetRelatedGroups: PropTypes.bool.isRequired, canSetRelatedGroups: PropTypes.bool.isRequired,
relatedGroupsEvent: PropTypes.instanceOf(MatrixEvent), relatedGroupsEvent: PropTypes.instanceOf(MatrixEvent),
}, };
contextTypes: { static contextTypes = {
matrixClient: PropTypes.instanceOf(MatrixClient), matrixClient: PropTypes.instanceOf(MatrixClient),
}, };
getDefaultProps: function() { static defaultProps = {
return { canSetRelatedGroups: false,
canSetRelatedGroups: false, };
constructor(props) {
super(props);
this.state = {
newGroupId: "",
newGroupsList: props.relatedGroupsEvent ? (props.relatedGroupsEvent.getContent().groups || []) : [],
}; };
}, }
getInitialState: function() { updateGroups(newGroupsList) {
return { this.context.matrixClient.sendStateEvent(this.props.roomId, 'm.room.related_groups', {
newGroupsList: this.getInitialGroupList(), groups: newGroupsList,
newGroupId: null, }, '').catch((err) => {
}; console.error(err);
}, Modal.createTrackedDialog('Error updating flair', '', ErrorDialog, {
title: _t("Error updating flair"),
description: _t(
"There was an error updating the flair for this room. The server may not allow it or " +
"a temporary error occurred.",
),
});
});
}
getInitialGroupList: function() { validateGroupId(groupId) {
return this.props.relatedGroupsEvent ? (this.props.relatedGroupsEvent.getContent().groups || []) : [];
},
needsSaving: function() {
const cli = this.context.matrixClient;
const room = cli.getRoom(this.props.roomId);
if (!room.currentState.maySendStateEvent('m.room.related_groups', cli.getUserId())) return false;
return !isEqual(this.getInitialGroupList(), this.state.newGroupsList);
},
saveSettings: function() {
if (!this.needsSaving()) return Promise.resolve();
return this.context.matrixClient.sendStateEvent(
this.props.roomId,
'm.room.related_groups',
{
groups: this.state.newGroupsList,
},
'',
);
},
validateGroupId: function(groupId) {
if (!GROUP_ID_REGEX.test(groupId)) { if (!GROUP_ID_REGEX.test(groupId)) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Invalid related community ID', '', ErrorDialog, { Modal.createTrackedDialog('Invalid related community ID', '', ErrorDialog, {
@ -84,38 +73,32 @@ module.exports = React.createClass({
return false; return false;
} }
return true; return true;
}, }
onNewGroupChanged: function(newGroupId) { onNewGroupChanged = (newGroupId) => {
this.setState({ newGroupId }); this.setState({ newGroupId });
}, };
onGroupAdded: function(groupId) { onGroupAdded = (groupId) => {
if (groupId.length === 0 || !this.validateGroupId(groupId)) { if (groupId.length === 0 || !this.validateGroupId(groupId)) {
return; return;
} }
const newGroupsList = [...this.state.newGroupsList, groupId];
this.setState({ this.setState({
newGroupsList: this.state.newGroupsList.concat([groupId]), newGroupsList: newGroupsList,
newGroupId: '', newGroupId: '',
}); });
}, this.updateGroups(newGroupsList);
};
onGroupEdited: function(groupId, index) { onGroupDeleted = (index) => {
if (groupId.length === 0 || !this.validateGroupId(groupId)) { const group = this.state.newGroupsList[index];
return; const newGroupsList = this.state.newGroupsList.filter((g) => g !== group);
}
this.setState({
newGroupsList: Object.assign(this.state.newGroupsList, {[index]: groupId}),
});
},
onGroupDeleted: function(index) {
const newGroupsList = this.state.newGroupsList.slice();
newGroupsList.splice(index, 1);
this.setState({ newGroupsList }); this.setState({ newGroupsList });
}, this.updateGroups(newGroupsList);
};
render: function() { render() {
const localDomain = this.context.matrixClient.getDomain(); const localDomain = this.context.matrixClient.getDomain();
const EditableItemList = sdk.getComponent('elements.EditableItemList'); const EditableItemList = sdk.getComponent('elements.EditableItemList');
return <div> return <div>
@ -123,10 +106,10 @@ module.exports = React.createClass({
items={this.state.newGroupsList} items={this.state.newGroupsList}
className={"mx_RelatedGroupSettings"} className={"mx_RelatedGroupSettings"}
newItem={this.state.newGroupId} newItem={this.state.newGroupId}
canRemove={this.props.canSetRelatedGroups}
canEdit={this.props.canSetRelatedGroups} canEdit={this.props.canSetRelatedGroups}
onNewItemChanged={this.onNewGroupChanged} onNewItemChanged={this.onNewGroupChanged}
onItemAdded={this.onGroupAdded} onItemAdded={this.onGroupAdded}
onItemEdited={this.onGroupEdited}
onItemRemoved={this.onGroupDeleted} onItemRemoved={this.onGroupDeleted}
itemsLabel={_t('Showing flair for these communities:')} itemsLabel={_t('Showing flair for these communities:')}
noItemsLabel={_t('This room is not showing flair for any communities')} noItemsLabel={_t('This room is not showing flair for any communities')}
@ -135,5 +118,5 @@ module.exports = React.createClass({
)} )}
/> />
</div>; </div>;
}, }
}); }

View file

@ -65,6 +65,7 @@ const stateEventTileTypes = {
'm.room.tombstone': 'messages.TextualEvent', 'm.room.tombstone': 'messages.TextualEvent',
'm.room.join_rules': 'messages.TextualEvent', 'm.room.join_rules': 'messages.TextualEvent',
'm.room.guest_access': 'messages.TextualEvent', 'm.room.guest_access': 'messages.TextualEvent',
'm.room.related_groups': 'messages.TextualEvent',
}; };
function getHandlerTile(ev) { function getHandlerTile(ev) {

View file

@ -68,18 +68,6 @@ export default class GeneralRoomSettingsTab extends React.Component {
}); });
}; };
_saveAliases = (e) => {
// TODO: Live modification?
if (!this.refs.aliasSettings) return;
this.refs.aliasSettings.saveSettings();
};
_saveGroups = (e) => {
// TODO: Live modification?
if (!this.refs.flairSettings) return;
this.refs.flairSettings.saveSettings();
};
_onLeaveClick = () => { _onLeaveClick = () => {
dis.dispatch({ dis.dispatch({
action: 'leave_room', action: 'leave_room',
@ -113,12 +101,9 @@ export default class GeneralRoomSettingsTab extends React.Component {
<span className='mx_SettingsTab_subheading'>{_t("Room Addresses")}</span> <span className='mx_SettingsTab_subheading'>{_t("Room Addresses")}</span>
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'> <div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
<AliasSettings ref="aliasSettings" roomId={this.props.roomId} <AliasSettings roomId={this.props.roomId}
canSetCanonicalAlias={canSetCanonical} canSetAliases={canSetAliases} canSetCanonicalAlias={canSetCanonical} canSetAliases={canSetAliases}
canonicalAliasEvent={canonicalAliasEv} aliasEvents={aliasEvents} /> canonicalAliasEvent={canonicalAliasEv} aliasEvents={aliasEvents} />
<AccessibleButton onClick={this._saveAliases} kind='primary'>
{_t("Save")}
</AccessibleButton>
</div> </div>
<div className='mx_SettingsTab_section'> <div className='mx_SettingsTab_section'>
<LabelledToggleSwitch value={this.state.isRoomPublished} <LabelledToggleSwitch value={this.state.isRoomPublished}
@ -131,12 +116,9 @@ export default class GeneralRoomSettingsTab extends React.Component {
<span className='mx_SettingsTab_subheading'>{_t("Flair")}</span> <span className='mx_SettingsTab_subheading'>{_t("Flair")}</span>
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'> <div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
<RelatedGroupSettings ref="flairSettings" roomId={room.roomId} <RelatedGroupSettings roomId={room.roomId}
canSetRelatedGroups={canChangeGroups} canSetRelatedGroups={canChangeGroups}
relatedGroupsEvent={groupsEvent} /> relatedGroupsEvent={groupsEvent} />
<AccessibleButton onClick={this._saveGroups} kind='primary'>
{_t("Save")}
</AccessibleButton>
</div> </div>
<span className='mx_SettingsTab_subheading'>{_t("URL Previews")}</span> <span className='mx_SettingsTab_subheading'>{_t("URL Previews")}</span>

View file

@ -192,6 +192,9 @@
"%(senderDisplayName)s has allowed guests to join the room.": "%(senderDisplayName)s has allowed guests to join the room.", "%(senderDisplayName)s has allowed guests to join the room.": "%(senderDisplayName)s has allowed guests to join the room.",
"%(senderDisplayName)s has prevented guests from joining the room.": "%(senderDisplayName)s has prevented guests from joining the room.", "%(senderDisplayName)s has prevented guests from joining the room.": "%(senderDisplayName)s has prevented guests from joining the room.",
"%(senderDisplayName)s changed guest access to %(rule)s": "%(senderDisplayName)s changed guest access to %(rule)s", "%(senderDisplayName)s changed guest access to %(rule)s": "%(senderDisplayName)s changed guest access to %(rule)s",
"%(senderDisplayName)s enabled flair for %(groups)s in this room.": "%(senderDisplayName)s enabled flair for %(groups)s in this room.",
"%(senderDisplayName)s disabled flair for %(groups)s in this room.": "%(senderDisplayName)s disabled flair for %(groups)s in this room.",
"%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.": "%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.",
"%(senderDisplayName)s sent an image.": "%(senderDisplayName)s sent an image.", "%(senderDisplayName)s sent an image.": "%(senderDisplayName)s sent an image.",
"%(senderName)s added %(count)s %(addedAddresses)s as addresses for this room.|other": "%(senderName)s added %(addedAddresses)s as addresses for this room.", "%(senderName)s added %(count)s %(addedAddresses)s as addresses for this room.|other": "%(senderName)s added %(addedAddresses)s as addresses for this room.",
"%(senderName)s added %(count)s %(addedAddresses)s as addresses for this room.|one": "%(senderName)s added %(addedAddresses)s as an address for this room.", "%(senderName)s added %(count)s %(addedAddresses)s as addresses for this room.|one": "%(senderName)s added %(addedAddresses)s as an address for this room.",
@ -804,17 +807,22 @@
"Hide Stickers": "Hide Stickers", "Hide Stickers": "Hide Stickers",
"Show Stickers": "Show Stickers", "Show Stickers": "Show Stickers",
"Jump to first unread message.": "Jump to first unread message.", "Jump to first unread message.": "Jump to first unread message.",
"Error updating main address": "Error updating main address",
"There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.": "There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.",
"Error creating alias": "Error creating alias",
"There was an error creating that alias. It may not be allowed by the server or a temporary failure occurred.": "There was an error creating that alias. It may not be allowed by the server or a temporary failure occurred.",
"Invalid alias format": "Invalid alias format", "Invalid alias format": "Invalid alias format",
"'%(alias)s' is not a valid format for an alias": "'%(alias)s' is not a valid format for an alias", "'%(alias)s' is not a valid format for an alias": "'%(alias)s' is not a valid format for an alias",
"Invalid address format": "Invalid address format", "Error removing alias": "Error removing alias",
"'%(alias)s' is not a valid format for an address": "'%(alias)s' is not a valid format for an address", "There was an error removing that alias. It may no longer exist or a temporary error occurred.": "There was an error removing that alias. It may no longer exist or a temporary error occurred.",
"Main address": "Main address", "Main address": "Main address",
"not specified": "not specified", "not specified": "not specified",
"not set": "not set",
"Remote addresses for this room:": "Remote addresses for this room:", "Remote addresses for this room:": "Remote addresses for this room:",
"Local addresses for this room:": "Local addresses for this room:", "Local addresses for this room:": "Local addresses for this room:",
"This room has no local addresses": "This room has no local addresses", "This room has no local addresses": "This room has no local addresses",
"New address (e.g. #foo:%(localDomain)s)": "New address (e.g. #foo:%(localDomain)s)", "New address (e.g. #foo:%(localDomain)s)": "New address (e.g. #foo:%(localDomain)s)",
"Error updating flair": "Error updating flair",
"There was an error updating the flair for this room. The server may not allow it or a temporary error occurred.": "There was an error updating the flair for this room. The server may not allow it or a temporary error occurred.",
"Invalid community ID": "Invalid community ID", "Invalid community ID": "Invalid community ID",
"'%(groupId)s' is not a valid community ID": "'%(groupId)s' is not a valid community ID", "'%(groupId)s' is not a valid community ID": "'%(groupId)s' is not a valid community ID",
"Showing flair for these communities:": "Showing flair for these communities:", "Showing flair for these communities:": "Showing flair for these communities:",
@ -925,7 +933,6 @@
"Verify...": "Verify...", "Verify...": "Verify...",
"Join": "Join", "Join": "Join",
"No results": "No results", "No results": "No results",
"Delete": "Delete",
"Communities": "Communities", "Communities": "Communities",
"Home": "Home", "Home": "Home",
"You cannot delete this image. (%(code)s)": "You cannot delete this image. (%(code)s)", "You cannot delete this image. (%(code)s)": "You cannot delete this image. (%(code)s)",