@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
@@ -1,11 +1,10 @@
1
1
  /** Props and label bundle for the Home space. */
2
2
  import type { Conversation, WidgetConfig } from '../../../types';
3
+ import type { LastMessageLabels } from '../../components/ConversationRow';
3
4
  import type { RelativeTimeLabels, TeamAgent } from '../../utils/conversations';
4
5
  export interface HomeViewLabels {
5
6
  close: string;
6
- /** Accessibility label for the "view all" control. */
7
- viewPrevious: string;
8
- viewAll: string;
7
+ /** Heading inside the "Recent message" card. */
9
8
  recentTitle: string;
10
9
  noConversations: string;
11
10
  statusOnline: string;
@@ -15,14 +14,15 @@ export interface HomeViewLabels {
15
14
  time: RelativeTimeLabels;
16
15
  /** Template with a `{count}` token. */
17
16
  unread: string;
17
+ lastMessage: LastMessageLabels;
18
18
  }
19
19
  export interface HomeViewProps {
20
20
  config: WidgetConfig | null;
21
21
  color: string;
22
22
  visitorName?: string;
23
23
  conversationCount: number;
24
- /** Already sorted newest-first and capped by the provider. */
25
- recentConversations: Conversation[];
24
+ /** The newest conversation, picked by the provider; none before the first one. */
25
+ recentConversation?: Conversation;
26
26
  unreadByConversation: Record<string, number>;
27
27
  teamAgents: TeamAgent[];
28
28
  isOnline: boolean;
@@ -34,7 +34,6 @@ export interface HomeViewProps {
34
34
  labels: HomeViewLabels;
35
35
  onClose: () => void;
36
36
  onStartConversation: () => void;
37
- onViewConversations: () => void;
38
37
  onOpenConversation: (conversationId: string) => void;
39
38
  }
40
39
  //# sourceMappingURL=HomeView.types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"HomeView.types.d.ts","sourceRoot":"","sources":["../../../../../../src/ui/screens/home/HomeView.types.ts"],"names":[],"mappings":"AAAA,iDAAiD;AAEjD,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACjE,OAAO,KAAK,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAE/E,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,sDAAsD;IACtD,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,gEAAgE;IAChE,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,kBAAkB,CAAC;IACzB,uCAAuC;IACvC,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,YAAY,GAAG,IAAI,CAAC;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,8DAA8D;IAC9D,mBAAmB,EAAE,YAAY,EAAE,CAAC;IACpC,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7C,UAAU,EAAE,SAAS,EAAE,CAAC;IACxB,QAAQ,EAAE,OAAO,CAAC;IAClB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,sBAAsB,EAAE,MAAM,CAAC;IAC/B,cAAc,EAAE,MAAM,CAAC;IACvB,qBAAqB,EAAE,OAAO,CAAC;IAC/B,yBAAyB,EAAE,OAAO,CAAC;IACnC,MAAM,EAAE,cAAc,CAAC;IACvB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,mBAAmB,EAAE,MAAM,IAAI,CAAC;IAChC,mBAAmB,EAAE,MAAM,IAAI,CAAC;IAChC,kBAAkB,EAAE,CAAC,cAAc,EAAE,MAAM,KAAK,IAAI,CAAC;CACtD"}
1
+ {"version":3,"file":"HomeView.types.d.ts","sourceRoot":"","sources":["../../../../../../src/ui/screens/home/HomeView.types.ts"],"names":[],"mappings":"AAAA,iDAAiD;AAEjD,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACjE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kCAAkC,CAAC;AAC1E,OAAO,KAAK,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAE/E,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,gDAAgD;IAChD,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,gEAAgE;IAChE,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,kBAAkB,CAAC;IACzB,uCAAuC;IACvC,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,iBAAiB,CAAC;CAChC;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,YAAY,GAAG,IAAI,CAAC;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,kFAAkF;IAClF,kBAAkB,CAAC,EAAE,YAAY,CAAC;IAClC,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7C,UAAU,EAAE,SAAS,EAAE,CAAC;IACxB,QAAQ,EAAE,OAAO,CAAC;IAClB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,sBAAsB,EAAE,MAAM,CAAC;IAC/B,cAAc,EAAE,MAAM,CAAC;IACvB,qBAAqB,EAAE,OAAO,CAAC;IAC/B,yBAAyB,EAAE,OAAO,CAAC;IACnC,MAAM,EAAE,cAAc,CAAC;IACvB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,mBAAmB,EAAE,MAAM,IAAI,CAAC;IAChC,kBAAkB,EAAE,CAAC,cAAc,EAAE,MAAM,KAAK,IAAI,CAAC;CACtD"}
@@ -6,6 +6,11 @@
6
6
  export declare function decodeEntities(text: string): string;
7
7
  /** Block-level tags become newlines so paragraphs survive the strip. */
8
8
  export declare function stripHtml(html: string): string;
9
+ /**
10
+ * Agent reply HTML → the Markdown subset `Markdown` draws: paragraphs, line
11
+ * breaks, lists, bold, italic, inline code and links. Other tags keep their text.
12
+ */
13
+ export declare function htmlToMarkdown(html: string): string;
9
14
  /** Single-line flattening used for campaign cards and list snippets. */
10
15
  export declare function htmlToText(html: string): string;
11
16
  //# sourceMappingURL=html.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"html.d.ts","sourceRoot":"","sources":["../../../../../src/ui/utils/html.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAoBH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAInD;AAED,wEAAwE;AACxE,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAW9C;AAED,wEAAwE;AACxE,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAU/C"}
1
+ {"version":3,"file":"html.d.ts","sourceRoot":"","sources":["../../../../../src/ui/utils/html.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAoBH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAInD;AAED,wEAAwE;AACxE,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAW9C;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAoBnD;AAED,wEAAwE;AACxE,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAU/C"}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Lets images load a few at a time, highest priority first. The chat runs its
3
+ * large images through it: Android holds a whole download in a memory pool
4
+ * capped at three quarters of the app's heap (192 MB on a 256 MB phone), so
5
+ * several 30–50 MB photos at once overflow it and every one of them fails.
6
+ *
7
+ * No `react-native` import on purpose: it stays testable in plain Node.
8
+ */
9
+ export declare class ImageLoadQueue {
10
+ private readonly concurrency;
11
+ private readonly waiting;
12
+ private readonly loading;
13
+ private readonly done;
14
+ /** The images on screen; null until the list says, so all of them count. */
15
+ private onScreen;
16
+ private readonly listeners;
17
+ private scheduled;
18
+ constructor(concurrency: number);
19
+ /** Queues a mounted image; the returned cleanup gives up its place. */
20
+ add(key: string, priority: number): () => void;
21
+ /** Loaded or failed: it keeps showing, and the next image may start. */
22
+ finish(key: string): void;
23
+ canLoad: (key: string) => boolean;
24
+ subscribe: (listener: () => void) => (() => void);
25
+ /**
26
+ * The images now on screen. One that scrolls off before it has loaded
27
+ * gives up its place and waits, so it can't hold up the ones in view:
28
+ * Android detaches a row scrolled out of view, and a detached image never
29
+ * finishes loading. Only images on screen are started.
30
+ */
31
+ setOnScreen(keys: Iterable<string>): void;
32
+ /** Scrolled out of the list before it loaded: it queues again if it comes back. */
33
+ private remove;
34
+ /**
35
+ * Hands out places once the render that mounted an image is done, so all
36
+ * the images mounted together are queued first, whatever order they
37
+ * mounted in.
38
+ */
39
+ private schedule;
40
+ /** Starts what fits; true when anything started (listeners were told). */
41
+ private advance;
42
+ private notify;
43
+ }
44
+ //# sourceMappingURL=image-load-queue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image-load-queue.d.ts","sourceRoot":"","sources":["../../../../../src/ui/utils/image-load-queue.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,qBAAa,cAAc;IAUb,OAAO,CAAC,QAAQ,CAAC,WAAW;IARxC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA6B;IACrD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA6B;IACrD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAqB;IAC1C,4EAA4E;IAC5E,OAAO,CAAC,QAAQ,CAAoC;IACpD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAyB;IACnD,OAAO,CAAC,SAAS,CAAS;gBAEG,WAAW,EAAE,MAAM;IAEhD,uEAAuE;IACvE,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,IAAI;IAQ9C,wEAAwE;IACxE,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAMzB,OAAO,GAAI,KAAK,MAAM,KAAG,OAAO,CAAgD;IAEhF,SAAS,GAAI,UAAU,MAAM,IAAI,KAAG,CAAC,MAAM,IAAI,CAAC,CAK9C;IAEF;;;;;OAKG;IACH,WAAW,CAAC,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,IAAI;IAYzC,mFAAmF;IACnF,OAAO,CAAC,MAAM;IAKd;;;;OAIG;IACH,OAAO,CAAC,QAAQ;IAShB,0EAA0E;IAC1E,OAAO,CAAC,OAAO;IAoBf,OAAO,CAAC,MAAM;CAGf"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Agent replies with images between their paragraphs. Workstation marks such
3
+ * images `inline` among the message's attachments and keeps an `<img>` for
4
+ * each in the HTML body, where the teammate put it. Same split as the web
5
+ * Messenger, so both show the text and images in the order they were written.
6
+ */
7
+ import type { WidgetAttachment } from '../../types';
8
+ /** Markdown for the bubble to draw, or one attachment. */
9
+ export type MessagePart = {
10
+ text: string;
11
+ } | {
12
+ attachment: WidgetAttachment;
13
+ };
14
+ /**
15
+ * The body and its inline images in order, plus the other attachments
16
+ * (`files`) that show after them. Null when nothing is inline: the bubble then
17
+ * shows the body, followed by every attachment.
18
+ *
19
+ * An `<img>` finds its attachment by the id in its URL. In a body whose
20
+ * images carry no ids, they take the attachments in order instead; in one
21
+ * that does, an image without an id (a logo, an emoji) isn't one of the files
22
+ * and takes nothing. One whose id names no remaining attachment (the same file
23
+ * twice) takes nothing either. Inline attachments with no `<img>` go at the end.
24
+ */
25
+ export declare function splitInlineAttachments(body: string | null | undefined, attachments: WidgetAttachment[] | null | undefined): {
26
+ parts: MessagePart[];
27
+ files: WidgetAttachment[];
28
+ } | null;
29
+ //# sourceMappingURL=inline-attachments.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inline-attachments.d.ts","sourceRoot":"","sources":["../../../../../src/ui/utils/inline-attachments.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAGpD,0DAA0D;AAC1D,MAAM,MAAM,WAAW,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,UAAU,EAAE,gBAAgB,CAAA;CAAE,CAAC;AAE9E;;;;;;;;;;GAUG;AACH,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAC/B,WAAW,EAAE,gBAAgB,EAAE,GAAG,IAAI,GAAG,SAAS,GACjD;IAAE,KAAK,EAAE,WAAW,EAAE,CAAC;IAAC,KAAK,EAAE,gBAAgB,EAAE,CAAA;CAAE,GAAG,IAAI,CA8B5D"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@repzo/widget-react-native",
3
- "version": "1.3.1",
3
+ "version": "1.4.0",
4
4
  "description": "Repzo chat widget for React Native",
5
5
  "source": "./src/index.tsx",
6
6
  "main": "./lib/commonjs/index.js",
package/src/api/client.ts CHANGED
@@ -8,6 +8,7 @@
8
8
  * The token itself lives in `SecureStorage`.
9
9
  */
10
10
 
11
+ import { measureFileSize } from '../attachments/file-size';
11
12
  import { SecureStorage, TOKEN_KEY } from '../storage';
12
13
  import type {
13
14
  AgentUiAction,
@@ -84,6 +85,60 @@ interface RequestOptions {
84
85
  authToken?: string | null;
85
86
  }
86
87
 
88
+ /**
89
+ * Sends a local file as the request body straight from disk and resolves the
90
+ * HTTP status. React Native streams a `{ uri }` body natively; `fetch` would
91
+ * stringify it, so this goes through XMLHttpRequest.
92
+ */
93
+ function sendFileFromDisk(
94
+ url: string,
95
+ method: string,
96
+ headers: Record<string, string>,
97
+ file: { uri: string; contentType: string },
98
+ signal?: AbortSignal
99
+ ): Promise<number> {
100
+ return new Promise((resolve, reject) => {
101
+ const xhr = new XMLHttpRequest();
102
+ const cancel = () => xhr.abort();
103
+ const settle = (finish: () => void) => {
104
+ signal?.removeEventListener('abort', cancel);
105
+ finish();
106
+ };
107
+ xhr.onload = () => settle(() => resolve(xhr.status));
108
+ xhr.onerror = () => settle(() => reject(new RepzoWidgetError('Network request failed', 0, 'network')));
109
+ xhr.ontimeout = xhr.onerror;
110
+ xhr.onabort = () => settle(() => reject(new RepzoWidgetError('Upload cancelled', 0, 'network')));
111
+ xhr.open(method, url);
112
+ Object.entries(headers).forEach(([name, value]) => xhr.setRequestHeader(name, value));
113
+ // Android refuses a `{ uri }` body without one.
114
+ if (!Object.keys(headers).some((name) => name.toLowerCase() === 'content-type')) {
115
+ xhr.setRequestHeader('Content-Type', file.contentType);
116
+ }
117
+ signal?.addEventListener('abort', cancel);
118
+ xhr.send({ uri: file.uri } as unknown as XMLHttpRequestBodyInit);
119
+ });
120
+ }
121
+
122
+ /**
123
+ * How long Workstation keeps an upload for a message to name, when its answer
124
+ * has no usable `expiresAt` (deployments without it, or a phone clock that
125
+ * reads it as already past).
126
+ */
127
+ const UPLOAD_LIFETIME_MS = 60 * 60 * 1000;
128
+
129
+ /** A file in storage, waiting for the message that names it. */
130
+ export interface UploadedFile {
131
+ uploadToken: string;
132
+ /** When Workstation stops taking `uploadToken`, on this phone's clock (ms). */
133
+ expiresAt: number;
134
+ }
135
+
136
+ function uploadExpiry(expiresAt: unknown, now: number): number {
137
+ const at =
138
+ typeof expiresAt === 'string' ? Date.parse(expiresAt) : typeof expiresAt === 'number' ? expiresAt : NaN;
139
+ return at > now ? at : now + UPLOAD_LIFETIME_MS;
140
+ }
141
+
87
142
  export class WidgetApiClient {
88
143
  private baseUrl = '';
89
144
 
@@ -360,54 +415,81 @@ export class WidgetApiClient {
360
415
  /**
361
416
  * Puts a local file in storage ahead of the message, which then names it by
362
417
  * the returned upload token. The target is presigned storage, or a path on
363
- * the server where its storage can't presign. The file is read as a native
364
- * blob, so it never becomes a JS string; its size is the blob's, since the
365
- * upload URL is signed for exactly the size asked for.
418
+ * the server where its storage can't presign. The upload URL is signed for
419
+ * exactly the size asked for: a `file://` file whose size `expo-file-system`
420
+ * can read streams from disk, anything else is read as a native blob and
421
+ * measured. `signal` cancels the transfer (the visitor removed the file, or
422
+ * left the conversation). Resolves the token and when it expires.
366
423
  */
367
- async uploadAttachment(file: {
368
- uri: string;
369
- name: string;
370
- contentType: string;
371
- size: number;
372
- }): Promise<string> {
373
- let blob: Blob;
374
- try {
375
- blob = await (await fetch(file.uri)).blob();
376
- } catch (error) {
377
- throw new RepzoWidgetError(
378
- error instanceof Error ? error.message : 'Could not read the file',
379
- 0,
380
- 'network'
381
- );
424
+ async uploadAttachment(
425
+ file: {
426
+ uri: string;
427
+ name: string;
428
+ contentType: string;
429
+ size: number;
430
+ },
431
+ signal?: AbortSignal
432
+ ): Promise<UploadedFile> {
433
+ // A file removed early is never read, nor given a slot in storage.
434
+ const stopIfCancelled = () => {
435
+ if (signal?.aborted) throw new RepzoWidgetError('Upload cancelled', 0, 'network');
436
+ };
437
+ stopIfCancelled();
438
+ // Streaming matters on Android: React Native also copies a blob it reads
439
+ // into a Base64 string for its network inspector, so a file past ~30 MB
440
+ // runs the app out of memory. Only file:// gives an exact Content-Length.
441
+ const streamedSize = urlScheme(file.uri) === 'file:' ? await measureFileSize(file.uri) : null;
442
+ let blob: Blob | null = null;
443
+ if (streamedSize === null) {
444
+ try {
445
+ blob = await (await fetch(file.uri, { signal })).blob();
446
+ } catch (error) {
447
+ throw new RepzoWidgetError(
448
+ error instanceof Error ? error.message : 'Could not read the file',
449
+ 0,
450
+ 'network'
451
+ );
452
+ }
382
453
  }
454
+ stopIfCancelled();
383
455
  const target = await this.request<{
384
456
  uploadUrl: string;
385
457
  method?: string;
386
458
  headers?: Record<string, string>;
387
459
  uploadToken: string;
460
+ expiresAt?: string | number;
388
461
  }>('/api/widget/uploads', {
389
462
  method: 'POST',
390
- body: { fileName: file.name, contentType: file.contentType, size: blob.size || file.size },
463
+ body: {
464
+ fileName: file.name,
465
+ contentType: file.contentType,
466
+ size: streamedSize ?? (blob?.size || file.size),
467
+ },
391
468
  });
469
+ // Counted from when the token was issued, not from when the file is up.
470
+ const expiresAt = uploadExpiry(target.expiresAt, Date.now());
392
471
  const uploadUrl = this.resolveUrl(target.uploadUrl);
393
472
  if (!uploadUrl) throw new RepzoWidgetError('Invalid upload target', 0, 'http');
473
+ stopIfCancelled();
394
474
 
395
- let response: Response;
396
- try {
397
- response = await fetch(uploadUrl, {
398
- method: target.method ?? 'PUT',
399
- headers: target.headers ?? { 'Content-Type': file.contentType },
400
- body: blob,
401
- });
402
- } catch (error) {
403
- throw new RepzoWidgetError(
404
- error instanceof Error ? error.message : 'Upload failed',
405
- 0,
406
- 'network'
407
- );
475
+ const method = target.method ?? 'PUT';
476
+ const headers = target.headers ?? { 'Content-Type': file.contentType };
477
+ let status: number;
478
+ if (blob) {
479
+ try {
480
+ status = (await fetch(uploadUrl, { method, headers, body: blob, signal })).status;
481
+ } catch (error) {
482
+ throw new RepzoWidgetError(
483
+ error instanceof Error ? error.message : 'Upload failed',
484
+ 0,
485
+ 'network'
486
+ );
487
+ }
488
+ } else {
489
+ status = await sendFileFromDisk(uploadUrl, method, headers, file, signal);
408
490
  }
409
- if (!response.ok) throw new RepzoWidgetError('Upload failed', response.status, 'http');
410
- return target.uploadToken;
491
+ if (status < 200 || status >= 300) throw new RepzoWidgetError('Upload failed', status, 'http');
492
+ return { uploadToken: target.uploadToken, expiresAt };
411
493
  }
412
494
 
413
495
  listConversations(): Promise<{ data: Conversation[] }> {
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Local file sizes from the host's `expo-file-system` (optional, like the
3
+ * pickers): read from the file's metadata, never from its contents. Shared by
4
+ * the pickers and the upload client, so the client doesn't pull in pickers.
5
+ *
6
+ * No `react-native` import on purpose: it stays testable in plain Node.
7
+ */
8
+
9
+ let fileSystem: any = null;
10
+ try {
11
+ fileSystem = require('expo-file-system') ?? null;
12
+ } catch {
13
+ fileSystem = null;
14
+ }
15
+
16
+ /** A positive, finite byte count, or null. */
17
+ export function validSize(size: unknown): number | null {
18
+ return typeof size === 'number' && Number.isFinite(size) && size > 0 ? size : null;
19
+ }
20
+
21
+ /**
22
+ * A local file's exact size in bytes, from its metadata without reading it.
23
+ * Null without `expo-file-system`, or when it can't tell.
24
+ */
25
+ export async function measureFileSize(uri: string): Promise<number | null> {
26
+ if (typeof fileSystem?.File === 'function') {
27
+ try {
28
+ const size = validSize(new fileSystem.File(uri).size);
29
+ if (size !== null) return size;
30
+ } catch {
31
+ // Fall through to the next reader.
32
+ }
33
+ }
34
+ if (typeof fileSystem?.getInfoAsync === 'function') {
35
+ try {
36
+ return validSize((await fileSystem.getInfoAsync(uri, { size: true }))?.size);
37
+ } catch {
38
+ return null;
39
+ }
40
+ }
41
+ return null;
42
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The phone's copy of each file the visitor sent, by the URL of the stored
3
+ * copy that replaced it in the thread. Right after a send the stored copy
4
+ * may not load yet (Workstation can still be storing it), so a photo keeps
5
+ * showing from the phone instead of turning into a file row. Kept in memory
6
+ * only: a later session shows the stored copy.
7
+ */
8
+
9
+ const MAX_PREVIEWS = 50;
10
+ const previews = new Map<string, string>();
11
+
12
+ export function rememberLocalPreview(storedUrl: string, localUrl: string): void {
13
+ if (!storedUrl || !localUrl) return;
14
+ previews.delete(storedUrl);
15
+ previews.set(storedUrl, localUrl);
16
+ // Oldest first: a Map iterates in insertion order.
17
+ if (previews.size > MAX_PREVIEWS) previews.delete(previews.keys().next().value as string);
18
+ }
19
+
20
+ export function localPreviewFor(storedUrl: string): string | null {
21
+ return (storedUrl && previews.get(storedUrl)) || null;
22
+ }
23
+
24
+ /** One that failed to load (the OS cleared the picker's cache): every view moves to the stored copy. */
25
+ export function forgetLocalPreview(storedUrl: string): void {
26
+ previews.delete(storedUrl);
27
+ }
28
+
29
+ /** Signing out forgets them: the next visitor on the phone doesn't inherit the paths. */
30
+ export function clearLocalPreviews(): void {
31
+ previews.clear();
32
+ }
@@ -2,7 +2,7 @@
2
2
  * Visitor attachments: picking photos/files for `POST /api/widget/messages`.
3
3
  *
4
4
  * A server that takes uploads names its cap (`maxAttachmentBytes`): a picked
5
- * file then stays a local URI and is uploaded when the message is sent. An
5
+ * file then stays a local URI, which the composer uploads right away. An
6
6
  * older server only takes files inline, as base64 data URLs within the
7
7
  * 1.5 MB / 6 MB caps below.
8
8
  *
@@ -24,6 +24,7 @@
24
24
  */
25
25
 
26
26
  import type { AttachmentSource, RepzoPickedFile, RepzoWidgetAttachmentOptions } from '../types';
27
+ import { measureFileSize, validSize } from './file-size';
27
28
 
28
29
  /** The server keeps the first five and silently drops the rest. */
29
30
  export const MAX_ATTACHMENTS = 5;
@@ -48,10 +49,19 @@ export interface DraftAttachment {
48
49
  size: number;
49
50
  /** `data:<mime>;base64,<…>` for an inline file; empty when `fileUri` is uploaded instead. */
50
51
  dataUrl: string;
51
- /** The local file uploaded when the message is sent. */
52
+ /** The local file uploaded ahead of the message. */
52
53
  fileUri?: string;
53
54
  /** What `<Image>` renders locally: the picked file's URI, else the data URL. */
54
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;
55
65
  }
56
66
 
57
67
  /** Why some (or all) of a pick didn't make it into the composer. */
@@ -119,6 +129,11 @@ export function setAttachmentOptions(options: RepzoWidgetAttachmentOptions | nul
119
129
  hostOptions = options;
120
130
  }
121
131
 
132
+ /** Whether a picked file starts uploading before Send (`attachments.uploadOnPick`). */
133
+ export function uploadsOnPick(): boolean {
134
+ return hostOptions?.uploadOnPick !== false;
135
+ }
136
+
122
137
  const ALL_SOURCES: AttachmentSource[] = ['photo', 'camera', 'file'];
123
138
 
124
139
  /**
@@ -356,7 +371,7 @@ async function toDraft(file: RepzoPickedFile): Promise<DraftAttachment> {
356
371
  };
357
372
  }
358
373
 
359
- /** A file that's uploaded when the message is sent: only its size is read here. */
374
+ /** A file that's uploaded on its own, ahead of the message: only its size is read here. */
360
375
  async function toUploadDraft(file: RepzoPickedFile, uploadLimit: number): Promise<DraftAttachment> {
361
376
  const uri = file.uri!;
362
377
  const size = validSize(file.size) ?? (await readFileSize(uri));
@@ -375,23 +390,9 @@ async function toUploadDraft(file: RepzoPickedFile, uploadLimit: number): Promis
375
390
 
376
391
  /** A local file's size in bytes, or 0 when nothing can tell. */
377
392
  async function readFileSize(uri: string): Promise<number> {
378
- if (typeof fileSystem?.File === 'function') {
379
- try {
380
- const size = validSize(new fileSystem.File(uri).size);
381
- if (size !== null) return size;
382
- } catch {
383
- // Fall through to the next reader.
384
- }
385
- }
386
- if (typeof fileSystem?.getInfoAsync === 'function') {
387
- try {
388
- const size = validSize((await fileSystem.getInfoAsync(uri, { size: true }))?.size);
389
- if (size !== null) return size;
390
- } catch {
391
- // Fall through to fetch.
392
- }
393
- }
394
- // A native blob: its size is known without reading it into JS.
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).
395
396
  const blob = await (await fetch(uri)).blob();
396
397
  return validSize(blob.size) ?? 0;
397
398
  }
@@ -524,10 +525,6 @@ export function isImageAttachment(contentType: string | null | undefined): boole
524
525
  return !!contentType && contentType.toLowerCase().startsWith('image/');
525
526
  }
526
527
 
527
- function validSize(size: unknown): number | null {
528
- return typeof size === 'number' && Number.isFinite(size) && size > 0 ? size : null;
529
- }
530
-
531
528
  function fileNameFromUri(uri: string | undefined): string | null {
532
529
  if (!uri) return null;
533
530
  const last = uri.split(/[?#]/)[0]?.split('/').pop();
@@ -563,7 +560,8 @@ const CONTENT_TYPES: Record<string, string> = {
563
560
  mov: 'video/quicktime',
564
561
  };
565
562
 
566
- 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 {
567
565
  const extension = /\.([a-z0-9]+)$/i.exec(nameOrUri?.split(/[?#]/)[0] ?? '')?.[1];
568
566
  return extension ? (CONTENT_TYPES[extension.toLowerCase()] ?? null) : null;
569
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,6 +92,8 @@ 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',
@@ -93,13 +108,17 @@ export const DEFAULT_LABELS = {
93
108
  preChatSubtitle: 'Let us know a bit about you so we can help you better.',
94
109
  preChatSkip: 'Skip',
95
110
  preChatSubmit: 'Start chat',
96
- viewPreviousConversations: 'View previous conversations',
97
- homeRecentTitle: 'Recent',
98
- homeViewAllCount: 'View all ({count})',
111
+ // Heading of Home's card that shows the newest conversation.
112
+ homeRecentMessageTitle: 'Recent message',
99
113
  homeNoConversations: 'Your conversations will show up here',
100
114
  homeStatusOnline: "We're online",
101
115
  homeStatusAway: "We're away",
102
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}',
103
122
  team: 'Team',
104
123
  home: 'Home',
105
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,12 +33,16 @@ 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: 'تعذر الإرسال',
@@ -48,13 +56,14 @@ const ar: LabelMap = {
48
56
  preChatSubtitle: 'أخبرنا قليلاً عنك حتى نتمكن من مساعدتك بشكل أفضل.',
49
57
  preChatSkip: 'تخطٍ',
50
58
  preChatSubmit: 'ابدأ المحادثة',
51
- viewPreviousConversations: 'عرض المحادثات السابقة',
52
- homeRecentTitle: 'الأحدث',
53
- homeViewAllCount: 'عرض الكل ({count})',
59
+ homeRecentMessageTitle: 'أحدث رسالة',
54
60
  homeNoConversations: 'ستظهر محادثاتك هنا',
55
61
  homeStatusOnline: 'نحن متصلون',
56
62
  homeStatusAway: 'نحن غير متصلين',
57
63
  unreadBadge: '{count} غير مقروءة',
64
+ lastMessagePhoto: 'صورة',
65
+ lastMessageFile: 'ملف',
66
+ lastMessageYou: 'أنت: {message}',
58
67
  team: 'الفريق',
59
68
  home: 'الرئيسية',
60
69
  close: 'إغلاق',