@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
package/src/storage.ts CHANGED
@@ -20,6 +20,8 @@ export const TOKEN_KEY = 'repzo_widget_token';
20
20
  export const USER_KEY = 'repzo_widget_user_key';
21
21
  export const DEVICE_ID_KEY = 'repzo_widget_device_id';
22
22
  export const UNREAD_KEY = 'repzo_widget_unread';
23
+ /** Who sent each conversation's newest message; Workstation's list doesn't say. */
24
+ export const LAST_MESSAGE_KEY = 'repzo_widget_last_message';
23
25
  /** Last config received — repaints the real theme instantly on relaunch. */
24
26
  /**
25
27
  * v2 wraps the config with the channel/baseUrl/platform it came from. v1 held a
package/src/types.ts CHANGED
@@ -64,6 +64,12 @@ export interface WidgetConfig {
64
64
  proactiveFrequency: 'session' | 'visitor';
65
65
  proactiveOpenMode: 'home' | 'new_message';
66
66
  attachmentsEnabled: boolean;
67
+ /**
68
+ * Largest file a visitor may attach. Set by servers that take uploads
69
+ * (`/api/widget/uploads`); absent on older ones, which only take files
70
+ * inline in the message.
71
+ */
72
+ maxAttachmentBytes?: number;
67
73
  analyticsEnabled: boolean;
68
74
  toursEnabled: boolean;
69
75
  defaultLocale: string;
@@ -94,7 +100,12 @@ export interface WidgetAttachment {
94
100
  */
95
101
  export interface OutgoingAttachment {
96
102
  name: string;
103
+ /** A base64 data URL; empty when `fileUri` is set. */
97
104
  url: string;
105
+ /** A local file uploaded before the message is sent (servers with `maxAttachmentBytes`). */
106
+ fileUri?: string;
107
+ /** `fileUri` is already uploaded under this token, so the send skips uploading it. */
108
+ uploadToken?: string;
98
109
  contentType: string;
99
110
  /** Decoded bytes. */
100
111
  size: number;
@@ -107,6 +118,21 @@ export interface Conversation {
107
118
  subject: string | null;
108
119
  status: string;
109
120
  snippet: string | null;
121
+ /**
122
+ * Set when the newest message had no words, only photos (`'photo'`) or
123
+ * other files (`'file'`); the lists then say "Photo" or "File", while
124
+ * `snippet` keeps Workstation's wording ("2 attachments"). `null` when it
125
+ * had words. Filled in from what this device sent, received or loaded.
126
+ * @since 1.4.0
127
+ */
128
+ lastMessageAttachment?: 'photo' | 'file' | null;
129
+ /**
130
+ * The newest message is the visitor's own; the lists put "You:" before it.
131
+ * A Workstation list that includes it is used as is; otherwise the SDK fills
132
+ * it in from what this device sent, received or loaded.
133
+ * @since 1.4.0
134
+ */
135
+ lastMessageFromVisitor?: boolean | null;
110
136
  messageCount: number;
111
137
  lastMessageAt: string | null;
112
138
  createdAt: string;
@@ -136,6 +162,14 @@ export interface Message {
136
162
  createdAt: string;
137
163
  /** Set when a teammate deleted the message; the content fields are then empty. */
138
164
  deletedAt?: string | null;
165
+ /**
166
+ * On the visitor's own messages: the id this device sent it under, which
167
+ * stays the same when a failed message is retried. Lets a stored message
168
+ * replace its pending or failed bubble. Only newer Workstation deployments
169
+ * send it.
170
+ * @since 1.3.1
171
+ */
172
+ clientMessageId?: string | null;
139
173
  }
140
174
 
141
175
  export interface OutboundMessage {
@@ -250,6 +284,22 @@ export interface BootOptions {
250
284
  * @since 1.2.0
251
285
  */
252
286
  posts?: boolean;
287
+ /**
288
+ * Show two or more photos in one message together as an album: one square
289
+ * holding them all, with "+N" on the last tile past four (default true).
290
+ * False shows each photo on its own, full width, as before 1.4.0. Either
291
+ * way, tapping a photo opens the message's photos in the viewer.
292
+ * @since 1.4.0
293
+ */
294
+ photoAlbums?: boolean;
295
+ /**
296
+ * Put "You: " before a conversation's newest message on Home and in
297
+ * Messages when the visitor sent it (default true; Arabic "أنت: ", the
298
+ * `lastMessageYou` translation key). False shows the message alone, as
299
+ * before 1.4.0. `Conversation.lastMessageFromVisitor` is filled in either way.
300
+ * @since 1.4.0
301
+ */
302
+ youPrefix?: boolean;
253
303
  }
254
304
 
255
305
  /** An option in the composer's attach menu. */
@@ -286,6 +336,14 @@ export interface RepzoWidgetAttachmentOptions {
286
336
  pick?: (source: AttachmentSource) => Promise<RepzoPickedFile[] | null>;
287
337
  /** Menu options offered with `pick`. Default all three. Ignored without `pick`. */
288
338
  sources?: AttachmentSource[];
339
+ /**
340
+ * Default true: with a Workstation that takes uploads, a picked file starts
341
+ * uploading right away, while the visitor writes, so the message goes out
342
+ * the moment they tap Send. False uploads the files only when they tap
343
+ * Send, so nothing leaves the phone for a file they remove first.
344
+ * @since 1.4.0
345
+ */
346
+ uploadOnPick?: boolean;
289
347
  }
290
348
 
291
349
  // ─── Agent UI blocks ────────────────────────────────────────────────────────
@@ -25,11 +25,12 @@ import {
25
25
  MAX_ATTACHMENT_BYTES,
26
26
  MAX_ATTACHMENTS,
27
27
  pickAttachments,
28
+ type DraftAttachment,
28
29
  } from '../attachments/pickers';
29
30
  import { controller } from '../controller';
30
31
  import { buildLabels, createTranslator, formatLabel, localizeWelcomeText } from '../i18n';
31
32
  import { useWidgetState } from '../state/use-widget-state';
32
- import type { AgentUiAction, OutgoingAttachment, PostInteractionKind, WidgetSpaceType } from '../types';
33
+ import type { AgentUiAction, AttachmentSource, OutgoingAttachment, PostInteractionKind, WidgetSpaceType } from '../types';
33
34
  import { WidgetErrorBoundary } from './components/ErrorBoundary';
34
35
  import { AlertIcon } from './components/icons';
35
36
  import { InAppNotification } from './components/InAppNotification';
@@ -103,9 +104,9 @@ export function RepzoWidgetProvider({
103
104
  [state.config, state.locale]
104
105
  );
105
106
 
106
- // Home previews the two newest threads and credits the agents behind them.
107
- const recentConversations = useMemo(
108
- () => 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],
109
110
  [state.conversations]
110
111
  );
111
112
  const teamAgents = useMemo(() => pickTeamAgents(state.conversations, 3), [state.conversations]);
@@ -131,6 +132,15 @@ export function RepzoWidgetProvider({
131
132
  }),
132
133
  [labels]
133
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
+ );
134
144
 
135
145
  const blockLabels = useMemo(
136
146
  () => ({
@@ -163,13 +173,19 @@ export function RepzoWidgetProvider({
163
173
  file: labels.attachChooseFile,
164
174
  cancel: labels.cancel,
165
175
  remove: labels.removeAttachment,
166
- tooLarge: formatLabel(labels.attachmentTooLarge, { max: formatBytes(MAX_ATTACHMENT_BYTES) }),
176
+ tooLarge: formatLabel(labels.attachmentTooLarge, {
177
+ max: formatBytes(theme.maxAttachmentBytes ?? MAX_ATTACHMENT_BYTES),
178
+ }),
167
179
  tooMany: formatLabel(labels.attachmentTooMany, { count: MAX_ATTACHMENTS }),
168
180
  tooLargeTogether: labels.attachmentsTooLargeTogether,
169
181
  failed: labels.attachmentFailed,
170
182
  cameraDenied: labels.cameraPermissionDenied,
183
+ uploading: labels.attachmentUploading,
184
+ uploadFailed: labels.attachmentUploadFailed,
185
+ uploadFailedNotice: labels.attachmentsUploadFailed,
186
+ retry: labels.tapToRetry,
171
187
  }),
172
- [labels]
188
+ [labels, theme.maxAttachmentBytes]
173
189
  );
174
190
 
175
191
  // Stable across renders so each message bubble only re-renders when its
@@ -181,7 +197,12 @@ export function RepzoWidgetProvider({
181
197
  loadEarlier: labels.loadEarlier,
182
198
  emptyConversation: labels.emptyConversation,
183
199
  messagePlaceholder: labels.messagePlaceholder,
200
+ send: labels.sendMessage,
201
+ jumpToLatest: labels.jumpToLatest,
202
+ jumpToLatestNew: labels.jumpToLatestNew,
203
+ morePhotos: labels.morePhotos,
184
204
  failedToSend: labels.failedToSend,
205
+ tapToRetry: labels.tapToRetry,
185
206
  openAttachment: labels.openAttachment,
186
207
  thinking: labels.thinking,
187
208
  team: labels.team,
@@ -224,6 +245,7 @@ export function RepzoWidgetProvider({
224
245
  close: labels.close,
225
246
  loadFailed: labels.imageLoadFailed,
226
247
  openExternally: labels.openImageExternally,
248
+ position: labels.imagePosition,
227
249
  },
228
250
  voice: {
229
251
  play: labels.voicePlay,
@@ -238,6 +260,19 @@ export function RepzoWidgetProvider({
238
260
  const attachmentSources = theme.attachmentsEnabled ? getAttachmentSources() : [];
239
261
 
240
262
  const handleClose = useCallback(() => store.hide(), [store]);
263
+ const handlePickAttachments = useCallback(
264
+ (source: AttachmentSource, current: DraftAttachment[]) =>
265
+ pickAttachments(source, current, theme.maxAttachmentBytes),
266
+ [theme.maxAttachmentBytes]
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
+ );
241
276
 
242
277
  const handleOpenNotification = useCallback(
243
278
  (conversationId: string) => void store.openConversation(conversationId),
@@ -304,6 +339,7 @@ export function RepzoWidgetProvider({
304
339
  store.sendMessage(body, attachments ?? [], action),
305
340
  [store]
306
341
  );
342
+ const handleRetryMessage = useCallback((messageId: string) => store.retryMessage(messageId), [store]);
307
343
 
308
344
  const handleIdentifyVisitor = useCallback(
309
345
  (data: { email?: string; name?: string; phone?: string }) => store.identifyVisitor(data),
@@ -442,8 +478,11 @@ export function RepzoWidgetProvider({
442
478
  offlineMessage={theme.offlineMessage}
443
479
  labels={chatLabels}
444
480
  attachmentSources={attachmentSources}
445
- onPickAttachments={pickAttachments}
481
+ onPickAttachments={handlePickAttachments}
482
+ onUploadAttachment={handleUploadAttachment}
446
483
  onSendMessage={handleSendMessage}
484
+ onRetryMessage={handleRetryMessage}
485
+ photoAlbums={store.showsPhotoAlbums()}
447
486
  onIdentifyVisitor={handleIdentifyVisitor}
448
487
  onRateConversation={handleRateConversation}
449
488
  onLoadMore={handleLoadMore}
@@ -461,7 +500,7 @@ export function RepzoWidgetProvider({
461
500
  color={theme.primaryColor}
462
501
  visitorName={state.visitorProfile.name}
463
502
  conversationCount={state.conversations.length}
464
- recentConversations={recentConversations}
503
+ recentConversation={recentConversation}
465
504
  unreadByConversation={state.unreadByConversation}
466
505
  teamAgents={teamAgents}
467
506
  isOnline={theme.isOnline}
@@ -472,19 +511,17 @@ export function RepzoWidgetProvider({
472
511
  showPreviousConversations={theme.showPreviousConversations}
473
512
  labels={{
474
513
  close: labels.close,
475
- viewPrevious: labels.viewPreviousConversations,
476
- viewAll: formatLabel(labels.homeViewAllCount, { count: state.conversations.length }),
477
- recentTitle: labels.homeRecentTitle,
514
+ recentTitle: labels.homeRecentMessageTitle,
478
515
  noConversations: labels.homeNoConversations,
479
516
  statusOnline: labels.homeStatusOnline,
480
517
  statusAway: labels.homeStatusAway,
481
518
  team: labels.team,
482
519
  time: timeLabels,
483
520
  unread: labels.unreadBadge,
521
+ lastMessage: lastMessageLabels,
484
522
  }}
485
523
  onClose={handleClose}
486
524
  onStartConversation={handleStartConversation}
487
- onViewConversations={() => handleTabChange('messages')}
488
525
  onOpenConversation={(conversationId) => store.openConversation(conversationId)}
489
526
  />
490
527
  );
@@ -510,6 +547,7 @@ export function RepzoWidgetProvider({
510
547
  statusAway: labels.homeStatusAway,
511
548
  time: timeLabels,
512
549
  unread: labels.unreadBadge,
550
+ lastMessage: lastMessageLabels,
513
551
  }}
514
552
  onSelect={(conversationId) => store.openConversation(conversationId)}
515
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
+ });