@repzo/widget-react-native 1.3.1 → 1.4.0
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 +137 -5
- package/README.md +64 -12
- package/lib/commonjs/api/client.js +96 -23
- package/lib/commonjs/api/client.js.map +1 -1
- package/lib/commonjs/attachments/file-size.js +52 -0
- package/lib/commonjs/attachments/file-size.js.map +1 -0
- package/lib/commonjs/attachments/local-previews.js +40 -0
- package/lib/commonjs/attachments/local-previews.js.map +1 -0
- package/lib/commonjs/attachments/pickers.js +20 -29
- package/lib/commonjs/attachments/pickers.js.map +1 -1
- package/lib/commonjs/i18n.js +22 -3
- package/lib/commonjs/i18n.js.map +1 -1
- package/lib/commonjs/locales.js +12 -3
- package/lib/commonjs/locales.js.map +1 -1
- package/lib/commonjs/state/widget-store.js +277 -30
- package/lib/commonjs/state/widget-store.js.map +1 -1
- package/lib/commonjs/storage.js +3 -1
- package/lib/commonjs/storage.js.map +1 -1
- package/lib/commonjs/types.js.map +1 -1
- package/lib/commonjs/ui/WidgetProvider.js +33 -13
- package/lib/commonjs/ui/WidgetProvider.js.map +1 -1
- package/lib/commonjs/ui/components/AttachmentChips.js +109 -40
- package/lib/commonjs/ui/components/AttachmentChips.js.map +1 -1
- package/lib/commonjs/ui/components/ConversationRow.js +65 -10
- package/lib/commonjs/ui/components/ConversationRow.js.map +1 -1
- package/lib/commonjs/ui/components/ImageViewer.js +229 -24
- package/lib/commonjs/ui/components/ImageViewer.js.map +1 -1
- package/lib/commonjs/ui/components/RatingCard.js +5 -2
- package/lib/commonjs/ui/components/RatingCard.js.map +1 -1
- package/lib/commonjs/ui/components/ZoomableImage.js +58 -10
- package/lib/commonjs/ui/components/ZoomableImage.js.map +1 -1
- package/lib/commonjs/ui/hooks/use-retained-state.js +39 -0
- package/lib/commonjs/ui/hooks/use-retained-state.js.map +1 -0
- package/lib/commonjs/ui/screens/chat/ChatView.js +637 -164
- package/lib/commonjs/ui/screens/chat/ChatView.js.map +1 -1
- package/lib/commonjs/ui/screens/chat/ChatView.styles.js +138 -1
- package/lib/commonjs/ui/screens/chat/ChatView.styles.js.map +1 -1
- package/lib/commonjs/ui/screens/conversations/ConversationsList.js +1 -0
- package/lib/commonjs/ui/screens/conversations/ConversationsList.js.map +1 -1
- package/lib/commonjs/ui/screens/home/HomeView.js +38 -55
- package/lib/commonjs/ui/screens/home/HomeView.js.map +1 -1
- package/lib/commonjs/ui/screens/home/HomeView.styles.js +36 -45
- package/lib/commonjs/ui/screens/home/HomeView.styles.js.map +1 -1
- package/lib/commonjs/ui/utils/html.js +13 -0
- package/lib/commonjs/ui/utils/html.js.map +1 -1
- package/lib/commonjs/ui/utils/image-load-queue.js +114 -0
- package/lib/commonjs/ui/utils/image-load-queue.js.map +1 -0
- package/lib/commonjs/ui/utils/inline-attachments.js +88 -0
- package/lib/commonjs/ui/utils/inline-attachments.js.map +1 -0
- package/lib/module/api/client.js +96 -23
- package/lib/module/api/client.js.map +1 -1
- package/lib/module/attachments/file-size.js +47 -0
- package/lib/module/attachments/file-size.js.map +1 -0
- package/lib/module/attachments/local-previews.js +33 -0
- package/lib/module/attachments/local-previews.js.map +1 -0
- package/lib/module/attachments/pickers.js +15 -25
- package/lib/module/attachments/pickers.js.map +1 -1
- package/lib/module/i18n.js +22 -3
- package/lib/module/i18n.js.map +1 -1
- package/lib/module/locales.js +12 -3
- package/lib/module/locales.js.map +1 -1
- package/lib/module/state/widget-store.js +279 -32
- package/lib/module/state/widget-store.js.map +1 -1
- package/lib/module/storage.js +2 -0
- package/lib/module/storage.js.map +1 -1
- package/lib/module/types.js.map +1 -1
- package/lib/module/ui/WidgetProvider.js +33 -13
- package/lib/module/ui/WidgetProvider.js.map +1 -1
- package/lib/module/ui/components/AttachmentChips.js +111 -42
- package/lib/module/ui/components/AttachmentChips.js.map +1 -1
- package/lib/module/ui/components/ConversationRow.js +65 -10
- package/lib/module/ui/components/ConversationRow.js.map +1 -1
- package/lib/module/ui/components/ImageViewer.js +231 -26
- package/lib/module/ui/components/ImageViewer.js.map +1 -1
- package/lib/module/ui/components/RatingCard.js +5 -2
- package/lib/module/ui/components/RatingCard.js.map +1 -1
- package/lib/module/ui/components/ZoomableImage.js +58 -10
- package/lib/module/ui/components/ZoomableImage.js.map +1 -1
- package/lib/module/ui/hooks/use-retained-state.js +34 -0
- package/lib/module/ui/hooks/use-retained-state.js.map +1 -0
- package/lib/module/ui/screens/chat/ChatView.js +641 -168
- package/lib/module/ui/screens/chat/ChatView.js.map +1 -1
- package/lib/module/ui/screens/chat/ChatView.styles.js +140 -2
- package/lib/module/ui/screens/chat/ChatView.styles.js.map +1 -1
- package/lib/module/ui/screens/conversations/ConversationsList.js +1 -0
- package/lib/module/ui/screens/conversations/ConversationsList.js.map +1 -1
- package/lib/module/ui/screens/home/HomeView.js +40 -57
- package/lib/module/ui/screens/home/HomeView.js.map +1 -1
- package/lib/module/ui/screens/home/HomeView.styles.js +36 -45
- package/lib/module/ui/screens/home/HomeView.styles.js.map +1 -1
- package/lib/module/ui/utils/html.js +12 -0
- package/lib/module/ui/utils/html.js.map +1 -1
- package/lib/module/ui/utils/image-load-queue.js +109 -0
- package/lib/module/ui/utils/image-load-queue.js.map +1 -0
- package/lib/module/ui/utils/inline-attachments.js +85 -0
- package/lib/module/ui/utils/inline-attachments.js.map +1 -0
- package/lib/typescript/commonjs/api/client.d.ts +12 -4
- package/lib/typescript/commonjs/api/client.d.ts.map +1 -1
- package/lib/typescript/commonjs/attachments/file-size.d.ts +15 -0
- package/lib/typescript/commonjs/attachments/file-size.d.ts.map +1 -0
- package/lib/typescript/commonjs/attachments/local-previews.d.ts +14 -0
- package/lib/typescript/commonjs/attachments/local-previews.d.ts.map +1 -0
- package/lib/typescript/commonjs/attachments/pickers.d.ts +15 -2
- package/lib/typescript/commonjs/attachments/pickers.d.ts.map +1 -1
- package/lib/typescript/commonjs/i18n.d.ts +12 -3
- 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 +40 -1
- package/lib/typescript/commonjs/state/widget-store.d.ts.map +1 -1
- package/lib/typescript/commonjs/storage.d.ts +2 -0
- package/lib/typescript/commonjs/storage.d.ts.map +1 -1
- package/lib/typescript/commonjs/types.d.ts +41 -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/AttachmentChips.d.ts +13 -3
- package/lib/typescript/commonjs/ui/components/AttachmentChips.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/ConversationRow.d.ts +15 -1
- package/lib/typescript/commonjs/ui/components/ConversationRow.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/ImageViewer.d.ts +26 -6
- package/lib/typescript/commonjs/ui/components/ImageViewer.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/RatingCard.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/ZoomableImage.d.ts +10 -1
- package/lib/typescript/commonjs/ui/components/ZoomableImage.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/use-retained-state.d.ts +11 -0
- package/lib/typescript/commonjs/ui/hooks/use-retained-state.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/screens/chat/ChatView.d.ts +4 -4
- package/lib/typescript/commonjs/ui/screens/chat/ChatView.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/chat/ChatView.styles.d.ts +144 -0
- package/lib/typescript/commonjs/ui/screens/chat/ChatView.styles.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/chat/ChatView.types.d.ts +26 -1
- package/lib/typescript/commonjs/ui/screens/chat/ChatView.types.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/conversations/ConversationsList.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/conversations/ConversationsList.types.d.ts +2 -0
- package/lib/typescript/commonjs/ui/screens/conversations/ConversationsList.types.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/home/HomeView.d.ts +7 -5
- package/lib/typescript/commonjs/ui/screens/home/HomeView.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/home/HomeView.styles.d.ts +30 -35
- package/lib/typescript/commonjs/ui/screens/home/HomeView.styles.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/home/HomeView.types.d.ts +5 -6
- package/lib/typescript/commonjs/ui/screens/home/HomeView.types.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/utils/html.d.ts +5 -0
- package/lib/typescript/commonjs/ui/utils/html.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/utils/image-load-queue.d.ts +44 -0
- package/lib/typescript/commonjs/ui/utils/image-load-queue.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/utils/inline-attachments.d.ts +29 -0
- package/lib/typescript/commonjs/ui/utils/inline-attachments.d.ts.map +1 -0
- package/lib/typescript/module/api/client.d.ts +12 -4
- package/lib/typescript/module/api/client.d.ts.map +1 -1
- package/lib/typescript/module/attachments/file-size.d.ts +15 -0
- package/lib/typescript/module/attachments/file-size.d.ts.map +1 -0
- package/lib/typescript/module/attachments/local-previews.d.ts +14 -0
- package/lib/typescript/module/attachments/local-previews.d.ts.map +1 -0
- package/lib/typescript/module/attachments/pickers.d.ts +15 -2
- package/lib/typescript/module/attachments/pickers.d.ts.map +1 -1
- package/lib/typescript/module/i18n.d.ts +12 -3
- 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 +40 -1
- package/lib/typescript/module/state/widget-store.d.ts.map +1 -1
- package/lib/typescript/module/storage.d.ts +2 -0
- package/lib/typescript/module/storage.d.ts.map +1 -1
- package/lib/typescript/module/types.d.ts +41 -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/AttachmentChips.d.ts +13 -3
- package/lib/typescript/module/ui/components/AttachmentChips.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/ConversationRow.d.ts +15 -1
- package/lib/typescript/module/ui/components/ConversationRow.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/ImageViewer.d.ts +26 -6
- package/lib/typescript/module/ui/components/ImageViewer.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/RatingCard.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/ZoomableImage.d.ts +10 -1
- package/lib/typescript/module/ui/components/ZoomableImage.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/use-retained-state.d.ts +11 -0
- package/lib/typescript/module/ui/hooks/use-retained-state.d.ts.map +1 -0
- package/lib/typescript/module/ui/screens/chat/ChatView.d.ts +4 -4
- package/lib/typescript/module/ui/screens/chat/ChatView.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/chat/ChatView.styles.d.ts +144 -0
- package/lib/typescript/module/ui/screens/chat/ChatView.styles.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/chat/ChatView.types.d.ts +26 -1
- package/lib/typescript/module/ui/screens/chat/ChatView.types.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/conversations/ConversationsList.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/conversations/ConversationsList.types.d.ts +2 -0
- package/lib/typescript/module/ui/screens/conversations/ConversationsList.types.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/home/HomeView.d.ts +7 -5
- package/lib/typescript/module/ui/screens/home/HomeView.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/home/HomeView.styles.d.ts +30 -35
- package/lib/typescript/module/ui/screens/home/HomeView.styles.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/home/HomeView.types.d.ts +5 -6
- package/lib/typescript/module/ui/screens/home/HomeView.types.d.ts.map +1 -1
- package/lib/typescript/module/ui/utils/html.d.ts +5 -0
- package/lib/typescript/module/ui/utils/html.d.ts.map +1 -1
- package/lib/typescript/module/ui/utils/image-load-queue.d.ts +44 -0
- package/lib/typescript/module/ui/utils/image-load-queue.d.ts.map +1 -0
- package/lib/typescript/module/ui/utils/inline-attachments.d.ts +29 -0
- package/lib/typescript/module/ui/utils/inline-attachments.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/api/client.ts +116 -34
- package/src/attachments/file-size.ts +42 -0
- package/src/attachments/local-previews.ts +32 -0
- package/src/attachments/pickers.ts +23 -25
- package/src/i18n.ts +22 -3
- package/src/locales.ts +12 -3
- package/src/state/widget-store.ts +322 -50
- package/src/storage.ts +2 -0
- package/src/types.ts +41 -0
- package/src/ui/WidgetProvider.tsx +35 -8
- package/src/ui/components/AttachmentChips.tsx +100 -29
- package/src/ui/components/ConversationRow.tsx +90 -9
- package/src/ui/components/ImageViewer.tsx +242 -24
- package/src/ui/components/RatingCard.tsx +5 -2
- package/src/ui/components/ZoomableImage.tsx +58 -5
- package/src/ui/hooks/use-retained-state.ts +35 -0
- package/src/ui/screens/chat/ChatView.styles.ts +139 -1
- package/src/ui/screens/chat/ChatView.tsx +721 -197
- package/src/ui/screens/chat/ChatView.types.ts +26 -1
- package/src/ui/screens/conversations/ConversationsList.tsx +1 -0
- package/src/ui/screens/conversations/ConversationsList.types.ts +2 -0
- package/src/ui/screens/home/HomeView.styles.ts +36 -45
- package/src/ui/screens/home/HomeView.tsx +54 -70
- package/src/ui/screens/home/HomeView.types.ts +5 -6
- package/src/ui/utils/html.ts +26 -0
- package/src/ui/utils/image-load-queue.ts +108 -0
- package/src/ui/utils/inline-attachments.ts +71 -0
|
@@ -11,9 +11,12 @@ var _deviceId = require("../device-id.js");
|
|
|
11
11
|
var _events = require("../events.js");
|
|
12
12
|
var _pushNotifications = require("../push/push-notifications.js");
|
|
13
13
|
var _storage = require("../storage.js");
|
|
14
|
+
var _localPreviews = require("../attachments/local-previews.js");
|
|
14
15
|
var _pickers = require("../attachments/pickers.js");
|
|
15
16
|
var _types = require("../types.js");
|
|
17
|
+
var _useRetainedState = require("../ui/hooks/use-retained-state.js");
|
|
16
18
|
var _conversations = require("../ui/utils/conversations.js");
|
|
19
|
+
var _html = require("../ui/utils/html.js");
|
|
17
20
|
var _safeUrl = require("../ui/utils/safe-url.js");
|
|
18
21
|
/**
|
|
19
22
|
* The widget's single source of truth.
|
|
@@ -26,7 +29,7 @@ var _safeUrl = require("../ui/utils/safe-url.js");
|
|
|
26
29
|
*/
|
|
27
30
|
|
|
28
31
|
/** Sent on every init; must match `package.json` (tests/version.test.js checks). */
|
|
29
|
-
const SDK_VERSION = '@repzo/widget-react-native@1.
|
|
32
|
+
const SDK_VERSION = '@repzo/widget-react-native@1.4.0';
|
|
30
33
|
|
|
31
34
|
/** The platform this session reports — mobile sessions get the Mobile SDKs overrides. */
|
|
32
35
|
function sessionPlatform() {
|
|
@@ -41,6 +44,12 @@ const TYPING_SETTLE_MS = 3_000;
|
|
|
41
44
|
const TYPING_PING_INTERVAL_MS = 4_000;
|
|
42
45
|
/** Guards against the SSE stream replaying messages after a reconnect. */
|
|
43
46
|
const SEEN_MESSAGE_LIMIT = 100;
|
|
47
|
+
/** Threads whose newest message is remembered across launches. */
|
|
48
|
+
const LAST_MESSAGE_LIMIT = 50;
|
|
49
|
+
/** How far the phone's clock may be from Workstation's for a sent message to still match the list. */
|
|
50
|
+
const SAME_MESSAGE_DRIFT_MS = 120_000;
|
|
51
|
+
/** How far the list's `lastMessageAt` may trail the message's own `createdAt` (7 ms seen live). */
|
|
52
|
+
const LIST_LAG_MS = 1_000;
|
|
44
53
|
/** Logout's server calls are best-effort — a dead network must not block sign-out. */
|
|
45
54
|
const LOGOUT_REQUEST_TIMEOUT_MS = 5_000;
|
|
46
55
|
/** A server that 401s every fresh stream must not spin init (10/min limit). */
|
|
@@ -134,6 +143,10 @@ class WidgetStore {
|
|
|
134
143
|
|
|
135
144
|
/** `boot({ posts })` — Send posts over the app. */
|
|
136
145
|
postsEnabled = true;
|
|
146
|
+
/** `boot({ photoAlbums })` — a message's photos share one album in the thread. */
|
|
147
|
+
photoAlbumsEnabled = true;
|
|
148
|
+
/** `boot({ youPrefix })` — "You:" before the visitor's own newest message in the lists. */
|
|
149
|
+
youPrefixEnabled = true;
|
|
137
150
|
|
|
138
151
|
/** Posts whose view was reported this session (the server also dedupes per day). */
|
|
139
152
|
viewedPostIds = new Set();
|
|
@@ -188,6 +201,10 @@ class WidgetStore {
|
|
|
188
201
|
lastTypingPingAt = 0;
|
|
189
202
|
visitorTypingActive = false;
|
|
190
203
|
seenMessageIds = [];
|
|
204
|
+
|
|
205
|
+
/** Who sent each thread's newest message and what it carried; see `withLastMessage`. */
|
|
206
|
+
lastMessages = {};
|
|
207
|
+
lastMessagesLoaded = false;
|
|
191
208
|
/** Statuses (e.g. "read") that arrived for an id before a send response revealed it. */
|
|
192
209
|
earlyStatuses = new Map();
|
|
193
210
|
/** The agent UI action behind a failed bubble, so Retry runs it again too. */
|
|
@@ -267,6 +284,8 @@ class WidgetStore {
|
|
|
267
284
|
|
|
268
285
|
/** Composer identity: explicit opens reset it; a draft receiving its server ID does not. */
|
|
269
286
|
getThreadGeneration = () => this.threadGeneration;
|
|
287
|
+
showsPhotoAlbums = () => this.photoAlbumsEnabled;
|
|
288
|
+
showsYouPrefix = () => this.youPrefixEnabled;
|
|
270
289
|
setState(patch) {
|
|
271
290
|
const previous = this.state;
|
|
272
291
|
const resolved = typeof patch === 'function' ? patch(previous) : patch;
|
|
@@ -338,6 +357,8 @@ class WidgetStore {
|
|
|
338
357
|
if (options.inAppNotifications !== undefined) {
|
|
339
358
|
this.inAppNotificationsEnabled = options.inAppNotifications !== false;
|
|
340
359
|
}
|
|
360
|
+
if (options.photoAlbums !== undefined) this.photoAlbumsEnabled = options.photoAlbums !== false;
|
|
361
|
+
if (options.youPrefix !== undefined) this.youPrefixEnabled = options.youPrefix !== false;
|
|
341
362
|
if (options.posts !== undefined) {
|
|
342
363
|
const wasEnabled = this.postsEnabled;
|
|
343
364
|
this.postsEnabled = options.posts !== false;
|
|
@@ -492,13 +513,13 @@ class WidgetStore {
|
|
|
492
513
|
// queued op must never see one without the other.
|
|
493
514
|
await Promise.all([this.client.setToken(result.token), userKey ? _storage.SecureStorage.setItem(_storage.USER_KEY, userKey) : _storage.SecureStorage.removeItem(_storage.USER_KEY)]);
|
|
494
515
|
if (this.isStale(epoch)) return;
|
|
495
|
-
const conversations = result.conversations.map(conversation => this.hydrateConversation(conversation));
|
|
496
516
|
|
|
497
517
|
// Server counts win (they include replies pushed while the app was
|
|
498
518
|
// killed); the locally persisted map only covers older Workstation
|
|
499
519
|
// deployments that don't send unreadCount yet.
|
|
500
|
-
const storedUnread = await this.loadUnread();
|
|
520
|
+
const [, storedUnread] = await Promise.all([this.loadLastMessages(epoch), this.loadUnread()]);
|
|
501
521
|
if (this.isStale(epoch)) return;
|
|
522
|
+
const conversations = result.conversations.map(conversation => this.hydrateConversation(conversation));
|
|
502
523
|
const unreadByConversation = {};
|
|
503
524
|
for (const conversation of conversations) {
|
|
504
525
|
const count = typeof conversation.unreadCount === 'number' ? conversation.unreadCount : storedUnread[conversation.id] ?? 0;
|
|
@@ -789,16 +810,20 @@ class WidgetStore {
|
|
|
789
810
|
this.userKeyLoaded = true;
|
|
790
811
|
this.pendingAgentByConversation = {};
|
|
791
812
|
this.seenMessageIds = [];
|
|
813
|
+
this.lastMessages = {};
|
|
814
|
+
this.lastMessagesLoaded = false;
|
|
792
815
|
this.earlyStatuses.clear();
|
|
793
816
|
this.visitorTypingActive = false;
|
|
794
817
|
this.lastTypingPingAt = 0;
|
|
795
818
|
this.launchCardOffered = false;
|
|
796
819
|
this.resetPost();
|
|
820
|
+
(0, _localPreviews.clearLocalPreviews)();
|
|
821
|
+
(0, _useRetainedState.clearRetainedState)();
|
|
797
822
|
this.setState({
|
|
798
823
|
...USER_SCOPED_STATE
|
|
799
824
|
});
|
|
800
825
|
const token = await this.client.getToken();
|
|
801
|
-
await Promise.all([this.client.clearToken(), _storage.SecureStorage.removeItem(_storage.USER_KEY), _storage.Storage.removeItem(_storage.UNREAD_KEY),
|
|
826
|
+
await Promise.all([this.client.clearToken(), _storage.SecureStorage.removeItem(_storage.USER_KEY), _storage.Storage.removeItem(_storage.UNREAD_KEY), _storage.Storage.removeItem(_storage.LAST_MESSAGE_KEY),
|
|
802
827
|
// A new anonymous visitor on this device gets a new tracking identity.
|
|
803
828
|
(0, _deviceId.rotateDeviceId)()]);
|
|
804
829
|
|
|
@@ -1178,10 +1203,11 @@ class WidgetStore {
|
|
|
1178
1203
|
|
|
1179
1204
|
// Set inside the updater, read after: `setState` runs synchronously.
|
|
1180
1205
|
let countedAsUnread = false;
|
|
1206
|
+
const rowFields = rowFieldsOf(message);
|
|
1181
1207
|
this.setState(current => {
|
|
1182
1208
|
const conversations = conversationId ? current.conversations.map(conversation => conversation.id === conversationId ? {
|
|
1183
1209
|
...conversation,
|
|
1184
|
-
|
|
1210
|
+
...rowFields,
|
|
1185
1211
|
lastMessageAt: message.createdAt,
|
|
1186
1212
|
messageCount: conversation.messageCount + 1
|
|
1187
1213
|
} : conversation) : current.conversations;
|
|
@@ -1219,11 +1245,12 @@ class WidgetStore {
|
|
|
1219
1245
|
messageId,
|
|
1220
1246
|
senderName: message.senderName || conversation?.agentName || null,
|
|
1221
1247
|
senderAvatar: conversation?.agentAvatar ?? null,
|
|
1222
|
-
preview: (message
|
|
1248
|
+
preview: plainTextOf(message),
|
|
1223
1249
|
hasAttachments: (message.attachments?.length ?? 0) > 0
|
|
1224
1250
|
}
|
|
1225
1251
|
};
|
|
1226
1252
|
});
|
|
1253
|
+
if (conversationId && rowFields) this.rememberLastMessage(conversationId, message.createdAt, rowFields, 'stream');
|
|
1227
1254
|
|
|
1228
1255
|
// The visitor is reading it live — move the server cursor too, or the
|
|
1229
1256
|
// next list/init would badge a reply they already saw.
|
|
@@ -1321,10 +1348,112 @@ class WidgetStore {
|
|
|
1321
1348
|
};
|
|
1322
1349
|
}
|
|
1323
1350
|
hydrateConversation(conversation) {
|
|
1324
|
-
return this.applyConversationAgent({
|
|
1351
|
+
return this.applyConversationAgent(this.withLastMessage({
|
|
1325
1352
|
...conversation,
|
|
1326
1353
|
agentAvatar: this.client.resolveUrl(conversation.agentAvatar)
|
|
1354
|
+
}));
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
/**
|
|
1358
|
+
* Workstation's list doesn't say who sent the newest message, and calls one
|
|
1359
|
+
* without words "2 attachments". While a row still describes the message
|
|
1360
|
+
* this device saw last, both are filled in from it. `snippet` keeps the
|
|
1361
|
+
* list's words: apps read it, and the lists draw "Photo" or "File" from
|
|
1362
|
+
* `lastMessageAttachment` instead.
|
|
1363
|
+
*/
|
|
1364
|
+
withLastMessage(conversation) {
|
|
1365
|
+
const known = this.lastMessages[conversation.id];
|
|
1366
|
+
if (!known || !describesSameMessage(conversation, known)) return conversation;
|
|
1367
|
+
// A list that says who sent it, or what it carried, is used as is.
|
|
1368
|
+
return {
|
|
1369
|
+
...conversation,
|
|
1370
|
+
lastMessageFromVisitor: conversation.lastMessageFromVisitor ?? known.fromVisitor,
|
|
1371
|
+
lastMessageAttachment: conversation.lastMessageAttachment !== undefined ? conversation.lastMessageAttachment : known.attachment
|
|
1372
|
+
};
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
/**
|
|
1376
|
+
* A send or a streamed message is the newest by arrival, whatever the two
|
|
1377
|
+
* clocks say; a history page only replaces something older. A sent message
|
|
1378
|
+
* is timed by the phone, which may run ahead of Workstation, so instead a
|
|
1379
|
+
* page that has it replaces it: whatever is newest there came after it, and
|
|
1380
|
+
* Workstation's own time then stands in for the phone's.
|
|
1381
|
+
*/
|
|
1382
|
+
rememberLastMessage(conversationId, at, fields, source, {
|
|
1383
|
+
messageId,
|
|
1384
|
+
pageIds
|
|
1385
|
+
} = {}) {
|
|
1386
|
+
const known = this.lastMessages[conversationId];
|
|
1387
|
+
if (source === 'history' && known) {
|
|
1388
|
+
const outdated = known.phoneClock && known.messageId ? !pageIds?.has(known.messageId) : Date.parse(known.at) > Date.parse(at);
|
|
1389
|
+
if (outdated) return;
|
|
1390
|
+
}
|
|
1391
|
+
const hint = {
|
|
1392
|
+
at,
|
|
1393
|
+
fromVisitor: !!fields.lastMessageFromVisitor,
|
|
1394
|
+
attachment: fields.lastMessageAttachment ?? null,
|
|
1395
|
+
...(source === 'sent' ? {
|
|
1396
|
+
phoneClock: true,
|
|
1397
|
+
...(messageId ? {
|
|
1398
|
+
messageId
|
|
1399
|
+
} : {})
|
|
1400
|
+
} : {})
|
|
1401
|
+
};
|
|
1402
|
+
this.lastMessages = Object.fromEntries(Object.entries({
|
|
1403
|
+
...this.lastMessages,
|
|
1404
|
+
[conversationId]: hint
|
|
1405
|
+
}).sort(([, a], [, b]) => Date.parse(b.at) - Date.parse(a.at)).slice(0, LAST_MESSAGE_LIMIT));
|
|
1406
|
+
// Until the stored hints are read, saving would overwrite them with these alone.
|
|
1407
|
+
if (this.lastMessagesLoaded) void _storage.Storage.setItem(_storage.LAST_MESSAGE_KEY, JSON.stringify(this.lastMessages));
|
|
1408
|
+
}
|
|
1409
|
+
async loadLastMessages(epoch) {
|
|
1410
|
+
if (this.lastMessagesLoaded) return;
|
|
1411
|
+
let stored = null;
|
|
1412
|
+
try {
|
|
1413
|
+
stored = JSON.parse((await _storage.Storage.getItem(_storage.LAST_MESSAGE_KEY)) ?? 'null');
|
|
1414
|
+
} catch {
|
|
1415
|
+
stored = null;
|
|
1416
|
+
}
|
|
1417
|
+
// Left unloaded: the init that replaces this one reads them again.
|
|
1418
|
+
if (this.isStale(epoch) || this.lastMessagesLoaded) return;
|
|
1419
|
+
this.lastMessagesLoaded = true;
|
|
1420
|
+
const learned = Object.keys(this.lastMessages).length > 0;
|
|
1421
|
+
if (stored && typeof stored === 'object') {
|
|
1422
|
+
for (const [conversationId, hint] of Object.entries(stored)) {
|
|
1423
|
+
if (typeof hint?.at !== 'string' || typeof hint.fromVisitor !== 'boolean') continue;
|
|
1424
|
+
const known = this.lastMessages[conversationId];
|
|
1425
|
+
if (known && Date.parse(known.at) >= Date.parse(hint.at)) continue;
|
|
1426
|
+
this.lastMessages[conversationId] = {
|
|
1427
|
+
at: hint.at,
|
|
1428
|
+
fromVisitor: hint.fromVisitor,
|
|
1429
|
+
attachment: hint.attachment === 'photo' || hint.attachment === 'file' ? hint.attachment : null,
|
|
1430
|
+
...(hint.phoneClock === true ? {
|
|
1431
|
+
phoneClock: true
|
|
1432
|
+
} : {}),
|
|
1433
|
+
...(typeof hint.messageId === 'string' ? {
|
|
1434
|
+
messageId: hint.messageId
|
|
1435
|
+
} : {})
|
|
1436
|
+
};
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
// Hints learned while the stored ones were being read are saved with them.
|
|
1440
|
+
if (learned) void _storage.Storage.setItem(_storage.LAST_MESSAGE_KEY, JSON.stringify(this.lastMessages));
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
/** A thread's newest page shows who sent its newest message, so its row can say too. */
|
|
1444
|
+
learnLastMessage(conversationId, page) {
|
|
1445
|
+
let newest = null;
|
|
1446
|
+
for (const message of page) {
|
|
1447
|
+
if (!newest || Date.parse(message.createdAt) > Date.parse(newest.createdAt)) newest = message;
|
|
1448
|
+
}
|
|
1449
|
+
const fields = newest && rowFieldsOf(newest);
|
|
1450
|
+
if (!newest || !fields) return;
|
|
1451
|
+
this.rememberLastMessage(conversationId, newest.createdAt, fields, 'history', {
|
|
1452
|
+
pageIds: new Set(page.map(message => message.id))
|
|
1327
1453
|
});
|
|
1454
|
+
this.setState(current => ({
|
|
1455
|
+
conversations: current.conversations.map(row => row.id === conversationId ? this.withLastMessage(row) : row)
|
|
1456
|
+
}));
|
|
1328
1457
|
}
|
|
1329
1458
|
|
|
1330
1459
|
/**
|
|
@@ -1333,16 +1462,22 @@ class WidgetStore {
|
|
|
1333
1462
|
*/
|
|
1334
1463
|
hydrateAttachments(attachments, messageId) {
|
|
1335
1464
|
if (!Array.isArray(attachments) || attachments.length === 0) return null;
|
|
1336
|
-
const hydrated = attachments.filter(item => !!item && typeof item === 'object').map((attachment, index) =>
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1465
|
+
const hydrated = attachments.filter(item => !!item && typeof item === 'object').map((attachment, index) => {
|
|
1466
|
+
const name = typeof attachment.name === 'string' && attachment.name ? attachment.name : 'Attachment';
|
|
1467
|
+
const declared = typeof attachment.contentType === 'string' ? attachment.contentType : '';
|
|
1468
|
+
return {
|
|
1469
|
+
...attachment,
|
|
1470
|
+
id: typeof attachment.id === 'string' || typeof attachment.id === 'number' ? String(attachment.id) : `${messageId ?? 'attachment'}-${index}`,
|
|
1471
|
+
name,
|
|
1472
|
+
// Without a type (or with a generic one) the name says what it is,
|
|
1473
|
+
// so a photo still shows as a photo rather than a file row.
|
|
1474
|
+
contentType: declared && !/^application\/octet-stream$/i.test(declared) ? declared : (0, _pickers.guessContentType)(name) ?? declared,
|
|
1475
|
+
size: typeof attachment.size === 'number' ? attachment.size : 0,
|
|
1476
|
+
// Empty (never the raw value) when the URL isn't http(s) — the preview
|
|
1477
|
+
// then renders the name only, with nothing to open.
|
|
1478
|
+
url: this.client.resolveUrl(typeof attachment.url === 'string' ? attachment.url : null) ?? ''
|
|
1479
|
+
};
|
|
1480
|
+
});
|
|
1346
1481
|
return hydrated.length > 0 ? hydrated : null;
|
|
1347
1482
|
}
|
|
1348
1483
|
clearTypingTimer() {
|
|
@@ -1598,10 +1733,10 @@ class WidgetStore {
|
|
|
1598
1733
|
const result = await this.client.listMessages(conversationId);
|
|
1599
1734
|
// Opening A then B quickly: A's page must never land under B.
|
|
1600
1735
|
if (this.isStale(epoch) || this.threadGeneration !== generation) return;
|
|
1736
|
+
const page = this.hydrateMessages(result.data);
|
|
1601
1737
|
this.setState(current => {
|
|
1602
1738
|
// Live replies and optimistic sends may have arrived since this page
|
|
1603
1739
|
// was requested. Keep them, preferring the live copy for shared ids.
|
|
1604
|
-
const page = this.hydrateMessages(result.data);
|
|
1605
1740
|
const live = adoptOwnServerRows(current.messages, page);
|
|
1606
1741
|
const liveIds = new Set(live.map(message => message.id));
|
|
1607
1742
|
const history = page.filter(message => !liveIds.has(message.id));
|
|
@@ -1612,6 +1747,7 @@ class WidgetStore {
|
|
|
1612
1747
|
messageCursor: result.meta.nextCursor
|
|
1613
1748
|
};
|
|
1614
1749
|
});
|
|
1750
|
+
this.learnLastMessage(conversationId, page);
|
|
1615
1751
|
} catch {
|
|
1616
1752
|
if (!this.isStale(epoch) && this.threadGeneration === generation) {
|
|
1617
1753
|
this.setState({
|
|
@@ -1673,6 +1809,7 @@ class WidgetStore {
|
|
|
1673
1809
|
if (this.isStale(epoch) || this.threadGeneration !== generation) return;
|
|
1674
1810
|
if (this.state.activeConversationId !== conversationId) return;
|
|
1675
1811
|
const fresh = this.hydrateMessages(result.data);
|
|
1812
|
+
this.learnLastMessage(conversationId, fresh);
|
|
1676
1813
|
this.setState(current => {
|
|
1677
1814
|
const own = adoptOwnServerRows(current.messages, fresh);
|
|
1678
1815
|
const known = new Set(own.map(message => message.id));
|
|
@@ -1839,6 +1976,15 @@ class WidgetStore {
|
|
|
1839
1976
|
return this.deliverMessage(tempId, trimmed, attachments, agentUiAction);
|
|
1840
1977
|
}
|
|
1841
1978
|
|
|
1979
|
+
/**
|
|
1980
|
+
* Puts a picked file in storage while the visitor is still writing, so
|
|
1981
|
+
* Send goes out at once. Resolves the token `sendMessage` names it by, and
|
|
1982
|
+
* when that token expires.
|
|
1983
|
+
*/
|
|
1984
|
+
uploadAttachment(file, signal) {
|
|
1985
|
+
return this.client.uploadAttachment(file, signal);
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1842
1988
|
/**
|
|
1843
1989
|
* Re-sends a text bubble that failed. It moves to the end of the thread,
|
|
1844
1990
|
* since it now goes out after anything sent meanwhile. Rejects like
|
|
@@ -1900,23 +2046,30 @@ class WidgetStore {
|
|
|
1900
2046
|
}
|
|
1901
2047
|
let createdId = null;
|
|
1902
2048
|
try {
|
|
1903
|
-
// Local files go up first
|
|
2049
|
+
// Local files go up first, unless the composer already put them up; the
|
|
2050
|
+
// message names each by its upload token.
|
|
1904
2051
|
const sentAttachments = await Promise.all(attachments.map(async ({
|
|
1905
2052
|
name,
|
|
1906
2053
|
url,
|
|
1907
2054
|
fileUri,
|
|
2055
|
+
uploadToken,
|
|
1908
2056
|
contentType,
|
|
1909
2057
|
size
|
|
1910
|
-
}) =>
|
|
2058
|
+
}) => uploadToken ? {
|
|
2059
|
+
name,
|
|
2060
|
+
contentType,
|
|
2061
|
+
size,
|
|
2062
|
+
uploadToken
|
|
2063
|
+
} : fileUri ? {
|
|
1911
2064
|
name,
|
|
1912
2065
|
contentType,
|
|
1913
2066
|
size,
|
|
1914
|
-
uploadToken: await this.client.uploadAttachment({
|
|
2067
|
+
uploadToken: (await this.client.uploadAttachment({
|
|
1915
2068
|
uri: fileUri,
|
|
1916
2069
|
name,
|
|
1917
2070
|
contentType,
|
|
1918
2071
|
size
|
|
1919
|
-
})
|
|
2072
|
+
})).uploadToken
|
|
1920
2073
|
} : {
|
|
1921
2074
|
name,
|
|
1922
2075
|
url,
|
|
@@ -1938,6 +2091,11 @@ class WidgetStore {
|
|
|
1938
2091
|
});
|
|
1939
2092
|
if (this.isStale(epoch)) return;
|
|
1940
2093
|
if (ownCreation) createdId = result.conversationId;
|
|
2094
|
+
|
|
2095
|
+
// Workstation doesn't stream the visitor's own messages back, so the
|
|
2096
|
+
// list row learns about this one here.
|
|
2097
|
+
const sentAt = new Date().toISOString();
|
|
2098
|
+
const sentRow = rowFieldsFor(trimmed, attachments.map(attachment => attachment.contentType), true);
|
|
1941
2099
|
this.setState(current => {
|
|
1942
2100
|
// The stream or a catch-up fetch may have delivered this message
|
|
1943
2101
|
// already — keep one copy, never two bubbles with the same id.
|
|
@@ -1947,18 +2105,27 @@ class WidgetStore {
|
|
|
1947
2105
|
id: result.messageId,
|
|
1948
2106
|
status: this.earlyStatuses.get(result.messageId) ?? 'sent'
|
|
1949
2107
|
} : message);
|
|
1950
|
-
if (!createdId)
|
|
1951
|
-
|
|
1952
|
-
|
|
2108
|
+
if (!createdId) {
|
|
2109
|
+
return {
|
|
2110
|
+
messages,
|
|
2111
|
+
conversations: current.conversations.map(row => row.id === result.conversationId ? {
|
|
2112
|
+
...row,
|
|
2113
|
+
...sentRow,
|
|
2114
|
+
lastMessageAt: sentAt,
|
|
2115
|
+
messageCount: row.messageCount + (delivered ? 0 : 1)
|
|
2116
|
+
} : row)
|
|
2117
|
+
};
|
|
2118
|
+
}
|
|
1953
2119
|
const known = current.conversations.some(item => item.id === result.conversationId);
|
|
1954
2120
|
const conversation = {
|
|
1955
2121
|
id: result.conversationId,
|
|
1956
2122
|
subject: null,
|
|
1957
2123
|
status: 'open',
|
|
1958
|
-
snippet:
|
|
2124
|
+
snippet: null,
|
|
2125
|
+
...sentRow,
|
|
1959
2126
|
messageCount: 1,
|
|
1960
|
-
lastMessageAt:
|
|
1961
|
-
createdAt:
|
|
2127
|
+
lastMessageAt: sentAt,
|
|
2128
|
+
createdAt: sentAt,
|
|
1962
2129
|
agentName: null,
|
|
1963
2130
|
agentAvatar: null
|
|
1964
2131
|
};
|
|
@@ -1971,6 +2138,9 @@ class WidgetStore {
|
|
|
1971
2138
|
} : {})
|
|
1972
2139
|
};
|
|
1973
2140
|
});
|
|
2141
|
+
this.rememberLastMessage(result.conversationId, sentAt, sentRow, 'sent', {
|
|
2142
|
+
messageId: result.messageId
|
|
2143
|
+
});
|
|
1974
2144
|
|
|
1975
2145
|
// New files to fetch the stored copy of: a fresh round of re-reads.
|
|
1976
2146
|
const sent = this.state.messages.find(message => message.id === result.messageId);
|
|
@@ -2494,6 +2664,65 @@ function asDeletedPlaceholder(message, deletedAt) {
|
|
|
2494
2664
|
};
|
|
2495
2665
|
}
|
|
2496
2666
|
|
|
2667
|
+
/** A message's words on one line; replies written in Workstation may only carry HTML. */
|
|
2668
|
+
function plainTextOf(message) {
|
|
2669
|
+
return (message.bodyPlain || (0, _html.stripHtml)(message.body ?? '')).replace(/\s+/g, ' ').trim();
|
|
2670
|
+
}
|
|
2671
|
+
|
|
2672
|
+
/**
|
|
2673
|
+
* The newest message of a thread as this device saw it. `at` is its
|
|
2674
|
+
* `createdAt`, which Workstation's list repeats as `lastMessageAt`; for a sent
|
|
2675
|
+
* message it's the phone's clock instead.
|
|
2676
|
+
*/
|
|
2677
|
+
|
|
2678
|
+
/** `snippet` is absent when there's nothing to quote: the row keeps its words. */
|
|
2679
|
+
|
|
2680
|
+
/** The list row once `message` is the newest. Null keeps the row as it is: system rows and notes. */
|
|
2681
|
+
function rowFieldsOf(message) {
|
|
2682
|
+
if (message.messageType && message.messageType !== 'message') return null;
|
|
2683
|
+
return rowFieldsFor(plainTextOf(message), (message.attachments ?? []).map(attachment => attachment.contentType), message.senderType === 'visitor' || message.direction === 'inbound', /<img\b/i.test(message.body ?? ''));
|
|
2684
|
+
}
|
|
2685
|
+
|
|
2686
|
+
/**
|
|
2687
|
+
* Its words, or with none, whether it carried only photos or other files too.
|
|
2688
|
+
* A message with neither (a card) still says who sent it, so a teammate's
|
|
2689
|
+
* card never leaves "You:" on the visitor's earlier message.
|
|
2690
|
+
*/
|
|
2691
|
+
function rowFieldsFor(text, contentTypes, fromVisitor, inlinePhoto = false) {
|
|
2692
|
+
if (text) return {
|
|
2693
|
+
snippet: text,
|
|
2694
|
+
lastMessageAttachment: null,
|
|
2695
|
+
lastMessageFromVisitor: fromVisitor
|
|
2696
|
+
};
|
|
2697
|
+
const count = contentTypes.length;
|
|
2698
|
+
if (count === 0 && !inlinePhoto) return {
|
|
2699
|
+
lastMessageAttachment: null,
|
|
2700
|
+
lastMessageFromVisitor: fromVisitor
|
|
2701
|
+
};
|
|
2702
|
+
return {
|
|
2703
|
+
// Workstation's list words it this way too, so apps reading `snippet` see the same after a reload.
|
|
2704
|
+
...(count > 0 ? {
|
|
2705
|
+
snippet: `${count} attachment${count === 1 ? '' : 's'}`
|
|
2706
|
+
} : {}),
|
|
2707
|
+
lastMessageAttachment: contentTypes.every(type => (0, _pickers.isImageAttachment)(type)) ? 'photo' : 'file',
|
|
2708
|
+
lastMessageFromVisitor: fromVisitor
|
|
2709
|
+
};
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2712
|
+
/**
|
|
2713
|
+
* The list stamps a conversation just after storing its message, so its time
|
|
2714
|
+
* trails the message's own by a few milliseconds, never more than `LIST_LAG_MS`.
|
|
2715
|
+
* A sent message is timed by the phone instead: a little drift either way
|
|
2716
|
+
* still matches, but only when the list rules out a team reply the visitor
|
|
2717
|
+
* hasn't read. Older Workstation deployments don't send `unreadCount`, so
|
|
2718
|
+
* there a sent message must match like any other.
|
|
2719
|
+
*/
|
|
2720
|
+
function describesSameMessage(conversation, known) {
|
|
2721
|
+
const lag = Date.parse(conversation.lastMessageAt ?? '') - Date.parse(known.at);
|
|
2722
|
+
if (lag >= 0 && lag <= LIST_LAG_MS) return true;
|
|
2723
|
+
return !!known.phoneClock && Math.abs(lag) <= SAME_MESSAGE_DRIFT_MS && conversation.unreadCount === 0;
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2497
2726
|
/** A deletion missed while the stream was down shows up on the next re-read. */
|
|
2498
2727
|
function adoptServerDeletions(messages, server) {
|
|
2499
2728
|
const deleted = new Map(server.filter(message => message.deletedAt).map(message => [message.id, message.deletedAt]));
|
|
@@ -2541,13 +2770,31 @@ function mergeAttachments(local, server) {
|
|
|
2541
2770
|
if (local.length !== server.length) {
|
|
2542
2771
|
return server.every(attachment => attachment.url) ? server : local;
|
|
2543
2772
|
}
|
|
2773
|
+
const owns = pairByName(local, server);
|
|
2544
2774
|
const merged = server.map((attachment, index) => {
|
|
2545
|
-
const own =
|
|
2546
|
-
|
|
2775
|
+
const own = owns[index];
|
|
2776
|
+
if (!isLocalAttachment(own) || !attachment.url) return own;
|
|
2777
|
+
(0, _localPreviews.rememberLocalPreview)(attachment.url, own.url);
|
|
2778
|
+
return attachment;
|
|
2547
2779
|
});
|
|
2548
2780
|
return merged.every((attachment, index) => attachment === local[index]) ? local : merged;
|
|
2549
2781
|
}
|
|
2550
2782
|
|
|
2783
|
+
/**
|
|
2784
|
+
* The local attachment each server one replaces. By name first: Workstation
|
|
2785
|
+
* may store a message's files in another order than they were sent, and a
|
|
2786
|
+
* photo paired with the wrong file would show it for the rest of the session.
|
|
2787
|
+
* Any it renamed pair up in the order left.
|
|
2788
|
+
*/
|
|
2789
|
+
function pairByName(local, server) {
|
|
2790
|
+
const unpaired = [...local];
|
|
2791
|
+
const named = server.map(attachment => {
|
|
2792
|
+
const index = unpaired.findIndex(own => own.name === attachment.name);
|
|
2793
|
+
return index < 0 ? null : unpaired.splice(index, 1)[0];
|
|
2794
|
+
});
|
|
2795
|
+
return named.map(own => own ?? unpaired.shift());
|
|
2796
|
+
}
|
|
2797
|
+
|
|
2551
2798
|
/**
|
|
2552
2799
|
* Newer Workstation deployments echo `clientMessageId` on the visitor's own
|
|
2553
2800
|
* rows. Such a row replaces the pending or failed bubble it came from instead
|