Skip to content
100 linesCodeBlameRaw
1import { Stack, router, useLocalSearchParams } from "expo-router";
2import { CircleDot, GitPullRequest } from "lucide-react-native";
3import { useState } from "react";
4import { FlatList, RefreshControl, View } from "react-native";
5
6import { Checks, IssueIcon, PullIcon } from "@/components/code";
7import { Segments } from "@/components/segments";
8import { Empty, ErrorView, Loading, Row, Screen, Text } from "@/components/ui";
9import { type ApiIssue, type ApiPull, useIssues, usePulls, useRepo } from "@/lib/code";
10import { ago } from "@/lib/time";
11import { space, useColors } from "@/theme";
12
13type Tab = "pulls" | "issues";
14type State = "open" | "closed";
15
16/** A repository: its pull requests and issues. */
17export default function Repository() {
18 const { owner, repo } = useLocalSearchParams<{ owner: string; repo: string }>();
19 const [tab, setTab] = useState<Tab>("pulls");
20 const [state, setState] = useState<State>("open");
21 const details = useRepo(owner, repo);
22 const pulls = usePulls(owner, repo, state);
23 const issues = useIssues(owner, repo, state);
24 const list = tab === "pulls" ? pulls : issues;
25 const colors = useColors();
26
27 return (
28 <Screen>
29 <Stack.Screen options={{ title: repo }} />
30 <View style={{ padding: space.lg, gap: space.md }}>
31 <Text size="small" tone="muted">
32 {owner}/{repo}
33 {details.data?.description ? ` · ${details.data.description}` : ""}
34 </Text>
35 <Segments
36 value={tab}
37 onChange={setTab}
38 options={[
39 { value: "pulls", label: "Pull requests" },
40 { value: "issues", label: "Issues" },
41 ]}
42 />
43 <Segments
44 value={state}
45 onChange={setState}
46 options={[
47 { value: "open", label: "Open" },
48 { value: "closed", label: "Closed" },
49 ]}
50 />
51 </View>
52 {list.isPending ? (
53 <Loading />
54 ) : list.error ? (
55 <ErrorView error={list.error} retry={() => list.refetch()} />
56 ) : tab === "pulls" ? (
57 <FlatList
58 data={pulls.data}
59 keyExtractor={(pull) => pull.id}
60 refreshControl={<RefreshControl refreshing={pulls.isRefetching} onRefresh={() => pulls.refetch()} tintColor={colors.accent} />}
61 ListEmptyComponent={<Empty icon={GitPullRequest} title={`No ${state} pull requests`} />}
62 renderItem={({ item }) => <PullRow pull={item} owner={owner} repo={repo} />}
63 />
64 ) : (
65 <FlatList
66 data={issues.data}
67 keyExtractor={(issue) => issue.id}
68 refreshControl={<RefreshControl refreshing={issues.isRefetching} onRefresh={() => issues.refetch()} tintColor={colors.accent} />}
69 ListEmptyComponent={<Empty icon={CircleDot} title={`No ${state} issues`} />}
70 renderItem={({ item }) => <IssueRow issue={item} owner={owner} repo={repo} />}
71 />
72 )}
73 </Screen>
74 );
75}
76
77function PullRow({ pull, owner, repo }: { pull: ApiPull; owner: string; repo: string }) {
78 const by = pull.author.display_username ?? pull.author.username;
79 return (
80 <Row
81 leading={<PullIcon pull={pull} />}
82 title={pull.title}
83 subtitle={`#${pull.number} · ${by}${pull.agent ? ` with ${pull.agent}` : ""} · ${ago(pull.updated_at)}`}
84 trailing={<Checks status={pull.check_status} />}
85 onPress={() => router.push({ pathname: "/code/[owner]/[repo]/pull/[number]", params: { owner, repo, number: String(pull.number) } })}
86 />
87 );
88}
89
90function IssueRow({ issue, owner, repo }: { issue: ApiIssue; owner: string; repo: string }) {
91 const by = issue.author.display_username ?? issue.author.username;
92 return (
93 <Row
94 leading={<IssueIcon issue={issue} />}
95 title={issue.title}
96 subtitle={`#${issue.number} · ${by} · ${ago(issue.updated_at)}${issue.comment_count ? ` · ${issue.comment_count} comments` : ""}`}
97 onPress={() => router.push({ pathname: "/code/[owner]/[repo]/issues/[number]", params: { owner, repo, number: String(issue.number) } })}
98 />
99 );
100}