From 66dca15a582292451e3bc87b970f2ad2c6c18ae6 Mon Sep 17 00:00:00 2001 From: Quan HL Date: Mon, 2 Oct 2023 11:47:06 +0700 Subject: [PATCH] new UI --- .vscode/settings.json | 3 +- package-lock.json | 27 +-- package.json | 1 + src/App.tsx | 4 +- src/common/types.ts | 22 +- src/components/password-input/index.tsx | 41 ++++ src/declarations.d.ts | 2 +- src/imgs/icons/Avatar-Green.svg | 17 ++ src/imgs/icons/Avatar.svg | 17 ++ src/imgs/icons/Hang-up.svg | 10 + src/imgs/icons/Info.svg | 10 + src/imgs/icons/Reset.svg | 10 + src/imgs/jambonz.png | Bin 7813 -> 0 bytes src/imgs/jambonz.svg | 14 ++ src/index.tsx | 3 +- src/lib/SipSession.ts | 8 + src/lib/SipUA.ts | 16 ++ src/storage/index.ts | 50 +++- src/theme.tsx | 50 ++++ src/utils/index.ts | 4 +- src/window/app.tsx | 77 ++++-- src/window/dial-pad.tsx | 40 ---- src/window/incomming-call.tsx | 81 ------- src/window/index.tsx | 3 +- src/window/outgoing-call.tsx | 94 -------- src/window/phone.tsx | 297 ------------------------ src/window/phone/dial-pad.tsx | 42 ++++ src/window/phone/incomming-call.tsx | 38 +++ src/window/phone/styles.scss | 4 + src/window/settings.tsx | 162 ------------- src/window/settings/index.tsx | 170 ++++++++++++++ webpack.config.js | 8 +- 32 files changed, 580 insertions(+), 745 deletions(-) create mode 100644 src/components/password-input/index.tsx create mode 100644 src/imgs/icons/Avatar-Green.svg create mode 100644 src/imgs/icons/Avatar.svg create mode 100644 src/imgs/icons/Hang-up.svg create mode 100644 src/imgs/icons/Info.svg create mode 100644 src/imgs/icons/Reset.svg delete mode 100644 src/imgs/jambonz.png create mode 100644 src/imgs/jambonz.svg create mode 100644 src/theme.tsx delete mode 100644 src/window/dial-pad.tsx delete mode 100644 src/window/incomming-call.tsx delete mode 100644 src/window/outgoing-call.tsx delete mode 100644 src/window/phone.tsx create mode 100644 src/window/phone/dial-pad.tsx create mode 100644 src/window/phone/incomming-call.tsx create mode 100644 src/window/phone/styles.scss delete mode 100644 src/window/settings.tsx create mode 100644 src/window/settings/index.tsx diff --git a/.vscode/settings.json b/.vscode/settings.json index 3501f16..befcc91 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,5 +5,6 @@ "editor.defaultFormatter": "esbenp.prettier-vscode", "prettier.configPath": ".prettierrc.json", "files.trimTrailingWhitespace": true, - "typescript.preferences.quoteStyle": "double" + "typescript.preferences.quoteStyle": "double", + "svg.preview.background": "white" } diff --git a/package-lock.json b/package-lock.json index 8604aa3..5063b97 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "buffer": "^6.0.3", + "dayjs": "^1.11.10", "google-libphonenumber": "^3.2.33", "jssip": "^3.2.17", "react": "^18.2.0", @@ -32,7 +33,6 @@ "copy-webpack-plugin": "^11.0.0", "css-loader": "^6.8.1", "file-loader": "^6.2.0", - "raw-loader": "^4.0.2", "sass": "^1.64.1", "sass-loader": "^13.3.2", "style-loader": "^3.3.3", @@ -8111,6 +8111,11 @@ "node": ">=10" } }, + "node_modules/dayjs": { + "version": "1.11.10", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", + "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -16169,26 +16174,6 @@ "node": ">=0.10.0" } }, - "node_modules/raw-loader": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-4.0.2.tgz", - "integrity": "sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA==", - "dev": true, - "dependencies": { - "loader-utils": "^2.0.0", - "schema-utils": "^3.0.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" - } - }, "node_modules/react": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", diff --git a/package.json b/package.json index 2aafc03..2c06a0a 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "buffer": "^6.0.3", + "dayjs": "^1.11.10", "google-libphonenumber": "^3.2.33", "jssip": "^3.2.17", "react": "^18.2.0", diff --git a/src/App.tsx b/src/App.tsx index 7d87f0a..efa9d2e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -16,14 +16,14 @@ export const App = () => { padding="20px" alignItems="center" > - + Click 'Start' to activate the service. diff --git a/src/common/types.ts b/src/common/types.ts index 231d0a1..65da9c4 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -27,20 +27,6 @@ export interface Contact { number: string; } -export interface CallHistory { - id: string; - callerName?: string; - callerId: string; - direction: string; - time: string; - duration: number; - strDuration?: string; - startTime?: number; - endTime?: number; - label?: string; - note: string; -} - export interface MessageResponse { event: MessageEvent; code: number; @@ -72,4 +58,12 @@ export interface AppSettings { apiKey: string; } +export interface CallHistory { + direction: SipCallDirection; + number: string; + duration: string; + timeStamp: number; +} + export type SipClientStatus = "online" | "offline"; +export type SipCallDirection = "" | "outgoing" | "incoming"; diff --git a/src/components/password-input/index.tsx b/src/components/password-input/index.tsx new file mode 100644 index 0000000..4c700b8 --- /dev/null +++ b/src/components/password-input/index.tsx @@ -0,0 +1,41 @@ +import { useState } from "react"; +import { + Input, + InputGroup, + InputRightElement, + Button, + Box, +} from "@chakra-ui/react"; +import { Eye, EyeOff } from "react-feather"; + +type PasswordInputProbs = { + password: [string, React.Dispatch>]; + placeHolder?: string; +}; + +function PasswordInput({ + password: [pass, setPass], + placeHolder, +}: PasswordInputProbs) { + const [showPassword, setShowPassword] = useState(false); + const handleClick = () => setShowPassword(!showPassword); + + return ( + + setPass(e.target.value)} + /> + + + + + ); +} + +export default PasswordInput; diff --git a/src/declarations.d.ts b/src/declarations.d.ts index ad63bdb..fc7dbd7 100644 --- a/src/declarations.d.ts +++ b/src/declarations.d.ts @@ -1,2 +1,2 @@ -declare module "*.png"; +declare module "*.svg"; declare module "*.txt"; diff --git a/src/imgs/icons/Avatar-Green.svg b/src/imgs/icons/Avatar-Green.svg new file mode 100644 index 0000000..4f19638 --- /dev/null +++ b/src/imgs/icons/Avatar-Green.svg @@ -0,0 +1,17 @@ + + + Avatar-Green@3x + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/imgs/icons/Avatar.svg b/src/imgs/icons/Avatar.svg new file mode 100644 index 0000000..42bb587 --- /dev/null +++ b/src/imgs/icons/Avatar.svg @@ -0,0 +1,17 @@ + + + Avatar@3x + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/imgs/icons/Hang-up.svg b/src/imgs/icons/Hang-up.svg new file mode 100644 index 0000000..c33ad56 --- /dev/null +++ b/src/imgs/icons/Hang-up.svg @@ -0,0 +1,10 @@ + + + Icons / Hang up + + + + + + + \ No newline at end of file diff --git a/src/imgs/icons/Info.svg b/src/imgs/icons/Info.svg new file mode 100644 index 0000000..d6626f2 --- /dev/null +++ b/src/imgs/icons/Info.svg @@ -0,0 +1,10 @@ + + + Icons / Info + + + + + + + \ No newline at end of file diff --git a/src/imgs/icons/Reset.svg b/src/imgs/icons/Reset.svg new file mode 100644 index 0000000..16b33e5 --- /dev/null +++ b/src/imgs/icons/Reset.svg @@ -0,0 +1,10 @@ + + + Icons / Reset + + + + + + + \ No newline at end of file diff --git a/src/imgs/jambonz.png b/src/imgs/jambonz.png deleted file mode 100644 index 66dd536456c476783cfc631c918b99dcef591f2a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7813 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 + + + + \ No newline at end of file diff --git a/src/index.tsx b/src/index.tsx index 8d12c5c..233995f 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -2,6 +2,7 @@ import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App"; import { ChakraProvider } from "@chakra-ui/react"; +import mainTheme from "./theme"; const root = document.createElement("div"); root.className = "container"; @@ -9,7 +10,7 @@ document.body.appendChild(root); const rootDiv = ReactDOM.createRoot(root); rootDiv.render( - + diff --git a/src/lib/SipSession.ts b/src/lib/SipSession.ts index 6c3cc32..331f31e 100644 --- a/src/lib/SipSession.ts +++ b/src/lib/SipSession.ts @@ -264,6 +264,10 @@ export default class SipSession extends events.EventEmitter { }); } + isMuted(): boolean { + return this.#rtcSession.isMuted().audio?.valueOf() || false; + } + mute(): void { this.#rtcSession.mute({ audio: true, video: true }); } @@ -272,6 +276,10 @@ export default class SipSession extends events.EventEmitter { this.#rtcSession.unmute({ audio: true, video: true }); } + isHolded(): boolean { + return this.#rtcSession.isOnHold().local.valueOf() || false; + } + hold(): void { this.#rtcSession.hold(); } diff --git a/src/lib/SipUA.ts b/src/lib/SipUA.ts index 95a7520..9412333 100644 --- a/src/lib/SipUA.ts +++ b/src/lib/SipUA.ts @@ -135,6 +135,14 @@ export default class SipUA extends events.EventEmitter { }); } + isMuted(id: string | undefined): boolean { + if (id) { + return this.#sessionManager.getSession(id).isMuted(); + } else { + return this.#sessionManager.activeSession.isMuted(); + } + } + mute(id: string | undefined): void { if (id) { this.#sessionManager.getSession(id).mute(); @@ -151,6 +159,14 @@ export default class SipUA extends events.EventEmitter { } } + isHolded(id: string | undefined): boolean { + if (id) { + return this.#sessionManager.getSession(id).isHolded(); + } else { + return this.#sessionManager.activeSession.isHolded(); + } + } + hold(id: string | undefined): void { if (id) { this.#sessionManager.getSession(id).hold(); diff --git a/src/storage/index.ts b/src/storage/index.ts index 15ed6bc..b1302fa 100644 --- a/src/storage/index.ts +++ b/src/storage/index.ts @@ -1,4 +1,4 @@ -import { AppSettings } from "src/common/types"; +import { AppSettings, CallHistory } from "src/common/types"; import { Buffer } from "buffer"; // Settings @@ -20,3 +20,51 @@ export const getSettings = (): AppSettings => { } return {} as AppSettings; }; + +// Call History +const historyKey = "History"; +const MAX_HISTORY_COUNT = 20; +export const saveCallHistory = (username: string, call: CallHistory) => { + const str = localStorage.getItem(`${username}_${historyKey}`); + let calls: CallHistory[] = []; + if (str) { + const c = Buffer.from(str, "base64").toString("utf-8"); + calls = JSON.parse(c); + } + calls.unshift(call); + + if (calls.length > MAX_HISTORY_COUNT) { + calls = calls.slice(0, MAX_HISTORY_COUNT); + } + const saveStr = JSON.stringify(calls); + const encoded = Buffer.from(saveStr, "utf-8").toString("base64"); + localStorage.setItem(`${username}_${historyKey}`, encoded); +}; + +export const getCallHistories = (username: string): CallHistory[] => { + const str = localStorage.getItem(`${username}_${historyKey}`); + if (str) { + const c = Buffer.from(str, "base64").toString("utf-8"); + return JSON.parse(c); + } + + return []; +}; + +// Current Call +const currentCallKey = "CurrentCall"; +export const saveCurrentCall = (call: CallHistory) => { + sessionStorage.setItem(currentCallKey, JSON.stringify(call)); +}; + +export const getCurrentCall = (): CallHistory | null => { + const str = sessionStorage.getItem(currentCallKey); + if (str) { + return JSON.parse(str); + } + return null; +}; + +export const deleteCurrentCall = () => { + sessionStorage.removeItem(currentCallKey); +}; diff --git a/src/theme.tsx b/src/theme.tsx new file mode 100644 index 0000000..d0c3299 --- /dev/null +++ b/src/theme.tsx @@ -0,0 +1,50 @@ +import { extendTheme } from "@chakra-ui/react"; + +const mainTheme = extendTheme({ + colors: { + jambonz: { + 50: "#ffe1f1", + 100: "#ffb3c6", + 200: "#fc839d", + 300: "#fa5575", + 400: "#f8274e", + 500: "#DA1C5C", + 600: "#c60921", + 700: "#99081a", + 800: "#6c0714", + 900: "#44060d", + }, + grey: { + 50: "#FFFFFF", + 100: "#F5F5F5", + 200: "#ECECEC", + 300: "#E3E3E3", + 400: "#D9D9D9", + 500: "#EBEBEB", + 600: "#BFBFBF", + 700: "#969696", + 800: "#6D6D6D", + 900: "#434343", + }, + }, + components: { + FormLabel: { + baseStyle: { + _parent: { name: "form" }, + fontSize: "14px", + fontWeight: "normal", + }, + }, + Input: { + baseStyle: { + field: { + _parent: { name: "form" }, + fontSize: "14px", + fontWeight: "bold", + }, + }, + }, + }, +}); + +export default mainTheme; diff --git a/src/utils/index.ts b/src/utils/index.ts index 684e1fb..ffdb5ff 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -32,8 +32,8 @@ export const openPhonePopup = () => { const initiateNewPhonePopup = (callback: (v: unknown) => void) => { const cfg: chrome.windows.CreateData = { url: chrome.runtime.getURL("window/index.html"), - width: 300, - height: 630, + width: 440, + height: 720, focused: true, type: "panel", state: "normal", diff --git a/src/window/app.tsx b/src/window/app.tsx index 7b3a00a..c1de3f9 100644 --- a/src/window/app.tsx +++ b/src/window/app.tsx @@ -1,9 +1,25 @@ -import { Box, Tab, TabList, TabPanel, TabPanels, Tabs } from "@chakra-ui/react"; +import { + Box, + Tab, + TabList, + TabPanel, + TabPanels, + Tabs, + Text, + Grid, + Center, + HStack, + Image, +} from "@chakra-ui/react"; import Phone from "./phone"; import Settings from "./settings"; import { DEFAULT_COLOR_SCHEME } from "src/common/constants"; import { useEffect, useState } from "react"; -import { getSettings } from "src/storage"; +import { getCallHistories, getSettings } from "src/storage"; + +import jambonz from "src/imgs/jambonz.svg"; +import CallHistories from "./history"; +import { CallHistory } from "src/common/types"; export const WindowApp = () => { const [sipDomain, setSipDomain] = useState(""); @@ -11,6 +27,7 @@ export const WindowApp = () => { const [sipServerAddress, setSipServerAddress] = useState(""); const [sipPassword, setSipPassword] = useState(""); const [sipDisplayName, setSipDisplayName] = useState(""); + const [callHistories, setCallHistories] = useState([]); const tabsSettings = [ { title: "Phone", @@ -24,6 +41,10 @@ export const WindowApp = () => { /> ), }, + { + title: "Recent", + content: , + }, { title: "Settings", content: , @@ -36,6 +57,7 @@ export const WindowApp = () => { const onTabsChange = () => { loadSettings(); + setCallHistories(getCallHistories(sipUsername)); }; const loadSettings = () => { @@ -60,26 +82,39 @@ export const WindowApp = () => { } }; return ( - - - - {tabsSettings.map((s) => ( - {s.title} - ))} - + + + + + {tabsSettings.map((s) => ( + + {s.title} + + ))} + - - {tabsSettings.map((s) => ( - {s.content} - ))} - - - + + {tabsSettings.map((s) => ( + {s.content} + ))} + + + +
+ + Powered by + Jambonz Logo + +
+ ); }; diff --git a/src/window/dial-pad.tsx b/src/window/dial-pad.tsx deleted file mode 100644 index 254591a..0000000 --- a/src/window/dial-pad.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { Button, HStack, VStack } from "@chakra-ui/react"; - -type DialPadProbs = { - handleDigitPress: (digit: string) => void; -}; - -export const DialPad = ({ handleDigitPress }: DialPadProbs) => { - const buttons = [ - ["1", "2", "3"], - ["4", "5", "6"], - ["7", "8", "9"], - ["*", "0", "#"], - ]; - - return ( - - {buttons.map((row, rowIndex) => ( - - {row.map((num) => ( - - ))} - - ))} - - ); -}; - -export default DialPad; diff --git a/src/window/incomming-call.tsx b/src/window/incomming-call.tsx deleted file mode 100644 index 0c45da9..0000000 --- a/src/window/incomming-call.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { - Button, - VStack, - Text, - Tooltip, - IconButton, - Collapse, -} from "@chakra-ui/react"; -import { useState } from "react"; -import { Smartphone } from "react-feather"; -import { DEFAULT_COLOR_SCHEME } from "src/common/constants"; -import { - formatPhoneNumber, - isSipClientAnswered, - isSipClientRinging, -} from "src/utils"; -import DialPad from "./dial-pad"; - -type IncommingCallProbs = { - number: string; - callStatus: string; - answer: () => void; - hangup: () => void; - decline: () => void; - handleDialPadClick: (s: string) => void; -}; - -export const IncommingCall = ({ - number, - callStatus, - answer, - hangup, - decline, - handleDialPadClick, -}: IncommingCallProbs) => { - const [showDialPad, setShowDialPad] = useState(false); - return ( - - {!showDialPad && ( - <> - - Incoming call from - - - {formatPhoneNumber(number)} - - - )} - - {isSipClientAnswered(callStatus) && ( - - } - onClick={() => setShowDialPad((prev) => !prev)} - /> - - )} - - - - - - - {isSipClientRinging(callStatus) && ( - - )} - - ); -}; - -export default IncommingCall; diff --git a/src/window/index.tsx b/src/window/index.tsx index 5662f6f..bb65d1d 100644 --- a/src/window/index.tsx +++ b/src/window/index.tsx @@ -2,6 +2,7 @@ import React from "react"; import ReactDOM from "react-dom/client"; import { ChakraProvider } from "@chakra-ui/react"; import WindownApp from "./app"; +import mainTheme from "../theme"; const root = document.createElement("div"); root.className = "container"; @@ -9,7 +10,7 @@ document.body.appendChild(root); const rootDiv = ReactDOM.createRoot(root); rootDiv.render( - + diff --git a/src/window/outgoing-call.tsx b/src/window/outgoing-call.tsx deleted file mode 100644 index afd9fde..0000000 --- a/src/window/outgoing-call.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { - Button, - VStack, - Text, - IconButton, - Collapse, - Tooltip, -} from "@chakra-ui/react"; -import { useEffect, useRef, useState } from "react"; -import { DEFAULT_COLOR_SCHEME } from "src/common/constants"; -import { formatPhoneNumber, isSipClientAnswered } from "src/utils"; -import DialPad from "./dial-pad"; -import { Smartphone } from "react-feather"; - -type IncommingCallProbs = { - number: string; - callStatus: string; - callHold: boolean; - hangup: () => void; - callOnHold: () => void; - handleDialPadClick: (s: string) => void; -}; - -export const OutgoingCall = ({ - number, - callStatus, - callHold, - hangup, - callOnHold, - handleDialPadClick, -}: IncommingCallProbs) => { - const [seconds, setSeconds] = useState(0); - const timerRef = useRef(null); - const [showDialPad, setShowDialPad] = useState(false); - - useEffect(() => { - if (timerRef.current) { - clearInterval(timerRef.current); - timerRef.current = null; - } - timerRef.current = setInterval(() => { - setSeconds((seconds) => seconds + 1); - }, 1000); - }, []); - return ( - - {!showDialPad && ( - <> - - Talking to - - - {formatPhoneNumber(number)} - - - - {new Date(seconds * 1000).toISOString().substr(11, 8)} - - - )} - {isSipClientAnswered(callStatus) && ( - - } - onClick={() => setShowDialPad((prev) => !prev)} - /> - - )} - - - - - - - - {isSipClientAnswered(callStatus) && ( - - )} - - - ); -}; - -export default OutgoingCall; diff --git a/src/window/phone.tsx b/src/window/phone.tsx deleted file mode 100644 index 450fec9..0000000 --- a/src/window/phone.tsx +++ /dev/null @@ -1,297 +0,0 @@ -import { - Button, - Center, - Flex, - HStack, - Heading, - IconButton, - Input, - InputGroup, - InputRightElement, - Spacer, - Text, - VStack, -} from "@chakra-ui/react"; -import { useEffect, useRef, useState } from "react"; -import { Delete } from "react-feather"; -import { DEFAULT_COLOR_SCHEME } from "src/common/constants"; -import { Call, Message, MessageEvent, SipClientStatus } from "src/common/types"; -import { SipConstants, SipUA } from "src/lib"; -import IncommingCall from "./incomming-call"; -import OutgoingCall from "./outgoing-call"; -import DialPad from "./dial-pad"; -import { - isSipClientAnswered, - isSipClientIdle, - isSipClientRinging, - openPhonePopup, -} from "src/utils"; - -type PhoneProbs = { - sipDomain: string; - sipServerAddress: string; - sipUsername: string; - sipPassword: string; - sipDisplayName: string; -}; - -export const Phone = ({ - sipDomain, - sipServerAddress, - sipUsername, - sipPassword, - sipDisplayName, -}: PhoneProbs) => { - const [inputNumber, setInputNumber] = useState(""); - const [status, setStatus] = useState("offline"); - const [goOffline, setGoOffline] = useState(false); - const [backtoOnline, setBackToOnline] = useState(false); - const [callStatus, setCallStatus] = useState(SipConstants.SESSION_ENDED); - const [sessionDirection, setSessionDirection] = useState("incoming"); - const [callHold, setCallHold] = useState(false); - const sipUA = useRef(null); - - useEffect(() => { - if (sipDomain && sipUsername && sipPassword) { - createSipClient(); - } - }, [sipDomain, sipUsername, sipPassword, sipServerAddress, sipDisplayName]); - - useEffect(() => { - chrome.runtime.onMessage.addListener(function (request) { - const msg = request as Message; - 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)) { - setInputNumber(call.number); - sipUA.current?.call(call.number); - } - }; - - const createSipClient = (forceOfflineMode = false) => { - if (goOffline && !forceOfflineMode) { - return; - } - - clientGoOffline(); - - const client = { - username: `${sipUsername}@${sipDomain}`, - password: sipPassword, - name: sipDisplayName ?? sipUsername, - }; - - const settings = { - pcConfig: { - iceServers: [{ urls: ["stun:stun.l.google.com:19302"] }], - }, - wsUri: sipServerAddress, - register: true, - }; - - const sipClient = new SipUA(client, settings); - - // UA Status - sipClient.on(SipConstants.UA_REGISTERED, (args) => { - setStatus("online"); - setBackToOnline(false); - }); - sipClient.on(SipConstants.UA_UNREGISTERED, (args) => { - setStatus("offline"); - clientGoOffline(); - setBackToOnline(false); - }); - // Call Status - sipClient.on(SipConstants.SESSION_RINGING, (args) => { - setCallStatus(SipConstants.SESSION_RINGING); - setSessionDirection(args.session.direction); - setInputNumber(args.session.user); - }); - sipClient.on(SipConstants.SESSION_ANSWERED, (args) => { - setCallStatus(SipConstants.SESSION_ANSWERED); - }); - sipClient.on(SipConstants.SESSION_ENDED, (args) => { - setCallStatus(SipConstants.SESSION_ENDED); - setSessionDirection(""); - }); - sipClient.on(SipConstants.SESSION_FAILED, (args) => { - setCallStatus(SipConstants.SESSION_FAILED); - setSessionDirection(""); - }); - sipClient.on(SipConstants.SESSION_HOLD, (args) => { - setCallHold(true); - }); - sipClient.on(SipConstants.SESSION_UNHOLD, (args) => { - setCallHold(false); - }); - - sipClient.start(); - sipUA.current = sipClient; - }; - - const handleDialPadClick = (value: string) => { - if (isSipClientIdle(callStatus)) { - setInputNumber((prev) => prev + value); - } else if (isSipClientAnswered(callStatus)) { - sipUA.current?.dtmf(value, undefined); - } - }; - - const handleCallButtion = () => { - if (sipUA.current) { - sipUA.current.call(inputNumber); - } - }; - - const clientGoOffline = () => { - if (sipUA.current) { - sipUA.current.stop(); - sipUA.current = null; - } - }; - - const handleGoOffline = () => { - const newVal = !goOffline; - setGoOffline(newVal); - if (newVal) { - clientGoOffline(); - } else { - createSipClient(true); - setBackToOnline(true); - } - }; - - const handleHangup = () => { - if (isSipClientAnswered(callStatus) || isSipClientRinging(callStatus)) { - sipUA.current?.terminate(480, "Call Finished", undefined); - } - }; - - const handleCallOnHold = () => { - if (isSipClientAnswered(callStatus)) { - if (callHold) { - sipUA.current?.unhold(undefined); - } else { - sipUA.current?.hold(undefined); - } - } - }; - - const handleAnswer = () => { - if (isSipClientRinging(callStatus)) { - sipUA.current?.answer(undefined); - } - }; - - const handleDecline = () => { - if (isSipClientRinging(callStatus)) { - sipUA.current?.terminate(486, "Busy here", undefined); - } - }; - - return ( -
- {status === "online" || goOffline || backtoOnline ? ( - - - {`${sipUsername}@${sipDomain}`} - - - - Status: {status} - - - {isSipClientIdle(callStatus) && ( - - )} - - - ) : ( - - Go to Settings to configure your account - - )} - - {!isSipClientIdle(callStatus) ? ( - sessionDirection === "outgoing" ? ( - - ) : ( - - ) - ) : ( - -
- - setInputNumber(e.target.value)} - /> - } - variant="ghost" - colorScheme={DEFAULT_COLOR_SCHEME} - _hover={{ bg: "none" }} - _active={{ bg: "none" }} - onClick={() => setInputNumber((prev) => prev.slice(0, -1))} - /> - } - /> - -
- - - - - - -
- )} -
- ); -}; - -export default Phone; diff --git a/src/window/phone/dial-pad.tsx b/src/window/phone/dial-pad.tsx new file mode 100644 index 0000000..8a35817 --- /dev/null +++ b/src/window/phone/dial-pad.tsx @@ -0,0 +1,42 @@ +import { Box, Button, HStack, VStack } from "@chakra-ui/react"; + +type DialPadProbs = { + handleDigitPress: (digit: string) => void; +}; + +export const DialPad = ({ handleDigitPress }: DialPadProbs) => { + const buttons = [ + ["1", "2", "3"], + ["4", "5", "6"], + ["7", "8", "9"], + ["*", "0", "#"], + ]; + + return ( + + + {buttons.map((row, rowIndex) => ( + + {row.map((num) => ( + + ))} + + ))} + + + ); +}; + +export default DialPad; diff --git a/src/window/phone/incomming-call.tsx b/src/window/phone/incomming-call.tsx new file mode 100644 index 0000000..1351511 --- /dev/null +++ b/src/window/phone/incomming-call.tsx @@ -0,0 +1,38 @@ +import { Button, VStack, Text, Icon, HStack, Spacer } from "@chakra-ui/react"; +import { PhoneCall } from "react-feather"; +import { formatPhoneNumber } from "src/utils"; + +type IncommingCallProbs = { + number: string; + answer: () => void; + decline: () => void; +}; + +export const IncommingCall = ({ + number, + answer, + decline, +}: IncommingCallProbs) => { + return ( + + + Incoming call from + + {formatPhoneNumber(number)} + + + + + + + + + + ); +}; + +export default IncommingCall; diff --git a/src/window/phone/styles.scss b/src/window/phone/styles.scss new file mode 100644 index 0000000..da900f9 --- /dev/null +++ b/src/window/phone/styles.scss @@ -0,0 +1,4 @@ +.blurred { + filter: blur(5px); + pointer-events: none; +} diff --git a/src/window/settings.tsx b/src/window/settings.tsx deleted file mode 100644 index 42d7415..0000000 --- a/src/window/settings.tsx +++ /dev/null @@ -1,162 +0,0 @@ -import { - ChakraProvider, - Button, - FormControl, - FormLabel, - Input, - VStack, - useToast, - extendTheme, -} from "@chakra-ui/react"; -import React, { useEffect, useState } from "react"; -import { DEFAULT_TOAST_DURATION } from "src/common/constants"; -import { AppSettings } from "src/common/types"; -import { getSettings, saveSettings } from "src/storage"; - -const settingsThem = extendTheme({ - components: { - FormLabel: { - baseStyle: { - fontSize: "12px", - }, - }, - Input: { - baseStyle: { - field: { - fontSize: "12px", // Change this as per your requirement - }, - }, - }, - }, -}); - -export const Settings = () => { - const [sipDomain, setSipDomain] = useState(""); - const [sipServerAddress, setSipServerAddress] = useState(""); - const [sipUsername, setSipUsername] = useState(""); - const [sipPassword, setSipPassword] = useState(""); - const [sipDisplayName, setSipDisplayName] = useState(""); - const [apiKey, setApiKey] = useState(""); - const toast = useToast(); - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - - const settings: AppSettings = { - sipDomain, - sipServerAddress: sipServerAddress, - sipUsername, - sipPassword, - sipDisplayName, - apiKey, - }; - - saveSettings(settings); - toast({ - title: "Settings saved successfully", - status: "success", - duration: DEFAULT_TOAST_DURATION, - isClosable: true, - }); - }; - - useEffect(() => { - const settings = getSettings(); - if (settings.sipDomain) { - setSipDomain(settings.sipDomain); - } - if (settings.sipServerAddress) { - setSipServerAddress(settings.sipServerAddress); - } - if (settings.sipUsername) { - setSipUsername(settings.sipUsername); - } - if (settings.sipPassword) { - setSipPassword(settings.sipPassword); - } - if (settings.sipDisplayName) { - setSipDisplayName(settings.sipDisplayName); - } - if (settings.apiKey) { - setApiKey(settings.apiKey); - } - }, []); - return ( - -
- - - Jambonz SIP Domain - setSipDomain(e.target.value)} - /> - - - - Jambonz Server Address - setSipServerAddress(e.target.value)} - /> - - - - SIP Username - setSipUsername(e.target.value)} - /> - - - - SIP Password - setSipPassword(e.target.value)} - /> - - - - SIP Display Name - setSipDisplayName(e.target.value)} - /> - - - - API Key (optional) - setApiKey(e.target.value)} - /> - - - - -
-
- ); -}; - -export default Settings; diff --git a/src/window/settings/index.tsx b/src/window/settings/index.tsx new file mode 100644 index 0000000..ed2c413 --- /dev/null +++ b/src/window/settings/index.tsx @@ -0,0 +1,170 @@ +import { + Button, + FormControl, + FormLabel, + HStack, + Input, + Spacer, + VStack, + useToast, + Image, + Box, + Flex, + Text, +} from "@chakra-ui/react"; +import React, { useEffect, useState } from "react"; +import { DEFAULT_TOAST_DURATION } from "src/common/constants"; +import { AppSettings } from "src/common/types"; +import PasswordInput from "src/components/password-input"; +import { getSettings, saveSettings } from "src/storage"; + +import InfoIcon from "src/imgs/icons/Info.svg"; +import ResetIcon from "src/imgs/icons/Reset.svg"; + +export const Settings = () => { + const [sipDomain, setSipDomain] = useState(""); + const [sipServerAddress, setSipServerAddress] = useState( + "wss://sip.jambonz.cloud:8443/" + ); + const [sipUsername, setSipUsername] = useState(""); + const [sipPassword, setSipPassword] = useState(""); + const [sipDisplayName, setSipDisplayName] = useState(""); + const [apiKey, setApiKey] = useState(""); + const toast = useToast(); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + + const settings: AppSettings = { + sipDomain, + sipServerAddress: sipServerAddress, + sipUsername, + sipPassword, + sipDisplayName, + apiKey, + }; + + saveSettings(settings); + toast({ + title: "Settings saved successfully", + status: "success", + duration: DEFAULT_TOAST_DURATION, + isClosable: true, + colorScheme: "jambonz", + }); + }; + + const resetSetting = () => { + saveSettings({} as AppSettings); + setSipDomain(""); + setSipServerAddress(""); + setSipUsername(""); + setSipPassword(""); + setSipDisplayName(""); + setApiKey(""); + }; + + useEffect(() => { + const settings = getSettings(); + if (settings.sipDomain) { + setSipDomain(settings.sipDomain); + } + if (settings.sipServerAddress) { + setSipServerAddress(settings.sipServerAddress); + } + if (settings.sipUsername) { + setSipUsername(settings.sipUsername); + } + if (settings.sipPassword) { + setSipPassword(settings.sipPassword); + } + if (settings.sipDisplayName) { + setSipDisplayName(settings.sipDisplayName); + } + if (settings.apiKey) { + setApiKey(settings.apiKey); + } + }, []); + return ( +
+ + + Jambonz SIP Domain + setSipDomain(e.target.value)} + /> + + + + Jambonz Server Address + setSipServerAddress(e.target.value)} + /> + + + + SIP Username + setSipUsername(e.target.value)} + /> + + + + SIP Password + + + + + SIP Display Name + setSipDisplayName(e.target.value)} + /> + + + + API Key (optional) + + + + + + + + Get help + + + + + + + Reset settings + + + + +
+ ); +}; + +export default Settings; diff --git a/webpack.config.js b/webpack.config.js index 39fb5c9..1938991 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -129,12 +129,8 @@ module.exports = [ ], }, { - test: /\.(png|jpe?g|gif|svg)$/i, - use: [ - { - loader: "file-loader", - }, - ], + test: /\.(png|jp(e*)g|svg|gif)$/, + type: "asset/resource", }, ], },