/* 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, { ReactNode, useEffect, useState } from "react"; import { JoinRule, RestrictedAllowType, Room, EventType, Visibility } from "matrix-js-sdk/src/matrix"; import { RoomJoinRulesEventContent } from "matrix-js-sdk/src/types"; import StyledRadioGroup, { IDefinition } from "../elements/StyledRadioGroup"; import { _t } from "../../../languageHandler"; import AccessibleButton from "../elements/AccessibleButton"; import RoomAvatar from "../avatars/RoomAvatar"; import SpaceStore from "../../../stores/spaces/SpaceStore"; import Modal from "../../../Modal"; import ManageRestrictedJoinRuleDialog from "../dialogs/ManageRestrictedJoinRuleDialog"; import RoomUpgradeWarningDialog, { IFinishedOpts } from "../dialogs/RoomUpgradeWarningDialog"; import { upgradeRoom } from "../../../utils/RoomUpgrade"; import { arrayHasDiff } from "../../../utils/arrays"; import { useLocalEcho } from "../../../hooks/useLocalEcho"; import dis from "../../../dispatcher/dispatcher"; import { RoomSettingsTab } from "../dialogs/RoomSettingsDialog"; import { Action } from "../../../dispatcher/actions"; import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; import { doesRoomVersionSupport, PreferredRoomVersions } from "../../../utils/PreferredRoomVersions"; import SettingsStore from "../../../settings/SettingsStore"; import LabelledCheckbox from "../elements/LabelledCheckbox"; export interface JoinRuleSettingsProps { room: Room; promptUpgrade?: boolean; closeSettingsFn(): void; onError(error: unknown): void; beforeChange?(joinRule: JoinRule): Promise; // if returns false then aborts the change aliasWarning?: ReactNode; } const JoinRuleSettings: React.FC = ({ room, promptUpgrade, aliasWarning, onError, beforeChange, closeSettingsFn, }) => { const cli = room.client; const askToJoinEnabled = SettingsStore.getValue("feature_ask_to_join"); const roomSupportsKnock = doesRoomVersionSupport(room.getVersion(), PreferredRoomVersions.KnockRooms); const preferredKnockVersion = !roomSupportsKnock && promptUpgrade ? PreferredRoomVersions.KnockRooms : undefined; const roomSupportsRestricted = doesRoomVersionSupport(room.getVersion(), PreferredRoomVersions.RestrictedRooms); const preferredRestrictionVersion = !roomSupportsRestricted && promptUpgrade ? PreferredRoomVersions.RestrictedRooms : 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 = JoinRule.Invite } = content || {}; const restrictedAllowRoomIds = joinRule === JoinRule.Restricted ? content?.allow?.filter((o) => o.type === RestrictedAllowType.RoomMembership).map((o) => o.room_id) : undefined; const [isPublicKnockRoom, setIsPublicKnockRoom] = useState(false); useEffect(() => { if (joinRule === JoinRule.Knock) { cli.getRoomDirectoryVisibility(room.roomId) .then(({ visibility }) => setIsPublicKnockRoom(visibility === Visibility.Public)) .catch(onError); } }, [cli, joinRule, onError, room.roomId]); const onIsPublicKnockRoomChange = (checked: boolean): void => { cli.setRoomDirectoryVisibility(room.roomId, checked ? Visibility.Public : Visibility.Private) .then(() => setIsPublicKnockRoom(checked)) .catch(onError); }; const editRestrictedRoomIds = async (): Promise => { let selected = restrictedAllowRoomIds; if (!selected?.length && SpaceStore.instance.activeSpaceRoom) { selected = [SpaceStore.instance.activeSpaceRoom.roomId]; } const { finished } = Modal.createDialog( ManageRestrictedJoinRuleDialog, { room, selected, }, "mx_ManageRestrictedJoinRuleDialog_wrapper", ); const [roomIds] = await finished; return roomIds; }; const upgradeRequiredDialog = (targetVersion: string, description?: ReactNode): void => { Modal.createDialog(RoomUpgradeWarningDialog, { roomId: room.roomId, targetVersion, description, doUpgrade: async ( opts: IFinishedOpts, fn: (progressText: string, progress: number, total: number) => void, ): Promise => { const roomId = await upgradeRoom(room, targetVersion, opts.invite, true, true, true, (progress) => { const total = 2 + progress.updateSpacesTotal + progress.inviteUsersTotal; if (!progress.roomUpgraded) { fn(_t("room_settings|security|join_rule_upgrade_upgrading_room"), 0, total); } else if (!progress.roomSynced) { fn(_t("room_settings|security|join_rule_upgrade_awaiting_room"), 1, total); } else if ( progress.inviteUsersProgress !== undefined && progress.inviteUsersProgress < progress.inviteUsersTotal ) { fn( _t("room_settings|security|join_rule_upgrade_sending_invites", { progress: progress.inviteUsersProgress, count: progress.inviteUsersTotal, }), 2 + progress.inviteUsersProgress, total, ); } else if ( progress.updateSpacesProgress !== undefined && progress.updateSpacesProgress < progress.updateSpacesTotal ) { fn( _t("room_settings|security|join_rule_upgrade_updating_spaces", { progress: progress.updateSpacesProgress, count: progress.updateSpacesTotal, }), 2 + (progress.inviteUsersProgress ?? 0) + progress.updateSpacesProgress, total, ); } }); closeSettingsFn(); // switch to the new room in the background dis.dispatch({ action: Action.ViewRoom, room_id: roomId, metricsTrigger: undefined, // other }); // open new settings on this tab dis.dispatch({ action: "open_room_settings", initial_tab_id: RoomSettingsTab.Security, }); }, }); }; const upgradeRequiredPill = ( {_t("room_settings|security|join_rule_upgrade_required")} ); const definitions: IDefinition[] = [ { value: JoinRule.Invite, label: _t("room_settings|security|join_rule_invite"), description: _t("room_settings|security|join_rule_invite_description"), checked: joinRule === JoinRule.Invite || (joinRule === JoinRule.Restricted && !restrictedAllowRoomIds?.length), }, { value: JoinRule.Public, label: _t("common|public"), description: ( <> {_t("room_settings|security|join_rule_public_description")} {aliasWarning} ), }, ]; if (roomSupportsRestricted || preferredRestrictionVersion || joinRule === JoinRule.Restricted) { 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()) .slice(0, 4) as Room[]; let moreText; if (shownSpaces.length < restrictedAllowRoomIds.length) { if (shownSpaces.length > 0) { moreText = _t("room_settings|security|join_rule_restricted_n_more", { count: restrictedAllowRoomIds.length - shownSpaces.length, }); } else { moreText = _t("room_settings|security|join_rule_restricted_summary", { count: restrictedAllowRoomIds.length, }); } } const onRestrictedRoomIdsChange = (newAllowRoomIds: string[]): void => { if (!arrayHasDiff(restrictedAllowRoomIds || [], newAllowRoomIds)) return; if (!newAllowRoomIds.length) { setContent({ join_rule: JoinRule.Invite, }); return; } setContent({ join_rule: JoinRule.Restricted, allow: newAllowRoomIds.map((roomId) => ({ type: RestrictedAllowType.RoomMembership, room_id: roomId, })), }); }; const onEditRestrictedClick = async (): Promise => { const restrictedAllowRoomIds = await editRestrictedRoomIds(); if (!Array.isArray(restrictedAllowRoomIds)) return; if (restrictedAllowRoomIds.length > 0) { onRestrictedRoomIdsChange(restrictedAllowRoomIds); } else { onChange(JoinRule.Invite); } }; description = (
{_t( "room_settings|security|join_rule_restricted_description", {}, { a: (sub) => ( {sub} ), }, )}

{_t("room_settings|security|join_rule_restricted_description_spaces")}

{shownSpaces.map((room) => { return ( {room.name} ); })} {moreText && {moreText}}
); } else if (SpaceStore.instance.activeSpaceRoom) { description = _t( "room_settings|security|join_rule_restricted_description_active_space", {}, { spaceName: () => {SpaceStore.instance.activeSpaceRoom!.name}, }, ); } else { description = _t("room_settings|security|join_rule_restricted_description_prompt"); } definitions.splice(1, 0, { value: JoinRule.Restricted, label: ( <> {_t("room_settings|security|join_rule_restricted")} {preferredRestrictionVersion && upgradeRequiredPill} ), description, // if there are 0 allowed spaces then render it as invite only instead checked: joinRule === JoinRule.Restricted && !!restrictedAllowRoomIds?.length, }); } if (askToJoinEnabled && (roomSupportsKnock || preferredKnockVersion)) { definitions.push({ value: JoinRule.Knock, label: ( <> {_t("room_settings|security|join_rule_knock")} {preferredKnockVersion && upgradeRequiredPill} ), description: ( <> {_t("room_settings|security|join_rule_knock_description")} ), }); } const onChange = async (joinRule: JoinRule): Promise => { const beforeJoinRule = content?.join_rule; let restrictedAllowRoomIds: string[] | undefined; 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; let warning: JSX.Element | undefined; const userId = cli.getUserId()!; const unableToUpdateSomeParents = Array.from(SpaceStore.instance.getKnownParents(room.roomId)).some( (roomId) => !cli.getRoom(roomId)?.currentState.maySendStateEvent(EventType.SpaceChild, userId), ); if (unableToUpdateSomeParents) { warning = {_t("room_settings|security|join_rule_restricted_upgrade_warning")}; } upgradeRequiredDialog( targetVersion, <> {_t("room_settings|security|join_rule_restricted_upgrade_description")} {warning} , ); return; } // when setting to 0 allowed rooms/spaces set to invite only instead as per the note if (!restrictedAllowRoomIds?.length) { joinRule = JoinRule.Invite; } } else if (joinRule === JoinRule.Knock) { if (preferredKnockVersion) { upgradeRequiredDialog(preferredKnockVersion); return; } } if (beforeJoinRule === joinRule && !restrictedAllowRoomIds) return; if (beforeChange && !(await beforeChange(joinRule))) return; const newContent: RoomJoinRulesEventContent = { 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;