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

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

IssueDetailScreen.tsxBlame257 lines · 1 contributor
c53cf00Claude1import React, { useState, useRef } from 'react';
2import {
3 View,
4 Text,
5 ScrollView,
6 StyleSheet,
7 TextInput,
8 TouchableOpacity,
9 ActivityIndicator,
10 KeyboardAvoidingView,
11 Platform,
12 Alert,
13} from 'react-native';
14import { SafeAreaView } from 'react-native-safe-area-context';
15import { type RouteProp } from '@react-navigation/native';
16import { colors } from '../theme/colors';
17import { fontSizes, fontWeights } from '../theme/typography';
18import { useIssue } from '../hooks/useIssues';
19import { LoadingSpinner } from '../components/LoadingSpinner';
20import { ErrorState } from '../components/ErrorState';
9d7a803Claude21import { type MainStackParamList } from '../navigation/types';
c53cf00Claude22
23type Props = {
24 route: RouteProp<MainStackParamList, 'IssueDetail'>;
25};
26
27function renderBody(text: string | null) {
28 if (!text) return null;
29 // Simple markdown-ish rendering: bold, italic, code spans
30 const lines = text.split('\n');
31 return lines.map((line, i) => {
32 // Headings
33 if (line.startsWith('### ')) {
34 return <Text key={i} style={styles.h3}>{line.slice(4)}</Text>;
35 }
36 if (line.startsWith('## ')) {
37 return <Text key={i} style={styles.h2}>{line.slice(3)}</Text>;
38 }
39 if (line.startsWith('# ')) {
40 return <Text key={i} style={styles.h1}>{line.slice(2)}</Text>;
41 }
42 // Code block lines
43 if (line.startsWith('```') || line.startsWith(' ')) {
44 return <Text key={i} style={styles.codeLine}>{line}</Text>;
45 }
46 if (!line.trim()) {
47 return <Text key={i} style={styles.emptyLine}>{'\n'}</Text>;
48 }
49 return <Text key={i} style={styles.bodyLine}>{line}</Text>;
50 });
51}
52
53function timeAgo(dateStr: string): string {
54 const diff = Date.now() - new Date(dateStr).getTime();
55 const minutes = Math.floor(diff / 60000);
56 if (minutes < 60) return `${minutes}m ago`;
57 const hours = Math.floor(minutes / 60);
58 if (hours < 24) return `${hours}h ago`;
59 const days = Math.floor(hours / 24);
60 return `${days}d ago`;
61}
62
63export function IssueDetailScreen({ route }: Props) {
64 const { owner, repo, number } = route.params;
65 const { issue, comments, loading, error, submitting, refresh, addComment } = useIssue(owner, repo, number);
66 const [commentText, setCommentText] = useState('');
67 const scrollRef = useRef<ScrollView>(null);
68
69 async function handleSubmit() {
70 const trimmed = commentText.trim();
71 if (!trimmed) return;
72 try {
73 await addComment(trimmed);
74 setCommentText('');
75 scrollRef.current?.scrollToEnd({ animated: true });
76 } catch (err) {
77 Alert.alert('Error', err instanceof Error ? err.message : 'Failed to post comment');
78 }
79 }
80
81 if (loading) return <LoadingSpinner fullScreen />;
82 if (error || !issue) return <ErrorState message={error || 'Issue not found'} onRetry={refresh} />;
83
84 const isOpen = issue.state === 'open';
85
86 return (
87 <SafeAreaView style={styles.safe} edges={['top', 'bottom']}>
88 <KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : undefined} style={styles.kav}>
89 <ScrollView ref={scrollRef} style={styles.scroll} contentContainerStyle={styles.content}>
90 {/* Issue header */}
91 <View style={styles.issueHeader}>
92 <View style={[styles.stateBadge, { backgroundColor: isOpen ? colors.green + '22' : colors.textMuted + '22' }]}>
93 <View style={[styles.stateDot, { backgroundColor: isOpen ? colors.green : colors.textMuted }]} />
94 <Text style={[styles.stateText, { color: isOpen ? colors.green : colors.textMuted }]}>
95 {isOpen ? 'Open' : 'Closed'}
96 </Text>
97 </View>
98 <Text style={styles.number}>#{issue.number}</Text>
99 </View>
100
101 <Text style={styles.title}>{issue.title}</Text>
102
103 <Text style={styles.meta}>
104 opened {timeAgo(issue.createdAt)}
105 </Text>
106
107 {/* Body */}
108 {issue.body ? (
109 <View style={styles.bodyWrap}>
110 {renderBody(issue.body)}
111 </View>
112 ) : (
113 <Text style={styles.noBody}>No description provided.</Text>
114 )}
115
116 {/* Comments */}
117 {comments.length > 0 && (
118 <View style={styles.commentsSection}>
119 <Text style={styles.commentsTitle}>{comments.length} comment{comments.length !== 1 ? 's' : ''}</Text>
120 {comments.map((comment) => (
121 <View key={comment.id} style={styles.commentCard}>
122 <View style={styles.commentHeader}>
123 <Text style={styles.commentAuthor}>
124 {comment.author?.username ?? 'Unknown'}
125 </Text>
126 <Text style={styles.commentTime}>{timeAgo(comment.createdAt)}</Text>
127 </View>
128 <View style={styles.commentBody}>
129 {renderBody(comment.body)}
130 </View>
131 </View>
132 ))}
133 </View>
134 )}
135 </ScrollView>
136
137 {/* Comment composer */}
138 <View style={styles.composer}>
139 <TextInput
140 style={styles.composerInput}
141 value={commentText}
142 onChangeText={setCommentText}
143 placeholder="Leave a comment..."
144 placeholderTextColor={colors.textMuted}
145 multiline
146 maxLength={65536}
147 />
148 <TouchableOpacity
149 style={[styles.sendBtn, (!commentText.trim() || submitting) && styles.sendBtnDisabled]}
150 onPress={handleSubmit}
151 disabled={!commentText.trim() || submitting}
152 activeOpacity={0.8}
153 >
154 {submitting ? (
155 <ActivityIndicator color={colors.white} size="small" />
156 ) : (
157 <Text style={styles.sendBtnText}>Comment</Text>
158 )}
159 </TouchableOpacity>
160 </View>
161 </KeyboardAvoidingView>
162 </SafeAreaView>
163 );
164}
165
166const styles = StyleSheet.create({
167 safe: { flex: 1, backgroundColor: colors.bg },
168 kav: { flex: 1 },
169 scroll: { flex: 1 },
170 content: { padding: 16, paddingBottom: 8 },
171 issueHeader: {
172 flexDirection: 'row',
173 alignItems: 'center',
174 gap: 10,
175 marginBottom: 10,
176 },
177 stateBadge: {
178 flexDirection: 'row',
179 alignItems: 'center',
180 gap: 6,
181 borderRadius: 12,
182 paddingHorizontal: 10,
183 paddingVertical: 4,
184 },
185 stateDot: { width: 8, height: 8, borderRadius: 4 },
186 stateText: { fontSize: fontSizes.sm, fontWeight: fontWeights.medium },
187 number: { color: colors.textMuted, fontSize: fontSizes.sm },
188 title: {
189 fontSize: fontSizes.xl,
190 fontWeight: fontWeights.bold,
191 color: colors.text,
192 lineHeight: 28,
193 marginBottom: 6,
194 },
195 meta: { color: colors.textMuted, fontSize: fontSizes.sm, marginBottom: 16 },
196 bodyWrap: {
197 backgroundColor: colors.bgSurface,
198 borderRadius: 8,
199 padding: 14,
200 marginBottom: 24,
201 borderWidth: 1,
202 borderColor: colors.border,
203 },
204 noBody: { color: colors.textMuted, fontSize: fontSizes.sm, fontStyle: 'italic', marginBottom: 24 },
205 h1: { color: colors.text, fontSize: fontSizes.xl, fontWeight: fontWeights.bold, marginBottom: 6 },
206 h2: { color: colors.text, fontSize: fontSizes.lg, fontWeight: fontWeights.bold, marginBottom: 4 },
207 h3: { color: colors.text, fontSize: fontSizes.md, fontWeight: fontWeights.semibold, marginBottom: 3 },
208 bodyLine: { color: colors.text, fontSize: fontSizes.sm, lineHeight: 20 },
209 codeLine: { color: colors.green, fontSize: fontSizes.xs, fontFamily: 'monospace', backgroundColor: colors.bgSecondary, paddingHorizontal: 4 },
210 emptyLine: { height: 8 },
211 commentsSection: { borderTopWidth: 1, borderTopColor: colors.border, paddingTop: 20 },
212 commentsTitle: { color: colors.textMuted, fontSize: fontSizes.sm, fontWeight: fontWeights.medium, marginBottom: 14 },
213 commentCard: {
214 backgroundColor: colors.bgSurface,
215 borderRadius: 8,
216 padding: 12,
217 marginBottom: 12,
218 borderWidth: 1,
219 borderColor: colors.border,
220 },
221 commentHeader: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 8 },
222 commentAuthor: { color: colors.accent, fontSize: fontSizes.sm, fontWeight: fontWeights.medium },
223 commentTime: { color: colors.textMuted, fontSize: fontSizes.xs },
224 commentBody: {},
225 composer: {
226 flexDirection: 'row',
227 alignItems: 'flex-end',
228 padding: 12,
229 borderTopWidth: 1,
230 borderTopColor: colors.border,
231 gap: 10,
232 backgroundColor: colors.bgSecondary,
233 },
234 composerInput: {
235 flex: 1,
236 backgroundColor: colors.bgSurface,
237 borderWidth: 1,
238 borderColor: colors.border,
239 borderRadius: 8,
240 paddingHorizontal: 12,
241 paddingVertical: 10,
242 color: colors.text,
243 fontSize: fontSizes.sm,
244 maxHeight: 120,
245 minHeight: 40,
246 },
247 sendBtn: {
248 backgroundColor: colors.accent,
249 borderRadius: 8,
250 paddingHorizontal: 16,
251 paddingVertical: 10,
252 minHeight: 40,
253 justifyContent: 'center',
254 },
255 sendBtnDisabled: { opacity: 0.5 },
256 sendBtnText: { color: colors.white, fontSize: fontSizes.sm, fontWeight: fontWeights.semibold },
257});