@tesark/chat-react
v0.1.10
Published
React SDK for Tesark Chat
Readme
@tesark/chat-react
React components and hooks for Tesark Chat — realtime messaging with server-side moderation.
Install
pnpm add @tesark/chat-react
# or
npm install @tesark/chat-react@supabase/supabase-js is a dependency — you do not install or wire it yourself. ChatProvider fetches Supabase URL and anon key from GET /v1/client-config on init.
Peer dependencies (must be installed in your app):
| Package | Version |
|---|---|
| react | ^18.0.0 \|\| ^19.0.0 |
| react-dom | ^18.0.0 \|\| ^19.0.0 |
Import styles once in your app entry:
import "@tesark/chat-react/styles";Auth and token exchange
End users are not Supabase Auth users. Your host server provisions identities and mints short-lived JWTs.
Server (API key — never expose to the browser)
# 1. Provision user (idempotent upsert on externalId)
curl -X POST "$API_URL/v1/users" \
-H "Authorization: Bearer $TENANT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"externalId":"user-42","displayName":"Ada"}'
# 2. Exchange for JWT (6 hour TTL)
curl -X POST "$API_URL/v1/token" \
-H "Authorization: Bearer $TENANT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"externalUserId":"user-42"}'Response:
{
"accessToken": "eyJ...",
"tokenType": "Bearer",
"expiresIn": 21600
}Decode the JWT payload (or track IDs from your provision step) for userId (sub) and tenantId (app_metadata.tenant_id).
Client (browser)
import { ChatProvider, ChatPanel } from "@tesark/chat-react";
import "@tesark/chat-react/styles";
<ChatProvider
accessToken={accessToken}
userId={userId}
tenantId={tenantId}
apiUrl={API_URL}
refreshAccessToken={async () => {
const response = await fetch("/api/chat/token");
const body = await response.json();
return body.accessToken;
}}
>
<div className="flex min-h-0 flex-1 flex-col">
<ChatPanel />
</div>
</ChatProvider>Pass an initial accessToken from your server on page load. Provide refreshAccessToken so the SDK can refresh before the 6-hour JWT expires and retry after auth failures. Your callback should call your backend (which holds the tenant API key and calls POST /v1/token) and return the new JWT string. The SDK updates Supabase and Realtime auth in place — no remount.
If refresh fails, ChatPanel shows a Session expired screen with a Reconnect action (when refreshAccessToken is configured) instead of raw JWT expired errors.
Give the chat a sized parent (e.g. flex-1 min-h-0 in a column layout, or height: calc(100vh - …)). ChatPanel fills width and height of that parent; it does not impose its own min-height.
Traffic split
| Action | Path |
|---|---|
| Send message | POST /v1/messages (moderation API, JWT) |
| Send system message | POST /v1/system-messages (API key + system:write, host server only) |
| Edit message | PATCH /v1/messages/:id (moderation API, JWT) |
| Read messages | Supabase client + RLS |
| Reactions, soft-delete | Supabase client + RLS |
| Realtime updates | Supabase Broadcast on conversation:{id} |
System messages
When your host server posts via POST /v1/system-messages, messages appear with a distinct layout: centered neutral bubble, system display name, and avatar (configured in the dashboard System profile).
ChatProvider loads the profile from Supabase (users where external_id is __system) using tenant RLS. MessageThread detects system messages via content.kind === "system" or matching senderId. System rows have no edit/delete, reactions, or read receipts.
Configure the profile before sending:
# From your server (tenant API key with system:write)
curl -X POST "$API_URL/v1/system-messages" \
-H "Authorization: Bearer $TENANT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"conversationId":"…","content":{"text":"Announcement"}}'Theming
Wrap chat UI in an element with class tesark-chat (or pass className to ChatPanel, which adds it). Override CSS variables on that wrapper or :root:
| Variable | Default | Used for |
|---|---|---|
| --tesark-color-bg | #fff | Panel background |
| --tesark-color-surface | #f4f4f5 | Sidebar, other bubbles |
| --tesark-color-border | #e4e4e7 | Borders |
| --tesark-color-text | #18181b | Body text |
| --tesark-color-muted | #71717a | Timestamps, previews |
| --tesark-color-primary | #2563eb | Own messages, send button |
| --tesark-color-primary-text | #fff | Text on primary |
| --tesark-border-radius | 8px | Corners |
| --tesark-list-width | 300px | Conversation list column (desktop) |
| --tesark-list-width-tablet | 240px | List column when the panel is tablet width |
Responsive layout
ChatPanel sizes itself from the width of the .tesark-chat container (not the browser viewport). Embed hosts should give the root width: 100%; height: 100% (or a fixed size).
| Container width | Layout | |---|---| | ≥ 1024px | Desktop split — 300px list + thread | | 640px – 1023px | Tablet compact split — 240px list + thread | | < 640px | Mobile master/detail — full-width list, or thread with header back button |
On mobile, selecting a conversation shows the thread; the header back control clears selection and returns to the list. onConversationChange(null, null) fires when the user goes back.
Override list width: --tesark-list-width-tablet on .tesark-chat.
Example — dark theme on a host page:
.my-chat-host.tesark-chat {
--tesark-color-bg: #0f0f12;
--tesark-color-surface: #18181b;
--tesark-color-border: #27272a;
--tesark-color-text: #fafafa;
--tesark-color-muted: #a1a1aa;
--tesark-color-primary: #3b82f6;
}<ChatPanel className="my-chat-host" />Components
Participant embed: ChatPanel. Read-only operator/review embed: OperatorChatPanel — see Operator view.
ChatProvider
Required wrapper. Sets Realtime auth and provides context to hooks.
| Prop | Type | Description |
|---|---|---|
| accessToken | string | Short-lived JWT from token exchange |
| refreshAccessToken | () => Promise<string>? | Mint a new JWT via your backend; SDK refreshes proactively and on 401 |
| userId | string | User UUID (sub claim) |
| tenantId | string | Tenant UUID |
| apiUrl | string | Edge Function base URL (e.g. https://….supabase.co/functions/v1/api) |
| enableWebPush | boolean? | When true, registers Web Push after init (default false) |
| serviceWorkerUrl | string? | URL of tesark-sw.js on your origin (default /tesark-sw.js) |
Web Push (background notifications)
Web Push requires VAPID keys on the Edge Function (VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, optional VAPID_SUBJECT). Set them as Supabase secrets before enabling push in production.
- Serve the service worker at your site root (or pass a custom
serviceWorkerUrl):- Copy from
@tesark/chat-react/swafter install, or fromnode_modules/@tesark/chat-react/dist/tesark-sw.js - The dashboard playground serves
public/tesark-sw.jsat/tesark-sw.js
- Copy from
- Enable in
ChatProvider:
<ChatProvider
accessToken={accessToken}
userId={userId}
tenantId={tenantId}
apiUrl={API_URL}
enableWebPush
>
<ChatPanel />
</ChatProvider>enableWebPush is off by default so embeds do not prompt for notification permission. In-app browser notifications (tab backgrounded or another conversation selected) work without Web Push via useMessageAlerts inside ChatPanel.
ChatPanel
Full UI: conversation list, message thread, composer. Composes lower-level components. Root element is width: 100%; height: 100% — size comes from the host layout.
| Prop | Type | Description |
|---|---|---|
| className | string? | Extra class on root (use for theming) |
| onConversationChange | (conversationId, conversation) => void | Fires on mount and when the user selects a conversation. conversationId is a UUID or null; conversation is the full ConversationResponse snapshot at selection time (also null when none selected). |
| onConversationInfoClick | (conversation) => void | Fires when the header info button is clicked. |
ChatHeader also accepts optional onBack and backLabel for custom layouts; ChatPanel wires these automatically on mobile thread view.
OperatorChatPanel
Read-only operator/review surface for a single conversation. Use with a review JWT from POST /v1/review-token (host backend) or the dashboard operator mint route — never a participant token.
| Prop | Type | Description |
|---|---|---|
| className | string? | Extra class on root (use for theming) |
| initialConversationId | string | Conversation UUID to display |
| conversation | ConversationResponse? | Optional listing snapshot to skip fetch |
| onConversationInfoClick | (conversation) => void | Fires when the header info button is clicked |
Includes a read-only banner, header with in-thread search, message thread (live Realtime updates, reaction summaries, attachments), and a read-only info sidebar. No composer, conversation list, typing/presence, mark-read, message chime, or web push.
import {
ChatProvider,
OperatorChatPanel,
} from "@tesark/chat-react";
import "@tesark/chat-react/styles";
// Review JWT from your server (1 hour TTL)
<ChatProvider
accessToken={reviewAccessToken}
userId={reviewUserId}
tenantId={tenantId}
apiUrl={API_URL}
refreshAccessToken={async () => {
const response = await fetch("/api/chat/review-token");
const body = await response.json();
return body.accessToken;
}}
>
<OperatorChatPanel initialConversationId={conversationId} />
</ChatProvider>ConversationList / MessageThread / Composer
Composable pieces for custom layouts. See exported *Props types in dist/index.d.ts.
Hooks
useChat()
Returns { supabase, accessToken, userId, tenantId, systemProfile, isReady }. systemProfile is { userId, displayName, avatarUrl } or null if not configured.
useConversations()
Returns { conversations, loading, error, refresh }. Loads conversations for the authenticated user via Supabase RLS.
useConversationUnreadCounts()
Returns per-conversation unread counts for the conversation list (used by ConversationList):
unreadByConversationId— map ofconversationId→ unread message countloading,error,refresh
useConversationCount()
Returns realtime aggregate counts for the host app (non-archived conversations only):
unreadMessages— total unread messages across all conversationsunreadConversations— conversations with at least one unread messagetotalConversations— conversations the user belongs tonewUnreadMessages/newUnreadConversations— delta since the hook first loaded (resets on remount)loading,error,refresh
Must be used inside ChatProvider (sibling to ChatPanel is fine):
function NavBadge() {
const { newUnreadMessages, loading } = useConversationCount();
if (loading || newUnreadMessages === 0) return null;
return <span className="nav-badge">{newUnreadMessages}</span>;
}useMessages(conversationId)
Returns:
messages,reactions,loading,error,refreshsendMessage(content, localId?)— moderated APIeditMessage(messageId, content)— moderated API (within 1 hour of send)deleteMessage(messageId)— soft delete via Supabase RPCsoft_delete_message(within 1 hour of send; conversation preview refreshes via DB trigger)toggleReaction(messageId, emoji)— via Supabase
Throws ModerationRejectedError when send/edit fails moderation (reason: harm | profanity | contact_id).
Create conversations
From your server with the tenant API key:
curl -X POST "$API_URL/v1/conversations" \
-H "Authorization: Bearer $TENANT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"memberExternalIds":["user-42","user-99"],"name":"Support"}'HTTP API reference
See HTTP API reference in the dashboard docs.
Admin console
Use the T-Chat dashboard (apps/dashboard) to sign up, issue API keys, and view users/conversations. Product owners authenticate with email and password (Supabase Auth).
