# @hiyve/react

React bindings for the Hiyve SDK. Provides a provider component, 31 hooks, and optional cloud integration for building video conferencing applications with React 18+.

## Installation

```bash
npx @hiyve/cli login
npm install @hiyve/react @hiyve/core @hiyve/rtc-client
```

To use cloud features (AI, room discovery, meeting intelligence), also install:

```bash
npm install @hiyve/cloud
```

## Quick Start

```tsx
import { HiyveProvider, useConnection, useRoom, useParticipants, useLocalMedia } from '@hiyve/react';

function App() {
  return (
    <HiyveProvider>
      <VideoRoom />
    </HiyveProvider>
  );
}

function VideoRoom() {
  const { joinRoom, isConnecting, error } = useConnection();
  const { room, isInRoom, isOwner } = useRoom();
  const { participants, participantCount } = useParticipants();
  const { isAudioMuted, isVideoMuted, toggleAudio, toggleVideo } = useLocalMedia();

  if (!isInRoom) {
    return (
      <button onClick={() => joinRoom('my-room', 'user-123')} disabled={isConnecting}>
        {isConnecting ? 'Joining...' : 'Join Room'}
      </button>
    );
  }

  return (
    <div>
      <h1>{room?.name} ({participantCount} participants)</h1>
      <button onClick={toggleAudio}>{isAudioMuted ? 'Unmute' : 'Mute'}</button>
      <button onClick={toggleVideo}>{isVideoMuted ? 'Camera On' : 'Camera Off'}</button>
    </div>
  );
}
```

## Hooks

Each hook tracks only its relevant state, so components re-render only when their data changes.

### Connection & Room

| Hook | Returns |
|------|---------|
| `useConnection()` | `isConnected`, `isConnecting`, `error`, `createRoom`, `joinRoom`, `joinRoomWithToken`, `getRoomInfoFromToken`, `leaveRoom` |
| `useRoom()` | `room`, `isOwner`, `isInRoom`, `isNoVideo` |
| `useNoVideoRoom()` | `isConnected`, `isConnecting`, `error`, `isNoVideo`, `isInRoom`, `room`, `createNoVideoRoom`, `joinNoVideoRoom`, `closeNoVideoRoom`, `setActiveRoom` |
| `useClient()` | Raw rtc-client instance (`Client \| null`) |

### Participants & Media

| Hook | Returns |
|------|---------|
| `useParticipants()` | `participants` (array), `participantsMap`, `localUserId`, `participantCount` |
| `useParticipant(userId)` | Single `Participant \| undefined` for a specific user |
| `useLocalMedia()` | `isAudioMuted`, `isVideoMuted`, `isOutputMuted`, `isScreenSharing`, `toggleAudio`, `toggleVideo`, `toggleOutputMute`, `startScreenShare`, `stopScreenShare` |
| `useDevices()` | `setVideoDevice`, `setAudioInputDevice`, `setAudioOutputDevice` |
| `useRemoteMute()` | `remoteMuteAudio`, `remoteMuteVideo`, `muteRemoteOutput` |
| `useOutputMute()` | `muteRemoteOutput` |

### Recording, Streaming & Transcription

| Hook | Returns |
|------|---------|
| `useRecording()` | `isRecording`, `isRecordingStarting`, `recordingId`, `recordingStartTime`, `responseId`, `error`, `recordingDuration`, `startRecording`, `stopRecording`, `clearError` |
| `useStreaming()` | `isStreaming`, `isStreamingStarting`, `streamingId`, `featuredUserId`, `streamingStartTime`, `streamingUrl`, `error`, `streamingDuration`, `startStreaming`, `stopStreaming`, `switchStreamingUser`, `getStreamingUrls`, `clearError` |
| `useTranscription()` | `isTranscribing`, `isTranscriptionStarting`, `transcriptions`, `startTranscription`, `stopTranscription`, `enrichTranscription` |

### Chat & Waiting Room

| Hook | Returns |
|------|---------|
| `useChat()` | `messages`, `unreadCount`, `sendMessage`, `sendDataMessage`, `clearUnread`, `loadChatHistory` |
| `useWaitingRoom()` | `waitingUsers`, `isWaitingForAdmission`, `wasRejected`, `admitUser`, `rejectUser` |
| `useWaitForHost()` | `isWaiting`, `roomName`, `timeout`, `elapsedTime` |
| `useAiChat()` | `messages`, `setMessages` |

### Intelligence

| Hook | Returns |
|------|---------|
| `useIntelligenceStream()` | `isActive`, `coachingEnabled`, `coachingVariant`, `hints`, `allHints`, `talkRatio`, `currentTopic`, `topicShifts`, `enableCoaching`, `disableCoaching`, `updateCoachingData`, `dismissHint` |

### Layout & Interaction

| Hook | Returns |
|------|---------|
| `useLayout()` | `dominantSpeaker`, `setDominant` |
| `useHandRaise()` | `raisedHands`, `isHandRaised`, `raisedHandCount`, `toggleHandRaised`, `lowerAllHands` |
| `useAudioProcessing()` | `feedbackDetected`, `audioValidation`, `gainNode`, `audioInputMonitor`, `setGain` |

### Stored Rooms, Files & Active Rooms

| Hook | Returns |
|------|---------|
| `useStoredRooms()` | `rooms`, `isLoading`, `error`, `lastFetchedAt`, `fetchStoredRooms`, `addStoredRoom`, `updateStoredRoom`, `deleteStoredRoom`, `getStoredRoom`, `clearError`, `clearStoredRooms` |
| `useUserFiles()` | `files`, `isLoading`, `error`, `lastFetchedAt`, `fetchUserFiles`, `deleteFile`, `renameFile`, `moveFile`, `shareFile`, `createFolder`, `deleteFolder`, `getFileUrl`, `uploadFile`, `clearError` |
| `useActiveRooms()` | `rooms`, `isConnected`, `isLoading`, `error`, `connectActiveRooms`, `disconnectActiveRooms`, `fetchActiveRooms`, `advertiseRoom`, `removeAdvertisedRoom`, `updateAdvertisedRoom`, `clearError` |

### Join Token

| Hook | Returns |
|------|---------|
| `useJoinToken(options)` | Token validation, room info, password handling, auto-join |

### Room Flow

| Hook | Returns |
|------|---------|
| `useRoomFlow()` | `screen` (`'lobby'` \| `'connecting'` \| `'waiting-for-host'` \| `'waiting-room'` \| `'waiting-room-rejected'` \| `'in-room'`), `isConnecting`, `isInRoom` |

### Additional Cameras

| Hook | Returns |
|------|---------|
| `useAdditionalCameras(options)` | `cameras`, `addCamera`, `removeCamera`, `removeAll`, `ghostUserIds`, `isGhostCamera`, `getLabel` |
| `useRemoteCamera(options)` | `startPairing`, `disconnect`, `qrUrl`, `status`, `ghostUserId`, `error`, `isExpired` |

### Cloud

| Hook | Returns |
|------|---------|
| `useCloudClient()` | `CloudClient \| null` -- the cloud client from HiyveProvider, or null if no cloud auth was configured |
| `useUserChat()` | `conversations`, `activeRoomName`, `activeMessages`, `isLoading`, `isLoadingHistory`, `error`, `nextCursor`, `fetchConversations`, `fetchHistory`, `sendMessage`, `setActiveConversation`, `loadMore` |

#### useUserChat

Manages global chat conversations via the cloud service. Fetches conversation lists, loads message history with cursor-based pagination, sends messages with optimistic updates, and subscribes to real-time message delivery when a conversation is active.

```tsx
import { useUserChat } from '@hiyve/react';

function ChatView({ userId }: { userId: string }) {
  const {
    conversations,
    activeMessages,
    isLoading,
    isLoadingHistory,
    nextCursor,
    fetchConversations,
    setActiveConversation,
    fetchHistory,
    sendMessage,
    loadMore,
  } = useUserChat();

  useEffect(() => {
    fetchConversations(userId);
  }, [userId, fetchConversations]);

  return (
    <div>
      {conversations.map((c) => (
        <button key={c.roomName} onClick={() => {
          setActiveConversation(c.roomName);
          fetchHistory(c.roomName);
        }}>
          {c.roomName}
        </button>
      ))}
      {activeMessages.map((m) => (
        <p key={m.id}>{m.userId}: {m.content}</p>
      ))}
      {nextCursor && <button onClick={loadMore} disabled={isLoadingHistory}>Load older</button>}
    </div>
  );
}
```

#### useAdditionalCameras

Manages additional camera feeds that join as separate ghost participants. Each camera creates an independent WebRTC connection, so the feed appears automatically in recordings and live streams.

```tsx
import { useAdditionalCameras } from '@hiyve/react';

function CameraManager() {
  const { cameras, addCamera, removeCamera, removeAll } = useAdditionalCameras({
    roomName: 'my-room',
    ownerUserId: 'user@example.com',
    generateRoomToken: async () => {
      const res = await fetch('/api/generate-room-token', { method: 'POST' });
      const data = await res.json();
      return data.roomToken;
    },
    enabled: true,
  });

  return (
    <div>
      <button onClick={() => addCamera('device-id-123', 'Document Camera')}>Add Camera</button>
      {cameras.map((cam) => (
        <div key={cam.id}>
          {cam.label} - {cam.isActive ? 'Active' : cam.isConnecting ? 'Connecting...' : 'Error'}
          <button onClick={() => removeCamera(cam.id)}>Remove</button>
        </div>
      ))}
    </div>
  );
}
```

**Options (`UseAdditionalCamerasOptions`):**

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `roomName` | `string` | -- | Room name to join ghost cameras into |
| `ownerUserId` | `string` | -- | Owner's userId (used to build ghost camera userIds) |
| `generateRoomToken` | `() => Promise<string>` | -- | Async function that returns a room token for the ghost client |
| `enabled` | `boolean` | `true` | Whether the hook is enabled |
| `onError` | `(error: Error, cameraId: string) => void` | -- | Callback when an error occurs |

**Returns (`UseAdditionalCamerasReturn`):**

| Property | Type | Description |
|----------|------|-------------|
| `cameras` | `AdditionalCamera[]` | List of all additional cameras (connecting, active, or errored) |
| `addCamera` | `(deviceId: string, label?: string) => Promise<void>` | Add a new camera by device ID |
| `removeCamera` | `(id: string) => Promise<void>` | Remove a specific camera by instance ID |
| `removeAll` | `() => Promise<void>` | Remove all additional cameras |
| `ghostUserIds` | `Set<string>` | Set of active ghost camera userIds |
| `isGhostCamera` | `(userId: string) => boolean` | Check if a userId is a ghost camera |
| `getLabel` | `(userId: string) => string \| null` | Get the display label for a ghost camera userId |

#### useRemoteCamera

Manages pairing a remote device (e.g., a phone) as an additional camera feed via QR code. The remote device joins the room as a ghost participant using a time-limited join token.

```tsx
import { useRemoteCamera } from '@hiyve/react';

function RemoteCameraPairing() {
  const { startPairing, disconnect, qrUrl, status, isExpired } = useRemoteCamera({
    roomName: 'my-room',
    ownerUserId: 'user@example.com',
    enabled: true,
  });

  if (status === 'connected') {
    return <button onClick={disconnect}>Disconnect Phone Camera</button>;
  }

  return (
    <div>
      <button onClick={startPairing}>Pair Phone Camera</button>
      {qrUrl && <img src={`https://api.qrserver.com/v1/create-qr-code/?data=${encodeURIComponent(qrUrl)}`} alt="QR Code" />}
      {isExpired && <p>Link expired. Tap to generate a new one.</p>}
    </div>
  );
}
```

**Options (`UseRemoteCameraOptions`):**

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `roomName` | `string` | -- | Room name to pair the remote camera into |
| `ownerUserId` | `string` | -- | Owner's userId |
| `baseUrl` | `string` | `window.location.origin` | Base URL for the QR code link |
| `joinPath` | `string` | `'/camera'` | Path for the camera page |
| `expiresIn` | `number` | `300` | Token expiry in seconds |
| `label` | `string` | `'Phone'` | Label for the ghost camera |
| `enabled` | `boolean` | `true` | Whether the hook is enabled |

**Returns (`UseRemoteCameraReturn`):**

| Property | Type | Description |
|----------|------|-------------|
| `startPairing` | `() => Promise<void>` | Initiate pairing (generates join token and QR URL) |
| `disconnect` | `() => void` | Disconnect the remote camera |
| `qrUrl` | `string \| null` | URL to display as a QR code |
| `status` | `'idle' \| 'waiting' \| 'connected' \| 'error'` | Current pairing status |
| `ghostUserId` | `string \| null` | Ghost userId for the remote camera |
| `error` | `string \| null` | Error message if pairing failed |
| `isExpired` | `boolean` | Whether the current token has expired |

## Ghost Camera Utilities

Utility functions for working with ghost camera participant IDs. Re-exported from `@hiyve/core`.

| Export | Type | Description |
|--------|------|-------------|
| `GHOST_CAMERA_DELIMITER` | `string` | The delimiter used in ghost camera userIds (`'__cam__'`) |
| `isGhostCamera(userId)` | `(userId: string) => boolean` | Returns `true` if the userId belongs to a ghost camera participant |
| `getGhostCameraLabel(userId)` | `(userId: string) => string \| null` | Extracts the human-readable label from a ghost camera userId |
| `getGhostCameraOwner(userId)` | `(userId: string) => string \| null` | Extracts the owner's userId from a ghost camera userId |
| `buildGhostCameraUserId(ownerUserId, label)` | `(ownerUserId: string, label: string) => string` | Builds a ghost camera userId from the owner's userId and a label |

## Provider Props

### HiyveProvider

`HiyveProvider` is the recommended provider. It accepts all `HiyveStoreOptions` plus optional cloud configuration:

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `generateRoomToken` | `() => Promise<string>` | auto | Returns a Hiyve room token. Defaults to `POST /api/generate-room-token` when using `@hiyve/admin` server middleware |
| `localVideoElementId` | `string` | `'local-video'` | ID of the local video element |
| `connectOptions` | `{ videoDeviceId?, audioDeviceId? }` | `{}` | Initial device IDs |
| `persistDeviceChanges` | `boolean` | `false` | Persist device selections across sessions |
| `onError` | `(error: Error) => void` | - | Error callback |
| `generateCloudToken` | `() => Promise<string>` | auto | Returns a short-lived cloud token. Defaults to `POST /api/generate-cloud-token` when using `@hiyve/admin` server middleware |
| `cloudToken` | `string` | - | Static cloud token. Use `generateCloudToken` instead for auto-refresh |
| `cloudEnvironment` | `'production' \| 'development'` | `'production'` | Cloud API environment |
| `cloudBaseUrl` | `string` | - | Override the cloud API base URL |
| `presence` | `boolean \| { intervalMs?: number }` | - | Enable automatic presence heartbeat. When true, the cloud client signals the server that the current user is online |

### Cloud Integration

Both room tokens and cloud tokens are generated automatically when your server uses `@hiyve/admin` middleware. `HiyveProvider` calls `POST /api/generate-room-token` and `POST /api/generate-cloud-token` by default -- no props needed.

```tsx
// Zero-config -- room and cloud tokens are generated automatically
<HiyveProvider>
  <App />
</HiyveProvider>
```

You can still pass custom generators or static tokens to override the defaults:

```tsx
// Custom token generators (override the defaults)
<HiyveProvider
  generateRoomToken={myCustomRoomTokenGenerator}
  generateCloudToken={myCustomCloudTokenGenerator}
>
  <App />
</HiyveProvider>
```

A stable `CloudClient` instance is available to all child components via `useCloudClient()`:

```tsx
import { useCloudClient } from '@hiyve/react';

function MyComponent() {
  const cloudClient = useCloudClient();

  if (!cloudClient) return <p>Cloud not configured</p>;

  // Use cloudClient for AI queries, room discovery, etc.
}
```

### Identity Bridge

When used alongside `@hiyve/react-identity`, the `IdentityProvider` automatically provides the authenticated user's email to cloud services and the `HiyveProvider`. This allows cloud tokens to be generated for the correct user without passing `userId` manually.

```tsx
import { IdentityBridgeContext, useIdentityEmail } from '@hiyve/react';
```

| Export | Type | Description |
|--------|------|-------------|
| `IdentityBridgeContext` | `React.Context` | Connects `IdentityProvider` to `CloudProvider` so cloud tokens include the authenticated user |
| `useIdentityEmail()` | `() => string \| null` | Returns the authenticated identity user's email, or `null` |

## Advanced: Direct Store Access

For custom integrations, access the `HiyveStore` directly:

```tsx
import { useStore } from '@hiyve/react';

function CustomComponent() {
  const store = useStore();
  // Access store methods for custom integrations
}
```

## Re-exported Types

All core state types are re-exported from `@hiyve/core` for convenience:

```tsx
import type {
  HiyveStoreState,
  Participant,
  RoomInfo,
  ChatMessage,
  TranscriptionEntry,
  RecordingState,
  StreamingState,
  WaitingRoomUser,
  // ... and more
} from '@hiyve/react';
```

Types specific to `@hiyve/react` hooks:

```tsx
import type {
  // Chat
  UserChatMessage,
  // Room flow
  RoomFlowScreen,
  UseRoomFlowReturn,
  // Join token
  UseJoinTokenState,
  UseJoinTokenActions,
  UseJoinTokenOptions,
  // Additional cameras
  AdditionalCamera,
  UseAdditionalCamerasOptions,
  UseAdditionalCamerasReturn,
  // Remote camera
  UseRemoteCameraOptions,
  UseRemoteCameraReturn,
} from '@hiyve/react';
```

## Constants

Join token error codes, re-exported from `@hiyve/core`:

| Constant | Value | Description |
|----------|-------|-------------|
| `TOKEN_NOT_FOUND` | `'TOKEN_NOT_FOUND'` | Token does not exist (HTTP 404) |
| `TOKEN_EXPIRED` | `'TOKEN_EXPIRED'` | Token has expired (HTTP 410) |
| `INVALID_PASSWORD` | `'INVALID_PASSWORD'` | Incorrect room password (HTTP 401) |
| `USER_NOT_AUTHORIZED` | `'USER_NOT_AUTHORIZED'` | User is not allowed to join via this token (HTTP 403) |
| `ROOM_NOT_ACTIVE` | `'ROOM_NOT_ACTIVE'` | Room is not active yet (HTTP 503) |

## Utilities

| Function | Description |
|----------|-------------|
| `resolveTabsConfig(config, mode)` | Resolves a `StoredRoomTabsConfig` into a flat `TabsFeatureConfig` for `'offline'` or `'live'` mode. Handles structured, legacy flat, and null configs. Re-exported from `@hiyve/core`. |
| `cleanUserId(userId)` | Sanitizes a user ID string for use with the signaling server. Re-exported from `@hiyve/rtc-client`. |

## Requirements

- React 18+
- `@hiyve/core` and `@hiyve/rtc-client` as peer dependencies
- `@hiyve/cloud` as optional peer dependency (for cloud features)

## Related Packages

- `@hiyve/core` -- Framework-agnostic state store (works with React, Angular, Vue, or any framework)
- `@hiyve/react-ui` -- Pre-built UI components (video tile, video grid, control bar, sidebar)
- `@hiyve/react-capture` -- Recording, streaming, and transcription components
- `@hiyve/react-collaboration` -- Chat, polls, Q&A, and file manager components
- `@hiyve/react-intelligence` -- AI assistant, meeting summary, alerts, and sentiment
- `@hiyve/react-notes` -- Collaborative meeting notes
- `@hiyve/react-whiteboard` -- Collaborative whiteboard

## License

Proprietary - Hiyve SDK
