@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
|
@@ -16,14 +16,17 @@ import { WidgetRealtime } from "../api/realtime.js";
|
|
|
16
16
|
import { getDeviceId, rotateDeviceId } from "../device-id.js";
|
|
17
17
|
import { EventEmitter, RepzoWidgetEvents } from "../events.js";
|
|
18
18
|
import { PushManager } from "../push/push-notifications.js";
|
|
19
|
-
import { CONFIG_KEY, LEGACY_CONFIG_KEY, SecureStorage, Storage, UNREAD_KEY, USER_KEY } from "../storage.js";
|
|
20
|
-
import {
|
|
19
|
+
import { CONFIG_KEY, LAST_MESSAGE_KEY, LEGACY_CONFIG_KEY, SecureStorage, Storage, UNREAD_KEY, USER_KEY } from "../storage.js";
|
|
20
|
+
import { clearLocalPreviews, rememberLocalPreview } from "../attachments/local-previews.js";
|
|
21
|
+
import { guessContentType, isImageAttachment, setAttachmentOptions } from "../attachments/pickers.js";
|
|
21
22
|
import { DEFAULT_BASE_URL } from "../types.js";
|
|
23
|
+
import { clearRetainedState } from "../ui/hooks/use-retained-state.js";
|
|
22
24
|
import { canRetryMessage, isLocalMessage } from "../ui/utils/conversations.js";
|
|
25
|
+
import { stripHtml } from "../ui/utils/html.js";
|
|
23
26
|
import { setOpenUrlHandler } from "../ui/utils/safe-url.js";
|
|
24
27
|
|
|
25
28
|
/** Sent on every init; must match `package.json` (tests/version.test.js checks). */
|
|
26
|
-
const SDK_VERSION = '@repzo/widget-react-native@1.
|
|
29
|
+
const SDK_VERSION = '@repzo/widget-react-native@1.4.0';
|
|
27
30
|
|
|
28
31
|
/** The platform this session reports — mobile sessions get the Mobile SDKs overrides. */
|
|
29
32
|
function sessionPlatform() {
|
|
@@ -38,6 +41,12 @@ const TYPING_SETTLE_MS = 3_000;
|
|
|
38
41
|
const TYPING_PING_INTERVAL_MS = 4_000;
|
|
39
42
|
/** Guards against the SSE stream replaying messages after a reconnect. */
|
|
40
43
|
const SEEN_MESSAGE_LIMIT = 100;
|
|
44
|
+
/** Threads whose newest message is remembered across launches. */
|
|
45
|
+
const LAST_MESSAGE_LIMIT = 50;
|
|
46
|
+
/** How far the phone's clock may be from Workstation's for a sent message to still match the list. */
|
|
47
|
+
const SAME_MESSAGE_DRIFT_MS = 120_000;
|
|
48
|
+
/** How far the list's `lastMessageAt` may trail the message's own `createdAt` (7 ms seen live). */
|
|
49
|
+
const LIST_LAG_MS = 1_000;
|
|
41
50
|
/** Logout's server calls are best-effort — a dead network must not block sign-out. */
|
|
42
51
|
const LOGOUT_REQUEST_TIMEOUT_MS = 5_000;
|
|
43
52
|
/** A server that 401s every fresh stream must not spin init (10/min limit). */
|
|
@@ -131,6 +140,10 @@ export class WidgetStore {
|
|
|
131
140
|
|
|
132
141
|
/** `boot({ posts })` — Send posts over the app. */
|
|
133
142
|
postsEnabled = true;
|
|
143
|
+
/** `boot({ photoAlbums })` — a message's photos share one album in the thread. */
|
|
144
|
+
photoAlbumsEnabled = true;
|
|
145
|
+
/** `boot({ youPrefix })` — "You:" before the visitor's own newest message in the lists. */
|
|
146
|
+
youPrefixEnabled = true;
|
|
134
147
|
|
|
135
148
|
/** Posts whose view was reported this session (the server also dedupes per day). */
|
|
136
149
|
viewedPostIds = new Set();
|
|
@@ -185,6 +198,10 @@ export class WidgetStore {
|
|
|
185
198
|
lastTypingPingAt = 0;
|
|
186
199
|
visitorTypingActive = false;
|
|
187
200
|
seenMessageIds = [];
|
|
201
|
+
|
|
202
|
+
/** Who sent each thread's newest message and what it carried; see `withLastMessage`. */
|
|
203
|
+
lastMessages = {};
|
|
204
|
+
lastMessagesLoaded = false;
|
|
188
205
|
/** Statuses (e.g. "read") that arrived for an id before a send response revealed it. */
|
|
189
206
|
earlyStatuses = new Map();
|
|
190
207
|
/** The agent UI action behind a failed bubble, so Retry runs it again too. */
|
|
@@ -264,6 +281,8 @@ export class WidgetStore {
|
|
|
264
281
|
|
|
265
282
|
/** Composer identity: explicit opens reset it; a draft receiving its server ID does not. */
|
|
266
283
|
getThreadGeneration = () => this.threadGeneration;
|
|
284
|
+
showsPhotoAlbums = () => this.photoAlbumsEnabled;
|
|
285
|
+
showsYouPrefix = () => this.youPrefixEnabled;
|
|
267
286
|
setState(patch) {
|
|
268
287
|
const previous = this.state;
|
|
269
288
|
const resolved = typeof patch === 'function' ? patch(previous) : patch;
|
|
@@ -335,6 +354,8 @@ export class WidgetStore {
|
|
|
335
354
|
if (options.inAppNotifications !== undefined) {
|
|
336
355
|
this.inAppNotificationsEnabled = options.inAppNotifications !== false;
|
|
337
356
|
}
|
|
357
|
+
if (options.photoAlbums !== undefined) this.photoAlbumsEnabled = options.photoAlbums !== false;
|
|
358
|
+
if (options.youPrefix !== undefined) this.youPrefixEnabled = options.youPrefix !== false;
|
|
338
359
|
if (options.posts !== undefined) {
|
|
339
360
|
const wasEnabled = this.postsEnabled;
|
|
340
361
|
this.postsEnabled = options.posts !== false;
|
|
@@ -489,13 +510,13 @@ export class WidgetStore {
|
|
|
489
510
|
// queued op must never see one without the other.
|
|
490
511
|
await Promise.all([this.client.setToken(result.token), userKey ? SecureStorage.setItem(USER_KEY, userKey) : SecureStorage.removeItem(USER_KEY)]);
|
|
491
512
|
if (this.isStale(epoch)) return;
|
|
492
|
-
const conversations = result.conversations.map(conversation => this.hydrateConversation(conversation));
|
|
493
513
|
|
|
494
514
|
// Server counts win (they include replies pushed while the app was
|
|
495
515
|
// killed); the locally persisted map only covers older Workstation
|
|
496
516
|
// deployments that don't send unreadCount yet.
|
|
497
|
-
const storedUnread = await this.loadUnread();
|
|
517
|
+
const [, storedUnread] = await Promise.all([this.loadLastMessages(epoch), this.loadUnread()]);
|
|
498
518
|
if (this.isStale(epoch)) return;
|
|
519
|
+
const conversations = result.conversations.map(conversation => this.hydrateConversation(conversation));
|
|
499
520
|
const unreadByConversation = {};
|
|
500
521
|
for (const conversation of conversations) {
|
|
501
522
|
const count = typeof conversation.unreadCount === 'number' ? conversation.unreadCount : storedUnread[conversation.id] ?? 0;
|
|
@@ -786,16 +807,20 @@ export class WidgetStore {
|
|
|
786
807
|
this.userKeyLoaded = true;
|
|
787
808
|
this.pendingAgentByConversation = {};
|
|
788
809
|
this.seenMessageIds = [];
|
|
810
|
+
this.lastMessages = {};
|
|
811
|
+
this.lastMessagesLoaded = false;
|
|
789
812
|
this.earlyStatuses.clear();
|
|
790
813
|
this.visitorTypingActive = false;
|
|
791
814
|
this.lastTypingPingAt = 0;
|
|
792
815
|
this.launchCardOffered = false;
|
|
793
816
|
this.resetPost();
|
|
817
|
+
clearLocalPreviews();
|
|
818
|
+
clearRetainedState();
|
|
794
819
|
this.setState({
|
|
795
820
|
...USER_SCOPED_STATE
|
|
796
821
|
});
|
|
797
822
|
const token = await this.client.getToken();
|
|
798
|
-
await Promise.all([this.client.clearToken(), SecureStorage.removeItem(USER_KEY), Storage.removeItem(UNREAD_KEY),
|
|
823
|
+
await Promise.all([this.client.clearToken(), SecureStorage.removeItem(USER_KEY), Storage.removeItem(UNREAD_KEY), Storage.removeItem(LAST_MESSAGE_KEY),
|
|
799
824
|
// A new anonymous visitor on this device gets a new tracking identity.
|
|
800
825
|
rotateDeviceId()]);
|
|
801
826
|
|
|
@@ -1175,10 +1200,11 @@ export class WidgetStore {
|
|
|
1175
1200
|
|
|
1176
1201
|
// Set inside the updater, read after: `setState` runs synchronously.
|
|
1177
1202
|
let countedAsUnread = false;
|
|
1203
|
+
const rowFields = rowFieldsOf(message);
|
|
1178
1204
|
this.setState(current => {
|
|
1179
1205
|
const conversations = conversationId ? current.conversations.map(conversation => conversation.id === conversationId ? {
|
|
1180
1206
|
...conversation,
|
|
1181
|
-
|
|
1207
|
+
...rowFields,
|
|
1182
1208
|
lastMessageAt: message.createdAt,
|
|
1183
1209
|
messageCount: conversation.messageCount + 1
|
|
1184
1210
|
} : conversation) : current.conversations;
|
|
@@ -1216,11 +1242,12 @@ export class WidgetStore {
|
|
|
1216
1242
|
messageId,
|
|
1217
1243
|
senderName: message.senderName || conversation?.agentName || null,
|
|
1218
1244
|
senderAvatar: conversation?.agentAvatar ?? null,
|
|
1219
|
-
preview: (message
|
|
1245
|
+
preview: plainTextOf(message),
|
|
1220
1246
|
hasAttachments: (message.attachments?.length ?? 0) > 0
|
|
1221
1247
|
}
|
|
1222
1248
|
};
|
|
1223
1249
|
});
|
|
1250
|
+
if (conversationId && rowFields) this.rememberLastMessage(conversationId, message.createdAt, rowFields, 'stream');
|
|
1224
1251
|
|
|
1225
1252
|
// The visitor is reading it live — move the server cursor too, or the
|
|
1226
1253
|
// next list/init would badge a reply they already saw.
|
|
@@ -1318,10 +1345,112 @@ export class WidgetStore {
|
|
|
1318
1345
|
};
|
|
1319
1346
|
}
|
|
1320
1347
|
hydrateConversation(conversation) {
|
|
1321
|
-
return this.applyConversationAgent({
|
|
1348
|
+
return this.applyConversationAgent(this.withLastMessage({
|
|
1322
1349
|
...conversation,
|
|
1323
1350
|
agentAvatar: this.client.resolveUrl(conversation.agentAvatar)
|
|
1351
|
+
}));
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
/**
|
|
1355
|
+
* Workstation's list doesn't say who sent the newest message, and calls one
|
|
1356
|
+
* without words "2 attachments". While a row still describes the message
|
|
1357
|
+
* this device saw last, both are filled in from it. `snippet` keeps the
|
|
1358
|
+
* list's words: apps read it, and the lists draw "Photo" or "File" from
|
|
1359
|
+
* `lastMessageAttachment` instead.
|
|
1360
|
+
*/
|
|
1361
|
+
withLastMessage(conversation) {
|
|
1362
|
+
const known = this.lastMessages[conversation.id];
|
|
1363
|
+
if (!known || !describesSameMessage(conversation, known)) return conversation;
|
|
1364
|
+
// A list that says who sent it, or what it carried, is used as is.
|
|
1365
|
+
return {
|
|
1366
|
+
...conversation,
|
|
1367
|
+
lastMessageFromVisitor: conversation.lastMessageFromVisitor ?? known.fromVisitor,
|
|
1368
|
+
lastMessageAttachment: conversation.lastMessageAttachment !== undefined ? conversation.lastMessageAttachment : known.attachment
|
|
1369
|
+
};
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
/**
|
|
1373
|
+
* A send or a streamed message is the newest by arrival, whatever the two
|
|
1374
|
+
* clocks say; a history page only replaces something older. A sent message
|
|
1375
|
+
* is timed by the phone, which may run ahead of Workstation, so instead a
|
|
1376
|
+
* page that has it replaces it: whatever is newest there came after it, and
|
|
1377
|
+
* Workstation's own time then stands in for the phone's.
|
|
1378
|
+
*/
|
|
1379
|
+
rememberLastMessage(conversationId, at, fields, source, {
|
|
1380
|
+
messageId,
|
|
1381
|
+
pageIds
|
|
1382
|
+
} = {}) {
|
|
1383
|
+
const known = this.lastMessages[conversationId];
|
|
1384
|
+
if (source === 'history' && known) {
|
|
1385
|
+
const outdated = known.phoneClock && known.messageId ? !pageIds?.has(known.messageId) : Date.parse(known.at) > Date.parse(at);
|
|
1386
|
+
if (outdated) return;
|
|
1387
|
+
}
|
|
1388
|
+
const hint = {
|
|
1389
|
+
at,
|
|
1390
|
+
fromVisitor: !!fields.lastMessageFromVisitor,
|
|
1391
|
+
attachment: fields.lastMessageAttachment ?? null,
|
|
1392
|
+
...(source === 'sent' ? {
|
|
1393
|
+
phoneClock: true,
|
|
1394
|
+
...(messageId ? {
|
|
1395
|
+
messageId
|
|
1396
|
+
} : {})
|
|
1397
|
+
} : {})
|
|
1398
|
+
};
|
|
1399
|
+
this.lastMessages = Object.fromEntries(Object.entries({
|
|
1400
|
+
...this.lastMessages,
|
|
1401
|
+
[conversationId]: hint
|
|
1402
|
+
}).sort(([, a], [, b]) => Date.parse(b.at) - Date.parse(a.at)).slice(0, LAST_MESSAGE_LIMIT));
|
|
1403
|
+
// Until the stored hints are read, saving would overwrite them with these alone.
|
|
1404
|
+
if (this.lastMessagesLoaded) void Storage.setItem(LAST_MESSAGE_KEY, JSON.stringify(this.lastMessages));
|
|
1405
|
+
}
|
|
1406
|
+
async loadLastMessages(epoch) {
|
|
1407
|
+
if (this.lastMessagesLoaded) return;
|
|
1408
|
+
let stored = null;
|
|
1409
|
+
try {
|
|
1410
|
+
stored = JSON.parse((await Storage.getItem(LAST_MESSAGE_KEY)) ?? 'null');
|
|
1411
|
+
} catch {
|
|
1412
|
+
stored = null;
|
|
1413
|
+
}
|
|
1414
|
+
// Left unloaded: the init that replaces this one reads them again.
|
|
1415
|
+
if (this.isStale(epoch) || this.lastMessagesLoaded) return;
|
|
1416
|
+
this.lastMessagesLoaded = true;
|
|
1417
|
+
const learned = Object.keys(this.lastMessages).length > 0;
|
|
1418
|
+
if (stored && typeof stored === 'object') {
|
|
1419
|
+
for (const [conversationId, hint] of Object.entries(stored)) {
|
|
1420
|
+
if (typeof hint?.at !== 'string' || typeof hint.fromVisitor !== 'boolean') continue;
|
|
1421
|
+
const known = this.lastMessages[conversationId];
|
|
1422
|
+
if (known && Date.parse(known.at) >= Date.parse(hint.at)) continue;
|
|
1423
|
+
this.lastMessages[conversationId] = {
|
|
1424
|
+
at: hint.at,
|
|
1425
|
+
fromVisitor: hint.fromVisitor,
|
|
1426
|
+
attachment: hint.attachment === 'photo' || hint.attachment === 'file' ? hint.attachment : null,
|
|
1427
|
+
...(hint.phoneClock === true ? {
|
|
1428
|
+
phoneClock: true
|
|
1429
|
+
} : {}),
|
|
1430
|
+
...(typeof hint.messageId === 'string' ? {
|
|
1431
|
+
messageId: hint.messageId
|
|
1432
|
+
} : {})
|
|
1433
|
+
};
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
// Hints learned while the stored ones were being read are saved with them.
|
|
1437
|
+
if (learned) void Storage.setItem(LAST_MESSAGE_KEY, JSON.stringify(this.lastMessages));
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
/** A thread's newest page shows who sent its newest message, so its row can say too. */
|
|
1441
|
+
learnLastMessage(conversationId, page) {
|
|
1442
|
+
let newest = null;
|
|
1443
|
+
for (const message of page) {
|
|
1444
|
+
if (!newest || Date.parse(message.createdAt) > Date.parse(newest.createdAt)) newest = message;
|
|
1445
|
+
}
|
|
1446
|
+
const fields = newest && rowFieldsOf(newest);
|
|
1447
|
+
if (!newest || !fields) return;
|
|
1448
|
+
this.rememberLastMessage(conversationId, newest.createdAt, fields, 'history', {
|
|
1449
|
+
pageIds: new Set(page.map(message => message.id))
|
|
1324
1450
|
});
|
|
1451
|
+
this.setState(current => ({
|
|
1452
|
+
conversations: current.conversations.map(row => row.id === conversationId ? this.withLastMessage(row) : row)
|
|
1453
|
+
}));
|
|
1325
1454
|
}
|
|
1326
1455
|
|
|
1327
1456
|
/**
|
|
@@ -1330,16 +1459,22 @@ export class WidgetStore {
|
|
|
1330
1459
|
*/
|
|
1331
1460
|
hydrateAttachments(attachments, messageId) {
|
|
1332
1461
|
if (!Array.isArray(attachments) || attachments.length === 0) return null;
|
|
1333
|
-
const hydrated = attachments.filter(item => !!item && typeof item === 'object').map((attachment, index) =>
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1462
|
+
const hydrated = attachments.filter(item => !!item && typeof item === 'object').map((attachment, index) => {
|
|
1463
|
+
const name = typeof attachment.name === 'string' && attachment.name ? attachment.name : 'Attachment';
|
|
1464
|
+
const declared = typeof attachment.contentType === 'string' ? attachment.contentType : '';
|
|
1465
|
+
return {
|
|
1466
|
+
...attachment,
|
|
1467
|
+
id: typeof attachment.id === 'string' || typeof attachment.id === 'number' ? String(attachment.id) : `${messageId ?? 'attachment'}-${index}`,
|
|
1468
|
+
name,
|
|
1469
|
+
// Without a type (or with a generic one) the name says what it is,
|
|
1470
|
+
// so a photo still shows as a photo rather than a file row.
|
|
1471
|
+
contentType: declared && !/^application\/octet-stream$/i.test(declared) ? declared : guessContentType(name) ?? declared,
|
|
1472
|
+
size: typeof attachment.size === 'number' ? attachment.size : 0,
|
|
1473
|
+
// Empty (never the raw value) when the URL isn't http(s) — the preview
|
|
1474
|
+
// then renders the name only, with nothing to open.
|
|
1475
|
+
url: this.client.resolveUrl(typeof attachment.url === 'string' ? attachment.url : null) ?? ''
|
|
1476
|
+
};
|
|
1477
|
+
});
|
|
1343
1478
|
return hydrated.length > 0 ? hydrated : null;
|
|
1344
1479
|
}
|
|
1345
1480
|
clearTypingTimer() {
|
|
@@ -1595,10 +1730,10 @@ export class WidgetStore {
|
|
|
1595
1730
|
const result = await this.client.listMessages(conversationId);
|
|
1596
1731
|
// Opening A then B quickly: A's page must never land under B.
|
|
1597
1732
|
if (this.isStale(epoch) || this.threadGeneration !== generation) return;
|
|
1733
|
+
const page = this.hydrateMessages(result.data);
|
|
1598
1734
|
this.setState(current => {
|
|
1599
1735
|
// Live replies and optimistic sends may have arrived since this page
|
|
1600
1736
|
// was requested. Keep them, preferring the live copy for shared ids.
|
|
1601
|
-
const page = this.hydrateMessages(result.data);
|
|
1602
1737
|
const live = adoptOwnServerRows(current.messages, page);
|
|
1603
1738
|
const liveIds = new Set(live.map(message => message.id));
|
|
1604
1739
|
const history = page.filter(message => !liveIds.has(message.id));
|
|
@@ -1609,6 +1744,7 @@ export class WidgetStore {
|
|
|
1609
1744
|
messageCursor: result.meta.nextCursor
|
|
1610
1745
|
};
|
|
1611
1746
|
});
|
|
1747
|
+
this.learnLastMessage(conversationId, page);
|
|
1612
1748
|
} catch {
|
|
1613
1749
|
if (!this.isStale(epoch) && this.threadGeneration === generation) {
|
|
1614
1750
|
this.setState({
|
|
@@ -1670,6 +1806,7 @@ export class WidgetStore {
|
|
|
1670
1806
|
if (this.isStale(epoch) || this.threadGeneration !== generation) return;
|
|
1671
1807
|
if (this.state.activeConversationId !== conversationId) return;
|
|
1672
1808
|
const fresh = this.hydrateMessages(result.data);
|
|
1809
|
+
this.learnLastMessage(conversationId, fresh);
|
|
1673
1810
|
this.setState(current => {
|
|
1674
1811
|
const own = adoptOwnServerRows(current.messages, fresh);
|
|
1675
1812
|
const known = new Set(own.map(message => message.id));
|
|
@@ -1836,6 +1973,15 @@ export class WidgetStore {
|
|
|
1836
1973
|
return this.deliverMessage(tempId, trimmed, attachments, agentUiAction);
|
|
1837
1974
|
}
|
|
1838
1975
|
|
|
1976
|
+
/**
|
|
1977
|
+
* Puts a picked file in storage while the visitor is still writing, so
|
|
1978
|
+
* Send goes out at once. Resolves the token `sendMessage` names it by, and
|
|
1979
|
+
* when that token expires.
|
|
1980
|
+
*/
|
|
1981
|
+
uploadAttachment(file, signal) {
|
|
1982
|
+
return this.client.uploadAttachment(file, signal);
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1839
1985
|
/**
|
|
1840
1986
|
* Re-sends a text bubble that failed. It moves to the end of the thread,
|
|
1841
1987
|
* since it now goes out after anything sent meanwhile. Rejects like
|
|
@@ -1897,23 +2043,30 @@ export class WidgetStore {
|
|
|
1897
2043
|
}
|
|
1898
2044
|
let createdId = null;
|
|
1899
2045
|
try {
|
|
1900
|
-
// Local files go up first
|
|
2046
|
+
// Local files go up first, unless the composer already put them up; the
|
|
2047
|
+
// message names each by its upload token.
|
|
1901
2048
|
const sentAttachments = await Promise.all(attachments.map(async ({
|
|
1902
2049
|
name,
|
|
1903
2050
|
url,
|
|
1904
2051
|
fileUri,
|
|
2052
|
+
uploadToken,
|
|
1905
2053
|
contentType,
|
|
1906
2054
|
size
|
|
1907
|
-
}) =>
|
|
2055
|
+
}) => uploadToken ? {
|
|
2056
|
+
name,
|
|
2057
|
+
contentType,
|
|
2058
|
+
size,
|
|
2059
|
+
uploadToken
|
|
2060
|
+
} : fileUri ? {
|
|
1908
2061
|
name,
|
|
1909
2062
|
contentType,
|
|
1910
2063
|
size,
|
|
1911
|
-
uploadToken: await this.client.uploadAttachment({
|
|
2064
|
+
uploadToken: (await this.client.uploadAttachment({
|
|
1912
2065
|
uri: fileUri,
|
|
1913
2066
|
name,
|
|
1914
2067
|
contentType,
|
|
1915
2068
|
size
|
|
1916
|
-
})
|
|
2069
|
+
})).uploadToken
|
|
1917
2070
|
} : {
|
|
1918
2071
|
name,
|
|
1919
2072
|
url,
|
|
@@ -1935,6 +2088,11 @@ export class WidgetStore {
|
|
|
1935
2088
|
});
|
|
1936
2089
|
if (this.isStale(epoch)) return;
|
|
1937
2090
|
if (ownCreation) createdId = result.conversationId;
|
|
2091
|
+
|
|
2092
|
+
// Workstation doesn't stream the visitor's own messages back, so the
|
|
2093
|
+
// list row learns about this one here.
|
|
2094
|
+
const sentAt = new Date().toISOString();
|
|
2095
|
+
const sentRow = rowFieldsFor(trimmed, attachments.map(attachment => attachment.contentType), true);
|
|
1938
2096
|
this.setState(current => {
|
|
1939
2097
|
// The stream or a catch-up fetch may have delivered this message
|
|
1940
2098
|
// already — keep one copy, never two bubbles with the same id.
|
|
@@ -1944,18 +2102,27 @@ export class WidgetStore {
|
|
|
1944
2102
|
id: result.messageId,
|
|
1945
2103
|
status: this.earlyStatuses.get(result.messageId) ?? 'sent'
|
|
1946
2104
|
} : message);
|
|
1947
|
-
if (!createdId)
|
|
1948
|
-
|
|
1949
|
-
|
|
2105
|
+
if (!createdId) {
|
|
2106
|
+
return {
|
|
2107
|
+
messages,
|
|
2108
|
+
conversations: current.conversations.map(row => row.id === result.conversationId ? {
|
|
2109
|
+
...row,
|
|
2110
|
+
...sentRow,
|
|
2111
|
+
lastMessageAt: sentAt,
|
|
2112
|
+
messageCount: row.messageCount + (delivered ? 0 : 1)
|
|
2113
|
+
} : row)
|
|
2114
|
+
};
|
|
2115
|
+
}
|
|
1950
2116
|
const known = current.conversations.some(item => item.id === result.conversationId);
|
|
1951
2117
|
const conversation = {
|
|
1952
2118
|
id: result.conversationId,
|
|
1953
2119
|
subject: null,
|
|
1954
2120
|
status: 'open',
|
|
1955
|
-
snippet:
|
|
2121
|
+
snippet: null,
|
|
2122
|
+
...sentRow,
|
|
1956
2123
|
messageCount: 1,
|
|
1957
|
-
lastMessageAt:
|
|
1958
|
-
createdAt:
|
|
2124
|
+
lastMessageAt: sentAt,
|
|
2125
|
+
createdAt: sentAt,
|
|
1959
2126
|
agentName: null,
|
|
1960
2127
|
agentAvatar: null
|
|
1961
2128
|
};
|
|
@@ -1968,6 +2135,9 @@ export class WidgetStore {
|
|
|
1968
2135
|
} : {})
|
|
1969
2136
|
};
|
|
1970
2137
|
});
|
|
2138
|
+
this.rememberLastMessage(result.conversationId, sentAt, sentRow, 'sent', {
|
|
2139
|
+
messageId: result.messageId
|
|
2140
|
+
});
|
|
1971
2141
|
|
|
1972
2142
|
// New files to fetch the stored copy of: a fresh round of re-reads.
|
|
1973
2143
|
const sent = this.state.messages.find(message => message.id === result.messageId);
|
|
@@ -2490,6 +2660,65 @@ function asDeletedPlaceholder(message, deletedAt) {
|
|
|
2490
2660
|
};
|
|
2491
2661
|
}
|
|
2492
2662
|
|
|
2663
|
+
/** A message's words on one line; replies written in Workstation may only carry HTML. */
|
|
2664
|
+
function plainTextOf(message) {
|
|
2665
|
+
return (message.bodyPlain || stripHtml(message.body ?? '')).replace(/\s+/g, ' ').trim();
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2668
|
+
/**
|
|
2669
|
+
* The newest message of a thread as this device saw it. `at` is its
|
|
2670
|
+
* `createdAt`, which Workstation's list repeats as `lastMessageAt`; for a sent
|
|
2671
|
+
* message it's the phone's clock instead.
|
|
2672
|
+
*/
|
|
2673
|
+
|
|
2674
|
+
/** `snippet` is absent when there's nothing to quote: the row keeps its words. */
|
|
2675
|
+
|
|
2676
|
+
/** The list row once `message` is the newest. Null keeps the row as it is: system rows and notes. */
|
|
2677
|
+
function rowFieldsOf(message) {
|
|
2678
|
+
if (message.messageType && message.messageType !== 'message') return null;
|
|
2679
|
+
return rowFieldsFor(plainTextOf(message), (message.attachments ?? []).map(attachment => attachment.contentType), message.senderType === 'visitor' || message.direction === 'inbound', /<img\b/i.test(message.body ?? ''));
|
|
2680
|
+
}
|
|
2681
|
+
|
|
2682
|
+
/**
|
|
2683
|
+
* Its words, or with none, whether it carried only photos or other files too.
|
|
2684
|
+
* A message with neither (a card) still says who sent it, so a teammate's
|
|
2685
|
+
* card never leaves "You:" on the visitor's earlier message.
|
|
2686
|
+
*/
|
|
2687
|
+
function rowFieldsFor(text, contentTypes, fromVisitor, inlinePhoto = false) {
|
|
2688
|
+
if (text) return {
|
|
2689
|
+
snippet: text,
|
|
2690
|
+
lastMessageAttachment: null,
|
|
2691
|
+
lastMessageFromVisitor: fromVisitor
|
|
2692
|
+
};
|
|
2693
|
+
const count = contentTypes.length;
|
|
2694
|
+
if (count === 0 && !inlinePhoto) return {
|
|
2695
|
+
lastMessageAttachment: null,
|
|
2696
|
+
lastMessageFromVisitor: fromVisitor
|
|
2697
|
+
};
|
|
2698
|
+
return {
|
|
2699
|
+
// Workstation's list words it this way too, so apps reading `snippet` see the same after a reload.
|
|
2700
|
+
...(count > 0 ? {
|
|
2701
|
+
snippet: `${count} attachment${count === 1 ? '' : 's'}`
|
|
2702
|
+
} : {}),
|
|
2703
|
+
lastMessageAttachment: contentTypes.every(type => isImageAttachment(type)) ? 'photo' : 'file',
|
|
2704
|
+
lastMessageFromVisitor: fromVisitor
|
|
2705
|
+
};
|
|
2706
|
+
}
|
|
2707
|
+
|
|
2708
|
+
/**
|
|
2709
|
+
* The list stamps a conversation just after storing its message, so its time
|
|
2710
|
+
* trails the message's own by a few milliseconds, never more than `LIST_LAG_MS`.
|
|
2711
|
+
* A sent message is timed by the phone instead: a little drift either way
|
|
2712
|
+
* still matches, but only when the list rules out a team reply the visitor
|
|
2713
|
+
* hasn't read. Older Workstation deployments don't send `unreadCount`, so
|
|
2714
|
+
* there a sent message must match like any other.
|
|
2715
|
+
*/
|
|
2716
|
+
function describesSameMessage(conversation, known) {
|
|
2717
|
+
const lag = Date.parse(conversation.lastMessageAt ?? '') - Date.parse(known.at);
|
|
2718
|
+
if (lag >= 0 && lag <= LIST_LAG_MS) return true;
|
|
2719
|
+
return !!known.phoneClock && Math.abs(lag) <= SAME_MESSAGE_DRIFT_MS && conversation.unreadCount === 0;
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2493
2722
|
/** A deletion missed while the stream was down shows up on the next re-read. */
|
|
2494
2723
|
function adoptServerDeletions(messages, server) {
|
|
2495
2724
|
const deleted = new Map(server.filter(message => message.deletedAt).map(message => [message.id, message.deletedAt]));
|
|
@@ -2537,13 +2766,31 @@ function mergeAttachments(local, server) {
|
|
|
2537
2766
|
if (local.length !== server.length) {
|
|
2538
2767
|
return server.every(attachment => attachment.url) ? server : local;
|
|
2539
2768
|
}
|
|
2769
|
+
const owns = pairByName(local, server);
|
|
2540
2770
|
const merged = server.map((attachment, index) => {
|
|
2541
|
-
const own =
|
|
2542
|
-
|
|
2771
|
+
const own = owns[index];
|
|
2772
|
+
if (!isLocalAttachment(own) || !attachment.url) return own;
|
|
2773
|
+
rememberLocalPreview(attachment.url, own.url);
|
|
2774
|
+
return attachment;
|
|
2543
2775
|
});
|
|
2544
2776
|
return merged.every((attachment, index) => attachment === local[index]) ? local : merged;
|
|
2545
2777
|
}
|
|
2546
2778
|
|
|
2779
|
+
/**
|
|
2780
|
+
* The local attachment each server one replaces. By name first: Workstation
|
|
2781
|
+
* may store a message's files in another order than they were sent, and a
|
|
2782
|
+
* photo paired with the wrong file would show it for the rest of the session.
|
|
2783
|
+
* Any it renamed pair up in the order left.
|
|
2784
|
+
*/
|
|
2785
|
+
function pairByName(local, server) {
|
|
2786
|
+
const unpaired = [...local];
|
|
2787
|
+
const named = server.map(attachment => {
|
|
2788
|
+
const index = unpaired.findIndex(own => own.name === attachment.name);
|
|
2789
|
+
return index < 0 ? null : unpaired.splice(index, 1)[0];
|
|
2790
|
+
});
|
|
2791
|
+
return named.map(own => own ?? unpaired.shift());
|
|
2792
|
+
}
|
|
2793
|
+
|
|
2547
2794
|
/**
|
|
2548
2795
|
* Newer Workstation deployments echo `clientMessageId` on the visitor's own
|
|
2549
2796
|
* rows. Such a row replaces the pending or failed bubble it came from instead
|