@repzo/widget-react-native 1.3.0 → 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 (251) hide show
  1. package/CHANGELOG.md +179 -0
  2. package/README.md +72 -16
  3. package/lib/commonjs/api/client.js +113 -0
  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 +76 -28
  10. package/lib/commonjs/attachments/pickers.js.map +1 -1
  11. package/lib/commonjs/i18n.js +24 -3
  12. package/lib/commonjs/i18n.js.map +1 -1
  13. package/lib/commonjs/locales.js +13 -3
  14. package/lib/commonjs/locales.js.map +1 -1
  15. package/lib/commonjs/state/widget-store.js +486 -63
  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 +40 -16
  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 +231 -25
  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/components/icons.js +22 -16
  33. package/lib/commonjs/ui/components/icons.js.map +1 -1
  34. package/lib/commonjs/ui/hooks/use-retained-state.js +39 -0
  35. package/lib/commonjs/ui/hooks/use-retained-state.js.map +1 -0
  36. package/lib/commonjs/ui/screens/chat/ChatView.js +666 -171
  37. package/lib/commonjs/ui/screens/chat/ChatView.js.map +1 -1
  38. package/lib/commonjs/ui/screens/chat/ChatView.styles.js +150 -3
  39. package/lib/commonjs/ui/screens/chat/ChatView.styles.js.map +1 -1
  40. package/lib/commonjs/ui/screens/conversations/ConversationsList.js +1 -0
  41. package/lib/commonjs/ui/screens/conversations/ConversationsList.js.map +1 -1
  42. package/lib/commonjs/ui/screens/home/HomeView.js +38 -55
  43. package/lib/commonjs/ui/screens/home/HomeView.js.map +1 -1
  44. package/lib/commonjs/ui/screens/home/HomeView.styles.js +36 -45
  45. package/lib/commonjs/ui/screens/home/HomeView.styles.js.map +1 -1
  46. package/lib/commonjs/ui/styles/theme.js +1 -0
  47. package/lib/commonjs/ui/styles/theme.js.map +1 -1
  48. package/lib/commonjs/ui/utils/conversations.js +17 -1
  49. package/lib/commonjs/ui/utils/conversations.js.map +1 -1
  50. package/lib/commonjs/ui/utils/html.js +13 -0
  51. package/lib/commonjs/ui/utils/html.js.map +1 -1
  52. package/lib/commonjs/ui/utils/image-load-queue.js +114 -0
  53. package/lib/commonjs/ui/utils/image-load-queue.js.map +1 -0
  54. package/lib/commonjs/ui/utils/inline-attachments.js +88 -0
  55. package/lib/commonjs/ui/utils/inline-attachments.js.map +1 -0
  56. package/lib/module/api/client.js +113 -0
  57. package/lib/module/api/client.js.map +1 -1
  58. package/lib/module/attachments/file-size.js +47 -0
  59. package/lib/module/attachments/file-size.js.map +1 -0
  60. package/lib/module/attachments/local-previews.js +33 -0
  61. package/lib/module/attachments/local-previews.js.map +1 -0
  62. package/lib/module/attachments/pickers.js +74 -27
  63. package/lib/module/attachments/pickers.js.map +1 -1
  64. package/lib/module/i18n.js +24 -3
  65. package/lib/module/i18n.js.map +1 -1
  66. package/lib/module/locales.js +13 -3
  67. package/lib/module/locales.js.map +1 -1
  68. package/lib/module/state/widget-store.js +488 -65
  69. package/lib/module/state/widget-store.js.map +1 -1
  70. package/lib/module/storage.js +2 -0
  71. package/lib/module/storage.js.map +1 -1
  72. package/lib/module/types.js.map +1 -1
  73. package/lib/module/ui/WidgetProvider.js +40 -16
  74. package/lib/module/ui/WidgetProvider.js.map +1 -1
  75. package/lib/module/ui/components/AttachmentChips.js +111 -42
  76. package/lib/module/ui/components/AttachmentChips.js.map +1 -1
  77. package/lib/module/ui/components/ConversationRow.js +65 -10
  78. package/lib/module/ui/components/ConversationRow.js.map +1 -1
  79. package/lib/module/ui/components/ImageViewer.js +233 -27
  80. package/lib/module/ui/components/ImageViewer.js.map +1 -1
  81. package/lib/module/ui/components/RatingCard.js +5 -2
  82. package/lib/module/ui/components/RatingCard.js.map +1 -1
  83. package/lib/module/ui/components/ZoomableImage.js +58 -10
  84. package/lib/module/ui/components/ZoomableImage.js.map +1 -1
  85. package/lib/module/ui/components/icons.js +21 -15
  86. package/lib/module/ui/components/icons.js.map +1 -1
  87. package/lib/module/ui/hooks/use-retained-state.js +34 -0
  88. package/lib/module/ui/hooks/use-retained-state.js.map +1 -0
  89. package/lib/module/ui/screens/chat/ChatView.js +672 -177
  90. package/lib/module/ui/screens/chat/ChatView.js.map +1 -1
  91. package/lib/module/ui/screens/chat/ChatView.styles.js +152 -4
  92. package/lib/module/ui/screens/chat/ChatView.styles.js.map +1 -1
  93. package/lib/module/ui/screens/conversations/ConversationsList.js +1 -0
  94. package/lib/module/ui/screens/conversations/ConversationsList.js.map +1 -1
  95. package/lib/module/ui/screens/home/HomeView.js +40 -57
  96. package/lib/module/ui/screens/home/HomeView.js.map +1 -1
  97. package/lib/module/ui/screens/home/HomeView.styles.js +36 -45
  98. package/lib/module/ui/screens/home/HomeView.styles.js.map +1 -1
  99. package/lib/module/ui/styles/theme.js +1 -0
  100. package/lib/module/ui/styles/theme.js.map +1 -1
  101. package/lib/module/ui/utils/conversations.js +15 -1
  102. package/lib/module/ui/utils/conversations.js.map +1 -1
  103. package/lib/module/ui/utils/html.js +12 -0
  104. package/lib/module/ui/utils/html.js.map +1 -1
  105. package/lib/module/ui/utils/image-load-queue.js +109 -0
  106. package/lib/module/ui/utils/image-load-queue.js.map +1 -0
  107. package/lib/module/ui/utils/inline-attachments.js +85 -0
  108. package/lib/module/ui/utils/inline-attachments.js.map +1 -0
  109. package/lib/typescript/commonjs/api/client.d.ts +35 -1
  110. package/lib/typescript/commonjs/api/client.d.ts.map +1 -1
  111. package/lib/typescript/commonjs/attachments/file-size.d.ts +15 -0
  112. package/lib/typescript/commonjs/attachments/file-size.d.ts.map +1 -0
  113. package/lib/typescript/commonjs/attachments/local-previews.d.ts +14 -0
  114. package/lib/typescript/commonjs/attachments/local-previews.d.ts.map +1 -0
  115. package/lib/typescript/commonjs/attachments/pickers.d.ts +35 -10
  116. package/lib/typescript/commonjs/attachments/pickers.d.ts.map +1 -1
  117. package/lib/typescript/commonjs/i18n.d.ts +13 -3
  118. package/lib/typescript/commonjs/i18n.d.ts.map +1 -1
  119. package/lib/typescript/commonjs/locales.d.ts.map +1 -1
  120. package/lib/typescript/commonjs/state/widget-store.d.ts +67 -4
  121. package/lib/typescript/commonjs/state/widget-store.d.ts.map +1 -1
  122. package/lib/typescript/commonjs/storage.d.ts +2 -0
  123. package/lib/typescript/commonjs/storage.d.ts.map +1 -1
  124. package/lib/typescript/commonjs/types.d.ts +58 -0
  125. package/lib/typescript/commonjs/types.d.ts.map +1 -1
  126. package/lib/typescript/commonjs/ui/WidgetProvider.d.ts.map +1 -1
  127. package/lib/typescript/commonjs/ui/components/AttachmentChips.d.ts +13 -3
  128. package/lib/typescript/commonjs/ui/components/AttachmentChips.d.ts.map +1 -1
  129. package/lib/typescript/commonjs/ui/components/ConversationRow.d.ts +15 -1
  130. package/lib/typescript/commonjs/ui/components/ConversationRow.d.ts.map +1 -1
  131. package/lib/typescript/commonjs/ui/components/ImageViewer.d.ts +26 -6
  132. package/lib/typescript/commonjs/ui/components/ImageViewer.d.ts.map +1 -1
  133. package/lib/typescript/commonjs/ui/components/RatingCard.d.ts.map +1 -1
  134. package/lib/typescript/commonjs/ui/components/ZoomableImage.d.ts +10 -1
  135. package/lib/typescript/commonjs/ui/components/ZoomableImage.d.ts.map +1 -1
  136. package/lib/typescript/commonjs/ui/components/icons.d.ts +2 -1
  137. package/lib/typescript/commonjs/ui/components/icons.d.ts.map +1 -1
  138. package/lib/typescript/commonjs/ui/hooks/use-retained-state.d.ts +11 -0
  139. package/lib/typescript/commonjs/ui/hooks/use-retained-state.d.ts.map +1 -0
  140. package/lib/typescript/commonjs/ui/screens/chat/ChatView.d.ts +4 -4
  141. package/lib/typescript/commonjs/ui/screens/chat/ChatView.d.ts.map +1 -1
  142. package/lib/typescript/commonjs/ui/screens/chat/ChatView.styles.d.ts +154 -2
  143. package/lib/typescript/commonjs/ui/screens/chat/ChatView.styles.d.ts.map +1 -1
  144. package/lib/typescript/commonjs/ui/screens/chat/ChatView.types.d.ts +30 -1
  145. package/lib/typescript/commonjs/ui/screens/chat/ChatView.types.d.ts.map +1 -1
  146. package/lib/typescript/commonjs/ui/screens/conversations/ConversationsList.d.ts.map +1 -1
  147. package/lib/typescript/commonjs/ui/screens/conversations/ConversationsList.types.d.ts +2 -0
  148. package/lib/typescript/commonjs/ui/screens/conversations/ConversationsList.types.d.ts.map +1 -1
  149. package/lib/typescript/commonjs/ui/screens/home/HomeView.d.ts +7 -5
  150. package/lib/typescript/commonjs/ui/screens/home/HomeView.d.ts.map +1 -1
  151. package/lib/typescript/commonjs/ui/screens/home/HomeView.styles.d.ts +30 -35
  152. package/lib/typescript/commonjs/ui/screens/home/HomeView.styles.d.ts.map +1 -1
  153. package/lib/typescript/commonjs/ui/screens/home/HomeView.types.d.ts +5 -6
  154. package/lib/typescript/commonjs/ui/screens/home/HomeView.types.d.ts.map +1 -1
  155. package/lib/typescript/commonjs/ui/styles/theme.d.ts +2 -0
  156. package/lib/typescript/commonjs/ui/styles/theme.d.ts.map +1 -1
  157. package/lib/typescript/commonjs/ui/utils/conversations.d.ts +10 -2
  158. package/lib/typescript/commonjs/ui/utils/conversations.d.ts.map +1 -1
  159. package/lib/typescript/commonjs/ui/utils/html.d.ts +5 -0
  160. package/lib/typescript/commonjs/ui/utils/html.d.ts.map +1 -1
  161. package/lib/typescript/commonjs/ui/utils/image-load-queue.d.ts +44 -0
  162. package/lib/typescript/commonjs/ui/utils/image-load-queue.d.ts.map +1 -0
  163. package/lib/typescript/commonjs/ui/utils/inline-attachments.d.ts +29 -0
  164. package/lib/typescript/commonjs/ui/utils/inline-attachments.d.ts.map +1 -0
  165. package/lib/typescript/module/api/client.d.ts +35 -1
  166. package/lib/typescript/module/api/client.d.ts.map +1 -1
  167. package/lib/typescript/module/attachments/file-size.d.ts +15 -0
  168. package/lib/typescript/module/attachments/file-size.d.ts.map +1 -0
  169. package/lib/typescript/module/attachments/local-previews.d.ts +14 -0
  170. package/lib/typescript/module/attachments/local-previews.d.ts.map +1 -0
  171. package/lib/typescript/module/attachments/pickers.d.ts +35 -10
  172. package/lib/typescript/module/attachments/pickers.d.ts.map +1 -1
  173. package/lib/typescript/module/i18n.d.ts +13 -3
  174. package/lib/typescript/module/i18n.d.ts.map +1 -1
  175. package/lib/typescript/module/locales.d.ts.map +1 -1
  176. package/lib/typescript/module/state/widget-store.d.ts +67 -4
  177. package/lib/typescript/module/state/widget-store.d.ts.map +1 -1
  178. package/lib/typescript/module/storage.d.ts +2 -0
  179. package/lib/typescript/module/storage.d.ts.map +1 -1
  180. package/lib/typescript/module/types.d.ts +58 -0
  181. package/lib/typescript/module/types.d.ts.map +1 -1
  182. package/lib/typescript/module/ui/WidgetProvider.d.ts.map +1 -1
  183. package/lib/typescript/module/ui/components/AttachmentChips.d.ts +13 -3
  184. package/lib/typescript/module/ui/components/AttachmentChips.d.ts.map +1 -1
  185. package/lib/typescript/module/ui/components/ConversationRow.d.ts +15 -1
  186. package/lib/typescript/module/ui/components/ConversationRow.d.ts.map +1 -1
  187. package/lib/typescript/module/ui/components/ImageViewer.d.ts +26 -6
  188. package/lib/typescript/module/ui/components/ImageViewer.d.ts.map +1 -1
  189. package/lib/typescript/module/ui/components/RatingCard.d.ts.map +1 -1
  190. package/lib/typescript/module/ui/components/ZoomableImage.d.ts +10 -1
  191. package/lib/typescript/module/ui/components/ZoomableImage.d.ts.map +1 -1
  192. package/lib/typescript/module/ui/components/icons.d.ts +2 -1
  193. package/lib/typescript/module/ui/components/icons.d.ts.map +1 -1
  194. package/lib/typescript/module/ui/hooks/use-retained-state.d.ts +11 -0
  195. package/lib/typescript/module/ui/hooks/use-retained-state.d.ts.map +1 -0
  196. package/lib/typescript/module/ui/screens/chat/ChatView.d.ts +4 -4
  197. package/lib/typescript/module/ui/screens/chat/ChatView.d.ts.map +1 -1
  198. package/lib/typescript/module/ui/screens/chat/ChatView.styles.d.ts +154 -2
  199. package/lib/typescript/module/ui/screens/chat/ChatView.styles.d.ts.map +1 -1
  200. package/lib/typescript/module/ui/screens/chat/ChatView.types.d.ts +30 -1
  201. package/lib/typescript/module/ui/screens/chat/ChatView.types.d.ts.map +1 -1
  202. package/lib/typescript/module/ui/screens/conversations/ConversationsList.d.ts.map +1 -1
  203. package/lib/typescript/module/ui/screens/conversations/ConversationsList.types.d.ts +2 -0
  204. package/lib/typescript/module/ui/screens/conversations/ConversationsList.types.d.ts.map +1 -1
  205. package/lib/typescript/module/ui/screens/home/HomeView.d.ts +7 -5
  206. package/lib/typescript/module/ui/screens/home/HomeView.d.ts.map +1 -1
  207. package/lib/typescript/module/ui/screens/home/HomeView.styles.d.ts +30 -35
  208. package/lib/typescript/module/ui/screens/home/HomeView.styles.d.ts.map +1 -1
  209. package/lib/typescript/module/ui/screens/home/HomeView.types.d.ts +5 -6
  210. package/lib/typescript/module/ui/screens/home/HomeView.types.d.ts.map +1 -1
  211. package/lib/typescript/module/ui/styles/theme.d.ts +2 -0
  212. package/lib/typescript/module/ui/styles/theme.d.ts.map +1 -1
  213. package/lib/typescript/module/ui/utils/conversations.d.ts +10 -2
  214. package/lib/typescript/module/ui/utils/conversations.d.ts.map +1 -1
  215. package/lib/typescript/module/ui/utils/html.d.ts +5 -0
  216. package/lib/typescript/module/ui/utils/html.d.ts.map +1 -1
  217. package/lib/typescript/module/ui/utils/image-load-queue.d.ts +44 -0
  218. package/lib/typescript/module/ui/utils/image-load-queue.d.ts.map +1 -0
  219. package/lib/typescript/module/ui/utils/inline-attachments.d.ts +29 -0
  220. package/lib/typescript/module/ui/utils/inline-attachments.d.ts.map +1 -0
  221. package/package.json +1 -1
  222. package/src/api/client.ts +148 -2
  223. package/src/attachments/file-size.ts +42 -0
  224. package/src/attachments/local-previews.ts +32 -0
  225. package/src/attachments/pickers.ts +90 -30
  226. package/src/i18n.ts +24 -3
  227. package/src/locales.ts +13 -3
  228. package/src/state/widget-store.ts +519 -70
  229. package/src/storage.ts +2 -0
  230. package/src/types.ts +58 -0
  231. package/src/ui/WidgetProvider.tsx +50 -12
  232. package/src/ui/components/AttachmentChips.tsx +100 -29
  233. package/src/ui/components/ConversationRow.tsx +90 -9
  234. package/src/ui/components/ImageViewer.tsx +250 -28
  235. package/src/ui/components/RatingCard.tsx +5 -2
  236. package/src/ui/components/ZoomableImage.tsx +58 -5
  237. package/src/ui/components/icons.tsx +10 -12
  238. package/src/ui/hooks/use-retained-state.ts +35 -0
  239. package/src/ui/screens/chat/ChatView.styles.ts +151 -3
  240. package/src/ui/screens/chat/ChatView.tsx +760 -213
  241. package/src/ui/screens/chat/ChatView.types.ts +30 -1
  242. package/src/ui/screens/conversations/ConversationsList.tsx +1 -0
  243. package/src/ui/screens/conversations/ConversationsList.types.ts +2 -0
  244. package/src/ui/screens/home/HomeView.styles.ts +36 -45
  245. package/src/ui/screens/home/HomeView.tsx +54 -70
  246. package/src/ui/screens/home/HomeView.types.ts +5 -6
  247. package/src/ui/styles/theme.ts +6 -0
  248. package/src/ui/utils/conversations.ts +16 -2
  249. package/src/ui/utils/html.ts +26 -0
  250. package/src/ui/utils/image-load-queue.ts +108 -0
  251. package/src/ui/utils/inline-attachments.ts +71 -0
@@ -1,6 +1,10 @@
1
1
  /**
2
- * Visitor attachments: picking photos/files and turning them into the base64
3
- * data URLs `POST /api/widget/messages` accepts.
2
+ * Visitor attachments: picking photos/files for `POST /api/widget/messages`.
3
+ *
4
+ * A server that takes uploads names its cap (`maxAttachmentBytes`): a picked
5
+ * file then stays a local URI, which the composer uploads right away. An
6
+ * older server only takes files inline, as base64 data URLs within the
7
+ * 1.5 MB / 6 MB caps below.
4
8
  *
5
9
  * The SDK ships no native code, so every picker is borrowed from the host —
6
10
  * the same approach as `storage.ts` and `native-gradient.ts`:
@@ -8,7 +12,7 @@
8
12
  * - `expo-document-picker` → any file
9
13
  * - `expo-file-system` → reads a picked file as base64 (optional; without it
10
14
  * the file is read through `fetch` + `FileReader`)
11
- * - `expo-image-manipulator` → shrinks photos to fit the 1.5 MB cap (optional;
15
+ * - `expo-image-manipulator` → shrinks photos, to fit the 1.5 MB inline cap (optional;
12
16
  * without it a full-resolution camera photo is usually refused as too large)
13
17
  *
14
18
  * A host without those (or with `react-native-image-picker` etc.) plugs its own
@@ -20,13 +24,14 @@
20
24
  */
21
25
 
22
26
  import type { AttachmentSource, RepzoPickedFile, RepzoWidgetAttachmentOptions } from '../types';
27
+ import { measureFileSize, validSize } from './file-size';
23
28
 
24
29
  /** The server keeps the first five and silently drops the rest. */
25
30
  export const MAX_ATTACHMENTS = 5;
26
- /** Per file, decoded. The server silently drops anything larger. */
31
+ /** Per inline file, decoded. The server silently drops anything larger. */
27
32
  export const MAX_ATTACHMENT_BYTES = 1_500_000;
28
33
  /**
29
- * All files in one message, decoded. Base64 inside JSON inflates bytes by
34
+ * All inline files in one message, decoded. Base64 inside JSON inflates bytes by
30
35
  * ~1.37x and the server caps a request body at 10 MB, so five files at the
31
36
  * per-file maximum would come back as a 413.
32
37
  */
@@ -35,17 +40,28 @@ export const MAX_TOTAL_ATTACHMENT_BYTES = 6_000_000;
35
40
  /** Photos are re-encoded at this JPEG quality — small enough to fit the cap more often. */
36
41
  const IMAGE_QUALITY = 0.7;
37
42
 
38
- /** A picked file, encoded and ready to send; what the composer shows as a chip. */
43
+ /** A picked file, ready to send; what the composer shows as a chip. */
39
44
  export interface DraftAttachment {
40
45
  id: string;
41
46
  name: string;
42
47
  contentType: string;
43
48
  /** Decoded bytes — what the server measures against its cap. */
44
49
  size: number;
45
- /** `data:<mime>;base64,<…>` — the wire format. */
50
+ /** `data:<mime>;base64,<…>` for an inline file; empty when `fileUri` is uploaded instead. */
46
51
  dataUrl: string;
52
+ /** The local file uploaded ahead of the message. */
53
+ fileUri?: string;
47
54
  /** What `<Image>` renders locally: the picked file's URI, else the data URL. */
48
55
  previewUri: string;
56
+ /**
57
+ * Where `fileUri`'s upload stands. The composer starts it as soon as the
58
+ * file is picked and holds Send until every file is up. Absent for inline files.
59
+ */
60
+ uploadStatus?: 'uploading' | 'uploaded' | 'failed';
61
+ /** What the message names the uploaded file by, once `uploadStatus` is `uploaded`. */
62
+ uploadToken?: string;
63
+ /** When `uploadToken` stops working, on this phone's clock (ms). */
64
+ uploadExpiresAt?: number;
49
65
  }
50
66
 
51
67
  /** Why some (or all) of a pick didn't make it into the composer. */
@@ -113,6 +129,11 @@ export function setAttachmentOptions(options: RepzoWidgetAttachmentOptions | nul
113
129
  hostOptions = options;
114
130
  }
115
131
 
132
+ /** Whether a picked file starts uploading before Send (`attachments.uploadOnPick`). */
133
+ export function uploadsOnPick(): boolean {
134
+ return hostOptions?.uploadOnPick !== false;
135
+ }
136
+
116
137
  const ALL_SOURCES: AttachmentSource[] = ['photo', 'camera', 'file'];
117
138
 
118
139
  /**
@@ -139,11 +160,13 @@ const CAMERA_DENIED = 'cameraDenied' as const;
139
160
 
140
161
  /**
141
162
  * Opens the picker for `source` and returns what fits alongside `current`.
163
+ * `uploadLimit` is the server's upload cap; without it files go inline.
142
164
  * Never throws: a cancelled pick is `{ added: [], problem: null }`.
143
165
  */
144
166
  export async function pickAttachments(
145
167
  source: AttachmentSource,
146
- current: DraftAttachment[]
168
+ current: DraftAttachment[],
169
+ uploadLimit: number | null = null
147
170
  ): Promise<PickResult> {
148
171
  const slots = MAX_ATTACHMENTS - current.length;
149
172
  if (slots <= 0) return { added: [], problem: 'tooMany' };
@@ -152,7 +175,7 @@ export async function pickAttachments(
152
175
  try {
153
176
  picked = hostOptions?.pick
154
177
  ? await hostOptions.pick(source)
155
- : await launchBuiltInPicker(source, slots);
178
+ : await launchBuiltInPicker(source, slots, !!uploadLimit);
156
179
  } catch (error) {
157
180
  console.warn('[repzo-widget] attachment picker failed', error);
158
181
  return { added: [], problem: 'failed' };
@@ -170,8 +193,9 @@ export async function pickAttachments(
170
193
  }
171
194
  // A size the picker already knows rules a file out before it's read, so
172
195
  // a 40 MB video never lands in memory as a base64 string.
173
- const declared = file.base64 ? null : validSize(file.size);
174
- const early = declared === null ? null : checkAttachmentFits(accepted, declared);
196
+ const uploads = !!uploadLimit && !!file.uri;
197
+ const declared = file.base64 && !uploads ? null : validSize(file.size);
198
+ const early = declared === null ? null : checkAttachmentFits(accepted, declared, uploads ? uploadLimit : null);
175
199
  if (early) {
176
200
  problem = problem ?? early;
177
201
  continue;
@@ -179,7 +203,7 @@ export async function pickAttachments(
179
203
 
180
204
  let draft: DraftAttachment;
181
205
  try {
182
- draft = await toDraft(file);
206
+ draft = uploads ? await toUploadDraft(file, uploadLimit!) : await toDraft(file);
183
207
  } catch (error) {
184
208
  if (error === TOO_LARGE) {
185
209
  problem = problem ?? 'tooLarge';
@@ -189,7 +213,8 @@ export async function pickAttachments(
189
213
  problem = problem ?? 'failed';
190
214
  continue;
191
215
  }
192
- const fits = draft.size > 0 ? checkAttachmentFits(accepted, draft.size) : 'failed';
216
+ const fits =
217
+ draft.size > 0 ? checkAttachmentFits(accepted, draft.size, uploads ? uploadLimit : null) : 'failed';
193
218
  if (fits) {
194
219
  problem = problem ?? fits;
195
220
  continue;
@@ -201,7 +226,8 @@ export async function pickAttachments(
201
226
 
202
227
  async function launchBuiltInPicker(
203
228
  source: AttachmentSource,
204
- slots: number
229
+ slots: number,
230
+ uploads: boolean
205
231
  ): Promise<RepzoPickedFile[] | null | typeof CAMERA_DENIED> {
206
232
  if (source === 'file') {
207
233
  if (!documentPicker) return null;
@@ -224,9 +250,10 @@ async function launchBuiltInPicker(
224
250
  if (!imagePicker) return null;
225
251
  const options = {
226
252
  quality: IMAGE_QUALITY,
227
- // With the manipulator the photo is re-encoded below, so the picker's own
228
- // (full-resolution) base64 would only waste memory.
229
- base64: !imageManipulator,
253
+ // With the manipulator the photo is re-encoded below, and an uploaded
254
+ // photo is read from its file, so the picker's own (full-resolution)
255
+ // base64 would only waste memory.
256
+ base64: !uploads && !imageManipulator,
230
257
  exif: false,
231
258
  };
232
259
 
@@ -246,7 +273,7 @@ async function launchBuiltInPicker(
246
273
  const assets: any[] = Array.isArray(result?.assets) ? result.assets : [];
247
274
  const files: RepzoPickedFile[] = [];
248
275
  for (const [index, asset] of assets.slice(0, slots).entries()) {
249
- const shrunk = await shrinkPhoto(asset);
276
+ const shrunk = await shrinkPhoto(asset, !uploads);
250
277
  const type = shrunk
251
278
  ? 'image/jpeg'
252
279
  : asset.mimeType || guessContentType(asset.fileName || asset.uri) || 'image/jpeg';
@@ -254,7 +281,7 @@ async function launchBuiltInPicker(
254
281
  files.push({
255
282
  name: `${baseName}.${extensionFor(type)}`,
256
283
  type,
257
- // No `size`: `fileSize` is the original's, not the re-encoded base64's.
284
+ // No `size`: `fileSize` is the original's, not the re-encoded file's.
258
285
  uri: shrunk?.uri ?? asset.uri,
259
286
  base64: shrunk?.base64 ?? asset.base64 ?? undefined,
260
287
  });
@@ -282,7 +309,10 @@ async function launchImagePicker(launch: (options: object) => Promise<any>, opti
282
309
  * MAX_IMAGE_EDGE. Null when the manipulator isn't installed or fails — the
283
310
  * caller then falls back to the picker's own output (and the size check).
284
311
  */
285
- async function shrinkPhoto(asset: any): Promise<{ uri: string; base64: string } | null> {
312
+ async function shrinkPhoto(
313
+ asset: any,
314
+ withBase64: boolean
315
+ ): Promise<{ uri: string; base64?: string } | null> {
286
316
  if (!imageManipulator || !asset?.uri) return null;
287
317
  const width = Number(asset.width) || 0;
288
318
  const height = Number(asset.height) || 0;
@@ -296,7 +326,7 @@ async function shrinkPhoto(asset: any): Promise<{ uri: string; base64: string }
296
326
  const saveOptions = {
297
327
  compress: IMAGE_QUALITY,
298
328
  format: imageManipulator.SaveFormat?.JPEG ?? 'jpeg',
299
- base64: true,
329
+ base64: withBase64,
300
330
  };
301
331
  try {
302
332
  let output: any;
@@ -308,6 +338,7 @@ async function shrinkPhoto(asset: any): Promise<{ uri: string; base64: string }
308
338
  const image = await context.renderAsync();
309
339
  output = await image.saveAsync(saveOptions);
310
340
  }
341
+ if (!withBase64) return typeof output?.uri === 'string' && output.uri ? { uri: output.uri } : null;
311
342
  return typeof output?.base64 === 'string' && output.base64
312
343
  ? { uri: output.uri ?? asset.uri, base64: output.base64 }
313
344
  : null;
@@ -340,6 +371,32 @@ async function toDraft(file: RepzoPickedFile): Promise<DraftAttachment> {
340
371
  };
341
372
  }
342
373
 
374
+ /** A file that's uploaded on its own, ahead of the message: only its size is read here. */
375
+ async function toUploadDraft(file: RepzoPickedFile, uploadLimit: number): Promise<DraftAttachment> {
376
+ const uri = file.uri!;
377
+ const size = validSize(file.size) ?? (await readFileSize(uri));
378
+ if (size > uploadLimit) throw TOO_LARGE;
379
+ const name = (file.name || fileNameFromUri(uri) || 'file').slice(0, 255);
380
+ return {
381
+ id: `draft-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
382
+ name,
383
+ contentType: normalizeContentType(file.type) ?? guessContentType(name) ?? 'application/octet-stream',
384
+ size,
385
+ dataUrl: '',
386
+ fileUri: uri,
387
+ previewUri: uri,
388
+ };
389
+ }
390
+
391
+ /** A local file's size in bytes, or 0 when nothing can tell. */
392
+ async function readFileSize(uri: string): Promise<number> {
393
+ const size = await measureFileSize(uri);
394
+ if (size !== null) return size;
395
+ // Last resort: the native blob reads the whole file (on Android, into memory).
396
+ const blob = await (await fetch(uri)).blob();
397
+ return validSize(blob.size) ?? 0;
398
+ }
399
+
343
400
  /** Thrown (by identity) when a file is over the per-file cap before it's read. */
344
401
  const TOO_LARGE = new Error('File exceeds the attachment cap');
345
402
 
@@ -407,14 +464,20 @@ async function readBase64WithFetch(uri: string): Promise<string> {
407
464
 
408
465
  // ─── Pure helpers ──────────────────────────────────────────────────────────
409
466
 
410
- /** Null when a file of `size` bytes still fits beside `current`; otherwise the limit it breaks. */
467
+ /**
468
+ * Null when a file of `size` bytes still fits beside `current`; otherwise the
469
+ * limit it breaks. With `uploadLimit` the file is uploaded on its own, so
470
+ * only its own size counts; inline files also share one request body.
471
+ */
411
472
  export function checkAttachmentFits(
412
- current: ReadonlyArray<{ size: number }>,
413
- size: number
473
+ current: ReadonlyArray<{ size: number; fileUri?: string }>,
474
+ size: number,
475
+ uploadLimit: number | null = null
414
476
  ): AttachmentProblem | null {
415
477
  if (current.length >= MAX_ATTACHMENTS) return 'tooMany';
478
+ if (uploadLimit) return size > uploadLimit ? 'tooLarge' : null;
416
479
  if (size > MAX_ATTACHMENT_BYTES) return 'tooLarge';
417
- const total = current.reduce((sum, item) => sum + item.size, 0);
480
+ const total = current.filter((item) => !item.fileUri).reduce((sum, item) => sum + item.size, 0);
418
481
  if (total + size > MAX_TOTAL_ATTACHMENT_BYTES) return 'tooLargeTogether';
419
482
  return null;
420
483
  }
@@ -462,10 +525,6 @@ export function isImageAttachment(contentType: string | null | undefined): boole
462
525
  return !!contentType && contentType.toLowerCase().startsWith('image/');
463
526
  }
464
527
 
465
- function validSize(size: unknown): number | null {
466
- return typeof size === 'number' && Number.isFinite(size) && size > 0 ? size : null;
467
- }
468
-
469
528
  function fileNameFromUri(uri: string | undefined): string | null {
470
529
  if (!uri) return null;
471
530
  const last = uri.split(/[?#]/)[0]?.split('/').pop();
@@ -501,7 +560,8 @@ const CONTENT_TYPES: Record<string, string> = {
501
560
  mov: 'video/quicktime',
502
561
  };
503
562
 
504
- function guessContentType(nameOrUri: string | null | undefined): string | null {
563
+ /** The type a file name's extension implies, or null for one this table doesn't know. */
564
+ export function guessContentType(nameOrUri: string | null | undefined): string | null {
505
565
  const extension = /\.([a-z0-9]+)$/i.exec(nameOrUri?.split(/[?#]/)[0] ?? '')?.[1];
506
566
  return extension ? (CONTENT_TYPES[extension.toLowerCase()] ?? null) : null;
507
567
  }
package/src/i18n.ts CHANGED
@@ -59,6 +59,14 @@ export const DEFAULT_LABELS = {
59
59
  loadEarlier: 'Load earlier messages',
60
60
  emptyConversation: 'Send a message to start the conversation',
61
61
  messagePlaceholder: 'Type a message...',
62
+ // Screen-reader label of the send button beside the message field.
63
+ sendMessage: 'Send message',
64
+ // Screen-reader labels of the button that returns to the newest message
65
+ // after scrolling up; the second when teammates replied in the meantime.
66
+ jumpToLatest: 'Jump to latest messages',
67
+ jumpToLatestNew: 'New messages: {count}. Jump to latest',
68
+ // Screen readers, on the last tile of a photo album showing "+{count}".
69
+ morePhotos: '{count} more photos',
62
70
  attachFile: 'Attach file',
63
71
  // Attach menu, composer chips and the notices under them.
64
72
  attachPhotoLibrary: 'Photo library',
@@ -70,6 +78,11 @@ export const DEFAULT_LABELS = {
70
78
  attachmentsTooLargeTogether:
71
79
  'Attachments are too large to send together — send some in another message',
72
80
  attachmentFailed: "Couldn't attach that file",
81
+ // A picked file uploads before Send is enabled. Screen-reader labels for its
82
+ // chip while it uploads and after it failed, then the notice for a failure.
83
+ attachmentUploading: 'Uploading {name}',
84
+ attachmentUploadFailed: "Couldn't upload {name}. Tap to try again",
85
+ attachmentsUploadFailed: "Couldn't upload a file. Tap it to try again",
73
86
  // A file in a message; tapping opens it. `{name}` is the file name.
74
87
  openAttachment: 'Open {name}',
75
88
  // Voice notes played in the chat (with expo-audio).
@@ -79,9 +92,13 @@ export const DEFAULT_LABELS = {
79
92
  // Full-screen image viewer.
80
93
  imageLoadFailed: "Couldn't load this image",
81
94
  openImageExternally: 'Open image',
95
+ // Screen readers: which of a message's photos is open.
96
+ imagePosition: '{current} of {total}',
82
97
  cameraPermissionDenied: 'Allow camera access in Settings to take a photo',
83
98
  cancel: 'Cancel',
84
99
  failedToSend: 'Failed to send',
100
+ // After "Failed to send" under a message the visitor can send again.
101
+ tapToRetry: 'Tap to retry',
85
102
  thinking: 'Typing',
86
103
  messages: 'Messages',
87
104
  noMessages: 'No messages yet',
@@ -91,13 +108,17 @@ export const DEFAULT_LABELS = {
91
108
  preChatSubtitle: 'Let us know a bit about you so we can help you better.',
92
109
  preChatSkip: 'Skip',
93
110
  preChatSubmit: 'Start chat',
94
- viewPreviousConversations: 'View previous conversations',
95
- homeRecentTitle: 'Recent',
96
- homeViewAllCount: 'View all ({count})',
111
+ // Heading of Home's card that shows the newest conversation.
112
+ homeRecentMessageTitle: 'Recent message',
97
113
  homeNoConversations: 'Your conversations will show up here',
98
114
  homeStatusOnline: "We're online",
99
115
  homeStatusAway: "We're away",
100
116
  unreadBadge: '{count} unread',
117
+ // A conversation row whose newest message was only photos, or other files.
118
+ lastMessagePhoto: 'Photo',
119
+ lastMessageFile: 'File',
120
+ // Before the visitor's own newest message in a conversation row.
121
+ lastMessageYou: 'You: {message}',
101
122
  team: 'Team',
102
123
  home: 'Home',
103
124
  close: 'Close',
package/src/locales.ts CHANGED
@@ -20,6 +20,10 @@ const ar: LabelMap = {
20
20
  loadEarlier: 'تحميل الرسائل السابقة',
21
21
  emptyConversation: 'أرسل رسالة لبدء المحادثة',
22
22
  messagePlaceholder: 'اكتب رسالة...',
23
+ sendMessage: 'إرسال الرسالة',
24
+ jumpToLatest: 'الانتقال إلى أحدث الرسائل',
25
+ jumpToLatestNew: 'رسائل جديدة: {count}. الانتقال إلى الأحدث',
26
+ morePhotos: 'صور أخرى: {count}',
23
27
  attachFile: 'إرفاق ملف',
24
28
  attachPhotoLibrary: 'مكتبة الصور',
25
29
  attachTakePhoto: 'التقاط صورة',
@@ -29,15 +33,20 @@ const ar: LabelMap = {
29
33
  attachmentTooMany: 'يمكنك إرفاق {count} ملفات كحد أقصى',
30
34
  attachmentsTooLargeTogether: 'المرفقات كبيرة جدًا لإرسالها معًا — أرسل بعضها في رسالة أخرى',
31
35
  attachmentFailed: 'تعذر إرفاق هذا الملف',
36
+ attachmentUploading: 'جارٍ رفع {name}',
37
+ attachmentUploadFailed: 'تعذر رفع {name}. اضغط لإعادة المحاولة',
38
+ attachmentsUploadFailed: 'تعذر رفع ملف. اضغط عليه لإعادة المحاولة',
32
39
  openAttachment: 'فتح {name}',
33
40
  voicePlay: 'تشغيل الرسالة الصوتية',
34
41
  voicePause: 'إيقاف الرسالة الصوتية مؤقتًا',
35
42
  voiceSpeed: 'سرعة التشغيل',
36
43
  imageLoadFailed: 'تعذر تحميل هذه الصورة',
37
44
  openImageExternally: 'فتح الصورة',
45
+ imagePosition: '{current} من {total}',
38
46
  cameraPermissionDenied: 'اسمح بالوصول إلى الكاميرا من الإعدادات لالتقاط صورة',
39
47
  cancel: 'إلغاء',
40
48
  failedToSend: 'تعذر الإرسال',
49
+ tapToRetry: 'اضغط لإعادة المحاولة',
41
50
  thinking: 'يكتب',
42
51
  messages: 'الرسائل',
43
52
  noMessages: 'لا توجد رسائل بعد',
@@ -47,13 +56,14 @@ const ar: LabelMap = {
47
56
  preChatSubtitle: 'أخبرنا قليلاً عنك حتى نتمكن من مساعدتك بشكل أفضل.',
48
57
  preChatSkip: 'تخطٍ',
49
58
  preChatSubmit: 'ابدأ المحادثة',
50
- viewPreviousConversations: 'عرض المحادثات السابقة',
51
- homeRecentTitle: 'الأحدث',
52
- homeViewAllCount: 'عرض الكل ({count})',
59
+ homeRecentMessageTitle: 'أحدث رسالة',
53
60
  homeNoConversations: 'ستظهر محادثاتك هنا',
54
61
  homeStatusOnline: 'نحن متصلون',
55
62
  homeStatusAway: 'نحن غير متصلين',
56
63
  unreadBadge: '{count} غير مقروءة',
64
+ lastMessagePhoto: 'صورة',
65
+ lastMessageFile: 'ملف',
66
+ lastMessageYou: 'أنت: {message}',
57
67
  team: 'الفريق',
58
68
  home: 'الرئيسية',
59
69
  close: 'إغلاق',