mirror of
https://github.com/block/buzz.git
synced 2026-11-13 04:41:12 +00:00
fix(desktop): constrain message timeline width with min-w-0 (#1092)
Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@sprout-oss.stage.blox.sqprod.co> Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@sprout-oss.stage.blox.sqprod.co> Co-authored-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
@@ -95,6 +95,7 @@ export function ChannelScreen({
|
||||
setTopbarSearchHidden,
|
||||
} = useAppShell();
|
||||
const {
|
||||
clearMessageRouteTarget,
|
||||
openAgentSessionPubkey,
|
||||
openThreadHeadId,
|
||||
profilePanelPubkey,
|
||||
@@ -495,6 +496,9 @@ export function ChannelScreen({
|
||||
const handleThreadScrollTargetResolved = React.useCallback(() => {
|
||||
setThreadScrollTargetId(null);
|
||||
}, []);
|
||||
const handleTargetReached = React.useCallback(() => {
|
||||
clearMessageRouteTarget({ replace: true });
|
||||
}, [clearMessageRouteTarget]);
|
||||
React.useEffect(() => {
|
||||
resetComposerTargets(activeChannelId);
|
||||
}, [activeChannelId, resetComposerTargets]);
|
||||
@@ -695,6 +699,7 @@ export function ChannelScreen({
|
||||
handleThreadScrollTargetResolved
|
||||
}
|
||||
onThreadPanelResizeStart={handleThreadPanelResizeStart}
|
||||
onTargetReached={handleTargetReached}
|
||||
onToggleReaction={effectiveToggleReaction}
|
||||
openAgentSessionPubkey={openAgentSessionPubkey}
|
||||
openThreadHeadId={openThreadHeadId}
|
||||
|
||||
@@ -23,11 +23,13 @@ export type PanelValueSetter = (
|
||||
options?: PanelSetterOptions,
|
||||
) => void;
|
||||
|
||||
const PANEL_SEARCH_KEYS = [
|
||||
const CHANNEL_SEARCH_KEYS = [
|
||||
"agentSession",
|
||||
"messageId",
|
||||
"profile",
|
||||
"profileView",
|
||||
"thread",
|
||||
"threadRootId",
|
||||
] as const;
|
||||
|
||||
function asProfilePanelView(value: string | null): ProfilePanelView {
|
||||
@@ -35,7 +37,7 @@ function asProfilePanelView(value: string | null): ProfilePanelView {
|
||||
}
|
||||
|
||||
export function useChannelPanelHistoryState() {
|
||||
const { applyPatch, values } = useHistorySearchState(PANEL_SEARCH_KEYS);
|
||||
const { applyPatch, values } = useHistorySearchState(CHANNEL_SEARCH_KEYS);
|
||||
|
||||
const setOpenThreadHeadId = React.useCallback<PanelValueSetter>(
|
||||
(value, options) => applyPatch({ thread: value }, options),
|
||||
@@ -61,7 +63,14 @@ export function useChannelPanelHistoryState() {
|
||||
[applyPatch],
|
||||
);
|
||||
|
||||
const clearMessageRouteTarget = React.useCallback(
|
||||
(options?: PanelSetterOptions) =>
|
||||
applyPatch({ messageId: null, threadRootId: null }, options),
|
||||
[applyPatch],
|
||||
);
|
||||
|
||||
return {
|
||||
clearMessageRouteTarget,
|
||||
openAgentSessionPubkey: values.agentSession,
|
||||
openThreadHeadId: values.thread,
|
||||
profilePanelPubkey: values.profile,
|
||||
|
||||
@@ -110,10 +110,27 @@ export function useChannelRouteTarget({
|
||||
}
|
||||
|
||||
const targetMessage = timelineMessageById.get(targetMessageId) ?? null;
|
||||
if (
|
||||
!targetMessage?.parentId ||
|
||||
isBroadcastReply(targetMessage.tags ?? [])
|
||||
) {
|
||||
if (!targetMessage) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!targetMessage.parentId) {
|
||||
closeAgentSession();
|
||||
// Root message links should open the reply panel for that root. The
|
||||
// timeline scroll/highlight target alone is not enough: root links have
|
||||
// no parent/thread metadata, so the reply-only branch below cannot infer
|
||||
// a thread head.
|
||||
setProfilePanelPubkey(null, { replace: true });
|
||||
setEditTargetId(null);
|
||||
setOpenThreadHeadId(targetMessage.id, { replace: true });
|
||||
setThreadReplyTargetId(targetMessage.id);
|
||||
setThreadScrollTargetId(null);
|
||||
setExpandedThreadReplyIds(new Set());
|
||||
handledThreadRouteTargetRef.current = targetKey;
|
||||
return;
|
||||
}
|
||||
|
||||
if (isBroadcastReply(targetMessage.tags ?? [])) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -324,7 +324,7 @@ export const MessageTimeline = React.memo(function MessageTimeline({
|
||||
showMessageList && !showIntro && "mt-auto",
|
||||
)}
|
||||
contentClassName={cn(
|
||||
"flex flex-col gap-2",
|
||||
"flex min-w-0 flex-col gap-2",
|
||||
(showIntro || showGenericEmpty) && "min-h-full",
|
||||
)}
|
||||
loading={isLoading}
|
||||
|
||||
@@ -1046,3 +1046,50 @@ test("ArrowUp edits your last thread reply right after sending it", async ({
|
||||
await expect(editBanner).toContainText(reply);
|
||||
await expect(threadInput).toHaveText(reply);
|
||||
});
|
||||
|
||||
test("action bar stays within the timeline when the thread panel is open", async ({
|
||||
page,
|
||||
}) => {
|
||||
// Narrow viewport + open thread panel => the timeline shrinks to a column.
|
||||
// A long unbreakable token must not widen message rows past that column,
|
||||
// or the right-anchored action bar is pushed offscreen (regression: #1081
|
||||
// fixed the wrap but rows still expanded to content min-width).
|
||||
await page.setViewportSize({ width: 1024, height: 800 });
|
||||
await page.goto("/");
|
||||
await page.getByTestId("channel-general").click();
|
||||
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
||||
|
||||
const timeline = page.getByTestId("message-timeline");
|
||||
const input = page.getByTestId("message-input").first();
|
||||
const send = page.getByTestId("send-message").first();
|
||||
|
||||
const longUrl = `https://example.com/${"a".repeat(180)}/path`;
|
||||
await input.fill(longUrl);
|
||||
await send.click();
|
||||
await expect(timeline).toContainText("example.com");
|
||||
|
||||
const rootMessage = timeline.getByTestId("message-row").first();
|
||||
await rootMessage.hover();
|
||||
await rootMessage.getByRole("button", { name: "Reply" }).click();
|
||||
await expect(page.getByTestId("message-thread-panel")).toBeVisible();
|
||||
|
||||
const wideRow = timeline.getByTestId("message-row").last();
|
||||
await wideRow.scrollIntoViewIfNeeded();
|
||||
await wideRow.hover();
|
||||
const bar = wideRow.locator('[data-testid^="message-action-bar-"]');
|
||||
await expect(bar).toBeVisible();
|
||||
|
||||
const timelineBox = await timeline.boundingBox();
|
||||
const rowBox = await wideRow.boundingBox();
|
||||
const barBox = await bar.boundingBox();
|
||||
if (!timelineBox || !rowBox || !barBox) {
|
||||
throw new Error("Expected timeline, row, and action bar to have geometry.");
|
||||
}
|
||||
|
||||
expect(rowBox.x + rowBox.width).toBeLessThanOrEqual(
|
||||
timelineBox.x + timelineBox.width + 1,
|
||||
);
|
||||
expect(barBox.x + barBox.width).toBeLessThanOrEqual(
|
||||
timelineBox.x + timelineBox.width + 1,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -266,6 +266,77 @@ test("settings is a route: section survives reload, closing returns to the previ
|
||||
await expect(threadPanel).toBeVisible();
|
||||
});
|
||||
|
||||
test("message links to visible root messages open the thread panel", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
await page.getByTestId("channel-general").click();
|
||||
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
||||
await expect(page.getByTestId("message-timeline")).toContainText(
|
||||
"Welcome to #general",
|
||||
);
|
||||
|
||||
const link =
|
||||
"buzz://message?channel=9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50&id=mock-general-welcome";
|
||||
await page.getByTestId("message-input").fill(`Root link repro ${link}`);
|
||||
await page.getByTestId("send-message").click();
|
||||
|
||||
const linkMessage = page
|
||||
.getByTestId("message-row")
|
||||
.filter({ hasText: "Root link repro" })
|
||||
.last();
|
||||
await expect(linkMessage).toBeVisible();
|
||||
await linkMessage
|
||||
.getByRole("button", { name: "Open message in general" })
|
||||
.click();
|
||||
|
||||
const threadPanel = page.getByTestId("message-thread-panel");
|
||||
await expect(threadPanel).toBeVisible();
|
||||
await expect(page).toHaveURL(/thread=mock-general-welcome/);
|
||||
await expect(threadPanel.getByTestId("message-thread-head")).toContainText(
|
||||
"Welcome to #general",
|
||||
);
|
||||
});
|
||||
|
||||
test("message links reopen a closed thread when the same messageId is already in the URL", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto(
|
||||
"/#/channels/9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50?messageId=mock-general-welcome",
|
||||
);
|
||||
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
||||
|
||||
const threadPanel = page.getByTestId("message-thread-panel");
|
||||
await expect(threadPanel).toBeVisible();
|
||||
await expect(threadPanel.getByTestId("message-thread-head")).toContainText(
|
||||
"Welcome to #general",
|
||||
);
|
||||
|
||||
await threadPanel.getByRole("button", { name: "Close thread" }).click();
|
||||
await expect(threadPanel).not.toBeVisible();
|
||||
|
||||
const link =
|
||||
"buzz://message?channel=9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50&id=mock-general-welcome";
|
||||
await page
|
||||
.getByTestId("message-input")
|
||||
.fill(`Reopen same root link repro ${link}`);
|
||||
await page.getByTestId("send-message").click();
|
||||
|
||||
const linkMessage = page
|
||||
.getByTestId("message-row")
|
||||
.filter({ hasText: "Reopen same root link repro" })
|
||||
.last();
|
||||
await expect(linkMessage).toBeVisible();
|
||||
await linkMessage
|
||||
.getByRole("button", { name: "Open message in general" })
|
||||
.click();
|
||||
|
||||
await expect(threadPanel).toBeVisible();
|
||||
await expect(threadPanel.getByTestId("message-thread-head")).toContainText(
|
||||
"Welcome to #general",
|
||||
);
|
||||
});
|
||||
|
||||
test("message deep links survive reload", async ({ page }) => {
|
||||
await page.goto(
|
||||
`/#/channels/${ENGINEERING_CHANNEL_ID}?messageId=mock-engineering-shipped`,
|
||||
|
||||
Reference in New Issue
Block a user