diff --git a/app/phone-calls/hooks/use-device.tsx b/app/phone-calls/hooks/use-device.tsx new file mode 100644 index 0000000..84c1b7c --- /dev/null +++ b/app/phone-calls/hooks/use-device.tsx @@ -0,0 +1,66 @@ +import { useEffect } from "react"; +import { useMutation } from "blitz"; +import type { TwilioError } from "@twilio/voice-sdk"; +import { Call, Device } from "@twilio/voice-sdk"; +import { atom, useAtom } from "jotai"; + +import getToken from "../mutations/get-token"; + +export default function useDevice() { + const [device, setDevice] = useAtom(deviceAtom); + const [getTokenMutation] = useMutation(getToken); + + useEffect(() => { + (async () => { + const token = await getTokenMutation(); + const device = new Device(token, { + codecPreferences: [Call.Codec.Opus, Call.Codec.PCMU], + sounds: { + [Device.SoundName.Disconnect]: undefined, // TODO + }, + }); + device.register(); + setDevice(device); + })(); + }, [getTokenMutation]); + + useEffect(() => { + if (!device) { + return; + } + + (window as any).device = device; + device.on("error", onDeviceError); + device.on("incoming", onDeviceIncoming); + + return () => { + device.off("error", onDeviceError); + device.off("incoming", onDeviceIncoming); + }; + }, [device]); + + return device; + + function onDeviceError(error: TwilioError.TwilioError, call?: Call) { + // TODO gracefully handle errors: possibly hang up the call, redirect to keypad + console.error("device error", error); + alert(error); + } + + function onDeviceIncoming(call: Call) { + // TODO show alert to accept/reject the incoming call /!\ it should persist between screens /!\ prevent making a new call when there is a pending incoming call + console.log("call", call); + console.log("Incoming connection from " + call.parameters.From); + let archEnemyPhoneNumber = "+12093373517"; + + if (call.parameters.From === archEnemyPhoneNumber) { + call.reject(); + console.log("It's your nemesis. Rejected call."); + } else { + // accept the incoming connection and start two-way audio + call.accept(); + } + } +} + +const deviceAtom = atom(null); diff --git a/app/phone-calls/hooks/use-make-call.ts b/app/phone-calls/hooks/use-make-call.ts index ae31f97..b6b188d 100644 --- a/app/phone-calls/hooks/use-make-call.ts +++ b/app/phone-calls/hooks/use-make-call.ts @@ -1,8 +1,8 @@ -import { useEffect, useState } from "react"; -import { useMutation } from "blitz"; -import { Call, Device, TwilioError } from "@twilio/voice-sdk"; +import { useState } from "react"; +import { useRouter, Routes } from "blitz"; +import { Call } from "@twilio/voice-sdk"; -import getToken from "../mutations/get-token"; +import useDevice from "./use-device"; type Params = { recipient: string; @@ -11,39 +11,9 @@ type Params = { export default function useMakeCall({ recipient, onHangUp }: Params) { const [outgoingConnection, setOutgoingConnection] = useState(null); - const [device, setDevice] = useState(null); - const [getTokenMutation] = useMutation(getToken); const [state, setState] = useState("initial"); - - useEffect(() => { - (async () => { - const token = await getTokenMutation(); - const device = new Device(token, { - codecPreferences: [Call.Codec.Opus, Call.Codec.PCMU], - sounds: { - [Device.SoundName.Disconnect]: undefined, // TODO - }, - }); - device.register(); - setDevice(device); - })(); - }, [getTokenMutation]); - - useEffect(() => { - if (!device) { - return; - } - - device.on("error", onDeviceError); - device.on("registered", onDeviceRegistered); - device.on("incoming", onDeviceIncoming); - - return () => { - device.off("error", onDeviceError); - device.off("registered", onDeviceRegistered); - device.off("incoming", onDeviceIncoming); - }; - }, [device]); + const device = useDevice(); + const router = useRouter(); return { makeCall, @@ -58,8 +28,7 @@ export default function useMakeCall({ recipient, onHangUp }: Params) { return; } - if (state !== "ready") { - console.error("not a good time", state); + if (state !== "initial") { return; } @@ -74,48 +43,34 @@ export default function useMakeCall({ recipient, onHangUp }: Params) { // TODO: setState("call_in_progress"); // TODO: remove event listeners - outgoingConnection.on("cancel", () => setState("call_ended")); - outgoingConnection.on("disconnect", () => setState("call_ending")); + outgoingConnection.on("cancel", endCall); + outgoingConnection.on("disconnect", endCall); outgoingConnection.on("error", (error) => { console.error("call error", error); alert(error); }); } + function endCall() { + setState("call_ending"); + setTimeout(() => { + setState("call_ended"); + setTimeout(() => router.replace(Routes.KeypadPage()), 100); + }, 150); + } + function sendDigits(digits: string) { return outgoingConnection?.sendDigits(digits); } function hangUp() { setState("call_ending"); - outgoingConnection?.reject(); + outgoingConnection?.disconnect(); device?.disconnectAll(); - device?.destroy(); onHangUp?.(); - } - - function onDeviceError(error: TwilioError.TwilioError, call?: Call) { - console.error("device error", error); - alert(error); - } - - function onDeviceRegistered() { - setState("ready"); - } - - function onDeviceIncoming(call: Call) { - // TODO - console.log("call", call); - console.log("Incoming connection from " + call.parameters.From); - let archEnemyPhoneNumber = "+12093373517"; - - if (call.parameters.From === archEnemyPhoneNumber) { - call.reject(); - console.log("It's your nemesis. Rejected call."); - } else { - // accept the incoming connection and start two-way audio - call.accept(); - } + router.replace(Routes.KeypadPage()); + outgoingConnection?.off("cancel", endCall); + outgoingConnection?.off("disconnect", endCall); } } diff --git a/app/phone-calls/pages/outgoing-call/[recipient].tsx b/app/phone-calls/pages/outgoing-call/[recipient].tsx index c3b620e..3b08f07 100644 --- a/app/phone-calls/pages/outgoing-call/[recipient].tsx +++ b/app/phone-calls/pages/outgoing-call/[recipient].tsx @@ -6,23 +6,18 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faPhoneAlt as faPhone } from "@fortawesome/pro-solid-svg-icons"; import useRequireOnboarding from "../../../core/hooks/use-require-onboarding"; -import Keypad from "../../components/keypad"; import useMakeCall from "../../hooks/use-make-call"; +import useDevice from "../../hooks/use-device"; + +import Keypad from "../../components/keypad"; const OutgoingCall: BlitzPage = () => { useRequireOnboarding(); const [phoneNumber, setPhoneNumber] = useAtom(phoneNumberAtom); const router = useRouter(); + const device = useDevice(); const recipient = decodeURIComponent(router.params.recipient); - const onHangUp = useMemo( - () => () => { - setPhoneNumber(""); - - // return router.replace(Routes.KeypadPage()); - return router.push(Routes.KeypadPage()); - }, - [router, setPhoneNumber], - ); + const onHangUp = useMemo(() => () => setPhoneNumber(""), [setPhoneNumber]); const call = useMakeCall({ recipient, onHangUp }); const pressDigit = useAtom(pressDigitAtom)[1]; const onDigitPressProps = useMemo( @@ -37,12 +32,10 @@ const OutgoingCall: BlitzPage = () => { ); useEffect(() => { - console.log("call.state", call.state); - if (call.state === "ready") { + if (device !== null) { call.makeCall(); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [call.state]); + }, [device]); return (