fix(desktop): clamp thread panel to channel surface (#4965)

**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.

<details>
<summary>File changes</summary>

**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.

</details>

### 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 <taylorkmho@gmail.com>
This commit is contained in:
Taylor Ho
2026-08-05 15:40:40 -07:00
committed by GitHub
parent cda33978b1
commit 24c7995740
3 changed files with 52 additions and 6 deletions

View File

@@ -137,16 +137,16 @@ export function ChannelScreen({
setProfilePanelPubkey,
setProfilePanelView,
} = useChannelPanelHistoryState();
const [channelContentRef, channelContentWidthPx] =
useElementWidth<HTMLDivElement>();
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<HTMLDivElement>();
const [expandedThreadReplyIds, setExpandedThreadReplyIds] = React.useState(
() => new Set<string>(),
);

View File

@@ -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<number>(() =>
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(() => {

View File

@@ -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);
});
});