@convokitapp/vue-ui 0.6.0 → 0.7.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 +58 -0
- package/PARITY.md +23 -0
- package/README.md +67 -3
- package/dist/index.cjs +129 -19
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +10 -0
- package/dist/index.css.map +1 -1
- package/dist/index.d.cts +38 -10
- package/dist/index.d.ts +38 -10
- package/dist/index.js +129 -19
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,63 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.7.0
|
|
4
|
+
|
|
5
|
+
- Private mark unread. `useConversationList` / `ConversationListController`
|
|
6
|
+
gain `markUnread(conversationId)` and
|
|
7
|
+
`clearUnread(conversationId, { ifVersion? })`; the latter resolves to the
|
|
8
|
+
response's `cleared` ("this request removed the marker"). On any 200 (a mark,
|
|
9
|
+
or a clear whether `cleared` is true or false) the row's current summary
|
|
10
|
+
takes the response's `unreadMarkedAt` and `privateStateVersion` as one unit,
|
|
11
|
+
only while the response is not older than the stored version, and
|
|
12
|
+
`isUnread` is recomputed as
|
|
13
|
+
`unreadCount > 0 || unreadCountCapped || unreadMarkedAt !== null`; a lower
|
|
14
|
+
version is ignored so a delayed response never resurrects a marker a newer
|
|
15
|
+
action removed. Both methods reject when the adapter lacks the new members
|
|
16
|
+
or the store is disposed or session-ended (nothing is sent); request
|
|
17
|
+
failures are reported through `error` without evicting rows and reject.
|
|
18
|
+
Other devices learn of the change through `inbox_activity`, which the
|
|
19
|
+
backend now also sends for the caller's own marker changes.
|
|
20
|
+
- Rows. The unread predicate (bold title, `data-unread`) is
|
|
21
|
+
`summary.isUnread || summary.unreadCount > 0 || summary.unreadCountCapped`,
|
|
22
|
+
so consumer-built summaries that omit `isUnread` keep their badges. A count
|
|
23
|
+
(or a capped count) keeps the numeric badge unchanged; `isUnread` without
|
|
24
|
+
one renders a numberless dot, `.ckui-unread-badge.ckui-unread-badge--dot`
|
|
25
|
+
(`role="img"`, accessible name `Unread`, never `0 unread`), in the `badge`
|
|
26
|
+
colour. It is spread into the row's meta span, so rows without a badge no
|
|
27
|
+
longer carry an empty comment vnode. `#conversation-item` slots receive the
|
|
28
|
+
same `summary` with its three new fields; no signature changes.
|
|
29
|
+
- Rooms. The store captures `conversation.membership.privateStateVersion` (and
|
|
30
|
+
whether `membership.unreadMarkedAt` was set) once per open, the first time
|
|
31
|
+
its `conversation` goes from null to a DTO: on `loadInitial`, or on the
|
|
32
|
+
reconcile that follows a transient first-load failure; a refresh that
|
|
33
|
+
replaces an existing DTO never recaptures, and the capture is dropped
|
|
34
|
+
whenever `conversation` returns to null (reload, dispose, authoritative
|
|
35
|
+
401/403/404). Every targeted acknowledgement of that open sends
|
|
36
|
+
`{ throughMessageId, privateStateVersion }`; a DTO without `membership`
|
|
37
|
+
(0.6 backend) sends the 0.6 body. An automatic acknowledgement for a row
|
|
38
|
+
received before the DTO (during the load, or while a transient first-load
|
|
39
|
+
failure stands) waits for the capture, like it waits while hidden, and is
|
|
40
|
+
issued with the version once the open captures. A room opened with a
|
|
41
|
+
marker that renders no non-pending, acknowledgeable row calls
|
|
42
|
+
`clearConversationUnread(conversationId, { ifVersion: captured })` once per
|
|
43
|
+
open under the load-acknowledgement triggers and visibility gating (also on
|
|
44
|
+
an explicit `markRead()`, never once a row is rendered); `cleared: false`
|
|
45
|
+
is not an error. The store still never sends an acknowledgement without a
|
|
46
|
+
target, fabricates no read state and holds no marker state.
|
|
47
|
+
- Adapter: optional `markConversationUnread?(conversationId)` and
|
|
48
|
+
`clearConversationUnread?(conversationId, { ifVersion? })`; the default
|
|
49
|
+
adapter implements both. `markConversationRead` options may now carry
|
|
50
|
+
`privateStateVersion`: custom adapters must forward the options object
|
|
51
|
+
unchanged, or acknowledgements advance the position but never clear the
|
|
52
|
+
marker. Adapters without `clearConversationUnread` leave the marker in
|
|
53
|
+
empty rooms.
|
|
54
|
+
- Requires core SDK 0.7.x (`markConversationUnread`, `clearConversationUnread`,
|
|
55
|
+
`Conversation.membership`, the required `InboxSummary` members `isUnread`,
|
|
56
|
+
`unreadMarkedAt` and `privateStateVersion`) and the coordinated backend.
|
|
57
|
+
Consumer-built `InboxSummary` literals gain those three members. Mixed
|
|
58
|
+
fleet: against a 0.6 backend the new adapter members fail with a 404
|
|
59
|
+
(`HTTP_ERROR`), acknowledgements carry no version and lists show no dots.
|
|
60
|
+
|
|
3
61
|
## 0.6.0
|
|
4
62
|
|
|
5
63
|
- Inbox previews and unread counts. SDK-backed lists page `listInbox` by
|
package/PARITY.md
CHANGED
|
@@ -20,6 +20,11 @@ while using Vue-native composition patterns.
|
|
|
20
20
|
| Row replacements receive `summary` / `currentUserId` | `rowBuilder` | Render props | `#conversation-item` slot |
|
|
21
21
|
| `inbox_activity` throttle (`activityRefreshWindowMs`) | Yes | Yes | Yes |
|
|
22
22
|
| Adapter `listInbox` / `onInboxActivity` | Required | Optional | Optional |
|
|
23
|
+
| List `markUnread` / `clearUnread` (0.7.0) | Controller | `useConversationList` | `useConversationList` |
|
|
24
|
+
| Numberless unread dot (`isUnread`, count 0, name `Unread`) | `ConvoKitUnreadDot` | `.ckui-badge--dot` | `.ckui-unread-badge--dot` |
|
|
25
|
+
| Acknowledgements carry the version captured at open | Yes | Yes | Yes |
|
|
26
|
+
| Empty marked room clears through `clearConversationUnread` | Yes | Yes | Yes |
|
|
27
|
+
| Adapter `markConversationUnread` / `clearConversationUnread` | Required | Optional | Optional |
|
|
23
28
|
| Inbox 404 fallback to the offset path | Yes | Yes | Yes |
|
|
24
29
|
| Search, archived, participant, predicate, sort filters | Yes | Yes | Yes |
|
|
25
30
|
| Realtime messages, typing, reads | Yes | Yes | Yes |
|
|
@@ -68,3 +73,21 @@ unread badge (`99+` above 99 or when capped; accessible name
|
|
|
68
73
|
A 404 from the inbox route falls back to the offset path for the store's life
|
|
69
74
|
without evicting rows. The parity script checks API presence only; the store,
|
|
70
75
|
rendered-view and actual-SDK protocol tests verify behavior.
|
|
76
|
+
|
|
77
|
+
## Mark-unread parity (0.7.0)
|
|
78
|
+
|
|
79
|
+
Every UI keeps the marker private to the caller, exposes list-level
|
|
80
|
+
`markUnread` / `clearUnread` (the latter returning the response's `cleared`),
|
|
81
|
+
applies a mutation response to the current summary only when its
|
|
82
|
+
`privateStateVersion` is not older than the stored one, and recomputes
|
|
83
|
+
`isUnread` as `unreadCount > 0 || unreadCountCapped || unreadMarkedAt != null`.
|
|
84
|
+
Rows keep the numeric badge for a count and render a numberless dot named
|
|
85
|
+
`Unread` for a marker without one. Room stores capture the opened
|
|
86
|
+
`membership.privateStateVersion` once per open and send it with every
|
|
87
|
+
targeted acknowledgement; a room opened with a marker that renders nothing
|
|
88
|
+
clears it once through `clearConversationUnread(id, { ifVersion })`, never
|
|
89
|
+
through an untargeted acknowledgement. The adapter additions are optional in
|
|
90
|
+
the TypeScript UIs and required in the Dart and Kotlin ones (a breaking
|
|
91
|
+
change for custom implementers there, like `listInbox` in 0.6.0). The parity
|
|
92
|
+
script checks API presence only; the store, rendered-view and actual-SDK
|
|
93
|
+
protocol tests verify behavior.
|
package/README.md
CHANGED
|
@@ -11,6 +11,68 @@ 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.7.0 private mark unread
|
|
15
|
+
|
|
16
|
+
This release requires JavaScript SDK 0.7.x (`markConversationUnread`,
|
|
17
|
+
`clearConversationUnread`, `Conversation.membership`, the `InboxSummary`
|
|
18
|
+
members `isUnread`, `unreadMarkedAt` and `privateStateVersion`) and the
|
|
19
|
+
coordinated backend (`POST`/`DELETE /api/v1/conversations/:id/unread`, the
|
|
20
|
+
`privateStateVersion` acknowledgement field, the `membership` sibling on
|
|
21
|
+
`GET /api/v1/conversations/:id`).
|
|
22
|
+
|
|
23
|
+
- The marker is private: other members, webhooks and `read` events never see
|
|
24
|
+
it. `useConversationList` and the `ConversationList` controller gain
|
|
25
|
+
`markUnread(conversationId)` and `clearUnread(conversationId, { ifVersion? })`.
|
|
26
|
+
Wire them through `#conversation-item` (a row action or menu) or
|
|
27
|
+
`@controller-change`; the default row adds no affordance. `clearUnread`
|
|
28
|
+
resolves to the response's `cleared` ("this request removed the marker",
|
|
29
|
+
not "the room is read"). Both reject from a disposed or session-ended
|
|
30
|
+
controller without sending anything.
|
|
31
|
+
- Summaries carry `isUnread`
|
|
32
|
+
(`unreadCount > 0 || unreadCountCapped || unreadMarkedAt !== null`),
|
|
33
|
+
`unreadMarkedAt` and `privateStateVersion`. After a mark or a clear the
|
|
34
|
+
row's summary takes the response only while it is not older than what the
|
|
35
|
+
row already holds; a delayed response never resurrects a marker a newer
|
|
36
|
+
action removed. Other devices refresh through `inbox_activity`, which the
|
|
37
|
+
backend also sends for your own marker changes; `activityRefreshWindowMs`
|
|
38
|
+
applies as before.
|
|
39
|
+
- Dot rule: a count (or a capped count) keeps the numeric badge (`5`, `99+`,
|
|
40
|
+
accessible name `<count> unread`); `isUnread` without one renders a
|
|
41
|
+
numberless dot with the accessible name `Unread`, never `0 unread`
|
|
42
|
+
(`.ckui-unread-badge.ckui-unread-badge--dot`, `badge` colour). The title is
|
|
43
|
+
bold and `data-unread` is set in both cases; rows without a summary are
|
|
44
|
+
unchanged.
|
|
45
|
+
- Capture at open: the room store captures
|
|
46
|
+
`conversation.membership.privateStateVersion` once per open (the first time
|
|
47
|
+
its `conversation` goes from null to a DTO: on load, or on the reconcile
|
|
48
|
+
after a transient first-load failure) and sends it with every targeted
|
|
49
|
+
acknowledgement of that open. A refresh never recaptures, so a mark made
|
|
50
|
+
elsewhere while the room is open survives its acknowledgements until the
|
|
51
|
+
room is reopened; a DTO without `membership` (0.6 backend) sends no version.
|
|
52
|
+
An automatic acknowledgement for a row that arrives before the DTO (during
|
|
53
|
+
the load, or while a transient first-load failure stands) waits for the
|
|
54
|
+
capture, like it waits while hidden, so it too carries the version.
|
|
55
|
+
- Empty rooms: when the opened membership carries a marker and nothing
|
|
56
|
+
non-pending is rendered, the store calls
|
|
57
|
+
`clearConversationUnread(conversationId, { ifVersion })` once per open under
|
|
58
|
+
the same triggers and visibility gating as the load acknowledgement (and on
|
|
59
|
+
an explicit `markRead()`); once a row is rendered its acknowledgement clears
|
|
60
|
+
the marker instead. The UI still never sends an acknowledgement without a
|
|
61
|
+
target. `cleared: false` is not an error.
|
|
62
|
+
- Adapter additions: `ConvoKitUiClient.markConversationUnread?(conversationId)`
|
|
63
|
+
and `clearConversationUnread?(conversationId, { ifVersion? })` are optional;
|
|
64
|
+
the default adapter implements both. Custom adapters without them make
|
|
65
|
+
`markUnread` / `clearUnread` reject and leave the marker in empty rooms.
|
|
66
|
+
(The Flutter and Android UIs make the same members, and the
|
|
67
|
+
`privateStateVersion` acknowledgement parameter, required on their
|
|
68
|
+
`ConvoKitUiClient`: a breaking change for custom implementers there, like
|
|
69
|
+
`listInbox` in 0.6.0.)
|
|
70
|
+
`markConversationRead` options now carry `privateStateVersion`; forward the
|
|
71
|
+
options object unchanged, or acknowledgements advance the position but never
|
|
72
|
+
clear the marker.
|
|
73
|
+
- Mixed fleet: 0.6 lists ignore `isUnread`; against a 0.6 backend the new
|
|
74
|
+
adapter members fail with a 404 (`HTTP_ERROR`) and no dots render.
|
|
75
|
+
|
|
14
76
|
## 0.6.0 inbox previews and unread counts
|
|
15
77
|
|
|
16
78
|
This release requires JavaScript SDK 0.6.x (`listInbox`, `onInboxActivity`)
|
|
@@ -248,8 +310,10 @@ Available slots include `conversation-item`, `separator`, `header`, `message`,
|
|
|
248
310
|
Use `useConversationList` and `useConversation` when you want ConvoKit's
|
|
249
311
|
pagination, de-duplication, realtime, typing, and read state without the
|
|
250
312
|
default UI. Both return readonly Vue refs plus actions and a `dispose()` method.
|
|
251
|
-
`useConversationList` also exposes `summaries` and `currentUserId
|
|
252
|
-
`activityRefreshWindowMs
|
|
313
|
+
`useConversationList` also exposes `summaries` and `currentUserId`, accepts
|
|
314
|
+
`activityRefreshWindowMs` (`ConversationList` forwards the same option), and
|
|
315
|
+
offers `markUnread(conversationId)` / `clearUnread(conversationId, options?)`
|
|
316
|
+
for a host-built row action.
|
|
253
317
|
`useConversation` also exposes `readPositionByUserId`, `readerIdsFor(message)`,
|
|
254
318
|
`markRead()` and `setVisible(visible)`; call `setVisible(false)` while your own
|
|
255
319
|
surface hides the room so automatic acknowledgements wait until it is shown.
|
|
@@ -261,7 +325,7 @@ use `isConvoKitPendingMessage(message)` to present the same state.
|
|
|
261
325
|
## Appearance
|
|
262
326
|
|
|
263
327
|
Wrap any subtree with `ConvoKitThemeProvider` (tokens include `badge` for the
|
|
264
|
-
unread counter), set `density="compact"`, or use
|
|
328
|
+
unread counter and dot), set `density="compact"`, or use
|
|
265
329
|
the per-part `classNames` and `styles` maps. `unstyled` removes package classes
|
|
266
330
|
from the configurable parts for a fully host-owned presentation.
|
|
267
331
|
|
package/dist/index.cjs
CHANGED
|
@@ -73,6 +73,8 @@ function createConvoKitUiClient(client) {
|
|
|
73
73
|
getMessage: (id) => client.getMessage(id),
|
|
74
74
|
sendMessage: (input) => client.sendMessage(input),
|
|
75
75
|
markConversationRead: (conversationId, options) => client.markConversationRead(conversationId, options),
|
|
76
|
+
markConversationUnread: (conversationId) => client.markConversationUnread(conversationId),
|
|
77
|
+
clearConversationUnread: (conversationId, options) => client.clearConversationUnread(conversationId, options),
|
|
76
78
|
sendTyping: (input) => client.sendTyping(input),
|
|
77
79
|
onMessage: (conversationId, handler, onError) => client.realtime.onMessage(conversationId, {
|
|
78
80
|
onEvent: handler,
|
|
@@ -257,6 +259,10 @@ function readEntry(participant) {
|
|
|
257
259
|
function acknowledgement() {
|
|
258
260
|
return { inFlight: void 0, followUp: false, suppressed: false, target: void 0, acknowledged: void 0, unacknowledgeable: /* @__PURE__ */ new Set() };
|
|
259
261
|
}
|
|
262
|
+
function capture(conversation) {
|
|
263
|
+
const membership = conversation?.membership;
|
|
264
|
+
return { version: membership?.privateStateVersion, clearPending: membership?.unreadMarkedAt != null };
|
|
265
|
+
}
|
|
260
266
|
function isTargetMiss(cause) {
|
|
261
267
|
if (typeof cause !== "object" || cause === null) return false;
|
|
262
268
|
const { code, status } = cause;
|
|
@@ -317,6 +323,7 @@ var ConversationStore = class {
|
|
|
317
323
|
// Keep tombstones until an explicit reload/session change, including across refreshes.
|
|
318
324
|
deleted = /* @__PURE__ */ new Set();
|
|
319
325
|
ack = acknowledgement();
|
|
326
|
+
captured = capture();
|
|
320
327
|
// Visible until the platform reports otherwise; unknown/prerender/no document count as visible.
|
|
321
328
|
visible = true;
|
|
322
329
|
sendRevision;
|
|
@@ -382,6 +389,7 @@ var ConversationStore = class {
|
|
|
382
389
|
this.hydrationPool.queued.clear();
|
|
383
390
|
this.deleted.clear();
|
|
384
391
|
this.ack = acknowledgement();
|
|
392
|
+
this.captured = capture();
|
|
385
393
|
this.sendRevision = void 0;
|
|
386
394
|
this.activeSend = void 0;
|
|
387
395
|
this.refreshQueued = false;
|
|
@@ -626,9 +634,13 @@ var ConversationStore = class {
|
|
|
626
634
|
this.validatePage(page);
|
|
627
635
|
this.cursor = page.at(-1);
|
|
628
636
|
this.patch({ conversation, messages: this.overlay(page, revision), hasOlderMessages: page.length === this.pageSize });
|
|
637
|
+
this.captured = capture(conversation);
|
|
629
638
|
this.mergeReads(conversation.participants.map(readEntry));
|
|
630
639
|
this.prune(revision);
|
|
631
|
-
if (this.options.markReadOnLoad ?? true)
|
|
640
|
+
if ((this.options.markReadOnLoad ?? true) || this.ack.suppressed) {
|
|
641
|
+
this.ack.suppressed = false;
|
|
642
|
+
await this.acknowledge(true);
|
|
643
|
+
}
|
|
632
644
|
} catch (cause) {
|
|
633
645
|
this.fail(cause, generation, true);
|
|
634
646
|
} finally {
|
|
@@ -685,9 +697,12 @@ var ConversationStore = class {
|
|
|
685
697
|
const survivingIds = new Set(reconciled.map((message) => message.id));
|
|
686
698
|
const known = this.state.messages.filter((message) => !isConvoKitPendingMessage(message)).map((message) => message.id).concat([...this.changes].filter(([, change]) => change.insert && change.revision <= revision).map(([id]) => id));
|
|
687
699
|
for (const id of known) if (!survivingIds.has(id)) this.forget(id);
|
|
700
|
+
const opening = this.state.conversation === null;
|
|
688
701
|
this.patch({ conversation, messages: reconciled, hasOlderMessages: hasOlder });
|
|
702
|
+
if (opening) this.captured = capture(conversation);
|
|
689
703
|
this.mergeReads(conversation.participants.map(readEntry));
|
|
690
704
|
this.prune(revision);
|
|
705
|
+
if (opening) void this.resumeAcknowledgement();
|
|
691
706
|
} catch (cause) {
|
|
692
707
|
this.fail(cause, generation, true);
|
|
693
708
|
} finally {
|
|
@@ -721,15 +736,22 @@ var ConversationStore = class {
|
|
|
721
736
|
}
|
|
722
737
|
}
|
|
723
738
|
};
|
|
724
|
-
/** Acknowledge through the newest rendered row now, regardless of visibility; no
|
|
739
|
+
/** Acknowledge through the newest rendered row now, regardless of visibility; no acknowledgement without a
|
|
740
|
+
* target (a room opened with a marker that renders nothing clears the marker instead, once).
|
|
741
|
+
*/
|
|
725
742
|
markRead = () => this.alive() ? this.acknowledge(false) : Promise.resolve();
|
|
726
743
|
/** Automatic acknowledgements wait while hidden and are re-issued (once) on becoming visible. */
|
|
727
744
|
setVisible = (visible) => {
|
|
728
745
|
this.visible = visible;
|
|
729
|
-
if (!visible || !this.
|
|
730
|
-
this.
|
|
731
|
-
void this.acknowledge(true);
|
|
746
|
+
if (!visible || !this.alive()) return;
|
|
747
|
+
void this.resumeAcknowledgement();
|
|
732
748
|
};
|
|
749
|
+
/** Re-issue (once) the automatic acknowledgement that waited while hidden or before this open's DTO, if any. */
|
|
750
|
+
resumeAcknowledgement() {
|
|
751
|
+
if (!this.ack.suppressed) return Promise.resolve();
|
|
752
|
+
this.ack.suppressed = false;
|
|
753
|
+
return this.acknowledge(true);
|
|
754
|
+
}
|
|
733
755
|
/** The newest non-pending rendered row by (createdAt, id), never by list index and never a raw realtime
|
|
734
756
|
* row; rows the server does not know are skipped, and nothing at or before the accepted target is re-sent.
|
|
735
757
|
*/
|
|
@@ -744,7 +766,7 @@ var ConversationStore = class {
|
|
|
744
766
|
/** Resolves when the request this call issued or joined settles; a follow-up is issued, not awaited. */
|
|
745
767
|
acknowledge(automatic) {
|
|
746
768
|
const ack = this.ack;
|
|
747
|
-
if (automatic && !this.visible) {
|
|
769
|
+
if (automatic && (!this.visible || this.state.conversation === null)) {
|
|
748
770
|
ack.suppressed = true;
|
|
749
771
|
return Promise.resolve();
|
|
750
772
|
}
|
|
@@ -757,9 +779,10 @@ var ConversationStore = class {
|
|
|
757
779
|
issue(ack, generation) {
|
|
758
780
|
ack.followUp = false;
|
|
759
781
|
const target = this.ackTarget(ack);
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
ack.
|
|
782
|
+
const request = target ? this.send(ack, generation, target) : this.clearMarker(ack, generation);
|
|
783
|
+
if (!request) return void 0;
|
|
784
|
+
ack.target = target?.id;
|
|
785
|
+
ack.inFlight = request.finally(() => {
|
|
763
786
|
ack.inFlight = void 0;
|
|
764
787
|
ack.target = void 0;
|
|
765
788
|
if (!this.alive(generation) || !ack.followUp) return;
|
|
@@ -770,9 +793,31 @@ var ConversationStore = class {
|
|
|
770
793
|
});
|
|
771
794
|
return ack.inFlight;
|
|
772
795
|
}
|
|
796
|
+
/** A room opened with the caller's unread marker that renders no non-pending, acknowledgeable row cannot clear
|
|
797
|
+
* it through a targeted acknowledgement, so it asks the adapter to clear the marker conditionally on the captured
|
|
798
|
+
* version, once per open, under the acknowledgement triggers and visibility gating. Rows rendered later clear it
|
|
799
|
+
* through their acknowledgements; adapters without the member leave it; `cleared: false` is not an error.
|
|
800
|
+
*/
|
|
801
|
+
clearMarker(ack, generation) {
|
|
802
|
+
const captured = this.captured;
|
|
803
|
+
if (!captured.clearPending || captured.version === void 0 || typeof this.client.clearConversationUnread !== "function" || this.state.messages.some((message) => !isConvoKitPendingMessage(message) && !ack.unacknowledgeable.has(message.id))) return void 0;
|
|
804
|
+
captured.clearPending = false;
|
|
805
|
+
return this.clearUnread(captured.version, generation);
|
|
806
|
+
}
|
|
807
|
+
async clearUnread(version2, generation) {
|
|
808
|
+
try {
|
|
809
|
+
await this.client.clearConversationUnread(this.room, { ifVersion: version2 });
|
|
810
|
+
} catch (cause) {
|
|
811
|
+
if (this.alive(generation)) this.fail(cause, generation);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
773
814
|
async send(ack, generation, target) {
|
|
774
815
|
try {
|
|
775
|
-
|
|
816
|
+
const version2 = this.captured.version;
|
|
817
|
+
await this.client.markConversationRead(this.room, {
|
|
818
|
+
throughMessageId: target.id,
|
|
819
|
+
...version2 === void 0 ? {} : { privateStateVersion: version2 }
|
|
820
|
+
});
|
|
776
821
|
if (!this.alive(generation)) return;
|
|
777
822
|
if (!ack.acknowledged || compare(target, ack.acknowledged) > 0) ack.acknowledged = target;
|
|
778
823
|
} catch (cause) {
|
|
@@ -1936,6 +1981,66 @@ var ConversationListStore = class {
|
|
|
1936
1981
|
else if (!this.state.conversations.length && this.state.hasMore) await this.loadMore();
|
|
1937
1982
|
};
|
|
1938
1983
|
setQuery = (query) => this.setFilter({ ...this.state.filter, query });
|
|
1984
|
+
/** Mark a room unread for the viewer only; the row's summary takes the response (D10). Rejects when the adapter
|
|
1985
|
+
* lacks `markConversationUnread` or the store is not active; a request failure is reported through `error`
|
|
1986
|
+
* without evicting rows and rejects.
|
|
1987
|
+
*/
|
|
1988
|
+
markUnread = async (conversationId) => {
|
|
1989
|
+
const client = this.options.client;
|
|
1990
|
+
if (typeof client.markConversationUnread !== "function") {
|
|
1991
|
+
throw new TypeError("markUnread requires a ConvoKitUiClient adapter with markConversationUnread (core SDK 0.7)");
|
|
1992
|
+
}
|
|
1993
|
+
this.assertActive();
|
|
1994
|
+
this.applyPrivateState(conversationId, await this.mutate(client.markConversationUnread(conversationId)));
|
|
1995
|
+
};
|
|
1996
|
+
/** Remove the viewer's marker (conditionally on `options.ifVersion`); resolves to the response's `cleared` ("this
|
|
1997
|
+
* request removed the marker", not "the room is read") and patches the summary on true and false alike (D10).
|
|
1998
|
+
* Rejects when the adapter lacks `clearConversationUnread` or the store is not active; failures are reported like
|
|
1999
|
+
* `markUnread`.
|
|
2000
|
+
*/
|
|
2001
|
+
clearUnread = async (conversationId, options) => {
|
|
2002
|
+
const client = this.options.client;
|
|
2003
|
+
if (typeof client.clearConversationUnread !== "function") {
|
|
2004
|
+
throw new TypeError("clearUnread requires a ConvoKitUiClient adapter with clearConversationUnread (core SDK 0.7)");
|
|
2005
|
+
}
|
|
2006
|
+
this.assertActive();
|
|
2007
|
+
const result = await this.mutate(client.clearConversationUnread(conversationId, options));
|
|
2008
|
+
this.applyPrivateState(conversationId, result);
|
|
2009
|
+
return result.cleared;
|
|
2010
|
+
};
|
|
2011
|
+
/** A disposed or session-evicted store never sends a private-state mutation: on a shared client it could go out
|
|
2012
|
+
* under a replacement login. Rejected without touching `error` (there is no live snapshot to report into).
|
|
2013
|
+
*/
|
|
2014
|
+
assertActive() {
|
|
2015
|
+
if (!this.alive()) throw new Error("ConversationListStore is not active");
|
|
2016
|
+
}
|
|
2017
|
+
async mutate(request) {
|
|
2018
|
+
try {
|
|
2019
|
+
return await request;
|
|
2020
|
+
} catch (cause) {
|
|
2021
|
+
if (this.alive()) this.patch({ error: cause });
|
|
2022
|
+
throw cause;
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
/** Apply a mark/clear response to the row's CURRENT summary (a refresh may have swapped it) as one unit, only while
|
|
2026
|
+
* the store is alive and the response is not older than the stored version: a delayed response never resurrects a
|
|
2027
|
+
* marker a newer action removed (equal versions are an idempotent no-op). `isUnread` is recomputed from the stored
|
|
2028
|
+
* counts and the response marker. Other devices learn of the change through `inbox_activity`.
|
|
2029
|
+
*/
|
|
2030
|
+
applyPrivateState(conversationId, state) {
|
|
2031
|
+
if (!this.alive()) return;
|
|
2032
|
+
const current = this.entries.find((entry) => entry.conversation.id === conversationId);
|
|
2033
|
+
if (!current || state.privateStateVersion < current.privateStateVersion) return;
|
|
2034
|
+
const { unreadMarkedAt, privateStateVersion } = state;
|
|
2035
|
+
const patched = {
|
|
2036
|
+
...current,
|
|
2037
|
+
unreadMarkedAt,
|
|
2038
|
+
privateStateVersion,
|
|
2039
|
+
isUnread: current.unreadCount > 0 || current.unreadCountCapped || unreadMarkedAt !== null
|
|
2040
|
+
};
|
|
2041
|
+
this.entries = this.entries.map((entry) => entry === current ? patched : entry);
|
|
2042
|
+
this.patch({ summaries: new Map(this.entries.map((entry) => [entry.conversation.id, summaryOf(entry)])) });
|
|
2043
|
+
}
|
|
1939
2044
|
};
|
|
1940
2045
|
|
|
1941
2046
|
// src/composables/use-conversation-list.ts
|
|
@@ -1983,6 +2088,8 @@ function useConversationList(options) {
|
|
|
1983
2088
|
loadMore: () => store.loadMore(),
|
|
1984
2089
|
setFilter: (filter) => store.setFilter(filter),
|
|
1985
2090
|
setQuery: (query) => store.setQuery(query),
|
|
2091
|
+
markUnread: (conversationId) => store.markUnread(conversationId),
|
|
2092
|
+
clearUnread: (conversationId, options2) => store.clearUnread(conversationId, options2),
|
|
1986
2093
|
dispose
|
|
1987
2094
|
};
|
|
1988
2095
|
}
|
|
@@ -2059,13 +2166,16 @@ var ConversationListView = (0, import_vue10.defineComponent)({
|
|
|
2059
2166
|
return void 0;
|
|
2060
2167
|
};
|
|
2061
2168
|
const unreadBadge = (summary) => {
|
|
2062
|
-
if (summary.unreadCount
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2169
|
+
if (summary.unreadCount > 0 || summary.unreadCountCapped) {
|
|
2170
|
+
const capped = summary.unreadCountCapped || summary.unreadCount > 99;
|
|
2171
|
+
return [(0, import_vue10.h)("span", {
|
|
2172
|
+
class: "ckui-unread-badge",
|
|
2173
|
+
role: "img",
|
|
2174
|
+
"aria-label": `${summary.unreadCountCapped ? "99+" : summary.unreadCount} unread`
|
|
2175
|
+
}, [(0, import_vue10.h)("span", { "aria-hidden": "true" }, capped ? "99+" : String(summary.unreadCount))])];
|
|
2176
|
+
}
|
|
2177
|
+
if (summary.isUnread) return [(0, import_vue10.h)("span", { class: "ckui-unread-badge ckui-unread-badge--dot", role: "img", "aria-label": "Unread" })];
|
|
2178
|
+
return [];
|
|
2069
2179
|
};
|
|
2070
2180
|
const renderContent = () => {
|
|
2071
2181
|
const currentAppearance = appearance();
|
|
@@ -2108,7 +2218,7 @@ var ConversationListView = (0, import_vue10.defineComponent)({
|
|
|
2108
2218
|
...props.currentUserId === void 0 ? {} : { currentUserId: props.currentUserId }
|
|
2109
2219
|
};
|
|
2110
2220
|
const preview = inboxPreview(conversation, summary, props.currentUserId);
|
|
2111
|
-
const unread = summary !== void 0 && (summary.unreadCount > 0 || summary.unreadCountCapped);
|
|
2221
|
+
const unread = summary !== void 0 && (summary.isUnread || summary.unreadCount > 0 || summary.unreadCountCapped);
|
|
2112
2222
|
const item = slots["conversation-item"]?.(slotProps) ?? (0, import_vue10.h)("button", {
|
|
2113
2223
|
type: "button",
|
|
2114
2224
|
"data-selected": selected || void 0,
|
|
@@ -2132,7 +2242,7 @@ var ConversationListView = (0, import_vue10.defineComponent)({
|
|
|
2132
2242
|
// summary must keep 0.5's exact markup.
|
|
2133
2243
|
...summary ? [(0, import_vue10.h)("span", { class: "ckui-conversation-item__meta" }, [
|
|
2134
2244
|
(0, import_vue10.h)("time", { class: "ckui-conversation-item__time", datetime: summary.activityAt.toISOString() }, formatMessageTime(summary.activityAt)),
|
|
2135
|
-
unreadBadge(summary)
|
|
2245
|
+
...unreadBadge(summary)
|
|
2136
2246
|
])] : [],
|
|
2137
2247
|
(0, import_vue10.h)(import_vue9.ChevronRight, { size: 18, "aria-hidden": "true" })
|
|
2138
2248
|
]);
|