@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.js
CHANGED
|
@@ -9,6 +9,10 @@ function createConvoKitUiClient(client) {
|
|
|
9
9
|
onEvent: handler,
|
|
10
10
|
...onError ? { onError } : {}
|
|
11
11
|
}),
|
|
12
|
+
onInboxActivity: (handler, onError) => client.realtime.onInboxActivity(client.clientId, {
|
|
13
|
+
onEvent: handler,
|
|
14
|
+
...onError ? { onError } : {}
|
|
15
|
+
}),
|
|
12
16
|
onMessageDeleted: (conversationId, handler, onError) => client.realtime.onMessageDeleted(conversationId, {
|
|
13
17
|
onEvent: handler,
|
|
14
18
|
...onError ? { onError } : {}
|
|
@@ -17,11 +21,12 @@ function createConvoKitUiClient(client) {
|
|
|
17
21
|
return client.connected ? client.currentUserId : "";
|
|
18
22
|
},
|
|
19
23
|
getConversations: (options) => client.getConversations(options),
|
|
24
|
+
listInbox: (options) => client.listInbox(options),
|
|
20
25
|
getConversation: (conversationId) => client.getConversation(conversationId),
|
|
21
26
|
getMessages: (options) => client.getMessages(options),
|
|
22
27
|
getMessage: (id) => client.getMessage(id),
|
|
23
28
|
sendMessage: (input) => client.sendMessage(input),
|
|
24
|
-
markConversationRead: (conversationId) => client.markConversationRead(conversationId),
|
|
29
|
+
markConversationRead: (conversationId, options) => client.markConversationRead(conversationId, options),
|
|
25
30
|
sendTyping: (input) => client.sendTyping(input),
|
|
26
31
|
onMessage: (conversationId, handler, onError) => client.realtime.onMessage(conversationId, {
|
|
27
32
|
onEvent: handler,
|
|
@@ -43,9 +48,13 @@ import { AvatarFallback, AvatarImage, AvatarRoot } from "reka-ui";
|
|
|
43
48
|
import { defineComponent, h } from "vue";
|
|
44
49
|
|
|
45
50
|
// src/utils.ts
|
|
51
|
+
import { readThrough } from "@convokitapp/sdk";
|
|
46
52
|
import { clsx } from "clsx";
|
|
47
53
|
import { normalizeClass } from "vue";
|
|
48
54
|
var pendingMessageIdPrefix = "convokit-pending-";
|
|
55
|
+
function compareMessageOrder(left, right) {
|
|
56
|
+
return left.createdAt.getTime() - right.createdAt.getTime() || (left.id < right.id ? -1 : left.id > right.id ? 1 : 0);
|
|
57
|
+
}
|
|
49
58
|
function isConvoKitPendingMessage(message) {
|
|
50
59
|
return message.id.startsWith(pendingMessageIdPrefix);
|
|
51
60
|
}
|
|
@@ -87,6 +96,31 @@ function mergeConversations(current, incoming) {
|
|
|
87
96
|
for (const conversation of incoming) byId.set(conversation.id, conversation);
|
|
88
97
|
return [...byId.values()];
|
|
89
98
|
}
|
|
99
|
+
function compareInboxOrder(left, right) {
|
|
100
|
+
return right.activityAt.getTime() - left.activityAt.getTime() || (left.conversation.id < right.conversation.id ? 1 : left.conversation.id > right.conversation.id ? -1 : 0);
|
|
101
|
+
}
|
|
102
|
+
function mergeInboxEntries(current, incoming) {
|
|
103
|
+
const byId = new Map(current.map((entry) => [entry.conversation.id, entry]));
|
|
104
|
+
for (const entry of incoming) byId.set(entry.conversation.id, entry);
|
|
105
|
+
return [...byId.values()].sort(compareInboxOrder);
|
|
106
|
+
}
|
|
107
|
+
function inboxPreview(conversation, summary, currentUserId) {
|
|
108
|
+
const message = summary?.latestMessage;
|
|
109
|
+
if (!message) return "";
|
|
110
|
+
const first = message.media[0];
|
|
111
|
+
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" : "");
|
|
112
|
+
if (!body) return "";
|
|
113
|
+
if (currentUserId !== void 0 && message.senderId === currentUserId) return `You: ${body}`;
|
|
114
|
+
if (conversation.participants.length > 2) {
|
|
115
|
+
const sender = conversation.participants.find((participant) => participant.appUserId === message.senderId || participant.id === message.senderId);
|
|
116
|
+
const name = sender?.name.trim();
|
|
117
|
+
if (name) return `${name}: ${body}`;
|
|
118
|
+
}
|
|
119
|
+
return body;
|
|
120
|
+
}
|
|
121
|
+
function formatMessageTime(date) {
|
|
122
|
+
return new Intl.DateTimeFormat(void 0, { hour: "numeric", minute: "2-digit" }).format(date);
|
|
123
|
+
}
|
|
90
124
|
function mergeMessages(current, incoming) {
|
|
91
125
|
const byId = new Map(current.map((message) => [message.id, message]));
|
|
92
126
|
for (const message of incoming) byId.set(message.id, message);
|
|
@@ -94,13 +128,16 @@ function mergeMessages(current, incoming) {
|
|
|
94
128
|
const leftPending = isConvoKitPendingMessage(left);
|
|
95
129
|
const rightPending = isConvoKitPendingMessage(right);
|
|
96
130
|
if (leftPending !== rightPending) return leftPending ? 1 : -1;
|
|
97
|
-
|
|
98
|
-
return byTime === 0 ? left.id.localeCompare(right.id) : byTime;
|
|
131
|
+
return compareMessageOrder(left, right);
|
|
99
132
|
});
|
|
100
133
|
}
|
|
101
|
-
function readerIdsFor(message, readAtByUserId) {
|
|
134
|
+
function readerIdsFor(message, readAtByUserId, readPositionByUserId = /* @__PURE__ */ new Map()) {
|
|
102
135
|
if (isConvoKitPendingMessage(message)) return /* @__PURE__ */ new Set();
|
|
103
|
-
|
|
136
|
+
const userIds = /* @__PURE__ */ new Set([...readAtByUserId.keys(), ...readPositionByUserId.keys()]);
|
|
137
|
+
return new Set([...userIds].filter((userId) => userId !== message.senderId && readThrough({
|
|
138
|
+
readPosition: readPositionByUserId.get(userId) ?? null,
|
|
139
|
+
lastReadAt: readAtByUserId.get(userId) ?? null
|
|
140
|
+
}, message)));
|
|
104
141
|
}
|
|
105
142
|
function partClass(part, appearance, defaultClass) {
|
|
106
143
|
return cx(!appearance.unstyled && defaultClass, appearance.classNames?.[part]);
|
|
@@ -151,6 +188,7 @@ import { ArrowLeft, LoaderCircle as LoaderCircle2, Paperclip, RefreshCw, Send }
|
|
|
151
188
|
import {
|
|
152
189
|
defineComponent as defineComponent3,
|
|
153
190
|
h as h3,
|
|
191
|
+
onBeforeUnmount,
|
|
154
192
|
ref as ref2,
|
|
155
193
|
watchEffect
|
|
156
194
|
} from "vue";
|
|
@@ -169,8 +207,20 @@ function hasContent(message) {
|
|
|
169
207
|
function newest(current, incoming, incomingComplete = true) {
|
|
170
208
|
return version(current) > version(incoming) || !incomingComplete && version(current) === version(incoming) ? current : incoming;
|
|
171
209
|
}
|
|
172
|
-
|
|
173
|
-
|
|
210
|
+
var compare = compareMessageOrder;
|
|
211
|
+
function positionCursor(position) {
|
|
212
|
+
return { createdAt: position.createdAt, id: position.messageId };
|
|
213
|
+
}
|
|
214
|
+
function readEntry(participant) {
|
|
215
|
+
return { userId: participant.appUserId, readAt: participant.lastReadAt, readPosition: participant.readPosition };
|
|
216
|
+
}
|
|
217
|
+
function acknowledgement() {
|
|
218
|
+
return { inFlight: void 0, followUp: false, suppressed: false, target: void 0, acknowledged: void 0, unacknowledgeable: /* @__PURE__ */ new Set() };
|
|
219
|
+
}
|
|
220
|
+
function isTargetMiss(cause) {
|
|
221
|
+
if (typeof cause !== "object" || cause === null) return false;
|
|
222
|
+
const { code, status } = cause;
|
|
223
|
+
return code === "MESSAGE_NOT_FOUND" || code === void 0 && status === 404;
|
|
174
224
|
}
|
|
175
225
|
function blank(currentUserId = "") {
|
|
176
226
|
return {
|
|
@@ -178,6 +228,7 @@ function blank(currentUserId = "") {
|
|
|
178
228
|
messages: [],
|
|
179
229
|
typingUserIds: /* @__PURE__ */ new Set(),
|
|
180
230
|
readAtByUserId: /* @__PURE__ */ new Map(),
|
|
231
|
+
readPositionByUserId: /* @__PURE__ */ new Map(),
|
|
181
232
|
isInitialLoading: false,
|
|
182
233
|
isLoadingOlder: false,
|
|
183
234
|
isReconciling: false,
|
|
@@ -225,6 +276,9 @@ var ConversationStore = class {
|
|
|
225
276
|
hydrationPool = { running: /* @__PURE__ */ new Set(), queued: /* @__PURE__ */ new Map() };
|
|
226
277
|
// Keep tombstones until an explicit reload/session change, including across refreshes.
|
|
227
278
|
deleted = /* @__PURE__ */ new Set();
|
|
279
|
+
ack = acknowledgement();
|
|
280
|
+
// Visible until the platform reports otherwise; unknown/prerender/no document count as visible.
|
|
281
|
+
visible = true;
|
|
228
282
|
sendRevision;
|
|
229
283
|
activeSend;
|
|
230
284
|
refreshQueued = false;
|
|
@@ -287,6 +341,7 @@ var ConversationStore = class {
|
|
|
287
341
|
this.hydrations.clear();
|
|
288
342
|
this.hydrationPool.queued.clear();
|
|
289
343
|
this.deleted.clear();
|
|
344
|
+
this.ack = acknowledgement();
|
|
290
345
|
this.sendRevision = void 0;
|
|
291
346
|
this.activeSend = void 0;
|
|
292
347
|
this.refreshQueued = false;
|
|
@@ -342,8 +397,8 @@ var ConversationStore = class {
|
|
|
342
397
|
if (!this.alive(generation) || conversationId !== this.room || !id.trim()) return;
|
|
343
398
|
this.removeMessage(id);
|
|
344
399
|
}, report));
|
|
345
|
-
add(() => this.client.onReadReceipt(this.room, ({ userId, readAt }) => {
|
|
346
|
-
if (this.alive(generation)) this.mergeReads([
|
|
400
|
+
add(() => this.client.onReadReceipt(this.room, ({ userId, readAt, readPosition }) => {
|
|
401
|
+
if (this.alive(generation)) this.mergeReads([{ userId, readAt, readPosition }]);
|
|
347
402
|
}, report));
|
|
348
403
|
add(() => this.client.onTyping(this.room, ({ userId, isTyping }) => {
|
|
349
404
|
if (!this.alive(generation) || !userId.trim() || userId === this.user) return;
|
|
@@ -386,9 +441,6 @@ var ConversationStore = class {
|
|
|
386
441
|
this.hydrations.set(message.id, job);
|
|
387
442
|
this.hydrationPool.queued.set(message.id, job);
|
|
388
443
|
this.drainHydration();
|
|
389
|
-
if (type === "insert" && !existing && message.senderId !== this.user && (this.options.markReadOnReceive ?? true)) {
|
|
390
|
-
void this.markRead();
|
|
391
|
-
}
|
|
392
444
|
}
|
|
393
445
|
record(message, insert, revision, complete) {
|
|
394
446
|
const existing = this.state.messages.find((item) => item.id === message.id);
|
|
@@ -398,7 +450,9 @@ var ConversationStore = class {
|
|
|
398
450
|
this.confirmSend(message);
|
|
399
451
|
}
|
|
400
452
|
this.changes.set(message.id, { revision, message, insert, complete });
|
|
401
|
-
if (existing
|
|
453
|
+
if (!existing && !(insert && hasContent(message))) return;
|
|
454
|
+
this.patch({ messages: mergeMessages(this.state.messages, [message]) });
|
|
455
|
+
if (!existing && message.senderId !== this.user && (this.options.markReadOnReceive ?? true)) void this.acknowledge(true);
|
|
402
456
|
}
|
|
403
457
|
confirmSend(message) {
|
|
404
458
|
const send = this.activeSend;
|
|
@@ -410,11 +464,19 @@ var ConversationStore = class {
|
|
|
410
464
|
return this.alive(job.generation) && !this.deleted.has(job.message.id) && this.hydrations.get(job.message.id) === job;
|
|
411
465
|
}
|
|
412
466
|
removeMessage(id) {
|
|
467
|
+
this.forget(id);
|
|
468
|
+
this.patch({ messages: this.state.messages.filter((message) => message.id !== id) });
|
|
469
|
+
}
|
|
470
|
+
/** Tombstone a row learned to be gone; a removed acknowledgement target is re-resolved from what remains. */
|
|
471
|
+
forget(id) {
|
|
413
472
|
this.deleted.add(id);
|
|
414
473
|
this.changes.delete(id);
|
|
415
474
|
this.hydrations.delete(id);
|
|
416
475
|
this.hydrationPool.queued.delete(id);
|
|
417
|
-
|
|
476
|
+
const ack = this.ack;
|
|
477
|
+
if (ack.target !== id && ack.acknowledged?.id !== id) return;
|
|
478
|
+
ack.unacknowledgeable.add(id);
|
|
479
|
+
if (ack.target === id) ack.followUp = true;
|
|
418
480
|
}
|
|
419
481
|
drainHydration() {
|
|
420
482
|
const pool = this.hydrationPool;
|
|
@@ -451,13 +513,23 @@ var ConversationStore = class {
|
|
|
451
513
|
});
|
|
452
514
|
}
|
|
453
515
|
}
|
|
516
|
+
/** Both maps only ever advance: acknowledgement times by time, positions by (createdAt, id). Participants and
|
|
517
|
+
* read events are the only sources; the local user's own read is never written from the device clock.
|
|
518
|
+
*/
|
|
454
519
|
mergeReads(entries) {
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
520
|
+
const readAt = new Map(this.state.readAtByUserId);
|
|
521
|
+
const positions = new Map(this.state.readPositionByUserId);
|
|
522
|
+
for (const entry of entries) {
|
|
523
|
+
const { userId, readPosition } = entry;
|
|
524
|
+
if (!userId.trim()) continue;
|
|
525
|
+
if (entry.readAt && Number.isFinite(entry.readAt.getTime()) && entry.readAt.getTime() > (readAt.get(userId)?.getTime() ?? -Infinity)) {
|
|
526
|
+
readAt.set(userId, entry.readAt);
|
|
527
|
+
}
|
|
528
|
+
if (!readPosition || typeof readPosition.messageId !== "string" || !readPosition.messageId.trim() || !(readPosition.createdAt instanceof Date) || !Number.isFinite(readPosition.createdAt.getTime())) continue;
|
|
529
|
+
const current = positions.get(userId);
|
|
530
|
+
if (!current || compare(positionCursor(readPosition), positionCursor(current)) > 0) positions.set(userId, readPosition);
|
|
459
531
|
}
|
|
460
|
-
this.patch({ readAtByUserId:
|
|
532
|
+
this.patch({ readAtByUserId: readAt, readPositionByUserId: positions });
|
|
461
533
|
}
|
|
462
534
|
validatePage(page, before) {
|
|
463
535
|
if (page.length > this.pageSize) throw new Error("Message page exceeds the requested limit");
|
|
@@ -514,9 +586,9 @@ var ConversationStore = class {
|
|
|
514
586
|
this.validatePage(page);
|
|
515
587
|
this.cursor = page.at(-1);
|
|
516
588
|
this.patch({ conversation, messages: this.overlay(page, revision), hasOlderMessages: page.length === this.pageSize });
|
|
517
|
-
this.mergeReads(conversation.participants.
|
|
589
|
+
this.mergeReads(conversation.participants.map(readEntry));
|
|
518
590
|
this.prune(revision);
|
|
519
|
-
if (this.options.markReadOnLoad ?? true) await this.
|
|
591
|
+
if (this.options.markReadOnLoad ?? true) await this.acknowledge(true);
|
|
520
592
|
} catch (cause) {
|
|
521
593
|
this.fail(cause, generation, true);
|
|
522
594
|
} finally {
|
|
@@ -572,14 +644,9 @@ var ConversationStore = class {
|
|
|
572
644
|
const reconciled = this.overlay(rows, revision);
|
|
573
645
|
const survivingIds = new Set(reconciled.map((message) => message.id));
|
|
574
646
|
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));
|
|
575
|
-
for (const id of known) if (!survivingIds.has(id))
|
|
576
|
-
this.deleted.add(id);
|
|
577
|
-
this.changes.delete(id);
|
|
578
|
-
this.hydrations.delete(id);
|
|
579
|
-
this.hydrationPool.queued.delete(id);
|
|
580
|
-
}
|
|
647
|
+
for (const id of known) if (!survivingIds.has(id)) this.forget(id);
|
|
581
648
|
this.patch({ conversation, messages: reconciled, hasOlderMessages: hasOlder });
|
|
582
|
-
this.mergeReads(conversation.participants.
|
|
649
|
+
this.mergeReads(conversation.participants.map(readEntry));
|
|
583
650
|
this.prune(revision);
|
|
584
651
|
} catch (cause) {
|
|
585
652
|
this.fail(cause, generation, true);
|
|
@@ -614,15 +681,68 @@ var ConversationStore = class {
|
|
|
614
681
|
}
|
|
615
682
|
}
|
|
616
683
|
};
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
684
|
+
/** Acknowledge through the newest rendered row now, regardless of visibility; no request without a target. */
|
|
685
|
+
markRead = () => this.alive() ? this.acknowledge(false) : Promise.resolve();
|
|
686
|
+
/** Automatic acknowledgements wait while hidden and are re-issued (once) on becoming visible. */
|
|
687
|
+
setVisible = (visible) => {
|
|
688
|
+
this.visible = visible;
|
|
689
|
+
if (!visible || !this.ack.suppressed || !this.alive()) return;
|
|
690
|
+
this.ack.suppressed = false;
|
|
691
|
+
void this.acknowledge(true);
|
|
692
|
+
};
|
|
693
|
+
/** The newest non-pending rendered row by (createdAt, id), never by list index and never a raw realtime
|
|
694
|
+
* row; rows the server does not know are skipped, and nothing at or before the accepted target is re-sent.
|
|
695
|
+
*/
|
|
696
|
+
ackTarget(ack) {
|
|
697
|
+
let target;
|
|
698
|
+
for (const message of this.state.messages) {
|
|
699
|
+
if (isConvoKitPendingMessage(message) || ack.unacknowledgeable.has(message.id)) continue;
|
|
700
|
+
if (!target || compare(message, target) > 0) target = { createdAt: message.createdAt, id: message.id };
|
|
701
|
+
}
|
|
702
|
+
return target && (!ack.acknowledged || compare(target, ack.acknowledged) > 0) ? target : void 0;
|
|
703
|
+
}
|
|
704
|
+
/** Resolves when the request this call issued or joined settles; a follow-up is issued, not awaited. */
|
|
705
|
+
acknowledge(automatic) {
|
|
706
|
+
const ack = this.ack;
|
|
707
|
+
if (automatic && !this.visible) {
|
|
708
|
+
ack.suppressed = true;
|
|
709
|
+
return Promise.resolve();
|
|
710
|
+
}
|
|
711
|
+
if (ack.inFlight) {
|
|
712
|
+
ack.followUp = true;
|
|
713
|
+
return ack.inFlight;
|
|
714
|
+
}
|
|
715
|
+
return this.issue(ack, this.generation) ?? Promise.resolve();
|
|
716
|
+
}
|
|
717
|
+
issue(ack, generation) {
|
|
718
|
+
ack.followUp = false;
|
|
719
|
+
const target = this.ackTarget(ack);
|
|
720
|
+
if (!target) return void 0;
|
|
721
|
+
ack.target = target.id;
|
|
722
|
+
ack.inFlight = this.send(ack, generation, target).finally(() => {
|
|
723
|
+
ack.inFlight = void 0;
|
|
724
|
+
ack.target = void 0;
|
|
725
|
+
if (!this.alive(generation) || !ack.followUp) return;
|
|
726
|
+
if (!this.visible) {
|
|
727
|
+
ack.followUp = false;
|
|
728
|
+
ack.suppressed = true;
|
|
729
|
+
} else this.issue(ack, generation);
|
|
730
|
+
});
|
|
731
|
+
return ack.inFlight;
|
|
732
|
+
}
|
|
733
|
+
async send(ack, generation, target) {
|
|
620
734
|
try {
|
|
621
|
-
await this.client.markConversationRead(this.room);
|
|
735
|
+
await this.client.markConversationRead(this.room, { throughMessageId: target.id });
|
|
736
|
+
if (!this.alive(generation)) return;
|
|
737
|
+
if (!ack.acknowledged || compare(target, ack.acknowledged) > 0) ack.acknowledged = target;
|
|
622
738
|
} catch (cause) {
|
|
623
|
-
this.
|
|
739
|
+
if (!this.alive(generation)) return;
|
|
740
|
+
if (isTargetMiss(cause)) {
|
|
741
|
+
ack.unacknowledgeable.add(target.id);
|
|
742
|
+
ack.followUp = true;
|
|
743
|
+
} else this.fail(cause, generation);
|
|
624
744
|
}
|
|
625
|
-
}
|
|
745
|
+
}
|
|
626
746
|
updateTyping = async (isTyping) => {
|
|
627
747
|
if (!this.alive()) return;
|
|
628
748
|
const generation = this.generation;
|
|
@@ -705,7 +825,7 @@ var ConversationStore = class {
|
|
|
705
825
|
}
|
|
706
826
|
}
|
|
707
827
|
};
|
|
708
|
-
readerIdsFor = (message) => readerIdsFor(message, this.state.readAtByUserId);
|
|
828
|
+
readerIdsFor = (message) => readerIdsFor(message, this.state.readAtByUserId, this.state.readPositionByUserId);
|
|
709
829
|
};
|
|
710
830
|
|
|
711
831
|
// src/composables/use-conversation.ts
|
|
@@ -718,6 +838,7 @@ function useConversation(options) {
|
|
|
718
838
|
let store = createStore();
|
|
719
839
|
const snapshot = shallowRef(store.getSnapshot());
|
|
720
840
|
let unsubscribe;
|
|
841
|
+
let visible = true;
|
|
721
842
|
const stop = watch(
|
|
722
843
|
() => [toValue(options.client), toValue(options.client).sessionIdentity, toValue(options.conversationId)],
|
|
723
844
|
() => {
|
|
@@ -728,6 +849,7 @@ function useConversation(options) {
|
|
|
728
849
|
unsubscribe = store.subscribe(() => {
|
|
729
850
|
snapshot.value = store.getSnapshot();
|
|
730
851
|
});
|
|
852
|
+
store.setVisible(visible);
|
|
731
853
|
store.start(options.autoLoad ?? true);
|
|
732
854
|
},
|
|
733
855
|
{ immediate: true, flush: "sync" }
|
|
@@ -747,6 +869,7 @@ function useConversation(options) {
|
|
|
747
869
|
messages: field("messages"),
|
|
748
870
|
typingUserIds: field("typingUserIds"),
|
|
749
871
|
readAtByUserId: field("readAtByUserId"),
|
|
872
|
+
readPositionByUserId: field("readPositionByUserId"),
|
|
750
873
|
isInitialLoading: field("isInitialLoading"),
|
|
751
874
|
isLoadingOlder: field("isLoadingOlder"),
|
|
752
875
|
isReconciling: field("isReconciling"),
|
|
@@ -762,6 +885,10 @@ function useConversation(options) {
|
|
|
762
885
|
sendMessage: (input) => store.sendMessage(input),
|
|
763
886
|
markRead: () => store.markRead(),
|
|
764
887
|
updateTyping: (isTyping) => store.updateTyping(isTyping),
|
|
888
|
+
setVisible: (value) => {
|
|
889
|
+
visible = value;
|
|
890
|
+
store.setVisible(value);
|
|
891
|
+
},
|
|
765
892
|
dispose
|
|
766
893
|
};
|
|
767
894
|
}
|
|
@@ -792,9 +919,6 @@ var appearanceProps = {
|
|
|
792
919
|
density: { type: String, default: "comfortable" },
|
|
793
920
|
unstyled: { type: Boolean, default: false }
|
|
794
921
|
};
|
|
795
|
-
function defaultFormatTime(date) {
|
|
796
|
-
return new Intl.DateTimeFormat(void 0, { hour: "numeric", minute: "2-digit" }).format(date);
|
|
797
|
-
}
|
|
798
922
|
function defaultMedia(media, open, imageLoading) {
|
|
799
923
|
const tag = open ? "button" : "div";
|
|
800
924
|
const interactive = open ? { type: "button", onClick: open } : {};
|
|
@@ -834,6 +958,7 @@ var MessageListView = defineComponent2({
|
|
|
834
958
|
messages: { type: Array, required: true },
|
|
835
959
|
currentUserId: { type: String, required: true },
|
|
836
960
|
readAtByUserId: { type: Object, default: () => /* @__PURE__ */ new Map() },
|
|
961
|
+
readPositionByUserId: { type: Object, default: () => /* @__PURE__ */ new Map() },
|
|
837
962
|
readersResolver: { type: Function, default: void 0 },
|
|
838
963
|
onLoadOlder: { type: Function, default: void 0 },
|
|
839
964
|
hasOlderMessages: { type: Boolean, default: false },
|
|
@@ -844,7 +969,7 @@ var MessageListView = defineComponent2({
|
|
|
844
969
|
paginationThreshold: { type: Number, default: 240 },
|
|
845
970
|
reverse: { type: Boolean, default: true },
|
|
846
971
|
stickToBottom: { type: Boolean, default: true },
|
|
847
|
-
formatTime: { type: Function, default:
|
|
972
|
+
formatTime: { type: Function, default: formatMessageTime },
|
|
848
973
|
imageLoading: { type: String, default: "lazy" }
|
|
849
974
|
},
|
|
850
975
|
emits: ["load-older", "attachment-click"],
|
|
@@ -893,7 +1018,7 @@ var MessageListView = defineComponent2({
|
|
|
893
1018
|
const isCurrentUser = message.senderId === props.currentUserId;
|
|
894
1019
|
const sender = participants.value.get(message.senderId);
|
|
895
1020
|
const isPending = isConvoKitPendingMessage(message);
|
|
896
|
-
const readerIds = isPending ? /* @__PURE__ */ new Set() : props.readersResolver ? props.readersResolver(message) : readerIdsFor(message, props.readAtByUserId);
|
|
1021
|
+
const readerIds = isPending ? /* @__PURE__ */ new Set() : props.readersResolver ? props.readersResolver(message) : readerIdsFor(message, props.readAtByUserId, props.readPositionByUserId);
|
|
897
1022
|
const slotProps = { message, chronologicalIndex: index, isCurrentUser, sender, readerIds };
|
|
898
1023
|
const custom = slots.message?.(slotProps);
|
|
899
1024
|
if (custom) return h2("div", { key: message.id, role: "listitem" }, custom);
|
|
@@ -992,8 +1117,8 @@ var MessageListView = defineComponent2({
|
|
|
992
1117
|
};
|
|
993
1118
|
}
|
|
994
1119
|
});
|
|
995
|
-
function defaultReadersResolver(readAtByUserId) {
|
|
996
|
-
return (message) => readerIdsFor(message, readAtByUserId);
|
|
1120
|
+
function defaultReadersResolver(readAtByUserId, readPositionByUserId = /* @__PURE__ */ new Map()) {
|
|
1121
|
+
return (message) => readerIdsFor(message, readAtByUserId, readPositionByUserId);
|
|
997
1122
|
}
|
|
998
1123
|
|
|
999
1124
|
// src/components/conversation.ts
|
|
@@ -1011,6 +1136,7 @@ var viewProps = {
|
|
|
1011
1136
|
onSendMessage: { type: Function, required: true },
|
|
1012
1137
|
typingUserIds: { type: Object, default: () => /* @__PURE__ */ new Set() },
|
|
1013
1138
|
readAtByUserId: { type: Object, default: () => /* @__PURE__ */ new Map() },
|
|
1139
|
+
readPositionByUserId: { type: Object, default: () => /* @__PURE__ */ new Map() },
|
|
1014
1140
|
readersResolver: { type: Function, default: void 0 },
|
|
1015
1141
|
onBack: { type: Function, default: void 0 },
|
|
1016
1142
|
onRefresh: { type: Function, default: void 0 },
|
|
@@ -1239,6 +1365,7 @@ var ConversationView = defineComponent3({
|
|
|
1239
1365
|
messages: props.messages,
|
|
1240
1366
|
currentUserId: props.currentUserId,
|
|
1241
1367
|
readAtByUserId: props.readAtByUserId,
|
|
1368
|
+
readPositionByUserId: props.readPositionByUserId,
|
|
1242
1369
|
...props.readersResolver ? { readersResolver: props.readersResolver } : {},
|
|
1243
1370
|
...props.onLoadOlder ? { onLoadOlder: loadOlder } : {},
|
|
1244
1371
|
hasOlderMessages: props.hasOlderMessages,
|
|
@@ -1301,6 +1428,12 @@ var Conversation = defineComponent3({
|
|
|
1301
1428
|
watchEffect(() => {
|
|
1302
1429
|
emit("controller-change", controller);
|
|
1303
1430
|
});
|
|
1431
|
+
if (typeof document !== "undefined") {
|
|
1432
|
+
const syncVisibility = () => controller.setVisible(document.visibilityState !== "hidden");
|
|
1433
|
+
syncVisibility();
|
|
1434
|
+
document.addEventListener("visibilitychange", syncVisibility);
|
|
1435
|
+
onBeforeUnmount(() => document.removeEventListener("visibilitychange", syncVisibility));
|
|
1436
|
+
}
|
|
1304
1437
|
return () => {
|
|
1305
1438
|
const loadedConversation = controller.conversation.value;
|
|
1306
1439
|
if (!loadedConversation) {
|
|
@@ -1335,6 +1468,7 @@ var Conversation = defineComponent3({
|
|
|
1335
1468
|
onSendMessage: _onSendMessage,
|
|
1336
1469
|
typingUserIds: _typingUserIds,
|
|
1337
1470
|
readAtByUserId: _readAtByUserId,
|
|
1471
|
+
readPositionByUserId: _readPositionByUserId,
|
|
1338
1472
|
onRefresh: _onRefresh,
|
|
1339
1473
|
onLoadOlder: _onLoadOlder,
|
|
1340
1474
|
onTypingChange: _onTypingChange,
|
|
@@ -1357,6 +1491,7 @@ var Conversation = defineComponent3({
|
|
|
1357
1491
|
},
|
|
1358
1492
|
typingUserIds: controller.typingUserIds.value,
|
|
1359
1493
|
readAtByUserId: controller.readAtByUserId.value,
|
|
1494
|
+
readPositionByUserId: controller.readPositionByUserId.value,
|
|
1360
1495
|
onRefresh: controller.refresh,
|
|
1361
1496
|
onLoadOlder: controller.loadOlderMessages,
|
|
1362
1497
|
onTypingChange: controller.updateTyping,
|
|
@@ -1396,30 +1531,64 @@ import {
|
|
|
1396
1531
|
import { computed as computed3, getCurrentScope as getCurrentScope2, onScopeDispose as onScopeDispose2, shallowRef as shallowRef2, toValue as toValue2, watch as watch3 } from "vue";
|
|
1397
1532
|
|
|
1398
1533
|
// src/conversation-list-store.ts
|
|
1534
|
+
var defaultActivityRefreshWindowMs = 500;
|
|
1399
1535
|
function blank2(filter) {
|
|
1400
|
-
return {
|
|
1536
|
+
return {
|
|
1537
|
+
conversations: [],
|
|
1538
|
+
summaries: /* @__PURE__ */ new Map(),
|
|
1539
|
+
currentUserId: "",
|
|
1540
|
+
filter,
|
|
1541
|
+
isInitialLoading: false,
|
|
1542
|
+
isLoadingMore: false,
|
|
1543
|
+
hasMore: true,
|
|
1544
|
+
hasLoaded: false,
|
|
1545
|
+
error: null
|
|
1546
|
+
};
|
|
1547
|
+
}
|
|
1548
|
+
function statusOf(cause) {
|
|
1549
|
+
return typeof cause === "object" && cause !== null && "status" in cause ? cause.status : void 0;
|
|
1550
|
+
}
|
|
1551
|
+
function summaryOf(entry) {
|
|
1552
|
+
const { conversation: _conversation, ...summary } = entry;
|
|
1553
|
+
return summary;
|
|
1554
|
+
}
|
|
1555
|
+
function ids(entries) {
|
|
1556
|
+
return entries.map((entry) => entry.conversation);
|
|
1401
1557
|
}
|
|
1402
1558
|
var ConversationListStore = class {
|
|
1403
1559
|
constructor(options) {
|
|
1404
1560
|
this.options = options;
|
|
1405
1561
|
this.owner = options.client.sessionIdentity;
|
|
1562
|
+
this.user = this.owner ? options.client.currentUserId : "";
|
|
1406
1563
|
this.pageSize = options.pageSize ?? 30;
|
|
1407
1564
|
if (!Number.isInteger(this.pageSize) || this.pageSize < 1 || this.pageSize > 100) {
|
|
1408
1565
|
throw new RangeError("pageSize must be an integer between 1 and 100");
|
|
1409
1566
|
}
|
|
1567
|
+
this.activityRefreshWindowMs = options.activityRefreshWindowMs ?? defaultActivityRefreshWindowMs;
|
|
1568
|
+
if (!Number.isFinite(this.activityRefreshWindowMs) || this.activityRefreshWindowMs < 0) {
|
|
1569
|
+
throw new RangeError("activityRefreshWindowMs must be a non-negative number");
|
|
1570
|
+
}
|
|
1410
1571
|
this.state = blank2(options.initialFilter ?? {});
|
|
1411
1572
|
}
|
|
1412
1573
|
options;
|
|
1413
1574
|
owner;
|
|
1575
|
+
user;
|
|
1414
1576
|
pageSize;
|
|
1577
|
+
activityRefreshWindowMs;
|
|
1415
1578
|
state;
|
|
1416
1579
|
source = [];
|
|
1580
|
+
entries = [];
|
|
1417
1581
|
offset = 0;
|
|
1582
|
+
cursor = null;
|
|
1583
|
+
inboxUnavailable = false;
|
|
1584
|
+
inboxWarned = false;
|
|
1418
1585
|
generation = 0;
|
|
1419
1586
|
lifecycleGeneration = 0;
|
|
1420
1587
|
disposed = true;
|
|
1421
1588
|
lifecycle;
|
|
1422
1589
|
inbox;
|
|
1590
|
+
activity;
|
|
1591
|
+
activityTimer;
|
|
1423
1592
|
refreshQueued = false;
|
|
1424
1593
|
refreshing = false;
|
|
1425
1594
|
listeners = /* @__PURE__ */ new Set();
|
|
@@ -1437,12 +1606,27 @@ var ConversationListStore = class {
|
|
|
1437
1606
|
alive(generation = this.generation) {
|
|
1438
1607
|
return !this.disposed && generation === this.generation && this.owner !== null && this.options.client.sessionIdentity === this.owner;
|
|
1439
1608
|
}
|
|
1609
|
+
get inboxMode() {
|
|
1610
|
+
return !this.options.pageLoader && typeof this.options.client.listInbox === "function" && !this.inboxUnavailable;
|
|
1611
|
+
}
|
|
1612
|
+
currentUserId() {
|
|
1613
|
+
return this.inboxMode ? this.user : "";
|
|
1614
|
+
}
|
|
1440
1615
|
start = (autoLoad = true) => {
|
|
1441
1616
|
if (!this.owner || this.options.client.sessionIdentity !== this.owner) return;
|
|
1442
1617
|
if (!this.disposed) return;
|
|
1443
1618
|
this.disposed = false;
|
|
1619
|
+
this.inboxUnavailable = false;
|
|
1444
1620
|
const lifecycleGeneration = ++this.lifecycleGeneration;
|
|
1621
|
+
const current = () => this.alive() && lifecycleGeneration === this.lifecycleGeneration;
|
|
1622
|
+
const reconcile = (cause) => {
|
|
1623
|
+
if (current()) {
|
|
1624
|
+
this.patch({ error: cause });
|
|
1625
|
+
this.queueRefresh();
|
|
1626
|
+
}
|
|
1627
|
+
};
|
|
1445
1628
|
try {
|
|
1629
|
+
this.patch({ currentUserId: this.currentUserId() });
|
|
1446
1630
|
const subscription = this.options.client.onConnectionEvent({
|
|
1447
1631
|
onEvent: () => {
|
|
1448
1632
|
},
|
|
@@ -1454,15 +1638,20 @@ var ConversationListStore = class {
|
|
|
1454
1638
|
else void subscription.unsubscribe().catch(() => void 0);
|
|
1455
1639
|
if (!this.alive()) return;
|
|
1456
1640
|
const inbox = this.options.client.onInboxChanged(() => {
|
|
1457
|
-
if (
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
this.queueRefresh();
|
|
1462
|
-
}
|
|
1463
|
-
});
|
|
1641
|
+
if (!current()) return;
|
|
1642
|
+
this.clearActivityTimer();
|
|
1643
|
+
this.queueRefresh();
|
|
1644
|
+
}, reconcile);
|
|
1464
1645
|
if (this.alive()) this.inbox = inbox;
|
|
1465
1646
|
else void inbox.unsubscribe().catch(() => void 0);
|
|
1647
|
+
if (!this.alive()) return;
|
|
1648
|
+
if (this.inboxMode && typeof this.options.client.onInboxActivity === "function") {
|
|
1649
|
+
const activity = this.options.client.onInboxActivity(() => {
|
|
1650
|
+
if (current()) this.scheduleActivityRefresh(lifecycleGeneration);
|
|
1651
|
+
}, reconcile);
|
|
1652
|
+
if (this.alive()) this.activity = activity;
|
|
1653
|
+
else void activity.unsubscribe().catch(() => void 0);
|
|
1654
|
+
}
|
|
1466
1655
|
if (autoLoad) void this.loadInitial();
|
|
1467
1656
|
} catch (cause) {
|
|
1468
1657
|
if (this.alive()) this.patch({ error: cause });
|
|
@@ -1472,27 +1661,109 @@ var ConversationListStore = class {
|
|
|
1472
1661
|
this.disposed = true;
|
|
1473
1662
|
this.generation++;
|
|
1474
1663
|
this.lifecycleGeneration++;
|
|
1664
|
+
this.clearActivityTimer();
|
|
1475
1665
|
const subscription = this.lifecycle;
|
|
1476
1666
|
this.lifecycle = void 0;
|
|
1477
1667
|
if (subscription) void subscription.unsubscribe().catch(() => void 0);
|
|
1478
1668
|
if (this.inbox) void this.inbox.unsubscribe().catch(() => void 0);
|
|
1479
1669
|
this.inbox = void 0;
|
|
1670
|
+
this.stopActivity();
|
|
1480
1671
|
this.refreshQueued = false;
|
|
1481
1672
|
this.refreshing = false;
|
|
1482
1673
|
this.source = [];
|
|
1674
|
+
this.entries = [];
|
|
1483
1675
|
this.offset = 0;
|
|
1676
|
+
this.cursor = null;
|
|
1484
1677
|
this.patch(blank2(this.state.filter));
|
|
1485
1678
|
};
|
|
1679
|
+
stopActivity() {
|
|
1680
|
+
if (this.activity) void this.activity.unsubscribe().catch(() => void 0);
|
|
1681
|
+
this.activity = void 0;
|
|
1682
|
+
}
|
|
1683
|
+
clearActivityTimer() {
|
|
1684
|
+
if (this.activityTimer !== void 0) clearTimeout(this.activityTimer);
|
|
1685
|
+
this.activityTimer = void 0;
|
|
1686
|
+
}
|
|
1687
|
+
/** Max-wait throttle: the first signal opens a window; later signals wait for it; one refresh runs when it closes. */
|
|
1688
|
+
scheduleActivityRefresh(lifecycleGeneration) {
|
|
1689
|
+
if (this.activityRefreshWindowMs === 0) return this.queueRefresh();
|
|
1690
|
+
if (this.activityTimer !== void 0) return;
|
|
1691
|
+
const timer = setTimeout(() => {
|
|
1692
|
+
this.activityTimer = void 0;
|
|
1693
|
+
if (this.alive() && lifecycleGeneration === this.lifecycleGeneration) this.queueRefresh();
|
|
1694
|
+
}, this.activityRefreshWindowMs);
|
|
1695
|
+
timer.unref?.();
|
|
1696
|
+
this.activityTimer = timer;
|
|
1697
|
+
}
|
|
1486
1698
|
fail(cause, generation) {
|
|
1487
1699
|
if (!this.alive(generation)) return;
|
|
1488
|
-
const status =
|
|
1700
|
+
const status = statusOf(cause);
|
|
1489
1701
|
if (status === 401 || status === 403 || status === 404) {
|
|
1490
1702
|
this.source = [];
|
|
1703
|
+
this.entries = [];
|
|
1491
1704
|
this.offset = 0;
|
|
1492
|
-
this.
|
|
1705
|
+
this.cursor = null;
|
|
1706
|
+
this.patch({ conversations: [], summaries: /* @__PURE__ */ new Map(), hasMore: false });
|
|
1493
1707
|
}
|
|
1494
1708
|
this.patch({ error: cause });
|
|
1495
1709
|
}
|
|
1710
|
+
/** Run an operation in inbox mode, falling back to the legacy path for the rest of this store's life when the
|
|
1711
|
+
* inbox route is absent (404: rollback, staging). Loaded rows are kept and the same operation continues.
|
|
1712
|
+
*/
|
|
1713
|
+
async withFallback(generation, inbox, legacy) {
|
|
1714
|
+
if (!this.inboxMode) return legacy();
|
|
1715
|
+
try {
|
|
1716
|
+
await inbox();
|
|
1717
|
+
} catch (cause) {
|
|
1718
|
+
if (!this.alive(generation) || statusOf(cause) !== 404) throw cause;
|
|
1719
|
+
this.inboxUnavailable = true;
|
|
1720
|
+
this.clearActivityTimer();
|
|
1721
|
+
this.stopActivity();
|
|
1722
|
+
this.entries = [];
|
|
1723
|
+
this.cursor = null;
|
|
1724
|
+
this.offset = this.source.length;
|
|
1725
|
+
if (!this.inboxWarned) {
|
|
1726
|
+
this.inboxWarned = true;
|
|
1727
|
+
console.warn("ConvoKit inbox endpoint unavailable (404); using getConversations without previews or unread counts.");
|
|
1728
|
+
}
|
|
1729
|
+
this.patch({ summaries: /* @__PURE__ */ new Map(), currentUserId: "" });
|
|
1730
|
+
await legacy();
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
validateInboxPage(page, limit, requestedCursor) {
|
|
1734
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1735
|
+
for (const entry of page.entries) {
|
|
1736
|
+
const id = entry.conversation.id;
|
|
1737
|
+
if (!id.trim() || seen.has(id)) throw new Error("Invalid conversation page");
|
|
1738
|
+
seen.add(id);
|
|
1739
|
+
}
|
|
1740
|
+
if (page.entries.length > limit) throw new Error("Invalid conversation page");
|
|
1741
|
+
if (page.nextCursor !== null && (page.nextCursor === requestedCursor || page.entries.length === 0)) {
|
|
1742
|
+
throw new Error("Inbox pagination did not advance");
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
/** Swap rows, summaries, cursor and hasMore together, filtered by the filter current at commit time. */
|
|
1746
|
+
commitInbox(entries, cursor) {
|
|
1747
|
+
this.entries = entries;
|
|
1748
|
+
this.source = ids(entries);
|
|
1749
|
+
this.cursor = cursor;
|
|
1750
|
+
this.patch({
|
|
1751
|
+
conversations: applyConversationFilter(this.source, this.state.filter),
|
|
1752
|
+
summaries: new Map(entries.map((entry) => [entry.conversation.id, summaryOf(entry)])),
|
|
1753
|
+
hasMore: cursor !== null
|
|
1754
|
+
});
|
|
1755
|
+
}
|
|
1756
|
+
async loadInboxUntilVisible(generation, filter) {
|
|
1757
|
+
const visibleBefore = applyConversationFilter(this.source, filter).length;
|
|
1758
|
+
while (this.alive(generation)) {
|
|
1759
|
+
const requested = this.cursor;
|
|
1760
|
+
const page = await this.options.client.listInbox({ limit: this.pageSize, cursor: requested, archived: filter.archived ?? false });
|
|
1761
|
+
if (!this.alive(generation)) return;
|
|
1762
|
+
this.validateInboxPage(page, this.pageSize, requested);
|
|
1763
|
+
this.commitInbox(mergeInboxEntries(this.entries, page.entries), page.nextCursor);
|
|
1764
|
+
if (page.nextCursor === null || this.state.conversations.length > visibleBefore) return;
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1496
1767
|
async loadUntilVisible(generation, filter) {
|
|
1497
1768
|
const visibleBefore = applyConversationFilter(this.source, filter).length;
|
|
1498
1769
|
while (this.alive(generation)) {
|
|
@@ -1519,10 +1790,17 @@ var ConversationListStore = class {
|
|
|
1519
1790
|
const generation = ++this.generation;
|
|
1520
1791
|
this.refreshing = false;
|
|
1521
1792
|
this.source = [];
|
|
1793
|
+
this.entries = [];
|
|
1522
1794
|
this.offset = 0;
|
|
1523
|
-
this.
|
|
1795
|
+
this.cursor = null;
|
|
1796
|
+
this.patch({ ...blank2(this.state.filter), currentUserId: this.currentUserId(), isInitialLoading: true });
|
|
1797
|
+
const filter = this.state.filter;
|
|
1524
1798
|
try {
|
|
1525
|
-
await this.
|
|
1799
|
+
await this.withFallback(
|
|
1800
|
+
generation,
|
|
1801
|
+
() => this.loadInboxUntilVisible(generation, filter),
|
|
1802
|
+
() => this.loadUntilVisible(generation, filter)
|
|
1803
|
+
);
|
|
1526
1804
|
} catch (cause) {
|
|
1527
1805
|
this.fail(cause, generation);
|
|
1528
1806
|
} finally {
|
|
@@ -1544,6 +1822,47 @@ var ConversationListStore = class {
|
|
|
1544
1822
|
void this.refresh();
|
|
1545
1823
|
});
|
|
1546
1824
|
}
|
|
1825
|
+
/** Re-walk the inbox from the head until the loaded window is covered and something is visible, or the inbox
|
|
1826
|
+
* ends. Rooms that moved are re-positioned by the merge; an exhausted inbox publishes what it found.
|
|
1827
|
+
*/
|
|
1828
|
+
async refreshInbox(generation, filter) {
|
|
1829
|
+
const target = Math.max(this.pageSize, this.entries.length);
|
|
1830
|
+
let rows = [], consumed = 0, cursor = null;
|
|
1831
|
+
while (this.alive(generation)) {
|
|
1832
|
+
const remaining = target - consumed;
|
|
1833
|
+
const limit = remaining >= 1 ? Math.min(100, remaining) : this.pageSize;
|
|
1834
|
+
const page = await this.options.client.listInbox({ limit, cursor, archived: filter.archived ?? false });
|
|
1835
|
+
if (!this.alive(generation)) return;
|
|
1836
|
+
this.validateInboxPage(page, limit, cursor);
|
|
1837
|
+
rows = mergeInboxEntries(rows, page.entries);
|
|
1838
|
+
consumed += page.entries.length;
|
|
1839
|
+
cursor = page.nextCursor;
|
|
1840
|
+
if (cursor === null || consumed >= target && applyConversationFilter(ids(rows), this.state.filter).length > 0) break;
|
|
1841
|
+
}
|
|
1842
|
+
if (!this.alive(generation)) return;
|
|
1843
|
+
this.commitInbox(rows, cursor);
|
|
1844
|
+
}
|
|
1845
|
+
async refreshLegacy(generation, filter) {
|
|
1846
|
+
const target = Math.max(this.pageSize, this.offset);
|
|
1847
|
+
const compare2 = (a, b) => a.createdAt.getTime() - b.createdAt.getTime() || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
1848
|
+
const boundary = this.options.pageLoader ? void 0 : this.source.reduce((oldest, row) => !oldest || compare2(row, oldest) < 0 ? row : oldest, void 0);
|
|
1849
|
+
let rows = [], offset = 0, hasMore = true;
|
|
1850
|
+
while (this.alive(generation)) {
|
|
1851
|
+
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 });
|
|
1852
|
+
if (!this.alive(generation)) return;
|
|
1853
|
+
if (page.length > this.pageSize || page.some((row) => !row.id.trim())) throw new Error("Invalid conversation page");
|
|
1854
|
+
const merged = mergeConversations(rows, page);
|
|
1855
|
+
if (page.length === this.pageSize && merged.length === rows.length) throw new Error("Conversation pagination did not advance");
|
|
1856
|
+
rows = merged;
|
|
1857
|
+
offset += page.length;
|
|
1858
|
+
hasMore = page.length === this.pageSize;
|
|
1859
|
+
if (!hasMore || offset >= target && applyConversationFilter(rows, this.state.filter).length > 0 && (!boundary || page.some((row) => compare2(row, boundary) <= 0))) break;
|
|
1860
|
+
}
|
|
1861
|
+
if (!this.alive(generation)) return;
|
|
1862
|
+
this.source = rows;
|
|
1863
|
+
this.offset = offset;
|
|
1864
|
+
this.patch({ conversations: applyConversationFilter(rows, this.state.filter), hasMore });
|
|
1865
|
+
}
|
|
1547
1866
|
/** Replace the loaded window atomically, retaining filters and rows during transient failures. */
|
|
1548
1867
|
refresh = async () => {
|
|
1549
1868
|
if (!this.alive()) return;
|
|
@@ -1554,28 +1873,14 @@ var ConversationListStore = class {
|
|
|
1554
1873
|
if (!this.state.hasLoaded) return this.loadInitial();
|
|
1555
1874
|
const generation = this.generation;
|
|
1556
1875
|
const filter = this.state.filter;
|
|
1557
|
-
const target = Math.max(this.pageSize, this.offset);
|
|
1558
|
-
const compare2 = (a, b) => a.createdAt.getTime() - b.createdAt.getTime() || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
1559
|
-
const boundary = this.options.pageLoader ? void 0 : this.source.reduce((oldest, row) => !oldest || compare2(row, oldest) < 0 ? row : oldest, void 0);
|
|
1560
1876
|
this.refreshing = true;
|
|
1561
1877
|
this.patch({ error: null });
|
|
1562
1878
|
try {
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
const merged = mergeConversations(rows, page);
|
|
1569
|
-
if (page.length === this.pageSize && merged.length === rows.length) throw new Error("Conversation pagination did not advance");
|
|
1570
|
-
rows = merged;
|
|
1571
|
-
offset += page.length;
|
|
1572
|
-
hasMore = page.length === this.pageSize;
|
|
1573
|
-
if (!hasMore || offset >= target && applyConversationFilter(rows, this.state.filter).length > 0 && (!boundary || page.some((row) => compare2(row, boundary) <= 0))) break;
|
|
1574
|
-
}
|
|
1575
|
-
if (!this.alive(generation)) return;
|
|
1576
|
-
this.source = rows;
|
|
1577
|
-
this.offset = offset;
|
|
1578
|
-
this.patch({ conversations: applyConversationFilter(rows, this.state.filter), hasMore });
|
|
1879
|
+
await this.withFallback(
|
|
1880
|
+
generation,
|
|
1881
|
+
() => this.refreshInbox(generation, filter),
|
|
1882
|
+
() => this.refreshLegacy(generation, filter)
|
|
1883
|
+
);
|
|
1579
1884
|
} catch (cause) {
|
|
1580
1885
|
this.fail(cause, generation);
|
|
1581
1886
|
} finally {
|
|
@@ -1588,9 +1893,14 @@ var ConversationListStore = class {
|
|
|
1588
1893
|
loadMore = async () => {
|
|
1589
1894
|
if (!this.alive() || this.refreshing || this.state.isInitialLoading || this.state.isLoadingMore || !this.state.hasMore) return;
|
|
1590
1895
|
const generation = this.generation;
|
|
1896
|
+
const filter = this.state.filter;
|
|
1591
1897
|
this.patch({ isLoadingMore: true, error: null });
|
|
1592
1898
|
try {
|
|
1593
|
-
await this.
|
|
1899
|
+
await this.withFallback(
|
|
1900
|
+
generation,
|
|
1901
|
+
() => this.loadInboxUntilVisible(generation, filter),
|
|
1902
|
+
() => this.loadUntilVisible(generation, filter)
|
|
1903
|
+
);
|
|
1594
1904
|
} catch (cause) {
|
|
1595
1905
|
this.fail(cause, generation);
|
|
1596
1906
|
} finally {
|
|
@@ -1642,6 +1952,8 @@ function useConversationList(options) {
|
|
|
1642
1952
|
});
|
|
1643
1953
|
return {
|
|
1644
1954
|
conversations: field("conversations"),
|
|
1955
|
+
summaries: field("summaries"),
|
|
1956
|
+
currentUserId: field("currentUserId"),
|
|
1645
1957
|
filter: field("filter"),
|
|
1646
1958
|
isInitialLoading: field("isInitialLoading"),
|
|
1647
1959
|
isLoadingMore: field("isLoadingMore"),
|
|
@@ -1667,6 +1979,8 @@ var appearanceProps3 = {
|
|
|
1667
1979
|
var listViewProps = {
|
|
1668
1980
|
...appearanceProps3,
|
|
1669
1981
|
conversations: { type: Array, required: true },
|
|
1982
|
+
summaries: { type: Object, default: void 0 },
|
|
1983
|
+
currentUserId: { type: String, default: void 0 },
|
|
1670
1984
|
selectedConversationId: { type: String, default: void 0 },
|
|
1671
1985
|
onConversationSelect: { type: Function, default: void 0 },
|
|
1672
1986
|
onRefresh: { type: Function, default: void 0 },
|
|
@@ -1716,6 +2030,25 @@ var ConversationListView = defineComponent4({
|
|
|
1716
2030
|
const refresh = () => {
|
|
1717
2031
|
return props.onRefresh?.();
|
|
1718
2032
|
};
|
|
2033
|
+
const inlineRetry = () => {
|
|
2034
|
+
if (props.hasMore && props.onLoadMore) return () => {
|
|
2035
|
+
lastRequestedLength = null;
|
|
2036
|
+
void requestMore();
|
|
2037
|
+
};
|
|
2038
|
+
if (props.onRefresh) return () => {
|
|
2039
|
+
void refresh();
|
|
2040
|
+
};
|
|
2041
|
+
return void 0;
|
|
2042
|
+
};
|
|
2043
|
+
const unreadBadge = (summary) => {
|
|
2044
|
+
if (summary.unreadCount <= 0 && !summary.unreadCountCapped) return null;
|
|
2045
|
+
const capped = summary.unreadCountCapped || summary.unreadCount > 99;
|
|
2046
|
+
return h4("span", {
|
|
2047
|
+
class: "ckui-unread-badge",
|
|
2048
|
+
role: "img",
|
|
2049
|
+
"aria-label": `${summary.unreadCountCapped ? "99+" : summary.unreadCount} unread`
|
|
2050
|
+
}, [h4("span", { "aria-hidden": "true" }, capped ? "99+" : String(summary.unreadCount))]);
|
|
2051
|
+
};
|
|
1719
2052
|
const renderContent = () => {
|
|
1720
2053
|
const currentAppearance = appearance();
|
|
1721
2054
|
if (props.isInitialLoading && props.conversations.length === 0) {
|
|
@@ -1747,10 +2080,21 @@ var ConversationListView = defineComponent4({
|
|
|
1747
2080
|
const children = props.conversations.flatMap((conversation, index) => {
|
|
1748
2081
|
const selected = props.selectedConversationId === conversation.id;
|
|
1749
2082
|
const select = () => selectConversation(conversation);
|
|
1750
|
-
const
|
|
2083
|
+
const summary = props.summaries?.get(conversation.id);
|
|
2084
|
+
const slotProps = {
|
|
2085
|
+
conversation,
|
|
2086
|
+
index,
|
|
2087
|
+
selected,
|
|
2088
|
+
select,
|
|
2089
|
+
...summary ? { summary } : {},
|
|
2090
|
+
...props.currentUserId === void 0 ? {} : { currentUserId: props.currentUserId }
|
|
2091
|
+
};
|
|
2092
|
+
const preview = inboxPreview(conversation, summary, props.currentUserId);
|
|
2093
|
+
const unread = summary !== void 0 && (summary.unreadCount > 0 || summary.unreadCountCapped);
|
|
1751
2094
|
const item = slots["conversation-item"]?.(slotProps) ?? h4("button", {
|
|
1752
2095
|
type: "button",
|
|
1753
2096
|
"data-selected": selected || void 0,
|
|
2097
|
+
"data-unread": unread || void 0,
|
|
1754
2098
|
"aria-current": selected ? "true" : void 0,
|
|
1755
2099
|
onClick: select,
|
|
1756
2100
|
class: partClass("listItem", currentAppearance, "ckui-conversation-item"),
|
|
@@ -1764,8 +2108,14 @@ var ConversationListView = defineComponent4({
|
|
|
1764
2108
|
}),
|
|
1765
2109
|
h4("span", { class: "ckui-conversation-item__body" }, [
|
|
1766
2110
|
h4("strong", conversation.displayTitle),
|
|
1767
|
-
h4("span", conversation.participants.map((participant) => participant.name).join(", ") || conversation.description || "No participants")
|
|
2111
|
+
h4("span", preview || conversation.participants.map((participant) => participant.name).join(", ") || conversation.description || "No participants")
|
|
1768
2112
|
]),
|
|
2113
|
+
// Spread rather than emit `null`: a null child renders a `<!---->` comment, and rows without a
|
|
2114
|
+
// summary must keep 0.5's exact markup.
|
|
2115
|
+
...summary ? [h4("span", { class: "ckui-conversation-item__meta" }, [
|
|
2116
|
+
h4("time", { class: "ckui-conversation-item__time", datetime: summary.activityAt.toISOString() }, formatMessageTime(summary.activityAt)),
|
|
2117
|
+
unreadBadge(summary)
|
|
2118
|
+
])] : [],
|
|
1769
2119
|
h4(ChevronRight, { size: 18, "aria-hidden": "true" })
|
|
1770
2120
|
]);
|
|
1771
2121
|
const nodes = [h4("div", { key: conversation.id, role: "listitem" }, [item])];
|
|
@@ -1775,13 +2125,15 @@ var ConversationListView = defineComponent4({
|
|
|
1775
2125
|
return nodes;
|
|
1776
2126
|
});
|
|
1777
2127
|
if (props.error) {
|
|
1778
|
-
|
|
2128
|
+
const retry = inlineRetry();
|
|
2129
|
+
children.push(slots.error?.({ error: props.error, ...retry ? { retry } : {} }) ?? h4("div", {
|
|
1779
2130
|
class: partClass("error", currentAppearance, "ckui-inline-state ckui-state--error"),
|
|
1780
2131
|
style: partStyle("error", currentAppearance),
|
|
1781
2132
|
role: "alert"
|
|
1782
|
-
}, [
|
|
1783
|
-
|
|
1784
|
-
|
|
2133
|
+
}, [
|
|
2134
|
+
h4("span", errorMessage(props.error)),
|
|
2135
|
+
...retry ? [h4("button", { type: "button", class: "ckui-link-button", onClick: retry }, "Retry")] : []
|
|
2136
|
+
]));
|
|
1785
2137
|
} else if (props.isLoadingMore) {
|
|
1786
2138
|
children.push(slots["load-more"]?.() ?? h4("div", {
|
|
1787
2139
|
class: partClass("loading", currentAppearance, "ckui-inline-state"),
|
|
@@ -1842,6 +2194,7 @@ var ConversationList = defineComponent4({
|
|
|
1842
2194
|
initialFilter: { type: Object, default: void 0 },
|
|
1843
2195
|
pageSize: { type: Number, default: 30 },
|
|
1844
2196
|
autoLoad: { type: Boolean, default: true },
|
|
2197
|
+
activityRefreshWindowMs: { type: Number, default: void 0 },
|
|
1845
2198
|
onControllerChange: { type: Function, default: void 0 }
|
|
1846
2199
|
},
|
|
1847
2200
|
emits: ["conversation-select", "controller-change"],
|
|
@@ -1851,7 +2204,8 @@ var ConversationList = defineComponent4({
|
|
|
1851
2204
|
...props.pageLoader ? { pageLoader: props.pageLoader } : {},
|
|
1852
2205
|
...props.initialFilter ? { initialFilter: props.initialFilter } : {},
|
|
1853
2206
|
pageSize: props.pageSize,
|
|
1854
|
-
autoLoad: props.autoLoad
|
|
2207
|
+
autoLoad: props.autoLoad,
|
|
2208
|
+
...props.activityRefreshWindowMs === void 0 ? {} : { activityRefreshWindowMs: props.activityRefreshWindowMs }
|
|
1855
2209
|
});
|
|
1856
2210
|
expose({ controller });
|
|
1857
2211
|
watchEffect2(() => {
|
|
@@ -1864,8 +2218,11 @@ var ConversationList = defineComponent4({
|
|
|
1864
2218
|
initialFilter: _initialFilter,
|
|
1865
2219
|
pageSize: _pageSize,
|
|
1866
2220
|
autoLoad: _autoLoad,
|
|
2221
|
+
activityRefreshWindowMs: _activityRefreshWindowMs,
|
|
1867
2222
|
onControllerChange: _onControllerChange,
|
|
1868
2223
|
conversations: _conversations,
|
|
2224
|
+
summaries: _summaries,
|
|
2225
|
+
currentUserId: _currentUserId,
|
|
1869
2226
|
onRefresh: _onRefresh,
|
|
1870
2227
|
onLoadMore: _onLoadMore,
|
|
1871
2228
|
isInitialLoading: _isInitialLoading,
|
|
@@ -1878,6 +2235,8 @@ var ConversationList = defineComponent4({
|
|
|
1878
2235
|
...attrs,
|
|
1879
2236
|
...forwarded,
|
|
1880
2237
|
conversations: controller.conversations.value,
|
|
2238
|
+
summaries: controller.summaries.value,
|
|
2239
|
+
currentUserId: controller.currentUserId.value,
|
|
1881
2240
|
onRefresh: controller.refresh,
|
|
1882
2241
|
onLoadMore: controller.loadMore,
|
|
1883
2242
|
isInitialLoading: controller.isInitialLoading.value,
|
|
@@ -1911,6 +2270,7 @@ var defaultConvoKitTheme = {
|
|
|
1911
2270
|
incomingBubble: "#f4f4f5",
|
|
1912
2271
|
outgoingBubble: "#18181b",
|
|
1913
2272
|
outgoingText: "#fafafa",
|
|
2273
|
+
badge: "#18181b",
|
|
1914
2274
|
radius: "10px",
|
|
1915
2275
|
avatarSize: "40px",
|
|
1916
2276
|
fontFamily: 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'
|
|
@@ -1942,6 +2302,7 @@ var ConvoKitThemeProvider = defineComponent5({
|
|
|
1942
2302
|
"--ckui-incoming": theme.incomingBubble,
|
|
1943
2303
|
"--ckui-outgoing": theme.outgoingBubble,
|
|
1944
2304
|
"--ckui-outgoing-text": theme.outgoingText,
|
|
2305
|
+
"--ckui-badge": theme.badge,
|
|
1945
2306
|
"--ckui-radius": theme.radius,
|
|
1946
2307
|
"--ckui-avatar-size": theme.avatarSize,
|
|
1947
2308
|
"--ckui-font": theme.fontFamily
|
|
@@ -1973,6 +2334,7 @@ export {
|
|
|
1973
2334
|
isConvoKitPendingMessage,
|
|
1974
2335
|
matchesConversation,
|
|
1975
2336
|
mergeConversations,
|
|
2337
|
+
mergeInboxEntries,
|
|
1976
2338
|
mergeMessages,
|
|
1977
2339
|
readerIdsFor,
|
|
1978
2340
|
useConversation,
|