@convokitapp/vue-ui 0.2.2 → 0.3.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 +19 -0
- package/PARITY.md +8 -1
- package/README.md +55 -1
- package/dist/index.cjs +827 -398
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +32 -28
- package/dist/index.d.ts +32 -28
- package/dist/index.js +711 -298
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
// src/client.ts
|
|
2
2
|
function createConvoKitUiClient(client) {
|
|
3
3
|
return {
|
|
4
|
+
get sessionIdentity() {
|
|
5
|
+
return client.connected ? client.realtime : null;
|
|
6
|
+
},
|
|
7
|
+
onConnectionEvent: (handlers) => client.realtime.onConnectionEvent(handlers),
|
|
8
|
+
onMessageDeleted: (conversationId, handler, onError) => client.realtime.onMessageDeleted(conversationId, {
|
|
9
|
+
onEvent: handler,
|
|
10
|
+
...onError ? { onError } : {}
|
|
11
|
+
}),
|
|
4
12
|
get currentUserId() {
|
|
5
|
-
return client.currentUserId;
|
|
13
|
+
return client.connected ? client.currentUserId : "";
|
|
6
14
|
},
|
|
7
15
|
getConversations: (options) => client.getConversations(options),
|
|
8
16
|
getConversation: (conversationId) => client.getConversation(conversationId),
|
|
9
17
|
getMessages: (options) => client.getMessages(options),
|
|
18
|
+
getMessage: (id) => client.getMessage(id),
|
|
10
19
|
sendMessage: (input) => client.sendMessage(input),
|
|
11
20
|
markConversationRead: (conversationId) => client.markConversationRead(conversationId),
|
|
12
21
|
sendTyping: (input) => client.sendTyping(input),
|
|
@@ -31,12 +40,13 @@ import { defineComponent, h } from "vue";
|
|
|
31
40
|
|
|
32
41
|
// src/utils.ts
|
|
33
42
|
import { clsx } from "clsx";
|
|
43
|
+
import { normalizeClass } from "vue";
|
|
34
44
|
var pendingMessageIdPrefix = "convokit-pending-";
|
|
35
45
|
function isConvoKitPendingMessage(message) {
|
|
36
46
|
return message.id.startsWith(pendingMessageIdPrefix);
|
|
37
47
|
}
|
|
38
48
|
function cx(...values) {
|
|
39
|
-
return clsx(values);
|
|
49
|
+
return clsx(values.map(normalizeClass));
|
|
40
50
|
}
|
|
41
51
|
function requestedParticipantIds(filter) {
|
|
42
52
|
const values = filter.participantIds ?? [];
|
|
@@ -137,253 +147,582 @@ import { ArrowLeft, LoaderCircle as LoaderCircle2, Paperclip, RefreshCw, Send }
|
|
|
137
147
|
import {
|
|
138
148
|
defineComponent as defineComponent3,
|
|
139
149
|
h as h3,
|
|
140
|
-
ref as
|
|
150
|
+
ref as ref2,
|
|
141
151
|
watchEffect
|
|
142
152
|
} from "vue";
|
|
143
153
|
|
|
144
154
|
// src/composables/use-conversation.ts
|
|
145
|
-
import {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
function
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
let typingTimer = null;
|
|
175
|
-
let subscriptions = [];
|
|
176
|
-
const pendingIds = /* @__PURE__ */ new Set();
|
|
177
|
-
let pendingSequence = 0;
|
|
178
|
-
const unsubscribe = async () => {
|
|
179
|
-
const active = subscriptions;
|
|
180
|
-
subscriptions = [];
|
|
181
|
-
await Promise.all(active.map((subscription) => subscription.unsubscribe()));
|
|
155
|
+
import { computed, getCurrentScope, onScopeDispose, shallowRef, toValue, watch } from "vue";
|
|
156
|
+
|
|
157
|
+
// src/conversation-store.ts
|
|
158
|
+
function version(message) {
|
|
159
|
+
return message.updatedAt?.getTime() ?? message.createdAt.getTime();
|
|
160
|
+
}
|
|
161
|
+
function hasContent(message) {
|
|
162
|
+
return !!message.text?.trim() || message.media.length > 0;
|
|
163
|
+
}
|
|
164
|
+
function newest(current, incoming, incomingComplete = true) {
|
|
165
|
+
return version(current) > version(incoming) || !incomingComplete && version(current) === version(incoming) ? current : incoming;
|
|
166
|
+
}
|
|
167
|
+
function compare(left, right) {
|
|
168
|
+
return left.createdAt.getTime() - right.createdAt.getTime() || (left.id < right.id ? -1 : left.id > right.id ? 1 : 0);
|
|
169
|
+
}
|
|
170
|
+
function blank(currentUserId = "") {
|
|
171
|
+
return {
|
|
172
|
+
conversation: null,
|
|
173
|
+
messages: [],
|
|
174
|
+
typingUserIds: /* @__PURE__ */ new Set(),
|
|
175
|
+
readAtByUserId: /* @__PURE__ */ new Map(),
|
|
176
|
+
isInitialLoading: false,
|
|
177
|
+
isLoadingOlder: false,
|
|
178
|
+
isReconciling: false,
|
|
179
|
+
isSending: false,
|
|
180
|
+
hasOlderMessages: true,
|
|
181
|
+
hasLoaded: false,
|
|
182
|
+
error: null,
|
|
183
|
+
currentUserId
|
|
182
184
|
};
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
185
|
+
}
|
|
186
|
+
var ConversationStore = class {
|
|
187
|
+
constructor(options) {
|
|
188
|
+
this.options = options;
|
|
189
|
+
this.client = options.client;
|
|
190
|
+
this.room = options.conversationId.trim();
|
|
191
|
+
this.pageSize = options.messagePageSize ?? 30;
|
|
192
|
+
this.typingTimeout = options.typingTimeoutMs ?? 3e3;
|
|
193
|
+
if (!this.room) throw new TypeError("conversationId is required");
|
|
194
|
+
if (!Number.isInteger(this.pageSize) || this.pageSize < 1 || this.pageSize > 100) {
|
|
195
|
+
throw new RangeError("messagePageSize must be an integer between 1 and 100");
|
|
196
|
+
}
|
|
197
|
+
if (!Number.isFinite(this.typingTimeout) || this.typingTimeout < 0) {
|
|
198
|
+
throw new RangeError("typingTimeoutMs must be non-negative");
|
|
199
|
+
}
|
|
200
|
+
this.owner = this.client.sessionIdentity;
|
|
201
|
+
this.user = this.owner ? this.client.currentUserId : "";
|
|
202
|
+
this.state = blank(this.user);
|
|
203
|
+
}
|
|
204
|
+
options;
|
|
205
|
+
client;
|
|
206
|
+
room;
|
|
207
|
+
owner;
|
|
208
|
+
user;
|
|
209
|
+
pageSize;
|
|
210
|
+
typingTimeout;
|
|
211
|
+
state;
|
|
212
|
+
listeners = /* @__PURE__ */ new Set();
|
|
213
|
+
subscriptions = [];
|
|
214
|
+
disposed = true;
|
|
215
|
+
generation = 0;
|
|
216
|
+
cursor;
|
|
217
|
+
revision = 0;
|
|
218
|
+
changes = /* @__PURE__ */ new Map();
|
|
219
|
+
hydrations = /* @__PURE__ */ new Map();
|
|
220
|
+
hydrationPool = { running: /* @__PURE__ */ new Set(), queued: /* @__PURE__ */ new Map() };
|
|
221
|
+
// Keep tombstones until an explicit reload/session change, including across refreshes.
|
|
222
|
+
deleted = /* @__PURE__ */ new Set();
|
|
223
|
+
sendRevision;
|
|
224
|
+
pendingSequence = 0;
|
|
225
|
+
refreshQueued = false;
|
|
226
|
+
typingTimers = /* @__PURE__ */ new Map();
|
|
227
|
+
ownTypingTimer;
|
|
228
|
+
sentTyping = false;
|
|
229
|
+
typingRevision = 0;
|
|
230
|
+
lastTypingSentAt = -Infinity;
|
|
231
|
+
getSnapshot = () => this.state;
|
|
232
|
+
subscribe = (listener) => {
|
|
233
|
+
this.listeners.add(listener);
|
|
234
|
+
return () => {
|
|
235
|
+
this.listeners.delete(listener);
|
|
236
|
+
};
|
|
237
|
+
};
|
|
238
|
+
patch(patch) {
|
|
239
|
+
this.state = { ...this.state, ...patch };
|
|
240
|
+
for (const listener of this.listeners) listener();
|
|
241
|
+
}
|
|
242
|
+
alive(generation = this.generation) {
|
|
243
|
+
return !this.disposed && generation === this.generation && this.owner !== null && this.client.sessionIdentity === this.owner;
|
|
244
|
+
}
|
|
245
|
+
start = (autoLoad = true) => {
|
|
246
|
+
if (this.owner === null || this.client.sessionIdentity !== this.owner) return;
|
|
247
|
+
this.disposed = false;
|
|
248
|
+
this.patch({ currentUserId: this.user });
|
|
249
|
+
if (autoLoad) void this.loadInitial();
|
|
250
|
+
else {
|
|
251
|
+
try {
|
|
252
|
+
this.attach(this.generation, false);
|
|
253
|
+
} catch {
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
detach() {
|
|
258
|
+
const active = this.subscriptions;
|
|
259
|
+
this.subscriptions = [];
|
|
260
|
+
for (const subscription of active) void subscription.unsubscribe().catch(() => void 0);
|
|
261
|
+
}
|
|
262
|
+
clearTyping() {
|
|
263
|
+
for (const timer of this.typingTimers.values()) clearTimeout(timer);
|
|
264
|
+
this.typingTimers.clear();
|
|
265
|
+
clearTimeout(this.ownTypingTimer);
|
|
266
|
+
this.ownTypingTimer = void 0;
|
|
267
|
+
this.sentTyping = false;
|
|
268
|
+
this.typingRevision++;
|
|
269
|
+
this.lastTypingSentAt = -Infinity;
|
|
270
|
+
this.patch({ typingUserIds: /* @__PURE__ */ new Set() });
|
|
271
|
+
}
|
|
272
|
+
clear() {
|
|
273
|
+
this.generation++;
|
|
274
|
+
this.detach();
|
|
275
|
+
this.clearTyping();
|
|
276
|
+
this.cursor = void 0;
|
|
277
|
+
this.changes.clear();
|
|
278
|
+
this.hydrations.clear();
|
|
279
|
+
this.hydrationPool.queued.clear();
|
|
280
|
+
this.deleted.clear();
|
|
281
|
+
this.sendRevision = void 0;
|
|
282
|
+
this.refreshQueued = false;
|
|
283
|
+
}
|
|
284
|
+
dispose = () => {
|
|
285
|
+
if (this.alive() && this.sentTyping) {
|
|
286
|
+
void this.client.sendTyping({ conversationId: this.room, isTyping: false }).catch(() => void 0);
|
|
192
287
|
}
|
|
288
|
+
this.disposed = true;
|
|
289
|
+
this.clear();
|
|
290
|
+
this.patch(blank());
|
|
193
291
|
};
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
292
|
+
fail(cause, generation, history = false) {
|
|
293
|
+
if (!this.alive(generation)) return;
|
|
294
|
+
const status = typeof cause === "object" && cause !== null && "status" in cause ? cause.status : void 0;
|
|
295
|
+
if (history && (status === 401 || status === 403 || status === 404)) {
|
|
296
|
+
this.clear();
|
|
297
|
+
this.patch({ ...blank(this.user), error: cause, hasLoaded: true, hasOlderMessages: false });
|
|
298
|
+
} else this.patch({ error: cause });
|
|
299
|
+
}
|
|
300
|
+
attach(generation, data = true) {
|
|
301
|
+
const report = (cause) => this.fail(cause, generation);
|
|
302
|
+
const add = (create) => {
|
|
303
|
+
if (!this.alive(generation)) return;
|
|
304
|
+
const subscription = create();
|
|
305
|
+
if (this.alive(generation)) this.subscriptions.push(subscription);
|
|
306
|
+
else void subscription.unsubscribe().catch(() => void 0);
|
|
197
307
|
};
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
client.
|
|
214
|
-
if (
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
}, report)
|
|
220
|
-
|
|
308
|
+
try {
|
|
309
|
+
add(() => this.client.onConnectionEvent({
|
|
310
|
+
onEvent: ({ topic, status }) => {
|
|
311
|
+
if (!data || !this.alive(generation) || topic !== `messages:${this.room}` && topic !== `conversation:${this.room}`) return;
|
|
312
|
+
if (status === "SUBSCRIBED") this.queueRefresh();
|
|
313
|
+
else this.clearTyping();
|
|
314
|
+
},
|
|
315
|
+
onSessionEnded: () => {
|
|
316
|
+
if (this.disposed || generation !== this.generation) return;
|
|
317
|
+
this.dispose();
|
|
318
|
+
},
|
|
319
|
+
onError: report
|
|
320
|
+
}));
|
|
321
|
+
if (!data) return;
|
|
322
|
+
add(() => this.client.onMessage(this.room, (event) => this.onMessage(event, generation), report));
|
|
323
|
+
add(() => this.client.onMessageDeleted(this.room, ({ id, conversationId }) => {
|
|
324
|
+
if (!this.alive(generation) || conversationId !== this.room || !id.trim()) return;
|
|
325
|
+
this.removeMessage(id);
|
|
326
|
+
}, report));
|
|
327
|
+
add(() => this.client.onReadReceipt(this.room, ({ userId, readAt }) => {
|
|
328
|
+
if (this.alive(generation)) this.mergeReads([[userId, readAt]]);
|
|
329
|
+
}, report));
|
|
330
|
+
add(() => this.client.onTyping(this.room, ({ userId, isTyping }) => {
|
|
331
|
+
if (!this.alive(generation) || !userId.trim() || userId === this.user) return;
|
|
332
|
+
clearTimeout(this.typingTimers.get(userId));
|
|
333
|
+
this.typingTimers.delete(userId);
|
|
334
|
+
const next = new Set(this.state.typingUserIds);
|
|
335
|
+
if (isTyping) {
|
|
336
|
+
next.add(userId);
|
|
337
|
+
this.typingTimers.set(userId, setTimeout(() => {
|
|
338
|
+
this.typingTimers.delete(userId);
|
|
339
|
+
if (!this.alive(generation)) return;
|
|
340
|
+
const remaining = new Set(this.state.typingUserIds);
|
|
341
|
+
remaining.delete(userId);
|
|
342
|
+
this.patch({ typingUserIds: remaining });
|
|
343
|
+
}, this.typingTimeout));
|
|
344
|
+
} else next.delete(userId);
|
|
345
|
+
this.patch({ typingUserIds: next });
|
|
346
|
+
}, report));
|
|
347
|
+
} catch (cause) {
|
|
348
|
+
this.detach();
|
|
349
|
+
this.fail(cause, generation);
|
|
350
|
+
throw cause;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
validMessage(message) {
|
|
354
|
+
return message.conversationId === this.room && !!message.id.trim() && !!message.senderId.trim() && !isConvoKitPendingMessage(message) && Number.isFinite(message.createdAt.getTime()) && Number.isFinite(version(message));
|
|
355
|
+
}
|
|
356
|
+
onMessage(event, generation) {
|
|
357
|
+
const { message, type } = event;
|
|
358
|
+
if (!this.alive(generation) || type !== "insert" && type !== "update" || !this.validMessage(message) || this.deleted.has(message.id)) return;
|
|
359
|
+
const existing = this.state.messages.find((item) => item.id === message.id);
|
|
360
|
+
const known = existing ?? this.changes.get(message.id)?.message;
|
|
361
|
+
if (known && version(message) < version(known)) return;
|
|
362
|
+
const insert = type === "insert" || this.changes.get(message.id)?.insert === true;
|
|
363
|
+
if (!existing && !insert && type === "update" && !this.state.isInitialLoading && !this.state.isLoadingOlder && !this.state.isReconciling) return;
|
|
364
|
+
const revision = ++this.revision;
|
|
365
|
+
const provisional = existing && !message.media.length ? { ...message, media: existing.media } : message;
|
|
366
|
+
this.record(provisional, insert, revision, false);
|
|
367
|
+
const job = { revision, generation, message, insert };
|
|
368
|
+
this.hydrations.set(message.id, job);
|
|
369
|
+
this.hydrationPool.queued.set(message.id, job);
|
|
370
|
+
this.drainHydration();
|
|
371
|
+
if (type === "insert" && !existing && message.senderId !== this.user && (this.options.markReadOnReceive ?? true)) {
|
|
372
|
+
void this.markRead();
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
record(message, insert, revision, complete) {
|
|
376
|
+
const existing = this.state.messages.find((item) => item.id === message.id);
|
|
377
|
+
if (existing && version(existing) > version(message)) return;
|
|
378
|
+
this.changes.set(message.id, { revision, message, insert, complete });
|
|
379
|
+
if (existing || insert && hasContent(message)) this.patch({ messages: mergeMessages(this.state.messages, [message]) });
|
|
380
|
+
}
|
|
381
|
+
currentHydration(job) {
|
|
382
|
+
return this.alive(job.generation) && !this.deleted.has(job.message.id) && this.hydrations.get(job.message.id) === job;
|
|
383
|
+
}
|
|
384
|
+
removeMessage(id) {
|
|
385
|
+
this.deleted.add(id);
|
|
386
|
+
this.changes.delete(id);
|
|
387
|
+
this.hydrations.delete(id);
|
|
388
|
+
this.hydrationPool.queued.delete(id);
|
|
389
|
+
this.patch({ messages: this.state.messages.filter((message) => message.id !== id) });
|
|
390
|
+
}
|
|
391
|
+
drainHydration() {
|
|
392
|
+
const pool = this.hydrationPool;
|
|
393
|
+
for (const [id, job] of pool.queued) {
|
|
394
|
+
if (pool.running.size >= 8) break;
|
|
395
|
+
if (pool.running.has(id)) continue;
|
|
396
|
+
pool.queued.delete(id);
|
|
397
|
+
if (!this.currentHydration(job)) continue;
|
|
398
|
+
pool.running.add(id);
|
|
399
|
+
void this.hydrate(job, pool);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
async hydrate(job, pool) {
|
|
403
|
+
const id = job.message.id;
|
|
404
|
+
try {
|
|
405
|
+
if (!this.currentHydration(job)) return;
|
|
406
|
+
const full = await this.client.getMessage(id);
|
|
407
|
+
if (!this.currentHydration(job)) return;
|
|
408
|
+
if (!this.validMessage(full) || full.id !== id || full.senderId !== job.message.senderId || version(full) < version(job.message)) {
|
|
409
|
+
throw new Error("Complete message response does not match the observed resource/revision");
|
|
410
|
+
}
|
|
411
|
+
this.record(full, job.insert, job.revision, true);
|
|
412
|
+
} catch (cause) {
|
|
413
|
+
if (!this.currentHydration(job)) return;
|
|
414
|
+
const status = typeof cause === "object" && cause !== null && "status" in cause ? cause.status : void 0;
|
|
415
|
+
if (status === 404) this.removeMessage(id);
|
|
416
|
+
this.fail(cause, job.generation);
|
|
417
|
+
this.queueRefresh();
|
|
418
|
+
} finally {
|
|
419
|
+
if (this.hydrations.get(id) === job) this.hydrations.delete(id);
|
|
420
|
+
pool.running.delete(id);
|
|
421
|
+
queueMicrotask(() => {
|
|
422
|
+
if (this.hydrationPool === pool) this.drainHydration();
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
mergeReads(entries) {
|
|
427
|
+
const next = new Map(this.state.readAtByUserId);
|
|
428
|
+
for (const [userId, readAt] of entries) {
|
|
429
|
+
if (!userId.trim() || !Number.isFinite(readAt.getTime())) continue;
|
|
430
|
+
if (readAt.getTime() > (next.get(userId)?.getTime() ?? -Infinity)) next.set(userId, readAt);
|
|
431
|
+
}
|
|
432
|
+
this.patch({ readAtByUserId: next });
|
|
433
|
+
}
|
|
434
|
+
validatePage(page, before) {
|
|
435
|
+
if (page.length > this.pageSize) throw new Error("Message page exceeds the requested limit");
|
|
436
|
+
let previous = before;
|
|
437
|
+
for (const message of page) {
|
|
438
|
+
if (!this.validMessage(message) || previous && compare(message, previous) >= 0) {
|
|
439
|
+
throw new Error("Message history must contain distinct, room-scoped rows in newest-first cursor order");
|
|
440
|
+
}
|
|
441
|
+
previous = message;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
fetchPage(before) {
|
|
445
|
+
return this.client.getMessages({
|
|
446
|
+
conversationId: this.room,
|
|
447
|
+
limit: this.pageSize,
|
|
448
|
+
...before ? { beforeCreatedAt: before.createdAt, beforeId: before.id } : {}
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
overlay(rows, revision) {
|
|
452
|
+
const byId = new Map(rows.filter((message) => !this.deleted.has(message.id)).map((message) => [message.id, message]));
|
|
453
|
+
for (const [id, change] of this.changes) {
|
|
454
|
+
if (change.revision > revision && !this.deleted.has(id) && (change.insert || byId.has(id))) {
|
|
455
|
+
const current = byId.get(id);
|
|
456
|
+
if (current || change.complete || hasContent(change.message)) {
|
|
457
|
+
byId.set(id, current ? newest(current, change.message, change.complete) : change.message);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
for (const message of this.state.messages) {
|
|
462
|
+
if (isConvoKitPendingMessage(message)) byId.set(message.id, message);
|
|
463
|
+
}
|
|
464
|
+
return mergeMessages([], [...byId.values()]);
|
|
465
|
+
}
|
|
466
|
+
prune(revision) {
|
|
467
|
+
const safeRevision = Math.min(revision, this.sendRevision ?? Infinity);
|
|
468
|
+
for (const [id, change] of this.changes) if (change.revision <= safeRevision) this.changes.delete(id);
|
|
469
|
+
for (const [id, job] of this.hydrations) if (job.revision <= revision) {
|
|
470
|
+
this.hydrations.delete(id);
|
|
471
|
+
this.hydrationPool.queued.delete(id);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
loadInitial = async () => {
|
|
475
|
+
if (!this.alive()) return;
|
|
476
|
+
this.clear();
|
|
477
|
+
const generation = this.generation;
|
|
478
|
+
this.patch({ ...blank(this.user), isInitialLoading: true });
|
|
479
|
+
const revision = this.revision;
|
|
480
|
+
try {
|
|
481
|
+
this.attach(generation);
|
|
482
|
+
if (!this.alive(generation)) return;
|
|
483
|
+
const [conversation, page] = await Promise.all([this.client.getConversation(this.room), this.fetchPage()]);
|
|
484
|
+
if (!this.alive(generation)) return;
|
|
485
|
+
if (conversation.id !== this.room) throw new Error("Conversation response belongs to a different room");
|
|
486
|
+
this.validatePage(page);
|
|
487
|
+
this.cursor = page.at(-1);
|
|
488
|
+
this.patch({ conversation, messages: this.overlay(page, revision), hasOlderMessages: page.length === this.pageSize });
|
|
489
|
+
this.mergeReads(conversation.participants.flatMap((participant) => participant.lastReadAt ? [[participant.appUserId, participant.lastReadAt]] : []));
|
|
490
|
+
this.prune(revision);
|
|
491
|
+
if (this.options.markReadOnLoad ?? true) await this.markRead();
|
|
492
|
+
} catch (cause) {
|
|
493
|
+
this.fail(cause, generation, true);
|
|
494
|
+
} finally {
|
|
495
|
+
if (this.alive(generation)) {
|
|
496
|
+
this.patch({ isInitialLoading: false, hasLoaded: true });
|
|
497
|
+
this.flushRefresh();
|
|
498
|
+
}
|
|
499
|
+
}
|
|
221
500
|
};
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
isLoadingOlder.
|
|
238
|
-
|
|
501
|
+
queueRefresh() {
|
|
502
|
+
this.refreshQueued = true;
|
|
503
|
+
this.flushRefresh();
|
|
504
|
+
}
|
|
505
|
+
flushRefresh() {
|
|
506
|
+
const generation = this.generation;
|
|
507
|
+
void Promise.resolve().then(() => {
|
|
508
|
+
if (!this.alive(generation) || !this.refreshQueued || this.state.isInitialLoading || this.state.isLoadingOlder || this.state.isReconciling) return;
|
|
509
|
+
this.refreshQueued = false;
|
|
510
|
+
void this.refresh();
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
/** Re-fetch the entire viewed range atomically; a first-page-only refresh loses history. */
|
|
514
|
+
refresh = async () => {
|
|
515
|
+
if (!this.alive()) return;
|
|
516
|
+
if (this.state.isInitialLoading || this.state.isLoadingOlder || this.state.isReconciling) {
|
|
517
|
+
this.refreshQueued = true;
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
if (!this.state.hasLoaded || !this.subscriptions.length) return this.loadInitial();
|
|
521
|
+
const generation = this.generation;
|
|
522
|
+
const revision = this.revision;
|
|
523
|
+
const observed = this.state.messages.filter((message) => !isConvoKitPendingMessage(message)).concat([...this.changes.values()].filter((change) => change.insert).map((change) => change.message));
|
|
524
|
+
const boundary = observed.reduce((oldest, message) => !oldest || compare(message, oldest) < 0 ? message : oldest, this.cursor);
|
|
525
|
+
const previouslyExhausted = !this.state.hasOlderMessages;
|
|
526
|
+
this.patch({ isReconciling: true, error: null });
|
|
239
527
|
try {
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
]
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
528
|
+
const conversation = await this.client.getConversation(this.room);
|
|
529
|
+
if (!this.alive(generation)) return;
|
|
530
|
+
if (conversation.id !== this.room) throw new Error("Conversation response belongs to a different room");
|
|
531
|
+
const rows = [];
|
|
532
|
+
let before;
|
|
533
|
+
let hasOlder = true;
|
|
534
|
+
while (this.alive(generation)) {
|
|
535
|
+
const page = await this.fetchPage(before);
|
|
536
|
+
if (!this.alive(generation)) return;
|
|
537
|
+
this.validatePage(page, before);
|
|
538
|
+
rows.push(...page);
|
|
539
|
+
before = page.at(-1) ?? before;
|
|
540
|
+
hasOlder = page.length === this.pageSize;
|
|
541
|
+
if (!hasOlder || !boundary || !previouslyExhausted && before && compare(before, boundary) < 0) break;
|
|
542
|
+
}
|
|
543
|
+
this.cursor = before;
|
|
544
|
+
const reconciled = this.overlay(rows, revision);
|
|
545
|
+
const survivingIds = new Set(reconciled.map((message) => message.id));
|
|
546
|
+
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));
|
|
547
|
+
for (const id of known) if (!survivingIds.has(id)) {
|
|
548
|
+
this.deleted.add(id);
|
|
549
|
+
this.changes.delete(id);
|
|
550
|
+
this.hydrations.delete(id);
|
|
551
|
+
this.hydrationPool.queued.delete(id);
|
|
552
|
+
}
|
|
553
|
+
this.patch({ conversation, messages: reconciled, hasOlderMessages: hasOlder });
|
|
554
|
+
this.mergeReads(conversation.participants.flatMap((participant) => participant.lastReadAt ? [[participant.appUserId, participant.lastReadAt]] : []));
|
|
555
|
+
this.prune(revision);
|
|
250
556
|
} catch (cause) {
|
|
251
|
-
|
|
557
|
+
this.fail(cause, generation, true);
|
|
252
558
|
} finally {
|
|
253
|
-
if (
|
|
254
|
-
|
|
255
|
-
|
|
559
|
+
if (this.alive(generation)) {
|
|
560
|
+
this.patch({ isReconciling: false });
|
|
561
|
+
this.flushRefresh();
|
|
256
562
|
}
|
|
257
563
|
}
|
|
258
564
|
};
|
|
259
|
-
|
|
260
|
-
if (isInitialLoading.
|
|
261
|
-
const
|
|
262
|
-
const
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
error.value = null;
|
|
565
|
+
loadOlderMessages = async () => {
|
|
566
|
+
if (!this.alive() || !this.state.hasLoaded || this.state.isInitialLoading || this.state.isLoadingOlder || this.state.isReconciling || !this.state.hasOlderMessages) return;
|
|
567
|
+
const generation = this.generation;
|
|
568
|
+
const revision = this.revision;
|
|
569
|
+
const cursor = this.cursor;
|
|
570
|
+
this.patch({ isLoadingOlder: true, error: null });
|
|
266
571
|
try {
|
|
267
|
-
const page = await
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
572
|
+
const page = await this.fetchPage(cursor);
|
|
573
|
+
if (!this.alive(generation)) return;
|
|
574
|
+
this.validatePage(page, cursor);
|
|
575
|
+
this.cursor = page.at(-1) ?? cursor;
|
|
576
|
+
this.patch({
|
|
577
|
+
messages: this.overlay(mergeMessages(page, this.state.messages), revision),
|
|
578
|
+
hasOlderMessages: page.length === this.pageSize
|
|
271
579
|
});
|
|
272
|
-
if (disposed || activeGeneration !== generation) return;
|
|
273
|
-
messages.value = mergeMessages(messages.value, page);
|
|
274
|
-
hasOlderMessages.value = page.length === messagePageSize;
|
|
275
580
|
} catch (cause) {
|
|
276
|
-
|
|
581
|
+
this.fail(cause, generation, true);
|
|
277
582
|
} finally {
|
|
278
|
-
if (
|
|
583
|
+
if (this.alive(generation)) {
|
|
584
|
+
this.patch({ isLoadingOlder: false });
|
|
585
|
+
this.flushRefresh();
|
|
586
|
+
}
|
|
279
587
|
}
|
|
280
588
|
};
|
|
281
|
-
|
|
282
|
-
if (
|
|
283
|
-
|
|
284
|
-
void updateTyping(false);
|
|
285
|
-
}, typingTimeoutMs);
|
|
286
|
-
if (sentTyping === nextTyping) return;
|
|
287
|
-
sentTyping = nextTyping;
|
|
589
|
+
markRead = async () => {
|
|
590
|
+
if (!this.alive()) return;
|
|
591
|
+
const generation = this.generation;
|
|
288
592
|
try {
|
|
289
|
-
await
|
|
290
|
-
conversationId: toValue(options.conversationId),
|
|
291
|
-
isTyping: nextTyping
|
|
292
|
-
});
|
|
593
|
+
await this.client.markConversationRead(this.room);
|
|
293
594
|
} catch (cause) {
|
|
294
|
-
|
|
595
|
+
this.fail(cause, generation);
|
|
295
596
|
}
|
|
296
597
|
};
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
598
|
+
updateTyping = async (isTyping) => {
|
|
599
|
+
if (!this.alive()) return;
|
|
600
|
+
const generation = this.generation;
|
|
601
|
+
clearTimeout(this.ownTypingTimer);
|
|
602
|
+
if (isTyping) this.ownTypingTimer = setTimeout(() => {
|
|
603
|
+
if (this.alive(generation)) void this.updateTyping(false);
|
|
604
|
+
}, this.typingTimeout);
|
|
605
|
+
const now = performance.now();
|
|
606
|
+
const renew = isTyping && now - this.lastTypingSentAt >= Math.max(1, this.typingTimeout / 2);
|
|
607
|
+
if (this.sentTyping === isTyping && !renew) return;
|
|
608
|
+
const revision = ++this.typingRevision;
|
|
609
|
+
this.sentTyping = isTyping;
|
|
610
|
+
this.lastTypingSentAt = isTyping ? now : -Infinity;
|
|
611
|
+
try {
|
|
612
|
+
await this.client.sendTyping({ conversationId: this.room, isTyping });
|
|
613
|
+
} catch (cause) {
|
|
614
|
+
if (this.alive(generation) && revision === this.typingRevision) {
|
|
615
|
+
this.sentTyping = false;
|
|
616
|
+
this.fail(cause, generation);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
};
|
|
620
|
+
sendMessage = async ({ text, media }) => {
|
|
621
|
+
const normalized = text?.trim();
|
|
622
|
+
if (!this.alive() || this.state.isSending || !normalized && !media?.length) return null;
|
|
623
|
+
const generation = this.generation;
|
|
624
|
+
const revision = this.revision;
|
|
625
|
+
this.sendRevision = revision;
|
|
626
|
+
const pendingId = `convokit-pending-${Date.now()}-${++this.pendingSequence}`;
|
|
627
|
+
const pending = {
|
|
303
628
|
id: pendingId,
|
|
304
|
-
conversationId:
|
|
305
|
-
senderId:
|
|
306
|
-
text:
|
|
629
|
+
conversationId: this.room,
|
|
630
|
+
senderId: this.user,
|
|
631
|
+
text: normalized || null,
|
|
307
632
|
media: media ?? [],
|
|
308
633
|
createdAt: /* @__PURE__ */ new Date(),
|
|
309
634
|
updatedAt: null
|
|
310
635
|
};
|
|
311
|
-
|
|
312
|
-
messages.value = mergeMessages(messages.value, [pendingMessage]);
|
|
313
|
-
isSending.value = true;
|
|
314
|
-
error.value = null;
|
|
315
|
-
const activeGeneration = generation;
|
|
636
|
+
this.patch({ messages: mergeMessages(this.state.messages, [pending]), isSending: true, error: null });
|
|
316
637
|
try {
|
|
317
|
-
const message = await
|
|
318
|
-
conversationId:
|
|
319
|
-
...
|
|
638
|
+
const message = await this.client.sendMessage({
|
|
639
|
+
conversationId: this.room,
|
|
640
|
+
...normalized ? { text: normalized } : {},
|
|
320
641
|
...media?.length ? { media } : {}
|
|
321
642
|
});
|
|
322
|
-
|
|
323
|
-
if (!
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
|
|
643
|
+
if (!this.alive(generation)) return null;
|
|
644
|
+
if (!this.validMessage(message) || message.senderId !== this.user) throw new Error("Send response belongs to a different room or sender");
|
|
645
|
+
const live = this.changes.get(message.id);
|
|
646
|
+
const existing = this.state.messages.find((item) => item.id === message.id);
|
|
647
|
+
let latest = existing ? newest(message, existing, live?.complete !== false) : message;
|
|
648
|
+
if (live && live.revision > revision) latest = newest(latest, live.message, live.complete);
|
|
649
|
+
if (!this.deleted.has(message.id)) this.changes.set(message.id, { revision: ++this.revision, message: latest, insert: true, complete: true });
|
|
650
|
+
this.patch({ messages: mergeMessages(
|
|
651
|
+
this.state.messages.filter((item) => item.id !== pendingId),
|
|
652
|
+
this.deleted.has(message.id) ? [] : [latest]
|
|
653
|
+
) });
|
|
654
|
+
void this.updateTyping(false);
|
|
655
|
+
return this.alive(generation) ? latest : null;
|
|
331
656
|
} catch (cause) {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
error.value = cause;
|
|
657
|
+
if (this.alive(generation)) {
|
|
658
|
+
this.patch({ messages: this.state.messages.filter((item) => item.id !== pendingId) });
|
|
659
|
+
this.fail(cause, generation);
|
|
336
660
|
}
|
|
337
661
|
return null;
|
|
338
662
|
} finally {
|
|
339
|
-
if (
|
|
663
|
+
if (this.alive(generation)) {
|
|
664
|
+
this.sendRevision = void 0;
|
|
665
|
+
this.patch({ isSending: false });
|
|
666
|
+
}
|
|
340
667
|
}
|
|
341
668
|
};
|
|
669
|
+
readerIdsFor = (message) => readerIdsFor(message, this.state.readAtByUserId);
|
|
670
|
+
};
|
|
671
|
+
|
|
672
|
+
// src/composables/use-conversation.ts
|
|
673
|
+
function useConversation(options) {
|
|
674
|
+
const createStore = () => new ConversationStore({
|
|
675
|
+
...options,
|
|
676
|
+
client: toValue(options.client),
|
|
677
|
+
conversationId: toValue(options.conversationId)
|
|
678
|
+
});
|
|
679
|
+
let store = createStore();
|
|
680
|
+
const snapshot = shallowRef(store.getSnapshot());
|
|
681
|
+
let unsubscribe;
|
|
682
|
+
const stop = watch(
|
|
683
|
+
() => [toValue(options.client), toValue(options.client).sessionIdentity, toValue(options.conversationId)],
|
|
684
|
+
() => {
|
|
685
|
+
store.dispose();
|
|
686
|
+
unsubscribe?.();
|
|
687
|
+
store = createStore();
|
|
688
|
+
snapshot.value = store.getSnapshot();
|
|
689
|
+
unsubscribe = store.subscribe(() => {
|
|
690
|
+
snapshot.value = store.getSnapshot();
|
|
691
|
+
});
|
|
692
|
+
store.start(options.autoLoad ?? true);
|
|
693
|
+
},
|
|
694
|
+
{ immediate: true, flush: "sync" }
|
|
695
|
+
);
|
|
696
|
+
const field = (key) => computed(() => snapshot.value[key]);
|
|
342
697
|
const dispose = async () => {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
await toValue(options.client).sendTyping({
|
|
348
|
-
conversationId: toValue(options.conversationId),
|
|
349
|
-
isTyping: false
|
|
350
|
-
}).catch(() => void 0);
|
|
351
|
-
}
|
|
352
|
-
await unsubscribe();
|
|
698
|
+
stop();
|
|
699
|
+
store.dispose();
|
|
700
|
+
unsubscribe?.();
|
|
701
|
+
unsubscribe = void 0;
|
|
353
702
|
};
|
|
354
|
-
if (options.autoLoad ?? true) {
|
|
355
|
-
watch(
|
|
356
|
-
() => [toValue(options.client), toValue(options.conversationId)],
|
|
357
|
-
() => {
|
|
358
|
-
disposed = false;
|
|
359
|
-
sentTyping = false;
|
|
360
|
-
void loadInitial();
|
|
361
|
-
},
|
|
362
|
-
{ immediate: true }
|
|
363
|
-
);
|
|
364
|
-
}
|
|
365
703
|
if (getCurrentScope()) onScopeDispose(() => {
|
|
366
704
|
void dispose();
|
|
367
705
|
});
|
|
368
706
|
return {
|
|
369
|
-
conversation,
|
|
370
|
-
messages,
|
|
371
|
-
typingUserIds,
|
|
372
|
-
readAtByUserId,
|
|
373
|
-
isInitialLoading,
|
|
374
|
-
isLoadingOlder,
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
707
|
+
conversation: field("conversation"),
|
|
708
|
+
messages: field("messages"),
|
|
709
|
+
typingUserIds: field("typingUserIds"),
|
|
710
|
+
readAtByUserId: field("readAtByUserId"),
|
|
711
|
+
isInitialLoading: field("isInitialLoading"),
|
|
712
|
+
isLoadingOlder: field("isLoadingOlder"),
|
|
713
|
+
isReconciling: field("isReconciling"),
|
|
714
|
+
isSending: field("isSending"),
|
|
715
|
+
hasOlderMessages: field("hasOlderMessages"),
|
|
716
|
+
hasLoaded: field("hasLoaded"),
|
|
717
|
+
error: field("error"),
|
|
718
|
+
currentUserId: field("currentUserId"),
|
|
719
|
+
readerIdsFor: (message) => store.readerIdsFor(message),
|
|
720
|
+
loadInitial: () => store.loadInitial(),
|
|
721
|
+
refresh: () => store.refresh(),
|
|
722
|
+
loadOlderMessages: () => store.loadOlderMessages(),
|
|
723
|
+
sendMessage: (input) => store.sendMessage(input),
|
|
724
|
+
markRead: () => store.markRead(),
|
|
725
|
+
updateTyping: (isTyping) => store.updateTyping(isTyping),
|
|
387
726
|
dispose
|
|
388
727
|
};
|
|
389
728
|
}
|
|
@@ -405,7 +744,7 @@ import {
|
|
|
405
744
|
defineComponent as defineComponent2,
|
|
406
745
|
h as h2,
|
|
407
746
|
nextTick,
|
|
408
|
-
ref
|
|
747
|
+
ref,
|
|
409
748
|
watch as watch2
|
|
410
749
|
} from "vue";
|
|
411
750
|
var appearanceProps = {
|
|
@@ -471,7 +810,7 @@ var MessageListView = defineComponent2({
|
|
|
471
810
|
},
|
|
472
811
|
emits: ["load-older", "attachment-click"],
|
|
473
812
|
setup(props, { attrs, emit, slots }) {
|
|
474
|
-
const internalElement =
|
|
813
|
+
const internalElement = ref(null);
|
|
475
814
|
let requestInFlight = false;
|
|
476
815
|
let lastRequestedLength = null;
|
|
477
816
|
let previousMessageCount = 0;
|
|
@@ -681,8 +1020,8 @@ var ConversationView = defineComponent3({
|
|
|
681
1020
|
"update:modelValue"
|
|
682
1021
|
],
|
|
683
1022
|
setup(props, { attrs, emit, slots }) {
|
|
684
|
-
const internalDraft =
|
|
685
|
-
const submitting =
|
|
1023
|
+
const internalDraft = ref2(props.defaultDraft);
|
|
1024
|
+
const submitting = ref2(false);
|
|
686
1025
|
const appearance = () => ({
|
|
687
1026
|
density: props.density,
|
|
688
1027
|
unstyled: props.unstyled,
|
|
@@ -1010,119 +1349,193 @@ import { ChevronRight, Inbox, LoaderCircle as LoaderCircle3, RefreshCw as Refres
|
|
|
1010
1349
|
import {
|
|
1011
1350
|
defineComponent as defineComponent4,
|
|
1012
1351
|
h as h4,
|
|
1013
|
-
ref as
|
|
1352
|
+
ref as ref3,
|
|
1014
1353
|
watchEffect as watchEffect2
|
|
1015
1354
|
} from "vue";
|
|
1016
1355
|
|
|
1017
1356
|
// src/composables/use-conversation-list.ts
|
|
1018
|
-
import {
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1357
|
+
import { computed as computed3, getCurrentScope as getCurrentScope2, onScopeDispose as onScopeDispose2, shallowRef as shallowRef2, toValue as toValue2, watch as watch3 } from "vue";
|
|
1358
|
+
|
|
1359
|
+
// src/conversation-list-store.ts
|
|
1360
|
+
function blank2(filter) {
|
|
1361
|
+
return { conversations: [], filter, isInitialLoading: false, isLoadingMore: false, hasMore: true, hasLoaded: false, error: null };
|
|
1362
|
+
}
|
|
1363
|
+
var ConversationListStore = class {
|
|
1364
|
+
constructor(options) {
|
|
1365
|
+
this.options = options;
|
|
1366
|
+
this.owner = options.client.sessionIdentity;
|
|
1367
|
+
this.pageSize = options.pageSize ?? 30;
|
|
1368
|
+
if (!Number.isInteger(this.pageSize) || this.pageSize < 1 || this.pageSize > 100) {
|
|
1369
|
+
throw new RangeError("pageSize must be an integer between 1 and 100");
|
|
1370
|
+
}
|
|
1371
|
+
this.state = blank2(options.initialFilter ?? {});
|
|
1372
|
+
}
|
|
1373
|
+
options;
|
|
1374
|
+
owner;
|
|
1375
|
+
pageSize;
|
|
1376
|
+
state;
|
|
1377
|
+
source = [];
|
|
1378
|
+
offset = 0;
|
|
1379
|
+
generation = 0;
|
|
1380
|
+
lifecycleGeneration = 0;
|
|
1381
|
+
disposed = true;
|
|
1382
|
+
lifecycle;
|
|
1383
|
+
listeners = /* @__PURE__ */ new Set();
|
|
1384
|
+
getSnapshot = () => this.state;
|
|
1385
|
+
subscribe = (listener) => {
|
|
1386
|
+
this.listeners.add(listener);
|
|
1387
|
+
return () => {
|
|
1388
|
+
this.listeners.delete(listener);
|
|
1389
|
+
};
|
|
1390
|
+
};
|
|
1391
|
+
patch(patch) {
|
|
1392
|
+
this.state = { ...this.state, ...patch };
|
|
1393
|
+
for (const listener of this.listeners) listener();
|
|
1394
|
+
}
|
|
1395
|
+
alive(generation = this.generation) {
|
|
1396
|
+
return !this.disposed && generation === this.generation && this.owner !== null && this.options.client.sessionIdentity === this.owner;
|
|
1397
|
+
}
|
|
1398
|
+
start = (autoLoad = true) => {
|
|
1399
|
+
if (!this.owner || this.options.client.sessionIdentity !== this.owner) return;
|
|
1400
|
+
this.disposed = false;
|
|
1401
|
+
const lifecycleGeneration = ++this.lifecycleGeneration;
|
|
1402
|
+
try {
|
|
1403
|
+
const subscription = this.options.client.onConnectionEvent({
|
|
1404
|
+
onEvent: () => {
|
|
1405
|
+
},
|
|
1406
|
+
onSessionEnded: () => {
|
|
1407
|
+
if (!this.disposed && lifecycleGeneration === this.lifecycleGeneration) this.dispose();
|
|
1408
|
+
}
|
|
1049
1409
|
});
|
|
1050
|
-
if (
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
if (
|
|
1410
|
+
if (this.alive()) this.lifecycle = subscription;
|
|
1411
|
+
else void subscription.unsubscribe().catch(() => void 0);
|
|
1412
|
+
if (autoLoad) void this.loadInitial();
|
|
1413
|
+
} catch (cause) {
|
|
1414
|
+
if (this.alive()) this.patch({ error: cause });
|
|
1055
1415
|
}
|
|
1056
1416
|
};
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1417
|
+
dispose = () => {
|
|
1418
|
+
this.disposed = true;
|
|
1419
|
+
this.generation++;
|
|
1420
|
+
this.lifecycleGeneration++;
|
|
1421
|
+
const subscription = this.lifecycle;
|
|
1422
|
+
this.lifecycle = void 0;
|
|
1423
|
+
if (subscription) void subscription.unsubscribe().catch(() => void 0);
|
|
1424
|
+
this.source = [];
|
|
1425
|
+
this.offset = 0;
|
|
1426
|
+
this.patch(blank2(this.state.filter));
|
|
1427
|
+
};
|
|
1428
|
+
fail(cause, generation) {
|
|
1429
|
+
if (!this.alive(generation)) return;
|
|
1430
|
+
const status = typeof cause === "object" && cause !== null && "status" in cause ? cause.status : void 0;
|
|
1431
|
+
if (status === 401 || status === 403 || status === 404) {
|
|
1432
|
+
this.source = [];
|
|
1433
|
+
this.offset = 0;
|
|
1434
|
+
this.patch({ conversations: [], hasMore: false });
|
|
1435
|
+
}
|
|
1436
|
+
this.patch({ error: cause });
|
|
1437
|
+
}
|
|
1438
|
+
async loadUntilVisible(generation, filter) {
|
|
1439
|
+
const visibleBefore = applyConversationFilter(this.source, filter).length;
|
|
1440
|
+
while (this.alive(generation)) {
|
|
1441
|
+
const request = { limit: this.pageSize, offset: this.offset, filter };
|
|
1442
|
+
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 });
|
|
1443
|
+
if (!this.alive(generation)) return;
|
|
1444
|
+
if (page.length > this.pageSize || page.some((conversation) => !conversation.id.trim())) {
|
|
1445
|
+
throw new Error("Invalid conversation page");
|
|
1446
|
+
}
|
|
1447
|
+
const merged = mergeConversations(this.source, page);
|
|
1448
|
+
if (page.length === this.pageSize && merged.length === this.source.length) {
|
|
1449
|
+
throw new Error("Conversation pagination did not advance");
|
|
1450
|
+
}
|
|
1451
|
+
this.offset += page.length;
|
|
1452
|
+
this.source = merged;
|
|
1453
|
+
const visible = applyConversationFilter(merged, filter);
|
|
1454
|
+
const hasMore = page.length === this.pageSize;
|
|
1455
|
+
this.patch({ conversations: visible, hasMore });
|
|
1456
|
+
if (!hasMore || visible.length > visibleBefore) return;
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
loadInitial = async () => {
|
|
1460
|
+
if (!this.alive()) return;
|
|
1461
|
+
const generation = ++this.generation;
|
|
1462
|
+
this.source = [];
|
|
1463
|
+
this.offset = 0;
|
|
1464
|
+
this.patch({ ...blank2(this.state.filter), isInitialLoading: true });
|
|
1065
1465
|
try {
|
|
1066
|
-
await loadUntilVisible(
|
|
1466
|
+
await this.loadUntilVisible(generation, this.state.filter);
|
|
1067
1467
|
} catch (cause) {
|
|
1068
|
-
|
|
1468
|
+
this.fail(cause, generation);
|
|
1069
1469
|
} finally {
|
|
1070
|
-
if (
|
|
1071
|
-
isInitialLoading.value = false;
|
|
1072
|
-
hasLoaded.value = true;
|
|
1073
|
-
}
|
|
1470
|
+
if (this.alive(generation)) this.patch({ isInitialLoading: false, hasLoaded: true });
|
|
1074
1471
|
}
|
|
1075
1472
|
};
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1473
|
+
refresh = () => this.loadInitial();
|
|
1474
|
+
loadMore = async () => {
|
|
1475
|
+
if (!this.alive() || this.state.isInitialLoading || this.state.isLoadingMore || !this.state.hasMore) return;
|
|
1476
|
+
const generation = this.generation;
|
|
1477
|
+
this.patch({ isLoadingMore: true, error: null });
|
|
1081
1478
|
try {
|
|
1082
|
-
await loadUntilVisible(
|
|
1479
|
+
await this.loadUntilVisible(generation, this.state.filter);
|
|
1083
1480
|
} catch (cause) {
|
|
1084
|
-
|
|
1481
|
+
this.fail(cause, generation);
|
|
1085
1482
|
} finally {
|
|
1086
|
-
if (
|
|
1483
|
+
if (this.alive(generation)) this.patch({ isLoadingMore: false });
|
|
1087
1484
|
}
|
|
1088
1485
|
};
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
filter.
|
|
1092
|
-
|
|
1093
|
-
if (
|
|
1094
|
-
|
|
1095
|
-
} else if (applyConversationFilter(source.value, nextFilter).length === 0 && hasMore.value) {
|
|
1096
|
-
await loadMore();
|
|
1097
|
-
}
|
|
1486
|
+
setFilter = async (filter) => {
|
|
1487
|
+
if (!this.alive()) return;
|
|
1488
|
+
const reload = this.options.pageLoader || !this.state.hasLoaded || this.state.isInitialLoading || this.state.isLoadingMore || (filter.archived ?? false) !== (this.state.filter.archived ?? false);
|
|
1489
|
+
this.patch({ filter, conversations: applyConversationFilter(this.source, filter), error: null });
|
|
1490
|
+
if (reload) await this.loadInitial();
|
|
1491
|
+
else if (!this.state.conversations.length && this.state.hasMore) await this.loadMore();
|
|
1098
1492
|
};
|
|
1099
|
-
|
|
1493
|
+
setQuery = (query) => this.setFilter({ ...this.state.filter, query });
|
|
1494
|
+
};
|
|
1495
|
+
|
|
1496
|
+
// src/composables/use-conversation-list.ts
|
|
1497
|
+
function useConversationList(options) {
|
|
1498
|
+
const createStore = () => new ConversationListStore({ ...options, client: toValue2(options.client) });
|
|
1499
|
+
let store = createStore();
|
|
1500
|
+
const snapshot = shallowRef2(store.getSnapshot());
|
|
1501
|
+
let unsubscribe;
|
|
1502
|
+
const stop = watch3(
|
|
1503
|
+
() => [toValue2(options.client), toValue2(options.client).sessionIdentity],
|
|
1504
|
+
() => {
|
|
1505
|
+
store.dispose();
|
|
1506
|
+
unsubscribe?.();
|
|
1507
|
+
store = createStore();
|
|
1508
|
+
snapshot.value = store.getSnapshot();
|
|
1509
|
+
unsubscribe = store.subscribe(() => {
|
|
1510
|
+
snapshot.value = store.getSnapshot();
|
|
1511
|
+
});
|
|
1512
|
+
store.start(options.autoLoad ?? true);
|
|
1513
|
+
},
|
|
1514
|
+
{ immediate: true, flush: "sync" }
|
|
1515
|
+
);
|
|
1516
|
+
const field = (key) => computed3(() => snapshot.value[key]);
|
|
1100
1517
|
const dispose = async () => {
|
|
1101
|
-
|
|
1102
|
-
|
|
1518
|
+
stop();
|
|
1519
|
+
store.dispose();
|
|
1520
|
+
unsubscribe?.();
|
|
1521
|
+
unsubscribe = void 0;
|
|
1103
1522
|
};
|
|
1104
|
-
if (options.autoLoad ?? true) {
|
|
1105
|
-
watch3(() => toValue2(options.client), () => {
|
|
1106
|
-
disposed = false;
|
|
1107
|
-
void loadInitialFor(filter.value);
|
|
1108
|
-
}, { immediate: true });
|
|
1109
|
-
}
|
|
1110
1523
|
if (getCurrentScope2()) onScopeDispose2(() => {
|
|
1111
1524
|
void dispose();
|
|
1112
1525
|
});
|
|
1113
1526
|
return {
|
|
1114
|
-
conversations,
|
|
1115
|
-
filter,
|
|
1116
|
-
isInitialLoading,
|
|
1117
|
-
isLoadingMore,
|
|
1118
|
-
hasMore,
|
|
1119
|
-
hasLoaded,
|
|
1120
|
-
error,
|
|
1121
|
-
loadInitial:
|
|
1122
|
-
refresh:
|
|
1123
|
-
loadMore,
|
|
1124
|
-
setFilter,
|
|
1125
|
-
setQuery,
|
|
1527
|
+
conversations: field("conversations"),
|
|
1528
|
+
filter: field("filter"),
|
|
1529
|
+
isInitialLoading: field("isInitialLoading"),
|
|
1530
|
+
isLoadingMore: field("isLoadingMore"),
|
|
1531
|
+
hasMore: field("hasMore"),
|
|
1532
|
+
hasLoaded: field("hasLoaded"),
|
|
1533
|
+
error: field("error"),
|
|
1534
|
+
loadInitial: () => store.loadInitial(),
|
|
1535
|
+
refresh: () => store.refresh(),
|
|
1536
|
+
loadMore: () => store.loadMore(),
|
|
1537
|
+
setFilter: (filter) => store.setFilter(filter),
|
|
1538
|
+
setQuery: (query) => store.setQuery(query),
|
|
1126
1539
|
dispose
|
|
1127
1540
|
};
|
|
1128
1541
|
}
|
|
@@ -1159,7 +1572,7 @@ var ConversationListView = defineComponent4({
|
|
|
1159
1572
|
"load-more": () => true
|
|
1160
1573
|
},
|
|
1161
1574
|
setup(props, { attrs, emit, slots }) {
|
|
1162
|
-
const internalElement =
|
|
1575
|
+
const internalElement = ref3(null);
|
|
1163
1576
|
let requestInFlight = false;
|
|
1164
1577
|
let lastRequestedLength = null;
|
|
1165
1578
|
const appearance = () => ({
|