Files
buzz/desktop/src/features/messages/ui/MessageThreadSummaryRow.tsx
Thomas Petersen 5c28fb783d feat(desktop): add deterministic nested thread panels
Add Slack-style thread summaries and drilldown navigation for channel replies, and scope typing indicators to the active thread so deeper conversations stay isolated from the main timeline.

Made-with: Cursor
2026-04-13 14:29:17 -04:00

70 lines
1.9 KiB
TypeScript

import { MessageSquareText } from "lucide-react";
import type {
TimelineThreadSummary,
TimelineThreadSummaryParticipant,
} from "@/features/messages/lib/threadPanel";
import type { TimelineMessage } from "@/features/messages/types";
import { UserAvatar } from "@/shared/ui/UserAvatar";
function ParticipantAvatar({
participant,
index,
}: {
participant: TimelineThreadSummaryParticipant;
index: number;
}) {
return (
<div
className={index > 0 ? "-ml-2" : ""}
style={{ zIndex: 10 - index }}
>
<UserAvatar
avatarUrl={participant.avatarUrl}
className="rounded-full border-2 border-background"
displayName={participant.author}
size="xs"
/>
</div>
);
}
export function MessageThreadSummaryRow({
message,
onOpenThread,
summary,
}: {
message: TimelineMessage;
onOpenThread: (message: TimelineMessage) => void;
summary: TimelineThreadSummary;
}) {
return (
<button
className="ml-8 flex w-fit max-w-full items-center gap-3 rounded-xl px-3 py-2 text-left text-sm text-muted-foreground transition-colors hover:bg-accent/50 hover:text-foreground"
data-thread-head-id={message.id}
data-testid="message-thread-summary"
onClick={() => onOpenThread(message)}
type="button"
>
<div className="flex shrink-0 items-center">
{summary.participants.map((participant, index) => (
<ParticipantAvatar
index={index}
key={participant.id}
participant={participant}
/>
))}
</div>
<div className="min-w-0">
<div className="flex items-center gap-1.5 font-medium">
<MessageSquareText className="h-3.5 w-3.5" />
<span>
{summary.replyCount}{" "}
{summary.replyCount === 1 ? "reply" : "replies"}
</span>
</div>
</div>
</button>
);
}