diff --git a/.eslintignore.errorfiles b/.eslintignore.errorfiles index 4830f8235a..d5eeebf1f2 100644 --- a/.eslintignore.errorfiles +++ b/.eslintignore.errorfiles @@ -31,7 +31,6 @@ src/components/views/globals/UpdateCheckBar.js src/components/views/messages/MFileBody.js src/components/views/messages/RoomAvatarEvent.js src/components/views/messages/TextualBody.js -src/components/views/room_settings/AliasSettings.js src/components/views/room_settings/ColorSettings.js src/components/views/rooms/Autocomplete.js src/components/views/rooms/AuxPanel.js diff --git a/res/css/views/elements/_EditableItemList.scss b/res/css/views/elements/_EditableItemList.scss index 9fbb39aa17..be96d811d3 100644 --- a/res/css/views/elements/_EditableItemList.scss +++ b/res/css/views/elements/_EditableItemList.scss @@ -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,47 +16,38 @@ limitations under the License. .mx_EditableItemList { margin-top: 12px; - margin-bottom: 0px; + margin-bottom: 10px; } .mx_EditableItem { - display: flex; - margin-left: 56px; + margin-bottom: 5px; + margin-left: 15px; } -.mx_EditableItem .mx_EditableItem_editable { - border: 0px; - 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; +.mx_EditableItem_delete { + margin-right: 5px; cursor: pointer; - - visibility: hidden; + vertical-align: middle; } -.mx_EditableItem:hover .mx_EditableItem_addButton, -.mx_EditableItem:hover .mx_EditableItem_removeButton { - visibility: visible; +.mx_EditableItem_email { + vertical-align: middle; +} + +.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 { - margin-bottom: 8px; -} + margin-bottom: 5px; +} \ No newline at end of file diff --git a/src/TextForEvent.js b/src/TextForEvent.js index ac0af82ff1..030c346ccc 100644 --- a/src/TextForEvent.js +++ b/src/TextForEvent.js @@ -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) { const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); const prevContent = ev.getPrevContent(); @@ -473,6 +503,7 @@ const stateHandlers = { 'm.room.tombstone': textForTombstoneEvent, 'm.room.join_rules': textForJoinRulesEvent, 'm.room.guest_access': textForGuestAccessEvent, + 'm.room.related_groups': textForRelatedGroupsEvent, 'im.vector.modular.widgets': textForWidgetEvent, }; diff --git a/src/components/views/elements/EditableItemList.js b/src/components/views/elements/EditableItemList.js index 7d96b1fd20..28c6e10612 100644 --- a/src/components/views/elements/EditableItemList.js +++ b/src/components/views/elements/EditableItemList.js @@ -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
- - { this.props.onAdd ? -
- {_t("Add")} + render() { + if (this.state.verifyRemove) { + return ( +
+ + {_t("Are you sure?")} + + + {_t("Yes")} + + + {_t("No")} +
- : -
- {_t("Delete")} -
- } -
; - }, -}); + ); + } -// TODO: Make this use the new Field element -module.exports = React.createClass({ - displayName: 'EditableItemList', + return ( +
+ {_t("Remove")} + {this.props.value} +
+ ); + } +} - 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 ( +
+ + + {_t("Add")} + + + ); + } - render: function() { + render() { const editableItems = this.props.items.map((item, index) => { + if (!this.props.canRemove) { + return
  • {item}
  • ; + } + return ; }); - const label = this.props.items.length > 0 ? - this.props.itemsLabel : this.props.noItemsLabel; + const editableItemsSection = this.props.canRemove ? editableItems :
      {editableItems}
    ; + const label = this.props.items.length > 0 ? this.props.itemsLabel : this.props.noItemsLabel; return (
    { label }
    - { 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. - :
    - } + { editableItemsSection } + { this.props.canEdit ? this._renderNewItemField() :
    }
    ); - }, -}); + } +} diff --git a/src/components/views/room_settings/AliasSettings.js b/src/components/views/room_settings/AliasSettings.js index 827656e770..df427171f1 100644 --- a/src/components/views/room_settings/AliasSettings.js +++ b/src/components/views/room_settings/AliasSettings.js @@ -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 = ( - - - { - Object.keys(self.state.domainToAliases).map((domain, i) => { - return self.state.domainToAliases[domain].map((alias, j) => { - if (alias === this.state.canonicalAlias) found = true; - return ( - - ); - }); - }) - } - { - found || !this.stateCanonicalAlias ? '' : - - } - - ); - } else { - canonical_alias_section = ( - { this.state.canonicalAlias || _t('not set') } - ); - } + let found = false; + const canonicalValue = this.state.canonicalAlias || ""; + const canonicalAliasSection = ( + + + { + Object.keys(this.state.domainToAliases).map((domain, i) => { + return this.state.domainToAliases[domain].map((alias, j) => { + if (alias === this.state.canonicalAlias) found = true; + return ( + + ); + }); + }) + } + { + found || !this.state.canonicalAlias ? '' : + + } + + ); - let remote_aliases_section; + let remoteAliasesSection; if (this.state.remoteDomains.length) { - remote_aliases_section = ( + remoteAliasesSection = (
    { _t("Remote addresses for this room:") }
    -
    +
      { this.state.remoteDomains.map((domain, i) => { - return this.state.domainToAliases[domain].map(function(alias, j) { - return ( -
      - -
      - ); + return this.state.domainToAliases[domain].map((alias, j) => { + return
    • {alias}
    • ; }); }) } -
    +
    ); } return (
    - {canonical_alias_section} + {canonicalAliasSection} - - { remote_aliases_section } - + {remoteAliasesSection}
    ); - }, -}); + } +} diff --git a/src/components/views/room_settings/RelatedGroupSettings.js b/src/components/views/room_settings/RelatedGroupSettings.js index 91a538ca93..b42ae1a8d7 100644 --- a/src/components/views/room_settings/RelatedGroupSettings.js +++ b/src/components/views/room_settings/RelatedGroupSettings.js @@ -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
    @@ -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({ )} />
    ; - }, -}); + } +} diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 8e1fb5af9f..15b8357b24 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -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) { diff --git a/src/components/views/settings/tabs/GeneralRoomSettingsTab.js b/src/components/views/settings/tabs/GeneralRoomSettingsTab.js index 75373a69bc..f43fc8a682 100644 --- a/src/components/views/settings/tabs/GeneralRoomSettingsTab.js +++ b/src/components/views/settings/tabs/GeneralRoomSettingsTab.js @@ -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 { {_t("Room Addresses")}
    - - - {_t("Save")} -
    {_t("Flair")}
    - - - {_t("Save")} -
    {_t("URL Previews")} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 63369ce991..84c9dacd07 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -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 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 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.", "%(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.", @@ -804,17 +807,22 @@ "Hide Stickers": "Hide Stickers", "Show Stickers": "Show Stickers", "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", "'%(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", - "'%(alias)s' is not a valid format for an address": "'%(alias)s' is not a valid format for an address", + "Error removing alias": "Error removing alias", + "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", "not specified": "not specified", - "not set": "not set", "Remote addresses for this room:": "Remote 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", "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", "'%(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:", @@ -925,7 +933,6 @@ "Verify...": "Verify...", "Join": "Join", "No results": "No results", - "Delete": "Delete", "Communities": "Communities", "Home": "Home", "You cannot delete this image. (%(code)s)": "You cannot delete this image. (%(code)s)",