@convokitapp/vue-ui 0.5.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 CHANGED
@@ -1,5 +1,103 @@
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
+
61
+ ## 0.6.0
62
+
63
+ - Inbox previews and unread counts. SDK-backed lists page `listInbox` by
64
+ opaque cursor in activity order (newest surviving message first, empty rooms
65
+ by creation time); `hasMore` follows the server's `nextCursor`. Pages merge
66
+ by conversation ID with the later entry winning, re-ordered by
67
+ `(activityAt desc, id desc)` unless `filter.comparator` is set. Page checks:
68
+ at most `limit` entries, non-blank unique IDs, and a cursor that advances
69
+ (`Inbox pagination did not advance`); the offset-era "full page with no new
70
+ IDs" rule applies to the legacy path only. `mergeInboxEntries` is exported.
71
+ - State: `summaries` (`ReadonlyMap<string, InboxSummary>`) and `currentUserId`
72
+ on `useConversationList` / `ConversationListState`; `''` and empty without a
73
+ session, on the legacy path and after disposal.
74
+ - Default rows: preview line (`You: …`, `<name>: …` in groups, media
75
+ placeholders `Photo` / file name or `File` / `Location` / `Contact`),
76
+ activity time, unread badge (`99+` above 99 or when capped; accessible name
77
+ `<count> unread`, `99+ unread` when capped; label hidden from the
78
+ accessibility tree), bold title while unread. New theme token `badge`
79
+ (`--ckui-badge`, default primary). `#conversation-item` slot props gain
80
+ `summary` and `currentUserId`; `ConversationListView` accepts `summaries`
81
+ and `currentUserId`. Inline `Retry` requests the next page when `hasMore`
82
+ and `onLoadMore` are bound (bypassing the duplicate-request guard), else
83
+ refreshes; it renders only when a callback exists.
84
+ - Live updates: `inbox_activity` is throttled by `activityRefreshWindowMs`
85
+ (default 500, 0 = immediate; max-wait window, generation-guarded, cleared on
86
+ dispose/session end, `unref`'d where available); `inbox_changed` refreshes
87
+ immediately and drops a pending window. `refresh()` walks from the head with
88
+ `limit = min(100, target − consumed)`, `target = max(pageSize, loaded)`,
89
+ continues past fully hidden pages and never publishes an empty list with
90
+ `hasMore` while pages remain; rows, summaries, cursor and `hasMore` swap
91
+ atomically.
92
+ - Adapter: optional `listInbox?({ limit, cursor, archived })` and
93
+ `onInboxActivity?(handler, onError?)`; the default adapter implements both.
94
+ Adapters without `listInbox` and custom `pageLoader`s keep the 0.5 offset
95
+ path with empty summaries. A 404 from `listInbox` falls back to that path for
96
+ the store's life without evicting rows (warned once); a legacy 404 still
97
+ evicts, 401/403 evict, 400 keeps rows and reports.
98
+ - Requires core SDK 0.6.x (`listInbox`, `InboxPage` / `InboxEntry` /
99
+ `InboxSummary`, `realtime.onInboxActivity`) and the coordinated backend.
100
+
3
101
  ## 0.5.0
4
102
 
5
103
  - Precise read positions. Receipts resolve per user from a monotonic read-through
package/PARITY.md CHANGED
@@ -14,6 +14,18 @@ while using Vue-native composition patterns.
14
14
  | Conversation state | Controller | `useConversation` | `useConversation` |
15
15
  | Conversation-list state | Controller | `useConversationList` | `useConversationList` |
16
16
  | Inbox offset pagination and de-duplication | Yes | Yes | Yes |
17
+ | Inbox cursor pagination, activity order, later-entry merge | Yes | Yes | Yes |
18
+ | Inbox summaries and viewer in list state (`summaries`, `currentUserId`) | Yes | Yes | Yes |
19
+ | Preview / time / unread badge rows (`badge` token) | `badgeColor` | `--ckui-badge` | `--ckui-badge` |
20
+ | Row replacements receive `summary` / `currentUserId` | `rowBuilder` | Render props | `#conversation-item` slot |
21
+ | `inbox_activity` throttle (`activityRefreshWindowMs`) | Yes | Yes | Yes |
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 |
28
+ | Inbox 404 fallback to the offset path | Yes | Yes | Yes |
17
29
  | Search, archived, participant, predicate, sort filters | Yes | Yes | Yes |
18
30
  | Realtime messages, typing, reads | Yes | Yes | Yes |
19
31
  | Pending state without provisional timestamps or receipts | Yes | Yes | Yes |
@@ -46,3 +58,36 @@ platform reports the view as hidden. The adapter interface gained the optional
46
58
  target in all libraries (`markConversationRead(conversationId, options?)` here).
47
59
  The parity script checks API presence only; the store, rendered-view and
48
60
  actual-SDK protocol tests verify behavior.
61
+
62
+ ## Inbox parity (0.6.0)
63
+
64
+ Every UI pages the inbox by the server cursor, keeps the server's order within a
65
+ page, merges pages by conversation ID with the later entry winning, and
66
+ re-orders merged rows by `(activityAt desc, id desc)` unless a comparator is
67
+ set. Rows show the same preview (`You:` for the viewer, `<name>:` for a named
68
+ member of a group of more than two, `Photo` / file name or `File` /
69
+ `Location` / `Contact` for media-only messages), the activity time, and an
70
+ unread badge (`99+` above 99 or when capped; accessible name
71
+ `<count> unread`). `inbox_activity` signals are coalesced by a max-wait window
72
+ (`activityRefreshWindowMs`, default 500 ms); `inbox_changed` stays immediate.
73
+ A 404 from the inbox route falls back to the offset path for the store's life
74
+ without evicting rows. The parity script checks API presence only; the store,
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,125 @@ 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
+
76
+ ## 0.6.0 inbox previews and unread counts
77
+
78
+ This release requires JavaScript SDK 0.6.x (`listInbox`, `onInboxActivity`)
79
+ and the coordinated backend (`GET /api/v1/inbox`, the `inbox_activity` event).
80
+
81
+ - SDK-backed lists arrive in inbox order: the newest surviving message time
82
+ first (an empty room sorts by its creation time), paged by an opaque server
83
+ cursor instead of an offset. The store keeps the server's order within a page
84
+ and, whenever pages are combined (load more, refresh), merges by conversation
85
+ ID with the later entry winning and re-orders by `(activityAt desc, id desc)`.
86
+ Setting `filter.comparator` replaces that order; `updatedAt` is never an
87
+ ordering input. `mergeInboxEntries(current, incoming)` is exported and applies
88
+ the same rule.
89
+ - State gains `summaries` (a `ReadonlyMap<string, InboxSummary>` by conversation
90
+ ID: `latestMessage`, `unreadCount`, `unreadCountCapped`, `readPosition`,
91
+ `lastReadAt`, `activityAt`) and `currentUserId` (the bound session's user;
92
+ `''` without a session, on the legacy path and after disposal). `conversations`,
93
+ filters, comparators, custom offset `pageLoader`s and the `#conversation-item`
94
+ slot signature are unchanged.
95
+ - Default rows show a one-line preview instead of the participants line when a
96
+ summary carries a non-empty latest message: `You: …` for the viewer's own
97
+ message (DMs included), `<name>: …` for a named member of a group of more
98
+ than two, otherwise the body alone; media-only messages read `Photo`,
99
+ the file name or `File`, `Location`, `Contact`. Rows show the activity time
100
+ in the device zone and an unread badge (`5`, `99+` above 99 or when the
101
+ server capped the count) with the accessible name `<count> unread`
102
+ (`99+ unread` when capped); the title is bold while unread. The badge uses
103
+ the new theme token `badge` (`--ckui-badge`, defaults to the primary color)
104
+ with `outgoingText` as its label color.
105
+ - `#conversation-item` slot props gain `summary` and `currentUserId` (present
106
+ when known); existing slots keep working. `ConversationListView` accepts
107
+ `summaries` and `currentUserId` for controlled use. The inline error's
108
+ `Retry` requests the next page when `hasMore` and `onLoadMore` are bound
109
+ (past the view's duplicate-request guard), otherwise it refreshes; it
110
+ renders only when one of those callbacks exists.
111
+ - Live updates: `inbox_changed` still refreshes immediately; the new
112
+ `inbox_activity` signal (message inserts/edits and read-position advances,
113
+ fanned out to every client of the app) is throttled by
114
+ `activityRefreshWindowMs` (default 500 ms, 0 = immediate) with max-wait
115
+ semantics: the first signal opens a window, later signals wait for it, and one
116
+ refresh runs when it closes. An `inbox_changed` during the window refreshes
117
+ at once and drops the timer. `refresh()`, topic-error reconciliation and the
118
+ rejoin replay stay immediate. A refresh re-walks from the head with
119
+ `limit = min(100, target − consumed)` where `target = max(pageSize, loaded)`,
120
+ continues past pages the local filter hides, and swaps rows, summaries,
121
+ cursor and `hasMore` atomically; it never publishes an empty list with
122
+ `hasMore` while more pages exist. Rooms are open with `useConversation` as
123
+ before; room stores ignore `inbox_activity`.
124
+ - Adapter additions: `ConvoKitUiClient.listInbox?({ limit, cursor, archived })`
125
+ and `onInboxActivity?(handler, onError?)` are optional. The default adapter
126
+ implements both; custom adapters without `listInbox`, and custom `pageLoader`s,
127
+ keep the 0.5 offset path with empty `summaries` and no activity subscription.
128
+ A 404 from `listInbox` (route absent on a rolled-back backend) switches the
129
+ store to that path for the rest of its life without evicting rows (a warning
130
+ is logged once); a 404 from `getConversations` still evicts, 401/403 from
131
+ either endpoint evict, and 400 keeps rows and reports the error.
132
+
14
133
  ## 0.5.0 precise read positions
15
134
 
16
135
  This release requires JavaScript SDK 0.5.x and the coordinated backend.
@@ -191,6 +310,10 @@ Available slots include `conversation-item`, `separator`, `header`, `message`,
191
310
  Use `useConversationList` and `useConversation` when you want ConvoKit's
192
311
  pagination, de-duplication, realtime, typing, and read state without the
193
312
  default UI. Both return readonly Vue refs plus actions and a `dispose()` method.
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.
194
317
  `useConversation` also exposes `readPositionByUserId`, `readerIdsFor(message)`,
195
318
  `markRead()` and `setVisible(visible)`; call `setVisible(false)` while your own
196
319
  surface hides the room so automatic acknowledgements wait until it is shown.
@@ -201,7 +324,8 @@ use `isConvoKitPendingMessage(message)` to present the same state.
201
324
 
202
325
  ## Appearance
203
326
 
204
- Wrap any subtree with `ConvoKitThemeProvider`, set `density="compact"`, or use
327
+ Wrap any subtree with `ConvoKitThemeProvider` (tokens include `badge` for the
328
+ unread counter and dot), set `density="compact"`, or use
205
329
  the per-part `classNames` and `styles` maps. `unstyled` removes package classes
206
330
  from the configurable parts for a fully host-owned presentation.
207
331