@convokitapp/vue-ui 0.8.0 → 0.9.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,130 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.0
4
+
5
+ - Quoted replies. `useConversation` / `ConversationController` gain
6
+ `replyTarget` (the message the composer is quoting, as it stood when the
7
+ quote was started; null otherwise), `replyPreviews` (a read-only map from
8
+ `Message.replyToMessageId` to the resolved `ReplyPreview`, or the terminal
9
+ `'unavailable'`; a MISSING key is the distinct third state, "not resolved
10
+ yet"), `canResolveReplyPreviews` (adapter support), `startReply(messageId)`
11
+ (a no-op unless the row is rendered, confirmed, not known to be gone and the
12
+ viewer's role when known is not `READ`; any member may quote any row, own or
13
+ not; sends nothing, not even a typing update) and `cancelReply()`. Replying
14
+ and editing are mutually exclusive: `startReply` leaves edit mode and
15
+ `startEditing` drops the quote. `sendMessage` stamps `replyToMessageId` on
16
+ the optimistic row so the quoted block renders before the server
17
+ acknowledges the send, sends the key only when a quote is set (a plain send
18
+ stays byte-identical to 0.8.0) and clears the target only once the send was
19
+ accepted; a failed send keeps the draft and the quote. Deleting the quoted
20
+ message drops the target.
21
+ - Batched preview resolution. After every page load, reconcile and live-insert
22
+ burst the store issues ONE `getReplyPreviews` call for the distinct quoted
23
+ parents the rendered rows point at and the loaded window cannot derive; a
24
+ parent that is on screen is derived locally (text cut at 500 characters,
25
+ `textTruncated` and `mediaCount` set) and costs no request, and a burst of
26
+ inserts is coalesced behind a short debounce whose handle is cleared with
27
+ the subscriptions. Previews are RE-READ, never copied: a `message_deleted`
28
+ event or a delete response for the id caches the terminal `'unavailable'`;
29
+ a row image, hydration or edit response for the id refreshes the entry from
30
+ the rendered row, or marks it stale when the parent is outside the window;
31
+ a reconnect (`SUBSCRIBED`) marks every non-terminal entry stale. An id a
32
+ RESOLVED call did not return is `'unavailable'` (the reference and the jump
33
+ affordance stay, the text becomes `Original message unavailable`) and is
34
+ never requested again. A REJECTED call writes no entry for any of its ids:
35
+ they stay unresolved, the failure surfaces through `error` without evicting
36
+ a row, and the next trigger asks again. A completed reconcile drops entries
37
+ no rendered row references, so the map stays bounded by the window.
38
+ - Jump to message. `jumpToMessage(messageId)` highlights a rendered row
39
+ without a request; otherwise it REPLACES the window with a
40
+ `getMessageContext` window centred on the target and sets
41
+ `windowMode: 'jumped'`, with `hasOlderMessages` / `hasNewerMessages` from
42
+ the returned cursors. A jump is a window operation, never a re-open:
43
+ tombstones, the acknowledgement floor, this open's captured private state,
44
+ edit mode and the reply target all survive it, it arms no acknowledgement,
45
+ and the replacement installs exactly the rows the response carried (filtered
46
+ by tombstones only, with no pending or live-tail replay). A coded 404
47
+ `MESSAGE_NOT_FOUND` — the guaranteed answer for a quoted message that was
48
+ deleted — marks that preview `'unavailable'` instead of reporting an error,
49
+ and leaves the window untouched. Context pages are checked by their own
50
+ validator (room-scoped, distinct, non-pending, newest-first, within the
51
+ requested limit, and for a centred window the target exactly once).
52
+ - Jumped windows. While `windowMode` is `jumped` the store acknowledges
53
+ nothing (the newest rendered row is not the newest row in the room), older
54
+ AND newer paging go through the window's own cursors (`loadNewerMessages()`
55
+ joins `loadOlderMessages()`), realtime inserts are recorded but not rendered
56
+ (ingestion still runs, so hydration, media completion, send confirmation and
57
+ tombstones keep working) and a queued reconcile re-reads the window with ONE
58
+ bounded request around its anchor instead of walking history from the live
59
+ tail — the tail reconcile is not dropped, it stays owed and flushes on the
60
+ return. `returnToLatest()` sets `live` BEFORE issuing the newest-page
61
+ request, so inserts arriving during it are folded in by the existing
62
+ precedence, then drains the deferred rows and issues the acknowledgement
63
+ they did not; a failed reload stays `jumped` with the window, the
64
+ affordance and the highlight intact. A newer page that reaches the tail runs
65
+ `returnToLatest()` rather than flipping in place. `sendMessage` returns to
66
+ the tail first (keeping the draft and the quote, and sending nothing when
67
+ that fails), and `jumpToMessage` is a no-op while a send is in flight.
68
+ - Default rows. A `Reply to message` action joins `.ckui-message-actions` on
69
+ any confirmed row of a writer (never pending rows, never a `READ` role);
70
+ `canReplyToMessage` replaces that eligibility and `canEditMessage`
71
+ deliberately does not. A row with `replyToMessageId` renders
72
+ `.ckui-message-quote` above its text in three states: the resolved author
73
+ and truncated text (accessible name `Quoted message from <author>`),
74
+ `Original message unavailable`, and — while the preview is not resolved yet
75
+ — the reference alone (`Quoted message`), never the unavailable copy. The
76
+ block is a button that jumps whenever the view can. Rows without the new
77
+ callbacks render byte-identically to 0.8.0.
78
+ - Highlight, scrolling and accessibility. The jumped-to row is centred with
79
+ `scrollIntoView({ block: 'center', behavior: 'instant' })`, focused with
80
+ `preventScroll` (only for an explicit jump, never for autoscroll, page loads
81
+ or live inserts) and tinted with the new `--ckui-highlight` theme token for
82
+ about two seconds; a user scroll clears the tint. One store-owned guard,
83
+ `jumpInFlight`, is set before the window is replaced and released on a short
84
+ timer: while it is set the list carries `aria-busy="true"` (omitted entirely
85
+ otherwise, never `aria-busy="false"`) and suspends stick-to-bottom, the
86
+ highlight clear and both pagination triggers. The custom-row slot wrapper
87
+ now carries `data-message-id`, so a replaced row is still a jump target; the
88
+ default row keeps carrying it on its `article`.
89
+ - Composer. A cancellable `.ckui-composer__replying` strip (`role="status"`,
90
+ visible `Cancel`, accessible name `Cancel reply`, also Escape) renders where
91
+ the edit banner does and never touches the draft — replying, unlike editing,
92
+ does not replace the field. Edit mode wins when a host sets both.
93
+ `#composer` slots receive `replying` and `cancelReply` in Vue's flat shape,
94
+ beside `editing` / `cancelEdit`.
95
+ - Controlled views. `ConversationView` gains `replyTarget`,
96
+ `onReplyToMessage`, `onCancelReply`, `canReplyToMessage`,
97
+ `replyPreviewByMessageId`, `onJumpToMessage`, `highlightedMessageId`,
98
+ `jumpInFlight`, `hasNewerMessages`, `isLoadingNewer`, `onLoadNewer` and
99
+ `onReturnToLatest` (and the `@reply-to-message`, `@cancel-reply`,
100
+ `@jump-to-message`, `@load-newer`, `@return-to-latest` events);
101
+ `MessageListView` gains all of those except `replyTarget`, `onCancelReply`
102
+ and `onReturnToLatest`, plus a `#loading-newer` slot. `onReturnToLatest` is
103
+ what renders the `Jump to latest` control, so the bound `Conversation`
104
+ passes it only while the window is jumped; a `#jump-to-latest` slot
105
+ (`JumpToLatestSlotProps`, carrying `returnToLatest`) replaces that control's
106
+ markup, and renders only where it does. `#message` slots gain the flat
107
+ `canReply`, `reply()`, `replyPreview` and `jumpToReplyTarget()` members.
108
+ - Adapter. `ConvoKitUiClient.getReplyPreviews?(conversationId, messageIds)`
109
+ and `getMessageContext?(conversationId, options)` are optional and the
110
+ default adapter implements both; without them the jump affordance is hidden
111
+ and reply references render with no quoted text. Replying itself needs no
112
+ adapter member, so rows offer the action even on a 0.8-era adapter.
113
+ - Mixed fleet. Against a 0.8 backend both new routes answer with an uncoded
114
+ 404 (`HTTP_ERROR`), which is never read as "the message is gone": no preview
115
+ becomes `'unavailable'` and no window is replaced. After the first such
116
+ rejection the store turns `canResolveReplyPreviews` / `canJumpToMessage`
117
+ false for its life so the affordance disappears instead of failing
118
+ repeatedly; a coded `404 MESSAGE_NOT_FOUND` never trips that.
119
+ - Requires `@convokitapp/sdk` 0.9.x and the corresponding backend deployment.
120
+ - Mixed fleet, sending: a pre-0.9 backend parses only `text` and `media`, so it
121
+ DROPS an unknown `replyToMessageId` and stores the message with no reference.
122
+ The optimistic row shows the quote and the confirmed row replaces it without
123
+ one, so the quote visibly disappears on send. The message itself is delivered;
124
+ only the reference is lost. This is not detected — the send response is the
125
+ only signal, and acting on it would mean a second request or rewriting a row
126
+ that is already delivered.
127
+
3
128
  ## 0.8.0
4
129
 
5
130
  - Edit and delete your own messages. `useConversation` /
package/PARITY.md CHANGED
@@ -34,6 +34,15 @@ while using Vue-native composition patterns.
34
34
  | Controlled edit surface (`editingMessage`, `onEditMessage`, `onSaveEdit`, `onCancelEdit`, `onDeleteMessage`, `canEditMessage`) | Named params | Props | Props + emits |
35
35
  | Row replacements receive `isEdited` / `canEdit` / `canDelete` / `edit` / `remove` | Controller state | Render props | `#message` slot |
36
36
  | Adapter `editMessage` / `deleteMessage` | Required | Optional | Optional |
37
+ | Quoted replies (0.9.0: `replyTarget`, `startReply`, `cancelReply`, `Message.replyToMessageId`) | Controller | `useConversation` | `useConversation` |
38
+ | Batched reply previews (`replyPreviews`, one request per burst, `'unavailable'` is terminal) | Controller | Store | Store |
39
+ | Jump to message (0.9.0: `jumpToMessage`, `returnToLatest`, `loadNewerMessages`, `windowMode`) | Controller | `useConversation` | `useConversation` |
40
+ | Quoted block + `Reply to message` action (`Original message unavailable`, `Quoted message`) | Scope builder | `.ckui-message-quote` | `.ckui-message-quote` |
41
+ | Composer reply strip (`Cancel reply`, draft untouched) | Scope builder | Nested `replying` | Flat `replying` / `cancelReply` |
42
+ | Jump highlight + focus (`--ckui-highlight`, `aria-busy` while landing) | `ensureVisible` | `scrollIntoView` | `scrollIntoView` |
43
+ | Controlled reply / jump surface (`replyTarget`, `onJumpToMessage`, `onReturnToLatest`, …) | Named params | Props | Props + emits |
44
+ | Row replacements receive `canReply` / `reply` / `replyPreview` / `jumpToReplyTarget` | Scope object | Render props | `#message` slot |
45
+ | Adapter `getReplyPreviews` / `getMessageContext` | Required | Optional | Optional |
37
46
  | Search, archived, participant, predicate, sort filters | Yes | Yes | Yes |
38
47
  | Realtime messages, typing, reads | Yes | Yes | Yes |
39
48
  | Pending state without provisional timestamps or receipts | Yes | Yes | Yes |
package/README.md CHANGED
@@ -11,6 +11,126 @@ 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.9.0 quoted replies and jump to message
15
+
16
+ This release requires JavaScript SDK 0.9.x (`Message.replyToMessageId`,
17
+ `SendMessageInput.replyToMessageId`, `ReplyPreview`, `MessageContextPage`,
18
+ `getReplyPreviews`, `getMessageContext`) and the coordinated backend
19
+ (`GET /api/v1/conversations/:id/reply-previews` and
20
+ `GET /api/v1/conversations/:id/context`). Everything is additive: a room with
21
+ no replies renders exactly as in 0.8.0.
22
+
23
+ - Who may: any active member may quote any message in the room, own or not,
24
+ while the viewer's role (the 0.7 `membership`, else the viewer's participant
25
+ row) is not `READ`; pending rows are never quotable. That is deliberately
26
+ wider than the edit/delete eligibility, and it is **not** routed through
27
+ `canEditMessage` — the separate `canReplyToMessage` prop replaces it.
28
+ - Controller members (`useConversation` and the `Conversation` controller):
29
+ `replyTarget` (the message the composer is quoting, or null),
30
+ `replyPreviews` (quoted parents by `Message.replyToMessageId`),
31
+ `highlightedMessageId`, `jumpInFlight`, `windowMode` (`live` or `jumped`),
32
+ `hasNewerMessages`, `isLoadingNewer`, `canJumpToMessage` /
33
+ `canResolveReplyPreviews` (adapter support), plus `startReply(messageId)`,
34
+ `cancelReply()`, `jumpToMessage(messageId)`, `loadNewerMessages()` and
35
+ `returnToLatest()`. Replying and editing are mutually exclusive: each clears
36
+ the other. `startReply` sends nothing, and unlike edit mode it leaves the
37
+ draft alone.
38
+ - Sending: the reply target is stamped on the optimistic row, so the quoted
39
+ block renders before the server acknowledges the send, and it is cleared
40
+ only once the send is accepted. A send without a quote omits
41
+ `replyToMessageId` entirely, so it stays byte-identical to 0.8.0. The
42
+ reference is write-once: the backend rejects a retry that changes it.
43
+ - Preview resolution is **batched, never per row**: after each page load,
44
+ reconcile and live-insert burst the store issues one `getReplyPreviews` call
45
+ for the distinct parents the loaded window cannot derive (a parent that is
46
+ on screen costs no request). Previews are re-read, never copied: a parent
47
+ edit, a deletion, or a reconnect invalidates the cached entry. An id that a
48
+ **resolved** call did not return is cached as the terminal `'unavailable'`
49
+ and rendered `Original message unavailable`, with the reference and the jump
50
+ affordance intact. A **rejected** call writes no entry for any of its ids:
51
+ they stay unresolved (the row renders the reference with no quoted text),
52
+ the failure surfaces through `error`, and the next trigger asks again.
53
+ - Jump windows: `jumpToMessage` highlights a rendered row without a request;
54
+ otherwise it **replaces** the window with a context window centred on the
55
+ target and sets `windowMode: 'jumped'`. A jump is a window operation, never
56
+ a re-open — tombstones, the acknowledgement floor, this open's captured
57
+ private state, edit mode and the reply target all survive it. While jumped
58
+ the store acknowledges nothing, realtime inserts are recorded but not
59
+ rendered, paging goes through the window's own cursors in both directions,
60
+ and a queued reconcile re-reads the window with one bounded request instead
61
+ of walking history from the tail (the tail reconcile stays owed and runs on
62
+ the return). `returnToLatest()` reloads the newest page, drains the deferred
63
+ rows and acknowledges them; if it fails the window and the affordance stay.
64
+ A jump is refused while a send is in flight, and a send from a jumped window
65
+ returns to the tail first, keeping the draft and the quote.
66
+ - Default rows: a `Reply to message` action joins the existing actions menu,
67
+ and a reply renders `.ckui-message-quote` above its text with the quoted
68
+ author and text, `Original message unavailable` when the parent is gone, or
69
+ the reference alone while the preview is not resolved yet. The block is a
70
+ button (name `Quoted message from <author>`) whenever the view can jump. The
71
+ jumped-to row is centred (`scrollIntoView({ block: 'center', behavior:
72
+ 'instant' })`), focused and tinted with `--ckui-highlight` for about two
73
+ seconds; the list carries `aria-busy="true"` only while the jump lands, and
74
+ stick-to-bottom, pagination and the highlight clear are suspended for that
75
+ window. Rows without the new callbacks render byte-identically to 0.8.0.
76
+ - Composer: a cancellable `.ckui-composer__replying` strip (`role="status"`,
77
+ `Cancel reply`, Escape) sits where the edit banner does. It never replaces
78
+ the draft. `#composer` slots receive `replying` and `cancelReply`.
79
+ - Controlled views: `ConversationView` accepts `replyTarget`,
80
+ `onReplyToMessage`, `onCancelReply`, `canReplyToMessage`,
81
+ `replyPreviewByMessageId`, `onJumpToMessage`, `highlightedMessageId`,
82
+ `jumpInFlight`, `hasNewerMessages`, `isLoadingNewer`, `onLoadNewer` and
83
+ `onReturnToLatest` (also as `@reply-to-message`, `@cancel-reply`,
84
+ `@jump-to-message`, `@load-newer`, `@return-to-latest`);
85
+ `MessageListView` accepts all of those except `replyTarget`,
86
+ `onCancelReply` and `onReturnToLatest`. `onReturnToLatest` is what renders
87
+ the `Jump to latest` control, so pass it only while the window is jumped.
88
+ `#message` slots receive `canReply`, `reply()`, `replyPreview` and
89
+ `jumpToReplyTarget()`; the slot wrapper carries `data-message-id` so a
90
+ replaced row is still a jump target.
91
+ - Adapter additions: `ConvoKitUiClient.getReplyPreviews?(conversationId,
92
+ messageIds)` and `getMessageContext?(conversationId, options)` are optional;
93
+ the default adapter implements both. (The Flutter and Android UIs make the
94
+ same members required on their `ConvoKitUiClient`, a breaking change for
95
+ custom implementers there.) Replying itself needs no adapter member.
96
+ - Mixed fleet: against a 0.8 backend both routes answer Express's unmatched
97
+ route with an uncoded 404 (`HTTP_ERROR`). That is **not** "the message is
98
+ gone": no preview becomes `unavailable` and no window is replaced. After the
99
+ first such rejection the store sets `canResolveReplyPreviews` /
100
+ `canJumpToMessage` to false for its life, so the affordance disappears
101
+ instead of failing repeatedly; a coded `404 MESSAGE_NOT_FOUND` never does
102
+ that — it is a real missing target and marks that one preview `unavailable`.
103
+ A 0.8 client against a 0.9 backend simply ignores the new row key.
104
+
105
+ ```vue
106
+ <script setup lang="ts">
107
+ import { ref } from 'vue'
108
+ import type { Message, ReplyPreview } from '@convokitapp/sdk'
109
+ import { ConversationView } from '@convokitapp/vue-ui'
110
+
111
+ const replyTarget = ref<Message | null>(null)
112
+ const previews = ref(new Map<string, ReplyPreview | 'unavailable'>())
113
+ const highlighted = ref<string | null>(null)
114
+ async function send(text: string) {
115
+ await api.sendMessage({ text, ...(replyTarget.value ? { replyToMessageId: replyTarget.value.id } : {}) })
116
+ replyTarget.value = null
117
+ }
118
+ </script>
119
+
120
+ <template>
121
+ <ConversationView
122
+ v-bind="conversationProps"
123
+ :reply-target="replyTarget"
124
+ :reply-preview-by-message-id="previews"
125
+ :highlighted-message-id="highlighted"
126
+ @send-message="send"
127
+ @reply-to-message="(message) => (replyTarget = message)"
128
+ @cancel-reply="replyTarget = null"
129
+ @jump-to-message="(messageId) => (highlighted = messageId)"
130
+ />
131
+ </template>
132
+ ```
133
+
14
134
  ## 0.8.0 edit and delete your own messages
15
135
 
16
136
  This release requires JavaScript SDK 0.8.x (`editMessage`, `deleteMessage`,
@@ -414,7 +534,12 @@ is eligible, `edit()` and `remove()`; `#composer` slots receive `editing` and
414
534
 
415
535
  Available slots include `conversation-item`, `separator`, `header`, `message`,
416
536
  `media`, `read-receipt`, `composer`, `typing-indicator`, `loading`, `empty`,
417
- `error`, `load-more`, `loading-older`, and `message-error`.
537
+ `error`, `load-more`, `loading-older`, `loading-newer`, `jump-to-latest`, and
538
+ `message-error`. `#loading-newer` (in `MessageListView`) replaces the newer-end
539
+ spinner of a jumped window, and `#jump-to-latest` (in `ConversationView`)
540
+ replaces the default `Jump to latest` control, receiving `returnToLatest`
541
+ (`JumpToLatestSlotProps`); both render only while the window is jumped, so a
542
+ live window stays byte-identical to 0.8.0 (0.9.0).
418
543
 
419
544
  ## Composables
420
545