mirror of
https://github.com/jambonz/chrome-extension-dialer.git
synced 2025-12-19 04:47:45 +00:00
2579
package-lock.json
generated
2579
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
19
package.json
19
package.json
@@ -1,14 +1,14 @@
|
|||||||
{
|
{
|
||||||
"name": "webrtc-chrome-ext",
|
"name": "webrtc-chrome-ext",
|
||||||
"version": "1.0.5",
|
"version": "1.0.6",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chakra-ui/react": "^2.8.0",
|
"@chakra-ui/react": "^2.8.0",
|
||||||
"@emotion/react": "^11.11.1",
|
"@emotion/react": "^11.11.1",
|
||||||
"@emotion/styled": "^11.11.0",
|
"@emotion/styled": "^11.11.0",
|
||||||
"@testing-library/jest-dom": "^5.17.0",
|
"@fortawesome/fontawesome-svg-core": "^6.5.2",
|
||||||
"@testing-library/react": "^13.4.0",
|
"@fortawesome/free-solid-svg-icons": "^6.5.2",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
"framer-motion": "^10.16.4",
|
"framer-motion": "^10.16.4",
|
||||||
@@ -17,13 +17,13 @@
|
|||||||
"jssip": "^3.10.0",
|
"jssip": "^3.10.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-feather": "^2.0.10",
|
|
||||||
"react-scripts": "5.0.1",
|
|
||||||
"typescript": "^4.9.5",
|
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^9.0.1",
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@testing-library/jest-dom": "^5.17.0",
|
||||||
|
"@testing-library/react": "^13.4.0",
|
||||||
|
"@testing-library/user-event": "^13.5.0",
|
||||||
"@types/chrome": "^0.0.242",
|
"@types/chrome": "^0.0.242",
|
||||||
"@types/google-libphonenumber": "^7.4.27",
|
"@types/google-libphonenumber": "^7.4.27",
|
||||||
"@types/jest": "^27.5.2",
|
"@types/jest": "^27.5.2",
|
||||||
@@ -34,8 +34,10 @@
|
|||||||
"copy-webpack-plugin": "^11.0.0",
|
"copy-webpack-plugin": "^11.0.0",
|
||||||
"css-loader": "^6.8.1",
|
"css-loader": "^6.8.1",
|
||||||
"file-loader": "^6.2.0",
|
"file-loader": "^6.2.0",
|
||||||
|
"react-scripts": "5.0.1",
|
||||||
"sass": "^1.64.1",
|
"sass": "^1.64.1",
|
||||||
"sass-loader": "^13.3.2",
|
"sass-loader": "^13.3.2",
|
||||||
|
"serve": "^14.2.1",
|
||||||
"style-loader": "^3.3.3",
|
"style-loader": "^3.3.3",
|
||||||
"ts-loader": "^9.4.4",
|
"ts-loader": "^9.4.4",
|
||||||
"webpack": "^5.88.2",
|
"webpack": "^5.88.2",
|
||||||
@@ -43,7 +45,8 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "webpack --config webpack.config.js",
|
"build": "webpack --config webpack.config.js",
|
||||||
"watch": "webpack -w --config webpack.config.js"
|
"watch": "webpack -w --config webpack.config.js",
|
||||||
|
"start": "react-scripts start"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": [
|
"extends": [
|
||||||
|
|||||||
43
public/index.html
Normal file
43
public/index.html
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Web site created using create-react-app"
|
||||||
|
/>
|
||||||
|
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||||
|
<!--
|
||||||
|
manifest.json provides metadata used when your web app is installed on a
|
||||||
|
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||||
|
-->
|
||||||
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||||
|
<!--
|
||||||
|
Notice the use of %PUBLIC_URL% in the tags above.
|
||||||
|
It will be replaced with the URL of the `public` folder during the build.
|
||||||
|
Only files inside the `public` folder can be referenced from the HTML.
|
||||||
|
|
||||||
|
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||||
|
work correctly both with client-side routing and a non-root public URL.
|
||||||
|
Learn how to configure a non-root public URL by running `npm run build`.
|
||||||
|
-->
|
||||||
|
<title>Jambonz Webphone</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
<!--
|
||||||
|
This HTML file is a template.
|
||||||
|
If you open it directly in the browser, you will see an empty page.
|
||||||
|
|
||||||
|
You can add webfonts, meta tags, or analytics to this file.
|
||||||
|
The build step will place the bundled scripts into the <body> tag.
|
||||||
|
|
||||||
|
To begin the development, run `npm start` or `yarn start`.
|
||||||
|
To create a production bundle, use `npm run build` or `yarn build`.
|
||||||
|
-->
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"version": "1.0.5",
|
"version": "1.0.6",
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "jambonz webrtc phone",
|
"name": "jambonz webrtc phone",
|
||||||
"description": "jambonz webrtc phone",
|
"description": "jambonz webrtc phone",
|
||||||
|
|||||||
33
src/App.tsx
33
src/App.tsx
@@ -1,33 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import "./styles.scss";
|
|
||||||
import { Button, Flex, Text } from "@chakra-ui/react";
|
|
||||||
import { openPhonePopup } from "./utils";
|
|
||||||
export const App = () => {
|
|
||||||
const handleClick = () => {
|
|
||||||
openPhonePopup();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex
|
|
||||||
justifyContent="center"
|
|
||||||
flexFlow="column"
|
|
||||||
height="100%"
|
|
||||||
padding="20px"
|
|
||||||
alignItems="center"
|
|
||||||
>
|
|
||||||
<Text fontSize="24px" mb={5}>
|
|
||||||
Click 'Start' to activate the service.
|
|
||||||
</Text>
|
|
||||||
<Button
|
|
||||||
size="lg"
|
|
||||||
width="full"
|
|
||||||
onClick={handleClick}
|
|
||||||
colorScheme="jambonz"
|
|
||||||
>
|
|
||||||
Start
|
|
||||||
</Button>
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default App;
|
|
||||||
@@ -1,13 +1,17 @@
|
|||||||
import {
|
import {
|
||||||
Application,
|
Application,
|
||||||
|
ConferenceParticipantAction,
|
||||||
|
ConferenceParticipantActions,
|
||||||
FetchError,
|
FetchError,
|
||||||
FetchTransport,
|
FetchTransport,
|
||||||
Queue,
|
Queue,
|
||||||
RegisteredUser,
|
RegisteredUser,
|
||||||
StatusCodes,
|
StatusCodes,
|
||||||
|
UpdateCall,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { MSG_SOMETHING_WRONG } from "./constants";
|
import { MSG_SOMETHING_WRONG } from "./constants";
|
||||||
import { getAdvancedSettings } from "src/storage";
|
import { getAdvancedSettings } from "src/storage";
|
||||||
|
import { EmptyData } from "src/common/types";
|
||||||
|
|
||||||
const fetchTransport = <Type>(
|
const fetchTransport = <Type>(
|
||||||
url: string,
|
url: string,
|
||||||
@@ -152,3 +156,23 @@ export const getSelfRegisteredUser = (username: string) => {
|
|||||||
`${advancedSettings.apiServer}/Accounts/${advancedSettings.accountSid}/RegisteredSipUsers/${username}`
|
`${advancedSettings.apiServer}/Accounts/${advancedSettings.accountSid}/RegisteredSipUsers/${username}`
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getConferences = () => {
|
||||||
|
const advancedSettings = getAdvancedSettings();
|
||||||
|
return getFetch<string[]>(
|
||||||
|
`${advancedSettings.apiServer}/Accounts/${advancedSettings.accountSid}/Conferences`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateConferenceParticipantAction = (
|
||||||
|
callSid: string,
|
||||||
|
payload: ConferenceParticipantAction
|
||||||
|
) => {
|
||||||
|
const advancedSettings = getAdvancedSettings();
|
||||||
|
return putFetch<EmptyData, UpdateCall>(
|
||||||
|
`${advancedSettings.apiServer}/Accounts/${advancedSettings.accountSid}/Calls/${callSid}`,
|
||||||
|
{
|
||||||
|
conferenceParticipantAction: payload,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -45,3 +45,23 @@ export interface RegisteredUser {
|
|||||||
allow_direct_user_calling: boolean;
|
allow_direct_user_calling: boolean;
|
||||||
registered_status: string;
|
registered_status: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ConferenceParticipantActions =
|
||||||
|
| "tag"
|
||||||
|
| "untag"
|
||||||
|
| "coach"
|
||||||
|
| "uncoach"
|
||||||
|
| "mute"
|
||||||
|
| "unmute"
|
||||||
|
| "hold"
|
||||||
|
| "unhold";
|
||||||
|
|
||||||
|
export type ConferenceModes = "full_participant" | "muted" | "coach";
|
||||||
|
|
||||||
|
export interface ConferenceParticipantAction {
|
||||||
|
action: ConferenceParticipantActions;
|
||||||
|
tag: string;
|
||||||
|
}
|
||||||
|
export interface UpdateCall {
|
||||||
|
conferenceParticipantAction: ConferenceParticipantAction;
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const initiateNewPhonePopup = () => {
|
|||||||
{
|
{
|
||||||
url: chrome.runtime.getURL("window/index.html"),
|
url: chrome.runtime.getURL("window/index.html"),
|
||||||
width: 440,
|
width: 440,
|
||||||
height: 720,
|
height: 750,
|
||||||
focused: true,
|
focused: true,
|
||||||
type: "panel",
|
type: "panel",
|
||||||
state: "normal",
|
state: "normal",
|
||||||
|
|||||||
42
src/components/outline-box/index.tsx
Normal file
42
src/components/outline-box/index.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { VStack, Text } from "@chakra-ui/react";
|
||||||
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
|
const OutlineBox = ({
|
||||||
|
title,
|
||||||
|
children,
|
||||||
|
h,
|
||||||
|
borderColor,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
children: ReactNode;
|
||||||
|
h?: string;
|
||||||
|
borderColor?: string;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<VStack align="stretch" h={h || ""} w="full">
|
||||||
|
<VStack
|
||||||
|
border="1px"
|
||||||
|
borderColor={borderColor || ""}
|
||||||
|
position="relative"
|
||||||
|
borderRadius={5}
|
||||||
|
p={4}
|
||||||
|
h={h || ""}
|
||||||
|
align="start"
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
position="absolute"
|
||||||
|
top={-3}
|
||||||
|
left={2}
|
||||||
|
bg="white"
|
||||||
|
color={borderColor || ""}
|
||||||
|
px={2}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
{children}
|
||||||
|
</VStack>
|
||||||
|
</VStack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OutlineBox;
|
||||||
@@ -1,12 +1,7 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import {
|
import { Input, InputGroup, InputRightElement, Button } from "@chakra-ui/react";
|
||||||
Input,
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
InputGroup,
|
import { faEye, faEyeSlash } from "@fortawesome/free-solid-svg-icons";
|
||||||
InputRightElement,
|
|
||||||
Button,
|
|
||||||
Box,
|
|
||||||
} from "@chakra-ui/react";
|
|
||||||
import { Eye, EyeOff } from "react-feather";
|
|
||||||
|
|
||||||
type PasswordInputProbs = {
|
type PasswordInputProbs = {
|
||||||
password: [string, React.Dispatch<React.SetStateAction<string>>];
|
password: [string, React.Dispatch<React.SetStateAction<string>>];
|
||||||
@@ -34,7 +29,7 @@ function PasswordInput({
|
|||||||
/>
|
/>
|
||||||
<InputRightElement width="4.5rem">
|
<InputRightElement width="4.5rem">
|
||||||
<Button h="1.75rem" size="sm" onClick={handleClick} variant="unstyled">
|
<Button h="1.75rem" size="sm" onClick={handleClick} variant="unstyled">
|
||||||
{showPassword ? <EyeOff /> : <Eye />}
|
<FontAwesomeIcon icon={showPassword ? faEyeSlash : faEye} />
|
||||||
</Button>
|
</Button>
|
||||||
</InputRightElement>
|
</InputRightElement>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
import App from "./App";
|
|
||||||
import { ChakraProvider } from "@chakra-ui/react";
|
import { ChakraProvider } from "@chakra-ui/react";
|
||||||
import mainTheme from "./theme";
|
import mainTheme from "./theme";
|
||||||
|
import WindowApp from "./window/app";
|
||||||
|
|
||||||
|
// This file is being used only for dev.
|
||||||
const root = document.createElement("div");
|
const root = document.createElement("div");
|
||||||
root.className = "container";
|
root.className = "container";
|
||||||
document.body.appendChild(root);
|
document.body.appendChild(root);
|
||||||
@@ -11,7 +12,7 @@ const rootDiv = ReactDOM.createRoot(root);
|
|||||||
rootDiv.render(
|
rootDiv.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<ChakraProvider theme={mainTheme}>
|
<ChakraProvider theme={mainTheme}>
|
||||||
<App />
|
<WindowApp />
|
||||||
</ChakraProvider>
|
</ChakraProvider>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,27 +8,41 @@ export default class SipAudioElements {
|
|||||||
#localHungup: HTMLAudioElement;
|
#localHungup: HTMLAudioElement;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.#ringing = new Audio(chrome.runtime.getURL("audios/ringing.mp3"));
|
this.#ringing = this.getAudio("audios/ringing.mp3");
|
||||||
this.#ringing.loop = true;
|
this.#ringing.loop = true;
|
||||||
this.#ringing.volume = 0.8;
|
this.#ringing.volume = 0.8;
|
||||||
this.#ringBack = new Audio(chrome.runtime.getURL("audios/us-ringback.mp3"));
|
this.#ringBack = this.getAudio("audios/us-ringback.mp3");
|
||||||
this.#ringBack.loop = true;
|
this.#ringBack.loop = true;
|
||||||
this.#ringBack.volume = 0.8;
|
this.#ringBack.volume = 0.8;
|
||||||
this.#failed = new Audio(chrome.runtime.getURL("audios/call-failed.mp3"));
|
this.#failed = this.getAudio("audios/call-failed.mp3");
|
||||||
this.#failed.volume = 0.3;
|
this.#failed.volume = 0.3;
|
||||||
this.#busy = new Audio(chrome.runtime.getURL("audios/us-busy-signal.mp3"));
|
this.#busy = this.getAudio("audios/us-busy-signal.mp3");
|
||||||
this.#busy.volume = 0.3;
|
this.#busy.volume = 0.3;
|
||||||
this.#hungup = new Audio(
|
this.#hungup = this.getAudio("audios/remote-party-hungup-tone.mp3");
|
||||||
chrome.runtime.getURL("audios/remote-party-hungup-tone.mp3")
|
|
||||||
);
|
|
||||||
this.#hungup.volume = 0.3;
|
this.#hungup.volume = 0.3;
|
||||||
this.#localHungup = new Audio(
|
this.#localHungup = this.getAudio("audios/local-party-hungup-tone.mp3");
|
||||||
chrome.runtime.getURL("audios/local-party-hungup-tone.mp3")
|
|
||||||
);
|
|
||||||
this.#localHungup.volume = 0.3;
|
this.#localHungup.volume = 0.3;
|
||||||
this.#remote = new Audio();
|
this.#remote = new Audio();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getAudio(path: string) {
|
||||||
|
let audioURL;
|
||||||
|
|
||||||
|
// Check if we're in a Chrome extension
|
||||||
|
if (
|
||||||
|
typeof chrome !== "undefined" &&
|
||||||
|
chrome.runtime &&
|
||||||
|
chrome.runtime.getURL
|
||||||
|
) {
|
||||||
|
audioURL = chrome.runtime.getURL(path);
|
||||||
|
} else {
|
||||||
|
// We're in a web context, adjust this path as necessary
|
||||||
|
audioURL = `/${path}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Audio(audioURL);
|
||||||
|
}
|
||||||
|
|
||||||
playLocalHungup(volume: number | undefined) {
|
playLocalHungup(volume: number | undefined) {
|
||||||
this.pauseRingback();
|
this.pauseRingback();
|
||||||
this.pauseRinging();
|
this.pauseRinging();
|
||||||
|
|||||||
@@ -62,12 +62,18 @@ export default class SipSession extends events.EventEmitter {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.#rtcSession.on("accepted", () => {
|
this.#rtcSession.on(
|
||||||
this.emit(SipConstants.SESSION_ANSWERED, {
|
"accepted",
|
||||||
status: SipConstants.SESSION_ANSWERED,
|
({ response }: { response: IncomingResponse }) => {
|
||||||
});
|
this.emit(SipConstants.SESSION_ANSWERED, {
|
||||||
this.#audio.playAnswer(undefined);
|
status: SipConstants.SESSION_ANSWERED,
|
||||||
});
|
callSid: response.hasHeader("X-Call-Sid")
|
||||||
|
? response.getHeader("X-Call-Sid")
|
||||||
|
: null,
|
||||||
|
});
|
||||||
|
this.#audio.playAnswer(undefined);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
this.#rtcSession.on("failed", (data: EndEvent): void => {
|
this.#rtcSession.on("failed", (data: EndEvent): void => {
|
||||||
let { originator, cause, message } = data;
|
let { originator, cause, message } = data;
|
||||||
@@ -195,16 +201,19 @@ export default class SipSession extends events.EventEmitter {
|
|||||||
direction: this.#rtcSession.direction,
|
direction: this.#rtcSession.direction,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
// pc.addEventListener('track', (event: RTCPeerConnectionEventMap["track"]): void => {
|
// pc.addEventListener(
|
||||||
// const stream: MediaStream = new MediaStream([event.track])
|
// "track",
|
||||||
// if (this.#rtcSession.direction === 'outgoing') {
|
// (event: RTCPeerConnectionEventMap["track"]): void => {
|
||||||
// this.#audio.pauseRinging();
|
// const stream: MediaStream = new MediaStream([event.track]);
|
||||||
|
// if (this.#rtcSession.direction === "outgoing") {
|
||||||
|
// this.#audio.pauseRinging();
|
||||||
// }
|
// }
|
||||||
// this.#audio.playRemote(stream, "track");
|
// this.#audio.playRemote(stream);
|
||||||
// this.emit(SipConstants.SESSION_TRACK, {
|
// this.emit(SipConstants.SESSION_TRACK, {
|
||||||
// direction: this.#rtcSession.direction
|
// direction: this.#rtcSession.direction,
|
||||||
// });
|
// });
|
||||||
// });
|
// }
|
||||||
|
// );
|
||||||
}
|
}
|
||||||
|
|
||||||
get rtcSession() {
|
get rtcSession() {
|
||||||
|
|||||||
@@ -1,108 +1,100 @@
|
|||||||
import {
|
import { SipSession, SipModel, SipConstants } from "./index";
|
||||||
SipSession, SipModel, SipConstants
|
|
||||||
} from "./index";
|
|
||||||
|
|
||||||
|
|
||||||
export default class SipSessionManager {
|
export default class SipSessionManager {
|
||||||
|
#sessions: Map<string, SipModel.SipSessionState>;
|
||||||
|
|
||||||
#sessions: Map<string, SipModel.SipSessionState>;
|
constructor() {
|
||||||
|
this.#sessions = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
constructor() {
|
activate(session: SipSession) {
|
||||||
this.#sessions = new Map();
|
this.#sessions.forEach((v, k) => {
|
||||||
|
if (k !== session.id) {
|
||||||
|
v.active = false;
|
||||||
|
session.setActive(false);
|
||||||
|
} else {
|
||||||
|
v.active = true;
|
||||||
|
session.setActive(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSession(field: string, session: SipSession, args: any): void {
|
||||||
|
const state: SipModel.SipSessionState = this.getSessionState(session.id);
|
||||||
|
if (state) {
|
||||||
|
switch (field) {
|
||||||
|
case SipConstants.SESSION_RINGING:
|
||||||
|
state.status = args.status;
|
||||||
|
break;
|
||||||
|
case SipConstants.SESSION_ANSWERED:
|
||||||
|
state.status = args.status;
|
||||||
|
break;
|
||||||
|
case SipConstants.SESSION_FAILED:
|
||||||
|
case SipConstants.SESSION_ENDED:
|
||||||
|
state.status = args.status;
|
||||||
|
state.endState = {
|
||||||
|
cause: args.cause,
|
||||||
|
status: args.status,
|
||||||
|
originator: args.endState,
|
||||||
|
description: args.description,
|
||||||
|
};
|
||||||
|
this.#sessions.delete(session.id);
|
||||||
|
break;
|
||||||
|
case SipConstants.SESSION_MUTED:
|
||||||
|
state.muteStatus = args.status;
|
||||||
|
break;
|
||||||
|
case SipConstants.SESSION_HOLD:
|
||||||
|
state.holdState = {
|
||||||
|
originator: args.originator,
|
||||||
|
status: args.status,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case SipConstants.SESSION_ICE_READY:
|
||||||
|
state.iceReady = true;
|
||||||
|
break;
|
||||||
|
case SipConstants.SESSION_ACTIVE:
|
||||||
|
state.active = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getSessionState(id: string): SipModel.SipSessionState {
|
||||||
|
const state = this.#sessions.get(id);
|
||||||
|
if (!state) {
|
||||||
|
throw new Error("Session not found");
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSession(id: string): SipSession {
|
||||||
|
return this.getSessionState(id).sipSession;
|
||||||
|
}
|
||||||
|
|
||||||
|
newSession(session: SipSession): void {
|
||||||
|
this.#sessions.set(session.id, {
|
||||||
|
id: session.id,
|
||||||
|
sipSession: session,
|
||||||
|
startDateTime: new Date(),
|
||||||
|
active: true,
|
||||||
|
status: "init",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get activeSession(): SipSession {
|
||||||
|
if (this.#sessions.size === 0) {
|
||||||
|
throw new Error("No sessions");
|
||||||
}
|
}
|
||||||
|
|
||||||
activate(session: SipSession) {
|
const state = [...this.#sessions.values()].filter((s) => s.active);
|
||||||
this.#sessions.forEach((v, k) => {
|
if (state.length) {
|
||||||
if (k !== session.id) {
|
return state[0].sipSession;
|
||||||
v.active = false;
|
} else {
|
||||||
session.setActive(false);
|
throw new Error("No Active sessions");
|
||||||
} else {
|
|
||||||
v.active = true;
|
|
||||||
session.setActive(true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
updateSession(field: string, session: SipSession, args: any): void {
|
get count() {
|
||||||
|
return this.#sessions.size;
|
||||||
const state: SipModel.SipSessionState = this.getSessionState(session.id);
|
}
|
||||||
if (state) {
|
}
|
||||||
switch (field) {
|
|
||||||
case SipConstants.SESSION_RINGING:
|
|
||||||
state.status = args.status;
|
|
||||||
break;
|
|
||||||
case SipConstants.SESSION_ANSWERED:
|
|
||||||
state.status = args.status;
|
|
||||||
break;
|
|
||||||
case SipConstants.SESSION_FAILED:
|
|
||||||
case SipConstants.SESSION_ENDED:
|
|
||||||
state.status = args.status;
|
|
||||||
state.endState = {
|
|
||||||
cause: args.cause,
|
|
||||||
status: args.status,
|
|
||||||
originator: args.endState,
|
|
||||||
description: args.description
|
|
||||||
}
|
|
||||||
this.#sessions.delete(session.id);
|
|
||||||
break;
|
|
||||||
case SipConstants.SESSION_MUTED:
|
|
||||||
state.muteStatus = args.status;
|
|
||||||
break;
|
|
||||||
case SipConstants.SESSION_HOLD:
|
|
||||||
state.holdState = {
|
|
||||||
originator: args.originator,
|
|
||||||
status: args.status
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case SipConstants.SESSION_ICE_READY:
|
|
||||||
state.iceReady = true;
|
|
||||||
break;
|
|
||||||
case SipConstants.SESSION_ACTIVE:
|
|
||||||
state.active = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getSessionState(id: string): SipModel.SipSessionState {
|
|
||||||
const state = this.#sessions.get(id);
|
|
||||||
if (!state) {
|
|
||||||
throw new Error("Session not found");
|
|
||||||
}
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
getSession(id: string): SipSession {
|
|
||||||
return this.getSessionState(id).sipSession;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
newSession(session: SipSession): void {
|
|
||||||
this.#sessions.set(session.id,
|
|
||||||
{
|
|
||||||
id: session.id,
|
|
||||||
sipSession: session,
|
|
||||||
startDateTime: new Date(),
|
|
||||||
active: true,
|
|
||||||
status: 'init',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
get activeSession(): SipSession {
|
|
||||||
if (this.#sessions.size === 0) {
|
|
||||||
throw new Error("No sessions");
|
|
||||||
}
|
|
||||||
|
|
||||||
const state = [...this.#sessions.values()].filter((s) => s.active);
|
|
||||||
if (state.length) {
|
|
||||||
return state[0].sipSession;
|
|
||||||
} else {
|
|
||||||
throw new Error("No Active sessions");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get count() {
|
|
||||||
return this.#sessions.size;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ export default class SipUA extends events.EventEmitter {
|
|||||||
display_name: client.name,
|
display_name: client.name,
|
||||||
sockets: [new WebSocketInterface(settings.wsUri)],
|
sockets: [new WebSocketInterface(settings.wsUri)],
|
||||||
register: settings.register,
|
register: settings.register,
|
||||||
|
register_expires: 600,
|
||||||
});
|
});
|
||||||
this.#ua.on("connecting", (data: UAConnectingEvent) =>
|
this.#ua.on("connecting", (data: UAConnectingEvent) =>
|
||||||
this.emit(SipConstants.UA_CONNECTING, { ...data, client })
|
this.emit(SipConstants.UA_CONNECTING, { ...data, client })
|
||||||
|
|||||||
@@ -3,7 +3,11 @@ function normalizeNumber(number: string): string {
|
|||||||
return number;
|
return number;
|
||||||
} else if (/@/i.test(number)) {
|
} else if (/@/i.test(number)) {
|
||||||
return number;
|
return number;
|
||||||
} else if (number.startsWith("app-") || number.startsWith("queue-")) {
|
} else if (
|
||||||
|
number.startsWith("app-") ||
|
||||||
|
number.startsWith("queue-") ||
|
||||||
|
number.startsWith("conference-")
|
||||||
|
) {
|
||||||
return number;
|
return number;
|
||||||
} else {
|
} else {
|
||||||
return number.replace(/[()\-. ]*/g, "");
|
return number.replace(/[()\-. ]*/g, "");
|
||||||
|
|||||||
@@ -104,10 +104,11 @@ export const WindowApp = () => {
|
|||||||
index={tabIndex}
|
index={tabIndex}
|
||||||
>
|
>
|
||||||
<TabList mb="1em" gap={1}>
|
<TabList mb="1em" gap={1}>
|
||||||
{tabsSettings.map((s) => (
|
{tabsSettings.map((s, i) => (
|
||||||
<Tab
|
<Tab
|
||||||
_selected={{ color: "white", bg: "jambonz.500" }}
|
_selected={{ color: "white", bg: "jambonz.500" }}
|
||||||
bg="grey.500"
|
bg="grey.500"
|
||||||
|
key={i}
|
||||||
>
|
>
|
||||||
{s.title}
|
{s.title}
|
||||||
</Tab>
|
</Tab>
|
||||||
@@ -115,8 +116,8 @@ export const WindowApp = () => {
|
|||||||
</TabList>
|
</TabList>
|
||||||
|
|
||||||
<TabPanels>
|
<TabPanels>
|
||||||
{tabsSettings.map((s) => (
|
{tabsSettings.map((s, i) => (
|
||||||
<TabPanel>{s.content}</TabPanel>
|
<TabPanel key={i}>{s.content}</TabPanel>
|
||||||
))}
|
))}
|
||||||
</TabPanels>
|
</TabPanels>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
import {
|
import {
|
||||||
HStack,
|
HStack,
|
||||||
Icon,
|
|
||||||
IconButton,
|
IconButton,
|
||||||
Spacer,
|
Spacer,
|
||||||
Text,
|
Text,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
VStack,
|
VStack,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
|
import {
|
||||||
|
faArrowRightFromBracket,
|
||||||
|
faArrowRightToBracket,
|
||||||
|
faPhone,
|
||||||
|
faSave,
|
||||||
|
faTrash,
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
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";
|
||||||
@@ -36,11 +36,11 @@ export const CallHistoryItem = ({
|
|||||||
const [callEnable, setCallEnable] = useState(false);
|
const [callEnable, setCallEnable] = useState(false);
|
||||||
const getDirectionIcon = (direction: SipCallDirection) => {
|
const getDirectionIcon = (direction: SipCallDirection) => {
|
||||||
if (direction === "outgoing") {
|
if (direction === "outgoing") {
|
||||||
return PhoneOutgoing;
|
return faArrowRightFromBracket;
|
||||||
} else if (direction === "incoming") {
|
} else if (direction === "incoming") {
|
||||||
return PhoneIncoming;
|
return faArrowRightToBracket;
|
||||||
} else {
|
} else {
|
||||||
return Phone;
|
return faPhone;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ export const CallHistoryItem = ({
|
|||||||
<Tooltip label="Call">
|
<Tooltip label="Call">
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label="call recents"
|
aria-label="call recents"
|
||||||
icon={<Phone />}
|
icon={<FontAwesomeIcon icon={faPhone} />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (onCallNumber) {
|
if (onCallNumber) {
|
||||||
onCallNumber(call.number, call.name);
|
onCallNumber(call.number, call.name);
|
||||||
@@ -69,7 +69,11 @@ export const CallHistoryItem = ({
|
|||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : (
|
) : (
|
||||||
<Icon as={getDirectionIcon(call.direction)} w="20px" h="20px" />
|
<FontAwesomeIcon
|
||||||
|
icon={getDirectionIcon(call.direction)}
|
||||||
|
width="20px"
|
||||||
|
height="20px"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<VStack align="start">
|
<VStack align="start">
|
||||||
@@ -88,7 +92,11 @@ export const CallHistoryItem = ({
|
|||||||
<Tooltip label={isSaved && call.isSaved ? "Delete" : "Save"}>
|
<Tooltip label={isSaved && call.isSaved ? "Delete" : "Save"}>
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label="save recents"
|
aria-label="save recents"
|
||||||
icon={isSaved && call.isSaved ? <Trash2 /> : <Save />}
|
icon={
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={isSaved && call.isSaved ? faTrash : faSave}
|
||||||
|
/>
|
||||||
|
}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!isSaved && call.isSaved) {
|
if (!isSaved && call.isSaved) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -13,11 +13,12 @@ import {
|
|||||||
TabPanel,
|
TabPanel,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Search, Sliders } from "react-feather";
|
|
||||||
import { CallHistory } from "src/common/types";
|
import { CallHistory } from "src/common/types";
|
||||||
|
|
||||||
import { DEFAULT_COLOR_SCHEME } from "src/common/constants";
|
import { DEFAULT_COLOR_SCHEME } from "src/common/constants";
|
||||||
import Recents from "./recent";
|
import Recents from "./recent";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { faSearch, faSliders } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
type CallHistoriesProbs = {
|
type CallHistoriesProbs = {
|
||||||
calls: CallHistory[];
|
calls: CallHistory[];
|
||||||
@@ -51,11 +52,11 @@ export const CallHistories = ({
|
|||||||
fontWeight="normal"
|
fontWeight="normal"
|
||||||
/>
|
/>
|
||||||
<InputLeftElement mr={2}>
|
<InputLeftElement mr={2}>
|
||||||
<Icon as={Search} w="20px" h="20px" />
|
<FontAwesomeIcon icon={faSearch} width="20px" height="20px" />
|
||||||
</InputLeftElement>
|
</InputLeftElement>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
<HStack spacing={2} bg="grey.100" p={2} borderRadius={7}>
|
<HStack spacing={2} bg="grey.100" p={2} borderRadius={7}>
|
||||||
<Icon as={Sliders} w="20px" h="20px" />
|
<FontAwesomeIcon icon={faSliders} width="20px" height="20px" />
|
||||||
<Text fontSize="12px" fontWeight="500">
|
<Text fontSize="12px" fontWeight="500">
|
||||||
Filter
|
Filter
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -51,8 +51,9 @@ export const Recents = ({
|
|||||||
spacing={2}
|
spacing={2}
|
||||||
mt={2}
|
mt={2}
|
||||||
>
|
>
|
||||||
{callHistories.map((c) => (
|
{callHistories.map((c, i) => (
|
||||||
<CallHistoryItem
|
<CallHistoryItem
|
||||||
|
key={i}
|
||||||
isSaved={isSaved}
|
isSaved={isSaved}
|
||||||
call={c}
|
call={c}
|
||||||
onCallNumber={onCallNumber}
|
onCallNumber={onCallNumber}
|
||||||
|
|||||||
@@ -4,9 +4,23 @@ export default class DialPadAudioElements {
|
|||||||
constructor() {
|
constructor() {
|
||||||
const arr = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "*", "#"];
|
const arr = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "*", "#"];
|
||||||
for (const i of arr) {
|
for (const i of arr) {
|
||||||
this.keySounds[i] = new Audio(
|
let audioURL;
|
||||||
chrome.runtime.getURL(`audios/dtmf-${encodeURIComponent(i)}.mp3`)
|
|
||||||
);
|
// Check if we're in a Chrome extension
|
||||||
|
if (
|
||||||
|
typeof chrome !== "undefined" &&
|
||||||
|
chrome.runtime &&
|
||||||
|
chrome.runtime.getURL
|
||||||
|
) {
|
||||||
|
audioURL = chrome.runtime.getURL(
|
||||||
|
`audios/dtmf-${encodeURIComponent(i)}.mp3`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// We're in a web context, adjust this path as necessary
|
||||||
|
audioURL = `/audios/dtmf-${encodeURIComponent(i)}.mp3`;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.keySounds[i] = new Audio(audioURL);
|
||||||
const audio = this.keySounds[i];
|
const audio = this.keySounds[i];
|
||||||
if (audio) {
|
if (audio) {
|
||||||
audio.volume = 0.5;
|
audio.volume = 0.5;
|
||||||
|
|||||||
194
src/window/phone/conference.tsx
Normal file
194
src/window/phone/conference.tsx
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
FormControl,
|
||||||
|
FormLabel,
|
||||||
|
HStack,
|
||||||
|
Input,
|
||||||
|
Radio,
|
||||||
|
RadioGroup,
|
||||||
|
Text,
|
||||||
|
VStack,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import { FormEvent, useEffect, useState } from "react";
|
||||||
|
import { updateConferenceParticipantAction } from "src/api";
|
||||||
|
import { ConferenceModes } from "src/api/types";
|
||||||
|
import OutlineBox from "src/components/outline-box";
|
||||||
|
import { SipConstants } from "src/lib";
|
||||||
|
|
||||||
|
type JoinConferenceProbs = {
|
||||||
|
conferenceId?: string;
|
||||||
|
callSid: string;
|
||||||
|
callDuration: number;
|
||||||
|
callStatus: string;
|
||||||
|
handleCancel: () => void;
|
||||||
|
call: (conference: string) => void;
|
||||||
|
};
|
||||||
|
export const JoinConference = ({
|
||||||
|
conferenceId,
|
||||||
|
callSid,
|
||||||
|
callDuration,
|
||||||
|
callStatus,
|
||||||
|
handleCancel,
|
||||||
|
call,
|
||||||
|
}: JoinConferenceProbs) => {
|
||||||
|
const [conferenceName, setConferenceName] = useState(conferenceId || "");
|
||||||
|
const [appTitle, setAppTitle] = useState(
|
||||||
|
!!conferenceId ? "Joining Conference" : "Start Conference"
|
||||||
|
);
|
||||||
|
const [submitTitle, setSubmitTitle] = useState(
|
||||||
|
!!conferenceId ? "Joining Conference" : "Start Conference"
|
||||||
|
);
|
||||||
|
const [cancelTitle, setCancelTitle] = useState("Cancel");
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [speakOnlyTo, setSpeakOnlyTo] = useState("");
|
||||||
|
const [tags, setTags] = useState("");
|
||||||
|
const [mode, setMode] = useState<ConferenceModes>("full_participant");
|
||||||
|
const [participantState, setParticipantState] = useState("Join as");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
switch (callStatus) {
|
||||||
|
case SipConstants.SESSION_ANSWERED:
|
||||||
|
setAppTitle("Conference");
|
||||||
|
setSubmitTitle("Update");
|
||||||
|
setCancelTitle("Hangup");
|
||||||
|
setParticipantState("Participant state");
|
||||||
|
setIsLoading(false);
|
||||||
|
configureConferenceSession();
|
||||||
|
break;
|
||||||
|
case SipConstants.SESSION_ENDED:
|
||||||
|
case SipConstants.SESSION_FAILED:
|
||||||
|
setIsLoading(false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}, [callStatus]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
switch (mode) {
|
||||||
|
case "full_participant":
|
||||||
|
break;
|
||||||
|
case "muted":
|
||||||
|
break;
|
||||||
|
case "coach":
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}, [mode]);
|
||||||
|
|
||||||
|
const handleSubmit = (e: FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (callStatus !== SipConstants.SESSION_ANSWERED) {
|
||||||
|
call(conferenceName);
|
||||||
|
if (!callSid) {
|
||||||
|
setIsLoading(true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
configureConferenceSession();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const configureConferenceSession = async () => {
|
||||||
|
if (callSid) {
|
||||||
|
await updateConferenceParticipantAction(callSid, {
|
||||||
|
action: mode === "muted" ? "mute" : "unmute",
|
||||||
|
tag: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
await updateConferenceParticipantAction(callSid, {
|
||||||
|
action: tags ? "tag" : "untag",
|
||||||
|
tag: tags,
|
||||||
|
});
|
||||||
|
|
||||||
|
await updateConferenceParticipantAction(callSid, {
|
||||||
|
action: mode === "coach" ? "coach" : "uncoach",
|
||||||
|
tag: speakOnlyTo,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box as="form" onSubmit={handleSubmit} w="full">
|
||||||
|
<VStack spacing={4} mt="20px" w="full">
|
||||||
|
<Text fontWeight="bold" fontSize="lg">
|
||||||
|
{appTitle}
|
||||||
|
</Text>
|
||||||
|
{callDuration > 0 && (
|
||||||
|
<Text fontSize="15px">
|
||||||
|
{new Date(callDuration * 1000).toISOString().substr(11, 8)}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
<FormControl id="conference_name">
|
||||||
|
<FormLabel>Conference name</FormLabel>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="Name"
|
||||||
|
isRequired
|
||||||
|
value={conferenceName}
|
||||||
|
onChange={(e) => setConferenceName(e.target.value)}
|
||||||
|
disabled={!!conferenceId}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<OutlineBox title={participantState}>
|
||||||
|
<RadioGroup
|
||||||
|
onChange={(e) => setMode(e as ConferenceModes)}
|
||||||
|
value={mode}
|
||||||
|
colorScheme="jambonz"
|
||||||
|
>
|
||||||
|
<VStack align="start">
|
||||||
|
<Radio value="full_participant" variant="">
|
||||||
|
Full participant
|
||||||
|
</Radio>
|
||||||
|
<Radio value="muted">Muted</Radio>
|
||||||
|
<Radio value="coach">Coach mode</Radio>
|
||||||
|
</VStack>
|
||||||
|
</RadioGroup>
|
||||||
|
|
||||||
|
<FormControl id="speak_only_to">
|
||||||
|
<FormLabel>Speak only to</FormLabel>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="tag"
|
||||||
|
value={speakOnlyTo}
|
||||||
|
onChange={(e) => setSpeakOnlyTo(e.target.value)}
|
||||||
|
disabled={mode !== "coach"}
|
||||||
|
required={mode === "coach"}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl id="tag">
|
||||||
|
<FormLabel>Tag</FormLabel>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="tag"
|
||||||
|
value={tags}
|
||||||
|
onChange={(e) => setTags(e.target.value)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</OutlineBox>
|
||||||
|
<HStack w="full">
|
||||||
|
<Button
|
||||||
|
colorScheme="jambonz"
|
||||||
|
type="submit"
|
||||||
|
w="full"
|
||||||
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
|
{submitTitle}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
colorScheme="grey"
|
||||||
|
type="button"
|
||||||
|
w="full"
|
||||||
|
textColor="black"
|
||||||
|
onClick={handleCancel}
|
||||||
|
>
|
||||||
|
{cancelTitle}
|
||||||
|
</Button>
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default JoinConference;
|
||||||
@@ -53,10 +53,16 @@ export const DialPad = ({ handleDigitPress }: DialPadProbs) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box p={2} w="full" ref={selfRef}>
|
<Box p={2} w="full" h="280px" ref={selfRef}>
|
||||||
<VStack w="full" bg="grey.500" spacing={0.5}>
|
<VStack w="full" h="full" bg="grey.500" spacing={0.5}>
|
||||||
{buttons.map((row, rowIndex) => (
|
{buttons.map((row, rowIndex) => (
|
||||||
<HStack key={rowIndex} justifyContent="space-between" spacing={0.5}>
|
<HStack
|
||||||
|
key={rowIndex}
|
||||||
|
justifyContent="space-between"
|
||||||
|
spacing={0.5}
|
||||||
|
w="full"
|
||||||
|
h="full"
|
||||||
|
>
|
||||||
{row.map((num) => (
|
{row.map((num) => (
|
||||||
<Button
|
<Button
|
||||||
key={num}
|
key={num}
|
||||||
@@ -66,8 +72,8 @@ export const DialPad = ({ handleDigitPress }: DialPadProbs) => {
|
|||||||
}}
|
}}
|
||||||
size="lg"
|
size="lg"
|
||||||
p={0}
|
p={0}
|
||||||
width="124px"
|
width="calc(100% / 3)"
|
||||||
height="70px"
|
height="100%"
|
||||||
variant="unstyled"
|
variant="unstyled"
|
||||||
bg="white"
|
bg="white"
|
||||||
_hover={{
|
_hover={{
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
import { Button, VStack, Text, Icon, HStack, Spacer } from "@chakra-ui/react";
|
import {
|
||||||
import { PhoneCall } from "react-feather";
|
Button,
|
||||||
|
VStack,
|
||||||
|
Text,
|
||||||
|
Icon,
|
||||||
|
HStack,
|
||||||
|
Spacer,
|
||||||
|
Box,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import { faPhone } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { formatPhoneNumber } from "src/utils";
|
import { formatPhoneNumber } from "src/utils";
|
||||||
|
|
||||||
type IncommingCallProbs = {
|
type IncommingCallProbs = {
|
||||||
@@ -15,7 +24,13 @@ export const IncommingCall = ({
|
|||||||
}: IncommingCallProbs) => {
|
}: IncommingCallProbs) => {
|
||||||
return (
|
return (
|
||||||
<VStack alignItems="center" spacing={4} mt="130px" w="full">
|
<VStack alignItems="center" spacing={4} mt="130px" w="full">
|
||||||
<Icon as={PhoneCall} color="jambonz.500" w="60px" h="60px" />
|
<Box
|
||||||
|
as={FontAwesomeIcon}
|
||||||
|
icon={faPhone}
|
||||||
|
color="jambonz.500"
|
||||||
|
width="30px"
|
||||||
|
height="30px"
|
||||||
|
/>
|
||||||
<Text fontSize="15px">Incoming call from</Text>
|
<Text fontSize="15px">Incoming call from</Text>
|
||||||
<Text fontSize="24px" fontWeight="bold">
|
<Text fontSize="24px" fontWeight="bold">
|
||||||
{formatPhoneNumber(number)}
|
{formatPhoneNumber(number)}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
IconButton,
|
IconButton,
|
||||||
Image,
|
Image,
|
||||||
Input,
|
Input,
|
||||||
Select,
|
|
||||||
Spacer,
|
Spacer,
|
||||||
Text,
|
Text,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
@@ -15,16 +14,6 @@ import {
|
|||||||
useToast,
|
useToast,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import {
|
|
||||||
GitMerge,
|
|
||||||
List,
|
|
||||||
Mic,
|
|
||||||
MicOff,
|
|
||||||
Pause,
|
|
||||||
PhoneOff,
|
|
||||||
Play,
|
|
||||||
Users,
|
|
||||||
} from "react-feather";
|
|
||||||
import {
|
import {
|
||||||
AdvancedAppSettings,
|
AdvancedAppSettings,
|
||||||
SipCallDirection,
|
SipCallDirection,
|
||||||
@@ -53,6 +42,7 @@ import { v4 as uuidv4 } from "uuid";
|
|||||||
import IconButtonMenu, { IconButtonMenuItems } from "src/components/menu";
|
import IconButtonMenu, { IconButtonMenuItems } from "src/components/menu";
|
||||||
import {
|
import {
|
||||||
getApplications,
|
getApplications,
|
||||||
|
getConferences,
|
||||||
getQueues,
|
getQueues,
|
||||||
getRegisteredUser,
|
getRegisteredUser,
|
||||||
getSelfRegisteredUser,
|
getSelfRegisteredUser,
|
||||||
@@ -60,6 +50,19 @@ import {
|
|||||||
import JambonzSwitch from "src/components/switch";
|
import JambonzSwitch from "src/components/switch";
|
||||||
import { DEFAULT_TOAST_DURATION } from "src/common/constants";
|
import { DEFAULT_TOAST_DURATION } from "src/common/constants";
|
||||||
import { RegisteredUser } from "src/api/types";
|
import { RegisteredUser } from "src/api/types";
|
||||||
|
import {
|
||||||
|
faCodeMerge,
|
||||||
|
faList,
|
||||||
|
faMicrophone,
|
||||||
|
faMicrophoneSlash,
|
||||||
|
faPause,
|
||||||
|
faPeopleGroup,
|
||||||
|
faPhoneSlash,
|
||||||
|
faPlay,
|
||||||
|
faUserGroup,
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import JoinConference from "./conference";
|
||||||
|
|
||||||
type PhoneProbs = {
|
type PhoneProbs = {
|
||||||
sipDomain: string;
|
sipDomain: string;
|
||||||
@@ -72,6 +75,13 @@ type PhoneProbs = {
|
|||||||
advancedSettings: AdvancedAppSettings;
|
advancedSettings: AdvancedAppSettings;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum PAGE_VIEW {
|
||||||
|
DIAL_PAD,
|
||||||
|
INCOMING_CALL,
|
||||||
|
OUTGOING_CALL,
|
||||||
|
JOIN_CONFERENCE,
|
||||||
|
}
|
||||||
|
|
||||||
export const Phone = ({
|
export const Phone = ({
|
||||||
sipDomain,
|
sipDomain,
|
||||||
sipServerAddress,
|
sipServerAddress,
|
||||||
@@ -84,29 +94,17 @@ export const Phone = ({
|
|||||||
}: PhoneProbs) => {
|
}: PhoneProbs) => {
|
||||||
const [inputNumber, setInputNumber] = useState("");
|
const [inputNumber, setInputNumber] = useState("");
|
||||||
const [appName, setAppName] = useState("");
|
const [appName, setAppName] = useState("");
|
||||||
const inputNumberRef = useRef(inputNumber);
|
|
||||||
const [status, setStatus] = useState<SipClientStatus>("stop");
|
const [status, setStatus] = useState<SipClientStatus>("stop");
|
||||||
const [isConfigured, setIsConfigured] = useState(false);
|
const [isConfigured, setIsConfigured] = useState(false);
|
||||||
const [callStatus, setCallStatus] = useState(SipConstants.SESSION_ENDED);
|
const [callStatus, setCallStatus] = useState(SipConstants.SESSION_ENDED);
|
||||||
const [sessionDirection, setSessionDirection] =
|
const [sessionDirection, setSessionDirection] =
|
||||||
useState<SipCallDirection>("");
|
useState<SipCallDirection>("");
|
||||||
const sessionDirectionRef = useRef(sessionDirection);
|
|
||||||
const sipUA = useRef<SipUA | null>(null);
|
|
||||||
const timerRef = useRef<NodeJS.Timer | null>(null);
|
|
||||||
const [seconds, setSeconds] = useState(0);
|
const [seconds, setSeconds] = useState(0);
|
||||||
const secondsRef = useRef(seconds);
|
|
||||||
const [isCallButtonLoading, setIsCallButtonLoading] = useState(false);
|
const [isCallButtonLoading, setIsCallButtonLoading] = useState(false);
|
||||||
const [isAdvanceMode, setIsAdvancedMode] = 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("");
|
|
||||||
const [isSwitchingUserStatus, setIsSwitchingUserStatus] = useState(true);
|
const [isSwitchingUserStatus, setIsSwitchingUserStatus] = useState(true);
|
||||||
const [isOnline, setIsOnline] = useState(false);
|
const [isOnline, setIsOnline] = useState(false);
|
||||||
const unregisteredReasonRef = useRef("");
|
const [pageView, setPageView] = useState<PAGE_VIEW>(PAGE_VIEW.DIAL_PAD);
|
||||||
const isInputNumberFocusRef = useRef(false);
|
|
||||||
const [registeredUser, setRegisteredUser] = useState<Partial<RegisteredUser>>(
|
const [registeredUser, setRegisteredUser] = useState<Partial<RegisteredUser>>(
|
||||||
{
|
{
|
||||||
allow_direct_app_calling: false,
|
allow_direct_app_calling: false,
|
||||||
@@ -114,6 +112,23 @@ export const Phone = ({
|
|||||||
allow_direct_user_calling: false,
|
allow_direct_user_calling: false,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
const [selectedConference, setSelectedConference] = useState("");
|
||||||
|
const [callSid, setCallSid] = useState("");
|
||||||
|
|
||||||
|
const inputNumberRef = useRef(inputNumber);
|
||||||
|
const sessionDirectionRef = useRef(sessionDirection);
|
||||||
|
const sipUA = useRef<SipUA | null>(null);
|
||||||
|
const timerRef = useRef<NodeJS.Timer | null>(null);
|
||||||
|
const isRestartRef = useRef(false);
|
||||||
|
const sipDomainRef = useRef("");
|
||||||
|
const sipUsernameRef = useRef("");
|
||||||
|
const sipPasswordRef = useRef("");
|
||||||
|
const sipServerAddressRef = useRef("");
|
||||||
|
const sipDisplayNameRef = useRef("");
|
||||||
|
const unregisteredReasonRef = useRef("");
|
||||||
|
const isInputNumberFocusRef = useRef(false);
|
||||||
|
const secondsRef = useRef(seconds);
|
||||||
|
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -156,13 +171,36 @@ export const Phone = ({
|
|||||||
if (isSipClientIdle(callStatus) && isCallButtonLoading) {
|
if (isSipClientIdle(callStatus) && isCallButtonLoading) {
|
||||||
setIsCallButtonLoading(false);
|
setIsCallButtonLoading(false);
|
||||||
}
|
}
|
||||||
|
switch (callStatus) {
|
||||||
|
case SipConstants.SESSION_RINGING:
|
||||||
|
if (sessionDirection === "incoming") {
|
||||||
|
setPageView(PAGE_VIEW.INCOMING_CALL);
|
||||||
|
} else {
|
||||||
|
setPageView(PAGE_VIEW.OUTGOING_CALL);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case SipConstants.SESSION_ANSWERED:
|
||||||
|
if (!!selectedConference) {
|
||||||
|
setPageView(PAGE_VIEW.JOIN_CONFERENCE);
|
||||||
|
} else {
|
||||||
|
setPageView(PAGE_VIEW.DIAL_PAD);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case SipConstants.SESSION_ENDED:
|
||||||
|
case SipConstants.SESSION_FAILED:
|
||||||
|
setSelectedConference("");
|
||||||
|
setPageView(PAGE_VIEW.DIAL_PAD);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}, [callStatus]);
|
}, [callStatus]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (calledANumber) {
|
if (calledANumber) {
|
||||||
if (
|
if (
|
||||||
!(
|
!(
|
||||||
calledANumber.startsWith("app-") || calledANumber.startsWith("queue-")
|
calledANumber.startsWith("app-") ||
|
||||||
|
calledANumber.startsWith("queue-") ||
|
||||||
|
calledANumber.startsWith("conference-")
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
setInputNumber(calledANumber);
|
setInputNumber(calledANumber);
|
||||||
@@ -183,34 +221,14 @@ export const Phone = ({
|
|||||||
}, [status]);
|
}, [status]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setInterval(() => {
|
const timer = setInterval(() => {
|
||||||
fetchRegisterUser();
|
fetchRegisterUser();
|
||||||
}, 10_000);
|
}, 10000);
|
||||||
|
return () => {
|
||||||
|
clearInterval(timer);
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// useEffect(() => {
|
|
||||||
// chrome.runtime.onMessage.addListener(function (request) {
|
|
||||||
// const msg = request as Message<any>;
|
|
||||||
// switch (msg.event) {
|
|
||||||
// case MessageEvent.Call:
|
|
||||||
// handleCallEvent(msg.data as Call);
|
|
||||||
// break;
|
|
||||||
// default:
|
|
||||||
// break;
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
// }, []);
|
|
||||||
|
|
||||||
// const handleCallEvent = (call: Call) => {
|
|
||||||
// if (!call.number) return;
|
|
||||||
|
|
||||||
// if (isSipClientIdle(callStatus)) {
|
|
||||||
// setIsCallButtonLoading(true);
|
|
||||||
// setInputNumber(call.number);
|
|
||||||
// sipUA.current?.call(call.number);
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
const fetchRegisterUser = () => {
|
const fetchRegisterUser = () => {
|
||||||
getSelfRegisteredUser(sipUsernameRef.current)
|
getSelfRegisteredUser(sipUsernameRef.current)
|
||||||
.then(({ json }) => {
|
.then(({ json }) => {
|
||||||
@@ -313,6 +331,7 @@ export const Phone = ({
|
|||||||
setInputNumber(args.session.user);
|
setInputNumber(args.session.user);
|
||||||
});
|
});
|
||||||
sipClient.on(SipConstants.SESSION_ANSWERED, (args) => {
|
sipClient.on(SipConstants.SESSION_ANSWERED, (args) => {
|
||||||
|
setCallSid(args.callSid);
|
||||||
const currentCall = getCurrentCall();
|
const currentCall = getCurrentCall();
|
||||||
if (currentCall) {
|
if (currentCall) {
|
||||||
currentCall.timeStamp = Date.now();
|
currentCall.timeStamp = Date.now();
|
||||||
@@ -514,21 +533,7 @@ export const Phone = ({
|
|||||||
Go to Settings to configure your account
|
Go to Settings to configure your account
|
||||||
</Heading>
|
</Heading>
|
||||||
)}
|
)}
|
||||||
|
{pageView === PAGE_VIEW.DIAL_PAD && (
|
||||||
{isSipClientRinging(callStatus) ? (
|
|
||||||
sessionDirection === "incoming" ? (
|
|
||||||
<IncommingCall
|
|
||||||
number={inputNumber}
|
|
||||||
answer={handleAnswer}
|
|
||||||
decline={handleDecline}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<OutGoingCall
|
|
||||||
number={inputNumber || appName}
|
|
||||||
cancelCall={handleDecline}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<VStack
|
<VStack
|
||||||
spacing={2}
|
spacing={2}
|
||||||
w="full"
|
w="full"
|
||||||
@@ -539,7 +544,7 @@ export const Phone = ({
|
|||||||
<HStack spacing={2} align="start" w="full">
|
<HStack spacing={2} align="start" w="full">
|
||||||
{registeredUser.allow_direct_user_calling && (
|
{registeredUser.allow_direct_user_calling && (
|
||||||
<IconButtonMenu
|
<IconButtonMenu
|
||||||
icon={<Users />}
|
icon={<FontAwesomeIcon icon={faUserGroup} />}
|
||||||
tooltip="Call an online user"
|
tooltip="Call an online user"
|
||||||
noResultLabel="No one else is online"
|
noResultLabel="No one else is online"
|
||||||
onClick={(_, value) => {
|
onClick={(_, value) => {
|
||||||
@@ -551,7 +556,9 @@ export const Phone = ({
|
|||||||
(resolve, reject) => {
|
(resolve, reject) => {
|
||||||
getRegisteredUser()
|
getRegisteredUser()
|
||||||
.then(({ json }) => {
|
.then(({ json }) => {
|
||||||
const sortedUsers = json.sort((a, b) => a.localeCompare(b));
|
const sortedUsers = json.sort((a, b) =>
|
||||||
|
a.localeCompare(b)
|
||||||
|
);
|
||||||
resolve(
|
resolve(
|
||||||
sortedUsers
|
sortedUsers
|
||||||
.filter((u) => !u.includes(sipUsername))
|
.filter((u) => !u.includes(sipUsername))
|
||||||
@@ -573,7 +580,7 @@ export const Phone = ({
|
|||||||
|
|
||||||
{registeredUser.allow_direct_queue_calling && (
|
{registeredUser.allow_direct_queue_calling && (
|
||||||
<IconButtonMenu
|
<IconButtonMenu
|
||||||
icon={<List />}
|
icon={<FontAwesomeIcon icon={faList} />}
|
||||||
tooltip="Take a call from queue"
|
tooltip="Take a call from queue"
|
||||||
noResultLabel="No calls in queue"
|
noResultLabel="No calls in queue"
|
||||||
onClick={(name, value) => {
|
onClick={(name, value) => {
|
||||||
@@ -587,7 +594,9 @@ export const Phone = ({
|
|||||||
(resolve, reject) => {
|
(resolve, reject) => {
|
||||||
getQueues()
|
getQueues()
|
||||||
.then(({ json }) => {
|
.then(({ json }) => {
|
||||||
const sortedQueues = json.sort((a, b) => a.name.localeCompare(b.name));
|
const sortedQueues = json.sort((a, b) =>
|
||||||
|
a.name.localeCompare(b.name)
|
||||||
|
);
|
||||||
resolve(
|
resolve(
|
||||||
sortedQueues.map((q) => ({
|
sortedQueues.map((q) => ({
|
||||||
name: `${q.name} (${q.length})`,
|
name: `${q.name} (${q.length})`,
|
||||||
@@ -604,7 +613,7 @@ export const Phone = ({
|
|||||||
|
|
||||||
{registeredUser.allow_direct_app_calling && (
|
{registeredUser.allow_direct_app_calling && (
|
||||||
<IconButtonMenu
|
<IconButtonMenu
|
||||||
icon={<GitMerge />}
|
icon={<FontAwesomeIcon icon={faCodeMerge} />}
|
||||||
tooltip="Call an application"
|
tooltip="Call an application"
|
||||||
noResultLabel="No applications"
|
noResultLabel="No applications"
|
||||||
onClick={(name, value) => {
|
onClick={(name, value) => {
|
||||||
@@ -618,7 +627,9 @@ export const Phone = ({
|
|||||||
(resolve, reject) => {
|
(resolve, reject) => {
|
||||||
getApplications()
|
getApplications()
|
||||||
.then(({ json }) => {
|
.then(({ json }) => {
|
||||||
const sortedApps = json.sort((a, b) => a.name.localeCompare(b.name));
|
const sortedApps = json.sort((a, b) =>
|
||||||
|
a.name.localeCompare(b.name)
|
||||||
|
);
|
||||||
resolve(
|
resolve(
|
||||||
sortedApps.map((a) => ({
|
sortedApps.map((a) => ({
|
||||||
name: a.name,
|
name: a.name,
|
||||||
@@ -632,6 +643,44 @@ export const Phone = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{registeredUser.allow_direct_app_calling && (
|
||||||
|
<IconButtonMenu
|
||||||
|
icon={<FontAwesomeIcon icon={faPeopleGroup} />}
|
||||||
|
tooltip="Join a conference"
|
||||||
|
noResultLabel="No conference"
|
||||||
|
onClick={(name, value) => {
|
||||||
|
setPageView(PAGE_VIEW.JOIN_CONFERENCE);
|
||||||
|
setSelectedConference(
|
||||||
|
value === PAGE_VIEW.JOIN_CONFERENCE.toString()
|
||||||
|
? ""
|
||||||
|
: value
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
onOpen={() => {
|
||||||
|
return new Promise<IconButtonMenuItems[]>(
|
||||||
|
(resolve, reject) => {
|
||||||
|
getConferences()
|
||||||
|
.then(({ json }) => {
|
||||||
|
const sortedApps = json.sort((a, b) =>
|
||||||
|
a.localeCompare(b)
|
||||||
|
);
|
||||||
|
resolve([
|
||||||
|
{
|
||||||
|
name: "Start new conference",
|
||||||
|
value: PAGE_VIEW.JOIN_CONFERENCE.toString(),
|
||||||
|
},
|
||||||
|
...sortedApps.map((a) => ({
|
||||||
|
name: a,
|
||||||
|
value: a,
|
||||||
|
})),
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
.catch((err) => reject(err));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</HStack>
|
</HStack>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -680,7 +729,11 @@ export const Phone = ({
|
|||||||
<IconButton
|
<IconButton
|
||||||
aria-label="Place call onhold"
|
aria-label="Place call onhold"
|
||||||
icon={
|
icon={
|
||||||
sipUA.current?.isHolded(undefined) ? <Play /> : <Pause />
|
<FontAwesomeIcon
|
||||||
|
icon={
|
||||||
|
sipUA.current?.isHolded(undefined) ? faPlay : faPause
|
||||||
|
}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
w="33%"
|
w="33%"
|
||||||
variant="unstyled"
|
variant="unstyled"
|
||||||
@@ -694,7 +747,7 @@ export const Phone = ({
|
|||||||
<Spacer />
|
<Spacer />
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label="Hangup"
|
aria-label="Hangup"
|
||||||
icon={<PhoneOff />}
|
icon={<FontAwesomeIcon icon={faPhoneSlash} />}
|
||||||
w="70px"
|
w="70px"
|
||||||
h="70px"
|
h="70px"
|
||||||
borderRadius="100%"
|
borderRadius="100%"
|
||||||
@@ -708,7 +761,13 @@ export const Phone = ({
|
|||||||
<IconButton
|
<IconButton
|
||||||
aria-label="Mute"
|
aria-label="Mute"
|
||||||
icon={
|
icon={
|
||||||
sipUA.current?.isMuted(undefined) ? <Mic /> : <MicOff />
|
<FontAwesomeIcon
|
||||||
|
icon={
|
||||||
|
sipUA.current?.isMuted(undefined)
|
||||||
|
? faMicrophone
|
||||||
|
: faMicrophoneSlash
|
||||||
|
}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
w="33%"
|
w="33%"
|
||||||
variant="unstyled"
|
variant="unstyled"
|
||||||
@@ -722,6 +781,39 @@ export const Phone = ({
|
|||||||
)}
|
)}
|
||||||
</VStack>
|
</VStack>
|
||||||
)}
|
)}
|
||||||
|
{pageView === PAGE_VIEW.INCOMING_CALL && (
|
||||||
|
<IncommingCall
|
||||||
|
number={inputNumber}
|
||||||
|
answer={handleAnswer}
|
||||||
|
decline={handleDecline}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{pageView === PAGE_VIEW.OUTGOING_CALL && (
|
||||||
|
<OutGoingCall
|
||||||
|
number={inputNumber || appName}
|
||||||
|
cancelCall={handleDecline}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{pageView === PAGE_VIEW.JOIN_CONFERENCE && (
|
||||||
|
<JoinConference
|
||||||
|
conferenceId={selectedConference}
|
||||||
|
callSid={callSid}
|
||||||
|
callDuration={seconds}
|
||||||
|
callStatus={callStatus}
|
||||||
|
handleCancel={() => {
|
||||||
|
if (isSipClientAnswered(callStatus)) {
|
||||||
|
sipUA.current?.terminate(480, "Call Finished", undefined);
|
||||||
|
}
|
||||||
|
setPageView(PAGE_VIEW.DIAL_PAD);
|
||||||
|
}}
|
||||||
|
call={(name) => {
|
||||||
|
const conference = `conference-${name}`;
|
||||||
|
setSelectedConference(name);
|
||||||
|
setInputNumber(conference);
|
||||||
|
makeOutboundCall(conference, `Conference ${name}`);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Center>
|
</Center>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Button, Icon, Text, VStack } from "@chakra-ui/react";
|
import { Box, Button, Icon, Text, VStack } from "@chakra-ui/react";
|
||||||
import { PhoneCall } from "react-feather";
|
import { faPhone } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { formatPhoneNumber } from "src/utils";
|
import { formatPhoneNumber } from "src/utils";
|
||||||
|
|
||||||
type OutGoingCallProbs = {
|
type OutGoingCallProbs = {
|
||||||
@@ -10,7 +11,13 @@ type OutGoingCallProbs = {
|
|||||||
export const OutGoingCall = ({ number, cancelCall }: OutGoingCallProbs) => {
|
export const OutGoingCall = ({ number, cancelCall }: OutGoingCallProbs) => {
|
||||||
return (
|
return (
|
||||||
<VStack alignItems="center" spacing={4} mt="130px" w="full">
|
<VStack alignItems="center" spacing={4} mt="130px" w="full">
|
||||||
<Icon as={PhoneCall} color="jambonz.500" w="60px" h="60px" />
|
<Box
|
||||||
|
as={FontAwesomeIcon}
|
||||||
|
icon={faPhone}
|
||||||
|
color="jambonz.500"
|
||||||
|
width="60px"
|
||||||
|
height="60px"
|
||||||
|
/>
|
||||||
<Text fontSize="15px">Dialing</Text>
|
<Text fontSize="15px">Dialing</Text>
|
||||||
<Text fontSize="24px" fontWeight="bold">
|
<Text fontSize="24px" fontWeight="bold">
|
||||||
{formatPhoneNumber(number)}
|
{formatPhoneNumber(number)}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
Box,
|
||||||
Button,
|
Button,
|
||||||
FormControl,
|
FormControl,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
@@ -6,19 +7,22 @@ import {
|
|||||||
Icon,
|
Icon,
|
||||||
Image,
|
Image,
|
||||||
Input,
|
Input,
|
||||||
Spacer,
|
|
||||||
Text,
|
Text,
|
||||||
VStack,
|
VStack,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
|
import {
|
||||||
|
faCheckCircle,
|
||||||
|
faCircleXmark,
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { CheckCircle, XCircle } from "react-feather";
|
|
||||||
import { getApplications } from "src/api";
|
import { getApplications } from "src/api";
|
||||||
import { AdvancedAppSettings } from "src/common/types";
|
import { AdvancedAppSettings } from "src/common/types";
|
||||||
import PasswordInput from "src/components/password-input";
|
import PasswordInput from "src/components/password-input";
|
||||||
import InfoIcon from "src/imgs/icons/Info.svg";
|
|
||||||
import ResetIcon from "src/imgs/icons/Reset.svg";
|
import ResetIcon from "src/imgs/icons/Reset.svg";
|
||||||
import { getAdvancedSettings, saveAddvancedSettings } from "src/storage";
|
import { getAdvancedSettings, saveAddvancedSettings } from "src/storage";
|
||||||
import { normalizeUrl } from "src/utils";
|
import { normalizeUrl } from "src/utils";
|
||||||
|
import { useToken } from "@chakra-ui/react";
|
||||||
|
|
||||||
export const AdvancedSettings = () => {
|
export const AdvancedSettings = () => {
|
||||||
const [apiKey, setApiKey] = useState("");
|
const [apiKey, setApiKey] = useState("");
|
||||||
@@ -109,10 +113,10 @@ export const AdvancedSettings = () => {
|
|||||||
</VStack>
|
</VStack>
|
||||||
{isAdvancedMode && (
|
{isAdvancedMode && (
|
||||||
<HStack w="full" mt={2} mb={2}>
|
<HStack w="full" mt={2} mb={2}>
|
||||||
<Icon
|
<Box
|
||||||
as={isCredentialOk ? CheckCircle : XCircle}
|
as={FontAwesomeIcon}
|
||||||
|
icon={isCredentialOk ? faCheckCircle : faCircleXmark}
|
||||||
color={isCredentialOk ? "green.500" : "red.500"}
|
color={isCredentialOk ? "green.500" : "red.500"}
|
||||||
boxSize={6}
|
|
||||||
/>
|
/>
|
||||||
<Text
|
<Text
|
||||||
fontSize="14px"
|
fontSize="14px"
|
||||||
|
|||||||
@@ -3,34 +3,34 @@ const HTMLPlugin = require("html-webpack-plugin");
|
|||||||
const CopyPlugin = require("copy-webpack-plugin");
|
const CopyPlugin = require("copy-webpack-plugin");
|
||||||
|
|
||||||
module.exports = [
|
module.exports = [
|
||||||
{
|
// {
|
||||||
entry: "./src/content/index.tsx",
|
// entry: "./src/content/index.tsx",
|
||||||
target: "web",
|
// target: "web",
|
||||||
mode: "production",
|
// mode: "production",
|
||||||
output: {
|
// output: {
|
||||||
path: path.join(__dirname, "dist"),
|
// path: path.join(__dirname, "dist"),
|
||||||
filename: "js/content.js",
|
// filename: "js/content.js",
|
||||||
},
|
// },
|
||||||
module: {
|
// module: {
|
||||||
rules: [
|
// rules: [
|
||||||
{
|
// {
|
||||||
test: /\.tsx?$/,
|
// test: /\.tsx?$/,
|
||||||
use: [
|
// use: [
|
||||||
{
|
// {
|
||||||
loader: "ts-loader",
|
// loader: "ts-loader",
|
||||||
options: {
|
// options: {
|
||||||
compilerOptions: { noEmit: false },
|
// compilerOptions: { noEmit: false },
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
],
|
// ],
|
||||||
exclude: /node_modules/,
|
// exclude: /node_modules/,
|
||||||
},
|
// },
|
||||||
],
|
// ],
|
||||||
},
|
// },
|
||||||
resolve: {
|
// resolve: {
|
||||||
extensions: [".ts", ".tsx", ".js"],
|
// extensions: [".ts", ".tsx", ".js"],
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
{
|
{
|
||||||
entry: "./src/background/index.ts",
|
entry: "./src/background/index.ts",
|
||||||
target: "web",
|
target: "web",
|
||||||
@@ -59,68 +59,6 @@ module.exports = [
|
|||||||
extensions: [".ts", ".tsx", ".js"],
|
extensions: [".ts", ".tsx", ".js"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
entry: {
|
|
||||||
index: "./src/index.tsx",
|
|
||||||
},
|
|
||||||
mode: "production",
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
test: /\.tsx?$/,
|
|
||||||
use: [
|
|
||||||
{
|
|
||||||
loader: "ts-loader",
|
|
||||||
options: {
|
|
||||||
compilerOptions: { noEmit: false },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
exclude: /node_modules/,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
exclude: /node_modules/,
|
|
||||||
test: /\.css$/i,
|
|
||||||
use: ["style-loader", "css-loader"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.s[ac]ss$/i,
|
|
||||||
use: [
|
|
||||||
// Creates `style` nodes from JS strings
|
|
||||||
"style-loader",
|
|
||||||
// Translates CSS into CommonJS
|
|
||||||
"css-loader",
|
|
||||||
// Compiles Sass to CSS
|
|
||||||
"sass-loader",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.txt$/i,
|
|
||||||
type: "asset/source",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
new CopyPlugin({
|
|
||||||
patterns: [{ from: "public", to: ".." }],
|
|
||||||
}),
|
|
||||||
...getHtmlPlugins(["index"]),
|
|
||||||
],
|
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
src: path.resolve(__dirname, "src/"),
|
|
||||||
},
|
|
||||||
extensions: [".tsx", ".ts", ".js", ".txt"],
|
|
||||||
},
|
|
||||||
output: {
|
|
||||||
path: path.join(__dirname, "dist/js"),
|
|
||||||
filename: "[name].js",
|
|
||||||
},
|
|
||||||
performance: {
|
|
||||||
maxEntrypointSize: 512000,
|
|
||||||
maxAssetSize: 512000,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
entry: {
|
entry: {
|
||||||
index: "./src/window/index.tsx",
|
index: "./src/window/index.tsx",
|
||||||
@@ -162,7 +100,12 @@ module.exports = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
plugins: [...getHtmlPlugins(["index"])],
|
plugins: [
|
||||||
|
new CopyPlugin({
|
||||||
|
patterns: [{ from: "public", to: ".." }],
|
||||||
|
}),
|
||||||
|
...getHtmlPlugins(["index"]),
|
||||||
|
],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
src: path.resolve(__dirname, "src/"),
|
src: path.resolve(__dirname, "src/"),
|
||||||
|
|||||||
Reference in New Issue
Block a user