npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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 not READ; pending rows are never quotable. That is deliberately wider than the edit/delete eligibility, and it is not routed through canEditMessage — the separate canReplyToMessage prop replaces it.
  • Controller members (useConversation and the Conversation controller): replyTarget (the message the composer is quoting, or null), replyPreviews (quoted parents by Message.replyToMessageId), highlightedMessageId, jumpInFlight, windowMode (live or jumped), hasNewerMessages, isLoadingNewer, canJumpToMessage / canResolveReplyPreviews (adapter support), plus startReply(messageId), cancelReply(), jumpToMessage(messageId), loadNewerMessages() and returnToLatest(). Replying and editing are mutually exclusive: each clears the other. startReply sends 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 replyToMessageId entirely, 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 getReplyPreviews call 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 rendered Original 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 through error, and the next trigger asks again.
  • Jump windows: jumpToMessage highlights a rendered row without a request; otherwise it replaces the window with a context window centred on the target and sets windowMode: '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 message action joins the existing actions menu, and a reply renders .ckui-message-quote above its text with the quoted author and text, Original message unavailable when the parent is gone, or the reference alone while the preview is not resolved yet. The block is a button (name Quoted message from <author>) whenever the view can jump. The jumped-to row is centred (scrollIntoView({ block: 'center', behavior: 'instant' })), focused and tinted with --ckui-highlight for about two seconds; the list carries aria-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__replying strip (role="status", Cancel reply, Escape) sits where the edit banner does. It never replaces the draft. #composer slots receive replying and cancelReply.
  • Controlled views: ConversationView accepts replyTarget, onReplyToMessage, onCancelReply, canReplyToMessage, replyPreviewByMessageId, onJumpToMessage, highlightedMessageId, jumpInFlight, hasNewerMessages, isLoadingNewer, onLoadNewer and onReturnToLatest (also as @reply-to-message, @cancel-reply, @jump-to-message, @load-newer, @return-to-latest); MessageListView accepts all of those except replyTarget, onCancelReply and onReturnToLatest. onReturnToLatest is what renders the Jump to latest control, so pass it only while the window is jumped. #message slots receive canReply, reply(), replyPreview and jumpToReplyTarget(); the slot wrapper carries data-message-id so a replaced row is still a jump target.
  • Adapter additions: ConvoKitUiClient.getReplyPreviews?(conversationId, messageIds) and getMessageContext?(conversationId, options) are optional; the default adapter implements both. (The Flutter and Android UIs make the same members required on their ConvoKitUiClient, 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 becomes unavailable and no window is replaced. After the first such rejection the store sets canResolveReplyPreviews / canJumpToMessage to false for its life, so the affordance disappears instead of failing repeatedly; a coded 404 MESSAGE_NOT_FOUND never does that — it is a real missing target and marks that one preview unavailable. 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 not READ. The server checks the same rules (403 for other members' messages and READ roles); custom row eligibility goes through canEditMessage.
  • Controller members (useConversation and the Conversation controller): editingMessage (the row being edited as the user saw it; null otherwise), canEditMessages / canDeleteMessages (adapter support), startEditing(messageId), cancelEditing(), saveEdit(text) and deleteMessage(messageId). saveEdit sends the snapshot's revision, never the live row's: a stale one answers 409 REVISION_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 through error; 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 404 MESSAGE_NOT_FOUND removes 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 set error. deleteMessage removes nothing until the server accepts (or answers MESSAGE_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.revision wins when both carry one and at least one is above 0; ties and rows without a usable revision keep the updatedAt ?? createdAt rule.
  • Default row actions: eligible rows render .ckui-message-actions with Edit message and Delete message buttons, always in the DOM. Only @media (hover: hover) and (pointer: fine) conceals them (opacity and pointer-events, never display: none) until the row is hovered or focused; touch and keyboard users always see them. Delete message opens an inline prompt (role="group", name Delete this message?) with Delete (Confirm delete) and Cancel (Cancel delete); pass confirmDelete to use your own dialog. Edited rows show Edited (name Edited) 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, name Save message) or a custom composer's send saves; Cancel (name Cancel editing) and Escape restore the stash. Save is enabled while the field has text or the message has attachments: an empty caption is sent as null and 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. #composer slots receive editing and cancelEdit while editing.
  • Controlled views: ConversationView accepts editingMessage, onEditMessage, onSaveEdit, onCancelEdit, onDeleteMessage, canEditMessage and confirmDelete (also as @edit-message, @save-edit, @cancel-edit, @delete-message); MessageListView accepts onEditMessage, onDeleteMessage, canEditMessage and confirmDelete. Without the callbacks nothing new renders. #message slots receive isEdited, canEdit, canDelete and, while eligible, edit() and remove() (remove runs confirmDelete when present, otherwise deletes at once). The bound Conversation wires the composable, forwards canEditMessage and confirmDelete, and emits the four events.
  • Adapter additions: ConvoKitUiClient.editMessage?(messageId, { text, revision }) and deleteMessage?(messageId) are optional; the default adapter implements both. Without them the rows render no actions and saveEdit / deleteMessage reject. (The Flutter and Android UIs make the same members required on their ConvoKitUiClient: a breaking change for custom implementers there, like listInbox in 0.6.0.) Custom adapters must reject a stale revision with code: 'REVISION_CONFLICT' and a gone message with code: '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 reads Edited.
<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 read events never see it. useConversationList and the ConversationList controller gain markUnread(conversationId) and clearUnread(conversationId, { ifVersion? }). Wire them through #conversation-item (a row action or menu) or @controller-change; the default row adds no affordance. clearUnread resolves to the response's cleared ("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), unreadMarkedAt and privateStateVersion. 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 through inbox_activity, which the backend also sends for your own marker changes; activityRefreshWindowMs applies as before.
  • Dot rule: a count (or a capped count) keeps the numeric badge (5, 99+, accessible name <count> unread); isUnread without one renders a numberless dot with the accessible name Unread, never 0 unread (.ckui-unread-badge.ckui-unread-badge--dot, badge colour). The title is bold and data-unread is set in both cases; rows without a summary are unchanged.
  • Capture at open: the room store captures conversation.membership.privateStateVersion once per open (the first time its conversation goes 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 without membership (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 explicit markRead()); once a row is rendered its acknowledgement clears the marker instead. The UI still never sends an acknowledgement without a target. cleared: false is not an error.
  • Adapter additions: ConvoKitUiClient.markConversationUnread?(conversationId) and clearConversationUnread?(conversationId, { ifVersion? }) are optional; the default adapter implements both. Custom adapters without them make markUnread / clearUnread reject and leave the marker in empty rooms. (The Flutter and Android UIs make the same members, and the privateStateVersion acknowledgement parameter, required on their ConvoKitUiClient: a breaking change for custom implementers there, like listInbox in 0.6.0.) markConversationRead options now carry privateStateVersion; 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). Setting filter.comparator replaces that order; updatedAt is never an ordering input. mergeInboxEntries(current, incoming) is exported and applies the same rule.
  • State gains summaries (a ReadonlyMap<string, InboxSummary> by conversation ID: latestMessage, unreadCount, unreadCountCapped, readPosition, lastReadAt, activityAt) and currentUserId (the bound session's user; '' without a session, on the legacy path and after disposal). conversations, filters, comparators, custom offset pageLoaders and the #conversation-item slot 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 read Photo, the file name or File, 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+ unread when capped); the title is bold while unread. The badge uses the new theme token badge (--ckui-badge, defaults to the primary color) with outgoingText as its label color.
  • #conversation-item slot props gain summary and currentUserId (present when known); existing slots keep working. ConversationListView accepts summaries and currentUserId for controlled use. The inline error's Retry requests the next page when hasMore and onLoadMore are bound (past the view's duplicate-request guard), otherwise it refreshes; it renders only when one of those callbacks exists.
  • Live updates: inbox_changed still refreshes immediately; the new inbox_activity signal (message inserts/edits and read-position advances, fanned out to every client of the app) is throttled by activityRefreshWindowMs (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. An inbox_changed during 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 with limit = min(100, target − consumed) where target = max(pageSize, loaded), continues past pages the local filter hides, and swaps rows, summaries, cursor and hasMore atomically; it never publishes an empty list with hasMore while more pages exist. Rooms are open with useConversation as before; room stores ignore inbox_activity.
  • Adapter additions: ConvoKitUiClient.listInbox?({ limit, cursor, archived }) and onInboxActivity?(handler, onError?) are optional. The default adapter implements both; custom adapters without listInbox, and custom pageLoaders, keep the 0.5 offset path with empty summaries and no activity subscription. A 404 from listInbox (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 from getConversations still 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. useConversation and Conversation expose readPositionByUserId beside readAtByUserId; 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 Conversation follows document.visibilityState and visibilitychange; controllers gain setVisible(visible) for host-driven surfaces (tabs, drawers, background routes). Unknown, prerender and server environments count as visible. markReadOnLoad and markReadOnReceive keep 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 readPositionByUserId next to readAtByUserId; readerIdsFor(message, readAtByUserId, readPositionByUserId?) and defaultReadersResolver apply the unified rule. Custom #message and #read-receipt slots and readersResolver receive 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-receipt when 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 with code: '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/beforeId values, 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. isReconciling exposes 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 ConvoKitUiClient adapters must implement sessionIdentity, onConnectionEvent (including onSessionEnded), typed onMessage, 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 vue

SDK-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.