@parlr/react-native 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +918 -0
  3. package/lib/commonjs/components/AttachmentPicker.js +292 -0
  4. package/lib/commonjs/components/AttachmentPicker.js.map +1 -0
  5. package/lib/commonjs/components/AttachmentPreview.js +200 -0
  6. package/lib/commonjs/components/AttachmentPreview.js.map +1 -0
  7. package/lib/commonjs/components/ChatBubble.js +391 -0
  8. package/lib/commonjs/components/ChatBubble.js.map +1 -0
  9. package/lib/commonjs/components/EmptyState.js +115 -0
  10. package/lib/commonjs/components/EmptyState.js.map +1 -0
  11. package/lib/commonjs/components/ParlrChat.js +745 -0
  12. package/lib/commonjs/components/ParlrChat.js.map +1 -0
  13. package/lib/commonjs/components/ParlrConversationList.js +509 -0
  14. package/lib/commonjs/components/ParlrConversationList.js.map +1 -0
  15. package/lib/commonjs/components/PreChatForm.js +263 -0
  16. package/lib/commonjs/components/PreChatForm.js.map +1 -0
  17. package/lib/commonjs/components/RichMessage.js +284 -0
  18. package/lib/commonjs/components/RichMessage.js.map +1 -0
  19. package/lib/commonjs/components/SatisfactionSurvey.js +292 -0
  20. package/lib/commonjs/components/SatisfactionSurvey.js.map +1 -0
  21. package/lib/commonjs/components/TypingIndicator.js +86 -0
  22. package/lib/commonjs/components/TypingIndicator.js.map +1 -0
  23. package/lib/commonjs/core/api.js +310 -0
  24. package/lib/commonjs/core/api.js.map +1 -0
  25. package/lib/commonjs/core/config.js +40 -0
  26. package/lib/commonjs/core/config.js.map +1 -0
  27. package/lib/commonjs/core/errors.js +73 -0
  28. package/lib/commonjs/core/errors.js.map +1 -0
  29. package/lib/commonjs/core/offlineQueue.js +89 -0
  30. package/lib/commonjs/core/offlineQueue.js.map +1 -0
  31. package/lib/commonjs/core/pushNotifications.js +21 -0
  32. package/lib/commonjs/core/pushNotifications.js.map +1 -0
  33. package/lib/commonjs/core/session.js +130 -0
  34. package/lib/commonjs/core/session.js.map +1 -0
  35. package/lib/commonjs/core/theme.js +110 -0
  36. package/lib/commonjs/core/theme.js.map +1 -0
  37. package/lib/commonjs/core/types.js +6 -0
  38. package/lib/commonjs/core/types.js.map +1 -0
  39. package/lib/commonjs/core/websocket.js +245 -0
  40. package/lib/commonjs/core/websocket.js.map +1 -0
  41. package/lib/commonjs/hooks/useChat.js +462 -0
  42. package/lib/commonjs/hooks/useChat.js.map +1 -0
  43. package/lib/commonjs/hooks/useParlr.js +44 -0
  44. package/lib/commonjs/hooks/useParlr.js.map +1 -0
  45. package/lib/commonjs/index.js +185 -0
  46. package/lib/commonjs/index.js.map +1 -0
  47. package/lib/commonjs/package.json +1 -0
  48. package/lib/commonjs/provider/ParlrContext.js +38 -0
  49. package/lib/commonjs/provider/ParlrContext.js.map +1 -0
  50. package/lib/commonjs/provider/ParlrProvider.js +256 -0
  51. package/lib/commonjs/provider/ParlrProvider.js.map +1 -0
  52. package/lib/module/components/AttachmentPicker.js +287 -0
  53. package/lib/module/components/AttachmentPicker.js.map +1 -0
  54. package/lib/module/components/AttachmentPreview.js +195 -0
  55. package/lib/module/components/AttachmentPreview.js.map +1 -0
  56. package/lib/module/components/ChatBubble.js +386 -0
  57. package/lib/module/components/ChatBubble.js.map +1 -0
  58. package/lib/module/components/EmptyState.js +110 -0
  59. package/lib/module/components/EmptyState.js.map +1 -0
  60. package/lib/module/components/ParlrChat.js +740 -0
  61. package/lib/module/components/ParlrChat.js.map +1 -0
  62. package/lib/module/components/ParlrConversationList.js +504 -0
  63. package/lib/module/components/ParlrConversationList.js.map +1 -0
  64. package/lib/module/components/PreChatForm.js +258 -0
  65. package/lib/module/components/PreChatForm.js.map +1 -0
  66. package/lib/module/components/RichMessage.js +280 -0
  67. package/lib/module/components/RichMessage.js.map +1 -0
  68. package/lib/module/components/SatisfactionSurvey.js +287 -0
  69. package/lib/module/components/SatisfactionSurvey.js.map +1 -0
  70. package/lib/module/components/TypingIndicator.js +81 -0
  71. package/lib/module/components/TypingIndicator.js.map +1 -0
  72. package/lib/module/core/api.js +305 -0
  73. package/lib/module/core/api.js.map +1 -0
  74. package/lib/module/core/config.js +36 -0
  75. package/lib/module/core/config.js.map +1 -0
  76. package/lib/module/core/errors.js +64 -0
  77. package/lib/module/core/errors.js.map +1 -0
  78. package/lib/module/core/offlineQueue.js +82 -0
  79. package/lib/module/core/offlineQueue.js.map +1 -0
  80. package/lib/module/core/pushNotifications.js +16 -0
  81. package/lib/module/core/pushNotifications.js.map +1 -0
  82. package/lib/module/core/session.js +122 -0
  83. package/lib/module/core/session.js.map +1 -0
  84. package/lib/module/core/theme.js +105 -0
  85. package/lib/module/core/theme.js.map +1 -0
  86. package/lib/module/core/types.js +4 -0
  87. package/lib/module/core/types.js.map +1 -0
  88. package/lib/module/core/websocket.js +241 -0
  89. package/lib/module/core/websocket.js.map +1 -0
  90. package/lib/module/hooks/useChat.js +458 -0
  91. package/lib/module/hooks/useChat.js.map +1 -0
  92. package/lib/module/hooks/useParlr.js +40 -0
  93. package/lib/module/hooks/useParlr.js.map +1 -0
  94. package/lib/module/index.js +58 -0
  95. package/lib/module/index.js.map +1 -0
  96. package/lib/module/package.json +1 -0
  97. package/lib/module/provider/ParlrContext.js +35 -0
  98. package/lib/module/provider/ParlrContext.js.map +1 -0
  99. package/lib/module/provider/ParlrProvider.js +251 -0
  100. package/lib/module/provider/ParlrProvider.js.map +1 -0
  101. package/lib/typescript/commonjs/components/AttachmentPicker.d.ts +23 -0
  102. package/lib/typescript/commonjs/components/AttachmentPicker.d.ts.map +1 -0
  103. package/lib/typescript/commonjs/components/AttachmentPreview.d.ts +16 -0
  104. package/lib/typescript/commonjs/components/AttachmentPreview.d.ts.map +1 -0
  105. package/lib/typescript/commonjs/components/ChatBubble.d.ts +14 -0
  106. package/lib/typescript/commonjs/components/ChatBubble.d.ts.map +1 -0
  107. package/lib/typescript/commonjs/components/EmptyState.d.ts +10 -0
  108. package/lib/typescript/commonjs/components/EmptyState.d.ts.map +1 -0
  109. package/lib/typescript/commonjs/components/ParlrChat.d.ts +34 -0
  110. package/lib/typescript/commonjs/components/ParlrChat.d.ts.map +1 -0
  111. package/lib/typescript/commonjs/components/ParlrConversationList.d.ts +17 -0
  112. package/lib/typescript/commonjs/components/ParlrConversationList.d.ts.map +1 -0
  113. package/lib/typescript/commonjs/components/PreChatForm.d.ts +20 -0
  114. package/lib/typescript/commonjs/components/PreChatForm.d.ts.map +1 -0
  115. package/lib/typescript/commonjs/components/RichMessage.d.ts +41 -0
  116. package/lib/typescript/commonjs/components/RichMessage.d.ts.map +1 -0
  117. package/lib/typescript/commonjs/components/SatisfactionSurvey.d.ts +17 -0
  118. package/lib/typescript/commonjs/components/SatisfactionSurvey.d.ts.map +1 -0
  119. package/lib/typescript/commonjs/components/TypingIndicator.d.ts +7 -0
  120. package/lib/typescript/commonjs/components/TypingIndicator.d.ts.map +1 -0
  121. package/lib/typescript/commonjs/core/api.d.ts +37 -0
  122. package/lib/typescript/commonjs/core/api.d.ts.map +1 -0
  123. package/lib/typescript/commonjs/core/config.d.ts +9 -0
  124. package/lib/typescript/commonjs/core/config.d.ts.map +1 -0
  125. package/lib/typescript/commonjs/core/errors.d.ts +35 -0
  126. package/lib/typescript/commonjs/core/errors.d.ts.map +1 -0
  127. package/lib/typescript/commonjs/core/offlineQueue.d.ts +16 -0
  128. package/lib/typescript/commonjs/core/offlineQueue.d.ts.map +1 -0
  129. package/lib/typescript/commonjs/core/pushNotifications.d.ts +6 -0
  130. package/lib/typescript/commonjs/core/pushNotifications.d.ts.map +1 -0
  131. package/lib/typescript/commonjs/core/session.d.ts +15 -0
  132. package/lib/typescript/commonjs/core/session.d.ts.map +1 -0
  133. package/lib/typescript/commonjs/core/theme.d.ts +43 -0
  134. package/lib/typescript/commonjs/core/theme.d.ts.map +1 -0
  135. package/lib/typescript/commonjs/core/types.d.ts +185 -0
  136. package/lib/typescript/commonjs/core/types.d.ts.map +1 -0
  137. package/lib/typescript/commonjs/core/websocket.d.ts +17 -0
  138. package/lib/typescript/commonjs/core/websocket.d.ts.map +1 -0
  139. package/lib/typescript/commonjs/hooks/useChat.d.ts +35 -0
  140. package/lib/typescript/commonjs/hooks/useChat.d.ts.map +1 -0
  141. package/lib/typescript/commonjs/hooks/useParlr.d.ts +11 -0
  142. package/lib/typescript/commonjs/hooks/useParlr.d.ts.map +1 -0
  143. package/lib/typescript/commonjs/index.d.ts +30 -0
  144. package/lib/typescript/commonjs/index.d.ts.map +1 -0
  145. package/lib/typescript/commonjs/package.json +1 -0
  146. package/lib/typescript/commonjs/provider/ParlrContext.d.ts +13 -0
  147. package/lib/typescript/commonjs/provider/ParlrContext.d.ts.map +1 -0
  148. package/lib/typescript/commonjs/provider/ParlrProvider.d.ts +5 -0
  149. package/lib/typescript/commonjs/provider/ParlrProvider.d.ts.map +1 -0
  150. package/lib/typescript/module/components/AttachmentPicker.d.ts +23 -0
  151. package/lib/typescript/module/components/AttachmentPicker.d.ts.map +1 -0
  152. package/lib/typescript/module/components/AttachmentPreview.d.ts +16 -0
  153. package/lib/typescript/module/components/AttachmentPreview.d.ts.map +1 -0
  154. package/lib/typescript/module/components/ChatBubble.d.ts +14 -0
  155. package/lib/typescript/module/components/ChatBubble.d.ts.map +1 -0
  156. package/lib/typescript/module/components/EmptyState.d.ts +10 -0
  157. package/lib/typescript/module/components/EmptyState.d.ts.map +1 -0
  158. package/lib/typescript/module/components/ParlrChat.d.ts +34 -0
  159. package/lib/typescript/module/components/ParlrChat.d.ts.map +1 -0
  160. package/lib/typescript/module/components/ParlrConversationList.d.ts +17 -0
  161. package/lib/typescript/module/components/ParlrConversationList.d.ts.map +1 -0
  162. package/lib/typescript/module/components/PreChatForm.d.ts +20 -0
  163. package/lib/typescript/module/components/PreChatForm.d.ts.map +1 -0
  164. package/lib/typescript/module/components/RichMessage.d.ts +41 -0
  165. package/lib/typescript/module/components/RichMessage.d.ts.map +1 -0
  166. package/lib/typescript/module/components/SatisfactionSurvey.d.ts +17 -0
  167. package/lib/typescript/module/components/SatisfactionSurvey.d.ts.map +1 -0
  168. package/lib/typescript/module/components/TypingIndicator.d.ts +7 -0
  169. package/lib/typescript/module/components/TypingIndicator.d.ts.map +1 -0
  170. package/lib/typescript/module/core/api.d.ts +37 -0
  171. package/lib/typescript/module/core/api.d.ts.map +1 -0
  172. package/lib/typescript/module/core/config.d.ts +9 -0
  173. package/lib/typescript/module/core/config.d.ts.map +1 -0
  174. package/lib/typescript/module/core/errors.d.ts +35 -0
  175. package/lib/typescript/module/core/errors.d.ts.map +1 -0
  176. package/lib/typescript/module/core/offlineQueue.d.ts +16 -0
  177. package/lib/typescript/module/core/offlineQueue.d.ts.map +1 -0
  178. package/lib/typescript/module/core/pushNotifications.d.ts +6 -0
  179. package/lib/typescript/module/core/pushNotifications.d.ts.map +1 -0
  180. package/lib/typescript/module/core/session.d.ts +15 -0
  181. package/lib/typescript/module/core/session.d.ts.map +1 -0
  182. package/lib/typescript/module/core/theme.d.ts +43 -0
  183. package/lib/typescript/module/core/theme.d.ts.map +1 -0
  184. package/lib/typescript/module/core/types.d.ts +185 -0
  185. package/lib/typescript/module/core/types.d.ts.map +1 -0
  186. package/lib/typescript/module/core/websocket.d.ts +17 -0
  187. package/lib/typescript/module/core/websocket.d.ts.map +1 -0
  188. package/lib/typescript/module/hooks/useChat.d.ts +35 -0
  189. package/lib/typescript/module/hooks/useChat.d.ts.map +1 -0
  190. package/lib/typescript/module/hooks/useParlr.d.ts +11 -0
  191. package/lib/typescript/module/hooks/useParlr.d.ts.map +1 -0
  192. package/lib/typescript/module/index.d.ts +30 -0
  193. package/lib/typescript/module/index.d.ts.map +1 -0
  194. package/lib/typescript/module/package.json +1 -0
  195. package/lib/typescript/module/provider/ParlrContext.d.ts +13 -0
  196. package/lib/typescript/module/provider/ParlrContext.d.ts.map +1 -0
  197. package/lib/typescript/module/provider/ParlrProvider.d.ts +5 -0
  198. package/lib/typescript/module/provider/ParlrProvider.d.ts.map +1 -0
  199. package/package.json +120 -0
  200. package/src/components/AttachmentPicker.tsx +310 -0
  201. package/src/components/AttachmentPreview.tsx +209 -0
  202. package/src/components/ChatBubble.tsx +424 -0
  203. package/src/components/EmptyState.tsx +118 -0
  204. package/src/components/ParlrChat.tsx +863 -0
  205. package/src/components/ParlrConversationList.tsx +559 -0
  206. package/src/components/PreChatForm.tsx +313 -0
  207. package/src/components/RichMessage.tsx +353 -0
  208. package/src/components/SatisfactionSurvey.tsx +333 -0
  209. package/src/components/TypingIndicator.tsx +89 -0
  210. package/src/core/api.ts +406 -0
  211. package/src/core/config.ts +39 -0
  212. package/src/core/errors.ts +68 -0
  213. package/src/core/offlineQueue.ts +94 -0
  214. package/src/core/pushNotifications.ts +22 -0
  215. package/src/core/session.ts +156 -0
  216. package/src/core/theme.ts +133 -0
  217. package/src/core/types.ts +237 -0
  218. package/src/core/websocket.ts +270 -0
  219. package/src/hooks/useChat.ts +534 -0
  220. package/src/hooks/useParlr.ts +43 -0
  221. package/src/index.ts +98 -0
  222. package/src/provider/ParlrContext.ts +40 -0
  223. package/src/provider/ParlrProvider.tsx +338 -0
@@ -0,0 +1,559 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Parlr React Native SDK - Conversation List Screen
3
+ // ---------------------------------------------------------------------------
4
+ //
5
+ // Displays all conversations for the current contact with last message preview,
6
+ // unread badges, status indicators, and pull-to-refresh.
7
+ // ---------------------------------------------------------------------------
8
+
9
+ import React, { useCallback, useContext, useEffect, useState } from 'react';
10
+ import {
11
+ ActivityIndicator,
12
+ FlatList,
13
+ Pressable,
14
+ RefreshControl,
15
+ StyleSheet,
16
+ Text,
17
+ View,
18
+ useColorScheme,
19
+ type ListRenderItemInfo,
20
+ } from 'react-native';
21
+ import type { Conversation } from '../core/types';
22
+ import { ParlrContext } from '../provider/ParlrContext';
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // Props
26
+ // ---------------------------------------------------------------------------
27
+
28
+ export interface ParlrConversationListProps {
29
+ /** Callback when a conversation is tapped. */
30
+ onSelectConversation: (conversationId: string) => void;
31
+ /** Callback for the "New Conversation" button. */
32
+ onNewConversation?: () => void;
33
+ /** Header title. Default: "Conversations". */
34
+ headerTitle?: string;
35
+ /** Primary accent color. */
36
+ accentColor?: string;
37
+ /** Show header bar. Default: true. */
38
+ showHeader?: boolean;
39
+ /** Filter conversations by status. */
40
+ statusFilter?: 'open' | 'closed' | 'pending';
41
+ }
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // i18n
45
+ // ---------------------------------------------------------------------------
46
+
47
+ function getI18n(locale?: string) {
48
+ if (locale?.startsWith('en')) {
49
+ return {
50
+ headerTitle: 'Conversations',
51
+ newConversation: 'New conversation',
52
+ empty: 'No conversations yet',
53
+ emptyDescription: 'Start a new conversation to get help.',
54
+ open: 'Open',
55
+ closed: 'Closed',
56
+ pending: 'Pending',
57
+ unread: 'unread',
58
+ };
59
+ }
60
+ return {
61
+ headerTitle: 'Conversations',
62
+ newConversation: 'Nouvelle conversation',
63
+ empty: 'Aucune conversation',
64
+ emptyDescription: 'D\u00e9marrez une conversation pour obtenir de l\u2019aide.',
65
+ open: 'Ouvert',
66
+ closed: 'Ferm\u00e9',
67
+ pending: 'En attente',
68
+ unread: 'non lu(s)',
69
+ };
70
+ }
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // Helpers
74
+ // ---------------------------------------------------------------------------
75
+
76
+ function formatRelativeTime(iso: string | null, locale?: string): string {
77
+ if (!iso) return '';
78
+ try {
79
+ const date = new Date(iso);
80
+ const now = new Date();
81
+ const diffMs = now.getTime() - date.getTime();
82
+ const diffMin = Math.floor(diffMs / 60_000);
83
+ const diffH = Math.floor(diffMs / 3_600_000);
84
+ const diffD = Math.floor(diffMs / 86_400_000);
85
+
86
+ const isFr = !locale?.startsWith('en');
87
+
88
+ if (diffMin < 1) return isFr ? '\u00c0 l\u2019instant' : 'Just now';
89
+ if (diffMin < 60) return `${diffMin}min`;
90
+ if (diffH < 24) return `${diffH}h`;
91
+ if (diffD < 7) return `${diffD}${isFr ? 'j' : 'd'}`;
92
+ return date.toLocaleDateString(locale, { month: 'short', day: 'numeric' });
93
+ } catch {
94
+ return '';
95
+ }
96
+ }
97
+
98
+ function statusColor(status: string): string {
99
+ switch (status) {
100
+ case 'open':
101
+ return '#22c55e';
102
+ case 'pending':
103
+ return '#f59e0b';
104
+ case 'closed':
105
+ return '#94a3b8';
106
+ default:
107
+ return '#94a3b8';
108
+ }
109
+ }
110
+
111
+ // ---------------------------------------------------------------------------
112
+ // Conversation Row
113
+ // ---------------------------------------------------------------------------
114
+
115
+ function ConversationRow({
116
+ conversation,
117
+ onPress,
118
+ accentColor,
119
+ locale,
120
+ isDark,
121
+ }: {
122
+ conversation: Conversation;
123
+ onPress: () => void;
124
+ accentColor: string;
125
+ locale?: string;
126
+ isDark: boolean;
127
+ }) {
128
+ const i18n = getI18n(locale);
129
+ const hasUnread = conversation.unreadCount > 0;
130
+ const assigneeName = conversation.assignee?.name;
131
+ const assigneeInitial = assigneeName ? assigneeName.charAt(0).toUpperCase() : null;
132
+ const statusLabel =
133
+ conversation.status === 'open'
134
+ ? i18n.open
135
+ : conversation.status === 'closed'
136
+ ? i18n.closed
137
+ : i18n.pending;
138
+
139
+ return (
140
+ <Pressable
141
+ onPress={onPress}
142
+ style={({ pressed }) => [
143
+ styles.row,
144
+ isDark ? styles.rowDark : styles.rowLight,
145
+ pressed && styles.rowPressed,
146
+ ]}
147
+ accessibilityRole="button"
148
+ accessibilityLabel={`${conversation.subject ?? 'Conversation'}, ${statusLabel}, ${conversation.unreadCount} ${i18n.unread}`}
149
+ >
150
+ {/* Avatar */}
151
+ <View style={[styles.avatar, { backgroundColor: assigneeInitial ? accentColor : '#e2e8f0' }]}>
152
+ <Text style={styles.avatarText}>
153
+ {assigneeInitial ?? '#'}
154
+ </Text>
155
+ </View>
156
+
157
+ {/* Content */}
158
+ <View style={styles.rowContent}>
159
+ <View style={styles.rowTopLine}>
160
+ <Text
161
+ style={[
162
+ styles.rowTitle,
163
+ isDark && styles.rowTitleDark,
164
+ hasUnread && styles.rowTitleBold,
165
+ ]}
166
+ numberOfLines={1}
167
+ >
168
+ {conversation.subject ?? (assigneeName ? `${assigneeName}` : 'Support')}
169
+ </Text>
170
+ <Text style={[styles.rowTime, isDark && styles.rowTimeDark]}>
171
+ {formatRelativeTime(conversation.lastMessageAt, locale)}
172
+ </Text>
173
+ </View>
174
+
175
+ <View style={styles.rowBottomLine}>
176
+ <View style={styles.statusBadgeRow}>
177
+ <View style={[styles.statusDot, { backgroundColor: statusColor(conversation.status) }]} />
178
+ <Text style={[styles.statusLabel, isDark && styles.statusLabelDark]} numberOfLines={1}>
179
+ {statusLabel}
180
+ </Text>
181
+ </View>
182
+
183
+ {hasUnread && (
184
+ <View style={[styles.unreadBadge, { backgroundColor: accentColor }]}>
185
+ <Text style={styles.unreadText}>
186
+ {conversation.unreadCount > 99 ? '99+' : conversation.unreadCount}
187
+ </Text>
188
+ </View>
189
+ )}
190
+ </View>
191
+ </View>
192
+ </Pressable>
193
+ );
194
+ }
195
+
196
+ // ---------------------------------------------------------------------------
197
+ // Empty State
198
+ // ---------------------------------------------------------------------------
199
+
200
+ function ListEmptyState({
201
+ onNewConversation,
202
+ accentColor,
203
+ locale,
204
+ isDark,
205
+ }: {
206
+ onNewConversation?: () => void;
207
+ accentColor: string;
208
+ locale?: string;
209
+ isDark: boolean;
210
+ }) {
211
+ const i18n = getI18n(locale);
212
+ return (
213
+ <View style={styles.emptyContainer}>
214
+ <Text style={[styles.emptyIcon]}>{'💬'}</Text>
215
+ <Text style={[styles.emptyTitle, isDark && styles.emptyTitleDark]}>
216
+ {i18n.empty}
217
+ </Text>
218
+ <Text style={[styles.emptyDescription, isDark && styles.emptyDescriptionDark]}>
219
+ {i18n.emptyDescription}
220
+ </Text>
221
+ {onNewConversation && (
222
+ <Pressable
223
+ onPress={onNewConversation}
224
+ style={[styles.newButton, { backgroundColor: accentColor }]}
225
+ accessibilityRole="button"
226
+ accessibilityLabel={i18n.newConversation}
227
+ >
228
+ <Text style={styles.newButtonText}>{i18n.newConversation}</Text>
229
+ </Pressable>
230
+ )}
231
+ </View>
232
+ );
233
+ }
234
+
235
+ // ---------------------------------------------------------------------------
236
+ // Main component
237
+ // ---------------------------------------------------------------------------
238
+
239
+ export function ParlrConversationList({
240
+ onSelectConversation,
241
+ onNewConversation,
242
+ headerTitle,
243
+ accentColor: accentColorProp,
244
+ showHeader = true,
245
+ statusFilter,
246
+ }: ParlrConversationListProps) {
247
+ const isDark = useColorScheme() === 'dark';
248
+ const { config, isReady, conversations, refreshConversations } =
249
+ useContext(ParlrContext);
250
+ const accentColor = accentColorProp ?? '#6366f1';
251
+ const i18n = getI18n(config.locale);
252
+
253
+ const [refreshing, setRefreshing] = useState(false);
254
+
255
+ // Filter conversations if a status filter is specified.
256
+ const filteredConversations = statusFilter
257
+ ? conversations.filter((c) => c.status === statusFilter)
258
+ : conversations;
259
+
260
+ // Sort by lastMessageAt descending.
261
+ const sortedConversations = [...filteredConversations].sort((a, b) => {
262
+ const aTime = a.lastMessageAt ? new Date(a.lastMessageAt).getTime() : 0;
263
+ const bTime = b.lastMessageAt ? new Date(b.lastMessageAt).getTime() : 0;
264
+ return bTime - aTime;
265
+ });
266
+
267
+ // Refresh on mount.
268
+ useEffect(() => {
269
+ if (isReady) {
270
+ refreshConversations();
271
+ }
272
+ }, [isReady, refreshConversations]);
273
+
274
+ const handleRefresh = useCallback(async () => {
275
+ setRefreshing(true);
276
+ try {
277
+ await refreshConversations();
278
+ } finally {
279
+ setRefreshing(false);
280
+ }
281
+ }, [refreshConversations]);
282
+
283
+ const renderItem = useCallback(
284
+ ({ item }: ListRenderItemInfo<Conversation>) => (
285
+ <ConversationRow
286
+ conversation={item}
287
+ onPress={() => onSelectConversation(item.id)}
288
+ accentColor={accentColor}
289
+ locale={config.locale}
290
+ isDark={isDark}
291
+ />
292
+ ),
293
+ [onSelectConversation, accentColor, config.locale, isDark],
294
+ );
295
+
296
+ const keyExtractor = useCallback((item: Conversation) => item.id, []);
297
+
298
+ // --- Loading ---
299
+ if (!isReady) {
300
+ return (
301
+ <View style={[styles.loadingContainer, isDark && styles.loadingContainerDark]}>
302
+ <ActivityIndicator size="large" color={accentColor} />
303
+ </View>
304
+ );
305
+ }
306
+
307
+ return (
308
+ <View style={[styles.root, isDark && styles.rootDark]}>
309
+ {/* Header */}
310
+ {showHeader && (
311
+ <View style={[styles.header, isDark ? styles.headerDark : styles.headerLight]}>
312
+ <Text style={[styles.headerTitle, isDark && styles.headerTitleDark]}>
313
+ {headerTitle ?? i18n.headerTitle}
314
+ </Text>
315
+ {onNewConversation && (
316
+ <Pressable
317
+ onPress={onNewConversation}
318
+ style={[styles.headerButton, { backgroundColor: accentColor }]}
319
+ hitSlop={8}
320
+ accessibilityRole="button"
321
+ accessibilityLabel={i18n.newConversation}
322
+ >
323
+ <Text style={styles.headerButtonText}>+</Text>
324
+ </Pressable>
325
+ )}
326
+ </View>
327
+ )}
328
+
329
+ {/* List */}
330
+ <FlatList
331
+ data={sortedConversations}
332
+ renderItem={renderItem}
333
+ keyExtractor={keyExtractor}
334
+ contentContainerStyle={sortedConversations.length === 0 ? styles.emptyList : undefined}
335
+ refreshControl={
336
+ <RefreshControl
337
+ refreshing={refreshing}
338
+ onRefresh={handleRefresh}
339
+ tintColor={accentColor}
340
+ colors={[accentColor]}
341
+ />
342
+ }
343
+ ListEmptyComponent={
344
+ <ListEmptyState
345
+ onNewConversation={onNewConversation}
346
+ accentColor={accentColor}
347
+ locale={config.locale}
348
+ isDark={isDark}
349
+ />
350
+ }
351
+ showsVerticalScrollIndicator={false}
352
+ removeClippedSubviews={true}
353
+ />
354
+ </View>
355
+ );
356
+ }
357
+
358
+ // ---------------------------------------------------------------------------
359
+ // Styles
360
+ // ---------------------------------------------------------------------------
361
+
362
+ const styles = StyleSheet.create({
363
+ root: {
364
+ flex: 1,
365
+ backgroundColor: '#ffffff',
366
+ },
367
+ rootDark: {
368
+ backgroundColor: '#0f172a',
369
+ },
370
+ loadingContainer: {
371
+ flex: 1,
372
+ justifyContent: 'center',
373
+ alignItems: 'center',
374
+ backgroundColor: '#ffffff',
375
+ },
376
+ loadingContainerDark: {
377
+ backgroundColor: '#0f172a',
378
+ },
379
+
380
+ // Header
381
+ header: {
382
+ flexDirection: 'row',
383
+ alignItems: 'center',
384
+ justifyContent: 'space-between',
385
+ paddingHorizontal: 16,
386
+ paddingTop: 56,
387
+ paddingBottom: 12,
388
+ borderBottomWidth: StyleSheet.hairlineWidth,
389
+ },
390
+ headerLight: {
391
+ backgroundColor: '#ffffff',
392
+ borderBottomColor: '#e2e8f0',
393
+ },
394
+ headerDark: {
395
+ backgroundColor: '#0f172a',
396
+ borderBottomColor: '#1e293b',
397
+ },
398
+ headerTitle: {
399
+ fontSize: 24,
400
+ fontWeight: '700',
401
+ color: '#0f172a',
402
+ },
403
+ headerTitleDark: {
404
+ color: '#f1f5f9',
405
+ },
406
+ headerButton: {
407
+ width: 32,
408
+ height: 32,
409
+ borderRadius: 16,
410
+ justifyContent: 'center',
411
+ alignItems: 'center',
412
+ },
413
+ headerButtonText: {
414
+ color: '#ffffff',
415
+ fontSize: 20,
416
+ fontWeight: '600',
417
+ lineHeight: 22,
418
+ },
419
+
420
+ // Row
421
+ row: {
422
+ flexDirection: 'row',
423
+ alignItems: 'center',
424
+ paddingHorizontal: 16,
425
+ paddingVertical: 14,
426
+ borderBottomWidth: StyleSheet.hairlineWidth,
427
+ },
428
+ rowLight: {
429
+ borderBottomColor: '#f1f5f9',
430
+ },
431
+ rowDark: {
432
+ borderBottomColor: '#1e293b',
433
+ },
434
+ rowPressed: {
435
+ opacity: 0.7,
436
+ },
437
+ avatar: {
438
+ width: 44,
439
+ height: 44,
440
+ borderRadius: 22,
441
+ justifyContent: 'center',
442
+ alignItems: 'center',
443
+ marginRight: 12,
444
+ },
445
+ avatarText: {
446
+ color: '#ffffff',
447
+ fontSize: 17,
448
+ fontWeight: '600',
449
+ },
450
+ rowContent: {
451
+ flex: 1,
452
+ },
453
+ rowTopLine: {
454
+ flexDirection: 'row',
455
+ justifyContent: 'space-between',
456
+ alignItems: 'center',
457
+ marginBottom: 4,
458
+ },
459
+ rowTitle: {
460
+ flex: 1,
461
+ fontSize: 15,
462
+ color: '#0f172a',
463
+ marginRight: 8,
464
+ },
465
+ rowTitleDark: {
466
+ color: '#f1f5f9',
467
+ },
468
+ rowTitleBold: {
469
+ fontWeight: '600',
470
+ },
471
+ rowTime: {
472
+ fontSize: 12,
473
+ color: '#94a3b8',
474
+ },
475
+ rowTimeDark: {
476
+ color: '#64748b',
477
+ },
478
+ rowBottomLine: {
479
+ flexDirection: 'row',
480
+ justifyContent: 'space-between',
481
+ alignItems: 'center',
482
+ },
483
+ statusBadgeRow: {
484
+ flexDirection: 'row',
485
+ alignItems: 'center',
486
+ },
487
+ statusDot: {
488
+ width: 6,
489
+ height: 6,
490
+ borderRadius: 3,
491
+ marginRight: 5,
492
+ },
493
+ statusLabel: {
494
+ fontSize: 13,
495
+ color: '#64748b',
496
+ },
497
+ statusLabelDark: {
498
+ color: '#94a3b8',
499
+ },
500
+ unreadBadge: {
501
+ minWidth: 20,
502
+ height: 20,
503
+ borderRadius: 10,
504
+ justifyContent: 'center',
505
+ alignItems: 'center',
506
+ paddingHorizontal: 6,
507
+ },
508
+ unreadText: {
509
+ color: '#ffffff',
510
+ fontSize: 11,
511
+ fontWeight: '700',
512
+ },
513
+
514
+ // Empty state
515
+ emptyList: {
516
+ flexGrow: 1,
517
+ },
518
+ emptyContainer: {
519
+ flex: 1,
520
+ justifyContent: 'center',
521
+ alignItems: 'center',
522
+ paddingHorizontal: 32,
523
+ paddingVertical: 48,
524
+ },
525
+ emptyIcon: {
526
+ fontSize: 48,
527
+ marginBottom: 16,
528
+ },
529
+ emptyTitle: {
530
+ fontSize: 18,
531
+ fontWeight: '600',
532
+ color: '#0f172a',
533
+ marginBottom: 8,
534
+ textAlign: 'center',
535
+ },
536
+ emptyTitleDark: {
537
+ color: '#f1f5f9',
538
+ },
539
+ emptyDescription: {
540
+ fontSize: 14,
541
+ color: '#64748b',
542
+ textAlign: 'center',
543
+ lineHeight: 20,
544
+ marginBottom: 24,
545
+ },
546
+ emptyDescriptionDark: {
547
+ color: '#94a3b8',
548
+ },
549
+ newButton: {
550
+ paddingHorizontal: 24,
551
+ paddingVertical: 12,
552
+ borderRadius: 22,
553
+ },
554
+ newButtonText: {
555
+ color: '#ffffff',
556
+ fontSize: 15,
557
+ fontWeight: '600',
558
+ },
559
+ });