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",
|
||||
"version": "1.0.5",
|
||||
"version": "1.0.6",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@chakra-ui/react": "^2.8.0",
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.5.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.5.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"buffer": "^6.0.3",
|
||||
"dayjs": "^1.11.10",
|
||||
"framer-motion": "^10.16.4",
|
||||
@@ -17,13 +17,13 @@
|
||||
"jssip": "^3.10.0",
|
||||
"react": "^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",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"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/google-libphonenumber": "^7.4.27",
|
||||
"@types/jest": "^27.5.2",
|
||||
@@ -34,8 +34,10 @@
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"css-loader": "^6.8.1",
|
||||
"file-loader": "^6.2.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"sass": "^1.64.1",
|
||||
"sass-loader": "^13.3.2",
|
||||
"serve": "^14.2.1",
|
||||
"style-loader": "^3.3.3",
|
||||
"ts-loader": "^9.4.4",
|
||||
"webpack": "^5.88.2",
|
||||
@@ -43,7 +45,8 @@
|
||||
},
|
||||
"scripts": {
|
||||
"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": {
|
||||
"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,
|
||||
"name": "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 {
|
||||
Application,
|
||||
ConferenceParticipantAction,
|
||||
ConferenceParticipantActions,
|
||||
FetchError,
|
||||
FetchTransport,
|
||||
Queue,
|
||||
RegisteredUser,
|
||||
StatusCodes,
|
||||
UpdateCall,
|
||||
} from "./types";
|
||||
import { MSG_SOMETHING_WRONG } from "./constants";
|
||||
import { getAdvancedSettings } from "src/storage";
|
||||
import { EmptyData } from "src/common/types";
|
||||
|
||||
const fetchTransport = <Type>(
|
||||
url: string,
|
||||
@@ -152,3 +156,23 @@ export const getSelfRegisteredUser = (username: string) => {
|
||||
`${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;
|
||||
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"),
|
||||
width: 440,
|
||||
height: 720,
|
||||
height: 750,
|
||||
focused: true,
|
||||
type: "panel",
|
||||
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 {
|
||||
Input,
|
||||
InputGroup,
|
||||
InputRightElement,
|
||||
Button,
|
||||
Box,
|
||||
} from "@chakra-ui/react";
|
||||
import { Eye, EyeOff } from "react-feather";
|
||||
import { Input, InputGroup, InputRightElement, Button } from "@chakra-ui/react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faEye, faEyeSlash } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
type PasswordInputProbs = {
|
||||
password: [string, React.Dispatch<React.SetStateAction<string>>];
|
||||
@@ -34,7 +29,7 @@ function PasswordInput({
|
||||
/>
|
||||
<InputRightElement width="4.5rem">
|
||||
<Button h="1.75rem" size="sm" onClick={handleClick} variant="unstyled">
|
||||
{showPassword ? <EyeOff /> : <Eye />}
|
||||
<FontAwesomeIcon icon={showPassword ? faEyeSlash : faEye} />
|
||||
</Button>
|
||||
</InputRightElement>
|
||||
</InputGroup>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App";
|
||||
import { ChakraProvider } from "@chakra-ui/react";
|
||||
import mainTheme from "./theme";
|
||||
import WindowApp from "./window/app";
|
||||
|
||||
// This file is being used only for dev.
|
||||
const root = document.createElement("div");
|
||||
root.className = "container";
|
||||
document.body.appendChild(root);
|
||||
@@ -11,7 +12,7 @@ const rootDiv = ReactDOM.createRoot(root);
|
||||
rootDiv.render(
|
||||
<React.StrictMode>
|
||||
<ChakraProvider theme={mainTheme}>
|
||||
<App />
|
||||
<WindowApp />
|
||||
</ChakraProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
@@ -8,27 +8,41 @@ export default class SipAudioElements {
|
||||
#localHungup: HTMLAudioElement;
|
||||
|
||||
constructor() {
|
||||
this.#ringing = new Audio(chrome.runtime.getURL("audios/ringing.mp3"));
|
||||
this.#ringing = this.getAudio("audios/ringing.mp3");
|
||||
this.#ringing.loop = true;
|
||||
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.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.#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.#hungup = new Audio(
|
||||
chrome.runtime.getURL("audios/remote-party-hungup-tone.mp3")
|
||||
);
|
||||
this.#hungup = this.getAudio("audios/remote-party-hungup-tone.mp3");
|
||||
this.#hungup.volume = 0.3;
|
||||
this.#localHungup = new Audio(
|
||||
chrome.runtime.getURL("audios/local-party-hungup-tone.mp3")
|
||||
);
|
||||
this.#localHungup = this.getAudio("audios/local-party-hungup-tone.mp3");
|
||||
this.#localHungup.volume = 0.3;
|
||||
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) {
|
||||
this.pauseRingback();
|
||||
this.pauseRinging();
|
||||
|
||||
@@ -62,12 +62,18 @@ export default class SipSession extends events.EventEmitter {
|
||||
}
|
||||
});
|
||||
|
||||
this.#rtcSession.on("accepted", () => {
|
||||
this.#rtcSession.on(
|
||||
"accepted",
|
||||
({ response }: { response: IncomingResponse }) => {
|
||||
this.emit(SipConstants.SESSION_ANSWERED, {
|
||||
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 => {
|
||||
let { originator, cause, message } = data;
|
||||
@@ -195,16 +201,19 @@ export default class SipSession extends events.EventEmitter {
|
||||
direction: this.#rtcSession.direction,
|
||||
});
|
||||
});
|
||||
// pc.addEventListener('track', (event: RTCPeerConnectionEventMap["track"]): void => {
|
||||
// const stream: MediaStream = new MediaStream([event.track])
|
||||
// if (this.#rtcSession.direction === 'outgoing') {
|
||||
// pc.addEventListener(
|
||||
// "track",
|
||||
// (event: RTCPeerConnectionEventMap["track"]): void => {
|
||||
// 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, {
|
||||
// direction: this.#rtcSession.direction
|
||||
// });
|
||||
// direction: this.#rtcSession.direction,
|
||||
// });
|
||||
// }
|
||||
// );
|
||||
}
|
||||
|
||||
get rtcSession() {
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import {
|
||||
SipSession, SipModel, SipConstants
|
||||
} from "./index";
|
||||
|
||||
import { SipSession, SipModel, SipConstants } from "./index";
|
||||
|
||||
export default class SipSessionManager {
|
||||
|
||||
#sessions: Map<string, SipModel.SipSessionState>;
|
||||
|
||||
constructor() {
|
||||
@@ -24,7 +20,6 @@ export default class SipSessionManager {
|
||||
}
|
||||
|
||||
updateSession(field: string, session: SipSession, args: any): void {
|
||||
|
||||
const state: SipModel.SipSessionState = this.getSessionState(session.id);
|
||||
if (state) {
|
||||
switch (field) {
|
||||
@@ -41,8 +36,8 @@ export default class SipSessionManager {
|
||||
cause: args.cause,
|
||||
status: args.status,
|
||||
originator: args.endState,
|
||||
description: args.description
|
||||
}
|
||||
description: args.description,
|
||||
};
|
||||
this.#sessions.delete(session.id);
|
||||
break;
|
||||
case SipConstants.SESSION_MUTED:
|
||||
@@ -51,8 +46,8 @@ export default class SipSessionManager {
|
||||
case SipConstants.SESSION_HOLD:
|
||||
state.holdState = {
|
||||
originator: args.originator,
|
||||
status: args.status
|
||||
}
|
||||
status: args.status,
|
||||
};
|
||||
break;
|
||||
case SipConstants.SESSION_ICE_READY:
|
||||
state.iceReady = true;
|
||||
@@ -76,16 +71,13 @@ export default class SipSessionManager {
|
||||
return this.getSessionState(id).sipSession;
|
||||
}
|
||||
|
||||
|
||||
|
||||
newSession(session: SipSession): void {
|
||||
this.#sessions.set(session.id,
|
||||
{
|
||||
this.#sessions.set(session.id, {
|
||||
id: session.id,
|
||||
sipSession: session,
|
||||
startDateTime: new Date(),
|
||||
active: true,
|
||||
status: 'init',
|
||||
status: "init",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ export default class SipUA extends events.EventEmitter {
|
||||
display_name: client.name,
|
||||
sockets: [new WebSocketInterface(settings.wsUri)],
|
||||
register: settings.register,
|
||||
register_expires: 600,
|
||||
});
|
||||
this.#ua.on("connecting", (data: UAConnectingEvent) =>
|
||||
this.emit(SipConstants.UA_CONNECTING, { ...data, client })
|
||||
|
||||
@@ -3,7 +3,11 @@ function normalizeNumber(number: string): string {
|
||||
return number;
|
||||
} else if (/@/i.test(number)) {
|
||||
return number;
|
||||
} else if (number.startsWith("app-") || number.startsWith("queue-")) {
|
||||
} else if (
|
||||
number.startsWith("app-") ||
|
||||
number.startsWith("queue-") ||
|
||||
number.startsWith("conference-")
|
||||
) {
|
||||
return number;
|
||||
} else {
|
||||
return number.replace(/[()\-. ]*/g, "");
|
||||
|
||||
@@ -104,10 +104,11 @@ export const WindowApp = () => {
|
||||
index={tabIndex}
|
||||
>
|
||||
<TabList mb="1em" gap={1}>
|
||||
{tabsSettings.map((s) => (
|
||||
{tabsSettings.map((s, i) => (
|
||||
<Tab
|
||||
_selected={{ color: "white", bg: "jambonz.500" }}
|
||||
bg="grey.500"
|
||||
key={i}
|
||||
>
|
||||
{s.title}
|
||||
</Tab>
|
||||
@@ -115,8 +116,8 @@ export const WindowApp = () => {
|
||||
</TabList>
|
||||
|
||||
<TabPanels>
|
||||
{tabsSettings.map((s) => (
|
||||
<TabPanel>{s.content}</TabPanel>
|
||||
{tabsSettings.map((s, i) => (
|
||||
<TabPanel key={i}>{s.content}</TabPanel>
|
||||
))}
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import {
|
||||
HStack,
|
||||
Icon,
|
||||
IconButton,
|
||||
Spacer,
|
||||
Text,
|
||||
Tooltip,
|
||||
VStack,
|
||||
} 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 { useState } from "react";
|
||||
import {
|
||||
Phone,
|
||||
PhoneIncoming,
|
||||
PhoneOutgoing,
|
||||
Save,
|
||||
Trash2,
|
||||
} from "react-feather";
|
||||
import { CallHistory, SipCallDirection } from "src/common/types";
|
||||
import { getSettings, isSaveCallHistory } from "src/storage";
|
||||
import { formatPhoneNumber } from "src/utils";
|
||||
@@ -36,11 +36,11 @@ export const CallHistoryItem = ({
|
||||
const [callEnable, setCallEnable] = useState(false);
|
||||
const getDirectionIcon = (direction: SipCallDirection) => {
|
||||
if (direction === "outgoing") {
|
||||
return PhoneOutgoing;
|
||||
return faArrowRightFromBracket;
|
||||
} else if (direction === "incoming") {
|
||||
return PhoneIncoming;
|
||||
return faArrowRightToBracket;
|
||||
} else {
|
||||
return Phone;
|
||||
return faPhone;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -57,7 +57,7 @@ export const CallHistoryItem = ({
|
||||
<Tooltip label="Call">
|
||||
<IconButton
|
||||
aria-label="call recents"
|
||||
icon={<Phone />}
|
||||
icon={<FontAwesomeIcon icon={faPhone} />}
|
||||
onClick={() => {
|
||||
if (onCallNumber) {
|
||||
onCallNumber(call.number, call.name);
|
||||
@@ -69,7 +69,11 @@ export const CallHistoryItem = ({
|
||||
/>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Icon as={getDirectionIcon(call.direction)} w="20px" h="20px" />
|
||||
<FontAwesomeIcon
|
||||
icon={getDirectionIcon(call.direction)}
|
||||
width="20px"
|
||||
height="20px"
|
||||
/>
|
||||
)}
|
||||
|
||||
<VStack align="start">
|
||||
@@ -88,7 +92,11 @@ export const CallHistoryItem = ({
|
||||
<Tooltip label={isSaved && call.isSaved ? "Delete" : "Save"}>
|
||||
<IconButton
|
||||
aria-label="save recents"
|
||||
icon={isSaved && call.isSaved ? <Trash2 /> : <Save />}
|
||||
icon={
|
||||
<FontAwesomeIcon
|
||||
icon={isSaved && call.isSaved ? faTrash : faSave}
|
||||
/>
|
||||
}
|
||||
onClick={() => {
|
||||
if (!isSaved && call.isSaved) {
|
||||
return;
|
||||
|
||||
@@ -13,11 +13,12 @@ import {
|
||||
TabPanel,
|
||||
} from "@chakra-ui/react";
|
||||
import { useState } from "react";
|
||||
import { Search, Sliders } from "react-feather";
|
||||
import { CallHistory } from "src/common/types";
|
||||
|
||||
import { DEFAULT_COLOR_SCHEME } from "src/common/constants";
|
||||
import Recents from "./recent";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faSearch, faSliders } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
type CallHistoriesProbs = {
|
||||
calls: CallHistory[];
|
||||
@@ -51,11 +52,11 @@ export const CallHistories = ({
|
||||
fontWeight="normal"
|
||||
/>
|
||||
<InputLeftElement mr={2}>
|
||||
<Icon as={Search} w="20px" h="20px" />
|
||||
<FontAwesomeIcon icon={faSearch} width="20px" height="20px" />
|
||||
</InputLeftElement>
|
||||
</InputGroup>
|
||||
<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">
|
||||
Filter
|
||||
</Text>
|
||||
|
||||
@@ -51,8 +51,9 @@ export const Recents = ({
|
||||
spacing={2}
|
||||
mt={2}
|
||||
>
|
||||
{callHistories.map((c) => (
|
||||
{callHistories.map((c, i) => (
|
||||
<CallHistoryItem
|
||||
key={i}
|
||||
isSaved={isSaved}
|
||||
call={c}
|
||||
onCallNumber={onCallNumber}
|
||||
|
||||
@@ -4,9 +4,23 @@ export default class DialPadAudioElements {
|
||||
constructor() {
|
||||
const arr = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "*", "#"];
|
||||
for (const i of arr) {
|
||||
this.keySounds[i] = new Audio(
|
||||
chrome.runtime.getURL(`audios/dtmf-${encodeURIComponent(i)}.mp3`)
|
||||
let audioURL;
|
||||
|
||||
// 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];
|
||||
if (audio) {
|
||||
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 (
|
||||
<Box p={2} w="full" ref={selfRef}>
|
||||
<VStack w="full" bg="grey.500" spacing={0.5}>
|
||||
<Box p={2} w="full" h="280px" ref={selfRef}>
|
||||
<VStack w="full" h="full" bg="grey.500" spacing={0.5}>
|
||||
{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) => (
|
||||
<Button
|
||||
key={num}
|
||||
@@ -66,8 +72,8 @@ export const DialPad = ({ handleDigitPress }: DialPadProbs) => {
|
||||
}}
|
||||
size="lg"
|
||||
p={0}
|
||||
width="124px"
|
||||
height="70px"
|
||||
width="calc(100% / 3)"
|
||||
height="100%"
|
||||
variant="unstyled"
|
||||
bg="white"
|
||||
_hover={{
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
import { Button, VStack, Text, Icon, HStack, Spacer } from "@chakra-ui/react";
|
||||
import { PhoneCall } from "react-feather";
|
||||
import {
|
||||
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";
|
||||
|
||||
type IncommingCallProbs = {
|
||||
@@ -15,7 +24,13 @@ export const IncommingCall = ({
|
||||
}: IncommingCallProbs) => {
|
||||
return (
|
||||
<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="24px" fontWeight="bold">
|
||||
{formatPhoneNumber(number)}
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
IconButton,
|
||||
Image,
|
||||
Input,
|
||||
Select,
|
||||
Spacer,
|
||||
Text,
|
||||
Tooltip,
|
||||
@@ -15,16 +14,6 @@ import {
|
||||
useToast,
|
||||
} from "@chakra-ui/react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
GitMerge,
|
||||
List,
|
||||
Mic,
|
||||
MicOff,
|
||||
Pause,
|
||||
PhoneOff,
|
||||
Play,
|
||||
Users,
|
||||
} from "react-feather";
|
||||
import {
|
||||
AdvancedAppSettings,
|
||||
SipCallDirection,
|
||||
@@ -53,6 +42,7 @@ import { v4 as uuidv4 } from "uuid";
|
||||
import IconButtonMenu, { IconButtonMenuItems } from "src/components/menu";
|
||||
import {
|
||||
getApplications,
|
||||
getConferences,
|
||||
getQueues,
|
||||
getRegisteredUser,
|
||||
getSelfRegisteredUser,
|
||||
@@ -60,6 +50,19 @@ import {
|
||||
import JambonzSwitch from "src/components/switch";
|
||||
import { DEFAULT_TOAST_DURATION } from "src/common/constants";
|
||||
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 = {
|
||||
sipDomain: string;
|
||||
@@ -72,6 +75,13 @@ type PhoneProbs = {
|
||||
advancedSettings: AdvancedAppSettings;
|
||||
};
|
||||
|
||||
enum PAGE_VIEW {
|
||||
DIAL_PAD,
|
||||
INCOMING_CALL,
|
||||
OUTGOING_CALL,
|
||||
JOIN_CONFERENCE,
|
||||
}
|
||||
|
||||
export const Phone = ({
|
||||
sipDomain,
|
||||
sipServerAddress,
|
||||
@@ -84,29 +94,17 @@ export const Phone = ({
|
||||
}: PhoneProbs) => {
|
||||
const [inputNumber, setInputNumber] = useState("");
|
||||
const [appName, setAppName] = useState("");
|
||||
const inputNumberRef = useRef(inputNumber);
|
||||
const [status, setStatus] = useState<SipClientStatus>("stop");
|
||||
const [isConfigured, setIsConfigured] = useState(false);
|
||||
const [callStatus, setCallStatus] = useState(SipConstants.SESSION_ENDED);
|
||||
const [sessionDirection, setSessionDirection] =
|
||||
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 secondsRef = useRef(seconds);
|
||||
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("");
|
||||
const [isSwitchingUserStatus, setIsSwitchingUserStatus] = useState(true);
|
||||
const [isOnline, setIsOnline] = useState(false);
|
||||
const unregisteredReasonRef = useRef("");
|
||||
const isInputNumberFocusRef = useRef(false);
|
||||
const [pageView, setPageView] = useState<PAGE_VIEW>(PAGE_VIEW.DIAL_PAD);
|
||||
const [registeredUser, setRegisteredUser] = useState<Partial<RegisteredUser>>(
|
||||
{
|
||||
allow_direct_app_calling: false,
|
||||
@@ -114,6 +112,23 @@ export const Phone = ({
|
||||
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();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -156,13 +171,36 @@ export const Phone = ({
|
||||
if (isSipClientIdle(callStatus) && isCallButtonLoading) {
|
||||
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]);
|
||||
|
||||
useEffect(() => {
|
||||
if (calledANumber) {
|
||||
if (
|
||||
!(
|
||||
calledANumber.startsWith("app-") || calledANumber.startsWith("queue-")
|
||||
calledANumber.startsWith("app-") ||
|
||||
calledANumber.startsWith("queue-") ||
|
||||
calledANumber.startsWith("conference-")
|
||||
)
|
||||
) {
|
||||
setInputNumber(calledANumber);
|
||||
@@ -183,34 +221,14 @@ export const Phone = ({
|
||||
}, [status]);
|
||||
|
||||
useEffect(() => {
|
||||
setInterval(() => {
|
||||
const timer = setInterval(() => {
|
||||
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 = () => {
|
||||
getSelfRegisteredUser(sipUsernameRef.current)
|
||||
.then(({ json }) => {
|
||||
@@ -313,6 +331,7 @@ export const Phone = ({
|
||||
setInputNumber(args.session.user);
|
||||
});
|
||||
sipClient.on(SipConstants.SESSION_ANSWERED, (args) => {
|
||||
setCallSid(args.callSid);
|
||||
const currentCall = getCurrentCall();
|
||||
if (currentCall) {
|
||||
currentCall.timeStamp = Date.now();
|
||||
@@ -514,21 +533,7 @@ export const Phone = ({
|
||||
Go to Settings to configure your account
|
||||
</Heading>
|
||||
)}
|
||||
|
||||
{isSipClientRinging(callStatus) ? (
|
||||
sessionDirection === "incoming" ? (
|
||||
<IncommingCall
|
||||
number={inputNumber}
|
||||
answer={handleAnswer}
|
||||
decline={handleDecline}
|
||||
/>
|
||||
) : (
|
||||
<OutGoingCall
|
||||
number={inputNumber || appName}
|
||||
cancelCall={handleDecline}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
{pageView === PAGE_VIEW.DIAL_PAD && (
|
||||
<VStack
|
||||
spacing={2}
|
||||
w="full"
|
||||
@@ -539,7 +544,7 @@ export const Phone = ({
|
||||
<HStack spacing={2} align="start" w="full">
|
||||
{registeredUser.allow_direct_user_calling && (
|
||||
<IconButtonMenu
|
||||
icon={<Users />}
|
||||
icon={<FontAwesomeIcon icon={faUserGroup} />}
|
||||
tooltip="Call an online user"
|
||||
noResultLabel="No one else is online"
|
||||
onClick={(_, value) => {
|
||||
@@ -551,7 +556,9 @@ export const Phone = ({
|
||||
(resolve, reject) => {
|
||||
getRegisteredUser()
|
||||
.then(({ json }) => {
|
||||
const sortedUsers = json.sort((a, b) => a.localeCompare(b));
|
||||
const sortedUsers = json.sort((a, b) =>
|
||||
a.localeCompare(b)
|
||||
);
|
||||
resolve(
|
||||
sortedUsers
|
||||
.filter((u) => !u.includes(sipUsername))
|
||||
@@ -573,7 +580,7 @@ export const Phone = ({
|
||||
|
||||
{registeredUser.allow_direct_queue_calling && (
|
||||
<IconButtonMenu
|
||||
icon={<List />}
|
||||
icon={<FontAwesomeIcon icon={faList} />}
|
||||
tooltip="Take a call from queue"
|
||||
noResultLabel="No calls in queue"
|
||||
onClick={(name, value) => {
|
||||
@@ -587,7 +594,9 @@ export const Phone = ({
|
||||
(resolve, reject) => {
|
||||
getQueues()
|
||||
.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(
|
||||
sortedQueues.map((q) => ({
|
||||
name: `${q.name} (${q.length})`,
|
||||
@@ -604,7 +613,7 @@ export const Phone = ({
|
||||
|
||||
{registeredUser.allow_direct_app_calling && (
|
||||
<IconButtonMenu
|
||||
icon={<GitMerge />}
|
||||
icon={<FontAwesomeIcon icon={faCodeMerge} />}
|
||||
tooltip="Call an application"
|
||||
noResultLabel="No applications"
|
||||
onClick={(name, value) => {
|
||||
@@ -618,7 +627,9 @@ export const Phone = ({
|
||||
(resolve, reject) => {
|
||||
getApplications()
|
||||
.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(
|
||||
sortedApps.map((a) => ({
|
||||
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>
|
||||
)}
|
||||
|
||||
@@ -680,7 +729,11 @@ export const Phone = ({
|
||||
<IconButton
|
||||
aria-label="Place call onhold"
|
||||
icon={
|
||||
sipUA.current?.isHolded(undefined) ? <Play /> : <Pause />
|
||||
<FontAwesomeIcon
|
||||
icon={
|
||||
sipUA.current?.isHolded(undefined) ? faPlay : faPause
|
||||
}
|
||||
/>
|
||||
}
|
||||
w="33%"
|
||||
variant="unstyled"
|
||||
@@ -694,7 +747,7 @@ export const Phone = ({
|
||||
<Spacer />
|
||||
<IconButton
|
||||
aria-label="Hangup"
|
||||
icon={<PhoneOff />}
|
||||
icon={<FontAwesomeIcon icon={faPhoneSlash} />}
|
||||
w="70px"
|
||||
h="70px"
|
||||
borderRadius="100%"
|
||||
@@ -708,7 +761,13 @@ export const Phone = ({
|
||||
<IconButton
|
||||
aria-label="Mute"
|
||||
icon={
|
||||
sipUA.current?.isMuted(undefined) ? <Mic /> : <MicOff />
|
||||
<FontAwesomeIcon
|
||||
icon={
|
||||
sipUA.current?.isMuted(undefined)
|
||||
? faMicrophone
|
||||
: faMicrophoneSlash
|
||||
}
|
||||
/>
|
||||
}
|
||||
w="33%"
|
||||
variant="unstyled"
|
||||
@@ -722,6 +781,39 @@ export const Phone = ({
|
||||
)}
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Button, Icon, Text, VStack } from "@chakra-ui/react";
|
||||
import { PhoneCall } from "react-feather";
|
||||
import { Box, Button, Icon, Text, VStack } from "@chakra-ui/react";
|
||||
import { faPhone } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { formatPhoneNumber } from "src/utils";
|
||||
|
||||
type OutGoingCallProbs = {
|
||||
@@ -10,7 +11,13 @@ type OutGoingCallProbs = {
|
||||
export const OutGoingCall = ({ number, cancelCall }: OutGoingCallProbs) => {
|
||||
return (
|
||||
<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="24px" fontWeight="bold">
|
||||
{formatPhoneNumber(number)}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
@@ -6,19 +7,22 @@ import {
|
||||
Icon,
|
||||
Image,
|
||||
Input,
|
||||
Spacer,
|
||||
Text,
|
||||
VStack,
|
||||
} 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 { 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";
|
||||
import { useToken } from "@chakra-ui/react";
|
||||
|
||||
export const AdvancedSettings = () => {
|
||||
const [apiKey, setApiKey] = useState("");
|
||||
@@ -109,10 +113,10 @@ export const AdvancedSettings = () => {
|
||||
</VStack>
|
||||
{isAdvancedMode && (
|
||||
<HStack w="full" mt={2} mb={2}>
|
||||
<Icon
|
||||
as={isCredentialOk ? CheckCircle : XCircle}
|
||||
<Box
|
||||
as={FontAwesomeIcon}
|
||||
icon={isCredentialOk ? faCheckCircle : faCircleXmark}
|
||||
color={isCredentialOk ? "green.500" : "red.500"}
|
||||
boxSize={6}
|
||||
/>
|
||||
<Text
|
||||
fontSize="14px"
|
||||
|
||||
@@ -3,34 +3,34 @@ const HTMLPlugin = require("html-webpack-plugin");
|
||||
const CopyPlugin = require("copy-webpack-plugin");
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
entry: "./src/content/index.tsx",
|
||||
target: "web",
|
||||
mode: "production",
|
||||
output: {
|
||||
path: path.join(__dirname, "dist"),
|
||||
filename: "js/content.js",
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
use: [
|
||||
{
|
||||
loader: "ts-loader",
|
||||
options: {
|
||||
compilerOptions: { noEmit: false },
|
||||
},
|
||||
},
|
||||
],
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
extensions: [".ts", ".tsx", ".js"],
|
||||
},
|
||||
},
|
||||
// {
|
||||
// entry: "./src/content/index.tsx",
|
||||
// target: "web",
|
||||
// mode: "production",
|
||||
// output: {
|
||||
// path: path.join(__dirname, "dist"),
|
||||
// filename: "js/content.js",
|
||||
// },
|
||||
// module: {
|
||||
// rules: [
|
||||
// {
|
||||
// test: /\.tsx?$/,
|
||||
// use: [
|
||||
// {
|
||||
// loader: "ts-loader",
|
||||
// options: {
|
||||
// compilerOptions: { noEmit: false },
|
||||
// },
|
||||
// },
|
||||
// ],
|
||||
// exclude: /node_modules/,
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// resolve: {
|
||||
// extensions: [".ts", ".tsx", ".js"],
|
||||
// },
|
||||
// },
|
||||
{
|
||||
entry: "./src/background/index.ts",
|
||||
target: "web",
|
||||
@@ -59,68 +59,6 @@ module.exports = [
|
||||
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: {
|
||||
index: "./src/window/index.tsx",
|
||||
@@ -162,7 +100,12 @@ module.exports = [
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [...getHtmlPlugins(["index"])],
|
||||
plugins: [
|
||||
new CopyPlugin({
|
||||
patterns: [{ from: "public", to: ".." }],
|
||||
}),
|
||||
...getHtmlPlugins(["index"]),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
src: path.resolve(__dirname, "src/"),
|
||||
|
||||
Reference in New Issue
Block a user