2025-01-03 19:35:20 +01:00
|
|
|
import React, { useEffect, useRef } from 'react';
|
2024-12-21 14:36:01 +01:00
|
|
|
import { useRecoilState } from 'recoil';
|
2025-01-03 19:35:20 +01:00
|
|
|
import {
|
|
|
|
|
Phone,
|
|
|
|
|
PhoneOff,
|
|
|
|
|
AlertCircle,
|
|
|
|
|
Mic,
|
|
|
|
|
MicOff,
|
|
|
|
|
Volume2,
|
|
|
|
|
VolumeX,
|
|
|
|
|
Activity,
|
|
|
|
|
} from 'lucide-react';
|
2024-12-21 14:36:01 +01:00
|
|
|
import { OGDialog, OGDialogContent, Button } from '~/components';
|
2024-12-30 01:47:21 +01:00
|
|
|
import { useWebSocket, useCall } from '~/hooks';
|
2025-01-03 19:35:20 +01:00
|
|
|
import { CallState } from '~/common';
|
2024-12-21 14:36:01 +01:00
|
|
|
import store from '~/store';
|
|
|
|
|
|
|
|
|
|
export const Call: React.FC = () => {
|
2025-01-03 19:35:20 +01:00
|
|
|
const { isConnected } = useWebSocket();
|
|
|
|
|
const {
|
|
|
|
|
callState,
|
|
|
|
|
error,
|
|
|
|
|
startCall,
|
|
|
|
|
hangUp,
|
|
|
|
|
isConnecting,
|
|
|
|
|
localStream,
|
|
|
|
|
remoteStream,
|
|
|
|
|
connectionQuality,
|
|
|
|
|
} = useCall();
|
2024-12-21 14:36:01 +01:00
|
|
|
|
|
|
|
|
const [open, setOpen] = useRecoilState(store.callDialogOpen(0));
|
2024-12-30 01:47:21 +01:00
|
|
|
const [eventLog, setEventLog] = React.useState<string[]>([]);
|
2025-01-03 19:35:20 +01:00
|
|
|
const [isMuted, setIsMuted] = React.useState(false);
|
|
|
|
|
const [isAudioEnabled, setIsAudioEnabled] = React.useState(true);
|
|
|
|
|
|
|
|
|
|
const remoteAudioRef = useRef<HTMLAudioElement>(null);
|
2024-12-30 01:47:21 +01:00
|
|
|
|
|
|
|
|
const logEvent = (message: string) => {
|
|
|
|
|
console.log(message);
|
2025-01-03 19:35:20 +01:00
|
|
|
setEventLog((prev) => [...prev, `${new Date().toISOString()}: ${message}`]);
|
2024-12-30 01:47:21 +01:00
|
|
|
};
|
|
|
|
|
|
2025-01-03 19:35:20 +01:00
|
|
|
useEffect(() => {
|
|
|
|
|
if (remoteAudioRef.current && remoteStream) {
|
|
|
|
|
remoteAudioRef.current.srcObject = remoteStream;
|
|
|
|
|
}
|
|
|
|
|
}, [remoteStream]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (localStream) {
|
|
|
|
|
localStream.getAudioTracks().forEach((track) => {
|
|
|
|
|
track.enabled = !isMuted;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}, [localStream, isMuted]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
2024-12-30 01:47:21 +01:00
|
|
|
if (isConnected) {
|
|
|
|
|
logEvent('Connected to server.');
|
|
|
|
|
} else {
|
|
|
|
|
logEvent('Disconnected from server.');
|
|
|
|
|
}
|
|
|
|
|
}, [isConnected]);
|
|
|
|
|
|
2025-01-03 19:35:20 +01:00
|
|
|
useEffect(() => {
|
|
|
|
|
if (error) {
|
|
|
|
|
logEvent(`Error: ${error.message} (${error.code})`);
|
2024-12-30 01:47:21 +01:00
|
|
|
}
|
2025-01-03 19:35:20 +01:00
|
|
|
}, [error]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
logEvent(`Call state changed to: ${callState}`);
|
|
|
|
|
}, [callState]);
|
2024-12-30 01:47:21 +01:00
|
|
|
|
|
|
|
|
const handleStartCall = () => {
|
|
|
|
|
logEvent('Attempting to start call...');
|
|
|
|
|
startCall();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleHangUp = () => {
|
|
|
|
|
logEvent('Attempting to hang up call...');
|
|
|
|
|
hangUp();
|
|
|
|
|
};
|
|
|
|
|
|
2025-01-03 19:35:20 +01:00
|
|
|
const toggleMute = () => {
|
|
|
|
|
setIsMuted((prev) => !prev);
|
|
|
|
|
logEvent(`Microphone ${isMuted ? 'unmuted' : 'muted'}`);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const toggleAudio = () => {
|
|
|
|
|
setIsAudioEnabled((prev) => !prev);
|
|
|
|
|
if (remoteAudioRef.current) {
|
|
|
|
|
remoteAudioRef.current.muted = !isAudioEnabled;
|
|
|
|
|
}
|
|
|
|
|
logEvent(`Speaker ${isAudioEnabled ? 'disabled' : 'enabled'}`);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const isActive = callState === CallState.ACTIVE;
|
|
|
|
|
const isError = callState === CallState.ERROR;
|
|
|
|
|
|
2024-12-21 14:36:01 +01:00
|
|
|
return (
|
|
|
|
|
<OGDialog open={open} onOpenChange={setOpen}>
|
2025-01-03 19:35:20 +01:00
|
|
|
<OGDialogContent className="w-[28rem] p-8">
|
2024-12-21 14:36:01 +01:00
|
|
|
<div className="flex flex-col items-center gap-6">
|
2025-01-03 19:35:20 +01:00
|
|
|
{/* Connection Status */}
|
|
|
|
|
<div className="flex w-full items-center justify-between">
|
2024-12-21 14:36:01 +01:00
|
|
|
<div
|
2025-01-03 19:35:20 +01:00
|
|
|
className={`flex items-center gap-2 rounded-full px-4 py-2 ${
|
|
|
|
|
isConnected ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
className={`h-2 w-2 rounded-full ${isConnected ? 'bg-green-500' : 'bg-red-500'}`}
|
|
|
|
|
/>
|
|
|
|
|
<span className="text-sm font-medium">
|
|
|
|
|
{isConnected ? 'Connected' : 'Disconnected'}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{isActive && (
|
|
|
|
|
<div
|
|
|
|
|
className={`flex items-center gap-2 rounded-full px-4 py-2 ${
|
|
|
|
|
(connectionQuality === 'good' && 'bg-green-100 text-green-700') ||
|
|
|
|
|
(connectionQuality === 'poor' && 'bg-yellow-100 text-yellow-700') ||
|
|
|
|
|
'bg-gray-100 text-gray-700'
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
<Activity size={16} />
|
|
|
|
|
<span className="text-sm font-medium capitalize">{connectionQuality} Quality</span>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2024-12-21 14:36:01 +01:00
|
|
|
</div>
|
|
|
|
|
|
2025-01-03 19:35:20 +01:00
|
|
|
{/* Error Display */}
|
|
|
|
|
{error && (
|
|
|
|
|
<div className="flex w-full items-center gap-2 rounded-md bg-red-100 p-3 text-red-700">
|
|
|
|
|
<AlertCircle size={16} />
|
|
|
|
|
<span className="text-sm">{error.message}</span>
|
|
|
|
|
</div>
|
2024-12-21 14:36:01 +01:00
|
|
|
)}
|
2024-12-30 01:47:21 +01:00
|
|
|
|
2025-01-03 19:35:20 +01:00
|
|
|
{/* Call Controls */}
|
|
|
|
|
<div className="flex items-center gap-4">
|
|
|
|
|
{isActive && (
|
|
|
|
|
<>
|
|
|
|
|
<Button
|
|
|
|
|
onClick={toggleMute}
|
|
|
|
|
className={`rounded-full p-3 ${
|
|
|
|
|
isMuted ? 'bg-red-100 text-red-700' : 'bg-gray-100 text-gray-700'
|
|
|
|
|
}`}
|
|
|
|
|
title={isMuted ? 'Unmute microphone' : 'Mute microphone'}
|
|
|
|
|
>
|
|
|
|
|
{isMuted ? <MicOff size={20} /> : <Mic size={20} />}
|
|
|
|
|
</Button>
|
|
|
|
|
|
|
|
|
|
<Button
|
|
|
|
|
onClick={toggleAudio}
|
|
|
|
|
className={`rounded-full p-3 ${
|
|
|
|
|
!isAudioEnabled ? 'bg-red-100 text-red-700' : 'bg-gray-100 text-gray-700'
|
|
|
|
|
}`}
|
|
|
|
|
title={isAudioEnabled ? 'Disable speaker' : 'Enable speaker'}
|
|
|
|
|
>
|
|
|
|
|
{isAudioEnabled ? <Volume2 size={20} /> : <VolumeX size={20} />}
|
|
|
|
|
</Button>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{isActive ? (
|
|
|
|
|
<Button
|
|
|
|
|
onClick={handleHangUp}
|
|
|
|
|
className="flex items-center gap-2 rounded-full bg-red-500 px-6 py-3 text-white hover:bg-red-600"
|
|
|
|
|
>
|
|
|
|
|
<PhoneOff size={20} />
|
|
|
|
|
<span>End Call</span>
|
|
|
|
|
</Button>
|
|
|
|
|
) : (
|
|
|
|
|
<Button
|
|
|
|
|
onClick={handleStartCall}
|
|
|
|
|
disabled={!isConnected || isError || isConnecting}
|
|
|
|
|
className="flex items-center gap-2 rounded-full bg-green-500 px-6 py-3 text-white hover:bg-green-600 disabled:opacity-50"
|
|
|
|
|
>
|
|
|
|
|
<Phone size={20} />
|
|
|
|
|
<span>{isConnecting ? 'Connecting...' : 'Start Call'}</span>
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Event Log */}
|
2024-12-30 01:47:21 +01:00
|
|
|
<div className="mt-4 w-full rounded-md bg-gray-100 p-4 shadow-sm">
|
|
|
|
|
<h3 className="mb-2 text-lg font-medium">Event Log</h3>
|
2025-01-03 19:35:20 +01:00
|
|
|
<div className="h-32 overflow-y-auto rounded-md bg-white p-2 shadow-inner">
|
|
|
|
|
<ul className="space-y-1 text-xs text-gray-600">
|
|
|
|
|
{eventLog.map((log, index) => (
|
|
|
|
|
<li key={index} className="font-mono">
|
|
|
|
|
{log}
|
|
|
|
|
</li>
|
|
|
|
|
))}
|
|
|
|
|
</ul>
|
|
|
|
|
</div>
|
2024-12-30 01:47:21 +01:00
|
|
|
</div>
|
2025-01-03 19:35:20 +01:00
|
|
|
|
|
|
|
|
{/* Hidden Audio Element */}
|
|
|
|
|
<audio ref={remoteAudioRef} autoPlay>
|
|
|
|
|
<track kind="captions" />
|
|
|
|
|
</audio>
|
2024-12-21 14:36:01 +01:00
|
|
|
</div>
|
|
|
|
|
</OGDialogContent>
|
|
|
|
|
</OGDialog>
|
|
|
|
|
);
|
|
|
|
|
};
|