@convokitapp/vue-ui 0.7.0 → 0.8.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,105 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.0
4
+
5
+ - Edit and delete your own messages. `useConversation` /
6
+ `ConversationController` gain `editingMessage` (the row being edited, as
7
+ the user saw it when editing started; null otherwise), `canEditMessages` /
8
+ `canDeleteMessages` (adapter support, decided once at construction),
9
+ `startEditing(messageId)` (a no-op unless the row is rendered, the viewer's
10
+ own, confirmed, not known to be gone, the viewer's role when known is not
11
+ `READ` and the adapter implements `editMessage`; sends nothing, not even a
12
+ typing update), `cancelEditing()`, `saveEdit(text)` and
13
+ `deleteMessage(messageId)` (both resolve to a boolean and reject when the
14
+ adapter lacks the member). `saveEdit` trims the text, sends `null` for an
15
+ empty caption, never sends an empty text for a text-only message, and always
16
+ sends the SNAPSHOT's `revision`, never the live row's; the response is
17
+ merged through the tombstone and precedence guards and edit mode ends. A
18
+ stale revision (409 `REVISION_CONFLICT`) reloads the row once through
19
+ `getMessage`, replaces `editingMessage` with it (so the next save carries
20
+ the fresh revision), reports the conflict through `error` and keeps edit
21
+ mode; when a row for the edited message with a higher revision reaches the
22
+ store by itself (a row image, a hydration, a reconcile or a refresh) the
23
+ same conflict state is entered without a request. A coded 404
24
+ (`MESSAGE_NOT_FOUND`, on the save or on the conflict reload) removes the
25
+ row and ends edit mode; any other failure, including a 403 and the uncoded
26
+ 404 a 0.7 backend answers, is reported through `error` without evicting
27
+ anything and keeps edit mode. `deleteMessage` keeps the row until the server
28
+ answers (no optimistic removal): success or `MESSAGE_NOT_FOUND` tombstone
29
+ and remove it (late responses, row images and hydrations for the id are
30
+ dropped, the acknowledgement target is re-resolved, edit mode on it ends);
31
+ other failures keep it and report through `error`. A `message_deleted`
32
+ notification, a reconcile that no longer sees the row or a history eviction
33
+ also ends edit mode on it.
34
+ - Row precedence. Wherever rows meet (live events, hydration, reconcile,
35
+ older pages, send and edit responses) the higher `Message.revision` now
36
+ wins when both rows carry one and at least one is above 0, and a lower
37
+ revision never overwrites a higher one; equal revisions and rows without a
38
+ usable revision (pending rows, 0.7 backends where every row is 0) keep the
39
+ `updatedAt ?? createdAt` rule with its "complete row wins on ties"
40
+ heuristic. Pending rows are created with `revision: 0`.
41
+ - Default rows. Eligible rows (the viewer's own confirmed rows while the
42
+ viewer's role is not `READ`, or whatever `canEditMessage` says; never
43
+ pending rows) render `.ckui-message-actions` with `Edit message` and
44
+ `Delete message` icon buttons only while the matching callback is present
45
+ and the adapter supports the action. The actions are always in the DOM;
46
+ only `@media (hover: hover) and (pointer: fine)` conceals them
47
+ (`opacity: 0; pointer-events: none`, never `display: none`) until the row is
48
+ hovered or focused, so touch and keyboard users always see them. `Delete
49
+ message` opens an inline prompt (`role="group"`, accessible name
50
+ `Delete this message?`) with `Delete` (`Confirm delete`) and `Cancel`
51
+ (`Cancel delete`); `confirmDelete` replaces it. Rows with `revision > 0`
52
+ show `Edited` (`.ckui-message-edited`, accessible name `Edited`) beside the
53
+ time, for every sender, never on pending rows. Apart from that label, rows
54
+ that are not eligible, and every row when the callbacks are absent, render
55
+ byte-identically to 0.7.0.
56
+ - Composer edit mode (`ConversationView`, driven by `editingMessage`).
57
+ Entering stashes the unsent draft and prefills the field with the message
58
+ text without a typing update; the submit path (Enter, the primary button,
59
+ a custom composer's `send`) calls `onSaveEdit(message, text)` instead of
60
+ `onSendMessage`, without clearing the field first. `false` keeps edit mode
61
+ and the edited text; any other result, and Cancel (visible `Cancel`,
62
+ accessible name `Cancel editing`) or Escape, restore the stash and report
63
+ typing from it (`onTypingChange(stash.trim().length > 0)`). When edit mode
64
+ ends externally the field keeps text the user changed and restores the
65
+ stash only when it is empty or still the snapshot's text; a refreshed
66
+ snapshot for the same message leaves the draft alone. The primary button is
67
+ `Check` with the accessible name `Save message` (`Send message` otherwise)
68
+ and is enabled while the field has text or the edited row has attachments
69
+ (an empty caption clears it); a banner (`.ckui-composer__editing`,
70
+ `role="status"`) shows `Editing message` with the message's text or
71
+ attachment count. `#composer` slots receive `editing` and `cancelEdit`
72
+ while editing.
73
+ - Controlled views. `ConversationView` gains `editingMessage`,
74
+ `onEditMessage`, `onSaveEdit`, `onCancelEdit`, `onDeleteMessage`,
75
+ `canEditMessage` and `confirmDelete` (and the `edit-message`, `save-edit`,
76
+ `cancel-edit`, `delete-message` emits); `MessageListView` gains
77
+ `onEditMessage`, `onDeleteMessage`, `canEditMessage` and `confirmDelete`.
78
+ `#message` slots receive `isEdited`, `canEdit`, `canDelete` and, while
79
+ eligible, `edit()` and `remove()` (`remove` asks `confirmDelete` when the
80
+ view has one, otherwise deletes at once: a custom row owns its prompt). The
81
+ bound `Conversation` wires the composable (`editingMessage`,
82
+ `startEditing`, `saveEdit`, `cancelEditing`, `deleteMessage`) only while the
83
+ adapter supports each action, forwards `canEditMessage` and
84
+ `confirmDelete`, and emits the four events like `send-message`.
85
+ - Adapter: optional `editMessage?(messageId, { text, revision })` and
86
+ `deleteMessage?(messageId)`; the default adapter implements both. Without
87
+ them nothing renders and `saveEdit` / `deleteMessage` reject. (The Flutter
88
+ and Android UIs make the members required on their `ConvoKitUiClient`: a
89
+ breaking change for custom implementers there, like `listInbox` in 0.6.0.)
90
+ A custom adapter must reject a stale revision with `code:
91
+ 'REVISION_CONFLICT'` and a gone message with `code: 'MESSAGE_NOT_FOUND'`;
92
+ an uncoded 404 is reported, never treated as a deleted message.
93
+ - Requires core SDK 0.8.x (`editMessage`, `deleteMessage`, the required
94
+ `Message.revision`, `isEditedMessage`) and the coordinated backend
95
+ (`PATCH`/`DELETE /api/v1/messages/:id/own`). Consumer-built `Message`
96
+ literals gain `revision`. Mixed fleet: against a 0.7 backend the author
97
+ routes answer an uncoded 404 (`HTTP_ERROR`) that surfaces as `error` and
98
+ keeps the row; every row parses with revision 0, so nothing reads `Edited`
99
+ and precedence falls back to timestamps. Deleting a message does not
100
+ retract files other members already received; stored files are reclaimed by
101
+ the existing user or app deletion cleanup.
102
+
3
103
  ## 0.7.0
4
104
 
5
105
  - Private mark unread. `useConversationList` / `ConversationListController`
package/PARITY.md CHANGED
@@ -26,6 +26,14 @@ 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 |
29
37
  | Search, archived, participant, predicate, sort filters | Yes | Yes | Yes |
30
38
  | Realtime messages, typing, reads | Yes | Yes | Yes |
31
39
  | Pending state without provisional timestamps or receipts | Yes | Yes | Yes |
@@ -91,3 +99,32 @@ the TypeScript UIs and required in the Dart and Kotlin ones (a breaking
91
99
  change for custom implementers there, like `listInbox` in 0.6.0). The parity
92
100
  script checks API presence only; the store, rendered-view and actual-SDK
93
101
  protocol tests verify behavior.
102
+
103
+ ## Message-edit parity (0.8.0)
104
+
105
+ Every UI lets the viewer edit and delete their own confirmed messages through
106
+ the room controller (`editingMessage`, `canEditMessages` /
107
+ `canDeleteMessages`, `startEditing`, `cancelEditing`, `saveEdit`,
108
+ `deleteMessage`), sends the SNAPSHOT's `revision` captured when editing
109
+ started (never the live row's), reloads the row once on 409
110
+ `REVISION_CONFLICT` and refreshes the snapshot so the next save carries the
111
+ fresh revision, enters the same conflict state without a request when a
112
+ newer row for the edited message arrives on its own, removes the row only on
113
+ a coded 404 `MESSAGE_NOT_FOUND` (an uncoded 404 from a 0.7 backend is an
114
+ error that keeps the row and edit mode), and keeps the row until the server
115
+ accepts a deletion. Rows merge by the higher `Message.revision` when both
116
+ carry one and at least one is above 0, falling back to `updatedAt ??
117
+ createdAt` on ties and for pending rows. Default rows offer `Edit message` /
118
+ `Delete message` (web: `.ckui-message-actions`, always in the DOM, concealed
119
+ only under `(hover: hover) and (pointer: fine)` until hover or focus; natives:
120
+ long press or context menu) with a confirmation (`Confirm delete` /
121
+ `Cancel delete`; `confirmDelete` overrides it) and show `Edited` for
122
+ `revision > 0`; rows that are not eligible render as in 0.7.0. The composer's
123
+ single submit path saves while editing, prefills the message text without a
124
+ typing update, stashes and restores the unsent draft, keeps the edited text on
125
+ failure or conflict, and allows an empty caption for a message with
126
+ attachments (`text: null`). The adapter additions are optional in the
127
+ TypeScript UIs and required in the Dart and Kotlin ones (a breaking change for
128
+ custom implementers there, like `listInbox` in 0.6.0); the Swift UI's bound
129
+ view gains `onController`. The parity script checks API presence only; the
130
+ store, rendered-view and actual-SDK protocol tests verify behavior.
package/README.md CHANGED
@@ -11,6 +11,112 @@ 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.8.0 edit and delete your own messages
15
+
16
+ This release requires JavaScript SDK 0.8.x (`editMessage`, `deleteMessage`,
17
+ the required `Message.revision`, `isEditedMessage`) and the coordinated
18
+ backend (`PATCH`/`DELETE /api/v1/messages/:id/own`). Consumer-built `Message`
19
+ literals (fixtures, controlled views) gain `revision`.
20
+
21
+ - Who may: the viewer's own confirmed messages, while the viewer's role (the
22
+ 0.7 `membership`, else the viewer's participant row) is not `READ`. The
23
+ server checks the same rules (403 for other members' messages and `READ`
24
+ roles); custom row eligibility goes through `canEditMessage`.
25
+ - Controller members (`useConversation` and the `Conversation` controller):
26
+ `editingMessage` (the row being edited as the user saw it; null otherwise),
27
+ `canEditMessages` / `canDeleteMessages` (adapter support),
28
+ `startEditing(messageId)`, `cancelEditing()`, `saveEdit(text)` and
29
+ `deleteMessage(messageId)`. `saveEdit` sends the snapshot's `revision`,
30
+ never the live row's: a stale one answers 409 `REVISION_CONFLICT`, the
31
+ store reloads the row once, shows its current content as the new snapshot
32
+ (the banner and the row update, the draft stays) and reports the conflict
33
+ through `error`; the next save carries the fresh revision. A newer row for
34
+ the edited message arriving on its own (row image, hydration, reconcile)
35
+ enters the same state without a request. A coded 404 `MESSAGE_NOT_FOUND`
36
+ removes the row and ends edit mode; a 403, a 500, a network failure or the
37
+ uncoded 404 a 0.7 backend answers keep the row, the history and edit mode
38
+ and set `error`. `deleteMessage` removes nothing until the server accepts
39
+ (or answers `MESSAGE_NOT_FOUND`); other devices learn through the room's
40
+ deletion notification and late responses for a removed id are dropped.
41
+ - Precedence: when rows meet, the higher `Message.revision` wins when both
42
+ carry one and at least one is above 0; ties and rows without a usable
43
+ revision keep the `updatedAt ?? createdAt` rule.
44
+ - Default row actions: eligible rows render `.ckui-message-actions` with
45
+ `Edit message` and `Delete message` buttons, always in the DOM. Only
46
+ `@media (hover: hover) and (pointer: fine)` conceals them (`opacity` and
47
+ `pointer-events`, never `display: none`) until the row is hovered or
48
+ focused; touch and keyboard users always see them. `Delete message` opens an
49
+ inline prompt (`role="group"`, name `Delete this message?`) with `Delete`
50
+ (`Confirm delete`) and `Cancel` (`Cancel delete`); pass `confirmDelete` to
51
+ use your own dialog. Edited rows show `Edited` (name `Edited`) beside the
52
+ time. Apart from that label, rows that are not eligible, and every row
53
+ without the callbacks, render byte-identically to 0.7.0.
54
+ - Composer edit mode: entering stashes the unsent draft and prefills the
55
+ field with the message text without a typing update. Enter, the primary
56
+ button (`Check`, name `Save message`) or a custom composer's `send` saves;
57
+ Cancel (name `Cancel editing`) and Escape restore the stash. Save is
58
+ enabled while the field has text or the message has attachments: an empty
59
+ caption is sent as `null` and clears it (attachments are never changed). A
60
+ refused save keeps the edited text and edit mode; a successful one and a
61
+ cancel restore the stash. `#composer` slots receive `editing` and
62
+ `cancelEdit` while editing.
63
+ - Controlled views: `ConversationView` accepts `editingMessage`,
64
+ `onEditMessage`, `onSaveEdit`, `onCancelEdit`, `onDeleteMessage`,
65
+ `canEditMessage` and `confirmDelete` (also as `@edit-message`, `@save-edit`,
66
+ `@cancel-edit`, `@delete-message`); `MessageListView` accepts
67
+ `onEditMessage`, `onDeleteMessage`, `canEditMessage` and `confirmDelete`.
68
+ Without the callbacks nothing new renders. `#message` slots receive
69
+ `isEdited`, `canEdit`, `canDelete` and, while eligible, `edit()` and
70
+ `remove()` (`remove` runs `confirmDelete` when present, otherwise deletes at
71
+ once). The bound `Conversation` wires the composable, forwards
72
+ `canEditMessage` and `confirmDelete`, and emits the four events.
73
+ - Adapter additions: `ConvoKitUiClient.editMessage?(messageId, { text,
74
+ revision })` and `deleteMessage?(messageId)` are optional; the default
75
+ adapter implements both. Without them the rows render no actions and
76
+ `saveEdit` / `deleteMessage` reject. (The Flutter and Android UIs make the
77
+ same members required on their `ConvoKitUiClient`: a breaking change for
78
+ custom implementers there, like `listInbox` in 0.6.0.) Custom adapters must
79
+ reject a stale revision with `code: 'REVISION_CONFLICT'` and a gone message
80
+ with `code: 'MESSAGE_NOT_FOUND'`.
81
+ - Deleting a message removes it and its attachments from the conversation for
82
+ every member and cannot be undone; files already received or downloaded
83
+ cannot be retracted, and stored files are reclaimed by the existing user or
84
+ app deletion cleanup.
85
+ - Mixed fleet: 0.7 clients ignore `revision`; against a 0.7 backend the author
86
+ routes answer an uncoded 404 (`HTTP_ERROR`) that keeps the row and edit
87
+ mode, every row parses with revision 0 and nothing reads `Edited`.
88
+
89
+ ```vue
90
+ <script setup lang="ts">
91
+ import { ref } from 'vue'
92
+ import type { Message } from '@convokitapp/sdk'
93
+ import { ConversationView } from '@convokitapp/vue-ui'
94
+
95
+ const editing = ref<Message | null>(null)
96
+ const rows = ref<Message[]>([])
97
+ async function saveEdit(message: Message, text: string) {
98
+ // Send message.revision; on 409 REVISION_CONFLICT reload the row and let the user retry.
99
+ const saved = await api.editMessage(message.id, { text: text || null, revision: message.revision })
100
+ rows.value = rows.value.map((row) => (row.id === saved.id ? saved : row))
101
+ editing.value = null
102
+ }
103
+ const confirmDelete = (message: Message) => window.confirm(`Delete "${message.text ?? 'this message'}"?`)
104
+ </script>
105
+
106
+ <template>
107
+ <ConversationView
108
+ v-bind="conversationProps"
109
+ :messages="rows"
110
+ :editing-message="editing"
111
+ @edit-message="(message) => (editing = message)"
112
+ @save-edit="saveEdit"
113
+ @cancel-edit="editing = null"
114
+ @delete-message="(message) => api.deleteMessage(message.id)"
115
+ :confirm-delete="confirmDelete"
116
+ />
117
+ </template>
118
+ ```
119
+
14
120
  ## 0.7.0 private mark unread
15
121
 
16
122
  This release requires JavaScript SDK 0.7.x (`markConversationUnread`,
@@ -292,15 +398,20 @@ state directly. Every important surface has a named slot:
292
398
  <template #header="{ conversation }">
293
399
  <SupportHeader :title="conversation.displayTitle" />
294
400
  </template>
295
- <template #message="{ message, isCurrentUser, readerIds }">
296
- <SupportBubble :message="message" :mine="isCurrentUser" :read-by="readerIds" />
401
+ <template #message="{ message, isCurrentUser, readerIds, isEdited, edit, remove }">
402
+ <SupportBubble :message="message" :mine="isCurrentUser" :read-by="readerIds" :edited="isEdited" @edit="edit" @delete="remove" />
297
403
  </template>
298
- <template #composer="{ value, setValue, send }">
299
- <BrandComposer :model-value="value" @update:model-value="setValue" @send="send" />
404
+ <template #composer="{ value, setValue, send, editing, cancelEdit }">
405
+ <BrandComposer :model-value="value" :editing="editing" @update:model-value="setValue" @send="send" @cancel="cancelEdit" />
300
406
  </template>
301
407
  </ConversationView>
302
408
  ```
303
409
 
410
+ `#message` slots receive `isEdited`, `canEdit`, `canDelete` and, while the row
411
+ is eligible, `edit()` and `remove()`; `#composer` slots receive `editing` and
412
+ `cancelEdit` while the view is in edit mode, and their `send` saves the edit
413
+ (0.8.0).
414
+
304
415
  Available slots include `conversation-item`, `separator`, `header`, `message`,
305
416
  `media`, `read-receipt`, `composer`, `typing-indicator`, `loading`, `empty`,
306
417
  `error`, `load-more`, `loading-older`, and `message-error`.
@@ -317,6 +428,9 @@ for a host-built row action.
317
428
  `useConversation` also exposes `readPositionByUserId`, `readerIdsFor(message)`,
318
429
  `markRead()` and `setVisible(visible)`; call `setVisible(false)` while your own
319
430
  surface hides the room so automatic acknowledgements wait until it is shown.
431
+ For the viewer's own messages it exposes `editingMessage`, `canEditMessages`,
432
+ `canDeleteMessages`, `startEditing(messageId)`, `cancelEditing()`,
433
+ `saveEdit(text)` and `deleteMessage(messageId)` (0.8.0).
320
434
 
321
435
  Optimistic rows display `Sending…` until acknowledgement. The final server
322
436
  timestamp is formatted in the viewer's local timezone. Custom message slots can