• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

agentic-dev-library / thumbcode / 21120323096

18 Jan 2026 11:16PM UTC coverage: 25.507% (-0.02%) from 25.529%
21120323096

Pull #57

github

web-flow
Merge cc03ec2b2 into 79c7ce934
Pull Request #57: Implement Comprehensive Accessibility Features

260 of 1595 branches covered (16.3%)

Branch coverage included in aggregate %.

0 of 29 new or added lines in 5 files covered. (0.0%)

2 existing lines in 2 files now uncovered.

621 of 1859 relevant lines covered (33.41%)

1.48 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/components/chat/ThreadList.tsx
1
/**
2
 * Thread List Component
3
 *
4
 * Displays a list of chat threads with pinned threads at the top.
5
 * Uses organic styling with visual indicators for unread messages.
6
 */
7

8
import {
9
  type ChatThread,
10
  selectPinnedThreads,
11
  selectRecentThreads,
12
  useChatStore,
13
} from '@thumbcode/state';
14
import { FlatList, Pressable, Text, View } from 'react-native';
15

16
interface ThreadListProps {
17
  onSelectThread: (threadId: string) => void;
18
  onCreateThread?: () => void;
19
}
20

21
interface ThreadItemProps {
22
  thread: ChatThread;
23
  onPress: () => void;
24
}
25

26
/**
27
 * Format relative time for thread
28
 */
29
function formatRelativeTime(timestamp: string): string {
30
  const date = new Date(timestamp);
×
31
  const now = new Date();
×
32
  const diff = now.getTime() - date.getTime();
×
33

34
  const minutes = Math.floor(diff / 60000);
×
35
  const hours = Math.floor(minutes / 60);
×
36
  const days = Math.floor(hours / 24);
×
37

38
  if (minutes < 1) return 'Just now';
×
39
  if (minutes < 60) return `${minutes}m ago`;
×
40
  if (hours < 24) return `${hours}h ago`;
×
41
  if (days < 7) return `${days}d ago`;
×
42
  return date.toLocaleDateString();
×
43
}
44

45
/**
46
 * Get participant badge colors
47
 */
48
function getParticipantBadge(participant: ChatThread['participants'][number]) {
49
  const colorMap: Record<string, string> = {
×
50
    architect: 'bg-coral-500',
51
    implementer: 'bg-gold-500',
52
    reviewer: 'bg-teal-500',
53
    tester: 'bg-neutral-500',
54
  };
55
  return colorMap[participant] || 'bg-neutral-600';
×
56
}
57

58
function ThreadItem({ thread, onPress }: ThreadItemProps) {
59
  const hasUnread = thread.unreadCount > 0;
×
NEW
60
  const accessibilityLabel = [thread.title, hasUnread ? `${thread.unreadCount} unread` : '']
×
61
    .filter(Boolean)
62
    .join(', ');
63

64
  return (
×
65
    <Pressable
66
      onPress={onPress}
67
      className="bg-surface-elevated p-4 mb-2 active:bg-neutral-700"
68
      accessibilityRole="button"
69
      accessibilityLabel={accessibilityLabel}
70
      accessibilityHint="Open this thread"
71
      style={{
72
        borderRadius: '14px 12px 16px 10px',
73
        transform: [{ rotate: '-0.2deg' }],
74
      }}
75
    >
76
      <View className="flex-row items-start justify-between">
77
        <View className="flex-1 mr-3">
78
          {/* Title with unread indicator */}
79
          <View className="flex-row items-center mb-1">
80
            {thread.isPinned && <Text className="mr-1">📌</Text>}
×
81
            <Text
82
              className={`font-display text-base ${hasUnread ? 'text-white' : 'text-neutral-200'}`}
×
83
              numberOfLines={1}
84
            >
85
              {thread.title}
86
            </Text>
87
          </View>
88

89
          {/* Participants */}
90
          <View className="flex-row items-center mb-1">
91
            {thread.participants
92
              .filter((p) => p !== 'user')
×
93
              .slice(0, 3)
94
              .map((participant, index) => (
95
                <View
×
96
                  key={participant}
97
                  className={`w-2 h-2 rounded-full ${getParticipantBadge(participant)} ${
98
                    index > 0 ? 'ml-1' : ''
×
99
                  }`}
100
                />
101
              ))}
102
            {thread.participants.length > 4 && (
×
103
              <Text className="text-xs text-neutral-500 ml-1">
104
                +{thread.participants.length - 4}
105
              </Text>
106
            )}
107
          </View>
108

109
          {/* Timestamp */}
110
          <Text className="text-xs text-neutral-500 font-body">
111
            {formatRelativeTime(thread.lastMessageAt)}
112
          </Text>
113
        </View>
114

115
        {/* Unread badge */}
116
        {hasUnread && (
×
117
          <View
118
            className="bg-coral-500 px-2 py-0.5 min-w-[20px] items-center"
119
            style={{ borderRadius: '8px 10px 8px 12px' }}
120
          >
121
            <Text className="text-xs font-body text-white font-semibold">
122
              {thread.unreadCount > 99 ? '99+' : thread.unreadCount}
×
123
            </Text>
124
          </View>
125
        )}
126
      </View>
127
    </Pressable>
128
  );
129
}
130

131
export function ThreadList({ onSelectThread, onCreateThread }: ThreadListProps) {
132
  const pinnedThreads = useChatStore(selectPinnedThreads);
×
133
  const recentThreads = useChatStore(selectRecentThreads);
×
134

135
  const allThreads = [...pinnedThreads, ...recentThreads];
×
136

137
  if (allThreads.length === 0) {
×
138
    return (
×
139
      <View className="flex-1 items-center justify-center p-6">
140
        <Text className="font-display text-lg text-neutral-400 text-center mb-2">
141
          No conversations yet
142
        </Text>
143
        <Text className="font-body text-sm text-neutral-500 text-center mb-4">
144
          Start a new thread to collaborate with AI agents
145
        </Text>
146
        {onCreateThread && (
×
147
          <Pressable
148
            onPress={onCreateThread}
149
            className="bg-coral-500 px-6 py-3 active:bg-coral-600"
150
            style={{ borderRadius: '12px 14px 10px 16px' }}
151
            accessibilityRole="button"
152
            accessibilityLabel="New Thread"
153
            accessibilityHint="Create a new chat thread"
154
          >
155
            <Text className="font-body text-white font-semibold">New Thread</Text>
156
          </Pressable>
157
        )}
158
      </View>
159
    );
160
  }
161

162
  return (
×
163
    <View className="flex-1">
164
      {/* Header with new thread button */}
165
      <View className="flex-row justify-between items-center px-4 py-3 border-b border-neutral-700">
166
        <Text className="font-display text-lg text-white">Conversations</Text>
167
        {onCreateThread && (
×
168
          <Pressable
169
            onPress={onCreateThread}
170
            className="bg-teal-600 px-3 py-1.5 active:bg-teal-700"
171
            style={{ borderRadius: '8px 10px 6px 12px' }}
172
            accessibilityRole="button"
173
            accessibilityLabel="New Thread"
174
            accessibilityHint="Create a new chat thread"
175
          >
176
            <Text className="font-body text-sm text-white font-semibold">+ New</Text>
177
          </Pressable>
178
        )}
179
      </View>
180

181
      {/* Pinned section */}
182
      {pinnedThreads.length > 0 && (
×
183
        <View className="mb-2">
184
          <Text className="px-4 py-2 text-xs text-neutral-500 font-body uppercase tracking-wider">
185
            Pinned
186
          </Text>
187
          {pinnedThreads.map((thread) => (
188
            <View key={thread.id} className="px-3">
×
189
              <ThreadItem thread={thread} onPress={() => onSelectThread(thread.id)} />
×
190
            </View>
191
          ))}
192
        </View>
193
      )}
194

195
      {/* Recent threads */}
196
      <FlatList
197
        data={recentThreads}
198
        keyExtractor={(item) => item.id}
×
199
        renderItem={({ item }) => (
200
          <View className="px-3">
×
201
            <ThreadItem thread={item} onPress={() => onSelectThread(item.id)} />
×
202
          </View>
203
        )}
204
        ListHeaderComponent={
205
          recentThreads.length > 0 ? (
×
206
            <Text className="px-4 py-2 text-xs text-neutral-500 font-body uppercase tracking-wider">
207
              Recent
208
            </Text>
209
          ) : null
210
        }
211
        showsVerticalScrollIndicator={false}
212
        contentContainerStyle={{ paddingBottom: 16 }}
213
      />
214
    </View>
215
  );
216
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc