Merge pull request #2679 from matrix-org/travis/settings/field-editable-list
Fix AliasSettings and RelatedGroups UX
This commit is contained in:
commit
d6f89f422b
9 changed files with 370 additions and 423 deletions
|
@ -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");
|
||||
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 PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import {_t} from '../../../languageHandler.js';
|
||||
import Field from "./Field";
|
||||
import AccessibleButton from "./AccessibleButton";
|
||||
|
||||
const EditableItem = React.createClass({
|
||||
displayName: 'EditableItem',
|
||||
|
||||
propTypes: {
|
||||
initialValue: PropTypes.string,
|
||||
export class EditableItem extends React.Component {
|
||||
static propTypes = {
|
||||
index: PropTypes.number,
|
||||
placeholder: PropTypes.string,
|
||||
|
||||
onChange: PropTypes.func,
|
||||
value: PropTypes.string,
|
||||
onRemove: PropTypes.func,
|
||||
onAdd: PropTypes.func,
|
||||
};
|
||||
|
||||
addOnChange: PropTypes.bool,
|
||||
},
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
onChange: function(value) {
|
||||
this.setState({ value });
|
||||
if (this.props.onChange) this.props.onChange(value, this.props.index);
|
||||
if (this.props.addOnChange && this.props.onAdd) this.props.onAdd(value);
|
||||
},
|
||||
this.state = {
|
||||
verifyRemove: false,
|
||||
};
|
||||
}
|
||||
|
||||
_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);
|
||||
},
|
||||
this.setState({verifyRemove: false});
|
||||
};
|
||||
|
||||
onAdd: function() {
|
||||
if (this.props.onAdd) this.props.onAdd(this.state.value);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const EditableText = sdk.getComponent('elements.EditableText');
|
||||
return <div className="mx_EditableItem">
|
||||
<EditableText
|
||||
className="mx_EditableItem_editable"
|
||||
placeholderClassName="mx_EditableItem_editablePlaceholder"
|
||||
placeholder={this.props.placeholder}
|
||||
blurToCancel={false}
|
||||
editable={true}
|
||||
initialValue={this.props.initialValue}
|
||||
onValueChanged={this.onChange} />
|
||||
{ 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} />
|
||||
render() {
|
||||
if (this.state.verifyRemove) {
|
||||
return (
|
||||
<div className="mx_EditableItem">
|
||||
<span className="mx_EditableItem_promptText">
|
||||
{_t("Are you sure?")}
|
||||
</span>
|
||||
<AccessibleButton onClick={this._onActuallyRemove} kind="primary_sm"
|
||||
className="mx_EditableItem_confirmBtn">
|
||||
{_t("Yes")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton onClick={this._onDontRemove} kind="danger_sm"
|
||||
className="mx_EditableItem_confirmBtn">
|
||||
{_t("No")}
|
||||
</AccessibleButton>
|
||||
</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
|
||||
module.exports = React.createClass({
|
||||
displayName: 'EditableItemList',
|
||||
return (
|
||||
<div className="mx_EditableItem">
|
||||
<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,
|
||||
onNewItemChanged: PropTypes.func,
|
||||
itemsLabel: PropTypes.string,
|
||||
noItemsLabel: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
newItem: PropTypes.string,
|
||||
|
||||
onItemAdded: PropTypes.func,
|
||||
onItemEdited: PropTypes.func,
|
||||
onItemRemoved: PropTypes.func,
|
||||
onNewItemChanged: PropTypes.func,
|
||||
|
||||
canEdit: PropTypes.bool,
|
||||
},
|
||||
canRemove: PropTypes.bool,
|
||||
};
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
onItemAdded: () => {},
|
||||
onItemEdited: () => {},
|
||||
onItemRemoved: () => {},
|
||||
onNewItemChanged: () => {},
|
||||
};
|
||||
},
|
||||
_onItemAdded = (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
onItemAdded: function(value) {
|
||||
this.props.onItemAdded(value);
|
||||
},
|
||||
if (this.props.onItemAdded) this.props.onItemAdded(this.props.newItem);
|
||||
};
|
||||
|
||||
onItemEdited: function(value, index) {
|
||||
if (value.length === 0) {
|
||||
this.onItemRemoved(index);
|
||||
} else {
|
||||
this.props.onItemEdited(value, index);
|
||||
}
|
||||
},
|
||||
_onItemRemoved = (index) => {
|
||||
if (this.props.onItemRemoved) this.props.onItemRemoved(index);
|
||||
};
|
||||
|
||||
onItemRemoved: function(index) {
|
||||
this.props.onItemRemoved(index);
|
||||
},
|
||||
_onNewItemChanged = (e) => {
|
||||
if (this.props.onNewItemChanged) this.props.onNewItemChanged(e.target.value);
|
||||
};
|
||||
|
||||
onNewItemChanged: function(value) {
|
||||
this.props.onNewItemChanged(value);
|
||||
},
|
||||
_renderNewItemField() {
|
||||
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) => {
|
||||
if (!this.props.canRemove) {
|
||||
return <li>{item}</li>;
|
||||
}
|
||||
|
||||
return <EditableItem
|
||||
key={index}
|
||||
index={index}
|
||||
initialValue={item}
|
||||
onChange={this.onItemEdited}
|
||||
onRemove={this.onItemRemoved}
|
||||
placeholder={this.props.placeholder}
|
||||
value={item}
|
||||
onRemove={this._onItemRemoved}
|
||||
/>;
|
||||
});
|
||||
|
||||
const label = this.props.items.length > 0 ?
|
||||
this.props.itemsLabel : this.props.noItemsLabel;
|
||||
const editableItemsSection = this.props.canRemove ? editableItems : <ul>{editableItems}</ul>;
|
||||
const label = this.props.items.length > 0 ? this.props.itemsLabel : this.props.noItemsLabel;
|
||||
|
||||
return (<div className="mx_EditableItemList">
|
||||
<div className="mx_EditableItemList_label">
|
||||
{ label }
|
||||
</div>
|
||||
{ editableItems }
|
||||
{ this.props.canEdit ?
|
||||
// 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 />
|
||||
}
|
||||
{ editableItemsSection }
|
||||
{ this.props.canEdit ? this._renderNewItemField() : <div /> }
|
||||
</div>);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
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");
|
||||
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.
|
||||
*/
|
||||
|
||||
import Promise from 'bluebird';
|
||||
const React = require('react');
|
||||
import PropTypes from 'prop-types';
|
||||
const ObjectUtils = require("../../../ObjectUtils");
|
||||
const MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
const sdk = require("../../../index");
|
||||
import { _t } from '../../../languageHandler';
|
||||
import Field from "../elements/Field";
|
||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||
const Modal = require("../../../Modal");
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'AliasSettings',
|
||||
|
||||
propTypes: {
|
||||
export default class AliasSettings extends React.Component {
|
||||
static propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
canSetCanonicalAlias: PropTypes.bool.isRequired,
|
||||
canSetAliases: PropTypes.bool.isRequired,
|
||||
aliasEvents: PropTypes.array, // [MatrixEvent]
|
||||
canonicalAliasEvent: PropTypes.object, // MatrixEvent
|
||||
},
|
||||
};
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
canSetAliases: false,
|
||||
canSetCanonicalAlias: false,
|
||||
aliasEvents: [],
|
||||
};
|
||||
},
|
||||
static defaultProps = {
|
||||
canSetAliases: false,
|
||||
canSetCanonicalAlias: false,
|
||||
aliasEvents: [],
|
||||
};
|
||||
|
||||
getInitialState: function() {
|
||||
return this.recalculateState(this.props.aliasEvents, this.props.canonicalAliasEvent);
|
||||
},
|
||||
|
||||
recalculateState: function(aliasEvents, canonicalAliasEvent) {
|
||||
aliasEvents = aliasEvents || [];
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const state = {
|
||||
domainToAliases: {}, // { domain.com => [#alias1:domain.com, #alias2:domain.com] }
|
||||
remoteDomains: [], // [ domain.com, foobar.com ]
|
||||
canonicalAlias: null, // #canonical:domain.com
|
||||
updatingCanonicalAlias: false,
|
||||
newItem: "",
|
||||
};
|
||||
|
||||
const localDomain = MatrixClientPeg.get().getDomain();
|
||||
|
||||
state.domainToAliases = this.aliasEventsToDictionary(aliasEvents);
|
||||
|
||||
state.domainToAliases = this.aliasEventsToDictionary(props.aliasEvents || []);
|
||||
state.remoteDomains = Object.keys(state.domainToAliases).filter((domain) => {
|
||||
return domain !== localDomain && state.domainToAliases[domain].length > 0;
|
||||
});
|
||||
|
||||
if (canonicalAliasEvent) {
|
||||
state.canonicalAlias = canonicalAliasEvent.getContent().alias;
|
||||
if (props.canonicalAliasEvent) {
|
||||
state.canonicalAlias = props.canonicalAliasEvent.getContent().alias;
|
||||
}
|
||||
|
||||
return state;
|
||||
},
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
saveSettings: function() {
|
||||
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
|
||||
aliasEventsToDictionary(aliasEvents) { // m.room.alias events
|
||||
const dict = {};
|
||||
aliasEvents.forEach((event) => {
|
||||
dict[event.getStateKey()] = (
|
||||
|
@ -128,35 +71,72 @@ module.exports = React.createClass({
|
|||
);
|
||||
});
|
||||
return dict;
|
||||
},
|
||||
}
|
||||
|
||||
isAliasValid: function(alias) {
|
||||
isAliasValid(alias) {
|
||||
// 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() {
|
||||
const oldAliases = this.aliasEventsToDictionary(this.props.aliasEvents);
|
||||
return ObjectUtils.getKeyValueArrayDiffs(oldAliases, this.state.domainToAliases);
|
||||
},
|
||||
changeCanonicalAlias(alias) {
|
||||
if (!this.props.canSetCanonicalAlias) return;
|
||||
|
||||
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});
|
||||
},
|
||||
};
|
||||
|
||||
onLocalAliasAdded: function(alias) {
|
||||
onLocalAliasAdded = (alias) => {
|
||||
if (!alias || alias.length === 0) return; // ignore attempts to create blank aliases
|
||||
|
||||
const localDomain = MatrixClientPeg.get().getDomain();
|
||||
if (!alias.includes(':')) alias += ':' + localDomain;
|
||||
if (this.isAliasValid(alias) && alias.endsWith(localDomain)) {
|
||||
this.state.domainToAliases[localDomain] = this.state.domainToAliases[localDomain] || [];
|
||||
this.state.domainToAliases[localDomain].push(alias);
|
||||
MatrixClientPeg.get().createAlias(alias, this.props.roomId).then(() => {
|
||||
const localAliases = this.state.domainToAliases[localDomain] || [];
|
||||
const domainAliases = Object.assign({}, this.state.domainToAliases);
|
||||
domainAliases[localDomain] = [...localAliases, alias];
|
||||
|
||||
this.setState({
|
||||
domainToAliases: this.state.domainToAliases,
|
||||
// Reset the add field
|
||||
newAlias: "",
|
||||
this.setState({
|
||||
domainToAliases: domainAliases,
|
||||
// Reset the add field
|
||||
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 {
|
||||
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 }),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (!this.props.canonicalAlias) {
|
||||
this.setState({
|
||||
canonicalAlias: alias,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onLocalAliasChanged: function(alias, index) {
|
||||
if (alias === "") return; // hit the delete button to delete please
|
||||
onLocalAliasDeleted = (index) => {
|
||||
const localDomain = MatrixClientPeg.get().getDomain();
|
||||
if (!alias.includes(':')) alias += ':' + localDomain;
|
||||
if (this.isAliasValid(alias) && alias.endsWith(localDomain)) {
|
||||
this.state.domainToAliases[localDomain][index] = alias;
|
||||
} else {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Invalid address format', '', ErrorDialog, {
|
||||
title: _t('Invalid address format'),
|
||||
description: _t('\'%(alias)s\' is not a valid format for an address', { alias: alias }),
|
||||
|
||||
const alias = this.state.domainToAliases[localDomain][index];
|
||||
|
||||
// TODO: In future, we should probably be making sure that the alias actually belongs
|
||||
// to this room. See https://github.com/vector-im/riot-web/issues/7353
|
||||
MatrixClientPeg.get().deleteAlias(alias).then(() => {
|
||||
const localAliases = this.state.domainToAliases[localDomain].filter((a) => a !== 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) {
|
||||
this.setState({
|
||||
canonicalAlias: event.target.value,
|
||||
});
|
||||
},
|
||||
onCanonicalAliasChange = (event) => {
|
||||
this.changeCanonicalAlias(event.target.value);
|
||||
};
|
||||
|
||||
render: function() {
|
||||
const self = this;
|
||||
const EditableText = sdk.getComponent("elements.EditableText");
|
||||
render() {
|
||||
const EditableItemList = sdk.getComponent("elements.EditableItemList");
|
||||
const localDomain = MatrixClientPeg.get().getDomain();
|
||||
|
||||
let canonical_alias_section;
|
||||
if (this.props.canSetCanonicalAlias) {
|
||||
let found = false;
|
||||
const canonicalValue = this.state.canonicalAlias || "";
|
||||
canonical_alias_section = (
|
||||
<Field onChange={this.onCanonicalAliasChange} value={canonicalValue}
|
||||
element='select' id='canonicalAlias' label={_t('Main address')}>
|
||||
<option value="" key="unset">{ _t('not specified') }</option>
|
||||
{
|
||||
Object.keys(self.state.domainToAliases).map((domain, i) => {
|
||||
return self.state.domainToAliases[domain].map((alias, j) => {
|
||||
if (alias === this.state.canonicalAlias) found = true;
|
||||
return (
|
||||
<option value={alias} key={i + "_" + j}>
|
||||
{ alias }
|
||||
</option>
|
||||
);
|
||||
});
|
||||
})
|
||||
}
|
||||
{
|
||||
found || !this.stateCanonicalAlias ? '' :
|
||||
<option value={ this.state.canonicalAlias } key='arbitrary'>
|
||||
{ this.state.canonicalAlias }
|
||||
</option>
|
||||
}
|
||||
</Field>
|
||||
);
|
||||
} else {
|
||||
canonical_alias_section = (
|
||||
<b>{ this.state.canonicalAlias || _t('not set') }</b>
|
||||
);
|
||||
}
|
||||
let found = false;
|
||||
const canonicalValue = this.state.canonicalAlias || "";
|
||||
const canonicalAliasSection = (
|
||||
<Field onChange={this.onCanonicalAliasChange} value={canonicalValue}
|
||||
disabled={this.state.updatingCanonicalAlias || !this.props.canSetCanonicalAlias}
|
||||
element='select' id='canonicalAlias' label={_t('Main address')}>
|
||||
<option value="" key="unset">{ _t('not specified') }</option>
|
||||
{
|
||||
Object.keys(this.state.domainToAliases).map((domain, i) => {
|
||||
return this.state.domainToAliases[domain].map((alias, j) => {
|
||||
if (alias === this.state.canonicalAlias) found = true;
|
||||
return (
|
||||
<option value={alias} key={i + "_" + j}>
|
||||
{ alias }
|
||||
</option>
|
||||
);
|
||||
});
|
||||
})
|
||||
}
|
||||
{
|
||||
found || !this.state.canonicalAlias ? '' :
|
||||
<option value={ this.state.canonicalAlias } key='arbitrary'>
|
||||
{ this.state.canonicalAlias }
|
||||
</option>
|
||||
}
|
||||
</Field>
|
||||
);
|
||||
|
||||
let remote_aliases_section;
|
||||
let remoteAliasesSection;
|
||||
if (this.state.remoteDomains.length) {
|
||||
remote_aliases_section = (
|
||||
remoteAliasesSection = (
|
||||
<div>
|
||||
<div>
|
||||
{ _t("Remote addresses for this room:") }
|
||||
</div>
|
||||
<div>
|
||||
<ul>
|
||||
{ this.state.remoteDomains.map((domain, i) => {
|
||||
return this.state.domainToAliases[domain].map(function(alias, j) {
|
||||
return (
|
||||
<div key={i + "_" + j}>
|
||||
<EditableText
|
||||
className="mx_AliasSettings_alias mx_AliasSettings_editable"
|
||||
blurToCancel={false}
|
||||
editable={false}
|
||||
initialValue={alias} />
|
||||
</div>
|
||||
);
|
||||
return this.state.domainToAliases[domain].map((alias, j) => {
|
||||
return <li key={i + "_" + j}>{alias}</li>;
|
||||
});
|
||||
}) }
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='mx_AliasSettings'>
|
||||
{canonical_alias_section}
|
||||
{canonicalAliasSection}
|
||||
<EditableItemList
|
||||
className={"mx_RoomSettings_localAliases"}
|
||||
items={this.state.domainToAliases[localDomain] || []}
|
||||
newItem={this.state.newAlias}
|
||||
onNewItemChanged={this.onNewAliasChanged}
|
||||
canRemove={this.props.canSetAliases}
|
||||
canEdit={this.props.canSetAliases}
|
||||
onItemAdded={this.onLocalAliasAdded}
|
||||
onItemEdited={this.onLocalAliasChanged}
|
||||
onItemRemoved={this.onLocalAliasDeleted}
|
||||
itemsLabel={_t('Local addresses for this room:')}
|
||||
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},
|
||||
)}
|
||||
/>
|
||||
|
||||
{ remote_aliases_section }
|
||||
|
||||
{remoteAliasesSection}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
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 { _t } from '../../../languageHandler';
|
||||
import Modal from '../../../Modal';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||
|
||||
const GROUP_ID_REGEX = /\+\S+:\S+/;
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'RelatedGroupSettings',
|
||||
|
||||
propTypes: {
|
||||
export default class RelatedGroupSettings extends React.Component {
|
||||
static propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
canSetRelatedGroups: PropTypes.bool.isRequired,
|
||||
relatedGroupsEvent: PropTypes.instanceOf(MatrixEvent),
|
||||
},
|
||||
};
|
||||
|
||||
contextTypes: {
|
||||
static contextTypes = {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
},
|
||||
};
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
canSetRelatedGroups: false,
|
||||
static defaultProps = {
|
||||
canSetRelatedGroups: false,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
newGroupId: "",
|
||||
newGroupsList: props.relatedGroupsEvent ? (props.relatedGroupsEvent.getContent().groups || []) : [],
|
||||
};
|
||||
},
|
||||
}
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
newGroupsList: this.getInitialGroupList(),
|
||||
newGroupId: null,
|
||||
};
|
||||
},
|
||||
updateGroups(newGroupsList) {
|
||||
this.context.matrixClient.sendStateEvent(this.props.roomId, 'm.room.related_groups', {
|
||||
groups: newGroupsList,
|
||||
}, '').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() {
|
||||
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) {
|
||||
validateGroupId(groupId) {
|
||||
if (!GROUP_ID_REGEX.test(groupId)) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Invalid related community ID', '', ErrorDialog, {
|
||||
|
@ -84,38 +73,32 @@ module.exports = React.createClass({
|
|||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
}
|
||||
|
||||
onNewGroupChanged: function(newGroupId) {
|
||||
onNewGroupChanged = (newGroupId) => {
|
||||
this.setState({ newGroupId });
|
||||
},
|
||||
};
|
||||
|
||||
onGroupAdded: function(groupId) {
|
||||
onGroupAdded = (groupId) => {
|
||||
if (groupId.length === 0 || !this.validateGroupId(groupId)) {
|
||||
return;
|
||||
}
|
||||
const newGroupsList = [...this.state.newGroupsList, groupId];
|
||||
this.setState({
|
||||
newGroupsList: this.state.newGroupsList.concat([groupId]),
|
||||
newGroupsList: newGroupsList,
|
||||
newGroupId: '',
|
||||
});
|
||||
},
|
||||
this.updateGroups(newGroupsList);
|
||||
};
|
||||
|
||||
onGroupEdited: function(groupId, index) {
|
||||
if (groupId.length === 0 || !this.validateGroupId(groupId)) {
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
newGroupsList: Object.assign(this.state.newGroupsList, {[index]: groupId}),
|
||||
});
|
||||
},
|
||||
|
||||
onGroupDeleted: function(index) {
|
||||
const newGroupsList = this.state.newGroupsList.slice();
|
||||
newGroupsList.splice(index, 1);
|
||||
onGroupDeleted = (index) => {
|
||||
const group = this.state.newGroupsList[index];
|
||||
const newGroupsList = this.state.newGroupsList.filter((g) => g !== group);
|
||||
this.setState({ newGroupsList });
|
||||
},
|
||||
this.updateGroups(newGroupsList);
|
||||
};
|
||||
|
||||
render: function() {
|
||||
render() {
|
||||
const localDomain = this.context.matrixClient.getDomain();
|
||||
const EditableItemList = sdk.getComponent('elements.EditableItemList');
|
||||
return <div>
|
||||
|
@ -123,10 +106,10 @@ module.exports = React.createClass({
|
|||
items={this.state.newGroupsList}
|
||||
className={"mx_RelatedGroupSettings"}
|
||||
newItem={this.state.newGroupId}
|
||||
canRemove={this.props.canSetRelatedGroups}
|
||||
canEdit={this.props.canSetRelatedGroups}
|
||||
onNewItemChanged={this.onNewGroupChanged}
|
||||
onItemAdded={this.onGroupAdded}
|
||||
onItemEdited={this.onGroupEdited}
|
||||
onItemRemoved={this.onGroupDeleted}
|
||||
itemsLabel={_t('Showing flair for these communities:')}
|
||||
noItemsLabel={_t('This room is not showing flair for any communities')}
|
||||
|
@ -135,5 +118,5 @@ module.exports = React.createClass({
|
|||
)}
|
||||
/>
|
||||
</div>;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -65,6 +65,7 @@ const stateEventTileTypes = {
|
|||
'm.room.tombstone': 'messages.TextualEvent',
|
||||
'm.room.join_rules': 'messages.TextualEvent',
|
||||
'm.room.guest_access': 'messages.TextualEvent',
|
||||
'm.room.related_groups': 'messages.TextualEvent',
|
||||
};
|
||||
|
||||
function getHandlerTile(ev) {
|
||||
|
|
|
@ -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 = () => {
|
||||
dis.dispatch({
|
||||
action: 'leave_room',
|
||||
|
@ -113,12 +101,9 @@ export default class GeneralRoomSettingsTab extends React.Component {
|
|||
|
||||
<span className='mx_SettingsTab_subheading'>{_t("Room Addresses")}</span>
|
||||
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
|
||||
<AliasSettings ref="aliasSettings" roomId={this.props.roomId}
|
||||
<AliasSettings roomId={this.props.roomId}
|
||||
canSetCanonicalAlias={canSetCanonical} canSetAliases={canSetAliases}
|
||||
canonicalAliasEvent={canonicalAliasEv} aliasEvents={aliasEvents} />
|
||||
<AccessibleButton onClick={this._saveAliases} kind='primary'>
|
||||
{_t("Save")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
<div className='mx_SettingsTab_section'>
|
||||
<LabelledToggleSwitch value={this.state.isRoomPublished}
|
||||
|
@ -131,12 +116,9 @@ export default class GeneralRoomSettingsTab extends React.Component {
|
|||
|
||||
<span className='mx_SettingsTab_subheading'>{_t("Flair")}</span>
|
||||
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
|
||||
<RelatedGroupSettings ref="flairSettings" roomId={room.roomId}
|
||||
<RelatedGroupSettings roomId={room.roomId}
|
||||
canSetRelatedGroups={canChangeGroups}
|
||||
relatedGroupsEvent={groupsEvent} />
|
||||
<AccessibleButton onClick={this._saveGroups} kind='primary'>
|
||||
{_t("Save")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
|
||||
<span className='mx_SettingsTab_subheading'>{_t("URL Previews")}</span>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue