diff --git a/apps/meteor/client/sidebar/header/CreateChannel.tsx b/apps/meteor/client/sidebar/header/CreateChannel.tsx deleted file mode 100644 index 282761d9f7ba..000000000000 --- a/apps/meteor/client/sidebar/header/CreateChannel.tsx +++ /dev/null @@ -1,224 +0,0 @@ -import { Box, Modal, Button, TextInput, Icon, Field, ToggleSwitch, FieldGroup } from '@rocket.chat/fuselage'; -import { useDebouncedCallback } from '@rocket.chat/fuselage-hooks'; -import type { TranslationKey } from '@rocket.chat/ui-contexts'; -import { useSetting, useTranslation, useEndpoint, usePermission } from '@rocket.chat/ui-contexts'; -import type { ReactElement } from 'react'; -import React, { useEffect, useMemo, useState } from 'react'; - -import { useHasLicenseModule } from '../../../ee/client/hooks/useHasLicenseModule'; -import UserAutoCompleteMultipleFederated from '../../components/UserAutoCompleteMultiple/UserAutoCompleteMultipleFederated'; - -export type CreateChannelProps = { - values: { - name: string; - type: boolean; - federated?: boolean; - readOnly?: boolean; - encrypted?: boolean; - broadcast?: boolean; - users: string[]; - description?: string; - }; - handlers: { - handleName?: () => void; - handleDescription?: () => void; - handleEncrypted?: () => void; - handleReadOnly?: () => void; - handleUsers: (users: Array) => void; - }; - hasUnsavedChanges: boolean; - onChangeType: React.FormEventHandler; - onChangeBroadcast: React.FormEventHandler; - onChangeFederated: React.FormEventHandler; - canOnlyCreateOneType?: false | 'p' | 'c'; - e2eEnabledForPrivateByDefault?: boolean; - onCreate: () => void; - onClose: () => void; -}; - -const getFederationHintKey = (licenseModule: ReturnType, featureToggle: boolean): TranslationKey => { - if (licenseModule === 'loading' || !licenseModule) { - return 'error-this-is-an-ee-feature'; - } - if (!featureToggle) { - return 'Federation_Matrix_Federated_Description_disabled'; - } - return 'Federation_Matrix_Federated_Description'; -}; - -const CreateChannel = ({ - values, - handlers, - hasUnsavedChanges, - onChangeType, - onChangeBroadcast, - canOnlyCreateOneType, - onChangeFederated, - e2eEnabledForPrivateByDefault, - onCreate, - onClose, -}: CreateChannelProps): ReactElement => { - const t = useTranslation(); - const e2eEnabled = useSetting('E2E_Enable'); - const canSetReadOnly = usePermission('set-readonly'); - const namesValidation = useSetting('UTF8_Channel_Names_Validation'); - const allowSpecialNames = useSetting('UI_Allow_room_names_with_special_chars'); - const federationEnabled = useSetting('Federation_Matrix_enabled'); - const channelNameExists = useEndpoint('GET', '/v1/rooms.nameExists'); - - const channelNameRegex = useMemo(() => new RegExp(`^${namesValidation}$`), [namesValidation]); - - const [nameError, setNameError] = useState(); - - const federatedModule = useHasLicenseModule('federation'); - - const canUseFederation = federatedModule !== 'loading' && federatedModule && federationEnabled; - - const checkName = useDebouncedCallback( - async (name: string) => { - setNameError(undefined); - if (hasUnsavedChanges) { - return; - } - if (!name || name.length === 0) { - return setNameError(t('Field_required')); - } - if (!allowSpecialNames && !channelNameRegex.test(name)) { - return setNameError(t('error-invalid-name')); - } - const { exists } = await channelNameExists({ roomName: name }); - - if (exists) { - return setNameError(t('Channel_already_exist', name)); - } - }, - 100, - [channelNameRegex], - ); - - useEffect(() => { - checkName(values.name); - }, [checkName, values.name]); - - const e2edisabled = useMemo( - () => !values.type || values.broadcast || Boolean(!e2eEnabled) || Boolean(e2eEnabledForPrivateByDefault), - [e2eEnabled, e2eEnabledForPrivateByDefault, values.broadcast, values.type], - ); - - const canSave = useMemo(() => hasUnsavedChanges && !nameError, [hasUnsavedChanges, nameError]); - - return ( - - - {t('Create_channel')} - - - - - - {t('Name')} - - } - placeholder={t('Channel_name')} - onChange={handlers.handleName} - /> - - {hasUnsavedChanges && nameError && {nameError}} - - - - {t('Topic')}{' '} - - ({t('optional')}) - - - - - - - - - - {t('Private')} - - {values.type ? t('Only_invited_users_can_acess_this_channel') : t('Everyone_can_access_this_channel')} - - - - - - - - - {t('Federation_Matrix_Federated')} - {t(getFederationHintKey(federatedModule, Boolean(federationEnabled)))} - - - - - - - - {t('Read_only')} - - {values.readOnly - ? t('Only_authorized_users_can_write_new_messages') - : t('All_users_in_the_channel_can_write_new_messages')} - - - - - - - - - {t('Encrypted')} - {values.type ? t('Encrypted_channel_Description') : t('Encrypted_not_available')} - - - - - - - - {t('Broadcast')} - {t('Broadcast_channel_Description')} - - - - - - {`${t('Add_members')} (${t('optional')})`} - - - - - - - - - - - - ); -}; - -export default CreateChannel; diff --git a/apps/meteor/client/sidebar/header/CreateChannel/CreateChannelModal.tsx b/apps/meteor/client/sidebar/header/CreateChannel/CreateChannelModal.tsx new file mode 100644 index 000000000000..301d19be929f --- /dev/null +++ b/apps/meteor/client/sidebar/header/CreateChannel/CreateChannelModal.tsx @@ -0,0 +1,310 @@ +import { Box, Modal, Button, TextInput, Icon, Field, ToggleSwitch, FieldGroup } from '@rocket.chat/fuselage'; +import type { TranslationKey } from '@rocket.chat/ui-contexts'; +import { useSetting, useTranslation, useEndpoint, usePermission, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; +import type { ReactElement } from 'react'; +import React, { useEffect, useMemo } from 'react'; +import { useForm, Controller } from 'react-hook-form'; + +import { useHasLicenseModule } from '../../../../ee/client/hooks/useHasLicenseModule'; +import UserAutoCompleteMultipleFederated from '../../../components/UserAutoCompleteMultiple/UserAutoCompleteMultipleFederated'; +import { goToRoomById } from '../../../lib/utils/goToRoomById'; + +type CreateChannelModalProps = { + teamId?: string; + onClose: () => void; +}; + +type CreateChannelModalPayload = { + name: string; + isPrivate: boolean; + topic?: string; + members: string[]; + readOnly: boolean; + encrypted: boolean; + broadcast: boolean; + federated: boolean; +}; + +const getFederationHintKey = (licenseModule: ReturnType, featureToggle: boolean): TranslationKey => { + if (licenseModule === 'loading' || !licenseModule) { + return 'error-this-is-an-ee-feature'; + } + if (!featureToggle) { + return 'Federation_Matrix_Federated_Description_disabled'; + } + return 'Federation_Matrix_Federated_Description'; +}; + +const CreateChannelModal = ({ teamId = '', onClose }: CreateChannelModalProps): ReactElement => { + const t = useTranslation(); + const e2eEnabled = useSetting('E2E_Enable'); + const canSetReadOnly = usePermission('set-readonly'); + const namesValidation = useSetting('UTF8_Channel_Names_Validation'); + const allowSpecialNames = useSetting('UI_Allow_room_names_with_special_chars'); + const federationEnabled = useSetting('Federation_Matrix_enabled'); + const channelNameExists = useEndpoint('GET', '/v1/rooms.nameExists'); + + const channelNameRegex = useMemo(() => new RegExp(`^${namesValidation}$`), [namesValidation]); + const federatedModule = useHasLicenseModule('federation'); + const canUseFederation = federatedModule !== 'loading' && federatedModule && federationEnabled; + + const createChannel = useEndpoint('POST', '/v1/channels.create'); + const createPrivateChannel = useEndpoint('POST', '/v1/groups.create'); + const canCreateChannel = usePermission('create-c'); + const canCreatePrivateChannel = usePermission('create-p'); + const e2eEnabledForPrivateByDefault = useSetting('E2E_Enabled_Default_PrivateRooms'); + const dispatchToastMessage = useToastMessageDispatch(); + + const canOnlyCreateOneType = useMemo(() => { + if (!canCreateChannel && canCreatePrivateChannel) { + return 'p'; + } + if (canCreateChannel && !canCreatePrivateChannel) { + return 'c'; + } + return false; + }, [canCreateChannel, canCreatePrivateChannel]); + + const { + register, + formState: { isDirty, errors }, + handleSubmit, + control, + setValue, + watch, + } = useForm({ + defaultValues: { + members: [], + name: '', + topic: '', + isPrivate: canOnlyCreateOneType ? canOnlyCreateOneType === 'p' : true, + readOnly: false, + encrypted: (e2eEnabledForPrivateByDefault as boolean) ?? false, + broadcast: false, + federated: false, + }, + }); + + const { isPrivate, broadcast, readOnly, federated } = watch(); + + useEffect(() => { + if (!isPrivate) { + setValue('encrypted', false); + } + + if (broadcast) { + setValue('encrypted', false); + } + + if (federated) { + // if room is federated, it cannot be encrypted or broadcast or readOnly + setValue('encrypted', false); + setValue('broadcast', false); + setValue('readOnly', false); + } + + setValue('readOnly', broadcast); + }, [federated, setValue, broadcast, isPrivate]); + + const validateChannelName = async (name: string): Promise => { + if (!name) { + return; + } + + if (!allowSpecialNames && !channelNameRegex.test(name)) { + return t('error-invalid-name'); + } + + const { exists } = await channelNameExists({ roomName: name }); + if (exists) { + return t('Channel_already_exist', name); + } + }; + + const handleCreateChannel = async ({ name, members, readOnly, topic, broadcast, encrypted, federated }: CreateChannelModalPayload) => { + let roomData; + const params = { + name, + members, + readOnly, + extraData: { + topic, + broadcast, + encrypted, + federated, + ...(teamId && { teamId }), + }, + }; + + try { + if (isPrivate) { + roomData = await createPrivateChannel(params); + !teamId && goToRoomById(roomData.group._id as string); + } else { + roomData = await createChannel(params); + !teamId && goToRoomById((roomData as any).channel._id); + } + + dispatchToastMessage({ type: 'success', message: t('Room_has_been_created') }); + } catch (error) { + dispatchToastMessage({ type: 'error', message: error }); + } finally { + onClose(); + } + }; + + const e2eDisabled = useMemo( + () => !isPrivate || broadcast || Boolean(!e2eEnabled) || Boolean(e2eEnabledForPrivateByDefault), + [e2eEnabled, e2eEnabledForPrivateByDefault, broadcast, isPrivate], + ); + + return ( + + + {t('Create_channel')} + + + + + + {t('Name')} + + validateChannelName(value), + })} + error={errors.name?.message} + addon={} + placeholder={t('Channel_name')} + /> + + {errors.name && {errors.name.message}} + + + + {t('Topic')}{' '} + + ({t('optional')}) + + + + + + + + + + {t('Private')} + + {isPrivate ? t('Only_invited_users_can_acess_this_channel') : t('Everyone_can_access_this_channel')} + + + ( + + )} + /> + + + + + + {t('Federation_Matrix_Federated')} + {t(getFederationHintKey(federatedModule, Boolean(federationEnabled)))} + + ( + + )} + /> + + + + + + {t('Read_only')} + + {readOnly ? t('Only_authorized_users_can_write_new_messages') : t('All_users_in_the_channel_can_write_new_messages')} + + + ( + + )} + /> + + + + + + {t('Encrypted')} + {isPrivate ? t('Encrypted_channel_Description') : t('Encrypted_not_available')} + + ( + + )} + /> + + + + + + {t('Broadcast')} + {t('Broadcast_channel_Description')} + + ( + + )} + /> + + + + + {t('Add_members')}{' '} + + ({t('optional')}) + + + ( + + )} + /> + + + + + + + + + + + ); +}; + +export default CreateChannelModal; diff --git a/apps/meteor/client/sidebar/header/CreateChannel/index.ts b/apps/meteor/client/sidebar/header/CreateChannel/index.ts new file mode 100644 index 000000000000..a1b32eb160a5 --- /dev/null +++ b/apps/meteor/client/sidebar/header/CreateChannel/index.ts @@ -0,0 +1 @@ +export { default } from './CreateChannelModal'; diff --git a/apps/meteor/client/sidebar/header/CreateChannelWithData.tsx b/apps/meteor/client/sidebar/header/CreateChannelWithData.tsx deleted file mode 100644 index 72b5de9a8c6d..000000000000 --- a/apps/meteor/client/sidebar/header/CreateChannelWithData.tsx +++ /dev/null @@ -1,137 +0,0 @@ -import type { RoomType } from '@rocket.chat/core-typings'; -import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { useSetting, usePermission } from '@rocket.chat/ui-contexts'; -import type { ReactElement, ComponentProps } from 'react'; -import React, { memo, useCallback, useMemo } from 'react'; - -import { useEndpointActionExperimental } from '../../hooks/useEndpointActionExperimental'; -import { useForm } from '../../hooks/useForm'; -import { goToRoomById } from '../../lib/utils/goToRoomById'; -import type { CreateChannelProps } from './CreateChannel'; -import CreateChannel from './CreateChannel'; - -type CreateChannelWithDataProps = { - onClose: () => void; - teamId?: string; -}; - -type UseFormValues = { - users: string[]; - name: string; - type: RoomType; - description: string; - readOnly: boolean; - encrypted: boolean; - broadcast: boolean; - federated: boolean; -}; - -const CreateChannelWithData = ({ onClose, teamId = '' }: CreateChannelWithDataProps): ReactElement => { - const createChannel = useEndpointActionExperimental('POST', '/v1/channels.create'); - const createPrivateChannel = useEndpointActionExperimental('POST', '/v1/groups.create'); - const canCreateChannel = usePermission('create-c'); - const canCreatePrivateChannel = usePermission('create-p'); - const e2eEnabledForPrivateByDefault = useSetting('E2E_Enabled_Default_PrivateRooms'); - const canOnlyCreateOneType = useMemo(() => { - if (!canCreateChannel && canCreatePrivateChannel) { - return 'p'; - } - if (canCreateChannel && !canCreatePrivateChannel) { - return 'c'; - } - return false; - }, [canCreateChannel, canCreatePrivateChannel]); - - const initialValues = { - users: [], - name: '', - description: '', - type: canOnlyCreateOneType ? canOnlyCreateOneType === 'p' : true, - readOnly: false, - encrypted: e2eEnabledForPrivateByDefault ?? false, - broadcast: false, - federated: false, - }; - const { values, handlers, hasUnsavedChanges } = useForm(initialValues); - - const { users, name, description, type, readOnly, broadcast, encrypted, federated } = values as UseFormValues; - const { handleEncrypted, handleType, handleBroadcast, handleReadOnly, handleFederated } = handlers; - - const onChangeType = useMutableCallback((value) => { - handleEncrypted(!value); - return handleType(value); - }); - - const onChangeBroadcast = useMutableCallback((value) => { - handleEncrypted(!value); - handleReadOnly(value); - return handleBroadcast(value); - }); - - const onChangeFederated = useMutableCallback((value) => { - // if room is federated, it cannot be encrypted - if (encrypted && value) { - handleEncrypted(false); - } - - // if room is federated, it cannot be broadcast - if (broadcast && value) { - handleBroadcast(false); - } - - // if room is federated, it cannot be readonly - if (readOnly && value) { - handleReadOnly(false); - } - - return handleFederated(value); - }); - - const onCreate = useCallback(async () => { - const goToRoom = (rid: string): void => { - goToRoomById(rid); - }; - - const params = { - name, - members: users, - readOnly, - extraData: { - description, - broadcast, - encrypted, - federated, - ...(teamId && { teamId }), - }, - }; - - if (type) { - const roomData = await createPrivateChannel(params); - console.log(roomData); - !teamId && goToRoom(roomData.group._id as string); - } else { - const roomData = await createChannel(params); - console.log(roomData); - !teamId && goToRoom((roomData as any).channel._id); - } - - onClose(); - }, [name, users, readOnly, description, broadcast, encrypted, federated, teamId, type, onClose, createPrivateChannel, createChannel]); - - return ( - ['handlers']} - hasUnsavedChanges={hasUnsavedChanges} - onChangeType={onChangeType} - onChangeFederated={onChangeFederated} - onChangeBroadcast={onChangeBroadcast} - canOnlyCreateOneType={canOnlyCreateOneType} - e2eEnabledForPrivateByDefault={Boolean(e2eEnabledForPrivateByDefault)} - onClose={onClose} - onCreate={onCreate} - /> - ); -}; - -export default memo(CreateChannelWithData); diff --git a/apps/meteor/client/sidebar/header/actions/CreateRoomList.tsx b/apps/meteor/client/sidebar/header/actions/CreateRoomList.tsx index 7665fe999d42..f293762a1e4e 100644 --- a/apps/meteor/client/sidebar/header/actions/CreateRoomList.tsx +++ b/apps/meteor/client/sidebar/header/actions/CreateRoomList.tsx @@ -5,7 +5,7 @@ import React from 'react'; import CreateDiscussion from '../../../components/CreateDiscussion'; import ListItem from '../../../components/Sidebar/ListItem'; -import CreateChannelWithData from '../CreateChannelWithData'; +import CreateChannelWithData from '../CreateChannel'; import CreateDirectMessage from '../CreateDirectMessage'; import CreateTeam from '../CreateTeam'; import { useCreateRoomModal } from '../hooks/useCreateRoomModal'; diff --git a/apps/meteor/client/views/home/cards/CreateChannelsCard.tsx b/apps/meteor/client/views/home/cards/CreateChannelsCard.tsx index 09ce0192a83c..0c67229e61d0 100644 --- a/apps/meteor/client/views/home/cards/CreateChannelsCard.tsx +++ b/apps/meteor/client/views/home/cards/CreateChannelsCard.tsx @@ -4,7 +4,7 @@ import { useTranslation, useSetModal } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; -import CreateChannelWithData from '../../../sidebar/header/CreateChannelWithData'; +import CreateChannelWithData from '../../../sidebar/header/CreateChannel'; const CreateChannelsCard = (): ReactElement => { const t = useTranslation(); diff --git a/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannels.js b/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannels.js index fd79b4e745b5..5b5297210d75 100644 --- a/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannels.js +++ b/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannels.js @@ -5,7 +5,7 @@ import React, { useCallback, useMemo, useState } from 'react'; import { useRecordList } from '../../../../hooks/lists/useRecordList'; import { AsyncStatePhase } from '../../../../lib/asyncState'; import { roomCoordinator } from '../../../../lib/rooms/roomCoordinator'; -import CreateChannelWithData from '../../../../sidebar/header/CreateChannelWithData'; +import CreateChannelWithData from '../../../../sidebar/header/CreateChannel'; import { useTabBarClose } from '../../../room/contexts/ToolboxContext'; import RoomInfo from '../../../room/contextualBar/Info'; import AddExistingModal from './AddExistingModal'; diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index 2b7a2df1bb56..80e02f629d7d 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -4092,6 +4092,7 @@ "room_disallowed_reactions": "disallowed reactions", "Room_Edit": "Room Edit", "Room_has_been_archived": "Room has been archived", + "Room_has_been_created": "Room has been created", "Room_has_been_deleted": "Room has been deleted", "Room_has_been_removed": "Room has been removed", "Room_has_been_unarchived": "Room has been unarchived",