Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
dc471cc
chore(storybook): expose DDPCommon stub on meteor mock
ggazzo May 18, 2026
c5e6df5
feat(ui): add HorizontalDivider wrapper around fuselage Divider
ggazzo May 18, 2026
9d719b3
feat(ui-voip): add inline mount mode to MediaCallWidget
ggazzo May 19, 2026
4f86d60
feat(sidebar): add SidebarRail component
ggazzo May 20, 2026
1c847d7
feat(layout): gate SidebarRail behind USE_SIDEBAR_RAIL flag
ggazzo May 20, 2026
ad38fd0
feat(layout): make USE_SIDEBAR_RAIL a runtime window flag
ggazzo Jun 3, 2026
2cb4ee6
chore(lint): disable naming-convention for Window augmentation
ggazzo Jun 3, 2026
a0fca61
test: align LayoutWithSidebar spec mocks with SidebarRail refactor
ggazzo Jun 3, 2026
232ca2c
feat(ui-voip): show dialpad during active SIP calls (DMV-16)
ggazzo Jun 8, 2026
3cc32bd
fix(sidebar): drive call dialer from the panel, not a route watcher
ggazzo Jun 8, 2026
97b01a5
fix(ui-voip): keep the DTMF dialpad on internal floating calls
ggazzo Jun 8, 2026
0933cc3
fix(ui-voip): add idempotent openDialer/closeDialer intents for the c…
ggazzo Jul 1, 2026
e332617
fix: Add missing sort button to SidebarRail
gabriellsh Jul 2, 2026
e7b402e
fix: Widget state left as "new" and "docked" when leaving call panel …
gabriellsh Jul 22, 2026
6626588
chore: Remove dangling references and old box props
gabriellsh Aug 5, 2026
e00a8e4
chore(ui-voip): Refactor docked widget to respect new sctructure
gabriellsh Aug 6, 2026
1fc66e6
chore: update SidebarRailCallPanel with new InlineMediaCallWidget
gabriellsh Aug 6, 2026
a4f1165
fix: Replace old `Box` styling props aliases
gabriellsh Aug 6, 2026
5b39cdd
feat: Proper feature preview for siderail
gabriellsh Aug 6, 2026
c701863
fix: Sidebar animation overlaying sideRail
gabriellsh Aug 6, 2026
a6f86da
chore: i18n
gabriellsh Aug 6, 2026
fb9db16
fix: Sidebar rail with same aria-label as Sidebar
gabriellsh Aug 6, 2026
e085431
fix: Call panel bg color not extending to the end due to Box wrapper …
gabriellsh Aug 6, 2026
dde1c0f
test: Add e2e tests for call panel
gabriellsh Aug 6, 2026
b59a1c8
test: fix failing tests
gabriellsh Aug 6, 2026
e1058eb
fix: Sidebar rail visible when in tablet view
gabriellsh Aug 7, 2026
607f717
fix: skip test on non-ee env
gabriellsh Aug 7, 2026
1094c73
test: fix unit tests
gabriellsh Aug 7, 2026
29899ed
test: remove .only
gabriellsh Aug 20, 2026
a7f9f03
test: fix Allow feature preview setting not turned on
gabriellsh Aug 21, 2026
58d78d4
Merge remote-tracking branch 'origin/develop' into feat/local-sidebar…
gabriellsh Aug 21, 2026
494fd36
test: fix snapshots
gabriellsh Aug 24, 2026
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
7 changes: 5 additions & 2 deletions apps/meteor/.storybook/mocks/meteor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ export const Meteor = {
users: {},
};

export const DDPCommon = {
parseDDP: () => undefined,
stringifyDDP: () => '',
};

export const Tracker = {
autorun: () => ({
stop: () => {},
Expand Down Expand Up @@ -94,5 +99,3 @@ export const Session = {
get: () => {},
set: () => {},
};

export const DDPCommon = {};
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Divider } from '@rocket.chat/fuselage';
import type { ComponentProps } from 'react';

type HorizontalDividerProps = Omit<ComponentProps<typeof Divider>, 'vertical'>;

const HorizontalDivider = (props: HorizontalDividerProps) => <Divider {...props} vertical={false} />;

export default HorizontalDivider;
1 change: 1 addition & 0 deletions apps/meteor/client/components/HorizontalDivider/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './HorizontalDivider';
64 changes: 64 additions & 0 deletions apps/meteor/client/sidebar/SidebarRail/SidebarRail.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { Box } from '@rocket.chat/fuselage';
import { mockAppRoot } from '@rocket.chat/mock-providers';
import { SessionContext } from '@rocket.chat/ui-contexts';
import type { SessionContextValue } from '@rocket.chat/ui-contexts';
import type { Meta, StoryObj } from '@storybook/react';
import type { ReactNode } from 'react';

import SidebarRail from './SidebarRail';

const sessionMock = (state: Record<string, unknown>): SessionContextValue => ({
query: (name) => [() => () => undefined, () => state[name]],
dispatch: () => undefined,
});

const baseRoot = () =>
mockAppRoot().withSetting('Layout_Show_Home_Button', true).withTranslations('en', 'core', {
Sidebar: 'Sidebar',
Home: 'Home',
Create_new: 'Create new',
Voice_Call: 'Voice Call',
Pages_and_actions: 'Pages and actions',
Workspace_and_user_preferences: 'Workspace and user preferences',
});

export default {
title: 'Sidebar/SidebarRail',

component: SidebarRail,
parameters: {
layout: 'fullscreen',
},
decorators: [
(Story) => (
<Box height='100vh' display='flex'>
<Story />
</Box>
),
],
} satisfies Meta<typeof SidebarRail>;

type Story = StoryObj<typeof SidebarRail>;

export const Anonymous: Story = {
decorators: [baseRoot().buildStoryDecorator()],
};

export const LoggedIn: Story = {
decorators: [baseRoot().withJohnDoe().buildStoryDecorator()],
};

export const WithUnreadBadge: Story = {
decorators: [
baseRoot()
.withJohnDoe()
.wrap((children: ReactNode) => <SessionContext.Provider value={sessionMock({ unread: 5 })}>{children}</SessionContext.Provider>)
.buildStoryDecorator(),
],
};

export const WithCreatePermissions: Story = {
decorators: [
baseRoot().withJohnDoe().withPermission('create-c').withPermission('create-p').withPermission('create-d').buildStoryDecorator(),
],
};
61 changes: 61 additions & 0 deletions apps/meteor/client/sidebar/SidebarRail/SidebarRail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Box, NavBarGroup } from '@rocket.chat/fuselage';
import { useUser } from '@rocket.chat/ui-contexts';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';

import SidebarRailCreateNew from './SidebarRailCreateNew';
import SidebarRailDivider from './SidebarRailDivider';
import SidebarRailLoginPage from './SidebarRailLoginPage';
import SidebarRailPhone from './SidebarRailPhone';
import SidebarRailSort from './SidebarRailSort';
import NavBarItemDirectoryPage from '../../navbar/NavBarPagesGroup/NavBarItemDirectoryPage';
import NavBarItemHomePage from '../../navbar/NavBarPagesGroup/NavBarItemHomePage';
import NavBarItemMarketPlaceMenu from '../../navbar/NavBarPagesGroup/NavBarItemMarketPlaceMenu';
import { NavBarItemAdministrationMenu, UserMenu } from '../../navbar/NavBarSettingsToolbar';

const SidebarRail = () => {
const { t } = useTranslation();
const user = useUser();

return (
<Box
is='nav'
aria-label={t('Sidebar_rail')}
className='rcx-sidebar-rail'
backgroundColor='surface-sidebar'
borderInlineEndWidth='default'
borderInlineEndStyle='solid'
borderInlineEndColor='stroke-light'
display='flex'
flexDirection='column'
alignItems='stretch'
width='x44'
height='full'
// secondarySidebar Feature Preview animates transitions between panels.
// This zIndex ensures the panels transition stays behind the SideRail
zIndex={10}
>
<Box flexGrow={1} minHeight={0} overflow='hidden auto' padding={8}>
<NavBarGroup vertical aria-label={t('Pages_and_actions')}>
<NavBarItemHomePage title={t('Home')} />
<SidebarRailSort />
<SidebarRailCreateNew />
</NavBarGroup>
<SidebarRailDivider />
<NavBarGroup vertical aria-label={t('Voice_Call')}>
<SidebarRailPhone />
<NavBarItemDirectoryPage title={t('Directory')} />
<NavBarItemMarketPlaceMenu />
</NavBarGroup>
</Box>
<Box padding={8}>
<NavBarGroup vertical aria-label={t('Workspace_and_user_preferences')}>
<NavBarItemAdministrationMenu />
{user ? <UserMenu user={user} /> : <SidebarRailLoginPage />}
</NavBarGroup>
</Box>
</Box>
);
};

export default memo(SidebarRail);
29 changes: 29 additions & 0 deletions apps/meteor/client/sidebar/SidebarRail/SidebarRailCallPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Box, Sidepanel } from '@rocket.chat/fuselage';
import { FeaturePreview, FeaturePreviewOn, FeaturePreviewOff } from '@rocket.chat/ui-client';
import { useLayout } from '@rocket.chat/ui-contexts';
import { InlineMediaCallWidget } from '@rocket.chat/ui-voip';
import { useTranslation } from 'react-i18next';

import SidebarPortal from '../../portals/SidebarPortal';

const SidebarRailCallPanel = () => {
const { t } = useTranslation();
const { isEmbedded: embeddedLayout, isMobile } = useLayout();

return (
<FeaturePreview feature='sidebarRail' disabled={embeddedLayout || isMobile}>
<FeaturePreviewOn>
<SidebarPortal>
<Sidepanel role='complementary' aria-label={t('Calls')}>
<Box padding={16}>
<InlineMediaCallWidget />
</Box>
</Sidepanel>
</SidebarPortal>
</FeaturePreviewOn>
<FeaturePreviewOff>{null}</FeaturePreviewOff>
</FeaturePreview>
);
};

export default SidebarRailCallPanel;
22 changes: 22 additions & 0 deletions apps/meteor/client/sidebar/SidebarRail/SidebarRailCreateNew.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { NavBarItem } from '@rocket.chat/fuselage';
import { GenericMenu } from '@rocket.chat/ui-client';
import type { HTMLAttributes } from 'react';
import { useTranslation } from 'react-i18next';

import { useCreateNewMenu } from '../../navbar/NavBarPagesGroup/hooks/useCreateNewMenu';

type SidebarRailCreateNewProps = Omit<HTMLAttributes<HTMLElement>, 'is'>;

const SidebarRailCreateNew = (props: SidebarRailCreateNewProps) => {
const { t } = useTranslation();

const sections = useCreateNewMenu();

if (sections.length === 0) {
return null;
}

return <GenericMenu icon='pencil-box' sections={sections} title={t('Create_new')} is={NavBarItem} placement='right-start' {...props} />;
};

export default SidebarRailCreateNew;
11 changes: 11 additions & 0 deletions apps/meteor/client/sidebar/SidebarRail/SidebarRailDivider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { ComponentProps } from 'react';

import HorizontalDivider from '../../components/HorizontalDivider';

type SidebarRailDividerProps = ComponentProps<typeof HorizontalDivider>;

const SidebarRailDivider = (props: SidebarRailDividerProps) => (
<HorizontalDivider marginBlock={16} marginInline={4} borderBlockStartColor='stroke-light' {...props} />
);

export default SidebarRailDivider;
15 changes: 15 additions & 0 deletions apps/meteor/client/sidebar/SidebarRail/SidebarRailHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Box, NavBar as NavBarComponent, NavBarSection } from '@rocket.chat/fuselage';

import NavBarNavigation from '../../navbar/NavBarNavigation';

const SidebarRailHeader = () => (
<NavBarComponent aria-label='header' style={{ paddingInline: '0.5rem' }}>
<NavBarSection>
<Box is='img' src='/images/logo/icon.svg' alt='Rocket.Chat' size='x28' />
</NavBarSection>
<NavBarNavigation />
<NavBarSection />
</NavBarComponent>
);

export default SidebarRailHeader;
15 changes: 15 additions & 0 deletions apps/meteor/client/sidebar/SidebarRail/SidebarRailLoginPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { NavBarItem } from '@rocket.chat/fuselage';
import { useSessionDispatch } from '@rocket.chat/ui-contexts';
import type { HTMLAttributes } from 'react';
import { useTranslation } from 'react-i18next';

type SidebarRailLoginPageProps = Omit<HTMLAttributes<HTMLElement>, 'is'>;

const SidebarRailLoginPage = (props: SidebarRailLoginPageProps) => {
const setForceLogin = useSessionDispatch('forceLogin');
const { t } = useTranslation();

return <NavBarItem {...props} icon='login' title={t('Login')} onClick={() => setForceLogin(true)} />;
};

export default SidebarRailLoginPage;
38 changes: 38 additions & 0 deletions apps/meteor/client/sidebar/SidebarRail/SidebarRailPhone.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { NavBarItem } from '@rocket.chat/fuselage';
import { useStableCallback } from '@rocket.chat/fuselage-hooks';
import { useCurrentRoutePath, useRouter } from '@rocket.chat/ui-contexts';
import { useMediaCallAction } from '@rocket.chat/ui-voip';
import type { HTMLAttributes } from 'react';
import { useTranslation } from 'react-i18next';

type SidebarRailPhoneProps = Omit<HTMLAttributes<HTMLElement>, 'is'>;

const SidebarRailPhone = (props: SidebarRailPhoneProps) => {
const { t } = useTranslation();
const callAction = useMediaCallAction();
const router = useRouter();
const currentRoute = useCurrentRoutePath();

const isActive = currentRoute?.includes('/call-history') ?? false;

const handleClick = useStableCallback(() => {
router.navigate('/call-history');
});

if (!callAction) {
return null;
}

return (
<NavBarItem
{...props}
title={t('Calls')}
icon='phone'
pressed={isActive}
aria-current={isActive ? 'page' : undefined}
onClick={handleClick}
/>
);
};

export default SidebarRailPhone;
28 changes: 28 additions & 0 deletions apps/meteor/client/sidebar/SidebarRail/SidebarRailSort.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { NavBarItem } from '@rocket.chat/fuselage';
import { GenericMenu } from '@rocket.chat/ui-client';
import type { HTMLAttributes } from 'react';
import { useTranslation } from 'react-i18next';

import { useSortMenu } from '../../navbar/NavBarPagesGroup/hooks/useSortMenu';

type SidebarRailSortProps = Omit<HTMLAttributes<HTMLElement>, 'is'>;

const SidebarRailSort = (props: SidebarRailSortProps) => {
const { t } = useTranslation();

const sections = useSortMenu();

return (
<GenericMenu
icon='sort'
sections={sections}
title={t('Display')}
selectionMode='multiple'
is={NavBarItem}
placement='right-start'
{...props}
/>
);
};

export default SidebarRailSort;
1 change: 1 addition & 0 deletions apps/meteor/client/sidebar/SidebarRail/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './SidebarRail';
2 changes: 2 additions & 0 deletions apps/meteor/client/startup/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const OAuthAuthorizationPage = lazy(() => import('../views/oauth/OAuthAuthorizat
const OAuthErrorPage = lazy(() => import('../views/oauth/OAuthErrorPage'));
const NotFoundPage = lazy(() => import('../views/notFound/NotFoundPage'));
const CallHistoryPage = lazy(() => import('../views/mediaCallHistory/CallHistoryPage'));
const SidebarRailCallPanel = lazy(() => import('../sidebar/SidebarRail/SidebarRailCallPanel'));
const SearchPage = lazy(() => import('../views/search/SearchPage'));

declare module '@rocket.chat/ui-contexts' {
Expand Down Expand Up @@ -253,6 +254,7 @@ router.defineRoutes([
element: appLayout.wrap(
<MainLayout>
<CallHistoryPage />
<SidebarRailCallPanel />
</MainLayout>,
),
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { mockAppRoot } from '@rocket.chat/mock-providers';
import { useCurrentRoutePath, useRouter } from '@rocket.chat/ui-contexts';
import { render } from '@testing-library/react';
import type { ReactNode } from 'react';

import LayoutWithSidebar from './LayoutWithSidebar';

Expand All @@ -12,19 +11,17 @@ jest.mock('@rocket.chat/ui-contexts', () => ({
}));

jest.mock('../../../navbar', () => () => <div>NavBar</div>);
jest.mock('../../../sidebar', () => () => <div>Sidebar</div>);
jest.mock('../../navigation', () => () => <div>NavigationRegion</div>);
jest.mock('../../../sidebar/SidebarRail', () => () => <div>SidebarRail</div>);
jest.mock('../../../sidebar/SidebarRail/SidebarRailHeader', () => () => <div>SidebarRailHeader</div>);
jest.mock('./AccessibilityShortcut', () => () => <div>AccessibilityShortcut</div>);
jest.mock('../../navigation/providers/RoomsNavigationProvider', () => ({
__esModule: true,
default: ({ children }: { children: ReactNode }) => <>{children}</>,
}));

jest.mock('../../navigation/providers/RoomsNavigationProvider', () => () => <div>Navigationprovider</div>);
jest.mock('../../navigation', () => () => <div>NavigationRegion</div>);
jest.mock('../../../sidebar', () => () => <div>Sidebar</div>);
jest.mock('@rocket.chat/ui-client', () => ({
...jest.requireActual('@rocket.chat/ui-client'),
FeaturePreview: ({ children }: { children: ReactNode }) => <>{children}</>,
FeaturePreviewOn: ({ children }: { children: ReactNode }) => <>{children}</>,
FeaturePreviewOff: ({ children }: { children: ReactNode }) => <>{children}</>,
FeaturePreview: ({ children }: any) => children,
FeaturePreviewOn: ({ children }: any) => children,
FeaturePreviewOff: () => null,
}));

const mockedUseCurrentRoutePath = useCurrentRoutePath as jest.MockedFunction<typeof useCurrentRoutePath>;
Expand Down
Loading
Loading