From 979bc71609beb7dfff0868135a12231d7535a1b1 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 1 Sep 2021 15:52:56 +0100 Subject: [PATCH 01/10] Deduplicate join rule management between rooms and spaces --- .../views/settings/JoinRuleSettings.tsx | 243 +++++++++++++ .../tabs/room/SecurityRoomSettingsTab.tsx | 320 ++++-------------- .../spaces/SpaceSettingsVisibilityTab.tsx | 56 +-- src/hooks/useLocalEcho.ts | 36 ++ src/i18n/strings/en_EN.json | 35 +- 5 files changed, 363 insertions(+), 327 deletions(-) create mode 100644 src/components/views/settings/JoinRuleSettings.tsx create mode 100644 src/hooks/useLocalEcho.ts diff --git a/src/components/views/settings/JoinRuleSettings.tsx b/src/components/views/settings/JoinRuleSettings.tsx new file mode 100644 index 0000000000..3cb4456643 --- /dev/null +++ b/src/components/views/settings/JoinRuleSettings.tsx @@ -0,0 +1,243 @@ +/* +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 React from "react"; +import { IJoinRuleEventContent, JoinRule, RestrictedAllowType } from "matrix-js-sdk/src/@types/partials"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { EventType } from "matrix-js-sdk/src/@types/event"; + +import StyledRadioGroup, { IDefinition } from "../elements/StyledRadioGroup"; +import { _t } from "../../../languageHandler"; +import AccessibleButton from "../elements/AccessibleButton"; +import RoomAvatar from "../avatars/RoomAvatar"; +import SpaceStore from "../../../stores/SpaceStore"; +import { MatrixClientPeg } from "../../../MatrixClientPeg"; +import Modal from "../../../Modal"; +import ManageRestrictedJoinRuleDialog from "../dialogs/ManageRestrictedJoinRuleDialog"; +import RoomUpgradeWarningDialog from "../dialogs/RoomUpgradeWarningDialog"; +import { upgradeRoom } from "../../../utils/RoomUpgrade"; +import { arrayHasDiff } from "../../../utils/arrays"; +import { useLocalEcho } from "../../../hooks/useLocalEcho"; + +interface IProps { + room: Room; + promptUpgrade?: boolean; + onError(error: Error): void; + beforeChange?(joinRule: JoinRule): Promise; // if returns false then aborts the change +} + +const JoinRuleSettings = ({ room, promptUpgrade, onError, beforeChange }: IProps) => { + const cli = room.client; + + const restrictedRoomCapabilities = SpaceStore.instance.restrictedJoinRuleSupport; + const roomSupportsRestricted = Array.isArray(restrictedRoomCapabilities?.support) + && restrictedRoomCapabilities.support.includes(room.getVersion()); + const preferredRestrictionVersion = !roomSupportsRestricted && promptUpgrade + ? restrictedRoomCapabilities?.preferred + : undefined; + + const disabled = !room.currentState.mayClientSendStateEvent(EventType.RoomJoinRules, cli); + + const [content, setContent] = useLocalEcho( + () => room.currentState.getStateEvents(EventType.RoomJoinRules, "")?.getContent(), + content => cli.sendStateEvent(room.roomId, EventType.RoomJoinRules, content, ""), + onError, + ); + + const { join_rule: joinRule } = content; + const restrictedAllowRoomIds = joinRule === JoinRule.Restricted + ? content.allow.filter(o => o.type === RestrictedAllowType.RoomMembership).map(o => o.room_id) + : undefined; + + const editRestrictedRoomIds = async (): Promise => { + let selected = restrictedAllowRoomIds; + if (!selected?.length && SpaceStore.instance.activeSpace) { + selected = [SpaceStore.instance.activeSpace.roomId]; + } + + const matrixClient = MatrixClientPeg.get(); + const { finished } = Modal.createTrackedDialog('Edit restricted', '', ManageRestrictedJoinRuleDialog, { + matrixClient, + room, + selected, + }, "mx_ManageRestrictedJoinRuleDialog_wrapper"); + + const [roomIds] = await finished; + return roomIds; + }; + + const definitions: IDefinition[] = [{ + value: JoinRule.Invite, + label: _t("Private (invite only)"), + description: _t("Only invited people can join."), + checked: joinRule === JoinRule.Invite || (joinRule === JoinRule.Restricted && !restrictedAllowRoomIds?.length), + }, { + value: JoinRule.Public, + label: _t("Public"), + description: _t("Anyone can find and join."), + }]; + + if (roomSupportsRestricted || preferredRestrictionVersion || joinRule === JoinRule.Restricted) { + let upgradeRequiredPill; + if (preferredRestrictionVersion) { + upgradeRequiredPill = + { _t("Upgrade required") } + ; + } + + let description; + if (joinRule === JoinRule.Restricted && restrictedAllowRoomIds?.length) { + const shownSpaces = restrictedAllowRoomIds + .map(roomId => cli.getRoom(roomId)) + .filter(room => room?.isSpaceRoom()) + .slice(0, 4); + + let moreText; + if (shownSpaces.length < restrictedAllowRoomIds.length) { + if (shownSpaces.length > 0) { + moreText = _t("& %(count)s more", { + count: restrictedAllowRoomIds.length - shownSpaces.length, + }); + } else { + moreText = _t("Currently, %(count)s spaces have access", { + count: restrictedAllowRoomIds.length, + }); + } + } + + const onRestrictedRoomIdsChange = (newAllowRoomIds: string[]) => { + if (!arrayHasDiff(restrictedAllowRoomIds || [], newAllowRoomIds)) return; + + const newContent: IJoinRuleEventContent = { + join_rule: JoinRule.Restricted, + allow: newAllowRoomIds.map(roomId => ({ + "type": RestrictedAllowType.RoomMembership, + "room_id": roomId, + })), + }; + setContent(newContent); + }; + + const onEditRestrictedClick = async () => { + const restrictedAllowRoomIds = await editRestrictedRoomIds(); + if (!Array.isArray(restrictedAllowRoomIds)) return; + if (restrictedAllowRoomIds.length > 0) { + onRestrictedRoomIdsChange(restrictedAllowRoomIds); + } else { + onChange(JoinRule.Invite); + } + }; + + description =
+ + { _t("Anyone in a space can find and join. Edit which spaces can access here.", {}, { + a: sub => + { sub } + , + }) } + + +
+

{ _t("Spaces with access") }

+ { shownSpaces.map(room => { + return + + { room.name } + ; + }) } + { moreText && { moreText } } +
+
; + } else if (SpaceStore.instance.activeSpace) { + description = _t("Anyone in can find and join. You can select other spaces too.", {}, { + spaceName: () => { SpaceStore.instance.activeSpace.name }, + }); + } else { + description = _t("Anyone in a space can find and join. You can select multiple spaces."); + } + + definitions.splice(1, 0, { + value: JoinRule.Restricted, + label: <> + { _t("Space members") } + { upgradeRequiredPill } + , + description, + // if there are 0 allowed spaces then render it as invite only instead + checked: joinRule === JoinRule.Restricted && !!restrictedAllowRoomIds?.length, + }); + } + + const onChange = async (joinRule: JoinRule) => { + const beforeJoinRule = content.join_rule; + + let restrictedAllowRoomIds: string[]; + if (joinRule === JoinRule.Restricted) { + if (beforeJoinRule === JoinRule.Restricted || roomSupportsRestricted) { + // Have the user pick which spaces to allow joins from + restrictedAllowRoomIds = await editRestrictedRoomIds(); + if (!Array.isArray(restrictedAllowRoomIds)) return; + } else if (preferredRestrictionVersion) { + // Block this action on a room upgrade otherwise it'd make their room unjoinable + const targetVersion = preferredRestrictionVersion; + Modal.createTrackedDialog('Restricted join rule upgrade', '', RoomUpgradeWarningDialog, { + roomId: room.roomId, + targetVersion, + description: _t("This upgrade will allow members of selected spaces " + + "access to this room without an invite."), + onFinished: (resp) => { + if (!resp?.continue) return; + upgradeRoom(room, targetVersion, resp.invite); + }, + }); + return; + } + } + + if (beforeJoinRule === joinRule && !restrictedAllowRoomIds) return; + if (beforeChange && !await beforeChange(joinRule)) return; + + const newContent: IJoinRuleEventContent = { + join_rule: joinRule, + }; + + // pre-set the accepted spaces with the currently viewed one as per the microcopy + if (joinRule === JoinRule.Restricted) { + newContent.allow = restrictedAllowRoomIds.map(roomId => ({ + "type": RestrictedAllowType.RoomMembership, + "room_id": roomId, + })); + } + + setContent(newContent); + }; + + return ( + + ); +}; + +export default JoinRuleSettings; diff --git a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx index 081b1a8698..40b15cfc78 100644 --- a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx @@ -16,7 +16,7 @@ limitations under the License. import React from 'react'; import { GuestAccess, HistoryVisibility, JoinRule, RestrictedAllowType } from "matrix-js-sdk/src/@types/partials"; -import { IContent, MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { EventType } from 'matrix-js-sdk/src/@types/event'; import { _t } from "../../../../../languageHandler"; @@ -24,35 +24,28 @@ import { MatrixClientPeg } from "../../../../../MatrixClientPeg"; import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch"; import Modal from "../../../../../Modal"; import QuestionDialog from "../../../dialogs/QuestionDialog"; -import StyledRadioGroup, { IDefinition } from '../../../elements/StyledRadioGroup'; +import StyledRadioGroup from '../../../elements/StyledRadioGroup'; import { SettingLevel } from "../../../../../settings/SettingLevel"; import SettingsStore from "../../../../../settings/SettingsStore"; import { UIFeature } from "../../../../../settings/UIFeature"; import { replaceableComponent } from "../../../../../utils/replaceableComponent"; import AccessibleButton from "../../../elements/AccessibleButton"; -import SpaceStore from "../../../../../stores/SpaceStore"; -import RoomAvatar from "../../../avatars/RoomAvatar"; -import ManageRestrictedJoinRuleDialog from '../../../dialogs/ManageRestrictedJoinRuleDialog'; -import RoomUpgradeWarningDialog from '../../../dialogs/RoomUpgradeWarningDialog'; -import { upgradeRoom } from "../../../../../utils/RoomUpgrade"; -import { arrayHasDiff } from "../../../../../utils/arrays"; import SettingsFlag from '../../../elements/SettingsFlag'; import createRoom, { IOpts } from '../../../../../createRoom'; import CreateRoomDialog from '../../../dialogs/CreateRoomDialog'; +import JoinRuleSettings from "../../JoinRuleSettings"; +import ErrorDialog from "../../../dialogs/ErrorDialog"; interface IProps { roomId: string; } interface IState { - joinRule: JoinRule; restrictedAllowRoomIds?: string[]; guestAccess: GuestAccess; history: HistoryVisibility; hasAliases: boolean; encrypted: boolean; - roomSupportsRestricted?: boolean; - preferredRestrictionVersion?: string; showAdvancedSection: boolean; } @@ -62,7 +55,6 @@ export default class SecurityRoomSettingsTab extends React.Component this.setState({ hasAliases })); } @@ -132,7 +119,7 @@ export default class SecurityRoomSettingsTab extends React.Component { - if (this.state.joinRule == "public") { + if (MatrixClientPeg.get().getRoom(this.props.roomId)?.getJoinRule() === JoinRule.Public) { const dialog = Modal.createTrackedDialog('Confirm Public Encrypted Room', '', QuestionDialog, { title: _t('Are you sure you want to add encryption to this public room?'), description:
@@ -199,117 +186,6 @@ export default class SecurityRoomSettingsTab extends React.Component { - const beforeJoinRule = this.state.joinRule; - - let restrictedAllowRoomIds: string[]; - if (joinRule === JoinRule.Restricted) { - const matrixClient = MatrixClientPeg.get(); - const roomId = this.props.roomId; - const room = matrixClient.getRoom(roomId); - - if (beforeJoinRule === JoinRule.Restricted || this.state.roomSupportsRestricted) { - // Have the user pick which spaces to allow joins from - restrictedAllowRoomIds = await this.editRestrictedRoomIds(); - if (!Array.isArray(restrictedAllowRoomIds)) return; - } else if (this.state.preferredRestrictionVersion) { - // Block this action on a room upgrade otherwise it'd make their room unjoinable - const targetVersion = this.state.preferredRestrictionVersion; - Modal.createTrackedDialog('Restricted join rule upgrade', '', RoomUpgradeWarningDialog, { - roomId, - targetVersion, - description: _t("This upgrade will allow members of selected spaces " + - "access to this room without an invite."), - onFinished: (resp) => { - if (!resp?.continue) return; - upgradeRoom(room, targetVersion, resp.invite); - }, - }); - return; - } - } - - if ( - this.state.encrypted && - this.state.joinRule !== JoinRule.Public && - joinRule === JoinRule.Public - ) { - const dialog = Modal.createTrackedDialog('Confirm Public Encrypted Room', '', QuestionDialog, { - title: _t("Are you sure you want to make this encrypted room public?"), - description:
-

{ _t( - "It's not recommended to make encrypted rooms public. " + - "It will mean anyone can find and join the room, so anyone can read messages. " + - "You'll get none of the benefits of encryption. Encrypting messages in a public " + - "room will make receiving and sending messages slower.", - null, - { "b": (sub) => { sub } }, - ) }

-

{ _t( - "To avoid these issues, create a new public room for the conversation " + - "you plan to have.", - null, - { - "a": (sub) => { - dialog.close(); - this.createNewRoom(true, false); - }}> { sub } , - }, - ) }

-
, - }); - - const { finished } = dialog; - const [confirm] = await finished; - if (!confirm) return; - } - - if (beforeJoinRule === joinRule && !restrictedAllowRoomIds) return; - - const content: IContent = { - join_rule: joinRule, - }; - - // pre-set the accepted spaces with the currently viewed one as per the microcopy - if (joinRule === JoinRule.Restricted) { - content.allow = restrictedAllowRoomIds.map(roomId => ({ - "type": RestrictedAllowType.RoomMembership, - "room_id": roomId, - })); - } - - this.setState({ joinRule, restrictedAllowRoomIds }); - - const client = MatrixClientPeg.get(); - client.sendStateEvent(this.props.roomId, EventType.RoomJoinRules, content, "").catch((e) => { - console.error(e); - this.setState({ - joinRule: beforeJoinRule, - restrictedAllowRoomIds: undefined, - }); - }); - }; - - private onRestrictedRoomIdsChange = (restrictedAllowRoomIds: string[]) => { - const beforeRestrictedAllowRoomIds = this.state.restrictedAllowRoomIds; - if (!arrayHasDiff(beforeRestrictedAllowRoomIds || [], restrictedAllowRoomIds)) return; - this.setState({ restrictedAllowRoomIds }); - - const client = MatrixClientPeg.get(); - client.sendStateEvent(this.props.roomId, EventType.RoomJoinRules, { - join_rule: JoinRule.Restricted, - allow: restrictedAllowRoomIds.map(roomId => ({ - "type": RestrictedAllowType.RoomMembership, - "room_id": roomId, - })), - }, "").catch((e) => { - console.error(e); - this.setState({ restrictedAllowRoomIds: beforeRestrictedAllowRoomIds }); - }); - }; - private onGuestAccessChange = (allowed: boolean) => { const guestAccess = allowed ? GuestAccess.CanJoin : GuestAccess.Forbidden; const beforeGuestAccess = this.state.guestAccess; @@ -371,42 +247,12 @@ export default class SecurityRoomSettingsTab extends React.Component => { - let selected = this.state.restrictedAllowRoomIds; - if (!selected?.length && SpaceStore.instance.activeSpace) { - selected = [SpaceStore.instance.activeSpace.roomId]; - } - - const matrixClient = MatrixClientPeg.get(); - const { finished } = Modal.createTrackedDialog('Edit restricted', '', ManageRestrictedJoinRuleDialog, { - matrixClient, - room: matrixClient.getRoom(this.props.roomId), - selected, - }, "mx_ManageRestrictedJoinRuleDialog_wrapper"); - - const [restrictedAllowRoomIds] = await finished; - return restrictedAllowRoomIds; - }; - - private onEditRestrictedClick = async () => { - const restrictedAllowRoomIds = await this.editRestrictedRoomIds(); - if (!Array.isArray(restrictedAllowRoomIds)) return; - if (restrictedAllowRoomIds.length > 0) { - this.onRestrictedRoomIdsChange(restrictedAllowRoomIds); - } else { - this.onJoinRuleChange(JoinRule.Invite); - } - }; - private renderJoinRule() { const client = MatrixClientPeg.get(); const room = client.getRoom(this.props.roomId); - const joinRule = this.state.joinRule; - - const canChangeJoinRule = room.currentState.mayClientSendStateEvent(EventType.RoomJoinRules, client); let aliasWarning = null; - if (joinRule === JoinRule.Public && !this.state.hasAliases) { + if (room.getJoinRule() === JoinRule.Public && !this.state.hasAliases) { aliasWarning = (
@@ -417,111 +263,67 @@ export default class SecurityRoomSettingsTab extends React.Component[] = [{ - value: JoinRule.Invite, - label: _t("Private (invite only)"), - description: _t("Only invited people can join."), - checked: this.state.joinRule === JoinRule.Invite - || (this.state.joinRule === JoinRule.Restricted && !this.state.restrictedAllowRoomIds?.length), - }, { - value: JoinRule.Public, - label: _t("Public"), - description: _t("Anyone can find and join."), - }]; + return
+
+ { _t("Decide who can join %(roomName)s.", { + roomName: room?.name, + }) } +
- if (this.state.roomSupportsRestricted || - this.state.preferredRestrictionVersion || - joinRule === JoinRule.Restricted - ) { - let upgradeRequiredPill; - if (this.state.preferredRestrictionVersion) { - upgradeRequiredPill = - { _t("Upgrade required") } - ; - } + { aliasWarning } - let description; - if (joinRule === JoinRule.Restricted && this.state.restrictedAllowRoomIds?.length) { - const shownSpaces = this.state.restrictedAllowRoomIds - .map(roomId => client.getRoom(roomId)) - .filter(room => room?.isSpaceRoom()) - .slice(0, 4); + +
; + } - let moreText; - if (shownSpaces.length < this.state.restrictedAllowRoomIds.length) { - if (shownSpaces.length > 0) { - moreText = _t("& %(count)s more", { - count: this.state.restrictedAllowRoomIds.length - shownSpaces.length, - }); - } else { - moreText = _t("Currently, %(count)s spaces have access", { - count: this.state.restrictedAllowRoomIds.length, - }); - } - } + private onJoinRuleChangeError = (error: Error) => { + Modal.createTrackedDialog('Room not found', '', ErrorDialog, { + title: _t("Failed to update the join rules"), + description: error.message ?? _t("Unknown failure"), + }); + }; - description =
- - { _t("Anyone in a space can find and join. Edit which spaces can access here.", {}, { - a: sub => - { sub } - , - }) } - - -
-

{ _t("Spaces with access") }

- { shownSpaces.map(room => { - return - - { room.name } - ; - }) } - { moreText && { moreText } } -
-
; - } else if (SpaceStore.instance.activeSpace) { - description = _t("Anyone in %(spaceName)s can find and join. You can select other spaces too.", { - spaceName: SpaceStore.instance.activeSpace.name, - }); - } else { - description = _t("Anyone in a space can find and join. You can select multiple spaces."); - } - - radioDefinitions.splice(1, 0, { - value: JoinRule.Restricted, - label: <> - { _t("Space members") } - { upgradeRequiredPill } - , - description, - // if there are 0 allowed spaces then render it as invite only instead - checked: this.state.joinRule === JoinRule.Restricted && !!this.state.restrictedAllowRoomIds?.length, + private onBeforeJoinRuleChange = async (joinRule: JoinRule): Promise => { + if (this.state.encrypted && joinRule === JoinRule.Public) { + const dialog = Modal.createTrackedDialog('Confirm Public Encrypted Room', '', QuestionDialog, { + title: _t("Are you sure you want to make this encrypted room public?"), + description:
+

{ _t( + "It's not recommended to make encrypted rooms public. " + + "It will mean anyone can find and join the room, so anyone can read messages. " + + "You'll get none of the benefits of encryption. Encrypting messages in a public " + + "room will make receiving and sending messages slower.", + null, + { "b": (sub) => { sub } }, + ) }

+

{ _t( + "To avoid these issues, create a new public room for the conversation " + + "you plan to have.", + null, + { + "a": (sub) => { + dialog.close(); + this.createNewRoom(true, false); + }}> { sub } , + }, + ) }

+
, }); + + const { finished } = dialog; + const [confirm] = await finished; + if (!confirm) return false; } - return ( -
-
- { _t("Decide who can join %(roomName)s.", { - roomName: client.getRoom(this.props.roomId)?.name, - }) } -
- { aliasWarning } - -
- ); - } + return true; + }; private renderHistory() { const client = MatrixClientPeg.get(); diff --git a/src/components/views/spaces/SpaceSettingsVisibilityTab.tsx b/src/components/views/spaces/SpaceSettingsVisibilityTab.tsx index b48f5c79c6..83587f54e6 100644 --- a/src/components/views/spaces/SpaceSettingsVisibilityTab.tsx +++ b/src/components/views/spaces/SpaceSettingsVisibilityTab.tsx @@ -25,49 +25,19 @@ import AccessibleButton from "../elements/AccessibleButton"; import AliasSettings from "../room_settings/AliasSettings"; import { useStateToggle } from "../../../hooks/useStateToggle"; import LabelledToggleSwitch from "../elements/LabelledToggleSwitch"; -import StyledRadioGroup from "../elements/StyledRadioGroup"; +import { useLocalEcho } from "../../../hooks/useLocalEcho"; +import JoinRuleSettings from "../settings/JoinRuleSettings"; interface IProps { matrixClient: MatrixClient; space: Room; } -enum SpaceVisibility { - Unlisted = "unlisted", - Private = "private", -} - -const useLocalEcho = ( - currentFactory: () => T, - setterFn: (value: T) => Promise, - errorFn: (error: Error) => void, -): [value: T, handler: (value: T) => void] => { - const [value, setValue] = useState(currentFactory); - const handler = async (value: T) => { - setValue(value); - try { - await setterFn(value); - } catch (e) { - setValue(currentFactory()); - errorFn(e); - } - }; - - return [value, handler]; -}; - const SpaceSettingsVisibilityTab = ({ matrixClient: cli, space }: IProps) => { const [error, setError] = useState(""); const userId = cli.getUserId(); - const [visibility, setVisibility] = useLocalEcho( - () => space.getJoinRule() === JoinRule.Invite ? SpaceVisibility.Private : SpaceVisibility.Unlisted, - visibility => cli.sendStateEvent(space.roomId, EventType.RoomJoinRules, { - join_rule: visibility === SpaceVisibility.Unlisted ? JoinRule.Public : JoinRule.Invite, - }, ""), - () => setError(_t("Failed to update the visibility of this space")), - ); const [guestAccessEnabled, setGuestAccessEnabled] = useLocalEcho( () => space.currentState.getStateEvents(EventType.RoomGuestAccess, "") ?.getContent()?.guest_access === GuestAccess.CanJoin, @@ -87,7 +57,6 @@ const SpaceSettingsVisibilityTab = ({ matrixClient: cli, space }: IProps) => { const [showAdvancedSection, toggleAdvancedSection] = useStateToggle(); - const canSetJoinRule = space.currentState.maySendStateEvent(EventType.RoomJoinRules, userId); const canSetGuestAccess = space.currentState.maySendStateEvent(EventType.RoomGuestAccess, userId); const canSetHistoryVisibility = space.currentState.maySendStateEvent(EventType.RoomHistoryVisibility, userId); const canSetCanonical = space.currentState.mayClientSendStateEvent(EventType.RoomCanonicalAlias, cli); @@ -121,7 +90,7 @@ const SpaceSettingsVisibilityTab = ({ matrixClient: cli, space }: IProps) => { } let addressesSection; - if (visibility !== SpaceVisibility.Private) { + if (space.getJoinRule() === JoinRule.Public) { addressesSection = <> { _t("Address") }
@@ -147,22 +116,9 @@ const SpaceSettingsVisibilityTab = ({ matrixClient: cli, space }: IProps) => {
- setError(_t("Failed to update the visibility of this space"))} />
diff --git a/src/hooks/useLocalEcho.ts b/src/hooks/useLocalEcho.ts new file mode 100644 index 0000000000..4b30fd2f00 --- /dev/null +++ b/src/hooks/useLocalEcho.ts @@ -0,0 +1,36 @@ +/* +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 { useState } from "react"; + +export const useLocalEcho = ( + currentFactory: () => T, + setterFn: (value: T) => Promise, + errorFn: (error: Error) => void, +): [value: T, handler: (value: T) => void] => { + const [value, setValue] = useState(currentFactory); + const handler = async (value: T) => { + setValue(value); + try { + await setterFn(value); + } catch (e) { + setValue(currentFactory()); + errorFn(e); + } + }; + + return [value, handler]; +}; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 2cb0a546aa..8e9f41f101 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1058,7 +1058,6 @@ "Saving...": "Saving...", "Save Changes": "Save Changes", "Leave Space": "Leave Space", - "Failed to update the visibility of this space": "Failed to update the visibility of this space", "Failed to update the guest access of this space": "Failed to update the guest access of this space", "Failed to update the history visibility of this space": "Failed to update the history visibility of this space", "Hide advanced": "Hide advanced", @@ -1068,9 +1067,7 @@ "Show advanced": "Show advanced", "Visibility": "Visibility", "Decide who can view and join %(spaceName)s.": "Decide who can view and join %(spaceName)s.", - "anyone with the link can view and join": "anyone with the link can view and join", - "Invite only": "Invite only", - "only invited people can view and join": "only invited people can view and join", + "Failed to update the visibility of this space": "Failed to update the visibility of this space", "Preview Space": "Preview Space", "Allow people to preview your space before they join.": "Allow people to preview your space before they join.", "Recommended for public spaces.": "Recommended for public spaces.", @@ -1144,6 +1141,18 @@ "Connecting to integration manager...": "Connecting to integration manager...", "Cannot connect to integration manager": "Cannot connect to integration manager", "The integration manager is offline or it cannot reach your homeserver.": "The integration manager is offline or it cannot reach your homeserver.", + "Private (invite only)": "Private (invite only)", + "Only invited people can join.": "Only invited people can join.", + "Anyone can find and join.": "Anyone can find and join.", + "Upgrade required": "Upgrade required", + "& %(count)s more|other": "& %(count)s more", + "Currently, %(count)s spaces have access|other": "Currently, %(count)s spaces have access", + "Anyone in a space can find and join. Edit which spaces can access here.": "Anyone in a space can find and join. Edit which spaces can access here.", + "Spaces with access": "Spaces with access", + "Anyone in can find and join. You can select other spaces too.": "Anyone in can find and join. You can select other spaces too.", + "Anyone in a space can find and join. You can select multiple spaces.": "Anyone in a space can find and join. You can select multiple spaces.", + "Space members": "Space members", + "This upgrade will allow members of selected spaces access to this room without an invite.": "This upgrade will allow members of selected spaces access to this room without an invite.", "Message layout": "Message layout", "IRC": "IRC", "Modern": "Modern", @@ -1452,23 +1461,13 @@ "To avoid these issues, create a new encrypted room for the conversation you plan to have.": "To avoid these issues, create a new encrypted room for the conversation you plan to have.", "Enable encryption?": "Enable encryption?", "Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. Learn more about encryption.": "Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. Learn more about encryption.", - "This upgrade will allow members of selected spaces access to this room without an invite.": "This upgrade will allow members of selected spaces access to this room without an invite.", + "To link to this room, please add an address.": "To link to this room, please add an address.", + "Decide who can join %(roomName)s.": "Decide who can join %(roomName)s.", + "Failed to update the join rules": "Failed to update the join rules", + "Unknown failure": "Unknown failure", "Are you sure you want to make this encrypted room public?": "Are you sure you want to make this encrypted room public?", "It's not recommended to make encrypted rooms public. It will mean anyone can find and join the room, so anyone can read messages. You'll get none of the benefits of encryption. Encrypting messages in a public room will make receiving and sending messages slower.": "It's not recommended to make encrypted rooms public. It will mean anyone can find and join the room, so anyone can read messages. You'll get none of the benefits of encryption. Encrypting messages in a public room will make receiving and sending messages slower.", "To avoid these issues, create a new public room for the conversation you plan to have.": "To avoid these issues, create a new public room for the conversation you plan to have.", - "To link to this room, please add an address.": "To link to this room, please add an address.", - "Private (invite only)": "Private (invite only)", - "Only invited people can join.": "Only invited people can join.", - "Anyone can find and join.": "Anyone can find and join.", - "Upgrade required": "Upgrade required", - "& %(count)s more|other": "& %(count)s more", - "Currently, %(count)s spaces have access|other": "Currently, %(count)s spaces have access", - "Anyone in a space can find and join. Edit which spaces can access here.": "Anyone in a space can find and join. Edit which spaces can access here.", - "Spaces with access": "Spaces with access", - "Anyone in %(spaceName)s can find and join. You can select other spaces too.": "Anyone in %(spaceName)s can find and join. You can select other spaces too.", - "Anyone in a space can find and join. You can select multiple spaces.": "Anyone in a space can find and join. You can select multiple spaces.", - "Space members": "Space members", - "Decide who can join %(roomName)s.": "Decide who can join %(roomName)s.", "Members only (since the point in time of selecting this option)": "Members only (since the point in time of selecting this option)", "Members only (since they were invited)": "Members only (since they were invited)", "Members only (since they joined)": "Members only (since they joined)", From d0b95b7d3da5d6d44ab069d9f78c4673fb477b1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 1 Sep 2021 18:03:15 +0200 Subject: [PATCH 02/10] Don't use a callback in setScreensharingEnabled() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/CallView.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx index 9d82291286..cec67499ae 100644 --- a/src/components/views/voip/CallView.tsx +++ b/src/components/views/voip/CallView.tsx @@ -273,14 +273,14 @@ export default class CallView extends React.Component { }; private onScreenshareClick = async (): Promise => { - const isScreensharing = await this.props.call.setScreensharingEnabled( - !this.state.screensharing, - async (): Promise => { - const { finished } = Modal.createDialog(DesktopCapturerSourcePicker); - const [source] = await finished; - return source; - }, - ); + let isScreensharing; + if (this.state.screensharing) { + isScreensharing = await this.props.call.setScreensharingEnabled(false); + } else { + const { finished } = Modal.createDialog(DesktopCapturerSourcePicker); + const [source] = await finished; + isScreensharing = await this.props.call.setScreensharingEnabled(true, source); + } this.setState({ sidebarShown: true, From 4777da4da77053111606f8c3aa17a67c272b7a60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 1 Sep 2021 18:15:02 +0200 Subject: [PATCH 03/10] Don't declare DesktopCapturerSource since we have a definition in the js-sdk MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../views/elements/DesktopCapturerSourcePicker.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/components/views/elements/DesktopCapturerSourcePicker.tsx b/src/components/views/elements/DesktopCapturerSourcePicker.tsx index 1f00353aeb..f650244aea 100644 --- a/src/components/views/elements/DesktopCapturerSourcePicker.tsx +++ b/src/components/views/elements/DesktopCapturerSourcePicker.tsx @@ -24,12 +24,6 @@ import { getDesktopCapturerSources } from "matrix-js-sdk/src/webrtc/call"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import TabbedView, { Tab, TabLocation } from '../../structures/TabbedView'; -export interface DesktopCapturerSource { - id: string; - name: string; - thumbnailURL; -} - export enum Tabs { Screens = "screen", Windows = "window", From 7ae9f3d1ae2d53d47eb13cfbd399205b5c87fc39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 1 Sep 2021 18:17:52 +0200 Subject: [PATCH 04/10] Remove Element-specifc screen-sharing code out of the js-sdk MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/@types/global.d.ts | 15 +++++++++++++++ .../elements/DesktopCapturerSourcePicker.tsx | 15 ++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 9d6bc2c6fb..cc6985eb66 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -49,6 +49,7 @@ import PerformanceMonitor from "../performance"; import UIStore from "../stores/UIStore"; import { SetupEncryptionStore } from "../stores/SetupEncryptionStore"; import { RoomScrollStateStore } from "../stores/RoomScrollStateStore"; +import { DesktopCapturerSource } from "matrix-js-sdk/src/webrtc/call"; /* eslint-disable @typescript-eslint/naming-convention */ @@ -93,6 +94,20 @@ declare global { mxSetupEncryptionStore?: SetupEncryptionStore; mxRoomScrollStateStore?: RoomScrollStateStore; mxOnRecaptchaLoaded?: () => void; + electron?: Electron; + } + + interface GetSourcesOptions { + types: Array; + thumbnailSize?: { + height: number; + width: number; + }; + fetchWindowIcons?: boolean; + } + + interface Electron { + getDesktopCapturerSources(options: GetSourcesOptions): Promise>; } interface Document { diff --git a/src/components/views/elements/DesktopCapturerSourcePicker.tsx b/src/components/views/elements/DesktopCapturerSourcePicker.tsx index f650244aea..3886b824a2 100644 --- a/src/components/views/elements/DesktopCapturerSourcePicker.tsx +++ b/src/components/views/elements/DesktopCapturerSourcePicker.tsx @@ -20,10 +20,23 @@ import BaseDialog from "..//dialogs/BaseDialog"; import DialogButtons from "./DialogButtons"; import classNames from 'classnames'; import AccessibleButton from './AccessibleButton'; -import { getDesktopCapturerSources } from "matrix-js-sdk/src/webrtc/call"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import TabbedView, { Tab, TabLocation } from '../../structures/TabbedView'; +export function getDesktopCapturerSources(): Promise> { + const options: GetSourcesOptions = { + thumbnailSize: { + height: 176, + width: 312, + }, + types: [ + "screen", + "window", + ], + }; + return window.electron.getDesktopCapturerSources(options); +} + export enum Tabs { Screens = "screen", Windows = "window", From 5ed4f3f54fd8cb241edb7e10a8aa52eb4cbbdd14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 1 Sep 2021 18:22:07 +0200 Subject: [PATCH 05/10] Move DesktopCapturerSource out of global.d.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/DesktopCapturerSourcePicker.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/elements/DesktopCapturerSourcePicker.tsx b/src/components/views/elements/DesktopCapturerSourcePicker.tsx index 3886b824a2..f014f0e3b8 100644 --- a/src/components/views/elements/DesktopCapturerSourcePicker.tsx +++ b/src/components/views/elements/DesktopCapturerSourcePicker.tsx @@ -22,6 +22,7 @@ import classNames from 'classnames'; import AccessibleButton from './AccessibleButton'; import { replaceableComponent } from "../../../utils/replaceableComponent"; import TabbedView, { Tab, TabLocation } from '../../structures/TabbedView'; +import { DesktopCapturerSource } from "matrix-js-sdk/src/webrtc/call"; export function getDesktopCapturerSources(): Promise> { const options: GetSourcesOptions = { From 2f1ee610d9d7a8ca7ce6f7e3a4cd3a4d8d4759b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 2 Sep 2021 14:10:28 +0200 Subject: [PATCH 06/10] Use source id directly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/@types/global.d.ts | 7 ++++++- .../views/elements/DesktopCapturerSourcePicker.tsx | 5 ++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index cc6985eb66..8ad93fa960 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -49,7 +49,6 @@ import PerformanceMonitor from "../performance"; import UIStore from "../stores/UIStore"; import { SetupEncryptionStore } from "../stores/SetupEncryptionStore"; import { RoomScrollStateStore } from "../stores/RoomScrollStateStore"; -import { DesktopCapturerSource } from "matrix-js-sdk/src/webrtc/call"; /* eslint-disable @typescript-eslint/naming-convention */ @@ -97,6 +96,12 @@ declare global { electron?: Electron; } + interface DesktopCapturerSource { + id: string; + name: string; + thumbnailURL: string; + } + interface GetSourcesOptions { types: Array; thumbnailSize?: { diff --git a/src/components/views/elements/DesktopCapturerSourcePicker.tsx b/src/components/views/elements/DesktopCapturerSourcePicker.tsx index f014f0e3b8..034fc3d49c 100644 --- a/src/components/views/elements/DesktopCapturerSourcePicker.tsx +++ b/src/components/views/elements/DesktopCapturerSourcePicker.tsx @@ -22,7 +22,6 @@ import classNames from 'classnames'; import AccessibleButton from './AccessibleButton'; import { replaceableComponent } from "../../../utils/replaceableComponent"; import TabbedView, { Tab, TabLocation } from '../../structures/TabbedView'; -import { DesktopCapturerSource } from "matrix-js-sdk/src/webrtc/call"; export function getDesktopCapturerSources(): Promise> { const options: GetSourcesOptions = { @@ -86,7 +85,7 @@ export interface PickerIState { selectedSource: DesktopCapturerSource | null; } export interface PickerIProps { - onFinished(source: DesktopCapturerSource): void; + onFinished(sourceId: string): void; } @replaceableComponent("views.elements.DesktopCapturerSourcePicker") @@ -131,7 +130,7 @@ export default class DesktopCapturerSourcePicker extends React.Component< }; private onShare = (): void => { - this.props.onFinished(this.state.selectedSource); + this.props.onFinished(this.state.selectedSource.id); }; private onTabChange = (): void => { From fa60b24a9f9f047cc99653dd45c09a3c77c16976 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 9 Sep 2021 10:50:12 +0100 Subject: [PATCH 07/10] fix react error in console --- src/components/views/spaces/SpaceTreeLevel.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx index dda58ae944..35c0275240 100644 --- a/src/components/views/spaces/SpaceTreeLevel.tsx +++ b/src/components/views/spaces/SpaceTreeLevel.tsx @@ -260,7 +260,7 @@ export class SpaceItem extends React.PureComponent { render() { // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { space, activeSpaces, isNested, isPanelCollapsed, onExpand, parents, innerRef, + const { space, activeSpaces, isNested, isPanelCollapsed, onExpand, parents, innerRef, dragHandleProps, ...otherProps } = this.props; const collapsed = this.isCollapsed; @@ -299,7 +299,7 @@ export class SpaceItem extends React.PureComponent { /> : null; // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { tabIndex, ...dragHandleProps } = this.props.dragHandleProps || {}; + const { tabIndex, ...restDragHandleProps } = dragHandleProps || {}; return (
  • { role="treeitem" > Date: Thu, 9 Sep 2021 10:54:31 +0100 Subject: [PATCH 08/10] Tweak edge case behaviour --- .../views/settings/JoinRuleSettings.tsx | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/components/views/settings/JoinRuleSettings.tsx b/src/components/views/settings/JoinRuleSettings.tsx index 3cb4456643..1ff7eb83f6 100644 --- a/src/components/views/settings/JoinRuleSettings.tsx +++ b/src/components/views/settings/JoinRuleSettings.tsx @@ -121,14 +121,20 @@ const JoinRuleSettings = ({ room, promptUpgrade, onError, beforeChange }: IProps const onRestrictedRoomIdsChange = (newAllowRoomIds: string[]) => { if (!arrayHasDiff(restrictedAllowRoomIds || [], newAllowRoomIds)) return; - const newContent: IJoinRuleEventContent = { + if (!newAllowRoomIds.length) { + setContent({ + join_rule: JoinRule.Invite, + }); + return; + } + + setContent({ join_rule: JoinRule.Restricted, allow: newAllowRoomIds.map(roomId => ({ "type": RestrictedAllowType.RoomMembership, "room_id": roomId, })), - }; - setContent(newContent); + }); }; const onEditRestrictedClick = async () => { @@ -209,6 +215,11 @@ const JoinRuleSettings = ({ room, promptUpgrade, onError, beforeChange }: IProps }); return; } + + // when setting to 0 allowed rooms/spaces set to invite only instead as per the note + if (!restrictedAllowRoomIds.length) { + joinRule = JoinRule.Invite; + } } if (beforeJoinRule === joinRule && !restrictedAllowRoomIds) return; From 2a8e8b93aa5bb2f652e3ecd396403df1ca5dce2a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 10 Sep 2021 09:48:46 +0100 Subject: [PATCH 09/10] add comment --- src/components/views/settings/JoinRuleSettings.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/settings/JoinRuleSettings.tsx b/src/components/views/settings/JoinRuleSettings.tsx index 2f457f2916..94c70f861e 100644 --- a/src/components/views/settings/JoinRuleSettings.tsx +++ b/src/components/views/settings/JoinRuleSettings.tsx @@ -103,6 +103,7 @@ const JoinRuleSettings = ({ room, promptUpgrade, onError, beforeChange, closeSet let description; if (joinRule === JoinRule.Restricted && restrictedAllowRoomIds?.length) { + // only show the first 4 spaces we know about, so that the UI doesn't grow out of proportion there are lots. const shownSpaces = restrictedAllowRoomIds .map(roomId => cli.getRoom(roomId)) .filter(room => room?.isSpaceRoom()) From d15ea932f69aa324965db54d779a3481622998b5 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 10 Sep 2021 11:54:25 +0100 Subject: [PATCH 10/10] fix unrelated issues --- .../views/settings/tabs/room/SecurityRoomSettingsTab.tsx | 2 +- src/components/views/spaces/SpaceSettingsVisibilityTab.tsx | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx index b467dc7680..d1c5bc8448 100644 --- a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx @@ -425,7 +425,7 @@ export default class SecurityRoomSettingsTab extends React.Component state.getJoinRule()); const [guestAccessEnabled, setGuestAccessEnabled] = useLocalEcho( () => space.currentState.getStateEvents(EventType.RoomGuestAccess, "") ?.getContent()?.guest_access === GuestAccess.CanJoin, @@ -64,7 +66,7 @@ const SpaceSettingsVisibilityTab = ({ matrixClient: cli, space, closeSettingsFn const canonicalAliasEv = space.currentState.getStateEvents(EventType.RoomCanonicalAlias, ""); let advancedSection; - if (visibility === SpaceVisibility.Unlisted) { + if (joinRule === JoinRule.Public) { if (showAdvancedSection) { advancedSection = <>