import { MaterialCommunityIcons } from "@expo/vector-icons"; import { router, useLocalSearchParams } from "expo-router"; import { useCallback, useEffect, useRef, useState } from "react"; import { Alert, Text, TouchableOpacity, View } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; import { RTCView } from "react-native-webrtc"; import { fetchAPI } from "@/lib/fetch"; import { useT } from "@/lib/i18n"; import { useCall } from "@/lib/use-call"; import type { ChatActiveRide } from "@/types/type"; // In-app WebRTC audio call screen. Two entry modes: // mode=start — caller opened this from the chat header; we place the call. // mode=incoming — CallWatcher detected a ringing call; we attach and wait // for the user to Accept/Decline. // Either way the authoritative ride/role/peer come from GET /(api)/chat/active // (so a stale nav param never dials the wrong ride). const Call = () => { const t = useT(); const params = useLocalSearchParams<{ rideId?: string; role?: "rider" | "driver"; mode?: "start" | "incoming"; }>(); const [active, setActive] = useState(null); const [resolving, setResolving] = useState(true); const call = useCall(); const startedRef = useRef(false); // Resolve the active ride + peer once, then kick off the right flow. useEffect(() => { let cancelled = false; (async () => { try { const res = await fetchAPI("/(api)/chat/active"); const a = (res.data ?? null) as ChatActiveRide | null; if (cancelled) return; setActive(a); if (!a) return; if (startedRef.current) return; startedRef.current = true; const peerName = a.peer?.name ?? ""; if (params.mode === "start") { void call.startCall(a.ride_id, a.role, peerName); } else { call.watch(a.ride_id, a.role, peerName); } } catch (err) { console.log("[CALL_SCREEN_RESOLVE]: ", err); } finally { if (!cancelled) setResolving(false); } })(); return () => { cancelled = true; }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Surface a mic-permission denial and back out. useEffect(() => { if (call.micError) { Alert.alert(t("call.micDeniedTitle"), t("call.micDeniedBody"), [ { text: "OK", onPress: () => router.back() }, ]); } }, [call.micError, t]); // When the call reaches a terminal state, show the label briefly, then // leave the screen so the user returns to where they came from. useEffect(() => { if (call.status !== "ended") return; const timer = setTimeout(() => router.back(), 1200); return () => clearTimeout(timer); }, [call.status]); const peerName = active?.peer?.name ?? call.peerName ?? ""; const handleEnd = useCallback(() => { void call.endCall(); }, [call]); const handleAccept = useCallback(() => { void call.answerCall(); }, [call]); const handleDecline = useCallback(() => { void call.declineCall(); }, [call]); if (resolving) { return ( {t("call.connecting")} ); } if (!active) { return ( {t("call.unavailable")} router.back()} className="mt-6 px-6 py-3 rounded-full bg-general-400" > {t("call.cancel")} ); } return ( {/* Audio sink — hidden; keeps the native audio pipeline attached even though this is an audio-only call (RTCView is the stream sink). */} {call.remoteStream ? ( ) : null} {/* Peer identity + status */} {(peerName.trim()[0] ?? "?").toUpperCase()} {peerName} {call.status === "incoming" ? t("call.incoming") : call.status === "outgoing" || call.status === "connecting" ? t("call.connectingWith", { name: peerName }) : call.status === "in-call" ? t("call.inCall") : call.status === "ended" ? t("call.ended") : t("call.connecting")} {/* Controls vary by state */} {call.status === "incoming" ? ( <> ) : ( <> )} ); }; const CallButton = ({ icon, color, label, onPress, }: { icon: React.ComponentProps["name"]; color: string; label: string; onPress: () => void; }) => ( {label} ); export default Call;