@repzo/widget-react-native 1.0.0 → 1.0.4

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 (153) hide show
  1. package/README.md +167 -74
  2. package/lib/commonjs/api/client.js +3 -1
  3. package/lib/commonjs/api/client.js.map +1 -1
  4. package/lib/commonjs/api/realtime.js +2 -0
  5. package/lib/commonjs/api/realtime.js.map +1 -1
  6. package/lib/commonjs/attachments/pickers.js +493 -0
  7. package/lib/commonjs/attachments/pickers.js.map +1 -0
  8. package/lib/commonjs/i18n.js +45 -0
  9. package/lib/commonjs/i18n.js.map +1 -1
  10. package/lib/commonjs/index.js.map +1 -1
  11. package/lib/commonjs/locales.js +21 -0
  12. package/lib/commonjs/locales.js.map +1 -1
  13. package/lib/commonjs/state/widget-store.js +256 -18
  14. package/lib/commonjs/state/widget-store.js.map +1 -1
  15. package/lib/commonjs/types.js +16 -0
  16. package/lib/commonjs/types.js.map +1 -1
  17. package/lib/commonjs/ui/WidgetProvider.js +70 -10
  18. package/lib/commonjs/ui/WidgetProvider.js.map +1 -1
  19. package/lib/commonjs/ui/components/AttachmentChips.js +135 -0
  20. package/lib/commonjs/ui/components/AttachmentChips.js.map +1 -0
  21. package/lib/commonjs/ui/components/AttachmentMenu.js +125 -0
  22. package/lib/commonjs/ui/components/AttachmentMenu.js.map +1 -0
  23. package/lib/commonjs/ui/components/ImageViewer.js +181 -0
  24. package/lib/commonjs/ui/components/ImageViewer.js.map +1 -0
  25. package/lib/commonjs/ui/components/InAppNotification.js +210 -0
  26. package/lib/commonjs/ui/components/InAppNotification.js.map +1 -0
  27. package/lib/commonjs/ui/components/TeamAvatars.js +63 -0
  28. package/lib/commonjs/ui/components/TeamAvatars.js.map +1 -0
  29. package/lib/commonjs/ui/components/icons.js +50 -0
  30. package/lib/commonjs/ui/components/icons.js.map +1 -1
  31. package/lib/commonjs/ui/screens/chat/ChatView.js +303 -24
  32. package/lib/commonjs/ui/screens/chat/ChatView.js.map +1 -1
  33. package/lib/commonjs/ui/screens/chat/ChatView.styles.js +70 -0
  34. package/lib/commonjs/ui/screens/chat/ChatView.styles.js.map +1 -1
  35. package/lib/module/api/client.js +3 -1
  36. package/lib/module/api/client.js.map +1 -1
  37. package/lib/module/api/realtime.js +2 -0
  38. package/lib/module/api/realtime.js.map +1 -1
  39. package/lib/module/attachments/pickers.js +479 -0
  40. package/lib/module/attachments/pickers.js.map +1 -0
  41. package/lib/module/i18n.js +44 -0
  42. package/lib/module/i18n.js.map +1 -1
  43. package/lib/module/index.js.map +1 -1
  44. package/lib/module/locales.js +21 -0
  45. package/lib/module/locales.js.map +1 -1
  46. package/lib/module/state/widget-store.js +256 -18
  47. package/lib/module/state/widget-store.js.map +1 -1
  48. package/lib/module/types.js +16 -0
  49. package/lib/module/types.js.map +1 -1
  50. package/lib/module/ui/WidgetProvider.js +71 -11
  51. package/lib/module/ui/WidgetProvider.js.map +1 -1
  52. package/lib/module/ui/components/AttachmentChips.js +131 -0
  53. package/lib/module/ui/components/AttachmentChips.js.map +1 -0
  54. package/lib/module/ui/components/AttachmentMenu.js +120 -0
  55. package/lib/module/ui/components/AttachmentMenu.js.map +1 -0
  56. package/lib/module/ui/components/ImageViewer.js +177 -0
  57. package/lib/module/ui/components/ImageViewer.js.map +1 -0
  58. package/lib/module/ui/components/InAppNotification.js +208 -0
  59. package/lib/module/ui/components/InAppNotification.js.map +1 -0
  60. package/lib/module/ui/components/TeamAvatars.js +59 -0
  61. package/lib/module/ui/components/TeamAvatars.js.map +1 -0
  62. package/lib/module/ui/components/icons.js +49 -0
  63. package/lib/module/ui/components/icons.js.map +1 -1
  64. package/lib/module/ui/screens/chat/ChatView.js +306 -27
  65. package/lib/module/ui/screens/chat/ChatView.js.map +1 -1
  66. package/lib/module/ui/screens/chat/ChatView.styles.js +70 -0
  67. package/lib/module/ui/screens/chat/ChatView.styles.js.map +1 -1
  68. package/lib/typescript/commonjs/api/client.d.ts +8 -2
  69. package/lib/typescript/commonjs/api/client.d.ts.map +1 -1
  70. package/lib/typescript/commonjs/api/realtime.d.ts.map +1 -1
  71. package/lib/typescript/commonjs/attachments/pickers.d.ts +81 -0
  72. package/lib/typescript/commonjs/attachments/pickers.d.ts.map +1 -0
  73. package/lib/typescript/commonjs/i18n.d.ts +23 -0
  74. package/lib/typescript/commonjs/i18n.d.ts.map +1 -1
  75. package/lib/typescript/commonjs/index.d.ts +1 -1
  76. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  77. package/lib/typescript/commonjs/locales.d.ts.map +1 -1
  78. package/lib/typescript/commonjs/state/widget-store.d.ts +40 -2
  79. package/lib/typescript/commonjs/state/widget-store.d.ts.map +1 -1
  80. package/lib/typescript/commonjs/types.d.ts +88 -1
  81. package/lib/typescript/commonjs/types.d.ts.map +1 -1
  82. package/lib/typescript/commonjs/ui/WidgetProvider.d.ts.map +1 -1
  83. package/lib/typescript/commonjs/ui/components/AttachmentChips.d.ts +16 -0
  84. package/lib/typescript/commonjs/ui/components/AttachmentChips.d.ts.map +1 -0
  85. package/lib/typescript/commonjs/ui/components/AttachmentMenu.d.ts +28 -0
  86. package/lib/typescript/commonjs/ui/components/AttachmentMenu.d.ts.map +1 -0
  87. package/lib/typescript/commonjs/ui/components/ImageViewer.d.ts +23 -0
  88. package/lib/typescript/commonjs/ui/components/ImageViewer.d.ts.map +1 -0
  89. package/lib/typescript/commonjs/ui/components/InAppNotification.d.ts +28 -0
  90. package/lib/typescript/commonjs/ui/components/InAppNotification.d.ts.map +1 -0
  91. package/lib/typescript/commonjs/ui/components/TeamAvatars.d.ts +17 -0
  92. package/lib/typescript/commonjs/ui/components/TeamAvatars.d.ts.map +1 -0
  93. package/lib/typescript/commonjs/ui/components/icons.d.ts +2 -0
  94. package/lib/typescript/commonjs/ui/components/icons.d.ts.map +1 -1
  95. package/lib/typescript/commonjs/ui/screens/chat/ChatView.d.ts +3 -2
  96. package/lib/typescript/commonjs/ui/screens/chat/ChatView.d.ts.map +1 -1
  97. package/lib/typescript/commonjs/ui/screens/chat/ChatView.styles.d.ts +69 -0
  98. package/lib/typescript/commonjs/ui/screens/chat/ChatView.styles.d.ts.map +1 -1
  99. package/lib/typescript/commonjs/ui/screens/chat/ChatView.types.d.ts +41 -2
  100. package/lib/typescript/commonjs/ui/screens/chat/ChatView.types.d.ts.map +1 -1
  101. package/lib/typescript/module/api/client.d.ts +8 -2
  102. package/lib/typescript/module/api/client.d.ts.map +1 -1
  103. package/lib/typescript/module/api/realtime.d.ts.map +1 -1
  104. package/lib/typescript/module/attachments/pickers.d.ts +81 -0
  105. package/lib/typescript/module/attachments/pickers.d.ts.map +1 -0
  106. package/lib/typescript/module/i18n.d.ts +23 -0
  107. package/lib/typescript/module/i18n.d.ts.map +1 -1
  108. package/lib/typescript/module/index.d.ts +1 -1
  109. package/lib/typescript/module/index.d.ts.map +1 -1
  110. package/lib/typescript/module/locales.d.ts.map +1 -1
  111. package/lib/typescript/module/state/widget-store.d.ts +40 -2
  112. package/lib/typescript/module/state/widget-store.d.ts.map +1 -1
  113. package/lib/typescript/module/types.d.ts +88 -1
  114. package/lib/typescript/module/types.d.ts.map +1 -1
  115. package/lib/typescript/module/ui/WidgetProvider.d.ts.map +1 -1
  116. package/lib/typescript/module/ui/components/AttachmentChips.d.ts +16 -0
  117. package/lib/typescript/module/ui/components/AttachmentChips.d.ts.map +1 -0
  118. package/lib/typescript/module/ui/components/AttachmentMenu.d.ts +28 -0
  119. package/lib/typescript/module/ui/components/AttachmentMenu.d.ts.map +1 -0
  120. package/lib/typescript/module/ui/components/ImageViewer.d.ts +23 -0
  121. package/lib/typescript/module/ui/components/ImageViewer.d.ts.map +1 -0
  122. package/lib/typescript/module/ui/components/InAppNotification.d.ts +28 -0
  123. package/lib/typescript/module/ui/components/InAppNotification.d.ts.map +1 -0
  124. package/lib/typescript/module/ui/components/TeamAvatars.d.ts +17 -0
  125. package/lib/typescript/module/ui/components/TeamAvatars.d.ts.map +1 -0
  126. package/lib/typescript/module/ui/components/icons.d.ts +2 -0
  127. package/lib/typescript/module/ui/components/icons.d.ts.map +1 -1
  128. package/lib/typescript/module/ui/screens/chat/ChatView.d.ts +3 -2
  129. package/lib/typescript/module/ui/screens/chat/ChatView.d.ts.map +1 -1
  130. package/lib/typescript/module/ui/screens/chat/ChatView.styles.d.ts +69 -0
  131. package/lib/typescript/module/ui/screens/chat/ChatView.styles.d.ts.map +1 -1
  132. package/lib/typescript/module/ui/screens/chat/ChatView.types.d.ts +41 -2
  133. package/lib/typescript/module/ui/screens/chat/ChatView.types.d.ts.map +1 -1
  134. package/metro.js +17 -32
  135. package/package.json +18 -1
  136. package/src/api/client.ts +5 -3
  137. package/src/api/realtime.ts +2 -0
  138. package/src/attachments/pickers.ts +515 -0
  139. package/src/i18n.ts +48 -0
  140. package/src/index.tsx +4 -0
  141. package/src/locales.ts +21 -0
  142. package/src/state/widget-store.ts +256 -19
  143. package/src/types.ts +95 -0
  144. package/src/ui/WidgetProvider.tsx +90 -9
  145. package/src/ui/components/AttachmentChips.tsx +135 -0
  146. package/src/ui/components/AttachmentMenu.tsx +132 -0
  147. package/src/ui/components/ImageViewer.tsx +178 -0
  148. package/src/ui/components/InAppNotification.tsx +225 -0
  149. package/src/ui/components/TeamAvatars.tsx +58 -0
  150. package/src/ui/components/icons.tsx +37 -0
  151. package/src/ui/screens/chat/ChatView.styles.ts +70 -0
  152. package/src/ui/screens/chat/ChatView.tsx +345 -35
  153. package/src/ui/screens/chat/ChatView.types.ts +51 -2
@@ -7,11 +7,16 @@ exports.ChatView = ChatView;
7
7
  var _react = require("react");
8
8
  var _reactNative = require("react-native");
9
9
  var _agentUiBlocks = require("../../../agent-ui-blocks.js");
10
+ var _pickers = require("../../../attachments/pickers.js");
10
11
  var _AgentUiBlocks = require("../../components/AgentUiBlocks.js");
12
+ var _AttachmentChips = require("../../components/AttachmentChips.js");
13
+ var _AttachmentMenu = require("../../components/AttachmentMenu.js");
11
14
  var _Avatar = require("../../components/Avatar.js");
15
+ var _ImageViewer = require("../../components/ImageViewer.js");
12
16
  var _ErrorBoundary = require("../../components/ErrorBoundary.js");
13
17
  var _Gradient = require("../../components/Gradient.js");
14
18
  var _icons = require("../../components/icons.js");
19
+ var _TeamAvatars = require("../../components/TeamAvatars.js");
15
20
  var _Markdown = require("../../components/Markdown.js");
16
21
  var _useReduceMotion = require("../../hooks/use-reduce-motion.js");
17
22
  var _global = require("../../styles/global.js");
@@ -28,7 +33,8 @@ var _jsxRuntime = require("react/jsx-runtime");
28
33
  *
29
34
  * Mobile differences: older messages load through an explicit "Load earlier
30
35
  * messages" control (a scroll-anchored infinite list fights the keyboard), and
31
- * the composer hides the attach button because v1 has no picker.
36
+ * the attach button only shows when the host app has a picker to open (see
37
+ * `attachments/pickers.ts`).
32
38
  */
33
39
 
34
40
  function ChatView({
@@ -39,11 +45,17 @@ function ChatView({
39
45
  color,
40
46
  agentName,
41
47
  agentAvatar,
48
+ agentOnline,
42
49
  isOnline,
50
+ teamName,
51
+ teamAvatars = [],
52
+ replyTimeLabel,
43
53
  locale,
44
54
  prefillText,
45
55
  offlineMessage,
46
56
  labels,
57
+ attachmentSources = [],
58
+ onPickAttachments,
47
59
  onSendMessage,
48
60
  onIdentifyVisitor,
49
61
  onLoadMore,
@@ -53,14 +65,38 @@ function ChatView({
53
65
  }) {
54
66
  const [input, setInput] = (0, _react.useState)(prefillText || '');
55
67
  const [composerFocused, setComposerFocused] = (0, _react.useState)(false);
68
+ const [drafts, setDrafts] = (0, _react.useState)([]);
69
+ const [menuOpen, setMenuOpen] = (0, _react.useState)(false);
70
+ const [viewerImage, setViewerImage] = (0, _react.useState)(null);
71
+ const [picking, setPicking] = (0, _react.useState)(false);
72
+ // Only attachment sends hold the composer; text sends stay fire-and-forget.
73
+ const [sending, setSending] = (0, _react.useState)(false);
74
+ const [notice, setNotice] = (0, _react.useState)(null);
56
75
  const reduceMotion = (0, _useReduceMotion.useReduceMotion)();
57
76
  const scrollRef = (0, _react.useRef)(null);
58
77
  const typingStopTimer = (0, _react.useRef)(null);
78
+ const noticeTimer = (0, _react.useRef)(null);
79
+ const pickerTimer = (0, _react.useRef)(null);
80
+ const mounted = (0, _react.useRef)(false);
59
81
  (0, _react.useEffect)(() => {
60
82
  if (prefillText) setInput(prefillText);
61
83
  }, [prefillText]);
62
- (0, _react.useEffect)(() => () => {
63
- if (typingStopTimer.current) clearTimeout(typingStopTimer.current);
84
+
85
+ // The provider keys this screen by the store's thread generation. Pending
86
+ // picks and sends belong to this mount, even before a draft gets a server ID.
87
+ (0, _react.useEffect)(() => {
88
+ mounted.current = true;
89
+ return () => {
90
+ mounted.current = false;
91
+ if (typingStopTimer.current) clearTimeout(typingStopTimer.current);
92
+ if (noticeTimer.current) clearTimeout(noticeTimer.current);
93
+ if (pickerTimer.current) clearTimeout(pickerTimer.current);
94
+ };
95
+ }, []);
96
+ const showNotice = (0, _react.useCallback)(message => {
97
+ if (noticeTimer.current) clearTimeout(noticeTimer.current);
98
+ setNotice(message);
99
+ if (message) noticeTimer.current = setTimeout(() => setNotice(null), 5000);
64
100
  }, []);
65
101
  const scrollToBottom = (0, _react.useCallback)((animated = true) => {
66
102
  scrollRef.current?.scrollToEnd({
@@ -104,15 +140,101 @@ function ChatView({
104
140
  viewportHeight.current = height;
105
141
  if (shrank) scrollToBottom(false);
106
142
  }, [scrollToBottom]);
107
- const canSend = input.trim().length > 0;
143
+ const showAttach = attachmentSources.length > 0 && !!onPickAttachments;
144
+ const canSend = !sending && (input.trim().length > 0 || drafts.length > 0);
145
+ const problemLabel = problem => {
146
+ switch (problem) {
147
+ case 'tooMany':
148
+ return labels.attach.tooMany;
149
+ case 'tooLarge':
150
+ return labels.attach.tooLarge;
151
+ case 'tooLargeTogether':
152
+ return labels.attach.tooLargeTogether;
153
+ case 'cameraDenied':
154
+ return labels.attach.cameraDenied;
155
+ case 'failed':
156
+ return labels.attach.failed;
157
+ default:
158
+ return null;
159
+ }
160
+ };
161
+ const handlePick = async source => {
162
+ if (!mounted.current || !onPickAttachments || picking) return;
163
+ setPicking(true);
164
+ try {
165
+ const result = await onPickAttachments(source, drafts);
166
+ if (!mounted.current) return;
167
+ if (result.added.length > 0) {
168
+ setDrafts(current => [...current, ...result.added].slice(0, _pickers.MAX_ATTACHMENTS));
169
+ }
170
+ showNotice(problemLabel(result.problem));
171
+ } finally {
172
+ if (mounted.current) setPicking(false);
173
+ }
174
+ };
175
+ const menuOptions = attachmentSources.map(source => ({
176
+ key: source,
177
+ label: source === 'photo' ? labels.attach.photoLibrary : source === 'camera' ? labels.attach.takePhoto : labels.attach.file
178
+ }));
179
+ const handleAttachPress = () => {
180
+ if (sending || picking) return;
181
+ if (drafts.length >= _pickers.MAX_ATTACHMENTS) {
182
+ showNotice(labels.attach.tooMany);
183
+ return;
184
+ }
185
+ const only = attachmentSources.length === 1 ? attachmentSources[0] : undefined;
186
+ if (only) {
187
+ void handlePick(only);
188
+ return;
189
+ }
190
+ const native = (0, _AttachmentMenu.showNativeAttachmentMenu)(menuOptions, labels.attach.cancel, source => {
191
+ if (!mounted.current) return;
192
+ // Presenting a picker while the action sheet is still animating away
193
+ // fails silently on iOS; let it finish first.
194
+ pickerTimer.current = setTimeout(() => void handlePick(source), 350);
195
+ });
196
+ if (!native) setMenuOpen(true);
197
+ };
198
+ const handleMenuSelect = source => {
199
+ setMenuOpen(false);
200
+ void handlePick(source);
201
+ };
108
202
  const handleSend = () => {
109
203
  const trimmed = input.trim();
110
- if (!trimmed) return;
204
+ if (!canSend) return;
111
205
  setInput('');
112
206
  if (typingStopTimer.current) clearTimeout(typingStopTimer.current);
113
207
  onTyping(false);
114
- onSendMessage(trimmed);
115
208
  scrollToBottom();
209
+ if (drafts.length === 0) {
210
+ // The failed bubble already surfaces a failure to the visitor.
211
+ onSendMessage(trimmed).catch(() => {});
212
+ return;
213
+ }
214
+ const sent = drafts;
215
+ setSending(true);
216
+ showNotice(null);
217
+ onSendMessage(trimmed, sent.map(draft => ({
218
+ name: draft.name,
219
+ url: draft.dataUrl,
220
+ contentType: draft.contentType,
221
+ size: draft.size,
222
+ previewUrl: draft.previewUri
223
+ }))).then(() => {
224
+ if (!mounted.current) return;
225
+ const sentIds = new Set(sent.map(draft => draft.id));
226
+ setDrafts(current => current.filter(draft => !sentIds.has(draft.id)));
227
+ }).catch(error => {
228
+ // The visitor has moved to another thread: nothing to restore there.
229
+ if (!mounted.current) return;
230
+ // Chips stay for a retry; put the text back too unless they've
231
+ // already started typing something else.
232
+ setInput(current => current || trimmed);
233
+ const status = error?.status;
234
+ showNotice(status === 413 ? labels.attach.tooLargeTogether : labels.failedToSend);
235
+ }).finally(() => {
236
+ if (mounted.current) setSending(false);
237
+ });
116
238
  };
117
239
  const handleChangeText = text => {
118
240
  setInput(text);
@@ -129,12 +251,19 @@ function ChatView({
129
251
  phone: stringPayload(action.payload.phone)
130
252
  });
131
253
  }
132
- onSendMessage((0, _agentUiBlocks.formatAgentUiActionMessage)(action), undefined, action);
254
+ onSendMessage((0, _agentUiBlocks.formatAgentUiActionMessage)(action), undefined, action).catch(() => {});
133
255
  scrollToBottom();
134
256
  }, [onIdentifyVisitor, onSendMessage, scrollToBottom]);
135
257
  const headerTextColor = (0, _theme.contrastingTextColor)(color);
136
258
  const onDarkHeader = headerTextColor === _theme.palette.white;
137
259
  const isEmptyThread = !loading && messages.length === 0;
260
+ const agentIsActive = agentOnline ?? isOnline;
261
+ // "Waiting for a teammate" until someone replies: no teammate in the header
262
+ // and the visitor spoke last (bot replies before that still count as waiting).
263
+ const lastChatMessage = [...messages].reverse().find(message => (message.messageType ?? 'message') === 'message' && !message.deletedAt);
264
+ const awaitingTeammate = !agentName && !agentTyping && lastChatMessage?.direction === 'inbound';
265
+ // "Sent" / "Seen" goes under the visitor's latest message only.
266
+ const lastVisitorMessageId = [...messages].reverse().find(message => message.direction === 'inbound' && (message.messageType ?? 'message') === 'message')?.id;
138
267
  // Two stops off the brand color: flat fill looked like an unstyled block.
139
268
  // A non-hex brand color falls through mixHex unchanged and renders solid.
140
269
  const headerGradient = [color, (0, _theme.mixHex)(color, '#000000', 0.16)];
@@ -161,7 +290,10 @@ function ChatView({
161
290
  direction: (0, _rtl.backDirection)(),
162
291
  color: headerTextColor
163
292
  })
164
- }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
293
+ }), agentName ?
294
+ /*#__PURE__*/
295
+ // A teammate joined: their photo, name and own presence.
296
+ (0, _jsxRuntime.jsxs)(_reactNative.View, {
165
297
  style: _ChatViewStyles.styles.headerIdentity,
166
298
  children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
167
299
  style: _ChatViewStyles.styles.avatarWrapper,
@@ -171,7 +303,7 @@ function ChatView({
171
303
  size: 36,
172
304
  translucent: onDarkHeader,
173
305
  color: _theme.palette.ink
174
- }), isOnline ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
306
+ }), agentIsActive ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
175
307
  style: _ChatViewStyles.styles.onlineDot
176
308
  }) : null]
177
309
  }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
@@ -181,15 +313,48 @@ function ChatView({
181
313
  color: headerTextColor
182
314
  }],
183
315
  numberOfLines: 1,
184
- children: agentName || labels.team
316
+ children: agentName
185
317
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
186
318
  style: [_ChatViewStyles.styles.headerStatus, {
187
319
  color: (0, _theme.withAlpha)(headerTextColor, 0.75)
188
320
  }],
189
321
  numberOfLines: 1,
190
- children: isOnline ? labels.statusOnline : labels.statusAway
322
+ children: agentIsActive ? labels.statusOnline : labels.statusAway
191
323
  })]
192
324
  })]
325
+ }) :
326
+ /*#__PURE__*/
327
+ // No teammate yet, or the conversation closed: the team, Intercom-style.
328
+ (0, _jsxRuntime.jsxs)(_reactNative.View, {
329
+ style: _ChatViewStyles.styles.headerIdentity,
330
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_TeamAvatars.TeamAvatars, {
331
+ avatars: teamAvatars,
332
+ size: 30,
333
+ color: _theme.palette.ink,
334
+ ringColor: color,
335
+ translucent: onDarkHeader
336
+ }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
337
+ style: _ChatViewStyles.styles.headerText,
338
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
339
+ style: [_ChatViewStyles.styles.headerName, {
340
+ color: headerTextColor
341
+ }],
342
+ numberOfLines: 1,
343
+ children: teamName || labels.team
344
+ }), replyTimeLabel ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
345
+ style: _ChatViewStyles.styles.headerReplyTime,
346
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_icons.ClockIcon, {
347
+ size: 12,
348
+ color: (0, _theme.withAlpha)(headerTextColor, 0.75)
349
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
350
+ style: [_ChatViewStyles.styles.headerStatus, _ChatViewStyles.styles.headerReplyTimeText, {
351
+ color: (0, _theme.withAlpha)(headerTextColor, 0.75)
352
+ }],
353
+ numberOfLines: 1,
354
+ children: replyTimeLabel
355
+ })]
356
+ }) : null]
357
+ })]
193
358
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Pressable, {
194
359
  onPress: onClose,
195
360
  accessibilityRole: "button",
@@ -266,14 +431,26 @@ function ChatView({
266
431
  message: message,
267
432
  color: color,
268
433
  labels: labels,
434
+ onOpenImage: setViewerImage,
269
435
  senderLabel: isAgentMessage && (startsRun || startsDay) ? message.senderName || agentName || labels.team : null,
270
436
  timeLabel: (0, _conversations.formatClockTime)(message.createdAt, locale),
271
- onSubmitProcedureForm: shouldShowProcedureForm(messages, message) ? body => onSendMessage(body) : undefined,
437
+ receiptLabel: message.id === lastVisitorMessageId ? receiptLabel(message, labels) : undefined,
438
+ onSubmitProcedureForm: shouldShowProcedureForm(messages, message) ? body => void onSendMessage(body).catch(() => {}) : undefined,
272
439
  onAgentUiAction: shouldShowAgentUiBlocks(messages, message) ? handleAgentUiAction : undefined
273
440
  })
274
441
  })]
275
442
  }, message.id);
276
- }), agentTyping ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
443
+ }), awaitingTeammate ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
444
+ style: _ChatViewStyles.styles.waitingRow,
445
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_TeamAvatars.TeamAvatars, {
446
+ avatars: teamAvatars,
447
+ size: 20,
448
+ color: color
449
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
450
+ style: _ChatViewStyles.styles.waitingText,
451
+ children: labels.waitingForTeammate
452
+ })]
453
+ }) : null, agentTyping ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
277
454
  style: _ChatViewStyles.styles.typingRow,
278
455
  children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
279
456
  style: _ChatViewStyles.styles.typingBubble,
@@ -298,13 +475,44 @@ function ChatView({
298
475
  style: _ChatViewStyles.styles.offlineText,
299
476
  children: offlineMessage
300
477
  })
301
- }) : null, /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
478
+ }) : null, /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
302
479
  style: _ChatViewStyles.styles.composerWrapper,
303
- children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
304
- style: [_ChatViewStyles.styles.composer, composerFocused && _ChatViewStyles.styles.composerFocused, composerFocused && {
480
+ children: [notice ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
481
+ style: _ChatViewStyles.styles.composerNotice,
482
+ accessibilityRole: "alert",
483
+ accessibilityLiveRegion: "polite",
484
+ children: notice
485
+ }) : null, drafts.length > 0 ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_AttachmentChips.AttachmentChips, {
486
+ attachments: drafts,
487
+ disabled: sending,
488
+ removeLabel: labels.attach.remove,
489
+ onRemove: id => {
490
+ setDrafts(current => current.filter(draft => draft.id !== id));
491
+ showNotice(null);
492
+ }
493
+ }) : null, /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
494
+ style: [_ChatViewStyles.styles.composer, showAttach && _ChatViewStyles.styles.composerWithAttach, composerFocused && _ChatViewStyles.styles.composerFocused, composerFocused && {
305
495
  borderColor: (0, _theme.withAlpha)(color, 0.55)
306
496
  }],
307
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TextInput, {
497
+ children: [showAttach ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Pressable, {
498
+ onPress: handleAttachPress,
499
+ disabled: sending || picking,
500
+ accessibilityRole: "button",
501
+ accessibilityLabel: labels.attach.button,
502
+ accessibilityState: {
503
+ disabled: sending || picking
504
+ }
505
+ // 36pt circle + 4pt on every side = a 44pt target.
506
+ ,
507
+ hitSlop: 4,
508
+ style: ({
509
+ pressed
510
+ }) => [_ChatViewStyles.styles.attachButton, (pressed || sending || picking) && _global.globalStyles.pressed],
511
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_icons.PaperclipIcon, {
512
+ size: 20,
513
+ color: _theme.palette.textMuted
514
+ })
515
+ }) : null, /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TextInput, {
308
516
  value: input,
309
517
  onChangeText: handleChangeText,
310
518
  onFocus: () => setComposerFocused(true),
@@ -332,7 +540,21 @@ function ChatView({
332
540
  color: canSend ? (0, _theme.contrastingTextColor)(color) : _theme.palette.textSubtle
333
541
  })
334
542
  })]
335
- })
543
+ })]
544
+ }), showAttach && _reactNative.Platform.OS !== 'ios' ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_AttachmentMenu.AttachmentMenu, {
545
+ visible: menuOpen,
546
+ options: menuOptions,
547
+ cancelLabel: labels.attach.cancel,
548
+ onSelect: handleMenuSelect,
549
+ onClose: () => setMenuOpen(false)
550
+ }) : null, /*#__PURE__*/(0, _jsxRuntime.jsx)(_ImageViewer.ImageViewer, {
551
+ image: viewerImage,
552
+ labels: labels.viewer,
553
+ onClose: () => setViewerImage(null),
554
+ onOpenExternally: url => {
555
+ setViewerImage(null);
556
+ void (0, _safeUrl.openExternalUrl)(url);
557
+ }
336
558
  })]
337
559
  });
338
560
  }
@@ -342,9 +564,29 @@ function MessageBubble({
342
564
  labels,
343
565
  senderLabel,
344
566
  timeLabel,
567
+ receiptLabel,
345
568
  onSubmitProcedureForm,
346
- onAgentUiAction
569
+ onAgentUiAction,
570
+ onOpenImage
347
571
  }) {
572
+ const joinedName = message.metadata?.event === 'agent_joined' ? message.metadata?.agentName : null;
573
+ if (message.messageType === 'system' && typeof joinedName === 'string' && joinedName) {
574
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
575
+ style: [_ChatViewStyles.styles.systemRow, _ChatViewStyles.styles.joinedRow],
576
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_Avatar.Avatar, {
577
+ avatar: typeof message.metadata?.agentAvatar === 'string' ? message.metadata.agentAvatar : null,
578
+ name: joinedName,
579
+ size: 20,
580
+ color: _theme.palette.ink
581
+ }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Text, {
582
+ style: _ChatViewStyles.styles.systemText,
583
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
584
+ style: _ChatViewStyles.styles.joinedName,
585
+ children: joinedName
586
+ }), " ", labels.joinedConversation]
587
+ })]
588
+ });
589
+ }
348
590
  if (message.messageType === 'system') {
349
591
  const text = message.bodyPlain || (0, _html.stripHtml)(message.body || '') || labels.conversationUpdated;
350
592
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
@@ -356,6 +598,18 @@ function MessageBubble({
356
598
  });
357
599
  }
358
600
  const isVisitor = message.direction === 'inbound';
601
+ if (message.deletedAt) {
602
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
603
+ style: [_ChatViewStyles.styles.messageRow, isVisitor ? _ChatViewStyles.styles.messageRowRight : _ChatViewStyles.styles.messageRowLeft],
604
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
605
+ style: _ChatViewStyles.styles.deletedBubble,
606
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
607
+ style: _ChatViewStyles.styles.deletedText,
608
+ children: labels.messageDeleted
609
+ })
610
+ })
611
+ });
612
+ }
359
613
  const rtl = (0, _rtl.isRTL)();
360
614
  const messageText = message.bodyPlain || (0, _html.stripHtml)(message.body || '');
361
615
  const hasBody = !!messageText;
@@ -383,7 +637,8 @@ function MessageBubble({
383
637
  style: [_ChatViewStyles.styles.attachments, hasBody && _ChatViewStyles.styles.attachmentsSpaced],
384
638
  children: message.attachments.map(attachment => /*#__PURE__*/(0, _jsxRuntime.jsx)(AttachmentPreview, {
385
639
  attachment: attachment,
386
- isVisitor: isVisitor
640
+ isVisitor: isVisitor,
641
+ onOpenImage: onOpenImage
387
642
  }, attachment.id))
388
643
  }) : null, message.status === 'failed' ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
389
644
  style: [_ChatViewStyles.styles.failedText, {
@@ -396,7 +651,10 @@ function MessageBubble({
396
651
  }],
397
652
  children: timeLabel
398
653
  }) : null]
399
- }), agentUiBlocks.length > 0 && onAgentUiAction ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_AgentUiBlocks.AgentUiBlocks, {
654
+ }), receiptLabel ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
655
+ style: _ChatViewStyles.styles.receiptText,
656
+ children: receiptLabel
657
+ }) : null, agentUiBlocks.length > 0 && onAgentUiAction ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_AgentUiBlocks.AgentUiBlocks, {
400
658
  blocks: agentUiBlocks,
401
659
  color: color,
402
660
  labels: labels.blocks,
@@ -409,16 +667,32 @@ function MessageBubble({
409
667
  }) : null]
410
668
  });
411
669
  }
670
+
671
+ /** Sent once the server has it, Seen once a teammate opened the conversation. */
672
+ function receiptLabel(message, labels) {
673
+ if (message.deletedAt || message.status === 'pending' || message.status === 'failed') return undefined;
674
+ return message.status === 'read' ? labels.messageSeen : labels.messageSent;
675
+ }
412
676
  function AttachmentPreview({
413
677
  attachment,
414
- isVisitor
678
+ isVisitor,
679
+ onOpenImage
415
680
  }) {
416
681
  // `url` is empty when the server sent a non-http(s) URL (see resolveUrl).
417
- const isImage = attachment.contentType?.startsWith('image/') && !!attachment.url;
418
- const open = () => void (0, _safeUrl.openExternalUrl)(attachment.url);
682
+ // An unsent attachment shows its local file (or data URL): it renders, but
683
+ // there is nothing to open until the server copy arrives.
684
+ const [imageFailed, setImageFailed] = (0, _react.useState)(false);
685
+ const isImage = (0, _pickers.isImageAttachment)(attachment.contentType) && !!attachment.url && !imageFailed;
686
+ const openable = (0, _safeUrl.isHttpUrl)(attachment.url);
687
+ // Images open in the in-app viewer; other files (PDF, sheets) need the OS.
688
+ const open = !openable ? undefined : isImage ? () => onOpenImage({
689
+ url: attachment.url,
690
+ name: attachment.name
691
+ }) : () => void (0, _safeUrl.openExternalUrl)(attachment.url);
419
692
  if (isImage) {
420
693
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Pressable, {
421
694
  onPress: open,
695
+ disabled: !openable,
422
696
  accessibilityRole: "imagebutton",
423
697
  accessibilityLabel: attachment.name,
424
698
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Image, {
@@ -428,11 +702,16 @@ function AttachmentPreview({
428
702
  style: _ChatViewStyles.styles.attachmentImage,
429
703
  resizeMode: "cover",
430
704
  accessibilityIgnoresInvertColors: true
705
+ // An image that can't load (expired link, deleted file) falls back to
706
+ // the file row below instead of an empty box.
707
+ ,
708
+ onError: () => setImageFailed(true)
431
709
  })
432
710
  });
433
711
  }
434
712
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Pressable, {
435
713
  onPress: open,
714
+ disabled: !openable,
436
715
  accessibilityRole: "button",
437
716
  style: [_ChatViewStyles.styles.attachmentFile, {
438
717
  borderColor: isVisitor ? (0, _theme.withAlpha)(_theme.palette.white, 0.35) : _theme.palette.border