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