mirror of
https://github.com/jambonz/chrome-extension-dialer.git
synced 2026-01-25 02:08:05 +00:00
wip
This commit is contained in:
1
src/api/constants.ts
Normal file
1
src/api/constants.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const MSG_SOMETHING_WRONG = "Something went wrong, please try again";
|
||||
140
src/api/index.ts
Normal file
140
src/api/index.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import { Application, FetchError, FetchTransport, StatusCodes } from "./types";
|
||||
import { MSG_SOMETHING_WRONG } from "./constants";
|
||||
import { getAdvancedSettings } from "src/storage";
|
||||
|
||||
const fetchTransport = <Type>(
|
||||
url: string,
|
||||
options: RequestInit
|
||||
): Promise<FetchTransport<Type>> => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const response = await fetch(url, options);
|
||||
const transport: FetchTransport<Type> = {
|
||||
headers: response.headers,
|
||||
status: response.status,
|
||||
json: <Type>{},
|
||||
};
|
||||
|
||||
// Redirect unauthorized
|
||||
if (response.status === StatusCodes.UNAUTHORIZED) {
|
||||
reject();
|
||||
}
|
||||
|
||||
// API error handling returns { msg: string; }
|
||||
// See @type StatusJSON and StatusEmpty in ./types
|
||||
if (
|
||||
response.status >= StatusCodes.BAD_REQUEST &&
|
||||
response.status <= StatusCodes.INTERNAL_SERVER_ERROR
|
||||
) {
|
||||
try {
|
||||
const errJson = await response.json();
|
||||
reject(<FetchError>{
|
||||
status: response.status,
|
||||
...errJson,
|
||||
});
|
||||
} catch (error) {
|
||||
reject(<FetchError>{
|
||||
status: response.status,
|
||||
msg: MSG_SOMETHING_WRONG,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// API success handling returns a valid JSON response
|
||||
// This could either be a DTO object or a generic response
|
||||
// See types for various responses in ./types
|
||||
if (
|
||||
response.status === StatusCodes.OK ||
|
||||
response.status === StatusCodes.CREATED
|
||||
) {
|
||||
// Handle blobs -- e.g. pcap file API for RecentCalls
|
||||
if (
|
||||
options.headers!["Content-Type" as keyof HeadersInit] ===
|
||||
"application/octet-stream"
|
||||
) {
|
||||
const blob: Blob = await response.blob();
|
||||
|
||||
transport.blob = blob;
|
||||
} else {
|
||||
const json: Type = await response.json();
|
||||
|
||||
transport.json = json;
|
||||
}
|
||||
}
|
||||
|
||||
resolve(transport);
|
||||
// TypeError "Failed to fetch"
|
||||
// net::ERR_CONNECTION_REFUSED
|
||||
// This is the case if the server is unreachable...
|
||||
} catch (error: unknown) {
|
||||
reject(<FetchError>{
|
||||
status: StatusCodes.INTERNAL_SERVER_ERROR,
|
||||
msg: (error as TypeError).message,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const getAuthHeaders = () => {
|
||||
const advancedSettings = getAdvancedSettings();
|
||||
let token = advancedSettings.apiKey ?? null;
|
||||
|
||||
return {
|
||||
"Content-Type": "application/json",
|
||||
...(token && { Authorization: `Bearer ${token}` }),
|
||||
};
|
||||
};
|
||||
|
||||
export const getFetch = <Type>(url: string) => {
|
||||
return fetchTransport<Type>(url, {
|
||||
headers: getAuthHeaders(),
|
||||
});
|
||||
};
|
||||
|
||||
export const postFetch = <Type, Payload = undefined>(
|
||||
url: string,
|
||||
payload?: Payload
|
||||
) => {
|
||||
return fetchTransport<Type>(url, {
|
||||
method: "POST",
|
||||
...(payload && { body: JSON.stringify(payload) }),
|
||||
headers: getAuthHeaders(),
|
||||
});
|
||||
};
|
||||
|
||||
export const putFetch = <Type, Payload>(url: string, payload: Payload) => {
|
||||
return fetchTransport<Type>(url, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(payload),
|
||||
headers: getAuthHeaders(),
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteFetch = <Type>(url: string) => {
|
||||
return fetchTransport<Type>(url, {
|
||||
method: "DELETE",
|
||||
headers: getAuthHeaders(),
|
||||
});
|
||||
};
|
||||
|
||||
// GET Devices Users
|
||||
export const getRegisteredUser = () => {
|
||||
const advancedSettings = getAdvancedSettings();
|
||||
return getFetch<string[]>(
|
||||
`${advancedSettings.apiServer}/Accounts/${advancedSettings.accountSid}/RegisteredSipUsers`
|
||||
);
|
||||
};
|
||||
|
||||
export const getApplications = () => {
|
||||
const advancedSettings = getAdvancedSettings();
|
||||
return getFetch<Application[]>(
|
||||
`${advancedSettings.apiServer}/Accounts/${advancedSettings.accountSid}/Applications`
|
||||
);
|
||||
};
|
||||
|
||||
export const getQueues = () => {
|
||||
const advancedSettings = getAdvancedSettings();
|
||||
return getFetch<string[]>(
|
||||
`${advancedSettings.apiServer}/Accounts/${advancedSettings.accountSid}/Queues`
|
||||
);
|
||||
};
|
||||
31
src/api/types.ts
Normal file
31
src/api/types.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
export enum StatusCodes {
|
||||
OK = 200,
|
||||
CREATED = 201,
|
||||
ACCEPTED = 202,
|
||||
NO_CONTENT = 204,
|
||||
BAD_REQUEST = 400,
|
||||
UNAUTHORIZED = 401,
|
||||
FORBIDDEN = 403,
|
||||
NOT_FOUND = 404,
|
||||
UNPROCESSABLE_ENTITY = 422,
|
||||
INTERNAL_SERVER_ERROR = 500,
|
||||
/** SMPP temporarily unavailable */
|
||||
TEMPORARILY_UNAVAILABLE = 480,
|
||||
}
|
||||
|
||||
export interface FetchTransport<Type> {
|
||||
headers: Headers;
|
||||
status: StatusCodes;
|
||||
json: Type;
|
||||
blob?: Blob;
|
||||
}
|
||||
|
||||
export interface FetchError {
|
||||
status: StatusCodes;
|
||||
msg: string;
|
||||
}
|
||||
|
||||
export interface Application {
|
||||
application_sid: string;
|
||||
name: string;
|
||||
}
|
||||
@@ -58,7 +58,8 @@ export interface AppSettings {
|
||||
}
|
||||
|
||||
export interface AdvancedAppSettings {
|
||||
apikey: string;
|
||||
accountSid: string;
|
||||
apiKey: string;
|
||||
apiServer: string;
|
||||
}
|
||||
|
||||
|
||||
60
src/components/menu/index.tsx
Normal file
60
src/components/menu/index.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import {
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
Spinner,
|
||||
} from "@chakra-ui/react";
|
||||
import { useState } from "react";
|
||||
|
||||
export interface IconButtonMenuItems {
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
type IconButtonMenuProbs = {
|
||||
icon: React.ReactElement;
|
||||
onOpen: () => Promise<IconButtonMenuItems[]>;
|
||||
onClick: (value: string) => void;
|
||||
};
|
||||
|
||||
export const IconButtonMenu = ({
|
||||
icon,
|
||||
onOpen,
|
||||
onClick,
|
||||
}: IconButtonMenuProbs) => {
|
||||
const [items, setItems] = useState<IconButtonMenuItems[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleOnOpen = () => {
|
||||
setIsLoading(true);
|
||||
onOpen()
|
||||
.then((values) => setItems(values))
|
||||
.finally(() => setIsLoading(false));
|
||||
};
|
||||
return (
|
||||
<Menu onOpen={handleOnOpen}>
|
||||
<MenuButton
|
||||
as={IconButton}
|
||||
aria-label="Options"
|
||||
icon={icon}
|
||||
variant="unstyled"
|
||||
/>
|
||||
<MenuList>
|
||||
{isLoading ? (
|
||||
<MenuItem>
|
||||
<Spinner color="jambonz.500" size="xs" />
|
||||
</MenuItem>
|
||||
) : (
|
||||
items.map((i, idx) => (
|
||||
<MenuItem key={idx} onClick={() => onClick(i.value)}>
|
||||
{i.name}
|
||||
</MenuItem>
|
||||
))
|
||||
)}
|
||||
</MenuList>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
export default IconButtonMenu;
|
||||
@@ -11,11 +11,13 @@ import { Eye, EyeOff } from "react-feather";
|
||||
type PasswordInputProbs = {
|
||||
password: [string, React.Dispatch<React.SetStateAction<string>>];
|
||||
placeHolder?: string;
|
||||
isRequired?: boolean;
|
||||
};
|
||||
|
||||
function PasswordInput({
|
||||
password: [pass, setPass],
|
||||
placeHolder,
|
||||
isRequired = false,
|
||||
}: PasswordInputProbs) {
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const handleClick = () => setShowPassword(!showPassword);
|
||||
@@ -27,6 +29,7 @@ function PasswordInput({
|
||||
type={showPassword ? "text" : "password"}
|
||||
placeholder={placeHolder || ""}
|
||||
value={pass}
|
||||
isRequired
|
||||
onChange={(e) => setPass(e.target.value)}
|
||||
/>
|
||||
<InputRightElement width="4.5rem">
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { AppSettings, CallHistory } from "src/common/types";
|
||||
import {
|
||||
AdvancedAppSettings,
|
||||
AppSettings,
|
||||
CallHistory,
|
||||
} from "src/common/types";
|
||||
import { Buffer } from "buffer";
|
||||
|
||||
// Settings
|
||||
@@ -21,6 +25,26 @@ export const getSettings = (): AppSettings => {
|
||||
return {} as AppSettings;
|
||||
};
|
||||
|
||||
// Advanced settings
|
||||
const ADVANCED_SETTINGS_KET = "AdvancedSettingsKey";
|
||||
|
||||
export const saveAddvancedSettings = (settings: AdvancedAppSettings) => {
|
||||
const encoded = Buffer.from(JSON.stringify(settings), "utf-8").toString(
|
||||
"base64"
|
||||
);
|
||||
|
||||
localStorage.setItem(ADVANCED_SETTINGS_KET, encoded);
|
||||
};
|
||||
|
||||
export const getAdvancedSettings = (): AdvancedAppSettings => {
|
||||
const str = localStorage.getItem(ADVANCED_SETTINGS_KET);
|
||||
if (str) {
|
||||
const planText = Buffer.from(str, "base64").toString("utf-8");
|
||||
return JSON.parse(planText) as AdvancedAppSettings;
|
||||
}
|
||||
return {} as AdvancedAppSettings;
|
||||
};
|
||||
|
||||
// Call History
|
||||
const historyKey = "History";
|
||||
const MAX_HISTORY_COUNT = 20;
|
||||
|
||||
@@ -40,12 +40,16 @@ import GreenAvatar from "src/imgs/icons/Avatar-Green.svg";
|
||||
import "./styles.scss";
|
||||
import {
|
||||
deleteCurrentCall,
|
||||
getAdvancedSettings,
|
||||
getCurrentCall,
|
||||
saveCallHistory,
|
||||
saveCurrentCall,
|
||||
} from "src/storage";
|
||||
import { OutGoingCall } from "./outgoing-call";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import IconButtonMenu, { IconButtonMenuItems } from "src/components/menu";
|
||||
import { Application } from "src/api/types";
|
||||
import { getApplications, getQueues, getRegisteredUser } from "src/api";
|
||||
|
||||
type PhoneProbs = {
|
||||
sipDomain: string;
|
||||
@@ -79,6 +83,7 @@ export const Phone = ({
|
||||
const [isStatusDropdownDisabled, setIsStatusDropdownDisabled] =
|
||||
useState(false);
|
||||
const [isCallButtonLoading, setIsCallButtonLoading] = useState(false);
|
||||
const [isAdvanceMode, setIsAdvancedMode] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (sipDomain && sipUsername && sipPassword && sipServerAddress) {
|
||||
@@ -88,6 +93,8 @@ export const Phone = ({
|
||||
setIsConfigured(false);
|
||||
clientGoOffline();
|
||||
}
|
||||
const advancedSettings = getAdvancedSettings();
|
||||
setIsAdvancedMode(!!advancedSettings.accountSid);
|
||||
}, [sipDomain, sipUsername, sipPassword, sipServerAddress, sipDisplayName]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -395,33 +402,86 @@ export const Phone = ({
|
||||
mt={5}
|
||||
className={isOnline() ? "" : "blurred"}
|
||||
>
|
||||
<HStack spacing={2} align="start" w="full">
|
||||
<Tooltip label="Call to user">
|
||||
<IconButton
|
||||
aria-label="Place call a user"
|
||||
icon={<Users />}
|
||||
variant="unstyled"
|
||||
onClick={handleCallOnHold}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip label="Call to a queue">
|
||||
<IconButton
|
||||
aria-label="Call to a queue"
|
||||
icon={<GitMerge />}
|
||||
variant="unstyled"
|
||||
onClick={handleCallOnHold}
|
||||
/>
|
||||
</Tooltip>
|
||||
{isAdvanceMode && (
|
||||
<HStack spacing={2} align="start" w="full">
|
||||
<Tooltip label="Call to user">
|
||||
<IconButtonMenu
|
||||
icon={<Users />}
|
||||
onClick={(value) => {
|
||||
console.log(value);
|
||||
}}
|
||||
onOpen={() => {
|
||||
return new Promise<IconButtonMenuItems[]>(
|
||||
(resolve, reject) => {
|
||||
getRegisteredUser()
|
||||
.then(({ json }) => {
|
||||
resolve(
|
||||
json.map((u) => {
|
||||
const uName = u.match(/(^.*)@.*/);
|
||||
return {
|
||||
name: uName ? uName[1] : u,
|
||||
value: uName ? uName[1] : u,
|
||||
};
|
||||
})
|
||||
);
|
||||
})
|
||||
.catch((err) => reject(err));
|
||||
}
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip label="Call to a queue">
|
||||
<IconButtonMenu
|
||||
icon={<GitMerge />}
|
||||
onClick={(value) => {
|
||||
console.log(value);
|
||||
}}
|
||||
onOpen={() => {
|
||||
return new Promise<IconButtonMenuItems[]>(
|
||||
(resolve, reject) => {
|
||||
getQueues()
|
||||
.then(({ json }) => {
|
||||
resolve(
|
||||
json.map((q) => ({
|
||||
name: q,
|
||||
value: q,
|
||||
}))
|
||||
);
|
||||
})
|
||||
.catch((err) => reject(err));
|
||||
}
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip label="Call to an application">
|
||||
<IconButton
|
||||
aria-label="Place call app"
|
||||
icon={<List />}
|
||||
variant="unstyled"
|
||||
onClick={handleCallOnHold}
|
||||
/>
|
||||
</Tooltip>
|
||||
</HStack>
|
||||
<Tooltip label="Call to an application">
|
||||
<IconButtonMenu
|
||||
icon={<List />}
|
||||
onClick={(value) => {
|
||||
console.log(value);
|
||||
}}
|
||||
onOpen={() => {
|
||||
return new Promise<IconButtonMenuItems[]>(
|
||||
(resolve, reject) => {
|
||||
getApplications()
|
||||
.then(({ json }) => {
|
||||
resolve(
|
||||
json.map((a) => ({
|
||||
name: a.name,
|
||||
value: a.application_sid,
|
||||
}))
|
||||
);
|
||||
})
|
||||
.catch((err) => reject(err));
|
||||
}
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</HStack>
|
||||
)}
|
||||
|
||||
{isSipClientIdle(callStatus) ? (
|
||||
<Input
|
||||
|
||||
@@ -9,16 +9,47 @@ import {
|
||||
Text,
|
||||
VStack,
|
||||
} from "@chakra-ui/react";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { AdvancedAppSettings } from "src/common/types";
|
||||
import PasswordInput from "src/components/password-input";
|
||||
import InfoIcon from "src/imgs/icons/Info.svg";
|
||||
import ResetIcon from "src/imgs/icons/Reset.svg";
|
||||
import { getAdvancedSettings, saveAddvancedSettings } from "src/storage";
|
||||
|
||||
export const AdvanceSettings = () => {
|
||||
export const AdvancedSettings = () => {
|
||||
const [apiKey, setApiKey] = useState("");
|
||||
const [apiServer, setApiServer] = useState("");
|
||||
const handleSubmit = () => {};
|
||||
const resetSetting = () => {};
|
||||
const [accountSid, setAccountSid] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
const settings = getAdvancedSettings();
|
||||
if (settings.apiServer) {
|
||||
setApiServer(settings.apiServer);
|
||||
}
|
||||
if (settings.apiKey) {
|
||||
setApiKey(settings.apiKey);
|
||||
}
|
||||
if (settings.accountSid) {
|
||||
setAccountSid(settings.accountSid);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleSubmit = () => {
|
||||
const settings: AdvancedAppSettings = {
|
||||
accountSid,
|
||||
apiKey,
|
||||
apiServer,
|
||||
};
|
||||
|
||||
saveAddvancedSettings(settings);
|
||||
};
|
||||
const resetSetting = () => {
|
||||
saveAddvancedSettings({} as AdvancedAppSettings);
|
||||
setApiKey("");
|
||||
setApiServer("");
|
||||
setAccountSid("");
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<VStack spacing={2} w="full" h="full" p={0}>
|
||||
@@ -29,7 +60,7 @@ export const AdvanceSettings = () => {
|
||||
w="full"
|
||||
p={0}
|
||||
>
|
||||
<FormControl id="jambonz_server_address">
|
||||
<FormControl id="jambonz_api_server">
|
||||
<FormLabel>Jambonz API Server Base URL</FormLabel>
|
||||
<Input
|
||||
type="text"
|
||||
@@ -39,9 +70,19 @@ export const AdvanceSettings = () => {
|
||||
onChange={(e) => setApiServer(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl id="jambonz_account_sid">
|
||||
<FormLabel>Jambonz Account Sid</FormLabel>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Account Sid"
|
||||
isRequired
|
||||
value={accountSid}
|
||||
onChange={(e) => setAccountSid(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl id="api_key">
|
||||
<FormLabel>API Key</FormLabel>
|
||||
<PasswordInput password={[apiKey, setApiKey]} />
|
||||
<PasswordInput password={[apiKey, setApiKey]} isRequired />
|
||||
</FormControl>
|
||||
</VStack>
|
||||
<Button colorScheme="jambonz" type="submit" w="full">
|
||||
@@ -66,4 +107,4 @@ export const AdvanceSettings = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default AdvanceSettings;
|
||||
export default AdvancedSettings;
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Tab, TabList, TabPanel, TabPanels, Tabs } from "@chakra-ui/react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React from "react";
|
||||
import { DEFAULT_COLOR_SCHEME } from "src/common/constants";
|
||||
import BasicSettings from "./basic";
|
||||
import AdvanceSettings from "./advance";
|
||||
import AdvancedSettings from "./advanced";
|
||||
|
||||
export const Settings = () => {
|
||||
return (
|
||||
@@ -17,7 +17,7 @@ export const Settings = () => {
|
||||
<BasicSettings />
|
||||
</TabPanel>
|
||||
<TabPanel p={0}>
|
||||
<AdvanceSettings />
|
||||
<AdvancedSettings />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
|
||||
Reference in New Issue
Block a user