chore: Purge and rename removed v1 files (#37912)
Some checks failed
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
Code scanning - action / CodeQL-Build (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (${{ matrix.type }}) (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (${{ matrix.type }}) (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release candidate cut / new-release (push) Has been cancelled

This commit is contained in:
Douglas Fabris 2025-12-20 17:52:23 -03:00 committed by GitHub
parent 62708dcd12
commit a587ab378c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
288 changed files with 738 additions and 8634 deletions

View File

@ -1,8 +0,0 @@
import { testCreateChannelModal } from './testCreateChannelModal';
import CreateChannelModalComponent from '../../../sidebar/header/CreateChannel';
jest.mock('../../../lib/utils/goToRoomById', () => ({
goToRoomById: jest.fn(),
}));
testCreateChannelModal(CreateChannelModalComponent);

View File

@ -1,269 +0,0 @@
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 { createFakeLicenseInfo } from '../../../../tests/mocks/data';
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', () => {
describe('Encryption', () => {
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();
});
});
describe('Federation', () => {
it('should render with federated option disabled when user lacks license module', async () => {
render(<CreateChannelModal onClose={() => null} />, {
wrapper: mockAppRoot().build(),
});
await userEvent.click(screen.getByText('Advanced_settings'));
const federated = screen.getByLabelText('Federation_Matrix_Federated');
expect(federated).toHaveAccessibleDescription('error-this-is-a-premium-feature');
expect(federated).toBeInTheDocument();
expect(federated).not.toBeChecked();
expect(federated).toBeDisabled();
});
it('should render with federated option disabled if the feature is disabled for workspaces', async () => {
render(<CreateChannelModal onClose={() => null} />, {
wrapper: mockAppRoot()
.withJohnDoe()
.withSetting('Federation_Matrix_enabled', false)
.withEndpoint(
'GET',
'/v1/licenses.info',
jest.fn().mockImplementation(() => ({
license: createFakeLicenseInfo({ activeModules: ['federation'] }),
})),
)
.build(),
});
await userEvent.click(screen.getByText('Advanced_settings'));
const federated = screen.getByLabelText('Federation_Matrix_Federated');
expect(federated).toBeInTheDocument();
expect(federated).not.toBeChecked();
expect(federated).toBeDisabled();
expect(federated).toHaveAccessibleDescription('Federation_Matrix_Federated_Description_disabled');
});
it('should render with federated option disabled when user lacks permission', async () => {
render(<CreateChannelModal onClose={() => null} />, {
wrapper: mockAppRoot()
.withJohnDoe()
.withSetting('Federation_Matrix_enabled', true)
.withEndpoint(
'GET',
'/v1/licenses.info',
jest.fn().mockImplementation(() => ({
license: createFakeLicenseInfo({ activeModules: ['federation'] }),
})),
)
.build(),
});
await userEvent.click(screen.getByText('Advanced_settings'));
const federated = screen.getByLabelText('Federation_Matrix_Federated');
expect(federated).toBeInTheDocument();
expect(federated).not.toBeChecked();
expect(federated).toBeDisabled();
expect(federated).toHaveAccessibleDescription('error-not-authorized-federation');
});
it('should render with federated option enabled when user has license module, permission and feature enabled', async () => {
render(<CreateChannelModal onClose={() => null} />, {
wrapper: mockAppRoot()
.withJohnDoe()
.withSetting('Federation_Matrix_enabled', true)
.withPermission('access-federation')
.withEndpoint(
'GET',
'/v1/licenses.info',
jest.fn().mockImplementation(() => ({
license: createFakeLicenseInfo({ activeModules: ['federation'] }),
})),
)
.build(),
});
await userEvent.click(screen.getByText('Advanced_settings'));
const federated = screen.getByLabelText('Federation_Matrix_Federated');
expect(federated).toBeInTheDocument();
expect(federated).not.toBeChecked();
expect(federated).not.toBeDisabled();
expect(federated).toHaveAccessibleDescription('Federation_Matrix_Federated_Description');
});
});
});
}

View File

@ -27,10 +27,10 @@ import type { ReactElement } from 'react';
import { useForm, Controller } from 'react-hook-form';
import { goToRoomById } from '../../lib/utils/goToRoomById';
import { useEncryptedRoomDescription } from '../../navbar/NavBarPagesGroup/actions/useEncryptedRoomDescription';
import RoomAutoComplete from '../RoomAutoComplete';
import UserAutoCompleteMultiple from '../UserAutoCompleteMultiple';
import DefaultParentRoomField from './DefaultParentRoomField';
import { useEncryptedRoomDescription } from '../../NavBarV2/NavBarPagesGroup/actions/useEncryptedRoomDescription';
type CreateDiscussionFormValues = {
name: string;

View File

@ -0,0 +1 @@
export { default } from './SidebarToggler';

View File

@ -1 +0,0 @@
export { default as SidebarTogglerV2 } from './SidebarToggler';

View File

@ -26,6 +26,7 @@ declare global {
mozAudioContext?: AudioContext;
/** @deprecated use `window.AudioContext` */
webkitAudioContext?: AudioContext;
opera?: string;
}
interface Navigator {
@ -57,6 +58,9 @@ declare global {
onSuccess?: (stream: MediaStream) => void,
onError?: (error: any) => void,
) => void;
userAgentData?: {
mobile: boolean;
};
}
interface RTCPeerConnection {

View File

@ -2,57 +2,49 @@ import { mockAppRoot } from '@rocket.chat/mock-providers';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import CreateTeamModal from './CreateTeamModal';
import CreateTeamModalOld from '../../../sidebar/header/CreateTeam';
import CreateChannelModal from './CreateChannelModal';
import { createFakeLicenseInfo } from '../../../../tests/mocks/data';
jest.mock('../../../lib/utils/goToRoomById', () => ({
goToRoomById: jest.fn(),
}));
type CreateTeamModalComponentType = typeof CreateTeamModal | typeof CreateTeamModalOld;
// eslint-disable-next-line @typescript-eslint/naming-convention
describe.each([
['CreateTeamModal', CreateTeamModalOld],
['CreateTeamModal in NavbarV2', CreateTeamModal],
] as const)(
'%s',
// eslint-disable-next-line @typescript-eslint/naming-convention
(_name: string, CreateTeamModalComponent: CreateTeamModalComponentType) => {
describe('CreateChannelModal', () => {
describe('Encryption', () => {
it('should render with encryption option disabled and set to off when E2E_Enable=false and E2E_Enabled_Default_PrivateRooms=false', async () => {
render(<CreateTeamModalComponent onClose={() => null} />, {
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('Teams_New_Encrypted_Label') as HTMLInputElement;
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(<CreateTeamModalComponent onClose={() => null} />, {
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('Teams_New_Encrypted_Label') as HTMLInputElement;
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(<CreateTeamModalComponent onClose={() => null} />, {
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('Teams_New_Encrypted_Label') as HTMLInputElement;
const encrypted = screen.getByLabelText('Encrypted') as HTMLInputElement;
expect(encrypted).toBeInTheDocument();
expect(encrypted).not.toBeChecked();
@ -60,26 +52,26 @@ describe.each([
});
it('should render with encryption option enabled and set to on when E2E_Enable=true and E2E_Enabled_Default_PrivateRooms=True', async () => {
render(<CreateTeamModalComponent onClose={() => null} />, {
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('Teams_New_Encrypted_Label') as HTMLInputElement;
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(<CreateTeamModalComponent onClose={() => null} />, {
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('Teams_New_Encrypted_Label') as HTMLInputElement;
const priv = screen.getByLabelText('Teams_New_Private_Label') as HTMLInputElement;
const encrypted = screen.getByLabelText('Encrypted') as HTMLInputElement;
const priv = screen.getByLabelText('Private') as HTMLInputElement;
// initial: private=true, encrypted ON and enabled
expect(priv).toBeChecked();
@ -94,14 +86,14 @@ describe.each([
});
it('when Private goes OFF → ON: keeps Encrypted OFF but re-enables it (E2E_Enable=true, E2E_Enabled_Default_PrivateRooms=true)', async () => {
render(<CreateTeamModalComponent onClose={() => null} />, {
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('Teams_New_Encrypted_Label') as HTMLInputElement;
const priv = screen.getByLabelText('Teams_New_Private_Label') as HTMLInputElement;
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);
@ -116,16 +108,16 @@ describe.each([
expect(encrypted).toBeEnabled();
});
it('private team: toggling Broadcast on/off does not change or disable Encrypted', async () => {
render(<CreateTeamModalComponent onClose={() => null} />, {
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('Teams_New_Encrypted_Label') as HTMLInputElement;
const broadcast = screen.getByLabelText('Teams_New_Broadcast_Label') as HTMLInputElement;
const priv = screen.getByLabelText('Teams_New_Private_Label') as HTMLInputElement;
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();
@ -155,16 +147,16 @@ describe.each([
expect(encrypted).toBeEnabled();
});
it('non-private team: Encrypted remains OFF and disabled regardless of Broadcast state', async () => {
render(<CreateTeamModalComponent onClose={() => null} />, {
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('Teams_New_Encrypted_Label') as HTMLInputElement;
const broadcast = screen.getByLabelText('Teams_New_Broadcast_Label') as HTMLInputElement;
const priv = screen.getByLabelText('Teams_New_Private_Label') as HTMLInputElement;
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);
@ -186,73 +178,92 @@ describe.each([
expect(encrypted).not.toBeChecked();
expect(encrypted).toBeDisabled();
});
});
it('should disable and turn on ReadOnly toggle when Broadcast is ON and no set-readonly permission', async () => {
render(<CreateTeamModalComponent onClose={() => null} />, {
describe('Federation', () => {
it('should render with federated option disabled when user lacks license module', async () => {
render(<CreateChannelModal onClose={() => null} />, {
wrapper: mockAppRoot().build(),
});
await userEvent.click(screen.getByText('Advanced_settings'));
const broadcast = screen.getByLabelText('Teams_New_Broadcast_Label') as HTMLInputElement;
const readOnly = screen.getByLabelText('Teams_New_Read_only_Label') as HTMLInputElement;
expect(readOnly).not.toBeChecked();
// Broadcast: OFF -> ON (ReadOnly stays ON + disabled)
await userEvent.click(broadcast);
expect(broadcast).toBeChecked();
expect(readOnly).toBeChecked();
expect(readOnly).toBeDisabled();
const federated = screen.getByLabelText('Federation_Matrix_Federated');
expect(federated).toHaveAccessibleDescription('error-this-is-a-premium-feature');
expect(federated).toBeInTheDocument();
expect(federated).not.toBeChecked();
expect(federated).toBeDisabled();
});
it('should disable and turn on ReadOnly toggle when Broadcast is ON with set-readonly permission', async () => {
render(<CreateTeamModalComponent onClose={() => null} />, {
wrapper: mockAppRoot().withPermission('set-readonly').build(),
it('should render with federated option disabled if the feature is disabled for workspaces', async () => {
render(<CreateChannelModal onClose={() => null} />, {
wrapper: mockAppRoot()
.withJohnDoe()
.withSetting('Federation_Matrix_enabled', false)
.withEndpoint(
'GET',
'/v1/licenses.info',
jest.fn().mockImplementation(() => ({
license: createFakeLicenseInfo({ activeModules: ['federation'] }),
})),
)
.build(),
});
await userEvent.click(screen.getByText('Advanced_settings'));
const federated = screen.getByLabelText('Federation_Matrix_Federated');
const broadcast = screen.getByLabelText('Teams_New_Broadcast_Label') as HTMLInputElement;
const readOnly = screen.getByLabelText('Teams_New_Read_only_Label') as HTMLInputElement;
expect(readOnly).not.toBeChecked();
// Broadcast: OFF -> ON (ReadOnly stays ON + disabled)
await userEvent.click(broadcast);
expect(broadcast).toBeChecked();
expect(readOnly).toBeChecked();
expect(readOnly).toBeDisabled();
expect(federated).toBeInTheDocument();
expect(federated).not.toBeChecked();
expect(federated).toBeDisabled();
expect(federated).toHaveAccessibleDescription('Federation_Matrix_Federated_Description_disabled');
});
it('should disable and turn off ReadOnly toggle when Broadcast is OFF with no set-readonly permission', async () => {
render(<CreateTeamModalComponent onClose={() => null} />, {
wrapper: mockAppRoot().build(),
it('should render with federated option disabled when user lacks permission', async () => {
render(<CreateChannelModal onClose={() => null} />, {
wrapper: mockAppRoot()
.withJohnDoe()
.withSetting('Federation_Matrix_enabled', true)
.withEndpoint(
'GET',
'/v1/licenses.info',
jest.fn().mockImplementation(() => ({
license: createFakeLicenseInfo({ activeModules: ['federation'] }),
})),
)
.build(),
});
await userEvent.click(screen.getByText('Advanced_settings'));
const federated = screen.getByLabelText('Federation_Matrix_Federated');
const broadcast = screen.getByLabelText('Teams_New_Broadcast_Label') as HTMLInputElement;
const readOnly = screen.getByLabelText('Teams_New_Read_only_Label') as HTMLInputElement;
expect(broadcast).not.toBeChecked();
expect(readOnly).not.toBeChecked();
expect(readOnly).toBeDisabled();
expect(federated).toBeInTheDocument();
expect(federated).not.toBeChecked();
expect(federated).toBeDisabled();
expect(federated).toHaveAccessibleDescription('error-not-authorized-federation');
});
it('should enable ReadOnly toggle when Broadcast is OFF with set-readonly permission', async () => {
render(<CreateTeamModalComponent onClose={() => null} />, {
wrapper: mockAppRoot().withPermission('set-readonly').build(),
it('should render with federated option enabled when user has license module, permission and feature enabled', async () => {
render(<CreateChannelModal onClose={() => null} />, {
wrapper: mockAppRoot()
.withJohnDoe()
.withSetting('Federation_Matrix_enabled', true)
.withPermission('access-federation')
.withEndpoint(
'GET',
'/v1/licenses.info',
jest.fn().mockImplementation(() => ({
license: createFakeLicenseInfo({ activeModules: ['federation'] }),
})),
)
.build(),
});
await userEvent.click(screen.getByText('Advanced_settings'));
const broadcast = screen.getByLabelText('Teams_New_Broadcast_Label') as HTMLInputElement;
const readOnly = screen.getByLabelText('Teams_New_Read_only_Label') as HTMLInputElement;
expect(broadcast).not.toBeChecked();
expect(readOnly).not.toBeChecked();
expect(readOnly).toBeEnabled();
const federated = screen.getByLabelText('Federation_Matrix_Federated');
expect(federated).toBeInTheDocument();
expect(federated).not.toBeChecked();
expect(federated).not.toBeDisabled();
expect(federated).toHaveAccessibleDescription('Federation_Matrix_Federated_Description');
});
},
);
});
});

View File

@ -1,3 +1,4 @@
import type { IRoom } from '@rocket.chat/core-typings';
import {
Box,
Modal,
@ -42,6 +43,7 @@ import { goToRoomById } from '../../../lib/utils/goToRoomById';
type CreateChannelModalProps = {
teamId?: string;
mainRoom?: IRoom;
onClose: () => void;
reload?: () => void;
};
@ -75,7 +77,7 @@ const getFederationHintKey = (federationModule: boolean, featureToggle: boolean,
const hasExternalMembers = (members: string[]): boolean => members.some((member) => member.startsWith('@'));
const CreateChannelModal = ({ teamId = '', onClose, reload }: CreateChannelModalProps) => {
const CreateChannelModal = ({ teamId = '', mainRoom, onClose, reload }: CreateChannelModalProps) => {
const t = useTranslation();
const canSetReadOnly = usePermissionWithScopedRoles('set-readonly', ['owner']);
const e2eEnabled = useSetting('E2E_Enable');
@ -99,7 +101,7 @@ const CreateChannelModal = ({ teamId = '', onClose, reload }: CreateChannelModal
const dispatchToastMessage = useToastMessageDispatch();
const canOnlyCreateOneType = useCreateChannelTypePermission();
const canOnlyCreateOneType = useCreateChannelTypePermission(mainRoom?._id);
const {
register,

View File

@ -0,0 +1,247 @@
import { mockAppRoot } from '@rocket.chat/mock-providers';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import CreateTeamModal from './CreateTeamModal';
jest.mock('../../../lib/utils/goToRoomById', () => ({
goToRoomById: jest.fn(),
}));
describe('CreateTeamModal', () => {
it('should render with encryption option disabled and set to off when E2E_Enable=false and E2E_Enabled_Default_PrivateRooms=false', async () => {
render(<CreateTeamModal 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('Teams_New_Encrypted_Label') 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(<CreateTeamModal 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('Teams_New_Encrypted_Label') 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(<CreateTeamModal 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('Teams_New_Encrypted_Label') 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(<CreateTeamModal 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('Teams_New_Encrypted_Label') 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(<CreateTeamModal 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('Teams_New_Encrypted_Label') as HTMLInputElement;
const priv = screen.getByLabelText('Teams_New_Private_Label') 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(<CreateTeamModal 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('Teams_New_Encrypted_Label') as HTMLInputElement;
const priv = screen.getByLabelText('Teams_New_Private_Label') 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 team: toggling Broadcast on/off does not change or disable Encrypted', async () => {
render(<CreateTeamModal 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('Teams_New_Encrypted_Label') as HTMLInputElement;
const broadcast = screen.getByLabelText('Teams_New_Broadcast_Label') as HTMLInputElement;
const priv = screen.getByLabelText('Teams_New_Private_Label') 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 team: Encrypted remains OFF and disabled regardless of Broadcast state', async () => {
render(<CreateTeamModal 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('Teams_New_Encrypted_Label') as HTMLInputElement;
const broadcast = screen.getByLabelText('Teams_New_Broadcast_Label') as HTMLInputElement;
const priv = screen.getByLabelText('Teams_New_Private_Label') 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();
});
it('should disable and turn on ReadOnly toggle when Broadcast is ON and no set-readonly permission', async () => {
render(<CreateTeamModal onClose={() => null} />, {
wrapper: mockAppRoot().build(),
});
await userEvent.click(screen.getByText('Advanced_settings'));
const broadcast = screen.getByLabelText('Teams_New_Broadcast_Label') as HTMLInputElement;
const readOnly = screen.getByLabelText('Teams_New_Read_only_Label') as HTMLInputElement;
expect(readOnly).not.toBeChecked();
// Broadcast: OFF -> ON (ReadOnly stays ON + disabled)
await userEvent.click(broadcast);
expect(broadcast).toBeChecked();
expect(readOnly).toBeChecked();
expect(readOnly).toBeDisabled();
});
it('should disable and turn on ReadOnly toggle when Broadcast is ON with set-readonly permission', async () => {
render(<CreateTeamModal onClose={() => null} />, {
wrapper: mockAppRoot().withPermission('set-readonly').build(),
});
await userEvent.click(screen.getByText('Advanced_settings'));
const broadcast = screen.getByLabelText('Teams_New_Broadcast_Label') as HTMLInputElement;
const readOnly = screen.getByLabelText('Teams_New_Read_only_Label') as HTMLInputElement;
expect(readOnly).not.toBeChecked();
// Broadcast: OFF -> ON (ReadOnly stays ON + disabled)
await userEvent.click(broadcast);
expect(broadcast).toBeChecked();
expect(readOnly).toBeChecked();
expect(readOnly).toBeDisabled();
});
it('should disable and turn off ReadOnly toggle when Broadcast is OFF with no set-readonly permission', async () => {
render(<CreateTeamModal onClose={() => null} />, {
wrapper: mockAppRoot().build(),
});
await userEvent.click(screen.getByText('Advanced_settings'));
const broadcast = screen.getByLabelText('Teams_New_Broadcast_Label') as HTMLInputElement;
const readOnly = screen.getByLabelText('Teams_New_Read_only_Label') as HTMLInputElement;
expect(broadcast).not.toBeChecked();
expect(readOnly).not.toBeChecked();
expect(readOnly).toBeDisabled();
});
it('should enable ReadOnly toggle when Broadcast is OFF with set-readonly permission', async () => {
render(<CreateTeamModal onClose={() => null} />, {
wrapper: mockAppRoot().withPermission('set-readonly').build(),
});
await userEvent.click(screen.getByText('Advanced_settings'));
const broadcast = screen.getByLabelText('Teams_New_Broadcast_Label') as HTMLInputElement;
const readOnly = screen.getByLabelText('Teams_New_Read_only_Label') as HTMLInputElement;
expect(broadcast).not.toBeChecked();
expect(readOnly).not.toBeChecked();
expect(readOnly).toBeEnabled();
});
});

View File

@ -2,23 +2,17 @@ import { mockAppRoot } from '@rocket.chat/mock-providers';
import { renderHook } from '@testing-library/react';
import { useEncryptedRoomDescription } from './useEncryptedRoomDescription';
import { useEncryptedRoomDescription as useEncryptedRoomDescriptionOld } from '../../../sidebar/header/hooks/useEncryptedRoomDescription';
type Hook = typeof useEncryptedRoomDescription | typeof useEncryptedRoomDescriptionOld;
const wrapper = mockAppRoot();
describe.each([
['useEncryptedRoomDescription in NavBarV2', useEncryptedRoomDescription],
['useEncryptedRoomDescription', useEncryptedRoomDescriptionOld],
] as const)('%s', (_name, useEncryptedRoomDescriptionHook: Hook) => {
describe('useEncryptedRoomDescription', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe.each(['channel', 'team'] as const)('roomType=%s', (roomType) => {
it('returns "Not_available_for_this_workspace" when E2E is disabled', () => {
const { result } = renderHook(() => useEncryptedRoomDescriptionHook(roomType), {
const { result } = renderHook(() => useEncryptedRoomDescription(roomType), {
wrapper: wrapper.withSetting('E2E_Enable', false).build(),
});
const describe = result.current;
@ -27,7 +21,7 @@ describe.each([
});
it('returns "Encrypted_not_available" when room is not private and E2E is enabled', () => {
const { result } = renderHook(() => useEncryptedRoomDescriptionHook(roomType), {
const { result } = renderHook(() => useEncryptedRoomDescription(roomType), {
wrapper: wrapper.withSetting('E2E_Enable', true).build(),
});
const describe = result.current;
@ -36,7 +30,7 @@ describe.each([
});
it('returns "Encrypted_messages" when private and encrypted are true and E2E is enabled', () => {
const { result } = renderHook(() => useEncryptedRoomDescriptionHook(roomType), {
const { result } = renderHook(() => useEncryptedRoomDescription(roomType), {
wrapper: wrapper.withSetting('E2E_Enable', true).build(),
});
const describe = result.current;
@ -45,7 +39,7 @@ describe.each([
});
it('returns "Encrypted_messages_false" when private and encrypted are false and E2E is enabled', () => {
const { result } = renderHook(() => useEncryptedRoomDescriptionHook(roomType), {
const { result } = renderHook(() => useEncryptedRoomDescription(roomType), {
wrapper: wrapper.withSetting('E2E_Enable', true).build(),
});
const describe = result.current;

View File

@ -2,7 +2,7 @@ import type { GenericMenuItemProps } from '@rocket.chat/ui-client';
import { useTranslation } from 'react-i18next';
import { useCreateRoomModal } from './useCreateRoomModal';
import MatrixFederationSearch from '../../../sidebarv2/header/MatrixFederationSearch';
import MatrixFederationSearch from '../../../sidebar/header/MatrixFederationSearch';
export const useMatrixFederationItems = ({
isMatrixEnabled,

View File

@ -2,7 +2,7 @@ import { NavBarGroup, NavBarSection } from '@rocket.chat/fuselage';
import { useLayout } from '@rocket.chat/ui-contexts';
import NavBarPagesGroup from './NavBarPagesGroup';
import { SidebarTogglerV2 } from '../components/SidebarTogglerV2';
import SidebarToggler from '../components/SidebarToggler';
const NavBarPagesSection = () => {
const { sidebar } = useLayout();
@ -12,7 +12,7 @@ const NavBarPagesSection = () => {
{sidebar.shouldToggle && (
<>
<NavBarGroup>
<SidebarTogglerV2 />
<SidebarToggler />
</NavBarGroup>
</>
)}

View File

@ -6,8 +6,8 @@ import { useTranslation } from 'react-i18next';
import NavBarSearchItem from './NavBarSearchItem';
import { RoomIcon } from '../../components/RoomIcon';
import { roomCoordinator } from '../../lib/rooms/roomCoordinator';
import SidebarItemBadges from '../../sidebarv2/badges/SidebarItemBadges';
import { useUnreadDisplay } from '../../sidebarv2/hooks/useUnreadDisplay';
import SidebarItemBadges from '../../sidebar/badges/SidebarItemBadges';
import { useUnreadDisplay } from '../../sidebar/hooks/useUnreadDisplay';
type NavBarSearchItemWithDataProps = {
room: SubscriptionWithRoom;

View File

@ -1,52 +1,41 @@
import { IconButton, Sidebar } from '@rocket.chat/fuselage';
import type { Keys as IconName } from '@rocket.chat/icons';
import type { ReactElement } from 'react';
import { IconButton, SidebarV2Item, SidebarV2ItemAvatarWrapper, SidebarV2ItemMenu, SidebarV2ItemTitle } from '@rocket.chat/fuselage';
import type { HTMLAttributes, ReactNode } from 'react';
import { memo, useState } from 'react';
type CondensedProps = {
title: ReactElement | string;
titleIcon?: ReactElement;
avatar: ReactElement | boolean;
icon?: IconName;
actions?: ReactElement;
title: ReactNode;
titleIcon?: ReactNode;
avatar: ReactNode;
icon?: ReactNode;
actions?: ReactNode;
href?: string;
unread?: boolean;
menu?: () => ReactElement;
menu?: () => ReactNode;
menuOptions?: any;
selected?: boolean;
badges?: ReactElement;
badges?: ReactNode;
clickable?: boolean;
};
} & Omit<HTMLAttributes<HTMLAnchorElement>, 'is'>;
const Condensed = ({ icon, title = '', avatar, actions, href, unread, menu, badges, ...props }: CondensedProps) => {
const Condensed = ({ icon, title, avatar, actions, unread, menu, badges, ...props }: CondensedProps) => {
const [menuVisibility, setMenuVisibility] = useState(!!window.DISABLE_ANIMATION);
const handleFocus = () => setMenuVisibility(true);
const handlePointerEnter = () => setMenuVisibility(true);
return (
<Sidebar.Item {...props} {...({ href } as any)} clickable={!!href} onFocus={handleFocus} onPointerEnter={handlePointerEnter}>
{avatar && <Sidebar.Item.Avatar>{avatar}</Sidebar.Item.Avatar>}
<Sidebar.Item.Content>
<Sidebar.Item.Wrapper>
{icon}
<Sidebar.Item.Title data-qa='sidebar-item-title' className={(unread && 'rcx-sidebar-item--highlighted') as string}>
{title}
</Sidebar.Item.Title>
</Sidebar.Item.Wrapper>
{badges && <Sidebar.Item.Badge>{badges}</Sidebar.Item.Badge>}
{menu && (
<Sidebar.Item.Menu>
{menuVisibility ? menu() : <IconButton tabIndex={-1} aria-hidden mini rcx-sidebar-item__menu icon='kebab' />}
</Sidebar.Item.Menu>
)}
</Sidebar.Item.Content>
{actions && (
<Sidebar.Item.Container>
<Sidebar.Item.Actions>{actions}</Sidebar.Item.Actions>
</Sidebar.Item.Container>
<SidebarV2Item {...props} onFocus={handleFocus} onPointerEnter={handlePointerEnter}>
{avatar && <SidebarV2ItemAvatarWrapper>{avatar}</SidebarV2ItemAvatarWrapper>}
{icon}
<SidebarV2ItemTitle unread={unread}>{title}</SidebarV2ItemTitle>
{badges}
{actions}
{menu && (
<SidebarV2ItemMenu>
{menuVisibility ? menu() : <IconButton tabIndex={-1} aria-hidden mini rcx-sidebar-v2-item__menu icon='kebab' />}
</SidebarV2ItemMenu>
)}
</Sidebar.Item>
</SidebarV2Item>
);
};

View File

@ -20,14 +20,7 @@ export default {
const Template: StoryFn<typeof Extended> = (args) => (
<Extended
{...args}
title={
<Box display='flex' flexDirection='row' w='full' alignItems='center'>
<Box flexGrow='1' withTruncatedText>
John Doe
</Box>
<Box fontScale='micro'>15:38</Box>
</Box>
}
title='John Doe'
subtitle={
<Box display='flex' flexDirection='row' w='full' alignItems='center'>
<Box flexGrow='1' withTruncatedText>

View File

@ -1,13 +1,22 @@
import { Sidebar, IconButton } from '@rocket.chat/fuselage';
import type { Keys as IconName } from '@rocket.chat/icons';
import type { ReactNode } from 'react';
import {
SidebarV2Item,
SidebarV2ItemAvatarWrapper,
SidebarV2ItemCol,
SidebarV2ItemRow,
SidebarV2ItemTitle,
SidebarV2ItemTimestamp,
SidebarV2ItemContent,
SidebarV2ItemMenu,
IconButton,
} from '@rocket.chat/fuselage';
import type { HTMLAttributes, ReactNode } from 'react';
import { memo, useState } from 'react';
import { useShortTimeAgo } from '../../hooks/useTimeAgo';
type ExtendedProps = {
icon?: IconName;
title?: ReactNode;
icon?: ReactNode;
title: ReactNode;
avatar?: ReactNode;
actions?: ReactNode;
href?: string;
@ -20,11 +29,11 @@ type ExtendedProps = {
menuOptions?: any;
titleIcon?: ReactNode;
threadUnread?: boolean;
};
} & Omit<HTMLAttributes<HTMLElement>, 'is'>;
const Extended = ({
icon,
title = '',
title,
avatar,
actions,
href,
@ -46,44 +55,26 @@ const Extended = ({
const handlePointerEnter = () => setMenuVisibility(true);
return (
<Sidebar.Item
selected={selected}
highlighted={unread}
{...props}
{...({ href } as any)}
clickable={!!href}
onFocus={handleFocus}
onPointerEnter={handlePointerEnter}
>
{avatar && <Sidebar.Item.Avatar>{avatar}</Sidebar.Item.Avatar>}
<Sidebar.Item.Content>
<Sidebar.Item.Content>
<Sidebar.Item.Wrapper>
{icon}
<Sidebar.Item.Title data-qa='sidebar-item-title' className={(unread && 'rcx-sidebar-item--highlighted') as string}>
{title}
</Sidebar.Item.Title>
{time && <Sidebar.Item.Time>{formatDate(time)}</Sidebar.Item.Time>}
</Sidebar.Item.Wrapper>
</Sidebar.Item.Content>
<Sidebar.Item.Content>
<Sidebar.Item.Wrapper>
<Sidebar.Item.Subtitle className={(unread && 'rcx-sidebar-item--highlighted') as string}>{subtitle}</Sidebar.Item.Subtitle>
<Sidebar.Item.Badge>{badges}</Sidebar.Item.Badge>
{menu && (
<Sidebar.Item.Menu>
{menuVisibility ? menu() : <IconButton tabIndex={-1} aria-hidden mini rcx-sidebar-item__menu icon='kebab' />}
</Sidebar.Item.Menu>
)}
</Sidebar.Item.Wrapper>
</Sidebar.Item.Content>
</Sidebar.Item.Content>
{actions && (
<Sidebar.Item.Container>
<Sidebar.Item.Actions>{actions}</Sidebar.Item.Actions>
</Sidebar.Item.Container>
)}
</Sidebar.Item>
<SidebarV2Item href={href} selected={selected} {...props} onFocus={handleFocus} onPointerEnter={handlePointerEnter}>
{avatar && <SidebarV2ItemAvatarWrapper>{avatar}</SidebarV2ItemAvatarWrapper>}
<SidebarV2ItemCol>
<SidebarV2ItemRow>
{icon}
<SidebarV2ItemTitle unread={unread}>{title}</SidebarV2ItemTitle>
{time && <SidebarV2ItemTimestamp>{formatDate(time)}</SidebarV2ItemTimestamp>}
</SidebarV2ItemRow>
<SidebarV2ItemRow>
<SidebarV2ItemContent unread={unread}>{subtitle}</SidebarV2ItemContent>
{badges}
{actions}
{menu && (
<SidebarV2ItemMenu>
{menuVisibility ? menu() : <IconButton tabIndex={-1} aria-hidden mini rcx-sidebar-v2-item__menu icon='kebab' />}
</SidebarV2ItemMenu>
)}
</SidebarV2ItemRow>
</SidebarV2ItemCol>
</SidebarV2Item>
);
};

View File

@ -1,12 +1,12 @@
import { Sidebar, IconButton } from '@rocket.chat/fuselage';
import type { ReactNode } from 'react';
import { IconButton, SidebarV2Item, SidebarV2ItemAvatarWrapper, SidebarV2ItemMenu, SidebarV2ItemTitle } from '@rocket.chat/fuselage';
import type { HTMLAttributes, ReactNode } from 'react';
import { memo, useState } from 'react';
type MediumProps = {
title: ReactNode;
titleIcon?: ReactNode;
avatar: ReactNode;
icon?: string;
icon?: ReactNode;
actions?: ReactNode;
href?: string;
unread?: boolean;
@ -14,37 +14,27 @@ type MediumProps = {
badges?: ReactNode;
selected?: boolean;
menuOptions?: any;
};
} & Omit<HTMLAttributes<HTMLElement>, 'is'>;
const Medium = ({ icon, title = '', avatar, actions, href, badges, unread, menu, ...props }: MediumProps) => {
const Medium = ({ icon, title, avatar, actions, badges, unread, menu, ...props }: MediumProps) => {
const [menuVisibility, setMenuVisibility] = useState(!!window.DISABLE_ANIMATION);
const handleFocus = () => setMenuVisibility(true);
const handlePointerEnter = () => setMenuVisibility(true);
return (
<Sidebar.Item {...props} href={href} clickable={!!href} onFocus={handleFocus} onPointerEnter={handlePointerEnter}>
{avatar && <Sidebar.Item.Avatar>{avatar}</Sidebar.Item.Avatar>}
<Sidebar.Item.Content>
<Sidebar.Item.Wrapper>
{icon}
<Sidebar.Item.Title data-qa='sidebar-item-title' className={unread ? 'rcx-sidebar-item--highlighted' : undefined}>
{title}
</Sidebar.Item.Title>
</Sidebar.Item.Wrapper>
{badges && <Sidebar.Item.Badge>{badges}</Sidebar.Item.Badge>}
{menu && (
<Sidebar.Item.Menu>
{menuVisibility ? menu() : <IconButton tabIndex={-1} aria-hidden mini rcx-sidebar-item__menu icon='kebab' />}
</Sidebar.Item.Menu>
)}
</Sidebar.Item.Content>
{actions && (
<Sidebar.Item.Container>
<Sidebar.Item.Actions>{actions}</Sidebar.Item.Actions>
</Sidebar.Item.Container>
<SidebarV2Item {...props} onFocus={handleFocus} onPointerEnter={handlePointerEnter}>
<SidebarV2ItemAvatarWrapper>{avatar}</SidebarV2ItemAvatarWrapper>
{icon}
<SidebarV2ItemTitle unread={unread}>{title}</SidebarV2ItemTitle>
{badges}
{actions}
{menu && (
<SidebarV2ItemMenu>
{menuVisibility ? menu() : <IconButton tabIndex={-1} aria-hidden mini rcx-sidebar-v2-item__menu icon='kebab' />}
</SidebarV2ItemMenu>
)}
</Sidebar.Item>
</SidebarV2Item>
);
};

View File

@ -1,33 +1,32 @@
import { css } from '@rocket.chat/css-in-js';
import { Box } from '@rocket.chat/fuselage';
import { useResizeObserver } from '@rocket.chat/fuselage-hooks';
import { VirtualizedScrollbars } from '@rocket.chat/ui-client';
import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts';
import { useUserPreference, useUserId } from '@rocket.chat/ui-contexts';
import type { ReactElement } from 'react';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Virtuoso } from 'react-virtuoso';
import { GroupedVirtuoso } from 'react-virtuoso';
import RoomListCollapser from './RoomListCollapser';
import RoomListRow from './RoomListRow';
import RoomListRowWrapper from './RoomListRowWrapper';
import RoomListWrapper from './RoomListWrapper';
import { useOpenedRoom } from '../../lib/RoomManager';
import { useAvatarTemplate } from '../hooks/useAvatarTemplate';
import { useCollapsedGroups } from '../hooks/useCollapsedGroups';
import { usePreventDefault } from '../hooks/usePreventDefault';
import { useRoomList } from '../hooks/useRoomList';
import { useShortcutOpenMenu } from '../hooks/useShortcutOpenMenu';
import { useTemplateByViewMode } from '../hooks/useTemplateByViewMode';
const computeItemKey = (index: number, room: SubscriptionWithRoom): SubscriptionWithRoom['_id'] | number => room._id || index;
const RoomList = (): ReactElement => {
const RoomList = () => {
const { t } = useTranslation();
const isAnonymous = !useUserId();
const roomsList = useRoomList();
const { collapsedGroups, handleClick, handleKeyDown } = useCollapsedGroups();
const { groupsCount, groupsList, roomList, groupedUnreadInfo } = useRoomList({ collapsedGroups });
const avatarTemplate = useAvatarTemplate();
const sideBarItemTemplate = useTemplateByViewMode();
const { ref } = useResizeObserver({ debounceDelay: 100 });
const { ref } = useResizeObserver<HTMLElement>({ debounceDelay: 100 });
const openedRoom = useOpenedRoom() ?? '';
const sidebarViewMode = useUserPreference<'extended' | 'medium' | 'condensed'>('sidebarViewMode') || 'extended';
@ -36,7 +35,7 @@ const RoomList = (): ReactElement => {
() => ({
extended,
t,
SideBarItemTemplate: sideBarItemTemplate,
SidebarItemTemplate: sideBarItemTemplate,
AvatarTemplate: avatarTemplate,
openedRoom,
sidebarViewMode,
@ -48,92 +47,26 @@ const RoomList = (): ReactElement => {
usePreventDefault(ref);
useShortcutOpenMenu(ref);
const roomsListStyle = css`
position: relative;
display: flex;
overflow-x: hidden;
overflow-y: hidden;
flex: 1 1 auto;
height: 100%;
&--embedded {
margin-top: 2rem;
}
&__list:not(:last-child) {
margin-bottom: 22px;
}
&__type {
display: flex;
flex-direction: row;
padding: 0 var(--sidebar-default-padding) 1rem var(--sidebar-default-padding);
color: var(--rooms-list-title-color);
font-size: var(--rooms-list-title-text-size);
align-items: center;
justify-content: space-between;
&-text--livechat {
flex: 1;
}
}
&__empty-room {
padding: 0 var(--sidebar-default-padding);
color: var(--rooms-list-empty-text-color);
font-size: var(--rooms-list-empty-text-size);
}
&__toolbar-search {
position: absolute;
z-index: 10;
left: 0;
overflow-y: scroll;
height: 100%;
background-color: var(--sidebar-background);
padding-block-start: 12px;
}
@media (max-width: 400px) {
padding: 0 calc(var(--sidebar-small-default-padding) - 4px);
&__type,
&__empty-room {
padding: 0 calc(var(--sidebar-small-default-padding) - 4px) 0.5rem calc(var(--sidebar-small-default-padding) - 4px);
}
}
`;
return (
<Box className={[roomsListStyle, 'sidebar--custom-colors'].filter(Boolean)}>
<Box h='full' w='full' ref={ref}>
<VirtualizedScrollbars>
<Virtuoso
totalCount={roomsList.length}
data={roomsList}
components={{
Item: RoomListRowWrapper,
List: RoomListWrapper,
}}
computeItemKey={computeItemKey}
itemContent={(_, data): ReactElement => <RoomListRow data={itemData} item={data} />}
/>
</VirtualizedScrollbars>
</Box>
<Box position='relative' overflow='hidden' height='full' ref={ref}>
<VirtualizedScrollbars>
<GroupedVirtuoso
groupCounts={groupsCount}
groupContent={(index) => (
<RoomListCollapser
collapsedGroups={collapsedGroups}
onClick={() => handleClick(groupsList[index])}
onKeyDown={(e) => handleKeyDown(e, groupsList[index])}
groupTitle={groupsList[index]}
unreadCount={groupedUnreadInfo[index]}
/>
)}
{...(roomList.length > 0 && {
itemContent: (index) => roomList[index] && <RoomListRow data={itemData} item={roomList[index]} />,
})}
components={{ Item: RoomListRowWrapper, List: RoomListWrapper }}
/>
</VirtualizedScrollbars>
</Box>
);
};

View File

@ -1,26 +1,27 @@
import { SidebarSection } from '@rocket.chat/fuselage';
import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts';
import { useVideoConfAcceptCall, useVideoConfRejectIncomingCall, useVideoConfIncomingCalls } from '@rocket.chat/ui-video-conf';
import type { TFunction } from 'i18next';
import type { ReactElement } from 'react';
import { memo, useMemo } from 'react';
import SideBarItemTemplateWithData from './SideBarItemTemplateWithData';
import SidebarItemTemplateWithData from './SidebarItemTemplateWithData';
import type { useAvatarTemplate } from '../hooks/useAvatarTemplate';
import type { useTemplateByViewMode } from '../hooks/useTemplateByViewMode';
type RoomListRowProps = {
extended: boolean;
t: TFunction;
SideBarItemTemplate: ReturnType<typeof useTemplateByViewMode>;
AvatarTemplate: ReturnType<typeof useAvatarTemplate>;
openedRoom: string;
sidebarViewMode: 'extended' | 'condensed' | 'medium';
isAnonymous: boolean;
data: {
extended: boolean;
t: TFunction;
SidebarItemTemplate: ReturnType<typeof useTemplateByViewMode>;
AvatarTemplate: ReturnType<typeof useAvatarTemplate>;
openedRoom: string;
sidebarViewMode: 'extended' | 'condensed' | 'medium';
isAnonymous: boolean;
};
item: SubscriptionWithRoom;
};
const RoomListRow = ({ data, item }: { data: RoomListRowProps; item: SubscriptionWithRoom }): ReactElement => {
const { extended, t, SideBarItemTemplate, AvatarTemplate, openedRoom, sidebarViewMode } = data;
const RoomListRow = ({ data, item }: RoomListRowProps) => {
const { extended, t, SidebarItemTemplate, AvatarTemplate, openedRoom, sidebarViewMode } = data;
const acceptCall = useVideoConfAcceptCall();
const rejectCall = useVideoConfRejectIncomingCall();
@ -36,22 +37,14 @@ const RoomListRow = ({ data, item }: { data: RoomListRowProps; item: Subscriptio
[acceptCall, rejectCall, currentCall],
);
if (typeof item === 'string') {
return (
<SidebarSection>
<SidebarSection.Title>{t(item)}</SidebarSection.Title>
</SidebarSection>
);
}
return (
<SideBarItemTemplateWithData
<SidebarItemTemplateWithData
sidebarViewMode={sidebarViewMode}
selected={item.rid === openedRoom}
t={t}
room={item}
extended={extended}
SideBarItemTemplate={SideBarItemTemplate}
SidebarItemTemplate={SidebarItemTemplate}
AvatarTemplate={AvatarTemplate}
videoConfActions={videoConfActions}
/>

View File

@ -1,8 +1,11 @@
import type { HTMLAttributes, Ref } from 'react';
import { SidebarV2ListItem } from '@rocket.chat/fuselage';
import type { ForwardedRef, HTMLAttributes } from 'react';
import { forwardRef } from 'react';
const RoomListRoomWrapper = forwardRef(function RoomListRoomWrapper(props: HTMLAttributes<HTMLDivElement>, ref: Ref<HTMLDivElement>) {
return <div role='listitem' ref={ref} {...props} />;
type RoomListRoomWrapperProps = HTMLAttributes<HTMLDivElement>;
const RoomListRoomWrapper = forwardRef(function RoomListRoomWrapper(props: RoomListRoomWrapperProps, ref: ForwardedRef<HTMLDivElement>) {
return <SidebarV2ListItem ref={ref} {...props} />;
});
export default RoomListRoomWrapper;

View File

@ -1,11 +1,13 @@
import { useMergedRefs } from '@rocket.chat/fuselage-hooks';
import type { HTMLAttributes, Ref } from 'react';
import type { ForwardedRef, HTMLAttributes } from 'react';
import { forwardRef } from 'react';
import { useTranslation } from 'react-i18next';
import { useSidebarListNavigation } from './useSidebarListNavigation';
const RoomListWrapper = forwardRef(function RoomListWrapper(props: HTMLAttributes<HTMLDivElement>, ref: Ref<HTMLDivElement>) {
type RoomListWrapperProps = HTMLAttributes<HTMLDivElement>;
const RoomListWrapper = forwardRef(function RoomListWrapper(props: RoomListWrapperProps, ref: ForwardedRef<HTMLDivElement>) {
const { t } = useTranslation();
const { sidebarListRef } = useSidebarListNavigation();
const mergedRefs = useMergedRefs(ref, sidebarListRef);

View File

@ -1,226 +0,0 @@
import type { IMessage } from '@rocket.chat/core-typings';
import { isDirectMessageRoom, isMultipleDirectMessageRoom, isOmnichannelRoom, isVideoConfMessage } from '@rocket.chat/core-typings';
import { Sidebar, SidebarItemAction, SidebarItemActions } from '@rocket.chat/fuselage';
import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts';
import { useLayout } from '@rocket.chat/ui-contexts';
import DOMPurify from 'dompurify';
import type { TFunction } from 'i18next';
import type { AllHTMLAttributes, ComponentType, ReactElement, ReactNode } from 'react';
import { memo, useMemo } from 'react';
import { normalizeSidebarMessage } from './normalizeSidebarMessage';
import { RoomIcon } from '../../components/RoomIcon';
import { roomCoordinator } from '../../lib/rooms/roomCoordinator';
import { isIOsDevice } from '../../lib/utils/isIOsDevice';
import { useOmnichannelPriorities } from '../../views/omnichannel/hooks/useOmnichannelPriorities';
import RoomMenu from '../RoomMenu';
import SidebarItemBadges from '../badges/SidebarItemBadges';
import type { useAvatarTemplate } from '../hooks/useAvatarTemplate';
import { useUnreadDisplay } from '../hooks/useUnreadDisplay';
const getMessage = (room: SubscriptionWithRoom, lastMessage: IMessage | undefined, t: TFunction): string | undefined => {
if (!lastMessage) {
return t('No_messages_yet');
}
if (isVideoConfMessage(lastMessage)) {
return t('Call_started');
}
if (!lastMessage.u) {
return normalizeSidebarMessage(lastMessage, t);
}
if (lastMessage.u?.username === room.u?.username) {
return `${t('You')}: ${normalizeSidebarMessage(lastMessage, t)}`;
}
if (isDirectMessageRoom(room) && !isMultipleDirectMessageRoom(room)) {
return normalizeSidebarMessage(lastMessage, t);
}
return `${lastMessage.u.name || lastMessage.u.username}: ${normalizeSidebarMessage(lastMessage, t)}`;
};
type RoomListRowProps = {
extended: boolean;
t: TFunction;
SideBarItemTemplate: ComponentType<
{
icon: ReactNode;
title: ReactNode;
avatar: ReactNode;
actions: unknown;
href: string;
time?: Date;
menu?: () => ReactNode;
menuOptions?: unknown;
subtitle?: ReactNode;
titleIcon?: string;
badges?: ReactNode;
threadUnread?: boolean;
unread?: boolean;
selected?: boolean;
is?: string;
} & AllHTMLAttributes<HTMLElement>
>;
AvatarTemplate: ReturnType<typeof useAvatarTemplate>;
openedRoom?: string;
// sidebarViewMode: 'extended';
isAnonymous?: boolean;
room: SubscriptionWithRoom;
id?: string;
/* @deprecated */
style?: AllHTMLAttributes<HTMLElement>['style'];
selected?: boolean;
sidebarViewMode?: unknown;
videoConfActions?: {
[action: string]: () => void;
};
};
function SideBarItemTemplateWithData({
room,
id,
selected,
style,
extended,
SideBarItemTemplate,
AvatarTemplate,
t,
isAnonymous,
videoConfActions,
}: RoomListRowProps): ReactElement {
const { sidebar } = useLayout();
const href = roomCoordinator.getRouteLink(room.t, room) || '';
const title = roomCoordinator.getRoomName(room.t, room) || '';
const { lastMessage, unread = 0, alert, rid, t: type, cl } = room;
const { unreadCount, unreadTitle, showUnread, highlightUnread: highlighted } = useUnreadDisplay(room);
const icon = (
// TODO: Remove icon='at'
<Sidebar.Item.Icon highlighted={highlighted} icon='at'>
<RoomIcon room={room} placement='sidebar' isIncomingCall={Boolean(videoConfActions)} />
</Sidebar.Item.Icon>
);
const actions = useMemo(
() =>
videoConfActions && (
<SidebarItemActions>
<SidebarItemAction onClick={videoConfActions.acceptCall} secondary success icon='phone' />
<SidebarItemAction onClick={videoConfActions.rejectCall} secondary danger icon='phone-off' />
</SidebarItemActions>
),
[videoConfActions],
);
const isQueued = isOmnichannelRoom(room) && room.status === 'queued';
const { enabled: isPriorityEnabled } = useOmnichannelPriorities();
const message = extended && getMessage(room, lastMessage, t);
const subtitle = message ? (
<span className='message-body--unstyled' dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(message) }} />
) : null;
return (
<SideBarItemTemplate
is='a'
id={id}
data-qa='sidebar-item'
data-unread={highlighted}
unread={highlighted}
selected={selected}
href={href}
onClick={(): void => {
!selected && sidebar.toggle();
}}
aria-label={showUnread ? t('__unreadTitle__from__roomTitle__', { unreadTitle, roomTitle: title }) : title}
title={title}
time={lastMessage?.ts}
subtitle={subtitle}
icon={icon}
style={style}
badges={<SidebarItemBadges room={room} roomTitle={title} />}
avatar={AvatarTemplate && <AvatarTemplate {...room} />}
actions={actions}
menu={
!isIOsDevice && !isAnonymous && (!isQueued || (isQueued && isPriorityEnabled))
? (): ReactElement => (
<RoomMenu
alert={alert}
threadUnread={unreadCount.threads > 0}
rid={rid}
unread={!!unread}
roomOpen={selected}
type={type}
cl={cl}
name={title}
hideDefaultOptions={isQueued}
/>
)
: undefined
}
/>
);
}
function safeDateNotEqualCheck(a: Date | string | undefined, b: Date | string | undefined): boolean {
if (!a || !b) {
return a !== b;
}
return new Date(a).toISOString() !== new Date(b).toISOString();
}
const keys: (keyof RoomListRowProps)[] = [
'id',
'style',
'extended',
'selected',
'SideBarItemTemplate',
'AvatarTemplate',
't',
'sidebarViewMode',
'videoConfActions',
];
// eslint-disable-next-line react/no-multi-comp
export default memo(SideBarItemTemplateWithData, (prevProps, nextProps) => {
if (keys.some((key) => prevProps[key] !== nextProps[key])) {
return false;
}
if (prevProps.room === nextProps.room) {
return true;
}
if (prevProps.room._id !== nextProps.room._id) {
return false;
}
if (prevProps.room._updatedAt?.toISOString() !== nextProps.room._updatedAt?.toISOString()) {
return false;
}
if (safeDateNotEqualCheck(prevProps.room.lastMessage?._updatedAt, nextProps.room.lastMessage?._updatedAt)) {
return false;
}
if (prevProps.room.alert !== nextProps.room.alert) {
return false;
}
if (isOmnichannelRoom(prevProps.room) && isOmnichannelRoom(nextProps.room) && prevProps.room?.v?.status !== nextProps.room?.v?.status) {
return false;
}
if (prevProps.room.teamMain !== nextProps.room.teamMain) {
return false;
}
if (
isOmnichannelRoom(prevProps.room) &&
isOmnichannelRoom(nextProps.room) &&
prevProps.room.priorityWeight !== nextProps.room.priorityWeight
) {
return false;
}
return true;
});

View File

@ -1,12 +1,13 @@
import { useCallback } from 'react';
import { useFocusManager } from 'react-aria';
const isListItem = (node: EventTarget) => (node as HTMLElement).classList.contains('rcx-sidebar-item');
const isListItemMenu = (node: EventTarget) => (node as HTMLElement).classList.contains('rcx-sidebar-item__menu');
const isListItem = (node: EventTarget) => (node as HTMLElement).classList.contains('rcx-sidebar-v2-item');
const isCollapseGroup = (node: EventTarget) => (node as HTMLElement).classList.contains('rcx-sidebar-v2-collapse-group__bar');
const isListItemMenu = (node: EventTarget) => (node as HTMLElement).classList.contains('rcx-sidebar-v2-item__menu');
/**
* Custom hook to provide the sidebar navigation by keyboard.
* @param ref - A ref to the message list DOM element.
* @returns ref - A ref to the message list DOM element.
*/
export const useSidebarListNavigation = () => {
const sidebarListFocusManager = useFocusManager();
@ -24,7 +25,7 @@ export const useSidebarListNavigation = () => {
return;
}
if (!isListItem(e.target)) {
if (!isListItem(e.target) && !isCollapseGroup(e.target)) {
return;
}
@ -34,26 +35,29 @@ export const useSidebarListNavigation = () => {
if (e.shiftKey) {
sidebarListFocusManager?.focusPrevious({
accept: (node) => !isListItem(node) && !isListItemMenu(node),
accept: (node) => !isListItem(node) && !isListItemMenu(node) && !isCollapseGroup(node),
});
} else if (isListItemMenu(e.target)) {
sidebarListFocusManager?.focusNext({
accept: (node) => !isListItem(node) && !isListItemMenu(node),
accept: (node) => !isListItem(node) && !isListItemMenu(node) && !isCollapseGroup(node),
});
} else {
sidebarListFocusManager?.focusNext({
accept: (node) => !isListItem(node),
accept: (node) => !isListItem(node) && !isCollapseGroup(node),
});
}
}
if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
e.preventDefault();
e.stopPropagation();
if (e.key === 'ArrowUp') {
sidebarListFocusManager?.focusPrevious({ accept: (node) => isListItem(node) });
sidebarListFocusManager?.focusPrevious({ accept: (node) => isListItem(node) || isCollapseGroup(node) });
}
if (e.key === 'ArrowDown') {
sidebarListFocusManager?.focusNext({ accept: (node) => isListItem(node) });
sidebarListFocusManager?.focusNext({ accept: (node) => isListItem(node) || isCollapseGroup(node) });
}
lastItemFocused = document.activeElement as HTMLElement;

Some files were not shown because too many files have changed in this diff Show More