mirror of
https://github.com/jambonz/chrome-extension-dialer.git
synced 2025-12-19 04:47:45 +00:00
done with implementation
This commit is contained in:
29
.gitignore
vendored
29
.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
27
src/components/animate/index.tsx
Normal file
27
src/components/animate/index.tsx
Normal 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;
|
||||
@@ -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%)"
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
15
src/utils/formatDate.ts
Normal 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}`;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
193
src/window/footer/footer.tsx
Normal file
193
src/window/footer/footer.tsx
Normal 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;
|
||||
5
src/window/footer/styles.scss
Normal file
5
src/window/footer/styles.scss
Normal file
@@ -0,0 +1,5 @@
|
||||
@media screen and (max-width: 400px) {
|
||||
.xs {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
|
||||
47
src/window/phone/availableAccounts.tsx
Normal file
47
src/window/phone/availableAccounts.tsx
Normal 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>
|
||||
|
||||
<Text>({`${el.decoded.sipUsername}@${el.decoded.sipDomain}`})</Text>
|
||||
</HStack>
|
||||
))}
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
|
||||
export default AvailableAccounts;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -2,3 +2,11 @@
|
||||
filter: blur(5px);
|
||||
pointer-events: none;
|
||||
}
|
||||
.relative {
|
||||
position: relative;
|
||||
}
|
||||
.absolute {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
50
src/window/settings/accordionList.tsx
Normal file
50
src/window/settings/accordionList.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
298
src/window/settings/accountForm.tsx
Normal file
298
src/window/settings/accountForm.tsx
Normal 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;
|
||||
@@ -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);
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
27
src/window/settings/settingItem.tsx
Normal file
27
src/window/settings/settingItem.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user