done with implementation

This commit is contained in:
abdurahman
2024-08-25 17:52:46 +01:00
parent 43df0fcd2c
commit d9286fb871
21 changed files with 1143 additions and 198 deletions

29
.gitignore vendored
View File

@@ -1,27 +1,2 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
/dist
/lib
/components
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
node_modules
dist

View File

@@ -10,7 +10,7 @@ import {
UpdateCall,
} from "./types";
import { MSG_SOMETHING_WRONG } from "./constants";
import { getAdvancedSettings } from "src/storage";
import { getActiveSettings } from "src/storage";
import { EmptyData } from "src/common/types";
const fetchTransport = <Type>(
@@ -87,8 +87,8 @@ const fetchTransport = <Type>(
};
const getAuthHeaders = () => {
const advancedSettings = getAdvancedSettings();
let token = advancedSettings.apiKey ?? null;
const advancedSettings = getActiveSettings();
let token = advancedSettings?.decoded?.apiKey ?? null;
return {
"Content-Type": "application/json",
@@ -130,37 +130,37 @@ export const deleteFetch = <Type>(url: string) => {
// GET Devices Users
export const getRegisteredUser = () => {
const advancedSettings = getAdvancedSettings();
const advancedSettings = getActiveSettings();
return getFetch<string[]>(
`${advancedSettings.apiServer}/Accounts/${advancedSettings.accountSid}/RegisteredSipUsers`
`${advancedSettings?.decoded?.apiServer}/Accounts/${advancedSettings?.decoded?.accountSid}/RegisteredSipUsers`
);
};
export const getApplications = () => {
const advancedSettings = getAdvancedSettings();
const advancedSettings = getActiveSettings();
return getFetch<Application[]>(
`${advancedSettings.apiServer}/Accounts/${advancedSettings.accountSid}/Applications`
`${advancedSettings?.decoded?.apiServer}/Accounts/${advancedSettings?.decoded?.accountSid}/Applications`
);
};
export const getQueues = () => {
const advancedSettings = getAdvancedSettings();
const advancedSettings = getActiveSettings();
return getFetch<Queue[]>(
`${advancedSettings.apiServer}/Accounts/${advancedSettings.accountSid}/Queues`
`${advancedSettings?.decoded?.apiServer}/Accounts/${advancedSettings?.decoded?.accountSid}/Queues`
);
};
export const getSelfRegisteredUser = (username: string) => {
const advancedSettings = getAdvancedSettings();
const advancedSettings = getActiveSettings();
return getFetch<RegisteredUser>(
`${advancedSettings.apiServer}/Accounts/${advancedSettings.accountSid}/RegisteredSipUsers/${username}`
`${advancedSettings?.decoded?.apiServer}/Accounts/${advancedSettings?.decoded?.accountSid}/RegisteredSipUsers/${username}`
);
};
export const getConferences = () => {
const advancedSettings = getAdvancedSettings();
const advancedSettings = getActiveSettings();
return getFetch<string[]>(
`${advancedSettings.apiServer}/Accounts/${advancedSettings.accountSid}/Conferences`
`${advancedSettings?.decoded?.apiServer}/Accounts/${advancedSettings?.decoded?.accountSid}/Conferences`
);
};
@@ -168,9 +168,9 @@ export const updateConferenceParticipantAction = (
callSid: string,
payload: ConferenceParticipantAction
) => {
const advancedSettings = getAdvancedSettings();
const advancedSettings = getActiveSettings();
return putFetch<EmptyData, UpdateCall>(
`${advancedSettings.apiServer}/Accounts/${advancedSettings.accountSid}/Calls/${callSid}`,
`${advancedSettings?.decoded?.apiServer}/Accounts/${advancedSettings?.decoded?.accountSid}/Calls/${callSid}`,
{
conferenceParticipantAction: payload,
}

View File

@@ -57,6 +57,16 @@ export interface AppSettings {
sipUsername: string;
sipPassword: string;
sipDisplayName: string;
accountSid?: string;
apiKey?: string;
apiServer?: string;
}
export interface IAppSettings {
active: boolean;
decoded: AppSettings;
id: number;
}
export interface ConferenceSettings {
@@ -71,6 +81,12 @@ export interface AdvancedAppSettings {
apiServer: string;
}
export interface IAdvancedAppSettings {
active: boolean;
decoded: AdvancedAppSettings;
id: number;
}
export interface CallHistory {
callSid: string;
direction: SipCallDirection;

View File

@@ -0,0 +1,27 @@
import React, { ReactNode } from "react";
import { motion } from "framer-motion";
function AnimateOnShow({
children,
initial = -20,
exit = -20,
duration = 0.3,
}: {
children: ReactNode;
initial?: number;
exit?: number;
duration?: number;
}) {
return (
<motion.div
initial={{ opacity: 0, y: initial }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: exit }}
transition={{ duration: duration }}
>
{children}
</motion.div>
);
}
export default AnimateOnShow;

View File

@@ -1,8 +1,8 @@
import { Box, Text } from "@chakra-ui/react";
type JambonzSwitchProbs = {
onlabel: string;
offLabel: string;
onlabel?: string;
offLabel?: string;
checked: [boolean, React.Dispatch<React.SetStateAction<boolean>>];
isDisabled?: boolean;
onChange: (value: boolean) => void;
@@ -17,7 +17,7 @@ function JambonzSwitch({
return (
<Box
position="relative"
w="90px"
w="50px"
h="30px"
bg={isToggled ? "green.500" : "grey.500"}
borderRadius="full"
@@ -30,22 +30,24 @@ function JambonzSwitch({
}}
_hover={{ cursor: "pointer" }}
>
<Text
position="absolute"
top="50%"
left={isToggled ? "40%" : "60%"}
transform="translate(-50%, -50%)"
color={isToggled ? "white" : "black"}
fontWeight="bold"
>
{isToggled ? onlabel : offLabel}
</Text>
{onlabel && offLabel && (
<Text
position="absolute"
top="50%"
left={isToggled ? "40%" : "60%"}
transform="translate(-50%, -50%)"
color={isToggled ? "white" : "black"}
fontWeight="bold"
>
{isToggled ? onlabel : offLabel}
</Text>
)}
<Box
position="absolute"
top="50%"
left={isToggled ? "70%" : "5%"}
w="24px"
h="24px"
left={isToggled ? "50%" : "5%"}
w="22px"
h="22px"
bg="white"
borderRadius="full"
transform="translateY(-50%)"

View File

@@ -3,6 +3,8 @@ import {
AppSettings,
CallHistory,
ConferenceSettings,
IAdvancedAppSettings,
IAppSettings,
} from "src/common/types";
import { Buffer } from "buffer";
@@ -25,21 +27,154 @@ export const deleteConferenceSettings = () => {
// Settings
const SETTINGS_KEY = "SettingsKey";
interface saveSettingFormat {
active: boolean;
encoded: string;
id: number;
}
export const saveSettings = (settings: AppSettings) => {
const encoded = Buffer.from(JSON.stringify(settings), "utf-8").toString(
"base64"
);
localStorage.setItem(SETTINGS_KEY, encoded);
};
export const getSettings = (): AppSettings => {
const str = localStorage.getItem(SETTINGS_KEY);
if (str) {
const planText = Buffer.from(str, "base64").toString("utf-8");
return JSON.parse(planText) as AppSettings;
const parsed = JSON.parse(str);
const data: IAppSettings[] = parsed.map((el: saveSettingFormat) => {
return {
active: el.active,
decoded: JSON.parse(
Buffer.from(el.encoded, "base64").toString("utf-8")
),
id: el.id,
};
});
const alreadyExists = data.filter(
(el) =>
el.decoded.sipDomain === settings.sipDomain &&
el.decoded.sipServerAddress === settings.sipServerAddress
);
if (!!alreadyExists.length) return;
localStorage.setItem(
SETTINGS_KEY,
JSON.stringify([
...parsed,
{ encoded, active: false, id: parsed.length + 1 },
])
);
} else {
localStorage.setItem(
SETTINGS_KEY,
JSON.stringify([{ id: 1, encoded, active: true }])
);
}
return {} as AppSettings;
};
export const editSettings = (settings: AppSettings, id: number) => {
const encoded = Buffer.from(JSON.stringify(settings), "utf-8").toString(
"base64"
);
const str = localStorage.getItem(SETTINGS_KEY);
if (str) {
const parsed = JSON.parse(str);
// for edit:
const newData = parsed.map((el: saveSettingFormat) => {
if (el.id === id)
return {
id: el.id,
active: el.active,
encoded: encoded,
};
else return el;
});
localStorage.setItem(SETTINGS_KEY, JSON.stringify(newData));
}
};
export const setActiveSettings = (id: number) => {
const str = localStorage.getItem(SETTINGS_KEY);
if (str) {
const parsed = JSON.parse(str);
// for edit:
const newData = parsed.map((el: saveSettingFormat) => {
if (el.id === id)
return {
id: el.id,
active: true,
encoded: el.encoded,
};
else
return {
id: el.id,
active: false,
encoded: el.encoded,
};
});
localStorage.setItem(SETTINGS_KEY, JSON.stringify(newData));
}
};
export const deleteSettings = (id: number) => {
const str = localStorage.getItem(SETTINGS_KEY);
if (str) {
const parsed = JSON.parse(str);
// for edit:
const newData = parsed.filter((el: saveSettingFormat) => el.id !== id);
localStorage.setItem(SETTINGS_KEY, JSON.stringify(newData));
}
};
export const getSettings = (): IAppSettings[] => {
const str = localStorage.getItem(SETTINGS_KEY);
if (str) {
const data: { active: boolean; encoded: string; id: number }[] =
JSON.parse(str);
const decoded: IAppSettings[] = data.map((el) => {
return {
active: el.active,
decoded: JSON.parse(
Buffer.from(el.encoded, "base64").toString("utf-8")
),
id: el.id,
};
});
return decoded;
// const planText = Buffer.from(str, "base64").toString("utf-8");
// return JSON.parse(planText) as AppSettings;
}
// return {} as AppSettings;
return [] as IAppSettings[];
};
export const getActiveSettings = (): IAppSettings => {
const str = localStorage.getItem(SETTINGS_KEY);
if (str) {
const data: { active: boolean; encoded: string; id: number }[] =
JSON.parse(str);
const decoded: IAppSettings[] = data.map((el) => {
return {
active: el.active,
decoded: JSON.parse(
Buffer.from(el.encoded, "base64").toString("utf-8")
),
id: el.id,
};
});
return decoded.find((el) => el.active) as IAppSettings;
// const planText = Buffer.from(str, "base64").toString("utf-8");
// return JSON.parse(planText) as AppSettings;
}
// return {} as AppSettings;
return {} as IAppSettings;
};
// Advanced settings
@@ -50,19 +185,76 @@ export const saveAddvancedSettings = (settings: AdvancedAppSettings) => {
"base64"
);
localStorage.setItem(ADVANCED_SETTINGS_KET, encoded);
const str = localStorage.getItem(ADVANCED_SETTINGS_KET);
if (str) {
const data = JSON.parse(str);
const alreadyExists = data.filter(
(el: { encoded: string; active: boolean }) => el.encoded === encoded
);
if (!!alreadyExists.length) return;
localStorage.setItem(
ADVANCED_SETTINGS_KET,
JSON.stringify([...data, { encoded, active: false, id: data.length + 1 }])
);
} else {
localStorage.setItem(
ADVANCED_SETTINGS_KET,
JSON.stringify([{ encoded, active: true, id: 1 }])
);
}
};
export const getAdvancedSettings = (): AdvancedAppSettings => {
export const getAdvancedSettings = (): IAdvancedAppSettings[] => {
const str = localStorage.getItem(ADVANCED_SETTINGS_KET);
if (str) {
const planText = Buffer.from(str, "base64").toString("utf-8");
return JSON.parse(planText) as AdvancedAppSettings;
const data: { active: boolean; encoded: string; id: number }[] =
JSON.parse(str);
const decoded: IAdvancedAppSettings[] = data.map((el) => {
return {
active: el.active,
decoded: JSON.parse(
Buffer.from(el.encoded, "base64").toString("utf-8")
),
id: el.id,
};
});
return decoded;
// const planText = Buffer.from(str, "base64").toString("utf-8");
// return JSON.parse(planText) as AppSettings;
}
return {} as AdvancedAppSettings;
// return {} as AppSettings;
return [] as IAdvancedAppSettings[];
// return [] as IAppSettings[];
};
export const getActiveAdvancedSettings = (): IAdvancedAppSettings => {
const str = localStorage.getItem(ADVANCED_SETTINGS_KET);
if (str) {
const data: { active: boolean; encoded: string; id: number }[] =
JSON.parse(str);
const decoded: IAdvancedAppSettings[] = data.map((el) => {
return {
active: el.active,
decoded: JSON.parse(
Buffer.from(el.encoded, "base64").toString("utf-8")
),
id: el.id,
};
});
return decoded.find((el) => el.active) as IAdvancedAppSettings;
// const planText = Buffer.from(str, "base64").toString("utf-8");
// return JSON.parse(planText) as AppSettings;
}
// return {} as AppSettings;
return {} as IAdvancedAppSettings;
// return [] as IAppSettings[];
};
// Call History
const historyKey = "History";
const MAX_HISTORY_COUNT = 20;
export const saveCallHistory = (username: string, call: CallHistory) => {

View File

@@ -47,4 +47,9 @@ const mainTheme = extendTheme({
},
});
export const colors = {
//to use outside of chakra component
jambonz: "#DA1C5C",
};
export default mainTheme;

15
src/utils/formatDate.ts Normal file
View File

@@ -0,0 +1,15 @@
// fn to format call date:
export function transform(t1: number, t2: number) {
const diff = Math.abs(t1 - t2) / 1000; // Get the difference in seconds
const hours = Math.floor(diff / 3600);
const minutes = Math.floor((diff % 3600) / 60);
const seconds = Math.floor(diff % 60);
// Pad the values with a leading zero if they are less than 10
const hours1 = hours < 10 ? "0" + hours : hours;
const minutes1 = minutes < 10 ? "0" + minutes : minutes;
const seconds1 = seconds < 10 ? "0" + seconds : seconds;
return `${hours1}:${minutes1}:${seconds1}`;
}

View File

@@ -5,25 +5,17 @@ import {
TabPanel,
TabPanels,
Tabs,
Text,
Grid,
Center,
HStack,
Image,
} from "@chakra-ui/react";
import Phone from "./phone";
import Settings from "./settings";
import { DEFAULT_COLOR_SCHEME } from "src/common/constants";
import { useEffect, useState } from "react";
import {
getAdvancedSettings,
getCallHistories,
getSettings,
} from "src/storage";
import { getActiveSettings, getCallHistories, getSettings } from "src/storage";
import jambonz from "src/imgs/jambonz.svg";
import CallHistories from "./history";
import { AdvancedAppSettings, CallHistory } from "src/common/types";
import { CallHistory, IAppSettings, SipClientStatus } from "src/common/types";
import Footer from "./footer/footer";
export const WindowApp = () => {
const [sipDomain, setSipDomain] = useState("");
@@ -35,9 +27,26 @@ export const WindowApp = () => {
const [calledNumber, setCalledNumber] = useState("");
const [calledName, setCalledName] = useState("");
const [tabIndex, setTabIndex] = useState(0);
const [advancedSettings, setAdvancedSettings] = useState<AdvancedAppSettings>(
getAdvancedSettings()
const [status, setStatus] = useState<SipClientStatus>("stop");
const [allSettings, setAllSettings] = useState<IAppSettings[]>([]);
const [advancedSettings, setAdvancedSettings] = useState<IAppSettings | null>(
null
);
const loadSettings = () => {
const settings = getSettings();
const activeSettings = settings.find((el) => el.active);
setAllSettings(getSettings());
setAdvancedSettings(getActiveSettings());
setSipDomain(activeSettings?.decoded.sipDomain || "");
setSipServerAddress(activeSettings?.decoded.sipServerAddress || "");
setSipUsername(activeSettings?.decoded.sipUsername || "");
setSipPassword(activeSettings?.decoded.sipPassword || "");
setSipDisplayName(activeSettings?.decoded.sipDisplayName || "");
};
const tabsSettings = [
{
title: "Dialer",
@@ -50,7 +59,10 @@ export const WindowApp = () => {
sipServerAddress={sipServerAddress}
calledNumber={[calledNumber, setCalledNumber]}
calledName={[calledName, setCalledName]}
stat={[status, setStatus]}
advancedSettings={advancedSettings}
allSettings={allSettings}
reload={loadSettings}
/>
),
},
@@ -83,16 +95,6 @@ export const WindowApp = () => {
setTabIndex(i);
setCallHistories(getCallHistories(sipUsername));
};
const loadSettings = () => {
const settings = getSettings();
setAdvancedSettings(getAdvancedSettings());
setSipDomain(settings.sipDomain);
setSipServerAddress(settings.sipServerAddress);
setSipUsername(settings.sipUsername);
setSipPassword(settings.sipPassword);
setSipDisplayName(settings.sipDisplayName);
};
return (
<Grid h="100vh" templateRows="1fr auto">
<Box p={2}>
@@ -122,12 +124,16 @@ export const WindowApp = () => {
</TabPanels>
</Tabs>
</Box>
<Center>
<HStack spacing={1} mb={2} align="start">
<Text fontSize="14px">Powered by</Text>
<Image src={jambonz} alt="Jambonz Logo" w="91px" h="31px" />
</HStack>
</Center>
<Footer
sipServerAddress={sipServerAddress}
sipUsername={sipUsername}
sipDomain={sipDomain}
sipDisplayName={sipDisplayName}
sipPassword={sipPassword}
status={status}
setStatus={setStatus}
/>
</Grid>
);
};

View File

@@ -0,0 +1,193 @@
import { HStack, Image, Text, useToast } from "@chakra-ui/react";
import jambonz from "src/imgs/jambonz.svg";
import { Dispatch, SetStateAction, useEffect, useRef, useState } from "react";
import { SipClientStatus } from "src/common/types";
import { SipConstants, SipUA } from "src/lib";
import { DEFAULT_TOAST_DURATION } from "src/common/constants";
import JambonzSwitch from "src/components/switch";
import "./styles.scss";
function Footer({
status,
setStatus,
sipServerAddress,
sipUsername,
sipDomain,
sipPassword,
sipDisplayName,
}: {
status: string;
setStatus: Dispatch<SetStateAction<SipClientStatus>>;
sipServerAddress: string;
sipUsername: string;
sipDomain: string;
sipPassword: string;
sipDisplayName: string;
}) {
const [isConfigured, setIsConfigured] = useState(false);
const [isSwitchingUserStatus, setIsSwitchingUserStatus] = useState(false);
const [isOnline, setIsOnline] = useState(false);
const sipUA = useRef<SipUA | null>(null);
const sipUsernameRef = useRef("");
const sipPasswordRef = useRef("");
const sipServerAddressRef = useRef("");
const sipDomainRef = useRef("");
const sipDisplayNameRef = useRef("");
const unregisteredReasonRef = useRef("");
const isRestartRef = useRef(false);
const toast = useToast();
useEffect(() => {
if (status === "registered" || status === "disconnected") {
setIsSwitchingUserStatus(false);
setIsOnline(status === "registered");
}
}, [status]);
useEffect(() => {
sipDomainRef.current = sipDomain;
sipUsernameRef.current = sipUsername;
sipPasswordRef.current = sipPassword;
sipServerAddressRef.current = sipServerAddress;
sipDisplayNameRef.current = sipDisplayName;
if (sipDomain && sipUsername && sipPassword && sipServerAddress) {
if (sipUA.current) {
if (sipUA.current.isConnected()) {
clientGoOffline();
isRestartRef.current = true;
} else {
createSipClient();
}
} else {
createSipClient();
}
setIsConfigured(true);
} else {
setIsConfigured(false);
clientGoOffline();
}
}, [sipDomain, sipUsername, sipPassword, sipServerAddress, sipDisplayName]);
const clientGoOffline = () => {
if (sipUA.current) {
sipUA.current.stop();
sipUA.current = null;
}
};
const handleGoOffline = (s: SipClientStatus) => {
if (s === status) {
return;
}
if (s === "unregistered") {
if (sipUA.current) {
sipUA.current.stop();
}
} else {
if (sipUA.current) {
sipUA.current.start();
}
}
};
const createSipClient = () => {
setIsSwitchingUserStatus(true);
const client = {
username: `${sipUsernameRef.current}@${sipDomainRef.current}`,
password: sipPasswordRef.current,
name: sipDisplayNameRef.current ?? sipUsernameRef.current,
};
const settings = {
pcConfig: {
iceServers: [{ urls: ["stun:stun.l.google.com:19302"] }],
},
wsUri: sipServerAddressRef.current,
register: true,
};
const sipClient = new SipUA(client, settings);
// UA Status
sipClient.on(SipConstants.UA_REGISTERED, (args) => {
setStatus("registered");
});
sipClient.on(SipConstants.UA_UNREGISTERED, (args) => {
setStatus("unregistered");
if (sipUA.current) {
sipUA.current.stop();
}
unregisteredReasonRef.current = `User is not registered${
args.cause ? `, ${args.cause}` : ""
}`;
});
sipClient.on(SipConstants.UA_DISCONNECTED, (args) => {
if (unregisteredReasonRef.current) {
toast({
title: unregisteredReasonRef.current,
status: "warning",
duration: DEFAULT_TOAST_DURATION,
isClosable: true,
});
unregisteredReasonRef.current = "";
}
setStatus("disconnected");
if (isRestartRef.current) {
createSipClient();
isRestartRef.current = false;
}
if (args.error) {
toast({
title: `Cannot connect to ${sipServerAddress}, ${args.reason}`,
status: "warning",
duration: DEFAULT_TOAST_DURATION,
isClosable: true,
});
}
});
sipClient.start();
sipUA.current = sipClient;
};
return (
<HStack
paddingX={6}
mb={2}
justifyContent={"space-between"}
alignItems={"center"}
>
{isConfigured ? (
<HStack alignItems={"center"} flexWrap={"nowrap"} className="xs">
<JambonzSwitch
isDisabled={isSwitchingUserStatus}
checked={[isOnline, setIsOnline]}
onChange={(v) => {
setIsSwitchingUserStatus(true);
handleGoOffline(v ? "registered" : "unregistered");
}}
/>
<Text>You are {isOnline ? "online" : "offline"}</Text>
</HStack>
) : (
<span></span>
)}
<HStack
spacing={1}
alignItems={"start"}
justify={"flex-end"}
flexWrap={"wrap"}
>
<Text fontSize="14px">Powered by</Text>
<Image src={jambonz} alt="Jambonz Logo" w="91px" h="31px" />
</HStack>
</HStack>
);
}
export default Footer;

View File

@@ -0,0 +1,5 @@
@media screen and (max-width: 400px) {
.xs {
font-size: 12px;
}
}

View File

@@ -102,9 +102,10 @@ export const CallHistoryItem = ({
return;
}
const settings = getSettings();
if (settings.sipUsername) {
const activeSettings = settings.find(el => el.active);
if (activeSettings?.decoded.sipUsername) {
isSaveCallHistory(
settings.sipUsername,
activeSettings?.decoded.sipUsername,
call.callSid,
!call.isSaved
);

View File

@@ -0,0 +1,47 @@
import { HStack, Text, VStack } from "@chakra-ui/react";
import { faCheck } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import React from "react";
import { IAppSettings } from "src/common/types";
function AvailableAccounts({
allSettings,
onSetActive,
}: {
allSettings: IAppSettings[];
onSetActive: (x: number) => void;
}) {
return (
<VStack
w={"full"}
alignItems={"start"}
bg={"grey.200"}
borderRadius={"xl"}
className="absolute"
padding={3}
border={"1px"}
borderColor={"gray.400"}
boxShadow={"lg"}
>
{allSettings.map((el, i) => (
<HStack
key={i}
justifyContent={"start"}
_hover={{
cursor: "pointer",
}}
onClick={() => onSetActive(el.id)}
>
{el.active && <FontAwesomeIcon icon={faCheck} />}
<Text marginLeft={el.active ? "-0.5" : "5"}>
{el.decoded.sipDisplayName || el.decoded.sipUsername}
</Text>
&nbsp;
<Text>({`${el.decoded.sipUsername}@${el.decoded.sipDomain}`})</Text>
</HStack>
))}
</VStack>
);
}
export default AvailableAccounts;

View File

@@ -1,6 +1,6 @@
import {
Box,
Button,
Center,
Circle,
HStack,
Heading,
@@ -13,9 +13,9 @@ import {
VStack,
useToast,
} from "@chakra-ui/react";
import { useEffect, useRef, useState } from "react";
import { Dispatch, SetStateAction, useEffect, useRef, useState } from "react";
import {
AdvancedAppSettings,
IAppSettings,
SipCallDirection,
SipClientStatus,
} from "src/common/types";
@@ -36,6 +36,7 @@ import {
getCurrentCall,
saveCallHistory,
saveCurrentCall,
setActiveSettings,
} from "src/storage";
import { OutGoingCall } from "./outgoing-call";
import { v4 as uuidv4 } from "uuid";
@@ -47,10 +48,10 @@ import {
getRegisteredUser,
getSelfRegisteredUser,
} from "src/api";
import JambonzSwitch from "src/components/switch";
import { DEFAULT_TOAST_DURATION } from "src/common/constants";
import { RegisteredUser } from "src/api/types";
import {
faChevronDown,
faCodeMerge,
faList,
faMicrophone,
@@ -63,6 +64,8 @@ import {
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import JoinConference from "./conference";
import AnimateOnShow from "src/components/animate";
import AvailableAccounts from "./availableAccounts";
type PhoneProbs = {
sipDomain: string;
@@ -72,7 +75,10 @@ type PhoneProbs = {
sipDisplayName: string;
calledNumber: [string, React.Dispatch<React.SetStateAction<string>>];
calledName: [string, React.Dispatch<React.SetStateAction<string>>];
advancedSettings: AdvancedAppSettings;
advancedSettings: IAppSettings | null;
stat: [string, Dispatch<SetStateAction<SipClientStatus>>];
allSettings: IAppSettings[];
reload: () => void;
};
enum PAGE_VIEW {
@@ -82,19 +88,24 @@ enum PAGE_VIEW {
JOIN_CONFERENCE,
}
// add some basic details to advanced to match them, make basic compulsory to fill advanced.
export const Phone = ({
sipDomain,
sipServerAddress,
sipUsername,
sipPassword,
sipDisplayName,
stat: [status, setStatus],
calledNumber: [calledANumber, setCalledANumber],
calledName: [calledAName, setCalledAName],
advancedSettings,
allSettings,
reload,
}: PhoneProbs) => {
const [inputNumber, setInputNumber] = useState("");
const [appName, setAppName] = useState("");
const [status, setStatus] = useState<SipClientStatus>("stop");
// const [status, setStatus] = useState<SipClientStatus>("stop");
const [isConfigured, setIsConfigured] = useState(false);
const [callStatus, setCallStatus] = useState(SipConstants.SESSION_ENDED);
const [sessionDirection, setSessionDirection] =
@@ -116,6 +127,8 @@ export const Phone = ({
const [callSid, setCallSid] = useState("");
const [showConference, setShowConference] = useState(false);
const [showAccounts, setShowAccounts] = useState(false);
const inputNumberRef = useRef(inputNumber);
const sessionDirectionRef = useRef(sessionDirection);
const sipUA = useRef<SipUA | null>(null);
@@ -156,7 +169,7 @@ export const Phone = ({
}
fetchRegisterUser();
getConferences()
.then(() => {
?.then(() => {
setShowConference(true);
})
.catch(() => {
@@ -165,7 +178,7 @@ export const Phone = ({
}, [sipDomain, sipUsername, sipPassword, sipServerAddress, sipDisplayName]);
useEffect(() => {
setIsAdvancedMode(!!advancedSettings.accountSid);
setIsAdvancedMode(!!advancedSettings?.decoded?.accountSid);
fetchRegisterUser();
}, [advancedSettings]);
@@ -440,21 +453,6 @@ export const Phone = ({
}
};
const handleGoOffline = (s: SipClientStatus) => {
if (s === status) {
return;
}
if (s === "unregistered") {
if (sipUA.current) {
sipUA.current.stop();
}
} else {
if (sipUA.current) {
sipUA.current.start();
}
}
};
const handleHangup = () => {
if (isSipClientAnswered(callStatus) || isSipClientRinging(callStatus)) {
sipUA.current?.terminate(480, "Call Finished", undefined);
@@ -497,47 +495,68 @@ export const Phone = ({
return status === "registered";
};
const handleSetActive = (id: number) => {
setActiveSettings(id);
setShowAccounts(false);
reload();
};
return (
<Center flexDirection="column">
<Box flexDirection="column">
{isConfigured ? (
<>
<HStack spacing={2} boxShadow="md" w="full" borderRadius={5} p={2}>
<Image
src={isStatusRegistered() ? GreenAvatar : Avatar}
boxSize="35px"
/>
<VStack alignItems="start" w="full" spacing={0}>
<HStack spacing={2} w="full">
<Text fontWeight="bold" fontSize="13px">
{sipDisplayName || sipUsername}
</Text>
<Circle
size="8px"
bg={isStatusRegistered() ? "green.500" : "gray.500"}
/>
</HStack>
<Text fontWeight="bold" w="full">
{`${sipUsername}@${sipDomain}`}
</Text>
</VStack>
<Spacer />
<VStack h="full" align="center">
<JambonzSwitch
isDisabled={isSwitchingUserStatus}
onlabel="Online"
offLabel="Offline"
checked={[isOnline, setIsOnline]}
onChange={(v) => {
setIsSwitchingUserStatus(true);
handleGoOffline(v ? "registered" : "unregistered");
}}
<Text fontSize={"small"} fontWeight={"semibold"} color={"gray.600"}>
Account
</Text>
<Box className="relative" w={"full"}>
<HStack
onClick={() => setShowAccounts(true)}
_hover={{
cursor: "pointer",
}}
spacing={2}
boxShadow="md"
w="full"
borderRadius={5}
paddingY={2}
paddingX={3.5}
>
<Image
src={isStatusRegistered() ? GreenAvatar : Avatar}
boxSize="35px"
/>
</VStack>
</HStack>
<VStack alignItems="start" w="full" spacing={0}>
<HStack spacing={2} w="full">
<Text fontWeight="bold" fontSize="13px">
{sipDisplayName || sipUsername}
</Text>
<Circle
size="8px"
bg={isStatusRegistered() ? "green.500" : "gray.500"}
/>
</HStack>
<Text fontWeight="bold" w="full">
{`${sipUsername}@${sipDomain}`}
</Text>
</VStack>
<Spacer />
<VStack h="full" align="center">
<FontAwesomeIcon icon={faChevronDown} />
</VStack>
</HStack>
{showAccounts && (
<AnimateOnShow initial={2} exit={0} duration={0.01}>
<AvailableAccounts
allSettings={allSettings}
onSetActive={handleSetActive}
/>
</AnimateOnShow>
)}
</Box>
</>
) : (
<Heading size="md" mb={2}>
<Heading textAlign={"center"} size="md" mb={2}>
Go to Settings to configure your account
</Heading>
)}
@@ -822,7 +841,7 @@ export const Phone = ({
}}
/>
)}
</Center>
</Box>
);
};

View File

@@ -2,3 +2,11 @@
filter: blur(5px);
pointer-events: none;
}
.relative {
position: relative;
}
.absolute {
position: absolute;
top: 5px;
left: 0;
}

View File

@@ -0,0 +1,50 @@
import {
Accordion,
AccordionButton,
AccordionItem,
AccordionPanel,
useDisclosure,
} from "@chakra-ui/react";
import { useState } from "react";
import { IAppSettings } from "src/common/types";
import AccountForm from "src/window/settings/accountForm";
import SettingItem from "src/window/settings/settingItem";
export function AccordionList({
allSettings,
reload,
}: {
allSettings: IAppSettings[];
reload: () => void;
}) {
const { isOpen, onToggle } = useDisclosure();
const [openAcc, setOpenAcc] = useState(0);
const closeFormInAccordion = function () {
reload();
onToggle();
};
function handleToggleAcc(accIndex: number) {
setOpenAcc(accIndex);
onToggle();
}
return (
<Accordion index={isOpen ? [openAcc] : []} allowToggle>
{allSettings.map((data, index) => (
<AccordionItem key={index}>
<AccordionButton onClick={() => handleToggleAcc(index)}>
<SettingItem data={data} />
</AccordionButton>
<AccordionPanel pb={4}>
<AccountForm
formData={data}
handleClose={closeFormInAccordion}
inputUniqueId={`${data.id}`}
/>
</AccordionPanel>
</AccordionItem>
))}
</Accordion>
);
}

View File

@@ -0,0 +1,298 @@
import {
Box,
Button,
Center,
FormControl,
FormLabel,
HStack,
Input,
Text,
useToast,
VStack,
} from "@chakra-ui/react";
import React, { useEffect, useState } from "react";
import { DEFAULT_TOAST_DURATION } from "src/common/constants";
import { AppSettings, IAppSettings } from "src/common/types";
import PasswordInput from "src/components/password-input";
import { deleteSettings, editSettings, saveSettings } from "src/storage";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faCheckCircle,
faCircleXmark,
faShuffle,
faTrashCan,
} from "@fortawesome/free-solid-svg-icons";
import { normalizeUrl } from "src/utils";
import { getApplications } from "src/api";
import { colors } from "src/theme";
function AccountForm({
closeForm,
formData,
handleClose,
inputUniqueId,
}: {
closeForm?: () => void;
formData?: IAppSettings;
handleClose?: () => void;
inputUniqueId?: string; //duplicate form input id error fix
}) {
const [showAdvanced, setShowAdvanced] = useState(false);
const [sipDomain, setSipDomain] = useState("");
const [sipServerAddress, setSipServerAddress] = useState("");
const [sipUsername, setSipUsername] = useState("");
const [sipPassword, setSipPassword] = useState("");
const [sipDisplayName, setSipDisplayName] = useState("");
const [apiKey, setApiKey] = useState<string>("");
const [apiServer, setApiServer] = useState<string | undefined>("");
const [accountSid, setAccountSid] = useState<string | undefined>("");
const [isCredentialOk, setIsCredentialOk] = useState<boolean>(false);
const [isAdvancedMode, setIsAdvancedMode] = useState<boolean>(false);
const toast = useToast();
useEffect(
function () {
if (formData) {
setSipDisplayName(formData.decoded.sipDisplayName);
setSipDomain(formData.decoded.sipDomain);
setSipServerAddress(formData.decoded.sipServerAddress);
setSipUsername(formData.decoded.sipUsername);
setSipPassword(formData.decoded.sipPassword);
setAccountSid(formData.decoded.accountSid);
setApiKey(formData.decoded.apiKey || "");
setApiServer(formData.decoded.apiServer);
}
},
[formData, handleClose]
);
const checkCredential = () => {
getApplications()
.then(() => {
setIsCredentialOk(true);
})
.catch(() => {
setIsCredentialOk(false);
});
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const settings: AppSettings = {
sipDomain,
sipServerAddress,
sipUsername,
sipPassword,
sipDisplayName,
accountSid,
apiKey,
apiServer: apiServer ? normalizeUrl(apiServer) : "",
};
formData ? editSettings(settings, formData.id) : saveSettings(settings);
if (showAdvanced) {
setIsAdvancedMode(true);
checkCredential();
}
toast({
title: "Settings saved successfully",
status: "success",
duration: DEFAULT_TOAST_DURATION,
isClosable: true,
colorScheme: "jambonz",
});
if (formData) {
handleClose && handleClose();
} else {
closeForm && closeForm();
}
};
const handleDeleteSetting = (id: number) => {
deleteSettings(id);
if (formData) {
handleClose && handleClose();
} else {
closeForm && closeForm();
}
};
const resetSetting = () => {
// saveSettings({} as AppSettings);
setSipDomain("");
setSipServerAddress("");
setSipUsername("");
setSipPassword("");
setSipDisplayName("");
setApiKey("");
setApiServer("");
setAccountSid("");
setIsAdvancedMode(false);
if (formData) {
handleClose && handleClose();
} else {
closeForm && closeForm();
}
};
return (
<form onSubmit={handleSubmit}>
<VStack spacing={2} w="full" p={0}>
<VStack spacing={2} w="full" p={0}>
<FormControl id={`sip_display_name${inputUniqueId}`}>
<FormLabel>SIP Display Name (Optional)</FormLabel>
<Input
type="text"
placeholder="Display name"
value={sipDisplayName}
onChange={(e) => setSipDisplayName(e.target.value)}
/>
</FormControl>
<FormControl id={`jambonz_sip_domain${inputUniqueId}`}>
<FormLabel>Jambonz SIP Domain</FormLabel>
<Input
type="text"
placeholder="Domain"
isRequired
value={sipDomain}
onChange={(e) => setSipDomain(e.target.value)}
/>
</FormControl>
<FormControl id={`jambonz_server_address${inputUniqueId}`}>
<FormLabel>Jambonz Server Address</FormLabel>
<Input
type="text"
placeholder="wss://sip.jambonz.cloud:8443/"
isRequired
value={sipServerAddress}
onChange={(e) => setSipServerAddress(e.target.value)}
/>
</FormControl>
<FormControl id={`username${inputUniqueId}`}>
<FormLabel>SIP Username</FormLabel>
<Input
type="text"
placeholder="Username"
isRequired
value={sipUsername}
onChange={(e) => setSipUsername(e.target.value)}
/>
</FormControl>
<FormControl id={`password${inputUniqueId}`}>
<FormLabel fontWeight="">SIP Password</FormLabel>
<PasswordInput
password={[sipPassword, setSipPassword]}
placeHolder="Enter your password"
/>
</FormControl>
{showAdvanced && (
<VStack w={"full"} bg={"gray.50"} borderRadius={"2xl"} p={"3.5"}>
<FormControl id={`jambonz_api_server${inputUniqueId}`}>
<FormLabel>Jambonz API Server Base URL</FormLabel>
<Input
type="text"
placeholder="https://jambonz.cloud/api"
isRequired
value={apiServer}
onChange={(e) => setApiServer(e.target.value)}
/>
</FormControl>
<FormControl id={`jambonz_account_sid${inputUniqueId}`}>
<FormLabel>Jambonz Account Sid</FormLabel>
<Input
type="text"
isRequired
value={accountSid}
onChange={(e) => setAccountSid(e.target.value)}
/>
</FormControl>
<FormControl id={`api_key${inputUniqueId}`}>
<FormLabel>API Key</FormLabel>
<PasswordInput
password={[apiKey || "", setApiKey]}
isRequired
/>
</FormControl>
</VStack>
)}
<Center flexDirection={"column"} gap={"2.5"}>
<Button
gap={"2.5"}
alignItems={"center"}
onClick={() => setShowAdvanced((prev) => !prev)}
>
<Text textColor={"jambonz.500"}>
{" "}
{showAdvanced ? "Hide" : "Show"} Advanced Settings
</Text>
<FontAwesomeIcon color={colors.jambonz} icon={faShuffle} />
</Button>
</Center>
</VStack>
{isAdvancedMode && (
<HStack w="full" mt={2} mb={2}>
<Box
as={FontAwesomeIcon}
icon={isCredentialOk ? faCheckCircle : faCircleXmark}
color={isCredentialOk ? "green.500" : "red.500"}
/>
<Text
fontSize="14px"
color={isCredentialOk ? "green.500" : "red.500"}
>
Credential is {isCredentialOk ? "valid" : "invalid"}
</Text>
</HStack>
)}
<HStack
w="full"
alignItems="center"
justifyContent={"space-between"}
mt={2}
>
<HStack>
<Button colorScheme="jambonz" type="submit" w="full">
Save
</Button>
<Button
colorScheme="jambonz"
type="reset"
w="full"
onClick={resetSetting}
>
Cancel
</Button>
</HStack>
<HStack
_hover={{
cursor: "pointer",
}}
>
{formData && (
<FontAwesomeIcon
onClick={() => handleDeleteSetting(formData.id)}
icon={faTrashCan}
color={colors.jambonz}
/>
)}
</HStack>
</HStack>
</VStack>
</form>
);
}
export default AccountForm;

View File

@@ -33,16 +33,19 @@ export const AdvancedSettings = () => {
useEffect(() => {
const settings = getAdvancedSettings();
if (settings.apiServer) {
const activeSettings = settings.find(
(el: { active: boolean }) => el.active
);
if (activeSettings?.decoded.apiServer) {
setIsAdvancedMode(true);
checkCredential();
setApiServer(settings.apiServer);
setApiServer(activeSettings?.decoded.apiServer);
}
if (settings.apiKey) {
setApiKey(settings.apiKey);
if (activeSettings?.decoded.apiKey) {
setApiKey(activeSettings?.decoded.apiKey);
}
if (settings.accountSid) {
setAccountSid(settings.accountSid);
if (activeSettings?.decoded.accountSid) {
setAccountSid(activeSettings?.decoded.accountSid);
}
}, []);

View File

@@ -1,12 +1,10 @@
import {
Box,
Button,
FormControl,
FormLabel,
HStack,
Image,
Input,
Spacer,
Text,
VStack,
useToast,
@@ -59,20 +57,22 @@ export const BasicSettings = () => {
useEffect(() => {
const settings = getSettings();
if (settings.sipDomain) {
setSipDomain(settings.sipDomain);
const activeSettings = settings.find((el) => el.active);
if (activeSettings?.decoded.sipDomain) {
setSipDomain(activeSettings?.decoded.sipDomain);
}
if (settings.sipServerAddress) {
setSipServerAddress(settings.sipServerAddress);
if (activeSettings?.decoded.sipServerAddress) {
setSipServerAddress(activeSettings?.decoded.sipServerAddress);
}
if (settings.sipUsername) {
setSipUsername(settings.sipUsername);
if (activeSettings?.decoded.sipUsername) {
setSipUsername(activeSettings?.decoded.sipUsername);
}
if (settings.sipPassword) {
setSipPassword(settings.sipPassword);
if (activeSettings?.decoded.sipPassword) {
setSipPassword(activeSettings?.decoded.sipPassword);
}
if (settings.sipDisplayName) {
setSipDisplayName(settings.sipDisplayName);
if (activeSettings?.decoded.sipDisplayName) {
setSipDisplayName(activeSettings?.decoded.sipDisplayName);
}
}, []);
return (

View File

@@ -1,26 +1,82 @@
import { Tab, TabList, TabPanel, TabPanels, Tabs } from "@chakra-ui/react";
import React from "react";
import { DEFAULT_COLOR_SCHEME } from "src/common/constants";
import BasicSettings from "./basic";
import AdvancedSettings from "./advanced";
import { Box, Button, Center, Text } from "@chakra-ui/react";
import { useEffect, useState } from "react";
import { getSettings } from "src/storage";
import { IAppSettings } from "src/common/types";
import AccountForm from "./accountForm";
import AnimateOnShow from "src/components/animate";
import { AccordionList } from "src/window/settings/accordionList";
const MAX_NUM_OF_ACCOUNTS = 5;
export const Settings = () => {
return (
<Tabs isFitted colorScheme={DEFAULT_COLOR_SCHEME}>
<TabList mb="1em" gap={1}>
<Tab>Basic</Tab>
<Tab>Advanced</Tab>
</TabList>
const [showForm, setShowForm] = useState(false);
const [allSettings, setAllSettings] = useState<IAppSettings[]>([]);
<TabPanels mt={1}>
<TabPanel p={0}>
<BasicSettings />
</TabPanel>
<TabPanel p={0}>
<AdvancedSettings />
</TabPanel>
</TabPanels>
</Tabs>
useEffect(
function () {
loadSettings();
},
[showForm]
);
function handleOpenForm() {
setShowForm(true);
}
function handleCloseForm() {
setShowForm(false);
}
const loadSettings = function () {
setAllSettings(getSettings());
};
return (
<div>
<Box>
<AccordionList allSettings={allSettings} reload={loadSettings} />
</Box>
{!showForm && (
<Button
marginY={"2.5"}
colorScheme="jambonz"
w="full"
onClick={handleOpenForm}
isDisabled={allSettings.length >= MAX_NUM_OF_ACCOUNTS}
>
Add Account
</Button>
)}
<Center marginBottom={"2.5"} flexDirection={"column"}>
<Text>
{allSettings.length} of {MAX_NUM_OF_ACCOUNTS}{" "}
</Text>
{allSettings.length >= MAX_NUM_OF_ACCOUNTS && (
<Text>Limit has been reached</Text>
)}
</Center>
{showForm && (
<AnimateOnShow>
<AccountForm closeForm={handleCloseForm} />
</AnimateOnShow>
)}
{/* <Tabs isFitted colorScheme={DEFAULT_COLOR_SCHEME}>
<TabList mb="1em" gap={1}>
<Tab>Basic</Tab>
<Tab>Advanced</Tab>
</TabList>
<TabPanels mt={1}>
<TabPanel p={0}>
<BasicSettings />
</TabPanel>
<TabPanel p={0}>
<AdvancedSettings />
</TabPanel>
</TabPanels>
</Tabs> */}
</div>
);
};

View File

@@ -0,0 +1,27 @@
import { HStack, Text, VStack } from "@chakra-ui/react";
import React from "react";
import { IAppSettings } from "src/common/types";
function SettingItem({ data }: { data: IAppSettings }) {
return (
<HStack
w={"full"}
display={"flex"}
marginY={"1.5"}
border={"1px"}
borderColor={"gray.200"}
justifyContent={"start"}
borderRadius={"2xl"}
padding={"2.5"}
>
<VStack gap={"0"} alignItems={"start"}>
<Text fontWeight={"bold"}>
{data.decoded.sipDisplayName || data.decoded.sipUsername}
</Text>
<Text>{`${data.decoded.sipUsername}@${data.decoded.sipDomain}`}</Text>
</VStack>
</HStack>
);
}
export default SettingItem;