@repzo/widget-react-native 1.2.2 → 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 +88 -0
- package/README.md +43 -33
- package/android/build.gradle +22 -0
- package/android/src/main/java/expo/modules/repzowidget/RepzoVoicePlayer.kt +222 -0
- package/android/src/main/java/expo/modules/repzowidget/RepzoWidgetAudioModule.kt +68 -0
- package/expo-module.config.json +9 -0
- package/ios/RepzoVoicePlayer.swift +175 -0
- package/ios/RepzoWidgetAudio.podspec +26 -0
- package/ios/RepzoWidgetAudioModule.swift +65 -0
- 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/audio/native-player.js +97 -0
- package/lib/commonjs/audio/native-player.js.map +1 -0
- package/lib/commonjs/audio/voice.js +34 -7
- package/lib/commonjs/audio/voice.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 +238 -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 +8 -6
- package/lib/commonjs/ui/WidgetProvider.js.map +1 -1
- package/lib/commonjs/ui/components/ImageViewer.js +20 -37
- package/lib/commonjs/ui/components/ImageViewer.js.map +1 -1
- package/lib/commonjs/ui/components/MessagesSkeleton.js +126 -0
- package/lib/commonjs/ui/components/MessagesSkeleton.js.map +1 -0
- package/lib/commonjs/ui/components/VoiceNote.js +8 -9
- package/lib/commonjs/ui/components/VoiceNote.js.map +1 -1
- package/lib/commonjs/ui/components/ZoomableImage.js +299 -0
- package/lib/commonjs/ui/components/ZoomableImage.js.map +1 -0
- 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 +42 -18
- package/lib/commonjs/ui/screens/chat/ChatView.js.map +1 -1
- package/lib/commonjs/ui/screens/chat/ChatView.styles.js +17 -9
- 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/audio/native-player.js +93 -0
- package/lib/module/audio/native-player.js.map +1 -0
- package/lib/module/audio/voice.js +32 -6
- package/lib/module/audio/voice.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 +238 -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 +8 -6
- package/lib/module/ui/WidgetProvider.js.map +1 -1
- package/lib/module/ui/components/ImageViewer.js +21 -38
- package/lib/module/ui/components/ImageViewer.js.map +1 -1
- package/lib/module/ui/components/MessagesSkeleton.js +124 -0
- package/lib/module/ui/components/MessagesSkeleton.js.map +1 -0
- package/lib/module/ui/components/VoiceNote.js +9 -10
- package/lib/module/ui/components/VoiceNote.js.map +1 -1
- package/lib/module/ui/components/ZoomableImage.js +291 -0
- package/lib/module/ui/components/ZoomableImage.js.map +1 -0
- 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 +44 -20
- package/lib/module/ui/screens/chat/ChatView.js.map +1 -1
- package/lib/module/ui/screens/chat/ChatView.styles.js +17 -9
- 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/audio/native-player.d.ts +36 -0
- package/lib/typescript/commonjs/audio/native-player.d.ts.map +1 -0
- package/lib/typescript/commonjs/audio/voice.d.ts +17 -4
- package/lib/typescript/commonjs/audio/voice.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 +38 -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 +2 -2
- package/lib/typescript/commonjs/ui/components/ImageViewer.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/MessagesSkeleton.d.ts +10 -0
- package/lib/typescript/commonjs/ui/components/MessagesSkeleton.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/VoiceNote.d.ts +2 -2
- package/lib/typescript/commonjs/ui/components/VoiceNote.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/ZoomableImage.d.ts +45 -0
- package/lib/typescript/commonjs/ui/components/ZoomableImage.d.ts.map +1 -0
- 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 +14 -9
- 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/audio/native-player.d.ts +36 -0
- package/lib/typescript/module/audio/native-player.d.ts.map +1 -0
- package/lib/typescript/module/audio/voice.d.ts +17 -4
- package/lib/typescript/module/audio/voice.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 +38 -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 +2 -2
- package/lib/typescript/module/ui/components/ImageViewer.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/MessagesSkeleton.d.ts +10 -0
- package/lib/typescript/module/ui/components/MessagesSkeleton.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/VoiceNote.d.ts +2 -2
- package/lib/typescript/module/ui/components/VoiceNote.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/ZoomableImage.d.ts +45 -0
- package/lib/typescript/module/ui/components/ZoomableImage.d.ts.map +1 -0
- 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 +14 -9
- 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 +10 -3
- package/react-native.config.js +12 -0
- package/src/api/client.ts +66 -2
- package/src/attachments/pickers.ts +87 -25
- package/src/audio/native-player.ts +106 -0
- package/src/audio/voice.ts +30 -6
- package/src/i18n.ts +2 -0
- package/src/locales.ts +1 -0
- package/src/state/widget-store.ts +233 -33
- package/src/types.ts +17 -0
- package/src/ui/WidgetProvider.tsx +16 -7
- package/src/ui/components/ImageViewer.tsx +21 -36
- package/src/ui/components/MessagesSkeleton.tsx +92 -0
- package/src/ui/components/VoiceNote.tsx +6 -14
- package/src/ui/components/ZoomableImage.tsx +250 -0
- package/src/ui/components/icons.tsx +10 -12
- package/src/ui/screens/chat/ChatView.styles.ts +17 -9
- package/src/ui/screens/chat/ChatView.tsx +49 -25
- 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/audio/voice.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Voice notes (and any audio file) play inside the chat
|
|
3
|
-
* `
|
|
2
|
+
* Voice notes (and any audio file) play inside the chat with the SDK's own
|
|
3
|
+
* native player (`src/audio/native-player.ts`). Where that isn't built into
|
|
4
|
+
* the app (Expo Go, bare React Native, an app built with SDK 1.2.2 or older),
|
|
5
|
+
* they play with the host app's `expo-audio` if it has one; otherwise they
|
|
6
|
+
* stay a file row that opens in the browser.
|
|
4
7
|
*
|
|
5
8
|
* Resolved once at module load, like the attachment pickers: each `require` is
|
|
6
9
|
* static so Metro can see it, and with `allowOptionalDependencies` (see
|
|
@@ -8,6 +11,8 @@
|
|
|
8
11
|
* `try` around it catches.
|
|
9
12
|
*/
|
|
10
13
|
|
|
14
|
+
import { hasNativeVoicePlayer, useNativeVoicePlayer } from './native-player';
|
|
15
|
+
|
|
11
16
|
let expoAudio: any = null;
|
|
12
17
|
try {
|
|
13
18
|
const mod = require('expo-audio');
|
|
@@ -31,9 +36,11 @@ try {
|
|
|
31
36
|
/** How often the playhead moves while a note plays. */
|
|
32
37
|
const UPDATE_INTERVAL_MS = 100;
|
|
33
38
|
|
|
34
|
-
/**
|
|
35
|
-
|
|
36
|
-
|
|
39
|
+
/** Which player voice notes use in this app, or null when audio stays a file row. */
|
|
40
|
+
const backend: 'native' | 'expo-audio' | null = hasNativeVoicePlayer() ? 'native' : expoAudio ? 'expo-audio' : null;
|
|
41
|
+
|
|
42
|
+
export function voicePlaybackBackend(): 'native' | 'expo-audio' | null {
|
|
43
|
+
return backend;
|
|
37
44
|
}
|
|
38
45
|
|
|
39
46
|
/** The second argument to `useAudioPlayer` for the installed expo-audio. */
|
|
@@ -41,9 +48,26 @@ export function audioPlayerOptions(): unknown {
|
|
|
41
48
|
return takesOptionsObject ? { updateInterval: UPDATE_INTERVAL_MS } : UPDATE_INTERVAL_MS;
|
|
42
49
|
}
|
|
43
50
|
|
|
51
|
+
/** expo-audio's player for a note, on the same terms as the native one. */
|
|
52
|
+
function useExpoAudioPlayer(url: string): { player: any; status: any } {
|
|
53
|
+
const player = expoAudio.useAudioPlayer({ uri: url }, audioPlayerOptions());
|
|
54
|
+
const status = expoAudio.useAudioPlayerStatus(player);
|
|
55
|
+
return { player, status };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The player for one note: `play()`, `pause()`, `seekTo(seconds)`,
|
|
60
|
+
* `setPlaybackRate(rate)`, and its status (`currentTime`, `duration`,
|
|
61
|
+
* `playing`, `isLoaded`, `isBuffering`, `didJustFinish`, `error`). Only use it
|
|
62
|
+
* when `voicePlaybackBackend()` isn't null. Picked once, since the backend
|
|
63
|
+
* never changes while the app runs.
|
|
64
|
+
*/
|
|
65
|
+
export const useVoicePlayer: (url: string) => { player: any; status: any } =
|
|
66
|
+
backend === 'native' ? useNativeVoicePlayer : useExpoAudioPlayer;
|
|
67
|
+
|
|
44
68
|
/** Audio the chat can play in place: an `audio/*` attachment with a web address. */
|
|
45
69
|
export function isPlayableAudio(contentType: string | null | undefined, url: string | null | undefined): boolean {
|
|
46
|
-
return
|
|
70
|
+
return backend != null && (contentType ?? '').toLowerCase().startsWith('audio/') && /^https?:\/\//i.test(url ?? '');
|
|
47
71
|
}
|
|
48
72
|
|
|
49
73
|
// ─── One note at a time ─────────────────────────────────────────────────────
|
package/src/i18n.ts
CHANGED
|
@@ -82,6 +82,8 @@ export const DEFAULT_LABELS = {
|
|
|
82
82
|
cameraPermissionDenied: 'Allow camera access in Settings to take a photo',
|
|
83
83
|
cancel: 'Cancel',
|
|
84
84
|
failedToSend: 'Failed to send',
|
|
85
|
+
// After "Failed to send" under a message the visitor can send again.
|
|
86
|
+
tapToRetry: 'Tap to retry',
|
|
85
87
|
thinking: 'Typing',
|
|
86
88
|
messages: 'Messages',
|
|
87
89
|
noMessages: 'No messages yet',
|
package/src/locales.ts
CHANGED
|
@@ -38,6 +38,7 @@ const ar: LabelMap = {
|
|
|
38
38
|
cameraPermissionDenied: 'اسمح بالوصول إلى الكاميرا من الإعدادات لالتقاط صورة',
|
|
39
39
|
cancel: 'إلغاء',
|
|
40
40
|
failedToSend: 'تعذر الإرسال',
|
|
41
|
+
tapToRetry: 'اضغط لإعادة المحاولة',
|
|
41
42
|
thinking: 'يكتب',
|
|
42
43
|
messages: 'الرسائل',
|
|
43
44
|
noMessages: 'لا توجد رسائل بعد',
|
|
@@ -37,10 +37,11 @@ import type {
|
|
|
37
37
|
WidgetUser,
|
|
38
38
|
WidgetView,
|
|
39
39
|
} from '../types';
|
|
40
|
+
import { canRetryMessage, isLocalMessage } from '../ui/utils/conversations';
|
|
40
41
|
import { setOpenUrlHandler } from '../ui/utils/safe-url';
|
|
41
42
|
|
|
42
43
|
/** Sent on every init; must match `package.json` (tests/version.test.js checks). */
|
|
43
|
-
const SDK_VERSION = '@repzo/widget-react-native@1.
|
|
44
|
+
const SDK_VERSION = '@repzo/widget-react-native@1.3.1';
|
|
44
45
|
|
|
45
46
|
/** The platform this session reports — mobile sessions get the Mobile SDKs overrides. */
|
|
46
47
|
function sessionPlatform(): 'ios' | 'android' | 'web' {
|
|
@@ -76,6 +77,8 @@ const MARK_READ_DEBOUNCE_MS = 1_000;
|
|
|
76
77
|
const FOREGROUND_REFRESH_AFTER_MS = 5 * 60_000;
|
|
77
78
|
/** Live events held back while a draft's first send is creating its thread. */
|
|
78
79
|
const DRAFT_EVENT_BUFFER_LIMIT = 50;
|
|
80
|
+
/** Waits between re-reads for a sent file's stored copy while Workstation is still storing it. */
|
|
81
|
+
const SERVER_COPY_RETRY_MS = [2_000, 5_000, 15_000];
|
|
79
82
|
/** How long a post stays up after a reaction, thanking them, before it goes. */
|
|
80
83
|
export const POST_THANKS_MS = 1600;
|
|
81
84
|
|
|
@@ -216,6 +219,13 @@ export class WidgetStore {
|
|
|
216
219
|
|
|
217
220
|
private pendingConversationId: string | null = null;
|
|
218
221
|
|
|
222
|
+
/**
|
|
223
|
+
* The screen a thread was opened from, so its back button returns there.
|
|
224
|
+
* Null when it was opened with the messenger closed (a push, a notification
|
|
225
|
+
* card, `presentConversation`): there is nothing to go back to.
|
|
226
|
+
*/
|
|
227
|
+
private chatOpenedFrom: 'home' | 'conversations' | null = null;
|
|
228
|
+
|
|
219
229
|
private appStateSubscription: { remove(): void } | null = null;
|
|
220
230
|
|
|
221
231
|
/** When the app last went to the background (0: hasn't this session). */
|
|
@@ -228,6 +238,12 @@ export class WidgetStore {
|
|
|
228
238
|
private seenMessageIds: string[] = [];
|
|
229
239
|
/** Statuses (e.g. "read") that arrived for an id before a send response revealed it. */
|
|
230
240
|
private earlyStatuses = new Map<string, string>();
|
|
241
|
+
/** The agent UI action behind a failed bubble, so Retry runs it again too. */
|
|
242
|
+
private failedActions = new Map<string, AgentUiAction>();
|
|
243
|
+
/** The next re-read for a sent file's stored copy (see `syncServerCopies`). */
|
|
244
|
+
private serverCopyTimer: ReturnType<typeof setTimeout> | null = null;
|
|
245
|
+
private serverCopyRetries = 0;
|
|
246
|
+
private serverCopySyncing = false;
|
|
231
247
|
|
|
232
248
|
/**
|
|
233
249
|
* Identity generation. Bumped whenever boot/login/logout is requested; every
|
|
@@ -321,6 +337,14 @@ export class WidgetStore {
|
|
|
321
337
|
}
|
|
322
338
|
this.state = next;
|
|
323
339
|
|
|
340
|
+
// Recorded here because a thread opens from many places (home cards, the
|
|
341
|
+
// list, the Messages tab, posts) and each would otherwise have to say so.
|
|
342
|
+
const intoThread = next.view === 'chat' || next.view === 'prechat';
|
|
343
|
+
if (intoThread && previous.view !== 'chat' && previous.view !== 'prechat') {
|
|
344
|
+
this.chatOpenedFrom =
|
|
345
|
+
previous.isOpen && (previous.view === 'home' || previous.view === 'conversations') ? previous.view : null;
|
|
346
|
+
}
|
|
347
|
+
|
|
324
348
|
if (next.unreadCount !== previous.unreadCount) {
|
|
325
349
|
this.events.emit(RepzoWidgetEvents.UnreadCountDidChange, { count: next.unreadCount });
|
|
326
350
|
}
|
|
@@ -875,7 +899,9 @@ export class WidgetStore {
|
|
|
875
899
|
this.threadGeneration += 1;
|
|
876
900
|
this.conversationCreation = null;
|
|
877
901
|
this.draftEvents = [];
|
|
902
|
+
this.failedActions.clear();
|
|
878
903
|
this.cancelMarkRead();
|
|
904
|
+
this.cancelServerCopySync();
|
|
879
905
|
}
|
|
880
906
|
|
|
881
907
|
private requireBootConfig() {
|
|
@@ -1229,6 +1255,7 @@ export class WidgetStore {
|
|
|
1229
1255
|
metadata: this.hydrateMetadata(data?.metadata || null),
|
|
1230
1256
|
status: 'delivered',
|
|
1231
1257
|
createdAt: data?.createdAt || new Date().toISOString(),
|
|
1258
|
+
...(typeof data?.clientMessageId === 'string' ? { clientMessageId: data.clientMessageId } : {}),
|
|
1232
1259
|
};
|
|
1233
1260
|
|
|
1234
1261
|
const clearsTyping = message.direction === 'outbound' && message.senderType !== 'visitor';
|
|
@@ -1276,12 +1303,13 @@ export class WidgetStore {
|
|
|
1276
1303
|
: current.conversations;
|
|
1277
1304
|
|
|
1278
1305
|
if (onScreen) {
|
|
1279
|
-
const
|
|
1306
|
+
const shown = adoptOwnServerRows(current.messages, [message]);
|
|
1307
|
+
const alreadyPresent = shown.some((existing) => existing.id === message.id);
|
|
1280
1308
|
return {
|
|
1281
1309
|
conversations,
|
|
1282
1310
|
messages: alreadyPresent
|
|
1283
|
-
? adoptServerAttachments(
|
|
1284
|
-
: [...
|
|
1311
|
+
? adoptServerAttachments(shown, [message])
|
|
1312
|
+
: [...shown, message],
|
|
1285
1313
|
agentTyping: clearsTyping ? false : current.agentTyping,
|
|
1286
1314
|
};
|
|
1287
1315
|
}
|
|
@@ -1524,6 +1552,14 @@ export class WidgetStore {
|
|
|
1524
1552
|
});
|
|
1525
1553
|
}
|
|
1526
1554
|
|
|
1555
|
+
/**
|
|
1556
|
+
* The thread's back button: the screen it was opened from, or the
|
|
1557
|
+
* conversation list when it was opened from outside the messenger.
|
|
1558
|
+
*/
|
|
1559
|
+
leaveConversation(): void {
|
|
1560
|
+
this.setView(this.chatOpenedFrom ?? (this.state.activeTab === 'messages' ? 'conversations' : 'home'));
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1527
1563
|
setActiveTab(tab: WidgetSpaceType): void {
|
|
1528
1564
|
if (tab === 'home') {
|
|
1529
1565
|
this.setState({ activeTab: 'home', view: 'home' });
|
|
@@ -1674,11 +1710,12 @@ export class WidgetStore {
|
|
|
1674
1710
|
this.setState((current) => {
|
|
1675
1711
|
// Live replies and optimistic sends may have arrived since this page
|
|
1676
1712
|
// was requested. Keep them, preferring the live copy for shared ids.
|
|
1677
|
-
const liveIds = new Set(current.messages.map((message) => message.id));
|
|
1678
1713
|
const page = this.hydrateMessages(result.data);
|
|
1714
|
+
const live = adoptOwnServerRows(current.messages, page);
|
|
1715
|
+
const liveIds = new Set(live.map((message) => message.id));
|
|
1679
1716
|
const history = page.filter((message) => !liveIds.has(message.id));
|
|
1680
1717
|
return {
|
|
1681
|
-
messages: sortByCreatedAt([...history, ...adoptServerAttachments(
|
|
1718
|
+
messages: sortByCreatedAt([...history, ...adoptServerAttachments(live, page)]),
|
|
1682
1719
|
messagesLoading: false,
|
|
1683
1720
|
hasMoreMessages: result.meta.hasMore,
|
|
1684
1721
|
messageCursor: result.meta.nextCursor,
|
|
@@ -1742,16 +1779,17 @@ export class WidgetStore {
|
|
|
1742
1779
|
|
|
1743
1780
|
const fresh = this.hydrateMessages(result.data);
|
|
1744
1781
|
this.setState((current) => {
|
|
1745
|
-
const
|
|
1782
|
+
const own = adoptOwnServerRows(current.messages, fresh);
|
|
1783
|
+
const known = new Set(own.map((message) => message.id));
|
|
1746
1784
|
const missing = fresh.filter((message) => !known.has(message.id));
|
|
1747
1785
|
const upgraded = adoptServerStatuses(
|
|
1748
|
-
adoptServerDeletions(adoptServerAttachments(
|
|
1786
|
+
adoptServerDeletions(adoptServerAttachments(own, fresh), fresh),
|
|
1749
1787
|
fresh
|
|
1750
1788
|
);
|
|
1751
1789
|
if (missing.length === 0) return upgraded === current.messages ? {} : { messages: upgraded };
|
|
1752
1790
|
|
|
1753
|
-
const local =
|
|
1754
|
-
const hasServerMessages = local.length <
|
|
1791
|
+
const local = own.filter(isLocalMessage);
|
|
1792
|
+
const hasServerMessages = local.length < own.length;
|
|
1755
1793
|
// No overlap means the gap is longer than a page: restart from the
|
|
1756
1794
|
// newest page (keeping unsent bubbles) rather than leave a hole.
|
|
1757
1795
|
if (missing.length === fresh.length || !hasServerMessages) {
|
|
@@ -1772,6 +1810,42 @@ export class WidgetStore {
|
|
|
1772
1810
|
}
|
|
1773
1811
|
}
|
|
1774
1812
|
|
|
1813
|
+
/**
|
|
1814
|
+
* Until its stored copy replaces it, a sent bubble points at the visitor's
|
|
1815
|
+
* local files: a photo still opens in the viewer, but a PDF has nothing to
|
|
1816
|
+
* open. When the stream's echo hasn't brought that copy, re-read the
|
|
1817
|
+
* thread, and try again a few times while Workstation is still storing it.
|
|
1818
|
+
*/
|
|
1819
|
+
private async syncServerCopies(): Promise<void> {
|
|
1820
|
+
this.cancelServerCopySync();
|
|
1821
|
+
if (this.serverCopySyncing || !this.state.messages.some(isAwaitingServerCopy)) return;
|
|
1822
|
+
// A send still in flight would come back as a stored row beside its own
|
|
1823
|
+
// pending bubble. It syncs again when it settles.
|
|
1824
|
+
if (this.state.messages.some((message) => isLocalMessage(message) && message.status === 'pending')) return;
|
|
1825
|
+
// Off screen: the thread reloads in full when it's opened again.
|
|
1826
|
+
const conversationId = this.state.activeConversationId;
|
|
1827
|
+
if (!conversationId || !this.isThreadOnScreen(conversationId)) return;
|
|
1828
|
+
|
|
1829
|
+
const generation = this.threadGeneration;
|
|
1830
|
+
this.serverCopySyncing = true;
|
|
1831
|
+
try {
|
|
1832
|
+
// A no-op while a page load is in flight; the retry below covers it.
|
|
1833
|
+
await this.refreshOpenThread();
|
|
1834
|
+
} finally {
|
|
1835
|
+
this.serverCopySyncing = false;
|
|
1836
|
+
}
|
|
1837
|
+
if (this.threadGeneration !== generation || !this.state.messages.some(isAwaitingServerCopy)) return;
|
|
1838
|
+
const delay = SERVER_COPY_RETRY_MS[this.serverCopyRetries];
|
|
1839
|
+
if (delay === undefined) return;
|
|
1840
|
+
this.serverCopyRetries += 1;
|
|
1841
|
+
this.serverCopyTimer = setTimeout(() => void this.syncServerCopies(), delay);
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
private cancelServerCopySync(): void {
|
|
1845
|
+
if (this.serverCopyTimer) clearTimeout(this.serverCopyTimer);
|
|
1846
|
+
this.serverCopyTimer = null;
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1775
1849
|
/** A history page (newest-first) as the oldest-first list the UI renders. */
|
|
1776
1850
|
private hydrateMessages(page: Message[]): Message[] {
|
|
1777
1851
|
return page
|
|
@@ -1816,9 +1890,11 @@ export class WidgetStore {
|
|
|
1816
1890
|
|
|
1817
1891
|
/**
|
|
1818
1892
|
* Rejects when the message wasn't sent, so the composer can keep
|
|
1819
|
-
* attachments for a retry. A text-only bubble is marked failed
|
|
1820
|
-
* attachments is removed instead — the
|
|
1821
|
-
* keeping both would leave a duplicate
|
|
1893
|
+
* attachments for a retry. A text-only bubble is marked failed and can be
|
|
1894
|
+
* re-sent with `retryMessage`; one with attachments is removed instead — the
|
|
1895
|
+
* composer's chips are the retry, and keeping both would leave a duplicate
|
|
1896
|
+
* after a successful resend. Likewise an agent UI action replaces the
|
|
1897
|
+
* failed bubble of the same action, since its card stays tappable.
|
|
1822
1898
|
*/
|
|
1823
1899
|
async sendMessage(
|
|
1824
1900
|
body: string,
|
|
@@ -1829,7 +1905,6 @@ export class WidgetStore {
|
|
|
1829
1905
|
if (!trimmed && attachments.length === 0) return;
|
|
1830
1906
|
|
|
1831
1907
|
const { visitorProfile } = this.state;
|
|
1832
|
-
const generation = this.threadGeneration;
|
|
1833
1908
|
const tempId = `temp-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1834
1909
|
const optimistic: Message = {
|
|
1835
1910
|
id: tempId,
|
|
@@ -1860,8 +1935,17 @@ export class WidgetStore {
|
|
|
1860
1935
|
createdAt: new Date().toISOString(),
|
|
1861
1936
|
};
|
|
1862
1937
|
|
|
1938
|
+
// Tapping the card again is the retry: offering the old bubble's retry
|
|
1939
|
+
// too would run the action twice (two items in the cart, two bookings).
|
|
1940
|
+
const superseded = agentUiAction
|
|
1941
|
+
? [...this.failedActions]
|
|
1942
|
+
.filter(([, failed]) => failed.id === agentUiAction.id && failed.type === agentUiAction.type)
|
|
1943
|
+
.map(([messageId]) => messageId)
|
|
1944
|
+
: [];
|
|
1945
|
+
superseded.forEach((messageId) => this.failedActions.delete(messageId));
|
|
1946
|
+
|
|
1863
1947
|
this.setState((current) => ({
|
|
1864
|
-
messages: [...current.messages, optimistic],
|
|
1948
|
+
messages: [...current.messages.filter((message) => !superseded.includes(message.id)), optimistic],
|
|
1865
1949
|
prefillText: '',
|
|
1866
1950
|
}));
|
|
1867
1951
|
this.track('widget_message_sent', {
|
|
@@ -1869,7 +1953,40 @@ export class WidgetStore {
|
|
|
1869
1953
|
attachmentCount: attachments.length,
|
|
1870
1954
|
});
|
|
1871
1955
|
|
|
1872
|
-
|
|
1956
|
+
return this.deliverMessage(tempId, trimmed, attachments, agentUiAction);
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
/**
|
|
1960
|
+
* Re-sends a text bubble that failed. It moves to the end of the thread,
|
|
1961
|
+
* since it now goes out after anything sent meanwhile. Rejects like
|
|
1962
|
+
* `sendMessage` when it fails again; the bubble is then failed once more.
|
|
1963
|
+
*/
|
|
1964
|
+
async retryMessage(messageId: string): Promise<void> {
|
|
1965
|
+
const failed = this.state.messages.find(
|
|
1966
|
+
(message) => message.id === messageId && canRetryMessage(message)
|
|
1967
|
+
);
|
|
1968
|
+
if (!failed) return;
|
|
1969
|
+
const agentUiAction = this.failedActions.get(messageId);
|
|
1970
|
+
this.failedActions.delete(messageId);
|
|
1971
|
+
const pending: Message = { ...failed, status: 'pending', createdAt: new Date().toISOString() };
|
|
1972
|
+
this.setState((current) => ({
|
|
1973
|
+
messages: [...current.messages.filter((message) => message.id !== messageId), pending],
|
|
1974
|
+
}));
|
|
1975
|
+
return this.deliverMessage(messageId, failed.bodyPlain ?? '', [], agentUiAction);
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
/** Sends the optimistic bubble `tempId`, already on screen as pending. */
|
|
1979
|
+
private async deliverMessage(
|
|
1980
|
+
tempId: string,
|
|
1981
|
+
trimmed: string,
|
|
1982
|
+
attachments: OutgoingAttachment[],
|
|
1983
|
+
agentUiAction: AgentUiAction | undefined
|
|
1984
|
+
): Promise<void> {
|
|
1985
|
+
const { visitorProfile } = this.state;
|
|
1986
|
+
const generation = this.threadGeneration;
|
|
1987
|
+
|
|
1988
|
+
const markFailed = () => {
|
|
1989
|
+
if (agentUiAction && attachments.length === 0) this.failedActions.set(tempId, agentUiAction);
|
|
1873
1990
|
this.setState((current) => ({
|
|
1874
1991
|
messages:
|
|
1875
1992
|
attachments.length > 0
|
|
@@ -1878,6 +1995,7 @@ export class WidgetStore {
|
|
|
1878
1995
|
message.id === tempId ? { ...message, status: 'failed' } : message
|
|
1879
1996
|
),
|
|
1880
1997
|
}));
|
|
1998
|
+
};
|
|
1881
1999
|
|
|
1882
2000
|
const epoch = this.epoch;
|
|
1883
2001
|
|
|
@@ -1905,22 +2023,31 @@ export class WidgetStore {
|
|
|
1905
2023
|
|
|
1906
2024
|
let createdId: string | null = null;
|
|
1907
2025
|
try {
|
|
2026
|
+
// Local files go up first; the message names each by its upload token.
|
|
2027
|
+
const sentAttachments = await Promise.all(
|
|
2028
|
+
attachments.map(async ({ name, url, fileUri, contentType, size }) =>
|
|
2029
|
+
fileUri
|
|
2030
|
+
? {
|
|
2031
|
+
name,
|
|
2032
|
+
contentType,
|
|
2033
|
+
size,
|
|
2034
|
+
uploadToken: await this.client.uploadAttachment({ uri: fileUri, name, contentType, size }),
|
|
2035
|
+
}
|
|
2036
|
+
: { name, url, contentType, size }
|
|
2037
|
+
)
|
|
2038
|
+
);
|
|
2039
|
+
if (this.isStale(epoch)) return;
|
|
1908
2040
|
const result = await this.client.sendMessage({
|
|
1909
2041
|
conversationId: conversationId || undefined,
|
|
1910
2042
|
forceNewConversation: !conversationId,
|
|
1911
2043
|
body: trimmed,
|
|
1912
|
-
attachments:
|
|
1913
|
-
attachments.length > 0
|
|
1914
|
-
? attachments.map(({ name, url, contentType, size }) => ({
|
|
1915
|
-
name,
|
|
1916
|
-
url,
|
|
1917
|
-
contentType,
|
|
1918
|
-
size,
|
|
1919
|
-
}))
|
|
1920
|
-
: undefined,
|
|
2044
|
+
attachments: sentAttachments.length > 0 ? sentAttachments : undefined,
|
|
1921
2045
|
visitorName: visitorProfile.name,
|
|
1922
2046
|
visitorEmail: visitorProfile.email,
|
|
1923
2047
|
agentUiAction,
|
|
2048
|
+
// The bubble's id, the same on every retry: a newer Workstation stores
|
|
2049
|
+
// the message once even when an earlier attempt's response was lost.
|
|
2050
|
+
clientMessageId: tempId,
|
|
1924
2051
|
});
|
|
1925
2052
|
if (this.isStale(epoch)) return;
|
|
1926
2053
|
if (ownCreation) createdId = result.conversationId;
|
|
@@ -1965,8 +2092,15 @@ export class WidgetStore {
|
|
|
1965
2092
|
: {}),
|
|
1966
2093
|
};
|
|
1967
2094
|
});
|
|
2095
|
+
|
|
2096
|
+
// New files to fetch the stored copy of: a fresh round of re-reads.
|
|
2097
|
+
const sent = this.state.messages.find((message) => message.id === result.messageId);
|
|
2098
|
+
if (sent && hasLocalAttachments(sent)) this.serverCopyRetries = 0;
|
|
1968
2099
|
} catch (error) {
|
|
1969
2100
|
if (this.isStale(epoch)) throw error;
|
|
2101
|
+
// The response was lost, but its stored row has already taken the
|
|
2102
|
+
// bubble's place (newer Workstation deployments): it was sent.
|
|
2103
|
+
if (this.state.messages.some((message) => message.clientMessageId === tempId)) return;
|
|
1970
2104
|
markFailed();
|
|
1971
2105
|
throw error;
|
|
1972
2106
|
} finally {
|
|
@@ -1977,6 +2111,8 @@ export class WidgetStore {
|
|
|
1977
2111
|
// Anything held back for the draft now has a home (or goes to unread).
|
|
1978
2112
|
this.replayDraftEvents();
|
|
1979
2113
|
}
|
|
2114
|
+
// Waits for every send to settle, so this runs when the last one does.
|
|
2115
|
+
if (!this.isStale(epoch)) void this.syncServerCopies();
|
|
1980
2116
|
}
|
|
1981
2117
|
}
|
|
1982
2118
|
|
|
@@ -2439,11 +2575,6 @@ function createConversationCreation(generation: number): ConversationCreation {
|
|
|
2439
2575
|
return { generation, promise, resolve };
|
|
2440
2576
|
}
|
|
2441
2577
|
|
|
2442
|
-
/** Optimistic visitor bubbles the server hasn't acknowledged (pending/failed). */
|
|
2443
|
-
function isLocalMessage(message: Message): boolean {
|
|
2444
|
-
return message.id.startsWith('temp-');
|
|
2445
|
-
}
|
|
2446
|
-
|
|
2447
2578
|
/** A "Seen" missed while the stream was down shows up on the next re-read. */
|
|
2448
2579
|
function adoptServerStatuses(messages: Message[], server: Message[]): Message[] {
|
|
2449
2580
|
const seen = new Set(server.filter((message) => message.status === 'read').map((message) => message.id));
|
|
@@ -2491,14 +2622,83 @@ function adoptServerAttachments(messages: Message[], server: Message[]): Message
|
|
|
2491
2622
|
let changed = false;
|
|
2492
2623
|
const next = messages.map((message) => {
|
|
2493
2624
|
const copy = serverCopies.get(message.id);
|
|
2494
|
-
|
|
2495
|
-
|
|
2625
|
+
if (!copy || !hasLocalAttachments(message)) return message;
|
|
2626
|
+
const attachments = mergeAttachments(message.attachments!, copy.attachments!);
|
|
2627
|
+
if (attachments === message.attachments) return message;
|
|
2496
2628
|
changed = true;
|
|
2497
|
-
return { ...message, attachments
|
|
2629
|
+
return { ...message, attachments };
|
|
2498
2630
|
});
|
|
2499
2631
|
return changed ? next : messages;
|
|
2500
2632
|
}
|
|
2501
2633
|
|
|
2634
|
+
/**
|
|
2635
|
+
* Each attachment's server copy, except one without a usable URL: that would
|
|
2636
|
+
* only blank its local preview, so the preview stays until a later copy has it.
|
|
2637
|
+
* Returns `local` itself when nothing changes.
|
|
2638
|
+
*/
|
|
2639
|
+
function mergeAttachments(local: WidgetAttachment[], server: WidgetAttachment[]): WidgetAttachment[] {
|
|
2640
|
+
// Without a one-to-one match, only a fully usable server copy is safe to take.
|
|
2641
|
+
if (local.length !== server.length) {
|
|
2642
|
+
return server.every((attachment) => attachment.url) ? server : local;
|
|
2643
|
+
}
|
|
2644
|
+
const merged = server.map((attachment, index) => {
|
|
2645
|
+
const own = local[index]!;
|
|
2646
|
+
return isLocalAttachment(own) && attachment.url ? attachment : own;
|
|
2647
|
+
});
|
|
2648
|
+
return merged.every((attachment, index) => attachment === local[index]) ? local : merged;
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
/**
|
|
2652
|
+
* Newer Workstation deployments echo `clientMessageId` on the visitor's own
|
|
2653
|
+
* rows. Such a row replaces the pending or failed bubble it came from instead
|
|
2654
|
+
* of showing beside it: no duplicate while a send is in flight, and no retry
|
|
2655
|
+
* for a message that was stored although its response was lost.
|
|
2656
|
+
*/
|
|
2657
|
+
function adoptOwnServerRows(messages: Message[], server: Message[]): Message[] {
|
|
2658
|
+
const rows = new Map<string, Message>();
|
|
2659
|
+
server.forEach((row) => {
|
|
2660
|
+
if (typeof row.clientMessageId === 'string' && row.clientMessageId) rows.set(row.clientMessageId, row);
|
|
2661
|
+
});
|
|
2662
|
+
if (rows.size === 0) return messages;
|
|
2663
|
+
|
|
2664
|
+
const shown = new Set(messages.map((message) => message.id));
|
|
2665
|
+
let changed = false;
|
|
2666
|
+
const next: Message[] = [];
|
|
2667
|
+
messages.forEach((message) => {
|
|
2668
|
+
const row = isLocalMessage(message) ? rows.get(message.id) : undefined;
|
|
2669
|
+
if (!row) {
|
|
2670
|
+
next.push(message);
|
|
2671
|
+
return;
|
|
2672
|
+
}
|
|
2673
|
+
changed = true;
|
|
2674
|
+
// Already on screen under its server id (an earlier echo): drop the bubble.
|
|
2675
|
+
if (shown.has(row.id)) return;
|
|
2676
|
+
next.push({
|
|
2677
|
+
...row,
|
|
2678
|
+
attachments:
|
|
2679
|
+
row.attachments && message.attachments
|
|
2680
|
+
? mergeAttachments(message.attachments, row.attachments)
|
|
2681
|
+
: (row.attachments ?? message.attachments),
|
|
2682
|
+
});
|
|
2683
|
+
});
|
|
2684
|
+
return changed ? next : messages;
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
/** Still the picked file (a `file://` URI, or no URL at all), not the uploaded copy. */
|
|
2688
|
+
function isLocalAttachment(attachment: WidgetAttachment): boolean {
|
|
2689
|
+
return !/^https?:\/\//i.test(attachment.url);
|
|
2690
|
+
}
|
|
2691
|
+
|
|
2692
|
+
/** A visitor bubble still showing the picked files rather than the uploaded copies. */
|
|
2693
|
+
function hasLocalAttachments(message: Message): boolean {
|
|
2694
|
+
return !!message.attachments?.some(isLocalAttachment);
|
|
2695
|
+
}
|
|
2696
|
+
|
|
2697
|
+
/** A sent bubble whose stored copy hasn't replaced the picked files yet. */
|
|
2698
|
+
function isAwaitingServerCopy(message: Message): boolean {
|
|
2699
|
+
return !isLocalMessage(message) && hasLocalAttachments(message);
|
|
2700
|
+
}
|
|
2701
|
+
|
|
2502
2702
|
/** Stable, so same-instant rows keep their order; bad dates sort as oldest. */
|
|
2503
2703
|
function sortByCreatedAt(messages: Message[]): Message[] {
|
|
2504
2704
|
const time = (message: Message) => Date.parse(message.createdAt) || 0;
|
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),
|
|
@@ -332,9 +342,7 @@ export function RepzoWidgetProvider({
|
|
|
332
342
|
[store]
|
|
333
343
|
);
|
|
334
344
|
|
|
335
|
-
const handleBack = useCallback(() =>
|
|
336
|
-
store.setView(state.activeTab === 'messages' ? 'conversations' : 'home');
|
|
337
|
-
}, [state.activeTab, store]);
|
|
345
|
+
const handleBack = useCallback(() => store.leaveConversation(), [store]);
|
|
338
346
|
|
|
339
347
|
const handleStartConversation = useCallback(() => {
|
|
340
348
|
if (!theme.allowNewConversations) return;
|
|
@@ -444,8 +452,9 @@ export function RepzoWidgetProvider({
|
|
|
444
452
|
offlineMessage={theme.offlineMessage}
|
|
445
453
|
labels={chatLabels}
|
|
446
454
|
attachmentSources={attachmentSources}
|
|
447
|
-
onPickAttachments={
|
|
455
|
+
onPickAttachments={handlePickAttachments}
|
|
448
456
|
onSendMessage={handleSendMessage}
|
|
457
|
+
onRetryMessage={handleRetryMessage}
|
|
449
458
|
onIdentifyVisitor={handleIdentifyVisitor}
|
|
450
459
|
onRateConversation={handleRateConversation}
|
|
451
460
|
onLoadMore={handleLoadMore}
|