import type { PullRequestActor, PullRequestDetailView } from "@t3tools/contracts";
import {
ChevronDownIcon,
ExternalLinkIcon,
FileCode2Icon,
GitCommitHorizontalIcon,
GitMergeIcon,
GitPullRequestClosedIcon,
GitPullRequestIcon,
MessageSquareIcon,
} from "lucide-react";
import { useState, type ReactNode } from "react";
import { cn } from "~/lib/utils";
import { readLocalApi } from "~/localApi";
import { formatRelativeTimeLabel } from "~/timestampFormat";
import { Button } from "../ui/button";
import { Collapsible, CollapsiblePanel, CollapsibleTrigger } from "../ui/collapsible";
import {
buildPullRequestTimeline,
groupPullRequestTimelineConversations,
type PullRequestTimelineEvent,
} from "./pullRequestDetail.logic";
import { PullRequestMarkdown } from "./PullRequestMarkdown";
import {
PullRequestActorAvatar,
PullRequestDiffStat,
PullRequestMetaLine,
} from "./pullRequestPresentation";
function TimelineBody({ body, markdown, cwd }: { body: string; markdown: boolean; cwd: string }) {
return (
{markdown ? (
) : (
{body}
)}
);
}
function ActorName({ actor }: { actor: PullRequestActor | null }) {
return {actor?.login ?? "ghost"};
}
function TimelineMarker({
children,
className,
}: {
children: ReactNode;
className?: string | undefined;
}) {
return (
{children}
);
}
function IconMarker({ icon, className }: { icon: ReactNode; className?: string | undefined }) {
return (
{icon}
);
}
function ActorTimelineMarker({
actors,
className,
fallback,
muted = false,
}: {
actors: ReadonlyArray;
className?: string | undefined;
fallback: ReactNode;
muted?: boolean;
}) {
const actor = actors[0];
return actor === undefined ? (
) : (
);
}
function friendlyReviewState(value: string): string {
const words = value.toLowerCase().replaceAll("_", " ").replaceAll("-", " ");
return words.replace(/^\w/u, (letter) => letter.toUpperCase());
}
function ReviewStateBadge({ state }: { state: string }) {
return (
{friendlyReviewState(state)}
);
}
function OpenOnHostButton({ url, onOpen }: { url: string | null; onOpen: (url: string) => void }) {
return url === null ? null : (
);
}
function ConversationCard({
event,
cwd,
onOpen,
}: {
event: PullRequestTimelineEvent;
cwd: string;
onOpen: (url: string) => void;
}) {
return (
{event.title}
{event.reviewState ?
: null}
{formatRelativeTimeLabel(event.at)}
{event.path ? (
{event.path}
) : null}
{event.body ? (
) : null}
);
}
function uniqueConversationActors(events: ReadonlyArray) {
const actors = new Map();
for (const event of events) {
const actor = event.actor;
if (actor !== null && !actors.has(actor.login)) actors.set(actor.login, actor);
}
return [...actors.values()];
}
function ConversationGroup({
events,
cwd,
onOpen,
}: {
events: ReadonlyArray;
cwd: string;
onOpen: (url: string) => void;
}) {
const [open, setOpen] = useState(false);
const actors = uniqueConversationActors(events);
const first = events[0];
if (first === undefined) return null;
return (
}
muted={!open}
/>
{events.length.toLocaleString()} {events.length === 1 ? "comment" : "comments"}
{actors.length.toLocaleString()} {actors.length === 1 ? "author" : "authors"} ยท{" "}
{formatRelativeTimeLabel(first.at)}
{open ? (
{events.map((event) => (
))}
) : null}
);
}
function CommitEvent({
event,
onOpen,
}: {
event: PullRequestTimelineEvent;
onOpen: (oid: string) => void;
}) {
return (
);
}
function LifecycleEvent({ event }: { event: PullRequestTimelineEvent }) {
const presentation =
event.kind === "opened"
? {
icon: ,
label: "Pull request opened",
}
: event.kind === "merged"
? {
icon: ,
label: "Pull request merged",
}
: {
icon: ,
label: "Pull request closed",
};
return (
{event.actor ?
: null}
{presentation.label}
{formatRelativeTimeLabel(event.at)}
);
}
export function PullRequestTimelineTab({
detail,
order,
onOpenCommit,
}: {
detail: PullRequestDetailView;
order: "newest" | "oldest";
onOpenCommit: (oid: string) => void;
}) {
const events = buildPullRequestTimeline(detail);
const orderedEvents = order === "newest" ? events : events.toReversed();
const rows = groupPullRequestTimelineConversations(orderedEvents);
const openOnHost = (url: string) => {
void readLocalApi()?.shell.openExternal(url);
};
return (
{rows.map((row) => {
if (row.kind === "comments") {
return (
);
}
const event = row.event;
if (event.kind === "commit") {
return ;
}
return ;
})}
{events.length === 0 ? (
) : null}
);
}