@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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.1
|
|
4
|
+
|
|
5
|
+
- Describe a confirmed outgoing message without readers as `Sent` instead of
|
|
6
|
+
`Delivered` in the default receipt text and status icon label. ConvoKit does
|
|
7
|
+
not report recipient delivery; pending and read states are unchanged.
|
|
8
|
+
|
|
9
|
+
## 0.4.0
|
|
10
|
+
|
|
11
|
+
- Reconcile private inbox invalidations automatically, retaining filters and
|
|
12
|
+
loaded pages without clearing visible data on transient failures.
|
|
13
|
+
- Reconcile open rooms after user/room cascade changes.
|
|
14
|
+
- Replace optimistic bubbles by exact client send IDs before HTTP acknowledgement;
|
|
15
|
+
keep confirmed sends successful after a lost response, without restoring deleted rows.
|
|
16
|
+
- Custom adapters must implement `onInboxChanged` (including initial/rejoin signals)
|
|
17
|
+
and forward `clientMessageId`. Requires core 0.4.0 and the coordinated backend.
|
|
18
|
+
|
|
3
19
|
## 0.3.0
|
|
4
20
|
|
|
5
21
|
- Ignore late typing failures after newer input or connection interruption.
|
package/README.md
CHANGED
|
@@ -11,9 +11,9 @@ React package: system typography, separated rows, subtle borders, compact
|
|
|
11
11
|
actions, and restrained radii. It remains framework-CSS independent and does
|
|
12
12
|
not require Tailwind in the host application.
|
|
13
13
|
|
|
14
|
-
## 0.
|
|
14
|
+
## 0.4.0 live inbox, sending and recovery
|
|
15
15
|
|
|
16
|
-
This release requires JavaScript SDK 0.
|
|
16
|
+
This release requires JavaScript SDK 0.4.x and the coordinated backend. The core
|
|
17
17
|
SDK discovers current private room/app topics automatically and rejoins when
|
|
18
18
|
membership changes; no additional customer configuration is needed.
|
|
19
19
|
|
|
@@ -46,9 +46,9 @@ membership changes; no additional customer configuration is needed.
|
|
|
46
46
|
monotonically. A successful mark-read request does not fabricate a timestamp
|
|
47
47
|
from the device clock. Remote typing expires and clears on disconnect.
|
|
48
48
|
- Pending sends are bound to their original room/session. Reconciliation uses
|
|
49
|
-
the
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
the exact clientMessageId, sender and room, never identical text or file count.
|
|
50
|
+
A matching live/history row replaces its pending bubble immediately, even
|
|
51
|
+
before HTTP acknowledgement; a later lost response cannot fail a confirmed send.
|
|
52
52
|
- Custom `ConvoKitUiClient` adapters must implement `sessionIdentity`,
|
|
53
53
|
`onConnectionEvent` (including `onSessionEnded`), typed `onMessage`,
|
|
54
54
|
`onMessageDeleted`, `getMessage(id)`, and paired message cursors. The message
|
|
@@ -61,9 +61,17 @@ The managed API default and customer token-provider workflow are unchanged.
|
|
|
61
61
|
The SDK discovers the Supabase URL and publishable key automatically; customers
|
|
62
62
|
do not configure those values or ship a backend secret.
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
SDK-backed components consume private inbox invalidations automatically,
|
|
65
|
+
including after reconnect, while preserving filters and the loaded page window.
|
|
66
|
+
It also correlates pending messages with live/history rows using the exact
|
|
67
|
+
`clientMessageId`, so a live-first delivery never produces two bubbles.
|
|
68
|
+
These changes require the paired 0.4.0 core and coordinated backend update.
|
|
69
|
+
|
|
70
|
+
For 0.4.0, custom adapters must add
|
|
71
|
+
`onInboxChanged(handler, onError?)`, notifying on committed changes and every
|
|
72
|
+
initial/reconnected subscription. Forward `clientMessageId` unchanged in sends
|
|
73
|
+
and parsed messages. Default SDK adapters do this for you; no component props
|
|
74
|
+
or demo-specific reconciliation code is required.
|
|
67
75
|
|
|
68
76
|
## Install published packages
|
|
69
77
|
|
package/dist/index.cjs
CHANGED
|
@@ -50,6 +50,10 @@ function createConvoKitUiClient(client) {
|
|
|
50
50
|
return client.connected ? client.realtime : null;
|
|
51
51
|
},
|
|
52
52
|
onConnectionEvent: (handlers) => client.realtime.onConnectionEvent(handlers),
|
|
53
|
+
onInboxChanged: (handler, onError) => client.realtime.onInboxChanged(client.clientId, {
|
|
54
|
+
onEvent: handler,
|
|
55
|
+
...onError ? { onError } : {}
|
|
56
|
+
}),
|
|
53
57
|
onMessageDeleted: (conversationId, handler, onError) => client.realtime.onMessageDeleted(conversationId, {
|
|
54
58
|
onEvent: handler,
|
|
55
59
|
...onError ? { onError } : {}
|
|
@@ -195,6 +199,7 @@ var import_vue7 = require("vue");
|
|
|
195
199
|
var import_vue3 = require("vue");
|
|
196
200
|
|
|
197
201
|
// src/conversation-store.ts
|
|
202
|
+
var import_sdk = require("@convokitapp/sdk");
|
|
198
203
|
function version(message) {
|
|
199
204
|
return message.updatedAt?.getTime() ?? message.createdAt.getTime();
|
|
200
205
|
}
|
|
@@ -261,7 +266,7 @@ var ConversationStore = class {
|
|
|
261
266
|
// Keep tombstones until an explicit reload/session change, including across refreshes.
|
|
262
267
|
deleted = /* @__PURE__ */ new Set();
|
|
263
268
|
sendRevision;
|
|
264
|
-
|
|
269
|
+
activeSend;
|
|
265
270
|
refreshQueued = false;
|
|
266
271
|
typingTimers = /* @__PURE__ */ new Map();
|
|
267
272
|
ownTypingTimer;
|
|
@@ -276,6 +281,10 @@ var ConversationStore = class {
|
|
|
276
281
|
};
|
|
277
282
|
};
|
|
278
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
|
+
}
|
|
279
288
|
this.state = { ...this.state, ...patch };
|
|
280
289
|
for (const listener of this.listeners) listener();
|
|
281
290
|
}
|
|
@@ -319,6 +328,7 @@ var ConversationStore = class {
|
|
|
319
328
|
this.hydrationPool.queued.clear();
|
|
320
329
|
this.deleted.clear();
|
|
321
330
|
this.sendRevision = void 0;
|
|
331
|
+
this.activeSend = void 0;
|
|
322
332
|
this.refreshQueued = false;
|
|
323
333
|
}
|
|
324
334
|
dispose = () => {
|
|
@@ -359,6 +369,14 @@ var ConversationStore = class {
|
|
|
359
369
|
onError: report
|
|
360
370
|
}));
|
|
361
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
|
+
}));
|
|
362
380
|
add(() => this.client.onMessage(this.room, (event) => this.onMessage(event, generation), report));
|
|
363
381
|
add(() => this.client.onMessageDeleted(this.room, ({ id, conversationId }) => {
|
|
364
382
|
if (!this.alive(generation) || conversationId !== this.room || !id.trim()) return;
|
|
@@ -415,9 +433,19 @@ var ConversationStore = class {
|
|
|
415
433
|
record(message, insert, revision, complete) {
|
|
416
434
|
const existing = this.state.messages.find((item) => item.id === message.id);
|
|
417
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
|
+
}
|
|
418
440
|
this.changes.set(message.id, { revision, message, insert, complete });
|
|
419
441
|
if (existing || insert && hasContent(message)) this.patch({ messages: mergeMessages(this.state.messages, [message]) });
|
|
420
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
|
+
}
|
|
421
449
|
currentHydration(job) {
|
|
422
450
|
return this.alive(job.generation) && !this.deleted.has(job.message.id) && this.hydrations.get(job.message.id) === job;
|
|
423
451
|
}
|
|
@@ -663,9 +691,11 @@ var ConversationStore = class {
|
|
|
663
691
|
const generation = this.generation;
|
|
664
692
|
const revision = this.revision;
|
|
665
693
|
this.sendRevision = revision;
|
|
666
|
-
const
|
|
694
|
+
const clientMessageId = (0, import_sdk.createClientMessageId)();
|
|
695
|
+
const pendingId = `convokit-pending-${clientMessageId}`;
|
|
667
696
|
const pending = {
|
|
668
697
|
id: pendingId,
|
|
698
|
+
clientMessageId,
|
|
669
699
|
conversationId: this.room,
|
|
670
700
|
senderId: this.user,
|
|
671
701
|
text: normalized || null,
|
|
@@ -673,15 +703,19 @@ var ConversationStore = class {
|
|
|
673
703
|
createdAt: /* @__PURE__ */ new Date(),
|
|
674
704
|
updatedAt: null
|
|
675
705
|
};
|
|
706
|
+
const send = { pending };
|
|
707
|
+
this.activeSend = send;
|
|
676
708
|
this.patch({ messages: mergeMessages(this.state.messages, [pending]), isSending: true, error: null });
|
|
677
709
|
try {
|
|
678
710
|
const message = await this.client.sendMessage({
|
|
679
711
|
conversationId: this.room,
|
|
712
|
+
clientMessageId,
|
|
680
713
|
...normalized ? { text: normalized } : {},
|
|
681
714
|
...media?.length ? { media } : {}
|
|
682
715
|
});
|
|
683
716
|
if (!this.alive(generation)) return null;
|
|
684
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");
|
|
685
719
|
const live = this.changes.get(message.id);
|
|
686
720
|
const existing = this.state.messages.find((item) => item.id === message.id);
|
|
687
721
|
let latest = existing ? newest(message, existing, live?.complete !== false) : message;
|
|
@@ -696,12 +730,17 @@ var ConversationStore = class {
|
|
|
696
730
|
} catch (cause) {
|
|
697
731
|
if (this.alive(generation)) {
|
|
698
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
|
+
}
|
|
699
737
|
this.fail(cause, generation);
|
|
700
738
|
}
|
|
701
739
|
return null;
|
|
702
740
|
} finally {
|
|
703
741
|
if (this.alive(generation)) {
|
|
704
742
|
this.sendRevision = void 0;
|
|
743
|
+
this.activeSend = void 0;
|
|
705
744
|
this.patch({ isSending: false });
|
|
706
745
|
}
|
|
707
746
|
}
|
|
@@ -912,13 +951,13 @@ var MessageListView = (0, import_vue5.defineComponent)({
|
|
|
912
951
|
...mediaNodes,
|
|
913
952
|
(0, import_vue5.h)("span", { class: "ckui-message-time" }, [
|
|
914
953
|
isPending ? "Sending\u2026" : props.formatTime(message.createdAt),
|
|
915
|
-
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": "
|
|
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": "Sent" }) : null
|
|
916
955
|
])
|
|
917
956
|
]),
|
|
918
957
|
isCurrentUser && !isPending ? slots["read-receipt"]?.(receiptSlotProps) ?? (0, import_vue5.h)("div", {
|
|
919
958
|
class: partClass("receipt", currentAppearance, "ckui-read-receipt"),
|
|
920
959
|
style: partStyle("receipt", currentAppearance)
|
|
921
|
-
}, readerIds.size > 0 ? `Read by ${readerIds.size}` : "
|
|
960
|
+
}, readerIds.size > 0 ? `Read by ${readerIds.size}` : "Sent") : null
|
|
922
961
|
])
|
|
923
962
|
]);
|
|
924
963
|
};
|
|
@@ -1398,6 +1437,9 @@ var ConversationListStore = class {
|
|
|
1398
1437
|
lifecycleGeneration = 0;
|
|
1399
1438
|
disposed = true;
|
|
1400
1439
|
lifecycle;
|
|
1440
|
+
inbox;
|
|
1441
|
+
refreshQueued = false;
|
|
1442
|
+
refreshing = false;
|
|
1401
1443
|
listeners = /* @__PURE__ */ new Set();
|
|
1402
1444
|
getSnapshot = () => this.state;
|
|
1403
1445
|
subscribe = (listener) => {
|
|
@@ -1415,6 +1457,7 @@ var ConversationListStore = class {
|
|
|
1415
1457
|
}
|
|
1416
1458
|
start = (autoLoad = true) => {
|
|
1417
1459
|
if (!this.owner || this.options.client.sessionIdentity !== this.owner) return;
|
|
1460
|
+
if (!this.disposed) return;
|
|
1418
1461
|
this.disposed = false;
|
|
1419
1462
|
const lifecycleGeneration = ++this.lifecycleGeneration;
|
|
1420
1463
|
try {
|
|
@@ -1427,6 +1470,17 @@ var ConversationListStore = class {
|
|
|
1427
1470
|
});
|
|
1428
1471
|
if (this.alive()) this.lifecycle = subscription;
|
|
1429
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);
|
|
1430
1484
|
if (autoLoad) void this.loadInitial();
|
|
1431
1485
|
} catch (cause) {
|
|
1432
1486
|
if (this.alive()) this.patch({ error: cause });
|
|
@@ -1439,6 +1493,10 @@ var ConversationListStore = class {
|
|
|
1439
1493
|
const subscription = this.lifecycle;
|
|
1440
1494
|
this.lifecycle = void 0;
|
|
1441
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;
|
|
1442
1500
|
this.source = [];
|
|
1443
1501
|
this.offset = 0;
|
|
1444
1502
|
this.patch(blank2(this.state.filter));
|
|
@@ -1477,6 +1535,7 @@ var ConversationListStore = class {
|
|
|
1477
1535
|
loadInitial = async () => {
|
|
1478
1536
|
if (!this.alive()) return;
|
|
1479
1537
|
const generation = ++this.generation;
|
|
1538
|
+
this.refreshing = false;
|
|
1480
1539
|
this.source = [];
|
|
1481
1540
|
this.offset = 0;
|
|
1482
1541
|
this.patch({ ...blank2(this.state.filter), isInitialLoading: true });
|
|
@@ -1485,12 +1544,67 @@ var ConversationListStore = class {
|
|
|
1485
1544
|
} catch (cause) {
|
|
1486
1545
|
this.fail(cause, generation);
|
|
1487
1546
|
} finally {
|
|
1488
|
-
if (this.alive(generation))
|
|
1547
|
+
if (this.alive(generation)) {
|
|
1548
|
+
this.patch({ isInitialLoading: false, hasLoaded: true });
|
|
1549
|
+
this.flushRefresh();
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
};
|
|
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 });
|
|
1580
|
+
try {
|
|
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 });
|
|
1597
|
+
} catch (cause) {
|
|
1598
|
+
this.fail(cause, generation);
|
|
1599
|
+
} finally {
|
|
1600
|
+
if (this.alive(generation)) {
|
|
1601
|
+
this.refreshing = false;
|
|
1602
|
+
this.flushRefresh();
|
|
1603
|
+
}
|
|
1489
1604
|
}
|
|
1490
1605
|
};
|
|
1491
|
-
refresh = () => this.loadInitial();
|
|
1492
1606
|
loadMore = async () => {
|
|
1493
|
-
if (!this.alive() || this.state.isInitialLoading || this.state.isLoadingMore || !this.state.hasMore) return;
|
|
1607
|
+
if (!this.alive() || this.refreshing || this.state.isInitialLoading || this.state.isLoadingMore || !this.state.hasMore) return;
|
|
1494
1608
|
const generation = this.generation;
|
|
1495
1609
|
this.patch({ isLoadingMore: true, error: null });
|
|
1496
1610
|
try {
|
|
@@ -1498,7 +1612,10 @@ var ConversationListStore = class {
|
|
|
1498
1612
|
} catch (cause) {
|
|
1499
1613
|
this.fail(cause, generation);
|
|
1500
1614
|
} finally {
|
|
1501
|
-
if (this.alive(generation))
|
|
1615
|
+
if (this.alive(generation)) {
|
|
1616
|
+
this.patch({ isLoadingMore: false });
|
|
1617
|
+
this.flushRefresh();
|
|
1618
|
+
}
|
|
1502
1619
|
}
|
|
1503
1620
|
};
|
|
1504
1621
|
setFilter = async (filter) => {
|