From 24c79957403d2b3a58a8e0450020e59212307896 Mon Sep 17 00:00:00 2001 From: Taylor Ho Date: Wed, 5 Aug 2026 15:40:40 -0700 Subject: [PATCH] fix(desktop): clamp thread panel to channel surface (#4965) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Category:** fix **User Impact:** Expanded thread panels now stay fully visible within the desktop channel area instead of being cut off. **Problem:** The resize handler clamped the thread panel against the full window width, even though the panel renders inside a narrower channel surface. On a 1720px window, this allowed a 1160px requested width where only 1111px could render, leaving persisted and visible geometry out of sync. **Solution:** Clamp resizing against the measured channel-surface width so the stored width matches what the layout can render while preserving the minimum 300px main pane.
File changes **desktop/src/features/channels/ui/ChannelScreen.tsx** Passes the measured channel-surface width into the thread-panel sizing hook. **desktop/src/shared/hooks/useThreadPanelWidth.ts** Clamps drag-resize updates against the available channel width instead of the full viewport. **desktop/tests/e2e/threadpane-ultrawide.spec.ts** Adds a 1720px regression proving the requested and rendered panel widths match, while retaining the ultrawide expansion case.
### Reproduction steps 1. Open a channel thread in the desktop app at a 1720×900 window size. 2. Drag the thread panel's left resize handle toward the left edge to expand it as far as possible. 3. Confirm the panel remains fully bounded inside the channel surface and the main channel pane remains at least 300px wide. 4. Reload the channel and confirm the persisted expanded width renders without clipping. ### Testing - `pnpm --dir desktop build:e2e` - `pnpm --dir desktop exec playwright test tests/e2e/threadpane-ultrawide.spec.ts` — 2 passed - Push hooks: `desktop-check` and `desktop-test` passed - `git diff --check origin/main..HEAD` ### Screenshot ![Expanded thread panel remains bounded at 1720×900](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/4965/threadpane-expanded-after-fix.png) ### Related issue None found. Signed-off-by: Taylor Ho --- .../features/channels/ui/ChannelScreen.tsx | 6 +-- .../src/shared/hooks/useThreadPanelWidth.ts | 13 +++++-- .../tests/e2e/threadpane-ultrawide.spec.ts | 39 +++++++++++++++++++ 3 files changed, 52 insertions(+), 6 deletions(-) diff --git a/desktop/src/features/channels/ui/ChannelScreen.tsx b/desktop/src/features/channels/ui/ChannelScreen.tsx index b235468cd8..8666394938 100644 --- a/desktop/src/features/channels/ui/ChannelScreen.tsx +++ b/desktop/src/features/channels/ui/ChannelScreen.tsx @@ -137,16 +137,16 @@ export function ChannelScreen({ setProfilePanelPubkey, setProfilePanelView, } = useChannelPanelHistoryState(); + const [channelContentRef, channelContentWidthPx] = + useElementWidth(); const { canReset: canResetThreadPanelWidth, onResetWidth: handleThreadPanelWidthReset, onResizeStart: handleThreadPanelResizeStart, widthPx: threadPanelWidthPx, - } = useThreadPanelWidth(); + } = useThreadPanelWidth(channelContentWidthPx || undefined); const [isMembersSidebarOpen, setIsMembersSidebarOpen] = React.useState(false); const [isAddBotOpen, setIsAddBotOpen] = React.useState(false); - const [channelContentRef, channelContentWidthPx] = - useElementWidth(); const [expandedThreadReplyIds, setExpandedThreadReplyIds] = React.useState( () => new Set(), ); diff --git a/desktop/src/shared/hooks/useThreadPanelWidth.ts b/desktop/src/shared/hooks/useThreadPanelWidth.ts index 18e36d7f9f..20891b5383 100644 --- a/desktop/src/shared/hooks/useThreadPanelWidth.ts +++ b/desktop/src/shared/hooks/useThreadPanelWidth.ts @@ -45,7 +45,11 @@ function getInitialThreadPanelWidth(): number { } } -export function useThreadPanelWidth() { +export function useThreadPanelWidth(availableWidthPx?: number) { + const getAvailableWidth = React.useCallback( + () => availableWidthPx ?? getViewportWidth(), + [availableWidthPx], + ); const [widthPx, setWidthPx] = React.useState(() => getInitialThreadPanelWidth(), ); @@ -79,7 +83,10 @@ export function useThreadPanelWidth() { const handlePointerMove = (moveEvent: PointerEvent) => { const deltaX = startX - moveEvent.clientX; - const nextWidth = clampThreadPanelWidth(startWidth + deltaX); + const nextWidth = clampAuxiliaryPanelWidth( + startWidth + deltaX, + getAvailableWidth(), + ); setWidthPx(nextWidth); }; @@ -92,7 +99,7 @@ export function useThreadPanelWidth() { window.addEventListener("pointermove", handlePointerMove); window.addEventListener("pointerup", handlePointerUp, { once: true }); }, - [widthPx], + [getAvailableWidth, widthPx], ); const onResetWidth = React.useCallback(() => { diff --git a/desktop/tests/e2e/threadpane-ultrawide.spec.ts b/desktop/tests/e2e/threadpane-ultrawide.spec.ts index 7057701624..1c52515630 100644 --- a/desktop/tests/e2e/threadpane-ultrawide.spec.ts +++ b/desktop/tests/e2e/threadpane-ultrawide.spec.ts @@ -118,4 +118,43 @@ test.describe("thread pane on ultrawide monitors", () => { path: "test-results/threadpane-ultrawide-after.png", }); }); + + test("clamps the requested width to the channel surface", async ({ + page, + }) => { + await page.setViewportSize({ width: 1720, height: 900 }); + await installMockBridge(page); + await openThread(page); + + const pane = page.getByTestId("message-thread-panel"); + const handle = page.getByTestId("right-auxiliary-pane-resize-handle"); + const handleBox = await handle.boundingBox(); + if (!handleBox) throw new Error("resize handle not laid out"); + + const startX = handleBox.x + handleBox.width / 2; + const startY = handleBox.y + handleBox.height / 2; + await page.mouse.move(startX, startY); + await page.mouse.down(); + for (let x = startX; x >= 500; x -= 60) { + await page.mouse.move(x, startY); + } + await page.mouse.up(); + + const renderedWidth = await pane.evaluate((element) => + Math.round(element.getBoundingClientRect().width), + ); + const storedWidth = await page.evaluate(() => + Number(window.sessionStorage.getItem("buzz.desktop.thread-panel-width")), + ); + const mainWidth = await page + .getByTestId("channel-drop-zone") + .evaluate((element) => Math.round(element.getBoundingClientRect().width)); + + await page.screenshot({ + path: "test-results/threadpane-expanded-after-fix.png", + }); + + expect(renderedWidth).toBe(storedWidth); + expect(mainWidth).toBeGreaterThanOrEqual(300); + }); });