mirror of
https://github.com/jambonz/chrome-extension-dialer.git
synced 2026-10-04 02:04:28 +00:00
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
export const MSG_SOMETHING_WRONG = "Something went wrong, please try again";
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
import {
|
||||||
|
Application,
|
||||||
|
FetchError,
|
||||||
|
FetchTransport,
|
||||||
|
Queue,
|
||||||
|
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<Queue[]>(
|
||||||
|
`${advancedSettings.apiServer}/Accounts/${advancedSettings.accountSid}/Queues`
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Queue {
|
||||||
|
name: string;
|
||||||
|
length: number;
|
||||||
|
}
|
||||||
@@ -55,13 +55,19 @@ export interface AppSettings {
|
|||||||
sipUsername: string;
|
sipUsername: string;
|
||||||
sipPassword: string;
|
sipPassword: string;
|
||||||
sipDisplayName: string;
|
sipDisplayName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdvancedAppSettings {
|
||||||
|
accountSid: string;
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
|
apiServer: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CallHistory {
|
export interface CallHistory {
|
||||||
callSid: string;
|
callSid: string;
|
||||||
direction: SipCallDirection;
|
direction: SipCallDirection;
|
||||||
number: string;
|
number: string;
|
||||||
|
name?: string;
|
||||||
duration: string;
|
duration: string;
|
||||||
timeStamp: number;
|
timeStamp: number;
|
||||||
isSaved?: boolean;
|
isSaved?: boolean;
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import {
|
||||||
|
IconButton,
|
||||||
|
Menu,
|
||||||
|
MenuButton,
|
||||||
|
MenuItem,
|
||||||
|
MenuList,
|
||||||
|
Spinner,
|
||||||
|
Tooltip,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export interface IconButtonMenuItems {
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
type IconButtonMenuProbs = {
|
||||||
|
icon: React.ReactElement;
|
||||||
|
onOpen: () => Promise<IconButtonMenuItems[]>;
|
||||||
|
onClick: (name: string, value: string) => void;
|
||||||
|
tooltip: string;
|
||||||
|
noResultLabel: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const IconButtonMenu = ({
|
||||||
|
icon,
|
||||||
|
onOpen,
|
||||||
|
onClick,
|
||||||
|
tooltip,
|
||||||
|
noResultLabel,
|
||||||
|
}: 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}>
|
||||||
|
<Tooltip label={tooltip}>
|
||||||
|
<MenuButton
|
||||||
|
as={IconButton}
|
||||||
|
aria-label="Options"
|
||||||
|
icon={icon}
|
||||||
|
variant="unstyled"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<MenuList>
|
||||||
|
{isLoading ? (
|
||||||
|
<MenuItem>
|
||||||
|
<Spinner color="jambonz.500" size="xs" />
|
||||||
|
</MenuItem>
|
||||||
|
) : items.length > 0 ? (
|
||||||
|
items.map((i, idx) => (
|
||||||
|
<MenuItem key={idx} onClick={() => onClick(i.name, i.value)}>
|
||||||
|
{i.name}
|
||||||
|
</MenuItem>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<MenuItem>{noResultLabel}</MenuItem>
|
||||||
|
)}
|
||||||
|
</MenuList>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IconButtonMenu;
|
||||||
@@ -11,11 +11,13 @@ import { Eye, EyeOff } from "react-feather";
|
|||||||
type PasswordInputProbs = {
|
type PasswordInputProbs = {
|
||||||
password: [string, React.Dispatch<React.SetStateAction<string>>];
|
password: [string, React.Dispatch<React.SetStateAction<string>>];
|
||||||
placeHolder?: string;
|
placeHolder?: string;
|
||||||
|
isRequired?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
function PasswordInput({
|
function PasswordInput({
|
||||||
password: [pass, setPass],
|
password: [pass, setPass],
|
||||||
placeHolder,
|
placeHolder,
|
||||||
|
isRequired = false,
|
||||||
}: PasswordInputProbs) {
|
}: PasswordInputProbs) {
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
const handleClick = () => setShowPassword(!showPassword);
|
const handleClick = () => setShowPassword(!showPassword);
|
||||||
@@ -27,6 +29,7 @@ function PasswordInput({
|
|||||||
type={showPassword ? "text" : "password"}
|
type={showPassword ? "text" : "password"}
|
||||||
placeholder={placeHolder || ""}
|
placeholder={placeHolder || ""}
|
||||||
value={pass}
|
value={pass}
|
||||||
|
isRequired
|
||||||
onChange={(e) => setPass(e.target.value)}
|
onChange={(e) => setPass(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<InputRightElement width="4.5rem">
|
<InputRightElement width="4.5rem">
|
||||||
|
|||||||
@@ -1,17 +1,26 @@
|
|||||||
export default class SipAudioElements {
|
export default class SipAudioElements {
|
||||||
#ringing: HTMLAudioElement;
|
#ringing: HTMLAudioElement;
|
||||||
|
#ringBack: HTMLAudioElement;
|
||||||
#failed: HTMLAudioElement;
|
#failed: HTMLAudioElement;
|
||||||
#answer: HTMLAudioElement;
|
#busy: HTMLAudioElement;
|
||||||
#remote: HTMLAudioElement;
|
#remote: HTMLAudioElement;
|
||||||
|
#hungup: HTMLAudioElement;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.#ringing = new Audio(chrome.runtime.getURL("audios/ringing.mp3"));
|
this.#ringing = new Audio(chrome.runtime.getURL("audios/ringing.mp3"));
|
||||||
this.#ringing.loop = true;
|
this.#ringing.loop = true;
|
||||||
this.#ringing.volume = 0.8;
|
this.#ringing.volume = 0.8;
|
||||||
this.#failed = new Audio(chrome.runtime.getURL("audios/failed.mp3"));
|
this.#ringBack = new Audio(chrome.runtime.getURL("audios/us-ringback.mp3"));
|
||||||
|
this.#ringBack.loop = true;
|
||||||
|
this.#ringBack.volume = 0.8;
|
||||||
|
this.#failed = new Audio(chrome.runtime.getURL("audios/call-failed.mp3"));
|
||||||
this.#failed.volume = 0.3;
|
this.#failed.volume = 0.3;
|
||||||
this.#answer = new Audio(chrome.runtime.getURL("audios/failed.mp3"));
|
this.#busy = new Audio(chrome.runtime.getURL("audios/us-busy-signal.mp3"));
|
||||||
this.#answer.volume = 0.3;
|
this.#busy.volume = 0.3;
|
||||||
|
this.#hungup = new Audio(
|
||||||
|
chrome.runtime.getURL("audios/remote-party-hungup-tone.mp3")
|
||||||
|
);
|
||||||
|
this.#hungup.volume = 0.3;
|
||||||
this.#remote = new Audio();
|
this.#remote = new Audio();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,20 +37,38 @@ export default class SipAudioElements {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
playRingback(volume: number | undefined): void {
|
||||||
|
if (volume) {
|
||||||
|
this.#ringBack.volume = volume;
|
||||||
|
}
|
||||||
|
this.#ringBack.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
pauseRingback(): void {
|
||||||
|
if (!this.#ringBack.paused) {
|
||||||
|
this.#ringBack.pause();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
playFailed(volume: number | undefined): void {
|
playFailed(volume: number | undefined): void {
|
||||||
this.pauseRinging();
|
this.pauseRinging();
|
||||||
|
this.pauseRingback();
|
||||||
if (volume) {
|
if (volume) {
|
||||||
this.#failed.volume = volume;
|
this.#failed.volume = volume;
|
||||||
}
|
}
|
||||||
this.#failed.play();
|
this.#failed.play();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
playRemotePartyHungup(volume: number | undefined): void {
|
||||||
|
if (volume) {
|
||||||
|
this.#hungup.volume = volume;
|
||||||
|
}
|
||||||
|
this.#hungup.play();
|
||||||
|
}
|
||||||
|
|
||||||
playAnswer(volume: number | undefined): void {
|
playAnswer(volume: number | undefined): void {
|
||||||
this.pauseRinging();
|
this.pauseRinging();
|
||||||
// if (volume) {
|
this.pauseRingback();
|
||||||
// this.#answer.volume = volume;
|
|
||||||
// }
|
|
||||||
// this.#answer.play();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isRemoteAudioPaused(): boolean {
|
isRemoteAudioPaused(): boolean {
|
||||||
@@ -53,6 +80,11 @@ export default class SipAudioElements {
|
|||||||
this.#remote.play();
|
this.#remote.play();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stopAll() {
|
||||||
|
this.pauseRinging();
|
||||||
|
this.pauseRingback();
|
||||||
|
}
|
||||||
|
|
||||||
isPLaying(audio: HTMLAudioElement) {
|
isPLaying(audio: HTMLAudioElement) {
|
||||||
return (
|
return (
|
||||||
audio.currentTime > 0 &&
|
audio.currentTime > 0 &&
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ export default class SipSession extends events.EventEmitter {
|
|||||||
});
|
});
|
||||||
if (this.#audio.isRemoteAudioPaused() && !this.replaces) {
|
if (this.#audio.isRemoteAudioPaused() && !this.replaces) {
|
||||||
this.#audio.playRinging(undefined);
|
this.#audio.playRinging(undefined);
|
||||||
|
} else {
|
||||||
|
this.#audio.playRingback(undefined);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -94,12 +96,16 @@ export default class SipSession extends events.EventEmitter {
|
|||||||
this.#audio.playFailed(undefined);
|
this.#audio.playFailed(undefined);
|
||||||
} else {
|
} else {
|
||||||
this.#audio.pauseRinging();
|
this.#audio.pauseRinging();
|
||||||
|
this.#audio.pauseRingback();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.#rtcSession.on("ended", (data: EndEvent): void => {
|
this.#rtcSession.on("ended", (data: EndEvent): void => {
|
||||||
const { originator, cause, message } = data;
|
const { originator, cause, message } = data;
|
||||||
let description;
|
let description;
|
||||||
|
if (originator === "remote") {
|
||||||
|
this.#audio.playRemotePartyHungup(undefined);
|
||||||
|
}
|
||||||
if (message && originator === "remote" && message.hasHeader("Reason")) {
|
if (message && originator === "remote" && message.hasHeader("Reason")) {
|
||||||
const reason = Grammar.parse(message.getHeader("Reason"), "Reason");
|
const reason = Grammar.parse(message.getHeader("Reason"), "Reason");
|
||||||
if (reason) {
|
if (reason) {
|
||||||
|
|||||||
+2
-2
@@ -126,10 +126,10 @@ export default class SipUA extends events.EventEmitter {
|
|||||||
this.emit(SipConstants.UA_STOP);
|
this.emit(SipConstants.UA_STOP);
|
||||||
}
|
}
|
||||||
|
|
||||||
call(number: string): void {
|
call(number: string, customHeaders: string[] = []): void {
|
||||||
let normalizedNumber: string = normalizeNumber(number);
|
let normalizedNumber: string = normalizeNumber(number);
|
||||||
this.#ua.call(normalizedNumber, {
|
this.#ua.call(normalizedNumber, {
|
||||||
extraHeaders: [`X-Original-Number:${number}`],
|
extraHeaders: [`X-Original-Number:${number}`].concat(customHeaders),
|
||||||
mediaConstraints: { audio: true, video: false },
|
mediaConstraints: { audio: true, video: false },
|
||||||
pcConfig: this.#rtcConfig,
|
pcConfig: this.#rtcConfig,
|
||||||
});
|
});
|
||||||
|
|||||||
+18
-17
@@ -1,23 +1,24 @@
|
|||||||
function normalizeNumber(number: string): string {
|
function normalizeNumber(number: string): string {
|
||||||
if (/^(sips?|tel):/i.test(number)) {
|
if (/^(sips?|tel):/i.test(number)) {
|
||||||
return number;
|
return number;
|
||||||
} else if (/@/i.test(number)) {
|
} else if (/@/i.test(number)) {
|
||||||
return number;
|
return number;
|
||||||
} else {
|
} else if (number.startsWith("app-") || number.startsWith("queue-")) {
|
||||||
return number.replace(/[()\-. ]*/g, '');
|
return number;
|
||||||
}
|
} else {
|
||||||
|
return number.replace(/[()\-. ]*/g, "");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function randomId(prefix: string): string {
|
function randomId(prefix: string): string {
|
||||||
const id: string = [...Array(16)].map(() => Math.floor(Math.random() * 16).toString(16)).join('');
|
const id: string = [...Array(16)]
|
||||||
if (prefix) {
|
.map(() => Math.floor(Math.random() * 16).toString(16))
|
||||||
return `${prefix}-${id}`;
|
.join("");
|
||||||
} else {
|
if (prefix) {
|
||||||
return id;
|
return `${prefix}-${id}`;
|
||||||
}
|
} else {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export { normalizeNumber, randomId };
|
||||||
normalizeNumber,
|
|
||||||
randomId
|
|
||||||
}
|
|
||||||
|
|||||||
+25
-1
@@ -1,4 +1,8 @@
|
|||||||
import { AppSettings, CallHistory } from "src/common/types";
|
import {
|
||||||
|
AdvancedAppSettings,
|
||||||
|
AppSettings,
|
||||||
|
CallHistory,
|
||||||
|
} from "src/common/types";
|
||||||
import { Buffer } from "buffer";
|
import { Buffer } from "buffer";
|
||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
@@ -21,6 +25,26 @@ export const getSettings = (): AppSettings => {
|
|||||||
return {} as 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
|
// Call History
|
||||||
const historyKey = "History";
|
const historyKey = "History";
|
||||||
const MAX_HISTORY_COUNT = 20;
|
const MAX_HISTORY_COUNT = 20;
|
||||||
|
|||||||
@@ -58,3 +58,11 @@ export const isSipClientIdle = (callStatus: string) => {
|
|||||||
callStatus === SipConstants.SESSION_FAILED
|
callStatus === SipConstants.SESSION_FAILED
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const normalizeUrl = (input: string): string => {
|
||||||
|
// Extract the domain name
|
||||||
|
const url = new URL(input.startsWith("http") ? input : `https://${input}`);
|
||||||
|
|
||||||
|
// Return the fully formed URL
|
||||||
|
return `${url.protocol}//${url.hostname}/api/v1`;
|
||||||
|
};
|
||||||
|
|||||||
+16
-4
@@ -15,11 +15,15 @@ import Phone from "./phone";
|
|||||||
import Settings from "./settings";
|
import Settings from "./settings";
|
||||||
import { DEFAULT_COLOR_SCHEME } from "src/common/constants";
|
import { DEFAULT_COLOR_SCHEME } from "src/common/constants";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { getCallHistories, getSettings } from "src/storage";
|
import {
|
||||||
|
getAdvancedSettings,
|
||||||
|
getCallHistories,
|
||||||
|
getSettings,
|
||||||
|
} from "src/storage";
|
||||||
|
|
||||||
import jambonz from "src/imgs/jambonz.svg";
|
import jambonz from "src/imgs/jambonz.svg";
|
||||||
import CallHistories from "./history";
|
import CallHistories from "./history";
|
||||||
import { CallHistory } from "src/common/types";
|
import { AdvancedAppSettings, CallHistory } from "src/common/types";
|
||||||
|
|
||||||
export const WindowApp = () => {
|
export const WindowApp = () => {
|
||||||
const [sipDomain, setSipDomain] = useState("");
|
const [sipDomain, setSipDomain] = useState("");
|
||||||
@@ -29,7 +33,11 @@ export const WindowApp = () => {
|
|||||||
const [sipDisplayName, setSipDisplayName] = useState("");
|
const [sipDisplayName, setSipDisplayName] = useState("");
|
||||||
const [callHistories, setCallHistories] = useState<CallHistory[]>([]);
|
const [callHistories, setCallHistories] = useState<CallHistory[]>([]);
|
||||||
const [calledNumber, setCalledNumber] = useState("");
|
const [calledNumber, setCalledNumber] = useState("");
|
||||||
|
const [calledName, setCalledName] = useState("");
|
||||||
const [tabIndex, setTabIndex] = useState(0);
|
const [tabIndex, setTabIndex] = useState(0);
|
||||||
|
const [advancedSettings, setAdvancedSettings] = useState<AdvancedAppSettings>(
|
||||||
|
getAdvancedSettings()
|
||||||
|
);
|
||||||
const tabsSettings = [
|
const tabsSettings = [
|
||||||
{
|
{
|
||||||
title: "Dialer",
|
title: "Dialer",
|
||||||
@@ -41,17 +49,20 @@ export const WindowApp = () => {
|
|||||||
sipDisplayName={sipDisplayName}
|
sipDisplayName={sipDisplayName}
|
||||||
sipServerAddress={sipServerAddress}
|
sipServerAddress={sipServerAddress}
|
||||||
calledNumber={[calledNumber, setCalledNumber]}
|
calledNumber={[calledNumber, setCalledNumber]}
|
||||||
|
calledName={[calledName, setCalledName]}
|
||||||
|
advancedSettings={advancedSettings}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "History",
|
title: "Calls",
|
||||||
content: (
|
content: (
|
||||||
<CallHistories
|
<CallHistories
|
||||||
calls={callHistories}
|
calls={callHistories}
|
||||||
onDataChange={() => setCallHistories(getCallHistories(sipUsername))}
|
onDataChange={() => setCallHistories(getCallHistories(sipUsername))}
|
||||||
onCallNumber={(number) => {
|
onCallNumber={(number, name) => {
|
||||||
setCalledNumber(number);
|
setCalledNumber(number);
|
||||||
|
setCalledName(name || "");
|
||||||
setTabIndex(0);
|
setTabIndex(0);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -75,6 +86,7 @@ export const WindowApp = () => {
|
|||||||
|
|
||||||
const loadSettings = () => {
|
const loadSettings = () => {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
setAdvancedSettings(getAdvancedSettings());
|
||||||
setSipDomain(settings.sipDomain);
|
setSipDomain(settings.sipDomain);
|
||||||
setSipServerAddress(settings.sipServerAddress);
|
setSipServerAddress(settings.sipServerAddress);
|
||||||
setSipUsername(settings.sipUsername);
|
setSipUsername(settings.sipUsername);
|
||||||
|
|||||||
@@ -9,7 +9,13 @@ import {
|
|||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Phone, PhoneIncoming, PhoneOutgoing, Save, Star } from "react-feather";
|
import {
|
||||||
|
Phone,
|
||||||
|
PhoneIncoming,
|
||||||
|
PhoneOutgoing,
|
||||||
|
Save,
|
||||||
|
Trash2,
|
||||||
|
} from "react-feather";
|
||||||
import { CallHistory, SipCallDirection } from "src/common/types";
|
import { CallHistory, SipCallDirection } from "src/common/types";
|
||||||
import { getSettings, isSaveCallHistory } from "src/storage";
|
import { getSettings, isSaveCallHistory } from "src/storage";
|
||||||
import { formatPhoneNumber } from "src/utils";
|
import { formatPhoneNumber } from "src/utils";
|
||||||
@@ -17,13 +23,15 @@ import { formatPhoneNumber } from "src/utils";
|
|||||||
type CallHistoryItemProbs = {
|
type CallHistoryItemProbs = {
|
||||||
call: CallHistory;
|
call: CallHistory;
|
||||||
onDataChange?: (call: CallHistory) => void;
|
onDataChange?: (call: CallHistory) => void;
|
||||||
onCallNumber?: (number: string) => void;
|
onCallNumber?: (number: string, name: string | undefined) => void;
|
||||||
|
isSaved?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CallHistoryItem = ({
|
export const CallHistoryItem = ({
|
||||||
call,
|
call,
|
||||||
onDataChange,
|
onDataChange,
|
||||||
onCallNumber,
|
onCallNumber,
|
||||||
|
isSaved,
|
||||||
}: CallHistoryItemProbs) => {
|
}: CallHistoryItemProbs) => {
|
||||||
const [callEnable, setCallEnable] = useState(false);
|
const [callEnable, setCallEnable] = useState(false);
|
||||||
const getDirectionIcon = (direction: SipCallDirection) => {
|
const getDirectionIcon = (direction: SipCallDirection) => {
|
||||||
@@ -45,21 +53,14 @@ export const CallHistoryItem = ({
|
|||||||
onMouseEnter={() => setCallEnable(true)}
|
onMouseEnter={() => setCallEnable(true)}
|
||||||
onMouseLeave={() => setCallEnable(false)}
|
onMouseLeave={() => setCallEnable(false)}
|
||||||
>
|
>
|
||||||
<Icon as={getDirectionIcon(call.direction)} w="20px" h="20px" />
|
{callEnable ? (
|
||||||
<VStack align="start">
|
|
||||||
<Text fontSize="14px" fontWeight="500">
|
|
||||||
{formatPhoneNumber(call.number)}
|
|
||||||
</Text>
|
|
||||||
<Text fontSize="12px">{call.duration}</Text>
|
|
||||||
</VStack>
|
|
||||||
{callEnable && (
|
|
||||||
<Tooltip label="Call">
|
<Tooltip label="Call">
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label="call recents"
|
aria-label="call recents"
|
||||||
icon={<Phone />}
|
icon={<Phone />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (onCallNumber) {
|
if (onCallNumber) {
|
||||||
onCallNumber(call.number);
|
onCallNumber(call.number, call.name);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
variant="unstyled"
|
variant="unstyled"
|
||||||
@@ -67,18 +68,31 @@ export const CallHistoryItem = ({
|
|||||||
color="green.500"
|
color="green.500"
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
<Icon as={getDirectionIcon(call.direction)} w="20px" h="20px" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<VStack align="start">
|
||||||
|
<Text fontSize="14px" fontWeight="500">
|
||||||
|
{call.name || formatPhoneNumber(call.number)}
|
||||||
|
</Text>
|
||||||
|
<Text fontSize="12px">{call.duration}</Text>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
<Spacer />
|
<Spacer />
|
||||||
<VStack align="start">
|
<VStack align="start">
|
||||||
<Text fontSize="12px">
|
<Text fontSize="12px">
|
||||||
{dayjs(call.timeStamp).format("MMM D, hh:mm A")}
|
{dayjs(call.timeStamp).format("MMM D, hh:mm A")}
|
||||||
</Text>
|
</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
<Tooltip label="Save">
|
<Tooltip label={isSaved && call.isSaved ? "Delete" : "Save"}>
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label="save recents"
|
aria-label="save recents"
|
||||||
icon={<Save />}
|
icon={isSaved && call.isSaved ? <Trash2 /> : <Save />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
if (!isSaved && call.isSaved) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
if (settings.sipUsername) {
|
if (settings.sipUsername) {
|
||||||
isSaveCallHistory(
|
isSaveCallHistory(
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import {
|
import {
|
||||||
VStack,
|
|
||||||
Grid,
|
Grid,
|
||||||
HStack,
|
HStack,
|
||||||
InputGroup,
|
InputGroup,
|
||||||
@@ -7,18 +6,15 @@ import {
|
|||||||
InputLeftElement,
|
InputLeftElement,
|
||||||
Icon,
|
Icon,
|
||||||
Text,
|
Text,
|
||||||
Spacer,
|
|
||||||
UnorderedList,
|
|
||||||
Tabs,
|
Tabs,
|
||||||
TabList,
|
TabList,
|
||||||
Tab,
|
Tab,
|
||||||
TabPanels,
|
TabPanels,
|
||||||
TabPanel,
|
TabPanel,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { useEffect, useState } from "react";
|
import { useState } from "react";
|
||||||
import { Search, Sliders } from "react-feather";
|
import { Search, Sliders } from "react-feather";
|
||||||
import { CallHistory } from "src/common/types";
|
import { CallHistory } from "src/common/types";
|
||||||
import CallHistoryItem from "./call-history-item";
|
|
||||||
|
|
||||||
import { DEFAULT_COLOR_SCHEME } from "src/common/constants";
|
import { DEFAULT_COLOR_SCHEME } from "src/common/constants";
|
||||||
import Recents from "./recent";
|
import Recents from "./recent";
|
||||||
@@ -26,7 +22,7 @@ import Recents from "./recent";
|
|||||||
type CallHistoriesProbs = {
|
type CallHistoriesProbs = {
|
||||||
calls: CallHistory[];
|
calls: CallHistory[];
|
||||||
onDataChange?: (call: CallHistory) => void;
|
onDataChange?: (call: CallHistory) => void;
|
||||||
onCallNumber?: (number: string) => void;
|
onCallNumber?: (number: string, name: string | undefined) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CallHistories = ({
|
export const CallHistories = ({
|
||||||
@@ -39,8 +35,8 @@ export const CallHistories = ({
|
|||||||
return (
|
return (
|
||||||
<Tabs isFitted colorScheme={DEFAULT_COLOR_SCHEME}>
|
<Tabs isFitted colorScheme={DEFAULT_COLOR_SCHEME}>
|
||||||
<TabList mb="1em" gap={1}>
|
<TabList mb="1em" gap={1}>
|
||||||
<Tab>Saved</Tab>
|
|
||||||
<Tab>Recent</Tab>
|
<Tab>Recent</Tab>
|
||||||
|
<Tab>Saved</Tab>
|
||||||
</TabList>
|
</TabList>
|
||||||
|
|
||||||
<Grid w="full" templateColumns="1fr auto" gap={5}>
|
<Grid w="full" templateColumns="1fr auto" gap={5}>
|
||||||
@@ -71,7 +67,6 @@ export const CallHistories = ({
|
|||||||
<Recents
|
<Recents
|
||||||
calls={calls}
|
calls={calls}
|
||||||
search={searchText}
|
search={searchText}
|
||||||
isSaved
|
|
||||||
onCallNumber={onCallNumber}
|
onCallNumber={onCallNumber}
|
||||||
onDataChange={onDataChange}
|
onDataChange={onDataChange}
|
||||||
/>
|
/>
|
||||||
@@ -80,6 +75,7 @@ export const CallHistories = ({
|
|||||||
<Recents
|
<Recents
|
||||||
calls={calls}
|
calls={calls}
|
||||||
search={searchText}
|
search={searchText}
|
||||||
|
isSaved
|
||||||
onCallNumber={onCallNumber}
|
onCallNumber={onCallNumber}
|
||||||
onDataChange={onDataChange}
|
onDataChange={onDataChange}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ type RecentsProbs = {
|
|||||||
search: string;
|
search: string;
|
||||||
isSaved?: boolean;
|
isSaved?: boolean;
|
||||||
onDataChange?: (call: CallHistory) => void;
|
onDataChange?: (call: CallHistory) => void;
|
||||||
onCallNumber?: (number: string) => void;
|
onCallNumber?: (number: string, name: string | undefined) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Recents = ({
|
export const Recents = ({
|
||||||
@@ -53,6 +53,7 @@ export const Recents = ({
|
|||||||
>
|
>
|
||||||
{callHistories.map((c) => (
|
{callHistories.map((c) => (
|
||||||
<CallHistoryItem
|
<CallHistoryItem
|
||||||
|
isSaved={isSaved}
|
||||||
call={c}
|
call={c}
|
||||||
onCallNumber={onCallNumber}
|
onCallNumber={onCallNumber}
|
||||||
onDataChange={onDataChange}
|
onDataChange={onDataChange}
|
||||||
@@ -61,7 +62,7 @@ export const Recents = ({
|
|||||||
</UnorderedList>
|
</UnorderedList>
|
||||||
) : (
|
) : (
|
||||||
<Text fontSize="24px" fontWeight="bold">
|
<Text fontSize="24px" fontWeight="bold">
|
||||||
No Call History
|
{isSaved ? "No saved calls" : "No Call History"}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</VStack>
|
</VStack>
|
||||||
|
|||||||
+188
-38
@@ -14,13 +14,25 @@ import {
|
|||||||
VStack,
|
VStack,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { Mic, MicOff, Pause, PhoneOff, Play } from "react-feather";
|
import {
|
||||||
import { Call, SipCallDirection, SipClientStatus } from "src/common/types";
|
GitMerge,
|
||||||
|
List,
|
||||||
|
Mic,
|
||||||
|
MicOff,
|
||||||
|
Pause,
|
||||||
|
PhoneOff,
|
||||||
|
Play,
|
||||||
|
Users,
|
||||||
|
} from "react-feather";
|
||||||
|
import {
|
||||||
|
AdvancedAppSettings,
|
||||||
|
SipCallDirection,
|
||||||
|
SipClientStatus,
|
||||||
|
} from "src/common/types";
|
||||||
import { SipConstants, SipUA } from "src/lib";
|
import { SipConstants, SipUA } from "src/lib";
|
||||||
import IncommingCall from "./incomming-call";
|
import IncommingCall from "./incomming-call";
|
||||||
import DialPad from "./dial-pad";
|
import DialPad from "./dial-pad";
|
||||||
import {
|
import {
|
||||||
formatPhoneNumber,
|
|
||||||
isSipClientAnswered,
|
isSipClientAnswered,
|
||||||
isSipClientIdle,
|
isSipClientIdle,
|
||||||
isSipClientRinging,
|
isSipClientRinging,
|
||||||
@@ -31,12 +43,15 @@ import GreenAvatar from "src/imgs/icons/Avatar-Green.svg";
|
|||||||
import "./styles.scss";
|
import "./styles.scss";
|
||||||
import {
|
import {
|
||||||
deleteCurrentCall,
|
deleteCurrentCall,
|
||||||
|
getAdvancedSettings,
|
||||||
getCurrentCall,
|
getCurrentCall,
|
||||||
saveCallHistory,
|
saveCallHistory,
|
||||||
saveCurrentCall,
|
saveCurrentCall,
|
||||||
} from "src/storage";
|
} from "src/storage";
|
||||||
import { OutGoingCall } from "./outgoing-call";
|
import { OutGoingCall } from "./outgoing-call";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
import IconButtonMenu, { IconButtonMenuItems } from "src/components/menu";
|
||||||
|
import { getApplications, getQueues, getRegisteredUser } from "src/api";
|
||||||
|
|
||||||
type PhoneProbs = {
|
type PhoneProbs = {
|
||||||
sipDomain: string;
|
sipDomain: string;
|
||||||
@@ -45,6 +60,8 @@ type PhoneProbs = {
|
|||||||
sipPassword: string;
|
sipPassword: string;
|
||||||
sipDisplayName: string;
|
sipDisplayName: string;
|
||||||
calledNumber: [string, React.Dispatch<React.SetStateAction<string>>];
|
calledNumber: [string, React.Dispatch<React.SetStateAction<string>>];
|
||||||
|
calledName: [string, React.Dispatch<React.SetStateAction<string>>];
|
||||||
|
advancedSettings: AdvancedAppSettings;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Phone = ({
|
export const Phone = ({
|
||||||
@@ -54,8 +71,11 @@ export const Phone = ({
|
|||||||
sipPassword,
|
sipPassword,
|
||||||
sipDisplayName,
|
sipDisplayName,
|
||||||
calledNumber: [calledANumber, setCalledANumber],
|
calledNumber: [calledANumber, setCalledANumber],
|
||||||
|
calledName: [calledAName, setCalledAName],
|
||||||
|
advancedSettings,
|
||||||
}: PhoneProbs) => {
|
}: PhoneProbs) => {
|
||||||
const [inputNumber, setInputNumber] = useState("");
|
const [inputNumber, setInputNumber] = useState("");
|
||||||
|
const [appName, setAppName] = useState("");
|
||||||
const inputNumberRef = useRef(inputNumber);
|
const inputNumberRef = useRef(inputNumber);
|
||||||
const [status, setStatus] = useState<SipClientStatus>("offline");
|
const [status, setStatus] = useState<SipClientStatus>("offline");
|
||||||
const [isConfigured, setIsConfigured] = useState(false);
|
const [isConfigured, setIsConfigured] = useState(false);
|
||||||
@@ -70,10 +90,27 @@ export const Phone = ({
|
|||||||
const [isStatusDropdownDisabled, setIsStatusDropdownDisabled] =
|
const [isStatusDropdownDisabled, setIsStatusDropdownDisabled] =
|
||||||
useState(false);
|
useState(false);
|
||||||
const [isCallButtonLoading, setIsCallButtonLoading] = useState(false);
|
const [isCallButtonLoading, setIsCallButtonLoading] = useState(false);
|
||||||
|
const [isAdvanceMode, setIsAdvancedMode] = useState(false);
|
||||||
|
const isRestartRef = useRef(false);
|
||||||
|
const sipDomainRef = useRef("");
|
||||||
|
const sipUsernameRef = useRef("");
|
||||||
|
const sipPasswordRef = useRef("");
|
||||||
|
const sipServerAddressRef = useRef("");
|
||||||
|
const sipDisplayNameRef = useRef("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
sipDomainRef.current = sipDomain;
|
||||||
|
sipUsernameRef.current = sipUsername;
|
||||||
|
sipPasswordRef.current = sipPassword;
|
||||||
|
sipServerAddressRef.current = sipServerAddress;
|
||||||
|
sipDisplayNameRef.current = sipDisplayName;
|
||||||
if (sipDomain && sipUsername && sipPassword && sipServerAddress) {
|
if (sipDomain && sipUsername && sipPassword && sipServerAddress) {
|
||||||
createSipClient();
|
if (sipUA.current) {
|
||||||
|
clientGoOffline();
|
||||||
|
isRestartRef.current = true;
|
||||||
|
} else {
|
||||||
|
createSipClient();
|
||||||
|
}
|
||||||
setIsConfigured(true);
|
setIsConfigured(true);
|
||||||
} else {
|
} else {
|
||||||
setIsConfigured(false);
|
setIsConfigured(false);
|
||||||
@@ -81,6 +118,11 @@ export const Phone = ({
|
|||||||
}
|
}
|
||||||
}, [sipDomain, sipUsername, sipPassword, sipServerAddress, sipDisplayName]);
|
}, [sipDomain, sipUsername, sipPassword, sipServerAddress, sipDisplayName]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const advancedSettings = getAdvancedSettings();
|
||||||
|
setIsAdvancedMode(!!advancedSettings.accountSid);
|
||||||
|
}, [advancedSettings]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
inputNumberRef.current = inputNumber;
|
inputNumberRef.current = inputNumber;
|
||||||
sessionDirectionRef.current = sessionDirection;
|
sessionDirectionRef.current = sessionDirection;
|
||||||
@@ -101,9 +143,18 @@ export const Phone = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (calledANumber) {
|
if (calledANumber) {
|
||||||
setInputNumber(calledANumber);
|
if (
|
||||||
makeOutboundCall(calledANumber);
|
!(
|
||||||
|
calledANumber.startsWith("app-") || calledANumber.startsWith("queue-")
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
setInputNumber(calledANumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
setAppName(calledAName);
|
||||||
|
makeOutboundCall(calledANumber, calledAName);
|
||||||
setCalledANumber("");
|
setCalledANumber("");
|
||||||
|
setCalledAName("");
|
||||||
}
|
}
|
||||||
}, [calledANumber]);
|
}, [calledANumber]);
|
||||||
|
|
||||||
@@ -146,19 +197,17 @@ export const Phone = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const createSipClient = () => {
|
const createSipClient = () => {
|
||||||
clientGoOffline();
|
|
||||||
|
|
||||||
const client = {
|
const client = {
|
||||||
username: `${sipUsername}@${sipDomain}`,
|
username: `${sipUsernameRef.current}@${sipDomainRef.current}`,
|
||||||
password: sipPassword,
|
password: sipPasswordRef.current,
|
||||||
name: sipDisplayName ?? sipUsername,
|
name: sipDisplayNameRef.current ?? sipUsernameRef.current,
|
||||||
};
|
};
|
||||||
|
|
||||||
const settings = {
|
const settings = {
|
||||||
pcConfig: {
|
pcConfig: {
|
||||||
iceServers: [{ urls: ["stun:stun.l.google.com:19302"] }],
|
iceServers: [{ urls: ["stun:stun.l.google.com:19302"] }],
|
||||||
},
|
},
|
||||||
wsUri: sipServerAddress,
|
wsUri: sipServerAddressRef.current,
|
||||||
register: true,
|
register: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -170,7 +219,12 @@ export const Phone = ({
|
|||||||
});
|
});
|
||||||
sipClient.on(SipConstants.UA_UNREGISTERED, (args) => {
|
sipClient.on(SipConstants.UA_UNREGISTERED, (args) => {
|
||||||
setStatus("offline");
|
setStatus("offline");
|
||||||
clientGoOffline();
|
if (isRestartRef.current) {
|
||||||
|
createSipClient();
|
||||||
|
isRestartRef.current = false;
|
||||||
|
} else {
|
||||||
|
clientGoOffline();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
// Call Status
|
// Call Status
|
||||||
sipClient.on(SipConstants.SESSION_RINGING, (args) => {
|
sipClient.on(SipConstants.SESSION_RINGING, (args) => {
|
||||||
@@ -222,6 +276,7 @@ export const Phone = ({
|
|||||||
duration: transform(Date.now(), call.timeStamp),
|
duration: transform(Date.now(), call.timeStamp),
|
||||||
timeStamp: call.timeStamp,
|
timeStamp: call.timeStamp,
|
||||||
callSid: call.callSid,
|
callSid: call.callSid,
|
||||||
|
name: call.name,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
deleteCurrentCall();
|
deleteCurrentCall();
|
||||||
@@ -254,19 +309,27 @@ export const Phone = ({
|
|||||||
makeOutboundCall(inputNumber);
|
makeOutboundCall(inputNumber);
|
||||||
};
|
};
|
||||||
|
|
||||||
const makeOutboundCall = (number: string) => {
|
const makeOutboundCall = (number: string, name: string = "") => {
|
||||||
if (sipUA.current && number) {
|
if (sipUA.current && number) {
|
||||||
setIsCallButtonLoading(true);
|
setIsCallButtonLoading(true);
|
||||||
setCallStatus(SipConstants.SESSION_RINGING);
|
setCallStatus(SipConstants.SESSION_RINGING);
|
||||||
setSessionDirection("outgoing");
|
setSessionDirection("outgoing");
|
||||||
saveCurrentCall({
|
saveCurrentCall({
|
||||||
number: number,
|
number: number,
|
||||||
|
name,
|
||||||
direction: "outgoing",
|
direction: "outgoing",
|
||||||
timeStamp: Date.now(),
|
timeStamp: Date.now(),
|
||||||
duration: "0",
|
duration: "0",
|
||||||
callSid: uuidv4(),
|
callSid: uuidv4(),
|
||||||
});
|
});
|
||||||
sipUA.current.call(number);
|
// Add custom header if this is special jambonz call
|
||||||
|
let customHeaders: string[] = [];
|
||||||
|
if (number.startsWith("app-")) {
|
||||||
|
customHeaders = [
|
||||||
|
`X-Application-Sid: ${number.substring(4, number.length)}`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
sipUA.current.call(number, customHeaders);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -339,7 +402,7 @@ export const Phone = ({
|
|||||||
<VStack spacing={2} alignItems="start" w="full">
|
<VStack spacing={2} alignItems="start" w="full">
|
||||||
<HStack spacing={2} w="full">
|
<HStack spacing={2} w="full">
|
||||||
<Text fontWeight="bold" fontSize="13px">
|
<Text fontWeight="bold" fontSize="13px">
|
||||||
{sipDisplayName ?? sipUsername}
|
{sipDisplayName || sipUsername}
|
||||||
</Text>
|
</Text>
|
||||||
<Circle size="8px" bg={isOnline() ? "green.500" : "gray.500"} />
|
<Circle size="8px" bg={isOnline() ? "green.500" : "gray.500"} />
|
||||||
<Select
|
<Select
|
||||||
@@ -377,35 +440,122 @@ export const Phone = ({
|
|||||||
decline={handleDecline}
|
decline={handleDecline}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<OutGoingCall number={inputNumber} cancelCall={handleDecline} />
|
<OutGoingCall
|
||||||
|
number={inputNumber || appName}
|
||||||
|
cancelCall={handleDecline}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<VStack
|
<VStack
|
||||||
spacing={4}
|
spacing={2}
|
||||||
w="full"
|
w="full"
|
||||||
mt={5}
|
mt={5}
|
||||||
className={isOnline() ? "" : "blurred"}
|
className={isOnline() ? "" : "blurred"}
|
||||||
>
|
>
|
||||||
{isSipClientIdle(callStatus) ? (
|
{isAdvanceMode && isSipClientIdle(callStatus) && (
|
||||||
<Input
|
<HStack spacing={2} align="start" w="full">
|
||||||
value={inputNumber}
|
<IconButtonMenu
|
||||||
bg="grey.500"
|
icon={<Users />}
|
||||||
fontWeight="bold"
|
tooltip="Call an online user"
|
||||||
fontSize="24px"
|
noResultLabel="No one else is online"
|
||||||
onChange={(e) => setInputNumber(e.target.value)}
|
onClick={(_, value) => {
|
||||||
textAlign="center"
|
setInputNumber(value);
|
||||||
/>
|
makeOutboundCall(value);
|
||||||
) : (
|
}}
|
||||||
<VStack>
|
onOpen={() => {
|
||||||
<Text fontSize="22px" fontWeight="bold">
|
return new Promise<IconButtonMenuItems[]>(
|
||||||
{formatPhoneNumber(inputNumber)}
|
(resolve, reject) => {
|
||||||
</Text>
|
getRegisteredUser()
|
||||||
{seconds >= 0 && (
|
.then(({ json }) => {
|
||||||
<Text fontSize="15px">
|
resolve(
|
||||||
{new Date(seconds * 1000).toISOString().substr(11, 8)}
|
json
|
||||||
</Text>
|
.filter((u) => !u.includes(sipUsername))
|
||||||
)}
|
.map((u) => {
|
||||||
</VStack>
|
const uName = u.match(/(^.*)@.*/);
|
||||||
|
return {
|
||||||
|
name: uName ? uName[1] : u,
|
||||||
|
value: uName ? uName[1] : u,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch((err) => reject(err));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<IconButtonMenu
|
||||||
|
icon={<List />}
|
||||||
|
tooltip="Take a call from queue"
|
||||||
|
noResultLabel="No calls in queue"
|
||||||
|
onClick={(name, value) => {
|
||||||
|
setAppName(`Queue ${name}`);
|
||||||
|
const calledQueue = `queue-${value}`;
|
||||||
|
setInputNumber("");
|
||||||
|
makeOutboundCall(calledQueue, `Queue ${name}`);
|
||||||
|
}}
|
||||||
|
onOpen={() => {
|
||||||
|
return new Promise<IconButtonMenuItems[]>(
|
||||||
|
(resolve, reject) => {
|
||||||
|
getQueues()
|
||||||
|
.then(({ json }) => {
|
||||||
|
resolve(
|
||||||
|
json.map((q) => ({
|
||||||
|
name: `${q.name} (${q.length})`,
|
||||||
|
value: q.name,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch((err) => reject(err));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<IconButtonMenu
|
||||||
|
icon={<GitMerge />}
|
||||||
|
tooltip="Call an application"
|
||||||
|
noResultLabel="No applications"
|
||||||
|
onClick={(name, value) => {
|
||||||
|
setAppName(`App ${name}`);
|
||||||
|
const calledAppId = `app-${value}`;
|
||||||
|
setInputNumber("");
|
||||||
|
makeOutboundCall(calledAppId, `App ${name}`);
|
||||||
|
}}
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Input
|
||||||
|
value={inputNumber}
|
||||||
|
bg="grey.500"
|
||||||
|
fontWeight="bold"
|
||||||
|
fontSize="24px"
|
||||||
|
onChange={(e) => setInputNumber(e.target.value)}
|
||||||
|
textAlign="center"
|
||||||
|
isReadOnly={!isSipClientIdle(callStatus)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{!isSipClientIdle(callStatus) && seconds >= 0 && (
|
||||||
|
<Text fontSize="15px">
|
||||||
|
{new Date(seconds * 1000).toISOString().substr(11, 8)}
|
||||||
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<DialPad handleDigitPress={handleDialPadClick} />
|
<DialPad handleDigitPress={handleDialPadClick} />
|
||||||
|
|||||||
@@ -0,0 +1,148 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
FormLabel,
|
||||||
|
HStack,
|
||||||
|
Icon,
|
||||||
|
Image,
|
||||||
|
Input,
|
||||||
|
Spacer,
|
||||||
|
Text,
|
||||||
|
VStack,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { CheckCircle, XCircle } from "react-feather";
|
||||||
|
import { getApplications } from "src/api";
|
||||||
|
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";
|
||||||
|
import { normalizeUrl } from "src/utils";
|
||||||
|
|
||||||
|
export const AdvancedSettings = () => {
|
||||||
|
const [apiKey, setApiKey] = useState("");
|
||||||
|
const [apiServer, setApiServer] = useState("");
|
||||||
|
const [accountSid, setAccountSid] = useState("");
|
||||||
|
const [isCredentialOk, setIsCredentialOk] = useState(false);
|
||||||
|
const [isAdvancedMode, setIsAdvancedMode] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const settings = getAdvancedSettings();
|
||||||
|
if (settings.apiServer) {
|
||||||
|
setIsAdvancedMode(true);
|
||||||
|
checkCredential();
|
||||||
|
setApiServer(settings.apiServer);
|
||||||
|
}
|
||||||
|
if (settings.apiKey) {
|
||||||
|
setApiKey(settings.apiKey);
|
||||||
|
}
|
||||||
|
if (settings.accountSid) {
|
||||||
|
setAccountSid(settings.accountSid);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const checkCredential = () => {
|
||||||
|
getApplications()
|
||||||
|
.then(() => {
|
||||||
|
setIsCredentialOk(true);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setIsCredentialOk(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setApiServer(normalizeUrl(apiServer));
|
||||||
|
const settings: AdvancedAppSettings = {
|
||||||
|
accountSid,
|
||||||
|
apiKey,
|
||||||
|
apiServer: normalizeUrl(apiServer),
|
||||||
|
};
|
||||||
|
|
||||||
|
saveAddvancedSettings(settings);
|
||||||
|
setIsAdvancedMode(true);
|
||||||
|
checkCredential();
|
||||||
|
};
|
||||||
|
const resetSetting = () => {
|
||||||
|
saveAddvancedSettings({} as AdvancedAppSettings);
|
||||||
|
setApiKey("");
|
||||||
|
setApiServer("");
|
||||||
|
setAccountSid("");
|
||||||
|
setIsAdvancedMode(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<VStack spacing={2} w="full" h="full" p={0}>
|
||||||
|
<VStack
|
||||||
|
spacing={2}
|
||||||
|
maxH="calc(100vh - 25em)"
|
||||||
|
overflowY="auto"
|
||||||
|
w="full"
|
||||||
|
p={0}
|
||||||
|
>
|
||||||
|
<FormControl id="jambonz_api_server">
|
||||||
|
<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">
|
||||||
|
<FormLabel>Jambonz Account Sid</FormLabel>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
isRequired
|
||||||
|
value={accountSid}
|
||||||
|
onChange={(e) => setAccountSid(e.target.value)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl id="api_key">
|
||||||
|
<FormLabel>API Key</FormLabel>
|
||||||
|
<PasswordInput password={[apiKey, setApiKey]} isRequired />
|
||||||
|
</FormControl>
|
||||||
|
</VStack>
|
||||||
|
{isAdvancedMode && (
|
||||||
|
<HStack w="full" mt={2} mb={2}>
|
||||||
|
<Icon
|
||||||
|
as={isCredentialOk ? CheckCircle : XCircle}
|
||||||
|
color={isCredentialOk ? "green.500" : "red.500"}
|
||||||
|
boxSize={6}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
fontSize="14px"
|
||||||
|
color={isCredentialOk ? "green.500" : "red.500"}
|
||||||
|
>
|
||||||
|
Credential is {isCredentialOk ? "valid" : "invalid"}
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button colorScheme="jambonz" type="submit" w="full">
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
<VStack w="full" align="center" mt={2}>
|
||||||
|
{/* <HStack spacing={1}>
|
||||||
|
<Image src={InfoIcon} w="30px" h="30px" />
|
||||||
|
<Text fontSize="14px">Get help</Text>
|
||||||
|
</HStack> */}
|
||||||
|
|
||||||
|
{/* <Spacer /> */}
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<Image src={ResetIcon} w="30px" h="30px" />
|
||||||
|
<Text fontSize="14px" onClick={resetSetting} cursor="pointer">
|
||||||
|
Reset settings
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
</VStack>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AdvancedSettings;
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
FormLabel,
|
||||||
|
HStack,
|
||||||
|
Image,
|
||||||
|
Input,
|
||||||
|
Spacer,
|
||||||
|
Text,
|
||||||
|
VStack,
|
||||||
|
useToast,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import PasswordInput from "src/components/password-input";
|
||||||
|
import { getSettings, saveSettings } from "src/storage";
|
||||||
|
import InfoIcon from "src/imgs/icons/Info.svg";
|
||||||
|
import ResetIcon from "src/imgs/icons/Reset.svg";
|
||||||
|
import { AppSettings } from "src/common/types";
|
||||||
|
import { DEFAULT_TOAST_DURATION } from "src/common/constants";
|
||||||
|
|
||||||
|
export const BasicSettings = () => {
|
||||||
|
const [sipDomain, setSipDomain] = useState("");
|
||||||
|
const [sipServerAddress, setSipServerAddress] = useState("");
|
||||||
|
const [sipUsername, setSipUsername] = useState("");
|
||||||
|
const [sipPassword, setSipPassword] = useState("");
|
||||||
|
const [sipDisplayName, setSipDisplayName] = useState("");
|
||||||
|
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const settings: AppSettings = {
|
||||||
|
sipDomain,
|
||||||
|
sipServerAddress: sipServerAddress,
|
||||||
|
sipUsername,
|
||||||
|
sipPassword,
|
||||||
|
sipDisplayName,
|
||||||
|
};
|
||||||
|
|
||||||
|
saveSettings(settings);
|
||||||
|
toast({
|
||||||
|
title: "Settings saved successfully",
|
||||||
|
status: "success",
|
||||||
|
duration: DEFAULT_TOAST_DURATION,
|
||||||
|
isClosable: true,
|
||||||
|
colorScheme: "jambonz",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetSetting = () => {
|
||||||
|
saveSettings({} as AppSettings);
|
||||||
|
setSipDomain("");
|
||||||
|
setSipServerAddress("");
|
||||||
|
setSipUsername("");
|
||||||
|
setSipPassword("");
|
||||||
|
setSipDisplayName("");
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const settings = getSettings();
|
||||||
|
if (settings.sipDomain) {
|
||||||
|
setSipDomain(settings.sipDomain);
|
||||||
|
}
|
||||||
|
if (settings.sipServerAddress) {
|
||||||
|
setSipServerAddress(settings.sipServerAddress);
|
||||||
|
}
|
||||||
|
if (settings.sipUsername) {
|
||||||
|
setSipUsername(settings.sipUsername);
|
||||||
|
}
|
||||||
|
if (settings.sipPassword) {
|
||||||
|
setSipPassword(settings.sipPassword);
|
||||||
|
}
|
||||||
|
if (settings.sipDisplayName) {
|
||||||
|
setSipDisplayName(settings.sipDisplayName);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<VStack spacing={2} w="full" p={0}>
|
||||||
|
<VStack
|
||||||
|
spacing={2}
|
||||||
|
maxH="calc(100vh - 25em)"
|
||||||
|
overflowY="auto"
|
||||||
|
w="full"
|
||||||
|
p={0}
|
||||||
|
>
|
||||||
|
<FormControl id="jambonz_sip_domain">
|
||||||
|
<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">
|
||||||
|
<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">
|
||||||
|
<FormLabel>SIP Username</FormLabel>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="Username"
|
||||||
|
isRequired
|
||||||
|
value={sipUsername}
|
||||||
|
onChange={(e) => setSipUsername(e.target.value)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl id="password">
|
||||||
|
<FormLabel fontWeight="">SIP Password</FormLabel>
|
||||||
|
<PasswordInput
|
||||||
|
password={[sipPassword, setSipPassword]}
|
||||||
|
placeHolder="Enter your password"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl id="sip_display_name">
|
||||||
|
<FormLabel>SIP Display Name (Optional)</FormLabel>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="Display name"
|
||||||
|
value={sipDisplayName}
|
||||||
|
onChange={(e) => setSipDisplayName(e.target.value)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</VStack>
|
||||||
|
<Button colorScheme="jambonz" type="submit" w="full">
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
<VStack w="full" alignItems="center" mt={2}>
|
||||||
|
{/* <HStack spacing={1}>
|
||||||
|
<Image src={InfoIcon} w="30px" h="30px" />
|
||||||
|
<Text fontSize="14px">Get help</Text>
|
||||||
|
</HStack> */}
|
||||||
|
|
||||||
|
{/* <Spacer /> */}
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<Image src={ResetIcon} w="30px" h="30px" />
|
||||||
|
<Text fontSize="14px" onClick={resetSetting} cursor="pointer">
|
||||||
|
Reset settings
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
</VStack>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BasicSettings;
|
||||||
+19
-160
@@ -1,167 +1,26 @@
|
|||||||
import {
|
import { Tab, TabList, TabPanel, TabPanels, Tabs } from "@chakra-ui/react";
|
||||||
Button,
|
import React from "react";
|
||||||
FormControl,
|
import { DEFAULT_COLOR_SCHEME } from "src/common/constants";
|
||||||
FormLabel,
|
import BasicSettings from "./basic";
|
||||||
HStack,
|
import AdvancedSettings from "./advanced";
|
||||||
Input,
|
|
||||||
Spacer,
|
|
||||||
VStack,
|
|
||||||
useToast,
|
|
||||||
Image,
|
|
||||||
Box,
|
|
||||||
Flex,
|
|
||||||
Text,
|
|
||||||
} from "@chakra-ui/react";
|
|
||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import { DEFAULT_TOAST_DURATION } from "src/common/constants";
|
|
||||||
import { AppSettings } from "src/common/types";
|
|
||||||
import PasswordInput from "src/components/password-input";
|
|
||||||
import { getSettings, saveSettings } from "src/storage";
|
|
||||||
|
|
||||||
import InfoIcon from "src/imgs/icons/Info.svg";
|
|
||||||
import ResetIcon from "src/imgs/icons/Reset.svg";
|
|
||||||
|
|
||||||
export const Settings = () => {
|
export const Settings = () => {
|
||||||
const [sipDomain, setSipDomain] = useState("");
|
|
||||||
const [sipServerAddress, setSipServerAddress] = useState("");
|
|
||||||
const [sipUsername, setSipUsername] = useState("");
|
|
||||||
const [sipPassword, setSipPassword] = useState("");
|
|
||||||
const [sipDisplayName, setSipDisplayName] = useState("");
|
|
||||||
const [apiKey, setApiKey] = useState("");
|
|
||||||
const toast = useToast();
|
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const settings: AppSettings = {
|
|
||||||
sipDomain,
|
|
||||||
sipServerAddress: sipServerAddress,
|
|
||||||
sipUsername,
|
|
||||||
sipPassword,
|
|
||||||
sipDisplayName,
|
|
||||||
apiKey,
|
|
||||||
};
|
|
||||||
|
|
||||||
saveSettings(settings);
|
|
||||||
toast({
|
|
||||||
title: "Settings saved successfully",
|
|
||||||
status: "success",
|
|
||||||
duration: DEFAULT_TOAST_DURATION,
|
|
||||||
isClosable: true,
|
|
||||||
colorScheme: "jambonz",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetSetting = () => {
|
|
||||||
saveSettings({} as AppSettings);
|
|
||||||
setSipDomain("");
|
|
||||||
setSipServerAddress("");
|
|
||||||
setSipUsername("");
|
|
||||||
setSipPassword("");
|
|
||||||
setSipDisplayName("");
|
|
||||||
setApiKey("");
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const settings = getSettings();
|
|
||||||
if (settings.sipDomain) {
|
|
||||||
setSipDomain(settings.sipDomain);
|
|
||||||
}
|
|
||||||
if (settings.sipServerAddress) {
|
|
||||||
setSipServerAddress(settings.sipServerAddress);
|
|
||||||
}
|
|
||||||
if (settings.sipUsername) {
|
|
||||||
setSipUsername(settings.sipUsername);
|
|
||||||
}
|
|
||||||
if (settings.sipPassword) {
|
|
||||||
setSipPassword(settings.sipPassword);
|
|
||||||
}
|
|
||||||
if (settings.sipDisplayName) {
|
|
||||||
setSipDisplayName(settings.sipDisplayName);
|
|
||||||
}
|
|
||||||
if (settings.apiKey) {
|
|
||||||
setApiKey(settings.apiKey);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit}>
|
<Tabs isFitted colorScheme={DEFAULT_COLOR_SCHEME}>
|
||||||
<VStack spacing={2}>
|
<TabList mb="1em" gap={1}>
|
||||||
<FormControl id="jambonz_sip_domain">
|
<Tab>Basic</Tab>
|
||||||
<FormLabel>Jambonz SIP Domain</FormLabel>
|
<Tab>Advanced</Tab>
|
||||||
<Input
|
</TabList>
|
||||||
type="text"
|
|
||||||
placeholder="Domain"
|
|
||||||
isRequired
|
|
||||||
value={sipDomain}
|
|
||||||
onChange={(e) => setSipDomain(e.target.value)}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormControl id="jambonz_server_address">
|
<TabPanels mt={1}>
|
||||||
<FormLabel>Jambonz Server Address</FormLabel>
|
<TabPanel p={0}>
|
||||||
<Input
|
<BasicSettings />
|
||||||
type="text"
|
</TabPanel>
|
||||||
placeholder="wss://sip.jambonz.cloud:8443/"
|
<TabPanel p={0}>
|
||||||
isRequired
|
<AdvancedSettings />
|
||||||
value={sipServerAddress}
|
</TabPanel>
|
||||||
onChange={(e) => setSipServerAddress(e.target.value)}
|
</TabPanels>
|
||||||
/>
|
</Tabs>
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormControl id="username">
|
|
||||||
<FormLabel>SIP Username</FormLabel>
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
placeholder="Username"
|
|
||||||
isRequired
|
|
||||||
value={sipUsername}
|
|
||||||
onChange={(e) => setSipUsername(e.target.value)}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormControl id="password">
|
|
||||||
<FormLabel fontWeight="">SIP Password</FormLabel>
|
|
||||||
<PasswordInput
|
|
||||||
password={[sipPassword, setSipPassword]}
|
|
||||||
placeHolder="Enter your password"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormControl id="sip_display_name">
|
|
||||||
<FormLabel>SIP Display Name</FormLabel>
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
placeholder="Display name"
|
|
||||||
isRequired
|
|
||||||
value={sipDisplayName}
|
|
||||||
onChange={(e) => setSipDisplayName(e.target.value)}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormControl id="api_key">
|
|
||||||
<FormLabel>API Key (optional)</FormLabel>
|
|
||||||
<PasswordInput password={[apiKey, setApiKey]} />
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<Button colorScheme="jambonz" type="submit" w="full">
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
<HStack w="full">
|
|
||||||
<HStack spacing={1}>
|
|
||||||
<Image src={InfoIcon} w="30px" h="30px" />
|
|
||||||
<Text fontSize="14px">Get help</Text>
|
|
||||||
</HStack>
|
|
||||||
|
|
||||||
<Spacer />
|
|
||||||
<HStack spacing={1}>
|
|
||||||
<Image src={ResetIcon} w="30px" h="30px" />
|
|
||||||
<Text fontSize="14px" onClick={resetSetting} cursor="pointer">
|
|
||||||
Reset settings
|
|
||||||
</Text>
|
|
||||||
</HStack>
|
|
||||||
</HStack>
|
|
||||||
</VStack>
|
|
||||||
</form>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -184,7 +184,7 @@ function getHtmlPlugins(chunks) {
|
|||||||
return chunks.map(
|
return chunks.map(
|
||||||
(chunk) =>
|
(chunk) =>
|
||||||
new HTMLPlugin({
|
new HTMLPlugin({
|
||||||
title: "Jambonz Webrtc Client",
|
title: "Jambonz Webphone",
|
||||||
filename: `${chunk}.html`,
|
filename: `${chunk}.html`,
|
||||||
chunks: [chunk],
|
chunks: [chunk],
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user