import type { IRoom } from '@rocket.chat/core-typings'; import { Box, Modal, Button, Icon, Accordion, AccordionItem, ModalHeader, ModalTitle, ModalClose, ModalContent, ModalFooter, ModalFooterControllers, } from '@rocket.chat/fuselage'; import { TextInput, Field, ToggleSwitch, FieldGroup, FieldLabel, FieldRow, FieldError, FieldHint } from '@rocket.chat/fuselage-forms'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; import { useSetting, useTranslation, useEndpoint, useToastMessageDispatch, usePermissionWithScopedRoles, usePermission, } from '@rocket.chat/ui-contexts'; import type { ComponentProps } from 'react'; import { useId, useEffect, useMemo } from 'react'; import { useForm, Controller } from 'react-hook-form'; import { useEncryptedRoomDescription } from './useEncryptedRoomDescription'; import UserAutoCompleteMultiple from '../../../components/UserAutoCompleteMultiple'; import { useCreateChannelTypePermission } from '../../../hooks/useCreateChannelTypePermission'; import { useHasLicenseModule } from '../../../hooks/useHasLicenseModule'; import { useIsFederationEnabled } from '../../../hooks/useIsFederationEnabled'; import { useGoToRoom } from '../../../views/room/hooks/useGoToRoom'; export type CreateChannelModalProps = { teamId?: string; mainRoom?: IRoom; onClose: () => void; reload?: () => void; onSuccess?: (rid: string) => void | Promise; }; type CreateChannelModalPayload = { name: string; isPrivate: boolean; topic?: string; members: string[]; readOnly: boolean; encrypted: boolean; broadcast: boolean; federated: boolean; }; const getFederationHintKey = (federationModule: boolean, featureToggle: boolean, federationAccessPermission: boolean): TranslationKey => { if (!federationModule) { return 'error-this-is-a-premium-feature'; } if (!featureToggle) { return 'Federation_Matrix_Federated_Description_disabled'; } if (!federationAccessPermission) { return 'error-not-authorized-federation'; } return 'Federation_Matrix_Federated_Description'; }; const hasExternalMembers = (members: string[]): boolean => members.some((member) => member.startsWith('@')); const CreateChannelModal = ({ teamId = '', mainRoom, onClose, reload, onSuccess }: CreateChannelModalProps) => { const t = useTranslation(); const canSetReadOnly = usePermissionWithScopedRoles('set-readonly', ['owner']); const e2eEnabled = useSetting('E2E_Enable'); const namesValidation = useSetting('UTF8_Channel_Names_Validation'); const allowSpecialNames = useSetting('UI_Allow_room_names_with_special_chars'); const e2eEnabledForPrivateByDefault = useSetting('E2E_Enabled_Default_PrivateRooms') && e2eEnabled; const e2eEnforcedForPrivate = Boolean(useSetting('E2E_Force_Encryption_For_Private_Rooms')) && Boolean(e2eEnabled); const getEncryptedHint = useEncryptedRoomDescription('channel'); const channelNameRegex = useMemo(() => new RegExp(`^${namesValidation}$`), [namesValidation]); const federationEnabled = useIsFederationEnabled(); const { data: federationModule = false } = useHasLicenseModule('federation'); const federationAccessPermission = usePermission('access-federation'); const canUseFederation = federationModule && federationEnabled && federationAccessPermission; const federationFieldHint = getFederationHintKey(federationModule, federationEnabled, federationAccessPermission); const channelNameExists = useEndpoint('GET', '/v1/rooms.nameExists'); const createChannel = useEndpoint('POST', '/v1/channels.create'); const createPrivateChannel = useEndpoint('POST', '/v1/groups.create'); const dispatchToastMessage = useToastMessageDispatch(); const canOnlyCreateOneType = useCreateChannelTypePermission(mainRoom?._id); const { formState: { errors }, handleSubmit, control, setValue, watch, } = useForm({ defaultValues: { members: [], name: '', topic: '', isPrivate: canOnlyCreateOneType ? canOnlyCreateOneType === 'p' : true, readOnly: false, encrypted: Boolean(e2eEnforcedForPrivate || e2eEnabledForPrivateByDefault), broadcast: false, federated: false, }, }); const { isPrivate, broadcast, readOnly, federated, encrypted } = watch(); useEffect(() => { if (federated) { // if room is federated, it cannot be encrypted or broadcast or readOnly setValue('encrypted', false); setValue('broadcast', false); setValue('readOnly', false); } }, [federated, setValue]); useEffect(() => { if (!isPrivate) { setValue('encrypted', false); } }, [isPrivate, setValue]); useEffect(() => { if (isPrivate && e2eEnforcedForPrivate && !federated) { setValue('encrypted', true); } }, [isPrivate, e2eEnforcedForPrivate, federated, setValue]); useEffect(() => { setValue('readOnly', broadcast); }, [broadcast, setValue]); const validateChannelName = async (name: string): Promise => { if (!name) { return; } if (!allowSpecialNames && !channelNameRegex.test(name)) { return t('Name_cannot_have_special_characters'); } const { exists } = await channelNameExists({ roomName: name }); if (exists) { return t('Channel_already_exist', { channelName: name }); } }; const goToRoom = useGoToRoom(); const handleCreateChannel = async ({ name, members, readOnly, topic, broadcast, encrypted, federated }: CreateChannelModalPayload) => { let roomData; const params = { name, members, readOnly, extraData: { topic, broadcast, encrypted, ...(federated && { federated }), ...(teamId && { teamId }), }, }; let rid: string; try { if (isPrivate) { roomData = await createPrivateChannel(params); rid = roomData.group._id; if (!teamId) goToRoom(roomData.group._id); } else { roomData = await createChannel(params); rid = roomData.channel._id; if (!teamId) goToRoom(roomData.channel._id); } dispatchToastMessage({ type: 'success', message: t('Room_has_been_created') }); void onSuccess?.(rid); reload?.(); onClose(); } catch (error) { dispatchToastMessage({ type: 'error', message: error }); } }; const e2eDisabled = useMemo( () => !isPrivate || Boolean(!e2eEnabled) || federated || (e2eEnforcedForPrivate && isPrivate), [e2eEnabled, federated, isPrivate, e2eEnforcedForPrivate], ); const createChannelFormId = useId(); return ( ) => ( )} > {t('Create_channel')} {t('Name')} validateChannelName(value), }} render={({ field }) => ( } aria-required='true' /> )} /> {errors.name && {errors.name.message}} {!allowSpecialNames && {t('No_spaces_or_special_characters')}} {t('Topic')} } /> {t('Displayed_next_to_name')} {t('Members')} !federated && hasExternalMembers(members) ? t('You_cannot_add_external_users_to_non_federated_room') : true, }} render={({ field }) => } /> {errors.members && {errors.members.message}} {t('Private')} ( )} /> {isPrivate ? t('People_can_only_join_by_being_invited') : t('Anyone_can_access')} {t('Security_and_permissions')} {t('Federation_Matrix_Federated')} } /> {t(federationFieldHint)} {t('Encrypted')} } /> {getEncryptedHint({ isPrivate, encrypted })} {t('Read_only')} ( )} /> {readOnly ? t('Read_only_field_hint_enabled', { roomType: 'channel' }) : t('Anyone_can_send_new_messages')} {t('Broadcast')} } /> {broadcast && {t('Broadcast_hint_enabled', { roomType: 'channel' })}} ); }; export default CreateChannelModal;