Merge pull request #34 from mister-abdurahman/main

Allow adding multiple accounts
This commit is contained in:
Hoan Luu Huu
2024-09-16 20:07:30 +07:00
committed by GitHub
27 changed files with 1882 additions and 818 deletions

2
.gitignore vendored
View File

@@ -24,4 +24,4 @@
npm-debug.log*
yarn-debug.log*
yarn-error.log*
yarn-error.log*

Binary file not shown.

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,47 @@ 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`
);
};
// validate user advanced credential
export const getAdvancedValidation = (
apiServer: string,
accountSid: string
) => {
return getFetch<Application[]>(
`${apiServer}/Accounts/${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 +178,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,28 @@
import React, { ReactNode } from "react";
import { motion } from "framer-motion";
function AnimateOnShow({
children,
initial = -20,
exit = -20,
duration = 0.5,
}: {
children: ReactNode;
initial?: number;
exit?: number;
duration?: number;
}) {
return (
<motion.div
initial={{ opacity: 0, y: initial, height: 0 }}
animate={{ opacity: 1, y: 0, height: "auto" }}
exit={{ opacity: 0, y: exit, height: 0 }}
transition={{ duration: duration }}
style={{ width: "100%" }}
>
{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,9 +17,9 @@ function JambonzSwitch({
return (
<Box
position="relative"
w="90px"
w="50px"
h="30px"
bg={isToggled ? "green.500" : "grey.500"}
bg={isToggled ? "blue.600" : "grey.500"}
borderRadius="full"
onClick={() => {
if (!isDisabled) {
@@ -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

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="16" viewBox="0 0 15 16" fill="none">
<path d="M10.5994 6.59375C10.3405 6.59375 10.1306 6.38388 10.1306 6.125L10.1306 1.75C10.1306 1.49112 10.3405 1.28125 10.5994 1.28125C10.8583 1.28125 11.0681 1.49112 11.0681 1.75L11.0681 6.125C11.0681 6.38388 10.8583 6.59375 10.5994 6.59375Z" fill="#BB225B"/>
<path d="M4.375 8.3125C3.33947 8.3125 2.5 7.47303 2.5 6.4375C2.5 5.40197 3.33947 4.5625 4.375 4.5625C5.41053 4.5625 6.25 5.40197 6.25 6.4375C6.25 7.47303 5.41053 8.3125 4.375 8.3125Z" fill="#BB225B"/>
<path d="M10.625 7.6875C9.58947 7.6875 8.75 8.52697 8.75 9.5625C8.75 10.598 9.58947 11.4375 10.625 11.4375C11.6605 11.4375 12.5 10.598 12.5 9.5625C12.5 8.52697 11.6605 7.6875 10.625 7.6875Z" fill="#BB225B"/>
<path d="M3.88063 9.875C3.88063 9.61612 4.0905 9.40625 4.34938 9.40625C4.60826 9.40625 4.81813 9.61612 4.81813 9.875V14.25C4.81813 14.5089 4.60826 14.7188 4.34938 14.7188C4.0905 14.7188 3.88063 14.5089 3.88063 14.25V9.875Z" fill="#BB225B"/>
<path d="M10.5994 14.7188C10.3405 14.7188 10.1306 14.5089 10.1306 14.25V13C10.1306 12.7411 10.3405 12.5312 10.5994 12.5312C10.8583 12.5312 11.0681 12.7411 11.0681 13V14.25C11.0681 14.5089 10.8583 14.7188 10.5994 14.7188Z" fill="#BB225B"/>
<path d="M3.88063 1.75C3.88063 1.49112 4.0905 1.28125 4.34938 1.28125C4.60826 1.28125 4.81813 1.49112 4.81813 1.75V3C4.81813 3.25888 4.60826 3.46875 4.34938 3.46875C4.0905 3.46875 3.88063 3.25888 3.88063 3V1.75Z" fill="#BB225B"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

4
src/imgs/icons/Trash.svg Normal file
View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M3 6.38597C3 5.90152 3.34538 5.50879 3.77143 5.50879L6.43567 5.50832C6.96502 5.49306 7.43202 5.11033 7.61214 4.54412C7.61688 4.52923 7.62232 4.51087 7.64185 4.44424L7.75665 4.05256C7.8269 3.81241 7.8881 3.60318 7.97375 3.41617C8.31209 2.67736 8.93808 2.16432 9.66147 2.03297C9.84457 1.99972 10.0385 1.99986 10.2611 2.00002H13.7391C13.9617 1.99986 14.1556 1.99972 14.3387 2.03297C15.0621 2.16432 15.6881 2.67736 16.0264 3.41617C16.1121 3.60318 16.1733 3.81241 16.2435 4.05256L16.3583 4.44424C16.3778 4.51087 16.3833 4.52923 16.388 4.54412C16.5682 5.11033 17.1278 5.49353 17.6571 5.50879H20.2286C20.6546 5.50879 21 5.90152 21 6.38597C21 6.87043 20.6546 7.26316 20.2286 7.26316H3.77143C3.34538 7.26316 3 6.87043 3 6.38597Z" fill="#BB225B"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.5956 22.0001H12.4044C15.1871 22.0001 16.5785 22.0001 17.4831 21.1142C18.3878 20.2283 18.4803 18.7751 18.6654 15.8686L18.9321 11.6807C19.0326 10.1037 19.0828 9.31524 18.6289 8.81558C18.1751 8.31592 17.4087 8.31592 15.876 8.31592H8.12404C6.59127 8.31592 5.82488 8.31592 5.37105 8.81558C4.91722 9.31524 4.96744 10.1037 5.06788 11.6807L5.33459 15.8686C5.5197 18.7751 5.61225 20.2283 6.51689 21.1142C7.42153 22.0001 8.81289 22.0001 11.5956 22.0001ZM10.2463 12.1886C10.2051 11.7548 9.83753 11.4382 9.42537 11.4816C9.01321 11.525 8.71251 11.9119 8.75372 12.3457L9.25372 17.6089C9.29494 18.0427 9.66247 18.3593 10.0746 18.3159C10.4868 18.2725 10.7875 17.8856 10.7463 17.4518L10.2463 12.1886ZM14.5746 11.4816C14.9868 11.525 15.2875 11.9119 15.2463 12.3457L14.7463 17.6089C14.7051 18.0427 14.3375 18.3593 13.9254 18.3159C13.5132 18.2725 13.2125 17.8856 13.2537 17.4518L13.7537 12.1886C13.7949 11.7548 14.1625 11.4382 14.5746 11.4816Z" fill="#BB225B"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,3 @@
<svg width="18" height="15" viewBox="0 0 18 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.5 13.75C12.1576 13.75 13.7473 13.0915 14.9194 11.9194C16.0915 10.7473 16.75 9.1576 16.75 7.5C16.75 5.8424 16.0915 4.25269 14.9194 3.08058C13.7473 1.90848 12.1576 1.25 10.5 1.25C8.8424 1.25 7.25269 1.90848 6.08058 3.08058C4.90848 4.25269 4.25 5.8424 4.25 7.5C4.25 9.1576 4.90848 10.7473 6.08058 11.9194C7.25269 13.0915 8.8424 13.75 10.5 13.75ZM9.5625 10.3125C9.5625 10.0639 9.66127 9.8254 9.83709 9.64959C10.0129 9.47377 10.2514 9.375 10.5 9.375C10.7486 9.375 10.9871 9.47377 11.1629 9.64959C11.3387 9.8254 11.4375 10.0639 11.4375 10.3125C11.4375 10.5611 11.3387 10.7996 11.1629 10.9754C10.9871 11.1512 10.7486 11.25 10.5 11.25C10.2514 11.25 10.0129 11.1512 9.83709 10.9754C9.66127 10.7996 9.5625 10.5611 9.5625 10.3125ZM9.885 4.2625C9.91109 4.1184 9.98696 3.98803 10.0994 3.89416C10.2118 3.80028 10.3536 3.74886 10.5 3.74886C10.6464 3.74886 10.7882 3.80028 10.9006 3.89416C11.013 3.98803 11.0889 4.1184 11.115 4.2625L11.125 4.375V7.5L11.115 7.6125C11.0889 7.7566 11.013 7.88697 10.9006 7.98084C10.7882 8.07472 10.6464 8.12614 10.5 8.12614C10.3536 8.12614 10.2118 8.07472 10.0994 7.98084C9.98696 7.88697 9.91109 7.7566 9.885 7.6125L9.875 7.5V4.375L9.885 4.2625Z" fill="#BD4782"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -5,6 +5,7 @@ import mainTheme from "./theme";
import WindowApp from "./window/app";
// This file is being used only for dev.
const root = document.createElement("div");
root.className = "container";
document.body.appendChild(root);

View File

@@ -3,6 +3,8 @@ import {
AppSettings,
CallHistory,
ConferenceSettings,
IAdvancedAppSettings,
IAppSettings,
} from "src/common/types";
import { Buffer } from "buffer";
@@ -25,21 +27,115 @@ 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);
const str = localStorage.getItem(SETTINGS_KEY);
const parsed = str ? JSON.parse(str) : [];
const newItem = {
id: parsed.length + 1,
encoded,
active: parsed.length === 0,
};
localStorage.setItem(SETTINGS_KEY, JSON.stringify([...parsed, newItem]));
};
export const getSettings = (): 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 planText = Buffer.from(str, "base64").toString("utf-8");
return JSON.parse(planText) as AppSettings;
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));
}
return {} as AppSettings;
};
export const setActiveSettings = (id: number) => {
const str = localStorage.getItem(SETTINGS_KEY);
if (str) {
const parsed = JSON.parse(str);
const newData = parsed.map((el: saveSettingFormat) => ({
...el,
active: el.id === id,
}));
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;
}
return [] as IAppSettings[];
};
export const getActiveSettings = (): IAppSettings => {
const str = localStorage.getItem(SETTINGS_KEY);
if (str) {
const parsed: { active: boolean; encoded: string; id: number }[] =
JSON.parse(str);
const activeSettings = parsed.find((el) => el.active);
const decoded = {
active: activeSettings?.active,
decoded: JSON.parse(
Buffer.from(activeSettings?.encoded!, "base64").toString("utf-8")
),
id: activeSettings?.id,
};
return decoded as IAppSettings;
}
return {} as IAppSettings;
};
// Advanced settings
@@ -50,16 +146,52 @@ export const saveAddvancedSettings = (settings: AdvancedAppSettings) => {
"base64"
);
localStorage.setItem(ADVANCED_SETTINGS_KET, encoded);
const str = localStorage.getItem(ADVANCED_SETTINGS_KET);
const data = str ? JSON.parse(str) : [];
if (data.some((el: { encoded: string }) => el.encoded === encoded)) return;
data.push({ encoded, active: data.length === 0, id: data.length + 1 });
localStorage.setItem(ADVANCED_SETTINGS_KET, JSON.stringify(data));
};
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;
}
return {} as AdvancedAppSettings;
return [] as IAdvancedAppSettings[];
};
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;
}
return {} as IAdvancedAppSettings;
};
// Call History

View File

@@ -1,7 +1,11 @@
@font-face {
font-family: "Source Sans";
src: url("../public/fonts/SourceSans3-Regular.ttf") format("truetype");
}
.container {
width: 280px;
height: 480px;
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
}

View File

@@ -1,14 +1,21 @@
import { extendTheme } from "@chakra-ui/react";
const mainTheme = extendTheme({
fonts: {
heading: `'Source Sans 3', Arial, sans-serif`,
body: `'Source Sans 3', Arial, sans-serif`,
},
colors: {
jambonz: {
0: "#EDDEE3",
50: "#ffe1f1",
100: "#ffb3c6",
200: "#fc839d",
300: "#fa5575",
400: "#f8274e",
500: "#DA1C5C",
450: "#BD4782",
500: "#BB225B",
550: "#DA1C5C",
600: "#c60921",
700: "#99081a",
800: "#6c0714",
@@ -16,6 +23,7 @@ const mainTheme = extendTheme({
},
grey: {
50: "#FFFFFF",
75: "#F9F9F9",
100: "#F5F5F5",
200: "#ECECEC",
300: "#E3E3E3",
@@ -26,6 +34,12 @@ const mainTheme = extendTheme({
800: "#6D6D6D",
900: "#434343",
},
greenish: {
500: "#158477",
},
blue: {
600: "#4492FF", //for toggle icon
},
},
components: {
FormLabel: {
@@ -47,4 +61,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 { useEffect, useRef, useState } from "react";
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,14 +27,50 @@ 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 [isSwitchingUserStatus, setIsSwitchingUserStatus] = useState(false);
const [isOnline, setIsOnline] = useState(false);
const phoneSipAschildRef = useRef<{
updateGoOffline: (x: string) => void;
} | null>(null);
const handleGoOffline = (s: SipClientStatus) => {
if (s === status) {
return;
}
if (phoneSipAschildRef.current) {
if (s === "unregistered") {
phoneSipAschildRef.current.updateGoOffline("stop");
} else {
phoneSipAschildRef.current.updateGoOffline("start");
}
}
};
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",
content: (
<Phone
ref={phoneSipAschildRef}
sipUsername={sipUsername}
sipPassword={sipPassword}
sipDomain={sipDomain}
@@ -50,7 +78,12 @@ export const WindowApp = () => {
sipServerAddress={sipServerAddress}
calledNumber={[calledNumber, setCalledNumber]}
calledName={[calledName, setCalledName]}
stat={[status, setStatus]}
advancedSettings={advancedSettings}
allSettings={allSettings}
reload={loadSettings}
setIsSwitchingUserStatus={setIsSwitchingUserStatus}
setIsOnline={setIsOnline}
/>
),
},
@@ -83,16 +116,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 +145,21 @@ 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}
isSwitchingUserStatus={isSwitchingUserStatus}
setIsSwitchingUserStatus={setIsSwitchingUserStatus}
isOnline={isOnline}
setIsOnline={setIsOnline}
onHandleGoOffline={handleGoOffline}
/>
</Grid>
);
};

View File

@@ -0,0 +1,87 @@
import { HStack, Image, Text } from "@chakra-ui/react";
import jambonz from "src/imgs/jambonz.svg";
import { Dispatch, SetStateAction, useEffect, useState } from "react";
import { SipClientStatus } from "src/common/types";
import JambonzSwitch from "src/components/switch";
import "./styles.scss";
function Footer({
status,
setStatus,
sipServerAddress,
sipUsername,
sipDomain,
sipPassword,
sipDisplayName,
isSwitchingUserStatus,
setIsSwitchingUserStatus,
isOnline,
setIsOnline,
onHandleGoOffline,
}: {
status: string;
setStatus: Dispatch<SetStateAction<SipClientStatus>>;
sipServerAddress: string;
sipUsername: string;
sipDomain: string;
sipPassword: string;
sipDisplayName: string;
isSwitchingUserStatus: boolean;
setIsSwitchingUserStatus: React.Dispatch<React.SetStateAction<boolean>>;
isOnline: boolean;
setIsOnline: React.Dispatch<React.SetStateAction<boolean>>;
onHandleGoOffline: (s: SipClientStatus) => void;
}) {
const [isConfigured, setIsConfigured] = useState(false);
useEffect(() => {
if (status === "registered" || status === "disconnected") {
setIsSwitchingUserStatus(false);
setIsOnline(status === "registered");
}
}, [status, setIsSwitchingUserStatus, setIsOnline]);
useEffect(() => {
if (sipDomain && sipUsername && sipPassword && sipServerAddress) {
setIsConfigured(true);
} else {
setIsConfigured(false);
}
}, [sipDomain, sipUsername, sipPassword, sipServerAddress]);
return (
<HStack
padding={"15px"}
justifyContent={"space-between"}
alignItems={"center"}
bg={"grey.75"}
>
{isConfigured ? (
<HStack alignItems={"center"} flexWrap={"nowrap"} className="xs">
<JambonzSwitch
isDisabled={isSwitchingUserStatus}
checked={[isOnline, setIsOnline]}
onChange={(v) => {
setIsSwitchingUserStatus(true);
onHandleGoOffline(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,52 @@
import { Box, HStack, Text, VStack } from "@chakra-ui/react";
import { faCheck } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import React, { RefObject } from "react";
import { IAppSettings } from "src/common/types";
function AvailableAccounts({
allSettings,
onSetActive,
refData,
}: {
allSettings: IAppSettings[];
onSetActive: (x: number) => void;
refData: RefObject<HTMLDivElement>;
}) {
return (
<VStack
zIndex={"modal"}
ref={refData}
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}
display={"flex"}
justifyContent={"start"}
_hover={{
cursor: "pointer",
}}
onClick={() => onSetActive(el.id)}
>
<Box w={"12px"}>
{el.active ? <FontAwesomeIcon icon={faCheck} /> : null}
</Box>
<Text>{el.decoded.sipDisplayName || el.decoded.sipUsername}</Text>
&nbsp;
<Text>({`${el.decoded.sipUsername}@${el.decoded.sipDomain}`})</Text>
</HStack>
))}
</VStack>
);
}
export default AvailableAccounts;

File diff suppressed because it is too large Load Diff

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,81 @@
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,
handleOpenFormInAccordion,
handleCloseFormInAccordion,
isNewFormOpen,
handleCloseNewForm,
}: {
allSettings: IAppSettings[];
reload: () => void;
handleOpenFormInAccordion: () => void;
handleCloseFormInAccordion: () => void;
isNewFormOpen: boolean;
handleCloseNewForm: () => void;
}) {
const { isOpen, onOpen, onClose } = useDisclosure();
const [openAcc, setOpenAcc] = useState(0);
const closeFormInAccordion = function () {
onClose();
handleCloseFormInAccordion();
reload();
};
function handleToggleAcc(accIndex: number) {
if (isNewFormOpen) handleCloseNewForm(); //closes new form if open
handleOpenFormInAccordion();
setOpenAcc(accIndex);
onOpen();
}
return (
<Accordion
index={isOpen ? [openAcc] : []}
allowToggle
sx={{
"& > *:last-child": {
marginBottom: "7px",
},
}}
>
{allSettings.map((data, index) => (
<AccordionItem borderColor={"white"} key={index}>
<AccordionButton
padding={0}
marginBottom={-1}
_hover={{
backgroundColor: "#fff",
cursor: "default",
}}
>
{isOpen && index === openAcc ? null : (
<SettingItem
onToggleAcc={() => handleToggleAcc(index)}
data={data}
/>
)}
</AccordionButton>
<AccordionPanel pb={4} px={0}>
<AccountForm
formData={data}
handleClose={closeFormInAccordion}
inputUniqueId={`${data.id}`}
/>
</AccordionPanel>
</AccordionItem>
))}
</Accordion>
);
}

View File

@@ -0,0 +1,352 @@
import {
Box,
Button,
Center,
FormControl,
FormLabel,
HStack,
Image,
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 } from "@fortawesome/free-solid-svg-icons";
import { normalizeUrl } from "src/utils";
import { getAdvancedValidation } from "src/api";
import Switch from "src/imgs/icons/Switch.svg";
import Trash from "src/imgs/icons/Trash.svg";
import invalid from "src/imgs/icons/invalid.svg";
import AnimateOnShow from "src/components/animate";
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);
if (
formData.decoded.accountSid ||
formData.decoded.apiKey ||
formData.decoded.apiServer
) {
setIsAdvancedMode(true);
checkCredential(
formData.decoded.apiServer || "",
formData.decoded.accountSid || ""
);
}
}
},
[formData, handleClose]
);
const checkCredential = (apiServer: string, accountSid: string) => {
getAdvancedValidation(apiServer, accountSid)
.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(settings.apiServer || "", settings.accountSid || "");
}
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} className="formStyle">
<VStack
spacing={2}
w="full"
p={0}
border={"1px"}
borderColor={"gray.200"}
borderRadius={"6px"}
paddingY={"15px"}
paddingRight={"15px"}
paddingLeft={"10px"}
>
<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 && (
<AnimateOnShow>
<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>
{isAdvancedMode && (
<HStack
w="full"
mt={2}
mb={2}
gap={"1.5"}
alignItems={"center"}
>
<Text
fontSize="12px"
fontWeight={600}
color={isCredentialOk ? "greenish.500" : "jambonz.450"}
>
{isCredentialOk
? "Credentials are valid"
: "We cant verify your credentials"}
</Text>
{isCredentialOk ? (
<Box
as={FontAwesomeIcon}
icon={faCheckCircle}
color={"greenish.500"}
/>
) : (
<Box w={"22px"} h={"22px"} p={0} marginLeft={-1}>
<Image
width={"full"}
height={"full"}
p={0}
src={invalid}
/>
</Box>
)}
</HStack>
)}
</VStack>
</AnimateOnShow>
)}
<Center flexDirection={"column"} gap={"2.5"}>
<Button
variant={"ghost"}
gap={"2.5"}
alignItems={"center"}
onClick={() => setShowAdvanced((prev) => !prev)}
>
<Text textColor={"jambonz.500"}>
{" "}
{showAdvanced ? "Hide" : "Show"} Advanced Settings
</Text>
<Image width={"15px"} height={"15px"} src={Switch} />
</Button>
</Center>
</VStack>
<HStack
w="full"
alignItems="center"
justifyContent={"space-between"}
mt={2}
>
<HStack>
<Button
textColor={"jambonz.500"}
fontWeight={"semibold"}
borderRadius={"11px"}
bg="jambonz.0"
type="submit"
w="full"
>
Save
</Button>
<Button
variant={"ghost"}
colorScheme="jambonz"
type="reset"
fontWeight={"semibold"}
w="full"
onClick={resetSetting}
>
Cancel
</Button>
</HStack>
<HStack
_hover={{
cursor: "pointer",
}}
>
{formData && (
<Image
width={"24px"}
height={"24px"}
src={Trash}
onClick={() => handleDeleteSetting(formData.id)}
/>
)}
</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,97 @@
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 [showFormInAccordion, setShowFormInAccordion] = 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);
}
function handleOpenFormInAccordion() {
setShowFormInAccordion(true);
}
function handleCloseFormInAccordion() {
setShowFormInAccordion(false);
}
const loadSettings = function () {
setAllSettings(getSettings());
};
const btnIsDisabled = allSettings.length >= MAX_NUM_OF_ACCOUNTS;
return (
<div>
<Box>
<AccordionList
handleOpenFormInAccordion={handleOpenFormInAccordion}
handleCloseFormInAccordion={handleCloseFormInAccordion}
allSettings={allSettings}
reload={loadSettings}
isNewFormOpen={showForm}
handleCloseNewForm={handleCloseForm}
/>
</Box>
{!showForm && !showFormInAccordion && (
<Button
marginY={"3"}
colorScheme="jambonz"
w="full"
onClick={handleOpenForm}
isDisabled={btnIsDisabled}
>
Add Account
</Button>
)}
{showForm && (
<AnimateOnShow>
<AccountForm closeForm={handleCloseForm} />
</AnimateOnShow>
)}
<Center marginBottom={"2.5"} flexDirection={"column"}>
<Text>
{allSettings.length} of {MAX_NUM_OF_ACCOUNTS}{" "}
</Text>
{btnIsDisabled && <Text>Limit has been reached</Text>}
</Center>
{/* <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,40 @@
import { HStack, Text, VStack } from "@chakra-ui/react";
import React from "react";
import { IAppSettings } from "src/common/types";
function SettingItem({
data,
onToggleAcc,
}: {
data: IAppSettings;
onToggleAcc: () => void;
}) {
return (
<HStack
w={"full"}
display={"flex"}
marginY={"1.5"}
border={"1px"}
borderColor={"gray.200"}
justifyContent={"start"}
borderRadius={"6px"}
paddingTop={"7px"}
paddingBottom={"5px"}
paddingX={"10px"}
onClick={onToggleAcc}
_hover={{
backgroundColor: "gray.200",
cursor: "pointer",
}}
>
<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;