From 081844413bb033ca664945529ea251ab9502506a Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Mon, 18 May 2026 15:12:51 -0300 Subject: [PATCH 01/18] chore(storybook): expose DDPCommon stub on meteor mock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `client/lib/sdk/ddpProtocol.ts` destructures `parseDDP` and `stringifyDDP` from `DDPCommon` at module load. Without a matching entry in the meteor mock any story whose bundle reaches the SDK chain crashes with: Cannot destructure property 'parseDDP' of 'DDPCommon' as it is undefined Add a narrow `DDPCommon` no-op (parseDDP/stringifyDDP) so the destructuring resolves. Scoped change — does not re-introduce the broader SDKClient stub. --- apps/meteor/.storybook/mocks/meteor.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/meteor/.storybook/mocks/meteor.ts b/apps/meteor/.storybook/mocks/meteor.ts index e3f0e0a979e70..7fdd0161454fe 100644 --- a/apps/meteor/.storybook/mocks/meteor.ts +++ b/apps/meteor/.storybook/mocks/meteor.ts @@ -29,6 +29,11 @@ export const Meteor = { users: {}, }; +export const DDPCommon = { + parseDDP: () => undefined, + stringifyDDP: () => '', +}; + export const Tracker = { autorun: () => ({ stop: () => {}, @@ -94,5 +99,3 @@ export const Session = { get: () => {}, set: () => {}, }; - -export const DDPCommon = {}; From 723450d5807ea920a947e19bc60958ec2c15ff5a Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Mon, 18 May 2026 15:12:57 -0300 Subject: [PATCH 02/18] feat(ui): add HorizontalDivider wrapper around fuselage Divider Thin wrapper that forces `vertical={false}` on `@rocket.chat/fuselage` Divider, providing a single horizontal-only entrypoint for callers that want to express intent at the type level without re-implementing the visual style. --- .../components/HorizontalDivider/HorizontalDivider.tsx | 8 ++++++++ apps/meteor/client/components/HorizontalDivider/index.ts | 1 + 2 files changed, 9 insertions(+) create mode 100644 apps/meteor/client/components/HorizontalDivider/HorizontalDivider.tsx create mode 100644 apps/meteor/client/components/HorizontalDivider/index.ts diff --git a/apps/meteor/client/components/HorizontalDivider/HorizontalDivider.tsx b/apps/meteor/client/components/HorizontalDivider/HorizontalDivider.tsx new file mode 100644 index 0000000000000..8e44ec39a1287 --- /dev/null +++ b/apps/meteor/client/components/HorizontalDivider/HorizontalDivider.tsx @@ -0,0 +1,8 @@ +import { Divider } from '@rocket.chat/fuselage'; +import type { ComponentProps } from 'react'; + +type HorizontalDividerProps = Omit, 'vertical'>; + +const HorizontalDivider = (props: HorizontalDividerProps) => ; + +export default HorizontalDivider; diff --git a/apps/meteor/client/components/HorizontalDivider/index.ts b/apps/meteor/client/components/HorizontalDivider/index.ts new file mode 100644 index 0000000000000..dc7f5fe8b5e2d --- /dev/null +++ b/apps/meteor/client/components/HorizontalDivider/index.ts @@ -0,0 +1 @@ +export { default } from './HorizontalDivider'; From 96b6ceb43e5ab396d257969ea5a7edaffc03a34d Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Tue, 19 May 2026 13:10:40 -0300 Subject: [PATCH 03/18] feat(ui-voip): add inline mount mode to MediaCallWidget The widget is currently a floating, draggable portal. The sidebar rail needs to host the same widget anchored inside a slot, so add a slot-based mount mechanism and let the widget render in place without dragging or border styles. Slot mechanism - `MediaCallWidgetSlotContext`: shared context exposing the active slot element + an `inline` boolean. - `MediaCallWidgetSlot`: drop-in component that registers its own `
` as the target. Removing it from the tree restores the floating portal automatically. The slot aligns its child to the top so the anchored widget keeps its intrinsic height instead of stretching to the container. - `MediaCallProvider`: hosts the slot state above the view provider so consumers in `children` and the widget itself share the same context value. `MediaCallProvider` now wraps `children` with `MediaCallViewProvider` so `useMediaCallView` resolves in the app tree. - `MediaCallViewProvider`: when a slot is registered the widget is rendered via `createPortal(widget, slot)`; otherwise it falls back to the previous `AnchorPortal` mount. Widget composition - `WidgetBase`: inline branch drops the fixed positioning, border, and z-index. Background, border-radius, shadow and width are kept so the anchored render looks the same as the floating one. - `Widget`: read inline from context, skip drag wiring when inline. - `WidgetHandle`: hide itself when inline (drag is disabled). - `NewCall`: render the numeric Keypad below the autocomplete when inline so the dialer matches the sidebar layout in the design. Visibility rules - Only "undock" the widget for active call states (calling, ringing, ongoing). When the slot is gone and the session is idle the widget renders nothing instead of popping out as a floating NewCall. Snapshot tests regenerated to reflect the inline-aware render tree. --- .../src/components/MediaCallWidgetSlot.tsx | 19 +++++++++++ .../ui-voip/src/components/Widget/Widget.tsx | 34 +++++++++++++------ .../src/components/Widget/WidgetHandle.tsx | 7 ++++ .../Widget/__snapshots__/Widget.spec.tsx.snap | 2 +- packages/ui-voip/src/components/index.ts | 1 + .../src/context/MediaCallWidgetSlotContext.ts | 19 +++++++++++ packages/ui-voip/src/index.ts | 2 ++ .../src/providers/MediaCallProvider.tsx | 26 ++++++++++---- .../ui-voip/src/providers/useMediaSession.ts | 6 ++++ .../src/views/MediaCallWidget/NewCall.tsx | 10 +++++- .../MediaCallWidget.spec.tsx.snap | 12 +++---- 11 files changed, 112 insertions(+), 26 deletions(-) create mode 100644 packages/ui-voip/src/components/MediaCallWidgetSlot.tsx create mode 100644 packages/ui-voip/src/context/MediaCallWidgetSlotContext.ts diff --git a/packages/ui-voip/src/components/MediaCallWidgetSlot.tsx b/packages/ui-voip/src/components/MediaCallWidgetSlot.tsx new file mode 100644 index 0000000000000..6a00996d002d3 --- /dev/null +++ b/packages/ui-voip/src/components/MediaCallWidgetSlot.tsx @@ -0,0 +1,19 @@ +import { useLayoutEffect, useRef } from 'react'; + +import { useMediaCallWidgetSlot } from '../context/MediaCallWidgetSlotContext'; + +const slotStyle = { display: 'flex', alignItems: 'flex-start', flex: '0 0 auto', width: '100%' } as const; + +const MediaCallWidgetSlot = () => { + const ref = useRef(null); + const { setSlot } = useMediaCallWidgetSlot(); + + useLayoutEffect(() => { + setSlot(ref.current); + return () => setSlot(null); + }, [setSlot]); + + return
; +}; + +export default MediaCallWidgetSlot; diff --git a/packages/ui-voip/src/components/Widget/Widget.tsx b/packages/ui-voip/src/components/Widget/Widget.tsx index 06e040949cd1b..f3f4bb9cd7db8 100644 --- a/packages/ui-voip/src/components/Widget/Widget.tsx +++ b/packages/ui-voip/src/components/Widget/Widget.tsx @@ -5,29 +5,36 @@ import type { ComponentProps, ReactNode } from 'react'; import { useLayoutEffect } from 'react'; import { DragContext } from './WidgetDraggableContext'; +import { useMediaCallWidgetSlot } from '../../context/MediaCallWidgetSlotContext'; import { useDraggable } from '../../hooks'; import { type LastKnownPosition } from '../../providers/useWidgetPositionTracker'; // TODO: Initial position from the draggable api instead of style props // TODO: A11Y -const WidgetBase = styled('div')` - position: fixed; +const WidgetBase = styled('div', ({ inline: _inline, ...props }: { inline?: boolean }) => props)` padding: 0; - right: 2em; - top: 11em; display: flex; flex-direction: column; width: 248px; min-height: 128px; border-radius: 4px; - border: 1px solid ${Palette.stroke['stroke-dark'].toString()}; - box-shadow: - 0px 0px 1px 0px ${Palette.shadow['shadow-elevation-2x'].toString()}, - 0px 0px 12px 0px ${Palette.shadow['shadow-elevation-2y'].toString()}; background-color: ${Palette.surface['surface-tint'].toString()}; color: ${Palette.text['font-default'].toString()}; - z-index: 100; overflow: hidden; + + ${({ inline }) => + inline + ? '' + : ` + position: fixed; + right: 2em; + top: 11em; + border: 1px solid ${Palette.stroke['stroke-dark'].toString()}; + box-shadow: + 0px 0px 1px 0px ${Palette.shadow['shadow-elevation-2x'].toString()}, + 0px 0px 12px 0px ${Palette.shadow['shadow-elevation-2y'].toString()}; + z-index: 100; + `} `; export type WidgetProps = { @@ -37,18 +44,23 @@ export type WidgetProps = { } & ComponentProps; const Widget = ({ children, onChangePosition, restorePosition, ...props }: WidgetProps) => { + const { inline } = useMediaCallWidgetSlot(); const [draggableRef, boundingRef, handleRef] = useDraggable({ onChangePosition, restorePosition }); useLayoutEffect(() => { + if (inline) { + return; + } boundingRef(document.body); - }, [boundingRef]); + }, [boundingRef, inline]); return ( diff --git a/packages/ui-voip/src/components/Widget/WidgetHandle.tsx b/packages/ui-voip/src/components/Widget/WidgetHandle.tsx index 059f063423df0..ff0288e3fabaf 100644 --- a/packages/ui-voip/src/components/Widget/WidgetHandle.tsx +++ b/packages/ui-voip/src/components/Widget/WidgetHandle.tsx @@ -3,6 +3,7 @@ import { Box, Icon, Palette } from '@rocket.chat/fuselage'; import type { ComponentProps } from 'react'; import { useDraggableWidget } from './WidgetDraggableContext'; +import { useMediaCallWidgetSlot } from '../../context/MediaCallWidgetSlotContext'; const dragHandle = css` cursor: grab; @@ -21,6 +22,12 @@ const dragHandle = css` const WidgetHandle = (props: ComponentProps) => { const { handleRef } = useDraggableWidget(); + const { inline } = useMediaCallWidgetSlot(); + + if (inline) { + return null; + } + return ( diff --git a/packages/ui-voip/src/components/Widget/__snapshots__/Widget.spec.tsx.snap b/packages/ui-voip/src/components/Widget/__snapshots__/Widget.spec.tsx.snap index 041b73b7e6363..22c3c847638bf 100644 --- a/packages/ui-voip/src/components/Widget/__snapshots__/Widget.spec.tsx.snap +++ b/packages/ui-voip/src/components/Widget/__snapshots__/Widget.spec.tsx.snap @@ -9,7 +9,7 @@ exports[`renders FullWidget without crashing 1`] = ` />