@repzo/widget-react-native 1.3.1 → 1.5.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 +173 -5
- package/README.md +99 -13
- package/lib/commonjs/api/client.js +125 -28
- 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/index.js.map +1 -1
- package/lib/commonjs/locales.js +12 -3
- package/lib/commonjs/locales.js.map +1 -1
- package/lib/commonjs/state/user-update-queue.js +168 -0
- package/lib/commonjs/state/user-update-queue.js.map +1 -0
- package/lib/commonjs/state/widget-store.js +303 -33
- 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 +651 -165
- 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 +123 -27
- 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/index.js.map +1 -1
- package/lib/module/locales.js +12 -3
- package/lib/module/locales.js.map +1 -1
- package/lib/module/state/user-update-queue.js +163 -0
- package/lib/module/state/user-update-queue.js.map +1 -0
- package/lib/module/state/widget-store.js +305 -35
- 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 +655 -169
- 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 +20 -6
- 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/index.d.ts +5 -0
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/locales.d.ts.map +1 -1
- package/lib/typescript/commonjs/state/user-update-queue.d.ts +62 -0
- package/lib/typescript/commonjs/state/user-update-queue.d.ts.map +1 -0
- package/lib/typescript/commonjs/state/widget-store.d.ts +44 -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 +53 -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 +20 -6
- 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/index.d.ts +5 -0
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/locales.d.ts.map +1 -1
- package/lib/typescript/module/state/user-update-queue.d.ts +62 -0
- package/lib/typescript/module/state/user-update-queue.d.ts.map +1 -0
- package/lib/typescript/module/state/widget-store.d.ts +44 -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 +53 -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 +149 -38
- 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/index.tsx +5 -0
- package/src/locales.ts +12 -3
- package/src/state/user-update-queue.ts +178 -0
- package/src/state/widget-store.ts +346 -51
- package/src/storage.ts +2 -0
- package/src/types.ts +53 -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 +735 -198
- 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,10 +11,14 @@ 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");
|
|
21
|
+
var _userUpdateQueue = require("./user-update-queue.js");
|
|
18
22
|
/**
|
|
19
23
|
* The widget's single source of truth.
|
|
20
24
|
*
|
|
@@ -26,7 +30,7 @@ var _safeUrl = require("../ui/utils/safe-url.js");
|
|
|
26
30
|
*/
|
|
27
31
|
|
|
28
32
|
/** Sent on every init; must match `package.json` (tests/version.test.js checks). */
|
|
29
|
-
const SDK_VERSION = '@repzo/widget-react-native@1.
|
|
33
|
+
const SDK_VERSION = '@repzo/widget-react-native@1.5.0';
|
|
30
34
|
|
|
31
35
|
/** The platform this session reports — mobile sessions get the Mobile SDKs overrides. */
|
|
32
36
|
function sessionPlatform() {
|
|
@@ -41,6 +45,12 @@ const TYPING_SETTLE_MS = 3_000;
|
|
|
41
45
|
const TYPING_PING_INTERVAL_MS = 4_000;
|
|
42
46
|
/** Guards against the SSE stream replaying messages after a reconnect. */
|
|
43
47
|
const SEEN_MESSAGE_LIMIT = 100;
|
|
48
|
+
/** Threads whose newest message is remembered across launches. */
|
|
49
|
+
const LAST_MESSAGE_LIMIT = 50;
|
|
50
|
+
/** How far the phone's clock may be from Workstation's for a sent message to still match the list. */
|
|
51
|
+
const SAME_MESSAGE_DRIFT_MS = 120_000;
|
|
52
|
+
/** How far the list's `lastMessageAt` may trail the message's own `createdAt` (7 ms seen live). */
|
|
53
|
+
const LIST_LAG_MS = 1_000;
|
|
44
54
|
/** Logout's server calls are best-effort — a dead network must not block sign-out. */
|
|
45
55
|
const LOGOUT_REQUEST_TIMEOUT_MS = 5_000;
|
|
46
56
|
/** A server that 401s every fresh stream must not spin init (10/min limit). */
|
|
@@ -118,6 +128,11 @@ const USER_SCOPED_STATE = {
|
|
|
118
128
|
};
|
|
119
129
|
class WidgetStore {
|
|
120
130
|
client = new _client.WidgetApiClient();
|
|
131
|
+
|
|
132
|
+
/** Paces `updateUser()` — see `boot({ throttleUpdateUser })`. */
|
|
133
|
+
userUpdates = new _userUpdateQueue.UserUpdateQueue(customAttributes => this.client.identify({
|
|
134
|
+
customAttributes
|
|
135
|
+
}));
|
|
121
136
|
events = new _events.EventEmitter();
|
|
122
137
|
push = new _pushNotifications.PushManager(this.client, conversationId => {
|
|
123
138
|
void this.openConversation(conversationId);
|
|
@@ -134,6 +149,12 @@ class WidgetStore {
|
|
|
134
149
|
|
|
135
150
|
/** `boot({ posts })` — Send posts over the app. */
|
|
136
151
|
postsEnabled = true;
|
|
152
|
+
/** `boot({ photoAlbums })` — a message's photos share one album in the thread. */
|
|
153
|
+
photoAlbumsEnabled = true;
|
|
154
|
+
/** `boot({ youPrefix })` — "You:" before the visitor's own newest message in the lists. */
|
|
155
|
+
youPrefixEnabled = true;
|
|
156
|
+
/** `boot({ throttleUpdateUser })` — skip unchanged `updateUser()` values and pace the rest. */
|
|
157
|
+
throttleUpdateUserEnabled = true;
|
|
137
158
|
|
|
138
159
|
/** Posts whose view was reported this session (the server also dedupes per day). */
|
|
139
160
|
viewedPostIds = new Set();
|
|
@@ -188,6 +209,10 @@ class WidgetStore {
|
|
|
188
209
|
lastTypingPingAt = 0;
|
|
189
210
|
visitorTypingActive = false;
|
|
190
211
|
seenMessageIds = [];
|
|
212
|
+
|
|
213
|
+
/** Who sent each thread's newest message and what it carried; see `withLastMessage`. */
|
|
214
|
+
lastMessages = {};
|
|
215
|
+
lastMessagesLoaded = false;
|
|
191
216
|
/** Statuses (e.g. "read") that arrived for an id before a send response revealed it. */
|
|
192
217
|
earlyStatuses = new Map();
|
|
193
218
|
/** The agent UI action behind a failed bubble, so Retry runs it again too. */
|
|
@@ -267,6 +292,8 @@ class WidgetStore {
|
|
|
267
292
|
|
|
268
293
|
/** Composer identity: explicit opens reset it; a draft receiving its server ID does not. */
|
|
269
294
|
getThreadGeneration = () => this.threadGeneration;
|
|
295
|
+
showsPhotoAlbums = () => this.photoAlbumsEnabled;
|
|
296
|
+
showsYouPrefix = () => this.youPrefixEnabled;
|
|
270
297
|
setState(patch) {
|
|
271
298
|
const previous = this.state;
|
|
272
299
|
const resolved = typeof patch === 'function' ? patch(previous) : patch;
|
|
@@ -338,6 +365,11 @@ class WidgetStore {
|
|
|
338
365
|
if (options.inAppNotifications !== undefined) {
|
|
339
366
|
this.inAppNotificationsEnabled = options.inAppNotifications !== false;
|
|
340
367
|
}
|
|
368
|
+
if (options.photoAlbums !== undefined) this.photoAlbumsEnabled = options.photoAlbums !== false;
|
|
369
|
+
if (options.youPrefix !== undefined) this.youPrefixEnabled = options.youPrefix !== false;
|
|
370
|
+
if (options.throttleUpdateUser !== undefined) {
|
|
371
|
+
this.throttleUpdateUserEnabled = options.throttleUpdateUser !== false;
|
|
372
|
+
}
|
|
341
373
|
if (options.posts !== undefined) {
|
|
342
374
|
const wasEnabled = this.postsEnabled;
|
|
343
375
|
this.postsEnabled = options.posts !== false;
|
|
@@ -492,13 +524,13 @@ class WidgetStore {
|
|
|
492
524
|
// queued op must never see one without the other.
|
|
493
525
|
await Promise.all([this.client.setToken(result.token), userKey ? _storage.SecureStorage.setItem(_storage.USER_KEY, userKey) : _storage.SecureStorage.removeItem(_storage.USER_KEY)]);
|
|
494
526
|
if (this.isStale(epoch)) return;
|
|
495
|
-
const conversations = result.conversations.map(conversation => this.hydrateConversation(conversation));
|
|
496
527
|
|
|
497
528
|
// Server counts win (they include replies pushed while the app was
|
|
498
529
|
// killed); the locally persisted map only covers older Workstation
|
|
499
530
|
// deployments that don't send unreadCount yet.
|
|
500
|
-
const storedUnread = await this.loadUnread();
|
|
531
|
+
const [, storedUnread] = await Promise.all([this.loadLastMessages(epoch), this.loadUnread()]);
|
|
501
532
|
if (this.isStale(epoch)) return;
|
|
533
|
+
const conversations = result.conversations.map(conversation => this.hydrateConversation(conversation));
|
|
502
534
|
const unreadByConversation = {};
|
|
503
535
|
for (const conversation of conversations) {
|
|
504
536
|
const count = typeof conversation.unreadCount === 'number' ? conversation.unreadCount : storedUnread[conversation.id] ?? 0;
|
|
@@ -508,6 +540,10 @@ class WidgetStore {
|
|
|
508
540
|
const onScreenId = this.state.activeConversationId;
|
|
509
541
|
if (onScreenId && this.isThreadOnScreen(onScreenId)) delete unreadByConversation[onScreenId];
|
|
510
542
|
this.persistUnread(unreadByConversation);
|
|
543
|
+
|
|
544
|
+
// A new identity (anonymous → signed in) may hold other attributes, so
|
|
545
|
+
// nothing is skipped as "already sent" for it.
|
|
546
|
+
if (result.identityId !== this.state.identityId) this.userUpdates.forgetSent();
|
|
511
547
|
this.setState(current => ({
|
|
512
548
|
booted: true,
|
|
513
549
|
loading: false,
|
|
@@ -736,9 +772,13 @@ class WidgetStore {
|
|
|
736
772
|
if (!this.state.booted) await this.bootPromise;
|
|
737
773
|
const customAttributes = sanitizeAttributes(attributes);
|
|
738
774
|
if (Object.keys(customAttributes).length === 0) return;
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
775
|
+
if (!this.throttleUpdateUserEnabled) {
|
|
776
|
+
await this.client.identify({
|
|
777
|
+
customAttributes
|
|
778
|
+
});
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
await this.userUpdates.update(customAttributes);
|
|
742
782
|
}
|
|
743
783
|
|
|
744
784
|
/**
|
|
@@ -750,6 +790,7 @@ class WidgetStore {
|
|
|
750
790
|
this.loggedOut = true;
|
|
751
791
|
this.currentUser = null;
|
|
752
792
|
this.pendingConversationId = null;
|
|
793
|
+
this.userUpdates.reset();
|
|
753
794
|
this.realtime.close();
|
|
754
795
|
this.clearTypingTimer();
|
|
755
796
|
this.resetThreadTracking();
|
|
@@ -781,6 +822,8 @@ class WidgetStore {
|
|
|
781
822
|
}
|
|
782
823
|
async wipeSessionUnguarded() {
|
|
783
824
|
this.realtime.close();
|
|
825
|
+
// Attributes waiting for the old visitor must never reach the next one.
|
|
826
|
+
this.userUpdates.reset();
|
|
784
827
|
this.clearTypingTimer();
|
|
785
828
|
this.resetThreadTracking();
|
|
786
829
|
this.appStateSubscription?.remove();
|
|
@@ -789,16 +832,20 @@ class WidgetStore {
|
|
|
789
832
|
this.userKeyLoaded = true;
|
|
790
833
|
this.pendingAgentByConversation = {};
|
|
791
834
|
this.seenMessageIds = [];
|
|
835
|
+
this.lastMessages = {};
|
|
836
|
+
this.lastMessagesLoaded = false;
|
|
792
837
|
this.earlyStatuses.clear();
|
|
793
838
|
this.visitorTypingActive = false;
|
|
794
839
|
this.lastTypingPingAt = 0;
|
|
795
840
|
this.launchCardOffered = false;
|
|
796
841
|
this.resetPost();
|
|
842
|
+
(0, _localPreviews.clearLocalPreviews)();
|
|
843
|
+
(0, _useRetainedState.clearRetainedState)();
|
|
797
844
|
this.setState({
|
|
798
845
|
...USER_SCOPED_STATE
|
|
799
846
|
});
|
|
800
847
|
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),
|
|
848
|
+
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
849
|
// A new anonymous visitor on this device gets a new tracking identity.
|
|
803
850
|
(0, _deviceId.rotateDeviceId)()]);
|
|
804
851
|
|
|
@@ -1178,10 +1225,11 @@ class WidgetStore {
|
|
|
1178
1225
|
|
|
1179
1226
|
// Set inside the updater, read after: `setState` runs synchronously.
|
|
1180
1227
|
let countedAsUnread = false;
|
|
1228
|
+
const rowFields = rowFieldsOf(message);
|
|
1181
1229
|
this.setState(current => {
|
|
1182
1230
|
const conversations = conversationId ? current.conversations.map(conversation => conversation.id === conversationId ? {
|
|
1183
1231
|
...conversation,
|
|
1184
|
-
|
|
1232
|
+
...rowFields,
|
|
1185
1233
|
lastMessageAt: message.createdAt,
|
|
1186
1234
|
messageCount: conversation.messageCount + 1
|
|
1187
1235
|
} : conversation) : current.conversations;
|
|
@@ -1219,11 +1267,12 @@ class WidgetStore {
|
|
|
1219
1267
|
messageId,
|
|
1220
1268
|
senderName: message.senderName || conversation?.agentName || null,
|
|
1221
1269
|
senderAvatar: conversation?.agentAvatar ?? null,
|
|
1222
|
-
preview: (message
|
|
1270
|
+
preview: plainTextOf(message),
|
|
1223
1271
|
hasAttachments: (message.attachments?.length ?? 0) > 0
|
|
1224
1272
|
}
|
|
1225
1273
|
};
|
|
1226
1274
|
});
|
|
1275
|
+
if (conversationId && rowFields) this.rememberLastMessage(conversationId, message.createdAt, rowFields, 'stream');
|
|
1227
1276
|
|
|
1228
1277
|
// The visitor is reading it live — move the server cursor too, or the
|
|
1229
1278
|
// next list/init would badge a reply they already saw.
|
|
@@ -1321,10 +1370,112 @@ class WidgetStore {
|
|
|
1321
1370
|
};
|
|
1322
1371
|
}
|
|
1323
1372
|
hydrateConversation(conversation) {
|
|
1324
|
-
return this.applyConversationAgent({
|
|
1373
|
+
return this.applyConversationAgent(this.withLastMessage({
|
|
1325
1374
|
...conversation,
|
|
1326
1375
|
agentAvatar: this.client.resolveUrl(conversation.agentAvatar)
|
|
1376
|
+
}));
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
/**
|
|
1380
|
+
* Workstation's list doesn't say who sent the newest message, and calls one
|
|
1381
|
+
* without words "2 attachments". While a row still describes the message
|
|
1382
|
+
* this device saw last, both are filled in from it. `snippet` keeps the
|
|
1383
|
+
* list's words: apps read it, and the lists draw "Photo" or "File" from
|
|
1384
|
+
* `lastMessageAttachment` instead.
|
|
1385
|
+
*/
|
|
1386
|
+
withLastMessage(conversation) {
|
|
1387
|
+
const known = this.lastMessages[conversation.id];
|
|
1388
|
+
if (!known || !describesSameMessage(conversation, known)) return conversation;
|
|
1389
|
+
// A list that says who sent it, or what it carried, is used as is.
|
|
1390
|
+
return {
|
|
1391
|
+
...conversation,
|
|
1392
|
+
lastMessageFromVisitor: conversation.lastMessageFromVisitor ?? known.fromVisitor,
|
|
1393
|
+
lastMessageAttachment: conversation.lastMessageAttachment !== undefined ? conversation.lastMessageAttachment : known.attachment
|
|
1394
|
+
};
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
/**
|
|
1398
|
+
* A send or a streamed message is the newest by arrival, whatever the two
|
|
1399
|
+
* clocks say; a history page only replaces something older. A sent message
|
|
1400
|
+
* is timed by the phone, which may run ahead of Workstation, so instead a
|
|
1401
|
+
* page that has it replaces it: whatever is newest there came after it, and
|
|
1402
|
+
* Workstation's own time then stands in for the phone's.
|
|
1403
|
+
*/
|
|
1404
|
+
rememberLastMessage(conversationId, at, fields, source, {
|
|
1405
|
+
messageId,
|
|
1406
|
+
pageIds
|
|
1407
|
+
} = {}) {
|
|
1408
|
+
const known = this.lastMessages[conversationId];
|
|
1409
|
+
if (source === 'history' && known) {
|
|
1410
|
+
const outdated = known.phoneClock && known.messageId ? !pageIds?.has(known.messageId) : Date.parse(known.at) > Date.parse(at);
|
|
1411
|
+
if (outdated) return;
|
|
1412
|
+
}
|
|
1413
|
+
const hint = {
|
|
1414
|
+
at,
|
|
1415
|
+
fromVisitor: !!fields.lastMessageFromVisitor,
|
|
1416
|
+
attachment: fields.lastMessageAttachment ?? null,
|
|
1417
|
+
...(source === 'sent' ? {
|
|
1418
|
+
phoneClock: true,
|
|
1419
|
+
...(messageId ? {
|
|
1420
|
+
messageId
|
|
1421
|
+
} : {})
|
|
1422
|
+
} : {})
|
|
1423
|
+
};
|
|
1424
|
+
this.lastMessages = Object.fromEntries(Object.entries({
|
|
1425
|
+
...this.lastMessages,
|
|
1426
|
+
[conversationId]: hint
|
|
1427
|
+
}).sort(([, a], [, b]) => Date.parse(b.at) - Date.parse(a.at)).slice(0, LAST_MESSAGE_LIMIT));
|
|
1428
|
+
// Until the stored hints are read, saving would overwrite them with these alone.
|
|
1429
|
+
if (this.lastMessagesLoaded) void _storage.Storage.setItem(_storage.LAST_MESSAGE_KEY, JSON.stringify(this.lastMessages));
|
|
1430
|
+
}
|
|
1431
|
+
async loadLastMessages(epoch) {
|
|
1432
|
+
if (this.lastMessagesLoaded) return;
|
|
1433
|
+
let stored = null;
|
|
1434
|
+
try {
|
|
1435
|
+
stored = JSON.parse((await _storage.Storage.getItem(_storage.LAST_MESSAGE_KEY)) ?? 'null');
|
|
1436
|
+
} catch {
|
|
1437
|
+
stored = null;
|
|
1438
|
+
}
|
|
1439
|
+
// Left unloaded: the init that replaces this one reads them again.
|
|
1440
|
+
if (this.isStale(epoch) || this.lastMessagesLoaded) return;
|
|
1441
|
+
this.lastMessagesLoaded = true;
|
|
1442
|
+
const learned = Object.keys(this.lastMessages).length > 0;
|
|
1443
|
+
if (stored && typeof stored === 'object') {
|
|
1444
|
+
for (const [conversationId, hint] of Object.entries(stored)) {
|
|
1445
|
+
if (typeof hint?.at !== 'string' || typeof hint.fromVisitor !== 'boolean') continue;
|
|
1446
|
+
const known = this.lastMessages[conversationId];
|
|
1447
|
+
if (known && Date.parse(known.at) >= Date.parse(hint.at)) continue;
|
|
1448
|
+
this.lastMessages[conversationId] = {
|
|
1449
|
+
at: hint.at,
|
|
1450
|
+
fromVisitor: hint.fromVisitor,
|
|
1451
|
+
attachment: hint.attachment === 'photo' || hint.attachment === 'file' ? hint.attachment : null,
|
|
1452
|
+
...(hint.phoneClock === true ? {
|
|
1453
|
+
phoneClock: true
|
|
1454
|
+
} : {}),
|
|
1455
|
+
...(typeof hint.messageId === 'string' ? {
|
|
1456
|
+
messageId: hint.messageId
|
|
1457
|
+
} : {})
|
|
1458
|
+
};
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
// Hints learned while the stored ones were being read are saved with them.
|
|
1462
|
+
if (learned) void _storage.Storage.setItem(_storage.LAST_MESSAGE_KEY, JSON.stringify(this.lastMessages));
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
/** A thread's newest page shows who sent its newest message, so its row can say too. */
|
|
1466
|
+
learnLastMessage(conversationId, page) {
|
|
1467
|
+
let newest = null;
|
|
1468
|
+
for (const message of page) {
|
|
1469
|
+
if (!newest || Date.parse(message.createdAt) > Date.parse(newest.createdAt)) newest = message;
|
|
1470
|
+
}
|
|
1471
|
+
const fields = newest && rowFieldsOf(newest);
|
|
1472
|
+
if (!newest || !fields) return;
|
|
1473
|
+
this.rememberLastMessage(conversationId, newest.createdAt, fields, 'history', {
|
|
1474
|
+
pageIds: new Set(page.map(message => message.id))
|
|
1327
1475
|
});
|
|
1476
|
+
this.setState(current => ({
|
|
1477
|
+
conversations: current.conversations.map(row => row.id === conversationId ? this.withLastMessage(row) : row)
|
|
1478
|
+
}));
|
|
1328
1479
|
}
|
|
1329
1480
|
|
|
1330
1481
|
/**
|
|
@@ -1333,16 +1484,22 @@ class WidgetStore {
|
|
|
1333
1484
|
*/
|
|
1334
1485
|
hydrateAttachments(attachments, messageId) {
|
|
1335
1486
|
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
|
-
|
|
1487
|
+
const hydrated = attachments.filter(item => !!item && typeof item === 'object').map((attachment, index) => {
|
|
1488
|
+
const name = typeof attachment.name === 'string' && attachment.name ? attachment.name : 'Attachment';
|
|
1489
|
+
const declared = typeof attachment.contentType === 'string' ? attachment.contentType : '';
|
|
1490
|
+
return {
|
|
1491
|
+
...attachment,
|
|
1492
|
+
id: typeof attachment.id === 'string' || typeof attachment.id === 'number' ? String(attachment.id) : `${messageId ?? 'attachment'}-${index}`,
|
|
1493
|
+
name,
|
|
1494
|
+
// Without a type (or with a generic one) the name says what it is,
|
|
1495
|
+
// so a photo still shows as a photo rather than a file row.
|
|
1496
|
+
contentType: declared && !/^application\/octet-stream$/i.test(declared) ? declared : (0, _pickers.guessContentType)(name) ?? declared,
|
|
1497
|
+
size: typeof attachment.size === 'number' ? attachment.size : 0,
|
|
1498
|
+
// Empty (never the raw value) when the URL isn't http(s) — the preview
|
|
1499
|
+
// then renders the name only, with nothing to open.
|
|
1500
|
+
url: this.client.resolveUrl(typeof attachment.url === 'string' ? attachment.url : null) ?? ''
|
|
1501
|
+
};
|
|
1502
|
+
});
|
|
1346
1503
|
return hydrated.length > 0 ? hydrated : null;
|
|
1347
1504
|
}
|
|
1348
1505
|
clearTypingTimer() {
|
|
@@ -1598,10 +1755,10 @@ class WidgetStore {
|
|
|
1598
1755
|
const result = await this.client.listMessages(conversationId);
|
|
1599
1756
|
// Opening A then B quickly: A's page must never land under B.
|
|
1600
1757
|
if (this.isStale(epoch) || this.threadGeneration !== generation) return;
|
|
1758
|
+
const page = this.hydrateMessages(result.data);
|
|
1601
1759
|
this.setState(current => {
|
|
1602
1760
|
// Live replies and optimistic sends may have arrived since this page
|
|
1603
1761
|
// was requested. Keep them, preferring the live copy for shared ids.
|
|
1604
|
-
const page = this.hydrateMessages(result.data);
|
|
1605
1762
|
const live = adoptOwnServerRows(current.messages, page);
|
|
1606
1763
|
const liveIds = new Set(live.map(message => message.id));
|
|
1607
1764
|
const history = page.filter(message => !liveIds.has(message.id));
|
|
@@ -1612,6 +1769,7 @@ class WidgetStore {
|
|
|
1612
1769
|
messageCursor: result.meta.nextCursor
|
|
1613
1770
|
};
|
|
1614
1771
|
});
|
|
1772
|
+
this.learnLastMessage(conversationId, page);
|
|
1615
1773
|
} catch {
|
|
1616
1774
|
if (!this.isStale(epoch) && this.threadGeneration === generation) {
|
|
1617
1775
|
this.setState({
|
|
@@ -1673,6 +1831,7 @@ class WidgetStore {
|
|
|
1673
1831
|
if (this.isStale(epoch) || this.threadGeneration !== generation) return;
|
|
1674
1832
|
if (this.state.activeConversationId !== conversationId) return;
|
|
1675
1833
|
const fresh = this.hydrateMessages(result.data);
|
|
1834
|
+
this.learnLastMessage(conversationId, fresh);
|
|
1676
1835
|
this.setState(current => {
|
|
1677
1836
|
const own = adoptOwnServerRows(current.messages, fresh);
|
|
1678
1837
|
const known = new Set(own.map(message => message.id));
|
|
@@ -1839,6 +1998,15 @@ class WidgetStore {
|
|
|
1839
1998
|
return this.deliverMessage(tempId, trimmed, attachments, agentUiAction);
|
|
1840
1999
|
}
|
|
1841
2000
|
|
|
2001
|
+
/**
|
|
2002
|
+
* Puts a picked file in storage while the visitor is still writing, so
|
|
2003
|
+
* Send goes out at once. Resolves the token `sendMessage` names it by, and
|
|
2004
|
+
* when that token expires.
|
|
2005
|
+
*/
|
|
2006
|
+
uploadAttachment(file, signal) {
|
|
2007
|
+
return this.client.uploadAttachment(file, signal);
|
|
2008
|
+
}
|
|
2009
|
+
|
|
1842
2010
|
/**
|
|
1843
2011
|
* Re-sends a text bubble that failed. It moves to the end of the thread,
|
|
1844
2012
|
* since it now goes out after anything sent meanwhile. Rejects like
|
|
@@ -1900,23 +2068,30 @@ class WidgetStore {
|
|
|
1900
2068
|
}
|
|
1901
2069
|
let createdId = null;
|
|
1902
2070
|
try {
|
|
1903
|
-
// Local files go up first
|
|
2071
|
+
// Local files go up first, unless the composer already put them up; the
|
|
2072
|
+
// message names each by its upload token.
|
|
1904
2073
|
const sentAttachments = await Promise.all(attachments.map(async ({
|
|
1905
2074
|
name,
|
|
1906
2075
|
url,
|
|
1907
2076
|
fileUri,
|
|
2077
|
+
uploadToken,
|
|
1908
2078
|
contentType,
|
|
1909
2079
|
size
|
|
1910
|
-
}) =>
|
|
2080
|
+
}) => uploadToken ? {
|
|
1911
2081
|
name,
|
|
1912
2082
|
contentType,
|
|
1913
2083
|
size,
|
|
1914
|
-
uploadToken
|
|
2084
|
+
uploadToken
|
|
2085
|
+
} : fileUri ? {
|
|
2086
|
+
name,
|
|
2087
|
+
contentType,
|
|
2088
|
+
size,
|
|
2089
|
+
uploadToken: (await this.client.uploadAttachment({
|
|
1915
2090
|
uri: fileUri,
|
|
1916
2091
|
name,
|
|
1917
2092
|
contentType,
|
|
1918
2093
|
size
|
|
1919
|
-
})
|
|
2094
|
+
})).uploadToken
|
|
1920
2095
|
} : {
|
|
1921
2096
|
name,
|
|
1922
2097
|
url,
|
|
@@ -1938,6 +2113,11 @@ class WidgetStore {
|
|
|
1938
2113
|
});
|
|
1939
2114
|
if (this.isStale(epoch)) return;
|
|
1940
2115
|
if (ownCreation) createdId = result.conversationId;
|
|
2116
|
+
|
|
2117
|
+
// Workstation doesn't stream the visitor's own messages back, so the
|
|
2118
|
+
// list row learns about this one here.
|
|
2119
|
+
const sentAt = new Date().toISOString();
|
|
2120
|
+
const sentRow = rowFieldsFor(trimmed, attachments.map(attachment => attachment.contentType), true);
|
|
1941
2121
|
this.setState(current => {
|
|
1942
2122
|
// The stream or a catch-up fetch may have delivered this message
|
|
1943
2123
|
// already — keep one copy, never two bubbles with the same id.
|
|
@@ -1947,18 +2127,27 @@ class WidgetStore {
|
|
|
1947
2127
|
id: result.messageId,
|
|
1948
2128
|
status: this.earlyStatuses.get(result.messageId) ?? 'sent'
|
|
1949
2129
|
} : message);
|
|
1950
|
-
if (!createdId)
|
|
1951
|
-
|
|
1952
|
-
|
|
2130
|
+
if (!createdId) {
|
|
2131
|
+
return {
|
|
2132
|
+
messages,
|
|
2133
|
+
conversations: current.conversations.map(row => row.id === result.conversationId ? {
|
|
2134
|
+
...row,
|
|
2135
|
+
...sentRow,
|
|
2136
|
+
lastMessageAt: sentAt,
|
|
2137
|
+
messageCount: row.messageCount + (delivered ? 0 : 1)
|
|
2138
|
+
} : row)
|
|
2139
|
+
};
|
|
2140
|
+
}
|
|
1953
2141
|
const known = current.conversations.some(item => item.id === result.conversationId);
|
|
1954
2142
|
const conversation = {
|
|
1955
2143
|
id: result.conversationId,
|
|
1956
2144
|
subject: null,
|
|
1957
2145
|
status: 'open',
|
|
1958
|
-
snippet:
|
|
2146
|
+
snippet: null,
|
|
2147
|
+
...sentRow,
|
|
1959
2148
|
messageCount: 1,
|
|
1960
|
-
lastMessageAt:
|
|
1961
|
-
createdAt:
|
|
2149
|
+
lastMessageAt: sentAt,
|
|
2150
|
+
createdAt: sentAt,
|
|
1962
2151
|
agentName: null,
|
|
1963
2152
|
agentAvatar: null
|
|
1964
2153
|
};
|
|
@@ -1971,6 +2160,9 @@ class WidgetStore {
|
|
|
1971
2160
|
} : {})
|
|
1972
2161
|
};
|
|
1973
2162
|
});
|
|
2163
|
+
this.rememberLastMessage(result.conversationId, sentAt, sentRow, 'sent', {
|
|
2164
|
+
messageId: result.messageId
|
|
2165
|
+
});
|
|
1974
2166
|
|
|
1975
2167
|
// New files to fetch the stored copy of: a fresh round of re-reads.
|
|
1976
2168
|
const sent = this.state.messages.find(message => message.id === result.messageId);
|
|
@@ -2442,6 +2634,7 @@ class WidgetStore {
|
|
|
2442
2634
|
// ─── Teardown (tests / hot reload) ──────────────────────────────────────
|
|
2443
2635
|
|
|
2444
2636
|
destroy() {
|
|
2637
|
+
this.userUpdates.destroy();
|
|
2445
2638
|
this.clearPostRecheck();
|
|
2446
2639
|
this.clearPostThanks();
|
|
2447
2640
|
this.realtime.close();
|
|
@@ -2494,6 +2687,65 @@ function asDeletedPlaceholder(message, deletedAt) {
|
|
|
2494
2687
|
};
|
|
2495
2688
|
}
|
|
2496
2689
|
|
|
2690
|
+
/** A message's words on one line; replies written in Workstation may only carry HTML. */
|
|
2691
|
+
function plainTextOf(message) {
|
|
2692
|
+
return (message.bodyPlain || (0, _html.stripHtml)(message.body ?? '')).replace(/\s+/g, ' ').trim();
|
|
2693
|
+
}
|
|
2694
|
+
|
|
2695
|
+
/**
|
|
2696
|
+
* The newest message of a thread as this device saw it. `at` is its
|
|
2697
|
+
* `createdAt`, which Workstation's list repeats as `lastMessageAt`; for a sent
|
|
2698
|
+
* message it's the phone's clock instead.
|
|
2699
|
+
*/
|
|
2700
|
+
|
|
2701
|
+
/** `snippet` is absent when there's nothing to quote: the row keeps its words. */
|
|
2702
|
+
|
|
2703
|
+
/** The list row once `message` is the newest. Null keeps the row as it is: system rows and notes. */
|
|
2704
|
+
function rowFieldsOf(message) {
|
|
2705
|
+
if (message.messageType && message.messageType !== 'message') return null;
|
|
2706
|
+
return rowFieldsFor(plainTextOf(message), (message.attachments ?? []).map(attachment => attachment.contentType), message.senderType === 'visitor' || message.direction === 'inbound', /<img\b/i.test(message.body ?? ''));
|
|
2707
|
+
}
|
|
2708
|
+
|
|
2709
|
+
/**
|
|
2710
|
+
* Its words, or with none, whether it carried only photos or other files too.
|
|
2711
|
+
* A message with neither (a card) still says who sent it, so a teammate's
|
|
2712
|
+
* card never leaves "You:" on the visitor's earlier message.
|
|
2713
|
+
*/
|
|
2714
|
+
function rowFieldsFor(text, contentTypes, fromVisitor, inlinePhoto = false) {
|
|
2715
|
+
if (text) return {
|
|
2716
|
+
snippet: text,
|
|
2717
|
+
lastMessageAttachment: null,
|
|
2718
|
+
lastMessageFromVisitor: fromVisitor
|
|
2719
|
+
};
|
|
2720
|
+
const count = contentTypes.length;
|
|
2721
|
+
if (count === 0 && !inlinePhoto) return {
|
|
2722
|
+
lastMessageAttachment: null,
|
|
2723
|
+
lastMessageFromVisitor: fromVisitor
|
|
2724
|
+
};
|
|
2725
|
+
return {
|
|
2726
|
+
// Workstation's list words it this way too, so apps reading `snippet` see the same after a reload.
|
|
2727
|
+
...(count > 0 ? {
|
|
2728
|
+
snippet: `${count} attachment${count === 1 ? '' : 's'}`
|
|
2729
|
+
} : {}),
|
|
2730
|
+
lastMessageAttachment: contentTypes.every(type => (0, _pickers.isImageAttachment)(type)) ? 'photo' : 'file',
|
|
2731
|
+
lastMessageFromVisitor: fromVisitor
|
|
2732
|
+
};
|
|
2733
|
+
}
|
|
2734
|
+
|
|
2735
|
+
/**
|
|
2736
|
+
* The list stamps a conversation just after storing its message, so its time
|
|
2737
|
+
* trails the message's own by a few milliseconds, never more than `LIST_LAG_MS`.
|
|
2738
|
+
* A sent message is timed by the phone instead: a little drift either way
|
|
2739
|
+
* still matches, but only when the list rules out a team reply the visitor
|
|
2740
|
+
* hasn't read. Older Workstation deployments don't send `unreadCount`, so
|
|
2741
|
+
* there a sent message must match like any other.
|
|
2742
|
+
*/
|
|
2743
|
+
function describesSameMessage(conversation, known) {
|
|
2744
|
+
const lag = Date.parse(conversation.lastMessageAt ?? '') - Date.parse(known.at);
|
|
2745
|
+
if (lag >= 0 && lag <= LIST_LAG_MS) return true;
|
|
2746
|
+
return !!known.phoneClock && Math.abs(lag) <= SAME_MESSAGE_DRIFT_MS && conversation.unreadCount === 0;
|
|
2747
|
+
}
|
|
2748
|
+
|
|
2497
2749
|
/** A deletion missed while the stream was down shows up on the next re-read. */
|
|
2498
2750
|
function adoptServerDeletions(messages, server) {
|
|
2499
2751
|
const deleted = new Map(server.filter(message => message.deletedAt).map(message => [message.id, message.deletedAt]));
|
|
@@ -2541,13 +2793,31 @@ function mergeAttachments(local, server) {
|
|
|
2541
2793
|
if (local.length !== server.length) {
|
|
2542
2794
|
return server.every(attachment => attachment.url) ? server : local;
|
|
2543
2795
|
}
|
|
2796
|
+
const owns = pairByName(local, server);
|
|
2544
2797
|
const merged = server.map((attachment, index) => {
|
|
2545
|
-
const own =
|
|
2546
|
-
|
|
2798
|
+
const own = owns[index];
|
|
2799
|
+
if (!isLocalAttachment(own) || !attachment.url) return own;
|
|
2800
|
+
(0, _localPreviews.rememberLocalPreview)(attachment.url, own.url);
|
|
2801
|
+
return attachment;
|
|
2547
2802
|
});
|
|
2548
2803
|
return merged.every((attachment, index) => attachment === local[index]) ? local : merged;
|
|
2549
2804
|
}
|
|
2550
2805
|
|
|
2806
|
+
/**
|
|
2807
|
+
* The local attachment each server one replaces. By name first: Workstation
|
|
2808
|
+
* may store a message's files in another order than they were sent, and a
|
|
2809
|
+
* photo paired with the wrong file would show it for the rest of the session.
|
|
2810
|
+
* Any it renamed pair up in the order left.
|
|
2811
|
+
*/
|
|
2812
|
+
function pairByName(local, server) {
|
|
2813
|
+
const unpaired = [...local];
|
|
2814
|
+
const named = server.map(attachment => {
|
|
2815
|
+
const index = unpaired.findIndex(own => own.name === attachment.name);
|
|
2816
|
+
return index < 0 ? null : unpaired.splice(index, 1)[0];
|
|
2817
|
+
});
|
|
2818
|
+
return named.map(own => own ?? unpaired.shift());
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2551
2821
|
/**
|
|
2552
2822
|
* Newer Workstation deployments echo `clientMessageId` on the visitor's own
|
|
2553
2823
|
* rows. Such a row replaces the pending or failed bubble it came from instead
|