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

26
package-lock.json generated
View File

@@ -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"
}

View File

@@ -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",

View File

@@ -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";

View File

@@ -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) {

View File

@@ -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) => (

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 { 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>
);
};

View File

@@ -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>
);
};

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;

View File

@@ -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>