@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.
- package/README.md +167 -74
- package/lib/commonjs/api/client.js +3 -1
- package/lib/commonjs/api/client.js.map +1 -1
- package/lib/commonjs/api/realtime.js +2 -0
- package/lib/commonjs/api/realtime.js.map +1 -1
- package/lib/commonjs/attachments/pickers.js +493 -0
- package/lib/commonjs/attachments/pickers.js.map +1 -0
- package/lib/commonjs/i18n.js +45 -0
- package/lib/commonjs/i18n.js.map +1 -1
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/locales.js +21 -0
- package/lib/commonjs/locales.js.map +1 -1
- package/lib/commonjs/state/widget-store.js +256 -18
- package/lib/commonjs/state/widget-store.js.map +1 -1
- package/lib/commonjs/types.js +16 -0
- package/lib/commonjs/types.js.map +1 -1
- package/lib/commonjs/ui/WidgetProvider.js +70 -10
- package/lib/commonjs/ui/WidgetProvider.js.map +1 -1
- package/lib/commonjs/ui/components/AttachmentChips.js +135 -0
- package/lib/commonjs/ui/components/AttachmentChips.js.map +1 -0
- package/lib/commonjs/ui/components/AttachmentMenu.js +125 -0
- package/lib/commonjs/ui/components/AttachmentMenu.js.map +1 -0
- package/lib/commonjs/ui/components/ImageViewer.js +181 -0
- package/lib/commonjs/ui/components/ImageViewer.js.map +1 -0
- package/lib/commonjs/ui/components/InAppNotification.js +210 -0
- package/lib/commonjs/ui/components/InAppNotification.js.map +1 -0
- package/lib/commonjs/ui/components/TeamAvatars.js +63 -0
- package/lib/commonjs/ui/components/TeamAvatars.js.map +1 -0
- package/lib/commonjs/ui/components/icons.js +50 -0
- package/lib/commonjs/ui/components/icons.js.map +1 -1
- package/lib/commonjs/ui/screens/chat/ChatView.js +303 -24
- package/lib/commonjs/ui/screens/chat/ChatView.js.map +1 -1
- package/lib/commonjs/ui/screens/chat/ChatView.styles.js +70 -0
- package/lib/commonjs/ui/screens/chat/ChatView.styles.js.map +1 -1
- package/lib/module/api/client.js +3 -1
- package/lib/module/api/client.js.map +1 -1
- package/lib/module/api/realtime.js +2 -0
- package/lib/module/api/realtime.js.map +1 -1
- package/lib/module/attachments/pickers.js +479 -0
- package/lib/module/attachments/pickers.js.map +1 -0
- package/lib/module/i18n.js +44 -0
- package/lib/module/i18n.js.map +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/locales.js +21 -0
- package/lib/module/locales.js.map +1 -1
- package/lib/module/state/widget-store.js +256 -18
- package/lib/module/state/widget-store.js.map +1 -1
- package/lib/module/types.js +16 -0
- package/lib/module/types.js.map +1 -1
- package/lib/module/ui/WidgetProvider.js +71 -11
- package/lib/module/ui/WidgetProvider.js.map +1 -1
- package/lib/module/ui/components/AttachmentChips.js +131 -0
- package/lib/module/ui/components/AttachmentChips.js.map +1 -0
- package/lib/module/ui/components/AttachmentMenu.js +120 -0
- package/lib/module/ui/components/AttachmentMenu.js.map +1 -0
- package/lib/module/ui/components/ImageViewer.js +177 -0
- package/lib/module/ui/components/ImageViewer.js.map +1 -0
- package/lib/module/ui/components/InAppNotification.js +208 -0
- package/lib/module/ui/components/InAppNotification.js.map +1 -0
- package/lib/module/ui/components/TeamAvatars.js +59 -0
- package/lib/module/ui/components/TeamAvatars.js.map +1 -0
- package/lib/module/ui/components/icons.js +49 -0
- package/lib/module/ui/components/icons.js.map +1 -1
- package/lib/module/ui/screens/chat/ChatView.js +306 -27
- package/lib/module/ui/screens/chat/ChatView.js.map +1 -1
- package/lib/module/ui/screens/chat/ChatView.styles.js +70 -0
- package/lib/module/ui/screens/chat/ChatView.styles.js.map +1 -1
- package/lib/typescript/commonjs/api/client.d.ts +8 -2
- package/lib/typescript/commonjs/api/client.d.ts.map +1 -1
- package/lib/typescript/commonjs/api/realtime.d.ts.map +1 -1
- package/lib/typescript/commonjs/attachments/pickers.d.ts +81 -0
- package/lib/typescript/commonjs/attachments/pickers.d.ts.map +1 -0
- package/lib/typescript/commonjs/i18n.d.ts +23 -0
- package/lib/typescript/commonjs/i18n.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +1 -1
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/locales.d.ts.map +1 -1
- package/lib/typescript/commonjs/state/widget-store.d.ts +40 -2
- package/lib/typescript/commonjs/state/widget-store.d.ts.map +1 -1
- package/lib/typescript/commonjs/types.d.ts +88 -1
- package/lib/typescript/commonjs/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/WidgetProvider.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/AttachmentChips.d.ts +16 -0
- package/lib/typescript/commonjs/ui/components/AttachmentChips.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/AttachmentMenu.d.ts +28 -0
- package/lib/typescript/commonjs/ui/components/AttachmentMenu.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/ImageViewer.d.ts +23 -0
- package/lib/typescript/commonjs/ui/components/ImageViewer.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/InAppNotification.d.ts +28 -0
- package/lib/typescript/commonjs/ui/components/InAppNotification.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/TeamAvatars.d.ts +17 -0
- package/lib/typescript/commonjs/ui/components/TeamAvatars.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/icons.d.ts +2 -0
- package/lib/typescript/commonjs/ui/components/icons.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/chat/ChatView.d.ts +3 -2
- package/lib/typescript/commonjs/ui/screens/chat/ChatView.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/chat/ChatView.styles.d.ts +69 -0
- package/lib/typescript/commonjs/ui/screens/chat/ChatView.styles.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/chat/ChatView.types.d.ts +41 -2
- package/lib/typescript/commonjs/ui/screens/chat/ChatView.types.d.ts.map +1 -1
- package/lib/typescript/module/api/client.d.ts +8 -2
- package/lib/typescript/module/api/client.d.ts.map +1 -1
- package/lib/typescript/module/api/realtime.d.ts.map +1 -1
- package/lib/typescript/module/attachments/pickers.d.ts +81 -0
- package/lib/typescript/module/attachments/pickers.d.ts.map +1 -0
- package/lib/typescript/module/i18n.d.ts +23 -0
- package/lib/typescript/module/i18n.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +1 -1
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/locales.d.ts.map +1 -1
- package/lib/typescript/module/state/widget-store.d.ts +40 -2
- package/lib/typescript/module/state/widget-store.d.ts.map +1 -1
- package/lib/typescript/module/types.d.ts +88 -1
- package/lib/typescript/module/types.d.ts.map +1 -1
- package/lib/typescript/module/ui/WidgetProvider.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/AttachmentChips.d.ts +16 -0
- package/lib/typescript/module/ui/components/AttachmentChips.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/AttachmentMenu.d.ts +28 -0
- package/lib/typescript/module/ui/components/AttachmentMenu.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/ImageViewer.d.ts +23 -0
- package/lib/typescript/module/ui/components/ImageViewer.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/InAppNotification.d.ts +28 -0
- package/lib/typescript/module/ui/components/InAppNotification.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/TeamAvatars.d.ts +17 -0
- package/lib/typescript/module/ui/components/TeamAvatars.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/icons.d.ts +2 -0
- package/lib/typescript/module/ui/components/icons.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/chat/ChatView.d.ts +3 -2
- package/lib/typescript/module/ui/screens/chat/ChatView.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/chat/ChatView.styles.d.ts +69 -0
- package/lib/typescript/module/ui/screens/chat/ChatView.styles.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/chat/ChatView.types.d.ts +41 -2
- package/lib/typescript/module/ui/screens/chat/ChatView.types.d.ts.map +1 -1
- package/metro.js +17 -32
- package/package.json +18 -1
- package/src/api/client.ts +5 -3
- package/src/api/realtime.ts +2 -0
- package/src/attachments/pickers.ts +515 -0
- package/src/i18n.ts +48 -0
- package/src/index.tsx +4 -0
- package/src/locales.ts +21 -0
- package/src/state/widget-store.ts +256 -19
- package/src/types.ts +95 -0
- package/src/ui/WidgetProvider.tsx +90 -9
- package/src/ui/components/AttachmentChips.tsx +135 -0
- package/src/ui/components/AttachmentMenu.tsx +132 -0
- package/src/ui/components/ImageViewer.tsx +178 -0
- package/src/ui/components/InAppNotification.tsx +225 -0
- package/src/ui/components/TeamAvatars.tsx +58 -0
- package/src/ui/components/icons.tsx +37 -0
- package/src/ui/screens/chat/ChatView.styles.ts +70 -0
- package/src/ui/screens/chat/ChatView.tsx +345 -35
- 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
|
|
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
|
-
|
|
59
|
-
|
|
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
|
|
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 (!
|
|
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
|
-
}),
|
|
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
|
-
}),
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
}),
|
|
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__*/
|
|
474
|
+
}) : null, /*#__PURE__*/_jsxs(View, {
|
|
298
475
|
style: styles.composerWrapper,
|
|
299
|
-
children: /*#__PURE__*/
|
|
300
|
-
style:
|
|
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(
|
|
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
|
-
}),
|
|
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
|
-
|
|
414
|
-
|
|
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
|