This commit is contained in:
Quan HL
2023-10-02 11:47:18 +07:00
parent 66dca15a58
commit 62057eef1d
3 changed files with 525 additions and 0 deletions

View File

@@ -0,0 +1,45 @@
import { HStack, Icon, Spacer, Text, VStack } from "@chakra-ui/react";
import dayjs from "dayjs";
import { Phone, PhoneIncoming, PhoneOutgoing } from "react-feather";
import { CallHistory, SipCallDirection } from "src/common/types";
import { formatPhoneNumber } from "src/utils";
type CallHistoryItemProbs = {
call: CallHistory;
};
export const CallHistoryItem = ({ call }: CallHistoryItemProbs) => {
const getDirectionIcon = (direction: SipCallDirection) => {
if (direction === "outgoing") {
return PhoneOutgoing;
} else if (direction === "incoming") {
return PhoneIncoming;
} else {
return Phone;
}
};
return (
<HStack
spacing={5}
borderBottomWidth="1px"
borderBottomColor="gray.200"
p={2}
>
<Icon as={getDirectionIcon(call.direction)} w="20px" h="20px" />
<VStack align="start">
<Text fontSize="14px" fontWeight="500">
{formatPhoneNumber(call.number)}
</Text>
<Text fontSize="12px">{call.duration}</Text>
</VStack>
<Spacer />
<VStack align="start">
<Text fontSize="12px">
{dayjs(call.timeStamp).format("MMM D, hh:mm A")}
</Text>
</VStack>
</HStack>
);
};
export default CallHistoryItem;

View File

@@ -0,0 +1,70 @@
import {
VStack,
Grid,
HStack,
InputGroup,
Input,
InputLeftElement,
Icon,
Text,
Spacer,
UnorderedList,
} from "@chakra-ui/react";
import { useState } from "react";
import { Search, Sliders } from "react-feather";
import { CallHistory } from "src/common/types";
import CallHistoryItem from "./call-history-item";
type CallHistoriesProbs = {
calls: CallHistory[];
};
export const CallHistories = ({ calls }: CallHistoriesProbs) => {
const [searchText, setSearchText] = useState("");
return (
<VStack spacing={2}>
<Grid w="full" templateColumns="1fr auto" gap={5}>
<InputGroup size="md">
<Input
isDisabled={calls.length === 0}
pr="4.5rem"
placeholder="Type to search..."
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
bg="grey.100"
fontWeight="normal"
/>
<InputLeftElement mr={2}>
<Icon as={Search} w="20px" h="20px" />
</InputLeftElement>
</InputGroup>
<HStack spacing={2} bg="grey.100" p={2} borderRadius={7}>
<Icon as={Sliders} w="20px" h="20px" />
<Text fontSize="12px" fontWeight="500">
Filter
</Text>
</HStack>
</Grid>
{calls.length > 0 ? (
<UnorderedList
w="full"
spacing={2}
maxH="500px"
overflowY="auto"
mt={2}
>
{calls.map((c) => (
<CallHistoryItem call={c} />
))}
</UnorderedList>
) : (
<Text fontSize="24px" fontWeight="bold">
No Call History
</Text>
)}
</VStack>
);
};
export default CallHistories;

410
src/window/phone/index.tsx Normal file
View File

@@ -0,0 +1,410 @@
import {
Button,
Center,
Circle,
HStack,
Heading,
IconButton,
Image,
Input,
Spacer,
Text,
Tooltip,
VStack,
} from "@chakra-ui/react";
import { useEffect, useRef, useState } from "react";
import { Mic, MicOff, Pause, PhoneOff, Play } from "react-feather";
import {
Call,
Message,
MessageEvent,
SipCallDirection,
SipClientStatus,
} from "src/common/types";
import { SipConstants, SipUA } from "src/lib";
import IncommingCall from "./incomming-call";
import DialPad from "./dial-pad";
import {
formatPhoneNumber,
isSipClientAnswered,
isSipClientIdle,
isSipClientRinging,
} from "src/utils";
import Avatar from "src/imgs/icons/Avatar.svg";
import GreenAvatar from "src/imgs/icons/Avatar-Green.svg";
import "./styles.scss";
import { getCurrentCall, saveCallHistory, saveCurrentCall } from "src/storage";
import dayjs from "dayjs";
type PhoneProbs = {
sipDomain: string;
sipServerAddress: string;
sipUsername: string;
sipPassword: string;
sipDisplayName: string;
};
export const Phone = ({
sipDomain,
sipServerAddress,
sipUsername,
sipPassword,
sipDisplayName,
}: PhoneProbs) => {
const [inputNumber, setInputNumber] = useState("");
const inputNumberRef = useRef(inputNumber);
const [status, setStatus] = useState<SipClientStatus>("offline");
const [goOffline, setGoOffline] = useState(false);
const [isConfigured, setIsConfigured] = useState(false);
const [callStatus, setCallStatus] = useState(SipConstants.SESSION_ENDED);
const [sessionDirection, setSessionDirection] =
useState<SipCallDirection>("");
const sessionDirectionRef = useRef(sessionDirection);
const sipUA = useRef<SipUA | null>(null);
const timerRef = useRef<NodeJS.Timer | null>(null);
const [seconds, setSeconds] = useState(0);
const secondsRef = useRef(seconds);
useEffect(() => {
if (sipDomain && sipUsername && sipPassword) {
createSipClient();
setIsConfigured(true);
}
}, [sipDomain, sipUsername, sipPassword, sipServerAddress, sipDisplayName]);
useEffect(() => {
inputNumberRef.current = inputNumber;
sessionDirectionRef.current = sessionDirection;
secondsRef.current = seconds;
}, [inputNumber, seconds, sessionDirection]);
useEffect(() => {
chrome.runtime.onMessage.addListener(function (request) {
const msg = request as Message<any>;
switch (msg.event) {
case MessageEvent.Call:
handleCallEvent(msg.data as Call);
break;
default:
break;
}
});
}, []);
const handleCallEvent = (call: Call) => {
if (!call.number) return;
if (isSipClientIdle(callStatus)) {
setInputNumber(call.number);
sipUA.current?.call(call.number);
}
};
const startCallDurationCounter = () => {
stopCallDurationCounter();
timerRef.current = setInterval(() => {
setSeconds((seconds) => seconds + 1);
}, 1000);
};
const stopCallDurationCounter = () => {
if (timerRef.current) {
clearInterval(timerRef.current);
timerRef.current = null;
setSeconds(0);
}
};
const createSipClient = (forceOfflineMode = false) => {
if (goOffline && !forceOfflineMode) {
return;
}
clientGoOffline();
const client = {
username: `${sipUsername}@${sipDomain}`,
password: sipPassword,
name: sipDisplayName ?? sipUsername,
};
const settings = {
pcConfig: {
iceServers: [{ urls: ["stun:stun.l.google.com:19302"] }],
},
wsUri: sipServerAddress,
register: true,
};
const sipClient = new SipUA(client, settings);
// UA Status
sipClient.on(SipConstants.UA_REGISTERED, (args) => {
setStatus("online");
});
sipClient.on(SipConstants.UA_UNREGISTERED, (args) => {
setStatus("offline");
clientGoOffline();
});
// Call Status
sipClient.on(SipConstants.SESSION_RINGING, (args) => {
saveCurrentCall({
number: args.session.user,
direction: args.session.direction,
timeStamp: Date.now(),
duration: "0",
});
setCallStatus(SipConstants.SESSION_RINGING);
setSessionDirection(args.session.direction);
setInputNumber(args.session.user);
});
sipClient.on(SipConstants.SESSION_ANSWERED, (args) => {
setCallStatus(SipConstants.SESSION_ANSWERED);
startCallDurationCounter();
});
sipClient.on(SipConstants.SESSION_ENDED, (args) => {
addCallHistory();
setCallStatus(SipConstants.SESSION_ENDED);
setSessionDirection("");
stopCallDurationCounter();
});
sipClient.on(SipConstants.SESSION_FAILED, (args) => {
addCallHistory();
setCallStatus(SipConstants.SESSION_FAILED);
setSessionDirection("");
stopCallDurationCounter();
});
sipClient.start();
sipUA.current = sipClient;
};
const addCallHistory = () => {
const call = getCurrentCall();
if (call) {
saveCallHistory(sipUsername, {
number: call.number,
direction: call.direction,
duration: new Date((Date.now() - call.timeStamp) / 1000)
.toISOString()
.substr(11, 8),
timeStamp: call.timeStamp,
});
}
};
const handleDialPadClick = (value: string) => {
if (isSipClientIdle(callStatus)) {
setInputNumber((prev) => prev + value);
} else if (isSipClientAnswered(callStatus)) {
sipUA.current?.dtmf(value, undefined);
}
};
const handleCallButtion = () => {
if (sipUA.current) {
sipUA.current.call(inputNumber);
}
};
const clientGoOffline = () => {
if (sipUA.current) {
sipUA.current.stop();
sipUA.current = null;
}
};
const handleGoOffline = () => {
const newVal = !goOffline;
setGoOffline(newVal);
if (newVal) {
clientGoOffline();
} else {
createSipClient(true);
}
};
const isOnline = () => {
return status === "online";
};
const handleHangup = () => {
if (isSipClientAnswered(callStatus) || isSipClientRinging(callStatus)) {
sipUA.current?.terminate(480, "Call Finished", undefined);
}
};
const handleCallOnHold = () => {
if (isSipClientAnswered(callStatus)) {
if (sipUA.current?.isHolded(undefined)) {
sipUA.current?.unhold(undefined);
} else {
sipUA.current?.hold(undefined);
}
}
};
const handleCallMute = () => {
if (isSipClientAnswered(callStatus)) {
if (sipUA.current?.isMuted(undefined)) {
sipUA.current?.unmute(undefined);
} else {
sipUA.current?.mute(undefined);
}
}
};
const handleAnswer = () => {
if (isSipClientRinging(callStatus)) {
sipUA.current?.answer(undefined);
}
};
const handleDecline = () => {
if (isSipClientRinging(callStatus)) {
sipUA.current?.terminate(486, "Busy here", undefined);
}
};
return (
<Center flexDirection="column">
{isConfigured ? (
<>
<HStack spacing={2} boxShadow="md" w="full" p={2} borderRadius={5}>
<Image src={isOnline() ? GreenAvatar : Avatar} />
<VStack spacing={2} alignItems="start">
<HStack spacing={2}>
<Text fontWeight="bold" fontSize="13px">
{sipDisplayName ?? sipUsername}
</Text>
<Circle size="8px" bg={isOnline() ? "green.500" : "gray.500"} />
<Text fontSize="13px">{isOnline() ? "Online" : "Offline"}</Text>
</HStack>
<Text fontWeight="bold" w="full">
{`${sipUsername}@${sipDomain}`}
</Text>
</VStack>
{isSipClientIdle(callStatus) && (
<>
<Spacer />
<Button
variant={isOnline() ? "outline" : "solid"}
borderRadius="50px 50px 50px 50px"
colorScheme={isOnline() ? "gray" : "jambonz"}
onClick={handleGoOffline}
>
{isOnline() ? "GO OFFLINE" : "GO ONLINE"}
</Button>
</>
)}
</HStack>
</>
) : (
<Heading size="md" mb={2}>
Go to Settings to configure your account
</Heading>
)}
{isSipClientRinging(callStatus) && sessionDirection === "incoming" ? (
<IncommingCall
number={inputNumber}
answer={handleAnswer}
decline={handleDecline}
/>
) : (
<VStack
spacing={4}
w="full"
mt={5}
className={isOnline() ? "" : "blurred"}
>
{isSipClientIdle(callStatus) ? (
<Input
value={inputNumber}
bg="grey.500"
fontWeight="bold"
fontSize="24px"
onChange={(e) => setInputNumber(e.target.value)}
textAlign="center"
/>
) : (
<VStack>
<Text fontSize="22px" fontWeight="bold">
{formatPhoneNumber(inputNumber)}
</Text>
{seconds > 0 && (
<Text fontSize="15px">
{new Date(seconds * 1000).toISOString().substr(11, 8)}
</Text>
)}
</VStack>
)}
<DialPad handleDigitPress={handleDialPadClick} />
{isSipClientIdle(callStatus) ? (
<Button
w="full"
onClick={handleCallButtion}
isDisabled={status === "offline"}
colorScheme="jambonz"
alignContent="center"
>
Call
</Button>
) : (
<HStack w="full">
<Tooltip
label={sipUA.current?.isHolded(undefined) ? "UnHold" : "Hold"}
>
<IconButton
aria-label="Place call onhold"
icon={
sipUA.current?.isHolded(undefined) ? <Play /> : <Pause />
}
w="33%"
variant="unstyled"
display="flex"
alignItems="center"
justifyContent="center"
onClick={handleCallOnHold}
/>
</Tooltip>
<Spacer />
<IconButton
aria-label="Hangup"
icon={<PhoneOff />}
w="70px"
h="70px"
borderRadius="100%"
colorScheme="jambonz"
onClick={handleHangup}
/>
<Spacer />
<Tooltip
label={sipUA.current?.isMuted(undefined) ? "Unmute" : "Mute"}
>
<IconButton
aria-label="Mute"
icon={
sipUA.current?.isMuted(undefined) ? <Mic /> : <MicOff />
}
w="33%"
variant="unstyled"
display="flex"
alignItems="center"
justifyContent="center"
onClick={handleCallMute}
/>
</Tooltip>
</HStack>
)}
</VStack>
)}
</Center>
);
};
export default Phone;