@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,424 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Parlr React Native SDK - Chat Bubble
3
+ // ---------------------------------------------------------------------------
4
+ //
5
+ // Renders a single message bubble. Agent messages appear on the left with an
6
+ // avatar; contact messages appear on the right with the accent color.
7
+ // Supports text, images, file attachments, and read receipts.
8
+ // ---------------------------------------------------------------------------
9
+
10
+ import React, { useContext, useState } from 'react';
11
+ import {
12
+ Dimensions,
13
+ Image,
14
+ Linking,
15
+ Modal,
16
+ Pressable,
17
+ StyleSheet,
18
+ Text,
19
+ View,
20
+ } from 'react-native';
21
+ import Animated, { FadeInUp } from 'react-native-reanimated';
22
+ import type { ParlrTheme } from '../core/theme';
23
+ import type { Attachment, Message } from '../core/types';
24
+ import { ParlrContext } from '../provider/ParlrContext';
25
+ import { RichMessage, tryParseRichContent } from './RichMessage';
26
+ import type { RichAction } from './RichMessage';
27
+
28
+ interface ChatBubbleProps {
29
+ message: Message;
30
+ accentColor?: string;
31
+ /** If true, apply the entrance animation (for newly-appended messages). */
32
+ animated?: boolean;
33
+ /** Called when a rich message action is triggered. */
34
+ onRichAction?: (action: RichAction) => void;
35
+ }
36
+
37
+ function formatTime(iso: string): string {
38
+ try {
39
+ const d = new Date(iso);
40
+ return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
41
+ } catch {
42
+ return '';
43
+ }
44
+ }
45
+
46
+ function AgentAvatar({ name, theme }: { name?: string; theme: ParlrTheme }) {
47
+ const initial = (name ?? 'A').charAt(0).toUpperCase();
48
+ return (
49
+ <View
50
+ style={[styles.avatar, { backgroundColor: theme.colors.surfaceDark }]}
51
+ accessibilityLabel={`${name ?? 'Agent'} avatar`}
52
+ >
53
+ <Text style={[styles.avatarText, { color: theme.colors.textSecondary }]}>{initial}</Text>
54
+ </View>
55
+ );
56
+ }
57
+
58
+ // ---------------------------------------------------------------------------
59
+ // Read receipt indicators
60
+ // ---------------------------------------------------------------------------
61
+
62
+ function ReadReceipt({ status }: { status?: Message['status'] }) {
63
+ if (!status || status === 'failed') return null;
64
+
65
+ if (status === 'sending') {
66
+ return <Text style={styles.statusIndicator}> ...</Text>;
67
+ }
68
+
69
+ if (status === 'sent') {
70
+ return <Text style={styles.checkmark}> {'\u2713'}</Text>;
71
+ }
72
+
73
+ if (status === 'read') {
74
+ return <Text style={styles.checkmarkRead}> {'\u2713\u2713'}</Text>;
75
+ }
76
+
77
+ return null;
78
+ }
79
+
80
+ // ---------------------------------------------------------------------------
81
+ // Attachment renderers
82
+ // ---------------------------------------------------------------------------
83
+
84
+ function isImageAttachment(attachment: Attachment): boolean {
85
+ return attachment.contentType.startsWith('image/');
86
+ }
87
+
88
+ function formatFileSize(bytes: number): string {
89
+ if (bytes < 1024) return `${bytes} B`;
90
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
91
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
92
+ }
93
+
94
+ function ImageAttachmentView({ attachment }: { attachment: Attachment }) {
95
+ const [visible, setVisible] = useState(false);
96
+ const screenWidth = Dimensions.get('window').width;
97
+ const screenHeight = Dimensions.get('window').height;
98
+
99
+ return (
100
+ <>
101
+ <Pressable
102
+ onPress={() => setVisible(true)}
103
+ accessibilityLabel={attachment.name}
104
+ accessibilityRole="image"
105
+ >
106
+ <Image
107
+ source={{ uri: attachment.url }}
108
+ style={styles.attachmentImage}
109
+ resizeMode="cover"
110
+ />
111
+ </Pressable>
112
+ <Modal visible={visible} transparent animationType="fade" onRequestClose={() => setVisible(false)}>
113
+ <Pressable style={styles.imageModalBackdrop} onPress={() => setVisible(false)}>
114
+ <Image
115
+ source={{ uri: attachment.url }}
116
+ style={{ width: screenWidth, height: screenHeight * 0.8 }}
117
+ resizeMode="contain"
118
+ />
119
+ <Text style={styles.imageModalClose}>{'\u2715'}</Text>
120
+ </Pressable>
121
+ </Modal>
122
+ </>
123
+ );
124
+ }
125
+
126
+ function AttachmentView({
127
+ attachment,
128
+ isAgent,
129
+ isDark,
130
+ }: {
131
+ attachment: Attachment;
132
+ isAgent: boolean;
133
+ isDark: boolean;
134
+ }) {
135
+ if (isImageAttachment(attachment)) {
136
+ return <ImageAttachmentView attachment={attachment} />;
137
+ }
138
+
139
+ return (
140
+ <Pressable
141
+ onPress={() => Linking.openURL(attachment.url).catch(() => {})}
142
+ style={[styles.fileAttachment, isDark && styles.fileAttachmentDark]}
143
+ accessibilityRole="link"
144
+ accessibilityLabel={`${attachment.name}, ${formatFileSize(attachment.size)}`}
145
+ >
146
+ <Text style={styles.fileIcon}>{'\ud83d\udcc4'}</Text>
147
+ <View style={styles.fileDetails}>
148
+ <Text
149
+ style={[
150
+ styles.fileName,
151
+ isAgent
152
+ ? [styles.fileNameAgent, isDark && styles.fileNameAgentDark]
153
+ : styles.fileNameContact,
154
+ ]}
155
+ numberOfLines={1}
156
+ >
157
+ {attachment.name}
158
+ </Text>
159
+ <Text
160
+ style={[
161
+ styles.fileSizeText,
162
+ isAgent
163
+ ? [styles.fileSizeAgent, isDark && styles.fileSizeAgentDark]
164
+ : styles.fileSizeContact,
165
+ ]}
166
+ >
167
+ {formatFileSize(attachment.size)}
168
+ </Text>
169
+ </View>
170
+ </Pressable>
171
+ );
172
+ }
173
+
174
+ // ---------------------------------------------------------------------------
175
+ // Main component
176
+ // ---------------------------------------------------------------------------
177
+
178
+ export function ChatBubble({
179
+ message,
180
+ accentColor,
181
+ animated = false,
182
+ onRichAction,
183
+ }: ChatBubbleProps) {
184
+ const { theme } = useContext(ParlrContext);
185
+ const isAgent = message.senderType === 'agent';
186
+ const accent = accentColor ?? theme.colors.primary;
187
+ const isFailed = message.status === 'failed';
188
+
189
+ const Wrapper = animated ? Animated.View : View;
190
+ const animationProps = animated
191
+ ? { entering: FadeInUp.duration(250).springify() }
192
+ : {};
193
+
194
+ const senderLabel = isAgent ? (message.senderName ?? 'Agent') : 'You';
195
+ const timeLabel = formatTime(message.createdAt);
196
+ const statusLabel = isFailed ? ', failed to send' : '';
197
+
198
+ const hasAttachments = message.attachments && message.attachments.length > 0;
199
+ const hasImageAttachments = hasAttachments && message.attachments!.some(isImageAttachment);
200
+ const richContent = message.metadata ? tryParseRichContent(message.metadata as Record<string, unknown>) : null;
201
+
202
+ return (
203
+ <Wrapper
204
+ {...animationProps}
205
+ style={[styles.row, isAgent ? styles.rowAgent : styles.rowContact]}
206
+ accessibilityLabel={`${senderLabel}: ${message.content}, ${timeLabel}${statusLabel}`}
207
+ accessibilityRole="text"
208
+ >
209
+ {isAgent && <AgentAvatar name={message.senderName} theme={theme} />}
210
+
211
+ <View
212
+ style={[
213
+ styles.bubble,
214
+ { borderRadius: theme.borderRadius.bubble },
215
+ isAgent
216
+ ? { backgroundColor: theme.colors.agentBubble, borderBottomLeftRadius: 6 }
217
+ : { backgroundColor: theme.colors.contactBubble, borderBottomRightRadius: 6 },
218
+ isFailed && styles.bubbleFailed,
219
+ ]}
220
+ >
221
+ {isAgent && message.senderName && (
222
+ <Text
223
+ style={[styles.senderName, { color: theme.colors.textSecondary }]}
224
+ numberOfLines={1}
225
+ >
226
+ {message.senderName}
227
+ </Text>
228
+ )}
229
+
230
+ {/* Attachments */}
231
+ {hasAttachments && (
232
+ <View style={styles.attachmentsContainer}>
233
+ {message.attachments!.map((att) => (
234
+ <AttachmentView
235
+ key={att.id}
236
+ attachment={att}
237
+ isAgent={isAgent}
238
+ isDark={theme.colors.background === '#111827' || theme.colors.background.toLowerCase() < '#888888'}
239
+ />
240
+ ))}
241
+ </View>
242
+ )}
243
+
244
+ {/* Text content — hidden when the message is just a filename for an image */}
245
+ {message.content.length > 0 && !hasImageAttachments && (
246
+ <Text
247
+ style={[
248
+ styles.content,
249
+ { color: isAgent ? theme.colors.agentText : theme.colors.contactText },
250
+ ]}
251
+ >
252
+ {message.content}
253
+ </Text>
254
+ )}
255
+
256
+ {/* Rich content */}
257
+ {richContent && (
258
+ <RichMessage
259
+ content={richContent}
260
+ onAction={onRichAction}
261
+ accentColor={accent}
262
+ />
263
+ )}
264
+
265
+ <View style={styles.meta}>
266
+ <Text
267
+ style={[
268
+ styles.time,
269
+ { color: isAgent ? theme.colors.textSecondary : 'rgba(255, 255, 255, 0.7)' },
270
+ ]}
271
+ >
272
+ {formatTime(message.createdAt)}
273
+ </Text>
274
+
275
+ {/* Read receipts for contact messages */}
276
+ {!isAgent && <ReadReceipt status={message.status} />}
277
+
278
+ {isFailed && (
279
+ <Text style={[styles.failedIndicator, { color: theme.colors.error }]}> !</Text>
280
+ )}
281
+ </View>
282
+ </View>
283
+ </Wrapper>
284
+ );
285
+ }
286
+
287
+ const styles = StyleSheet.create({
288
+ row: {
289
+ flexDirection: 'row',
290
+ marginVertical: 3,
291
+ paddingHorizontal: 12,
292
+ },
293
+ rowAgent: {
294
+ justifyContent: 'flex-start',
295
+ alignItems: 'flex-end',
296
+ },
297
+ rowContact: {
298
+ justifyContent: 'flex-end',
299
+ alignItems: 'flex-end',
300
+ },
301
+ avatar: {
302
+ width: 30,
303
+ height: 30,
304
+ borderRadius: 15,
305
+ justifyContent: 'center',
306
+ alignItems: 'center',
307
+ marginRight: 8,
308
+ marginBottom: 2,
309
+ },
310
+ avatarText: {
311
+ fontSize: 13,
312
+ fontWeight: '600',
313
+ },
314
+ bubble: {
315
+ maxWidth: '75%',
316
+ paddingHorizontal: 14,
317
+ paddingVertical: 10,
318
+ },
319
+ bubbleFailed: {
320
+ opacity: 0.6,
321
+ },
322
+ senderName: {
323
+ fontSize: 12,
324
+ fontWeight: '600',
325
+ marginBottom: 3,
326
+ },
327
+ content: {
328
+ fontSize: 15,
329
+ lineHeight: 21,
330
+ },
331
+ meta: {
332
+ flexDirection: 'row',
333
+ alignItems: 'center',
334
+ marginTop: 4,
335
+ alignSelf: 'flex-end',
336
+ },
337
+ time: {
338
+ fontSize: 11,
339
+ },
340
+ statusIndicator: {
341
+ fontSize: 11,
342
+ color: 'rgba(255, 255, 255, 0.7)',
343
+ },
344
+ failedIndicator: {
345
+ fontSize: 12,
346
+ fontWeight: '700',
347
+ },
348
+ checkmark: {
349
+ fontSize: 11,
350
+ color: 'rgba(255, 255, 255, 0.7)',
351
+ },
352
+ checkmarkRead: {
353
+ fontSize: 11,
354
+ color: '#22c55e',
355
+ },
356
+
357
+ // Attachments
358
+ attachmentsContainer: {
359
+ marginBottom: 6,
360
+ gap: 4,
361
+ },
362
+ attachmentImage: {
363
+ width: 220,
364
+ height: 165,
365
+ borderRadius: 10,
366
+ },
367
+ imageModalBackdrop: {
368
+ flex: 1,
369
+ backgroundColor: 'rgba(0, 0, 0, 0.95)',
370
+ justifyContent: 'center',
371
+ alignItems: 'center',
372
+ },
373
+ imageModalClose: {
374
+ position: 'absolute',
375
+ top: 60,
376
+ right: 20,
377
+ color: '#ffffff',
378
+ fontSize: 28,
379
+ fontWeight: '600',
380
+ },
381
+ fileAttachment: {
382
+ flexDirection: 'row',
383
+ alignItems: 'center',
384
+ backgroundColor: 'rgba(0, 0, 0, 0.05)',
385
+ borderRadius: 8,
386
+ padding: 8,
387
+ },
388
+ fileAttachmentDark: {
389
+ backgroundColor: 'rgba(255, 255, 255, 0.05)',
390
+ },
391
+ fileIcon: {
392
+ fontSize: 24,
393
+ marginRight: 8,
394
+ },
395
+ fileDetails: {
396
+ flex: 1,
397
+ },
398
+ fileName: {
399
+ fontSize: 13,
400
+ fontWeight: '500',
401
+ },
402
+ fileNameAgent: {
403
+ color: '#0f172a',
404
+ },
405
+ fileNameAgentDark: {
406
+ color: '#e2e8f0',
407
+ },
408
+ fileNameContact: {
409
+ color: '#ffffff',
410
+ },
411
+ fileSizeText: {
412
+ fontSize: 11,
413
+ marginTop: 1,
414
+ },
415
+ fileSizeAgent: {
416
+ color: '#94a3b8',
417
+ },
418
+ fileSizeAgentDark: {
419
+ color: '#64748b',
420
+ },
421
+ fileSizeContact: {
422
+ color: 'rgba(255, 255, 255, 0.7)',
423
+ },
424
+ });
@@ -0,0 +1,118 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Parlr React Native SDK - Empty State
3
+ // ---------------------------------------------------------------------------
4
+ //
5
+ // Displayed when the user hasn't started a conversation yet.
6
+ // Shows a friendly welcome message with a chat icon.
7
+ // ---------------------------------------------------------------------------
8
+
9
+ import React from 'react';
10
+ import { StyleSheet, Text, View, useColorScheme } from 'react-native';
11
+
12
+ interface EmptyStateProps {
13
+ title?: string;
14
+ description?: string;
15
+ accentColor?: string;
16
+ locale?: string;
17
+ }
18
+
19
+ const DEFAULTS_FR = {
20
+ title: 'Besoin d\u2019aide ?',
21
+ description:
22
+ 'Notre equipe est la pour vous aider. Envoyez-nous un message et nous vous repondrons au plus vite.',
23
+ };
24
+
25
+ const DEFAULTS_EN = {
26
+ title: 'Need help?',
27
+ description:
28
+ 'Our team is here to help. Send us a message and we\u2019ll get back to you as soon as possible.',
29
+ };
30
+
31
+ function getDefaults(locale?: string) {
32
+ if (locale?.startsWith('en')) return DEFAULTS_EN;
33
+ return DEFAULTS_FR;
34
+ }
35
+
36
+ export function EmptyState({
37
+ title,
38
+ description,
39
+ accentColor,
40
+ locale,
41
+ }: EmptyStateProps) {
42
+ const isDark = useColorScheme() === 'dark';
43
+ const defaults = getDefaults(locale);
44
+ const accent = accentColor ?? '#6366f1';
45
+
46
+ return (
47
+ <View style={styles.container}>
48
+ {/* Chat bubble icon */}
49
+ <View
50
+ style={[styles.iconContainer, { backgroundColor: accent + '18' }]}
51
+ accessibilityLabel="Chat icon"
52
+ accessibilityRole="image"
53
+ >
54
+ <View style={[styles.iconBubble, { backgroundColor: accent }]}>
55
+ <Text style={styles.iconText}>...</Text>
56
+ </View>
57
+ </View>
58
+
59
+ <Text style={[styles.title, isDark && styles.titleDark]}>
60
+ {title ?? defaults.title}
61
+ </Text>
62
+
63
+ <Text style={[styles.description, isDark && styles.descriptionDark]}>
64
+ {description ?? defaults.description}
65
+ </Text>
66
+ </View>
67
+ );
68
+ }
69
+
70
+ const styles = StyleSheet.create({
71
+ container: {
72
+ flex: 1,
73
+ justifyContent: 'center',
74
+ alignItems: 'center',
75
+ paddingHorizontal: 40,
76
+ },
77
+ iconContainer: {
78
+ width: 80,
79
+ height: 80,
80
+ borderRadius: 40,
81
+ justifyContent: 'center',
82
+ alignItems: 'center',
83
+ marginBottom: 24,
84
+ },
85
+ iconBubble: {
86
+ width: 44,
87
+ height: 36,
88
+ borderRadius: 18,
89
+ justifyContent: 'center',
90
+ alignItems: 'center',
91
+ },
92
+ iconText: {
93
+ color: '#ffffff',
94
+ fontSize: 20,
95
+ fontWeight: '700',
96
+ marginTop: -4,
97
+ letterSpacing: 2,
98
+ },
99
+ title: {
100
+ fontSize: 22,
101
+ fontWeight: '700',
102
+ color: '#0f172a',
103
+ textAlign: 'center',
104
+ marginBottom: 10,
105
+ },
106
+ titleDark: {
107
+ color: '#f1f5f9',
108
+ },
109
+ description: {
110
+ fontSize: 15,
111
+ lineHeight: 22,
112
+ color: '#64748b',
113
+ textAlign: 'center',
114
+ },
115
+ descriptionDark: {
116
+ color: '#94a3b8',
117
+ },
118
+ });