npm.io
0.2.0 • Published 6h ago

@growblic/client-react

Licence
MIT
Version
0.2.0
Deps
1
Size
128 kB
Vulns
0
Weekly
0

@growblic/client-react

React hooks for the Growblic chat SDK: live conversations, messages, typing, and connection state.

npm install @growblic/client-react @growblic/client-core

Peer dependencies: react and react-dom ≥ 18. Requires Node ≥ 20 to build/SSR.

Setup

Create one client (see @growblic/client-core for the token model) and provide it:

import { createClient } from "@growblic/client-core";
import { GrowblicProvider } from "@growblic/client-react";

const client = createClient({
  apiUrl: "https://api.your-growblic-host.com",
  wsUrl: "wss://api.your-growblic-host.com/socket",
  tokenProvider: async () => (await fetch("/api/growblic-token", { method: "POST" })).json(),
});

export function App() {
  return (
    <GrowblicProvider client={client}>
      <Chat />
    </GrowblicProvider>
  );
}

Hooks

Hook Gives you
useConversations() The live inbox: { conversations, loading, error, refresh } — prepends new conversations, updates unread/title/preview rows live
useMessages(conversationId) The watched channel's readonly Message[], live-updating (new/edited/deleted)
useChannel(conversationId) { channel, ready } — the channel handle (send, react, markRead, loadMessages) + backfill readiness
useTyping(conversationId) The user ids currently typing in this conversation
useConnectionState() "offline" | "connecting" | "connected" | "reconnecting" for a status pill
usePresence(userIds) Live { [userId]: { online, lastSeen? } } for a set of users — snapshot + subscription, privacy-filtered server-side
useViewing(conversationId) The user ids currently VIEWING this conversation (like typing, but "has it open")
useCall() The single-active-call state machine: status, call, token, and start / accept / reject / end / reset

Minimal chat

import { useChannel, useConversations, useMessages } from "@growblic/client-react";

function Chat() {
  const { conversations } = useConversations();
  const id = conversations[0]?.id;
  if (!id) return <p>No conversations yet.</p>;
  return <Thread conversationId={id} />;
}

function Thread({ conversationId }: { conversationId: string }) {
  const messages = useMessages(conversationId);
  const { channel } = useChannel(conversationId);
  return (
    <>
      <ul>{messages.map((m) => <li key={m.id}>{m.text}</li>)}</ul>
      <button onClick={() => channel.sendMessage({ text: "hi" })}>Send</button>
    </>
  );
}

Presence

import { usePresence, useViewing } from "@growblic/client-react";

function Roster({ memberIds }: { memberIds: string[] }) {
  // Pass a fresh array every render — the hook keys on the SET, not array identity (no resubscribe storm).
  const presence = usePresence(memberIds);
  return (
    <ul>
      {memberIds.map((id) => (
        <li key={id}>
          {id} {presence[id]?.online ? "🟢" : presence[id]?.lastSeen ?? "offline"}
        </li>
      ))}
    </ul>
  );
}

function Header({ conversationId }: { conversationId: string }) {
  const viewing = useViewing(conversationId);
  return viewing.length > 0 ? <p>{viewing.length} viewing now</p> : null;
}

A user you're not permitted to see simply reads offline (privacy is enforced server-side); the snapshot endpoint caps at 200 ids per request.

Calls

useCall() runs the whole lifecycle; connecting media is your job (pair token{url, token} — with call.room in your LiveKit client; this package has no livekit dependency):

import { useCall } from "@growblic/client-react";

function CallUI() {
  const { status, call, token, endedReason, start, accept, reject, end, reset } = useCall();

  if (status === "idle") return <button onClick={() => start("bob", "voice")}>Call Bob</button>;
  if (status === "ringing")
    return (
      <>
        <p>{call?.peerName ?? "Someone"} is calling…</p>
        <button onClick={accept}>Answer</button>
        <button onClick={reject}>Decline</button>
      </>
    );
  if (status === "connected") return <button onClick={end}>Hang up</button>; // connect LiveKit with {token, call.room}
  if (status === "ended") return <button onClick={reset}>Done ({endedReason})</button>;
  return <p>{status}…</p>; // dialing / connecting
}

One active call at a time: a call.incoming while you're already dialing/connected is ignored by the hook (listen to the raw client.on("call.incoming") if you want call-waiting UX). Note: call.peerId on an incoming call is currently the backend's internal id, not an external id — use peerName for display.

MIT Growblic

Keywords