diff --git a/package-lock.json b/package-lock.json index 25444f8..f8a8f9c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "react-feather": "^2.0.10", "react-scripts": "5.0.1", "typescript": "^4.9.5", + "uuid": "^9.0.1", "web-vitals": "^2.1.4" }, "devDependencies": { @@ -31,6 +32,7 @@ "@types/node": "^16.18.53", "@types/react": "^18.2.22", "@types/react-dom": "^18.2.7", + "@types/uuid": "^9.0.6", "copy-webpack-plugin": "^11.0.0", "css-loader": "^6.8.1", "file-loader": "^6.2.0", @@ -5617,6 +5619,12 @@ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.4.tgz", "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": { "version": "8.5.5", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.5.tgz", @@ -17459,6 +17467,14 @@ "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": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", @@ -18863,9 +18879,13 @@ } }, "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==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], "bin": { "uuid": "dist/bin/uuid" } diff --git a/package.json b/package.json index 0b6f528..f265a69 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "react-feather": "^2.0.10", "react-scripts": "5.0.1", "typescript": "^4.9.5", + "uuid": "^9.0.1", "web-vitals": "^2.1.4" }, "devDependencies": { @@ -26,6 +27,7 @@ "@types/node": "^16.18.53", "@types/react": "^18.2.22", "@types/react-dom": "^18.2.7", + "@types/uuid": "^9.0.6", "copy-webpack-plugin": "^11.0.0", "css-loader": "^6.8.1", "file-loader": "^6.2.0", diff --git a/src/common/types.ts b/src/common/types.ts index 65da9c4..4da3f3e 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -59,10 +59,12 @@ export interface AppSettings { } export interface CallHistory { + callSid: string; direction: SipCallDirection; number: string; duration: string; timeStamp: number; + isSaved?: boolean; } export type SipClientStatus = "online" | "offline"; diff --git a/src/storage/index.ts b/src/storage/index.ts index b1302fa..86fc5e2 100644 --- a/src/storage/index.ts +++ b/src/storage/index.ts @@ -41,6 +41,23 @@ export const saveCallHistory = (username: string, call: CallHistory) => { 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[] => { const str = localStorage.getItem(`${username}_${historyKey}`); if (str) { diff --git a/src/window/app.tsx b/src/window/app.tsx index 41922dd..fa2286e 100644 --- a/src/window/app.tsx +++ b/src/window/app.tsx @@ -28,6 +28,8 @@ export const WindowApp = () => { const [sipPassword, setSipPassword] = useState(""); const [sipDisplayName, setSipDisplayName] = useState(""); const [callHistories, setCallHistories] = useState([]); + const [calledNumber, setCalledNumber] = useState(""); + const [tabIndex, setTabIndex] = useState(0); const tabsSettings = [ { title: "Dialer", @@ -38,12 +40,22 @@ export const WindowApp = () => { sipDomain={sipDomain} sipDisplayName={sipDisplayName} sipServerAddress={sipServerAddress} + calledNumber={[calledNumber, setCalledNumber]} /> ), }, { title: "History", - content: , + content: ( + setCallHistories(getCallHistories(sipUsername))} + onCallNumber={(number) => { + setCalledNumber(number); + setTabIndex(0); + }} + /> + ), }, { title: "Settings", @@ -55,8 +67,9 @@ export const WindowApp = () => { loadSettings(); }, []); - const onTabsChange = () => { + const onTabsChange = (i: number) => { loadSettings(); + setTabIndex(i); setCallHistories(getCallHistories(sipUsername)); }; @@ -76,6 +89,7 @@ export const WindowApp = () => { variant="enclosed" colorScheme={DEFAULT_COLOR_SCHEME} onChange={onTabsChange} + index={tabIndex} > {tabsSettings.map((s) => ( diff --git a/src/window/history/call-history-item.tsx b/src/window/history/call-history-item.tsx index 9d97412..89811f8 100644 --- a/src/window/history/call-history-item.tsx +++ b/src/window/history/call-history-item.tsx @@ -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 { 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 { getSettings, isSaveCallHistory } from "src/storage"; import { formatPhoneNumber } from "src/utils"; type CallHistoryItemProbs = { 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) => { if (direction === "outgoing") { return PhoneOutgoing; @@ -18,12 +35,15 @@ export const CallHistoryItem = ({ call }: CallHistoryItemProbs) => { return Phone; } }; + return ( setCallEnable(true)} + onMouseLeave={() => setCallEnable(false)} > @@ -32,12 +52,50 @@ export const CallHistoryItem = ({ call }: CallHistoryItemProbs) => { {call.duration} + {callEnable && ( + + } + onClick={() => { + if (onCallNumber) { + onCallNumber(call.number); + } + }} + variant="unstyled" + size="sm" + color="green.500" + /> + + )} {dayjs(call.timeStamp).format("MMM D, hh:mm A")} + + } + 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" : ""} + /> + ); }; diff --git a/src/window/history/index.tsx b/src/window/history/index.tsx index da592b8..d1d20b6 100644 --- a/src/window/history/index.tsx +++ b/src/window/history/index.tsx @@ -9,41 +9,40 @@ import { Text, Spacer, UnorderedList, + Tabs, + TabList, + Tab, + TabPanels, + TabPanel, } from "@chakra-ui/react"; import { useEffect, useState } from "react"; import { Search, Sliders } from "react-feather"; import { CallHistory } from "src/common/types"; 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 = { 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 [callHistories, setCallHistories] = useState(calls); - - useEffect(() => { - if (searchText) { - setCallHistories( - new Fuse(calls, { - keys: ["number"], - }) - .search(searchText) - .map(({ item }) => item) - ); - } else { - setCallHistories(calls); - } - }, [searchText]); - - useEffect(() => { - setCallHistories(calls); - }, [calls]); return ( - + + + Saved + Recent + + { - {callHistories.length > 0 ? ( - - {callHistories.map((c) => ( - - ))} - - ) : ( - - No Call History - - )} - + + + + + + + + + + ); }; diff --git a/src/window/history/recent.tsx b/src/window/history/recent.tsx new file mode 100644 index 0000000..01a89b6 --- /dev/null +++ b/src/window/history/recent.tsx @@ -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(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 ( + + {callHistories.length > 0 ? ( + + {callHistories.map((c) => ( + + ))} + + ) : ( + + No Call History + + )} + + ); +}; + +export default Recents; diff --git a/src/window/phone/index.tsx b/src/window/phone/index.tsx index 9098f36..0301fa1 100644 --- a/src/window/phone/index.tsx +++ b/src/window/phone/index.tsx @@ -15,13 +15,7 @@ import { } 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 { Call, SipCallDirection, SipClientStatus } from "src/common/types"; import { SipConstants, SipUA } from "src/lib"; import IncommingCall from "./incomming-call"; import DialPad from "./dial-pad"; @@ -42,6 +36,7 @@ import { saveCurrentCall, } from "src/storage"; import { OutGoingCall } from "./outgoing-call"; +import { v4 as uuidv4 } from "uuid"; type PhoneProbs = { sipDomain: string; @@ -49,6 +44,7 @@ type PhoneProbs = { sipUsername: string; sipPassword: string; sipDisplayName: string; + calledNumber: [string, React.Dispatch>]; }; export const Phone = ({ @@ -57,6 +53,7 @@ export const Phone = ({ sipUsername, sipPassword, sipDisplayName, + calledNumber: [calledANumber, setCalledANumber], }: PhoneProbs) => { const [inputNumber, setInputNumber] = useState(""); const inputNumberRef = useRef(inputNumber); @@ -102,6 +99,14 @@ export const Phone = ({ } }, [callStatus]); + useEffect(() => { + if (calledANumber) { + setInputNumber(calledANumber); + makeOutboundCall(calledANumber); + setCalledANumber(""); + } + }, [calledANumber]); + // useEffect(() => { // chrome.runtime.onMessage.addListener(function (request) { // const msg = request as Message; @@ -115,15 +120,15 @@ export const Phone = ({ // }); // }, []); - const handleCallEvent = (call: Call) => { - if (!call.number) return; + // const handleCallEvent = (call: Call) => { + // if (!call.number) return; - if (isSipClientIdle(callStatus)) { - setIsCallButtonLoading(true); - setInputNumber(call.number); - sipUA.current?.call(call.number); - } - }; + // if (isSipClientIdle(callStatus)) { + // setIsCallButtonLoading(true); + // setInputNumber(call.number); + // sipUA.current?.call(call.number); + // } + // }; const startCallDurationCounter = () => { stopCallDurationCounter(); @@ -175,6 +180,7 @@ export const Phone = ({ direction: args.session.direction, timeStamp: Date.now(), duration: "0", + callSid: uuidv4(), }); } setCallStatus(SipConstants.SESSION_RINGING); @@ -215,6 +221,7 @@ export const Phone = ({ direction: call.direction, duration: transform(Date.now(), call.timeStamp), timeStamp: call.timeStamp, + callSid: call.callSid, }); } deleteCurrentCall(); @@ -244,17 +251,22 @@ export const Phone = ({ }; const handleCallButtion = () => { - if (sipUA.current && inputNumber) { + makeOutboundCall(inputNumber); + }; + + const makeOutboundCall = (number: string) => { + if (sipUA.current && number) { setIsCallButtonLoading(true); setCallStatus(SipConstants.SESSION_RINGING); setSessionDirection("outgoing"); saveCurrentCall({ - number: inputNumber, + number: number, direction: "outgoing", timeStamp: Date.now(), duration: "0", + callSid: uuidv4(), }); - sipUA.current.call(inputNumber); + sipUA.current.call(number); } }; @@ -388,7 +400,7 @@ export const Phone = ({ {formatPhoneNumber(inputNumber)} - {seconds > 0 && ( + {seconds >= 0 && ( {new Date(seconds * 1000).toISOString().substr(11, 8)}