From b2b95257a8571edde2cf1a7ed110eb1a65652252 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 7 Jun 2021 08:54:41 +0100 Subject: [PATCH 01/17] Convert RoomAliasField to Typescript --- src/components/views/elements/Field.tsx | 7 +- .../{RoomAliasField.js => RoomAliasField.tsx} | 75 ++++++++++--------- src/i18n/strings/en_EN.json | 2 +- 3 files changed, 48 insertions(+), 36 deletions(-) rename src/components/views/elements/{RoomAliasField.js => RoomAliasField.tsx} (67%) diff --git a/src/components/views/elements/Field.tsx b/src/components/views/elements/Field.tsx index 59d9a11596..1373c2df0e 100644 --- a/src/components/views/elements/Field.tsx +++ b/src/components/views/elements/Field.tsx @@ -29,6 +29,11 @@ function getId() { return `${BASE_ID}_${count++}`; } +export interface IValidateOpts { + focused?: boolean; + allowEmpty?: boolean; +} + interface IProps { // The field's ID, which binds the input and label together. Immutable. id?: string; @@ -180,7 +185,7 @@ export default class Field extends React.PureComponent { } }; - public async validate({ focused, allowEmpty = true }: {focused?: boolean, allowEmpty?: boolean}) { + public async validate({ focused, allowEmpty = true }: IValidateOpts) { if (!this.props.onValidate) { return; } diff --git a/src/components/views/elements/RoomAliasField.js b/src/components/views/elements/RoomAliasField.tsx similarity index 67% rename from src/components/views/elements/RoomAliasField.js rename to src/components/views/elements/RoomAliasField.tsx index 813dd8b5cc..7eff529c46 100644 --- a/src/components/views/elements/RoomAliasField.js +++ b/src/components/views/elements/RoomAliasField.tsx @@ -1,5 +1,5 @@ /* -Copyright 2019 New Vector Ltd +Copyright 2019, 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -13,67 +13,74 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ + +import React, { createRef } from "react"; + import { _t } from '../../../languageHandler'; -import React from 'react'; -import PropTypes from 'prop-types'; -import * as sdk from '../../../index'; import withValidation from './Validation'; -import {MatrixClientPeg} from '../../../MatrixClientPeg'; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { MatrixClientPeg } from '../../../MatrixClientPeg'; +import { replaceableComponent } from "../../../utils/replaceableComponent"; +import Field, { IValidateOpts } from "./Field"; + +interface IProps { + domain: string; + value: string; + label?: string; + placeholder?: string; + onChange?(value: string): void; +} + +interface IState { + isValid: boolean; +} // Controlled form component wrapping Field for inputting a room alias scoped to a given domain @replaceableComponent("views.elements.RoomAliasField") -export default class RoomAliasField extends React.PureComponent { - static propTypes = { - domain: PropTypes.string.isRequired, - onChange: PropTypes.func, - value: PropTypes.string.isRequired, +export default class RoomAliasField extends React.PureComponent { + private fieldRef = createRef(); + + public state = { + isValid: true, }; - constructor(props) { - super(props); - this.state = {isValid: true}; - } - - _asFullAlias(localpart) { + private asFullAlias(localpart: string): string { return `#${localpart}:${this.props.domain}`; } render() { - const Field = sdk.getComponent('views.elements.Field'); const poundSign = (#); const aliasPostfix = ":" + this.props.domain; const domain = ({aliasPostfix}); const maxlength = 255 - this.props.domain.length - 2; // 2 for # and : return ( this._fieldRef = ref} - onValidate={this._onValidate} - placeholder={_t("e.g. my-room")} - onChange={this._onChange} + ref={this.fieldRef} + onValidate={this.onValidate} + placeholder={this.props.placeholder || _t("e.g. my-room")} + onChange={this.onChange} value={this.props.value.substring(1, this.props.value.length - this.props.domain.length - 1)} maxLength={maxlength} /> ); } - _onChange = (ev) => { + private onChange = (ev) => { if (this.props.onChange) { - this.props.onChange(this._asFullAlias(ev.target.value)); + this.props.onChange(this.asFullAlias(ev.target.value)); } }; - _onValidate = async (fieldState) => { - const result = await this._validationRules(fieldState); + private onValidate = async (fieldState) => { + const result = await this.validationRules(fieldState); this.setState({isValid: result.valid}); return result; }; - _validationRules = withValidation({ + private validationRules = withValidation({ rules: [ { key: "safeLocalpart", @@ -81,7 +88,7 @@ export default class RoomAliasField extends React.PureComponent { if (!value) { return true; } - const fullAlias = this._asFullAlias(value); + const fullAlias = this.asFullAlias(value); // XXX: FIXME https://github.com/matrix-org/matrix-doc/issues/668 return !value.includes("#") && !value.includes(":") && !value.includes(",") && encodeURI(fullAlias) === fullAlias; @@ -90,7 +97,7 @@ export default class RoomAliasField extends React.PureComponent { }, { key: "required", test: async ({ value, allowEmpty }) => allowEmpty || !!value, - invalid: () => _t("Please provide a room address"), + invalid: () => _t("Please provide an address"), }, { key: "taken", final: true, @@ -100,7 +107,7 @@ export default class RoomAliasField extends React.PureComponent { } const client = MatrixClientPeg.get(); try { - await client.getRoomIdForAlias(this._asFullAlias(value)); + await client.getRoomIdForAlias(this.asFullAlias(value)); // we got a room id, so the alias is taken return false; } catch (err) { @@ -120,11 +127,11 @@ export default class RoomAliasField extends React.PureComponent { return this.state.isValid; } - validate(options) { - return this._fieldRef.validate(options); + validate(options: IValidateOpts) { + return this.fieldRef.current?.validate(options); } focus() { - this._fieldRef.focus(); + this.fieldRef.current?.focus(); } } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 9e85ea28c8..02662aa508 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2014,7 +2014,7 @@ "Room address": "Room address", "e.g. my-room": "e.g. my-room", "Some characters not allowed": "Some characters not allowed", - "Please provide a room address": "Please provide a room address", + "Please provide an address": "Please provide an address", "This address is available to use": "This address is available to use", "This address is already in use": "This address is already in use", "Server Options": "Server Options", From 8c34a8461ee64949b670715cafac5948bedbca57 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 7 Jun 2021 08:57:39 +0100 Subject: [PATCH 02/17] Add way to specify address during public space creation --- res/css/views/spaces/_SpaceBasicSettings.scss | 2 +- .../views/spaces/SpaceCreateMenu.tsx | 45 +++++++++++++++---- src/i18n/strings/en_EN.json | 2 + 3 files changed, 40 insertions(+), 9 deletions(-) diff --git a/res/css/views/spaces/_SpaceBasicSettings.scss b/res/css/views/spaces/_SpaceBasicSettings.scss index 204ccab2b7..e6e06e7181 100644 --- a/res/css/views/spaces/_SpaceBasicSettings.scss +++ b/res/css/views/spaces/_SpaceBasicSettings.scss @@ -16,7 +16,7 @@ limitations under the License. .mx_SpaceBasicSettings { .mx_Field { - margin: 32px 0; + margin: 24px 0; } .mx_SpaceBasicSettings_avatarContainer { diff --git a/src/components/views/spaces/SpaceCreateMenu.tsx b/src/components/views/spaces/SpaceCreateMenu.tsx index 0ebf511018..a65b53a045 100644 --- a/src/components/views/spaces/SpaceCreateMenu.tsx +++ b/src/components/views/spaces/SpaceCreateMenu.tsx @@ -33,6 +33,7 @@ import {USER_LABS_TAB} from "../dialogs/UserSettingsDialog"; import Field from "../elements/Field"; import withValidation from "../elements/Validation"; import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView"; +import RoomAliasField from "../elements/RoomAliasField"; const SpaceCreateMenuType = ({ title, description, className, onClick }) => { return ( @@ -58,6 +59,11 @@ const spaceNameValidator = withValidation({ ], }); +const nameToAlias = (name: string, domain: string): string => { + const localpart = name.trim().toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9_-]+/gi, ""); + return `#${localpart}:${domain}`; +}; + const SpaceCreateMenu = ({ onFinished }) => { const cli = useContext(MatrixClientContext); const [visibility, setVisibility] = useState(null); @@ -65,6 +71,8 @@ const SpaceCreateMenu = ({ onFinished }) => { const [name, setName] = useState(""); const spaceNameField = useRef(); + const [alias, setAlias] = useState(""); + const spaceAliasField = useRef(); const [avatar, setAvatar] = useState(null); const [topic, setTopic] = useState(""); @@ -80,6 +88,13 @@ const SpaceCreateMenu = ({ onFinished }) => { setBusy(false); return; } + // validate the space name alias field but do not require it + if (visibility === Visibility.Public && !await spaceAliasField.current.validate({ allowEmpty: true })) { + spaceAliasField.current.focus(); + spaceAliasField.current.validate({ allowEmpty: true, focused: true }); + setBusy(false); + return; + } const initialState: IStateEvent[] = [ { @@ -97,12 +112,6 @@ const SpaceCreateMenu = ({ onFinished }) => { content: { url }, }); } - if (topic) { - initialState.push({ - type: EventType.RoomTopic, - content: { topic }, - }); - } try { await createRoom({ @@ -110,7 +119,6 @@ const SpaceCreateMenu = ({ onFinished }) => { preset: visibility === Visibility.Public ? Preset.PublicChat : Preset.PrivateChat, name, creation_content: { - // Based on MSC1840 [RoomCreateTypeField]: RoomType.Space, }, initial_state: initialState, @@ -119,6 +127,8 @@ const SpaceCreateMenu = ({ onFinished }) => { events_default: 100, ...Visibility.Public ? { invite: 0 } : {}, }, + room_alias_name: alias ? alias.substr(1, alias.indexOf(":") - 1) : undefined, + topic, }, spinner: false, encryption: false, @@ -157,6 +167,7 @@ const SpaceCreateMenu = ({ onFinished }) => { ; } else { + const domain = cli.getDomain(); body = { label={_t("Name")} autoFocus={true} value={name} - onChange={ev => setName(ev.target.value)} + onChange={ev => { + const newName = ev.target.value; + if (!alias || alias === nameToAlias(name, domain)) { + setAlias(nameToAlias(newName, domain)); + } + setName(newName); + }} ref={spaceNameField} onValidate={spaceNameValidator} disabled={busy} /> + { visibility === Visibility.Public + ? + : null + } + Date: Mon, 7 Jun 2021 15:48:55 +0100 Subject: [PATCH 03/17] Convert EditableItemList & AliasSettings to Typescript --- ...itableItemList.js => EditableItemList.tsx} | 133 +++++++++-------- .../{AliasSettings.js => AliasSettings.tsx} | 134 ++++++++++-------- 2 files changed, 146 insertions(+), 121 deletions(-) rename src/components/views/elements/{EditableItemList.js => EditableItemList.tsx} (54%) rename src/components/views/room_settings/{AliasSettings.js => AliasSettings.tsx} (78%) diff --git a/src/components/views/elements/EditableItemList.js b/src/components/views/elements/EditableItemList.tsx similarity index 54% rename from src/components/views/elements/EditableItemList.js rename to src/components/views/elements/EditableItemList.tsx index d8ec5af278..89e2e1b8a0 100644 --- a/src/components/views/elements/EditableItemList.js +++ b/src/components/views/elements/EditableItemList.tsx @@ -1,5 +1,5 @@ /* -Copyright 2017, 2019 New Vector Ltd. +Copyright 2017-2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,48 +14,48 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; -import PropTypes from 'prop-types'; -import {_t} from '../../../languageHandler'; +import React from "react"; + +import { _t } from '../../../languageHandler'; import Field from "./Field"; import AccessibleButton from "./AccessibleButton"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; -export class EditableItem extends React.Component { - static propTypes = { - index: PropTypes.number, - value: PropTypes.string, - onRemove: PropTypes.func, +interface IItemProps { + index?: number; + value?: string; + onRemove?(index: number): void; +} + +interface IItemState { + verifyRemove: boolean; +} + +export class EditableItem extends React.Component { + public state = { + verifyRemove: false, }; - constructor() { - super(); - - this.state = { - verifyRemove: false, - }; - } - - _onRemove = (e) => { + private onRemove = (e) => { e.stopPropagation(); e.preventDefault(); - this.setState({verifyRemove: true}); + this.setState({ verifyRemove: true }); }; - _onDontRemove = (e) => { + private onDontRemove = (e) => { e.stopPropagation(); e.preventDefault(); - this.setState({verifyRemove: false}); + this.setState({ verifyRemove: false }); }; - _onActuallyRemove = (e) => { + private onActuallyRemove = (e) => { e.stopPropagation(); e.preventDefault(); if (this.props.onRemove) this.props.onRemove(this.props.index); - this.setState({verifyRemove: false}); + this.setState({ verifyRemove: false }); }; render() { @@ -66,14 +66,14 @@ export class EditableItem extends React.Component { {_t("Are you sure?")} {_t("Yes")} @@ -85,59 +85,68 @@ export class EditableItem extends React.Component { return (
-
+
{this.props.value}
); } } +interface IProps { + id: string; + items: string[]; + itemsLabel?: string; + noItemsLabel?: string; + placeholder?: string; + newItem?: string; + canEdit?: boolean; + canRemove?: boolean; + suggestionsListId?: string; + onItemAdded?(item: string): void; + onItemRemoved?(index: number): void; + onNewItemChanged?(item: string): void; +} + @replaceableComponent("views.elements.EditableItemList") -export default class EditableItemList extends React.Component { - static propTypes = { - id: PropTypes.string.isRequired, - items: PropTypes.arrayOf(PropTypes.string).isRequired, - itemsLabel: PropTypes.string, - noItemsLabel: PropTypes.string, - placeholder: PropTypes.string, - newItem: PropTypes.string, - - onItemAdded: PropTypes.func, - onItemRemoved: PropTypes.func, - onNewItemChanged: PropTypes.func, - - canEdit: PropTypes.bool, - canRemove: PropTypes.bool, - }; - - _onItemAdded = (e) => { +export default class EditableItemList

extends React.PureComponent { + protected onItemAdded = (e) => { e.stopPropagation(); e.preventDefault(); if (this.props.onItemAdded) this.props.onItemAdded(this.props.newItem); }; - _onItemRemoved = (index) => { + protected onItemRemoved = (index) => { if (this.props.onItemRemoved) this.props.onItemRemoved(index); }; - _onNewItemChanged = (e) => { + protected onNewItemChanged = (e) => { if (this.props.onNewItemChanged) this.props.onNewItemChanged(e.target.value); }; - _renderNewItemField() { + protected renderNewItemField() { return (

- - - {_t("Add")} + + + { _t("Add") } ); @@ -153,19 +162,21 @@ export default class EditableItemList extends React.Component { key={item} index={index} value={item} - onRemove={this._onItemRemoved} + onRemove={this.onItemRemoved} />; }); const editableItemsSection = this.props.canRemove ? editableItems :
    {editableItems}
; const label = this.props.items.length > 0 ? this.props.itemsLabel : this.props.noItemsLabel; - return (
-
- { label } + return ( +
+
+ { label } +
+ { editableItemsSection } + { this.props.canEdit ? this.renderNewItemField() :
}
- { editableItemsSection } - { this.props.canEdit ? this._renderNewItemField() :
} -
); + ); } } diff --git a/src/components/views/room_settings/AliasSettings.js b/src/components/views/room_settings/AliasSettings.tsx similarity index 78% rename from src/components/views/room_settings/AliasSettings.js rename to src/components/views/room_settings/AliasSettings.tsx index 80e0099ab3..d6e79c4ee9 100644 --- a/src/components/views/room_settings/AliasSettings.js +++ b/src/components/views/room_settings/AliasSettings.tsx @@ -1,6 +1,5 @@ /* -Copyright 2016 OpenMarket Ltd -Copyright 2018, 2019 New Vector Ltd +Copyright 2016-2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,59 +14,60 @@ See the License for the specific language governing permissions and limitations under the License. */ +import React, { ChangeEvent, createRef } from "react"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; + import EditableItemList from "../elements/EditableItemList"; -import React, {createRef} from 'react'; -import PropTypes from 'prop-types'; -import {MatrixClientPeg} from "../../../MatrixClientPeg"; -import * as sdk from "../../../index"; +import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { _t } from '../../../languageHandler'; import Field from "../elements/Field"; +import Spinner from "../elements/Spinner"; import ErrorDialog from "../dialogs/ErrorDialog"; import AccessibleButton from "../elements/AccessibleButton"; import Modal from "../../../Modal"; import RoomPublishSetting from "./RoomPublishSetting"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; +import RoomAliasField from "../elements/RoomAliasField"; -class EditableAliasesList extends EditableItemList { - constructor(props) { - super(props); +interface IEditableAliasesListProps { + domain?: string; +} - this._aliasField = createRef(); - } +class EditableAliasesList extends EditableItemList { + private aliasField = createRef(); - _onAliasAdded = async () => { - await this._aliasField.current.validate({ allowEmpty: false }); + private onAliasAdded = async () => { + await this.aliasField.current.validate({ allowEmpty: false }); - if (this._aliasField.current.isValid) { + if (this.aliasField.current.isValid) { if (this.props.onItemAdded) this.props.onItemAdded(this.props.newItem); return; } - this._aliasField.current.focus(); - this._aliasField.current.validate({ allowEmpty: false, focused: true }); + this.aliasField.current.focus(); + this.aliasField.current.validate({ allowEmpty: false, focused: true }); }; - _renderNewItemField() { + protected renderNewItemField() { // if we don't need the RoomAliasField, - // we don't need to overriden version of _renderNewItemField + // we don't need to overriden version of renderNewItemField if (!this.props.domain) { - return super._renderNewItemField(); + return super.renderNewItemField(); } - const RoomAliasField = sdk.getComponent('views.elements.RoomAliasField'); - const onChange = (alias) => this._onNewItemChanged({target: {value: alias}}); + const onChange = (alias) => this.onNewItemChanged({target: {value: alias}}); return (
- + { _t("Add") } @@ -75,15 +75,27 @@ class EditableAliasesList extends EditableItemList { } } -@replaceableComponent("views.room_settings.AliasSettings") -export default class AliasSettings extends React.Component { - static propTypes = { - roomId: PropTypes.string.isRequired, - canSetCanonicalAlias: PropTypes.bool.isRequired, - canSetAliases: PropTypes.bool.isRequired, - canonicalAliasEvent: PropTypes.object, // MatrixEvent - }; +interface IProps { + roomId: string; + canSetCanonicalAlias: boolean; + canSetAliases: boolean; + canonicalAliasEvent?: MatrixEvent; + aliasEvents?: MatrixEvent[]; +} +interface IState { + altAliases: string[]; + localAliases: string[]; + canonicalAlias?: string; + updatingCanonicalAlias: boolean; + localAliasesLoading: boolean; + detailsOpen: boolean; + newAlias?: string; + newAltAlias?: string; +} + +@replaceableComponent("views.room_settings.AliasSettings") +export default class AliasSettings extends React.Component { static defaultProps = { canSetAliases: false, canSetCanonicalAlias: false, @@ -122,7 +134,7 @@ export default class AliasSettings extends React.Component { } } - async loadLocalAliases() { + private async loadLocalAliases() { this.setState({ localAliasesLoading: true }); try { const cli = MatrixClientPeg.get(); @@ -139,7 +151,7 @@ export default class AliasSettings extends React.Component { } } - changeCanonicalAlias(alias) { + private changeCanonicalAlias(alias: string) { if (!this.props.canSetCanonicalAlias) return; const oldAlias = this.state.canonicalAlias; @@ -170,7 +182,7 @@ export default class AliasSettings extends React.Component { }); } - changeAltAliases(altAliases) { + private changeAltAliases(altAliases: string[]) { if (!this.props.canSetCanonicalAlias) return; this.setState({ @@ -181,7 +193,7 @@ export default class AliasSettings extends React.Component { const eventContent = {}; if (this.state.canonicalAlias) { - eventContent.alias = this.state.canonicalAlias; + eventContent["alias"] = this.state.canonicalAlias; } if (altAliases) { eventContent["alt_aliases"] = altAliases; @@ -202,11 +214,11 @@ export default class AliasSettings extends React.Component { }); } - onNewAliasChanged = (value) => { - this.setState({newAlias: value}); + private onNewAliasChanged = (value: string) => { + this.setState({ newAlias: value }); }; - onLocalAliasAdded = (alias) => { + private onLocalAliasAdded = (alias: string) => { if (!alias || alias.length === 0) return; // ignore attempts to create blank aliases const localDomain = MatrixClientPeg.get().getDomain(); @@ -232,7 +244,7 @@ export default class AliasSettings extends React.Component { }); }; - onLocalAliasDeleted = (index) => { + private onLocalAliasDeleted = (index: number) => { const alias = this.state.localAliases[index]; // TODO: In future, we should probably be making sure that the alias actually belongs // to this room. See https://github.com/vector-im/element-web/issues/7353 @@ -261,7 +273,7 @@ export default class AliasSettings extends React.Component { }); }; - onLocalAliasesToggled = (event) => { + private onLocalAliasesToggled = (event: ChangeEvent) => { // expanded if (event.target.open) { // if local aliases haven't been preloaded yet at component mount @@ -269,37 +281,37 @@ export default class AliasSettings extends React.Component { this.loadLocalAliases(); } } - this.setState({detailsOpen: event.target.open}); + this.setState({ detailsOpen: event.currentTarget.open }); }; - onCanonicalAliasChange = (event) => { + private onCanonicalAliasChange = (event: ChangeEvent) => { this.changeCanonicalAlias(event.target.value); }; - onNewAltAliasChanged = (value) => { - this.setState({newAltAlias: value}); + private onNewAltAliasChanged = (value: string) => { + this.setState({ newAltAlias: value }); } - onAltAliasAdded = (alias) => { + private onAltAliasAdded = (alias: string) => { const altAliases = this.state.altAliases.slice(); if (!altAliases.some(a => a.trim() === alias.trim())) { altAliases.push(alias.trim()); this.changeAltAliases(altAliases); - this.setState({newAltAlias: ""}); + this.setState({ newAltAlias: "" }); } } - onAltAliasDeleted = (index) => { + private onAltAliasDeleted = (index: number) => { const altAliases = this.state.altAliases.slice(); altAliases.splice(index, 1); this.changeAltAliases(altAliases); } - _getAliases() { - return this.state.altAliases.concat(this._getLocalNonAltAliases()); + private getAliases() { + return this.state.altAliases.concat(this.getLocalNonAltAliases()); } - _getLocalNonAltAliases() { + private getLocalNonAltAliases() { const {altAliases} = this.state; return this.state.localAliases.filter(alias => !altAliases.includes(alias)); } @@ -320,7 +332,7 @@ export default class AliasSettings extends React.Component { > { - this._getAliases().map((alias, i) => { + this.getAliases().map((alias, i) => { if (alias === this.state.canonicalAlias) found = true; return (
); From 4725a9e8fa954b3858f4dd8e8d4212f6a0d61488 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 7 Jun 2021 16:42:59 +0100 Subject: [PATCH 04/17] Extract useRoomState hook into hooks directory --- .../views/right_panel/PinnedMessagesCard.tsx | 19 +-------- src/hooks/useRoomState.ts | 40 +++++++++++++++++++ 2 files changed, 41 insertions(+), 18 deletions(-) create mode 100644 src/hooks/useRoomState.ts diff --git a/src/components/views/right_panel/PinnedMessagesCard.tsx b/src/components/views/right_panel/PinnedMessagesCard.tsx index a3f1f2d9df..ad62619593 100644 --- a/src/components/views/right_panel/PinnedMessagesCard.tsx +++ b/src/components/views/right_panel/PinnedMessagesCard.tsx @@ -28,6 +28,7 @@ import { useEventEmitter } from "../../../hooks/useEventEmitter"; import PinningUtils from "../../../utils/PinningUtils"; import { useAsyncMemo } from "../../../hooks/useAsyncMemo"; import PinnedEventTile from "../rooms/PinnedEventTile"; +import { useRoomState } from "../../../hooks/useRoomState"; interface IProps { room: Room; @@ -75,24 +76,6 @@ export const useReadPinnedEvents = (room: Room): Set => { return readPinnedEvents; }; -const useRoomState = (room: Room, mapper: (state: RoomState) => T): T => { - const [value, setValue] = useState(room ? mapper(room.currentState) : undefined); - - const update = useCallback(() => { - if (!room) return; - setValue(mapper(room.currentState)); - }, [room, mapper]); - - useEventEmitter(room?.currentState, "RoomState.events", update); - useEffect(() => { - update(); - return () => { - setValue(undefined); - }; - }, [update]); - return value; -}; - const PinnedMessagesCard = ({ room, onClose }: IProps) => { const cli = useContext(MatrixClientContext); const canUnpin = useRoomState(room, state => state.mayClientSendStateEvent(EventType.RoomPinnedEvents, cli)); diff --git a/src/hooks/useRoomState.ts b/src/hooks/useRoomState.ts new file mode 100644 index 0000000000..ded51c3900 --- /dev/null +++ b/src/hooks/useRoomState.ts @@ -0,0 +1,40 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { useCallback, useEffect, useState } from "react"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { RoomState } from "matrix-js-sdk/src/models/room-state"; + +import { useEventEmitter } from "./useEventEmitter"; + +// Hook to simplify watching Matrix Room state +export const useRoomState = (room: Room, mapper: (state: RoomState) => T): T => { + const [value, setValue] = useState(room ? mapper(room.currentState) : undefined); + + const update = useCallback(() => { + if (!room) return; + setValue(mapper(room.currentState)); + }, [room, mapper]); + + useEventEmitter(room?.currentState, "RoomState.events", update); + useEffect(() => { + update(); + return () => { + setValue(undefined); + }; + }, [update]); + return value; +}; From 9454a4e6c79f0ecfa5ebd91bd7af689e478ca559 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 8 Jun 2021 16:28:02 +0100 Subject: [PATCH 05/17] Convert AdvancedRoomSettingsTab to Typescript --- ...ingsTab.js => AdvancedRoomSettingsTab.tsx} | 87 ++++++++++--------- 1 file changed, 47 insertions(+), 40 deletions(-) rename src/components/views/settings/tabs/room/{AdvancedRoomSettingsTab.js => AdvancedRoomSettingsTab.tsx} (73%) diff --git a/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.js b/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.tsx similarity index 73% rename from src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.js rename to src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.tsx index 28aad65129..f587210095 100644 --- a/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.js +++ b/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.tsx @@ -1,5 +1,5 @@ /* -Copyright 2019 New Vector Ltd +Copyright 2019, 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,68 +15,75 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; -import {_t} from "../../../../../languageHandler"; -import {MatrixClientPeg} from "../../../../../MatrixClientPeg"; -import * as sdk from "../../../../.."; + +import { _t } from "../../../../../languageHandler"; +import { MatrixClientPeg } from "../../../../../MatrixClientPeg"; import AccessibleButton from "../../../elements/AccessibleButton"; +import RoomUpgradeDialog from "../../../dialogs/RoomUpgradeDialog"; +import DevtoolsDialog from "../../../dialogs/DevtoolsDialog"; import Modal from "../../../../../Modal"; import dis from "../../../../../dispatcher/dispatcher"; -import {replaceableComponent} from "../../../../../utils/replaceableComponent"; +import { replaceableComponent } from "../../../../../utils/replaceableComponent"; + +interface IProps { + roomId: string; + closeSettingsFn(): void; +} + +interface IRecommendedVersion { + version: string; + needsUpgrade: boolean; + urgent: boolean; +} + +interface IState { + upgradeRecommendation?: IRecommendedVersion; + oldRoomId?: string; + oldEventId?: string; + upgraded?: boolean; +} @replaceableComponent("views.settings.tabs.room.AdvancedRoomSettingsTab") -export default class AdvancedRoomSettingsTab extends React.Component { - static propTypes = { - roomId: PropTypes.string.isRequired, - closeSettingsFn: PropTypes.func.isRequired, - }; - - constructor(props) { - super(props); +export default class AdvancedRoomSettingsTab extends React.Component { + constructor(props, context) { + super(props, context); this.state = { // This is eventually set to the value of room.getRecommendedVersion() upgradeRecommendation: null, }; - } - // TODO: [REACT-WARNING] Move this to constructor - UNSAFE_componentWillMount() { // eslint-disable-line camelcase // we handle lack of this object gracefully later, so don't worry about it failing here. const room = MatrixClientPeg.get().getRoom(this.props.roomId); room.getRecommendedVersion().then((v) => { const tombstone = room.currentState.getStateEvents("m.room.tombstone", ""); - const additionalStateChanges = {}; + const additionalStateChanges: Partial = {}; const createEvent = room.currentState.getStateEvents("m.room.create", ""); const predecessor = createEvent ? createEvent.getContent().predecessor : null; if (predecessor && predecessor.room_id) { - additionalStateChanges['oldRoomId'] = predecessor.room_id; - additionalStateChanges['oldEventId'] = predecessor.event_id; - additionalStateChanges['hasPreviousRoom'] = true; + additionalStateChanges.oldRoomId = predecessor.room_id; + additionalStateChanges.oldEventId = predecessor.event_id; } - this.setState({ - upgraded: tombstone && tombstone.getContent().replacement_room, + upgraded: !!tombstone?.getContent().replacement_room, upgradeRecommendation: v, ...additionalStateChanges, }); }); } - _upgradeRoom = (e) => { - const RoomUpgradeDialog = sdk.getComponent('dialogs.RoomUpgradeDialog'); + private upgradeRoom = (e) => { const room = MatrixClientPeg.get().getRoom(this.props.roomId); Modal.createTrackedDialog('Upgrade Room Version', '', RoomUpgradeDialog, {room: room}); }; - _openDevtools = (e) => { - const DevtoolsDialog = sdk.getComponent('dialogs.DevtoolsDialog'); + private openDevtools = (e) => { Modal.createDialog(DevtoolsDialog, {roomId: this.props.roomId}); }; - _onOldRoomClicked = (e) => { + private onOldRoomClicked = (e) => { e.preventDefault(); e.stopPropagation(); @@ -113,7 +120,7 @@ export default class AdvancedRoomSettingsTab extends React.Component { }, )}

- + {_t("Upgrade this room to the recommended room version")}
@@ -121,13 +128,13 @@ export default class AdvancedRoomSettingsTab extends React.Component { } let oldRoomLink; - if (this.state.hasPreviousRoom) { + if (this.state.oldRoomId) { let name = _t("this room"); const room = MatrixClientPeg.get().getRoom(this.props.roomId); if (room && room.name) name = room.name; oldRoomLink = ( - - {_t("View older messages in %(roomName)s.", {roomName: name})} + + { _t("View older messages in %(roomName)s.", { roomName: name }) } ); } @@ -139,23 +146,23 @@ export default class AdvancedRoomSettingsTab extends React.Component { {_t("Room information")}
{_t("Internal room ID:")}  - {this.props.roomId} + { this.props.roomId }
- {unfederatableSection} + { unfederatableSection }
{_t("Room version")}
{_t("Room version:")}  - {room.getVersion()} + { room.getVersion() }
- {oldRoomLink} - {roomUpgradeButton} + { oldRoomLink } + { roomUpgradeButton }
- {_t("Developer options")} - - {_t("Open Devtools")} + { _t("Developer options") } + + { _t("Open Devtools") }
From 8d4ac90265839a98aeb83277834ef8673fc2b97e Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 8 Jun 2021 16:28:37 +0100 Subject: [PATCH 06/17] Convert RoomPublishSetting and LabelledToggleSwitch to Typescript --- ...ggleSwitch.js => LabelledToggleSwitch.tsx} | 47 +++++++++---------- ...blishSetting.js => RoomPublishSetting.tsx} | 47 ++++++++++++------- 2 files changed, 51 insertions(+), 43 deletions(-) rename src/components/views/elements/{LabelledToggleSwitch.js => LabelledToggleSwitch.tsx} (63%) rename src/components/views/room_settings/{RoomPublishSetting.js => RoomPublishSetting.tsx} (62%) diff --git a/src/components/views/elements/LabelledToggleSwitch.js b/src/components/views/elements/LabelledToggleSwitch.tsx similarity index 63% rename from src/components/views/elements/LabelledToggleSwitch.js rename to src/components/views/elements/LabelledToggleSwitch.tsx index ef60eeed7b..957e3dbc97 100644 --- a/src/components/views/elements/LabelledToggleSwitch.js +++ b/src/components/views/elements/LabelledToggleSwitch.tsx @@ -1,5 +1,5 @@ /* -Copyright 2019 New Vector Ltd +Copyright 2019, 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,38 +14,33 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; -import PropTypes from "prop-types"; +import React from "react"; + import ToggleSwitch from "./ToggleSwitch"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +interface IProps { + // The value for the toggle switch + value: boolean; + // The translated label for the switch + label: string; + // Whether or not to disable the toggle switch + disabled?: boolean; + // True to put the toggle in front of the label + // Default false. + toggleInFront?: boolean; + // Additional class names to append to the switch. Optional. + className?: string; + // The function to call when the value changes + onChange(checked: boolean): void; +} + @replaceableComponent("views.elements.LabelledToggleSwitch") -export default class LabelledToggleSwitch extends React.Component { - static propTypes = { - // The value for the toggle switch - value: PropTypes.bool.isRequired, - - // The function to call when the value changes - onChange: PropTypes.func.isRequired, - - // The translated label for the switch - label: PropTypes.string.isRequired, - - // Whether or not to disable the toggle switch - disabled: PropTypes.bool, - - // True to put the toggle in front of the label - // Default false. - toggleInFront: PropTypes.bool, - - // Additional class names to append to the switch. Optional. - className: PropTypes.string, - }; - +export default class LabelledToggleSwitch extends React.PureComponent { render() { // This is a minimal version of a SettingsFlag - let firstPart = {this.props.label}; + let firstPart = { this.props.label }; let secondPart = { + public state = { + isRoomPublished: false, + }; - onRoomPublishChange = (e) => { + private onRoomPublishChange = (e) => { const valueBefore = this.state.isRoomPublished; const newValue = !valueBefore; this.setState({isRoomPublished: newValue}); @@ -52,11 +62,14 @@ export default class RoomPublishSetting extends React.PureComponent { render() { const client = MatrixClientPeg.get(); - return (); + return ( + + ); } } From 13a2f779b962e8498ae09d9675a91baeca3cd671 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 8 Jun 2021 16:29:06 +0100 Subject: [PATCH 07/17] improve defaults for useRoomState and useStateToggle hooks --- src/hooks/useRoomState.ts | 5 ++++- src/hooks/useStateToggle.ts | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/hooks/useRoomState.ts b/src/hooks/useRoomState.ts index ded51c3900..11ac7de49e 100644 --- a/src/hooks/useRoomState.ts +++ b/src/hooks/useRoomState.ts @@ -20,8 +20,11 @@ import { RoomState } from "matrix-js-sdk/src/models/room-state"; import { useEventEmitter } from "./useEventEmitter"; +type Mapper = (roomState: RoomState) => T; +const defaultMapper: Mapper = (roomState: RoomState) => roomState; + // Hook to simplify watching Matrix Room state -export const useRoomState = (room: Room, mapper: (state: RoomState) => T): T => { +export const useRoomState = (room: Room, mapper: Mapper = defaultMapper): T => { const [value, setValue] = useState(room ? mapper(room.currentState) : undefined); const update = useCallback(() => { diff --git a/src/hooks/useStateToggle.ts b/src/hooks/useStateToggle.ts index b50a923234..33701c4f16 100644 --- a/src/hooks/useStateToggle.ts +++ b/src/hooks/useStateToggle.ts @@ -18,7 +18,7 @@ import {Dispatch, SetStateAction, useState} from "react"; // Hook to simplify toggling of a boolean state value // Returns value, method to toggle boolean value and method to set the boolean value -export const useStateToggle = (initialValue: boolean): [boolean, () => void, Dispatch>] => { +export const useStateToggle = (initialValue = false): [boolean, () => void, Dispatch>] => { const [value, setValue] = useState(initialValue); const toggleValue = () => { setValue(!value); From 5c85ee1ea03097325680ed4574006c559fcccaa9 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 8 Jun 2021 16:30:46 +0100 Subject: [PATCH 08/17] Make AdvancedRoomSettingsTab space-aware --- .../views/settings/tabs/room/AdvancedRoomSettingsTab.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.tsx index f587210095..7e7d9cba90 100644 --- a/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.tsx @@ -76,7 +76,7 @@ export default class AdvancedRoomSettingsTab extends React.Component { const room = MatrixClientPeg.get().getRoom(this.props.roomId); - Modal.createTrackedDialog('Upgrade Room Version', '', RoomUpgradeDialog, {room: room}); + Modal.createTrackedDialog('Upgrade Room Version', '', RoomUpgradeDialog, { room }); }; private openDevtools = (e) => { @@ -143,7 +143,9 @@ export default class AdvancedRoomSettingsTab extends React.Component
{_t("Advanced")}
- {_t("Room information")} + + { room?.isSpaceRoom() ? _t("Space information") : _t("Room information") } +
{_t("Internal room ID:")}  { this.props.roomId } From 78debcc93b99eea96f270cbff8a5266a10a52db5 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 8 Jun 2021 16:31:39 +0100 Subject: [PATCH 09/17] Add method to disable StyledRadioGroup and wrap description in element with a className --- .../views/elements/StyledRadioGroup.tsx | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/components/views/elements/StyledRadioGroup.tsx b/src/components/views/elements/StyledRadioGroup.tsx index 6b9e992f92..744b6f2059 100644 --- a/src/components/views/elements/StyledRadioGroup.tsx +++ b/src/components/views/elements/StyledRadioGroup.tsx @@ -34,10 +34,19 @@ interface IProps { definitions: IDefinition[]; value?: T; // if not provided no options will be selected outlined?: boolean; + disabled?: boolean; onChange(newValue: T): void; } -function StyledRadioGroup({name, definitions, value, className, outlined, onChange}: IProps) { +function StyledRadioGroup({ + name, + definitions, + value, + className, + outlined, + disabled, + onChange, +}: IProps) { const _onChange = e => { onChange(e.target.value); }; @@ -50,12 +59,12 @@ function StyledRadioGroup({name, definitions, value, className checked={d.checked !== undefined ? d.checked : d.value === value} name={name} value={d.value} - disabled={d.disabled} + disabled={disabled || d.disabled} outlined={outlined} > - {d.label} + { d.label } - {d.description} + { d.description ? { d.description } : null } )} ; } From fdecba2fe54a5bfd4e5b9e6974e1fd9467858539 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 8 Jun 2021 16:32:43 +0100 Subject: [PATCH 10/17] Make AliasSettings space-aware, remove stale unused props --- .../views/room_settings/AliasSettings.tsx | 34 ++++++++++++++----- .../tabs/room/GeneralRoomSettingsTab.js | 3 +- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/components/views/room_settings/AliasSettings.tsx b/src/components/views/room_settings/AliasSettings.tsx index d6e79c4ee9..6f83d64eaf 100644 --- a/src/components/views/room_settings/AliasSettings.tsx +++ b/src/components/views/room_settings/AliasSettings.tsx @@ -80,7 +80,7 @@ interface IProps { canSetCanonicalAlias: boolean; canSetAliases: boolean; canonicalAliasEvent?: MatrixEvent; - aliasEvents?: MatrixEvent[]; + hidePublishSetting?: boolean; } interface IState { @@ -99,7 +99,6 @@ export default class AliasSettings extends React.Component { static defaultProps = { canSetAliases: false, canSetCanonicalAlias: false, - aliasEvents: [], }; constructor(props) { @@ -317,7 +316,9 @@ export default class AliasSettings extends React.Component { } render() { - const localDomain = MatrixClientPeg.get().getDomain(); + const cli = MatrixClientPeg.get(); + const localDomain = cli.getDomain(); + const isSpaceRoom = cli.getRoom(this.props.roomId)?.isSpaceRoom(); let found = false; const canonicalValue = this.state.canonicalAlias || ""; @@ -363,7 +364,9 @@ export default class AliasSettings extends React.Component { canEdit={this.props.canSetAliases} onItemAdded={this.onLocalAliasAdded} onItemRemoved={this.onLocalAliasDeleted} - noItemsLabel={_t('This room has no local addresses')} + noItemsLabel={isSpaceRoom + ? _t("This space has no local addresses") + : _t("This room has no local addresses")} placeholder={_t('Local address')} domain={localDomain} />); @@ -372,10 +375,20 @@ export default class AliasSettings extends React.Component { return (
{_t("Published Addresses")} -

{_t("Published addresses can be used by anyone on any server to join your room. " + - "To publish an address, it needs to be set as a local address first.")}

- {canonicalAliasSection} - +

+ { isSpaceRoom + ? _t("Published addresses can be used by anyone on any server to join your space.") + : _t("Published addresses can be used by anyone on any server to join your room.")} +   + { _t("To publish an address, it needs to be set as a local address first.") } +

+ { canonicalAliasSection } + { this.props.hidePublishSetting + ? null + : } {this.getLocalNonAltAliases().map(alias => { return
; + unfederatableSection =
{ _t('This room is not accessible by remote Matrix servers') }
; } let roomUpgradeButton; @@ -110,7 +111,7 @@ export default class AdvancedRoomSettingsTab extends React.Component

- {_t( + { _t( "Warning: Upgrading a room will not automatically migrate room members " + "to the new version of the room. We'll post a link to the new room in the old " + "version of the room - room members will have to click this link to join the new room.", @@ -118,10 +119,10 @@ export default class AdvancedRoomSettingsTab extends React.Component {sub}, "i": (sub) => {sub}, }, - )} + ) }

- {_t("Upgrade this room to the recommended room version")} + { _t("Upgrade this room to the recommended room version") }
); @@ -141,21 +142,21 @@ export default class AdvancedRoomSettingsTab extends React.Component -
{_t("Advanced")}
+
{ _t("Advanced") }
{ room?.isSpaceRoom() ? _t("Space information") : _t("Room information") }
- {_t("Internal room ID:")}  + { _t("Internal room ID:") }  { this.props.roomId }
{ unfederatableSection }
- {_t("Room version")} + { _t("Room version") }
- {_t("Room version:")}  + { _t("Room version:") }  { room.getVersion() }
{ oldRoomLink } diff --git a/src/components/views/spaces/SpaceSettingsGeneralTab.tsx b/src/components/views/spaces/SpaceSettingsGeneralTab.tsx index db0a180846..3afdc629e4 100644 --- a/src/components/views/spaces/SpaceSettingsGeneralTab.tsx +++ b/src/components/views/spaces/SpaceSettingsGeneralTab.tsx @@ -90,7 +90,7 @@ const SpaceSettingsGeneralTab = ({ matrixClient: cli, space, onFinished }: IProp }; return
-
{_t("General")}
+
{ _t("General") }
{ _t("Edit settings relating to your space.") }
diff --git a/src/components/views/spaces/SpaceSettingsVisibilityTab.tsx b/src/components/views/spaces/SpaceSettingsVisibilityTab.tsx index 2f80ad97a6..263823603b 100644 --- a/src/components/views/spaces/SpaceSettingsVisibilityTab.tsx +++ b/src/components/views/spaces/SpaceSettingsVisibilityTab.tsx @@ -137,7 +137,7 @@ const SpaceSettingsVisibilityTab = ({ matrixClient: cli, space }: IProps) => { } return
-
{_t("Visibility")}
+
{ _t("Visibility") }
{ error &&
{ error }
} From 83296b74404c6a6e594c9bd382bbb9d37f4248ab Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 22 Jun 2021 22:19:01 +0100 Subject: [PATCH 17/17] Fix typing --- src/hooks/useRoomState.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/hooks/useRoomState.ts b/src/hooks/useRoomState.ts index 11ac7de49e..e778acf8a9 100644 --- a/src/hooks/useRoomState.ts +++ b/src/hooks/useRoomState.ts @@ -24,7 +24,10 @@ type Mapper = (roomState: RoomState) => T; const defaultMapper: Mapper = (roomState: RoomState) => roomState; // Hook to simplify watching Matrix Room state -export const useRoomState = (room: Room, mapper: Mapper = defaultMapper): T => { +export const useRoomState = ( + room: Room, + mapper: Mapper = defaultMapper as Mapper, +): T => { const [value, setValue] = useState(room ? mapper(room.currentState) : undefined); const update = useCallback(() => {