@convokitapp/vue-ui 0.7.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 +225 -0
- package/PARITY.md +46 -0
- package/README.md +244 -5
- package/dist/index.cjs +1235 -36
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +167 -0
- package/dist/index.css.map +1 -1
- package/dist/index.d.cts +723 -7
- package/dist/index.d.ts +723 -7
- package/dist/index.js +1243 -40
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,230 @@
|
|
|
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
|
+
|
|
128
|
+
## 0.8.0
|
|
129
|
+
|
|
130
|
+
- Edit and delete your own messages. `useConversation` /
|
|
131
|
+
`ConversationController` gain `editingMessage` (the row being edited, as
|
|
132
|
+
the user saw it when editing started; null otherwise), `canEditMessages` /
|
|
133
|
+
`canDeleteMessages` (adapter support, decided once at construction),
|
|
134
|
+
`startEditing(messageId)` (a no-op unless the row is rendered, the viewer's
|
|
135
|
+
own, confirmed, not known to be gone, the viewer's role when known is not
|
|
136
|
+
`READ` and the adapter implements `editMessage`; sends nothing, not even a
|
|
137
|
+
typing update), `cancelEditing()`, `saveEdit(text)` and
|
|
138
|
+
`deleteMessage(messageId)` (both resolve to a boolean and reject when the
|
|
139
|
+
adapter lacks the member). `saveEdit` trims the text, sends `null` for an
|
|
140
|
+
empty caption, never sends an empty text for a text-only message, and always
|
|
141
|
+
sends the SNAPSHOT's `revision`, never the live row's; the response is
|
|
142
|
+
merged through the tombstone and precedence guards and edit mode ends. A
|
|
143
|
+
stale revision (409 `REVISION_CONFLICT`) reloads the row once through
|
|
144
|
+
`getMessage`, replaces `editingMessage` with it (so the next save carries
|
|
145
|
+
the fresh revision), reports the conflict through `error` and keeps edit
|
|
146
|
+
mode; when a row for the edited message with a higher revision reaches the
|
|
147
|
+
store by itself (a row image, a hydration, a reconcile or a refresh) the
|
|
148
|
+
same conflict state is entered without a request. A coded 404
|
|
149
|
+
(`MESSAGE_NOT_FOUND`, on the save or on the conflict reload) removes the
|
|
150
|
+
row and ends edit mode; any other failure, including a 403 and the uncoded
|
|
151
|
+
404 a 0.7 backend answers, is reported through `error` without evicting
|
|
152
|
+
anything and keeps edit mode. `deleteMessage` keeps the row until the server
|
|
153
|
+
answers (no optimistic removal): success or `MESSAGE_NOT_FOUND` tombstone
|
|
154
|
+
and remove it (late responses, row images and hydrations for the id are
|
|
155
|
+
dropped, the acknowledgement target is re-resolved, edit mode on it ends);
|
|
156
|
+
other failures keep it and report through `error`. A `message_deleted`
|
|
157
|
+
notification, a reconcile that no longer sees the row or a history eviction
|
|
158
|
+
also ends edit mode on it.
|
|
159
|
+
- Row precedence. Wherever rows meet (live events, hydration, reconcile,
|
|
160
|
+
older pages, send and edit responses) the higher `Message.revision` now
|
|
161
|
+
wins when both rows carry one and at least one is above 0, and a lower
|
|
162
|
+
revision never overwrites a higher one; equal revisions and rows without a
|
|
163
|
+
usable revision (pending rows, 0.7 backends where every row is 0) keep the
|
|
164
|
+
`updatedAt ?? createdAt` rule with its "complete row wins on ties"
|
|
165
|
+
heuristic. Pending rows are created with `revision: 0`.
|
|
166
|
+
- Default rows. Eligible rows (the viewer's own confirmed rows while the
|
|
167
|
+
viewer's role is not `READ`, or whatever `canEditMessage` says; never
|
|
168
|
+
pending rows) render `.ckui-message-actions` with `Edit message` and
|
|
169
|
+
`Delete message` icon buttons only while the matching callback is present
|
|
170
|
+
and the adapter supports the action. The actions are always in the DOM;
|
|
171
|
+
only `@media (hover: hover) and (pointer: fine)` conceals them
|
|
172
|
+
(`opacity: 0; pointer-events: none`, never `display: none`) until the row is
|
|
173
|
+
hovered or focused, so touch and keyboard users always see them. `Delete
|
|
174
|
+
message` opens an inline prompt (`role="group"`, accessible name
|
|
175
|
+
`Delete this message?`) with `Delete` (`Confirm delete`) and `Cancel`
|
|
176
|
+
(`Cancel delete`); `confirmDelete` replaces it. Rows with `revision > 0`
|
|
177
|
+
show `Edited` (`.ckui-message-edited`, accessible name `Edited`) beside the
|
|
178
|
+
time, for every sender, never on pending rows. Apart from that label, rows
|
|
179
|
+
that are not eligible, and every row when the callbacks are absent, render
|
|
180
|
+
byte-identically to 0.7.0.
|
|
181
|
+
- Composer edit mode (`ConversationView`, driven by `editingMessage`).
|
|
182
|
+
Entering stashes the unsent draft and prefills the field with the message
|
|
183
|
+
text without a typing update; the submit path (Enter, the primary button,
|
|
184
|
+
a custom composer's `send`) calls `onSaveEdit(message, text)` instead of
|
|
185
|
+
`onSendMessage`, without clearing the field first. `false` keeps edit mode
|
|
186
|
+
and the edited text; any other result, and Cancel (visible `Cancel`,
|
|
187
|
+
accessible name `Cancel editing`) or Escape, restore the stash and report
|
|
188
|
+
typing from it (`onTypingChange(stash.trim().length > 0)`). When edit mode
|
|
189
|
+
ends externally the field keeps text the user changed and restores the
|
|
190
|
+
stash only when it is empty or still the snapshot's text; a refreshed
|
|
191
|
+
snapshot for the same message leaves the draft alone. The primary button is
|
|
192
|
+
`Check` with the accessible name `Save message` (`Send message` otherwise)
|
|
193
|
+
and is enabled while the field has text or the edited row has attachments
|
|
194
|
+
(an empty caption clears it); a banner (`.ckui-composer__editing`,
|
|
195
|
+
`role="status"`) shows `Editing message` with the message's text or
|
|
196
|
+
attachment count. `#composer` slots receive `editing` and `cancelEdit`
|
|
197
|
+
while editing.
|
|
198
|
+
- Controlled views. `ConversationView` gains `editingMessage`,
|
|
199
|
+
`onEditMessage`, `onSaveEdit`, `onCancelEdit`, `onDeleteMessage`,
|
|
200
|
+
`canEditMessage` and `confirmDelete` (and the `edit-message`, `save-edit`,
|
|
201
|
+
`cancel-edit`, `delete-message` emits); `MessageListView` gains
|
|
202
|
+
`onEditMessage`, `onDeleteMessage`, `canEditMessage` and `confirmDelete`.
|
|
203
|
+
`#message` slots receive `isEdited`, `canEdit`, `canDelete` and, while
|
|
204
|
+
eligible, `edit()` and `remove()` (`remove` asks `confirmDelete` when the
|
|
205
|
+
view has one, otherwise deletes at once: a custom row owns its prompt). The
|
|
206
|
+
bound `Conversation` wires the composable (`editingMessage`,
|
|
207
|
+
`startEditing`, `saveEdit`, `cancelEditing`, `deleteMessage`) only while the
|
|
208
|
+
adapter supports each action, forwards `canEditMessage` and
|
|
209
|
+
`confirmDelete`, and emits the four events like `send-message`.
|
|
210
|
+
- Adapter: optional `editMessage?(messageId, { text, revision })` and
|
|
211
|
+
`deleteMessage?(messageId)`; the default adapter implements both. Without
|
|
212
|
+
them nothing renders and `saveEdit` / `deleteMessage` reject. (The Flutter
|
|
213
|
+
and Android UIs make the members required on their `ConvoKitUiClient`: a
|
|
214
|
+
breaking change for custom implementers there, like `listInbox` in 0.6.0.)
|
|
215
|
+
A custom adapter must reject a stale revision with `code:
|
|
216
|
+
'REVISION_CONFLICT'` and a gone message with `code: 'MESSAGE_NOT_FOUND'`;
|
|
217
|
+
an uncoded 404 is reported, never treated as a deleted message.
|
|
218
|
+
- Requires core SDK 0.8.x (`editMessage`, `deleteMessage`, the required
|
|
219
|
+
`Message.revision`, `isEditedMessage`) and the coordinated backend
|
|
220
|
+
(`PATCH`/`DELETE /api/v1/messages/:id/own`). Consumer-built `Message`
|
|
221
|
+
literals gain `revision`. Mixed fleet: against a 0.7 backend the author
|
|
222
|
+
routes answer an uncoded 404 (`HTTP_ERROR`) that surfaces as `error` and
|
|
223
|
+
keeps the row; every row parses with revision 0, so nothing reads `Edited`
|
|
224
|
+
and precedence falls back to timestamps. Deleting a message does not
|
|
225
|
+
retract files other members already received; stored files are reclaimed by
|
|
226
|
+
the existing user or app deletion cleanup.
|
|
227
|
+
|
|
3
228
|
## 0.7.0
|
|
4
229
|
|
|
5
230
|
- Private mark unread. `useConversationList` / `ConversationListController`
|
package/PARITY.md
CHANGED
|
@@ -26,6 +26,23 @@ while using Vue-native composition patterns.
|
|
|
26
26
|
| Empty marked room clears through `clearConversationUnread` | Yes | Yes | Yes |
|
|
27
27
|
| Adapter `markConversationUnread` / `clearConversationUnread` | Required | Optional | Optional |
|
|
28
28
|
| Inbox 404 fallback to the offset path | Yes | Yes | Yes |
|
|
29
|
+
| Edit / delete own messages (0.8.0: `editingMessage`, `startEditing`, `cancelEditing`, `saveEdit`, `deleteMessage`) | Controller | `useConversation` | `useConversation` |
|
|
30
|
+
| Row precedence by `Message.revision`, timestamps on ties | Yes | Yes | Yes |
|
|
31
|
+
| Default row actions + `Edited` label (accessible names `Edit message` / `Delete message` / `Edited`) | Long press + sheet | `.ckui-message-actions` hover/focus reveal | `.ckui-message-actions` hover/focus reveal |
|
|
32
|
+
| Delete confirmation (`Confirm delete` / `Cancel delete`), `confirmDelete` override | `showDialog` | Inline prompt | Inline prompt |
|
|
33
|
+
| Composer edit mode (banner, `Save message`, `Cancel editing`, caption clearing, draft stash) | Yes | Yes | Yes |
|
|
34
|
+
| Controlled edit surface (`editingMessage`, `onEditMessage`, `onSaveEdit`, `onCancelEdit`, `onDeleteMessage`, `canEditMessage`) | Named params | Props | Props + emits |
|
|
35
|
+
| Row replacements receive `isEdited` / `canEdit` / `canDelete` / `edit` / `remove` | Controller state | Render props | `#message` slot |
|
|
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 |
|
|
29
46
|
| Search, archived, participant, predicate, sort filters | Yes | Yes | Yes |
|
|
30
47
|
| Realtime messages, typing, reads | Yes | Yes | Yes |
|
|
31
48
|
| Pending state without provisional timestamps or receipts | Yes | Yes | Yes |
|
|
@@ -91,3 +108,32 @@ the TypeScript UIs and required in the Dart and Kotlin ones (a breaking
|
|
|
91
108
|
change for custom implementers there, like `listInbox` in 0.6.0). The parity
|
|
92
109
|
script checks API presence only; the store, rendered-view and actual-SDK
|
|
93
110
|
protocol tests verify behavior.
|
|
111
|
+
|
|
112
|
+
## Message-edit parity (0.8.0)
|
|
113
|
+
|
|
114
|
+
Every UI lets the viewer edit and delete their own confirmed messages through
|
|
115
|
+
the room controller (`editingMessage`, `canEditMessages` /
|
|
116
|
+
`canDeleteMessages`, `startEditing`, `cancelEditing`, `saveEdit`,
|
|
117
|
+
`deleteMessage`), sends the SNAPSHOT's `revision` captured when editing
|
|
118
|
+
started (never the live row's), reloads the row once on 409
|
|
119
|
+
`REVISION_CONFLICT` and refreshes the snapshot so the next save carries the
|
|
120
|
+
fresh revision, enters the same conflict state without a request when a
|
|
121
|
+
newer row for the edited message arrives on its own, removes the row only on
|
|
122
|
+
a coded 404 `MESSAGE_NOT_FOUND` (an uncoded 404 from a 0.7 backend is an
|
|
123
|
+
error that keeps the row and edit mode), and keeps the row until the server
|
|
124
|
+
accepts a deletion. Rows merge by the higher `Message.revision` when both
|
|
125
|
+
carry one and at least one is above 0, falling back to `updatedAt ??
|
|
126
|
+
createdAt` on ties and for pending rows. Default rows offer `Edit message` /
|
|
127
|
+
`Delete message` (web: `.ckui-message-actions`, always in the DOM, concealed
|
|
128
|
+
only under `(hover: hover) and (pointer: fine)` until hover or focus; natives:
|
|
129
|
+
long press or context menu) with a confirmation (`Confirm delete` /
|
|
130
|
+
`Cancel delete`; `confirmDelete` overrides it) and show `Edited` for
|
|
131
|
+
`revision > 0`; rows that are not eligible render as in 0.7.0. The composer's
|
|
132
|
+
single submit path saves while editing, prefills the message text without a
|
|
133
|
+
typing update, stashes and restores the unsent draft, keeps the edited text on
|
|
134
|
+
failure or conflict, and allows an empty caption for a message with
|
|
135
|
+
attachments (`text: null`). The adapter additions are optional in the
|
|
136
|
+
TypeScript UIs and required in the Dart and Kotlin ones (a breaking change for
|
|
137
|
+
custom implementers there, like `listInbox` in 0.6.0); the Swift UI's bound
|
|
138
|
+
view gains `onController`. The parity script checks API presence only; the
|
|
139
|
+
store, rendered-view and actual-SDK protocol tests verify behavior.
|
package/README.md
CHANGED
|
@@ -11,6 +11,232 @@ 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
|
+
|
|
134
|
+
## 0.8.0 edit and delete your own messages
|
|
135
|
+
|
|
136
|
+
This release requires JavaScript SDK 0.8.x (`editMessage`, `deleteMessage`,
|
|
137
|
+
the required `Message.revision`, `isEditedMessage`) and the coordinated
|
|
138
|
+
backend (`PATCH`/`DELETE /api/v1/messages/:id/own`). Consumer-built `Message`
|
|
139
|
+
literals (fixtures, controlled views) gain `revision`.
|
|
140
|
+
|
|
141
|
+
- Who may: the viewer's own confirmed messages, while the viewer's role (the
|
|
142
|
+
0.7 `membership`, else the viewer's participant row) is not `READ`. The
|
|
143
|
+
server checks the same rules (403 for other members' messages and `READ`
|
|
144
|
+
roles); custom row eligibility goes through `canEditMessage`.
|
|
145
|
+
- Controller members (`useConversation` and the `Conversation` controller):
|
|
146
|
+
`editingMessage` (the row being edited as the user saw it; null otherwise),
|
|
147
|
+
`canEditMessages` / `canDeleteMessages` (adapter support),
|
|
148
|
+
`startEditing(messageId)`, `cancelEditing()`, `saveEdit(text)` and
|
|
149
|
+
`deleteMessage(messageId)`. `saveEdit` sends the snapshot's `revision`,
|
|
150
|
+
never the live row's: a stale one answers 409 `REVISION_CONFLICT`, the
|
|
151
|
+
store reloads the row once, shows its current content as the new snapshot
|
|
152
|
+
(the banner and the row update, the draft stays) and reports the conflict
|
|
153
|
+
through `error`; the next save carries the fresh revision. A newer row for
|
|
154
|
+
the edited message arriving on its own (row image, hydration, reconcile)
|
|
155
|
+
enters the same state without a request. A coded 404 `MESSAGE_NOT_FOUND`
|
|
156
|
+
removes the row and ends edit mode; a 403, a 500, a network failure or the
|
|
157
|
+
uncoded 404 a 0.7 backend answers keep the row, the history and edit mode
|
|
158
|
+
and set `error`. `deleteMessage` removes nothing until the server accepts
|
|
159
|
+
(or answers `MESSAGE_NOT_FOUND`); other devices learn through the room's
|
|
160
|
+
deletion notification and late responses for a removed id are dropped.
|
|
161
|
+
- Precedence: when rows meet, the higher `Message.revision` wins when both
|
|
162
|
+
carry one and at least one is above 0; ties and rows without a usable
|
|
163
|
+
revision keep the `updatedAt ?? createdAt` rule.
|
|
164
|
+
- Default row actions: eligible rows render `.ckui-message-actions` with
|
|
165
|
+
`Edit message` and `Delete message` buttons, always in the DOM. Only
|
|
166
|
+
`@media (hover: hover) and (pointer: fine)` conceals them (`opacity` and
|
|
167
|
+
`pointer-events`, never `display: none`) until the row is hovered or
|
|
168
|
+
focused; touch and keyboard users always see them. `Delete message` opens an
|
|
169
|
+
inline prompt (`role="group"`, name `Delete this message?`) with `Delete`
|
|
170
|
+
(`Confirm delete`) and `Cancel` (`Cancel delete`); pass `confirmDelete` to
|
|
171
|
+
use your own dialog. Edited rows show `Edited` (name `Edited`) beside the
|
|
172
|
+
time. Apart from that label, rows that are not eligible, and every row
|
|
173
|
+
without the callbacks, render byte-identically to 0.7.0.
|
|
174
|
+
- Composer edit mode: entering stashes the unsent draft and prefills the
|
|
175
|
+
field with the message text without a typing update. Enter, the primary
|
|
176
|
+
button (`Check`, name `Save message`) or a custom composer's `send` saves;
|
|
177
|
+
Cancel (name `Cancel editing`) and Escape restore the stash. Save is
|
|
178
|
+
enabled while the field has text or the message has attachments: an empty
|
|
179
|
+
caption is sent as `null` and clears it (attachments are never changed). A
|
|
180
|
+
refused save keeps the edited text and edit mode; a successful one and a
|
|
181
|
+
cancel restore the stash. `#composer` slots receive `editing` and
|
|
182
|
+
`cancelEdit` while editing.
|
|
183
|
+
- Controlled views: `ConversationView` accepts `editingMessage`,
|
|
184
|
+
`onEditMessage`, `onSaveEdit`, `onCancelEdit`, `onDeleteMessage`,
|
|
185
|
+
`canEditMessage` and `confirmDelete` (also as `@edit-message`, `@save-edit`,
|
|
186
|
+
`@cancel-edit`, `@delete-message`); `MessageListView` accepts
|
|
187
|
+
`onEditMessage`, `onDeleteMessage`, `canEditMessage` and `confirmDelete`.
|
|
188
|
+
Without the callbacks nothing new renders. `#message` slots receive
|
|
189
|
+
`isEdited`, `canEdit`, `canDelete` and, while eligible, `edit()` and
|
|
190
|
+
`remove()` (`remove` runs `confirmDelete` when present, otherwise deletes at
|
|
191
|
+
once). The bound `Conversation` wires the composable, forwards
|
|
192
|
+
`canEditMessage` and `confirmDelete`, and emits the four events.
|
|
193
|
+
- Adapter additions: `ConvoKitUiClient.editMessage?(messageId, { text,
|
|
194
|
+
revision })` and `deleteMessage?(messageId)` are optional; the default
|
|
195
|
+
adapter implements both. Without them the rows render no actions and
|
|
196
|
+
`saveEdit` / `deleteMessage` reject. (The Flutter and Android UIs make the
|
|
197
|
+
same members required on their `ConvoKitUiClient`: a breaking change for
|
|
198
|
+
custom implementers there, like `listInbox` in 0.6.0.) Custom adapters must
|
|
199
|
+
reject a stale revision with `code: 'REVISION_CONFLICT'` and a gone message
|
|
200
|
+
with `code: 'MESSAGE_NOT_FOUND'`.
|
|
201
|
+
- Deleting a message removes it and its attachments from the conversation for
|
|
202
|
+
every member and cannot be undone; files already received or downloaded
|
|
203
|
+
cannot be retracted, and stored files are reclaimed by the existing user or
|
|
204
|
+
app deletion cleanup.
|
|
205
|
+
- Mixed fleet: 0.7 clients ignore `revision`; against a 0.7 backend the author
|
|
206
|
+
routes answer an uncoded 404 (`HTTP_ERROR`) that keeps the row and edit
|
|
207
|
+
mode, every row parses with revision 0 and nothing reads `Edited`.
|
|
208
|
+
|
|
209
|
+
```vue
|
|
210
|
+
<script setup lang="ts">
|
|
211
|
+
import { ref } from 'vue'
|
|
212
|
+
import type { Message } from '@convokitapp/sdk'
|
|
213
|
+
import { ConversationView } from '@convokitapp/vue-ui'
|
|
214
|
+
|
|
215
|
+
const editing = ref<Message | null>(null)
|
|
216
|
+
const rows = ref<Message[]>([])
|
|
217
|
+
async function saveEdit(message: Message, text: string) {
|
|
218
|
+
// Send message.revision; on 409 REVISION_CONFLICT reload the row and let the user retry.
|
|
219
|
+
const saved = await api.editMessage(message.id, { text: text || null, revision: message.revision })
|
|
220
|
+
rows.value = rows.value.map((row) => (row.id === saved.id ? saved : row))
|
|
221
|
+
editing.value = null
|
|
222
|
+
}
|
|
223
|
+
const confirmDelete = (message: Message) => window.confirm(`Delete "${message.text ?? 'this message'}"?`)
|
|
224
|
+
</script>
|
|
225
|
+
|
|
226
|
+
<template>
|
|
227
|
+
<ConversationView
|
|
228
|
+
v-bind="conversationProps"
|
|
229
|
+
:messages="rows"
|
|
230
|
+
:editing-message="editing"
|
|
231
|
+
@edit-message="(message) => (editing = message)"
|
|
232
|
+
@save-edit="saveEdit"
|
|
233
|
+
@cancel-edit="editing = null"
|
|
234
|
+
@delete-message="(message) => api.deleteMessage(message.id)"
|
|
235
|
+
:confirm-delete="confirmDelete"
|
|
236
|
+
/>
|
|
237
|
+
</template>
|
|
238
|
+
```
|
|
239
|
+
|
|
14
240
|
## 0.7.0 private mark unread
|
|
15
241
|
|
|
16
242
|
This release requires JavaScript SDK 0.7.x (`markConversationUnread`,
|
|
@@ -292,18 +518,28 @@ state directly. Every important surface has a named slot:
|
|
|
292
518
|
<template #header="{ conversation }">
|
|
293
519
|
<SupportHeader :title="conversation.displayTitle" />
|
|
294
520
|
</template>
|
|
295
|
-
<template #message="{ message, isCurrentUser, readerIds }">
|
|
296
|
-
<SupportBubble :message="message" :mine="isCurrentUser" :read-by="readerIds" />
|
|
521
|
+
<template #message="{ message, isCurrentUser, readerIds, isEdited, edit, remove }">
|
|
522
|
+
<SupportBubble :message="message" :mine="isCurrentUser" :read-by="readerIds" :edited="isEdited" @edit="edit" @delete="remove" />
|
|
297
523
|
</template>
|
|
298
|
-
<template #composer="{ value, setValue, send }">
|
|
299
|
-
<BrandComposer :model-value="value" @update:model-value="setValue" @send="send" />
|
|
524
|
+
<template #composer="{ value, setValue, send, editing, cancelEdit }">
|
|
525
|
+
<BrandComposer :model-value="value" :editing="editing" @update:model-value="setValue" @send="send" @cancel="cancelEdit" />
|
|
300
526
|
</template>
|
|
301
527
|
</ConversationView>
|
|
302
528
|
```
|
|
303
529
|
|
|
530
|
+
`#message` slots receive `isEdited`, `canEdit`, `canDelete` and, while the row
|
|
531
|
+
is eligible, `edit()` and `remove()`; `#composer` slots receive `editing` and
|
|
532
|
+
`cancelEdit` while the view is in edit mode, and their `send` saves the edit
|
|
533
|
+
(0.8.0).
|
|
534
|
+
|
|
304
535
|
Available slots include `conversation-item`, `separator`, `header`, `message`,
|
|
305
536
|
`media`, `read-receipt`, `composer`, `typing-indicator`, `loading`, `empty`,
|
|
306
|
-
`error`, `load-more`, `loading-older`,
|
|
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).
|
|
307
543
|
|
|
308
544
|
## Composables
|
|
309
545
|
|
|
@@ -317,6 +553,9 @@ for a host-built row action.
|
|
|
317
553
|
`useConversation` also exposes `readPositionByUserId`, `readerIdsFor(message)`,
|
|
318
554
|
`markRead()` and `setVisible(visible)`; call `setVisible(false)` while your own
|
|
319
555
|
surface hides the room so automatic acknowledgements wait until it is shown.
|
|
556
|
+
For the viewer's own messages it exposes `editingMessage`, `canEditMessages`,
|
|
557
|
+
`canDeleteMessages`, `startEditing(messageId)`, `cancelEditing()`,
|
|
558
|
+
`saveEdit(text)` and `deleteMessage(messageId)` (0.8.0).
|
|
320
559
|
|
|
321
560
|
Optimistic rows display `Sending…` until acknowledgement. The final server
|
|
322
561
|
timestamp is formatted in the viewer's local timezone. Custom message slots can
|