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

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

  1. Serve the service worker at your site root (or pass a custom serviceWorkerUrl):
    • Copy from @tesark/chat-react/sw after install, or from node_modules/@tesark/chat-react/dist/tesark-sw.js
    • The dashboard playground serves public/tesark-sw.js at /tesark-sw.js
  2. 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 of conversationId → unread message count
  • loading, error, refresh

useConversationCount()

Returns realtime aggregate counts for the host app (non-archived conversations only):

  • unreadMessages — total unread messages across all conversations
  • unreadConversations — conversations with at least one unread message
  • totalConversations — conversations the user belongs to
  • newUnreadMessages / 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, refresh
  • sendMessage(content, localId?) — moderated API
  • editMessage(messageId, content) — moderated API (within 1 hour of send)
  • deleteMessage(messageId) — soft delete via Supabase RPC soft_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).