mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
feat(desktop): let thread pane expand on ultrawide monitors (#1407)
Signed-off-by: Tyler Longwell <tlongwell@block.xyz> Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@sprout-oss.stage.blox.sqprod.co> Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
This commit is contained in:
co-authored by
npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d
Tyler Longwell
parent
5784ff0f1a
commit
f86f97bbde
@@ -51,6 +51,7 @@ export default defineConfig({
|
||||
"**/home-collapsed-top-chrome.spec.ts",
|
||||
"**/thread-unread.spec.ts",
|
||||
"**/thread-reply-anchor-roleplay.spec.ts",
|
||||
"**/threadpane-ultrawide.spec.ts",
|
||||
"**/animated-avatar.spec.ts",
|
||||
"**/reminders.spec.ts",
|
||||
"**/virtualization.spec.ts",
|
||||
|
||||
@@ -2,17 +2,25 @@ import * as React from "react";
|
||||
|
||||
import {
|
||||
AUXILIARY_PANEL_DEFAULT_WIDTH_PX,
|
||||
AUXILIARY_PANEL_MAX_WIDTH_PX,
|
||||
AUXILIARY_PANEL_MIN_WIDTH_PX,
|
||||
clampAuxiliaryPanelWidth,
|
||||
} from "@/shared/layout/AuxiliaryPanel";
|
||||
|
||||
const THREAD_PANEL_WIDTH_SESSION_KEY = "buzz.desktop.thread-panel-width";
|
||||
|
||||
function getViewportWidth(): number {
|
||||
return typeof window === "undefined" ? 0 : window.innerWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clamp the stored panel width for the current viewport.
|
||||
*
|
||||
* The upper bound grows with the viewport (see {@link clampAuxiliaryPanelWidth}) so
|
||||
* the pane can expand on ultrawide displays. `AuxiliaryPanelShell` additionally
|
||||
* clamps the rendered width to `calc(100% - MIN)` at paint time, so a stored width
|
||||
* larger than the current viewport never collapses the main pane.
|
||||
*/
|
||||
function clampThreadPanelWidth(width: number): number {
|
||||
return Math.max(
|
||||
AUXILIARY_PANEL_MIN_WIDTH_PX,
|
||||
Math.min(AUXILIARY_PANEL_MAX_WIDTH_PX, width),
|
||||
);
|
||||
return clampAuxiliaryPanelWidth(width, getViewportWidth());
|
||||
}
|
||||
|
||||
function getInitialThreadPanelWidth(): number {
|
||||
|
||||
@@ -25,4 +25,6 @@ export {
|
||||
AUXILIARY_PANEL_MAX_WIDTH_PX,
|
||||
AUXILIARY_PANEL_MIN_WIDTH_PX,
|
||||
AUXILIARY_PANEL_SINGLE_COLUMN_BREAKPOINT_PX,
|
||||
clampAuxiliaryPanelWidth,
|
||||
getAuxiliaryPanelMaxWidth,
|
||||
} from "@/shared/layout/auxiliaryPanelLayout";
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
AUXILIARY_PANEL_MAX_WIDTH_PX,
|
||||
AUXILIARY_PANEL_MIN_WIDTH_PX,
|
||||
clampAuxiliaryPanelWidth,
|
||||
getAuxiliaryPanelMaxWidth,
|
||||
} from "./auxiliaryPanelLayout.ts";
|
||||
|
||||
test("max width falls back to the static cap on narrow viewports", () => {
|
||||
// On viewports where `viewportWidth - MIN` is below the static cap, the floor wins.
|
||||
assert.equal(getAuxiliaryPanelMaxWidth(900), AUXILIARY_PANEL_MAX_WIDTH_PX);
|
||||
assert.equal(getAuxiliaryPanelMaxWidth(0), AUXILIARY_PANEL_MAX_WIDTH_PX);
|
||||
});
|
||||
|
||||
test("max width grows with the viewport, reserving the main pane", () => {
|
||||
assert.equal(
|
||||
getAuxiliaryPanelMaxWidth(3440),
|
||||
3440 - AUXILIARY_PANEL_MIN_WIDTH_PX,
|
||||
);
|
||||
});
|
||||
|
||||
test("clamp keeps width within [min, viewport-aware max]", () => {
|
||||
// Below the floor clamps up to the min.
|
||||
assert.equal(
|
||||
clampAuxiliaryPanelWidth(100, 3440),
|
||||
AUXILIARY_PANEL_MIN_WIDTH_PX,
|
||||
);
|
||||
// A wide drag is allowed on an ultrawide viewport.
|
||||
assert.equal(clampAuxiliaryPanelWidth(2000, 3440), 2000);
|
||||
// The same wide value is clamped down on a small viewport.
|
||||
assert.equal(
|
||||
clampAuxiliaryPanelWidth(2000, 900),
|
||||
AUXILIARY_PANEL_MAX_WIDTH_PX,
|
||||
);
|
||||
});
|
||||
@@ -3,3 +3,29 @@ export const AUXILIARY_PANEL_MIN_WIDTH_PX = 300;
|
||||
export const AUXILIARY_PANEL_SINGLE_COLUMN_BREAKPOINT_PX =
|
||||
AUXILIARY_PANEL_MIN_WIDTH_PX * 2;
|
||||
export const AUXILIARY_PANEL_MAX_WIDTH_PX = 720;
|
||||
|
||||
/**
|
||||
* Upper bound for the auxiliary panel width clamp, given the current viewport width.
|
||||
*
|
||||
* On ultrawide displays the static {@link AUXILIARY_PANEL_MAX_WIDTH_PX} is too small,
|
||||
* so the panel is allowed to grow with the viewport while always reserving at least
|
||||
* {@link AUXILIARY_PANEL_MIN_WIDTH_PX} for the main pane. The static cap acts as a
|
||||
* floor, so narrow viewports keep their existing behavior.
|
||||
*/
|
||||
export function getAuxiliaryPanelMaxWidth(viewportWidth: number): number {
|
||||
return Math.max(
|
||||
AUXILIARY_PANEL_MAX_WIDTH_PX,
|
||||
viewportWidth - AUXILIARY_PANEL_MIN_WIDTH_PX,
|
||||
);
|
||||
}
|
||||
|
||||
/** Clamp a stored panel width into the allowed range for the current viewport. */
|
||||
export function clampAuxiliaryPanelWidth(
|
||||
width: number,
|
||||
viewportWidth: number,
|
||||
): number {
|
||||
return Math.max(
|
||||
AUXILIARY_PANEL_MIN_WIDTH_PX,
|
||||
Math.min(getAuxiliaryPanelMaxWidth(viewportWidth), width),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
import { TEST_IDENTITIES, installMockBridge } from "../helpers/bridge";
|
||||
|
||||
// Ultrawide viewport: 3440px is a common 21:9 monitor width.
|
||||
const ULTRAWIDE = { width: 3440, height: 1440 };
|
||||
|
||||
async function waitForMockLiveSubscription(
|
||||
page: import("@playwright/test").Page,
|
||||
channelName: string,
|
||||
) {
|
||||
await expect
|
||||
.poll(async () =>
|
||||
page.evaluate(
|
||||
({ ch }) =>
|
||||
(
|
||||
window as Window & {
|
||||
__BUZZ_E2E_HAS_MOCK_LIVE_SUBSCRIPTION__?: (input: {
|
||||
channelName: string;
|
||||
}) => boolean;
|
||||
}
|
||||
).__BUZZ_E2E_HAS_MOCK_LIVE_SUBSCRIPTION__?.({ channelName: ch }) ??
|
||||
false,
|
||||
{ ch: channelName },
|
||||
),
|
||||
)
|
||||
.toBe(true);
|
||||
}
|
||||
|
||||
async function emitMockReply(
|
||||
page: import("@playwright/test").Page,
|
||||
channelName: string,
|
||||
content: string,
|
||||
parentEventId: string,
|
||||
) {
|
||||
await page.evaluate(
|
||||
({ ch, msg, parent, pubkey }) =>
|
||||
(
|
||||
window as Window & {
|
||||
__BUZZ_E2E_EMIT_MOCK_MESSAGE__?: (input: {
|
||||
channelName: string;
|
||||
content: string;
|
||||
parentEventId?: string | null;
|
||||
pubkey?: string;
|
||||
createdAt?: number;
|
||||
}) => unknown;
|
||||
}
|
||||
).__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({
|
||||
channelName: ch,
|
||||
content: msg,
|
||||
parentEventId: parent,
|
||||
pubkey,
|
||||
createdAt: Math.floor(Date.now() / 1000) - 10,
|
||||
}),
|
||||
{
|
||||
ch: channelName,
|
||||
msg: content,
|
||||
parent: parentEventId,
|
||||
pubkey: TEST_IDENTITIES.alice.pubkey,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async function openThread(page: import("@playwright/test").Page) {
|
||||
await page.goto("/");
|
||||
await page.getByTestId("channel-general").click();
|
||||
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
||||
await waitForMockLiveSubscription(page, "general");
|
||||
|
||||
// Seed a reply so a thread summary row appears, then open it.
|
||||
await emitMockReply(
|
||||
page,
|
||||
"general",
|
||||
"Reply to welcome",
|
||||
"mock-general-welcome",
|
||||
);
|
||||
const threadSummary = page.getByTestId("message-thread-summary").first();
|
||||
await expect(threadSummary).toBeVisible();
|
||||
await threadSummary.click();
|
||||
await expect(page.getByTestId("message-thread-panel")).toBeVisible();
|
||||
}
|
||||
|
||||
test.describe("thread pane on ultrawide monitors", () => {
|
||||
test("expands well past the legacy 720px cap", async ({ page }) => {
|
||||
await page.setViewportSize(ULTRAWIDE);
|
||||
await installMockBridge(page);
|
||||
await openThread(page);
|
||||
|
||||
const pane = page.getByTestId("message-thread-panel");
|
||||
const handle = page.getByTestId("right-auxiliary-pane-resize-handle");
|
||||
|
||||
const beforeBox = await pane.boundingBox();
|
||||
if (!beforeBox) throw new Error("thread panel not laid out");
|
||||
// The panel opens at its default narrow width, far from the viewport edge.
|
||||
expect(beforeBox.width).toBeLessThan(720);
|
||||
await page.screenshot({
|
||||
path: "test-results/threadpane-ultrawide-before.png",
|
||||
});
|
||||
|
||||
// Drag the resize handle far to the left to widen the right-hand pane.
|
||||
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();
|
||||
// Move left in steps so pointermove fires repeatedly.
|
||||
for (let x = startX; x >= 600; x -= 120) {
|
||||
await page.mouse.move(x, startY);
|
||||
}
|
||||
await page.mouse.up();
|
||||
|
||||
const afterBox = await pane.boundingBox();
|
||||
if (!afterBox) throw new Error("thread panel not laid out after resize");
|
||||
// The pane is now far wider than the old 720px hard cap.
|
||||
expect(afterBox.width).toBeGreaterThan(1200);
|
||||
await page.screenshot({
|
||||
path: "test-results/threadpane-ultrawide-after.png",
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user