update history page

This commit is contained in:
Quan HL
2023-10-19 13:10:29 +07:00
parent 52ea895fed
commit e315936757
9 changed files with 265 additions and 67 deletions
+23 -3
View File
@@ -22,6 +22,7 @@
"react-feather": "^2.0.10", "react-feather": "^2.0.10",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"typescript": "^4.9.5", "typescript": "^4.9.5",
"uuid": "^9.0.1",
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"
}, },
"devDependencies": { "devDependencies": {
@@ -31,6 +32,7 @@
"@types/node": "^16.18.53", "@types/node": "^16.18.53",
"@types/react": "^18.2.22", "@types/react": "^18.2.22",
"@types/react-dom": "^18.2.7", "@types/react-dom": "^18.2.7",
"@types/uuid": "^9.0.6",
"copy-webpack-plugin": "^11.0.0", "copy-webpack-plugin": "^11.0.0",
"css-loader": "^6.8.1", "css-loader": "^6.8.1",
"file-loader": "^6.2.0", "file-loader": "^6.2.0",
@@ -5617,6 +5619,12 @@
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.4.tgz", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.4.tgz",
"integrity": "sha512-IDaobHimLQhjwsQ/NMwRVfa/yL7L/wriQPMhw1ZJall0KX6E1oxk29XMDeilW5qTIg5aoiqf5Udy8U/51aNoQQ==" "integrity": "sha512-IDaobHimLQhjwsQ/NMwRVfa/yL7L/wriQPMhw1ZJall0KX6E1oxk29XMDeilW5qTIg5aoiqf5Udy8U/51aNoQQ=="
}, },
"node_modules/@types/uuid": {
"version": "9.0.6",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.6.tgz",
"integrity": "sha512-BT2Krtx4xaO6iwzwMFUYvWBWkV2pr37zD68Vmp1CDV196MzczBRxuEpD6Pr395HAgebC/co7hOphs53r8V7jew==",
"dev": true
},
"node_modules/@types/ws": { "node_modules/@types/ws": {
"version": "8.5.5", "version": "8.5.5",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.5.tgz", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.5.tgz",
@@ -17459,6 +17467,14 @@
"websocket-driver": "^0.7.4" "websocket-driver": "^0.7.4"
} }
}, },
"node_modules/sockjs/node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/source-list-map": { "node_modules/source-list-map": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz",
@@ -18863,9 +18879,13 @@
} }
}, },
"node_modules/uuid": { "node_modules/uuid": {
"version": "8.3.2", "version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"bin": { "bin": {
"uuid": "dist/bin/uuid" "uuid": "dist/bin/uuid"
} }
+2
View File
@@ -17,6 +17,7 @@
"react-feather": "^2.0.10", "react-feather": "^2.0.10",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"typescript": "^4.9.5", "typescript": "^4.9.5",
"uuid": "^9.0.1",
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"
}, },
"devDependencies": { "devDependencies": {
@@ -26,6 +27,7 @@
"@types/node": "^16.18.53", "@types/node": "^16.18.53",
"@types/react": "^18.2.22", "@types/react": "^18.2.22",
"@types/react-dom": "^18.2.7", "@types/react-dom": "^18.2.7",
"@types/uuid": "^9.0.6",
"copy-webpack-plugin": "^11.0.0", "copy-webpack-plugin": "^11.0.0",
"css-loader": "^6.8.1", "css-loader": "^6.8.1",
"file-loader": "^6.2.0", "file-loader": "^6.2.0",
+2
View File
@@ -59,10 +59,12 @@ export interface AppSettings {
} }
export interface CallHistory { export interface CallHistory {
callSid: string;
direction: SipCallDirection; direction: SipCallDirection;
number: string; number: string;
duration: string; duration: string;
timeStamp: number; timeStamp: number;
isSaved?: boolean;
} }
export type SipClientStatus = "online" | "offline"; export type SipClientStatus = "online" | "offline";
+17
View File
@@ -41,6 +41,23 @@ export const saveCallHistory = (username: string, call: CallHistory) => {
localStorage.setItem(`${username}_${historyKey}`, encoded); localStorage.setItem(`${username}_${historyKey}`, encoded);
}; };
export const isSaveCallHistory = (
username: string,
callSid: string,
isSaved: boolean
) => {
const calls = getCallHistories(username).map((c) => {
if (c.callSid === callSid) {
return { ...c, isSaved };
} else {
return c;
}
});
const saveStr = JSON.stringify(calls);
const encoded = Buffer.from(saveStr, "utf-8").toString("base64");
localStorage.setItem(`${username}_${historyKey}`, encoded);
};
export const getCallHistories = (username: string): CallHistory[] => { export const getCallHistories = (username: string): CallHistory[] => {
const str = localStorage.getItem(`${username}_${historyKey}`); const str = localStorage.getItem(`${username}_${historyKey}`);
if (str) { if (str) {
+16 -2
View File
@@ -28,6 +28,8 @@ export const WindowApp = () => {
const [sipPassword, setSipPassword] = useState(""); const [sipPassword, setSipPassword] = useState("");
const [sipDisplayName, setSipDisplayName] = useState(""); const [sipDisplayName, setSipDisplayName] = useState("");
const [callHistories, setCallHistories] = useState<CallHistory[]>([]); const [callHistories, setCallHistories] = useState<CallHistory[]>([]);
const [calledNumber, setCalledNumber] = useState("");
const [tabIndex, setTabIndex] = useState(0);
const tabsSettings = [ const tabsSettings = [
{ {
title: "Dialer", title: "Dialer",
@@ -38,12 +40,22 @@ export const WindowApp = () => {
sipDomain={sipDomain} sipDomain={sipDomain}
sipDisplayName={sipDisplayName} sipDisplayName={sipDisplayName}
sipServerAddress={sipServerAddress} sipServerAddress={sipServerAddress}
calledNumber={[calledNumber, setCalledNumber]}
/> />
), ),
}, },
{ {
title: "History", title: "History",
content: <CallHistories calls={callHistories} />, content: (
<CallHistories
calls={callHistories}
onDataChange={() => setCallHistories(getCallHistories(sipUsername))}
onCallNumber={(number) => {
setCalledNumber(number);
setTabIndex(0);
}}
/>
),
}, },
{ {
title: "Settings", title: "Settings",
@@ -55,8 +67,9 @@ export const WindowApp = () => {
loadSettings(); loadSettings();
}, []); }, []);
const onTabsChange = () => { const onTabsChange = (i: number) => {
loadSettings(); loadSettings();
setTabIndex(i);
setCallHistories(getCallHistories(sipUsername)); setCallHistories(getCallHistories(sipUsername));
}; };
@@ -76,6 +89,7 @@ export const WindowApp = () => {
variant="enclosed" variant="enclosed"
colorScheme={DEFAULT_COLOR_SCHEME} colorScheme={DEFAULT_COLOR_SCHEME}
onChange={onTabsChange} onChange={onTabsChange}
index={tabIndex}
> >
<TabList mb="1em" gap={1}> <TabList mb="1em" gap={1}>
{tabsSettings.map((s) => ( {tabsSettings.map((s) => (
+61 -3
View File
@@ -1,14 +1,31 @@
import { HStack, Icon, Spacer, Text, VStack } from "@chakra-ui/react"; import {
HStack,
Icon,
IconButton,
Spacer,
Text,
Tooltip,
VStack,
} from "@chakra-ui/react";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { Phone, PhoneIncoming, PhoneOutgoing } from "react-feather"; import { useState } from "react";
import { Phone, PhoneIncoming, PhoneOutgoing, Save, Star } from "react-feather";
import { CallHistory, SipCallDirection } from "src/common/types"; import { CallHistory, SipCallDirection } from "src/common/types";
import { getSettings, isSaveCallHistory } from "src/storage";
import { formatPhoneNumber } from "src/utils"; import { formatPhoneNumber } from "src/utils";
type CallHistoryItemProbs = { type CallHistoryItemProbs = {
call: CallHistory; call: CallHistory;
onDataChange?: (call: CallHistory) => void;
onCallNumber?: (number: string) => void;
}; };
export const CallHistoryItem = ({ call }: CallHistoryItemProbs) => { export const CallHistoryItem = ({
call,
onDataChange,
onCallNumber,
}: CallHistoryItemProbs) => {
const [callEnable, setCallEnable] = useState(false);
const getDirectionIcon = (direction: SipCallDirection) => { const getDirectionIcon = (direction: SipCallDirection) => {
if (direction === "outgoing") { if (direction === "outgoing") {
return PhoneOutgoing; return PhoneOutgoing;
@@ -18,12 +35,15 @@ export const CallHistoryItem = ({ call }: CallHistoryItemProbs) => {
return Phone; return Phone;
} }
}; };
return ( return (
<HStack <HStack
spacing={5} spacing={5}
borderBottomWidth="1px" borderBottomWidth="1px"
borderBottomColor="gray.200" borderBottomColor="gray.200"
p={2} p={2}
onMouseEnter={() => setCallEnable(true)}
onMouseLeave={() => setCallEnable(false)}
> >
<Icon as={getDirectionIcon(call.direction)} w="20px" h="20px" /> <Icon as={getDirectionIcon(call.direction)} w="20px" h="20px" />
<VStack align="start"> <VStack align="start">
@@ -32,12 +52,50 @@ export const CallHistoryItem = ({ call }: CallHistoryItemProbs) => {
</Text> </Text>
<Text fontSize="12px">{call.duration}</Text> <Text fontSize="12px">{call.duration}</Text>
</VStack> </VStack>
{callEnable && (
<Tooltip label="Call">
<IconButton
aria-label="call recents"
icon={<Phone />}
onClick={() => {
if (onCallNumber) {
onCallNumber(call.number);
}
}}
variant="unstyled"
size="sm"
color="green.500"
/>
</Tooltip>
)}
<Spacer /> <Spacer />
<VStack align="start"> <VStack align="start">
<Text fontSize="12px"> <Text fontSize="12px">
{dayjs(call.timeStamp).format("MMM D, hh:mm A")} {dayjs(call.timeStamp).format("MMM D, hh:mm A")}
</Text> </Text>
</VStack> </VStack>
<Tooltip label="Save">
<IconButton
aria-label="save recents"
icon={<Save />}
onClick={() => {
const settings = getSettings();
if (settings.sipUsername) {
isSaveCallHistory(
settings.sipUsername,
call.callSid,
!call.isSaved
);
if (onDataChange) {
onDataChange(call);
}
}
}}
variant="unstyled"
size="sm"
color={call.isSaved ? "jambonz.500" : ""}
/>
</Tooltip>
</HStack> </HStack>
); );
}; };
+42 -40
View File
@@ -9,41 +9,40 @@ import {
Text, Text,
Spacer, Spacer,
UnorderedList, UnorderedList,
Tabs,
TabList,
Tab,
TabPanels,
TabPanel,
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Search, Sliders } from "react-feather"; import { Search, Sliders } from "react-feather";
import { CallHistory } from "src/common/types"; import { CallHistory } from "src/common/types";
import CallHistoryItem from "./call-history-item"; import CallHistoryItem from "./call-history-item";
import Fuse from "fuse.js";
import { DEFAULT_COLOR_SCHEME } from "src/common/constants";
import Recents from "./recent";
type CallHistoriesProbs = { type CallHistoriesProbs = {
calls: CallHistory[]; calls: CallHistory[];
onDataChange?: (call: CallHistory) => void;
onCallNumber?: (number: string) => void;
}; };
export const CallHistories = ({ calls }: CallHistoriesProbs) => { export const CallHistories = ({
calls,
onDataChange,
onCallNumber,
}: CallHistoriesProbs) => {
const [searchText, setSearchText] = useState(""); const [searchText, setSearchText] = useState("");
const [callHistories, setCallHistories] = useState<CallHistory[]>(calls);
useEffect(() => {
if (searchText) {
setCallHistories(
new Fuse(calls, {
keys: ["number"],
})
.search(searchText)
.map(({ item }) => item)
);
} else {
setCallHistories(calls);
}
}, [searchText]);
useEffect(() => {
setCallHistories(calls);
}, [calls]);
return ( return (
<VStack spacing={2}> <Tabs isFitted colorScheme={DEFAULT_COLOR_SCHEME}>
<TabList mb="1em" gap={1}>
<Tab>Saved</Tab>
<Tab>Recent</Tab>
</TabList>
<Grid w="full" templateColumns="1fr auto" gap={5}> <Grid w="full" templateColumns="1fr auto" gap={5}>
<InputGroup size="md"> <InputGroup size="md">
<Input <Input
@@ -66,24 +65,27 @@ export const CallHistories = ({ calls }: CallHistoriesProbs) => {
</Text> </Text>
</HStack> </HStack>
</Grid> </Grid>
{callHistories.length > 0 ? (
<UnorderedList <TabPanels mt={1}>
w="full" <TabPanel p={0}>
spacing={2} <Recents
maxH="500px" calls={calls}
overflowY="auto" search={searchText}
mt={2} isSaved
> onCallNumber={onCallNumber}
{callHistories.map((c) => ( onDataChange={onDataChange}
<CallHistoryItem call={c} /> />
))} </TabPanel>
</UnorderedList> <TabPanel p={0}>
) : ( <Recents
<Text fontSize="24px" fontWeight="bold"> calls={calls}
No Call History search={searchText}
</Text> onCallNumber={onCallNumber}
)} onDataChange={onDataChange}
</VStack> />
</TabPanel>
</TabPanels>
</Tabs>
); );
}; };
+71
View File
@@ -0,0 +1,71 @@
import { Text, UnorderedList, VStack } from "@chakra-ui/react";
import CallHistoryItem from "./call-history-item";
import { CallHistory } from "src/common/types";
import { useEffect, useState } from "react";
import Fuse from "fuse.js";
type RecentsProbs = {
calls: CallHistory[];
search: string;
isSaved?: boolean;
onDataChange?: (call: CallHistory) => void;
onCallNumber?: (number: string) => void;
};
export const Recents = ({
calls,
search,
isSaved,
onDataChange,
onCallNumber,
}: RecentsProbs) => {
const [callHistories, setCallHistories] = useState<CallHistory[]>(calls);
useEffect(() => {
if (search) {
setCallHistories((prev) =>
new Fuse(prev, {
keys: ["number"],
})
.search(search)
.map(({ item }) => item)
);
} else {
setCallHistories(
isSaved ? calls.filter((c) => c.isSaved === true) : calls
);
}
}, [search]);
useEffect(() => {
setCallHistories(isSaved ? calls.filter((c) => c.isSaved === true) : calls);
}, [calls]);
return (
<VStack spacing={2}>
{callHistories.length > 0 ? (
<UnorderedList
w="full"
maxH="calc(100vh - 21em)"
overflowY="auto"
spacing={2}
mt={2}
>
{callHistories.map((c) => (
<CallHistoryItem
call={c}
onCallNumber={onCallNumber}
onDataChange={onDataChange}
/>
))}
</UnorderedList>
) : (
<Text fontSize="24px" fontWeight="bold">
No Call History
</Text>
)}
</VStack>
);
};
export default Recents;
+31 -19
View File
@@ -15,13 +15,7 @@ import {
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { Mic, MicOff, Pause, PhoneOff, Play } from "react-feather"; import { Mic, MicOff, Pause, PhoneOff, Play } from "react-feather";
import { import { Call, SipCallDirection, SipClientStatus } from "src/common/types";
Call,
Message,
MessageEvent,
SipCallDirection,
SipClientStatus,
} from "src/common/types";
import { SipConstants, SipUA } from "src/lib"; import { SipConstants, SipUA } from "src/lib";
import IncommingCall from "./incomming-call"; import IncommingCall from "./incomming-call";
import DialPad from "./dial-pad"; import DialPad from "./dial-pad";
@@ -42,6 +36,7 @@ import {
saveCurrentCall, saveCurrentCall,
} from "src/storage"; } from "src/storage";
import { OutGoingCall } from "./outgoing-call"; import { OutGoingCall } from "./outgoing-call";
import { v4 as uuidv4 } from "uuid";
type PhoneProbs = { type PhoneProbs = {
sipDomain: string; sipDomain: string;
@@ -49,6 +44,7 @@ type PhoneProbs = {
sipUsername: string; sipUsername: string;
sipPassword: string; sipPassword: string;
sipDisplayName: string; sipDisplayName: string;
calledNumber: [string, React.Dispatch<React.SetStateAction<string>>];
}; };
export const Phone = ({ export const Phone = ({
@@ -57,6 +53,7 @@ export const Phone = ({
sipUsername, sipUsername,
sipPassword, sipPassword,
sipDisplayName, sipDisplayName,
calledNumber: [calledANumber, setCalledANumber],
}: PhoneProbs) => { }: PhoneProbs) => {
const [inputNumber, setInputNumber] = useState(""); const [inputNumber, setInputNumber] = useState("");
const inputNumberRef = useRef(inputNumber); const inputNumberRef = useRef(inputNumber);
@@ -102,6 +99,14 @@ export const Phone = ({
} }
}, [callStatus]); }, [callStatus]);
useEffect(() => {
if (calledANumber) {
setInputNumber(calledANumber);
makeOutboundCall(calledANumber);
setCalledANumber("");
}
}, [calledANumber]);
// useEffect(() => { // useEffect(() => {
// chrome.runtime.onMessage.addListener(function (request) { // chrome.runtime.onMessage.addListener(function (request) {
// const msg = request as Message<any>; // const msg = request as Message<any>;
@@ -115,15 +120,15 @@ export const Phone = ({
// }); // });
// }, []); // }, []);
const handleCallEvent = (call: Call) => { // const handleCallEvent = (call: Call) => {
if (!call.number) return; // if (!call.number) return;
if (isSipClientIdle(callStatus)) { // if (isSipClientIdle(callStatus)) {
setIsCallButtonLoading(true); // setIsCallButtonLoading(true);
setInputNumber(call.number); // setInputNumber(call.number);
sipUA.current?.call(call.number); // sipUA.current?.call(call.number);
} // }
}; // };
const startCallDurationCounter = () => { const startCallDurationCounter = () => {
stopCallDurationCounter(); stopCallDurationCounter();
@@ -175,6 +180,7 @@ export const Phone = ({
direction: args.session.direction, direction: args.session.direction,
timeStamp: Date.now(), timeStamp: Date.now(),
duration: "0", duration: "0",
callSid: uuidv4(),
}); });
} }
setCallStatus(SipConstants.SESSION_RINGING); setCallStatus(SipConstants.SESSION_RINGING);
@@ -215,6 +221,7 @@ export const Phone = ({
direction: call.direction, direction: call.direction,
duration: transform(Date.now(), call.timeStamp), duration: transform(Date.now(), call.timeStamp),
timeStamp: call.timeStamp, timeStamp: call.timeStamp,
callSid: call.callSid,
}); });
} }
deleteCurrentCall(); deleteCurrentCall();
@@ -244,17 +251,22 @@ export const Phone = ({
}; };
const handleCallButtion = () => { const handleCallButtion = () => {
if (sipUA.current && inputNumber) { makeOutboundCall(inputNumber);
};
const makeOutboundCall = (number: string) => {
if (sipUA.current && number) {
setIsCallButtonLoading(true); setIsCallButtonLoading(true);
setCallStatus(SipConstants.SESSION_RINGING); setCallStatus(SipConstants.SESSION_RINGING);
setSessionDirection("outgoing"); setSessionDirection("outgoing");
saveCurrentCall({ saveCurrentCall({
number: inputNumber, number: number,
direction: "outgoing", direction: "outgoing",
timeStamp: Date.now(), timeStamp: Date.now(),
duration: "0", duration: "0",
callSid: uuidv4(),
}); });
sipUA.current.call(inputNumber); sipUA.current.call(number);
} }
}; };
@@ -388,7 +400,7 @@ export const Phone = ({
<Text fontSize="22px" fontWeight="bold"> <Text fontSize="22px" fontWeight="bold">
{formatPhoneNumber(inputNumber)} {formatPhoneNumber(inputNumber)}
</Text> </Text>
{seconds > 0 && ( {seconds >= 0 && (
<Text fontSize="15px"> <Text fontSize="15px">
{new Date(seconds * 1000).toISOString().substr(11, 8)} {new Date(seconds * 1000).toISOString().substr(11, 8)}
</Text> </Text>