# @hiyve/rn-react

React Native provider, hooks, and pre-built UI components for Hiyve video conferencing.

Provides `HiyveRNProvider` with 18 hooks and 5 components covering the core mobile video calling experience: video tiles, grid layout, control bar, participant list, and waiting room.

## Installation

```bash
npm install @hiyve/rn-react @hiyve/rn-core @hiyve/rtc-client-rn react-native-webrtc react-native-safe-area-context @react-native-vector-icons/material-design-icons
```

### iOS Setup

After installing, link the native pods:

```bash
cd ios && pod install
```

The `ControlBar` component uses Material Design Icons. You **must** register the font in your `Info.plist`:

```xml
<key>UIAppFonts</key>
<array>
    <string>MaterialDesignIcons.ttf</string>
</array>
```

Without this entry, icons will render as "?" placeholders. CocoaPods copies the font file into your app bundle automatically, but iOS requires `UIAppFonts` to load it at runtime.

### Android Setup

No additional setup needed — the font is bundled automatically via Gradle.

## Quick Start

```tsx
import React from 'react';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import {
  HiyveRNProvider,
  useConnection,
  useLocalMedia,
  useParticipants,
  VideoGrid,
  ControlBar,
} from '@hiyve/rn-react';

const options = {
  generateRoomToken: async () => {
    const res = await fetch('https://your-api.com/token', { method: 'POST' });
    return res.json();
  },
  onError: (err) => console.error('Hiyve:', err.message),
};

function VideoRoom() {
  const { isConnected, createRoom, leaveRoom } = useConnection();
  const { isAudioMuted, isVideoMuted, localStream, toggleAudio, toggleVideo, switchCamera } = useLocalMedia();
  const { participants, localUserId } = useParticipants();

  if (!isConnected) {
    return <Button title="Create Room" onPress={() => createRoom('my-room', 'user1')} />;
  }

  return (
    <View style={{ flex: 1 }}>
      <VideoGrid
        localStream={localStream}
        localUserId={localUserId}
        isLocalAudioMuted={isAudioMuted}
        isLocalVideoMuted={isVideoMuted}
        participants={participants}
      />
      <ControlBar
        isAudioMuted={isAudioMuted}
        isVideoMuted={isVideoMuted}
        onToggleAudio={toggleAudio}
        onToggleVideo={toggleVideo}
        onFlipCamera={switchCamera}
        onLeave={leaveRoom}
      />
    </View>
  );
}

export default function App() {
  return (
    <SafeAreaProvider>
      <HiyveRNProvider options={options}>
        <VideoRoom />
      </HiyveRNProvider>
    </SafeAreaProvider>
  );
}
```

## Provider

### HiyveRNProvider

Wrap your app (or the screen that uses video) with the provider:

```tsx
<HiyveRNProvider options={options}>
  <YourApp />
</HiyveRNProvider>
```

| Prop | Type | Description |
|------|------|-------------|
| `options` | `HiyveStoreRNOptions` | **Required.** Store configuration with `generateRoomToken` callback |
| `children` | `ReactNode` | Child components |

## Hooks

All hooks must be used within a `HiyveRNProvider`.

### useConnection

Connection state and room management.

| Return | Type | Description |
|--------|------|-------------|
| `isConnected` | `boolean` | Whether connected to the server |
| `isConnecting` | `boolean` | Whether a connection attempt is in progress |
| `error` | `string \| null` | Error message if connection failed |
| `createRoom` | `(name, userId, opts?) => Promise` | Create a new room as host |
| `joinRoom` | `(name, userId) => Promise` | Join an existing room |
| `leaveRoom` | `() => Promise` | Leave room and disconnect |

### useRoom

Room info and ownership.

| Return | Type | Description |
|--------|------|-------------|
| `room` | `RoomInfo \| null` | Current room info |
| `isOwner` | `boolean` | Whether the local user is the host |
| `isInRoom` | `boolean` | Whether the user is in a room |

### useParticipants

Remote participants in the room.

| Return | Type | Description |
|--------|------|-------------|
| `participants` | `Participant[]` | Array of all participants |
| `participantsMap` | `Map<string, Participant>` | Map for direct lookup |
| `localUserId` | `string \| null` | Local user's ID |
| `participantCount` | `number` | Total participant count |

### useParticipant(userId)

Single participant lookup.

| Return | Type | Description |
|--------|------|-------------|
| (value) | `Participant \| undefined` | The participant, or undefined |

### useLocalMedia

Local media state and controls (RN-extended).

| Return | Type | Description |
|--------|------|-------------|
| `isAudioMuted` | `boolean` | Microphone muted |
| `isVideoMuted` | `boolean` | Camera off |
| `isOutputMuted` | `boolean` | Speaker muted |
| `isScreenSharing` | `boolean` | Always false (not supported in RN) |
| `localStream` | `MediaStream \| null` | Local camera stream for RTCView |
| `toggleAudio` | `() => Promise` | Toggle mic mute |
| `toggleVideo` | `() => Promise` | Toggle camera |
| `switchCamera` | `() => Promise` | Flip front/back camera |

### useLocalStream

Convenience hook returning just the local `MediaStream`.

```tsx
const stream = useLocalStream();
```

### useRecording

Recording state with auto-incrementing duration.

| Return | Type | Description |
|--------|------|-------------|
| `isRecording` | `boolean` | Recording active |
| `isRecordingStarting` | `boolean` | Recording starting |
| `recordingDuration` | `number` | Elapsed seconds |
| `startRecording` | `(opts?) => Promise<boolean>` | Start recording |
| `stopRecording` | `() => Promise` | Stop recording |

### useStreaming

Live streaming state with duration.

| Return | Type | Description |
|--------|------|-------------|
| `isStreaming` | `boolean` | Streaming active |
| `streamingDuration` | `number` | Elapsed seconds |
| `startStreaming` | `(opts?) => Promise` | Start streaming |
| `stopStreaming` | `() => Promise` | Stop streaming |

### useTranscription

Real-time transcription.

| Return | Type | Description |
|--------|------|-------------|
| `isTranscribing` | `boolean` | Transcription active |
| `transcriptions` | `TranscriptionEntry[]` | Transcription entries |
| `startTranscription` | `() => Promise<boolean>` | Start transcription |
| `stopTranscription` | `() => Promise` | Stop transcription |

### useChat

Text chat messaging.

| Return | Type | Description |
|--------|------|-------------|
| `messages` | `ChatMessage[]` | All messages |
| `unreadCount` | `number` | Unread count |
| `sendMessage` | `(content: string) => void` | Send a message |
| `clearUnread` | `() => void` | Reset unread count |

### useWaitingRoom

Waiting room management.

| Return | Type | Description |
|--------|------|-------------|
| `waitingUsers` | `WaitingRoomUser[]` | Users waiting (host view) |
| `isWaitingForAdmission` | `boolean` | Waiting to be admitted (guest) |
| `wasRejected` | `boolean` | Was rejected (guest) |
| `admitUser` | `(userId) => Promise` | Admit a user (host) |
| `rejectUser` | `(userId) => Promise` | Reject a user (host) |

### useHandRaise

Hand raise feature.

| Return | Type | Description |
|--------|------|-------------|
| `raisedHands` | `Map<string, number>` | User IDs with raise timestamps |
| `toggleHandRaised` | `() => Promise` | Toggle local hand |
| `lowerAllHands` | `() => Promise` | Lower all hands (host) |

### useLayout

Layout and dominant speaker.

| Return | Type | Description |
|--------|------|-------------|
| `dominantSpeaker` | `string \| null` | Dominant speaker user ID |
| `setDominant` | `(userId) => void` | Set dominant (host) |

### useAudioProcessing

Audio feedback detection state.

| Return | Type | Description |
|--------|------|-------------|
| `feedbackDetected` | `boolean` | Whether audio feedback (echo/howl) is currently detected |

### useWaitForHost

Wait-for-host state when joining before the host starts the room.

| Return | Type | Description |
|--------|------|-------------|
| `isWaiting` | `boolean` | Whether waiting for the host |
| `roomName` | `string \| null` | Room being waited on |
| `timeout` | `number \| null` | Timeout duration in seconds |
| `elapsedTime` | `number` | Seconds spent waiting |

### useRemoteMute

Host-only actions for remotely muting participants. Actions are no-ops when called by a non-owner.

| Return | Type | Description |
|--------|------|-------------|
| `remoteMuteAudio` | `(userId, mute) => Promise` | Mute/unmute a participant's mic |
| `remoteMuteVideo` | `(userId, mute) => Promise` | Mute/unmute a participant's camera |
| `muteRemoteOutput` | `(userId, mute) => Promise` | Mute/unmute a participant's speaker |

### useClient

Escape hatch for accessing the raw store and client instances. Prefer the typed hooks for most cases.

| Return | Type | Description |
|--------|------|-------------|
| `store` | `HiyveStoreRN` | The store instance |
| `client` | `Client \| null` | The underlying RN WebRTC client |

### usePermissions

Android/iOS permission requests.

| Return | Type | Description |
|--------|------|-------------|
| `granted` | `boolean` | Permissions granted |
| `denied` | `boolean` | Permissions denied |
| `requesting` | `boolean` | Request in progress |
| `requestPermissions` | `() => Promise<boolean>` | Request camera + mic |

## Components

### VideoTile

Renders a participant's video using `RTCView`, with avatar fallback and mute indicators.

```tsx
<VideoTile
  stream={participant.videoStream}
  userId={participant.userId}
  displayName="Alice"
  isAudioMuted={participant.isAudioMuted}
  isVideoMuted={participant.isVideoMuted}
/>
```

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `stream` | `MediaStream \| null` | — | Video stream to render |
| `userId` | `string` | — | User ID (for avatar color) |
| `displayName` | `string` | userId | Display name |
| `isAudioMuted` | `boolean` | `false` | Show mute badge |
| `isVideoMuted` | `boolean` | `false` | Show avatar instead of video |
| `isLocal` | `boolean` | `false` | Mirror video + show "(You)" |
| `mirror` | `boolean` | `false` | Mirror the video |
| `colors` | `Partial<VideoTileColors>` | — | Color overrides |
| `muteIcon` | `ReactNode` | — | Custom mute indicator |
| `width` | `number` | fill parent | Tile width |
| `height` | `number` | fill parent | Tile height |

### VideoGrid

Auto-layout grid for local + remote video tiles.

```tsx
<VideoGrid
  localStream={localStream}
  localUserId={localUserId}
  localDisplayName="Me"
  isLocalAudioMuted={isAudioMuted}
  isLocalVideoMuted={isVideoMuted}
  participants={participants}
  gap={4}
  maxColumns={2}
/>
```

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `localStream` | `MediaStream \| null` | — | Local camera stream |
| `localUserId` | `string \| null` | — | Local user's ID |
| `localDisplayName` | `string` | — | Local user's display name |
| `isLocalAudioMuted` | `boolean` | `false` | Local audio muted |
| `isLocalVideoMuted` | `boolean` | `false` | Local video off |
| `participants` | `Participant[]` | — | Remote participants |
| `tileColors` | `Partial<VideoTileColors>` | — | Colors for all tiles |
| `muteIcon` | `ReactNode` | — | Custom mute icon |
| `gap` | `number` | `4` | Gap between tiles (px) |
| `maxColumns` | `number` | `2` | Maximum columns |

### ControlBar

Bottom control bar with mic, camera, flip, and leave buttons.

```tsx
<ControlBar
  isAudioMuted={isAudioMuted}
  isVideoMuted={isVideoMuted}
  onToggleAudio={toggleAudio}
  onToggleVideo={toggleVideo}
  onFlipCamera={switchCamera}
  onLeave={leaveRoom}
/>
```

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `isAudioMuted` | `boolean` | — | Microphone state |
| `isVideoMuted` | `boolean` | — | Camera state |
| `onToggleAudio` | `() => void` | — | Mic toggle handler |
| `onToggleVideo` | `() => void` | — | Camera toggle handler |
| `onFlipCamera` | `() => void` | — | Camera flip handler |
| `onLeave` | `() => void` | — | Leave handler |
| `colors` | `Partial<ControlBarColors>` | — | Color overrides |
| `labels` | `Partial<ControlBarLabels>` | — | Label overrides |
| `icons` | `{ mic?, micOff?, camera?, ... }` | — | Custom icon elements |
| `safeArea` | `boolean` | `true` | Apply safe area padding |

### ControlBarButton

Reusable circular button used inside the ControlBar. Can also be used standalone to build custom control bar layouts.

```tsx
<ControlBarButton
  icon={<Icon name="microphone" size={24} color="#fff" />}
  label="Mute"
  onPress={toggleAudio}
  backgroundColor="#333"
  textColor="#fff"
/>
```

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `icon` | `ReactNode` | -- | Icon element to render |
| `label` | `string` | -- | Button label text |
| `onPress` | `() => void` | -- | Press handler |
| `backgroundColor` | `string` | -- | Button background color |
| `textColor` | `string` | -- | Label text color |
| `disabled` | `boolean` | `false` | Disable the button |

### DurationBadge

Small badge showing a pulsing dot, a label, and a formatted timer. Used to indicate active recording or streaming.

```tsx
<DurationBadge duration={recordingDuration} label="REC" />
<DurationBadge duration={streamingDuration} label="LIVE" backgroundColor="#22c55e" />
```

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `duration` | `number` | -- | Duration in seconds |
| `label` | `string` | -- | Badge label (e.g. `"REC"`, `"LIVE"`) |
| `backgroundColor` | `string` | `'#ef4444'` | Badge background color |
| `pulseColor` | `string` | `'#ffffff'` | Pulsing dot color |

### ParticipantList

Scrollable list of participants with avatar, name, and status.

```tsx
<ParticipantList
  participants={participants}
  localUserId={localUserId}
  ownerId={room?.owner}
/>
```

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `participants` | `Participant[]` | — | Participants to display |
| `localUserId` | `string \| null` | — | Local user ID (shows "You") |
| `ownerId` | `string` | — | Host user ID (shows "Host" badge) |
| `colors` | `Partial<ParticipantListColors>` | — | Color overrides |
| `labels` | `Partial<ParticipantListLabels>` | — | Label overrides |
| `showHeader` | `boolean` | `true` | Show title with count |

### WaitingRoom

Dual-mode waiting room for guests and hosts.

**Guest mode** — shows a waiting spinner or rejection message:

```tsx
<WaitingRoom
  mode="guest"
  isWaitingForAdmission={isWaitingForAdmission}
  wasRejected={wasRejected}
  onGoBack={() => navigation.goBack()}
/>
```

**Host mode** — shows list of waiting users with admit/reject buttons:

```tsx
<WaitingRoom
  mode="host"
  waitingUsers={waitingUsers}
  onAdmitUser={admitUser}
  onRejectUser={rejectUser}
/>
```

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `mode` | `'guest' \| 'host'` | — | Render mode |
| `waitingUsers` | `WaitingRoomUser[]` | `[]` | Users waiting (host mode) |
| `isWaitingForAdmission` | `boolean` | `false` | Waiting state (guest mode) |
| `wasRejected` | `boolean` | `false` | Rejected state (guest mode) |
| `onAdmitUser` | `(userId) => void` | — | Admit handler (host mode) |
| `onRejectUser` | `(userId) => void` | — | Reject handler (host mode) |
| `onGoBack` | `() => void` | — | Back handler (guest rejected) |
| `colors` | `Partial<WaitingRoomColors>` | — | Color overrides |
| `labels` | `Partial<WaitingRoomLabels>` | — | Label overrides |

## Utility Functions

### requestCameraAndMicPermissions

Requests camera and microphone permissions from the OS. On Android, prompts via the native permission dialog. On iOS, returns `true` immediately since `react-native-webrtc` handles prompts automatically on first access. For a reactive hook-based API, use `usePermissions` instead.

```typescript
import { requestCameraAndMicPermissions } from '@hiyve/rn-react';

const granted = await requestCameraAndMicPermissions();
if (!granted) {
  Alert.alert('Permissions required', 'Camera and microphone access are needed.');
}
```

| Returns | Description |
|---------|-------------|
| `Promise<boolean>` | `true` if both camera and microphone permissions are granted |

## Customization

All components accept `colors`, `labels`, and `icons` props. Pass partial objects to override only what you need:

```tsx
<ControlBar
  colors={{
    background: '#1e1e3f',
    leaveBackground: '#dc2626',
  }}
  labels={{
    mute: 'Stummschalten',
    unmute: 'Mikrofon ein',
    leave: 'Verlassen',
  }}
  icons={{
    mic: <Icon name="microphone" size={24} color="#fff" />,
    micOff: <Icon name="microphone-off" size={24} color="#fff" />,
  }}
  // ... other props
/>
```

## Requirements

- React 18+
- React Native 0.70+
- `react-native-webrtc` >= 118.0.0
- `react-native-safe-area-context` >= 4.0.0
- `@react-native-vector-icons/material-design-icons` >= 12.0.0
- `@hiyve/rn-core`
- `@hiyve/rtc-client-rn`
