@convokitapp/vue-ui 0.5.0 → 0.6.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 +40 -0
- package/PARITY.md +22 -0
- package/README.md +61 -1
- package/dist/index.cjs +298 -43
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +46 -1
- package/dist/index.css.map +1 -1
- package/dist/index.d.cts +98 -18
- package/dist/index.d.ts +98 -18
- package/dist/index.js +297 -43
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -35,6 +35,7 @@ __export(index_exports, {
|
|
|
35
35
|
isConvoKitPendingMessage: () => isConvoKitPendingMessage,
|
|
36
36
|
matchesConversation: () => matchesConversation,
|
|
37
37
|
mergeConversations: () => mergeConversations,
|
|
38
|
+
mergeInboxEntries: () => mergeInboxEntries,
|
|
38
39
|
mergeMessages: () => mergeMessages,
|
|
39
40
|
readerIdsFor: () => readerIdsFor,
|
|
40
41
|
useConversation: () => useConversation,
|
|
@@ -54,6 +55,10 @@ function createConvoKitUiClient(client) {
|
|
|
54
55
|
onEvent: handler,
|
|
55
56
|
...onError ? { onError } : {}
|
|
56
57
|
}),
|
|
58
|
+
onInboxActivity: (handler, onError) => client.realtime.onInboxActivity(client.clientId, {
|
|
59
|
+
onEvent: handler,
|
|
60
|
+
...onError ? { onError } : {}
|
|
61
|
+
}),
|
|
57
62
|
onMessageDeleted: (conversationId, handler, onError) => client.realtime.onMessageDeleted(conversationId, {
|
|
58
63
|
onEvent: handler,
|
|
59
64
|
...onError ? { onError } : {}
|
|
@@ -62,6 +67,7 @@ function createConvoKitUiClient(client) {
|
|
|
62
67
|
return client.connected ? client.currentUserId : "";
|
|
63
68
|
},
|
|
64
69
|
getConversations: (options) => client.getConversations(options),
|
|
70
|
+
listInbox: (options) => client.listInbox(options),
|
|
65
71
|
getConversation: (conversationId) => client.getConversation(conversationId),
|
|
66
72
|
getMessages: (options) => client.getMessages(options),
|
|
67
73
|
getMessage: (id) => client.getMessage(id),
|
|
@@ -136,6 +142,31 @@ function mergeConversations(current, incoming) {
|
|
|
136
142
|
for (const conversation of incoming) byId.set(conversation.id, conversation);
|
|
137
143
|
return [...byId.values()];
|
|
138
144
|
}
|
|
145
|
+
function compareInboxOrder(left, right) {
|
|
146
|
+
return right.activityAt.getTime() - left.activityAt.getTime() || (left.conversation.id < right.conversation.id ? 1 : left.conversation.id > right.conversation.id ? -1 : 0);
|
|
147
|
+
}
|
|
148
|
+
function mergeInboxEntries(current, incoming) {
|
|
149
|
+
const byId = new Map(current.map((entry) => [entry.conversation.id, entry]));
|
|
150
|
+
for (const entry of incoming) byId.set(entry.conversation.id, entry);
|
|
151
|
+
return [...byId.values()].sort(compareInboxOrder);
|
|
152
|
+
}
|
|
153
|
+
function inboxPreview(conversation, summary, currentUserId) {
|
|
154
|
+
const message = summary?.latestMessage;
|
|
155
|
+
if (!message) return "";
|
|
156
|
+
const first = message.media[0];
|
|
157
|
+
const body = message.text?.trim() || (!first ? "" : first.type === "image" ? "Photo" : first.type === "file" ? first.name?.trim() || "File" : first.type === "location" ? "Location" : first.type === "contact" ? "Contact" : "");
|
|
158
|
+
if (!body) return "";
|
|
159
|
+
if (currentUserId !== void 0 && message.senderId === currentUserId) return `You: ${body}`;
|
|
160
|
+
if (conversation.participants.length > 2) {
|
|
161
|
+
const sender = conversation.participants.find((participant) => participant.appUserId === message.senderId || participant.id === message.senderId);
|
|
162
|
+
const name = sender?.name.trim();
|
|
163
|
+
if (name) return `${name}: ${body}`;
|
|
164
|
+
}
|
|
165
|
+
return body;
|
|
166
|
+
}
|
|
167
|
+
function formatMessageTime(date) {
|
|
168
|
+
return new Intl.DateTimeFormat(void 0, { hour: "numeric", minute: "2-digit" }).format(date);
|
|
169
|
+
}
|
|
139
170
|
function mergeMessages(current, incoming) {
|
|
140
171
|
const byId = new Map(current.map((message) => [message.id, message]));
|
|
141
172
|
for (const message of incoming) byId.set(message.id, message);
|
|
@@ -911,9 +942,6 @@ var appearanceProps = {
|
|
|
911
942
|
density: { type: String, default: "comfortable" },
|
|
912
943
|
unstyled: { type: Boolean, default: false }
|
|
913
944
|
};
|
|
914
|
-
function defaultFormatTime(date) {
|
|
915
|
-
return new Intl.DateTimeFormat(void 0, { hour: "numeric", minute: "2-digit" }).format(date);
|
|
916
|
-
}
|
|
917
945
|
function defaultMedia(media, open, imageLoading) {
|
|
918
946
|
const tag = open ? "button" : "div";
|
|
919
947
|
const interactive = open ? { type: "button", onClick: open } : {};
|
|
@@ -964,7 +992,7 @@ var MessageListView = (0, import_vue5.defineComponent)({
|
|
|
964
992
|
paginationThreshold: { type: Number, default: 240 },
|
|
965
993
|
reverse: { type: Boolean, default: true },
|
|
966
994
|
stickToBottom: { type: Boolean, default: true },
|
|
967
|
-
formatTime: { type: Function, default:
|
|
995
|
+
formatTime: { type: Function, default: formatMessageTime },
|
|
968
996
|
imageLoading: { type: String, default: "lazy" }
|
|
969
997
|
},
|
|
970
998
|
emits: ["load-older", "attachment-click"],
|
|
@@ -1521,30 +1549,64 @@ var import_vue10 = require("vue");
|
|
|
1521
1549
|
var import_vue8 = require("vue");
|
|
1522
1550
|
|
|
1523
1551
|
// src/conversation-list-store.ts
|
|
1552
|
+
var defaultActivityRefreshWindowMs = 500;
|
|
1524
1553
|
function blank2(filter) {
|
|
1525
|
-
return {
|
|
1554
|
+
return {
|
|
1555
|
+
conversations: [],
|
|
1556
|
+
summaries: /* @__PURE__ */ new Map(),
|
|
1557
|
+
currentUserId: "",
|
|
1558
|
+
filter,
|
|
1559
|
+
isInitialLoading: false,
|
|
1560
|
+
isLoadingMore: false,
|
|
1561
|
+
hasMore: true,
|
|
1562
|
+
hasLoaded: false,
|
|
1563
|
+
error: null
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
function statusOf(cause) {
|
|
1567
|
+
return typeof cause === "object" && cause !== null && "status" in cause ? cause.status : void 0;
|
|
1568
|
+
}
|
|
1569
|
+
function summaryOf(entry) {
|
|
1570
|
+
const { conversation: _conversation, ...summary } = entry;
|
|
1571
|
+
return summary;
|
|
1572
|
+
}
|
|
1573
|
+
function ids(entries) {
|
|
1574
|
+
return entries.map((entry) => entry.conversation);
|
|
1526
1575
|
}
|
|
1527
1576
|
var ConversationListStore = class {
|
|
1528
1577
|
constructor(options) {
|
|
1529
1578
|
this.options = options;
|
|
1530
1579
|
this.owner = options.client.sessionIdentity;
|
|
1580
|
+
this.user = this.owner ? options.client.currentUserId : "";
|
|
1531
1581
|
this.pageSize = options.pageSize ?? 30;
|
|
1532
1582
|
if (!Number.isInteger(this.pageSize) || this.pageSize < 1 || this.pageSize > 100) {
|
|
1533
1583
|
throw new RangeError("pageSize must be an integer between 1 and 100");
|
|
1534
1584
|
}
|
|
1585
|
+
this.activityRefreshWindowMs = options.activityRefreshWindowMs ?? defaultActivityRefreshWindowMs;
|
|
1586
|
+
if (!Number.isFinite(this.activityRefreshWindowMs) || this.activityRefreshWindowMs < 0) {
|
|
1587
|
+
throw new RangeError("activityRefreshWindowMs must be a non-negative number");
|
|
1588
|
+
}
|
|
1535
1589
|
this.state = blank2(options.initialFilter ?? {});
|
|
1536
1590
|
}
|
|
1537
1591
|
options;
|
|
1538
1592
|
owner;
|
|
1593
|
+
user;
|
|
1539
1594
|
pageSize;
|
|
1595
|
+
activityRefreshWindowMs;
|
|
1540
1596
|
state;
|
|
1541
1597
|
source = [];
|
|
1598
|
+
entries = [];
|
|
1542
1599
|
offset = 0;
|
|
1600
|
+
cursor = null;
|
|
1601
|
+
inboxUnavailable = false;
|
|
1602
|
+
inboxWarned = false;
|
|
1543
1603
|
generation = 0;
|
|
1544
1604
|
lifecycleGeneration = 0;
|
|
1545
1605
|
disposed = true;
|
|
1546
1606
|
lifecycle;
|
|
1547
1607
|
inbox;
|
|
1608
|
+
activity;
|
|
1609
|
+
activityTimer;
|
|
1548
1610
|
refreshQueued = false;
|
|
1549
1611
|
refreshing = false;
|
|
1550
1612
|
listeners = /* @__PURE__ */ new Set();
|
|
@@ -1562,12 +1624,27 @@ var ConversationListStore = class {
|
|
|
1562
1624
|
alive(generation = this.generation) {
|
|
1563
1625
|
return !this.disposed && generation === this.generation && this.owner !== null && this.options.client.sessionIdentity === this.owner;
|
|
1564
1626
|
}
|
|
1627
|
+
get inboxMode() {
|
|
1628
|
+
return !this.options.pageLoader && typeof this.options.client.listInbox === "function" && !this.inboxUnavailable;
|
|
1629
|
+
}
|
|
1630
|
+
currentUserId() {
|
|
1631
|
+
return this.inboxMode ? this.user : "";
|
|
1632
|
+
}
|
|
1565
1633
|
start = (autoLoad = true) => {
|
|
1566
1634
|
if (!this.owner || this.options.client.sessionIdentity !== this.owner) return;
|
|
1567
1635
|
if (!this.disposed) return;
|
|
1568
1636
|
this.disposed = false;
|
|
1637
|
+
this.inboxUnavailable = false;
|
|
1569
1638
|
const lifecycleGeneration = ++this.lifecycleGeneration;
|
|
1639
|
+
const current = () => this.alive() && lifecycleGeneration === this.lifecycleGeneration;
|
|
1640
|
+
const reconcile = (cause) => {
|
|
1641
|
+
if (current()) {
|
|
1642
|
+
this.patch({ error: cause });
|
|
1643
|
+
this.queueRefresh();
|
|
1644
|
+
}
|
|
1645
|
+
};
|
|
1570
1646
|
try {
|
|
1647
|
+
this.patch({ currentUserId: this.currentUserId() });
|
|
1571
1648
|
const subscription = this.options.client.onConnectionEvent({
|
|
1572
1649
|
onEvent: () => {
|
|
1573
1650
|
},
|
|
@@ -1579,15 +1656,20 @@ var ConversationListStore = class {
|
|
|
1579
1656
|
else void subscription.unsubscribe().catch(() => void 0);
|
|
1580
1657
|
if (!this.alive()) return;
|
|
1581
1658
|
const inbox = this.options.client.onInboxChanged(() => {
|
|
1582
|
-
if (
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
this.queueRefresh();
|
|
1587
|
-
}
|
|
1588
|
-
});
|
|
1659
|
+
if (!current()) return;
|
|
1660
|
+
this.clearActivityTimer();
|
|
1661
|
+
this.queueRefresh();
|
|
1662
|
+
}, reconcile);
|
|
1589
1663
|
if (this.alive()) this.inbox = inbox;
|
|
1590
1664
|
else void inbox.unsubscribe().catch(() => void 0);
|
|
1665
|
+
if (!this.alive()) return;
|
|
1666
|
+
if (this.inboxMode && typeof this.options.client.onInboxActivity === "function") {
|
|
1667
|
+
const activity = this.options.client.onInboxActivity(() => {
|
|
1668
|
+
if (current()) this.scheduleActivityRefresh(lifecycleGeneration);
|
|
1669
|
+
}, reconcile);
|
|
1670
|
+
if (this.alive()) this.activity = activity;
|
|
1671
|
+
else void activity.unsubscribe().catch(() => void 0);
|
|
1672
|
+
}
|
|
1591
1673
|
if (autoLoad) void this.loadInitial();
|
|
1592
1674
|
} catch (cause) {
|
|
1593
1675
|
if (this.alive()) this.patch({ error: cause });
|
|
@@ -1597,27 +1679,109 @@ var ConversationListStore = class {
|
|
|
1597
1679
|
this.disposed = true;
|
|
1598
1680
|
this.generation++;
|
|
1599
1681
|
this.lifecycleGeneration++;
|
|
1682
|
+
this.clearActivityTimer();
|
|
1600
1683
|
const subscription = this.lifecycle;
|
|
1601
1684
|
this.lifecycle = void 0;
|
|
1602
1685
|
if (subscription) void subscription.unsubscribe().catch(() => void 0);
|
|
1603
1686
|
if (this.inbox) void this.inbox.unsubscribe().catch(() => void 0);
|
|
1604
1687
|
this.inbox = void 0;
|
|
1688
|
+
this.stopActivity();
|
|
1605
1689
|
this.refreshQueued = false;
|
|
1606
1690
|
this.refreshing = false;
|
|
1607
1691
|
this.source = [];
|
|
1692
|
+
this.entries = [];
|
|
1608
1693
|
this.offset = 0;
|
|
1694
|
+
this.cursor = null;
|
|
1609
1695
|
this.patch(blank2(this.state.filter));
|
|
1610
1696
|
};
|
|
1697
|
+
stopActivity() {
|
|
1698
|
+
if (this.activity) void this.activity.unsubscribe().catch(() => void 0);
|
|
1699
|
+
this.activity = void 0;
|
|
1700
|
+
}
|
|
1701
|
+
clearActivityTimer() {
|
|
1702
|
+
if (this.activityTimer !== void 0) clearTimeout(this.activityTimer);
|
|
1703
|
+
this.activityTimer = void 0;
|
|
1704
|
+
}
|
|
1705
|
+
/** Max-wait throttle: the first signal opens a window; later signals wait for it; one refresh runs when it closes. */
|
|
1706
|
+
scheduleActivityRefresh(lifecycleGeneration) {
|
|
1707
|
+
if (this.activityRefreshWindowMs === 0) return this.queueRefresh();
|
|
1708
|
+
if (this.activityTimer !== void 0) return;
|
|
1709
|
+
const timer = setTimeout(() => {
|
|
1710
|
+
this.activityTimer = void 0;
|
|
1711
|
+
if (this.alive() && lifecycleGeneration === this.lifecycleGeneration) this.queueRefresh();
|
|
1712
|
+
}, this.activityRefreshWindowMs);
|
|
1713
|
+
timer.unref?.();
|
|
1714
|
+
this.activityTimer = timer;
|
|
1715
|
+
}
|
|
1611
1716
|
fail(cause, generation) {
|
|
1612
1717
|
if (!this.alive(generation)) return;
|
|
1613
|
-
const status =
|
|
1718
|
+
const status = statusOf(cause);
|
|
1614
1719
|
if (status === 401 || status === 403 || status === 404) {
|
|
1615
1720
|
this.source = [];
|
|
1721
|
+
this.entries = [];
|
|
1616
1722
|
this.offset = 0;
|
|
1617
|
-
this.
|
|
1723
|
+
this.cursor = null;
|
|
1724
|
+
this.patch({ conversations: [], summaries: /* @__PURE__ */ new Map(), hasMore: false });
|
|
1618
1725
|
}
|
|
1619
1726
|
this.patch({ error: cause });
|
|
1620
1727
|
}
|
|
1728
|
+
/** Run an operation in inbox mode, falling back to the legacy path for the rest of this store's life when the
|
|
1729
|
+
* inbox route is absent (404: rollback, staging). Loaded rows are kept and the same operation continues.
|
|
1730
|
+
*/
|
|
1731
|
+
async withFallback(generation, inbox, legacy) {
|
|
1732
|
+
if (!this.inboxMode) return legacy();
|
|
1733
|
+
try {
|
|
1734
|
+
await inbox();
|
|
1735
|
+
} catch (cause) {
|
|
1736
|
+
if (!this.alive(generation) || statusOf(cause) !== 404) throw cause;
|
|
1737
|
+
this.inboxUnavailable = true;
|
|
1738
|
+
this.clearActivityTimer();
|
|
1739
|
+
this.stopActivity();
|
|
1740
|
+
this.entries = [];
|
|
1741
|
+
this.cursor = null;
|
|
1742
|
+
this.offset = this.source.length;
|
|
1743
|
+
if (!this.inboxWarned) {
|
|
1744
|
+
this.inboxWarned = true;
|
|
1745
|
+
console.warn("ConvoKit inbox endpoint unavailable (404); using getConversations without previews or unread counts.");
|
|
1746
|
+
}
|
|
1747
|
+
this.patch({ summaries: /* @__PURE__ */ new Map(), currentUserId: "" });
|
|
1748
|
+
await legacy();
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
validateInboxPage(page, limit, requestedCursor) {
|
|
1752
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1753
|
+
for (const entry of page.entries) {
|
|
1754
|
+
const id = entry.conversation.id;
|
|
1755
|
+
if (!id.trim() || seen.has(id)) throw new Error("Invalid conversation page");
|
|
1756
|
+
seen.add(id);
|
|
1757
|
+
}
|
|
1758
|
+
if (page.entries.length > limit) throw new Error("Invalid conversation page");
|
|
1759
|
+
if (page.nextCursor !== null && (page.nextCursor === requestedCursor || page.entries.length === 0)) {
|
|
1760
|
+
throw new Error("Inbox pagination did not advance");
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
/** Swap rows, summaries, cursor and hasMore together, filtered by the filter current at commit time. */
|
|
1764
|
+
commitInbox(entries, cursor) {
|
|
1765
|
+
this.entries = entries;
|
|
1766
|
+
this.source = ids(entries);
|
|
1767
|
+
this.cursor = cursor;
|
|
1768
|
+
this.patch({
|
|
1769
|
+
conversations: applyConversationFilter(this.source, this.state.filter),
|
|
1770
|
+
summaries: new Map(entries.map((entry) => [entry.conversation.id, summaryOf(entry)])),
|
|
1771
|
+
hasMore: cursor !== null
|
|
1772
|
+
});
|
|
1773
|
+
}
|
|
1774
|
+
async loadInboxUntilVisible(generation, filter) {
|
|
1775
|
+
const visibleBefore = applyConversationFilter(this.source, filter).length;
|
|
1776
|
+
while (this.alive(generation)) {
|
|
1777
|
+
const requested = this.cursor;
|
|
1778
|
+
const page = await this.options.client.listInbox({ limit: this.pageSize, cursor: requested, archived: filter.archived ?? false });
|
|
1779
|
+
if (!this.alive(generation)) return;
|
|
1780
|
+
this.validateInboxPage(page, this.pageSize, requested);
|
|
1781
|
+
this.commitInbox(mergeInboxEntries(this.entries, page.entries), page.nextCursor);
|
|
1782
|
+
if (page.nextCursor === null || this.state.conversations.length > visibleBefore) return;
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1621
1785
|
async loadUntilVisible(generation, filter) {
|
|
1622
1786
|
const visibleBefore = applyConversationFilter(this.source, filter).length;
|
|
1623
1787
|
while (this.alive(generation)) {
|
|
@@ -1644,10 +1808,17 @@ var ConversationListStore = class {
|
|
|
1644
1808
|
const generation = ++this.generation;
|
|
1645
1809
|
this.refreshing = false;
|
|
1646
1810
|
this.source = [];
|
|
1811
|
+
this.entries = [];
|
|
1647
1812
|
this.offset = 0;
|
|
1648
|
-
this.
|
|
1813
|
+
this.cursor = null;
|
|
1814
|
+
this.patch({ ...blank2(this.state.filter), currentUserId: this.currentUserId(), isInitialLoading: true });
|
|
1815
|
+
const filter = this.state.filter;
|
|
1649
1816
|
try {
|
|
1650
|
-
await this.
|
|
1817
|
+
await this.withFallback(
|
|
1818
|
+
generation,
|
|
1819
|
+
() => this.loadInboxUntilVisible(generation, filter),
|
|
1820
|
+
() => this.loadUntilVisible(generation, filter)
|
|
1821
|
+
);
|
|
1651
1822
|
} catch (cause) {
|
|
1652
1823
|
this.fail(cause, generation);
|
|
1653
1824
|
} finally {
|
|
@@ -1669,6 +1840,47 @@ var ConversationListStore = class {
|
|
|
1669
1840
|
void this.refresh();
|
|
1670
1841
|
});
|
|
1671
1842
|
}
|
|
1843
|
+
/** Re-walk the inbox from the head until the loaded window is covered and something is visible, or the inbox
|
|
1844
|
+
* ends. Rooms that moved are re-positioned by the merge; an exhausted inbox publishes what it found.
|
|
1845
|
+
*/
|
|
1846
|
+
async refreshInbox(generation, filter) {
|
|
1847
|
+
const target = Math.max(this.pageSize, this.entries.length);
|
|
1848
|
+
let rows = [], consumed = 0, cursor = null;
|
|
1849
|
+
while (this.alive(generation)) {
|
|
1850
|
+
const remaining = target - consumed;
|
|
1851
|
+
const limit = remaining >= 1 ? Math.min(100, remaining) : this.pageSize;
|
|
1852
|
+
const page = await this.options.client.listInbox({ limit, cursor, archived: filter.archived ?? false });
|
|
1853
|
+
if (!this.alive(generation)) return;
|
|
1854
|
+
this.validateInboxPage(page, limit, cursor);
|
|
1855
|
+
rows = mergeInboxEntries(rows, page.entries);
|
|
1856
|
+
consumed += page.entries.length;
|
|
1857
|
+
cursor = page.nextCursor;
|
|
1858
|
+
if (cursor === null || consumed >= target && applyConversationFilter(ids(rows), this.state.filter).length > 0) break;
|
|
1859
|
+
}
|
|
1860
|
+
if (!this.alive(generation)) return;
|
|
1861
|
+
this.commitInbox(rows, cursor);
|
|
1862
|
+
}
|
|
1863
|
+
async refreshLegacy(generation, filter) {
|
|
1864
|
+
const target = Math.max(this.pageSize, this.offset);
|
|
1865
|
+
const compare2 = (a, b) => a.createdAt.getTime() - b.createdAt.getTime() || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
1866
|
+
const boundary = this.options.pageLoader ? void 0 : this.source.reduce((oldest, row) => !oldest || compare2(row, oldest) < 0 ? row : oldest, void 0);
|
|
1867
|
+
let rows = [], offset = 0, hasMore = true;
|
|
1868
|
+
while (this.alive(generation)) {
|
|
1869
|
+
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 });
|
|
1870
|
+
if (!this.alive(generation)) return;
|
|
1871
|
+
if (page.length > this.pageSize || page.some((row) => !row.id.trim())) throw new Error("Invalid conversation page");
|
|
1872
|
+
const merged = mergeConversations(rows, page);
|
|
1873
|
+
if (page.length === this.pageSize && merged.length === rows.length) throw new Error("Conversation pagination did not advance");
|
|
1874
|
+
rows = merged;
|
|
1875
|
+
offset += page.length;
|
|
1876
|
+
hasMore = page.length === this.pageSize;
|
|
1877
|
+
if (!hasMore || offset >= target && applyConversationFilter(rows, this.state.filter).length > 0 && (!boundary || page.some((row) => compare2(row, boundary) <= 0))) break;
|
|
1878
|
+
}
|
|
1879
|
+
if (!this.alive(generation)) return;
|
|
1880
|
+
this.source = rows;
|
|
1881
|
+
this.offset = offset;
|
|
1882
|
+
this.patch({ conversations: applyConversationFilter(rows, this.state.filter), hasMore });
|
|
1883
|
+
}
|
|
1672
1884
|
/** Replace the loaded window atomically, retaining filters and rows during transient failures. */
|
|
1673
1885
|
refresh = async () => {
|
|
1674
1886
|
if (!this.alive()) return;
|
|
@@ -1679,28 +1891,14 @@ var ConversationListStore = class {
|
|
|
1679
1891
|
if (!this.state.hasLoaded) return this.loadInitial();
|
|
1680
1892
|
const generation = this.generation;
|
|
1681
1893
|
const filter = this.state.filter;
|
|
1682
|
-
const target = Math.max(this.pageSize, this.offset);
|
|
1683
|
-
const compare2 = (a, b) => a.createdAt.getTime() - b.createdAt.getTime() || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
1684
|
-
const boundary = this.options.pageLoader ? void 0 : this.source.reduce((oldest, row) => !oldest || compare2(row, oldest) < 0 ? row : oldest, void 0);
|
|
1685
1894
|
this.refreshing = true;
|
|
1686
1895
|
this.patch({ error: null });
|
|
1687
1896
|
try {
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
const merged = mergeConversations(rows, page);
|
|
1694
|
-
if (page.length === this.pageSize && merged.length === rows.length) throw new Error("Conversation pagination did not advance");
|
|
1695
|
-
rows = merged;
|
|
1696
|
-
offset += page.length;
|
|
1697
|
-
hasMore = page.length === this.pageSize;
|
|
1698
|
-
if (!hasMore || offset >= target && applyConversationFilter(rows, this.state.filter).length > 0 && (!boundary || page.some((row) => compare2(row, boundary) <= 0))) break;
|
|
1699
|
-
}
|
|
1700
|
-
if (!this.alive(generation)) return;
|
|
1701
|
-
this.source = rows;
|
|
1702
|
-
this.offset = offset;
|
|
1703
|
-
this.patch({ conversations: applyConversationFilter(rows, this.state.filter), hasMore });
|
|
1897
|
+
await this.withFallback(
|
|
1898
|
+
generation,
|
|
1899
|
+
() => this.refreshInbox(generation, filter),
|
|
1900
|
+
() => this.refreshLegacy(generation, filter)
|
|
1901
|
+
);
|
|
1704
1902
|
} catch (cause) {
|
|
1705
1903
|
this.fail(cause, generation);
|
|
1706
1904
|
} finally {
|
|
@@ -1713,9 +1911,14 @@ var ConversationListStore = class {
|
|
|
1713
1911
|
loadMore = async () => {
|
|
1714
1912
|
if (!this.alive() || this.refreshing || this.state.isInitialLoading || this.state.isLoadingMore || !this.state.hasMore) return;
|
|
1715
1913
|
const generation = this.generation;
|
|
1914
|
+
const filter = this.state.filter;
|
|
1716
1915
|
this.patch({ isLoadingMore: true, error: null });
|
|
1717
1916
|
try {
|
|
1718
|
-
await this.
|
|
1917
|
+
await this.withFallback(
|
|
1918
|
+
generation,
|
|
1919
|
+
() => this.loadInboxUntilVisible(generation, filter),
|
|
1920
|
+
() => this.loadUntilVisible(generation, filter)
|
|
1921
|
+
);
|
|
1719
1922
|
} catch (cause) {
|
|
1720
1923
|
this.fail(cause, generation);
|
|
1721
1924
|
} finally {
|
|
@@ -1767,6 +1970,8 @@ function useConversationList(options) {
|
|
|
1767
1970
|
});
|
|
1768
1971
|
return {
|
|
1769
1972
|
conversations: field("conversations"),
|
|
1973
|
+
summaries: field("summaries"),
|
|
1974
|
+
currentUserId: field("currentUserId"),
|
|
1770
1975
|
filter: field("filter"),
|
|
1771
1976
|
isInitialLoading: field("isInitialLoading"),
|
|
1772
1977
|
isLoadingMore: field("isLoadingMore"),
|
|
@@ -1792,6 +1997,8 @@ var appearanceProps3 = {
|
|
|
1792
1997
|
var listViewProps = {
|
|
1793
1998
|
...appearanceProps3,
|
|
1794
1999
|
conversations: { type: Array, required: true },
|
|
2000
|
+
summaries: { type: Object, default: void 0 },
|
|
2001
|
+
currentUserId: { type: String, default: void 0 },
|
|
1795
2002
|
selectedConversationId: { type: String, default: void 0 },
|
|
1796
2003
|
onConversationSelect: { type: Function, default: void 0 },
|
|
1797
2004
|
onRefresh: { type: Function, default: void 0 },
|
|
@@ -1841,6 +2048,25 @@ var ConversationListView = (0, import_vue10.defineComponent)({
|
|
|
1841
2048
|
const refresh = () => {
|
|
1842
2049
|
return props.onRefresh?.();
|
|
1843
2050
|
};
|
|
2051
|
+
const inlineRetry = () => {
|
|
2052
|
+
if (props.hasMore && props.onLoadMore) return () => {
|
|
2053
|
+
lastRequestedLength = null;
|
|
2054
|
+
void requestMore();
|
|
2055
|
+
};
|
|
2056
|
+
if (props.onRefresh) return () => {
|
|
2057
|
+
void refresh();
|
|
2058
|
+
};
|
|
2059
|
+
return void 0;
|
|
2060
|
+
};
|
|
2061
|
+
const unreadBadge = (summary) => {
|
|
2062
|
+
if (summary.unreadCount <= 0 && !summary.unreadCountCapped) return null;
|
|
2063
|
+
const capped = summary.unreadCountCapped || summary.unreadCount > 99;
|
|
2064
|
+
return (0, import_vue10.h)("span", {
|
|
2065
|
+
class: "ckui-unread-badge",
|
|
2066
|
+
role: "img",
|
|
2067
|
+
"aria-label": `${summary.unreadCountCapped ? "99+" : summary.unreadCount} unread`
|
|
2068
|
+
}, [(0, import_vue10.h)("span", { "aria-hidden": "true" }, capped ? "99+" : String(summary.unreadCount))]);
|
|
2069
|
+
};
|
|
1844
2070
|
const renderContent = () => {
|
|
1845
2071
|
const currentAppearance = appearance();
|
|
1846
2072
|
if (props.isInitialLoading && props.conversations.length === 0) {
|
|
@@ -1872,10 +2098,21 @@ var ConversationListView = (0, import_vue10.defineComponent)({
|
|
|
1872
2098
|
const children = props.conversations.flatMap((conversation, index) => {
|
|
1873
2099
|
const selected = props.selectedConversationId === conversation.id;
|
|
1874
2100
|
const select = () => selectConversation(conversation);
|
|
1875
|
-
const
|
|
2101
|
+
const summary = props.summaries?.get(conversation.id);
|
|
2102
|
+
const slotProps = {
|
|
2103
|
+
conversation,
|
|
2104
|
+
index,
|
|
2105
|
+
selected,
|
|
2106
|
+
select,
|
|
2107
|
+
...summary ? { summary } : {},
|
|
2108
|
+
...props.currentUserId === void 0 ? {} : { currentUserId: props.currentUserId }
|
|
2109
|
+
};
|
|
2110
|
+
const preview = inboxPreview(conversation, summary, props.currentUserId);
|
|
2111
|
+
const unread = summary !== void 0 && (summary.unreadCount > 0 || summary.unreadCountCapped);
|
|
1876
2112
|
const item = slots["conversation-item"]?.(slotProps) ?? (0, import_vue10.h)("button", {
|
|
1877
2113
|
type: "button",
|
|
1878
2114
|
"data-selected": selected || void 0,
|
|
2115
|
+
"data-unread": unread || void 0,
|
|
1879
2116
|
"aria-current": selected ? "true" : void 0,
|
|
1880
2117
|
onClick: select,
|
|
1881
2118
|
class: partClass("listItem", currentAppearance, "ckui-conversation-item"),
|
|
@@ -1889,8 +2126,14 @@ var ConversationListView = (0, import_vue10.defineComponent)({
|
|
|
1889
2126
|
}),
|
|
1890
2127
|
(0, import_vue10.h)("span", { class: "ckui-conversation-item__body" }, [
|
|
1891
2128
|
(0, import_vue10.h)("strong", conversation.displayTitle),
|
|
1892
|
-
(0, import_vue10.h)("span", conversation.participants.map((participant) => participant.name).join(", ") || conversation.description || "No participants")
|
|
2129
|
+
(0, import_vue10.h)("span", preview || conversation.participants.map((participant) => participant.name).join(", ") || conversation.description || "No participants")
|
|
1893
2130
|
]),
|
|
2131
|
+
// Spread rather than emit `null`: a null child renders a `<!---->` comment, and rows without a
|
|
2132
|
+
// summary must keep 0.5's exact markup.
|
|
2133
|
+
...summary ? [(0, import_vue10.h)("span", { class: "ckui-conversation-item__meta" }, [
|
|
2134
|
+
(0, import_vue10.h)("time", { class: "ckui-conversation-item__time", datetime: summary.activityAt.toISOString() }, formatMessageTime(summary.activityAt)),
|
|
2135
|
+
unreadBadge(summary)
|
|
2136
|
+
])] : [],
|
|
1894
2137
|
(0, import_vue10.h)(import_vue9.ChevronRight, { size: 18, "aria-hidden": "true" })
|
|
1895
2138
|
]);
|
|
1896
2139
|
const nodes = [(0, import_vue10.h)("div", { key: conversation.id, role: "listitem" }, [item])];
|
|
@@ -1900,13 +2143,15 @@ var ConversationListView = (0, import_vue10.defineComponent)({
|
|
|
1900
2143
|
return nodes;
|
|
1901
2144
|
});
|
|
1902
2145
|
if (props.error) {
|
|
1903
|
-
|
|
2146
|
+
const retry = inlineRetry();
|
|
2147
|
+
children.push(slots.error?.({ error: props.error, ...retry ? { retry } : {} }) ?? (0, import_vue10.h)("div", {
|
|
1904
2148
|
class: partClass("error", currentAppearance, "ckui-inline-state ckui-state--error"),
|
|
1905
2149
|
style: partStyle("error", currentAppearance),
|
|
1906
2150
|
role: "alert"
|
|
1907
|
-
}, [
|
|
1908
|
-
|
|
1909
|
-
|
|
2151
|
+
}, [
|
|
2152
|
+
(0, import_vue10.h)("span", errorMessage(props.error)),
|
|
2153
|
+
...retry ? [(0, import_vue10.h)("button", { type: "button", class: "ckui-link-button", onClick: retry }, "Retry")] : []
|
|
2154
|
+
]));
|
|
1910
2155
|
} else if (props.isLoadingMore) {
|
|
1911
2156
|
children.push(slots["load-more"]?.() ?? (0, import_vue10.h)("div", {
|
|
1912
2157
|
class: partClass("loading", currentAppearance, "ckui-inline-state"),
|
|
@@ -1967,6 +2212,7 @@ var ConversationList = (0, import_vue10.defineComponent)({
|
|
|
1967
2212
|
initialFilter: { type: Object, default: void 0 },
|
|
1968
2213
|
pageSize: { type: Number, default: 30 },
|
|
1969
2214
|
autoLoad: { type: Boolean, default: true },
|
|
2215
|
+
activityRefreshWindowMs: { type: Number, default: void 0 },
|
|
1970
2216
|
onControllerChange: { type: Function, default: void 0 }
|
|
1971
2217
|
},
|
|
1972
2218
|
emits: ["conversation-select", "controller-change"],
|
|
@@ -1976,7 +2222,8 @@ var ConversationList = (0, import_vue10.defineComponent)({
|
|
|
1976
2222
|
...props.pageLoader ? { pageLoader: props.pageLoader } : {},
|
|
1977
2223
|
...props.initialFilter ? { initialFilter: props.initialFilter } : {},
|
|
1978
2224
|
pageSize: props.pageSize,
|
|
1979
|
-
autoLoad: props.autoLoad
|
|
2225
|
+
autoLoad: props.autoLoad,
|
|
2226
|
+
...props.activityRefreshWindowMs === void 0 ? {} : { activityRefreshWindowMs: props.activityRefreshWindowMs }
|
|
1980
2227
|
});
|
|
1981
2228
|
expose({ controller });
|
|
1982
2229
|
(0, import_vue10.watchEffect)(() => {
|
|
@@ -1989,8 +2236,11 @@ var ConversationList = (0, import_vue10.defineComponent)({
|
|
|
1989
2236
|
initialFilter: _initialFilter,
|
|
1990
2237
|
pageSize: _pageSize,
|
|
1991
2238
|
autoLoad: _autoLoad,
|
|
2239
|
+
activityRefreshWindowMs: _activityRefreshWindowMs,
|
|
1992
2240
|
onControllerChange: _onControllerChange,
|
|
1993
2241
|
conversations: _conversations,
|
|
2242
|
+
summaries: _summaries,
|
|
2243
|
+
currentUserId: _currentUserId,
|
|
1994
2244
|
onRefresh: _onRefresh,
|
|
1995
2245
|
onLoadMore: _onLoadMore,
|
|
1996
2246
|
isInitialLoading: _isInitialLoading,
|
|
@@ -2003,6 +2253,8 @@ var ConversationList = (0, import_vue10.defineComponent)({
|
|
|
2003
2253
|
...attrs,
|
|
2004
2254
|
...forwarded,
|
|
2005
2255
|
conversations: controller.conversations.value,
|
|
2256
|
+
summaries: controller.summaries.value,
|
|
2257
|
+
currentUserId: controller.currentUserId.value,
|
|
2006
2258
|
onRefresh: controller.refresh,
|
|
2007
2259
|
onLoadMore: controller.loadMore,
|
|
2008
2260
|
isInitialLoading: controller.isInitialLoading.value,
|
|
@@ -2030,6 +2282,7 @@ var defaultConvoKitTheme = {
|
|
|
2030
2282
|
incomingBubble: "#f4f4f5",
|
|
2031
2283
|
outgoingBubble: "#18181b",
|
|
2032
2284
|
outgoingText: "#fafafa",
|
|
2285
|
+
badge: "#18181b",
|
|
2033
2286
|
radius: "10px",
|
|
2034
2287
|
avatarSize: "40px",
|
|
2035
2288
|
fontFamily: 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'
|
|
@@ -2061,6 +2314,7 @@ var ConvoKitThemeProvider = (0, import_vue11.defineComponent)({
|
|
|
2061
2314
|
"--ckui-incoming": theme.incomingBubble,
|
|
2062
2315
|
"--ckui-outgoing": theme.outgoingBubble,
|
|
2063
2316
|
"--ckui-outgoing-text": theme.outgoingText,
|
|
2317
|
+
"--ckui-badge": theme.badge,
|
|
2064
2318
|
"--ckui-radius": theme.radius,
|
|
2065
2319
|
"--ckui-avatar-size": theme.avatarSize,
|
|
2066
2320
|
"--ckui-font": theme.fontFamily
|
|
@@ -2093,6 +2347,7 @@ function useConvoKitTheme() {
|
|
|
2093
2347
|
isConvoKitPendingMessage,
|
|
2094
2348
|
matchesConversation,
|
|
2095
2349
|
mergeConversations,
|
|
2350
|
+
mergeInboxEntries,
|
|
2096
2351
|
mergeMessages,
|
|
2097
2352
|
readerIdsFor,
|
|
2098
2353
|
useConversation,
|