@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/CHANGELOG.md +29 -0
- package/PARITY.md +8 -1
- package/README.md +63 -1
- package/dist/index.cjs +942 -396
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +35 -33
- package/dist/index.d.ts +35 -33
- package/dist/index.js +826 -296
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
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
|
|
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,
|
|
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,
|
|
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,
|
|
171
|
-
(0,
|
|
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
|
|
182
|
-
var
|
|
195
|
+
var import_vue6 = require("@lucide/vue");
|
|
196
|
+
var import_vue7 = require("vue");
|
|
183
197
|
|
|
184
198
|
// src/composables/use-conversation.ts
|
|
185
|
-
var
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
}
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
isLoadingOlder.
|
|
270
|
-
|
|
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
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
]
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
625
|
+
this.fail(cause, generation, true);
|
|
284
626
|
} finally {
|
|
285
|
-
if (
|
|
286
|
-
|
|
287
|
-
|
|
627
|
+
if (this.alive(generation)) {
|
|
628
|
+
this.patch({ isReconciling: false });
|
|
629
|
+
this.flushRefresh();
|
|
288
630
|
}
|
|
289
631
|
}
|
|
290
632
|
};
|
|
291
|
-
|
|
292
|
-
if (isInitialLoading.
|
|
293
|
-
const
|
|
294
|
-
const
|
|
295
|
-
const
|
|
296
|
-
|
|
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
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
|
|
649
|
+
this.fail(cause, generation, true);
|
|
309
650
|
} finally {
|
|
310
|
-
if (
|
|
651
|
+
if (this.alive(generation)) {
|
|
652
|
+
this.patch({ isLoadingOlder: false });
|
|
653
|
+
this.flushRefresh();
|
|
654
|
+
}
|
|
311
655
|
}
|
|
312
656
|
};
|
|
313
|
-
|
|
314
|
-
if (
|
|
315
|
-
|
|
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
|
|
322
|
-
|
|
323
|
-
|
|
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 (
|
|
682
|
+
if (this.alive(generation) && revision === this.typingRevision) {
|
|
683
|
+
this.sentTyping = false;
|
|
684
|
+
this.fail(cause, generation);
|
|
685
|
+
}
|
|
327
686
|
}
|
|
328
687
|
};
|
|
329
|
-
|
|
330
|
-
const
|
|
331
|
-
if (!
|
|
332
|
-
|
|
333
|
-
const
|
|
334
|
-
|
|
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
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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
|
|
350
|
-
conversationId:
|
|
351
|
-
|
|
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
|
-
|
|
355
|
-
if (!
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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 (
|
|
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
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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 (
|
|
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
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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
|
|
425
|
-
var
|
|
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,
|
|
440
|
-
media.url ? (0,
|
|
441
|
-
media.name ? (0,
|
|
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,
|
|
447
|
-
(0,
|
|
448
|
-
(0,
|
|
449
|
-
open ? (0,
|
|
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,
|
|
455
|
-
(0,
|
|
456
|
-
(0,
|
|
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,
|
|
461
|
-
(0,
|
|
462
|
-
(0,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
552
|
-
(0,
|
|
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,
|
|
563
|
-
!isCurrentUser ? (0,
|
|
564
|
-
message.text ? (0,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
598
|
-
retry ? (0,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
700
|
-
const submitting = (0,
|
|
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,
|
|
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,
|
|
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,
|
|
761
|
-
(0,
|
|
762
|
-
(0,
|
|
763
|
-
(0,
|
|
764
|
-
(0,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
810
|
-
(0,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
953
|
-
(0,
|
|
954
|
-
(0,
|
|
955
|
-
]) : slots.loading?.() ?? (0,
|
|
956
|
-
(0,
|
|
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,
|
|
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
|
|
1025
|
-
var
|
|
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
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
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 (
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
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
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
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(
|
|
1543
|
+
await this.loadUntilVisible(generation, this.state.filter);
|
|
1069
1544
|
} catch (cause) {
|
|
1070
|
-
|
|
1545
|
+
this.fail(cause, generation);
|
|
1071
1546
|
} finally {
|
|
1072
|
-
if (
|
|
1073
|
-
|
|
1074
|
-
|
|
1547
|
+
if (this.alive(generation)) {
|
|
1548
|
+
this.patch({ isInitialLoading: false, hasLoaded: true });
|
|
1549
|
+
this.flushRefresh();
|
|
1075
1550
|
}
|
|
1076
1551
|
}
|
|
1077
1552
|
};
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1598
|
+
this.fail(cause, generation);
|
|
1087
1599
|
} finally {
|
|
1088
|
-
if (
|
|
1600
|
+
if (this.alive(generation)) {
|
|
1601
|
+
this.refreshing = false;
|
|
1602
|
+
this.flushRefresh();
|
|
1603
|
+
}
|
|
1089
1604
|
}
|
|
1090
1605
|
};
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
await
|
|
1097
|
-
}
|
|
1098
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1104
|
-
|
|
1653
|
+
stop();
|
|
1654
|
+
store.dispose();
|
|
1655
|
+
unsubscribe?.();
|
|
1656
|
+
unsubscribe = void 0;
|
|
1105
1657
|
};
|
|
1106
|
-
if (
|
|
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:
|
|
1124
|
-
refresh:
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
1210
|
-
retry ? (0,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
1238
|
-
(0,
|
|
1239
|
-
(0,
|
|
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,
|
|
1787
|
+
(0, import_vue10.h)(import_vue9.ChevronRight, { size: 18, "aria-hidden": "true" })
|
|
1242
1788
|
]);
|
|
1243
|
-
const nodes = [(0,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
1808
|
+
}, [(0, import_vue10.h)(import_vue9.LoaderCircle, { class: "ckui-spin", "aria-hidden": "true" }), " Loading more\u2026"]));
|
|
1263
1809
|
}
|
|
1264
|
-
return (0,
|
|
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,
|
|
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,
|
|
1277
|
-
(0,
|
|
1278
|
-
(0,
|
|
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,
|
|
1832
|
+
}, [(0, import_vue10.h)(import_vue9.RefreshCw, { size: 17, "aria-hidden": "true" })])
|
|
1287
1833
|
]) : null,
|
|
1288
|
-
(0,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
1386
|
-
var ConvoKitThemeProvider = (0,
|
|
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,
|
|
1396
|
-
const value = (0,
|
|
1397
|
-
(0,
|
|
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,
|
|
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,
|
|
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 = {
|