@convokitapp/vue-ui 0.3.0 → 0.4.1
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 +16 -0
- package/README.md +16 -8
- package/dist/index.cjs +125 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -6
- package/dist/index.d.ts +4 -6
- package/dist/index.js +125 -8
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Conversation as Conversation$1, MessageListOptions, Message,
|
|
1
|
+
import { Conversation as Conversation$1, MessageListOptions, Message, SendMessageInput, RealtimeConnectionHandlers, RealtimeSubscription, MessageEvent, MessageDeletedEvent, ReadEvent, TypingEvent, MessageMedia, ConvoKitClient } from '@convokitapp/sdk';
|
|
2
2
|
import * as vue from 'vue';
|
|
3
3
|
import { CSSProperties, HTMLAttributes, Ref, VNodeChild, PropType, MaybeRefOrGetter, TextareaHTMLAttributes } from 'vue';
|
|
4
4
|
|
|
@@ -16,17 +16,15 @@ interface ConvoKitUiClient {
|
|
|
16
16
|
getMessages(options: MessageListOptions): Promise<Message[]>;
|
|
17
17
|
/** Complete authorized message, including related media absent from raw row events. */
|
|
18
18
|
getMessage(id: string): Promise<Message>;
|
|
19
|
-
sendMessage(input:
|
|
20
|
-
conversationId: string;
|
|
21
|
-
text?: string;
|
|
22
|
-
media?: MessageMedia[];
|
|
23
|
-
}): Promise<Message>;
|
|
19
|
+
sendMessage(input: SendMessageInput): Promise<Message>;
|
|
24
20
|
markConversationRead(conversationId: string): Promise<void>;
|
|
25
21
|
sendTyping(input: {
|
|
26
22
|
conversationId: string;
|
|
27
23
|
isTyping: boolean;
|
|
28
24
|
}): Promise<void>;
|
|
29
25
|
onConnectionEvent(handlers: RealtimeConnectionHandlers): RealtimeSubscription;
|
|
26
|
+
/** Notify on inbox mutations AND on initial subscription/reconnect. No room data in the signal. */
|
|
27
|
+
onInboxChanged(handler: () => void, onError?: (error: Error) => void): RealtimeSubscription;
|
|
30
28
|
onMessage(conversationId: string, handler: (event: MessageEvent) => void, onError?: (error: Error) => void): RealtimeSubscription;
|
|
31
29
|
onMessageDeleted(conversationId: string, handler: (event: MessageDeletedEvent) => void, onError?: (error: Error) => void): RealtimeSubscription;
|
|
32
30
|
onReadReceipt(conversationId: string, handler: (event: ReadEvent) => void, onError?: (error: Error) => void): RealtimeSubscription;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Conversation as Conversation$1, MessageListOptions, Message,
|
|
1
|
+
import { Conversation as Conversation$1, MessageListOptions, Message, SendMessageInput, RealtimeConnectionHandlers, RealtimeSubscription, MessageEvent, MessageDeletedEvent, ReadEvent, TypingEvent, MessageMedia, ConvoKitClient } from '@convokitapp/sdk';
|
|
2
2
|
import * as vue from 'vue';
|
|
3
3
|
import { CSSProperties, HTMLAttributes, Ref, VNodeChild, PropType, MaybeRefOrGetter, TextareaHTMLAttributes } from 'vue';
|
|
4
4
|
|
|
@@ -16,17 +16,15 @@ interface ConvoKitUiClient {
|
|
|
16
16
|
getMessages(options: MessageListOptions): Promise<Message[]>;
|
|
17
17
|
/** Complete authorized message, including related media absent from raw row events. */
|
|
18
18
|
getMessage(id: string): Promise<Message>;
|
|
19
|
-
sendMessage(input:
|
|
20
|
-
conversationId: string;
|
|
21
|
-
text?: string;
|
|
22
|
-
media?: MessageMedia[];
|
|
23
|
-
}): Promise<Message>;
|
|
19
|
+
sendMessage(input: SendMessageInput): Promise<Message>;
|
|
24
20
|
markConversationRead(conversationId: string): Promise<void>;
|
|
25
21
|
sendTyping(input: {
|
|
26
22
|
conversationId: string;
|
|
27
23
|
isTyping: boolean;
|
|
28
24
|
}): Promise<void>;
|
|
29
25
|
onConnectionEvent(handlers: RealtimeConnectionHandlers): RealtimeSubscription;
|
|
26
|
+
/** Notify on inbox mutations AND on initial subscription/reconnect. No room data in the signal. */
|
|
27
|
+
onInboxChanged(handler: () => void, onError?: (error: Error) => void): RealtimeSubscription;
|
|
30
28
|
onMessage(conversationId: string, handler: (event: MessageEvent) => void, onError?: (error: Error) => void): RealtimeSubscription;
|
|
31
29
|
onMessageDeleted(conversationId: string, handler: (event: MessageDeletedEvent) => void, onError?: (error: Error) => void): RealtimeSubscription;
|
|
32
30
|
onReadReceipt(conversationId: string, handler: (event: ReadEvent) => void, onError?: (error: Error) => void): RealtimeSubscription;
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,10 @@ function createConvoKitUiClient(client) {
|
|
|
5
5
|
return client.connected ? client.realtime : null;
|
|
6
6
|
},
|
|
7
7
|
onConnectionEvent: (handlers) => client.realtime.onConnectionEvent(handlers),
|
|
8
|
+
onInboxChanged: (handler, onError) => client.realtime.onInboxChanged(client.clientId, {
|
|
9
|
+
onEvent: handler,
|
|
10
|
+
...onError ? { onError } : {}
|
|
11
|
+
}),
|
|
8
12
|
onMessageDeleted: (conversationId, handler, onError) => client.realtime.onMessageDeleted(conversationId, {
|
|
9
13
|
onEvent: handler,
|
|
10
14
|
...onError ? { onError } : {}
|
|
@@ -155,6 +159,7 @@ import {
|
|
|
155
159
|
import { computed, getCurrentScope, onScopeDispose, shallowRef, toValue, watch } from "vue";
|
|
156
160
|
|
|
157
161
|
// src/conversation-store.ts
|
|
162
|
+
import { createClientMessageId } from "@convokitapp/sdk";
|
|
158
163
|
function version(message) {
|
|
159
164
|
return message.updatedAt?.getTime() ?? message.createdAt.getTime();
|
|
160
165
|
}
|
|
@@ -221,7 +226,7 @@ var ConversationStore = class {
|
|
|
221
226
|
// Keep tombstones until an explicit reload/session change, including across refreshes.
|
|
222
227
|
deleted = /* @__PURE__ */ new Set();
|
|
223
228
|
sendRevision;
|
|
224
|
-
|
|
229
|
+
activeSend;
|
|
225
230
|
refreshQueued = false;
|
|
226
231
|
typingTimers = /* @__PURE__ */ new Map();
|
|
227
232
|
ownTypingTimer;
|
|
@@ -236,6 +241,10 @@ var ConversationStore = class {
|
|
|
236
241
|
};
|
|
237
242
|
};
|
|
238
243
|
patch(patch) {
|
|
244
|
+
if (patch.messages && this.activeSend) {
|
|
245
|
+
for (const message of patch.messages) this.confirmSend(message);
|
|
246
|
+
if (this.activeSend.confirmed) patch.messages = patch.messages.filter((message) => message.id !== this.activeSend.pending.id);
|
|
247
|
+
}
|
|
239
248
|
this.state = { ...this.state, ...patch };
|
|
240
249
|
for (const listener of this.listeners) listener();
|
|
241
250
|
}
|
|
@@ -279,6 +288,7 @@ var ConversationStore = class {
|
|
|
279
288
|
this.hydrationPool.queued.clear();
|
|
280
289
|
this.deleted.clear();
|
|
281
290
|
this.sendRevision = void 0;
|
|
291
|
+
this.activeSend = void 0;
|
|
282
292
|
this.refreshQueued = false;
|
|
283
293
|
}
|
|
284
294
|
dispose = () => {
|
|
@@ -319,6 +329,14 @@ var ConversationStore = class {
|
|
|
319
329
|
onError: report
|
|
320
330
|
}));
|
|
321
331
|
if (!data) return;
|
|
332
|
+
add(() => this.client.onInboxChanged(() => {
|
|
333
|
+
if (this.alive(generation)) this.queueRefresh();
|
|
334
|
+
}, (cause) => {
|
|
335
|
+
if (this.alive(generation)) {
|
|
336
|
+
report(cause);
|
|
337
|
+
this.queueRefresh();
|
|
338
|
+
}
|
|
339
|
+
}));
|
|
322
340
|
add(() => this.client.onMessage(this.room, (event) => this.onMessage(event, generation), report));
|
|
323
341
|
add(() => this.client.onMessageDeleted(this.room, ({ id, conversationId }) => {
|
|
324
342
|
if (!this.alive(generation) || conversationId !== this.room || !id.trim()) return;
|
|
@@ -375,9 +393,19 @@ var ConversationStore = class {
|
|
|
375
393
|
record(message, insert, revision, complete) {
|
|
376
394
|
const existing = this.state.messages.find((item) => item.id === message.id);
|
|
377
395
|
if (existing && version(existing) > version(message)) return;
|
|
396
|
+
if (this.confirmSend(message) && !complete && !message.media.length) {
|
|
397
|
+
message = { ...message, media: this.activeSend.pending.media };
|
|
398
|
+
this.confirmSend(message);
|
|
399
|
+
}
|
|
378
400
|
this.changes.set(message.id, { revision, message, insert, complete });
|
|
379
401
|
if (existing || insert && hasContent(message)) this.patch({ messages: mergeMessages(this.state.messages, [message]) });
|
|
380
402
|
}
|
|
403
|
+
confirmSend(message) {
|
|
404
|
+
const send = this.activeSend;
|
|
405
|
+
if (!send || !this.validMessage(message) || message.senderId !== this.user || message.clientMessageId !== send.pending.clientMessageId) return false;
|
|
406
|
+
send.confirmed = send.confirmed ? newest(send.confirmed, message) : message;
|
|
407
|
+
return true;
|
|
408
|
+
}
|
|
381
409
|
currentHydration(job) {
|
|
382
410
|
return this.alive(job.generation) && !this.deleted.has(job.message.id) && this.hydrations.get(job.message.id) === job;
|
|
383
411
|
}
|
|
@@ -623,9 +651,11 @@ var ConversationStore = class {
|
|
|
623
651
|
const generation = this.generation;
|
|
624
652
|
const revision = this.revision;
|
|
625
653
|
this.sendRevision = revision;
|
|
626
|
-
const
|
|
654
|
+
const clientMessageId = createClientMessageId();
|
|
655
|
+
const pendingId = `convokit-pending-${clientMessageId}`;
|
|
627
656
|
const pending = {
|
|
628
657
|
id: pendingId,
|
|
658
|
+
clientMessageId,
|
|
629
659
|
conversationId: this.room,
|
|
630
660
|
senderId: this.user,
|
|
631
661
|
text: normalized || null,
|
|
@@ -633,15 +663,19 @@ var ConversationStore = class {
|
|
|
633
663
|
createdAt: /* @__PURE__ */ new Date(),
|
|
634
664
|
updatedAt: null
|
|
635
665
|
};
|
|
666
|
+
const send = { pending };
|
|
667
|
+
this.activeSend = send;
|
|
636
668
|
this.patch({ messages: mergeMessages(this.state.messages, [pending]), isSending: true, error: null });
|
|
637
669
|
try {
|
|
638
670
|
const message = await this.client.sendMessage({
|
|
639
671
|
conversationId: this.room,
|
|
672
|
+
clientMessageId,
|
|
640
673
|
...normalized ? { text: normalized } : {},
|
|
641
674
|
...media?.length ? { media } : {}
|
|
642
675
|
});
|
|
643
676
|
if (!this.alive(generation)) return null;
|
|
644
677
|
if (!this.validMessage(message) || message.senderId !== this.user) throw new Error("Send response belongs to a different room or sender");
|
|
678
|
+
if (message.clientMessageId && message.clientMessageId !== clientMessageId) throw new Error("Send response belongs to a different send");
|
|
645
679
|
const live = this.changes.get(message.id);
|
|
646
680
|
const existing = this.state.messages.find((item) => item.id === message.id);
|
|
647
681
|
let latest = existing ? newest(message, existing, live?.complete !== false) : message;
|
|
@@ -656,12 +690,17 @@ var ConversationStore = class {
|
|
|
656
690
|
} catch (cause) {
|
|
657
691
|
if (this.alive(generation)) {
|
|
658
692
|
this.patch({ messages: this.state.messages.filter((item) => item.id !== pendingId) });
|
|
693
|
+
if (send.confirmed) {
|
|
694
|
+
void this.updateTyping(false);
|
|
695
|
+
return send.confirmed;
|
|
696
|
+
}
|
|
659
697
|
this.fail(cause, generation);
|
|
660
698
|
}
|
|
661
699
|
return null;
|
|
662
700
|
} finally {
|
|
663
701
|
if (this.alive(generation)) {
|
|
664
702
|
this.sendRevision = void 0;
|
|
703
|
+
this.activeSend = void 0;
|
|
665
704
|
this.patch({ isSending: false });
|
|
666
705
|
}
|
|
667
706
|
}
|
|
@@ -889,13 +928,13 @@ var MessageListView = defineComponent2({
|
|
|
889
928
|
...mediaNodes,
|
|
890
929
|
h2("span", { class: "ckui-message-time" }, [
|
|
891
930
|
isPending ? "Sending\u2026" : props.formatTime(message.createdAt),
|
|
892
|
-
isCurrentUser && !isPending ? readerIds.size > 0 ? h2(CheckCheck, { size: 14, "aria-label": "Read" }) : h2(Check, { size: 14, "aria-label": "
|
|
931
|
+
isCurrentUser && !isPending ? readerIds.size > 0 ? h2(CheckCheck, { size: 14, "aria-label": "Read" }) : h2(Check, { size: 14, "aria-label": "Sent" }) : null
|
|
893
932
|
])
|
|
894
933
|
]),
|
|
895
934
|
isCurrentUser && !isPending ? slots["read-receipt"]?.(receiptSlotProps) ?? h2("div", {
|
|
896
935
|
class: partClass("receipt", currentAppearance, "ckui-read-receipt"),
|
|
897
936
|
style: partStyle("receipt", currentAppearance)
|
|
898
|
-
}, readerIds.size > 0 ? `Read by ${readerIds.size}` : "
|
|
937
|
+
}, readerIds.size > 0 ? `Read by ${readerIds.size}` : "Sent") : null
|
|
899
938
|
])
|
|
900
939
|
]);
|
|
901
940
|
};
|
|
@@ -1380,6 +1419,9 @@ var ConversationListStore = class {
|
|
|
1380
1419
|
lifecycleGeneration = 0;
|
|
1381
1420
|
disposed = true;
|
|
1382
1421
|
lifecycle;
|
|
1422
|
+
inbox;
|
|
1423
|
+
refreshQueued = false;
|
|
1424
|
+
refreshing = false;
|
|
1383
1425
|
listeners = /* @__PURE__ */ new Set();
|
|
1384
1426
|
getSnapshot = () => this.state;
|
|
1385
1427
|
subscribe = (listener) => {
|
|
@@ -1397,6 +1439,7 @@ var ConversationListStore = class {
|
|
|
1397
1439
|
}
|
|
1398
1440
|
start = (autoLoad = true) => {
|
|
1399
1441
|
if (!this.owner || this.options.client.sessionIdentity !== this.owner) return;
|
|
1442
|
+
if (!this.disposed) return;
|
|
1400
1443
|
this.disposed = false;
|
|
1401
1444
|
const lifecycleGeneration = ++this.lifecycleGeneration;
|
|
1402
1445
|
try {
|
|
@@ -1409,6 +1452,17 @@ var ConversationListStore = class {
|
|
|
1409
1452
|
});
|
|
1410
1453
|
if (this.alive()) this.lifecycle = subscription;
|
|
1411
1454
|
else void subscription.unsubscribe().catch(() => void 0);
|
|
1455
|
+
if (!this.alive()) return;
|
|
1456
|
+
const inbox = this.options.client.onInboxChanged(() => {
|
|
1457
|
+
if (this.alive() && lifecycleGeneration === this.lifecycleGeneration) this.queueRefresh();
|
|
1458
|
+
}, (cause) => {
|
|
1459
|
+
if (this.alive() && lifecycleGeneration === this.lifecycleGeneration) {
|
|
1460
|
+
this.patch({ error: cause });
|
|
1461
|
+
this.queueRefresh();
|
|
1462
|
+
}
|
|
1463
|
+
});
|
|
1464
|
+
if (this.alive()) this.inbox = inbox;
|
|
1465
|
+
else void inbox.unsubscribe().catch(() => void 0);
|
|
1412
1466
|
if (autoLoad) void this.loadInitial();
|
|
1413
1467
|
} catch (cause) {
|
|
1414
1468
|
if (this.alive()) this.patch({ error: cause });
|
|
@@ -1421,6 +1475,10 @@ var ConversationListStore = class {
|
|
|
1421
1475
|
const subscription = this.lifecycle;
|
|
1422
1476
|
this.lifecycle = void 0;
|
|
1423
1477
|
if (subscription) void subscription.unsubscribe().catch(() => void 0);
|
|
1478
|
+
if (this.inbox) void this.inbox.unsubscribe().catch(() => void 0);
|
|
1479
|
+
this.inbox = void 0;
|
|
1480
|
+
this.refreshQueued = false;
|
|
1481
|
+
this.refreshing = false;
|
|
1424
1482
|
this.source = [];
|
|
1425
1483
|
this.offset = 0;
|
|
1426
1484
|
this.patch(blank2(this.state.filter));
|
|
@@ -1459,6 +1517,7 @@ var ConversationListStore = class {
|
|
|
1459
1517
|
loadInitial = async () => {
|
|
1460
1518
|
if (!this.alive()) return;
|
|
1461
1519
|
const generation = ++this.generation;
|
|
1520
|
+
this.refreshing = false;
|
|
1462
1521
|
this.source = [];
|
|
1463
1522
|
this.offset = 0;
|
|
1464
1523
|
this.patch({ ...blank2(this.state.filter), isInitialLoading: true });
|
|
@@ -1467,12 +1526,67 @@ var ConversationListStore = class {
|
|
|
1467
1526
|
} catch (cause) {
|
|
1468
1527
|
this.fail(cause, generation);
|
|
1469
1528
|
} finally {
|
|
1470
|
-
if (this.alive(generation))
|
|
1529
|
+
if (this.alive(generation)) {
|
|
1530
|
+
this.patch({ isInitialLoading: false, hasLoaded: true });
|
|
1531
|
+
this.flushRefresh();
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
};
|
|
1535
|
+
queueRefresh() {
|
|
1536
|
+
this.refreshQueued = true;
|
|
1537
|
+
this.flushRefresh();
|
|
1538
|
+
}
|
|
1539
|
+
flushRefresh() {
|
|
1540
|
+
const lifecycle = this.lifecycleGeneration;
|
|
1541
|
+
void Promise.resolve().then(() => {
|
|
1542
|
+
if (!this.alive() || lifecycle !== this.lifecycleGeneration || !this.refreshQueued || this.refreshing || this.state.isInitialLoading || this.state.isLoadingMore) return;
|
|
1543
|
+
this.refreshQueued = false;
|
|
1544
|
+
void this.refresh();
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
/** Replace the loaded window atomically, retaining filters and rows during transient failures. */
|
|
1548
|
+
refresh = async () => {
|
|
1549
|
+
if (!this.alive()) return;
|
|
1550
|
+
if (this.refreshing || this.state.isInitialLoading || this.state.isLoadingMore) {
|
|
1551
|
+
this.refreshQueued = true;
|
|
1552
|
+
return;
|
|
1553
|
+
}
|
|
1554
|
+
if (!this.state.hasLoaded) return this.loadInitial();
|
|
1555
|
+
const generation = this.generation;
|
|
1556
|
+
const filter = this.state.filter;
|
|
1557
|
+
const target = Math.max(this.pageSize, this.offset);
|
|
1558
|
+
const compare2 = (a, b) => a.createdAt.getTime() - b.createdAt.getTime() || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
1559
|
+
const boundary = this.options.pageLoader ? void 0 : this.source.reduce((oldest, row) => !oldest || compare2(row, oldest) < 0 ? row : oldest, void 0);
|
|
1560
|
+
this.refreshing = true;
|
|
1561
|
+
this.patch({ error: null });
|
|
1562
|
+
try {
|
|
1563
|
+
let rows = [], offset = 0, hasMore = true;
|
|
1564
|
+
while (this.alive(generation)) {
|
|
1565
|
+
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 });
|
|
1566
|
+
if (!this.alive(generation)) return;
|
|
1567
|
+
if (page.length > this.pageSize || page.some((row) => !row.id.trim())) throw new Error("Invalid conversation page");
|
|
1568
|
+
const merged = mergeConversations(rows, page);
|
|
1569
|
+
if (page.length === this.pageSize && merged.length === rows.length) throw new Error("Conversation pagination did not advance");
|
|
1570
|
+
rows = merged;
|
|
1571
|
+
offset += page.length;
|
|
1572
|
+
hasMore = page.length === this.pageSize;
|
|
1573
|
+
if (!hasMore || offset >= target && applyConversationFilter(rows, this.state.filter).length > 0 && (!boundary || page.some((row) => compare2(row, boundary) <= 0))) break;
|
|
1574
|
+
}
|
|
1575
|
+
if (!this.alive(generation)) return;
|
|
1576
|
+
this.source = rows;
|
|
1577
|
+
this.offset = offset;
|
|
1578
|
+
this.patch({ conversations: applyConversationFilter(rows, this.state.filter), hasMore });
|
|
1579
|
+
} catch (cause) {
|
|
1580
|
+
this.fail(cause, generation);
|
|
1581
|
+
} finally {
|
|
1582
|
+
if (this.alive(generation)) {
|
|
1583
|
+
this.refreshing = false;
|
|
1584
|
+
this.flushRefresh();
|
|
1585
|
+
}
|
|
1471
1586
|
}
|
|
1472
1587
|
};
|
|
1473
|
-
refresh = () => this.loadInitial();
|
|
1474
1588
|
loadMore = async () => {
|
|
1475
|
-
if (!this.alive() || this.state.isInitialLoading || this.state.isLoadingMore || !this.state.hasMore) return;
|
|
1589
|
+
if (!this.alive() || this.refreshing || this.state.isInitialLoading || this.state.isLoadingMore || !this.state.hasMore) return;
|
|
1476
1590
|
const generation = this.generation;
|
|
1477
1591
|
this.patch({ isLoadingMore: true, error: null });
|
|
1478
1592
|
try {
|
|
@@ -1480,7 +1594,10 @@ var ConversationListStore = class {
|
|
|
1480
1594
|
} catch (cause) {
|
|
1481
1595
|
this.fail(cause, generation);
|
|
1482
1596
|
} finally {
|
|
1483
|
-
if (this.alive(generation))
|
|
1597
|
+
if (this.alive(generation)) {
|
|
1598
|
+
this.patch({ isLoadingMore: false });
|
|
1599
|
+
this.flushRefresh();
|
|
1600
|
+
}
|
|
1484
1601
|
}
|
|
1485
1602
|
};
|
|
1486
1603
|
setFilter = async (filter) => {
|