import { useState } from "react"; import { Pressable, ScrollView, Text, View } from "react-native"; import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { ChevronDown, ChevronRight, CircleCheck, CircleDot, CircleSlash, CircleX, ExternalLink, GitMerge, GitPullRequest, GitPullRequestClosed, GitPullRequestDraft, MessageSquare, } from "lucide-react-native"; import { openExternalUrl } from "@/utils/open-external-url"; import type { CheckStatus, PrPaneActivity, PrPaneCheck, PrPaneData, PrState, } from "@/utils/pr-pane-data"; export function PrPane({ data }: { data: PrPaneData }) { const { theme } = useUnistyles(); const [checksOpen, setChecksOpen] = useState(true); const [reviewsOpen, setReviewsOpen] = useState(true); const passed = data.checks.filter((c) => c.status === "success").length; const failed = data.checks.filter((c) => c.status === "failure").length; const pending = data.checks.filter((c) => c.status === "pending").length; const approvals = data.activity.filter( (a) => a.kind === "review" && a.reviewState === "approved", ).length; const changesRequested = data.activity.filter( (a) => a.kind === "review" && a.reviewState === "changes_requested", ).length; const commentCount = data.activity.filter( (a) => a.kind === "comment" || (a.kind === "review" && a.reviewState === "commented"), ).length; const stateColor = getStateColor(data.state, theme); const StateIcon = getStateIcon(data.state); const stateLabel = getStateLabel(data.state); return ( void openExternalUrl(data.url)} style={styles.header}> {({ hovered }) => ( <> {stateLabel} {data.title} {hovered ? ( {" "} ) : null} )}
setChecksOpen((o) => !o)} summary={ <> } /> } /> } /> } > {data.checks.map((check, idx) => ( ))}
setReviewsOpen((o) => !o)} summary={ <> } /> } /> } /> } > {data.activity.map((item, idx) => ( ))}
); } interface SectionProps { title: string; open: boolean; onToggle: () => void; summary: React.ReactNode; children: React.ReactNode; } function Section({ title, open, onToggle, summary, children }: SectionProps) { const { theme } = useUnistyles(); return ( {open ? ( ) : ( )} {title} {summary} {open && ( {children} )} ); } function SummaryPill({ count, color, icon, }: { count: number; color: string; icon: React.ReactNode; }) { if (count === 0) return null; return ( {icon} {count} ); } function CheckRow({ check }: { check: PrPaneCheck }) { return ( void openExternalUrl(check.url)} style={({ hovered }) => [styles.row, hovered && styles.hoverable]} > {check.name} {check.workflow && ( {check.workflow} )} {check.duration && {check.duration}} ); } function CheckStatusIcon({ status }: { status: CheckStatus }) { const { theme } = useUnistyles(); if (status === "success") return ; if (status === "failure") return ; if (status === "pending") return ; return ; } function ActivityRow({ item }: { item: PrPaneActivity }) { const verb = getActivityVerb(item); return ( void openExternalUrl(item.url)} style={({ hovered }) => [styles.activityRow, hovered && styles.hoverable]} > {item.author.slice(0, 1).toUpperCase()} {item.author} {verb} {item.age} {item.body} ); } function getActivityVerb(item: PrPaneActivity): string { if (item.kind === "comment") return "Commented"; if (item.reviewState === "approved") return "Approved"; if (item.reviewState === "changes_requested") return "Requested changes"; return "Reviewed"; } function getStateColor(state: PrState, theme: ReturnType["theme"]): string { if (state === "open") return theme.colors.statusSuccess; if (state === "draft") return theme.colors.foregroundMuted; if (state === "merged") return theme.colors.statusMerged; return theme.colors.statusDanger; } function getStateIcon(state: PrState) { if (state === "draft") return GitPullRequestDraft; if (state === "merged") return GitMerge; if (state === "closed") return GitPullRequestClosed; return GitPullRequest; } function getStateLabel(state: PrState): string { if (state === "draft") return "Draft"; if (state === "merged") return "Merged"; if (state === "closed") return "Closed"; return "Open"; } const styles = StyleSheet.create((theme) => ({ root: { flex: 1, minHeight: 0, backgroundColor: theme.colors.surfaceSidebar, }, hoverable: { backgroundColor: theme.colors.surfaceSidebarHover, }, header: { flexDirection: "column", gap: theme.spacing[1], paddingHorizontal: theme.spacing[4], paddingVertical: theme.spacing[4], }, stateLine: { flexDirection: "row", alignItems: "center", gap: theme.spacing[1], }, stateLabel: { fontSize: theme.fontSize.xs, fontWeight: theme.fontWeight.normal, }, title: { fontSize: theme.fontSize.sm, fontWeight: theme.fontWeight.normal, color: theme.colors.foreground, lineHeight: 19, }, divider: { height: 1, backgroundColor: theme.colors.border, }, sectionOpen: { flexShrink: 1, minHeight: 0, }, sectionBody: { flexShrink: 1, minHeight: 0, }, sectionBodyContent: { paddingBottom: theme.spacing[3], }, sectionHeader: { flexDirection: "row", alignItems: "center", gap: theme.spacing[2], paddingHorizontal: theme.spacing[3], paddingVertical: theme.spacing[2], }, sectionTitle: { fontSize: theme.fontSize.sm, fontWeight: theme.fontWeight.normal, color: theme.colors.foregroundMuted, }, summaryWrap: { marginLeft: "auto", flexDirection: "row", alignItems: "center", gap: theme.spacing[2], }, summaryPill: { flexDirection: "row", alignItems: "center", gap: 3, }, summaryPillText: { fontSize: theme.fontSize.xs, fontWeight: theme.fontWeight.normal, }, row: { flexDirection: "row", alignItems: "center", gap: theme.spacing[2], paddingHorizontal: theme.spacing[3], paddingVertical: theme.spacing[2], }, activityRow: { flexDirection: "row", alignItems: "flex-start", gap: theme.spacing[2], paddingHorizontal: theme.spacing[3], paddingVertical: theme.spacing[2], }, activityMain: { flex: 1, minWidth: 0, gap: 2 }, activityHeader: { flexDirection: "row", alignItems: "center", gap: theme.spacing[2], }, rowTitle: { fontSize: theme.fontSize.sm, fontWeight: theme.fontWeight.normal, color: theme.colors.foreground, flexShrink: 1, }, rowMeta: { fontSize: theme.fontSize.xs, color: theme.colors.foregroundMuted, marginLeft: "auto", }, rowMetaMid: { fontSize: theme.fontSize.xs, color: theme.colors.foregroundMuted, flexShrink: 1, }, rowBody: { fontSize: theme.fontSize.xs, color: theme.colors.foregroundMuted, lineHeight: 16, }, avatar: { width: 20, height: 20, borderRadius: 10, alignItems: "center", justifyContent: "center", marginTop: 1, }, avatarText: { fontSize: 10, fontWeight: theme.fontWeight.normal, color: "#fff", }, }));