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

@antzsoft/chat-web-sdk

v1.0.6

Published

Pluggable web chat UI SDK — React, Next.js, Vue, any JS framework

Readme

@antzsoft/chat-web-sdk

Drop-in chat UI for React and Next.js — real-time messaging with 5 layout modes, built-in auth, theming, file uploads, voice messages, and push notifications, all in a single component.

npm version license


Overview

@antzsoft/chat-web-sdk gives you a fully-featured chat UI that you can drop into any React or Next.js app with a single component. It re-exports everything from @antzsoft/chat-core and layers on top of it:

  • React components<AntzChat />, <AntzChatProvider />, individual layout shells, and lower-level chat UI components
  • Hooks — conversations, messages, send/edit/delete, typing, uploads, socket status, responsive breakpoints
  • 5 layout modes — floating bubble, embedded panel, sidebar drawer, fullscreen overlay, mobile webview
  • Built-in auth UI — optional login/register screens with token-based and provider-based flows
  • Theming — light/dark/system modes with full color, font, and border-radius customization
  • File uploads — images, video, audio, and documents with per-type size limits and progress tracking
  • Voice messages — in-browser recording and playback
  • Reactions — emoji reactions on messages
  • Read receipts — per-message delivery and read status
  • Typing indicators — real-time "is typing" display
  • Starred messages — bookmark important messages
  • Message search — search within a conversation
  • Groups — create and manage group conversations
  • Push notifications — Web Push (VAPID) integration via tokenProvider
  • Responsive — container-query-aware layout that adapts from 220 px to full-width

Installation

npm install @antzsoft/chat-web-sdk

Peer dependencies

npm install react react-dom

React 18 or higher is required.

Optional dependencies

# Required if you use the hook-based query API (useConversations, useMessages, etc.)
npm install @tanstack/react-query

# Required for icon rendering inside the built-in UI components
npm install lucide-react

Both packages are listed as optionalDependencies in the SDK. They are bundled into the SDK's own React components, so if you only use <AntzChat /> you do not need to install them separately — they will be resolved from the SDK's own node_modules. You only need to install them yourself if your application code imports from them directly.


Quick Start

// app/chat/page.tsx  (Next.js App Router)
'use client';

import { AntzChat } from '@antzsoft/chat-web-sdk';

export default function ChatPage() {
  return (
    <AntzChat
      config={{
        apiUrl: 'https://api.yourapp.com/api/v1',
        authToken: 'your-jwt-token',
      }}
      layout={{ mode: 'floating' }}
    />
  );
}

That is the entire integration. The component:

  1. Connects to the WebSocket server
  2. Loads conversations and messages
  3. Shows a floating chat bubble that expands into a full chat panel
  4. Manages auth state (restores session from localStorage between page reloads)

The AntzChat Component

<AntzChat /> is the main entry point. It composes AntzChatProvider (config, theme, socket), the selected layout shell, an optional auth gate, and the chat UI.

import { AntzChat } from '@antzsoft/chat-web-sdk';

<AntzChat
  config={webChatConfig}
  theme={themeConfig}
  layout={layoutConfig}
  features={featureConfig}
/>

Props (AntzChatProps)

| Prop | Type | Required | Description | |------|------|----------|-------------| | config | WebChatConfig | Yes | Server URLs, auth, upload settings | | theme | ThemeConfig | No | Colors, fonts, border radius, pattern | | layout | LayoutConfig | No | Layout mode and mode-specific options | | features | FeatureConfig | No | Feature flags and view mode | | conversationListFilters | ConversationListFilters | No | Server-side filters applied to the conversation list — changing these refetches from the server with correct pagination |


WebChatConfig

WebChatConfig extends the platform-agnostic AntzChatConfig from @antzsoft/chat-core. The web SDK pre-fills platformUploadFn (XHR-based upload with progress) and persistStorage (localStorage), so you do not need to supply them.

interface WebChatConfig {
  /** REST API base URL — e.g. "https://api.yourapp.com/api/v1" */
  apiUrl: string;

  /**
   * WebSocket server URL.
   * Defaults to apiUrl with the /api/vN path stripped.
   * The SDK connects to {socketUrl}/chat
   */
  socketUrl?: string;

  /**
   * Static JWT — use this when you already have a token at render time
   * (e.g. SSO / your own auth flow).
   * Use either authToken OR authProvider, not both.
   */
  authToken?: string;

  /**
   * Async function that returns a token string.
   * Called before REST requests and on socket reconnect.
   * Preferred when the host app manages its own token refresh lifecycle.
   */
  authProvider?: () => Promise<string>;

  /** Required for multi-tenant backends. Sent as X-Tenant-ID header. */
  tenantId?: string;

  /**
   * Enable payload-level transit encryption for all HTTP and socket traffic.
   * Uses ECDH key exchange (X25519/P-256) + AES-256-GCM to encrypt every
   * request, response, and socket event on the wire — independent of TLS.
   * Server must have TRANSIT_ENCRYPTION_ENABLED=true (default).
   * Default: true. Set false only for local development or debugging.
   * Safe to toggle anytime — no data migration needed (wire-only, never stored).
   */
  transitEncryption?: boolean;

  /** File upload constraints and callbacks. */
  upload?: {
    /**
     * Per-type size limits in MB, or a single number applied to all types.
     * Defaults: image 5 MB, video 25 MB, audio 10 MB, document 10 MB.
     */
    maxFileSizeMB?: number | {
      image?: number;
      video?: number;
      audio?: number;
      document?: number;
      default?: number;
    };

    /** Max number of files per message. Default: 10 */
    maxFilesPerMessage?: number;

    /** Which file categories users can attach. Default: all four types. */
    allowedTypes?: Array<'image' | 'video' | 'audio' | 'document'>;

    /** Called when a file fails validation or upload. */
    onUploadError?: (file: UploadableFile, error: Error) => void;

    /** Called with aggregate progress (0–100) during a batch upload. */
    onProgress?: (progress: number) => void;
  };

  /**
   * The user's ID in the external auth system.
   * Required for non-builtin authentication modes (antz, external, wso2).
   * Sent as the "x-user-id" request header when provided.
   */
  userId?: string;

  /**
   * Optional profile picture for non-builtin authentication modes.
   * Supply a publicly accessible URL or a base64-encoded data URI.
   * The server fetches/decodes the image, stores it in its own storage,
   * and serves back a 15-minute signed URL.
   */
  avatar?: {
    url?: string;
    base64?: string;
  };

  /**
   * Pre-filled by the web SDK — XHR-based upload with progress events.
   * Override only if you need a custom upload transport.
   */
  platformUploadFn?: PlatformUploadFn;

  /**
   * Pre-filled by the web SDK — canvas + CompressionStream compressor.
   * Override to use a custom compression implementation.
   * Set compression: { enabled: false } to disable without overriding.
   */
  platformCompressFn?: PlatformCompressFn;

  /**
   * Pre-filled by the web SDK — localStorage adapter for auth token persistence.
   * Override to use sessionStorage or an in-memory store.
   */
  persistStorage?: PersistStorage;

  /**
   * Optional compression settings. Compression is enabled by default on web.
   * Pass { enabled: false } to disable.
   */
  compression?: {
    /** Master switch. Default: true */
    enabled?: boolean;
    /** WebP encode quality, 0–1. Default: 0.85 */
    imageQuality?: number;
    /** Longest side cap in px. Default: 1920 */
    imageMaxDimension?: number;
    /** gzip text/JSON/CSV/XML/YAML/SVG. Default: true */
    compressDocuments?: boolean;
  };
}

Layout Modes

Pass a LayoutConfig to the layout prop to control how the chat UI is presented in your app.

interface LayoutConfig {
  mode?: 'floating' | 'embedded' | 'sidebar' | 'fullscreen' | 'mobile-webview';
  position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left'; // floating only
  width?: string;       // CSS width value, e.g. '400px'
  height?: string;      // CSS height value, e.g. '600px'
  zIndex?: number;      // default: 9999
  initialOpen?: boolean;
  drawerSide?: 'left' | 'right'; // sidebar only, default: 'right'
  showBackdrop?: boolean;
}

1. floating (default)

A fixed chat bubble anchored to a corner of the viewport. Clicking the bubble opens a panel above it. On mobile, the panel expands to full-screen with a semi-transparent backdrop.

Use case: Customer support widgets, contextual help, any situation where chat should not disrupt the main page layout.

<AntzChat
  config={config}
  layout={{
    mode: 'floating',
    position: 'bottom-right',   // 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left'
    width: '400px',             // panel width on desktop
    height: '600px',            // panel height on desktop
    initialOpen: false,         // start with panel closed
    showBackdrop: false,        // show backdrop behind panel on desktop (always shown on mobile)
    zIndex: 9999,
  }}
/>

The FAB (floating action button) is 56 px and uses var(--antz-primary) as its background color. On mobile the margin auto-reduces from 24 px to 16 px.


2. embedded

Renders inline inside whatever container you give it. The chat panel fills 100% of the container's width and height. No fixed positioning — behaves like any other block element.

Use case: A dedicated /chat page, a chat section inside a dashboard layout, a split-pane view you control yourself.

// Give the container explicit dimensions
<div style={{ width: '100%', height: '600px' }}>
  <AntzChat
    config={config}
    layout={{
      mode: 'embedded',
      // width/height default to '100%' and fill the container
    }}
  />
</div>

3. sidebar

A slide-in drawer that covers the right (or left) edge of the viewport. Opens via initialOpen: true or by calling setOpen on the SidebarLayout component. A semi-transparent backdrop overlays the main content.

Use case: Help/support panels that slide in over existing content without a full navigation change, CRM sidebars.

<AntzChat
  config={config}
  layout={{
    mode: 'sidebar',
    drawerSide: 'right',     // 'left' | 'right'
    width: '420px',          // drawer width on desktop; auto-fills on mobile/tablet
    initialOpen: false,
    showBackdrop: true,      // default: true
    zIndex: 9999,
  }}
/>

Width behavior:

  • Mobile (< 640 px): 100vw
  • Tablet (640–1023 px): min(width, 100vw)
  • Desktop (≥ 1024 px): the configured width

4. fullscreen

A fixed overlay that covers the entire viewport (position: fixed; inset: 0). Unlike embedded, this uses fixed positioning and sits on top of all other content.

Use case: A full-screen chat experience triggered from a nav item, modals, or apps where chat is the primary UI.

<AntzChat
  config={config}
  layout={{
    mode: 'fullscreen',
    zIndex: 9999,
  }}
/>

5. mobile-webview

Fills 100dvw × 100dvh and is specifically optimized for rendering inside a React Native <WebView>. It applies CSS env(safe-area-inset-*) for iPhone notch and home-indicator clearance, disables iOS rubber-band scroll, injects a viewport-fit=cover meta tag if one is not already present, and forces a single-column layout regardless of container width.

Use case: Hybrid mobile apps that serve the chat UI inside a React Native WebView.

// In your Next.js or standalone web page served to the WebView:
<AntzChat
  config={config}
  layout={{ mode: 'mobile-webview' }}
/>
// In your React Native app:
import { WebView } from 'react-native-webview';

<WebView
  source={{ uri: 'https://yourapp.com/chat' }}
  style={{ flex: 1 }}
  contentInsetAdjustmentBehavior="never"
  bounces={false}
/>

Theming

ThemeConfig

interface ThemeConfig {
  /**
   * Color scheme mode.
   * 'system' reads the user's OS preference via prefers-color-scheme.
   * Default: 'light'
   */
  mode?: 'light' | 'dark' | 'system';

  colors?: {
    primary?: string;          // FAB, send button, active states (default: #25D366)
    primaryDark?: string;      // Hover/active variant of primary (default: #128C7E)
    chatBackground?: string;   // Message list background (default: #ECE5DD light / #0B141A dark)
    bubbleOutgoing?: string;   // Sent message bubble background (default: #DCF8C6 light / #005C4B dark)
    bubbleIncoming?: string;   // Received message bubble background (default: #FFFFFF light / #202C33 dark)
    sidebar?: string;          // Conversation list panel background
    header?: string;           // Conversation list header background
    inputBackground?: string;  // Message input bar background
    textPrimary?: string;      // Primary text color
    textSecondary?: string;    // Secondary / muted text color
  };

  fonts?: {
    family?: string;     // CSS font-family string
    sizeBase?: string;   // CSS font-size, e.g. '14px'
  };

  /** CSS border-radius applied to message bubbles and other rounded elements */
  borderRadius?: string;

  /**
   * Background pattern for the message area.
   * 'default' — subtle radial dot grid
   * 'none'    — flat background color
   * Any other string is treated as a raw CSS `background-image` value
   */
  backgroundPattern?: 'default' | 'none' | string;
}

Colors are injected as CSS custom properties on the root chat element, so your own CSS can reference them:

/* Available CSS variables (scoped to .antz-chat-root) */
--antz-primary
--antz-primary-dark
--antz-chat-bg
--antz-bubble-outgoing
--antz-bubble-incoming
--antz-sidebar
--antz-header
--antz-input-bg
--antz-text-primary
--antz-text-secondary
--antz-font-family
--antz-font-size-base
--antz-border-radius

Light theme example

<AntzChat
  config={config}
  theme={{
    mode: 'light',
    colors: {
      primary: '#25D366',
      primaryDark: '#128C7E',
      chatBackground: '#ECE5DD',
      bubbleOutgoing: '#DCF8C6',
      bubbleIncoming: '#FFFFFF',
      sidebar: '#FFFFFF',
      header: '#008069',
      inputBackground: '#F0F2F5',
      textPrimary: '#111B21',
      textSecondary: '#667781',
    },
  }}
/>

Dark theme example

<AntzChat
  config={config}
  theme={{
    mode: 'dark',
    colors: {
      primary: '#25D366',
      primaryDark: '#128C7E',
      chatBackground: '#0B141A',
      bubbleOutgoing: '#005C4B',
      bubbleIncoming: '#202C33',
      sidebar: '#111B21',
      header: '#202C33',
      inputBackground: '#2A3942',
      textPrimary: '#E9EDEF',
      textSecondary: '#8696A0',
    },
  }}
/>

Matching your app's brand colors

<AntzChat
  config={config}
  theme={{
    mode: 'system',   // automatically follows OS preference
    colors: {
      primary: '#6366F1',       // Indigo — your brand primary
      primaryDark: '#4F46E5',   // Darker shade for hover states
      header: '#4F46E5',        // Sidebar header matches brand
    },
    fonts: {
      family: '"Inter", sans-serif',
      sizeBase: '14px',
    },
    borderRadius: '12px',
    backgroundPattern: 'none',  // clean flat background
  }}
/>

Feature Flags (FeatureConfig)

All features default to true (enabled). Set a flag to false to hide the corresponding UI.

interface FeatureConfig {
  /** Emoji reactions on messages. Default: true */
  reactions?: boolean;

  /** Voice message recording and playback. Default: true */
  voiceMessages?: boolean;

  /** Per-message read receipts (sent / delivered / read ticks). Default: true */
  readReceipts?: boolean;

  /** "is typing..." indicator. Default: true */
  typingIndicators?: boolean;

  /** Star / bookmark messages. Default: true */
  starredMessages?: boolean;

  /** In-conversation message search. Default: true */
  messageSearch?: boolean;

  /** Group conversation creation and management. Default: true */
  groups?: boolean;

  /**
   * Show the built-in login and register UI when the user is not authenticated.
   * Set to false when you handle auth externally (authToken / authProvider).
   * Default: true
   */
  builtInAuth?: boolean;

  /**
   * 'full'                — shows conversation list + chat panel (default)
   * 'single-conversation' — skips the conversation list and opens directly
   *                         into the conversation specified by defaultConversationId
   */
  viewMode?: 'full' | 'single-conversation';

  /**
   * Used with viewMode: 'single-conversation'.
   * The SDK opens this conversation immediately on mount.
   */
  defaultConversationId?: string;

  /** Push notification integration. See Push Notifications section. */
  pushNotifications?: PushNotificationConfig;
}

Example: customer support widget (minimal feature set)

<AntzChat
  config={config}
  features={{
    reactions: false,
    voiceMessages: false,
    starredMessages: false,
    messageSearch: false,
    groups: false,
    builtInAuth: false,     // auth handled externally via authToken
    viewMode: 'single-conversation',
    defaultConversationId: 'support-conv-id',
  }}
/>

Authentication

The SDK supports three authentication modes. Use exactly one at a time.

Mode 1: authToken (static JWT)

Pass a token you have already fetched — from your SSO provider, a login API call, a cookie, etc. The SDK injects it into all REST requests and the WebSocket handshake.

// Fetch your token from your auth server, then pass it in
const token = await fetchMyAuthToken();

<AntzChat
  config={{
    apiUrl: 'https://api.yourapp.com/api/v1',
    authToken: token,
  }}
  features={{ builtInAuth: false }}
/>

The token is stored in localStorage via the built-in persistStorage adapter so it survives page reloads. If you need finer control over persistence, override persistStorage in the config.

Mode 2: authProvider (dynamic token getter)

Pass an async function that returns a fresh token. The SDK calls this before REST requests and on WebSocket reconnect, making it ideal for short-lived tokens or OAuth flows with refresh logic.

<AntzChat
  config={{
    apiUrl: 'https://api.yourapp.com/api/v1',
    authProvider: async () => {
      // Your token refresh logic here
      const { accessToken } = await refreshMyToken();
      return accessToken;
    },
  }}
  features={{ builtInAuth: false }}
/>

Mode 3: builtInAuth (built-in login/register UI)

Leave authToken and authProvider unset and enable builtInAuth (the default). The SDK renders a login form inside the chat panel. Users log in with email/password or register a new account.

<AntzChat
  config={{
    apiUrl: 'https://api.yourapp.com/api/v1',
    tenantId: 'your-tenant-id',   // required when using built-in auth on multi-tenant servers
  }}
  features={{
    builtInAuth: true,   // this is the default — shown here for clarity
  }}
/>

After a successful login the SDK stores the tokens in localStorage and immediately shows the chat UI. A "Register" link lets new users sign up within the same panel.


Push Notifications

The SDK never requests push permission on its own — that is always the parent app's responsibility. You supply a tokenProvider function that the SDK calls automatically after the user authenticates, or use the useDeviceToken hook for manual control from a settings screen.

The server stores one token document per physical device in chat_device_tokens. Every call to register() is an upsert keyed on deviceId — re-registering the same device updates the existing record, never creates a duplicate.

The deviceId rule

deviceId must be a stable UUID stored in localStorage — generated once on first visit, never regenerated. Losing it causes orphan token records in the server DB; the user may receive duplicate notifications until the stale token expires.

// Generate once, reuse forever
function getStableDeviceId(): string {
  let id = localStorage.getItem('chat-device-id');
  if (!id) { id = crypto.randomUUID(); localStorage.setItem('chat-device-id', id); }
  return id;
}

When to register and remove

  • On app init after login — if a push subscription already exists, re-register it. This refreshes lastUsedAt and catches any silent endpoint rotation the browser may have done. The SDK handles this automatically when you pass tokenProvider.
  • When the user enables notifications — request permission, subscribe, register. Only call pushManager.subscribe() at this point (not on every page load — that would prompt the user repeatedly).
  • On logout or "Disable notifications" — call remove(deviceId). This deactivates the token on the server immediately. Also call sub.unsubscribe() to tell the browser to drop the subscription.
interface PushNotificationConfig {
  /**
   * Stable UUID for this device. Store in localStorage, never regenerate.
   * Used as the upsert key — omitting it causes a new record on every page load.
   */
  deviceId?: string;

  /**
   * Called once after login. Return the push payload, or null if permission
   * is not granted. The SDK registers it with the server automatically.
   * On subsequent page loads with an existing subscription, return the
   * existing subscription rather than calling requestPermission() again.
   */
  tokenProvider?: () => Promise<RegisterDeviceTokenPayload | null>;

  /** Called after successful registration. Use to persist the deviceId. */
  onRegistered?: (deviceId: string) => void;

  /** Called if registration fails. Defaults to console.warn. */
  onError?: (error: Error) => void;
}

Web Push (VAPID) — auto-registration via tokenProvider

const VAPID_PUBLIC_KEY = 'your-vapid-public-key';

function base64url(buffer: ArrayBuffer | null): string {
  return buffer ? btoa(String.fromCharCode(...new Uint8Array(buffer))) : '';
}

<AntzChat
  config={config}
  features={{
    pushNotifications: {
      deviceId: localStorage.getItem('chat-device-id') ?? undefined,

      tokenProvider: async () => {
        if (!('serviceWorker' in navigator)) return null;

        // If permission already granted, re-register the existing subscription
        // (upsert — no duplicate created). Don't re-prompt the user.
        if (Notification.permission === 'granted') {
          const reg = await navigator.serviceWorker.ready;
          const existing = await reg.pushManager.getSubscription();
          if (existing) {
            return {
              deviceId: getStableDeviceId(),
              platform: 'web', provider: 'web-push',
              endpoint: existing.endpoint,
              p256dh: base64url(existing.getKey('p256dh')),
              auth: base64url(existing.getKey('auth')),
            };
          }
        }

        // No existing subscription — request permission and subscribe
        const permission = await Notification.requestPermission();
        if (permission !== 'granted') return null;

        const reg = await navigator.serviceWorker.ready;
        const sub = await reg.pushManager.subscribe({
          userVisibleOnly: true,
          applicationServerKey: VAPID_PUBLIC_KEY,
        });

        return {
          deviceId: getStableDeviceId(),
          platform: 'web', provider: 'web-push',
          endpoint: sub.endpoint,
          p256dh: base64url(sub.getKey('p256dh')),
          auth: base64url(sub.getKey('auth')),
        };
      },

      onRegistered: (deviceId) => {
        localStorage.setItem('chat-device-id', deviceId);
      },

      onError: (err) => {
        console.warn('Push registration failed:', err.message);
      },
    },
  }}
/>

Manual registration with useDeviceToken

For full control (e.g. a settings screen where the user explicitly opts in):

import { useDeviceToken } from '@antzsoft/chat-web-sdk';

function NotificationSettings() {
  const { register, remove } = useDeviceToken();

  async function enablePush() {
    const permission = await Notification.requestPermission();
    if (permission !== 'granted') return;

    const reg = await navigator.serviceWorker.ready;
    const sub = await reg.pushManager.subscribe({
      userVisibleOnly: true,
      applicationServerKey: VAPID_PUBLIC_KEY,
    });

    await register({
      deviceId: getStableDeviceId(),
      platform: 'web',
      provider: 'web-push',
      endpoint: sub.endpoint,
      p256dh: base64url(sub.getKey('p256dh')),
      auth: base64url(sub.getKey('auth')),
    });
  }

  async function disablePush() {
    const reg = await navigator.serviceWorker.ready;
    const sub = await reg.pushManager.getSubscription();
    if (sub) await sub.unsubscribe(); // tells browser to drop the subscription
    await remove(getStableDeviceId()); // tells server to stop pushing
  }

  return (
    <div>
      <button onClick={enablePush}>Enable notifications</button>
      <button onClick={disablePush}>Disable notifications</button>
    </div>
  );
}

Notification Preferences

User notification preferences are stored server-side in chat_user_prefs. A record with all defaults is created automatically when a push token is first registered — so this API is always available after push setup. Call it via usersApi from @antzsoft/chat-core.

All fields are optional — only send what changed. Future preference fields are added to the same collection and same API; no new endpoints.

import { usersApi } from '@antzsoft/chat-core';
import type { UserPreferences } from '@antzsoft/chat-core';

// Read current preferences
const prefs = await usersApi.getPreferences();

// Partial updates — only send changed fields
await usersApi.updatePreferences({ notifyOnReaction: false });
await usersApi.updatePreferences({ messagePreview: false }); // privacy mode
await usersApi.updatePreferences({ notificationsEnabled: false }); // master off
await usersApi.updatePreferences({
  quietHours: { enabled: true, start: '23:00', end: '07:00', timezone: 'Europe/London' },
});

Preferences reference

| Field | Default | Description | |---|---|---| | notificationsEnabled | true | Master switch — false disables all push | | soundEnabled | true | Play sound with notifications | | messagePreview | true | Show message text in body. false = show "New message" only | | notifyOnMention | true | Notify when @mentioned in a group | | notifyOnReaction | true | Notify when someone reacts to your message | | notifyOnGroupInvite | true | Notify when added to a group | | quietHours.enabled | false | Enable quiet hours window | | quietHours.start | "22:00" | Start of quiet window (HH:MM) | | quietHours.end | "08:00" | End of quiet window (HH:MM) | | quietHours.timezone | "UTC" | IANA timezone for the quiet window |

Settings UI example

import { useState, useEffect } from 'react';
import { usersApi } from '@antzsoft/chat-core';
import type { UserPreferences } from '@antzsoft/chat-core';

function NotificationSettings() {
  const [prefs, setPrefs] = useState<UserPreferences | null>(null);

  useEffect(() => {
    usersApi.getPreferences().then(setPrefs);
  }, []);

  async function toggle(field: keyof UserPreferences, value: boolean) {
    await usersApi.updatePreferences({ [field]: value });
    setPrefs(prev => ({ ...prev, [field]: value }));
  }

  if (!prefs) return null;

  return (
    <div>
      <label>
        <input type="checkbox" checked={prefs.notificationsEnabled ?? true}
          onChange={e => toggle('notificationsEnabled', e.target.checked)} />
        Enable notifications
      </label>
      <label>
        <input type="checkbox" checked={prefs.messagePreview ?? true}
          onChange={e => toggle('messagePreview', e.target.checked)} />
        Show message preview
      </label>
      <label>
        <input type="checkbox" checked={prefs.notifyOnMention ?? true}
          onChange={e => toggle('notifyOnMention', e.target.checked)} />
        Mentions
      </label>
      <label>
        <input type="checkbox" checked={prefs.notifyOnReaction ?? true}
          onChange={e => toggle('notifyOnReaction', e.target.checked)} />
        Reactions
      </label>
      <label>
        <input type="checkbox" checked={prefs.notifyOnGroupInvite ?? true}
          onChange={e => toggle('notifyOnGroupInvite', e.target.checked)} />
        Group invites
      </label>
    </div>
  );
}

Hooks

All hooks must be called from within a component that is a descendant of AntzChatProvider (or <AntzChat />, which mounts the provider internally).

useAntzChat()

Access the resolved config, theme, layout, and features from anywhere inside the provider tree. Throws if called outside of AntzChatProvider.

import { useAntzChat } from '@antzsoft/chat-web-sdk';

function MyComponent() {
  const { config, theme, layout, features, resolvedMode } = useAntzChat();

  return (
    <div>
      <p>Connected to: {config.apiUrl}</p>
      <p>Theme: {resolvedMode}</p>
      <p>Reactions enabled: {String(features.reactions)}</p>
    </div>
  );
}

Returns:

| Field | Type | Description | |-------|------|-------------| | config | ResolvedConfig | Resolved config with all defaults filled | | theme | ThemeConfig | Raw theme prop as passed | | layout | LayoutConfig | Raw layout prop as passed | | features | Required<FeatureConfig> | Merged feature flags (user overrides + defaults) | | resolvedMode | 'light' \| 'dark' | Actual color mode after resolving 'system' |


useConversations(filters?)

Fetches and manages the conversation list. Initial load is via REST; all subsequent updates arrive through the WebSocket (no polling). Pass an optional filters object to narrow the list using server-side filtering — changing any filter triggers a new fetch with correct pagination.

import { useConversations } from '@antzsoft/chat-web-sdk';

function ConversationSidebar() {
  // No filters — full list
  const { conversations, isLoading } = useConversations();

  // With server-side filters — each unique filter set is a separate query cache entry
  const { conversations: groups }  = useConversations({ type: 'group' });
  const { conversations: pinned }  = useConversations({ isPinned: true });
  const { conversations: unread }  = useConversations({ hasUnread: true });
  const { conversations: results } = useConversations({ search: 'design' });
}

function ConversationSidebar() {
  const {
    conversations,
    isLoading,
    error,
    refetch,
    createDirect,
    createGroup,
    mute,
    unmute,
    pin,
    unpin,
    leaveGroup,
    leaveAndDeleteGroup,
    deleteGroup,
  } = useConversations({ type: 'group', hasUnread: true });

  async function startChat(userId: string) {
    await createDirect.mutateAsync(userId);
  }

  async function newGroup() {
    await createGroup.mutateAsync({
      name: 'Project Alpha',
      memberIds: ['user-1', 'user-2'],
    });
  }

  return (
    <ul>
      {conversations.map((conv) => (
        <li key={conv.id}>
          {conv.name}
          <button onClick={() => pin.mutate(conv.id)}>Pin</button>
          <button onClick={() => mute.mutate({ id: conv.id })}>Mute</button>
          <button onClick={() => leaveGroup.mutate(conv.id)}>Exit Group</button>
          <button onClick={() => leaveAndDeleteGroup.mutate(conv.id)}>Exit &amp; Delete</button>
        </li>
      ))}
    </ul>
  );
}

Returns:

| Field | Type | Description | |-------|------|-------------| | conversations | Conversation[] | Sorted list (pinned first, then by last message time) | | isLoading | boolean | True during the initial fetch | | error | Error \| null | Fetch error | | refetch | () => void | Manually re-fetch the conversation list | | createDirect | UseMutationResult | Create a direct message conversation with a userId | | createGroup | UseMutationResult | Create a group conversation | | mute | UseMutationResult | Mute a conversation (optionally until a date) | | unmute | UseMutationResult | Unmute a conversation | | pin | UseMutationResult | Pin a conversation to the top | | unpin | UseMutationResult | Unpin a conversation | | leaveGroup | UseMutationResult<void, Error, string> | Exit a group — stays in list as read-only. Auto-promotes if last admin. | | leaveAndDeleteGroup | UseMutationResult<void, Error, string> | Exit a group and remove it from the list in one call ("Exit and Delete"). | | deleteGroup | UseMutationResult<void, Error, string> | Hide a conversation from the list. For DMs this is "Delete Chat"; for groups already exited this is "Delete Group". Any participant can call this — no admin role required. |

Accepted filters (ConversationListFilters):

| Filter | Type | Description | |--------|------|-------------| | type | 'direct' \| 'group' | Show only DMs or only group conversations | | isPinned | boolean | true = only pinned, false = only unpinned | | isMuted | boolean | true = only muted, false = only unmuted | | hasUnread | boolean | Only conversations with at least 1 unread message | | search | string | Server-side text search on group name / description | | role | 'admin' \| 'member' | Filter by the current user's role in conversations | | hasAttachments | boolean | Filter by whether the last message has attachments | | attachmentType | 'image' \| 'video' \| 'document' \| 'audio' | Filter by last message attachment type | | notificationsEnabled | boolean | Filter by notification setting |

All filters are applied server-side. Omit page and limit to receive all matching conversations in one response. Pass both to opt into paginated results.

Passing filters to <AntzChat />

Pass conversationListFilters directly on the component. The conversation list refetches from the server whenever the filter object changes — pagination is always correct.

// Show only group conversations
<AntzChat
  config={config}
  conversationListFilters={{ type: 'group' }}
/>

// Show only unread conversations
<AntzChat
  config={config}
  conversationListFilters={{ hasUnread: true }}
/>

// Combine filters
<AntzChat
  config={config}
  conversationListFilters={{ type: 'group', isPinned: true }}
/>

Unread counts

Each Conversation object in useConversations() already has unreadCount populated from the initial REST fetch. The socket then keeps it live — you don't need to poll.

Chat icon badge — the complete pattern

useConversations() is already subscribed to every conversation_updated and unread_count_changed socket event internally. Summing unreadCount from the hook is all you need — no extra socket listener, no polling:

import { useConversations } from '@antzsoft/chat-web-sdk';

function ChatIconButton({ onClick }: { onClick: () => void }) {
  const { conversations } = useConversations();

  // Recalculates automatically every time any conversation's unreadCount changes
  const totalUnread = conversations.reduce(
    (sum, c) => sum + (c.unreadCount ?? 0), 0
  );

  return (
    <button onClick={onClick} style={{ position: 'relative' }}>
      💬
      {totalUnread > 0 && (
        <span style={{
          position: 'absolute',
          top: -6, right: -6,
          backgroundColor: '#e53935',
          color: '#fff',
          borderRadius: '50%',
          minWidth: 18, height: 18,
          fontSize: 11, fontWeight: 700,
          display: 'flex', alignItems: 'center', justifyContent: 'center',
          padding: '0 4px',
        }}>
          {totalUnread > 99 ? '99+' : totalUnread}
        </span>
      )}
    </button>
  );
}

The update chain is fully automatic:

New message sent
  → server emits conversation_updated with DB-accurate unreadCount
  → SocketProvider updates useConversations() cache
  → totalUnread recalculates
  → badge re-renders

Reading a conversation clears it the same way via unread_count_changed, including on other tabs/devices of the same user.

When to call the REST APIs

The socket may have been down (tab was backgrounded, network drop, etc.). Refresh from the server on:

import { conversationsApi } from '@antzsoft/chat-core';
import type { UnreadSummary } from '@antzsoft/chat-core';

// On app focus / visibility change — catch up after tab was hidden
useEffect(() => {
  function onVisible() {
    if (document.visibilityState === 'visible') {
      // Option A — refetch the full conversation list (also updates unreadCount)
      refetch();

      // Option B — lightweight, just the counts (no full list fetch)
      conversationsApi.getUnreadSummary().then((summary: UnreadSummary) => {
        console.log('Total unread:', summary.totalUnread);
        // summary.byConversation = [{ conversationId, unreadCount }, ...]
        // Update your own state / badge here
      });
    }
  }
  document.addEventListener('visibilitychange', onVisible);
  return () => document.removeEventListener('visibilitychange', onVisible);
}, [refetch]);

// Refresh a single conversation after a push notification opens it
const { unreadCount } = await conversationsApi.getUnreadCount(conversationId);

How the socket events work

The server emits to the user's private room — all browser tabs and devices of the same user receive these simultaneously:

// conversation_updated — fires when a new message arrives
// unreadCount is server-calculated (always accurate, not optimistic)
socket?.on('conversation_updated', ({ conversationId, unreadCount, lastMessage }) => {
  // The web SDK's SocketProvider handles this automatically —
  // useConversations() updates in real-time with no extra code needed
});

// unread_count_changed — fires when the user reads a conversation
// Also fires on your OTHER devices so they clear the badge too
socket?.on('unread_count_changed', ({ conversationId, unreadCount }) => {
  // unreadCount is 0 here — the SDK handles this automatically as well
});

Both events are handled internally by the SDK's SocketProvider. You only need to listen manually if you're building a custom badge (e.g. browser tab title or favicon).

conversation_deleted

Emitted only to the acting user's own sockets when they self-delete a conversation (delete()), exit+delete a group (leave(id, true)), or hide an already-exited group (delete()). Other participants never receive this event. The web SDK's SocketProvider handles it automatically — the conversation is removed from the useConversations() cache across all tabs. Only listen manually if you need a side effect such as navigation:

import { tryGetSocket } from '@antzsoft/chat-core';

useEffect(() => {
  const socket = tryGetSocket();
  if (!socket) return;

  const onDeleted = ({ conversationId }: { conversationId: string }) => {
    if (activeConversationId === conversationId) {
      router.push('/conversations');
    }
  };

  socket.on('conversation_deleted', onDeleted);
  return () => { socket.off('conversation_deleted', onDeleted); };
}, [activeConversationId]);
// Custom tab title badge
import { tryGetSocket } from '@antzsoft/chat-core';

useEffect(() => {
  const socket = tryGetSocket();
  if (!socket) return;

  const update = () => {
    const total = conversations.reduce((s, c) => s + (c.unreadCount ?? 0), 0);
    document.title = total > 0 ? `(${total}) Antz Chat` : 'Antz Chat';
  };

  socket.on('conversation_updated', update);
  socket.on('unread_count_changed', update);
  return () => { socket.off('conversation_updated', update); socket.off('unread_count_changed', update); };
}, [conversations]);

useMessages(conversationId)

Infinite-scroll message list for a conversation. Messages are loaded oldest-to-newest in pages of 40. After the initial load, new messages arrive via the WebSocket.

import { useMessages } from '@antzsoft/chat-web-sdk';

function MessageFeed({ conversationId }: { conversationId: string }) {
  const {
    messages,
    isLoading,
    isFetchingMore,
    hasMore,
    fetchMore,
    refresh,
    error,
  } = useMessages(conversationId);

  return (
    <div onScroll={(e) => {
      const el = e.currentTarget;
      // Load older messages when scrolled near the top
      if (el.scrollTop < 100 && hasMore && !isFetchingMore) {
        fetchMore();
      }
    }}>
      {messages.map((msg) => (
        <div key={msg.id}>{msg.content.text}</div>
      ))}
    </div>
  );
}

Returns:

| Field | Type | Description | |-------|------|-------------| | messages | Message[] | Flat array of all loaded messages, newest first | | isLoading | boolean | True during the initial page fetch | | isFetchingMore | boolean | True while loading an older page | | hasMore | boolean | True if there are older messages to load | | fetchMore | () => void | Load the next (older) page | | refresh | () => void | Invalidate and re-fetch all pages | | error | Error \| null | Fetch error |


useChat(conversationId)

The primary hook for all message-level interactions: send, edit, delete, typing, mark read, and reply/edit state.

import { useChat } from '@antzsoft/chat-web-sdk';

function ChatInput({ conversationId }: { conversationId: string }) {
  const {
    sendMessage,
    editMessage,
    deleteMessage,
    startTyping,
    stopTyping,
    markRead,
    replyingTo,
    setReplyingTo,
    editingMessage,
    setEditingMessage,
  } = useChat(conversationId);

  const [text, setText] = useState('');
  const [files, setFiles] = useState<File[]>([]);

  return (
    <form onSubmit={async (e) => {
      e.preventDefault();
      if (editingMessage) {
        await editMessage(editingMessage.id, text);
      } else {
        await sendMessage(text, files);
      }
      setText('');
      setFiles([]);
    }}>
      {replyingTo && (
        <div>Replying to: {replyingTo.content?.text}
          <button type="button" onClick={() => setReplyingTo(null)}>Cancel</button>
        </div>
      )}
      <input
        value={text}
        onChange={(e) => { setText(e.target.value); startTyping(); }}
        onBlur={stopTyping}
      />
      <input type="file" multiple onChange={(e) => setFiles(Array.from(e.target.files ?? []))} />
      <button type="submit">Send</button>
    </form>
  );
}

sendMessage optimistically inserts the message into the cache before the server confirms, so the UI feels instant. If the socket emit fails, the message is marked with deliveryStatus: 'failed'.

Returns:

| Field | Type | Description | |-------|------|-------------| | sendMessage | (text: string, files?: File[]) => Promise<void> | Send text and/or file attachments | | editMessage | (messageId: string, newText: string) => Promise<void> | Edit a sent message | | deleteMessage | (messageId: string) => Promise<void> | Delete a message for everyone. Own messages are subject to the conversation's delete window (default 30 min); group admins can delete any message with no time restriction. | | startTyping | () => void | Broadcast typing started | | stopTyping | () => void | Broadcast typing stopped | | markRead | (messageId?: string) => void | Mark conversation (or specific message) as read | | replyingTo | Message \| null | The message currently being replied to | | setReplyingTo | (msg: Message \| null) => void | Set or clear the reply target | | editingMessage | Message \| null | The message currently being edited | | setEditingMessage | (msg: Message \| null) => void | Set or clear the edit target |


useSocket()

Real-time WebSocket connection status.

import { useSocket } from '@antzsoft/chat-web-sdk';

function ConnectionIndicator() {
  const { status, isConnected, isReconnecting, isError } = useSocket();

  if (isConnected) return <span style={{ color: 'green' }}>Connected</span>;
  if (isReconnecting) return <span style={{ color: 'orange' }}>Reconnecting…</span>;
  if (isError) return <span style={{ color: 'red' }}>Connection error</span>;
  return <span style={{ color: 'grey' }}>Connecting…</span>;
}

Returns:

| Field | Type | Description | |-------|------|-------------| | status | SocketStatus | Raw status string: 'connecting' \| 'connected' \| 'reconnecting' \| 'error' \| 'disconnected' | | isConnected | boolean | status === 'connected' | | isReconnecting | boolean | status === 'reconnecting' | | isError | boolean | status === 'error' |


useUpload(conversationId?)

Low-level file upload hook with MIME validation, size-limit enforcement, and progress tracking. Prefer using sendMessage from useChat for message attachments — useUpload is for standalone upload flows (e.g. a profile picture uploader).

import { useUpload } from '@antzsoft/chat-web-sdk';

function FileUploader({ conversationId }: { conversationId: string }) {
  const { upload, validateFiles, progress, isUploading, error } = useUpload(conversationId);

  async function handleFiles(files: File[]) {
    const { valid, errors } = validateFiles(files);
    errors.forEach(({ file, reason }) => console.warn(file.name, reason));

    if (valid.length > 0) {
      const result = await upload(valid);
      console.log('Uploaded:', result.successful);
      console.log('Failed:', result.failed);
    }
  }

  return (
    <div>
      <input
        type="file"
        multiple
        onChange={(e) => handleFiles(Array.from(e.target.files ?? []))}
      />
      {isUploading && <progress value={progress} max={100} />}
      {error && <p style={{ color: 'red' }}>{error}</p>}
    </div>
  );
}

Returns:

| Field | Type | Description | |-------|------|-------------| | upload | (files: File[]) => Promise<BatchUploadResult> | Validate and upload files. Enforces maxFilesPerMessage. | | validateFiles | (files: File[]) => { valid: File[]; errors: Array<{ file: File; reason: string }> } | Validate without uploading (MIME check + size limit) | | progress | number | Aggregate upload progress, 0–100 | | isUploading | boolean | True while a batch upload is in progress | | error | string \| null | Last upload error message |


useResponsive()

Viewport-width-based breakpoints using a resize event listener. Useful for conditional rendering in custom components.

import { useResponsive } from '@antzsoft/chat-web-sdk';

function AdaptivePanel() {
  const { isMobile, isTablet, isDesktop, width } = useResponsive();

  return (
    <div>
      {isMobile && <MobileView />}
      {isTablet && <TabletView />}
      {isDesktop && <DesktopView />}
      <small>Viewport: {width}px</small>
    </div>
  );
}

Returns:

| Field | Type | Description | |-------|------|-------------| | isMobile | boolean | width < 640 | | isTablet | boolean | 640 <= width < 1024 | | isDesktop | boolean | width >= 1024 | | width | number | Current window.innerWidth |

Note: the chat UI itself uses a ResizeObserver on the container element (not the window) for its internal responsive behavior, so an embedded chat panel narrower than 640 px switches to the single-column layout even on a wide monitor. useResponsive uses window width and is intended for your own components.


updateProfile()

Update the current user's profile fields (firstName, lastName, email, displayName, phone) immediately. Works in both builtin and non-builtin modes. In non-builtin modes, use this to push a change from your identity provider to the chat server without waiting for the next background sync cycle.

import { usersApi, type UpdateProfilePayload } from '@antzsoft/chat-web-sdk';

await usersApi.updateProfile({
  firstName:   'Jane',
  lastName:    'Smith',
  email:       '[email protected]',
  displayName: 'Jane S.',
  phone:       '+919900000000',
});

email and phone must be unique within the tenant — a 409 Conflict is returned if another user already holds the same value.


useProfile()

Exposes avatar upload and sync operations for the current user. Use in builtin-auth mode to let users change their own profile picture, or in non-builtin mode to push an updated avatar after init.

import { useProfile } from '@antzsoft/chat-web-sdk';

const { uploadAvatar, syncAvatar, isUploading, error } = useProfile();

// Builtin mode — upload a File from an <input type="file">
await uploadAvatar(file);

// Non-builtin — sync from URL or base64 post-init
await syncAvatar({ url: 'https://cdn.example.com/avatar.jpg' });
await syncAvatar({ base64: 'data:image/jpeg;base64,...' });

After a successful call, the SDK invalidates the ['users', 'me'] query and updates the auth store — the new signed URL appears throughout the UI immediately.

Returns:

| Field | Type | Description | |-------|------|-------------| | uploadAvatar | (file: File) => Promise<void> | Multipart avatar upload (builtin mode) | | syncAvatar | (source: { url?: string; base64?: string }) => Promise<void> | Sync avatar from URL or base64 | | isUploading | boolean | True while an upload or sync is in progress | | error | string \| null | Last error message |


Using the Provider Directly

If you want to build a fully custom chat UI using only the hooks and stores — without the built-in components — use AntzChatProvider directly and render your own children.

import {
  AntzChatProvider,
  useConversations,
  useMessages,
  useChat,
  useSocket,
} from '@antzsoft/chat-web-sdk';

function MyCustomChat() {
  const { conversations } = useConversations();
  const { isConnected } = useSocket();
  // ... your own rendering

  return <div>{/* your UI */}</div>;
}

export default function App() {
  return (
    <AntzChatProvider
      config={{ apiUrl: 'https://api.yourapp.com/api/v1', authToken: 'token' }}
      theme={{ mode: 'dark' }}
      layout={{ mode: 'embedded' }}
      features={{ reactions: true }}
    >
      <MyCustomChat />
    </AntzChatProvider>
  );
}

AntzChatProvider sets up:

  • The QueryProvider (@tanstack/react-query)
  • The SocketProvider (WebSocket connection + event handlers)
  • CSS custom property injection for the active theme
  • Global base styles scoped to .antz-chat-root

File Uploads

The SDK handles the full upload flow:

  1. Your app passes File objects to sendMessage or useUpload
  2. The SDK validates MIME type and file size against your UploadConfig
  3. The SDK requests a presigned URL from your backend (POST /storage/presigned-url)
  4. The browser uploads the file directly to S3/Azure/your storage via XHR with progress events
  5. The SDK confirms the upload with the backend and embeds the file reference in the message

Supported file types

| Category | MIME types | |----------|-----------| | image | image/jpeg, image/png, image/gif, image/webp, image/svg+xml | | video | video/mp4, video/webm, video/quicktime | | audio | audio/mpeg, audio/wav, audio/ogg, audio/webm, audio/mp4 | | document | PDF, Word, Excel, PowerPoint, plain text, CSV, Markdown, XML, YAML, RTF, JSON, ZIP |

Configuring limits

<AntzChat
  config={{
    apiUrl: 'https://api.yourapp.com/api/v1',
    authToken: 'token',
    upload: {
      // Per-type limits in MB
      maxFileSizeMB: {
        image: 10,
        video: 50,
        audio: 20,
        document: 25,
      },
      maxFilesPerMessage: 5,
      allowedTypes: ['image', 'document'],  // disable video and audio

      onUploadError: (file, error) => {
        toast.error(`Failed to upload ${file.name}: ${error.message}`);
      },

      onProgress: (pct) => {
        console.log(`Upload progress: ${pct}%`);
      },
    },
  }}
/>

Default limits: image 5 MB, video 25 MB, audio 10 MB, document 10 MB, max 10 files per message.


File Compression

Compression runs client-side in the browser before upload, using zero external dependencies. It is on by default — pass compression: { enabled: false } to opt out.

What gets compressed

| File type | What happens | Algorithm | |-----------|-------------|-----------| | JPEG, PNG, GIF, BMP, TIFF, WebP | Resize to imageMaxDimension (default 1920px longest side) + re-encode | WebP via canvas.toBlob | | SVG, JSON, CSV, plain text, XML, YAML, RTF, Markdown | Compress bytes | gzip via CompressionStream | | Video, audio, PDF, ZIP, Office formats (docx/xlsx/pptx) | Skipped — already compressed | — |

If the compressed result is larger than the original, the original is used automatically.

Default behavior (nothing to configure)

Compression is on with sensible defaults. No code change needed:

<AntzChat
  config={{
    apiUrl: 'https://api.yourapp.com/api/v1',
    authToken: 'token',
  }}
/>
// ↑ Images compressed to WebP @ 85% quality, max 1920px
// ↑ Text files gzip-compressed
// ↑ Everything else uploaded as-is

Tuning compression

<AntzChat
  config={{
    apiUrl: 'https://api.yourapp.com/api/v1',
    authToken: 'token',
    compression: {
      imageQuality: 0.75,        // 0–1, default: 0.85
      imageMaxDimension: 1280,   // longest side cap in px, default: 1920
      compressDocuments: false,  // skip gzip on text/json/csv, default: true
    },
  }}
/>

Disable compression entirely

<AntzChat
  config={{
    apiUrl: 'https://api.yourapp.com/api/v1',
    authToken: 'token',
    compression: { enabled: false },
  }}
/>

Custom compressor

Override platformCompressFn to plug in your own compression logic:

import type { PlatformCompressFn } from '@antzsoft/chat-web-sdk';

const myCompressFn: PlatformCompressFn = async (file, options) => {
  // Return a CompressedFile — same shape as UploadableFile plus:
  // { originalSize, compressed, compressionAlgorithm }
  // Return compressed: false to signal no compression was applied.
  return { ...file, originalSize: file.size, compressed: false, compressionAlgorithm: 'none' };
};

<AntzChat
  config={{
    apiUrl: '...',
    authToken: '...',
    platformCompressFn: myCompressFn,
  }}
/>

TypeScript types

import type { CompressionConfig, CompressedFile, CompressionAlgorithm } from '@antzsoft/chat-web-sdk';

Next.js Setup

Because <AntzChat /> uses browser APIs (WebSocket, localStorage, ResizeObserver), it must be rendered in a Client Component.

App Router (recommended pattern)

Create a wrapper component that marks itself as a Client Component:

// components/ChatWrapper.tsx
'use client';

import { AntzChat } from '@antzsoft/chat-web-sdk';
import type { WebChatConfig } from '@antzsoft/chat-web-sdk';

const chatConfig: WebChatConfig = {
  apiUrl: process.env.NEXT_PUBLIC_CHAT_API_URL!,
  tenantId: process.env.NEXT_PUBLIC_TENANT_ID,
};

export function ChatWrapper() {
  return (
    <AntzChat
      config={chatConfig}
      layout={{ mode: 'floating', position: 'bottom-right' }}
      theme={{ mode: 'system' }}
    />
  );
}
// app/layout.tsx  (Server Component — no 'use client')
import { ChatWrapper } from '@/components/ChatWrapper';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        {children}
        <ChatWrapper />
      </body>
    </html>
  );
}

This pattern keeps app/layout.tsx as a Server Component while isolating the browser-only SDK in the wrapper.

Pages Router

// pages/_app.tsx
import type { AppProps } from 'next/app';
import dynamic from 'next/dynamic';

// Disable SSR for the chat component
const AntzChat = dynamic(
  () => import('@antzsoft/chat-web-sdk').then((m) => m.AntzChat),
  { ssr: false }
);

export default function App({ Component, pageProps }: AppProps) {
  return (
    <>
      <Component {...pageProps} />
      <AntzChat
        config={{ apiUrl: process.env.NEXT_PUBLIC_CHAT_API_URL! }}
        layout={{ mode: 'floating' }}
      />
    </>
  );
}

Re-exported Core Utilities

All exports from @antzsoft/chat-core are available directly from this package. You do not need to install @antzsoft/chat-core separately.

import {
  // API clients
  conversationsApi,
  messagesApi,
  devicesApi,
  storageApi,
  authApi,

  // Stores
  getAuthStore,
  useChatStore,   // lastRead, lastSeen, onlineUsers, typingUsers, activeConversationId, …
  initAuthStore,
  initApiClient,
  setApiClientInstance,

  // Socket
  socketEmit,
  getSocketStatus,
  onSocketStatus,

  // Config helpers
  resolveConfig,

  // Types
  type AntzChatConfig,
  type ResolvedConfig,
  type Conversation,
  type Message,
  type FileType,
  type UploadableFile,
  type BatchUploadResult,
  type RegisterDeviceTokenPayload,
  type SocketStatus,
  type LastReadEntry,  // { messageId: string; readAt: string }
} from '@antzsoft/chat-web-sdk';

// Access last-read and last-seen from the store (kept live by socket events)
const lastRead = useChatStore((s) => s.lastRead['conv-abc']);   // LastReadEntry | undefined
const lastSeen = useChatStore((s) => s.lastSeen['user-xyz']);   // ISO string | undefined
const isOnline = useChatStore((s) => s.onlineUsers.includes('user-xyz'));

// Seed on first open (before any socket event has fired)
import { messagesApi, usersApi } from '@antzsoft/chat-web-sdk';
const { lastReadMessageId, lastReadAt } = await messagesApi.getLastRead('conv-abc');
if (lastReadMessageId && lastReadAt) {
  useChatStore.getState().setLastRead('conv-abc', lastReadMessageId, lastReadAt);
}
const { lastSeenAt } = await usersApi.getLastSeen('user-xyz');
if (lastSeenAt) useChatStore.getState().setLastSeen('user-xyz', lastSeenAt);

TypeScript

All props, hooks, and internal types are fully typed. Import the types you need for your own components:

import type {
  AntzChatProps,
  WebChatConfig,
  ThemeConfig,
  LayoutConfig,
  LayoutMode,
  FeatureConfig,
  PushNotificationConfig,
  AntzChatProviderProps,
} from '@antzsoft/chat-web-sdk';

Importable UI components

Beyond hooks, the following components can be imported for use in custom layouts:

| Component | Description | |---|---| | Avatar | Circular avatar with firstName/lastName initials fallback | | AvatarUpload | Click-to-upload avatar widget (builtin mode) | | ContactProfilePanel | Contact/group info panel, opened from the chat header | | OwnProfilePanel | Own profile panel with avatar upload, opened from the sidebar | | GroupInfoPanel | Group info side panel — shows group avatar, name, description, and member list. Admins see a camera button on the avatar to upload or replace the group icon. Uses the same presigned URL pipeline as message attachments — camera button only visible to admins; server enforces the admin check independently. | | NewChatModal | New chat / new group modal. When ≥ 2 members are selected, shows a group name input and an optional icon picker (camera button). Icon is uploaded after the group is created using the presigned URL pipeline. If the user closes without submitting, nothing is uploaded — no orphaned files. |

Example: typed config factory

import type { WebChatConfig, ThemeConfig, LayoutConfig, FeatureConfig } from '@antzsoft/chat-web-sdk';

function buildChatConfig(userId: string, token: string): WebChatConfig {
  return {
    apiUrl: 'https://api.yourapp.com/api/v1',
    authToken: token,
    tenantId: 'acme-corp',
    upload: {
      maxFileSizeMB: { image: 10, video: 50, audio: 20, document: 25 },
      maxFilesPerMessage: 5,
    },
  };
}

const theme: ThemeConfig = {
  mode: 'system',
  colors: { primary: '#6366F1' },
};

const layout: LayoutConfig = {
  mode: 'floating',
  position: 'bottom-right',
};

const features: FeatureConfig = {
  groups: false,
  builtInAuth: false,
};

Profile & Avatar

Viewing profiles

Click any avatar or display name in the chat header to open ContactProfilePanel — showing the contact's photo, name, email, and phone for direct chats, or the member list for groups.

Three ways to open your own profile:

  1. ··· menu → "My profile" — always accessible, including in floating/single-column layouts where the sidebar is hidden.
  2. Click your own avatar (top-left of the sidebar, next to "Chats") — available when the sidebar is visible.
  3. Click the name/avatar in the "My Notes" conversation header — shows your own profile with upload option (builtin mode).

useProfile hook

See the useProfile hook documentation in the Hooks section above.

AvatarUpload component

Drop-in click-to-upload avatar widget. Shows a camera overlay on hover and a spinner during upload. Handles its own error state.

import { AvatarUpload } from '@antzsoft/chat-web-sdk';

// Renders the current user's avatar with click-to-upload (builtin mode)
<AvatarUpload size={80} />

ContactProfilePanel component

Shows contact or group info. For direct messages: contact photo, display name, email, and phone. For groups: group name, description, and member list. Opened automatically when the user clicks a name or avatar in the chat header.

OwnProfilePanel component

Displays the current user's own profile with an inline avatar upload control (wraps AvatarUpload). Accessible from the ··· menu ("My profile"), the own avatar in the sidebar header, or via the "My Notes" conversation.

Avatar component — initials fallback

The Avatar component accepts firstName and lastName for precise two-letter initials:

import { Avatar } from '@antzsoft/chat-web-sdk';

<Avatar
  src={user.avatarUrl}
  name={user.displayName}
  firstName={user.firstName}
  lastName={user.lastName}
  size={48}
/>

If avatarUrl is missing or the image fails to load, the component renders a colored circle with initials derived from firstName[0] + lastName[0] (e.g. "NP" for Nidhin Pratap).

How avatar storage works

Avatars are stored in the chat server's own storage (local/S3/Azure) under avatars/{userId}/{timestamp}.jpg. The server:

  1. Fetches the URL or decodes the base64
  2. Validates: must be image/jpeg, image/png, image/gif, or image/webp — max 5 MB by default (configurable via AVATAR_MAX_SIZE env var in bytes)
  3. Computes a SHA-256 hash — if it matches the stored hash, skips the upload entirely (no-op on repeat logins)
  4. Stores storageKey, provider, bucket, mimeType, size, hash in the user record
  5. Returns a 15-minute signed URL on every profile fetch — never stores the URL itself

This means changing your storage provider (local → S3) or CDN doesn't break existing avatar references.

Server configuration:

AVATAR_MAX_SIZE=5242880    # 5 MB — max profile picture size in bytes

Error Handling

All errors thrown by the SDK — REST calls, socket operations, or internal state — are instances of typed AntzChatError subclasses exported from @antzsoft/chat-core (re-exported from this package). Every error carries a machine-readable code, a retryable boolean, and a context object.

Imports

import {
  AntzChatError,
  AntzChatAuthError,
  AntzChatValidationError,
  AntzChatNetworkError,
  AntzChatPermissionError,
  AntzChatServerError,
} from '@antzsoft/chat-web-sdk'; // re-exported from @antzsoft/chat-core

Error classes and codes

| Class | Codes | Typical cause | |---|---|---| | AntzChatAuthError | SESSION_EXPIRED, AUTH_FAILED | 401 — expired/missing token | | AntzChatValidationError | VALIDATION_ERROR | 400 / 422 — bad input. .fields array for multi-field server errors | | AntzChatPermissionError | PERMISSION_DENIED | 403 — insufficient role | | AntzChatNetworkError | NETWORK_ERROR, RATE_LIMITED, SOCKET_TIMEOUT, SOCKET_NOT_CONNECTED, SEND_QUEUE_FULL, MESSAGE_DROPPED | Network, socket, queue overflow | | AntzChatServerError | SERVER_ERROR, NOT_FOUND | 5xx, 404 | | AntzChatError (base) | TRANSIT_MISMATCH | SDK/server transit-encryption config mismatch |

retryable = true on AntzChatNetworkError and AntzChatServerError.

Common patterns for React / Next.js

Auth errors — show login screen

import { AntzChatAuthError } from '@antzsoft/chat-web-sdk';

try {
  await authApi.login({ email, password });
} catch (err) {
  if (err instanceof AntzChatAuthError) {
    setLoginError(err.message); // "Unauthorized" or server-provided message
  }
}

Validation errors — field-level feedback

import { AntzChatValidationError } from '@antzsoft/chat-web-sdk';

try {
  await authApi.register(payload);
} catch (err) {
  if (err instanceof An