@repzo/widget-react-native 1.3.0 → 1.3.1
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/CHANGELOG.md +47 -0
- package/README.md +8 -4
- package/lib/commonjs/api/client.js +40 -0
- package/lib/commonjs/api/client.js.map +1 -1
- package/lib/commonjs/attachments/pickers.js +80 -23
- package/lib/commonjs/attachments/pickers.js.map +1 -1
- package/lib/commonjs/i18n.js +2 -0
- package/lib/commonjs/i18n.js.map +1 -1
- package/lib/commonjs/locales.js +1 -0
- package/lib/commonjs/locales.js.map +1 -1
- package/lib/commonjs/state/widget-store.js +216 -40
- package/lib/commonjs/state/widget-store.js.map +1 -1
- package/lib/commonjs/types.js.map +1 -1
- package/lib/commonjs/ui/WidgetProvider.js +7 -3
- package/lib/commonjs/ui/WidgetProvider.js.map +1 -1
- package/lib/commonjs/ui/components/ImageViewer.js +3 -2
- package/lib/commonjs/ui/components/ImageViewer.js.map +1 -1
- package/lib/commonjs/ui/components/icons.js +22 -16
- package/lib/commonjs/ui/components/icons.js.map +1 -1
- package/lib/commonjs/ui/screens/chat/ChatView.js +36 -14
- package/lib/commonjs/ui/screens/chat/ChatView.js.map +1 -1
- package/lib/commonjs/ui/screens/chat/ChatView.styles.js +12 -2
- package/lib/commonjs/ui/screens/chat/ChatView.styles.js.map +1 -1
- package/lib/commonjs/ui/styles/theme.js +1 -0
- package/lib/commonjs/ui/styles/theme.js.map +1 -1
- package/lib/commonjs/ui/utils/conversations.js +17 -1
- package/lib/commonjs/ui/utils/conversations.js.map +1 -1
- package/lib/module/api/client.js +40 -0
- package/lib/module/api/client.js.map +1 -1
- package/lib/module/attachments/pickers.js +80 -23
- package/lib/module/attachments/pickers.js.map +1 -1
- package/lib/module/i18n.js +2 -0
- package/lib/module/i18n.js.map +1 -1
- package/lib/module/locales.js +1 -0
- package/lib/module/locales.js.map +1 -1
- package/lib/module/state/widget-store.js +216 -40
- package/lib/module/state/widget-store.js.map +1 -1
- package/lib/module/types.js.map +1 -1
- package/lib/module/ui/WidgetProvider.js +7 -3
- package/lib/module/ui/WidgetProvider.js.map +1 -1
- package/lib/module/ui/components/ImageViewer.js +3 -2
- package/lib/module/ui/components/ImageViewer.js.map +1 -1
- package/lib/module/ui/components/icons.js +21 -15
- package/lib/module/ui/components/icons.js.map +1 -1
- package/lib/module/ui/screens/chat/ChatView.js +38 -16
- package/lib/module/ui/screens/chat/ChatView.js.map +1 -1
- package/lib/module/ui/screens/chat/ChatView.styles.js +12 -2
- package/lib/module/ui/screens/chat/ChatView.styles.js.map +1 -1
- package/lib/module/ui/styles/theme.js +1 -0
- package/lib/module/ui/styles/theme.js.map +1 -1
- package/lib/module/ui/utils/conversations.js +15 -1
- package/lib/module/ui/utils/conversations.js.map +1 -1
- package/lib/typescript/commonjs/api/client.d.ts +27 -1
- package/lib/typescript/commonjs/api/client.d.ts.map +1 -1
- package/lib/typescript/commonjs/attachments/pickers.d.ts +22 -10
- package/lib/typescript/commonjs/attachments/pickers.d.ts.map +1 -1
- package/lib/typescript/commonjs/i18n.d.ts +1 -0
- package/lib/typescript/commonjs/i18n.d.ts.map +1 -1
- package/lib/typescript/commonjs/locales.d.ts.map +1 -1
- package/lib/typescript/commonjs/state/widget-store.d.ts +27 -3
- package/lib/typescript/commonjs/state/widget-store.d.ts.map +1 -1
- package/lib/typescript/commonjs/types.d.ts +17 -0
- 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/ImageViewer.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/icons.d.ts +2 -1
- package/lib/typescript/commonjs/ui/components/icons.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/chat/ChatView.d.ts +1 -1
- package/lib/typescript/commonjs/ui/screens/chat/ChatView.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/chat/ChatView.styles.d.ts +10 -2
- 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 +4 -0
- package/lib/typescript/commonjs/ui/screens/chat/ChatView.types.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/styles/theme.d.ts +2 -0
- package/lib/typescript/commonjs/ui/styles/theme.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/utils/conversations.d.ts +10 -2
- package/lib/typescript/commonjs/ui/utils/conversations.d.ts.map +1 -1
- package/lib/typescript/module/api/client.d.ts +27 -1
- package/lib/typescript/module/api/client.d.ts.map +1 -1
- package/lib/typescript/module/attachments/pickers.d.ts +22 -10
- package/lib/typescript/module/attachments/pickers.d.ts.map +1 -1
- package/lib/typescript/module/i18n.d.ts +1 -0
- package/lib/typescript/module/i18n.d.ts.map +1 -1
- package/lib/typescript/module/locales.d.ts.map +1 -1
- package/lib/typescript/module/state/widget-store.d.ts +27 -3
- package/lib/typescript/module/state/widget-store.d.ts.map +1 -1
- package/lib/typescript/module/types.d.ts +17 -0
- 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/ImageViewer.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/icons.d.ts +2 -1
- package/lib/typescript/module/ui/components/icons.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/chat/ChatView.d.ts +1 -1
- package/lib/typescript/module/ui/screens/chat/ChatView.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/chat/ChatView.styles.d.ts +10 -2
- 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 +4 -0
- package/lib/typescript/module/ui/screens/chat/ChatView.types.d.ts.map +1 -1
- package/lib/typescript/module/ui/styles/theme.d.ts +2 -0
- package/lib/typescript/module/ui/styles/theme.d.ts.map +1 -1
- package/lib/typescript/module/ui/utils/conversations.d.ts +10 -2
- package/lib/typescript/module/ui/utils/conversations.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/api/client.ts +66 -2
- package/src/attachments/pickers.ts +87 -25
- package/src/i18n.ts +2 -0
- package/src/locales.ts +1 -0
- package/src/state/widget-store.ts +210 -33
- package/src/types.ts +17 -0
- package/src/ui/WidgetProvider.tsx +15 -4
- package/src/ui/components/ImageViewer.tsx +10 -6
- package/src/ui/components/icons.tsx +10 -12
- package/src/ui/screens/chat/ChatView.styles.ts +12 -2
- package/src/ui/screens/chat/ChatView.tsx +45 -22
- package/src/ui/screens/chat/ChatView.types.ts +4 -0
- package/src/ui/styles/theme.ts +6 -0
- package/src/ui/utils/conversations.ts +16 -2
package/src/types.ts
CHANGED
|
@@ -64,6 +64,12 @@ export interface WidgetConfig {
|
|
|
64
64
|
proactiveFrequency: 'session' | 'visitor';
|
|
65
65
|
proactiveOpenMode: 'home' | 'new_message';
|
|
66
66
|
attachmentsEnabled: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Largest file a visitor may attach. Set by servers that take uploads
|
|
69
|
+
* (`/api/widget/uploads`); absent on older ones, which only take files
|
|
70
|
+
* inline in the message.
|
|
71
|
+
*/
|
|
72
|
+
maxAttachmentBytes?: number;
|
|
67
73
|
analyticsEnabled: boolean;
|
|
68
74
|
toursEnabled: boolean;
|
|
69
75
|
defaultLocale: string;
|
|
@@ -94,7 +100,10 @@ export interface WidgetAttachment {
|
|
|
94
100
|
*/
|
|
95
101
|
export interface OutgoingAttachment {
|
|
96
102
|
name: string;
|
|
103
|
+
/** A base64 data URL; empty when `fileUri` is set. */
|
|
97
104
|
url: string;
|
|
105
|
+
/** A local file uploaded before the message is sent (servers with `maxAttachmentBytes`). */
|
|
106
|
+
fileUri?: string;
|
|
98
107
|
contentType: string;
|
|
99
108
|
/** Decoded bytes. */
|
|
100
109
|
size: number;
|
|
@@ -136,6 +145,14 @@ export interface Message {
|
|
|
136
145
|
createdAt: string;
|
|
137
146
|
/** Set when a teammate deleted the message; the content fields are then empty. */
|
|
138
147
|
deletedAt?: string | null;
|
|
148
|
+
/**
|
|
149
|
+
* On the visitor's own messages: the id this device sent it under, which
|
|
150
|
+
* stays the same when a failed message is retried. Lets a stored message
|
|
151
|
+
* replace its pending or failed bubble. Only newer Workstation deployments
|
|
152
|
+
* send it.
|
|
153
|
+
* @since 1.3.1
|
|
154
|
+
*/
|
|
155
|
+
clientMessageId?: string | null;
|
|
139
156
|
}
|
|
140
157
|
|
|
141
158
|
export interface OutboundMessage {
|
|
@@ -25,11 +25,12 @@ import {
|
|
|
25
25
|
MAX_ATTACHMENT_BYTES,
|
|
26
26
|
MAX_ATTACHMENTS,
|
|
27
27
|
pickAttachments,
|
|
28
|
+
type DraftAttachment,
|
|
28
29
|
} from '../attachments/pickers';
|
|
29
30
|
import { controller } from '../controller';
|
|
30
31
|
import { buildLabels, createTranslator, formatLabel, localizeWelcomeText } from '../i18n';
|
|
31
32
|
import { useWidgetState } from '../state/use-widget-state';
|
|
32
|
-
import type { AgentUiAction, OutgoingAttachment, PostInteractionKind, WidgetSpaceType } from '../types';
|
|
33
|
+
import type { AgentUiAction, AttachmentSource, OutgoingAttachment, PostInteractionKind, WidgetSpaceType } from '../types';
|
|
33
34
|
import { WidgetErrorBoundary } from './components/ErrorBoundary';
|
|
34
35
|
import { AlertIcon } from './components/icons';
|
|
35
36
|
import { InAppNotification } from './components/InAppNotification';
|
|
@@ -163,13 +164,15 @@ export function RepzoWidgetProvider({
|
|
|
163
164
|
file: labels.attachChooseFile,
|
|
164
165
|
cancel: labels.cancel,
|
|
165
166
|
remove: labels.removeAttachment,
|
|
166
|
-
tooLarge: formatLabel(labels.attachmentTooLarge, {
|
|
167
|
+
tooLarge: formatLabel(labels.attachmentTooLarge, {
|
|
168
|
+
max: formatBytes(theme.maxAttachmentBytes ?? MAX_ATTACHMENT_BYTES),
|
|
169
|
+
}),
|
|
167
170
|
tooMany: formatLabel(labels.attachmentTooMany, { count: MAX_ATTACHMENTS }),
|
|
168
171
|
tooLargeTogether: labels.attachmentsTooLargeTogether,
|
|
169
172
|
failed: labels.attachmentFailed,
|
|
170
173
|
cameraDenied: labels.cameraPermissionDenied,
|
|
171
174
|
}),
|
|
172
|
-
[labels]
|
|
175
|
+
[labels, theme.maxAttachmentBytes]
|
|
173
176
|
);
|
|
174
177
|
|
|
175
178
|
// Stable across renders so each message bubble only re-renders when its
|
|
@@ -182,6 +185,7 @@ export function RepzoWidgetProvider({
|
|
|
182
185
|
emptyConversation: labels.emptyConversation,
|
|
183
186
|
messagePlaceholder: labels.messagePlaceholder,
|
|
184
187
|
failedToSend: labels.failedToSend,
|
|
188
|
+
tapToRetry: labels.tapToRetry,
|
|
185
189
|
openAttachment: labels.openAttachment,
|
|
186
190
|
thinking: labels.thinking,
|
|
187
191
|
team: labels.team,
|
|
@@ -238,6 +242,11 @@ export function RepzoWidgetProvider({
|
|
|
238
242
|
const attachmentSources = theme.attachmentsEnabled ? getAttachmentSources() : [];
|
|
239
243
|
|
|
240
244
|
const handleClose = useCallback(() => store.hide(), [store]);
|
|
245
|
+
const handlePickAttachments = useCallback(
|
|
246
|
+
(source: AttachmentSource, current: DraftAttachment[]) =>
|
|
247
|
+
pickAttachments(source, current, theme.maxAttachmentBytes),
|
|
248
|
+
[theme.maxAttachmentBytes]
|
|
249
|
+
);
|
|
241
250
|
|
|
242
251
|
const handleOpenNotification = useCallback(
|
|
243
252
|
(conversationId: string) => void store.openConversation(conversationId),
|
|
@@ -304,6 +313,7 @@ export function RepzoWidgetProvider({
|
|
|
304
313
|
store.sendMessage(body, attachments ?? [], action),
|
|
305
314
|
[store]
|
|
306
315
|
);
|
|
316
|
+
const handleRetryMessage = useCallback((messageId: string) => store.retryMessage(messageId), [store]);
|
|
307
317
|
|
|
308
318
|
const handleIdentifyVisitor = useCallback(
|
|
309
319
|
(data: { email?: string; name?: string; phone?: string }) => store.identifyVisitor(data),
|
|
@@ -442,8 +452,9 @@ export function RepzoWidgetProvider({
|
|
|
442
452
|
offlineMessage={theme.offlineMessage}
|
|
443
453
|
labels={chatLabels}
|
|
444
454
|
attachmentSources={attachmentSources}
|
|
445
|
-
onPickAttachments={
|
|
455
|
+
onPickAttachments={handlePickAttachments}
|
|
446
456
|
onSendMessage={handleSendMessage}
|
|
457
|
+
onRetryMessage={handleRetryMessage}
|
|
447
458
|
onIdentifyVisitor={handleIdentifyVisitor}
|
|
448
459
|
onRateConversation={handleRateConversation}
|
|
449
460
|
onLoadMore={handleLoadMore}
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
|
|
18
18
|
import { useWidgetInsets } from '../hooks/use-widget-insets';
|
|
19
19
|
import { fontSize, palette, spacing } from '../styles/theme';
|
|
20
|
+
import { isHttpUrl } from '../utils/safe-url';
|
|
20
21
|
import { CloseIcon } from './icons';
|
|
21
22
|
import { ZoomableImage } from './ZoomableImage';
|
|
22
23
|
|
|
@@ -68,12 +69,15 @@ export function ImageViewer({ image, labels, onClose, onOpenExternally }: ImageV
|
|
|
68
69
|
{image && state === 'failed' ? (
|
|
69
70
|
<View style={[StyleSheet.absoluteFill, styles.center, styles.failed]}>
|
|
70
71
|
<Text style={styles.failedText}>{labels.loadFailed}</Text>
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
72
|
+
{/* A visitor's local photo has no web copy to open yet. */}
|
|
73
|
+
{isHttpUrl(image.url) ? (
|
|
74
|
+
<Pressable
|
|
75
|
+
onPress={() => onOpenExternally(image.url)}
|
|
76
|
+
accessibilityRole="button"
|
|
77
|
+
style={({ pressed }) => [styles.externalButton, pressed && styles.pressed]}>
|
|
78
|
+
<Text style={styles.externalLabel}>{labels.openExternally}</Text>
|
|
79
|
+
</Pressable>
|
|
80
|
+
) : null}
|
|
77
81
|
</View>
|
|
78
82
|
) : null}
|
|
79
83
|
|
|
@@ -322,20 +322,18 @@ export function AlertIcon({ size = 16, color = palette.warningIcon, style }: Ico
|
|
|
322
322
|
);
|
|
323
323
|
}
|
|
324
324
|
|
|
325
|
-
|
|
326
|
-
|
|
325
|
+
/** Two upright crossed bars — the composer's attach button. */
|
|
326
|
+
export function PlusIcon({ size = 18, color = palette.textMuted, style }: IconProps) {
|
|
327
|
+
const thickness = Math.max(2, size * 0.1);
|
|
328
|
+
const bar: ViewStyle = {
|
|
329
|
+
position: 'absolute',
|
|
330
|
+
borderRadius: thickness / 2,
|
|
331
|
+
backgroundColor: color,
|
|
332
|
+
};
|
|
327
333
|
return (
|
|
328
334
|
<View style={[{ width: size, height: size }, styles.center, style]}>
|
|
329
|
-
<View
|
|
330
|
-
|
|
331
|
-
width: size * 0.42,
|
|
332
|
-
height: size * 0.82,
|
|
333
|
-
borderRadius: size * 0.21,
|
|
334
|
-
borderWidth: thickness,
|
|
335
|
-
borderColor: color,
|
|
336
|
-
transform: [{ rotate: '35deg' }],
|
|
337
|
-
}}
|
|
338
|
-
/>
|
|
335
|
+
<View style={[bar, { width: size * 0.8, height: thickness }]} />
|
|
336
|
+
<View style={[bar, { width: thickness, height: size * 0.8 }]} />
|
|
339
337
|
</View>
|
|
340
338
|
);
|
|
341
339
|
}
|
|
@@ -212,9 +212,19 @@ export const styles = StyleSheet.create({
|
|
|
212
212
|
height: 140,
|
|
213
213
|
borderRadius: radius.md,
|
|
214
214
|
},
|
|
215
|
+
// Follows the bubble's side. Under the visitor's it lines up with
|
|
216
|
+
// `receiptText`, which it replaces on a failed message.
|
|
217
|
+
failedRow: {
|
|
218
|
+
flexDirection: 'row',
|
|
219
|
+
alignItems: 'center',
|
|
220
|
+
gap: 4,
|
|
221
|
+
marginTop: 3,
|
|
222
|
+
marginEnd: spacing.xs,
|
|
223
|
+
},
|
|
215
224
|
failedText: {
|
|
216
|
-
fontSize:
|
|
217
|
-
|
|
225
|
+
fontSize: 11,
|
|
226
|
+
fontWeight: '600',
|
|
227
|
+
color: palette.danger,
|
|
218
228
|
},
|
|
219
229
|
deletedBubble: {
|
|
220
230
|
maxWidth: '80%',
|
|
@@ -49,14 +49,14 @@ import { ImageViewer } from '../../components/ImageViewer';
|
|
|
49
49
|
import { getRatingRequest, RatingCard } from '../../components/RatingCard';
|
|
50
50
|
import { WidgetErrorBoundary } from '../../components/ErrorBoundary';
|
|
51
51
|
import { GradientBackground } from '../../components/Gradient';
|
|
52
|
-
import { ArrowUpIcon, ChatOutlineIcon, ChevronIcon, ClockIcon,
|
|
52
|
+
import { AlertIcon, ArrowUpIcon, ChatOutlineIcon, ChevronIcon, ClockIcon, PlusIcon } from '../../components/icons';
|
|
53
53
|
import { TeamAvatars } from '../../components/TeamAvatars';
|
|
54
54
|
import { Markdown } from '../../components/Markdown';
|
|
55
55
|
import { useReduceMotion } from '../../hooks/use-reduce-motion';
|
|
56
56
|
import { useTopInset } from '../../hooks/use-top-inset';
|
|
57
57
|
import { globalStyles } from '../../styles/global';
|
|
58
58
|
import { contrastingTextColor, mixHex, palette, spacing, withAlpha } from '../../styles/theme';
|
|
59
|
-
import { formatClockTime, formatDayLabel, isSameDay } from '../../utils/conversations';
|
|
59
|
+
import { canRetryMessage, formatClockTime, formatDayLabel, isSameDay } from '../../utils/conversations';
|
|
60
60
|
import { stripHtml } from '../../utils/html';
|
|
61
61
|
import { isHttpUrl, openExternalUrl } from '../../utils/safe-url';
|
|
62
62
|
import { isPlayableAudio } from '../../../audio/voice';
|
|
@@ -87,6 +87,7 @@ export function ChatView({
|
|
|
87
87
|
attachmentSources = [],
|
|
88
88
|
onPickAttachments,
|
|
89
89
|
onSendMessage,
|
|
90
|
+
onRetryMessage,
|
|
90
91
|
onIdentifyVisitor,
|
|
91
92
|
onRateConversation,
|
|
92
93
|
defaultLocale,
|
|
@@ -268,6 +269,7 @@ export function ChatView({
|
|
|
268
269
|
sent.map((draft) => ({
|
|
269
270
|
name: draft.name,
|
|
270
271
|
url: draft.dataUrl,
|
|
272
|
+
fileUri: draft.fileUri,
|
|
271
273
|
contentType: draft.contentType,
|
|
272
274
|
size: draft.size,
|
|
273
275
|
previewUrl: draft.previewUri,
|
|
@@ -325,6 +327,16 @@ export function ChatView({
|
|
|
325
327
|
[onSendMessage]
|
|
326
328
|
);
|
|
327
329
|
|
|
330
|
+
// The retried bubble moves to the end of the thread; follow it there.
|
|
331
|
+
// A second failure shows on the bubble again.
|
|
332
|
+
const handleRetryMessage = useCallback(
|
|
333
|
+
(messageId: string) => {
|
|
334
|
+
onRetryMessage?.(messageId).catch(() => {});
|
|
335
|
+
scrollToBottom();
|
|
336
|
+
},
|
|
337
|
+
[onRetryMessage, scrollToBottom]
|
|
338
|
+
);
|
|
339
|
+
|
|
328
340
|
// Recomputed only when the list changes — never on a keystroke.
|
|
329
341
|
const thread = useMemo(() => deriveThread(messages), [messages]);
|
|
330
342
|
const locales = useMemo(() => [locale, defaultLocale], [locale, defaultLocale]);
|
|
@@ -521,6 +533,7 @@ export function ChatView({
|
|
|
521
533
|
onAgentUiAction={
|
|
522
534
|
thread.agentBlocksShown.has(message.id) ? handleAgentUiAction : undefined
|
|
523
535
|
}
|
|
536
|
+
onRetry={onRetryMessage && canRetryMessage(message) ? handleRetryMessage : undefined}
|
|
524
537
|
/>
|
|
525
538
|
</WidgetErrorBoundary>
|
|
526
539
|
</Fragment>
|
|
@@ -592,7 +605,7 @@ export function ChatView({
|
|
|
592
605
|
styles.attachButton,
|
|
593
606
|
(pressed || sending || picking) && globalStyles.pressed,
|
|
594
607
|
]}>
|
|
595
|
-
<
|
|
608
|
+
<PlusIcon size={20} color={palette.textMuted} />
|
|
596
609
|
</Pressable>
|
|
597
610
|
) : null}
|
|
598
611
|
<TextInput
|
|
@@ -663,6 +676,7 @@ const MessageBubble = memo(function MessageBubble({
|
|
|
663
676
|
onOpenImage,
|
|
664
677
|
onRateConversation,
|
|
665
678
|
locales,
|
|
679
|
+
onRetry,
|
|
666
680
|
}: {
|
|
667
681
|
message: Message;
|
|
668
682
|
color: string;
|
|
@@ -675,6 +689,8 @@ const MessageBubble = memo(function MessageBubble({
|
|
|
675
689
|
onOpenImage: (image: { url: string; name: string }) => void;
|
|
676
690
|
onRateConversation?: ChatViewProps['onRateConversation'];
|
|
677
691
|
locales: Array<string | null | undefined>;
|
|
692
|
+
/** Re-sends this message when it failed. */
|
|
693
|
+
onRetry?: (messageId: string) => void;
|
|
678
694
|
}) {
|
|
679
695
|
const joinedName = message.metadata?.event === 'agent_joined' ? message.metadata?.agentName : null;
|
|
680
696
|
if (message.messageType === 'system' && typeof joinedName === 'string' && joinedName) {
|
|
@@ -772,16 +788,6 @@ const MessageBubble = memo(function MessageBubble({
|
|
|
772
788
|
</View>
|
|
773
789
|
) : null}
|
|
774
790
|
|
|
775
|
-
{message.status === 'failed' ? (
|
|
776
|
-
<Text
|
|
777
|
-
style={[
|
|
778
|
-
styles.failedText,
|
|
779
|
-
{ color: isVisitor ? withAlpha(palette.white, 0.7) : palette.danger },
|
|
780
|
-
]}>
|
|
781
|
-
{labels.failedToSend}
|
|
782
|
-
</Text>
|
|
783
|
-
) : null}
|
|
784
|
-
|
|
785
791
|
{timeLabel ? (
|
|
786
792
|
<Text
|
|
787
793
|
style={[
|
|
@@ -795,6 +801,22 @@ const MessageBubble = memo(function MessageBubble({
|
|
|
795
801
|
|
|
796
802
|
{receiptLabel ? <Text style={styles.receiptText}>{receiptLabel}</Text> : null}
|
|
797
803
|
|
|
804
|
+
{/* Under the bubble, where Sent/Seen would be: in the danger color it
|
|
805
|
+
reads on any brand color, and the whole line is the retry target. */}
|
|
806
|
+
{message.status === 'failed' ? (
|
|
807
|
+
<Pressable
|
|
808
|
+
onPress={onRetry ? () => onRetry(message.id) : undefined}
|
|
809
|
+
disabled={!onRetry}
|
|
810
|
+
accessibilityRole={onRetry ? 'button' : 'text'}
|
|
811
|
+
hitSlop={8}
|
|
812
|
+
style={({ pressed }) => [styles.failedRow, pressed && globalStyles.pressed]}>
|
|
813
|
+
<AlertIcon size={12} color={palette.danger} />
|
|
814
|
+
<Text style={styles.failedText}>
|
|
815
|
+
{onRetry ? `${labels.failedToSend} · ${labels.tapToRetry}` : labels.failedToSend}
|
|
816
|
+
</Text>
|
|
817
|
+
</Pressable>
|
|
818
|
+
) : null}
|
|
819
|
+
|
|
798
820
|
{agentUiBlocks.length > 0 && onAgentUiAction ? (
|
|
799
821
|
<AgentUiBlocks
|
|
800
822
|
blocks={agentUiBlocks}
|
|
@@ -839,18 +861,20 @@ function AttachmentPreview({
|
|
|
839
861
|
onOpenImage: (image: { url: string; name: string }) => void;
|
|
840
862
|
}) {
|
|
841
863
|
// `url` is empty when the server sent a non-http(s) URL (see resolveUrl).
|
|
842
|
-
//
|
|
843
|
-
//
|
|
864
|
+
// The visitor's own attachment points at the picked file until the server
|
|
865
|
+
// copy replaces it (or is empty without one).
|
|
844
866
|
const [imageFailed, setImageFailed] = useState(false);
|
|
845
867
|
const [audioFailed, setAudioFailed] = useState(false);
|
|
846
868
|
const isImage = isImageAttachment(attachment.contentType) && !!attachment.url && !imageFailed;
|
|
847
869
|
const openable = isHttpUrl(attachment.url);
|
|
848
|
-
// Images open in the in-app viewer
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
870
|
+
// Images open in the in-app viewer, a local photo too since it already
|
|
871
|
+
// renders here. Other files (PDF, sheets) need the OS, which can only open
|
|
872
|
+
// the server copy.
|
|
873
|
+
const open = isImage
|
|
874
|
+
? () => onOpenImage({ url: attachment.url, name: attachment.name })
|
|
875
|
+
: openable
|
|
876
|
+
? () => void openExternalUrl(attachment.url)
|
|
877
|
+
: undefined;
|
|
854
878
|
|
|
855
879
|
// Voice notes from the team (and any other audio) play in place, like the
|
|
856
880
|
// web Messenger — when the app has expo-audio; otherwise the file row below.
|
|
@@ -873,7 +897,6 @@ function AttachmentPreview({
|
|
|
873
897
|
return (
|
|
874
898
|
<Pressable
|
|
875
899
|
onPress={open}
|
|
876
|
-
disabled={!openable}
|
|
877
900
|
accessibilityRole="imagebutton"
|
|
878
901
|
accessibilityLabel={attachment.name}>
|
|
879
902
|
<Image
|
|
@@ -21,6 +21,8 @@ export interface ChatLabels {
|
|
|
21
21
|
emptyConversation: string;
|
|
22
22
|
messagePlaceholder: string;
|
|
23
23
|
failedToSend: string;
|
|
24
|
+
/** After `failedToSend` on a bubble that can be re-sent. */
|
|
25
|
+
tapToRetry: string;
|
|
24
26
|
thinking: string;
|
|
25
27
|
team: string;
|
|
26
28
|
statusOnline: string;
|
|
@@ -104,6 +106,8 @@ export interface ChatViewProps {
|
|
|
104
106
|
attachments?: OutgoingAttachment[],
|
|
105
107
|
action?: AgentUiAction
|
|
106
108
|
) => Promise<void>;
|
|
109
|
+
/** Re-sends a failed bubble; rejects when it fails again. Absent: no retry. */
|
|
110
|
+
onRetryMessage?: (messageId: string) => Promise<void>;
|
|
107
111
|
onIdentifyVisitor?: (data: { email?: string; name?: string; phone?: string }) => Promise<void>;
|
|
108
112
|
/** Answers a rating card; rejects with the server error (409 locked, 410 expired). */
|
|
109
113
|
onRateConversation?: (
|
package/src/ui/styles/theme.ts
CHANGED
|
@@ -68,6 +68,8 @@ export interface WidgetTheme {
|
|
|
68
68
|
allowNewConversations: boolean;
|
|
69
69
|
showPreviousConversations: boolean;
|
|
70
70
|
attachmentsEnabled: boolean;
|
|
71
|
+
/** The server's upload cap; null when it only takes files inline in the message. */
|
|
72
|
+
maxAttachmentBytes: number | null;
|
|
71
73
|
preChatEnabled: boolean;
|
|
72
74
|
}
|
|
73
75
|
|
|
@@ -94,6 +96,10 @@ export function buildTheme(config: WidgetConfig | null): WidgetTheme {
|
|
|
94
96
|
allowNewConversations: config?.allowNewConversations ?? true,
|
|
95
97
|
showPreviousConversations: config?.showPreviousConversations ?? true,
|
|
96
98
|
attachmentsEnabled: config?.attachmentsEnabled ?? true,
|
|
99
|
+
maxAttachmentBytes:
|
|
100
|
+
typeof config?.maxAttachmentBytes === 'number' && config.maxAttachmentBytes > 0
|
|
101
|
+
? config.maxAttachmentBytes
|
|
102
|
+
: null,
|
|
97
103
|
preChatEnabled: config?.preChatEnabled ?? false,
|
|
98
104
|
};
|
|
99
105
|
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Conversation selectors and formatting shared by the Home and Messages
|
|
3
|
-
* spaces
|
|
3
|
+
* spaces (and the store and chat thread, for message helpers), so they can
|
|
4
|
+
* never disagree about ordering or wording.
|
|
4
5
|
* Pure and free of React so they stay trivial to test.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import { formatLabel } from '../../i18n';
|
|
8
|
-
import type { Conversation } from '../../types';
|
|
9
|
+
import type { Conversation, Message } from '../../types';
|
|
9
10
|
|
|
10
11
|
export interface TeamAgent {
|
|
11
12
|
name: string | null;
|
|
@@ -120,3 +121,16 @@ export function formatDayLabel(
|
|
|
120
121
|
return date.toDateString();
|
|
121
122
|
}
|
|
122
123
|
}
|
|
124
|
+
|
|
125
|
+
/** Optimistic visitor bubbles the server hasn't acknowledged (pending/failed). */
|
|
126
|
+
export function isLocalMessage(message: Message): boolean {
|
|
127
|
+
return message.id.startsWith('temp-');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The visitor's own bubble that never reached the server. A `failed` status
|
|
132
|
+
* the server sets on any other message isn't something this device can re-send.
|
|
133
|
+
*/
|
|
134
|
+
export function canRetryMessage(message: Message): boolean {
|
|
135
|
+
return message.status === 'failed' && message.direction === 'inbound' && isLocalMessage(message);
|
|
136
|
+
}
|