"use client";
import { useRouter } from "next/navigation";
import { useState, useTransition, type ReactNode } from "react";
import { Surface, Textarea } from "@unlocalhosted/metalui";
import { replyThreadAction } from "@/app/actions/pulls";
import { AgentAvatar } from "@/components/agent";
import { CheckIcon } from "@/components/icons";
import { Alert, Avatar, Button, Label } from "@/components/ui";
export type ThreadComment = { id: string; author: string; agent: string | null; when: string; body: ReactNode };
/** A review thread on a line of the changes: its comments, a reply box, and resolving it. */
export function ThreadView({
owner,
name,
number,
rootId,
resolved,
outdated,
comments,
code,
canReply,
canResolve,
}: {
owner: string;
name: string;
number: number;
rootId: string;
resolved: boolean;
outdated: boolean;
comments: ThreadComment[];
/** The lines it's about, for threads shown away from the diff. */
code?: ReactNode;
canReply: boolean;
canResolve: boolean;
}) {
const router = useRouter();
const [open, setOpen] = useState(!resolved);
const [replying, setReplying] = useState(false);
const [body, setBody] = useState("");
const [error, setError] = useState<string | null>(null);
const [pending, startTransition] = useTransition();
const send = (input: { body: string; resolve?: boolean }) =>
startTransition(async () => {
const result = await replyThreadAction(owner, name, number, rootId, input);
if (result.error) return setError(result.error);
setError(null);
setBody("");
setReplying(false);
if (input.resolve !== undefined) setOpen(!input.resolve);
router.refresh();
});
return (
<Surface material="raise-lite" radius="plate" className="overflow-hidden font-sans">
<button type="button" onClick={() => setOpen((o) => !o)} className="flex w-full items-center gap-2 px-4 py-2 text-left type-meta text-ink3 hover:text-ink">
{resolved ? <Label tone="success">Resolved</Label> : <Label tone="attention">Open</Label>}
{outdated && <Label>Outdated</Label>}
<span className="truncate">
{comments.length} comment{comments.length === 1 ? "" : "s"} · {comments[0]?.author}
</span>
<span className="ml-auto">{open ? "Hide" : "Show"}</span>
</button>
{open && (
<div className="rule-t">
{code}
<ul className="divide-y divide-[var(--mu-rule)]">
{comments.map((c) => (
<li key={c.id} className="flex gap-3 px-4 py-3">
{c.agent ? <AgentAvatar size={24} /> : <Avatar name={c.author} size={24} />}
<div className="min-w-0 flex-1">
<p className="type-meta text-ink3">
<strong className="font-medium text-ink">{c.agent ?? c.author}</strong>
{c.agent && <> for {c.author}</>} · {c.when}
</p>
<div className="mt-1 type-body">{c.body}</div>
</div>
</li>
))}
</ul>
{(canReply || canResolve) && (
<div className="rule-t space-y-2 px-4 py-3">
{replying && (
<Textarea value={body} onChange={(e) => setBody(e.target.value)} minRows={2} maxRows={10} aria-label="Reply" placeholder="Reply…" autoFocus />
)}
{error && <Alert>{error}</Alert>}
<div className="flex flex-wrap justify-end gap-2">
{canResolve && (
<Button type="button" disabled={pending} icon={resolved ? undefined : <CheckIcon />} onClick={() => send({ body: replying ? body : "", resolve: !resolved })}>
{resolved ? "Unresolve" : replying && body.trim() ? "Reply and Resolve" : "Resolve"}
</Button>
)}
{canReply &&
(replying ? (
<Button type="button" variant="primary" disabled={pending || !body.trim()} onClick={() => send({ body })}>
Reply
</Button>
) : (
<Button type="button" onClick={() => setReplying(true)}>
Reply
</Button>
))}
</div>
</div>
)}
</div>
)}
</Surface>
);
}