React Integration

Build React applications with the Talker SDK using hooks and components.

useTalker Hook

A complete React hook for managing Talker SDK state:

hooks/useTalker.ts
import { useEffect, useState, useCallback, useRef } from 'react';
import { TalkerClient, LogLevel } from '@talker-network/talker-sdk';
import type {
  ConnectionStatus,
  BroadcastStartEvent,
  Channel,
} from '@talker-network/talker-sdk';

interface Credentials {
  userAuthToken: string;
  userId: string;
  a_username: string;
  a_password: string;
}

interface UseTalkerReturn {
  connectionStatus: ConnectionStatus;
  isConnected: boolean;
  isTalking: boolean;
  activeBroadcast: BroadcastStartEvent | null;
  channels: Channel[];
  startTalking: (channelId: string) => Promise;
  stopTalking: () => Promise;
  loadChannels: (workspaceId?: string) => Promise;
}

export function useTalker(credentials: Credentials | null): UseTalkerReturn {
  const [connectionStatus, setConnectionStatus] = useState('disconnected');
  const [isTalking, setIsTalking] = useState(false);
  const [activeBroadcast, setActiveBroadcast] = useState(null);
  const [channels, setChannels] = useState([]);
  const clientRef = useRef(null);

  // Initialize client when credentials are available
  useEffect(() => {
    if (!credentials) return;

    const client = new TalkerClient({
      userAuthToken: credentials.userAuthToken,
      userId: credentials.userId,
      a_username: credentials.a_username,
      a_password: credentials.a_password,
      loggerConfig: {
        level: LogLevel.DEBUG,
        enableConsole: true,
      },
    });

    clientRef.current = client;

    // Connection events
    client.on('connection_change', ({ status }) => {
      setConnectionStatus(status);
    });

    // Broadcast events
    client.on('broadcast_start', (data) => {
      setActiveBroadcast(data);
    });

    client.on('broadcast_end', () => {
      setActiveBroadcast(null);
    });

    // Cleanup on unmount
    return () => {
      client.disconnect();
      clientRef.current = null;
    };
  }, [credentials]);

  const startTalking = useCallback(async (channelId: string) => {
    if (!clientRef.current) return;

    const result = await clientRef.current.startTalking(channelId);
    if (result.success) {
      setIsTalking(true);
    }
  }, []);

  const stopTalking = useCallback(async () => {
    if (!clientRef.current) return;

    await clientRef.current.stopTalking();
    setIsTalking(false);
  }, []);

  const loadChannels = useCallback(async (workspaceId?: string) => {
    if (!clientRef.current) return;

    const channelList = await clientRef.current.getChannels(workspaceId);
    setChannels(channelList);
  }, []);

  return {
    connectionStatus,
    isConnected: connectionStatus === 'connected',
    isTalking,
    activeBroadcast,
    channels,
    startTalking,
    stopTalking,
    loadChannels,
  };
}

Using the Hook

App.tsx
import { useState, useEffect } from 'react';
import { useTalker } from './hooks/useTalker';

function App() {
  const [credentials, setCredentials] = useState(null);

  // Fetch credentials from your backend
  useEffect(() => {
    fetch('/api/auth', { method: 'POST' })
      .then(res => res.json())
      .then(setCredentials);
  }, []);

  const {
    connectionStatus,
    isConnected,
    isTalking,
    activeBroadcast,
    startTalking,
    stopTalking,
  } = useTalker(credentials);

  return (
    <div className="app">
      <div className={`status ${connectionStatus}`}>
        {connectionStatus}
      </div>

      {activeBroadcast && (
        <div className="broadcast-indicator">
          {activeBroadcast.senderName} is speaking
        </div>
      )}

      <button
        className={`ptt-button ${isTalking ? 'talking' : ''}`}
        onMouseDown={() => startTalking('channel-id')}
        onMouseUp={stopTalking}
        onMouseLeave={stopTalking}
        disabled={!isConnected}
      >
        {isTalking ? 'Release to Stop' : 'Hold to Talk'}
      </button>
    </div>
  );
}

PTT Button Component

components/PTTButton.tsx
import { useCallback, useRef } from 'react';

interface PTTButtonProps {
  channelId: string;
  onStartTalking: (channelId: string) => Promise;
  onStopTalking: () => Promise;
  isTalking: boolean;
  isConnected: boolean;
  disabled?: boolean;
}

export function PTTButton({
  channelId,
  onStartTalking,
  onStopTalking,
  isTalking,
  isConnected,
  disabled = false,
}: PTTButtonProps) {
  const isPressed = useRef(false);

  const handleStart = useCallback(async () => {
    if (isPressed.current || disabled || !isConnected) return;
    isPressed.current = true;
    await onStartTalking(channelId);
  }, [channelId, onStartTalking, disabled, isConnected]);

  const handleStop = useCallback(async () => {
    if (!isPressed.current) return;
    isPressed.current = false;
    await onStopTalking();
  }, [onStopTalking]);

  return (
    <button
      className={`ptt-button ${isTalking ? 'talking' : ''} ${!isConnected ? 'disconnected' : ''}`}
      onMouseDown={handleStart}
      onMouseUp={handleStop}
      onMouseLeave={handleStop}
      onTouchStart={(e) => {
        e.preventDefault();
        handleStart();
      }}
      onTouchEnd={(e) => {
        e.preventDefault();
        handleStop();
      }}
      onTouchCancel={handleStop}
      disabled={disabled || !isConnected}
    >
      {!isConnected ? 'Connecting...' : isTalking ? 'Speaking...' : 'Push to Talk'}
    </button>
  );
}

Channel List Component

components/ChannelList.tsx
import type { Channel } from '@talker-network/talker-sdk';

interface ChannelListProps {
  channels: Channel[];
  selectedChannel: string | null;
  onSelectChannel: (channelId: string) => void;
}

export function ChannelList({
  channels,
  selectedChannel,
  onSelectChannel,
}: ChannelListProps) {
  return (
    <div className="channel-list">
      <h3>Channels</h3>
      {channels.map(channel => (
        <button
          key={channel.channelId}
          className={`channel-item ${channel.channelId === selectedChannel ? 'selected' : ''}`}
          onClick={() => onSelectChannel(channel.channelId)}
        >
          <span className="channel-name">{channel.channelName}</span>
          <span className="channel-type">{channel.channelType}</span>
        </button>
      ))}
    </div>
  );
}

Broadcast Indicator Component

components/BroadcastIndicator.tsx
import type { BroadcastStartEvent } from '@talker-network/talker-sdk';

interface BroadcastIndicatorProps {
  broadcast: BroadcastStartEvent | null;
}

export function BroadcastIndicator({ broadcast }: BroadcastIndicatorProps) {
  if (!broadcast) return null;

  return (
    <div className="broadcast-indicator">
      <div className="pulse"></div>
      <span className="sender-name">{broadcast.senderName}</span>
      <span className="speaking-text">is speaking</span>
    </div>
  );
}

Connection Status Component

components/ConnectionStatus.tsx
import type { ConnectionStatus } from '@talker-network/talker-sdk';

interface ConnectionStatusProps {
  status: ConnectionStatus;
}

export function ConnectionStatusBadge({ status }: ConnectionStatusProps) {
  const statusConfig = {
    connected: { color: '#10b981', label: 'Connected' },
    connecting: { color: '#f59e0b', label: 'Connecting...' },
    disconnected: { color: '#ef4444', label: 'Disconnected' },
  };

  const config = statusConfig[status];

  return (
    <div
      className="connection-status"
      style={{ backgroundColor: config.color }}
    >
      {config.label}
    </div>
  );
}

useAudioLevel Hook

hooks/useAudioLevel.ts
import { useEffect, useState, useRef } from 'react';
import type { TalkerClient } from '@talker-network/talker-sdk';

export function useAudioLevel(
  client: TalkerClient | null,
  isActive: boolean
): number {
  const [level, setLevel] = useState(0);
  const frameRef = useRef();

  useEffect(() => {
    if (!client || !isActive) {
      setLevel(0);
      return;
    }

    function update() {
      setLevel(client!.getAudioLevel());
      frameRef.current = requestAnimationFrame(update);
    }

    update();

    return () => {
      if (frameRef.current) {
        cancelAnimationFrame(frameRef.current);
      }
    };
  }, [client, isActive]);

  return level;
}

Audio Meter Component

components/AudioMeter.tsx
interface AudioMeterProps {
  level: number; // 0-1
}

export function AudioMeter({ level }: AudioMeterProps) {
  const percentage = Math.round(level * 100);

  // Color based on level
  let color = '#64748b'; // Gray
  if (level > 0.8) {
    color = '#ef4444'; // Red - too loud
  } else if (level > 0.1) {
    color = '#10b981'; // Green - good
  }

  return (
    <div className="audio-meter">
      <div
        className="audio-meter-fill"
        style={{
          width: `${percentage}%`,
          backgroundColor: color,
        }}
      />
    </div>
  );
}

Complete React App Example

App.tsx
import { useState, useEffect } from 'react';
import { useTalker } from './hooks/useTalker';
import { PTTButton } from './components/PTTButton';
import { ChannelList } from './components/ChannelList';
import { BroadcastIndicator } from './components/BroadcastIndicator';
import { ConnectionStatusBadge } from './components/ConnectionStatus';
import './App.css';

function App() {
  const [credentials, setCredentials] = useState(null);
  const [selectedChannel, setSelectedChannel] = useState(null);

  // Fetch credentials on mount
  useEffect(() => {
    async function authenticate() {
      const res = await fetch('/api/auth', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ name: 'User' }),
      });
      const creds = await res.json();
      setCredentials(creds);
    }

    authenticate();
  }, []);

  const {
    connectionStatus,
    isConnected,
    isTalking,
    activeBroadcast,
    channels,
    startTalking,
    stopTalking,
    loadChannels,
  } = useTalker(credentials);

  // Load channels when connected
  useEffect(() => {
    if (isConnected) {
      loadChannels();
    }
  }, [isConnected, loadChannels]);

  // Select first channel by default
  useEffect(() => {
    if (channels.length > 0 && !selectedChannel) {
      setSelectedChannel(channels[0].channelId);
    }
  }, [channels, selectedChannel]);

  return (
    <div className="app">
      <header>
        <h1>Talker Demo</h1>
        <ConnectionStatusBadge status={connectionStatus} />
      </header>

      <main>
        <aside>
          <ChannelList
            channels={channels}
            selectedChannel={selectedChannel}
            onSelectChannel={setSelectedChannel}
          />
        </aside>

        <section className="chat-area">
          <BroadcastIndicator broadcast={activeBroadcast} />

          <div className="ptt-container">
            {selectedChannel && (
              <PTTButton
                channelId={selectedChannel}
                onStartTalking={startTalking}
                onStopTalking={stopTalking}
                isTalking={isTalking}
                isConnected={isConnected}
              />
            )}
          </div>
        </section>
      </main>
    </div>
  );
}

export default App;

CSS Styles

App.css
.app {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 16px 24px;
  border-bottom: 1px solid #e2e8f0;
}

main {
  display: flex;
  flex: 1;
}

aside {
  width: 280px;
  border-right: 1px solid #e2e8f0;
  padding: 16px;
}

.chat-area {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 24px;
}

.ptt-button {
  width: 200px;
  height: 200px;
  border-radius: 50%;
  border: none;
  background: #3b82f6;
  color: white;
  font-size: 18px;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.2s;
}

.ptt-button:hover {
  background: #2563eb;
}

.ptt-button.talking {
  background: #ef4444;
  transform: scale(1.1);
}

.ptt-button.disconnected {
  background: #94a3b8;
  cursor: not-allowed;
}

.broadcast-indicator {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 24px;
  background: #f0fdf4;
  border-radius: 24px;
  margin-bottom: 24px;
}

.broadcast-indicator .pulse {
  width: 12px;
  height: 12px;
  background: #10b981;
  border-radius: 50%;
  animation: pulse 1s infinite;
}

@keyframes pulse {
  0%, 100% { opacity: 1; transform: scale(1); }
  50% { opacity: 0.5; transform: scale(1.2); }
}

.connection-status {
  padding: 6px 12px;
  border-radius: 16px;
  color: white;
  font-size: 12px;
  font-weight: 500;
}

.channel-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.channel-item {
  padding: 12px;
  border: 1px solid #e2e8f0;
  border-radius: 8px;
  background: white;
  cursor: pointer;
  text-align: left;
}

.channel-item.selected {
  border-color: #3b82f6;
  background: #eff6ff;
}

.audio-meter {
  width: 100%;
  height: 8px;
  background: #e2e8f0;
  border-radius: 4px;
  overflow: hidden;
}

.audio-meter-fill {
  height: 100%;
  transition: width 0.1s;
}