Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

PullDetailScreen.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.

PullDetailScreen.tsxBlame210 lines · 1 contributor
c53cf00Claude1import React from 'react';
2import {
3 View,
4 Text,
5 ScrollView,
6 StyleSheet,
7} from 'react-native';
8import { SafeAreaView } from 'react-native-safe-area-context';
9import { type RouteProp } from '@react-navigation/native';
10import { colors } from '../theme/colors';
11import { fontSizes, fontWeights, fonts } from '../theme/typography';
12import { usePull } from '../hooks/usePulls';
13import { LoadingSpinner } from '../components/LoadingSpinner';
14import { ErrorState } from '../components/ErrorState';
9d7a803Claude15import { type MainStackParamList } from '../navigation/types';
c53cf00Claude16
17type Props = {
18 route: RouteProp<MainStackParamList, 'PullDetail'>;
19};
20
21function stateColor(state: string): string {
22 switch (state) {
23 case 'open': return colors.green;
24 case 'merged': return colors.accent;
25 case 'closed': return colors.red;
26 default: return colors.textMuted;
27 }
28}
29
30function stateLabel(state: string): string {
31 switch (state) {
32 case 'open': return 'Open';
33 case 'merged': return 'Merged';
34 case 'closed': return 'Closed';
35 default: return state;
36 }
37}
38
39function timeAgo(dateStr: string): string {
40 const diff = Date.now() - new Date(dateStr).getTime();
41 const minutes = Math.floor(diff / 60000);
42 if (minutes < 60) return `${minutes}m ago`;
43 const hours = Math.floor(minutes / 60);
44 if (hours < 24) return `${hours}h ago`;
45 const days = Math.floor(hours / 24);
46 return `${days}d ago`;
47}
48
49function renderBody(text: string | null) {
50 if (!text) return null;
51 return text.split('\n').map((line, i) => {
52 if (!line.trim()) return <Text key={i} style={{ height: 8 }} />;
53 return <Text key={i} style={styles.bodyLine}>{line}</Text>;
54 });
55}
56
57export function PullDetailScreen({ route }: Props) {
58 const { owner, repo, number } = route.params;
59 const { pull, comments, loading, error, refresh } = usePull(owner, repo, number);
60
61 if (loading) return <LoadingSpinner fullScreen />;
62 if (error || !pull) return <ErrorState message={error || 'PR not found'} onRetry={refresh} />;
63
64 const sc = stateColor(pull.state);
65
66 return (
67 <SafeAreaView style={styles.safe} edges={['top']}>
68 <ScrollView style={styles.scroll} contentContainerStyle={styles.content}>
69 {/* Header */}
70 <View style={styles.prHeader}>
71 <View style={[styles.stateBadge, { backgroundColor: sc + '22', borderColor: sc }]}>
72 <Text style={[styles.stateText, { color: sc }]}>{stateLabel(pull.state)}</Text>
73 </View>
74 <Text style={styles.prNum}>#{pull.number}</Text>
75 </View>
76
77 <Text style={styles.title}>{pull.title}</Text>
78
79 <View style={styles.branchRow}>
80 <Text style={styles.branch}>{pull.headBranch}</Text>
81 <Text style={styles.branchArrow}> → </Text>
82 <Text style={styles.branch}>{pull.baseBranch}</Text>
83 </View>
84
85 <Text style={styles.meta}>
86 opened {timeAgo(pull.createdAt)}
87 {pull.mergedAt ? ` · merged ${timeAgo(pull.mergedAt)}` : ''}
88 {pull.closedAt && !pull.mergedAt ? ` · closed ${timeAgo(pull.closedAt)}` : ''}
89 </Text>
90
91 {/* Description */}
92 {pull.body ? (
93 <View style={styles.bodyWrap}>
94 {renderBody(pull.body)}
95 </View>
96 ) : (
97 <Text style={styles.noBody}>No description provided.</Text>
98 )}
99
100 {/* Comments */}
101 {comments.length > 0 && (
102 <View style={styles.commentsSection}>
103 <Text style={styles.commentsTitle}>
104 {comments.length} comment{comments.length !== 1 ? 's' : ''}
105 </Text>
106 {comments.map((c) => (
107 <View key={c.id} style={[styles.commentCard, c.isAiReview && styles.aiCommentCard]}>
108 <View style={styles.commentHeader}>
109 <Text style={styles.commentAuthor}>
110 {c.isAiReview ? '⬡ AI Review' : (c.author?.username ?? 'Unknown')}
111 </Text>
112 <Text style={styles.commentTime}>{timeAgo(c.createdAt)}</Text>
113 </View>
114 {c.filePath && (
115 <Text style={styles.filePath}>
116 {c.filePath}{c.line != null ? `:${c.line}` : ''}
117 </Text>
118 )}
119 <Text style={styles.commentBody}>{c.body}</Text>
120 </View>
121 ))}
122 </View>
123 )}
124 </ScrollView>
125 </SafeAreaView>
126 );
127}
128
129const styles = StyleSheet.create({
130 safe: { flex: 1, backgroundColor: colors.bg },
131 scroll: { flex: 1 },
132 content: { padding: 16, paddingBottom: 32 },
133 prHeader: {
134 flexDirection: 'row',
135 alignItems: 'center',
136 gap: 10,
137 marginBottom: 10,
138 },
139 stateBadge: {
140 borderRadius: 12,
141 paddingHorizontal: 10,
142 paddingVertical: 4,
143 borderWidth: 1,
144 },
145 stateText: { fontSize: fontSizes.sm, fontWeight: fontWeights.medium },
146 prNum: { color: colors.textMuted, fontSize: fontSizes.sm },
147 title: {
148 fontSize: fontSizes.xl,
149 fontWeight: fontWeights.bold,
150 color: colors.text,
151 lineHeight: 28,
152 marginBottom: 8,
153 },
154 branchRow: {
155 flexDirection: 'row',
156 alignItems: 'center',
157 marginBottom: 6,
158 flexWrap: 'wrap',
159 },
160 branch: {
161 color: colors.accent,
162 fontSize: fontSizes.sm,
163 fontFamily: fonts.mono,
164 backgroundColor: colors.accentDim,
165 paddingHorizontal: 6,
166 paddingVertical: 2,
167 borderRadius: 4,
168 },
169 branchArrow: { color: colors.textMuted, fontSize: fontSizes.sm, marginHorizontal: 4 },
170 meta: { color: colors.textMuted, fontSize: fontSizes.sm, marginBottom: 16 },
171 bodyWrap: {
172 backgroundColor: colors.bgSurface,
173 borderRadius: 8,
174 padding: 14,
175 marginBottom: 24,
176 borderWidth: 1,
177 borderColor: colors.border,
178 },
179 bodyLine: { color: colors.text, fontSize: fontSizes.sm, lineHeight: 20 },
180 noBody: { color: colors.textMuted, fontSize: fontSizes.sm, fontStyle: 'italic', marginBottom: 24 },
181 commentsSection: { borderTopWidth: 1, borderTopColor: colors.border, paddingTop: 20 },
182 commentsTitle: { color: colors.textMuted, fontSize: fontSizes.sm, fontWeight: fontWeights.medium, marginBottom: 14 },
183 commentCard: {
184 backgroundColor: colors.bgSurface,
185 borderRadius: 8,
186 padding: 12,
187 marginBottom: 12,
188 borderWidth: 1,
189 borderColor: colors.border,
190 },
191 aiCommentCard: {
192 borderColor: colors.accent + '55',
193 backgroundColor: colors.accentDim,
194 },
195 commentHeader: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 6 },
196 commentAuthor: { color: colors.accent, fontSize: fontSizes.sm, fontWeight: fontWeights.medium },
197 commentTime: { color: colors.textMuted, fontSize: fontSizes.xs },
198 filePath: {
199 color: colors.textMuted,
200 fontSize: fontSizes.xs,
201 fontFamily: fonts.mono,
202 marginBottom: 6,
203 backgroundColor: colors.bgSecondary,
204 paddingHorizontal: 6,
205 paddingVertical: 2,
206 borderRadius: 4,
207 alignSelf: 'flex-start',
208 },
209 commentBody: { color: colors.text, fontSize: fontSizes.sm, lineHeight: 20 },
210});