mirror of
https://github.com/block/buzz.git
synced 2026-10-27 04:30:52 +00:00
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  ### Related issue None found. Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
This commit is contained in:
@@ -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>(),
|
||||
);
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user