Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/loud-wombats-cross.md
Original file line number Diff line number Diff line change
@@ -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.

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { testCreateChannelModal } from './testCreateChannelModal';
import CreateChannelModalComponent from '../../../sidebar/header/CreateChannel';

jest.mock('../../../lib/utils/goToRoomById', () => ({
goToRoomById: jest.fn(),
}));

testCreateChannelModal(CreateChannelModalComponent);
Original file line number Diff line number Diff line change
Expand Up @@ -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<string | undefined> => {
if (!name) {
Expand Down Expand Up @@ -173,10 +173,7 @@ const CreateChannelModal = ({ teamId = '', onClose, reload }: CreateChannelModal
}
};

const e2eDisabled = useMemo<boolean>(
() => !isPrivate || broadcast || Boolean(!e2eEnabled) || Boolean(e2eEnabledForPrivateByDefault),
[e2eEnabled, e2eEnabledForPrivateByDefault, broadcast, isPrivate],
);
const e2eDisabled = useMemo<boolean>(() => !isPrivate || Boolean(!e2eEnabled) || federated, [e2eEnabled, federated, isPrivate]);

const createChannelFormId = useId();
const nameId = useId();
Expand Down Expand Up @@ -276,13 +273,16 @@ const CreateChannelModal = ({ teamId = '', onClose, reload }: CreateChannelModal
</Box>
<Field>
<FieldRow>
<FieldLabel htmlFor={federatedId}>{t('Federation_Matrix_Federated')}</FieldLabel>
<FieldLabel htmlFor={federatedId} id={`${federatedId}-label`}>
{t('Federation_Matrix_Federated')}
</FieldLabel>
<Controller
control={control}
name='federated'
render={({ field: { onChange, value, ref } }): ReactElement => (
<ToggleSwitch
aria-describedby={`${federatedId}-hint`}
aria-labelledby={`${federatedId}-label`}
id={federatedId}
ref={ref}
checked={value}
Expand All @@ -296,7 +296,9 @@ const CreateChannelModal = ({ teamId = '', onClose, reload }: CreateChannelModal
</Field>
<Field>
<FieldRow>
<FieldLabel htmlFor={encryptedId}>{t('Encrypted')}</FieldLabel>
<FieldLabel htmlFor={encryptedId} id={`${encryptedId}-label`}>
{t('Encrypted')}
</FieldLabel>
<Controller
control={control}
name='encrypted'
Expand All @@ -305,10 +307,10 @@ const CreateChannelModal = ({ teamId = '', onClose, reload }: CreateChannelModal
id={encryptedId}
ref={ref}
checked={value}
disabled={e2eDisabled || federated}
disabled={e2eDisabled}
onChange={onChange}
aria-describedby={`${encryptedId}-hint`}
aria-labelledby='Encrypted_channel_Label'
aria-labelledby={`${encryptedId}-label`}
/>
)}
/>
Expand All @@ -317,14 +319,17 @@ const CreateChannelModal = ({ teamId = '', onClose, reload }: CreateChannelModal
</Field>
<Field>
<FieldRow>
<FieldLabel htmlFor={readOnlyId}>{t('Read_only')}</FieldLabel>
<FieldLabel htmlFor={readOnlyId} id={`${readOnlyId}-label`}>
{t('Read_only')}
</FieldLabel>
<Controller
control={control}
name='readOnly'
render={({ field: { onChange, value, ref } }): ReactElement => (
<ToggleSwitch
id={readOnlyId}
aria-describedby={`${readOnlyId}-hint`}
aria-labelledby={`${readOnlyId}-label`}
ref={ref}
checked={value}
disabled={!canSetReadOnly || broadcast || federated}
Expand All @@ -339,13 +344,16 @@ const CreateChannelModal = ({ teamId = '', onClose, reload }: CreateChannelModal
</Field>
<Field>
<FieldRow>
<FieldLabel htmlFor={broadcastId}>{t('Broadcast')}</FieldLabel>
<FieldLabel htmlFor={broadcastId} id={`${broadcastId}-label`}>
{t('Broadcast')}
</FieldLabel>
<Controller
control={control}
name='broadcast'
render={({ field: { onChange, value, ref } }): ReactElement => (
<ToggleSwitch
aria-describedby={`${broadcastId}-hint`}
aria-labelledby={`${broadcastId}-label`}
id={broadcastId}
ref={ref}
checked={value}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import { mockAppRoot } from '@rocket.chat/mock-providers';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import type CreateChannelModal2Component from './CreateChannelModal';
import type CreateChannelModalComponent from '../../../sidebar/header/CreateChannel';

// eslint-disable-next-line @typescript-eslint/naming-convention
export function testCreateChannelModal(CreateChannelModal: typeof CreateChannelModalComponent | typeof CreateChannelModal2Component) {
describe('CreateChannelModal', () => {
it('should render with encryption option disabled and set to off when E2E_Enable=false and E2E_Enabled_Default_PrivateRooms=false', async () => {
render(<CreateChannelModal onClose={() => 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(<CreateChannelModal onClose={() => 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(<CreateChannelModal onClose={() => 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(<CreateChannelModal onClose={() => 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(<CreateChannelModal onClose={() => 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(<CreateChannelModal onClose={() => 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(<CreateChannelModal onClose={() => 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(<CreateChannelModal onClose={() => 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();
});
});
}
Original file line number Diff line number Diff line change
@@ -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);
Original file line number Diff line number Diff line change
Expand Up @@ -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<string | undefined> => {
if (!name) {
Expand Down Expand Up @@ -175,10 +175,7 @@ const CreateChannelModal = ({ teamId = '', mainRoom, onClose, reload }: CreateCh
}
};

const e2eDisabled = useMemo<boolean>(
() => !isPrivate || broadcast || Boolean(!e2eEnabled) || Boolean(e2eEnabledForPrivateByDefault),
[e2eEnabled, e2eEnabledForPrivateByDefault, broadcast, isPrivate],
);
const e2eDisabled = useMemo<boolean>(() => !isPrivate || Boolean(!e2eEnabled) || federated, [e2eEnabled, federated, isPrivate]);

const createChannelFormId = useId();
const nameId = useId();
Expand Down Expand Up @@ -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`}
/>
Expand Down
Loading