refactor(desktop): extract message depth guides

Signed-off-by: Cea Stapleton Cordasco <261786559+cea@users.noreply.github.com>
This commit is contained in:
Cea Stapleton Cordasco
2026-08-11 16:49:20 -05:00
parent 9d67b027c3
commit 806e62f823
2 changed files with 116 additions and 93 deletions

View File

@@ -0,0 +1,106 @@
import * as React from "react";
import {
threadReplyLength,
THREAD_REPLY_LINE_WIDTH_REM,
} from "@/features/messages/lib/threadTreeLayout";
import type { TimelineMessage } from "@/features/messages/types";
import { cn } from "@/shared/lib/cn";
type DepthGuideAction = {
active?: boolean;
depth: number;
label: string;
message: TimelineMessage;
};
export function MessageDepthGuides({
collapseDepthGuideActionsByDepth,
depthGuideItems,
guideBleedRem,
highlightThreadLineDepths,
messageId,
onCollapse,
onHoverChange,
}: {
collapseDepthGuideActionsByDepth: ReadonlyMap<number, DepthGuideAction>;
depthGuideItems: ReadonlyArray<{ depth: number; offset: number }>;
guideBleedRem: number;
highlightThreadLineDepths?: ReadonlyArray<number>;
messageId: string;
onCollapse: (
event: React.MouseEvent<HTMLButtonElement>,
message: TimelineMessage,
) => void;
onHoverChange: (message: TimelineMessage, active: boolean) => void;
}) {
return (
<div
aria-hidden={collapseDepthGuideActionsByDepth.size > 0 ? undefined : true}
className={cn(
"absolute left-0",
collapseDepthGuideActionsByDepth.size === 0 && "pointer-events-none",
)}
style={{
bottom: threadReplyLength(-guideBleedRem),
top: threadReplyLength(-guideBleedRem),
}}
>
{depthGuideItems.map(({ depth, offset }) => {
const collapseAction = collapseDepthGuideActionsByDepth.get(depth);
const isHighlighted =
Boolean(collapseAction?.active) ||
Boolean(highlightThreadLineDepths?.includes(depth));
if (collapseAction) {
return (
<React.Fragment key={`${messageId}-depth-guide-${offset}`}>
<div
aria-hidden
className={cn(
"pointer-events-none absolute bottom-0 top-0 border-l transition-[border-color]",
isHighlighted ? "border-primary" : "border-border/45",
)}
style={{
borderLeftWidth: threadReplyLength(
THREAD_REPLY_LINE_WIDTH_REM,
),
left: threadReplyLength(offset),
}}
/>
<button
aria-label={collapseAction.label}
className="absolute bottom-0 top-0 z-20 w-5 -translate-x-1/2 cursor-pointer rounded-full focus-visible:outline-hidden"
data-thread-head-id={collapseAction.message.id}
data-testid="thread-collapse-guide"
onBlur={() => onHoverChange(collapseAction.message, false)}
onClick={(event) => onCollapse(event, collapseAction.message)}
onFocus={() => onHoverChange(collapseAction.message, true)}
onMouseEnter={() => onHoverChange(collapseAction.message, true)}
onMouseLeave={() =>
onHoverChange(collapseAction.message, false)
}
style={{ left: threadReplyLength(offset) }}
type="button"
/>
</React.Fragment>
);
}
return (
<div
aria-hidden
className={cn(
"pointer-events-none absolute bottom-0 top-0 border-l transition-[border-color]",
isHighlighted ? "border-primary" : "border-border/45",
)}
key={`${messageId}-depth-guide-${offset}`}
style={{
borderLeftWidth: threadReplyLength(THREAD_REPLY_LINE_WIDTH_REM),
left: threadReplyLength(offset),
}}
/>
);
})}
</div>
);
}

View File

@@ -57,6 +57,7 @@ import { hasLinkPreviewSuppression } from "@/features/messages/lib/formatTimelin
import { toast } from "sonner";
import { MessageAgentOwner } from "./MessageAgentOwner";
import { MessageAuthorText, MessageHeaderRow } from "./MessageHeader";
import { MessageDepthGuides } from "./MessageDepthGuides";
import { MessageStatusMetadata } from "./MessageStatusMetadata";
import { MessageTimestamp } from "./MessageTimestamp";
import { SentFromThreadLine } from "./SentFromThreadLine";
@@ -730,99 +731,15 @@ export const MessageRow = React.memo(
}
>
{showDepthGuides && depthGuideItems.length > 0 ? (
<div
aria-hidden={
collapseDepthGuideActionsByDepth.size > 0 ? undefined : true
}
className={cn(
"absolute left-0",
collapseDepthGuideActionsByDepth.size === 0 &&
"pointer-events-none",
)}
style={{
bottom: threadReplyLength(-guideBleedRem),
top: threadReplyLength(-guideBleedRem),
}}
>
{depthGuideItems.map(({ depth, offset }) => {
const collapseAction =
collapseDepthGuideActionsByDepth.get(depth);
const isHighlighted =
Boolean(collapseAction?.active) ||
Boolean(highlightThreadLineDepths?.includes(depth));
if (collapseAction) {
return (
<React.Fragment key={`${message.id}-depth-guide-${offset}`}>
<div
aria-hidden
className={cn(
"pointer-events-none absolute bottom-0 top-0 border-l transition-[border-color]",
isHighlighted ? "border-primary" : "border-border/45",
)}
style={{
borderLeftWidth: threadReplyLength(
THREAD_REPLY_LINE_WIDTH_REM,
),
left: threadReplyLength(offset),
}}
/>
<button
aria-label={collapseAction.label}
className="absolute bottom-0 top-0 z-20 w-5 -translate-x-1/2 cursor-pointer rounded-full focus-visible:outline-hidden"
data-thread-head-id={collapseAction.message.id}
data-testid="thread-collapse-guide"
onBlur={() =>
handleCollapseDepthGuideHoverChange(
collapseAction.message,
false,
)
}
onClick={(event) =>
handleCollapseDepthGuide(event, collapseAction.message)
}
onFocus={() =>
handleCollapseDepthGuideHoverChange(
collapseAction.message,
true,
)
}
onMouseEnter={() =>
handleCollapseDepthGuideHoverChange(
collapseAction.message,
true,
)
}
onMouseLeave={() =>
handleCollapseDepthGuideHoverChange(
collapseAction.message,
false,
)
}
style={{ left: threadReplyLength(offset) }}
type="button"
/>
</React.Fragment>
);
}
return (
<div
aria-hidden
className={cn(
"pointer-events-none absolute bottom-0 top-0 border-l transition-[border-color]",
isHighlighted ? "border-primary" : "border-border/45",
)}
key={`${message.id}-depth-guide-${offset}`}
style={{
borderLeftWidth: threadReplyLength(
THREAD_REPLY_LINE_WIDTH_REM,
),
left: threadReplyLength(offset),
}}
/>
);
})}
</div>
<MessageDepthGuides
collapseDepthGuideActionsByDepth={collapseDepthGuideActionsByDepth}
depthGuideItems={depthGuideItems}
guideBleedRem={guideBleedRem}
highlightThreadLineDepths={highlightThreadLineDepths}
messageId={message.id}
onCollapse={handleCollapseDepthGuide}
onHoverChange={handleCollapseDepthGuideHoverChange}
/>
) : null}
{showDepthGuides && descendantGuideOffsetRem !== null ? (
<>