@convokitapp/vue-ui 0.4.1 → 0.6.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 +73 -0
- package/PARITY.md +35 -6
- package/README.md +107 -1
- package/dist/index.cjs +445 -83
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +46 -1
- package/dist/index.css.map +1 -1
- package/dist/index.d.cts +142 -17
- package/dist/index.d.ts +142 -17
- package/dist/index.js +443 -81
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -35,6 +35,7 @@ __export(index_exports, {
|
|
|
35
35
|
isConvoKitPendingMessage: () => isConvoKitPendingMessage,
|
|
36
36
|
matchesConversation: () => matchesConversation,
|
|
37
37
|
mergeConversations: () => mergeConversations,
|
|
38
|
+
mergeInboxEntries: () => mergeInboxEntries,
|
|
38
39
|
mergeMessages: () => mergeMessages,
|
|
39
40
|
readerIdsFor: () => readerIdsFor,
|
|
40
41
|
useConversation: () => useConversation,
|
|
@@ -54,6 +55,10 @@ function createConvoKitUiClient(client) {
|
|
|
54
55
|
onEvent: handler,
|
|
55
56
|
...onError ? { onError } : {}
|
|
56
57
|
}),
|
|
58
|
+
onInboxActivity: (handler, onError) => client.realtime.onInboxActivity(client.clientId, {
|
|
59
|
+
onEvent: handler,
|
|
60
|
+
...onError ? { onError } : {}
|
|
61
|
+
}),
|
|
57
62
|
onMessageDeleted: (conversationId, handler, onError) => client.realtime.onMessageDeleted(conversationId, {
|
|
58
63
|
onEvent: handler,
|
|
59
64
|
...onError ? { onError } : {}
|
|
@@ -62,11 +67,12 @@ function createConvoKitUiClient(client) {
|
|
|
62
67
|
return client.connected ? client.currentUserId : "";
|
|
63
68
|
},
|
|
64
69
|
getConversations: (options) => client.getConversations(options),
|
|
70
|
+
listInbox: (options) => client.listInbox(options),
|
|
65
71
|
getConversation: (conversationId) => client.getConversation(conversationId),
|
|
66
72
|
getMessages: (options) => client.getMessages(options),
|
|
67
73
|
getMessage: (id) => client.getMessage(id),
|
|
68
74
|
sendMessage: (input) => client.sendMessage(input),
|
|
69
|
-
markConversationRead: (conversationId) => client.markConversationRead(conversationId),
|
|
75
|
+
markConversationRead: (conversationId, options) => client.markConversationRead(conversationId, options),
|
|
70
76
|
sendTyping: (input) => client.sendTyping(input),
|
|
71
77
|
onMessage: (conversationId, handler, onError) => client.realtime.onMessage(conversationId, {
|
|
72
78
|
onEvent: handler,
|
|
@@ -88,9 +94,13 @@ var import_reka_ui = require("reka-ui");
|
|
|
88
94
|
var import_vue2 = require("vue");
|
|
89
95
|
|
|
90
96
|
// src/utils.ts
|
|
97
|
+
var import_sdk = require("@convokitapp/sdk");
|
|
91
98
|
var import_clsx = require("clsx");
|
|
92
99
|
var import_vue = require("vue");
|
|
93
100
|
var pendingMessageIdPrefix = "convokit-pending-";
|
|
101
|
+
function compareMessageOrder(left, right) {
|
|
102
|
+
return left.createdAt.getTime() - right.createdAt.getTime() || (left.id < right.id ? -1 : left.id > right.id ? 1 : 0);
|
|
103
|
+
}
|
|
94
104
|
function isConvoKitPendingMessage(message) {
|
|
95
105
|
return message.id.startsWith(pendingMessageIdPrefix);
|
|
96
106
|
}
|
|
@@ -132,6 +142,31 @@ function mergeConversations(current, incoming) {
|
|
|
132
142
|
for (const conversation of incoming) byId.set(conversation.id, conversation);
|
|
133
143
|
return [...byId.values()];
|
|
134
144
|
}
|
|
145
|
+
function compareInboxOrder(left, right) {
|
|
146
|
+
return right.activityAt.getTime() - left.activityAt.getTime() || (left.conversation.id < right.conversation.id ? 1 : left.conversation.id > right.conversation.id ? -1 : 0);
|
|
147
|
+
}
|
|
148
|
+
function mergeInboxEntries(current, incoming) {
|
|
149
|
+
const byId = new Map(current.map((entry) => [entry.conversation.id, entry]));
|
|
150
|
+
for (const entry of incoming) byId.set(entry.conversation.id, entry);
|
|
151
|
+
return [...byId.values()].sort(compareInboxOrder);
|
|
152
|
+
}
|
|
153
|
+
function inboxPreview(conversation, summary, currentUserId) {
|
|
154
|
+
const message = summary?.latestMessage;
|
|
155
|
+
if (!message) return "";
|
|
156
|
+
const first = message.media[0];
|
|
157
|
+
const body = message.text?.trim() || (!first ? "" : first.type === "image" ? "Photo" : first.type === "file" ? first.name?.trim() || "File" : first.type === "location" ? "Location" : first.type === "contact" ? "Contact" : "");
|
|
158
|
+
if (!body) return "";
|
|
159
|
+
if (currentUserId !== void 0 && message.senderId === currentUserId) return `You: ${body}`;
|
|
160
|
+
if (conversation.participants.length > 2) {
|
|
161
|
+
const sender = conversation.participants.find((participant) => participant.appUserId === message.senderId || participant.id === message.senderId);
|
|
162
|
+
const name = sender?.name.trim();
|
|
163
|
+
if (name) return `${name}: ${body}`;
|
|
164
|
+
}
|
|
165
|
+
return body;
|
|
166
|
+
}
|
|
167
|
+
function formatMessageTime(date) {
|
|
168
|
+
return new Intl.DateTimeFormat(void 0, { hour: "numeric", minute: "2-digit" }).format(date);
|
|
169
|
+
}
|
|
135
170
|
function mergeMessages(current, incoming) {
|
|
136
171
|
const byId = new Map(current.map((message) => [message.id, message]));
|
|
137
172
|
for (const message of incoming) byId.set(message.id, message);
|
|
@@ -139,13 +174,16 @@ function mergeMessages(current, incoming) {
|
|
|
139
174
|
const leftPending = isConvoKitPendingMessage(left);
|
|
140
175
|
const rightPending = isConvoKitPendingMessage(right);
|
|
141
176
|
if (leftPending !== rightPending) return leftPending ? 1 : -1;
|
|
142
|
-
|
|
143
|
-
return byTime === 0 ? left.id.localeCompare(right.id) : byTime;
|
|
177
|
+
return compareMessageOrder(left, right);
|
|
144
178
|
});
|
|
145
179
|
}
|
|
146
|
-
function readerIdsFor(message, readAtByUserId) {
|
|
180
|
+
function readerIdsFor(message, readAtByUserId, readPositionByUserId = /* @__PURE__ */ new Map()) {
|
|
147
181
|
if (isConvoKitPendingMessage(message)) return /* @__PURE__ */ new Set();
|
|
148
|
-
|
|
182
|
+
const userIds = /* @__PURE__ */ new Set([...readAtByUserId.keys(), ...readPositionByUserId.keys()]);
|
|
183
|
+
return new Set([...userIds].filter((userId) => userId !== message.senderId && (0, import_sdk.readThrough)({
|
|
184
|
+
readPosition: readPositionByUserId.get(userId) ?? null,
|
|
185
|
+
lastReadAt: readAtByUserId.get(userId) ?? null
|
|
186
|
+
}, message)));
|
|
149
187
|
}
|
|
150
188
|
function partClass(part, appearance, defaultClass) {
|
|
151
189
|
return cx(!appearance.unstyled && defaultClass, appearance.classNames?.[part]);
|
|
@@ -199,7 +237,7 @@ var import_vue7 = require("vue");
|
|
|
199
237
|
var import_vue3 = require("vue");
|
|
200
238
|
|
|
201
239
|
// src/conversation-store.ts
|
|
202
|
-
var
|
|
240
|
+
var import_sdk2 = require("@convokitapp/sdk");
|
|
203
241
|
function version(message) {
|
|
204
242
|
return message.updatedAt?.getTime() ?? message.createdAt.getTime();
|
|
205
243
|
}
|
|
@@ -209,8 +247,20 @@ function hasContent(message) {
|
|
|
209
247
|
function newest(current, incoming, incomingComplete = true) {
|
|
210
248
|
return version(current) > version(incoming) || !incomingComplete && version(current) === version(incoming) ? current : incoming;
|
|
211
249
|
}
|
|
212
|
-
|
|
213
|
-
|
|
250
|
+
var compare = compareMessageOrder;
|
|
251
|
+
function positionCursor(position) {
|
|
252
|
+
return { createdAt: position.createdAt, id: position.messageId };
|
|
253
|
+
}
|
|
254
|
+
function readEntry(participant) {
|
|
255
|
+
return { userId: participant.appUserId, readAt: participant.lastReadAt, readPosition: participant.readPosition };
|
|
256
|
+
}
|
|
257
|
+
function acknowledgement() {
|
|
258
|
+
return { inFlight: void 0, followUp: false, suppressed: false, target: void 0, acknowledged: void 0, unacknowledgeable: /* @__PURE__ */ new Set() };
|
|
259
|
+
}
|
|
260
|
+
function isTargetMiss(cause) {
|
|
261
|
+
if (typeof cause !== "object" || cause === null) return false;
|
|
262
|
+
const { code, status } = cause;
|
|
263
|
+
return code === "MESSAGE_NOT_FOUND" || code === void 0 && status === 404;
|
|
214
264
|
}
|
|
215
265
|
function blank(currentUserId = "") {
|
|
216
266
|
return {
|
|
@@ -218,6 +268,7 @@ function blank(currentUserId = "") {
|
|
|
218
268
|
messages: [],
|
|
219
269
|
typingUserIds: /* @__PURE__ */ new Set(),
|
|
220
270
|
readAtByUserId: /* @__PURE__ */ new Map(),
|
|
271
|
+
readPositionByUserId: /* @__PURE__ */ new Map(),
|
|
221
272
|
isInitialLoading: false,
|
|
222
273
|
isLoadingOlder: false,
|
|
223
274
|
isReconciling: false,
|
|
@@ -265,6 +316,9 @@ var ConversationStore = class {
|
|
|
265
316
|
hydrationPool = { running: /* @__PURE__ */ new Set(), queued: /* @__PURE__ */ new Map() };
|
|
266
317
|
// Keep tombstones until an explicit reload/session change, including across refreshes.
|
|
267
318
|
deleted = /* @__PURE__ */ new Set();
|
|
319
|
+
ack = acknowledgement();
|
|
320
|
+
// Visible until the platform reports otherwise; unknown/prerender/no document count as visible.
|
|
321
|
+
visible = true;
|
|
268
322
|
sendRevision;
|
|
269
323
|
activeSend;
|
|
270
324
|
refreshQueued = false;
|
|
@@ -327,6 +381,7 @@ var ConversationStore = class {
|
|
|
327
381
|
this.hydrations.clear();
|
|
328
382
|
this.hydrationPool.queued.clear();
|
|
329
383
|
this.deleted.clear();
|
|
384
|
+
this.ack = acknowledgement();
|
|
330
385
|
this.sendRevision = void 0;
|
|
331
386
|
this.activeSend = void 0;
|
|
332
387
|
this.refreshQueued = false;
|
|
@@ -382,8 +437,8 @@ var ConversationStore = class {
|
|
|
382
437
|
if (!this.alive(generation) || conversationId !== this.room || !id.trim()) return;
|
|
383
438
|
this.removeMessage(id);
|
|
384
439
|
}, report));
|
|
385
|
-
add(() => this.client.onReadReceipt(this.room, ({ userId, readAt }) => {
|
|
386
|
-
if (this.alive(generation)) this.mergeReads([
|
|
440
|
+
add(() => this.client.onReadReceipt(this.room, ({ userId, readAt, readPosition }) => {
|
|
441
|
+
if (this.alive(generation)) this.mergeReads([{ userId, readAt, readPosition }]);
|
|
387
442
|
}, report));
|
|
388
443
|
add(() => this.client.onTyping(this.room, ({ userId, isTyping }) => {
|
|
389
444
|
if (!this.alive(generation) || !userId.trim() || userId === this.user) return;
|
|
@@ -426,9 +481,6 @@ var ConversationStore = class {
|
|
|
426
481
|
this.hydrations.set(message.id, job);
|
|
427
482
|
this.hydrationPool.queued.set(message.id, job);
|
|
428
483
|
this.drainHydration();
|
|
429
|
-
if (type === "insert" && !existing && message.senderId !== this.user && (this.options.markReadOnReceive ?? true)) {
|
|
430
|
-
void this.markRead();
|
|
431
|
-
}
|
|
432
484
|
}
|
|
433
485
|
record(message, insert, revision, complete) {
|
|
434
486
|
const existing = this.state.messages.find((item) => item.id === message.id);
|
|
@@ -438,7 +490,9 @@ var ConversationStore = class {
|
|
|
438
490
|
this.confirmSend(message);
|
|
439
491
|
}
|
|
440
492
|
this.changes.set(message.id, { revision, message, insert, complete });
|
|
441
|
-
if (existing
|
|
493
|
+
if (!existing && !(insert && hasContent(message))) return;
|
|
494
|
+
this.patch({ messages: mergeMessages(this.state.messages, [message]) });
|
|
495
|
+
if (!existing && message.senderId !== this.user && (this.options.markReadOnReceive ?? true)) void this.acknowledge(true);
|
|
442
496
|
}
|
|
443
497
|
confirmSend(message) {
|
|
444
498
|
const send = this.activeSend;
|
|
@@ -450,11 +504,19 @@ var ConversationStore = class {
|
|
|
450
504
|
return this.alive(job.generation) && !this.deleted.has(job.message.id) && this.hydrations.get(job.message.id) === job;
|
|
451
505
|
}
|
|
452
506
|
removeMessage(id) {
|
|
507
|
+
this.forget(id);
|
|
508
|
+
this.patch({ messages: this.state.messages.filter((message) => message.id !== id) });
|
|
509
|
+
}
|
|
510
|
+
/** Tombstone a row learned to be gone; a removed acknowledgement target is re-resolved from what remains. */
|
|
511
|
+
forget(id) {
|
|
453
512
|
this.deleted.add(id);
|
|
454
513
|
this.changes.delete(id);
|
|
455
514
|
this.hydrations.delete(id);
|
|
456
515
|
this.hydrationPool.queued.delete(id);
|
|
457
|
-
|
|
516
|
+
const ack = this.ack;
|
|
517
|
+
if (ack.target !== id && ack.acknowledged?.id !== id) return;
|
|
518
|
+
ack.unacknowledgeable.add(id);
|
|
519
|
+
if (ack.target === id) ack.followUp = true;
|
|
458
520
|
}
|
|
459
521
|
drainHydration() {
|
|
460
522
|
const pool = this.hydrationPool;
|
|
@@ -491,13 +553,23 @@ var ConversationStore = class {
|
|
|
491
553
|
});
|
|
492
554
|
}
|
|
493
555
|
}
|
|
556
|
+
/** Both maps only ever advance: acknowledgement times by time, positions by (createdAt, id). Participants and
|
|
557
|
+
* read events are the only sources; the local user's own read is never written from the device clock.
|
|
558
|
+
*/
|
|
494
559
|
mergeReads(entries) {
|
|
495
|
-
const
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
560
|
+
const readAt = new Map(this.state.readAtByUserId);
|
|
561
|
+
const positions = new Map(this.state.readPositionByUserId);
|
|
562
|
+
for (const entry of entries) {
|
|
563
|
+
const { userId, readPosition } = entry;
|
|
564
|
+
if (!userId.trim()) continue;
|
|
565
|
+
if (entry.readAt && Number.isFinite(entry.readAt.getTime()) && entry.readAt.getTime() > (readAt.get(userId)?.getTime() ?? -Infinity)) {
|
|
566
|
+
readAt.set(userId, entry.readAt);
|
|
567
|
+
}
|
|
568
|
+
if (!readPosition || typeof readPosition.messageId !== "string" || !readPosition.messageId.trim() || !(readPosition.createdAt instanceof Date) || !Number.isFinite(readPosition.createdAt.getTime())) continue;
|
|
569
|
+
const current = positions.get(userId);
|
|
570
|
+
if (!current || compare(positionCursor(readPosition), positionCursor(current)) > 0) positions.set(userId, readPosition);
|
|
499
571
|
}
|
|
500
|
-
this.patch({ readAtByUserId:
|
|
572
|
+
this.patch({ readAtByUserId: readAt, readPositionByUserId: positions });
|
|
501
573
|
}
|
|
502
574
|
validatePage(page, before) {
|
|
503
575
|
if (page.length > this.pageSize) throw new Error("Message page exceeds the requested limit");
|
|
@@ -554,9 +626,9 @@ var ConversationStore = class {
|
|
|
554
626
|
this.validatePage(page);
|
|
555
627
|
this.cursor = page.at(-1);
|
|
556
628
|
this.patch({ conversation, messages: this.overlay(page, revision), hasOlderMessages: page.length === this.pageSize });
|
|
557
|
-
this.mergeReads(conversation.participants.
|
|
629
|
+
this.mergeReads(conversation.participants.map(readEntry));
|
|
558
630
|
this.prune(revision);
|
|
559
|
-
if (this.options.markReadOnLoad ?? true) await this.
|
|
631
|
+
if (this.options.markReadOnLoad ?? true) await this.acknowledge(true);
|
|
560
632
|
} catch (cause) {
|
|
561
633
|
this.fail(cause, generation, true);
|
|
562
634
|
} finally {
|
|
@@ -612,14 +684,9 @@ var ConversationStore = class {
|
|
|
612
684
|
const reconciled = this.overlay(rows, revision);
|
|
613
685
|
const survivingIds = new Set(reconciled.map((message) => message.id));
|
|
614
686
|
const known = this.state.messages.filter((message) => !isConvoKitPendingMessage(message)).map((message) => message.id).concat([...this.changes].filter(([, change]) => change.insert && change.revision <= revision).map(([id]) => id));
|
|
615
|
-
for (const id of known) if (!survivingIds.has(id))
|
|
616
|
-
this.deleted.add(id);
|
|
617
|
-
this.changes.delete(id);
|
|
618
|
-
this.hydrations.delete(id);
|
|
619
|
-
this.hydrationPool.queued.delete(id);
|
|
620
|
-
}
|
|
687
|
+
for (const id of known) if (!survivingIds.has(id)) this.forget(id);
|
|
621
688
|
this.patch({ conversation, messages: reconciled, hasOlderMessages: hasOlder });
|
|
622
|
-
this.mergeReads(conversation.participants.
|
|
689
|
+
this.mergeReads(conversation.participants.map(readEntry));
|
|
623
690
|
this.prune(revision);
|
|
624
691
|
} catch (cause) {
|
|
625
692
|
this.fail(cause, generation, true);
|
|
@@ -654,15 +721,68 @@ var ConversationStore = class {
|
|
|
654
721
|
}
|
|
655
722
|
}
|
|
656
723
|
};
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
724
|
+
/** Acknowledge through the newest rendered row now, regardless of visibility; no request without a target. */
|
|
725
|
+
markRead = () => this.alive() ? this.acknowledge(false) : Promise.resolve();
|
|
726
|
+
/** Automatic acknowledgements wait while hidden and are re-issued (once) on becoming visible. */
|
|
727
|
+
setVisible = (visible) => {
|
|
728
|
+
this.visible = visible;
|
|
729
|
+
if (!visible || !this.ack.suppressed || !this.alive()) return;
|
|
730
|
+
this.ack.suppressed = false;
|
|
731
|
+
void this.acknowledge(true);
|
|
732
|
+
};
|
|
733
|
+
/** The newest non-pending rendered row by (createdAt, id), never by list index and never a raw realtime
|
|
734
|
+
* row; rows the server does not know are skipped, and nothing at or before the accepted target is re-sent.
|
|
735
|
+
*/
|
|
736
|
+
ackTarget(ack) {
|
|
737
|
+
let target;
|
|
738
|
+
for (const message of this.state.messages) {
|
|
739
|
+
if (isConvoKitPendingMessage(message) || ack.unacknowledgeable.has(message.id)) continue;
|
|
740
|
+
if (!target || compare(message, target) > 0) target = { createdAt: message.createdAt, id: message.id };
|
|
741
|
+
}
|
|
742
|
+
return target && (!ack.acknowledged || compare(target, ack.acknowledged) > 0) ? target : void 0;
|
|
743
|
+
}
|
|
744
|
+
/** Resolves when the request this call issued or joined settles; a follow-up is issued, not awaited. */
|
|
745
|
+
acknowledge(automatic) {
|
|
746
|
+
const ack = this.ack;
|
|
747
|
+
if (automatic && !this.visible) {
|
|
748
|
+
ack.suppressed = true;
|
|
749
|
+
return Promise.resolve();
|
|
750
|
+
}
|
|
751
|
+
if (ack.inFlight) {
|
|
752
|
+
ack.followUp = true;
|
|
753
|
+
return ack.inFlight;
|
|
754
|
+
}
|
|
755
|
+
return this.issue(ack, this.generation) ?? Promise.resolve();
|
|
756
|
+
}
|
|
757
|
+
issue(ack, generation) {
|
|
758
|
+
ack.followUp = false;
|
|
759
|
+
const target = this.ackTarget(ack);
|
|
760
|
+
if (!target) return void 0;
|
|
761
|
+
ack.target = target.id;
|
|
762
|
+
ack.inFlight = this.send(ack, generation, target).finally(() => {
|
|
763
|
+
ack.inFlight = void 0;
|
|
764
|
+
ack.target = void 0;
|
|
765
|
+
if (!this.alive(generation) || !ack.followUp) return;
|
|
766
|
+
if (!this.visible) {
|
|
767
|
+
ack.followUp = false;
|
|
768
|
+
ack.suppressed = true;
|
|
769
|
+
} else this.issue(ack, generation);
|
|
770
|
+
});
|
|
771
|
+
return ack.inFlight;
|
|
772
|
+
}
|
|
773
|
+
async send(ack, generation, target) {
|
|
660
774
|
try {
|
|
661
|
-
await this.client.markConversationRead(this.room);
|
|
775
|
+
await this.client.markConversationRead(this.room, { throughMessageId: target.id });
|
|
776
|
+
if (!this.alive(generation)) return;
|
|
777
|
+
if (!ack.acknowledged || compare(target, ack.acknowledged) > 0) ack.acknowledged = target;
|
|
662
778
|
} catch (cause) {
|
|
663
|
-
this.
|
|
779
|
+
if (!this.alive(generation)) return;
|
|
780
|
+
if (isTargetMiss(cause)) {
|
|
781
|
+
ack.unacknowledgeable.add(target.id);
|
|
782
|
+
ack.followUp = true;
|
|
783
|
+
} else this.fail(cause, generation);
|
|
664
784
|
}
|
|
665
|
-
}
|
|
785
|
+
}
|
|
666
786
|
updateTyping = async (isTyping) => {
|
|
667
787
|
if (!this.alive()) return;
|
|
668
788
|
const generation = this.generation;
|
|
@@ -691,7 +811,7 @@ var ConversationStore = class {
|
|
|
691
811
|
const generation = this.generation;
|
|
692
812
|
const revision = this.revision;
|
|
693
813
|
this.sendRevision = revision;
|
|
694
|
-
const clientMessageId = (0,
|
|
814
|
+
const clientMessageId = (0, import_sdk2.createClientMessageId)();
|
|
695
815
|
const pendingId = `convokit-pending-${clientMessageId}`;
|
|
696
816
|
const pending = {
|
|
697
817
|
id: pendingId,
|
|
@@ -745,7 +865,7 @@ var ConversationStore = class {
|
|
|
745
865
|
}
|
|
746
866
|
}
|
|
747
867
|
};
|
|
748
|
-
readerIdsFor = (message) => readerIdsFor(message, this.state.readAtByUserId);
|
|
868
|
+
readerIdsFor = (message) => readerIdsFor(message, this.state.readAtByUserId, this.state.readPositionByUserId);
|
|
749
869
|
};
|
|
750
870
|
|
|
751
871
|
// src/composables/use-conversation.ts
|
|
@@ -758,6 +878,7 @@ function useConversation(options) {
|
|
|
758
878
|
let store = createStore();
|
|
759
879
|
const snapshot = (0, import_vue3.shallowRef)(store.getSnapshot());
|
|
760
880
|
let unsubscribe;
|
|
881
|
+
let visible = true;
|
|
761
882
|
const stop = (0, import_vue3.watch)(
|
|
762
883
|
() => [(0, import_vue3.toValue)(options.client), (0, import_vue3.toValue)(options.client).sessionIdentity, (0, import_vue3.toValue)(options.conversationId)],
|
|
763
884
|
() => {
|
|
@@ -768,6 +889,7 @@ function useConversation(options) {
|
|
|
768
889
|
unsubscribe = store.subscribe(() => {
|
|
769
890
|
snapshot.value = store.getSnapshot();
|
|
770
891
|
});
|
|
892
|
+
store.setVisible(visible);
|
|
771
893
|
store.start(options.autoLoad ?? true);
|
|
772
894
|
},
|
|
773
895
|
{ immediate: true, flush: "sync" }
|
|
@@ -787,6 +909,7 @@ function useConversation(options) {
|
|
|
787
909
|
messages: field("messages"),
|
|
788
910
|
typingUserIds: field("typingUserIds"),
|
|
789
911
|
readAtByUserId: field("readAtByUserId"),
|
|
912
|
+
readPositionByUserId: field("readPositionByUserId"),
|
|
790
913
|
isInitialLoading: field("isInitialLoading"),
|
|
791
914
|
isLoadingOlder: field("isLoadingOlder"),
|
|
792
915
|
isReconciling: field("isReconciling"),
|
|
@@ -802,6 +925,10 @@ function useConversation(options) {
|
|
|
802
925
|
sendMessage: (input) => store.sendMessage(input),
|
|
803
926
|
markRead: () => store.markRead(),
|
|
804
927
|
updateTyping: (isTyping) => store.updateTyping(isTyping),
|
|
928
|
+
setVisible: (value) => {
|
|
929
|
+
visible = value;
|
|
930
|
+
store.setVisible(value);
|
|
931
|
+
},
|
|
805
932
|
dispose
|
|
806
933
|
};
|
|
807
934
|
}
|
|
@@ -815,9 +942,6 @@ var appearanceProps = {
|
|
|
815
942
|
density: { type: String, default: "comfortable" },
|
|
816
943
|
unstyled: { type: Boolean, default: false }
|
|
817
944
|
};
|
|
818
|
-
function defaultFormatTime(date) {
|
|
819
|
-
return new Intl.DateTimeFormat(void 0, { hour: "numeric", minute: "2-digit" }).format(date);
|
|
820
|
-
}
|
|
821
945
|
function defaultMedia(media, open, imageLoading) {
|
|
822
946
|
const tag = open ? "button" : "div";
|
|
823
947
|
const interactive = open ? { type: "button", onClick: open } : {};
|
|
@@ -857,6 +981,7 @@ var MessageListView = (0, import_vue5.defineComponent)({
|
|
|
857
981
|
messages: { type: Array, required: true },
|
|
858
982
|
currentUserId: { type: String, required: true },
|
|
859
983
|
readAtByUserId: { type: Object, default: () => /* @__PURE__ */ new Map() },
|
|
984
|
+
readPositionByUserId: { type: Object, default: () => /* @__PURE__ */ new Map() },
|
|
860
985
|
readersResolver: { type: Function, default: void 0 },
|
|
861
986
|
onLoadOlder: { type: Function, default: void 0 },
|
|
862
987
|
hasOlderMessages: { type: Boolean, default: false },
|
|
@@ -867,7 +992,7 @@ var MessageListView = (0, import_vue5.defineComponent)({
|
|
|
867
992
|
paginationThreshold: { type: Number, default: 240 },
|
|
868
993
|
reverse: { type: Boolean, default: true },
|
|
869
994
|
stickToBottom: { type: Boolean, default: true },
|
|
870
|
-
formatTime: { type: Function, default:
|
|
995
|
+
formatTime: { type: Function, default: formatMessageTime },
|
|
871
996
|
imageLoading: { type: String, default: "lazy" }
|
|
872
997
|
},
|
|
873
998
|
emits: ["load-older", "attachment-click"],
|
|
@@ -916,7 +1041,7 @@ var MessageListView = (0, import_vue5.defineComponent)({
|
|
|
916
1041
|
const isCurrentUser = message.senderId === props.currentUserId;
|
|
917
1042
|
const sender = participants.value.get(message.senderId);
|
|
918
1043
|
const isPending = isConvoKitPendingMessage(message);
|
|
919
|
-
const readerIds = isPending ? /* @__PURE__ */ new Set() : props.readersResolver ? props.readersResolver(message) : readerIdsFor(message, props.readAtByUserId);
|
|
1044
|
+
const readerIds = isPending ? /* @__PURE__ */ new Set() : props.readersResolver ? props.readersResolver(message) : readerIdsFor(message, props.readAtByUserId, props.readPositionByUserId);
|
|
920
1045
|
const slotProps = { message, chronologicalIndex: index, isCurrentUser, sender, readerIds };
|
|
921
1046
|
const custom = slots.message?.(slotProps);
|
|
922
1047
|
if (custom) return (0, import_vue5.h)("div", { key: message.id, role: "listitem" }, custom);
|
|
@@ -1015,8 +1140,8 @@ var MessageListView = (0, import_vue5.defineComponent)({
|
|
|
1015
1140
|
};
|
|
1016
1141
|
}
|
|
1017
1142
|
});
|
|
1018
|
-
function defaultReadersResolver(readAtByUserId) {
|
|
1019
|
-
return (message) => readerIdsFor(message, readAtByUserId);
|
|
1143
|
+
function defaultReadersResolver(readAtByUserId, readPositionByUserId = /* @__PURE__ */ new Map()) {
|
|
1144
|
+
return (message) => readerIdsFor(message, readAtByUserId, readPositionByUserId);
|
|
1020
1145
|
}
|
|
1021
1146
|
|
|
1022
1147
|
// src/components/conversation.ts
|
|
@@ -1034,6 +1159,7 @@ var viewProps = {
|
|
|
1034
1159
|
onSendMessage: { type: Function, required: true },
|
|
1035
1160
|
typingUserIds: { type: Object, default: () => /* @__PURE__ */ new Set() },
|
|
1036
1161
|
readAtByUserId: { type: Object, default: () => /* @__PURE__ */ new Map() },
|
|
1162
|
+
readPositionByUserId: { type: Object, default: () => /* @__PURE__ */ new Map() },
|
|
1037
1163
|
readersResolver: { type: Function, default: void 0 },
|
|
1038
1164
|
onBack: { type: Function, default: void 0 },
|
|
1039
1165
|
onRefresh: { type: Function, default: void 0 },
|
|
@@ -1262,6 +1388,7 @@ var ConversationView = (0, import_vue7.defineComponent)({
|
|
|
1262
1388
|
messages: props.messages,
|
|
1263
1389
|
currentUserId: props.currentUserId,
|
|
1264
1390
|
readAtByUserId: props.readAtByUserId,
|
|
1391
|
+
readPositionByUserId: props.readPositionByUserId,
|
|
1265
1392
|
...props.readersResolver ? { readersResolver: props.readersResolver } : {},
|
|
1266
1393
|
...props.onLoadOlder ? { onLoadOlder: loadOlder } : {},
|
|
1267
1394
|
hasOlderMessages: props.hasOlderMessages,
|
|
@@ -1324,6 +1451,12 @@ var Conversation = (0, import_vue7.defineComponent)({
|
|
|
1324
1451
|
(0, import_vue7.watchEffect)(() => {
|
|
1325
1452
|
emit("controller-change", controller);
|
|
1326
1453
|
});
|
|
1454
|
+
if (typeof document !== "undefined") {
|
|
1455
|
+
const syncVisibility = () => controller.setVisible(document.visibilityState !== "hidden");
|
|
1456
|
+
syncVisibility();
|
|
1457
|
+
document.addEventListener("visibilitychange", syncVisibility);
|
|
1458
|
+
(0, import_vue7.onBeforeUnmount)(() => document.removeEventListener("visibilitychange", syncVisibility));
|
|
1459
|
+
}
|
|
1327
1460
|
return () => {
|
|
1328
1461
|
const loadedConversation = controller.conversation.value;
|
|
1329
1462
|
if (!loadedConversation) {
|
|
@@ -1358,6 +1491,7 @@ var Conversation = (0, import_vue7.defineComponent)({
|
|
|
1358
1491
|
onSendMessage: _onSendMessage,
|
|
1359
1492
|
typingUserIds: _typingUserIds,
|
|
1360
1493
|
readAtByUserId: _readAtByUserId,
|
|
1494
|
+
readPositionByUserId: _readPositionByUserId,
|
|
1361
1495
|
onRefresh: _onRefresh,
|
|
1362
1496
|
onLoadOlder: _onLoadOlder,
|
|
1363
1497
|
onTypingChange: _onTypingChange,
|
|
@@ -1380,6 +1514,7 @@ var Conversation = (0, import_vue7.defineComponent)({
|
|
|
1380
1514
|
},
|
|
1381
1515
|
typingUserIds: controller.typingUserIds.value,
|
|
1382
1516
|
readAtByUserId: controller.readAtByUserId.value,
|
|
1517
|
+
readPositionByUserId: controller.readPositionByUserId.value,
|
|
1383
1518
|
onRefresh: controller.refresh,
|
|
1384
1519
|
onLoadOlder: controller.loadOlderMessages,
|
|
1385
1520
|
onTypingChange: controller.updateTyping,
|
|
@@ -1414,30 +1549,64 @@ var import_vue10 = require("vue");
|
|
|
1414
1549
|
var import_vue8 = require("vue");
|
|
1415
1550
|
|
|
1416
1551
|
// src/conversation-list-store.ts
|
|
1552
|
+
var defaultActivityRefreshWindowMs = 500;
|
|
1417
1553
|
function blank2(filter) {
|
|
1418
|
-
return {
|
|
1554
|
+
return {
|
|
1555
|
+
conversations: [],
|
|
1556
|
+
summaries: /* @__PURE__ */ new Map(),
|
|
1557
|
+
currentUserId: "",
|
|
1558
|
+
filter,
|
|
1559
|
+
isInitialLoading: false,
|
|
1560
|
+
isLoadingMore: false,
|
|
1561
|
+
hasMore: true,
|
|
1562
|
+
hasLoaded: false,
|
|
1563
|
+
error: null
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
function statusOf(cause) {
|
|
1567
|
+
return typeof cause === "object" && cause !== null && "status" in cause ? cause.status : void 0;
|
|
1568
|
+
}
|
|
1569
|
+
function summaryOf(entry) {
|
|
1570
|
+
const { conversation: _conversation, ...summary } = entry;
|
|
1571
|
+
return summary;
|
|
1572
|
+
}
|
|
1573
|
+
function ids(entries) {
|
|
1574
|
+
return entries.map((entry) => entry.conversation);
|
|
1419
1575
|
}
|
|
1420
1576
|
var ConversationListStore = class {
|
|
1421
1577
|
constructor(options) {
|
|
1422
1578
|
this.options = options;
|
|
1423
1579
|
this.owner = options.client.sessionIdentity;
|
|
1580
|
+
this.user = this.owner ? options.client.currentUserId : "";
|
|
1424
1581
|
this.pageSize = options.pageSize ?? 30;
|
|
1425
1582
|
if (!Number.isInteger(this.pageSize) || this.pageSize < 1 || this.pageSize > 100) {
|
|
1426
1583
|
throw new RangeError("pageSize must be an integer between 1 and 100");
|
|
1427
1584
|
}
|
|
1585
|
+
this.activityRefreshWindowMs = options.activityRefreshWindowMs ?? defaultActivityRefreshWindowMs;
|
|
1586
|
+
if (!Number.isFinite(this.activityRefreshWindowMs) || this.activityRefreshWindowMs < 0) {
|
|
1587
|
+
throw new RangeError("activityRefreshWindowMs must be a non-negative number");
|
|
1588
|
+
}
|
|
1428
1589
|
this.state = blank2(options.initialFilter ?? {});
|
|
1429
1590
|
}
|
|
1430
1591
|
options;
|
|
1431
1592
|
owner;
|
|
1593
|
+
user;
|
|
1432
1594
|
pageSize;
|
|
1595
|
+
activityRefreshWindowMs;
|
|
1433
1596
|
state;
|
|
1434
1597
|
source = [];
|
|
1598
|
+
entries = [];
|
|
1435
1599
|
offset = 0;
|
|
1600
|
+
cursor = null;
|
|
1601
|
+
inboxUnavailable = false;
|
|
1602
|
+
inboxWarned = false;
|
|
1436
1603
|
generation = 0;
|
|
1437
1604
|
lifecycleGeneration = 0;
|
|
1438
1605
|
disposed = true;
|
|
1439
1606
|
lifecycle;
|
|
1440
1607
|
inbox;
|
|
1608
|
+
activity;
|
|
1609
|
+
activityTimer;
|
|
1441
1610
|
refreshQueued = false;
|
|
1442
1611
|
refreshing = false;
|
|
1443
1612
|
listeners = /* @__PURE__ */ new Set();
|
|
@@ -1455,12 +1624,27 @@ var ConversationListStore = class {
|
|
|
1455
1624
|
alive(generation = this.generation) {
|
|
1456
1625
|
return !this.disposed && generation === this.generation && this.owner !== null && this.options.client.sessionIdentity === this.owner;
|
|
1457
1626
|
}
|
|
1627
|
+
get inboxMode() {
|
|
1628
|
+
return !this.options.pageLoader && typeof this.options.client.listInbox === "function" && !this.inboxUnavailable;
|
|
1629
|
+
}
|
|
1630
|
+
currentUserId() {
|
|
1631
|
+
return this.inboxMode ? this.user : "";
|
|
1632
|
+
}
|
|
1458
1633
|
start = (autoLoad = true) => {
|
|
1459
1634
|
if (!this.owner || this.options.client.sessionIdentity !== this.owner) return;
|
|
1460
1635
|
if (!this.disposed) return;
|
|
1461
1636
|
this.disposed = false;
|
|
1637
|
+
this.inboxUnavailable = false;
|
|
1462
1638
|
const lifecycleGeneration = ++this.lifecycleGeneration;
|
|
1639
|
+
const current = () => this.alive() && lifecycleGeneration === this.lifecycleGeneration;
|
|
1640
|
+
const reconcile = (cause) => {
|
|
1641
|
+
if (current()) {
|
|
1642
|
+
this.patch({ error: cause });
|
|
1643
|
+
this.queueRefresh();
|
|
1644
|
+
}
|
|
1645
|
+
};
|
|
1463
1646
|
try {
|
|
1647
|
+
this.patch({ currentUserId: this.currentUserId() });
|
|
1464
1648
|
const subscription = this.options.client.onConnectionEvent({
|
|
1465
1649
|
onEvent: () => {
|
|
1466
1650
|
},
|
|
@@ -1472,15 +1656,20 @@ var ConversationListStore = class {
|
|
|
1472
1656
|
else void subscription.unsubscribe().catch(() => void 0);
|
|
1473
1657
|
if (!this.alive()) return;
|
|
1474
1658
|
const inbox = this.options.client.onInboxChanged(() => {
|
|
1475
|
-
if (
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
this.queueRefresh();
|
|
1480
|
-
}
|
|
1481
|
-
});
|
|
1659
|
+
if (!current()) return;
|
|
1660
|
+
this.clearActivityTimer();
|
|
1661
|
+
this.queueRefresh();
|
|
1662
|
+
}, reconcile);
|
|
1482
1663
|
if (this.alive()) this.inbox = inbox;
|
|
1483
1664
|
else void inbox.unsubscribe().catch(() => void 0);
|
|
1665
|
+
if (!this.alive()) return;
|
|
1666
|
+
if (this.inboxMode && typeof this.options.client.onInboxActivity === "function") {
|
|
1667
|
+
const activity = this.options.client.onInboxActivity(() => {
|
|
1668
|
+
if (current()) this.scheduleActivityRefresh(lifecycleGeneration);
|
|
1669
|
+
}, reconcile);
|
|
1670
|
+
if (this.alive()) this.activity = activity;
|
|
1671
|
+
else void activity.unsubscribe().catch(() => void 0);
|
|
1672
|
+
}
|
|
1484
1673
|
if (autoLoad) void this.loadInitial();
|
|
1485
1674
|
} catch (cause) {
|
|
1486
1675
|
if (this.alive()) this.patch({ error: cause });
|
|
@@ -1490,27 +1679,109 @@ var ConversationListStore = class {
|
|
|
1490
1679
|
this.disposed = true;
|
|
1491
1680
|
this.generation++;
|
|
1492
1681
|
this.lifecycleGeneration++;
|
|
1682
|
+
this.clearActivityTimer();
|
|
1493
1683
|
const subscription = this.lifecycle;
|
|
1494
1684
|
this.lifecycle = void 0;
|
|
1495
1685
|
if (subscription) void subscription.unsubscribe().catch(() => void 0);
|
|
1496
1686
|
if (this.inbox) void this.inbox.unsubscribe().catch(() => void 0);
|
|
1497
1687
|
this.inbox = void 0;
|
|
1688
|
+
this.stopActivity();
|
|
1498
1689
|
this.refreshQueued = false;
|
|
1499
1690
|
this.refreshing = false;
|
|
1500
1691
|
this.source = [];
|
|
1692
|
+
this.entries = [];
|
|
1501
1693
|
this.offset = 0;
|
|
1694
|
+
this.cursor = null;
|
|
1502
1695
|
this.patch(blank2(this.state.filter));
|
|
1503
1696
|
};
|
|
1697
|
+
stopActivity() {
|
|
1698
|
+
if (this.activity) void this.activity.unsubscribe().catch(() => void 0);
|
|
1699
|
+
this.activity = void 0;
|
|
1700
|
+
}
|
|
1701
|
+
clearActivityTimer() {
|
|
1702
|
+
if (this.activityTimer !== void 0) clearTimeout(this.activityTimer);
|
|
1703
|
+
this.activityTimer = void 0;
|
|
1704
|
+
}
|
|
1705
|
+
/** Max-wait throttle: the first signal opens a window; later signals wait for it; one refresh runs when it closes. */
|
|
1706
|
+
scheduleActivityRefresh(lifecycleGeneration) {
|
|
1707
|
+
if (this.activityRefreshWindowMs === 0) return this.queueRefresh();
|
|
1708
|
+
if (this.activityTimer !== void 0) return;
|
|
1709
|
+
const timer = setTimeout(() => {
|
|
1710
|
+
this.activityTimer = void 0;
|
|
1711
|
+
if (this.alive() && lifecycleGeneration === this.lifecycleGeneration) this.queueRefresh();
|
|
1712
|
+
}, this.activityRefreshWindowMs);
|
|
1713
|
+
timer.unref?.();
|
|
1714
|
+
this.activityTimer = timer;
|
|
1715
|
+
}
|
|
1504
1716
|
fail(cause, generation) {
|
|
1505
1717
|
if (!this.alive(generation)) return;
|
|
1506
|
-
const status =
|
|
1718
|
+
const status = statusOf(cause);
|
|
1507
1719
|
if (status === 401 || status === 403 || status === 404) {
|
|
1508
1720
|
this.source = [];
|
|
1721
|
+
this.entries = [];
|
|
1509
1722
|
this.offset = 0;
|
|
1510
|
-
this.
|
|
1723
|
+
this.cursor = null;
|
|
1724
|
+
this.patch({ conversations: [], summaries: /* @__PURE__ */ new Map(), hasMore: false });
|
|
1511
1725
|
}
|
|
1512
1726
|
this.patch({ error: cause });
|
|
1513
1727
|
}
|
|
1728
|
+
/** Run an operation in inbox mode, falling back to the legacy path for the rest of this store's life when the
|
|
1729
|
+
* inbox route is absent (404: rollback, staging). Loaded rows are kept and the same operation continues.
|
|
1730
|
+
*/
|
|
1731
|
+
async withFallback(generation, inbox, legacy) {
|
|
1732
|
+
if (!this.inboxMode) return legacy();
|
|
1733
|
+
try {
|
|
1734
|
+
await inbox();
|
|
1735
|
+
} catch (cause) {
|
|
1736
|
+
if (!this.alive(generation) || statusOf(cause) !== 404) throw cause;
|
|
1737
|
+
this.inboxUnavailable = true;
|
|
1738
|
+
this.clearActivityTimer();
|
|
1739
|
+
this.stopActivity();
|
|
1740
|
+
this.entries = [];
|
|
1741
|
+
this.cursor = null;
|
|
1742
|
+
this.offset = this.source.length;
|
|
1743
|
+
if (!this.inboxWarned) {
|
|
1744
|
+
this.inboxWarned = true;
|
|
1745
|
+
console.warn("ConvoKit inbox endpoint unavailable (404); using getConversations without previews or unread counts.");
|
|
1746
|
+
}
|
|
1747
|
+
this.patch({ summaries: /* @__PURE__ */ new Map(), currentUserId: "" });
|
|
1748
|
+
await legacy();
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
validateInboxPage(page, limit, requestedCursor) {
|
|
1752
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1753
|
+
for (const entry of page.entries) {
|
|
1754
|
+
const id = entry.conversation.id;
|
|
1755
|
+
if (!id.trim() || seen.has(id)) throw new Error("Invalid conversation page");
|
|
1756
|
+
seen.add(id);
|
|
1757
|
+
}
|
|
1758
|
+
if (page.entries.length > limit) throw new Error("Invalid conversation page");
|
|
1759
|
+
if (page.nextCursor !== null && (page.nextCursor === requestedCursor || page.entries.length === 0)) {
|
|
1760
|
+
throw new Error("Inbox pagination did not advance");
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
/** Swap rows, summaries, cursor and hasMore together, filtered by the filter current at commit time. */
|
|
1764
|
+
commitInbox(entries, cursor) {
|
|
1765
|
+
this.entries = entries;
|
|
1766
|
+
this.source = ids(entries);
|
|
1767
|
+
this.cursor = cursor;
|
|
1768
|
+
this.patch({
|
|
1769
|
+
conversations: applyConversationFilter(this.source, this.state.filter),
|
|
1770
|
+
summaries: new Map(entries.map((entry) => [entry.conversation.id, summaryOf(entry)])),
|
|
1771
|
+
hasMore: cursor !== null
|
|
1772
|
+
});
|
|
1773
|
+
}
|
|
1774
|
+
async loadInboxUntilVisible(generation, filter) {
|
|
1775
|
+
const visibleBefore = applyConversationFilter(this.source, filter).length;
|
|
1776
|
+
while (this.alive(generation)) {
|
|
1777
|
+
const requested = this.cursor;
|
|
1778
|
+
const page = await this.options.client.listInbox({ limit: this.pageSize, cursor: requested, archived: filter.archived ?? false });
|
|
1779
|
+
if (!this.alive(generation)) return;
|
|
1780
|
+
this.validateInboxPage(page, this.pageSize, requested);
|
|
1781
|
+
this.commitInbox(mergeInboxEntries(this.entries, page.entries), page.nextCursor);
|
|
1782
|
+
if (page.nextCursor === null || this.state.conversations.length > visibleBefore) return;
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1514
1785
|
async loadUntilVisible(generation, filter) {
|
|
1515
1786
|
const visibleBefore = applyConversationFilter(this.source, filter).length;
|
|
1516
1787
|
while (this.alive(generation)) {
|
|
@@ -1537,10 +1808,17 @@ var ConversationListStore = class {
|
|
|
1537
1808
|
const generation = ++this.generation;
|
|
1538
1809
|
this.refreshing = false;
|
|
1539
1810
|
this.source = [];
|
|
1811
|
+
this.entries = [];
|
|
1540
1812
|
this.offset = 0;
|
|
1541
|
-
this.
|
|
1813
|
+
this.cursor = null;
|
|
1814
|
+
this.patch({ ...blank2(this.state.filter), currentUserId: this.currentUserId(), isInitialLoading: true });
|
|
1815
|
+
const filter = this.state.filter;
|
|
1542
1816
|
try {
|
|
1543
|
-
await this.
|
|
1817
|
+
await this.withFallback(
|
|
1818
|
+
generation,
|
|
1819
|
+
() => this.loadInboxUntilVisible(generation, filter),
|
|
1820
|
+
() => this.loadUntilVisible(generation, filter)
|
|
1821
|
+
);
|
|
1544
1822
|
} catch (cause) {
|
|
1545
1823
|
this.fail(cause, generation);
|
|
1546
1824
|
} finally {
|
|
@@ -1562,6 +1840,47 @@ var ConversationListStore = class {
|
|
|
1562
1840
|
void this.refresh();
|
|
1563
1841
|
});
|
|
1564
1842
|
}
|
|
1843
|
+
/** Re-walk the inbox from the head until the loaded window is covered and something is visible, or the inbox
|
|
1844
|
+
* ends. Rooms that moved are re-positioned by the merge; an exhausted inbox publishes what it found.
|
|
1845
|
+
*/
|
|
1846
|
+
async refreshInbox(generation, filter) {
|
|
1847
|
+
const target = Math.max(this.pageSize, this.entries.length);
|
|
1848
|
+
let rows = [], consumed = 0, cursor = null;
|
|
1849
|
+
while (this.alive(generation)) {
|
|
1850
|
+
const remaining = target - consumed;
|
|
1851
|
+
const limit = remaining >= 1 ? Math.min(100, remaining) : this.pageSize;
|
|
1852
|
+
const page = await this.options.client.listInbox({ limit, cursor, archived: filter.archived ?? false });
|
|
1853
|
+
if (!this.alive(generation)) return;
|
|
1854
|
+
this.validateInboxPage(page, limit, cursor);
|
|
1855
|
+
rows = mergeInboxEntries(rows, page.entries);
|
|
1856
|
+
consumed += page.entries.length;
|
|
1857
|
+
cursor = page.nextCursor;
|
|
1858
|
+
if (cursor === null || consumed >= target && applyConversationFilter(ids(rows), this.state.filter).length > 0) break;
|
|
1859
|
+
}
|
|
1860
|
+
if (!this.alive(generation)) return;
|
|
1861
|
+
this.commitInbox(rows, cursor);
|
|
1862
|
+
}
|
|
1863
|
+
async refreshLegacy(generation, filter) {
|
|
1864
|
+
const target = Math.max(this.pageSize, this.offset);
|
|
1865
|
+
const compare2 = (a, b) => a.createdAt.getTime() - b.createdAt.getTime() || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
1866
|
+
const boundary = this.options.pageLoader ? void 0 : this.source.reduce((oldest, row) => !oldest || compare2(row, oldest) < 0 ? row : oldest, void 0);
|
|
1867
|
+
let rows = [], offset = 0, hasMore = true;
|
|
1868
|
+
while (this.alive(generation)) {
|
|
1869
|
+
const page = this.options.pageLoader ? await this.options.pageLoader({ limit: this.pageSize, offset, filter }) : await this.options.client.getConversations({ limit: this.pageSize, offset, archived: filter.archived ?? false });
|
|
1870
|
+
if (!this.alive(generation)) return;
|
|
1871
|
+
if (page.length > this.pageSize || page.some((row) => !row.id.trim())) throw new Error("Invalid conversation page");
|
|
1872
|
+
const merged = mergeConversations(rows, page);
|
|
1873
|
+
if (page.length === this.pageSize && merged.length === rows.length) throw new Error("Conversation pagination did not advance");
|
|
1874
|
+
rows = merged;
|
|
1875
|
+
offset += page.length;
|
|
1876
|
+
hasMore = page.length === this.pageSize;
|
|
1877
|
+
if (!hasMore || offset >= target && applyConversationFilter(rows, this.state.filter).length > 0 && (!boundary || page.some((row) => compare2(row, boundary) <= 0))) break;
|
|
1878
|
+
}
|
|
1879
|
+
if (!this.alive(generation)) return;
|
|
1880
|
+
this.source = rows;
|
|
1881
|
+
this.offset = offset;
|
|
1882
|
+
this.patch({ conversations: applyConversationFilter(rows, this.state.filter), hasMore });
|
|
1883
|
+
}
|
|
1565
1884
|
/** Replace the loaded window atomically, retaining filters and rows during transient failures. */
|
|
1566
1885
|
refresh = async () => {
|
|
1567
1886
|
if (!this.alive()) return;
|
|
@@ -1572,28 +1891,14 @@ var ConversationListStore = class {
|
|
|
1572
1891
|
if (!this.state.hasLoaded) return this.loadInitial();
|
|
1573
1892
|
const generation = this.generation;
|
|
1574
1893
|
const filter = this.state.filter;
|
|
1575
|
-
const target = Math.max(this.pageSize, this.offset);
|
|
1576
|
-
const compare2 = (a, b) => a.createdAt.getTime() - b.createdAt.getTime() || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
1577
|
-
const boundary = this.options.pageLoader ? void 0 : this.source.reduce((oldest, row) => !oldest || compare2(row, oldest) < 0 ? row : oldest, void 0);
|
|
1578
1894
|
this.refreshing = true;
|
|
1579
1895
|
this.patch({ error: null });
|
|
1580
1896
|
try {
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
const merged = mergeConversations(rows, page);
|
|
1587
|
-
if (page.length === this.pageSize && merged.length === rows.length) throw new Error("Conversation pagination did not advance");
|
|
1588
|
-
rows = merged;
|
|
1589
|
-
offset += page.length;
|
|
1590
|
-
hasMore = page.length === this.pageSize;
|
|
1591
|
-
if (!hasMore || offset >= target && applyConversationFilter(rows, this.state.filter).length > 0 && (!boundary || page.some((row) => compare2(row, boundary) <= 0))) break;
|
|
1592
|
-
}
|
|
1593
|
-
if (!this.alive(generation)) return;
|
|
1594
|
-
this.source = rows;
|
|
1595
|
-
this.offset = offset;
|
|
1596
|
-
this.patch({ conversations: applyConversationFilter(rows, this.state.filter), hasMore });
|
|
1897
|
+
await this.withFallback(
|
|
1898
|
+
generation,
|
|
1899
|
+
() => this.refreshInbox(generation, filter),
|
|
1900
|
+
() => this.refreshLegacy(generation, filter)
|
|
1901
|
+
);
|
|
1597
1902
|
} catch (cause) {
|
|
1598
1903
|
this.fail(cause, generation);
|
|
1599
1904
|
} finally {
|
|
@@ -1606,9 +1911,14 @@ var ConversationListStore = class {
|
|
|
1606
1911
|
loadMore = async () => {
|
|
1607
1912
|
if (!this.alive() || this.refreshing || this.state.isInitialLoading || this.state.isLoadingMore || !this.state.hasMore) return;
|
|
1608
1913
|
const generation = this.generation;
|
|
1914
|
+
const filter = this.state.filter;
|
|
1609
1915
|
this.patch({ isLoadingMore: true, error: null });
|
|
1610
1916
|
try {
|
|
1611
|
-
await this.
|
|
1917
|
+
await this.withFallback(
|
|
1918
|
+
generation,
|
|
1919
|
+
() => this.loadInboxUntilVisible(generation, filter),
|
|
1920
|
+
() => this.loadUntilVisible(generation, filter)
|
|
1921
|
+
);
|
|
1612
1922
|
} catch (cause) {
|
|
1613
1923
|
this.fail(cause, generation);
|
|
1614
1924
|
} finally {
|
|
@@ -1660,6 +1970,8 @@ function useConversationList(options) {
|
|
|
1660
1970
|
});
|
|
1661
1971
|
return {
|
|
1662
1972
|
conversations: field("conversations"),
|
|
1973
|
+
summaries: field("summaries"),
|
|
1974
|
+
currentUserId: field("currentUserId"),
|
|
1663
1975
|
filter: field("filter"),
|
|
1664
1976
|
isInitialLoading: field("isInitialLoading"),
|
|
1665
1977
|
isLoadingMore: field("isLoadingMore"),
|
|
@@ -1685,6 +1997,8 @@ var appearanceProps3 = {
|
|
|
1685
1997
|
var listViewProps = {
|
|
1686
1998
|
...appearanceProps3,
|
|
1687
1999
|
conversations: { type: Array, required: true },
|
|
2000
|
+
summaries: { type: Object, default: void 0 },
|
|
2001
|
+
currentUserId: { type: String, default: void 0 },
|
|
1688
2002
|
selectedConversationId: { type: String, default: void 0 },
|
|
1689
2003
|
onConversationSelect: { type: Function, default: void 0 },
|
|
1690
2004
|
onRefresh: { type: Function, default: void 0 },
|
|
@@ -1734,6 +2048,25 @@ var ConversationListView = (0, import_vue10.defineComponent)({
|
|
|
1734
2048
|
const refresh = () => {
|
|
1735
2049
|
return props.onRefresh?.();
|
|
1736
2050
|
};
|
|
2051
|
+
const inlineRetry = () => {
|
|
2052
|
+
if (props.hasMore && props.onLoadMore) return () => {
|
|
2053
|
+
lastRequestedLength = null;
|
|
2054
|
+
void requestMore();
|
|
2055
|
+
};
|
|
2056
|
+
if (props.onRefresh) return () => {
|
|
2057
|
+
void refresh();
|
|
2058
|
+
};
|
|
2059
|
+
return void 0;
|
|
2060
|
+
};
|
|
2061
|
+
const unreadBadge = (summary) => {
|
|
2062
|
+
if (summary.unreadCount <= 0 && !summary.unreadCountCapped) return null;
|
|
2063
|
+
const capped = summary.unreadCountCapped || summary.unreadCount > 99;
|
|
2064
|
+
return (0, import_vue10.h)("span", {
|
|
2065
|
+
class: "ckui-unread-badge",
|
|
2066
|
+
role: "img",
|
|
2067
|
+
"aria-label": `${summary.unreadCountCapped ? "99+" : summary.unreadCount} unread`
|
|
2068
|
+
}, [(0, import_vue10.h)("span", { "aria-hidden": "true" }, capped ? "99+" : String(summary.unreadCount))]);
|
|
2069
|
+
};
|
|
1737
2070
|
const renderContent = () => {
|
|
1738
2071
|
const currentAppearance = appearance();
|
|
1739
2072
|
if (props.isInitialLoading && props.conversations.length === 0) {
|
|
@@ -1765,10 +2098,21 @@ var ConversationListView = (0, import_vue10.defineComponent)({
|
|
|
1765
2098
|
const children = props.conversations.flatMap((conversation, index) => {
|
|
1766
2099
|
const selected = props.selectedConversationId === conversation.id;
|
|
1767
2100
|
const select = () => selectConversation(conversation);
|
|
1768
|
-
const
|
|
2101
|
+
const summary = props.summaries?.get(conversation.id);
|
|
2102
|
+
const slotProps = {
|
|
2103
|
+
conversation,
|
|
2104
|
+
index,
|
|
2105
|
+
selected,
|
|
2106
|
+
select,
|
|
2107
|
+
...summary ? { summary } : {},
|
|
2108
|
+
...props.currentUserId === void 0 ? {} : { currentUserId: props.currentUserId }
|
|
2109
|
+
};
|
|
2110
|
+
const preview = inboxPreview(conversation, summary, props.currentUserId);
|
|
2111
|
+
const unread = summary !== void 0 && (summary.unreadCount > 0 || summary.unreadCountCapped);
|
|
1769
2112
|
const item = slots["conversation-item"]?.(slotProps) ?? (0, import_vue10.h)("button", {
|
|
1770
2113
|
type: "button",
|
|
1771
2114
|
"data-selected": selected || void 0,
|
|
2115
|
+
"data-unread": unread || void 0,
|
|
1772
2116
|
"aria-current": selected ? "true" : void 0,
|
|
1773
2117
|
onClick: select,
|
|
1774
2118
|
class: partClass("listItem", currentAppearance, "ckui-conversation-item"),
|
|
@@ -1782,8 +2126,14 @@ var ConversationListView = (0, import_vue10.defineComponent)({
|
|
|
1782
2126
|
}),
|
|
1783
2127
|
(0, import_vue10.h)("span", { class: "ckui-conversation-item__body" }, [
|
|
1784
2128
|
(0, import_vue10.h)("strong", conversation.displayTitle),
|
|
1785
|
-
(0, import_vue10.h)("span", conversation.participants.map((participant) => participant.name).join(", ") || conversation.description || "No participants")
|
|
2129
|
+
(0, import_vue10.h)("span", preview || conversation.participants.map((participant) => participant.name).join(", ") || conversation.description || "No participants")
|
|
1786
2130
|
]),
|
|
2131
|
+
// Spread rather than emit `null`: a null child renders a `<!---->` comment, and rows without a
|
|
2132
|
+
// summary must keep 0.5's exact markup.
|
|
2133
|
+
...summary ? [(0, import_vue10.h)("span", { class: "ckui-conversation-item__meta" }, [
|
|
2134
|
+
(0, import_vue10.h)("time", { class: "ckui-conversation-item__time", datetime: summary.activityAt.toISOString() }, formatMessageTime(summary.activityAt)),
|
|
2135
|
+
unreadBadge(summary)
|
|
2136
|
+
])] : [],
|
|
1787
2137
|
(0, import_vue10.h)(import_vue9.ChevronRight, { size: 18, "aria-hidden": "true" })
|
|
1788
2138
|
]);
|
|
1789
2139
|
const nodes = [(0, import_vue10.h)("div", { key: conversation.id, role: "listitem" }, [item])];
|
|
@@ -1793,13 +2143,15 @@ var ConversationListView = (0, import_vue10.defineComponent)({
|
|
|
1793
2143
|
return nodes;
|
|
1794
2144
|
});
|
|
1795
2145
|
if (props.error) {
|
|
1796
|
-
|
|
2146
|
+
const retry = inlineRetry();
|
|
2147
|
+
children.push(slots.error?.({ error: props.error, ...retry ? { retry } : {} }) ?? (0, import_vue10.h)("div", {
|
|
1797
2148
|
class: partClass("error", currentAppearance, "ckui-inline-state ckui-state--error"),
|
|
1798
2149
|
style: partStyle("error", currentAppearance),
|
|
1799
2150
|
role: "alert"
|
|
1800
|
-
}, [
|
|
1801
|
-
|
|
1802
|
-
|
|
2151
|
+
}, [
|
|
2152
|
+
(0, import_vue10.h)("span", errorMessage(props.error)),
|
|
2153
|
+
...retry ? [(0, import_vue10.h)("button", { type: "button", class: "ckui-link-button", onClick: retry }, "Retry")] : []
|
|
2154
|
+
]));
|
|
1803
2155
|
} else if (props.isLoadingMore) {
|
|
1804
2156
|
children.push(slots["load-more"]?.() ?? (0, import_vue10.h)("div", {
|
|
1805
2157
|
class: partClass("loading", currentAppearance, "ckui-inline-state"),
|
|
@@ -1860,6 +2212,7 @@ var ConversationList = (0, import_vue10.defineComponent)({
|
|
|
1860
2212
|
initialFilter: { type: Object, default: void 0 },
|
|
1861
2213
|
pageSize: { type: Number, default: 30 },
|
|
1862
2214
|
autoLoad: { type: Boolean, default: true },
|
|
2215
|
+
activityRefreshWindowMs: { type: Number, default: void 0 },
|
|
1863
2216
|
onControllerChange: { type: Function, default: void 0 }
|
|
1864
2217
|
},
|
|
1865
2218
|
emits: ["conversation-select", "controller-change"],
|
|
@@ -1869,7 +2222,8 @@ var ConversationList = (0, import_vue10.defineComponent)({
|
|
|
1869
2222
|
...props.pageLoader ? { pageLoader: props.pageLoader } : {},
|
|
1870
2223
|
...props.initialFilter ? { initialFilter: props.initialFilter } : {},
|
|
1871
2224
|
pageSize: props.pageSize,
|
|
1872
|
-
autoLoad: props.autoLoad
|
|
2225
|
+
autoLoad: props.autoLoad,
|
|
2226
|
+
...props.activityRefreshWindowMs === void 0 ? {} : { activityRefreshWindowMs: props.activityRefreshWindowMs }
|
|
1873
2227
|
});
|
|
1874
2228
|
expose({ controller });
|
|
1875
2229
|
(0, import_vue10.watchEffect)(() => {
|
|
@@ -1882,8 +2236,11 @@ var ConversationList = (0, import_vue10.defineComponent)({
|
|
|
1882
2236
|
initialFilter: _initialFilter,
|
|
1883
2237
|
pageSize: _pageSize,
|
|
1884
2238
|
autoLoad: _autoLoad,
|
|
2239
|
+
activityRefreshWindowMs: _activityRefreshWindowMs,
|
|
1885
2240
|
onControllerChange: _onControllerChange,
|
|
1886
2241
|
conversations: _conversations,
|
|
2242
|
+
summaries: _summaries,
|
|
2243
|
+
currentUserId: _currentUserId,
|
|
1887
2244
|
onRefresh: _onRefresh,
|
|
1888
2245
|
onLoadMore: _onLoadMore,
|
|
1889
2246
|
isInitialLoading: _isInitialLoading,
|
|
@@ -1896,6 +2253,8 @@ var ConversationList = (0, import_vue10.defineComponent)({
|
|
|
1896
2253
|
...attrs,
|
|
1897
2254
|
...forwarded,
|
|
1898
2255
|
conversations: controller.conversations.value,
|
|
2256
|
+
summaries: controller.summaries.value,
|
|
2257
|
+
currentUserId: controller.currentUserId.value,
|
|
1899
2258
|
onRefresh: controller.refresh,
|
|
1900
2259
|
onLoadMore: controller.loadMore,
|
|
1901
2260
|
isInitialLoading: controller.isInitialLoading.value,
|
|
@@ -1923,6 +2282,7 @@ var defaultConvoKitTheme = {
|
|
|
1923
2282
|
incomingBubble: "#f4f4f5",
|
|
1924
2283
|
outgoingBubble: "#18181b",
|
|
1925
2284
|
outgoingText: "#fafafa",
|
|
2285
|
+
badge: "#18181b",
|
|
1926
2286
|
radius: "10px",
|
|
1927
2287
|
avatarSize: "40px",
|
|
1928
2288
|
fontFamily: 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'
|
|
@@ -1954,6 +2314,7 @@ var ConvoKitThemeProvider = (0, import_vue11.defineComponent)({
|
|
|
1954
2314
|
"--ckui-incoming": theme.incomingBubble,
|
|
1955
2315
|
"--ckui-outgoing": theme.outgoingBubble,
|
|
1956
2316
|
"--ckui-outgoing-text": theme.outgoingText,
|
|
2317
|
+
"--ckui-badge": theme.badge,
|
|
1957
2318
|
"--ckui-radius": theme.radius,
|
|
1958
2319
|
"--ckui-avatar-size": theme.avatarSize,
|
|
1959
2320
|
"--ckui-font": theme.fontFamily
|
|
@@ -1986,6 +2347,7 @@ function useConvoKitTheme() {
|
|
|
1986
2347
|
isConvoKitPendingMessage,
|
|
1987
2348
|
matchesConversation,
|
|
1988
2349
|
mergeConversations,
|
|
2350
|
+
mergeInboxEntries,
|
|
1989
2351
|
mergeMessages,
|
|
1990
2352
|
readerIdsFor,
|
|
1991
2353
|
useConversation,
|