Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
0818444
chore(storybook): expose DDPCommon stub on meteor mock
ggazzo May 18, 2026
723450d
feat(ui): add HorizontalDivider wrapper around fuselage Divider
ggazzo May 18, 2026
96b6ceb
feat(ui-voip): add inline mount mode to MediaCallWidget
ggazzo May 19, 2026
f37fd7a
feat(sidebar): add SidebarRail component
ggazzo May 20, 2026
e612864
feat(layout): gate SidebarRail behind USE_SIDEBAR_RAIL flag
ggazzo May 20, 2026
2eb1614
feat(layout): make USE_SIDEBAR_RAIL a runtime window flag
ggazzo Jun 3, 2026
b429ee5
chore(lint): disable naming-convention for Window augmentation
ggazzo Jun 3, 2026
3e5b327
test: align LayoutWithSidebar spec mocks with SidebarRail refactor
ggazzo Jun 3, 2026
982a633
feat(ui-voip): show dialpad during active SIP calls (DMV-16)
ggazzo Jun 8, 2026
843f917
fix(sidebar): drive call dialer from the panel, not a route watcher
ggazzo Jun 8, 2026
982e88d
fix(ui-voip): keep the DTMF dialpad on internal floating calls
ggazzo Jun 8, 2026
c2ded85
fix(ui-voip): add idempotent openDialer/closeDialer intents for the c…
ggazzo Jul 1, 2026
c409982
fix(ui-voip): keep the call panel dialer open after a call ends
ggazzo Jul 1, 2026
77e852b
fix(ui-voip): don't float the docked call-panel dialer on navigation
ggazzo Jul 1, 2026
1e64755
fix: Add missing sort button to SidebarRail
gabriellsh Jul 2, 2026
70fd502
fix: Widget disappearing on SidebarRail after ending a call
gabriellsh Jul 2, 2026
000a5e9
fix: Widget state left as "new" and "docked" when leaving call panel …
gabriellsh Jul 22, 2026
3b10fa1
fix: peerInfo overwritten with undefined value when opening Siderail …
gabriellsh Jul 22, 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,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: DDPCommon.parseDDP/stringifyDDP should preserve DDP payload shape; the current no-op stub can break any Storybook path that decodes or re-encodes messages.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/meteor/.storybook/mocks/meteor.js, line 32:

<comment>`DDPCommon.parseDDP/stringifyDDP` should preserve DDP payload shape; the current no-op stub can break any Storybook path that decodes or re-encodes messages.</comment>

<file context>
@@ -28,6 +28,11 @@ export const Meteor = {
 };
 
+export const DDPCommon = {
+	parseDDP: () => undefined,
+	stringifyDDP: () => '',
+};
</file context>

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(),
],
};
58 changes: 58 additions & 0 deletions apps/meteor/client/sidebar/SidebarRail/SidebarRail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
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')}
className='rcx-sidebar-rail'
bg='surface-sidebar'
borderInlineEndWidth='default'
borderInlineEndStyle='solid'
borderInlineEndColor='stroke-light'
display='flex'
flexDirection='column'
alignItems='stretch'
width='x44'
height='full'
>
<Box flexGrow={1} minHeight={0} overflow='hidden auto' p={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 p={8}>
<NavBarGroup vertical aria-label={t('Workspace_and_user_preferences')}>
<NavBarItemAdministrationMenu />
{user ? <UserMenu user={user} /> : <SidebarRailLoginPage />}
</NavBarGroup>
</Box>
</Box>
);
};

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

const SidebarRailCallPanel = () => {
const { t } = useTranslation();
const { openDialer, closeDialer } = useWidgetExternalControls();

// The call panel hosts the dialer: while it is mounted and the session is idle
// (initial open, or right after a call ends) show the dialer instead of an empty
// panel. `openDialer` is idempotent (only acts on the "closed" state), so React
// StrictMode double-invoking this layout effect on mount is harmless.
useLayoutEffect(() => {
openDialer();
}, [openDialer]);

// Leaving the telephony screen with only the idle dialer open must drop it, so it
// does not pop out as a floating widget. The `state === 'new'` guard scopes this to
// the idle dialer (never an ongoing call) and skips StrictMode's fake unmount, where
// the just-issued open has not re-rendered yet.
useLayoutEffect(
() => () => {
closeDialer();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The cleanup effect now calls closeDialer() unconditionally on unmount instead of only when the dialer is idle (state === 'new'). The adjacent comment still describes a guard that no longer exists. During an ongoing call, navigating away from the rail will now close the dialer/interrupt the call UI — the previous guard intentionally scoped this to the idle state only. In development mode with StrictMode, the fake unmount/remount will close then reopen the dialer, which can cause visual flicker or state-transition issues if closeDialer transitions to a state other than 'closed'. Either restore the idle-only guard or update the comment to reflect the new behavior.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/meteor/client/sidebar/SidebarRail/SidebarRailCallPanel.tsx, line 24:

<comment>The cleanup effect now calls `closeDialer()` unconditionally on unmount instead of only when the dialer is idle (`state === 'new'`). The adjacent comment still describes a guard that no longer exists. During an ongoing call, navigating away from the rail will now close the dialer/interrupt the call UI — the previous guard intentionally scoped this to the idle state only. In development mode with StrictMode, the fake unmount/remount will close then reopen the dialer, which can cause visual flicker or state-transition issues if `closeDialer` transitions to a state other than 'closed'. Either restore the idle-only guard or update the comment to reflect the new behavior.</comment>

<file context>
@@ -1,38 +1,27 @@
-			if (stateRef.current === 'new') {
-				closeDialer();
-			}
+			closeDialer();
 		},
 		[closeDialer],
</file context>

},
[closeDialer],
);

return (
<Box width='x280' minWidth='x280'>
<Sidepanel role='complementary' aria-label={t('Calls')}>
<Box p={16}>
<MediaCallWidgetSlot />
</Box>
</Sidepanel>
</Box>
);
};

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 mbs={16} mbe={16} mi={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';
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,20 +11,10 @@ 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('./SecondaryPanel', () => () => <div>SecondaryPanel</div>);
jest.mock('./AccessibilityShortcut', () => () => <div>AccessibilityShortcut</div>);
jest.mock('../../navigation/providers/RoomsNavigationProvider', () => ({
__esModule: true,
default: ({ children }: { children: ReactNode }) => <>{children}</>,
}));

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}</>,
}));

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