@repzo/widget-react-native 1.3.1 → 1.4.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 (224) hide show
  1. package/CHANGELOG.md +137 -5
  2. package/README.md +64 -12
  3. package/lib/commonjs/api/client.js +96 -23
  4. package/lib/commonjs/api/client.js.map +1 -1
  5. package/lib/commonjs/attachments/file-size.js +52 -0
  6. package/lib/commonjs/attachments/file-size.js.map +1 -0
  7. package/lib/commonjs/attachments/local-previews.js +40 -0
  8. package/lib/commonjs/attachments/local-previews.js.map +1 -0
  9. package/lib/commonjs/attachments/pickers.js +20 -29
  10. package/lib/commonjs/attachments/pickers.js.map +1 -1
  11. package/lib/commonjs/i18n.js +22 -3
  12. package/lib/commonjs/i18n.js.map +1 -1
  13. package/lib/commonjs/locales.js +12 -3
  14. package/lib/commonjs/locales.js.map +1 -1
  15. package/lib/commonjs/state/widget-store.js +277 -30
  16. package/lib/commonjs/state/widget-store.js.map +1 -1
  17. package/lib/commonjs/storage.js +3 -1
  18. package/lib/commonjs/storage.js.map +1 -1
  19. package/lib/commonjs/types.js.map +1 -1
  20. package/lib/commonjs/ui/WidgetProvider.js +33 -13
  21. package/lib/commonjs/ui/WidgetProvider.js.map +1 -1
  22. package/lib/commonjs/ui/components/AttachmentChips.js +109 -40
  23. package/lib/commonjs/ui/components/AttachmentChips.js.map +1 -1
  24. package/lib/commonjs/ui/components/ConversationRow.js +65 -10
  25. package/lib/commonjs/ui/components/ConversationRow.js.map +1 -1
  26. package/lib/commonjs/ui/components/ImageViewer.js +229 -24
  27. package/lib/commonjs/ui/components/ImageViewer.js.map +1 -1
  28. package/lib/commonjs/ui/components/RatingCard.js +5 -2
  29. package/lib/commonjs/ui/components/RatingCard.js.map +1 -1
  30. package/lib/commonjs/ui/components/ZoomableImage.js +58 -10
  31. package/lib/commonjs/ui/components/ZoomableImage.js.map +1 -1
  32. package/lib/commonjs/ui/hooks/use-retained-state.js +39 -0
  33. package/lib/commonjs/ui/hooks/use-retained-state.js.map +1 -0
  34. package/lib/commonjs/ui/screens/chat/ChatView.js +637 -164
  35. package/lib/commonjs/ui/screens/chat/ChatView.js.map +1 -1
  36. package/lib/commonjs/ui/screens/chat/ChatView.styles.js +138 -1
  37. package/lib/commonjs/ui/screens/chat/ChatView.styles.js.map +1 -1
  38. package/lib/commonjs/ui/screens/conversations/ConversationsList.js +1 -0
  39. package/lib/commonjs/ui/screens/conversations/ConversationsList.js.map +1 -1
  40. package/lib/commonjs/ui/screens/home/HomeView.js +38 -55
  41. package/lib/commonjs/ui/screens/home/HomeView.js.map +1 -1
  42. package/lib/commonjs/ui/screens/home/HomeView.styles.js +36 -45
  43. package/lib/commonjs/ui/screens/home/HomeView.styles.js.map +1 -1
  44. package/lib/commonjs/ui/utils/html.js +13 -0
  45. package/lib/commonjs/ui/utils/html.js.map +1 -1
  46. package/lib/commonjs/ui/utils/image-load-queue.js +114 -0
  47. package/lib/commonjs/ui/utils/image-load-queue.js.map +1 -0
  48. package/lib/commonjs/ui/utils/inline-attachments.js +88 -0
  49. package/lib/commonjs/ui/utils/inline-attachments.js.map +1 -0
  50. package/lib/module/api/client.js +96 -23
  51. package/lib/module/api/client.js.map +1 -1
  52. package/lib/module/attachments/file-size.js +47 -0
  53. package/lib/module/attachments/file-size.js.map +1 -0
  54. package/lib/module/attachments/local-previews.js +33 -0
  55. package/lib/module/attachments/local-previews.js.map +1 -0
  56. package/lib/module/attachments/pickers.js +15 -25
  57. package/lib/module/attachments/pickers.js.map +1 -1
  58. package/lib/module/i18n.js +22 -3
  59. package/lib/module/i18n.js.map +1 -1
  60. package/lib/module/locales.js +12 -3
  61. package/lib/module/locales.js.map +1 -1
  62. package/lib/module/state/widget-store.js +279 -32
  63. package/lib/module/state/widget-store.js.map +1 -1
  64. package/lib/module/storage.js +2 -0
  65. package/lib/module/storage.js.map +1 -1
  66. package/lib/module/types.js.map +1 -1
  67. package/lib/module/ui/WidgetProvider.js +33 -13
  68. package/lib/module/ui/WidgetProvider.js.map +1 -1
  69. package/lib/module/ui/components/AttachmentChips.js +111 -42
  70. package/lib/module/ui/components/AttachmentChips.js.map +1 -1
  71. package/lib/module/ui/components/ConversationRow.js +65 -10
  72. package/lib/module/ui/components/ConversationRow.js.map +1 -1
  73. package/lib/module/ui/components/ImageViewer.js +231 -26
  74. package/lib/module/ui/components/ImageViewer.js.map +1 -1
  75. package/lib/module/ui/components/RatingCard.js +5 -2
  76. package/lib/module/ui/components/RatingCard.js.map +1 -1
  77. package/lib/module/ui/components/ZoomableImage.js +58 -10
  78. package/lib/module/ui/components/ZoomableImage.js.map +1 -1
  79. package/lib/module/ui/hooks/use-retained-state.js +34 -0
  80. package/lib/module/ui/hooks/use-retained-state.js.map +1 -0
  81. package/lib/module/ui/screens/chat/ChatView.js +641 -168
  82. package/lib/module/ui/screens/chat/ChatView.js.map +1 -1
  83. package/lib/module/ui/screens/chat/ChatView.styles.js +140 -2
  84. package/lib/module/ui/screens/chat/ChatView.styles.js.map +1 -1
  85. package/lib/module/ui/screens/conversations/ConversationsList.js +1 -0
  86. package/lib/module/ui/screens/conversations/ConversationsList.js.map +1 -1
  87. package/lib/module/ui/screens/home/HomeView.js +40 -57
  88. package/lib/module/ui/screens/home/HomeView.js.map +1 -1
  89. package/lib/module/ui/screens/home/HomeView.styles.js +36 -45
  90. package/lib/module/ui/screens/home/HomeView.styles.js.map +1 -1
  91. package/lib/module/ui/utils/html.js +12 -0
  92. package/lib/module/ui/utils/html.js.map +1 -1
  93. package/lib/module/ui/utils/image-load-queue.js +109 -0
  94. package/lib/module/ui/utils/image-load-queue.js.map +1 -0
  95. package/lib/module/ui/utils/inline-attachments.js +85 -0
  96. package/lib/module/ui/utils/inline-attachments.js.map +1 -0
  97. package/lib/typescript/commonjs/api/client.d.ts +12 -4
  98. package/lib/typescript/commonjs/api/client.d.ts.map +1 -1
  99. package/lib/typescript/commonjs/attachments/file-size.d.ts +15 -0
  100. package/lib/typescript/commonjs/attachments/file-size.d.ts.map +1 -0
  101. package/lib/typescript/commonjs/attachments/local-previews.d.ts +14 -0
  102. package/lib/typescript/commonjs/attachments/local-previews.d.ts.map +1 -0
  103. package/lib/typescript/commonjs/attachments/pickers.d.ts +15 -2
  104. package/lib/typescript/commonjs/attachments/pickers.d.ts.map +1 -1
  105. package/lib/typescript/commonjs/i18n.d.ts +12 -3
  106. package/lib/typescript/commonjs/i18n.d.ts.map +1 -1
  107. package/lib/typescript/commonjs/locales.d.ts.map +1 -1
  108. package/lib/typescript/commonjs/state/widget-store.d.ts +40 -1
  109. package/lib/typescript/commonjs/state/widget-store.d.ts.map +1 -1
  110. package/lib/typescript/commonjs/storage.d.ts +2 -0
  111. package/lib/typescript/commonjs/storage.d.ts.map +1 -1
  112. package/lib/typescript/commonjs/types.d.ts +41 -0
  113. package/lib/typescript/commonjs/types.d.ts.map +1 -1
  114. package/lib/typescript/commonjs/ui/WidgetProvider.d.ts.map +1 -1
  115. package/lib/typescript/commonjs/ui/components/AttachmentChips.d.ts +13 -3
  116. package/lib/typescript/commonjs/ui/components/AttachmentChips.d.ts.map +1 -1
  117. package/lib/typescript/commonjs/ui/components/ConversationRow.d.ts +15 -1
  118. package/lib/typescript/commonjs/ui/components/ConversationRow.d.ts.map +1 -1
  119. package/lib/typescript/commonjs/ui/components/ImageViewer.d.ts +26 -6
  120. package/lib/typescript/commonjs/ui/components/ImageViewer.d.ts.map +1 -1
  121. package/lib/typescript/commonjs/ui/components/RatingCard.d.ts.map +1 -1
  122. package/lib/typescript/commonjs/ui/components/ZoomableImage.d.ts +10 -1
  123. package/lib/typescript/commonjs/ui/components/ZoomableImage.d.ts.map +1 -1
  124. package/lib/typescript/commonjs/ui/hooks/use-retained-state.d.ts +11 -0
  125. package/lib/typescript/commonjs/ui/hooks/use-retained-state.d.ts.map +1 -0
  126. package/lib/typescript/commonjs/ui/screens/chat/ChatView.d.ts +4 -4
  127. package/lib/typescript/commonjs/ui/screens/chat/ChatView.d.ts.map +1 -1
  128. package/lib/typescript/commonjs/ui/screens/chat/ChatView.styles.d.ts +144 -0
  129. package/lib/typescript/commonjs/ui/screens/chat/ChatView.styles.d.ts.map +1 -1
  130. package/lib/typescript/commonjs/ui/screens/chat/ChatView.types.d.ts +26 -1
  131. package/lib/typescript/commonjs/ui/screens/chat/ChatView.types.d.ts.map +1 -1
  132. package/lib/typescript/commonjs/ui/screens/conversations/ConversationsList.d.ts.map +1 -1
  133. package/lib/typescript/commonjs/ui/screens/conversations/ConversationsList.types.d.ts +2 -0
  134. package/lib/typescript/commonjs/ui/screens/conversations/ConversationsList.types.d.ts.map +1 -1
  135. package/lib/typescript/commonjs/ui/screens/home/HomeView.d.ts +7 -5
  136. package/lib/typescript/commonjs/ui/screens/home/HomeView.d.ts.map +1 -1
  137. package/lib/typescript/commonjs/ui/screens/home/HomeView.styles.d.ts +30 -35
  138. package/lib/typescript/commonjs/ui/screens/home/HomeView.styles.d.ts.map +1 -1
  139. package/lib/typescript/commonjs/ui/screens/home/HomeView.types.d.ts +5 -6
  140. package/lib/typescript/commonjs/ui/screens/home/HomeView.types.d.ts.map +1 -1
  141. package/lib/typescript/commonjs/ui/utils/html.d.ts +5 -0
  142. package/lib/typescript/commonjs/ui/utils/html.d.ts.map +1 -1
  143. package/lib/typescript/commonjs/ui/utils/image-load-queue.d.ts +44 -0
  144. package/lib/typescript/commonjs/ui/utils/image-load-queue.d.ts.map +1 -0
  145. package/lib/typescript/commonjs/ui/utils/inline-attachments.d.ts +29 -0
  146. package/lib/typescript/commonjs/ui/utils/inline-attachments.d.ts.map +1 -0
  147. package/lib/typescript/module/api/client.d.ts +12 -4
  148. package/lib/typescript/module/api/client.d.ts.map +1 -1
  149. package/lib/typescript/module/attachments/file-size.d.ts +15 -0
  150. package/lib/typescript/module/attachments/file-size.d.ts.map +1 -0
  151. package/lib/typescript/module/attachments/local-previews.d.ts +14 -0
  152. package/lib/typescript/module/attachments/local-previews.d.ts.map +1 -0
  153. package/lib/typescript/module/attachments/pickers.d.ts +15 -2
  154. package/lib/typescript/module/attachments/pickers.d.ts.map +1 -1
  155. package/lib/typescript/module/i18n.d.ts +12 -3
  156. package/lib/typescript/module/i18n.d.ts.map +1 -1
  157. package/lib/typescript/module/locales.d.ts.map +1 -1
  158. package/lib/typescript/module/state/widget-store.d.ts +40 -1
  159. package/lib/typescript/module/state/widget-store.d.ts.map +1 -1
  160. package/lib/typescript/module/storage.d.ts +2 -0
  161. package/lib/typescript/module/storage.d.ts.map +1 -1
  162. package/lib/typescript/module/types.d.ts +41 -0
  163. package/lib/typescript/module/types.d.ts.map +1 -1
  164. package/lib/typescript/module/ui/WidgetProvider.d.ts.map +1 -1
  165. package/lib/typescript/module/ui/components/AttachmentChips.d.ts +13 -3
  166. package/lib/typescript/module/ui/components/AttachmentChips.d.ts.map +1 -1
  167. package/lib/typescript/module/ui/components/ConversationRow.d.ts +15 -1
  168. package/lib/typescript/module/ui/components/ConversationRow.d.ts.map +1 -1
  169. package/lib/typescript/module/ui/components/ImageViewer.d.ts +26 -6
  170. package/lib/typescript/module/ui/components/ImageViewer.d.ts.map +1 -1
  171. package/lib/typescript/module/ui/components/RatingCard.d.ts.map +1 -1
  172. package/lib/typescript/module/ui/components/ZoomableImage.d.ts +10 -1
  173. package/lib/typescript/module/ui/components/ZoomableImage.d.ts.map +1 -1
  174. package/lib/typescript/module/ui/hooks/use-retained-state.d.ts +11 -0
  175. package/lib/typescript/module/ui/hooks/use-retained-state.d.ts.map +1 -0
  176. package/lib/typescript/module/ui/screens/chat/ChatView.d.ts +4 -4
  177. package/lib/typescript/module/ui/screens/chat/ChatView.d.ts.map +1 -1
  178. package/lib/typescript/module/ui/screens/chat/ChatView.styles.d.ts +144 -0
  179. package/lib/typescript/module/ui/screens/chat/ChatView.styles.d.ts.map +1 -1
  180. package/lib/typescript/module/ui/screens/chat/ChatView.types.d.ts +26 -1
  181. package/lib/typescript/module/ui/screens/chat/ChatView.types.d.ts.map +1 -1
  182. package/lib/typescript/module/ui/screens/conversations/ConversationsList.d.ts.map +1 -1
  183. package/lib/typescript/module/ui/screens/conversations/ConversationsList.types.d.ts +2 -0
  184. package/lib/typescript/module/ui/screens/conversations/ConversationsList.types.d.ts.map +1 -1
  185. package/lib/typescript/module/ui/screens/home/HomeView.d.ts +7 -5
  186. package/lib/typescript/module/ui/screens/home/HomeView.d.ts.map +1 -1
  187. package/lib/typescript/module/ui/screens/home/HomeView.styles.d.ts +30 -35
  188. package/lib/typescript/module/ui/screens/home/HomeView.styles.d.ts.map +1 -1
  189. package/lib/typescript/module/ui/screens/home/HomeView.types.d.ts +5 -6
  190. package/lib/typescript/module/ui/screens/home/HomeView.types.d.ts.map +1 -1
  191. package/lib/typescript/module/ui/utils/html.d.ts +5 -0
  192. package/lib/typescript/module/ui/utils/html.d.ts.map +1 -1
  193. package/lib/typescript/module/ui/utils/image-load-queue.d.ts +44 -0
  194. package/lib/typescript/module/ui/utils/image-load-queue.d.ts.map +1 -0
  195. package/lib/typescript/module/ui/utils/inline-attachments.d.ts +29 -0
  196. package/lib/typescript/module/ui/utils/inline-attachments.d.ts.map +1 -0
  197. package/package.json +1 -1
  198. package/src/api/client.ts +116 -34
  199. package/src/attachments/file-size.ts +42 -0
  200. package/src/attachments/local-previews.ts +32 -0
  201. package/src/attachments/pickers.ts +23 -25
  202. package/src/i18n.ts +22 -3
  203. package/src/locales.ts +12 -3
  204. package/src/state/widget-store.ts +322 -50
  205. package/src/storage.ts +2 -0
  206. package/src/types.ts +41 -0
  207. package/src/ui/WidgetProvider.tsx +35 -8
  208. package/src/ui/components/AttachmentChips.tsx +100 -29
  209. package/src/ui/components/ConversationRow.tsx +90 -9
  210. package/src/ui/components/ImageViewer.tsx +242 -24
  211. package/src/ui/components/RatingCard.tsx +5 -2
  212. package/src/ui/components/ZoomableImage.tsx +58 -5
  213. package/src/ui/hooks/use-retained-state.ts +35 -0
  214. package/src/ui/screens/chat/ChatView.styles.ts +139 -1
  215. package/src/ui/screens/chat/ChatView.tsx +721 -197
  216. package/src/ui/screens/chat/ChatView.types.ts +26 -1
  217. package/src/ui/screens/conversations/ConversationsList.tsx +1 -0
  218. package/src/ui/screens/conversations/ConversationsList.types.ts +2 -0
  219. package/src/ui/screens/home/HomeView.styles.ts +36 -45
  220. package/src/ui/screens/home/HomeView.tsx +54 -70
  221. package/src/ui/screens/home/HomeView.types.ts +5 -6
  222. package/src/ui/utils/html.ts +26 -0
  223. package/src/ui/utils/image-load-queue.ts +108 -0
  224. package/src/ui/utils/inline-attachments.ts +71 -0
@@ -104,9 +104,9 @@ export function RepzoWidgetProvider({
104
104
  [state.config, state.locale]
105
105
  );
106
106
 
107
- // Home previews the two newest threads and credits the agents behind them.
108
- const recentConversations = useMemo(
109
- () => pickRecentConversations(state.conversations, 2),
107
+ // Home previews the newest thread and credits the agents behind the recent ones.
108
+ const recentConversation = useMemo(
109
+ () => pickRecentConversations(state.conversations, 1)[0],
110
110
  [state.conversations]
111
111
  );
112
112
  const teamAgents = useMemo(() => pickTeamAgents(state.conversations, 3), [state.conversations]);
@@ -132,6 +132,15 @@ export function RepzoWidgetProvider({
132
132
  }),
133
133
  [labels]
134
134
  );
135
+ const youPrefix = store.showsYouPrefix();
136
+ const lastMessageLabels = useMemo(
137
+ () => ({
138
+ photo: labels.lastMessagePhoto,
139
+ file: labels.lastMessageFile,
140
+ you: youPrefix ? labels.lastMessageYou : '',
141
+ }),
142
+ [labels, youPrefix]
143
+ );
135
144
 
136
145
  const blockLabels = useMemo(
137
146
  () => ({
@@ -171,6 +180,10 @@ export function RepzoWidgetProvider({
171
180
  tooLargeTogether: labels.attachmentsTooLargeTogether,
172
181
  failed: labels.attachmentFailed,
173
182
  cameraDenied: labels.cameraPermissionDenied,
183
+ uploading: labels.attachmentUploading,
184
+ uploadFailed: labels.attachmentUploadFailed,
185
+ uploadFailedNotice: labels.attachmentsUploadFailed,
186
+ retry: labels.tapToRetry,
174
187
  }),
175
188
  [labels, theme.maxAttachmentBytes]
176
189
  );
@@ -184,6 +197,10 @@ export function RepzoWidgetProvider({
184
197
  loadEarlier: labels.loadEarlier,
185
198
  emptyConversation: labels.emptyConversation,
186
199
  messagePlaceholder: labels.messagePlaceholder,
200
+ send: labels.sendMessage,
201
+ jumpToLatest: labels.jumpToLatest,
202
+ jumpToLatestNew: labels.jumpToLatestNew,
203
+ morePhotos: labels.morePhotos,
187
204
  failedToSend: labels.failedToSend,
188
205
  tapToRetry: labels.tapToRetry,
189
206
  openAttachment: labels.openAttachment,
@@ -228,6 +245,7 @@ export function RepzoWidgetProvider({
228
245
  close: labels.close,
229
246
  loadFailed: labels.imageLoadFailed,
230
247
  openExternally: labels.openImageExternally,
248
+ position: labels.imagePosition,
231
249
  },
232
250
  voice: {
233
251
  play: labels.voicePlay,
@@ -247,6 +265,14 @@ export function RepzoWidgetProvider({
247
265
  pickAttachments(source, current, theme.maxAttachmentBytes),
248
266
  [theme.maxAttachmentBytes]
249
267
  );
268
+ const handleUploadAttachment = useCallback(
269
+ (draft: DraftAttachment, signal?: AbortSignal) =>
270
+ store.uploadAttachment(
271
+ { uri: draft.fileUri!, name: draft.name, contentType: draft.contentType, size: draft.size },
272
+ signal
273
+ ),
274
+ [store]
275
+ );
250
276
 
251
277
  const handleOpenNotification = useCallback(
252
278
  (conversationId: string) => void store.openConversation(conversationId),
@@ -453,8 +479,10 @@ export function RepzoWidgetProvider({
453
479
  labels={chatLabels}
454
480
  attachmentSources={attachmentSources}
455
481
  onPickAttachments={handlePickAttachments}
482
+ onUploadAttachment={handleUploadAttachment}
456
483
  onSendMessage={handleSendMessage}
457
484
  onRetryMessage={handleRetryMessage}
485
+ photoAlbums={store.showsPhotoAlbums()}
458
486
  onIdentifyVisitor={handleIdentifyVisitor}
459
487
  onRateConversation={handleRateConversation}
460
488
  onLoadMore={handleLoadMore}
@@ -472,7 +500,7 @@ export function RepzoWidgetProvider({
472
500
  color={theme.primaryColor}
473
501
  visitorName={state.visitorProfile.name}
474
502
  conversationCount={state.conversations.length}
475
- recentConversations={recentConversations}
503
+ recentConversation={recentConversation}
476
504
  unreadByConversation={state.unreadByConversation}
477
505
  teamAgents={teamAgents}
478
506
  isOnline={theme.isOnline}
@@ -483,19 +511,17 @@ export function RepzoWidgetProvider({
483
511
  showPreviousConversations={theme.showPreviousConversations}
484
512
  labels={{
485
513
  close: labels.close,
486
- viewPrevious: labels.viewPreviousConversations,
487
- viewAll: formatLabel(labels.homeViewAllCount, { count: state.conversations.length }),
488
- recentTitle: labels.homeRecentTitle,
514
+ recentTitle: labels.homeRecentMessageTitle,
489
515
  noConversations: labels.homeNoConversations,
490
516
  statusOnline: labels.homeStatusOnline,
491
517
  statusAway: labels.homeStatusAway,
492
518
  team: labels.team,
493
519
  time: timeLabels,
494
520
  unread: labels.unreadBadge,
521
+ lastMessage: lastMessageLabels,
495
522
  }}
496
523
  onClose={handleClose}
497
524
  onStartConversation={handleStartConversation}
498
- onViewConversations={() => handleTabChange('messages')}
499
525
  onOpenConversation={(conversationId) => store.openConversation(conversationId)}
500
526
  />
501
527
  );
@@ -521,6 +547,7 @@ export function RepzoWidgetProvider({
521
547
  statusAway: labels.homeStatusAway,
522
548
  time: timeLabels,
523
549
  unread: labels.unreadBadge,
550
+ lastMessage: lastMessageLabels,
524
551
  }}
525
552
  onSelect={(conversationId) => store.openConversation(conversationId)}
526
553
  onNewConversation={handleStartConversation}
@@ -1,9 +1,11 @@
1
1
  /**
2
2
  * Picked-but-unsent attachments above the composer: a thumbnail for images, a
3
3
  * name + size card for everything else, each with its own remove button.
4
+ * A file that uploads ahead of the send shows a spinner until it's up, and
5
+ * turns into a retry button when the upload fails.
4
6
  */
5
7
 
6
- import { Image, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
8
+ import { ActivityIndicator, Image, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
7
9
 
8
10
  import {
9
11
  formatBytes,
@@ -11,24 +13,29 @@ import {
11
13
  type DraftAttachment,
12
14
  } from '../../attachments/pickers';
13
15
  import { formatLabel } from '../../i18n';
16
+ import { globalStyles } from '../styles/global';
14
17
  import { fontSize, palette, radius, spacing, withAlpha } from '../styles/theme';
15
18
  import { START_ALIGN } from '../utils/rtl';
16
- import { CloseIcon } from './icons';
19
+ import { AlertIcon, CloseIcon } from './icons';
17
20
 
18
21
  interface AttachmentChipsProps {
19
22
  attachments: DraftAttachment[];
20
23
  /** While a send is in flight the chips are dimmed and can't be removed. */
21
24
  disabled?: boolean;
22
- /** Keeps its `{name}` placeholder; filled per chip. */
23
- removeLabel: string;
25
+ /** `remove`, `uploading` and `uploadFailed` keep their `{name}` placeholder; filled per chip. */
26
+ labels: { remove: string; uploading: string; uploadFailed: string; retry: string };
27
+ /** Removing a chip mid-upload cancels the upload. */
24
28
  onRemove: (id: string) => void;
29
+ /** Starts a failed upload again. */
30
+ onRetry: (id: string) => void;
25
31
  }
26
32
 
27
33
  export function AttachmentChips({
28
34
  attachments,
29
35
  disabled,
30
- removeLabel,
36
+ labels,
31
37
  onRemove,
38
+ onRetry,
32
39
  }: AttachmentChipsProps) {
33
40
  return (
34
41
  <ScrollView
@@ -37,38 +44,82 @@ export function AttachmentChips({
37
44
  keyboardShouldPersistTaps="handled"
38
45
  style={styles.scroller}
39
46
  contentContainerStyle={styles.row}>
40
- {attachments.map((attachment) => (
41
- <View key={attachment.id} style={[styles.chip, disabled && styles.chipDisabled]}>
42
- {isImageAttachment(attachment.contentType) ? (
47
+ {attachments.map((attachment) => {
48
+ const uploading = attachment.uploadStatus === 'uploading';
49
+ const failed = attachment.uploadStatus === 'failed';
50
+ const preview = isImageAttachment(attachment.contentType) ? (
51
+ <View>
43
52
  <Image
44
53
  source={{ uri: attachment.previewUri }}
45
54
  style={styles.thumbnail}
46
55
  resizeMode="cover"
47
- accessible
56
+ accessible={!uploading && !failed}
48
57
  accessibilityLabel={attachment.name}
49
58
  accessibilityIgnoresInvertColors
50
59
  />
51
- ) : (
52
- <View style={styles.file}>
53
- <Text style={styles.fileName} numberOfLines={1}>
54
- {attachment.name}
60
+ {uploading || failed ? (
61
+ <View style={[styles.scrim, failed && styles.scrimFailed]}>
62
+ {uploading ? (
63
+ <ActivityIndicator color={palette.white} />
64
+ ) : (
65
+ <AlertIcon size={20} color={palette.white} />
66
+ )}
67
+ </View>
68
+ ) : null}
69
+ </View>
70
+ ) : (
71
+ <View style={styles.file}>
72
+ <Text style={styles.fileName} numberOfLines={1}>
73
+ {attachment.name}
74
+ </Text>
75
+ <View style={styles.fileStatus}>
76
+ {uploading ? (
77
+ <ActivityIndicator size="small" color={palette.textMuted} />
78
+ ) : failed ? (
79
+ <AlertIcon size={12} color={palette.danger} />
80
+ ) : null}
81
+ <Text style={[styles.fileSize, failed && styles.fileFailed]} numberOfLines={1}>
82
+ {failed ? labels.retry : formatBytes(attachment.size)}
55
83
  </Text>
56
- <Text style={styles.fileSize}>{formatBytes(attachment.size)}</Text>
57
84
  </View>
58
- )}
59
- <Pressable
60
- onPress={() => onRemove(attachment.id)}
61
- disabled={disabled}
62
- accessibilityRole="button"
63
- accessibilityLabel={formatLabel(removeLabel, { name: attachment.name })}
64
- accessibilityState={{ disabled: !!disabled }}
65
- // 22pt badge + 11pt on every side = a 44pt target.
66
- hitSlop={11}
67
- style={({ pressed }) => [styles.remove, pressed && styles.removePressed]}>
68
- <CloseIcon size={10} color={palette.white} />
69
- </Pressable>
70
- </View>
71
- ))}
85
+ </View>
86
+ );
87
+
88
+ return (
89
+ <View key={attachment.id} style={[styles.chip, disabled && styles.chipDisabled]}>
90
+ {failed ? (
91
+ <Pressable
92
+ onPress={() => onRetry(attachment.id)}
93
+ disabled={disabled}
94
+ accessibilityRole="button"
95
+ accessibilityLabel={formatLabel(labels.uploadFailed, { name: attachment.name })}
96
+ style={({ pressed }) => pressed && globalStyles.pressed}>
97
+ {preview}
98
+ </Pressable>
99
+ ) : uploading ? (
100
+ <View
101
+ accessible
102
+ accessibilityLabel={formatLabel(labels.uploading, { name: attachment.name })}
103
+ accessibilityState={{ busy: true }}>
104
+ {preview}
105
+ </View>
106
+ ) : (
107
+ preview
108
+ )}
109
+ <Pressable
110
+ onPress={() => onRemove(attachment.id)}
111
+ disabled={disabled}
112
+ accessibilityRole="button"
113
+ accessibilityLabel={formatLabel(labels.remove, { name: attachment.name })}
114
+ accessibilityState={{ disabled: !!disabled }}
115
+ // 22pt badge + 11pt on every side = a 44pt target.
116
+ hitSlop={11}
117
+ style={({ pressed }) => [styles.remove, pressed && styles.removePressed]}>
118
+ <CloseIcon size={10} color={palette.white} />
119
+ </Pressable>
120
+ </View>
121
+ );
122
+ })}
72
123
  </ScrollView>
73
124
  );
74
125
  }
@@ -98,6 +149,16 @@ const styles = StyleSheet.create({
98
149
  width: CHIP_HEIGHT,
99
150
  height: CHIP_HEIGHT,
100
151
  },
152
+ // Over the thumbnail while it uploads; darker once it failed, so the alert reads.
153
+ scrim: {
154
+ ...StyleSheet.absoluteFillObject,
155
+ alignItems: 'center',
156
+ justifyContent: 'center',
157
+ backgroundColor: withAlpha(palette.ink, 0.4),
158
+ },
159
+ scrimFailed: {
160
+ backgroundColor: withAlpha(palette.ink, 0.6),
161
+ },
101
162
  file: {
102
163
  width: 156,
103
164
  height: CHIP_HEIGHT,
@@ -112,11 +173,21 @@ const styles = StyleSheet.create({
112
173
  fontWeight: '600',
113
174
  color: palette.ink,
114
175
  },
176
+ fileStatus: {
177
+ flexDirection: 'row',
178
+ alignItems: 'center',
179
+ gap: spacing.xs,
180
+ marginTop: 2,
181
+ },
115
182
  fileSize: {
183
+ flexShrink: 1,
116
184
  textAlign: START_ALIGN,
117
185
  fontSize: fontSize.caption,
118
186
  color: palette.textMuted,
119
- marginTop: 2,
187
+ },
188
+ fileFailed: {
189
+ color: palette.danger,
190
+ fontWeight: '600',
120
191
  },
121
192
  remove: {
122
193
  position: 'absolute',
@@ -15,6 +15,15 @@ import { forwardDirection, START_ALIGN } from '../utils/rtl';
15
15
  import { fontSize, palette, spacing } from '../styles/theme';
16
16
 
17
17
  const AVATAR_SIZE = 40;
18
+ const CARD_AVATAR_SIZE = 32;
19
+
20
+ export interface LastMessageLabels {
21
+ /** Stand in for a newest message that had no words, only photos or other files. */
22
+ photo: string;
23
+ file: string;
24
+ /** Before the visitor's own newest message; keeps `{message}`. Empty: no prefix (`boot({ youPrefix: false })`). */
25
+ you: string;
26
+ }
18
27
 
19
28
  interface ConversationRowProps {
20
29
  conversation: Conversation;
@@ -26,8 +35,15 @@ interface ConversationRowProps {
26
35
  unreadCount?: number;
27
36
  /** Template with a `{count}` token, for screen readers. */
28
37
  unreadLabel?: string;
38
+ lastMessageLabels?: LastMessageLabels;
29
39
  /** Messages runs full-bleed; Home insets it inside a card. */
30
40
  horizontalPadding?: number;
41
+ /**
42
+ * `card` is Home's "Recent message" card: a smaller avatar, a regular-weight
43
+ * name with the time right after it, and no chevron. The words, unread state
44
+ * and screen-reader label are the list's.
45
+ */
46
+ variant?: 'list' | 'card';
31
47
  onPress: () => void;
32
48
  }
33
49
 
@@ -38,47 +54,70 @@ export function ConversationRow({
38
54
  timeLabels,
39
55
  unreadCount = 0,
40
56
  unreadLabel,
57
+ lastMessageLabels,
41
58
  horizontalPadding = spacing.xl,
59
+ variant = 'list',
42
60
  onPress,
43
61
  }: ConversationRowProps) {
62
+ const card = variant === 'card';
44
63
  const displayName = conversation.agentName || fallbackName;
45
64
  const hasUnread = unreadCount > 0;
46
65
  const unreadSuffix =
47
66
  hasUnread && unreadLabel ? `. ${formatLabel(unreadLabel, { count: unreadCount })}` : '';
67
+ const attachment = conversation.lastMessageAttachment;
68
+ // "Photo" or "File" over Workstation's "2 attachments".
69
+ const snippet = (attachment && lastMessageLabels?.[attachment]) || conversation.snippet || '';
70
+ // "You: " is drawn a shade stronger than the message, so it reads as a label.
71
+ const [youBefore, youAfter] =
72
+ snippet && conversation.lastMessageFromVisitor && lastMessageLabels?.you
73
+ ? splitAround(lastMessageLabels.you, '{message}')
74
+ : ['', ''];
75
+ const snippetLabel = `${youBefore}${snippet}${youAfter}`;
48
76
 
49
77
  return (
50
78
  <Pressable
51
79
  onPress={onPress}
52
80
  accessibilityRole="button"
53
81
  accessibilityLabel={`${
54
- conversation.snippet ? `${displayName}. ${conversation.snippet}` : displayName
82
+ snippet ? `${displayName}. ${snippetLabel}` : displayName
55
83
  }${unreadSuffix}`}
56
84
  android_ripple={{ color: palette.borderSoft }}
57
85
  style={({ pressed }) => [
58
86
  styles.row,
87
+ card && cardStyles.row,
59
88
  { paddingHorizontal: horizontalPadding },
60
89
  pressed && styles.rowPressed,
61
90
  ]}>
62
91
  <Avatar
63
92
  avatar={conversation.agentAvatar}
64
93
  name={conversation.agentName}
65
- size={AVATAR_SIZE}
94
+ size={card ? CARD_AVATAR_SIZE : AVATAR_SIZE}
66
95
  color={color}
67
96
  />
68
97
  <View style={styles.content}>
69
- <View style={styles.topLine}>
70
- <Text style={[styles.name, hasUnread && styles.nameUnread]} numberOfLines={1}>
98
+ <View style={[styles.topLine, card && cardStyles.topLine]}>
99
+ <Text
100
+ style={
101
+ card
102
+ ? [cardStyles.name, hasUnread && cardStyles.nameUnread]
103
+ : [styles.name, hasUnread && styles.nameUnread]
104
+ }
105
+ numberOfLines={1}>
71
106
  {displayName}
72
107
  </Text>
73
- <Text style={styles.time}>
108
+ <Text style={[styles.time, card && cardStyles.time]}>
74
109
  {conversation.lastMessageAt
75
110
  ? formatRelativeTime(conversation.lastMessageAt, timeLabels)
76
111
  : ''}
77
112
  </Text>
78
113
  </View>
79
- {conversation.snippet ? (
80
- <Text style={[styles.snippet, hasUnread && styles.snippetUnread]} numberOfLines={2}>
81
- {conversation.snippet}
114
+ {snippet ? (
115
+ <Text
116
+ style={[styles.snippet, card && cardStyles.snippet, hasUnread && styles.snippetUnread]}
117
+ numberOfLines={2}>
118
+ {youBefore ? <Text style={styles.snippetYou}>{youBefore}</Text> : null}
119
+ {snippet}
120
+ {youAfter}
82
121
  </Text>
83
122
  ) : null}
84
123
  </View>
@@ -86,13 +125,20 @@ export function ConversationRow({
86
125
  <View style={[styles.badge, { backgroundColor: color }]}>
87
126
  <Text style={styles.badgeLabel}>{unreadCount > 9 ? '9+' : unreadCount}</Text>
88
127
  </View>
89
- ) : (
128
+ ) : card ? null : (
90
129
  <ChevronIcon size={16} direction={forwardDirection()} color={palette.textSubtle} />
91
130
  )}
92
131
  </Pressable>
93
132
  );
94
133
  }
95
134
 
135
+ /** `'You: {message}'` → `['You: ', '']`; a template without the token goes in front. */
136
+ function splitAround(template: string, token: string): [string, string] {
137
+ const index = template.indexOf(token);
138
+ if (index < 0) return [`${template} `, ''];
139
+ return [template.slice(0, index), template.slice(index + token.length)];
140
+ }
141
+
96
142
  /** Where a divider should start so it sits under the text, not the avatar. */
97
143
  export function rowDividerInset(horizontalPadding: number = spacing.xl): number {
98
144
  return horizontalPadding + AVATAR_SIZE + spacing.md;
@@ -145,6 +191,10 @@ const styles = StyleSheet.create({
145
191
  color: palette.text,
146
192
  fontWeight: '500',
147
193
  },
194
+ snippetYou: {
195
+ color: palette.text,
196
+ fontWeight: '600',
197
+ },
148
198
  // Replaces the chevron rather than crowding beside it, so the row keeps
149
199
  // the same width whether or not there is anything unread.
150
200
  badge: {
@@ -161,3 +211,34 @@ const styles = StyleSheet.create({
161
211
  fontWeight: '700',
162
212
  },
163
213
  });
214
+
215
+ // Overrides for the `card` variant. It sits under the card's title, so it
216
+ // drops the list's minimum height, and its name, time and last message share
217
+ // one size, the way the Home design draws them.
218
+ const cardStyles = StyleSheet.create({
219
+ row: {
220
+ minHeight: 0,
221
+ paddingVertical: spacing.sm,
222
+ },
223
+ // The time follows the name instead of being pushed to the far edge.
224
+ topLine: {
225
+ justifyContent: 'flex-start',
226
+ },
227
+ name: {
228
+ textAlign: START_ALIGN,
229
+ flexShrink: 1,
230
+ fontSize: fontSize.body,
231
+ color: palette.ink,
232
+ },
233
+ nameUnread: {
234
+ fontWeight: '600',
235
+ },
236
+ time: {
237
+ fontSize: fontSize.body,
238
+ },
239
+ snippet: {
240
+ fontSize: fontSize.body,
241
+ lineHeight: fontSize.body * 1.35,
242
+ color: palette.textSubtle,
243
+ },
244
+ });