@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.js
CHANGED
|
@@ -9,6 +9,10 @@ function createConvoKitUiClient(client) {
|
|
|
9
9
|
onEvent: handler,
|
|
10
10
|
...onError ? { onError } : {}
|
|
11
11
|
}),
|
|
12
|
+
onInboxActivity: (handler, onError) => client.realtime.onInboxActivity(client.clientId, {
|
|
13
|
+
onEvent: handler,
|
|
14
|
+
...onError ? { onError } : {}
|
|
15
|
+
}),
|
|
12
16
|
onMessageDeleted: (conversationId, handler, onError) => client.realtime.onMessageDeleted(conversationId, {
|
|
13
17
|
onEvent: handler,
|
|
14
18
|
...onError ? { onError } : {}
|
|
@@ -17,6 +21,7 @@ function createConvoKitUiClient(client) {
|
|
|
17
21
|
return client.connected ? client.currentUserId : "";
|
|
18
22
|
},
|
|
19
23
|
getConversations: (options) => client.getConversations(options),
|
|
24
|
+
listInbox: (options) => client.listInbox(options),
|
|
20
25
|
getConversation: (conversationId) => client.getConversation(conversationId),
|
|
21
26
|
getMessages: (options) => client.getMessages(options),
|
|
22
27
|
getMessage: (id) => client.getMessage(id),
|
|
@@ -91,6 +96,31 @@ function mergeConversations(current, incoming) {
|
|
|
91
96
|
for (const conversation of incoming) byId.set(conversation.id, conversation);
|
|
92
97
|
return [...byId.values()];
|
|
93
98
|
}
|
|
99
|
+
function compareInboxOrder(left, right) {
|
|
100
|
+
return right.activityAt.getTime() - left.activityAt.getTime() || (left.conversation.id < right.conversation.id ? 1 : left.conversation.id > right.conversation.id ? -1 : 0);
|
|
101
|
+
}
|
|
102
|
+
function mergeInboxEntries(current, incoming) {
|
|
103
|
+
const byId = new Map(current.map((entry) => [entry.conversation.id, entry]));
|
|
104
|
+
for (const entry of incoming) byId.set(entry.conversation.id, entry);
|
|
105
|
+
return [...byId.values()].sort(compareInboxOrder);
|
|
106
|
+
}
|
|
107
|
+
function inboxPreview(conversation, summary, currentUserId) {
|
|
108
|
+
const message = summary?.latestMessage;
|
|
109
|
+
if (!message) return "";
|
|
110
|
+
const first = message.media[0];
|
|
111
|
+
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" : "");
|
|
112
|
+
if (!body) return "";
|
|
113
|
+
if (currentUserId !== void 0 && message.senderId === currentUserId) return `You: ${body}`;
|
|
114
|
+
if (conversation.participants.length > 2) {
|
|
115
|
+
const sender = conversation.participants.find((participant) => participant.appUserId === message.senderId || participant.id === message.senderId);
|
|
116
|
+
const name = sender?.name.trim();
|
|
117
|
+
if (name) return `${name}: ${body}`;
|
|
118
|
+
}
|
|
119
|
+
return body;
|
|
120
|
+
}
|
|
121
|
+
function formatMessageTime(date) {
|
|
122
|
+
return new Intl.DateTimeFormat(void 0, { hour: "numeric", minute: "2-digit" }).format(date);
|
|
123
|
+
}
|
|
94
124
|
function mergeMessages(current, incoming) {
|
|
95
125
|
const byId = new Map(current.map((message) => [message.id, message]));
|
|
96
126
|
for (const message of incoming) byId.set(message.id, message);
|
|
@@ -889,9 +919,6 @@ var appearanceProps = {
|
|
|
889
919
|
density: { type: String, default: "comfortable" },
|
|
890
920
|
unstyled: { type: Boolean, default: false }
|
|
891
921
|
};
|
|
892
|
-
function defaultFormatTime(date) {
|
|
893
|
-
return new Intl.DateTimeFormat(void 0, { hour: "numeric", minute: "2-digit" }).format(date);
|
|
894
|
-
}
|
|
895
922
|
function defaultMedia(media, open, imageLoading) {
|
|
896
923
|
const tag = open ? "button" : "div";
|
|
897
924
|
const interactive = open ? { type: "button", onClick: open } : {};
|
|
@@ -942,7 +969,7 @@ var MessageListView = defineComponent2({
|
|
|
942
969
|
paginationThreshold: { type: Number, default: 240 },
|
|
943
970
|
reverse: { type: Boolean, default: true },
|
|
944
971
|
stickToBottom: { type: Boolean, default: true },
|
|
945
|
-
formatTime: { type: Function, default:
|
|
972
|
+
formatTime: { type: Function, default: formatMessageTime },
|
|
946
973
|
imageLoading: { type: String, default: "lazy" }
|
|
947
974
|
},
|
|
948
975
|
emits: ["load-older", "attachment-click"],
|
|
@@ -1504,30 +1531,64 @@ import {
|
|
|
1504
1531
|
import { computed as computed3, getCurrentScope as getCurrentScope2, onScopeDispose as onScopeDispose2, shallowRef as shallowRef2, toValue as toValue2, watch as watch3 } from "vue";
|
|
1505
1532
|
|
|
1506
1533
|
// src/conversation-list-store.ts
|
|
1534
|
+
var defaultActivityRefreshWindowMs = 500;
|
|
1507
1535
|
function blank2(filter) {
|
|
1508
|
-
return {
|
|
1536
|
+
return {
|
|
1537
|
+
conversations: [],
|
|
1538
|
+
summaries: /* @__PURE__ */ new Map(),
|
|
1539
|
+
currentUserId: "",
|
|
1540
|
+
filter,
|
|
1541
|
+
isInitialLoading: false,
|
|
1542
|
+
isLoadingMore: false,
|
|
1543
|
+
hasMore: true,
|
|
1544
|
+
hasLoaded: false,
|
|
1545
|
+
error: null
|
|
1546
|
+
};
|
|
1547
|
+
}
|
|
1548
|
+
function statusOf(cause) {
|
|
1549
|
+
return typeof cause === "object" && cause !== null && "status" in cause ? cause.status : void 0;
|
|
1550
|
+
}
|
|
1551
|
+
function summaryOf(entry) {
|
|
1552
|
+
const { conversation: _conversation, ...summary } = entry;
|
|
1553
|
+
return summary;
|
|
1554
|
+
}
|
|
1555
|
+
function ids(entries) {
|
|
1556
|
+
return entries.map((entry) => entry.conversation);
|
|
1509
1557
|
}
|
|
1510
1558
|
var ConversationListStore = class {
|
|
1511
1559
|
constructor(options) {
|
|
1512
1560
|
this.options = options;
|
|
1513
1561
|
this.owner = options.client.sessionIdentity;
|
|
1562
|
+
this.user = this.owner ? options.client.currentUserId : "";
|
|
1514
1563
|
this.pageSize = options.pageSize ?? 30;
|
|
1515
1564
|
if (!Number.isInteger(this.pageSize) || this.pageSize < 1 || this.pageSize > 100) {
|
|
1516
1565
|
throw new RangeError("pageSize must be an integer between 1 and 100");
|
|
1517
1566
|
}
|
|
1567
|
+
this.activityRefreshWindowMs = options.activityRefreshWindowMs ?? defaultActivityRefreshWindowMs;
|
|
1568
|
+
if (!Number.isFinite(this.activityRefreshWindowMs) || this.activityRefreshWindowMs < 0) {
|
|
1569
|
+
throw new RangeError("activityRefreshWindowMs must be a non-negative number");
|
|
1570
|
+
}
|
|
1518
1571
|
this.state = blank2(options.initialFilter ?? {});
|
|
1519
1572
|
}
|
|
1520
1573
|
options;
|
|
1521
1574
|
owner;
|
|
1575
|
+
user;
|
|
1522
1576
|
pageSize;
|
|
1577
|
+
activityRefreshWindowMs;
|
|
1523
1578
|
state;
|
|
1524
1579
|
source = [];
|
|
1580
|
+
entries = [];
|
|
1525
1581
|
offset = 0;
|
|
1582
|
+
cursor = null;
|
|
1583
|
+
inboxUnavailable = false;
|
|
1584
|
+
inboxWarned = false;
|
|
1526
1585
|
generation = 0;
|
|
1527
1586
|
lifecycleGeneration = 0;
|
|
1528
1587
|
disposed = true;
|
|
1529
1588
|
lifecycle;
|
|
1530
1589
|
inbox;
|
|
1590
|
+
activity;
|
|
1591
|
+
activityTimer;
|
|
1531
1592
|
refreshQueued = false;
|
|
1532
1593
|
refreshing = false;
|
|
1533
1594
|
listeners = /* @__PURE__ */ new Set();
|
|
@@ -1545,12 +1606,27 @@ var ConversationListStore = class {
|
|
|
1545
1606
|
alive(generation = this.generation) {
|
|
1546
1607
|
return !this.disposed && generation === this.generation && this.owner !== null && this.options.client.sessionIdentity === this.owner;
|
|
1547
1608
|
}
|
|
1609
|
+
get inboxMode() {
|
|
1610
|
+
return !this.options.pageLoader && typeof this.options.client.listInbox === "function" && !this.inboxUnavailable;
|
|
1611
|
+
}
|
|
1612
|
+
currentUserId() {
|
|
1613
|
+
return this.inboxMode ? this.user : "";
|
|
1614
|
+
}
|
|
1548
1615
|
start = (autoLoad = true) => {
|
|
1549
1616
|
if (!this.owner || this.options.client.sessionIdentity !== this.owner) return;
|
|
1550
1617
|
if (!this.disposed) return;
|
|
1551
1618
|
this.disposed = false;
|
|
1619
|
+
this.inboxUnavailable = false;
|
|
1552
1620
|
const lifecycleGeneration = ++this.lifecycleGeneration;
|
|
1621
|
+
const current = () => this.alive() && lifecycleGeneration === this.lifecycleGeneration;
|
|
1622
|
+
const reconcile = (cause) => {
|
|
1623
|
+
if (current()) {
|
|
1624
|
+
this.patch({ error: cause });
|
|
1625
|
+
this.queueRefresh();
|
|
1626
|
+
}
|
|
1627
|
+
};
|
|
1553
1628
|
try {
|
|
1629
|
+
this.patch({ currentUserId: this.currentUserId() });
|
|
1554
1630
|
const subscription = this.options.client.onConnectionEvent({
|
|
1555
1631
|
onEvent: () => {
|
|
1556
1632
|
},
|
|
@@ -1562,15 +1638,20 @@ var ConversationListStore = class {
|
|
|
1562
1638
|
else void subscription.unsubscribe().catch(() => void 0);
|
|
1563
1639
|
if (!this.alive()) return;
|
|
1564
1640
|
const inbox = this.options.client.onInboxChanged(() => {
|
|
1565
|
-
if (
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
this.queueRefresh();
|
|
1570
|
-
}
|
|
1571
|
-
});
|
|
1641
|
+
if (!current()) return;
|
|
1642
|
+
this.clearActivityTimer();
|
|
1643
|
+
this.queueRefresh();
|
|
1644
|
+
}, reconcile);
|
|
1572
1645
|
if (this.alive()) this.inbox = inbox;
|
|
1573
1646
|
else void inbox.unsubscribe().catch(() => void 0);
|
|
1647
|
+
if (!this.alive()) return;
|
|
1648
|
+
if (this.inboxMode && typeof this.options.client.onInboxActivity === "function") {
|
|
1649
|
+
const activity = this.options.client.onInboxActivity(() => {
|
|
1650
|
+
if (current()) this.scheduleActivityRefresh(lifecycleGeneration);
|
|
1651
|
+
}, reconcile);
|
|
1652
|
+
if (this.alive()) this.activity = activity;
|
|
1653
|
+
else void activity.unsubscribe().catch(() => void 0);
|
|
1654
|
+
}
|
|
1574
1655
|
if (autoLoad) void this.loadInitial();
|
|
1575
1656
|
} catch (cause) {
|
|
1576
1657
|
if (this.alive()) this.patch({ error: cause });
|
|
@@ -1580,27 +1661,109 @@ var ConversationListStore = class {
|
|
|
1580
1661
|
this.disposed = true;
|
|
1581
1662
|
this.generation++;
|
|
1582
1663
|
this.lifecycleGeneration++;
|
|
1664
|
+
this.clearActivityTimer();
|
|
1583
1665
|
const subscription = this.lifecycle;
|
|
1584
1666
|
this.lifecycle = void 0;
|
|
1585
1667
|
if (subscription) void subscription.unsubscribe().catch(() => void 0);
|
|
1586
1668
|
if (this.inbox) void this.inbox.unsubscribe().catch(() => void 0);
|
|
1587
1669
|
this.inbox = void 0;
|
|
1670
|
+
this.stopActivity();
|
|
1588
1671
|
this.refreshQueued = false;
|
|
1589
1672
|
this.refreshing = false;
|
|
1590
1673
|
this.source = [];
|
|
1674
|
+
this.entries = [];
|
|
1591
1675
|
this.offset = 0;
|
|
1676
|
+
this.cursor = null;
|
|
1592
1677
|
this.patch(blank2(this.state.filter));
|
|
1593
1678
|
};
|
|
1679
|
+
stopActivity() {
|
|
1680
|
+
if (this.activity) void this.activity.unsubscribe().catch(() => void 0);
|
|
1681
|
+
this.activity = void 0;
|
|
1682
|
+
}
|
|
1683
|
+
clearActivityTimer() {
|
|
1684
|
+
if (this.activityTimer !== void 0) clearTimeout(this.activityTimer);
|
|
1685
|
+
this.activityTimer = void 0;
|
|
1686
|
+
}
|
|
1687
|
+
/** Max-wait throttle: the first signal opens a window; later signals wait for it; one refresh runs when it closes. */
|
|
1688
|
+
scheduleActivityRefresh(lifecycleGeneration) {
|
|
1689
|
+
if (this.activityRefreshWindowMs === 0) return this.queueRefresh();
|
|
1690
|
+
if (this.activityTimer !== void 0) return;
|
|
1691
|
+
const timer = setTimeout(() => {
|
|
1692
|
+
this.activityTimer = void 0;
|
|
1693
|
+
if (this.alive() && lifecycleGeneration === this.lifecycleGeneration) this.queueRefresh();
|
|
1694
|
+
}, this.activityRefreshWindowMs);
|
|
1695
|
+
timer.unref?.();
|
|
1696
|
+
this.activityTimer = timer;
|
|
1697
|
+
}
|
|
1594
1698
|
fail(cause, generation) {
|
|
1595
1699
|
if (!this.alive(generation)) return;
|
|
1596
|
-
const status =
|
|
1700
|
+
const status = statusOf(cause);
|
|
1597
1701
|
if (status === 401 || status === 403 || status === 404) {
|
|
1598
1702
|
this.source = [];
|
|
1703
|
+
this.entries = [];
|
|
1599
1704
|
this.offset = 0;
|
|
1600
|
-
this.
|
|
1705
|
+
this.cursor = null;
|
|
1706
|
+
this.patch({ conversations: [], summaries: /* @__PURE__ */ new Map(), hasMore: false });
|
|
1601
1707
|
}
|
|
1602
1708
|
this.patch({ error: cause });
|
|
1603
1709
|
}
|
|
1710
|
+
/** Run an operation in inbox mode, falling back to the legacy path for the rest of this store's life when the
|
|
1711
|
+
* inbox route is absent (404: rollback, staging). Loaded rows are kept and the same operation continues.
|
|
1712
|
+
*/
|
|
1713
|
+
async withFallback(generation, inbox, legacy) {
|
|
1714
|
+
if (!this.inboxMode) return legacy();
|
|
1715
|
+
try {
|
|
1716
|
+
await inbox();
|
|
1717
|
+
} catch (cause) {
|
|
1718
|
+
if (!this.alive(generation) || statusOf(cause) !== 404) throw cause;
|
|
1719
|
+
this.inboxUnavailable = true;
|
|
1720
|
+
this.clearActivityTimer();
|
|
1721
|
+
this.stopActivity();
|
|
1722
|
+
this.entries = [];
|
|
1723
|
+
this.cursor = null;
|
|
1724
|
+
this.offset = this.source.length;
|
|
1725
|
+
if (!this.inboxWarned) {
|
|
1726
|
+
this.inboxWarned = true;
|
|
1727
|
+
console.warn("ConvoKit inbox endpoint unavailable (404); using getConversations without previews or unread counts.");
|
|
1728
|
+
}
|
|
1729
|
+
this.patch({ summaries: /* @__PURE__ */ new Map(), currentUserId: "" });
|
|
1730
|
+
await legacy();
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
validateInboxPage(page, limit, requestedCursor) {
|
|
1734
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1735
|
+
for (const entry of page.entries) {
|
|
1736
|
+
const id = entry.conversation.id;
|
|
1737
|
+
if (!id.trim() || seen.has(id)) throw new Error("Invalid conversation page");
|
|
1738
|
+
seen.add(id);
|
|
1739
|
+
}
|
|
1740
|
+
if (page.entries.length > limit) throw new Error("Invalid conversation page");
|
|
1741
|
+
if (page.nextCursor !== null && (page.nextCursor === requestedCursor || page.entries.length === 0)) {
|
|
1742
|
+
throw new Error("Inbox pagination did not advance");
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
/** Swap rows, summaries, cursor and hasMore together, filtered by the filter current at commit time. */
|
|
1746
|
+
commitInbox(entries, cursor) {
|
|
1747
|
+
this.entries = entries;
|
|
1748
|
+
this.source = ids(entries);
|
|
1749
|
+
this.cursor = cursor;
|
|
1750
|
+
this.patch({
|
|
1751
|
+
conversations: applyConversationFilter(this.source, this.state.filter),
|
|
1752
|
+
summaries: new Map(entries.map((entry) => [entry.conversation.id, summaryOf(entry)])),
|
|
1753
|
+
hasMore: cursor !== null
|
|
1754
|
+
});
|
|
1755
|
+
}
|
|
1756
|
+
async loadInboxUntilVisible(generation, filter) {
|
|
1757
|
+
const visibleBefore = applyConversationFilter(this.source, filter).length;
|
|
1758
|
+
while (this.alive(generation)) {
|
|
1759
|
+
const requested = this.cursor;
|
|
1760
|
+
const page = await this.options.client.listInbox({ limit: this.pageSize, cursor: requested, archived: filter.archived ?? false });
|
|
1761
|
+
if (!this.alive(generation)) return;
|
|
1762
|
+
this.validateInboxPage(page, this.pageSize, requested);
|
|
1763
|
+
this.commitInbox(mergeInboxEntries(this.entries, page.entries), page.nextCursor);
|
|
1764
|
+
if (page.nextCursor === null || this.state.conversations.length > visibleBefore) return;
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1604
1767
|
async loadUntilVisible(generation, filter) {
|
|
1605
1768
|
const visibleBefore = applyConversationFilter(this.source, filter).length;
|
|
1606
1769
|
while (this.alive(generation)) {
|
|
@@ -1627,10 +1790,17 @@ var ConversationListStore = class {
|
|
|
1627
1790
|
const generation = ++this.generation;
|
|
1628
1791
|
this.refreshing = false;
|
|
1629
1792
|
this.source = [];
|
|
1793
|
+
this.entries = [];
|
|
1630
1794
|
this.offset = 0;
|
|
1631
|
-
this.
|
|
1795
|
+
this.cursor = null;
|
|
1796
|
+
this.patch({ ...blank2(this.state.filter), currentUserId: this.currentUserId(), isInitialLoading: true });
|
|
1797
|
+
const filter = this.state.filter;
|
|
1632
1798
|
try {
|
|
1633
|
-
await this.
|
|
1799
|
+
await this.withFallback(
|
|
1800
|
+
generation,
|
|
1801
|
+
() => this.loadInboxUntilVisible(generation, filter),
|
|
1802
|
+
() => this.loadUntilVisible(generation, filter)
|
|
1803
|
+
);
|
|
1634
1804
|
} catch (cause) {
|
|
1635
1805
|
this.fail(cause, generation);
|
|
1636
1806
|
} finally {
|
|
@@ -1652,6 +1822,47 @@ var ConversationListStore = class {
|
|
|
1652
1822
|
void this.refresh();
|
|
1653
1823
|
});
|
|
1654
1824
|
}
|
|
1825
|
+
/** Re-walk the inbox from the head until the loaded window is covered and something is visible, or the inbox
|
|
1826
|
+
* ends. Rooms that moved are re-positioned by the merge; an exhausted inbox publishes what it found.
|
|
1827
|
+
*/
|
|
1828
|
+
async refreshInbox(generation, filter) {
|
|
1829
|
+
const target = Math.max(this.pageSize, this.entries.length);
|
|
1830
|
+
let rows = [], consumed = 0, cursor = null;
|
|
1831
|
+
while (this.alive(generation)) {
|
|
1832
|
+
const remaining = target - consumed;
|
|
1833
|
+
const limit = remaining >= 1 ? Math.min(100, remaining) : this.pageSize;
|
|
1834
|
+
const page = await this.options.client.listInbox({ limit, cursor, archived: filter.archived ?? false });
|
|
1835
|
+
if (!this.alive(generation)) return;
|
|
1836
|
+
this.validateInboxPage(page, limit, cursor);
|
|
1837
|
+
rows = mergeInboxEntries(rows, page.entries);
|
|
1838
|
+
consumed += page.entries.length;
|
|
1839
|
+
cursor = page.nextCursor;
|
|
1840
|
+
if (cursor === null || consumed >= target && applyConversationFilter(ids(rows), this.state.filter).length > 0) break;
|
|
1841
|
+
}
|
|
1842
|
+
if (!this.alive(generation)) return;
|
|
1843
|
+
this.commitInbox(rows, cursor);
|
|
1844
|
+
}
|
|
1845
|
+
async refreshLegacy(generation, filter) {
|
|
1846
|
+
const target = Math.max(this.pageSize, this.offset);
|
|
1847
|
+
const compare2 = (a, b) => a.createdAt.getTime() - b.createdAt.getTime() || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
1848
|
+
const boundary = this.options.pageLoader ? void 0 : this.source.reduce((oldest, row) => !oldest || compare2(row, oldest) < 0 ? row : oldest, void 0);
|
|
1849
|
+
let rows = [], offset = 0, hasMore = true;
|
|
1850
|
+
while (this.alive(generation)) {
|
|
1851
|
+
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 });
|
|
1852
|
+
if (!this.alive(generation)) return;
|
|
1853
|
+
if (page.length > this.pageSize || page.some((row) => !row.id.trim())) throw new Error("Invalid conversation page");
|
|
1854
|
+
const merged = mergeConversations(rows, page);
|
|
1855
|
+
if (page.length === this.pageSize && merged.length === rows.length) throw new Error("Conversation pagination did not advance");
|
|
1856
|
+
rows = merged;
|
|
1857
|
+
offset += page.length;
|
|
1858
|
+
hasMore = page.length === this.pageSize;
|
|
1859
|
+
if (!hasMore || offset >= target && applyConversationFilter(rows, this.state.filter).length > 0 && (!boundary || page.some((row) => compare2(row, boundary) <= 0))) break;
|
|
1860
|
+
}
|
|
1861
|
+
if (!this.alive(generation)) return;
|
|
1862
|
+
this.source = rows;
|
|
1863
|
+
this.offset = offset;
|
|
1864
|
+
this.patch({ conversations: applyConversationFilter(rows, this.state.filter), hasMore });
|
|
1865
|
+
}
|
|
1655
1866
|
/** Replace the loaded window atomically, retaining filters and rows during transient failures. */
|
|
1656
1867
|
refresh = async () => {
|
|
1657
1868
|
if (!this.alive()) return;
|
|
@@ -1662,28 +1873,14 @@ var ConversationListStore = class {
|
|
|
1662
1873
|
if (!this.state.hasLoaded) return this.loadInitial();
|
|
1663
1874
|
const generation = this.generation;
|
|
1664
1875
|
const filter = this.state.filter;
|
|
1665
|
-
const target = Math.max(this.pageSize, this.offset);
|
|
1666
|
-
const compare2 = (a, b) => a.createdAt.getTime() - b.createdAt.getTime() || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
1667
|
-
const boundary = this.options.pageLoader ? void 0 : this.source.reduce((oldest, row) => !oldest || compare2(row, oldest) < 0 ? row : oldest, void 0);
|
|
1668
1876
|
this.refreshing = true;
|
|
1669
1877
|
this.patch({ error: null });
|
|
1670
1878
|
try {
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
const merged = mergeConversations(rows, page);
|
|
1677
|
-
if (page.length === this.pageSize && merged.length === rows.length) throw new Error("Conversation pagination did not advance");
|
|
1678
|
-
rows = merged;
|
|
1679
|
-
offset += page.length;
|
|
1680
|
-
hasMore = page.length === this.pageSize;
|
|
1681
|
-
if (!hasMore || offset >= target && applyConversationFilter(rows, this.state.filter).length > 0 && (!boundary || page.some((row) => compare2(row, boundary) <= 0))) break;
|
|
1682
|
-
}
|
|
1683
|
-
if (!this.alive(generation)) return;
|
|
1684
|
-
this.source = rows;
|
|
1685
|
-
this.offset = offset;
|
|
1686
|
-
this.patch({ conversations: applyConversationFilter(rows, this.state.filter), hasMore });
|
|
1879
|
+
await this.withFallback(
|
|
1880
|
+
generation,
|
|
1881
|
+
() => this.refreshInbox(generation, filter),
|
|
1882
|
+
() => this.refreshLegacy(generation, filter)
|
|
1883
|
+
);
|
|
1687
1884
|
} catch (cause) {
|
|
1688
1885
|
this.fail(cause, generation);
|
|
1689
1886
|
} finally {
|
|
@@ -1696,9 +1893,14 @@ var ConversationListStore = class {
|
|
|
1696
1893
|
loadMore = async () => {
|
|
1697
1894
|
if (!this.alive() || this.refreshing || this.state.isInitialLoading || this.state.isLoadingMore || !this.state.hasMore) return;
|
|
1698
1895
|
const generation = this.generation;
|
|
1896
|
+
const filter = this.state.filter;
|
|
1699
1897
|
this.patch({ isLoadingMore: true, error: null });
|
|
1700
1898
|
try {
|
|
1701
|
-
await this.
|
|
1899
|
+
await this.withFallback(
|
|
1900
|
+
generation,
|
|
1901
|
+
() => this.loadInboxUntilVisible(generation, filter),
|
|
1902
|
+
() => this.loadUntilVisible(generation, filter)
|
|
1903
|
+
);
|
|
1702
1904
|
} catch (cause) {
|
|
1703
1905
|
this.fail(cause, generation);
|
|
1704
1906
|
} finally {
|
|
@@ -1750,6 +1952,8 @@ function useConversationList(options) {
|
|
|
1750
1952
|
});
|
|
1751
1953
|
return {
|
|
1752
1954
|
conversations: field("conversations"),
|
|
1955
|
+
summaries: field("summaries"),
|
|
1956
|
+
currentUserId: field("currentUserId"),
|
|
1753
1957
|
filter: field("filter"),
|
|
1754
1958
|
isInitialLoading: field("isInitialLoading"),
|
|
1755
1959
|
isLoadingMore: field("isLoadingMore"),
|
|
@@ -1775,6 +1979,8 @@ var appearanceProps3 = {
|
|
|
1775
1979
|
var listViewProps = {
|
|
1776
1980
|
...appearanceProps3,
|
|
1777
1981
|
conversations: { type: Array, required: true },
|
|
1982
|
+
summaries: { type: Object, default: void 0 },
|
|
1983
|
+
currentUserId: { type: String, default: void 0 },
|
|
1778
1984
|
selectedConversationId: { type: String, default: void 0 },
|
|
1779
1985
|
onConversationSelect: { type: Function, default: void 0 },
|
|
1780
1986
|
onRefresh: { type: Function, default: void 0 },
|
|
@@ -1824,6 +2030,25 @@ var ConversationListView = defineComponent4({
|
|
|
1824
2030
|
const refresh = () => {
|
|
1825
2031
|
return props.onRefresh?.();
|
|
1826
2032
|
};
|
|
2033
|
+
const inlineRetry = () => {
|
|
2034
|
+
if (props.hasMore && props.onLoadMore) return () => {
|
|
2035
|
+
lastRequestedLength = null;
|
|
2036
|
+
void requestMore();
|
|
2037
|
+
};
|
|
2038
|
+
if (props.onRefresh) return () => {
|
|
2039
|
+
void refresh();
|
|
2040
|
+
};
|
|
2041
|
+
return void 0;
|
|
2042
|
+
};
|
|
2043
|
+
const unreadBadge = (summary) => {
|
|
2044
|
+
if (summary.unreadCount <= 0 && !summary.unreadCountCapped) return null;
|
|
2045
|
+
const capped = summary.unreadCountCapped || summary.unreadCount > 99;
|
|
2046
|
+
return h4("span", {
|
|
2047
|
+
class: "ckui-unread-badge",
|
|
2048
|
+
role: "img",
|
|
2049
|
+
"aria-label": `${summary.unreadCountCapped ? "99+" : summary.unreadCount} unread`
|
|
2050
|
+
}, [h4("span", { "aria-hidden": "true" }, capped ? "99+" : String(summary.unreadCount))]);
|
|
2051
|
+
};
|
|
1827
2052
|
const renderContent = () => {
|
|
1828
2053
|
const currentAppearance = appearance();
|
|
1829
2054
|
if (props.isInitialLoading && props.conversations.length === 0) {
|
|
@@ -1855,10 +2080,21 @@ var ConversationListView = defineComponent4({
|
|
|
1855
2080
|
const children = props.conversations.flatMap((conversation, index) => {
|
|
1856
2081
|
const selected = props.selectedConversationId === conversation.id;
|
|
1857
2082
|
const select = () => selectConversation(conversation);
|
|
1858
|
-
const
|
|
2083
|
+
const summary = props.summaries?.get(conversation.id);
|
|
2084
|
+
const slotProps = {
|
|
2085
|
+
conversation,
|
|
2086
|
+
index,
|
|
2087
|
+
selected,
|
|
2088
|
+
select,
|
|
2089
|
+
...summary ? { summary } : {},
|
|
2090
|
+
...props.currentUserId === void 0 ? {} : { currentUserId: props.currentUserId }
|
|
2091
|
+
};
|
|
2092
|
+
const preview = inboxPreview(conversation, summary, props.currentUserId);
|
|
2093
|
+
const unread = summary !== void 0 && (summary.unreadCount > 0 || summary.unreadCountCapped);
|
|
1859
2094
|
const item = slots["conversation-item"]?.(slotProps) ?? h4("button", {
|
|
1860
2095
|
type: "button",
|
|
1861
2096
|
"data-selected": selected || void 0,
|
|
2097
|
+
"data-unread": unread || void 0,
|
|
1862
2098
|
"aria-current": selected ? "true" : void 0,
|
|
1863
2099
|
onClick: select,
|
|
1864
2100
|
class: partClass("listItem", currentAppearance, "ckui-conversation-item"),
|
|
@@ -1872,8 +2108,14 @@ var ConversationListView = defineComponent4({
|
|
|
1872
2108
|
}),
|
|
1873
2109
|
h4("span", { class: "ckui-conversation-item__body" }, [
|
|
1874
2110
|
h4("strong", conversation.displayTitle),
|
|
1875
|
-
h4("span", conversation.participants.map((participant) => participant.name).join(", ") || conversation.description || "No participants")
|
|
2111
|
+
h4("span", preview || conversation.participants.map((participant) => participant.name).join(", ") || conversation.description || "No participants")
|
|
1876
2112
|
]),
|
|
2113
|
+
// Spread rather than emit `null`: a null child renders a `<!---->` comment, and rows without a
|
|
2114
|
+
// summary must keep 0.5's exact markup.
|
|
2115
|
+
...summary ? [h4("span", { class: "ckui-conversation-item__meta" }, [
|
|
2116
|
+
h4("time", { class: "ckui-conversation-item__time", datetime: summary.activityAt.toISOString() }, formatMessageTime(summary.activityAt)),
|
|
2117
|
+
unreadBadge(summary)
|
|
2118
|
+
])] : [],
|
|
1877
2119
|
h4(ChevronRight, { size: 18, "aria-hidden": "true" })
|
|
1878
2120
|
]);
|
|
1879
2121
|
const nodes = [h4("div", { key: conversation.id, role: "listitem" }, [item])];
|
|
@@ -1883,13 +2125,15 @@ var ConversationListView = defineComponent4({
|
|
|
1883
2125
|
return nodes;
|
|
1884
2126
|
});
|
|
1885
2127
|
if (props.error) {
|
|
1886
|
-
|
|
2128
|
+
const retry = inlineRetry();
|
|
2129
|
+
children.push(slots.error?.({ error: props.error, ...retry ? { retry } : {} }) ?? h4("div", {
|
|
1887
2130
|
class: partClass("error", currentAppearance, "ckui-inline-state ckui-state--error"),
|
|
1888
2131
|
style: partStyle("error", currentAppearance),
|
|
1889
2132
|
role: "alert"
|
|
1890
|
-
}, [
|
|
1891
|
-
|
|
1892
|
-
|
|
2133
|
+
}, [
|
|
2134
|
+
h4("span", errorMessage(props.error)),
|
|
2135
|
+
...retry ? [h4("button", { type: "button", class: "ckui-link-button", onClick: retry }, "Retry")] : []
|
|
2136
|
+
]));
|
|
1893
2137
|
} else if (props.isLoadingMore) {
|
|
1894
2138
|
children.push(slots["load-more"]?.() ?? h4("div", {
|
|
1895
2139
|
class: partClass("loading", currentAppearance, "ckui-inline-state"),
|
|
@@ -1950,6 +2194,7 @@ var ConversationList = defineComponent4({
|
|
|
1950
2194
|
initialFilter: { type: Object, default: void 0 },
|
|
1951
2195
|
pageSize: { type: Number, default: 30 },
|
|
1952
2196
|
autoLoad: { type: Boolean, default: true },
|
|
2197
|
+
activityRefreshWindowMs: { type: Number, default: void 0 },
|
|
1953
2198
|
onControllerChange: { type: Function, default: void 0 }
|
|
1954
2199
|
},
|
|
1955
2200
|
emits: ["conversation-select", "controller-change"],
|
|
@@ -1959,7 +2204,8 @@ var ConversationList = defineComponent4({
|
|
|
1959
2204
|
...props.pageLoader ? { pageLoader: props.pageLoader } : {},
|
|
1960
2205
|
...props.initialFilter ? { initialFilter: props.initialFilter } : {},
|
|
1961
2206
|
pageSize: props.pageSize,
|
|
1962
|
-
autoLoad: props.autoLoad
|
|
2207
|
+
autoLoad: props.autoLoad,
|
|
2208
|
+
...props.activityRefreshWindowMs === void 0 ? {} : { activityRefreshWindowMs: props.activityRefreshWindowMs }
|
|
1963
2209
|
});
|
|
1964
2210
|
expose({ controller });
|
|
1965
2211
|
watchEffect2(() => {
|
|
@@ -1972,8 +2218,11 @@ var ConversationList = defineComponent4({
|
|
|
1972
2218
|
initialFilter: _initialFilter,
|
|
1973
2219
|
pageSize: _pageSize,
|
|
1974
2220
|
autoLoad: _autoLoad,
|
|
2221
|
+
activityRefreshWindowMs: _activityRefreshWindowMs,
|
|
1975
2222
|
onControllerChange: _onControllerChange,
|
|
1976
2223
|
conversations: _conversations,
|
|
2224
|
+
summaries: _summaries,
|
|
2225
|
+
currentUserId: _currentUserId,
|
|
1977
2226
|
onRefresh: _onRefresh,
|
|
1978
2227
|
onLoadMore: _onLoadMore,
|
|
1979
2228
|
isInitialLoading: _isInitialLoading,
|
|
@@ -1986,6 +2235,8 @@ var ConversationList = defineComponent4({
|
|
|
1986
2235
|
...attrs,
|
|
1987
2236
|
...forwarded,
|
|
1988
2237
|
conversations: controller.conversations.value,
|
|
2238
|
+
summaries: controller.summaries.value,
|
|
2239
|
+
currentUserId: controller.currentUserId.value,
|
|
1989
2240
|
onRefresh: controller.refresh,
|
|
1990
2241
|
onLoadMore: controller.loadMore,
|
|
1991
2242
|
isInitialLoading: controller.isInitialLoading.value,
|
|
@@ -2019,6 +2270,7 @@ var defaultConvoKitTheme = {
|
|
|
2019
2270
|
incomingBubble: "#f4f4f5",
|
|
2020
2271
|
outgoingBubble: "#18181b",
|
|
2021
2272
|
outgoingText: "#fafafa",
|
|
2273
|
+
badge: "#18181b",
|
|
2022
2274
|
radius: "10px",
|
|
2023
2275
|
avatarSize: "40px",
|
|
2024
2276
|
fontFamily: 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'
|
|
@@ -2050,6 +2302,7 @@ var ConvoKitThemeProvider = defineComponent5({
|
|
|
2050
2302
|
"--ckui-incoming": theme.incomingBubble,
|
|
2051
2303
|
"--ckui-outgoing": theme.outgoingBubble,
|
|
2052
2304
|
"--ckui-outgoing-text": theme.outgoingText,
|
|
2305
|
+
"--ckui-badge": theme.badge,
|
|
2053
2306
|
"--ckui-radius": theme.radius,
|
|
2054
2307
|
"--ckui-avatar-size": theme.avatarSize,
|
|
2055
2308
|
"--ckui-font": theme.fontFamily
|
|
@@ -2081,6 +2334,7 @@ export {
|
|
|
2081
2334
|
isConvoKitPendingMessage,
|
|
2082
2335
|
matchesConversation,
|
|
2083
2336
|
mergeConversations,
|
|
2337
|
+
mergeInboxEntries,
|
|
2084
2338
|
mergeMessages,
|
|
2085
2339
|
readerIdsFor,
|
|
2086
2340
|
useConversation,
|