@convokitapp/vue-ui 0.2.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -46,12 +46,25 @@ module.exports = __toCommonJS(index_exports);
46
46
  // src/client.ts
47
47
  function createConvoKitUiClient(client) {
48
48
  return {
49
+ get sessionIdentity() {
50
+ return client.connected ? client.realtime : null;
51
+ },
52
+ onConnectionEvent: (handlers) => client.realtime.onConnectionEvent(handlers),
53
+ onInboxChanged: (handler, onError) => client.realtime.onInboxChanged(client.clientId, {
54
+ onEvent: handler,
55
+ ...onError ? { onError } : {}
56
+ }),
57
+ onMessageDeleted: (conversationId, handler, onError) => client.realtime.onMessageDeleted(conversationId, {
58
+ onEvent: handler,
59
+ ...onError ? { onError } : {}
60
+ }),
49
61
  get currentUserId() {
50
- return client.currentUserId;
62
+ return client.connected ? client.currentUserId : "";
51
63
  },
52
64
  getConversations: (options) => client.getConversations(options),
53
65
  getConversation: (conversationId) => client.getConversation(conversationId),
54
66
  getMessages: (options) => client.getMessages(options),
67
+ getMessage: (id) => client.getMessage(id),
55
68
  sendMessage: (input) => client.sendMessage(input),
56
69
  markConversationRead: (conversationId) => client.markConversationRead(conversationId),
57
70
  sendTyping: (input) => client.sendTyping(input),
@@ -72,16 +85,17 @@ function createConvoKitUiClient(client) {
72
85
 
73
86
  // src/components/avatar.ts
74
87
  var import_reka_ui = require("reka-ui");
75
- var import_vue = require("vue");
88
+ var import_vue2 = require("vue");
76
89
 
77
90
  // src/utils.ts
78
91
  var import_clsx = require("clsx");
92
+ var import_vue = require("vue");
79
93
  var pendingMessageIdPrefix = "convokit-pending-";
80
94
  function isConvoKitPendingMessage(message) {
81
95
  return message.id.startsWith(pendingMessageIdPrefix);
82
96
  }
83
97
  function cx(...values) {
84
- return (0, import_clsx.clsx)(values);
98
+ return (0, import_clsx.clsx)(values.map(import_vue.normalizeClass));
85
99
  }
86
100
  function requestedParticipantIds(filter) {
87
101
  const values = filter.participantIds ?? [];
@@ -154,7 +168,7 @@ function initials(value) {
154
168
  }
155
169
 
156
170
  // src/components/avatar.ts
157
- var ConvoKitAvatar = (0, import_vue.defineComponent)({
171
+ var ConvoKitAvatar = (0, import_vue2.defineComponent)({
158
172
  name: "ConvoKitAvatar",
159
173
  inheritAttrs: false,
160
174
  props: {
@@ -162,13 +176,13 @@ var ConvoKitAvatar = (0, import_vue.defineComponent)({
162
176
  src: { type: String, default: null }
163
177
  },
164
178
  setup(props, { attrs }) {
165
- return () => (0, import_vue.h)(import_reka_ui.AvatarRoot, {
179
+ return () => (0, import_vue2.h)(import_reka_ui.AvatarRoot, {
166
180
  ...attrs,
167
181
  class: cx("ckui-avatar", attrs.class)
168
182
  }, {
169
183
  default: () => [
170
- props.src ? (0, import_vue.h)(import_reka_ui.AvatarImage, { class: "ckui-avatar__image", src: props.src, alt: "" }) : null,
171
- (0, import_vue.h)(import_reka_ui.AvatarFallback, {
184
+ props.src ? (0, import_vue2.h)(import_reka_ui.AvatarImage, { class: "ckui-avatar__image", src: props.src, alt: "" }) : null,
185
+ (0, import_vue2.h)(import_reka_ui.AvatarFallback, {
172
186
  class: "ckui-avatar__fallback",
173
187
  ...props.src ? { delayMs: 300 } : {}
174
188
  }, () => initials(props.name))
@@ -178,251 +192,623 @@ var ConvoKitAvatar = (0, import_vue.defineComponent)({
178
192
  });
179
193
 
180
194
  // src/components/conversation.ts
181
- var import_vue5 = require("@lucide/vue");
182
- var import_vue6 = require("vue");
195
+ var import_vue6 = require("@lucide/vue");
196
+ var import_vue7 = require("vue");
183
197
 
184
198
  // src/composables/use-conversation.ts
185
- var import_vue2 = require("vue");
186
- function useConversation(options) {
187
- const messagePageSize = options.messagePageSize ?? 30;
188
- const typingTimeoutMs = options.typingTimeoutMs ?? 3e3;
189
- if (!(0, import_vue2.toValue)(options.conversationId).trim()) throw new TypeError("conversationId is required");
190
- if (!Number.isInteger(messagePageSize) || messagePageSize <= 0) throw new RangeError("messagePageSize must be a positive integer");
191
- if (!Number.isFinite(typingTimeoutMs) || typingTimeoutMs < 0) throw new RangeError("typingTimeoutMs must be non-negative");
192
- const conversation = (0, import_vue2.shallowRef)(null);
193
- const messages = (0, import_vue2.shallowRef)([]);
194
- const typingUserIds = (0, import_vue2.shallowRef)(/* @__PURE__ */ new Set());
195
- const readAtByUserId = (0, import_vue2.shallowRef)(/* @__PURE__ */ new Map());
196
- const isInitialLoading = (0, import_vue2.ref)(false);
197
- const isLoadingOlder = (0, import_vue2.ref)(false);
198
- const isSending = (0, import_vue2.ref)(false);
199
- const hasOlderMessages = (0, import_vue2.ref)(true);
200
- const hasLoaded = (0, import_vue2.ref)(false);
201
- const error = (0, import_vue2.shallowRef)(null);
202
- const currentUserId = (0, import_vue2.computed)(() => (0, import_vue2.toValue)(options.client).currentUserId);
203
- let generation = 0;
204
- let disposed = false;
205
- let sentTyping = false;
206
- let typingTimer = null;
207
- let subscriptions = [];
208
- const pendingIds = /* @__PURE__ */ new Set();
209
- let pendingSequence = 0;
210
- const unsubscribe = async () => {
211
- const active = subscriptions;
212
- subscriptions = [];
213
- await Promise.all(active.map((subscription) => subscription.unsubscribe()));
199
+ var import_vue3 = require("vue");
200
+
201
+ // src/conversation-store.ts
202
+ var import_sdk = require("@convokitapp/sdk");
203
+ function version(message) {
204
+ return message.updatedAt?.getTime() ?? message.createdAt.getTime();
205
+ }
206
+ function hasContent(message) {
207
+ return !!message.text?.trim() || message.media.length > 0;
208
+ }
209
+ function newest(current, incoming, incomingComplete = true) {
210
+ return version(current) > version(incoming) || !incomingComplete && version(current) === version(incoming) ? current : incoming;
211
+ }
212
+ function compare(left, right) {
213
+ return left.createdAt.getTime() - right.createdAt.getTime() || (left.id < right.id ? -1 : left.id > right.id ? 1 : 0);
214
+ }
215
+ function blank(currentUserId = "") {
216
+ return {
217
+ conversation: null,
218
+ messages: [],
219
+ typingUserIds: /* @__PURE__ */ new Set(),
220
+ readAtByUserId: /* @__PURE__ */ new Map(),
221
+ isInitialLoading: false,
222
+ isLoadingOlder: false,
223
+ isReconciling: false,
224
+ isSending: false,
225
+ hasOlderMessages: true,
226
+ hasLoaded: false,
227
+ error: null,
228
+ currentUserId
214
229
  };
215
- const markRead = async () => {
216
- const client = (0, import_vue2.toValue)(options.client);
217
- const conversationId = (0, import_vue2.toValue)(options.conversationId);
218
- try {
219
- await client.markConversationRead(conversationId);
220
- if (disposed) return;
221
- readAtByUserId.value = new Map(readAtByUserId.value).set(client.currentUserId, /* @__PURE__ */ new Date());
222
- } catch (cause) {
223
- if (!disposed) error.value = cause;
230
+ }
231
+ var ConversationStore = class {
232
+ constructor(options) {
233
+ this.options = options;
234
+ this.client = options.client;
235
+ this.room = options.conversationId.trim();
236
+ this.pageSize = options.messagePageSize ?? 30;
237
+ this.typingTimeout = options.typingTimeoutMs ?? 3e3;
238
+ if (!this.room) throw new TypeError("conversationId is required");
239
+ if (!Number.isInteger(this.pageSize) || this.pageSize < 1 || this.pageSize > 100) {
240
+ throw new RangeError("messagePageSize must be an integer between 1 and 100");
241
+ }
242
+ if (!Number.isFinite(this.typingTimeout) || this.typingTimeout < 0) {
243
+ throw new RangeError("typingTimeoutMs must be non-negative");
244
+ }
245
+ this.owner = this.client.sessionIdentity;
246
+ this.user = this.owner ? this.client.currentUserId : "";
247
+ this.state = blank(this.user);
248
+ }
249
+ options;
250
+ client;
251
+ room;
252
+ owner;
253
+ user;
254
+ pageSize;
255
+ typingTimeout;
256
+ state;
257
+ listeners = /* @__PURE__ */ new Set();
258
+ subscriptions = [];
259
+ disposed = true;
260
+ generation = 0;
261
+ cursor;
262
+ revision = 0;
263
+ changes = /* @__PURE__ */ new Map();
264
+ hydrations = /* @__PURE__ */ new Map();
265
+ hydrationPool = { running: /* @__PURE__ */ new Set(), queued: /* @__PURE__ */ new Map() };
266
+ // Keep tombstones until an explicit reload/session change, including across refreshes.
267
+ deleted = /* @__PURE__ */ new Set();
268
+ sendRevision;
269
+ activeSend;
270
+ refreshQueued = false;
271
+ typingTimers = /* @__PURE__ */ new Map();
272
+ ownTypingTimer;
273
+ sentTyping = false;
274
+ typingRevision = 0;
275
+ lastTypingSentAt = -Infinity;
276
+ getSnapshot = () => this.state;
277
+ subscribe = (listener) => {
278
+ this.listeners.add(listener);
279
+ return () => {
280
+ this.listeners.delete(listener);
281
+ };
282
+ };
283
+ patch(patch) {
284
+ if (patch.messages && this.activeSend) {
285
+ for (const message of patch.messages) this.confirmSend(message);
286
+ if (this.activeSend.confirmed) patch.messages = patch.messages.filter((message) => message.id !== this.activeSend.pending.id);
287
+ }
288
+ this.state = { ...this.state, ...patch };
289
+ for (const listener of this.listeners) listener();
290
+ }
291
+ alive(generation = this.generation) {
292
+ return !this.disposed && generation === this.generation && this.owner !== null && this.client.sessionIdentity === this.owner;
293
+ }
294
+ start = (autoLoad = true) => {
295
+ if (this.owner === null || this.client.sessionIdentity !== this.owner) return;
296
+ this.disposed = false;
297
+ this.patch({ currentUserId: this.user });
298
+ if (autoLoad) void this.loadInitial();
299
+ else {
300
+ try {
301
+ this.attach(this.generation, false);
302
+ } catch {
303
+ }
304
+ }
305
+ };
306
+ detach() {
307
+ const active = this.subscriptions;
308
+ this.subscriptions = [];
309
+ for (const subscription of active) void subscription.unsubscribe().catch(() => void 0);
310
+ }
311
+ clearTyping() {
312
+ for (const timer of this.typingTimers.values()) clearTimeout(timer);
313
+ this.typingTimers.clear();
314
+ clearTimeout(this.ownTypingTimer);
315
+ this.ownTypingTimer = void 0;
316
+ this.sentTyping = false;
317
+ this.typingRevision++;
318
+ this.lastTypingSentAt = -Infinity;
319
+ this.patch({ typingUserIds: /* @__PURE__ */ new Set() });
320
+ }
321
+ clear() {
322
+ this.generation++;
323
+ this.detach();
324
+ this.clearTyping();
325
+ this.cursor = void 0;
326
+ this.changes.clear();
327
+ this.hydrations.clear();
328
+ this.hydrationPool.queued.clear();
329
+ this.deleted.clear();
330
+ this.sendRevision = void 0;
331
+ this.activeSend = void 0;
332
+ this.refreshQueued = false;
333
+ }
334
+ dispose = () => {
335
+ if (this.alive() && this.sentTyping) {
336
+ void this.client.sendTyping({ conversationId: this.room, isTyping: false }).catch(() => void 0);
224
337
  }
338
+ this.disposed = true;
339
+ this.clear();
340
+ this.patch(blank());
225
341
  };
226
- const subscribe = (activeGeneration, client, conversationId) => {
227
- const report = (cause) => {
228
- if (!disposed && activeGeneration === generation) error.value = cause;
342
+ fail(cause, generation, history = false) {
343
+ if (!this.alive(generation)) return;
344
+ const status = typeof cause === "object" && cause !== null && "status" in cause ? cause.status : void 0;
345
+ if (history && (status === 401 || status === 403 || status === 404)) {
346
+ this.clear();
347
+ this.patch({ ...blank(this.user), error: cause, hasLoaded: true, hasOlderMessages: false });
348
+ } else this.patch({ error: cause });
349
+ }
350
+ attach(generation, data = true) {
351
+ const report = (cause) => this.fail(cause, generation);
352
+ const add = (create) => {
353
+ if (!this.alive(generation)) return;
354
+ const subscription = create();
355
+ if (this.alive(generation)) this.subscriptions.push(subscription);
356
+ else void subscription.unsubscribe().catch(() => void 0);
229
357
  };
230
- subscriptions = [
231
- client.onMessage(conversationId, (message) => {
232
- if (disposed || activeGeneration !== generation) return;
233
- const pending = messages.value.find((candidate) => pendingIds.has(candidate.id) && message.senderId === client.currentUserId && candidate.text === message.text && candidate.media.length === message.media.length);
234
- if (pending) pendingIds.delete(pending.id);
235
- messages.value = mergeMessages(
236
- pending ? messages.value.filter((candidate) => candidate.id !== pending.id) : messages.value,
237
- [message]
238
- );
239
- if ((options.markReadOnReceive ?? true) && message.senderId !== client.currentUserId) void markRead();
240
- }, report),
241
- client.onReadReceipt(conversationId, ({ userId, readAt }) => {
242
- if (disposed || activeGeneration !== generation) return;
243
- readAtByUserId.value = new Map(readAtByUserId.value).set(userId, readAt);
244
- }, report),
245
- client.onTyping(conversationId, ({ userId, isTyping }) => {
246
- if (disposed || activeGeneration !== generation || userId === client.currentUserId) return;
247
- const next = new Set(typingUserIds.value);
248
- if (isTyping) next.add(userId);
249
- else next.delete(userId);
250
- typingUserIds.value = next;
251
- }, report)
252
- ];
358
+ try {
359
+ add(() => this.client.onConnectionEvent({
360
+ onEvent: ({ topic, status }) => {
361
+ if (!data || !this.alive(generation) || topic !== `messages:${this.room}` && topic !== `conversation:${this.room}`) return;
362
+ if (status === "SUBSCRIBED") this.queueRefresh();
363
+ else this.clearTyping();
364
+ },
365
+ onSessionEnded: () => {
366
+ if (this.disposed || generation !== this.generation) return;
367
+ this.dispose();
368
+ },
369
+ onError: report
370
+ }));
371
+ if (!data) return;
372
+ add(() => this.client.onInboxChanged(() => {
373
+ if (this.alive(generation)) this.queueRefresh();
374
+ }, (cause) => {
375
+ if (this.alive(generation)) {
376
+ report(cause);
377
+ this.queueRefresh();
378
+ }
379
+ }));
380
+ add(() => this.client.onMessage(this.room, (event) => this.onMessage(event, generation), report));
381
+ add(() => this.client.onMessageDeleted(this.room, ({ id, conversationId }) => {
382
+ if (!this.alive(generation) || conversationId !== this.room || !id.trim()) return;
383
+ this.removeMessage(id);
384
+ }, report));
385
+ add(() => this.client.onReadReceipt(this.room, ({ userId, readAt }) => {
386
+ if (this.alive(generation)) this.mergeReads([[userId, readAt]]);
387
+ }, report));
388
+ add(() => this.client.onTyping(this.room, ({ userId, isTyping }) => {
389
+ if (!this.alive(generation) || !userId.trim() || userId === this.user) return;
390
+ clearTimeout(this.typingTimers.get(userId));
391
+ this.typingTimers.delete(userId);
392
+ const next = new Set(this.state.typingUserIds);
393
+ if (isTyping) {
394
+ next.add(userId);
395
+ this.typingTimers.set(userId, setTimeout(() => {
396
+ this.typingTimers.delete(userId);
397
+ if (!this.alive(generation)) return;
398
+ const remaining = new Set(this.state.typingUserIds);
399
+ remaining.delete(userId);
400
+ this.patch({ typingUserIds: remaining });
401
+ }, this.typingTimeout));
402
+ } else next.delete(userId);
403
+ this.patch({ typingUserIds: next });
404
+ }, report));
405
+ } catch (cause) {
406
+ this.detach();
407
+ this.fail(cause, generation);
408
+ throw cause;
409
+ }
410
+ }
411
+ validMessage(message) {
412
+ return message.conversationId === this.room && !!message.id.trim() && !!message.senderId.trim() && !isConvoKitPendingMessage(message) && Number.isFinite(message.createdAt.getTime()) && Number.isFinite(version(message));
413
+ }
414
+ onMessage(event, generation) {
415
+ const { message, type } = event;
416
+ if (!this.alive(generation) || type !== "insert" && type !== "update" || !this.validMessage(message) || this.deleted.has(message.id)) return;
417
+ const existing = this.state.messages.find((item) => item.id === message.id);
418
+ const known = existing ?? this.changes.get(message.id)?.message;
419
+ if (known && version(message) < version(known)) return;
420
+ const insert = type === "insert" || this.changes.get(message.id)?.insert === true;
421
+ if (!existing && !insert && type === "update" && !this.state.isInitialLoading && !this.state.isLoadingOlder && !this.state.isReconciling) return;
422
+ const revision = ++this.revision;
423
+ const provisional = existing && !message.media.length ? { ...message, media: existing.media } : message;
424
+ this.record(provisional, insert, revision, false);
425
+ const job = { revision, generation, message, insert };
426
+ this.hydrations.set(message.id, job);
427
+ this.hydrationPool.queued.set(message.id, job);
428
+ this.drainHydration();
429
+ if (type === "insert" && !existing && message.senderId !== this.user && (this.options.markReadOnReceive ?? true)) {
430
+ void this.markRead();
431
+ }
432
+ }
433
+ record(message, insert, revision, complete) {
434
+ const existing = this.state.messages.find((item) => item.id === message.id);
435
+ if (existing && version(existing) > version(message)) return;
436
+ if (this.confirmSend(message) && !complete && !message.media.length) {
437
+ message = { ...message, media: this.activeSend.pending.media };
438
+ this.confirmSend(message);
439
+ }
440
+ this.changes.set(message.id, { revision, message, insert, complete });
441
+ if (existing || insert && hasContent(message)) this.patch({ messages: mergeMessages(this.state.messages, [message]) });
442
+ }
443
+ confirmSend(message) {
444
+ const send = this.activeSend;
445
+ if (!send || !this.validMessage(message) || message.senderId !== this.user || message.clientMessageId !== send.pending.clientMessageId) return false;
446
+ send.confirmed = send.confirmed ? newest(send.confirmed, message) : message;
447
+ return true;
448
+ }
449
+ currentHydration(job) {
450
+ return this.alive(job.generation) && !this.deleted.has(job.message.id) && this.hydrations.get(job.message.id) === job;
451
+ }
452
+ removeMessage(id) {
453
+ this.deleted.add(id);
454
+ this.changes.delete(id);
455
+ this.hydrations.delete(id);
456
+ this.hydrationPool.queued.delete(id);
457
+ this.patch({ messages: this.state.messages.filter((message) => message.id !== id) });
458
+ }
459
+ drainHydration() {
460
+ const pool = this.hydrationPool;
461
+ for (const [id, job] of pool.queued) {
462
+ if (pool.running.size >= 8) break;
463
+ if (pool.running.has(id)) continue;
464
+ pool.queued.delete(id);
465
+ if (!this.currentHydration(job)) continue;
466
+ pool.running.add(id);
467
+ void this.hydrate(job, pool);
468
+ }
469
+ }
470
+ async hydrate(job, pool) {
471
+ const id = job.message.id;
472
+ try {
473
+ if (!this.currentHydration(job)) return;
474
+ const full = await this.client.getMessage(id);
475
+ if (!this.currentHydration(job)) return;
476
+ if (!this.validMessage(full) || full.id !== id || full.senderId !== job.message.senderId || version(full) < version(job.message)) {
477
+ throw new Error("Complete message response does not match the observed resource/revision");
478
+ }
479
+ this.record(full, job.insert, job.revision, true);
480
+ } catch (cause) {
481
+ if (!this.currentHydration(job)) return;
482
+ const status = typeof cause === "object" && cause !== null && "status" in cause ? cause.status : void 0;
483
+ if (status === 404) this.removeMessage(id);
484
+ this.fail(cause, job.generation);
485
+ this.queueRefresh();
486
+ } finally {
487
+ if (this.hydrations.get(id) === job) this.hydrations.delete(id);
488
+ pool.running.delete(id);
489
+ queueMicrotask(() => {
490
+ if (this.hydrationPool === pool) this.drainHydration();
491
+ });
492
+ }
493
+ }
494
+ mergeReads(entries) {
495
+ const next = new Map(this.state.readAtByUserId);
496
+ for (const [userId, readAt] of entries) {
497
+ if (!userId.trim() || !Number.isFinite(readAt.getTime())) continue;
498
+ if (readAt.getTime() > (next.get(userId)?.getTime() ?? -Infinity)) next.set(userId, readAt);
499
+ }
500
+ this.patch({ readAtByUserId: next });
501
+ }
502
+ validatePage(page, before) {
503
+ if (page.length > this.pageSize) throw new Error("Message page exceeds the requested limit");
504
+ let previous = before;
505
+ for (const message of page) {
506
+ if (!this.validMessage(message) || previous && compare(message, previous) >= 0) {
507
+ throw new Error("Message history must contain distinct, room-scoped rows in newest-first cursor order");
508
+ }
509
+ previous = message;
510
+ }
511
+ }
512
+ fetchPage(before) {
513
+ return this.client.getMessages({
514
+ conversationId: this.room,
515
+ limit: this.pageSize,
516
+ ...before ? { beforeCreatedAt: before.createdAt, beforeId: before.id } : {}
517
+ });
518
+ }
519
+ overlay(rows, revision) {
520
+ const byId = new Map(rows.filter((message) => !this.deleted.has(message.id)).map((message) => [message.id, message]));
521
+ for (const [id, change] of this.changes) {
522
+ if (change.revision > revision && !this.deleted.has(id) && (change.insert || byId.has(id))) {
523
+ const current = byId.get(id);
524
+ if (current || change.complete || hasContent(change.message)) {
525
+ byId.set(id, current ? newest(current, change.message, change.complete) : change.message);
526
+ }
527
+ }
528
+ }
529
+ for (const message of this.state.messages) {
530
+ if (isConvoKitPendingMessage(message)) byId.set(message.id, message);
531
+ }
532
+ return mergeMessages([], [...byId.values()]);
533
+ }
534
+ prune(revision) {
535
+ const safeRevision = Math.min(revision, this.sendRevision ?? Infinity);
536
+ for (const [id, change] of this.changes) if (change.revision <= safeRevision) this.changes.delete(id);
537
+ for (const [id, job] of this.hydrations) if (job.revision <= revision) {
538
+ this.hydrations.delete(id);
539
+ this.hydrationPool.queued.delete(id);
540
+ }
541
+ }
542
+ loadInitial = async () => {
543
+ if (!this.alive()) return;
544
+ this.clear();
545
+ const generation = this.generation;
546
+ this.patch({ ...blank(this.user), isInitialLoading: true });
547
+ const revision = this.revision;
548
+ try {
549
+ this.attach(generation);
550
+ if (!this.alive(generation)) return;
551
+ const [conversation, page] = await Promise.all([this.client.getConversation(this.room), this.fetchPage()]);
552
+ if (!this.alive(generation)) return;
553
+ if (conversation.id !== this.room) throw new Error("Conversation response belongs to a different room");
554
+ this.validatePage(page);
555
+ this.cursor = page.at(-1);
556
+ this.patch({ conversation, messages: this.overlay(page, revision), hasOlderMessages: page.length === this.pageSize });
557
+ this.mergeReads(conversation.participants.flatMap((participant) => participant.lastReadAt ? [[participant.appUserId, participant.lastReadAt]] : []));
558
+ this.prune(revision);
559
+ if (this.options.markReadOnLoad ?? true) await this.markRead();
560
+ } catch (cause) {
561
+ this.fail(cause, generation, true);
562
+ } finally {
563
+ if (this.alive(generation)) {
564
+ this.patch({ isInitialLoading: false, hasLoaded: true });
565
+ this.flushRefresh();
566
+ }
567
+ }
253
568
  };
254
- const loadInitial = async () => {
255
- const client = (0, import_vue2.toValue)(options.client);
256
- const conversationId = (0, import_vue2.toValue)(options.conversationId).trim();
257
- if (!conversationId) throw new TypeError("conversationId is required");
258
- const activeGeneration = ++generation;
259
- await unsubscribe();
260
- if (disposed || activeGeneration !== generation) return;
261
- messages.value = [];
262
- pendingIds.clear();
263
- conversation.value = null;
264
- typingUserIds.value = /* @__PURE__ */ new Set();
265
- readAtByUserId.value = /* @__PURE__ */ new Map();
266
- hasOlderMessages.value = true;
267
- error.value = null;
268
- isInitialLoading.value = true;
269
- isLoadingOlder.value = false;
270
- subscribe(activeGeneration, client, conversationId);
569
+ queueRefresh() {
570
+ this.refreshQueued = true;
571
+ this.flushRefresh();
572
+ }
573
+ flushRefresh() {
574
+ const generation = this.generation;
575
+ void Promise.resolve().then(() => {
576
+ if (!this.alive(generation) || !this.refreshQueued || this.state.isInitialLoading || this.state.isLoadingOlder || this.state.isReconciling) return;
577
+ this.refreshQueued = false;
578
+ void this.refresh();
579
+ });
580
+ }
581
+ /** Re-fetch the entire viewed range atomically; a first-page-only refresh loses history. */
582
+ refresh = async () => {
583
+ if (!this.alive()) return;
584
+ if (this.state.isInitialLoading || this.state.isLoadingOlder || this.state.isReconciling) {
585
+ this.refreshQueued = true;
586
+ return;
587
+ }
588
+ if (!this.state.hasLoaded || !this.subscriptions.length) return this.loadInitial();
589
+ const generation = this.generation;
590
+ const revision = this.revision;
591
+ const observed = this.state.messages.filter((message) => !isConvoKitPendingMessage(message)).concat([...this.changes.values()].filter((change) => change.insert).map((change) => change.message));
592
+ const boundary = observed.reduce((oldest, message) => !oldest || compare(message, oldest) < 0 ? message : oldest, this.cursor);
593
+ const previouslyExhausted = !this.state.hasOlderMessages;
594
+ this.patch({ isReconciling: true, error: null });
271
595
  try {
272
- const [nextConversation, page] = await Promise.all([
273
- client.getConversation(conversationId),
274
- client.getMessages({ conversationId, limit: messagePageSize, offset: 0 })
275
- ]);
276
- if (disposed || activeGeneration !== generation) return;
277
- conversation.value = nextConversation;
278
- readAtByUserId.value = new Map(nextConversation.participants.flatMap((participant) => participant.lastReadAt ? [[participant.appUserId, participant.lastReadAt]] : []));
279
- messages.value = mergeMessages([], page);
280
- hasOlderMessages.value = page.length === messagePageSize;
281
- if (options.markReadOnLoad ?? true) await markRead();
596
+ const conversation = await this.client.getConversation(this.room);
597
+ if (!this.alive(generation)) return;
598
+ if (conversation.id !== this.room) throw new Error("Conversation response belongs to a different room");
599
+ const rows = [];
600
+ let before;
601
+ let hasOlder = true;
602
+ while (this.alive(generation)) {
603
+ const page = await this.fetchPage(before);
604
+ if (!this.alive(generation)) return;
605
+ this.validatePage(page, before);
606
+ rows.push(...page);
607
+ before = page.at(-1) ?? before;
608
+ hasOlder = page.length === this.pageSize;
609
+ if (!hasOlder || !boundary || !previouslyExhausted && before && compare(before, boundary) < 0) break;
610
+ }
611
+ this.cursor = before;
612
+ const reconciled = this.overlay(rows, revision);
613
+ const survivingIds = new Set(reconciled.map((message) => message.id));
614
+ 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
+ }
621
+ this.patch({ conversation, messages: reconciled, hasOlderMessages: hasOlder });
622
+ this.mergeReads(conversation.participants.flatMap((participant) => participant.lastReadAt ? [[participant.appUserId, participant.lastReadAt]] : []));
623
+ this.prune(revision);
282
624
  } catch (cause) {
283
- if (!disposed && activeGeneration === generation) error.value = cause;
625
+ this.fail(cause, generation, true);
284
626
  } finally {
285
- if (!disposed && activeGeneration === generation) {
286
- isInitialLoading.value = false;
287
- hasLoaded.value = true;
627
+ if (this.alive(generation)) {
628
+ this.patch({ isReconciling: false });
629
+ this.flushRefresh();
288
630
  }
289
631
  }
290
632
  };
291
- const loadOlderMessages = async () => {
292
- if (isInitialLoading.value || isLoadingOlder.value || !hasOlderMessages.value) return;
293
- const activeGeneration = generation;
294
- const client = (0, import_vue2.toValue)(options.client);
295
- const conversationId = (0, import_vue2.toValue)(options.conversationId);
296
- isLoadingOlder.value = true;
297
- error.value = null;
633
+ loadOlderMessages = async () => {
634
+ if (!this.alive() || !this.state.hasLoaded || this.state.isInitialLoading || this.state.isLoadingOlder || this.state.isReconciling || !this.state.hasOlderMessages) return;
635
+ const generation = this.generation;
636
+ const revision = this.revision;
637
+ const cursor = this.cursor;
638
+ this.patch({ isLoadingOlder: true, error: null });
298
639
  try {
299
- const page = await client.getMessages({
300
- conversationId,
301
- limit: messagePageSize,
302
- offset: messages.value.filter((message) => !pendingIds.has(message.id)).length
640
+ const page = await this.fetchPage(cursor);
641
+ if (!this.alive(generation)) return;
642
+ this.validatePage(page, cursor);
643
+ this.cursor = page.at(-1) ?? cursor;
644
+ this.patch({
645
+ messages: this.overlay(mergeMessages(page, this.state.messages), revision),
646
+ hasOlderMessages: page.length === this.pageSize
303
647
  });
304
- if (disposed || activeGeneration !== generation) return;
305
- messages.value = mergeMessages(messages.value, page);
306
- hasOlderMessages.value = page.length === messagePageSize;
307
648
  } catch (cause) {
308
- if (!disposed && activeGeneration === generation) error.value = cause;
649
+ this.fail(cause, generation, true);
309
650
  } finally {
310
- if (!disposed && activeGeneration === generation) isLoadingOlder.value = false;
651
+ if (this.alive(generation)) {
652
+ this.patch({ isLoadingOlder: false });
653
+ this.flushRefresh();
654
+ }
311
655
  }
312
656
  };
313
- const updateTyping = async (nextTyping) => {
314
- if (typingTimer) clearTimeout(typingTimer);
315
- if (nextTyping) typingTimer = setTimeout(() => {
316
- void updateTyping(false);
317
- }, typingTimeoutMs);
318
- if (sentTyping === nextTyping) return;
319
- sentTyping = nextTyping;
657
+ markRead = async () => {
658
+ if (!this.alive()) return;
659
+ const generation = this.generation;
320
660
  try {
321
- await (0, import_vue2.toValue)(options.client).sendTyping({
322
- conversationId: (0, import_vue2.toValue)(options.conversationId),
323
- isTyping: nextTyping
324
- });
661
+ await this.client.markConversationRead(this.room);
662
+ } catch (cause) {
663
+ this.fail(cause, generation);
664
+ }
665
+ };
666
+ updateTyping = async (isTyping) => {
667
+ if (!this.alive()) return;
668
+ const generation = this.generation;
669
+ clearTimeout(this.ownTypingTimer);
670
+ if (isTyping) this.ownTypingTimer = setTimeout(() => {
671
+ if (this.alive(generation)) void this.updateTyping(false);
672
+ }, this.typingTimeout);
673
+ const now = performance.now();
674
+ const renew = isTyping && now - this.lastTypingSentAt >= Math.max(1, this.typingTimeout / 2);
675
+ if (this.sentTyping === isTyping && !renew) return;
676
+ const revision = ++this.typingRevision;
677
+ this.sentTyping = isTyping;
678
+ this.lastTypingSentAt = isTyping ? now : -Infinity;
679
+ try {
680
+ await this.client.sendTyping({ conversationId: this.room, isTyping });
325
681
  } catch (cause) {
326
- if (!disposed) error.value = cause;
682
+ if (this.alive(generation) && revision === this.typingRevision) {
683
+ this.sentTyping = false;
684
+ this.fail(cause, generation);
685
+ }
327
686
  }
328
687
  };
329
- const sendMessage = async ({ text, media }) => {
330
- const normalizedText = text?.trim();
331
- if (!normalizedText && (!media || media.length === 0)) return null;
332
- if (isSending.value) return null;
333
- const pendingId = `convokit-pending-${Date.now()}-${++pendingSequence}`;
334
- const pendingMessage = {
688
+ sendMessage = async ({ text, media }) => {
689
+ const normalized = text?.trim();
690
+ if (!this.alive() || this.state.isSending || !normalized && !media?.length) return null;
691
+ const generation = this.generation;
692
+ const revision = this.revision;
693
+ this.sendRevision = revision;
694
+ const clientMessageId = (0, import_sdk.createClientMessageId)();
695
+ const pendingId = `convokit-pending-${clientMessageId}`;
696
+ const pending = {
335
697
  id: pendingId,
336
- conversationId: (0, import_vue2.toValue)(options.conversationId),
337
- senderId: (0, import_vue2.toValue)(options.client).currentUserId,
338
- text: normalizedText ?? null,
698
+ clientMessageId,
699
+ conversationId: this.room,
700
+ senderId: this.user,
701
+ text: normalized || null,
339
702
  media: media ?? [],
340
703
  createdAt: /* @__PURE__ */ new Date(),
341
704
  updatedAt: null
342
705
  };
343
- pendingIds.add(pendingId);
344
- messages.value = mergeMessages(messages.value, [pendingMessage]);
345
- isSending.value = true;
346
- error.value = null;
347
- const activeGeneration = generation;
706
+ const send = { pending };
707
+ this.activeSend = send;
708
+ this.patch({ messages: mergeMessages(this.state.messages, [pending]), isSending: true, error: null });
348
709
  try {
349
- const message = await (0, import_vue2.toValue)(options.client).sendMessage({
350
- conversationId: (0, import_vue2.toValue)(options.conversationId),
351
- ...normalizedText ? { text: normalizedText } : {},
710
+ const message = await this.client.sendMessage({
711
+ conversationId: this.room,
712
+ clientMessageId,
713
+ ...normalized ? { text: normalized } : {},
352
714
  ...media?.length ? { media } : {}
353
715
  });
354
- pendingIds.delete(pendingId);
355
- if (!disposed && activeGeneration === generation) {
356
- messages.value = mergeMessages(
357
- messages.value.filter((candidate) => candidate.id !== pendingId),
358
- [message]
359
- );
360
- }
361
- await updateTyping(false);
362
- return message;
716
+ if (!this.alive(generation)) return null;
717
+ if (!this.validMessage(message) || message.senderId !== this.user) throw new Error("Send response belongs to a different room or sender");
718
+ if (message.clientMessageId && message.clientMessageId !== clientMessageId) throw new Error("Send response belongs to a different send");
719
+ const live = this.changes.get(message.id);
720
+ const existing = this.state.messages.find((item) => item.id === message.id);
721
+ let latest = existing ? newest(message, existing, live?.complete !== false) : message;
722
+ if (live && live.revision > revision) latest = newest(latest, live.message, live.complete);
723
+ if (!this.deleted.has(message.id)) this.changes.set(message.id, { revision: ++this.revision, message: latest, insert: true, complete: true });
724
+ this.patch({ messages: mergeMessages(
725
+ this.state.messages.filter((item) => item.id !== pendingId),
726
+ this.deleted.has(message.id) ? [] : [latest]
727
+ ) });
728
+ void this.updateTyping(false);
729
+ return this.alive(generation) ? latest : null;
363
730
  } catch (cause) {
364
- pendingIds.delete(pendingId);
365
- if (!disposed) {
366
- messages.value = messages.value.filter((candidate) => candidate.id !== pendingId);
367
- error.value = cause;
731
+ if (this.alive(generation)) {
732
+ this.patch({ messages: this.state.messages.filter((item) => item.id !== pendingId) });
733
+ if (send.confirmed) {
734
+ void this.updateTyping(false);
735
+ return send.confirmed;
736
+ }
737
+ this.fail(cause, generation);
368
738
  }
369
739
  return null;
370
740
  } finally {
371
- if (!disposed && activeGeneration === generation) isSending.value = false;
741
+ if (this.alive(generation)) {
742
+ this.sendRevision = void 0;
743
+ this.activeSend = void 0;
744
+ this.patch({ isSending: false });
745
+ }
372
746
  }
373
747
  };
748
+ readerIdsFor = (message) => readerIdsFor(message, this.state.readAtByUserId);
749
+ };
750
+
751
+ // src/composables/use-conversation.ts
752
+ function useConversation(options) {
753
+ const createStore = () => new ConversationStore({
754
+ ...options,
755
+ client: (0, import_vue3.toValue)(options.client),
756
+ conversationId: (0, import_vue3.toValue)(options.conversationId)
757
+ });
758
+ let store = createStore();
759
+ const snapshot = (0, import_vue3.shallowRef)(store.getSnapshot());
760
+ let unsubscribe;
761
+ const stop = (0, import_vue3.watch)(
762
+ () => [(0, import_vue3.toValue)(options.client), (0, import_vue3.toValue)(options.client).sessionIdentity, (0, import_vue3.toValue)(options.conversationId)],
763
+ () => {
764
+ store.dispose();
765
+ unsubscribe?.();
766
+ store = createStore();
767
+ snapshot.value = store.getSnapshot();
768
+ unsubscribe = store.subscribe(() => {
769
+ snapshot.value = store.getSnapshot();
770
+ });
771
+ store.start(options.autoLoad ?? true);
772
+ },
773
+ { immediate: true, flush: "sync" }
774
+ );
775
+ const field = (key) => (0, import_vue3.computed)(() => snapshot.value[key]);
374
776
  const dispose = async () => {
375
- disposed = true;
376
- generation += 1;
377
- if (typingTimer) clearTimeout(typingTimer);
378
- if (sentTyping) {
379
- await (0, import_vue2.toValue)(options.client).sendTyping({
380
- conversationId: (0, import_vue2.toValue)(options.conversationId),
381
- isTyping: false
382
- }).catch(() => void 0);
383
- }
384
- await unsubscribe();
777
+ stop();
778
+ store.dispose();
779
+ unsubscribe?.();
780
+ unsubscribe = void 0;
385
781
  };
386
- if (options.autoLoad ?? true) {
387
- (0, import_vue2.watch)(
388
- () => [(0, import_vue2.toValue)(options.client), (0, import_vue2.toValue)(options.conversationId)],
389
- () => {
390
- disposed = false;
391
- sentTyping = false;
392
- void loadInitial();
393
- },
394
- { immediate: true }
395
- );
396
- }
397
- if ((0, import_vue2.getCurrentScope)()) (0, import_vue2.onScopeDispose)(() => {
782
+ if ((0, import_vue3.getCurrentScope)()) (0, import_vue3.onScopeDispose)(() => {
398
783
  void dispose();
399
784
  });
400
785
  return {
401
- conversation,
402
- messages,
403
- typingUserIds,
404
- readAtByUserId,
405
- isInitialLoading,
406
- isLoadingOlder,
407
- isSending,
408
- hasOlderMessages,
409
- hasLoaded,
410
- error,
411
- currentUserId,
412
- readerIdsFor: (message) => readerIdsFor(message, readAtByUserId.value),
413
- loadInitial,
414
- refresh: loadInitial,
415
- loadOlderMessages,
416
- sendMessage,
417
- markRead,
418
- updateTyping,
786
+ conversation: field("conversation"),
787
+ messages: field("messages"),
788
+ typingUserIds: field("typingUserIds"),
789
+ readAtByUserId: field("readAtByUserId"),
790
+ isInitialLoading: field("isInitialLoading"),
791
+ isLoadingOlder: field("isLoadingOlder"),
792
+ isReconciling: field("isReconciling"),
793
+ isSending: field("isSending"),
794
+ hasOlderMessages: field("hasOlderMessages"),
795
+ hasLoaded: field("hasLoaded"),
796
+ error: field("error"),
797
+ currentUserId: field("currentUserId"),
798
+ readerIdsFor: (message) => store.readerIdsFor(message),
799
+ loadInitial: () => store.loadInitial(),
800
+ refresh: () => store.refresh(),
801
+ loadOlderMessages: () => store.loadOlderMessages(),
802
+ sendMessage: (input) => store.sendMessage(input),
803
+ markRead: () => store.markRead(),
804
+ updateTyping: (isTyping) => store.updateTyping(isTyping),
419
805
  dispose
420
806
  };
421
807
  }
422
808
 
423
809
  // src/components/message-list.ts
424
- var import_vue3 = require("@lucide/vue");
425
- var import_vue4 = require("vue");
810
+ var import_vue4 = require("@lucide/vue");
811
+ var import_vue5 = require("vue");
426
812
  var appearanceProps = {
427
813
  classNames: { type: Object, default: void 0 },
428
814
  styles: { type: Object, default: void 0 },
@@ -436,33 +822,33 @@ function defaultMedia(media, open, imageLoading) {
436
822
  const tag = open ? "button" : "div";
437
823
  const interactive = open ? { type: "button", onClick: open } : {};
438
824
  if (media.type === "image") {
439
- return (0, import_vue4.h)(tag, { ...interactive, class: "ckui-media-card ckui-media-card--image" }, [
440
- media.url ? (0, import_vue4.h)("img", { src: media.url, alt: media.name ?? "Shared image", loading: imageLoading }) : (0, import_vue4.h)("span", { class: "ckui-media-placeholder" }, [(0, import_vue4.h)(import_vue3.ImageOff, { "aria-hidden": "true" }), " Image unavailable"]),
441
- media.name ? (0, import_vue4.h)("span", { class: "ckui-media-name" }, media.name) : null
825
+ return (0, import_vue5.h)(tag, { ...interactive, class: "ckui-media-card ckui-media-card--image" }, [
826
+ media.url ? (0, import_vue5.h)("img", { src: media.url, alt: media.name ?? "Shared image", loading: imageLoading }) : (0, import_vue5.h)("span", { class: "ckui-media-placeholder" }, [(0, import_vue5.h)(import_vue4.ImageOff, { "aria-hidden": "true" }), " Image unavailable"]),
827
+ media.name ? (0, import_vue5.h)("span", { class: "ckui-media-name" }, media.name) : null
442
828
  ]);
443
829
  }
444
830
  if (media.type === "file") {
445
831
  const size = formatFileSize(media.size);
446
- return (0, import_vue4.h)(tag, { ...interactive, class: "ckui-media-card ckui-media-card--file" }, [
447
- (0, import_vue4.h)(import_vue3.FileText, { "aria-hidden": "true" }),
448
- (0, import_vue4.h)("span", [(0, import_vue4.h)("strong", media.name || "Attachment"), size ? (0, import_vue4.h)("small", size) : null]),
449
- open ? (0, import_vue4.h)(import_vue3.Download, { size: 18, "aria-hidden": "true" }) : null
832
+ return (0, import_vue5.h)(tag, { ...interactive, class: "ckui-media-card ckui-media-card--file" }, [
833
+ (0, import_vue5.h)(import_vue4.FileText, { "aria-hidden": "true" }),
834
+ (0, import_vue5.h)("span", [(0, import_vue5.h)("strong", media.name || "Attachment"), size ? (0, import_vue5.h)("small", size) : null]),
835
+ open ? (0, import_vue5.h)(import_vue4.Download, { size: 18, "aria-hidden": "true" }) : null
450
836
  ]);
451
837
  }
452
838
  if (media.type === "location") {
453
839
  const label = media.name || `${media.metadata.lat}, ${media.metadata.lng}`;
454
- return (0, import_vue4.h)(tag, { ...interactive, class: "ckui-media-card ckui-media-card--location" }, [
455
- (0, import_vue4.h)(import_vue3.MapPin, { "aria-hidden": "true" }),
456
- (0, import_vue4.h)("span", [(0, import_vue4.h)("strong", label), (0, import_vue4.h)("small", `${media.metadata.lat}, ${media.metadata.lng}`)])
840
+ return (0, import_vue5.h)(tag, { ...interactive, class: "ckui-media-card ckui-media-card--location" }, [
841
+ (0, import_vue5.h)(import_vue4.MapPin, { "aria-hidden": "true" }),
842
+ (0, import_vue5.h)("span", [(0, import_vue5.h)("strong", label), (0, import_vue5.h)("small", `${media.metadata.lat}, ${media.metadata.lng}`)])
457
843
  ]);
458
844
  }
459
845
  const contact = media.metadata.email || media.metadata.phone || "Contact details";
460
- return (0, import_vue4.h)(tag, { ...interactive, class: "ckui-media-card ckui-media-card--contact" }, [
461
- (0, import_vue4.h)(import_vue3.ContactRound, { "aria-hidden": "true" }),
462
- (0, import_vue4.h)("span", [(0, import_vue4.h)("strong", media.name || "Shared contact"), (0, import_vue4.h)("small", String(contact))])
846
+ return (0, import_vue5.h)(tag, { ...interactive, class: "ckui-media-card ckui-media-card--contact" }, [
847
+ (0, import_vue5.h)(import_vue4.ContactRound, { "aria-hidden": "true" }),
848
+ (0, import_vue5.h)("span", [(0, import_vue5.h)("strong", media.name || "Shared contact"), (0, import_vue5.h)("small", String(contact))])
463
849
  ]);
464
850
  }
465
- var MessageListView = (0, import_vue4.defineComponent)({
851
+ var MessageListView = (0, import_vue5.defineComponent)({
466
852
  name: "MessageListView",
467
853
  inheritAttrs: false,
468
854
  props: {
@@ -486,11 +872,11 @@ var MessageListView = (0, import_vue4.defineComponent)({
486
872
  },
487
873
  emits: ["load-older", "attachment-click"],
488
874
  setup(props, { attrs, emit, slots }) {
489
- const internalElement = (0, import_vue4.ref)(null);
875
+ const internalElement = (0, import_vue5.ref)(null);
490
876
  let requestInFlight = false;
491
877
  let lastRequestedLength = null;
492
878
  let previousMessageCount = 0;
493
- const participants = (0, import_vue4.computed)(() => new Map(props.conversation.participants.flatMap((participant) => [
879
+ const participants = (0, import_vue5.computed)(() => new Map(props.conversation.participants.flatMap((participant) => [
494
880
  [participant.id, participant],
495
881
  [participant.appUserId, participant]
496
882
  ])));
@@ -512,7 +898,7 @@ var MessageListView = (0, import_vue4.defineComponent)({
512
898
  requestInFlight = false;
513
899
  }
514
900
  };
515
- (0, import_vue4.watch)(() => [props.messages.length, props.hasOlderMessages], async ([count, hasOlder]) => {
901
+ (0, import_vue5.watch)(() => [props.messages.length, props.hasOlderMessages], async ([count, hasOlder]) => {
516
902
  const previous = previousMessageCount;
517
903
  if (count !== previousMessageCount || !hasOlder) lastRequestedLength = null;
518
904
  const appended = count > previous;
@@ -521,7 +907,7 @@ var MessageListView = (0, import_vue4.defineComponent)({
521
907
  if (element && props.reverse && props.stickToBottom && appended) {
522
908
  const distanceFromBottom = element.scrollHeight - element.scrollTop - element.clientHeight;
523
909
  if (previous === 0 || distanceFromBottom < 320) {
524
- await (0, import_vue4.nextTick)();
910
+ await (0, import_vue5.nextTick)();
525
911
  element.scrollTop = element.scrollHeight;
526
912
  }
527
913
  }
@@ -533,7 +919,7 @@ var MessageListView = (0, import_vue4.defineComponent)({
533
919
  const readerIds = isPending ? /* @__PURE__ */ new Set() : props.readersResolver ? props.readersResolver(message) : readerIdsFor(message, props.readAtByUserId);
534
920
  const slotProps = { message, chronologicalIndex: index, isCurrentUser, sender, readerIds };
535
921
  const custom = slots.message?.(slotProps);
536
- if (custom) return (0, import_vue4.h)("div", { key: message.id, role: "listitem" }, custom);
922
+ if (custom) return (0, import_vue5.h)("div", { key: message.id, role: "listitem" }, custom);
537
923
  const currentAppearance = appearance();
538
924
  const messagePart = isCurrentUser ? "outgoingMessage" : "incomingMessage";
539
925
  const mediaNodes = message.media.map((media, mediaIndex) => {
@@ -541,15 +927,15 @@ var MessageListView = (0, import_vue4.defineComponent)({
541
927
  props.onAttachmentClick?.(media, message);
542
928
  } : void 0;
543
929
  const mediaSlotProps = { media, message, isCurrentUser, ...open ? { open } : {} };
544
- return (0, import_vue4.h)("div", {
930
+ return (0, import_vue5.h)("div", {
545
931
  key: media.id ?? `${media.type}-${mediaIndex}`,
546
932
  class: partClass("media", currentAppearance, "ckui-media"),
547
933
  style: partStyle("media", currentAppearance)
548
934
  }, slots.media?.(mediaSlotProps) ?? [defaultMedia(media, open, props.imageLoading)]);
549
935
  });
550
936
  const receiptSlotProps = { message, readerIds };
551
- return (0, import_vue4.h)("div", { key: message.id, role: "listitem" }, [
552
- (0, import_vue4.h)("article", {
937
+ return (0, import_vue5.h)("div", { key: message.id, role: "listitem" }, [
938
+ (0, import_vue5.h)("article", {
553
939
  class: cx(
554
940
  !props.unstyled && "ckui-message-row",
555
941
  isCurrentUser && !props.unstyled && "ckui-message-row--outgoing",
@@ -559,16 +945,16 @@ var MessageListView = (0, import_vue4.defineComponent)({
559
945
  style: [props.styles?.message, props.styles?.[messagePart]],
560
946
  "data-message-id": message.id
561
947
  }, [
562
- (0, import_vue4.h)("div", { class: "ckui-message-bubble" }, [
563
- !isCurrentUser ? (0, import_vue4.h)("strong", { class: "ckui-message-sender" }, sender?.name || message.senderId) : null,
564
- message.text ? (0, import_vue4.h)("div", { class: "ckui-message-text" }, message.text) : null,
948
+ (0, import_vue5.h)("div", { class: "ckui-message-bubble" }, [
949
+ !isCurrentUser ? (0, import_vue5.h)("strong", { class: "ckui-message-sender" }, sender?.name || message.senderId) : null,
950
+ message.text ? (0, import_vue5.h)("div", { class: "ckui-message-text" }, message.text) : null,
565
951
  ...mediaNodes,
566
- (0, import_vue4.h)("span", { class: "ckui-message-time" }, [
952
+ (0, import_vue5.h)("span", { class: "ckui-message-time" }, [
567
953
  isPending ? "Sending\u2026" : props.formatTime(message.createdAt),
568
- isCurrentUser && !isPending ? readerIds.size > 0 ? (0, import_vue4.h)(import_vue3.CheckCheck, { size: 14, "aria-label": "Read" }) : (0, import_vue4.h)(import_vue3.Check, { size: 14, "aria-label": "Delivered" }) : null
954
+ isCurrentUser && !isPending ? readerIds.size > 0 ? (0, import_vue5.h)(import_vue4.CheckCheck, { size: 14, "aria-label": "Read" }) : (0, import_vue5.h)(import_vue4.Check, { size: 14, "aria-label": "Delivered" }) : null
569
955
  ])
570
956
  ]),
571
- isCurrentUser && !isPending ? slots["read-receipt"]?.(receiptSlotProps) ?? (0, import_vue4.h)("div", {
957
+ isCurrentUser && !isPending ? slots["read-receipt"]?.(receiptSlotProps) ?? (0, import_vue5.h)("div", {
572
958
  class: partClass("receipt", currentAppearance, "ckui-read-receipt"),
573
959
  style: partStyle("receipt", currentAppearance)
574
960
  }, readerIds.size > 0 ? `Read by ${readerIds.size}` : "Delivered") : null
@@ -579,34 +965,34 @@ var MessageListView = (0, import_vue4.defineComponent)({
579
965
  const currentAppearance = appearance();
580
966
  const children = [];
581
967
  if (props.isLoadingOlder) {
582
- children.push(slots["loading-older"]?.() ?? (0, import_vue4.h)("div", {
968
+ children.push(slots["loading-older"]?.() ?? (0, import_vue5.h)("div", {
583
969
  class: partClass("loading", currentAppearance, "ckui-inline-state"),
584
970
  style: partStyle("loading", currentAppearance),
585
971
  role: "status"
586
- }, [(0, import_vue4.h)(import_vue3.LoaderCircle, { class: "ckui-spin", "aria-hidden": "true" }), " Loading older messages\u2026"]));
972
+ }, [(0, import_vue5.h)(import_vue4.LoaderCircle, { class: "ckui-spin", "aria-hidden": "true" }), " Loading older messages\u2026"]));
587
973
  }
588
974
  if (props.error) {
589
975
  const retry = props.onLoadOlder ? () => {
590
976
  void requestOlder();
591
977
  } : void 0;
592
- children.push(slots.error?.({ error: props.error, ...retry ? { retry } : {} }) ?? (0, import_vue4.h)("div", {
978
+ children.push(slots.error?.({ error: props.error, ...retry ? { retry } : {} }) ?? (0, import_vue5.h)("div", {
593
979
  class: partClass("error", currentAppearance, "ckui-inline-state ckui-state--error"),
594
980
  style: partStyle("error", currentAppearance),
595
981
  role: "alert"
596
982
  }, [
597
- (0, import_vue4.h)("span", errorMessage(props.error)),
598
- retry ? (0, import_vue4.h)("button", { type: "button", class: "ckui-link-button", onClick: retry }, "Retry") : null
983
+ (0, import_vue5.h)("span", errorMessage(props.error)),
984
+ retry ? (0, import_vue5.h)("button", { type: "button", class: "ckui-link-button", onClick: retry }, "Retry") : null
599
985
  ]));
600
986
  }
601
987
  if (props.messages.length === 0 && !props.isLoadingOlder) {
602
- children.push(slots.empty?.() ?? (0, import_vue4.h)("div", {
988
+ children.push(slots.empty?.() ?? (0, import_vue5.h)("div", {
603
989
  class: partClass("empty", currentAppearance, "ckui-state"),
604
990
  style: partStyle("empty", currentAppearance)
605
- }, [(0, import_vue4.h)(import_vue3.MessageCircle, { "aria-hidden": "true" }), " No messages yet"]));
991
+ }, [(0, import_vue5.h)(import_vue4.MessageCircle, { "aria-hidden": "true" }), " No messages yet"]));
606
992
  } else {
607
993
  children.push(...props.messages.map(renderMessage));
608
994
  }
609
- return (0, import_vue4.h)("div", {
995
+ return (0, import_vue5.h)("div", {
610
996
  ...attrs,
611
997
  ref: (element) => {
612
998
  internalElement.value = element;
@@ -681,7 +1067,7 @@ function typingLabel(userIds, displayNameForUser) {
681
1067
  if (names.length === 2) return `${names[0]} and ${names[1]} are typing\u2026`;
682
1068
  return `${names[0]} and ${names.length - 1} others are typing\u2026`;
683
1069
  }
684
- var ConversationView = (0, import_vue6.defineComponent)({
1070
+ var ConversationView = (0, import_vue7.defineComponent)({
685
1071
  name: "ConversationView",
686
1072
  inheritAttrs: false,
687
1073
  props: viewProps,
@@ -696,8 +1082,8 @@ var ConversationView = (0, import_vue6.defineComponent)({
696
1082
  "update:modelValue"
697
1083
  ],
698
1084
  setup(props, { attrs, emit, slots }) {
699
- const internalDraft = (0, import_vue6.ref)(props.defaultDraft);
700
- const submitting = (0, import_vue6.ref)(false);
1085
+ const internalDraft = (0, import_vue7.ref)(props.defaultDraft);
1086
+ const submitting = (0, import_vue7.ref)(false);
701
1087
  const appearance = () => ({
702
1088
  density: props.density,
703
1089
  unstyled: props.unstyled,
@@ -747,23 +1133,23 @@ var ConversationView = (0, import_vue6.defineComponent)({
747
1133
  ...props.onBack ? { onBack: goBack } : {},
748
1134
  ...props.onRefresh ? { onRefresh: refresh } : {}
749
1135
  };
750
- return slots.header?.(slotProps) ?? (0, import_vue6.h)("header", {
1136
+ return slots.header?.(slotProps) ?? (0, import_vue7.h)("header", {
751
1137
  class: partClass("header", appearance(), "ckui-conversation-header"),
752
1138
  style: partStyle("header", appearance())
753
1139
  }, [
754
- props.onBack ? (0, import_vue6.h)("button", {
1140
+ props.onBack ? (0, import_vue7.h)("button", {
755
1141
  type: "button",
756
1142
  "aria-label": "Back",
757
1143
  onClick: goBack,
758
1144
  class: partClass("button", appearance(), "ckui-icon-button"),
759
1145
  style: partStyle("button", appearance())
760
- }, [(0, import_vue6.h)(import_vue5.ArrowLeft, { size: 20, "aria-hidden": "true" })]) : null,
761
- (0, import_vue6.h)(ConvoKitAvatar, { name: props.conversation.displayTitle, src: props.conversation.imageUrl }),
762
- (0, import_vue6.h)("div", { class: "ckui-conversation-header__body" }, [
763
- (0, import_vue6.h)("strong", props.conversation.displayTitle),
764
- (0, import_vue6.h)("span", `${props.conversation.participants.length} participant${props.conversation.participants.length === 1 ? "" : "s"}`)
1146
+ }, [(0, import_vue7.h)(import_vue6.ArrowLeft, { size: 20, "aria-hidden": "true" })]) : null,
1147
+ (0, import_vue7.h)(ConvoKitAvatar, { name: props.conversation.displayTitle, src: props.conversation.imageUrl }),
1148
+ (0, import_vue7.h)("div", { class: "ckui-conversation-header__body" }, [
1149
+ (0, import_vue7.h)("strong", props.conversation.displayTitle),
1150
+ (0, import_vue7.h)("span", `${props.conversation.participants.length} participant${props.conversation.participants.length === 1 ? "" : "s"}`)
765
1151
  ]),
766
- props.onRefresh ? (0, import_vue6.h)("button", {
1152
+ props.onRefresh ? (0, import_vue7.h)("button", {
767
1153
  type: "button",
768
1154
  "aria-label": "Refresh conversation",
769
1155
  onClick: () => {
@@ -771,12 +1157,12 @@ var ConversationView = (0, import_vue6.defineComponent)({
771
1157
  },
772
1158
  class: partClass("button", appearance(), "ckui-icon-button"),
773
1159
  style: partStyle("button", appearance())
774
- }, [(0, import_vue6.h)(import_vue5.RefreshCw, { size: 18, "aria-hidden": "true" })]) : null
1160
+ }, [(0, import_vue7.h)(import_vue6.RefreshCw, { size: 18, "aria-hidden": "true" })]) : null
775
1161
  ]);
776
1162
  };
777
1163
  const renderTyping = () => {
778
1164
  const slotProps = { userIds: props.typingUserIds, displayNameForUser: nameForUser };
779
- return slots["typing-indicator"]?.(slotProps) ?? (0, import_vue6.h)("div", {
1165
+ return slots["typing-indicator"]?.(slotProps) ?? (0, import_vue7.h)("div", {
780
1166
  class: partClass("typing", appearance(), "ckui-typing"),
781
1167
  style: partStyle("typing", appearance()),
782
1168
  "aria-live": "polite"
@@ -792,7 +1178,7 @@ var ConversationView = (0, import_vue6.defineComponent)({
792
1178
  },
793
1179
  ...props.onAddAttachment ? { addAttachment } : {}
794
1180
  };
795
- return slots.composer?.(slotProps) ?? (0, import_vue6.h)("form", {
1181
+ return slots.composer?.(slotProps) ?? (0, import_vue7.h)("form", {
796
1182
  class: partClass("composer", appearance(), "ckui-composer"),
797
1183
  style: partStyle("composer", appearance()),
798
1184
  onSubmit: (event) => {
@@ -800,14 +1186,14 @@ var ConversationView = (0, import_vue6.defineComponent)({
800
1186
  void submit();
801
1187
  }
802
1188
  }, [
803
- props.onAddAttachment ? (0, import_vue6.h)("button", {
1189
+ props.onAddAttachment ? (0, import_vue7.h)("button", {
804
1190
  type: "button",
805
1191
  "aria-label": "Add attachment",
806
1192
  onClick: addAttachment,
807
1193
  class: partClass("button", appearance(), "ckui-icon-button"),
808
1194
  style: partStyle("button", appearance())
809
- }, [(0, import_vue6.h)(import_vue5.Paperclip, { size: 20, "aria-hidden": "true" })]) : null,
810
- (0, import_vue6.h)("textarea", {
1195
+ }, [(0, import_vue7.h)(import_vue6.Paperclip, { size: 20, "aria-hidden": "true" })]) : null,
1196
+ (0, import_vue7.h)("textarea", {
811
1197
  ...props.composerProps,
812
1198
  rows: props.composerProps?.rows ?? 1,
813
1199
  placeholder: props.composerPlaceholder,
@@ -830,38 +1216,38 @@ var ConversationView = (0, import_vue6.defineComponent)({
830
1216
  }
831
1217
  }
832
1218
  }),
833
- (0, import_vue6.h)("button", {
1219
+ (0, import_vue7.h)("button", {
834
1220
  type: "submit",
835
1221
  "aria-label": "Send message",
836
1222
  disabled: !draft().trim() || props.isSending || submitting.value,
837
1223
  class: partClass("button", appearance(), "ckui-send-button"),
838
1224
  style: partStyle("button", appearance())
839
- }, [props.isSending || submitting.value ? (0, import_vue6.h)(import_vue5.LoaderCircle, { class: "ckui-spin", size: 18, "aria-hidden": "true" }) : (0, import_vue6.h)(import_vue5.Send, { size: 18, "aria-hidden": "true" })])
1225
+ }, [props.isSending || submitting.value ? (0, import_vue7.h)(import_vue6.LoaderCircle, { class: "ckui-spin", size: 18, "aria-hidden": "true" }) : (0, import_vue7.h)(import_vue6.Send, { size: 18, "aria-hidden": "true" })])
840
1226
  ]);
841
1227
  };
842
1228
  return () => {
843
1229
  if (props.isInitialLoading && props.messages.length === 0) {
844
- return (0, import_vue6.h)("div", {
1230
+ return (0, import_vue7.h)("div", {
845
1231
  ...attrs,
846
1232
  class: cx(!props.unstyled && "ckui ckui-conversation", props.classNames?.root, attrs.class),
847
1233
  style: [props.styles?.root, attrs.style],
848
1234
  "data-density": props.density
849
- }, slots.loading?.() ?? (0, import_vue6.h)("div", {
1235
+ }, slots.loading?.() ?? (0, import_vue7.h)("div", {
850
1236
  class: partClass("loading", appearance(), "ckui-state"),
851
1237
  style: partStyle("loading", appearance()),
852
1238
  role: "status"
853
- }, [(0, import_vue6.h)(import_vue5.LoaderCircle, { class: "ckui-spin", "aria-hidden": "true" }), " Loading conversation\u2026"]));
1239
+ }, [(0, import_vue7.h)(import_vue6.LoaderCircle, { class: "ckui-spin", "aria-hidden": "true" }), " Loading conversation\u2026"]));
854
1240
  }
855
1241
  const children = [renderHeader()];
856
1242
  if (props.error) {
857
1243
  const retry = props.onRefresh ? () => {
858
1244
  void refresh();
859
1245
  } : void 0;
860
- children.push(slots.error?.({ error: props.error, ...retry ? { retry } : {} }) ?? (0, import_vue6.h)("div", {
1246
+ children.push(slots.error?.({ error: props.error, ...retry ? { retry } : {} }) ?? (0, import_vue7.h)("div", {
861
1247
  class: partClass("error", appearance(), "ckui-conversation-error"),
862
1248
  style: partStyle("error", appearance()),
863
1249
  role: "alert"
864
- }, [(0, import_vue6.h)("span", errorMessage(props.error)), retry ? (0, import_vue6.h)("button", { type: "button", class: "ckui-link-button", onClick: retry }, "Retry") : null]));
1250
+ }, [(0, import_vue7.h)("span", errorMessage(props.error)), retry ? (0, import_vue7.h)("button", { type: "button", class: "ckui-link-button", onClick: retry }, "Retry") : null]));
865
1251
  }
866
1252
  const messageSlots = {
867
1253
  ...slots.message ? { message: slots.message } : {},
@@ -871,7 +1257,7 @@ var ConversationView = (0, import_vue6.defineComponent)({
871
1257
  ...slots["loading-older"] ? { "loading-older": slots["loading-older"] } : {},
872
1258
  ...slots["message-error"] ? { error: slots["message-error"] } : {}
873
1259
  };
874
- children.push((0, import_vue6.h)(MessageListView, {
1260
+ children.push((0, import_vue7.h)(MessageListView, {
875
1261
  conversation: props.conversation,
876
1262
  messages: props.messages,
877
1263
  currentUserId: props.currentUserId,
@@ -895,7 +1281,7 @@ var ConversationView = (0, import_vue6.defineComponent)({
895
1281
  unstyled: props.unstyled
896
1282
  }, messageSlots));
897
1283
  children.push(renderTyping(), renderComposer());
898
- return (0, import_vue6.h)("section", {
1284
+ return (0, import_vue7.h)("section", {
899
1285
  ...attrs,
900
1286
  class: cx(!props.unstyled && "ckui ckui-conversation", props.classNames?.root, attrs.class),
901
1287
  style: [props.styles?.root, attrs.style],
@@ -905,7 +1291,7 @@ var ConversationView = (0, import_vue6.defineComponent)({
905
1291
  };
906
1292
  }
907
1293
  });
908
- var Conversation = (0, import_vue6.defineComponent)({
1294
+ var Conversation = (0, import_vue7.defineComponent)({
909
1295
  name: "Conversation",
910
1296
  inheritAttrs: false,
911
1297
  props: {
@@ -935,7 +1321,7 @@ var Conversation = (0, import_vue6.defineComponent)({
935
1321
  autoLoad: props.autoLoad
936
1322
  });
937
1323
  expose({ controller });
938
- (0, import_vue6.watchEffect)(() => {
1324
+ (0, import_vue7.watchEffect)(() => {
939
1325
  emit("controller-change", controller);
940
1326
  });
941
1327
  return () => {
@@ -944,16 +1330,16 @@ var Conversation = (0, import_vue6.defineComponent)({
944
1330
  const retry = () => {
945
1331
  void controller.refresh();
946
1332
  };
947
- return (0, import_vue6.h)("div", {
1333
+ return (0, import_vue7.h)("div", {
948
1334
  ...attrs,
949
1335
  class: cx(!props.unstyled && "ckui ckui-conversation", props.classNames?.root, attrs.class),
950
1336
  style: [props.styles?.root, attrs.style],
951
1337
  "data-density": props.density
952
- }, controller.error.value ? slots.error?.({ error: controller.error.value, retry }) ?? (0, import_vue6.h)("div", { class: "ckui-state ckui-state--error", role: "alert" }, [
953
- (0, import_vue6.h)("span", errorMessage(controller.error.value)),
954
- (0, import_vue6.h)("button", { type: "button", class: "ckui-link-button", onClick: retry }, "Try again")
955
- ]) : slots.loading?.() ?? (0, import_vue6.h)("div", { class: "ckui-state", role: "status" }, [
956
- (0, import_vue6.h)(import_vue5.LoaderCircle, { class: "ckui-spin", "aria-hidden": "true" }),
1338
+ }, controller.error.value ? slots.error?.({ error: controller.error.value, retry }) ?? (0, import_vue7.h)("div", { class: "ckui-state ckui-state--error", role: "alert" }, [
1339
+ (0, import_vue7.h)("span", errorMessage(controller.error.value)),
1340
+ (0, import_vue7.h)("button", { type: "button", class: "ckui-link-button", onClick: retry }, "Try again")
1341
+ ]) : slots.loading?.() ?? (0, import_vue7.h)("div", { class: "ckui-state", role: "status" }, [
1342
+ (0, import_vue7.h)(import_vue6.LoaderCircle, { class: "ckui-spin", "aria-hidden": "true" }),
957
1343
  " Loading conversation\u2026"
958
1344
  ]));
959
1345
  }
@@ -982,7 +1368,7 @@ var Conversation = (0, import_vue6.defineComponent)({
982
1368
  error: _error,
983
1369
  ...forwarded
984
1370
  } = props;
985
- return (0, import_vue6.h)(ConversationView, {
1371
+ return (0, import_vue7.h)(ConversationView, {
986
1372
  ...attrs,
987
1373
  ...forwarded,
988
1374
  conversation: loadedConversation,
@@ -1021,110 +1407,270 @@ var Conversation = (0, import_vue6.defineComponent)({
1021
1407
  });
1022
1408
 
1023
1409
  // src/components/conversation-list.ts
1024
- var import_vue8 = require("@lucide/vue");
1025
- var import_vue9 = require("vue");
1410
+ var import_vue9 = require("@lucide/vue");
1411
+ var import_vue10 = require("vue");
1026
1412
 
1027
1413
  // src/composables/use-conversation-list.ts
1028
- var import_vue7 = require("vue");
1029
- function useConversationList(options) {
1030
- const pageSize = options.pageSize ?? 30;
1031
- if (!Number.isInteger(pageSize) || pageSize <= 0) throw new RangeError("pageSize must be a positive integer");
1032
- const source = (0, import_vue7.shallowRef)([]);
1033
- const filter = (0, import_vue7.shallowRef)(options.initialFilter ?? {});
1034
- const isInitialLoading = (0, import_vue7.ref)(false);
1035
- const isLoadingMore = (0, import_vue7.ref)(false);
1036
- const hasMore = (0, import_vue7.ref)(true);
1037
- const hasLoaded = (0, import_vue7.ref)(false);
1038
- const error = (0, import_vue7.shallowRef)(null);
1039
- const conversations = (0, import_vue7.computed)(() => applyConversationFilter(source.value, filter.value));
1040
- let offset = 0;
1041
- let generation = 0;
1042
- let disposed = false;
1043
- const loadUntilVisible = async (activeGeneration, activeFilter) => {
1044
- const visibleBefore = applyConversationFilter(source.value, activeFilter).length;
1045
- while (true) {
1046
- const request = { limit: pageSize, offset, filter: activeFilter };
1047
- const page = options.pageLoader ? await options.pageLoader(request) : await (0, import_vue7.toValue)(options.client).getConversations({
1048
- limit: pageSize,
1049
- offset: request.offset,
1050
- archived: activeFilter.archived ?? false
1414
+ var import_vue8 = require("vue");
1415
+
1416
+ // src/conversation-list-store.ts
1417
+ function blank2(filter) {
1418
+ return { conversations: [], filter, isInitialLoading: false, isLoadingMore: false, hasMore: true, hasLoaded: false, error: null };
1419
+ }
1420
+ var ConversationListStore = class {
1421
+ constructor(options) {
1422
+ this.options = options;
1423
+ this.owner = options.client.sessionIdentity;
1424
+ this.pageSize = options.pageSize ?? 30;
1425
+ if (!Number.isInteger(this.pageSize) || this.pageSize < 1 || this.pageSize > 100) {
1426
+ throw new RangeError("pageSize must be an integer between 1 and 100");
1427
+ }
1428
+ this.state = blank2(options.initialFilter ?? {});
1429
+ }
1430
+ options;
1431
+ owner;
1432
+ pageSize;
1433
+ state;
1434
+ source = [];
1435
+ offset = 0;
1436
+ generation = 0;
1437
+ lifecycleGeneration = 0;
1438
+ disposed = true;
1439
+ lifecycle;
1440
+ inbox;
1441
+ refreshQueued = false;
1442
+ refreshing = false;
1443
+ listeners = /* @__PURE__ */ new Set();
1444
+ getSnapshot = () => this.state;
1445
+ subscribe = (listener) => {
1446
+ this.listeners.add(listener);
1447
+ return () => {
1448
+ this.listeners.delete(listener);
1449
+ };
1450
+ };
1451
+ patch(patch) {
1452
+ this.state = { ...this.state, ...patch };
1453
+ for (const listener of this.listeners) listener();
1454
+ }
1455
+ alive(generation = this.generation) {
1456
+ return !this.disposed && generation === this.generation && this.owner !== null && this.options.client.sessionIdentity === this.owner;
1457
+ }
1458
+ start = (autoLoad = true) => {
1459
+ if (!this.owner || this.options.client.sessionIdentity !== this.owner) return;
1460
+ if (!this.disposed) return;
1461
+ this.disposed = false;
1462
+ const lifecycleGeneration = ++this.lifecycleGeneration;
1463
+ try {
1464
+ const subscription = this.options.client.onConnectionEvent({
1465
+ onEvent: () => {
1466
+ },
1467
+ onSessionEnded: () => {
1468
+ if (!this.disposed && lifecycleGeneration === this.lifecycleGeneration) this.dispose();
1469
+ }
1051
1470
  });
1052
- if (disposed || activeGeneration !== generation) return;
1053
- offset += page.length;
1054
- hasMore.value = page.length === pageSize;
1055
- source.value = mergeConversations(source.value, page);
1056
- if (!hasMore.value || applyConversationFilter(source.value, activeFilter).length > visibleBefore) return;
1471
+ if (this.alive()) this.lifecycle = subscription;
1472
+ else void subscription.unsubscribe().catch(() => void 0);
1473
+ if (!this.alive()) return;
1474
+ const inbox = this.options.client.onInboxChanged(() => {
1475
+ if (this.alive() && lifecycleGeneration === this.lifecycleGeneration) this.queueRefresh();
1476
+ }, (cause) => {
1477
+ if (this.alive() && lifecycleGeneration === this.lifecycleGeneration) {
1478
+ this.patch({ error: cause });
1479
+ this.queueRefresh();
1480
+ }
1481
+ });
1482
+ if (this.alive()) this.inbox = inbox;
1483
+ else void inbox.unsubscribe().catch(() => void 0);
1484
+ if (autoLoad) void this.loadInitial();
1485
+ } catch (cause) {
1486
+ if (this.alive()) this.patch({ error: cause });
1057
1487
  }
1058
1488
  };
1059
- const loadInitialFor = async (activeFilter = filter.value) => {
1060
- const activeGeneration = ++generation;
1061
- source.value = [];
1062
- offset = 0;
1063
- hasMore.value = true;
1064
- isInitialLoading.value = true;
1065
- isLoadingMore.value = false;
1066
- error.value = null;
1489
+ dispose = () => {
1490
+ this.disposed = true;
1491
+ this.generation++;
1492
+ this.lifecycleGeneration++;
1493
+ const subscription = this.lifecycle;
1494
+ this.lifecycle = void 0;
1495
+ if (subscription) void subscription.unsubscribe().catch(() => void 0);
1496
+ if (this.inbox) void this.inbox.unsubscribe().catch(() => void 0);
1497
+ this.inbox = void 0;
1498
+ this.refreshQueued = false;
1499
+ this.refreshing = false;
1500
+ this.source = [];
1501
+ this.offset = 0;
1502
+ this.patch(blank2(this.state.filter));
1503
+ };
1504
+ fail(cause, generation) {
1505
+ if (!this.alive(generation)) return;
1506
+ const status = typeof cause === "object" && cause !== null && "status" in cause ? cause.status : void 0;
1507
+ if (status === 401 || status === 403 || status === 404) {
1508
+ this.source = [];
1509
+ this.offset = 0;
1510
+ this.patch({ conversations: [], hasMore: false });
1511
+ }
1512
+ this.patch({ error: cause });
1513
+ }
1514
+ async loadUntilVisible(generation, filter) {
1515
+ const visibleBefore = applyConversationFilter(this.source, filter).length;
1516
+ while (this.alive(generation)) {
1517
+ const request = { limit: this.pageSize, offset: this.offset, filter };
1518
+ const page = this.options.pageLoader ? await this.options.pageLoader(request) : await this.options.client.getConversations({ limit: this.pageSize, offset: this.offset, archived: filter.archived ?? false });
1519
+ if (!this.alive(generation)) return;
1520
+ if (page.length > this.pageSize || page.some((conversation) => !conversation.id.trim())) {
1521
+ throw new Error("Invalid conversation page");
1522
+ }
1523
+ const merged = mergeConversations(this.source, page);
1524
+ if (page.length === this.pageSize && merged.length === this.source.length) {
1525
+ throw new Error("Conversation pagination did not advance");
1526
+ }
1527
+ this.offset += page.length;
1528
+ this.source = merged;
1529
+ const visible = applyConversationFilter(merged, filter);
1530
+ const hasMore = page.length === this.pageSize;
1531
+ this.patch({ conversations: visible, hasMore });
1532
+ if (!hasMore || visible.length > visibleBefore) return;
1533
+ }
1534
+ }
1535
+ loadInitial = async () => {
1536
+ if (!this.alive()) return;
1537
+ const generation = ++this.generation;
1538
+ this.refreshing = false;
1539
+ this.source = [];
1540
+ this.offset = 0;
1541
+ this.patch({ ...blank2(this.state.filter), isInitialLoading: true });
1067
1542
  try {
1068
- await loadUntilVisible(activeGeneration, activeFilter);
1543
+ await this.loadUntilVisible(generation, this.state.filter);
1069
1544
  } catch (cause) {
1070
- if (!disposed && activeGeneration === generation) error.value = cause;
1545
+ this.fail(cause, generation);
1071
1546
  } finally {
1072
- if (!disposed && activeGeneration === generation) {
1073
- isInitialLoading.value = false;
1074
- hasLoaded.value = true;
1547
+ if (this.alive(generation)) {
1548
+ this.patch({ isInitialLoading: false, hasLoaded: true });
1549
+ this.flushRefresh();
1075
1550
  }
1076
1551
  }
1077
1552
  };
1078
- const loadMore = async () => {
1079
- if (isInitialLoading.value || isLoadingMore.value || !hasMore.value) return;
1080
- const activeGeneration = generation;
1081
- isLoadingMore.value = true;
1082
- error.value = null;
1553
+ queueRefresh() {
1554
+ this.refreshQueued = true;
1555
+ this.flushRefresh();
1556
+ }
1557
+ flushRefresh() {
1558
+ const lifecycle = this.lifecycleGeneration;
1559
+ void Promise.resolve().then(() => {
1560
+ if (!this.alive() || lifecycle !== this.lifecycleGeneration || !this.refreshQueued || this.refreshing || this.state.isInitialLoading || this.state.isLoadingMore) return;
1561
+ this.refreshQueued = false;
1562
+ void this.refresh();
1563
+ });
1564
+ }
1565
+ /** Replace the loaded window atomically, retaining filters and rows during transient failures. */
1566
+ refresh = async () => {
1567
+ if (!this.alive()) return;
1568
+ if (this.refreshing || this.state.isInitialLoading || this.state.isLoadingMore) {
1569
+ this.refreshQueued = true;
1570
+ return;
1571
+ }
1572
+ if (!this.state.hasLoaded) return this.loadInitial();
1573
+ const generation = this.generation;
1574
+ 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
+ this.refreshing = true;
1579
+ this.patch({ error: null });
1083
1580
  try {
1084
- await loadUntilVisible(activeGeneration, filter.value);
1581
+ let rows = [], offset = 0, hasMore = true;
1582
+ while (this.alive(generation)) {
1583
+ 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 });
1584
+ if (!this.alive(generation)) return;
1585
+ if (page.length > this.pageSize || page.some((row) => !row.id.trim())) throw new Error("Invalid conversation page");
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 });
1085
1597
  } catch (cause) {
1086
- if (!disposed && activeGeneration === generation) error.value = cause;
1598
+ this.fail(cause, generation);
1087
1599
  } finally {
1088
- if (!disposed && activeGeneration === generation) isLoadingMore.value = false;
1600
+ if (this.alive(generation)) {
1601
+ this.refreshing = false;
1602
+ this.flushRefresh();
1603
+ }
1089
1604
  }
1090
1605
  };
1091
- const setFilter = async (nextFilter) => {
1092
- const previousArchived = filter.value.archived ?? false;
1093
- filter.value = nextFilter;
1094
- error.value = null;
1095
- if ((nextFilter.archived ?? false) !== previousArchived || !hasLoaded.value) {
1096
- await loadInitialFor(nextFilter);
1097
- } else if (applyConversationFilter(source.value, nextFilter).length === 0 && hasMore.value) {
1098
- await loadMore();
1606
+ loadMore = async () => {
1607
+ if (!this.alive() || this.refreshing || this.state.isInitialLoading || this.state.isLoadingMore || !this.state.hasMore) return;
1608
+ const generation = this.generation;
1609
+ this.patch({ isLoadingMore: true, error: null });
1610
+ try {
1611
+ await this.loadUntilVisible(generation, this.state.filter);
1612
+ } catch (cause) {
1613
+ this.fail(cause, generation);
1614
+ } finally {
1615
+ if (this.alive(generation)) {
1616
+ this.patch({ isLoadingMore: false });
1617
+ this.flushRefresh();
1618
+ }
1099
1619
  }
1100
1620
  };
1101
- const setQuery = (query) => setFilter({ ...filter.value, query });
1621
+ setFilter = async (filter) => {
1622
+ if (!this.alive()) return;
1623
+ const reload = this.options.pageLoader || !this.state.hasLoaded || this.state.isInitialLoading || this.state.isLoadingMore || (filter.archived ?? false) !== (this.state.filter.archived ?? false);
1624
+ this.patch({ filter, conversations: applyConversationFilter(this.source, filter), error: null });
1625
+ if (reload) await this.loadInitial();
1626
+ else if (!this.state.conversations.length && this.state.hasMore) await this.loadMore();
1627
+ };
1628
+ setQuery = (query) => this.setFilter({ ...this.state.filter, query });
1629
+ };
1630
+
1631
+ // src/composables/use-conversation-list.ts
1632
+ function useConversationList(options) {
1633
+ const createStore = () => new ConversationListStore({ ...options, client: (0, import_vue8.toValue)(options.client) });
1634
+ let store = createStore();
1635
+ const snapshot = (0, import_vue8.shallowRef)(store.getSnapshot());
1636
+ let unsubscribe;
1637
+ const stop = (0, import_vue8.watch)(
1638
+ () => [(0, import_vue8.toValue)(options.client), (0, import_vue8.toValue)(options.client).sessionIdentity],
1639
+ () => {
1640
+ store.dispose();
1641
+ unsubscribe?.();
1642
+ store = createStore();
1643
+ snapshot.value = store.getSnapshot();
1644
+ unsubscribe = store.subscribe(() => {
1645
+ snapshot.value = store.getSnapshot();
1646
+ });
1647
+ store.start(options.autoLoad ?? true);
1648
+ },
1649
+ { immediate: true, flush: "sync" }
1650
+ );
1651
+ const field = (key) => (0, import_vue8.computed)(() => snapshot.value[key]);
1102
1652
  const dispose = async () => {
1103
- disposed = true;
1104
- generation += 1;
1653
+ stop();
1654
+ store.dispose();
1655
+ unsubscribe?.();
1656
+ unsubscribe = void 0;
1105
1657
  };
1106
- if (options.autoLoad ?? true) {
1107
- (0, import_vue7.watch)(() => (0, import_vue7.toValue)(options.client), () => {
1108
- disposed = false;
1109
- void loadInitialFor(filter.value);
1110
- }, { immediate: true });
1111
- }
1112
- if ((0, import_vue7.getCurrentScope)()) (0, import_vue7.onScopeDispose)(() => {
1658
+ if ((0, import_vue8.getCurrentScope)()) (0, import_vue8.onScopeDispose)(() => {
1113
1659
  void dispose();
1114
1660
  });
1115
1661
  return {
1116
- conversations,
1117
- filter,
1118
- isInitialLoading,
1119
- isLoadingMore,
1120
- hasMore,
1121
- hasLoaded,
1122
- error,
1123
- loadInitial: loadInitialFor,
1124
- refresh: loadInitialFor,
1125
- loadMore,
1126
- setFilter,
1127
- setQuery,
1662
+ conversations: field("conversations"),
1663
+ filter: field("filter"),
1664
+ isInitialLoading: field("isInitialLoading"),
1665
+ isLoadingMore: field("isLoadingMore"),
1666
+ hasMore: field("hasMore"),
1667
+ hasLoaded: field("hasLoaded"),
1668
+ error: field("error"),
1669
+ loadInitial: () => store.loadInitial(),
1670
+ refresh: () => store.refresh(),
1671
+ loadMore: () => store.loadMore(),
1672
+ setFilter: (filter) => store.setFilter(filter),
1673
+ setQuery: (query) => store.setQuery(query),
1128
1674
  dispose
1129
1675
  };
1130
1676
  }
@@ -1151,7 +1697,7 @@ var listViewProps = {
1151
1697
  paginationThreshold: { type: Number, default: 240 },
1152
1698
  ariaLabel: { type: String, default: "Conversations" }
1153
1699
  };
1154
- var ConversationListView = (0, import_vue9.defineComponent)({
1700
+ var ConversationListView = (0, import_vue10.defineComponent)({
1155
1701
  name: "ConversationListView",
1156
1702
  inheritAttrs: false,
1157
1703
  props: listViewProps,
@@ -1161,7 +1707,7 @@ var ConversationListView = (0, import_vue9.defineComponent)({
1161
1707
  "load-more": () => true
1162
1708
  },
1163
1709
  setup(props, { attrs, emit, slots }) {
1164
- const internalElement = (0, import_vue9.ref)(null);
1710
+ const internalElement = (0, import_vue10.ref)(null);
1165
1711
  let requestInFlight = false;
1166
1712
  let lastRequestedLength = null;
1167
1713
  const appearance = () => ({
@@ -1191,36 +1737,36 @@ var ConversationListView = (0, import_vue9.defineComponent)({
1191
1737
  const renderContent = () => {
1192
1738
  const currentAppearance = appearance();
1193
1739
  if (props.isInitialLoading && props.conversations.length === 0) {
1194
- return slots["initial-loading"]?.() ?? (0, import_vue9.h)("div", {
1740
+ return slots["initial-loading"]?.() ?? (0, import_vue10.h)("div", {
1195
1741
  class: partClass("loading", currentAppearance, "ckui-state"),
1196
1742
  style: partStyle("loading", currentAppearance),
1197
1743
  role: "status"
1198
- }, [(0, import_vue9.h)(import_vue8.LoaderCircle, { class: "ckui-spin", "aria-hidden": "true" }), " Loading conversations\u2026"]);
1744
+ }, [(0, import_vue10.h)(import_vue9.LoaderCircle, { class: "ckui-spin", "aria-hidden": "true" }), " Loading conversations\u2026"]);
1199
1745
  }
1200
1746
  if (props.error && props.conversations.length === 0) {
1201
1747
  const retry = props.onRefresh ? () => {
1202
1748
  void refresh();
1203
1749
  } : void 0;
1204
- return slots.error?.({ error: props.error, ...retry ? { retry } : {} }) ?? (0, import_vue9.h)("div", {
1750
+ return slots.error?.({ error: props.error, ...retry ? { retry } : {} }) ?? (0, import_vue10.h)("div", {
1205
1751
  class: partClass("error", currentAppearance, "ckui-state ckui-state--error"),
1206
1752
  style: partStyle("error", currentAppearance),
1207
1753
  role: "alert"
1208
1754
  }, [
1209
- (0, import_vue9.h)("span", errorMessage(props.error)),
1210
- retry ? (0, import_vue9.h)("button", { type: "button", class: "ckui-link-button", onClick: retry }, "Try again") : null
1755
+ (0, import_vue10.h)("span", errorMessage(props.error)),
1756
+ retry ? (0, import_vue10.h)("button", { type: "button", class: "ckui-link-button", onClick: retry }, "Try again") : null
1211
1757
  ]);
1212
1758
  }
1213
1759
  if (props.conversations.length === 0) {
1214
- return slots.empty?.() ?? (0, import_vue9.h)("div", {
1760
+ return slots.empty?.() ?? (0, import_vue10.h)("div", {
1215
1761
  class: partClass("empty", currentAppearance, "ckui-state"),
1216
1762
  style: partStyle("empty", currentAppearance)
1217
- }, [(0, import_vue9.h)(import_vue8.Inbox, { "aria-hidden": "true" }), " No conversations yet"]);
1763
+ }, [(0, import_vue10.h)(import_vue9.Inbox, { "aria-hidden": "true" }), " No conversations yet"]);
1218
1764
  }
1219
1765
  const children = props.conversations.flatMap((conversation, index) => {
1220
1766
  const selected = props.selectedConversationId === conversation.id;
1221
1767
  const select = () => selectConversation(conversation);
1222
1768
  const slotProps = { conversation, index, selected, select };
1223
- const item = slots["conversation-item"]?.(slotProps) ?? (0, import_vue9.h)("button", {
1769
+ const item = slots["conversation-item"]?.(slotProps) ?? (0, import_vue10.h)("button", {
1224
1770
  type: "button",
1225
1771
  "data-selected": selected || void 0,
1226
1772
  "aria-current": selected ? "true" : void 0,
@@ -1228,54 +1774,54 @@ var ConversationListView = (0, import_vue9.defineComponent)({
1228
1774
  class: partClass("listItem", currentAppearance, "ckui-conversation-item"),
1229
1775
  style: partStyle("listItem", currentAppearance)
1230
1776
  }, [
1231
- (0, import_vue9.h)(ConvoKitAvatar, {
1777
+ (0, import_vue10.h)(ConvoKitAvatar, {
1232
1778
  name: conversation.displayTitle,
1233
1779
  src: conversation.imageUrl,
1234
1780
  class: partClass("avatar", currentAppearance, ""),
1235
1781
  style: partStyle("avatar", currentAppearance)
1236
1782
  }),
1237
- (0, import_vue9.h)("span", { class: "ckui-conversation-item__body" }, [
1238
- (0, import_vue9.h)("strong", conversation.displayTitle),
1239
- (0, import_vue9.h)("span", conversation.participants.map((participant) => participant.name).join(", ") || conversation.description || "No participants")
1783
+ (0, import_vue10.h)("span", { class: "ckui-conversation-item__body" }, [
1784
+ (0, import_vue10.h)("strong", conversation.displayTitle),
1785
+ (0, import_vue10.h)("span", conversation.participants.map((participant) => participant.name).join(", ") || conversation.description || "No participants")
1240
1786
  ]),
1241
- (0, import_vue9.h)(import_vue8.ChevronRight, { size: 18, "aria-hidden": "true" })
1787
+ (0, import_vue10.h)(import_vue9.ChevronRight, { size: 18, "aria-hidden": "true" })
1242
1788
  ]);
1243
- const nodes = [(0, import_vue9.h)("div", { key: conversation.id, role: "listitem" }, [item])];
1789
+ const nodes = [(0, import_vue10.h)("div", { key: conversation.id, role: "listitem" }, [item])];
1244
1790
  if (index < props.conversations.length - 1) {
1245
- nodes.push((0, import_vue9.h)("div", { key: `${conversation.id}-separator` }, slots.separator?.({ index }) ?? (0, import_vue9.h)("div", { class: "ckui-separator" })));
1791
+ nodes.push((0, import_vue10.h)("div", { key: `${conversation.id}-separator` }, slots.separator?.({ index }) ?? (0, import_vue10.h)("div", { class: "ckui-separator" })));
1246
1792
  }
1247
1793
  return nodes;
1248
1794
  });
1249
1795
  if (props.error) {
1250
- children.push(slots.error?.({ error: props.error, retry: requestMore }) ?? (0, import_vue9.h)("div", {
1796
+ children.push(slots.error?.({ error: props.error, retry: requestMore }) ?? (0, import_vue10.h)("div", {
1251
1797
  class: partClass("error", currentAppearance, "ckui-inline-state ckui-state--error"),
1252
1798
  style: partStyle("error", currentAppearance),
1253
1799
  role: "alert"
1254
- }, [(0, import_vue9.h)("span", errorMessage(props.error)), (0, import_vue9.h)("button", { type: "button", class: "ckui-link-button", onClick: () => {
1800
+ }, [(0, import_vue10.h)("span", errorMessage(props.error)), (0, import_vue10.h)("button", { type: "button", class: "ckui-link-button", onClick: () => {
1255
1801
  void requestMore();
1256
1802
  } }, "Retry")]));
1257
1803
  } else if (props.isLoadingMore) {
1258
- children.push(slots["load-more"]?.() ?? (0, import_vue9.h)("div", {
1804
+ children.push(slots["load-more"]?.() ?? (0, import_vue10.h)("div", {
1259
1805
  class: partClass("loading", currentAppearance, "ckui-inline-state"),
1260
1806
  style: partStyle("loading", currentAppearance),
1261
1807
  role: "status"
1262
- }, [(0, import_vue9.h)(import_vue8.LoaderCircle, { class: "ckui-spin", "aria-hidden": "true" }), " Loading more\u2026"]));
1808
+ }, [(0, import_vue10.h)(import_vue9.LoaderCircle, { class: "ckui-spin", "aria-hidden": "true" }), " Loading more\u2026"]));
1263
1809
  }
1264
- return (0, import_vue9.h)("div", {
1810
+ return (0, import_vue10.h)("div", {
1265
1811
  role: "list",
1266
1812
  class: partClass("list", currentAppearance, "ckui-conversation-list__items"),
1267
1813
  style: partStyle("list", currentAppearance)
1268
1814
  }, children);
1269
1815
  };
1270
- return () => (0, import_vue9.h)("div", {
1816
+ return () => (0, import_vue10.h)("div", {
1271
1817
  ...attrs,
1272
1818
  class: cx(!props.unstyled && "ckui ckui-conversation-list", props.classNames?.root, attrs.class),
1273
1819
  style: [props.styles?.root, attrs.style],
1274
1820
  "data-density": props.density
1275
1821
  }, [
1276
- props.onRefresh ? (0, import_vue9.h)("div", { class: "ckui-conversation-list__toolbar" }, [
1277
- (0, import_vue9.h)("span", props.ariaLabel),
1278
- (0, import_vue9.h)("button", {
1822
+ props.onRefresh ? (0, import_vue10.h)("div", { class: "ckui-conversation-list__toolbar" }, [
1823
+ (0, import_vue10.h)("span", props.ariaLabel),
1824
+ (0, import_vue10.h)("button", {
1279
1825
  type: "button",
1280
1826
  "aria-label": "Refresh conversations",
1281
1827
  class: partClass("button", appearance(), "ckui-icon-button"),
@@ -1283,9 +1829,9 @@ var ConversationListView = (0, import_vue9.defineComponent)({
1283
1829
  onClick: () => {
1284
1830
  void refresh();
1285
1831
  }
1286
- }, [(0, import_vue9.h)(import_vue8.RefreshCw, { size: 17, "aria-hidden": "true" })])
1832
+ }, [(0, import_vue10.h)(import_vue9.RefreshCw, { size: 17, "aria-hidden": "true" })])
1287
1833
  ]) : null,
1288
- (0, import_vue9.h)("div", {
1834
+ (0, import_vue10.h)("div", {
1289
1835
  ref: (element) => {
1290
1836
  internalElement.value = element;
1291
1837
  if (props.scrollElement) props.scrollElement.value = element;
@@ -1303,7 +1849,7 @@ var ConversationListView = (0, import_vue9.defineComponent)({
1303
1849
  ]);
1304
1850
  }
1305
1851
  });
1306
- var ConversationList = (0, import_vue9.defineComponent)({
1852
+ var ConversationList = (0, import_vue10.defineComponent)({
1307
1853
  name: "ConversationList",
1308
1854
  inheritAttrs: false,
1309
1855
  props: {
@@ -1326,7 +1872,7 @@ var ConversationList = (0, import_vue9.defineComponent)({
1326
1872
  autoLoad: props.autoLoad
1327
1873
  });
1328
1874
  expose({ controller });
1329
- (0, import_vue9.watchEffect)(() => {
1875
+ (0, import_vue10.watchEffect)(() => {
1330
1876
  emit("controller-change", controller);
1331
1877
  });
1332
1878
  return () => {
@@ -1346,7 +1892,7 @@ var ConversationList = (0, import_vue9.defineComponent)({
1346
1892
  error: _error,
1347
1893
  ...forwarded
1348
1894
  } = props;
1349
- return (0, import_vue9.h)(ConversationListView, {
1895
+ return (0, import_vue10.h)(ConversationListView, {
1350
1896
  ...attrs,
1351
1897
  ...forwarded,
1352
1898
  conversations: controller.conversations.value,
@@ -1365,7 +1911,7 @@ var ConversationList = (0, import_vue9.defineComponent)({
1365
1911
  });
1366
1912
 
1367
1913
  // src/theme.ts
1368
- var import_vue10 = require("vue");
1914
+ var import_vue11 = require("vue");
1369
1915
  var defaultConvoKitTheme = {
1370
1916
  background: "#fafafa",
1371
1917
  surface: "#ffffff",
@@ -1382,8 +1928,8 @@ var defaultConvoKitTheme = {
1382
1928
  fontFamily: 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'
1383
1929
  };
1384
1930
  var themeKey = /* @__PURE__ */ Symbol("ConvoKitTheme");
1385
- var defaultThemeRef = (0, import_vue10.computed)(() => defaultConvoKitTheme);
1386
- var ConvoKitThemeProvider = (0, import_vue10.defineComponent)({
1931
+ var defaultThemeRef = (0, import_vue11.computed)(() => defaultConvoKitTheme);
1932
+ var ConvoKitThemeProvider = (0, import_vue11.defineComponent)({
1387
1933
  name: "ConvoKitThemeProvider",
1388
1934
  inheritAttrs: false,
1389
1935
  props: {
@@ -1392,9 +1938,9 @@ var ConvoKitThemeProvider = (0, import_vue10.defineComponent)({
1392
1938
  style: { type: [String, Array, Object], default: void 0 }
1393
1939
  },
1394
1940
  setup(props, { attrs, slots }) {
1395
- const parent = (0, import_vue10.inject)(themeKey, defaultThemeRef);
1396
- const value = (0, import_vue10.computed)(() => ({ ...parent.value, ...props.theme }));
1397
- (0, import_vue10.provide)(themeKey, value);
1941
+ const parent = (0, import_vue11.inject)(themeKey, defaultThemeRef);
1942
+ const value = (0, import_vue11.computed)(() => ({ ...parent.value, ...props.theme }));
1943
+ (0, import_vue11.provide)(themeKey, value);
1398
1944
  return () => {
1399
1945
  const theme = value.value;
1400
1946
  const variables = {
@@ -1412,7 +1958,7 @@ var ConvoKitThemeProvider = (0, import_vue10.defineComponent)({
1412
1958
  "--ckui-avatar-size": theme.avatarSize,
1413
1959
  "--ckui-font": theme.fontFamily
1414
1960
  };
1415
- return (0, import_vue10.h)("div", {
1961
+ return (0, import_vue11.h)("div", {
1416
1962
  ...attrs,
1417
1963
  class: cx("ckui-theme", props.class, attrs.class),
1418
1964
  style: [variables, props.style, attrs.style]
@@ -1421,7 +1967,7 @@ var ConvoKitThemeProvider = (0, import_vue10.defineComponent)({
1421
1967
  }
1422
1968
  });
1423
1969
  function useConvoKitTheme() {
1424
- return (0, import_vue10.inject)(themeKey, defaultThemeRef);
1970
+ return (0, import_vue11.inject)(themeKey, defaultThemeRef);
1425
1971
  }
1426
1972
  // Annotate the CommonJS export names for ESM import in node:
1427
1973
  0 && (module.exports = {