@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
|
@@ -13,6 +13,7 @@ var _pushNotifications = require("../push/push-notifications.js");
|
|
|
13
13
|
var _storage = require("../storage.js");
|
|
14
14
|
var _pickers = require("../attachments/pickers.js");
|
|
15
15
|
var _types = require("../types.js");
|
|
16
|
+
var _conversations = require("../ui/utils/conversations.js");
|
|
16
17
|
var _safeUrl = require("../ui/utils/safe-url.js");
|
|
17
18
|
/**
|
|
18
19
|
* The widget's single source of truth.
|
|
@@ -25,7 +26,7 @@ var _safeUrl = require("../ui/utils/safe-url.js");
|
|
|
25
26
|
*/
|
|
26
27
|
|
|
27
28
|
/** Sent on every init; must match `package.json` (tests/version.test.js checks). */
|
|
28
|
-
const SDK_VERSION = '@repzo/widget-react-native@1.
|
|
29
|
+
const SDK_VERSION = '@repzo/widget-react-native@1.3.1';
|
|
29
30
|
|
|
30
31
|
/** The platform this session reports — mobile sessions get the Mobile SDKs overrides. */
|
|
31
32
|
function sessionPlatform() {
|
|
@@ -55,6 +56,8 @@ const MARK_READ_DEBOUNCE_MS = 1_000;
|
|
|
55
56
|
const FOREGROUND_REFRESH_AFTER_MS = 5 * 60_000;
|
|
56
57
|
/** Live events held back while a draft's first send is creating its thread. */
|
|
57
58
|
const DRAFT_EVENT_BUFFER_LIMIT = 50;
|
|
59
|
+
/** Waits between re-reads for a sent file's stored copy while Workstation is still storing it. */
|
|
60
|
+
const SERVER_COPY_RETRY_MS = [2_000, 5_000, 15_000];
|
|
58
61
|
/** How long a post stays up after a reaction, thanking them, before it goes. */
|
|
59
62
|
const POST_THANKS_MS = exports.POST_THANKS_MS = 1600;
|
|
60
63
|
|
|
@@ -171,6 +174,13 @@ class WidgetStore {
|
|
|
171
174
|
/** True once `currentUserKey` mirrors storage — memory is then authoritative. */
|
|
172
175
|
userKeyLoaded = false;
|
|
173
176
|
pendingConversationId = null;
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* The screen a thread was opened from, so its back button returns there.
|
|
180
|
+
* Null when it was opened with the messenger closed (a push, a notification
|
|
181
|
+
* card, `presentConversation`): there is nothing to go back to.
|
|
182
|
+
*/
|
|
183
|
+
chatOpenedFrom = null;
|
|
174
184
|
appStateSubscription = null;
|
|
175
185
|
|
|
176
186
|
/** When the app last went to the background (0: hasn't this session). */
|
|
@@ -180,6 +190,12 @@ class WidgetStore {
|
|
|
180
190
|
seenMessageIds = [];
|
|
181
191
|
/** Statuses (e.g. "read") that arrived for an id before a send response revealed it. */
|
|
182
192
|
earlyStatuses = new Map();
|
|
193
|
+
/** The agent UI action behind a failed bubble, so Retry runs it again too. */
|
|
194
|
+
failedActions = new Map();
|
|
195
|
+
/** The next re-read for a sent file's stored copy (see `syncServerCopies`). */
|
|
196
|
+
serverCopyTimer = null;
|
|
197
|
+
serverCopyRetries = 0;
|
|
198
|
+
serverCopySyncing = false;
|
|
183
199
|
|
|
184
200
|
/**
|
|
185
201
|
* Identity generation. Bumped whenever boot/login/logout is requested; every
|
|
@@ -269,6 +285,13 @@ class WidgetStore {
|
|
|
269
285
|
next.inAppNotification = null;
|
|
270
286
|
}
|
|
271
287
|
this.state = next;
|
|
288
|
+
|
|
289
|
+
// Recorded here because a thread opens from many places (home cards, the
|
|
290
|
+
// list, the Messages tab, posts) and each would otherwise have to say so.
|
|
291
|
+
const intoThread = next.view === 'chat' || next.view === 'prechat';
|
|
292
|
+
if (intoThread && previous.view !== 'chat' && previous.view !== 'prechat') {
|
|
293
|
+
this.chatOpenedFrom = previous.isOpen && (previous.view === 'home' || previous.view === 'conversations') ? previous.view : null;
|
|
294
|
+
}
|
|
272
295
|
if (next.unreadCount !== previous.unreadCount) {
|
|
273
296
|
this.events.emit(_events.RepzoWidgetEvents.UnreadCountDidChange, {
|
|
274
297
|
count: next.unreadCount
|
|
@@ -811,7 +834,9 @@ class WidgetStore {
|
|
|
811
834
|
this.threadGeneration += 1;
|
|
812
835
|
this.conversationCreation = null;
|
|
813
836
|
this.draftEvents = [];
|
|
837
|
+
this.failedActions.clear();
|
|
814
838
|
this.cancelMarkRead();
|
|
839
|
+
this.cancelServerCopySync();
|
|
815
840
|
}
|
|
816
841
|
requireBootConfig() {
|
|
817
842
|
if (!this.bootConfig) {
|
|
@@ -1127,7 +1152,10 @@ class WidgetStore {
|
|
|
1127
1152
|
attachments: this.hydrateAttachments(data?.attachments, messageId),
|
|
1128
1153
|
metadata: this.hydrateMetadata(data?.metadata || null),
|
|
1129
1154
|
status: 'delivered',
|
|
1130
|
-
createdAt: data?.createdAt || new Date().toISOString()
|
|
1155
|
+
createdAt: data?.createdAt || new Date().toISOString(),
|
|
1156
|
+
...(typeof data?.clientMessageId === 'string' ? {
|
|
1157
|
+
clientMessageId: data.clientMessageId
|
|
1158
|
+
} : {})
|
|
1131
1159
|
};
|
|
1132
1160
|
const clearsTyping = message.direction === 'outbound' && message.senderType !== 'visitor';
|
|
1133
1161
|
if (clearsTyping) this.clearTypingTimer();
|
|
@@ -1158,10 +1186,11 @@ class WidgetStore {
|
|
|
1158
1186
|
messageCount: conversation.messageCount + 1
|
|
1159
1187
|
} : conversation) : current.conversations;
|
|
1160
1188
|
if (onScreen) {
|
|
1161
|
-
const
|
|
1189
|
+
const shown = adoptOwnServerRows(current.messages, [message]);
|
|
1190
|
+
const alreadyPresent = shown.some(existing => existing.id === message.id);
|
|
1162
1191
|
return {
|
|
1163
1192
|
conversations,
|
|
1164
|
-
messages: alreadyPresent ? adoptServerAttachments(
|
|
1193
|
+
messages: alreadyPresent ? adoptServerAttachments(shown, [message]) : [...shown, message],
|
|
1165
1194
|
agentTyping: clearsTyping ? false : current.agentTyping
|
|
1166
1195
|
};
|
|
1167
1196
|
}
|
|
@@ -1400,6 +1429,14 @@ class WidgetStore {
|
|
|
1400
1429
|
} : {})
|
|
1401
1430
|
});
|
|
1402
1431
|
}
|
|
1432
|
+
|
|
1433
|
+
/**
|
|
1434
|
+
* The thread's back button: the screen it was opened from, or the
|
|
1435
|
+
* conversation list when it was opened from outside the messenger.
|
|
1436
|
+
*/
|
|
1437
|
+
leaveConversation() {
|
|
1438
|
+
this.setView(this.chatOpenedFrom ?? (this.state.activeTab === 'messages' ? 'conversations' : 'home'));
|
|
1439
|
+
}
|
|
1403
1440
|
setActiveTab(tab) {
|
|
1404
1441
|
if (tab === 'home') {
|
|
1405
1442
|
this.setState({
|
|
@@ -1564,11 +1601,12 @@ class WidgetStore {
|
|
|
1564
1601
|
this.setState(current => {
|
|
1565
1602
|
// Live replies and optimistic sends may have arrived since this page
|
|
1566
1603
|
// was requested. Keep them, preferring the live copy for shared ids.
|
|
1567
|
-
const liveIds = new Set(current.messages.map(message => message.id));
|
|
1568
1604
|
const page = this.hydrateMessages(result.data);
|
|
1605
|
+
const live = adoptOwnServerRows(current.messages, page);
|
|
1606
|
+
const liveIds = new Set(live.map(message => message.id));
|
|
1569
1607
|
const history = page.filter(message => !liveIds.has(message.id));
|
|
1570
1608
|
return {
|
|
1571
|
-
messages: sortByCreatedAt([...history, ...adoptServerAttachments(
|
|
1609
|
+
messages: sortByCreatedAt([...history, ...adoptServerAttachments(live, page)]),
|
|
1572
1610
|
messagesLoading: false,
|
|
1573
1611
|
hasMoreMessages: result.meta.hasMore,
|
|
1574
1612
|
messageCursor: result.meta.nextCursor
|
|
@@ -1636,14 +1674,15 @@ class WidgetStore {
|
|
|
1636
1674
|
if (this.state.activeConversationId !== conversationId) return;
|
|
1637
1675
|
const fresh = this.hydrateMessages(result.data);
|
|
1638
1676
|
this.setState(current => {
|
|
1639
|
-
const
|
|
1677
|
+
const own = adoptOwnServerRows(current.messages, fresh);
|
|
1678
|
+
const known = new Set(own.map(message => message.id));
|
|
1640
1679
|
const missing = fresh.filter(message => !known.has(message.id));
|
|
1641
|
-
const upgraded = adoptServerStatuses(adoptServerDeletions(adoptServerAttachments(
|
|
1680
|
+
const upgraded = adoptServerStatuses(adoptServerDeletions(adoptServerAttachments(own, fresh), fresh), fresh);
|
|
1642
1681
|
if (missing.length === 0) return upgraded === current.messages ? {} : {
|
|
1643
1682
|
messages: upgraded
|
|
1644
1683
|
};
|
|
1645
|
-
const local =
|
|
1646
|
-
const hasServerMessages = local.length <
|
|
1684
|
+
const local = own.filter(_conversations.isLocalMessage);
|
|
1685
|
+
const hasServerMessages = local.length < own.length;
|
|
1647
1686
|
// No overlap means the gap is longer than a page: restart from the
|
|
1648
1687
|
// newest page (keeping unsent bubbles) rather than leave a hole.
|
|
1649
1688
|
if (missing.length === fresh.length || !hasServerMessages) {
|
|
@@ -1668,6 +1707,40 @@ class WidgetStore {
|
|
|
1668
1707
|
}
|
|
1669
1708
|
}
|
|
1670
1709
|
|
|
1710
|
+
/**
|
|
1711
|
+
* Until its stored copy replaces it, a sent bubble points at the visitor's
|
|
1712
|
+
* local files: a photo still opens in the viewer, but a PDF has nothing to
|
|
1713
|
+
* open. When the stream's echo hasn't brought that copy, re-read the
|
|
1714
|
+
* thread, and try again a few times while Workstation is still storing it.
|
|
1715
|
+
*/
|
|
1716
|
+
async syncServerCopies() {
|
|
1717
|
+
this.cancelServerCopySync();
|
|
1718
|
+
if (this.serverCopySyncing || !this.state.messages.some(isAwaitingServerCopy)) return;
|
|
1719
|
+
// A send still in flight would come back as a stored row beside its own
|
|
1720
|
+
// pending bubble. It syncs again when it settles.
|
|
1721
|
+
if (this.state.messages.some(message => (0, _conversations.isLocalMessage)(message) && message.status === 'pending')) return;
|
|
1722
|
+
// Off screen: the thread reloads in full when it's opened again.
|
|
1723
|
+
const conversationId = this.state.activeConversationId;
|
|
1724
|
+
if (!conversationId || !this.isThreadOnScreen(conversationId)) return;
|
|
1725
|
+
const generation = this.threadGeneration;
|
|
1726
|
+
this.serverCopySyncing = true;
|
|
1727
|
+
try {
|
|
1728
|
+
// A no-op while a page load is in flight; the retry below covers it.
|
|
1729
|
+
await this.refreshOpenThread();
|
|
1730
|
+
} finally {
|
|
1731
|
+
this.serverCopySyncing = false;
|
|
1732
|
+
}
|
|
1733
|
+
if (this.threadGeneration !== generation || !this.state.messages.some(isAwaitingServerCopy)) return;
|
|
1734
|
+
const delay = SERVER_COPY_RETRY_MS[this.serverCopyRetries];
|
|
1735
|
+
if (delay === undefined) return;
|
|
1736
|
+
this.serverCopyRetries += 1;
|
|
1737
|
+
this.serverCopyTimer = setTimeout(() => void this.syncServerCopies(), delay);
|
|
1738
|
+
}
|
|
1739
|
+
cancelServerCopySync() {
|
|
1740
|
+
if (this.serverCopyTimer) clearTimeout(this.serverCopyTimer);
|
|
1741
|
+
this.serverCopyTimer = null;
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1671
1744
|
/** A history page (newest-first) as the oldest-first list the UI renders. */
|
|
1672
1745
|
hydrateMessages(page) {
|
|
1673
1746
|
return page.map(message => ({
|
|
@@ -1715,9 +1788,11 @@ class WidgetStore {
|
|
|
1715
1788
|
|
|
1716
1789
|
/**
|
|
1717
1790
|
* Rejects when the message wasn't sent, so the composer can keep
|
|
1718
|
-
* attachments for a retry. A text-only bubble is marked failed
|
|
1719
|
-
* attachments is removed instead — the
|
|
1720
|
-
* keeping both would leave a duplicate
|
|
1791
|
+
* attachments for a retry. A text-only bubble is marked failed and can be
|
|
1792
|
+
* re-sent with `retryMessage`; one with attachments is removed instead — the
|
|
1793
|
+
* composer's chips are the retry, and keeping both would leave a duplicate
|
|
1794
|
+
* after a successful resend. Likewise an agent UI action replaces the
|
|
1795
|
+
* failed bubble of the same action, since its card stays tappable.
|
|
1721
1796
|
*/
|
|
1722
1797
|
async sendMessage(body, attachments = [], agentUiAction) {
|
|
1723
1798
|
const trimmed = body.trim();
|
|
@@ -1725,7 +1800,6 @@ class WidgetStore {
|
|
|
1725
1800
|
const {
|
|
1726
1801
|
visitorProfile
|
|
1727
1802
|
} = this.state;
|
|
1728
|
-
const generation = this.threadGeneration;
|
|
1729
1803
|
const tempId = `temp-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1730
1804
|
const optimistic = {
|
|
1731
1805
|
id: tempId,
|
|
@@ -1749,20 +1823,58 @@ class WidgetStore {
|
|
|
1749
1823
|
status: 'pending',
|
|
1750
1824
|
createdAt: new Date().toISOString()
|
|
1751
1825
|
};
|
|
1826
|
+
|
|
1827
|
+
// Tapping the card again is the retry: offering the old bubble's retry
|
|
1828
|
+
// too would run the action twice (two items in the cart, two bookings).
|
|
1829
|
+
const superseded = agentUiAction ? [...this.failedActions].filter(([, failed]) => failed.id === agentUiAction.id && failed.type === agentUiAction.type).map(([messageId]) => messageId) : [];
|
|
1830
|
+
superseded.forEach(messageId => this.failedActions.delete(messageId));
|
|
1752
1831
|
this.setState(current => ({
|
|
1753
|
-
messages: [...current.messages, optimistic],
|
|
1832
|
+
messages: [...current.messages.filter(message => !superseded.includes(message.id)), optimistic],
|
|
1754
1833
|
prefillText: ''
|
|
1755
1834
|
}));
|
|
1756
1835
|
this.track('widget_message_sent', {
|
|
1757
1836
|
hasText: !!trimmed,
|
|
1758
1837
|
attachmentCount: attachments.length
|
|
1759
1838
|
});
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1839
|
+
return this.deliverMessage(tempId, trimmed, attachments, agentUiAction);
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
/**
|
|
1843
|
+
* Re-sends a text bubble that failed. It moves to the end of the thread,
|
|
1844
|
+
* since it now goes out after anything sent meanwhile. Rejects like
|
|
1845
|
+
* `sendMessage` when it fails again; the bubble is then failed once more.
|
|
1846
|
+
*/
|
|
1847
|
+
async retryMessage(messageId) {
|
|
1848
|
+
const failed = this.state.messages.find(message => message.id === messageId && (0, _conversations.canRetryMessage)(message));
|
|
1849
|
+
if (!failed) return;
|
|
1850
|
+
const agentUiAction = this.failedActions.get(messageId);
|
|
1851
|
+
this.failedActions.delete(messageId);
|
|
1852
|
+
const pending = {
|
|
1853
|
+
...failed,
|
|
1854
|
+
status: 'pending',
|
|
1855
|
+
createdAt: new Date().toISOString()
|
|
1856
|
+
};
|
|
1857
|
+
this.setState(current => ({
|
|
1858
|
+
messages: [...current.messages.filter(message => message.id !== messageId), pending]
|
|
1765
1859
|
}));
|
|
1860
|
+
return this.deliverMessage(messageId, failed.bodyPlain ?? '', [], agentUiAction);
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
/** Sends the optimistic bubble `tempId`, already on screen as pending. */
|
|
1864
|
+
async deliverMessage(tempId, trimmed, attachments, agentUiAction) {
|
|
1865
|
+
const {
|
|
1866
|
+
visitorProfile
|
|
1867
|
+
} = this.state;
|
|
1868
|
+
const generation = this.threadGeneration;
|
|
1869
|
+
const markFailed = () => {
|
|
1870
|
+
if (agentUiAction && attachments.length === 0) this.failedActions.set(tempId, agentUiAction);
|
|
1871
|
+
this.setState(current => ({
|
|
1872
|
+
messages: attachments.length > 0 ? current.messages.filter(message => message.id !== tempId) : current.messages.map(message => message.id === tempId ? {
|
|
1873
|
+
...message,
|
|
1874
|
+
status: 'failed'
|
|
1875
|
+
} : message)
|
|
1876
|
+
}));
|
|
1877
|
+
};
|
|
1766
1878
|
const epoch = this.epoch;
|
|
1767
1879
|
|
|
1768
1880
|
// A draft's first send creates the conversation. Anything sent from the
|
|
@@ -1788,24 +1900,41 @@ class WidgetStore {
|
|
|
1788
1900
|
}
|
|
1789
1901
|
let createdId = null;
|
|
1790
1902
|
try {
|
|
1903
|
+
// Local files go up first; the message names each by its upload token.
|
|
1904
|
+
const sentAttachments = await Promise.all(attachments.map(async ({
|
|
1905
|
+
name,
|
|
1906
|
+
url,
|
|
1907
|
+
fileUri,
|
|
1908
|
+
contentType,
|
|
1909
|
+
size
|
|
1910
|
+
}) => fileUri ? {
|
|
1911
|
+
name,
|
|
1912
|
+
contentType,
|
|
1913
|
+
size,
|
|
1914
|
+
uploadToken: await this.client.uploadAttachment({
|
|
1915
|
+
uri: fileUri,
|
|
1916
|
+
name,
|
|
1917
|
+
contentType,
|
|
1918
|
+
size
|
|
1919
|
+
})
|
|
1920
|
+
} : {
|
|
1921
|
+
name,
|
|
1922
|
+
url,
|
|
1923
|
+
contentType,
|
|
1924
|
+
size
|
|
1925
|
+
}));
|
|
1926
|
+
if (this.isStale(epoch)) return;
|
|
1791
1927
|
const result = await this.client.sendMessage({
|
|
1792
1928
|
conversationId: conversationId || undefined,
|
|
1793
1929
|
forceNewConversation: !conversationId,
|
|
1794
1930
|
body: trimmed,
|
|
1795
|
-
attachments:
|
|
1796
|
-
name,
|
|
1797
|
-
url,
|
|
1798
|
-
contentType,
|
|
1799
|
-
size
|
|
1800
|
-
}) => ({
|
|
1801
|
-
name,
|
|
1802
|
-
url,
|
|
1803
|
-
contentType,
|
|
1804
|
-
size
|
|
1805
|
-
})) : undefined,
|
|
1931
|
+
attachments: sentAttachments.length > 0 ? sentAttachments : undefined,
|
|
1806
1932
|
visitorName: visitorProfile.name,
|
|
1807
1933
|
visitorEmail: visitorProfile.email,
|
|
1808
|
-
agentUiAction
|
|
1934
|
+
agentUiAction,
|
|
1935
|
+
// The bubble's id, the same on every retry: a newer Workstation stores
|
|
1936
|
+
// the message once even when an earlier attempt's response was lost.
|
|
1937
|
+
clientMessageId: tempId
|
|
1809
1938
|
});
|
|
1810
1939
|
if (this.isStale(epoch)) return;
|
|
1811
1940
|
if (ownCreation) createdId = result.conversationId;
|
|
@@ -1842,8 +1971,15 @@ class WidgetStore {
|
|
|
1842
1971
|
} : {})
|
|
1843
1972
|
};
|
|
1844
1973
|
});
|
|
1974
|
+
|
|
1975
|
+
// New files to fetch the stored copy of: a fresh round of re-reads.
|
|
1976
|
+
const sent = this.state.messages.find(message => message.id === result.messageId);
|
|
1977
|
+
if (sent && hasLocalAttachments(sent)) this.serverCopyRetries = 0;
|
|
1845
1978
|
} catch (error) {
|
|
1846
1979
|
if (this.isStale(epoch)) throw error;
|
|
1980
|
+
// The response was lost, but its stored row has already taken the
|
|
1981
|
+
// bubble's place (newer Workstation deployments): it was sent.
|
|
1982
|
+
if (this.state.messages.some(message => message.clientMessageId === tempId)) return;
|
|
1847
1983
|
markFailed();
|
|
1848
1984
|
throw error;
|
|
1849
1985
|
} finally {
|
|
@@ -1854,6 +1990,8 @@ class WidgetStore {
|
|
|
1854
1990
|
// Anything held back for the draft now has a home (or goes to unread).
|
|
1855
1991
|
this.replayDraftEvents();
|
|
1856
1992
|
}
|
|
1993
|
+
// Waits for every send to settle, so this runs when the last one does.
|
|
1994
|
+
if (!this.isStale(epoch)) void this.syncServerCopies();
|
|
1857
1995
|
}
|
|
1858
1996
|
}
|
|
1859
1997
|
async identifyVisitor(data) {
|
|
@@ -2328,11 +2466,6 @@ function createConversationCreation(generation) {
|
|
|
2328
2466
|
};
|
|
2329
2467
|
}
|
|
2330
2468
|
|
|
2331
|
-
/** Optimistic visitor bubbles the server hasn't acknowledged (pending/failed). */
|
|
2332
|
-
function isLocalMessage(message) {
|
|
2333
|
-
return message.id.startsWith('temp-');
|
|
2334
|
-
}
|
|
2335
|
-
|
|
2336
2469
|
/** A "Seen" missed while the stream was down shows up on the next re-read. */
|
|
2337
2470
|
function adoptServerStatuses(messages, server) {
|
|
2338
2471
|
const seen = new Set(server.filter(message => message.status === 'read').map(message => message.id));
|
|
@@ -2386,17 +2519,82 @@ function adoptServerAttachments(messages, server) {
|
|
|
2386
2519
|
let changed = false;
|
|
2387
2520
|
const next = messages.map(message => {
|
|
2388
2521
|
const copy = serverCopies.get(message.id);
|
|
2389
|
-
|
|
2390
|
-
|
|
2522
|
+
if (!copy || !hasLocalAttachments(message)) return message;
|
|
2523
|
+
const attachments = mergeAttachments(message.attachments, copy.attachments);
|
|
2524
|
+
if (attachments === message.attachments) return message;
|
|
2391
2525
|
changed = true;
|
|
2392
2526
|
return {
|
|
2393
2527
|
...message,
|
|
2394
|
-
attachments
|
|
2528
|
+
attachments
|
|
2395
2529
|
};
|
|
2396
2530
|
});
|
|
2397
2531
|
return changed ? next : messages;
|
|
2398
2532
|
}
|
|
2399
2533
|
|
|
2534
|
+
/**
|
|
2535
|
+
* Each attachment's server copy, except one without a usable URL: that would
|
|
2536
|
+
* only blank its local preview, so the preview stays until a later copy has it.
|
|
2537
|
+
* Returns `local` itself when nothing changes.
|
|
2538
|
+
*/
|
|
2539
|
+
function mergeAttachments(local, server) {
|
|
2540
|
+
// Without a one-to-one match, only a fully usable server copy is safe to take.
|
|
2541
|
+
if (local.length !== server.length) {
|
|
2542
|
+
return server.every(attachment => attachment.url) ? server : local;
|
|
2543
|
+
}
|
|
2544
|
+
const merged = server.map((attachment, index) => {
|
|
2545
|
+
const own = local[index];
|
|
2546
|
+
return isLocalAttachment(own) && attachment.url ? attachment : own;
|
|
2547
|
+
});
|
|
2548
|
+
return merged.every((attachment, index) => attachment === local[index]) ? local : merged;
|
|
2549
|
+
}
|
|
2550
|
+
|
|
2551
|
+
/**
|
|
2552
|
+
* Newer Workstation deployments echo `clientMessageId` on the visitor's own
|
|
2553
|
+
* rows. Such a row replaces the pending or failed bubble it came from instead
|
|
2554
|
+
* of showing beside it: no duplicate while a send is in flight, and no retry
|
|
2555
|
+
* for a message that was stored although its response was lost.
|
|
2556
|
+
*/
|
|
2557
|
+
function adoptOwnServerRows(messages, server) {
|
|
2558
|
+
const rows = new Map();
|
|
2559
|
+
server.forEach(row => {
|
|
2560
|
+
if (typeof row.clientMessageId === 'string' && row.clientMessageId) rows.set(row.clientMessageId, row);
|
|
2561
|
+
});
|
|
2562
|
+
if (rows.size === 0) return messages;
|
|
2563
|
+
const shown = new Set(messages.map(message => message.id));
|
|
2564
|
+
let changed = false;
|
|
2565
|
+
const next = [];
|
|
2566
|
+
messages.forEach(message => {
|
|
2567
|
+
const row = (0, _conversations.isLocalMessage)(message) ? rows.get(message.id) : undefined;
|
|
2568
|
+
if (!row) {
|
|
2569
|
+
next.push(message);
|
|
2570
|
+
return;
|
|
2571
|
+
}
|
|
2572
|
+
changed = true;
|
|
2573
|
+
// Already on screen under its server id (an earlier echo): drop the bubble.
|
|
2574
|
+
if (shown.has(row.id)) return;
|
|
2575
|
+
next.push({
|
|
2576
|
+
...row,
|
|
2577
|
+
attachments: row.attachments && message.attachments ? mergeAttachments(message.attachments, row.attachments) : row.attachments ?? message.attachments
|
|
2578
|
+
});
|
|
2579
|
+
});
|
|
2580
|
+
return changed ? next : messages;
|
|
2581
|
+
}
|
|
2582
|
+
|
|
2583
|
+
/** Still the picked file (a `file://` URI, or no URL at all), not the uploaded copy. */
|
|
2584
|
+
function isLocalAttachment(attachment) {
|
|
2585
|
+
return !/^https?:\/\//i.test(attachment.url);
|
|
2586
|
+
}
|
|
2587
|
+
|
|
2588
|
+
/** A visitor bubble still showing the picked files rather than the uploaded copies. */
|
|
2589
|
+
function hasLocalAttachments(message) {
|
|
2590
|
+
return !!message.attachments?.some(isLocalAttachment);
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2593
|
+
/** A sent bubble whose stored copy hasn't replaced the picked files yet. */
|
|
2594
|
+
function isAwaitingServerCopy(message) {
|
|
2595
|
+
return !(0, _conversations.isLocalMessage)(message) && hasLocalAttachments(message);
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2400
2598
|
/** Stable, so same-instant rows keep their order; bad dates sort as oldest. */
|
|
2401
2599
|
function sortByCreatedAt(messages) {
|
|
2402
2600
|
const time = message => Date.parse(message.createdAt) || 0;
|