@convokitapp/vue-ui
v0.10.1
Published
Accessible, SDK-backed Vue 3 chat components for ConvoKit.
Readme
ConvoKit Vue UI
Production-ready Vue 3 chat surfaces powered by
@convokitapp/sdk. The
components ship accessible defaults, realtime state, media and read receipts,
while named slots, CSS variables, and controlled views keep the host app in
control.
The default skin follows the same neutral, shadcn-style web vocabulary as the React package: system typography, separated rows, subtle borders, compact actions, and restrained radii. It remains framework-CSS independent and does not require Tailwind in the host application.
0.9.0 quoted replies and jump to message
This release requires JavaScript SDK 0.9.x (Message.replyToMessageId,
SendMessageInput.replyToMessageId, ReplyPreview, MessageContextPage,
getReplyPreviews, getMessageContext) and the coordinated backend
(GET /api/v1/conversations/:id/reply-previews and
GET /api/v1/conversations/:id/context). Everything is additive: a room with
no replies renders exactly as in 0.8.0.
- Who may: any active member may quote any message in the room, own or not,
while the viewer's role (the 0.7
membership, else the viewer's participant row) is notREAD; pending rows are never quotable. That is deliberately wider than the edit/delete eligibility, and it is not routed throughcanEditMessage— the separatecanReplyToMessageprop replaces it. - Controller members (
useConversationand theConversationcontroller):replyTarget(the message the composer is quoting, or null),replyPreviews(quoted parents byMessage.replyToMessageId),highlightedMessageId,jumpInFlight,windowMode(liveorjumped),hasNewerMessages,isLoadingNewer,canJumpToMessage/canResolveReplyPreviews(adapter support), plusstartReply(messageId),cancelReply(),jumpToMessage(messageId),loadNewerMessages()andreturnToLatest(). Replying and editing are mutually exclusive: each clears the other.startReplysends nothing, and unlike edit mode it leaves the draft alone. - Sending: the reply target is stamped on the optimistic row, so the quoted
block renders before the server acknowledges the send, and it is cleared
only once the send is accepted. A send without a quote omits
replyToMessageIdentirely, so it stays byte-identical to 0.8.0. The reference is write-once: the backend rejects a retry that changes it. - Preview resolution is batched, never per row: after each page load,
reconcile and live-insert burst the store issues one
getReplyPreviewscall for the distinct parents the loaded window cannot derive (a parent that is on screen costs no request). Previews are re-read, never copied: a parent edit, a deletion, or a reconnect invalidates the cached entry. An id that a resolved call did not return is cached as the terminal'unavailable'and renderedOriginal message unavailable, with the reference and the jump affordance intact. A rejected call writes no entry for any of its ids: they stay unresolved (the row renders the reference with no quoted text), the failure surfaces througherror, and the next trigger asks again. - Jump windows:
jumpToMessagehighlights a rendered row without a request; otherwise it replaces the window with a context window centred on the target and setswindowMode: 'jumped'. A jump is a window operation, never a re-open — tombstones, the acknowledgement floor, this open's captured private state, edit mode and the reply target all survive it. While jumped the store acknowledges nothing, realtime inserts are recorded but not rendered, paging goes through the window's own cursors in both directions, and a queued reconcile re-reads the window with one bounded request instead of walking history from the tail (the tail reconcile stays owed and runs on the return).returnToLatest()reloads the newest page, drains the deferred rows and acknowledges them; if it fails the window and the affordance stay. A jump is refused while a send is in flight, and a send from a jumped window returns to the tail first, keeping the draft and the quote. - Default rows: a
Reply to messageaction joins the existing actions menu, and a reply renders.ckui-message-quoteabove its text with the quoted author and text,Original message unavailablewhen the parent is gone, or the reference alone while the preview is not resolved yet. The block is a button (nameQuoted message from <author>) whenever the view can jump. The jumped-to row is centred (scrollIntoView({ block: 'center', behavior: 'instant' })), focused and tinted with--ckui-highlightfor about two seconds; the list carriesaria-busy="true"only while the jump lands, and stick-to-bottom, pagination and the highlight clear are suspended for that window. Rows without the new callbacks render byte-identically to 0.8.0. - Composer: a cancellable
.ckui-composer__replyingstrip (role="status",Cancel reply, Escape) sits where the edit banner does. It never replaces the draft.#composerslots receivereplyingandcancelReply. - Controlled views:
ConversationViewacceptsreplyTarget,onReplyToMessage,onCancelReply,canReplyToMessage,replyPreviewByMessageId,onJumpToMessage,highlightedMessageId,jumpInFlight,hasNewerMessages,isLoadingNewer,onLoadNewerandonReturnToLatest(also as@reply-to-message,@cancel-reply,@jump-to-message,@load-newer,@return-to-latest);MessageListViewaccepts all of those exceptreplyTarget,onCancelReplyandonReturnToLatest.onReturnToLatestis what renders theJump to latestcontrol, so pass it only while the window is jumped.#messageslots receivecanReply,reply(),replyPreviewandjumpToReplyTarget(); the slot wrapper carriesdata-message-idso a replaced row is still a jump target. - Adapter additions:
ConvoKitUiClient.getReplyPreviews?(conversationId, messageIds)andgetMessageContext?(conversationId, options)are optional; the default adapter implements both. (The Flutter and Android UIs make the same members required on theirConvoKitUiClient, a breaking change for custom implementers there.) Replying itself needs no adapter member. - Mixed fleet: against a 0.8 backend both routes answer Express's unmatched
route with an uncoded 404 (
HTTP_ERROR). That is not "the message is gone": no preview becomesunavailableand no window is replaced. After the first such rejection the store setscanResolveReplyPreviews/canJumpToMessageto false for its life, so the affordance disappears instead of failing repeatedly; a coded404 MESSAGE_NOT_FOUNDnever does that — it is a real missing target and marks that one previewunavailable. A 0.8 client against a 0.9 backend simply ignores the new row key.
<script setup lang="ts">
import { ref } from 'vue'
import type { Message, ReplyPreview } from '@convokitapp/sdk'
import { ConversationView } from '@convokitapp/vue-ui'
const replyTarget = ref<Message | null>(null)
const previews = ref(new Map<string, ReplyPreview | 'unavailable'>())
const highlighted = ref<string | null>(null)
async function send(text: string) {
await api.sendMessage({ text, ...(replyTarget.value ? { replyToMessageId: replyTarget.value.id } : {}) })
replyTarget.value = null
}
</script>
<template>
<ConversationView
v-bind="conversationProps"
:reply-target="replyTarget"
:reply-preview-by-message-id="previews"
:highlighted-message-id="highlighted"
@send-message="send"
@reply-to-message="(message) => (replyTarget = message)"
@cancel-reply="replyTarget = null"
@jump-to-message="(messageId) => (highlighted = messageId)"
/>
</template>0.8.0 edit and delete your own messages
This release requires JavaScript SDK 0.8.x (editMessage, deleteMessage,
the required Message.revision, isEditedMessage) and the coordinated
backend (PATCH/DELETE /api/v1/messages/:id/own). Consumer-built Message
literals (fixtures, controlled views) gain revision.
- Who may: the viewer's own confirmed messages, while the viewer's role (the
0.7
membership, else the viewer's participant row) is notREAD. The server checks the same rules (403 for other members' messages andREADroles); custom row eligibility goes throughcanEditMessage. - Controller members (
useConversationand theConversationcontroller):editingMessage(the row being edited as the user saw it; null otherwise),canEditMessages/canDeleteMessages(adapter support),startEditing(messageId),cancelEditing(),saveEdit(text)anddeleteMessage(messageId).saveEditsends the snapshot'srevision, never the live row's: a stale one answers 409REVISION_CONFLICT, the store reloads the row once, shows its current content as the new snapshot (the banner and the row update, the draft stays) and reports the conflict througherror; the next save carries the fresh revision. A newer row for the edited message arriving on its own (row image, hydration, reconcile) enters the same state without a request. A coded 404MESSAGE_NOT_FOUNDremoves the row and ends edit mode; a 403, a 500, a network failure or the uncoded 404 a 0.7 backend answers keep the row, the history and edit mode and seterror.deleteMessageremoves nothing until the server accepts (or answersMESSAGE_NOT_FOUND); other devices learn through the room's deletion notification and late responses for a removed id are dropped. - Precedence: when rows meet, the higher
Message.revisionwins when both carry one and at least one is above 0; ties and rows without a usable revision keep theupdatedAt ?? createdAtrule. - Default row actions: eligible rows render
.ckui-message-actionswithEdit messageandDelete messagebuttons, always in the DOM. Only@media (hover: hover) and (pointer: fine)conceals them (opacityandpointer-events, neverdisplay: none) until the row is hovered or focused; touch and keyboard users always see them.Delete messageopens an inline prompt (role="group", nameDelete this message?) withDelete(Confirm delete) andCancel(Cancel delete); passconfirmDeleteto use your own dialog. Edited rows showEdited(nameEdited) beside the time. Apart from that label, rows that are not eligible, and every row without the callbacks, render byte-identically to 0.7.0. - Composer edit mode: entering stashes the unsent draft and prefills the
field with the message text without a typing update. Enter, the primary
button (
Check, nameSave message) or a custom composer'ssendsaves; Cancel (nameCancel editing) and Escape restore the stash. Save is enabled while the field has text or the message has attachments: an empty caption is sent asnulland clears it (attachments are never changed). A refused save keeps the edited text and edit mode; a successful one and a cancel restore the stash.#composerslots receiveeditingandcancelEditwhile editing. - Controlled views:
ConversationViewacceptseditingMessage,onEditMessage,onSaveEdit,onCancelEdit,onDeleteMessage,canEditMessageandconfirmDelete(also as@edit-message,@save-edit,@cancel-edit,@delete-message);MessageListViewacceptsonEditMessage,onDeleteMessage,canEditMessageandconfirmDelete. Without the callbacks nothing new renders.#messageslots receiveisEdited,canEdit,canDeleteand, while eligible,edit()andremove()(removerunsconfirmDeletewhen present, otherwise deletes at once). The boundConversationwires the composable, forwardscanEditMessageandconfirmDelete, and emits the four events. - Adapter additions:
ConvoKitUiClient.editMessage?(messageId, { text, revision })anddeleteMessage?(messageId)are optional; the default adapter implements both. Without them the rows render no actions andsaveEdit/deleteMessagereject. (The Flutter and Android UIs make the same members required on theirConvoKitUiClient: a breaking change for custom implementers there, likelistInboxin 0.6.0.) Custom adapters must reject a stale revision withcode: 'REVISION_CONFLICT'and a gone message withcode: 'MESSAGE_NOT_FOUND'. - Deleting a message removes it and its attachments from the conversation for every member and cannot be undone; files already received or downloaded cannot be retracted, and stored files are reclaimed by the existing user or app deletion cleanup.
- Mixed fleet: 0.7 clients ignore
revision; against a 0.7 backend the author routes answer an uncoded 404 (HTTP_ERROR) that keeps the row and edit mode, every row parses with revision 0 and nothing readsEdited.
<script setup lang="ts">
import { ref } from 'vue'
import type { Message } from '@convokitapp/sdk'
import { ConversationView } from '@convokitapp/vue-ui'
const editing = ref<Message | null>(null)
const rows = ref<Message[]>([])
async function saveEdit(message: Message, text: string) {
// Send message.revision; on 409 REVISION_CONFLICT reload the row and let the user retry.
const saved = await api.editMessage(message.id, { text: text || null, revision: message.revision })
rows.value = rows.value.map((row) => (row.id === saved.id ? saved : row))
editing.value = null
}
const confirmDelete = (message: Message) => window.confirm(`Delete "${message.text ?? 'this message'}"?`)
</script>
<template>
<ConversationView
v-bind="conversationProps"
:messages="rows"
:editing-message="editing"
@edit-message="(message) => (editing = message)"
@save-edit="saveEdit"
@cancel-edit="editing = null"
@delete-message="(message) => api.deleteMessage(message.id)"
:confirm-delete="confirmDelete"
/>
</template>0.7.0 private mark unread
This release requires JavaScript SDK 0.7.x (markConversationUnread,
clearConversationUnread, Conversation.membership, the InboxSummary
members isUnread, unreadMarkedAt and privateStateVersion) and the
coordinated backend (POST/DELETE /api/v1/conversations/:id/unread, the
privateStateVersion acknowledgement field, the membership sibling on
GET /api/v1/conversations/:id).
- The marker is private: other members, webhooks and
readevents never see it.useConversationListand theConversationListcontroller gainmarkUnread(conversationId)andclearUnread(conversationId, { ifVersion? }). Wire them through#conversation-item(a row action or menu) or@controller-change; the default row adds no affordance.clearUnreadresolves to the response'scleared("this request removed the marker", not "the room is read"). Both reject from a disposed or session-ended controller without sending anything. - Summaries carry
isUnread(unreadCount > 0 || unreadCountCapped || unreadMarkedAt !== null),unreadMarkedAtandprivateStateVersion. After a mark or a clear the row's summary takes the response only while it is not older than what the row already holds; a delayed response never resurrects a marker a newer action removed. Other devices refresh throughinbox_activity, which the backend also sends for your own marker changes;activityRefreshWindowMsapplies as before. - Dot rule: a count (or a capped count) keeps the numeric badge (
5,99+, accessible name<count> unread);isUnreadwithout one renders a numberless dot with the accessible nameUnread, never0 unread(.ckui-unread-badge.ckui-unread-badge--dot,badgecolour). The title is bold anddata-unreadis set in both cases; rows without a summary are unchanged. - Capture at open: the room store captures
conversation.membership.privateStateVersiononce per open (the first time itsconversationgoes from null to a DTO: on load, or on the reconcile after a transient first-load failure) and sends it with every targeted acknowledgement of that open. A refresh never recaptures, so a mark made elsewhere while the room is open survives its acknowledgements until the room is reopened; a DTO withoutmembership(0.6 backend) sends no version. An automatic acknowledgement for a row that arrives before the DTO (during the load, or while a transient first-load failure stands) waits for the capture, like it waits while hidden, so it too carries the version. - Empty rooms: when the opened membership carries a marker and nothing
non-pending is rendered, the store calls
clearConversationUnread(conversationId, { ifVersion })once per open under the same triggers and visibility gating as the load acknowledgement (and on an explicitmarkRead()); once a row is rendered its acknowledgement clears the marker instead. The UI still never sends an acknowledgement without a target.cleared: falseis not an error. - Adapter additions:
ConvoKitUiClient.markConversationUnread?(conversationId)andclearConversationUnread?(conversationId, { ifVersion? })are optional; the default adapter implements both. Custom adapters without them makemarkUnread/clearUnreadreject and leave the marker in empty rooms. (The Flutter and Android UIs make the same members, and theprivateStateVersionacknowledgement parameter, required on theirConvoKitUiClient: a breaking change for custom implementers there, likelistInboxin 0.6.0.)markConversationReadoptions now carryprivateStateVersion; forward the options object unchanged, or acknowledgements advance the position but never clear the marker. - Mixed fleet: 0.6 lists ignore
isUnread; against a 0.6 backend the new adapter members fail with a 404 (HTTP_ERROR) and no dots render.
0.6.0 inbox previews and unread counts
This release requires JavaScript SDK 0.6.x (listInbox, onInboxActivity)
and the coordinated backend (GET /api/v1/inbox, the inbox_activity event).
- SDK-backed lists arrive in inbox order: the newest surviving message time
first (an empty room sorts by its creation time), paged by an opaque server
cursor instead of an offset. The store keeps the server's order within a page
and, whenever pages are combined (load more, refresh), merges by conversation
ID with the later entry winning and re-orders by
(activityAt desc, id desc). Settingfilter.comparatorreplaces that order;updatedAtis never an ordering input.mergeInboxEntries(current, incoming)is exported and applies the same rule. - State gains
summaries(aReadonlyMap<string, InboxSummary>by conversation ID:latestMessage,unreadCount,unreadCountCapped,readPosition,lastReadAt,activityAt) andcurrentUserId(the bound session's user;''without a session, on the legacy path and after disposal).conversations, filters, comparators, custom offsetpageLoaders and the#conversation-itemslot signature are unchanged. - Default rows show a one-line preview instead of the participants line when a
summary carries a non-empty latest message:
You: …for the viewer's own message (DMs included),<name>: …for a named member of a group of more than two, otherwise the body alone; media-only messages readPhoto, the file name orFile,Location,Contact. Rows show the activity time in the device zone and an unread badge (5,99+above 99 or when the server capped the count) with the accessible name<count> unread(99+ unreadwhen capped); the title is bold while unread. The badge uses the new theme tokenbadge(--ckui-badge, defaults to the primary color) withoutgoingTextas its label color. #conversation-itemslot props gainsummaryandcurrentUserId(present when known); existing slots keep working.ConversationListViewacceptssummariesandcurrentUserIdfor controlled use. The inline error'sRetryrequests the next page whenhasMoreandonLoadMoreare bound (past the view's duplicate-request guard), otherwise it refreshes; it renders only when one of those callbacks exists.- Live updates:
inbox_changedstill refreshes immediately; the newinbox_activitysignal (message inserts/edits and read-position advances, fanned out to every client of the app) is throttled byactivityRefreshWindowMs(default 500 ms, 0 = immediate) with max-wait semantics: the first signal opens a window, later signals wait for it, and one refresh runs when it closes. Aninbox_changedduring the window refreshes at once and drops the timer.refresh(), topic-error reconciliation and the rejoin replay stay immediate. A refresh re-walks from the head withlimit = min(100, target − consumed)wheretarget = max(pageSize, loaded), continues past pages the local filter hides, and swaps rows, summaries, cursor andhasMoreatomically; it never publishes an empty list withhasMorewhile more pages exist. Rooms are open withuseConversationas before; room stores ignoreinbox_activity. - Adapter additions:
ConvoKitUiClient.listInbox?({ limit, cursor, archived })andonInboxActivity?(handler, onError?)are optional. The default adapter implements both; custom adapters withoutlistInbox, and custompageLoaders, keep the 0.5 offset path with emptysummariesand no activity subscription. A 404 fromlistInbox(route absent on a rolled-back backend) switches the store to that path for the rest of its life without evicting rows (a warning is logged once); a 404 fromgetConversationsstill evicts, 401/403 from either endpoint evict, and 400 keeps rows and reports the error.
0.5.0 precise read positions
This release requires JavaScript SDK 0.5.x and the coordinated backend.
- Read receipts resolve per user from a monotonic read-through position, the
(createdAt, id)of a concrete message the server resolved, and fall back to the acknowledgement time only for legacy participants without a position. A read request that arrives late no longer marks messages rendered after it as read.useConversationandConversationexposereadPositionByUserIdbesidereadAtByUserId; both are seeded from participants and advanced by server read events only, never from the device clock. - Acknowledgements name the newest rendered, non-pending message
(
throughMessageId). Nothing is sent while nothing is rendered, and a media-only row is acknowledged only once its hydration renders it. One request is in flight at a time; a follow-up is resolved at send time and skipped when the newest row is already acknowledged. A target the server does not know (MESSAGE_NOT_FOUND, for example deleted meanwhile) is skipped and the next newest row is acknowledged once. Membership failures still surface as errors. - Automatic acknowledgements wait while the document is hidden and resume when
it becomes visible. The SDK-backed
Conversationfollowsdocument.visibilityStateandvisibilitychange; controllers gainsetVisible(visible)for host-driven surfaces (tabs, drawers, background routes). Unknown, prerender and server environments count as visible.markReadOnLoadandmarkReadOnReceivekeep their meaning; with both off, no request is ever sent, including on visibility changes.markRead()is the host's decision and is not gated. - Controlled views accept
readPositionByUserIdnext toreadAtByUserId;readerIdsFor(message, readAtByUserId, readPositionByUserId?)anddefaultReadersResolverapply the unified rule. Custom#messageand#read-receiptslots andreadersResolverreceive the same reader IDs. By default, outgoing messages show a compact sent/read check beside the timestamp; the read check announces the reader count. Use#read-receiptwhen additional visible receipt details are needed. - Adapter change:
ConvoKitUiClient.markConversationRead(conversationId, options?)receives{ throughMessageId }. Custom adapters must forward it to the core SDK (or their backend) and report an unknown target withcode: 'MESSAGE_NOT_FOUND'(or a 404 without a code). One-argument implementations still type-check but acknowledge the server's newest message instead of the rendered one. - Mixed fleet: precise receipts need both the sender's and the reader's clients on 0.5. 0.4 readers keep timestamp semantics and keep parsing the additive payload; the dashboard shows both the read-through and the last acknowledgement time.
0.4.0 live inbox, sending and recovery
This release requires JavaScript SDK 0.4.x and the coordinated backend. The core SDK discovers current private room/app topics automatically and rejoins when membership changes; no additional customer configuration is needed.
- The default adapter exposes a session identity that stays stable on token renewal and changes on every new login, including the same user ID. Room and inbox caches are cleared when that session ends. After reconnecting a user, update application authentication state so the hook/composable can bind the new session (or recreate the adapter).
- Initial history and older pages preserve live changes received while loading.
Message pagination uses newest-first
beforeCreatedAt/beforeIdvalues, not an offset calculated from the current visible row count. Page sizes are integers from 1 through 100. refresh()and successful room-channel joins reconcile the entire viewed history range, retaining the old snapshot until all required pages succeed.isReconcilingexposes this background work. Transient failures preserve the view for retry; authoritative history 401/403/404 responses clear it.- Typed INSERT/UPDATE events and private ID-only deletion events are distinct. Deletion removes text and attachment cards, and a late HTTP response cannot restore a known-deleted message. A reconnect also removes deletions missed while offline.
- Raw Postgres events contain the message row, not its related attachments.
The UI displays text immediately and calls authenticated
getMessage(id)for the complete message, including files and images. Existing attachments remain visible during edits until the full response confirms a change; media-only messages wait for that response instead of showing empty bubbles. This adds a REST lookup for observed row changes, limited to eight concurrent lookups per room store and coalesced by message ID. Obsolete responses cannot restore deleted messages or data from a previous login. - Read receipts use persisted participant positions and server read events, monotonically. A successful mark-read request does not fabricate a timestamp from the device clock. Remote typing expires and clears on disconnect. (0.5.0 replaces the timestamp comparison with read positions; see above.)
- Pending sends are bound to their original room/session. Reconciliation uses the exact clientMessageId, sender and room, never identical text or file count. A matching live/history row replaces its pending bubble immediately, even before HTTP acknowledgement; a later lost response cannot fail a confirmed send.
- Custom
ConvoKitUiClientadapters must implementsessionIdentity,onConnectionEvent(includingonSessionEnded), typedonMessage,onMessageDeleted,getMessage(id), and paired message cursors. The message lookup must enforce the same app/room authorization and return the complete current record, including an authoritative attachment list. Custom inbox page loaders must be scoped to the same authenticated app and return bounded pages; their filters restart at offset zero. No legacy adapter fallback is provided.
The managed API default and customer token-provider workflow are unchanged. The SDK discovers the Supabase URL and publishable key automatically; customers do not configure those values or ship a backend secret.
SDK-backed components consume private inbox invalidations automatically,
including after reconnect, while preserving filters and the loaded page window.
It also correlates pending messages with live/history rows using the exact
clientMessageId, so a live-first delivery never produces two bubbles.
These changes require the paired 0.4.0 core and coordinated backend update.
For 0.4.0, custom adapters must add
onInboxChanged(handler, onError?), notifying on committed changes and every
initial/reconnected subscription. Forward clientMessageId unchanged in sends
and parsed messages. Default SDK adapters do this for you; no component props
or demo-specific reconciliation code is required.
Install published packages
npm install @convokitapp/sdk @convokitapp/vue-ui vueSDK-backed conversation
Connect a browser-safe ConvoKit client, adapt it once, then render a room:
<script setup lang="ts">
import { ConvoKitClient } from '@convokitapp/sdk'
import { Conversation, createConvoKitUiClient } from '@convokitapp/vue-ui'
import '@convokitapp/vue-ui/styles.css'
const sdk = new ConvoKitClient({
clientId: import.meta.env.VITE_CONVOKIT_CLIENT_ID,
tokenProvider: async (appUserId) => {
const response = await fetch(import.meta.env.VITE_CONVOKIT_TOKEN_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ appUserId }),
})
return (await response.json()).token
},
})
await sdk.connectUser('maya')
const client = createConvoKitUiClient(sdk)
</script>
<template>
<Conversation :client="client" conversation-id="room-123" />
</template>The client secret belongs only in your token backend. Never put it in Vue, Vite environment variables, or a shipped browser bundle.
The core SDK uses ConvoKit's managed https://api.convokit.app endpoint. Set
backendUrl only for local testing or a self-hosted deployment.
SDK-backed conversations render an outgoing message immediately, reconcile it with the server response and realtime echo, and restore an unchanged draft if the send fails.
Controlled components and slots
ConversationListView, MessageListView, and ConversationView accept host
state directly. Every important surface has a named slot:
<ConversationView v-model="draft" v-bind="conversationProps">
<template #header="{ conversation }">
<SupportHeader :title="conversation.displayTitle" />
</template>
<template #message="{ message, isCurrentUser, readerIds, isEdited, edit, remove }">
<SupportBubble :message="message" :mine="isCurrentUser" :read-by="readerIds" :edited="isEdited" @edit="edit" @delete="remove" />
</template>
<template #composer="{ value, setValue, send, editing, cancelEdit }">
<BrandComposer :model-value="value" :editing="editing" @update:model-value="setValue" @send="send" @cancel="cancelEdit" />
</template>
</ConversationView>#message slots receive isEdited, canEdit, canDelete and, while the row
is eligible, edit() and remove(); #composer slots receive editing and
cancelEdit while the view is in edit mode, and their send saves the edit
(0.8.0).
Available slots include conversation-item, separator, header, message,
media, read-receipt, composer, typing-indicator, loading, empty,
error, load-more, loading-older, loading-newer, jump-to-latest, and
message-error. #loading-newer (in MessageListView) replaces the newer-end
spinner of a jumped window, and #jump-to-latest (in ConversationView)
replaces the default Jump to latest control, receiving returnToLatest
(JumpToLatestSlotProps); both render only while the window is jumped, so a
live window stays byte-identical to 0.8.0 (0.9.0).
Composables
Use useConversationList and useConversation when you want ConvoKit's
pagination, de-duplication, realtime, typing, and read state without the
default UI. Both return readonly Vue refs plus actions and a dispose() method.
useConversationList also exposes summaries and currentUserId, accepts
activityRefreshWindowMs (ConversationList forwards the same option), and
offers markUnread(conversationId) / clearUnread(conversationId, options?)
for a host-built row action.
useConversation also exposes readPositionByUserId, readerIdsFor(message),
markRead() and setVisible(visible); call setVisible(false) while your own
surface hides the room so automatic acknowledgements wait until it is shown.
For the viewer's own messages it exposes editingMessage, canEditMessages,
canDeleteMessages, startEditing(messageId), cancelEditing(),
saveEdit(text) and deleteMessage(messageId) (0.8.0).
Optimistic rows display Sending… until acknowledgement. The final server
timestamp is formatted in the viewer's local timezone. Custom message slots can
use isConvoKitPendingMessage(message) to present the same state.
Appearance
Wrap any subtree with ConvoKitThemeProvider (tokens include badge for the
unread counter and dot), set density="compact", or use
the per-part classNames and styles maps. unstyled removes package classes
from the configurable parts for a fully host-owned presentation.
See the Vue UI documentation and the public examples and screenshots. The examples repository contains runnable application code only; the package implementation remains in its private source repository.
Emoji reactions
Conversation renders a picker, count chips, and a paged reactor list. Custom slots can render the exported ReactionBar. The store batches visible summaries and refetches on ID-only invalidations and reconnect. Emoji sequences are exact (👍 differs from 👍🏽); READ members can inspect reactors, while READ_WRITE members can toggle their own reaction.
