mirror of
https://github.com/jambonz/chrome-extension-dialer.git
synced 2025-12-19 04:47:45 +00:00
update history page
This commit is contained in:
26
package-lock.json
generated
26
package-lock.json
generated
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -28,6 +28,8 @@ export const WindowApp = () => {
|
||||
const [sipPassword, setSipPassword] = useState("");
|
||||
const [sipDisplayName, setSipDisplayName] = useState("");
|
||||
const [callHistories, setCallHistories] = useState<CallHistory[]>([]);
|
||||
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: <CallHistories calls={callHistories} />,
|
||||
content: (
|
||||
<CallHistories
|
||||
calls={callHistories}
|
||||
onDataChange={() => 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}
|
||||
>
|
||||
<TabList mb="1em" gap={1}>
|
||||
{tabsSettings.map((s) => (
|
||||
|
||||
@@ -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 (
|
||||
<HStack
|
||||
spacing={5}
|
||||
borderBottomWidth="1px"
|
||||
borderBottomColor="gray.200"
|
||||
p={2}
|
||||
onMouseEnter={() => setCallEnable(true)}
|
||||
onMouseLeave={() => setCallEnable(false)}
|
||||
>
|
||||
<Icon as={getDirectionIcon(call.direction)} w="20px" h="20px" />
|
||||
<VStack align="start">
|
||||
@@ -32,12 +52,50 @@ export const CallHistoryItem = ({ call }: CallHistoryItemProbs) => {
|
||||
</Text>
|
||||
<Text fontSize="12px">{call.duration}</Text>
|
||||
</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 />
|
||||
<VStack align="start">
|
||||
<Text fontSize="12px">
|
||||
{dayjs(call.timeStamp).format("MMM D, hh:mm A")}
|
||||
</Text>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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<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 (
|
||||
<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}>
|
||||
<InputGroup size="md">
|
||||
<Input
|
||||
@@ -66,24 +65,27 @@ export const CallHistories = ({ calls }: CallHistoriesProbs) => {
|
||||
</Text>
|
||||
</HStack>
|
||||
</Grid>
|
||||
{callHistories.length > 0 ? (
|
||||
<UnorderedList
|
||||
w="full"
|
||||
spacing={2}
|
||||
maxH="500px"
|
||||
overflowY="auto"
|
||||
mt={2}
|
||||
>
|
||||
{callHistories.map((c) => (
|
||||
<CallHistoryItem call={c} />
|
||||
))}
|
||||
</UnorderedList>
|
||||
) : (
|
||||
<Text fontSize="24px" fontWeight="bold">
|
||||
No Call History
|
||||
</Text>
|
||||
)}
|
||||
</VStack>
|
||||
|
||||
<TabPanels mt={1}>
|
||||
<TabPanel p={0}>
|
||||
<Recents
|
||||
calls={calls}
|
||||
search={searchText}
|
||||
isSaved
|
||||
onCallNumber={onCallNumber}
|
||||
onDataChange={onDataChange}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel p={0}>
|
||||
<Recents
|
||||
calls={calls}
|
||||
search={searchText}
|
||||
onCallNumber={onCallNumber}
|
||||
onDataChange={onDataChange}
|
||||
/>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
71
src/window/history/recent.tsx
Normal file
71
src/window/history/recent.tsx
Normal 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;
|
||||
@@ -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<React.SetStateAction<string>>];
|
||||
};
|
||||
|
||||
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<any>;
|
||||
@@ -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 = ({
|
||||
<Text fontSize="22px" fontWeight="bold">
|
||||
{formatPhoneNumber(inputNumber)}
|
||||
</Text>
|
||||
{seconds > 0 && (
|
||||
{seconds >= 0 && (
|
||||
<Text fontSize="15px">
|
||||
{new Date(seconds * 1000).toISOString().substr(11, 8)}
|
||||
</Text>
|
||||
|
||||
Reference in New Issue
Block a user