diff --git a/.changeset/loud-wombats-cross.md b/.changeset/loud-wombats-cross.md new file mode 100644 index 0000000000000..5b2506394df3b --- /dev/null +++ b/.changeset/loud-wombats-cross.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes an issue where the Encrypted toggle in the `Create Channel Modal` would change unexpectedly or become disabled after switching the Private or Broadcast options when E2E defaults are enabled. + diff --git a/apps/meteor/client/NavBarV2/NavBarPagesGroup/actions/CreateChannelModal.spec.tsx b/apps/meteor/client/NavBarV2/NavBarPagesGroup/actions/CreateChannelModal.spec.tsx new file mode 100644 index 0000000000000..81603cdeeca54 --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarPagesGroup/actions/CreateChannelModal.spec.tsx @@ -0,0 +1,8 @@ +import { testCreateChannelModal } from './testCreateChannelModal'; +import CreateChannelModalComponent from '../../../sidebar/header/CreateChannel'; + +jest.mock('../../../lib/utils/goToRoomById', () => ({ + goToRoomById: jest.fn(), +})); + +testCreateChannelModal(CreateChannelModalComponent); diff --git a/apps/meteor/client/NavBarV2/NavBarPagesGroup/actions/CreateChannelModal.tsx b/apps/meteor/client/NavBarV2/NavBarPagesGroup/actions/CreateChannelModal.tsx index 66a36866c9d29..bc3b71fcbfcfc 100644 --- a/apps/meteor/client/NavBarV2/NavBarPagesGroup/actions/CreateChannelModal.tsx +++ b/apps/meteor/client/NavBarV2/NavBarPagesGroup/actions/CreateChannelModal.tsx @@ -107,23 +107,23 @@ const CreateChannelModal = ({ teamId = '', onClose, reload }: CreateChannelModal const { isPrivate, broadcast, readOnly, federated, encrypted } = 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); } + }, [federated, setValue]); + useEffect(() => { + if (!isPrivate) { + setValue('encrypted', false); + } + }, [isPrivate, setValue]); + + useEffect(() => { setValue('readOnly', broadcast); - }, [federated, setValue, broadcast, isPrivate]); + }, [broadcast, setValue]); const validateChannelName = async (name: string): Promise => { if (!name) { @@ -173,10 +173,7 @@ const CreateChannelModal = ({ teamId = '', onClose, reload }: CreateChannelModal } }; - const e2eDisabled = useMemo( - () => !isPrivate || broadcast || Boolean(!e2eEnabled) || Boolean(e2eEnabledForPrivateByDefault), - [e2eEnabled, e2eEnabledForPrivateByDefault, broadcast, isPrivate], - ); + const e2eDisabled = useMemo(() => !isPrivate || Boolean(!e2eEnabled) || federated, [e2eEnabled, federated, isPrivate]); const createChannelFormId = useId(); const nameId = useId(); @@ -276,13 +273,16 @@ const CreateChannelModal = ({ teamId = '', onClose, reload }: CreateChannelModal - {t('Federation_Matrix_Federated')} + + {t('Federation_Matrix_Federated')} + ( - {t('Encrypted')} + + {t('Encrypted')} + )} /> @@ -317,7 +319,9 @@ const CreateChannelModal = ({ teamId = '', onClose, reload }: CreateChannelModal - {t('Read_only')} + + {t('Read_only')} + - {t('Broadcast')} + + {t('Broadcast')} + ( { + it('should render with encryption option disabled and set to off when E2E_Enable=false and E2E_Enabled_Default_PrivateRooms=false', async () => { + render( null} />, { + wrapper: mockAppRoot().withSetting('E2E_Enable', false).withSetting('E2E_Enabled_Default_PrivateRooms', false).build(), + }); + + await userEvent.click(screen.getByText('Advanced_settings')); + + const encrypted = screen.getByLabelText('Encrypted') as HTMLInputElement; + expect(encrypted).toBeInTheDocument(); + expect(encrypted).not.toBeChecked(); + expect(encrypted).toBeDisabled(); + }); + + it('should render with encryption option enabled and set to off when E2E_Enable=true and E2E_Enabled_Default_PrivateRooms=false', async () => { + render( null} />, { + wrapper: mockAppRoot().withSetting('E2E_Enable', true).withSetting('E2E_Enabled_Default_PrivateRooms', false).build(), + }); + + await userEvent.click(screen.getByText('Advanced_settings')); + + const encrypted = screen.getByLabelText('Encrypted') as HTMLInputElement; + expect(encrypted).toBeInTheDocument(); + expect(encrypted).not.toBeChecked(); + expect(encrypted).toBeEnabled(); + }); + + it('should render with encryption option disabled and set to off when E2E_Enable=false and E2E_Enabled_Default_PrivateRooms=true', async () => { + render( null} />, { + wrapper: mockAppRoot().withSetting('E2E_Enable', false).withSetting('E2E_Enabled_Default_PrivateRooms', true).build(), + }); + + await userEvent.click(screen.getByText('Advanced_settings')); + + const encrypted = screen.getByLabelText('Encrypted') as HTMLInputElement; + expect(encrypted).toBeInTheDocument(); + + expect(encrypted).not.toBeChecked(); + expect(encrypted).toBeDisabled(); + }); + + it('should render with encryption option enabled and set to on when E2E_Enable=true and E2E_Enabled_Default_PrivateRooms=True', async () => { + render( null} />, { + wrapper: mockAppRoot().withSetting('E2E_Enable', true).withSetting('E2E_Enabled_Default_PrivateRooms', true).build(), + }); + + await userEvent.click(screen.getByText('Advanced_settings')); + + const encrypted = screen.getByLabelText('Encrypted') as HTMLInputElement; + expect(encrypted).toBeChecked(); + expect(encrypted).toBeEnabled(); + }); + + it('when Private goes ON → OFF: forces Encrypted OFF and disables it (E2E_Enable=true, E2E_Enabled_Default_PrivateRooms=true)', async () => { + render( null} />, { + wrapper: mockAppRoot().withSetting('E2E_Enable', true).withSetting('E2E_Enabled_Default_PrivateRooms', true).build(), + }); + + await userEvent.click(screen.getByText('Advanced_settings')); + + const encrypted = screen.getByLabelText('Encrypted') as HTMLInputElement; + const priv = screen.getByLabelText('Private') as HTMLInputElement; + + // initial: private=true, encrypted ON and enabled + expect(priv).toBeChecked(); + expect(encrypted).toBeChecked(); + expect(encrypted).toBeEnabled(); + + // Private ON -> OFF: encrypted must become OFF and disabled + await userEvent.click(priv); + expect(priv).not.toBeChecked(); + expect(encrypted).not.toBeChecked(); + expect(encrypted).toBeDisabled(); + }); + + it('when Private goes OFF → ON: keeps Encrypted OFF but re-enables it (E2E_Enable=true, E2E_Enabled_Default_PrivateRooms=true)', async () => { + render( null} />, { + wrapper: mockAppRoot().withSetting('E2E_Enable', true).withSetting('E2E_Enabled_Default_PrivateRooms', true).build(), + }); + + await userEvent.click(screen.getByText('Advanced_settings')); + + const encrypted = screen.getByLabelText('Encrypted') as HTMLInputElement; + const priv = screen.getByLabelText('Private') as HTMLInputElement; + + // turn private OFF to simulate user path from non-private + await userEvent.click(priv); + expect(priv).not.toBeChecked(); + expect(encrypted).not.toBeChecked(); + expect(encrypted).toBeDisabled(); + + // turn private back ON -> encrypted should remain OFF but become enabled + await userEvent.click(priv); + expect(priv).toBeChecked(); + expect(encrypted).not.toBeChecked(); + expect(encrypted).toBeEnabled(); + }); + + it('private room: toggling Broadcast on/off does not change or disable Encrypted', async () => { + render( null} />, { + wrapper: mockAppRoot().withSetting('E2E_Enable', true).withSetting('E2E_Enabled_Default_PrivateRooms', true).build(), + }); + + await userEvent.click(screen.getByText('Advanced_settings')); + + const encrypted = screen.getByLabelText('Encrypted') as HTMLInputElement; + const broadcast = screen.getByLabelText('Broadcast') as HTMLInputElement; + const priv = screen.getByLabelText('Private') as HTMLInputElement; + + expect(priv).toBeChecked(); + expect(encrypted).toBeChecked(); + expect(encrypted).toBeEnabled(); + expect(broadcast).not.toBeChecked(); + + // Broadcast: OFF -> ON (Encrypted unchanged + enabled) + await userEvent.click(broadcast); + expect(broadcast).toBeChecked(); + expect(encrypted).toBeChecked(); + expect(encrypted).toBeEnabled(); + + // Broadcast: ON -> OFF (Encrypted unchanged + enabled) + await userEvent.click(broadcast); + expect(broadcast).not.toBeChecked(); + expect(encrypted).toBeChecked(); + expect(encrypted).toBeEnabled(); + + // User can still toggle Encrypted freely while Broadcast is OFF + await userEvent.click(encrypted); + expect(encrypted).not.toBeChecked(); + + // User can still toggle Encrypted freely while Broadcast is ON + await userEvent.click(broadcast); + expect(broadcast).toBeChecked(); + expect(encrypted).not.toBeChecked(); + expect(encrypted).toBeEnabled(); + }); + + it('non-private room: Encrypted remains OFF and disabled regardless of Broadcast state', async () => { + render( null} />, { + wrapper: mockAppRoot().withSetting('E2E_Enable', true).withSetting('E2E_Enabled_Default_PrivateRooms', true).build(), + }); + + await userEvent.click(screen.getByText('Advanced_settings')); + + const encrypted = screen.getByLabelText('Encrypted') as HTMLInputElement; + const broadcast = screen.getByLabelText('Broadcast') as HTMLInputElement; + const priv = screen.getByLabelText('Private') as HTMLInputElement; + + // Switch to non-private + await userEvent.click(priv); + expect(priv).not.toBeChecked(); + + // Encrypted must be OFF + disabled (non-private cannot be encrypted) + expect(encrypted).not.toBeChecked(); + expect(encrypted).toBeDisabled(); + + // Broadcast: OFF -> ON (Encrypted stays OFF + disabled) + await userEvent.click(broadcast); + expect(broadcast).toBeChecked(); + expect(encrypted).not.toBeChecked(); + expect(encrypted).toBeDisabled(); + + // Broadcast: ON -> OFF (Encrypted still OFF + disabled) + await userEvent.click(broadcast); + expect(broadcast).not.toBeChecked(); + expect(encrypted).not.toBeChecked(); + expect(encrypted).toBeDisabled(); + }); + }); +} diff --git a/apps/meteor/client/sidebar/header/CreateChannel/CreateChannelModal.spec.tsx b/apps/meteor/client/sidebar/header/CreateChannel/CreateChannelModal.spec.tsx new file mode 100644 index 0000000000000..8c979f0a59b3e --- /dev/null +++ b/apps/meteor/client/sidebar/header/CreateChannel/CreateChannelModal.spec.tsx @@ -0,0 +1,8 @@ +import CreateChannelModal from './CreateChannelModal'; +import { testCreateChannelModal } from '../../../NavBarV2/NavBarPagesGroup/actions/testCreateChannelModal'; + +jest.mock('../../../lib/utils/goToRoomById', () => ({ + goToRoomById: jest.fn(), +})); + +testCreateChannelModal(CreateChannelModal); diff --git a/apps/meteor/client/sidebar/header/CreateChannel/CreateChannelModal.tsx b/apps/meteor/client/sidebar/header/CreateChannel/CreateChannelModal.tsx index 42e9032d9dc78..1fe7b6bafe37c 100644 --- a/apps/meteor/client/sidebar/header/CreateChannel/CreateChannelModal.tsx +++ b/apps/meteor/client/sidebar/header/CreateChannel/CreateChannelModal.tsx @@ -109,23 +109,23 @@ const CreateChannelModal = ({ teamId = '', mainRoom, onClose, reload }: CreateCh const { isPrivate, broadcast, readOnly, federated, encrypted } = 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); } + }, [federated, setValue]); + useEffect(() => { + if (!isPrivate) { + setValue('encrypted', false); + } + }, [isPrivate, setValue]); + + useEffect(() => { setValue('readOnly', broadcast); - }, [federated, setValue, broadcast, isPrivate]); + }, [broadcast, setValue]); const validateChannelName = async (name: string): Promise => { if (!name) { @@ -175,10 +175,7 @@ const CreateChannelModal = ({ teamId = '', mainRoom, onClose, reload }: CreateCh } }; - const e2eDisabled = useMemo( - () => !isPrivate || broadcast || Boolean(!e2eEnabled) || Boolean(e2eEnabledForPrivateByDefault), - [e2eEnabled, e2eEnabledForPrivateByDefault, broadcast, isPrivate], - ); + const e2eDisabled = useMemo(() => !isPrivate || Boolean(!e2eEnabled) || federated, [e2eEnabled, federated, isPrivate]); const createChannelFormId = useId(); const nameId = useId(); @@ -307,7 +304,7 @@ const CreateChannelModal = ({ teamId = '', mainRoom, onClose, reload }: CreateCh id={encryptedId} ref={ref} checked={value} - disabled={e2eDisabled || federated} + disabled={e2eDisabled} onChange={onChange} aria-describedby={`${encryptedId}-hint`} />