Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesPull RequestsActionsSecurityInsightsSettings
✨ AI
More
Blame · Line-by-line history

PullsScreen.tsx

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

PullsScreen.tsxBlame172 lines · 1 contributor
9a8fbedClaude1import React, { useCallback, useEffect, useState } from 'react';
2import {
3 FlatList,
4 StyleSheet,
5 Text,
6 TouchableOpacity,
7 View,
8} from 'react-native';
9import { SafeAreaView } from 'react-native-safe-area-context';
10import type { NativeStackScreenProps } from '@react-navigation/native-stack';
11import { listPullRequests } from '../api/pulls';
12import type { PullRequest } from '../api/pulls';
13import { PullRow } from '../components/PullRow';
14import { LoadingSpinner } from '../components/LoadingSpinner';
15import { ErrorBanner } from '../components/ErrorBanner';
16import { colors } from '../theme/colors';
17import type { RepoStackParamList } from '../navigation/AppNavigator';
18
19type Props = NativeStackScreenProps<RepoStackParamList, 'Pulls'>;
20type FilterState = 'open' | 'closed';
21
22export function PullsScreen({ route, navigation }: Props): React.ReactElement {
23 const { owner, repo } = route.params;
24 const [filter, setFilter] = useState<FilterState>('open');
25 const [pulls, setPulls] = useState<PullRequest[]>([]);
26 const [isLoading, setIsLoading] = useState(true);
27 const [error, setError] = useState<string | null>(null);
28 const [page, setPage] = useState(1);
29 const [hasMore, setHasMore] = useState(true);
30
31 useEffect(() => {
32 let cancelled = false;
33 setIsLoading(true);
34 setError(null);
35
36 listPullRequests(owner, repo, filter, 1)
37 .then((data) => {
38 if (!cancelled) {
39 setPulls(data);
40 setPage(1);
41 setHasMore(data.length === 30);
42 }
43 })
44 .catch((err) => {
45 if (!cancelled) {
46 setError(err instanceof Error ? err.message : 'Failed to load pull requests');
47 }
48 })
49 .finally(() => {
50 if (!cancelled) setIsLoading(false);
51 });
52
53 return () => {
54 cancelled = true;
55 };
56 }, [owner, repo, filter]);
57
58 const loadMore = useCallback(() => {
59 if (!hasMore || isLoading) return;
60 const nextPage = page + 1;
61 listPullRequests(owner, repo, filter, nextPage)
62 .then((data) => {
63 setPulls((prev) => [...prev, ...data]);
64 setPage(nextPage);
65 setHasMore(data.length === 30);
66 })
67 .catch(() => {/* ignore */});
68 }, [owner, repo, filter, page, hasMore, isLoading]);
69
70 const handlePrPress = useCallback(
71 (pr: PullRequest) => {
72 navigation.navigate('PullDetail', { owner, repo, number: pr.number });
73 },
74 [navigation, owner, repo],
75 );
76
77 const renderPr = useCallback(
78 ({ item }: { item: PullRequest }) => <PullRow pr={item} onPress={handlePrPress} />,
79 [handlePrPress],
80 );
81
82 const keyExtractor = useCallback((item: PullRequest) => String(item.id), []);
83
84 const renderFooter = useCallback(
85 () => (hasMore ? <LoadingSpinner size="small" /> : null),
86 [hasMore],
87 );
88
89 if (isLoading && pulls.length === 0) return <LoadingSpinner fullScreen />;
90
91 return (
92 <SafeAreaView style={styles.safe} edges={['bottom']}>
93 <View style={styles.filterTabs}>
94 <TouchableOpacity
95 style={[styles.filterTab, filter === 'open' && styles.filterTabActive]}
96 onPress={() => setFilter('open')}
97 >
98 <Text style={[styles.filterTabText, filter === 'open' && styles.filterTabTextActive]}>
99 Open
100 </Text>
101 </TouchableOpacity>
102 <TouchableOpacity
103 style={[styles.filterTab, filter === 'closed' && styles.filterTabActive]}
104 onPress={() => setFilter('closed')}
105 >
106 <Text style={[styles.filterTabText, filter === 'closed' && styles.filterTabTextActive]}>
107 Closed / Merged
108 </Text>
109 </TouchableOpacity>
110 </View>
111
112 {error !== null && <ErrorBanner message={error} />}
113
114 <FlatList
115 data={pulls}
116 keyExtractor={keyExtractor}
117 renderItem={renderPr}
118 onEndReached={loadMore}
119 onEndReachedThreshold={0.3}
120 ListFooterComponent={renderFooter}
121 ListEmptyComponent={
122 <View style={styles.emptyState}>
123 <Text style={styles.emptyText}>No {filter} pull requests</Text>
124 </View>
125 }
126 style={styles.list}
127 />
128 </SafeAreaView>
129 );
130}
131
132const styles = StyleSheet.create({
133 safe: {
134 flex: 1,
135 backgroundColor: colors.bg,
136 },
137 filterTabs: {
138 flexDirection: 'row',
139 backgroundColor: colors.bgSecondary,
140 borderBottomWidth: 1,
141 borderBottomColor: colors.border,
142 },
143 filterTab: {
144 flex: 1,
145 paddingVertical: 12,
146 alignItems: 'center',
147 borderBottomWidth: 2,
148 borderBottomColor: 'transparent',
149 },
150 filterTabActive: {
151 borderBottomColor: colors.accentBlue,
152 },
153 filterTabText: {
154 fontSize: 14,
155 fontWeight: '500',
156 color: colors.textMuted,
157 },
158 filterTabTextActive: {
159 color: colors.text,
160 },
161 list: {
162 flex: 1,
163 },
164 emptyState: {
165 padding: 40,
166 alignItems: 'center',
167 },
168 emptyText: {
169 fontSize: 14,
170 color: colors.textMuted,
171 },
172});