This commit is contained in:
Quan HL
2023-10-02 11:47:06 +07:00
parent c55f80cd95
commit 66dca15a58
32 changed files with 580 additions and 745 deletions

View File

@@ -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"
}

27
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -16,14 +16,14 @@ export const App = () => {
padding="20px"
alignItems="center"
>
<Text fontSize="xl" mb={5}>
<Text fontSize="24px" mb={5}>
Click 'Start' to activate the service.
</Text>
<Button
size="lg"
width="full"
onClick={handleClick}
colorScheme={DEFAULT_COLOR_SCHEME}
colorScheme="jambonz"
>
Start
</Button>

View File

@@ -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<T> {
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";

View File

@@ -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<React.SetStateAction<string>>];
placeHolder?: string;
};
function PasswordInput({
password: [pass, setPass],
placeHolder,
}: PasswordInputProbs) {
const [showPassword, setShowPassword] = useState(false);
const handleClick = () => setShowPassword(!showPassword);
return (
<InputGroup size="md">
<Input
pr="4.5rem"
type={showPassword ? "text" : "password"}
placeholder={placeHolder || ""}
value={pass}
onChange={(e) => setPass(e.target.value)}
/>
<InputRightElement width="4.5rem">
<Button h="1.75rem" size="sm" onClick={handleClick} variant="unstyled">
{showPassword ? <EyeOff /> : <Eye />}
</Button>
</InputRightElement>
</InputGroup>
);
}
export default PasswordInput;

View File

@@ -1,2 +1,2 @@
declare module "*.png";
declare module "*.svg";
declare module "*.txt";

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="40px" height="40px" viewBox="0 0 40 40" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Avatar-Green@3x</title>
<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Profile-/-Dark-/-Online" transform="translate(-15, -15)">
<g id="Avatar-Green" transform="translate(15, 15)">
<circle id="Oval" stroke="#449A57" stroke-width="2" cx="20" cy="20" r="19"></circle>
<g id="Icons-/-User" transform="translate(5, 5)">
<rect id="Rectangle" fill-opacity="0" fill="#FFFFFF" x="0" y="0" width="30" height="30"></rect>
<g id="User" transform="translate(7, 5)" fill="#449A57" fill-rule="nonzero">
<path d="M8,0 C5.247396,0 3,2.247396 3,5 C3,7.752604 5.247396,10 8,10 C10.752604,10 13,7.752604 13,5 C13,2.247396 10.752604,0 8,0 Z M8,1.5 C9.942708,1.5 11.5,3.057292 11.5,5 C11.5,6.942708 9.942708,8.5 8,8.5 C6.057292,8.5 4.5,6.942708 4.5,5 C4.5,3.057292 6.057292,1.5 8,1.5 Z M1.98958333,12 C0.898437333,12 0,12.8984373 0,13.9895833 L0,14.75 C0,16.5520833 1.14062533,17.9453127 2.653646,18.78125 C4.16666667,19.6171873 6.08333333,20 8,20 C9.91666667,20 11.8333333,19.6171873 13.346354,18.78125 C14.6380207,18.0703127 15.6093747,16.932292 15.880208,15.5 L16,15.5 L16,13.9895833 C16,12.8984373 15.1015627,12 14.0104167,12 L1.98958333,12 Z M1.98958333,13.5 L14.0104167,13.5 C14.2890627,13.5 14.5,13.7109373 14.5,13.9895833 L14.5,14.75 C14.5,15.9479167 13.8281253,16.8046873 12.622396,17.46875 C11.4166667,18.1328127 9.70833333,18.5 8,18.5 C6.29166667,18.5 4.58333333,18.1328127 3.377604,17.46875 C2.17187467,16.8046873 1.5,15.9479167 1.5,14.75 L1.5,13.9895833 C1.5,13.7109373 1.71093733,13.5 1.98958333,13.5 Z" id="Shape"></path>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

17
src/imgs/icons/Avatar.svg Normal file
View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="40px" height="40px" viewBox="0 0 40 40" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Avatar@3x</title>
<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Profile-/-Dark-/-Offline" transform="translate(-15, -15)">
<g id="Avatar" transform="translate(15, 15)">
<circle id="Oval" stroke="#393536" stroke-width="2" cx="20" cy="20" r="19"></circle>
<g id="Icons-/-User" transform="translate(5, 5)">
<rect id="Rectangle" fill-opacity="0" fill="#FFFFFF" x="0" y="0" width="30" height="30"></rect>
<g id="User" transform="translate(7, 5)" fill="#393536" fill-rule="nonzero">
<path d="M8,0 C5.247396,0 3,2.247396 3,5 C3,7.752604 5.247396,10 8,10 C10.752604,10 13,7.752604 13,5 C13,2.247396 10.752604,0 8,0 Z M8,1.5 C9.942708,1.5 11.5,3.057292 11.5,5 C11.5,6.942708 9.942708,8.5 8,8.5 C6.057292,8.5 4.5,6.942708 4.5,5 C4.5,3.057292 6.057292,1.5 8,1.5 Z M1.98958333,12 C0.898437333,12 0,12.8984373 0,13.9895833 L0,14.75 C0,16.5520833 1.14062533,17.9453127 2.653646,18.78125 C4.16666667,19.6171873 6.08333333,20 8,20 C9.91666667,20 11.8333333,19.6171873 13.346354,18.78125 C14.6380207,18.0703127 15.6093747,16.932292 15.880208,15.5 L16,15.5 L16,13.9895833 C16,12.8984373 15.1015627,12 14.0104167,12 L1.98958333,12 Z M1.98958333,13.5 L14.0104167,13.5 C14.2890627,13.5 14.5,13.7109373 14.5,13.9895833 L14.5,14.75 C14.5,15.9479167 13.8281253,16.8046873 12.622396,17.46875 C11.4166667,18.1328127 9.70833333,18.5 8,18.5 C6.29166667,18.5 4.58333333,18.1328127 3.377604,17.46875 C2.17187467,16.8046873 1.5,15.9479167 1.5,14.75 L1.5,13.9895833 C1.5,13.7109373 1.71093733,13.5 1.98958333,13.5 Z" id="Shape"></path>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="30px" height="30px" viewBox="0 0 30 30" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Icons / Hang up</title>
<g id="Icons-/-Hang-up" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<rect id="Rectangle" fill-opacity="0" fill="#FFFFFF" x="0" y="0" width="30" height="30"></rect>
<g id="End-Call" transform="translate(5, 6)" fill="#171718" fill-rule="nonzero">
<path d="M10.0535742,2.27741994e-05 C5.86732285,-0.00516223737 3.04245402,0.875043643 1.07106761,2.60420615 C0.243765946,3.33076706 -0.146322331,4.40628514 0.0500310805,5.43232467 L0.249002368,6.46878017 C0.434883606,7.43752784 1.34858053,8.08856761 2.38008924,7.98960985 L4.43787157,7.79429765 C5.33586057,7.70575653 6.09771066,7.07554938 6.32809846,6.22919742 L6.95119309,4.09378589 C7.89107079,3.71097415 9.04824557,3.50003732 10.0535742,3.50003732 C11.0589028,3.50003732 12.108738,3.68493288 13.0486157,4.06774396 L13.6743282,6.20315615 C13.9020981,7.04690345 14.6665661,7.67971459 15.5645551,7.76565238 L17.6197188,7.96356791 C18.6538461,8.06252634 19.5649251,7.41148657 19.7508064,6.44273824 L19.9497777,5.40628273 C20.1461311,4.3802432 19.7586607,3.30472513 18.9287411,2.57816488 C16.9599726,0.849002373 14.2398255,0.00525373755 10.0535742,2.27741994e-05 Z M10.0404841,7.48954799 C9.62683296,7.4948197 9.29434197,7.83596422 9.29957772,8.25002588 L9.29957772,16.4401102 L7.32033702,14.4687609 C7.12922003,14.2734487 6.84908924,14.1953242 6.58466704,14.2630321 C6.32024484,14.3307406 6.11341925,14.5364694 6.04534979,14.7994894 C5.977281,15.0625095 6.05582197,15.3411548 6.25217538,15.5312584 L9.51949336,18.7812505 C9.81533208,19.0729165 10.2918163,19.0729165 10.587655,18.7812505 L13.854973,15.5312584 C14.0513264,15.3411548 14.1298674,15.0625095 14.0617986,14.7994894 C13.9937291,14.5364694 13.7869035,14.3307406 13.5224813,14.2630321 C13.2580591,14.1953242 12.9779283,14.2734487 12.7868113,14.4687609 L10.8075706,16.4401102 L10.8075706,8.25002588 C10.8101885,8.04690104 10.7316475,7.85158951 10.587655,7.70836052 C10.4410446,7.56513153 10.2446912,7.48700706 10.0404841,7.48954799 Z" id="Shape"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

10
src/imgs/icons/Info.svg Normal file
View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="30px" height="30px" viewBox="0 0 30 30" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Icons / Info</title>
<g id="Icons-/-Info" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<rect id="Rectangle" fill-opacity="0" fill="#FFFFFF" x="0" y="0" width="30" height="30"></rect>
<g id="Info" transform="translate(5, 5)" fill="#171718" fill-rule="nonzero">
<path d="M10,0 C4.48697933,0 0,4.48697933 0,10 C0,15.5130207 4.48697933,20 10,20 C15.5130207,20 20,15.5130207 20,10 C20,4.48697933 15.5130207,0 10,0 Z M10,1.5 C14.7031253,1.5 18.5,5.29687467 18.5,10 C18.5,14.7031253 14.7031253,18.5 10,18.5 C5.29687467,18.5 1.5,14.7031253 1.5,10 C1.5,5.29687467 5.29687467,1.5 10,1.5 Z M10,5 C9.44791667,5 9,5.44791667 9,6 C9,6.55208333 9.44791667,7 10,7 C10.5520833,7 11,6.55208333 11,6 C11,5.44791667 10.5520833,5 10,5 Z M9.98697933,8.48952027 C9.57552067,8.494792 9.244792,8.83593733 9.25,9.25 L9.25,14.75 C9.247396,15.0208333 9.38802067,15.2708333 9.622396,15.408854 C9.85677067,15.5442707 10.1432293,15.5442707 10.377604,15.408854 C10.6119793,15.2708333 10.752604,15.0208333 10.75,14.75 L10.75,9.25 C10.752604,9.04687467 10.6744793,8.85156267 10.53125,8.70833333 C10.3854167,8.565104 10.190104,8.48697933 9.98697933,8.48952027 Z" id="Shape"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

10
src/imgs/icons/Reset.svg Normal file
View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="30px" height="30px" viewBox="0 0 30 30" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Icons / Reset</title>
<g id="Icons-/-Reset" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<rect id="Rectangle" fill-opacity="0" fill="#FFFFFF" x="0" y="0" width="30" height="30"></rect>
<g id="Reset" transform="translate(6, 6)" fill="#171718" fill-rule="nonzero">
<path d="M15.7369793,0 C15.3255207,0.00526866127 14.994792,0.346215493 15,0.76003723 L15,2.31121745 C13.408854,0.882362011 11.3046873,0.0104736309 9,0.0104736309 C4.03906267,0.0104736309 0,4.04718609 0,9.00523682 C0,13.9632875 4.03906267,18 9,18 C13.9609373,18 18,13.9632875 18,9.00523682 C18,8.52895145 17.9531253,8.07609044 17.8880207,7.64404997 C17.8541667,7.37077175 17.6770833,7.13913594 17.4218747,7.03763237 C17.1692707,6.93352631 16.880208,6.97777122 16.6666667,7.15214903 C16.4557293,7.32392369 16.3541667,7.59980507 16.403646,7.86787833 C16.4635413,8.25307073 16.5,8.63045502 16.5,9.00523682 C16.5,13.1538628 13.1510413,16.5008728 9,16.5008728 C4.84895867,16.5008728 1.5,13.1538628 1.5,9.00523682 C1.5,4.85661084 4.84895867,1.50960083 9,1.50960083 C10.966146,1.50960083 12.7395833,2.27217752 14.0755207,3.50843709 L12.25,3.50843709 C11.9791667,3.50583461 11.7291667,3.64637745 11.591146,3.88061641 C11.4557293,4.1148547 11.4557293,4.40114668 11.591146,4.63538497 C11.7291667,4.86962393 11.9791667,5.01016677 12.25,5.00756429 L15.75,5.00756429 C16.1640627,5.00756429 16.5,4.67182243 16.5,4.25800069 L16.5,0.76003723 C16.502604,0.557030088 16.4244793,0.361831734 16.28125,0.218685742 C16.1354167,0.0755397489 15.940104,-0.00253945945 15.7369793,0 Z" id="Path"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

14
src/imgs/jambonz.svg Normal file
View File

@@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="649px" height="213px" viewBox="0 0 649 213" version="1.1">
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Logo">
<path d="M16.7269,19.605 C15.4359,21.9104 14.9386,24.5764 15.3117,27.1923 C15.6177,29.3235 16.4884,31.3341 17.8334,33.0154 C19.1785,34.6967 20.9489,35.9875 22.961,36.7539 C25.4312,37.6921 28.1412,37.7922 30.6738,37.0387 C33.2065,36.2853 35.4212,34.7202 36.9769,32.5843 C38.5326,30.4485 39.3429,27.8605 39.2832,25.2189 C39.2234,22.5772 38.2969,20.0284 36.6462,17.9651 C34.9956,15.9018 32.7124,14.4384 30.1483,13.8002 C27.5842,13.1621 24.8814,13.3846 22.4562,14.4335 C20.031,15.4824 18.0179,17.2995 16.7269,19.605 Z" id="Path" fill="#231F20" fill-rule="nonzero"/>
<path d="M16.1782,117.568 C16.1782,123.305 12.9082,126.862 8.26124,126.862 L0,126.862 L0,143.9 C1.72109,145.105 6.36805,146.138 11.015,146.138 C25.6442,146.138 38.036,134.779 38.036,120.149 L38.036,49.0583 L16.1782,49.0583 L16.1782,117.568 Z" id="Path" fill="#231F20" fill-rule="nonzero"/>
<path d="M130.732,99.9951 L130.732,117.894 C128.897,118.753 126.905,119.222 124.88,119.271 C120.637,119.338 116.45,118.293 112.736,116.24 C109.022,114.187 105.91,111.197 103.711,107.568 C100.986,111.254 97.4234,114.238 93.3164,116.274 C89.2094,118.309 84.6767,119.336 80.0935,119.271 C60.9703,119.271 46.0159,103.437 46.0159,83.4727 C46.0159,63.508 60.9703,47.674 80.0935,47.674 C87.3363,47.559 94.3513,50.205 99.7139,55.0747 L99.7139,49.0509 L121.572,49.0509 L121.572,93.2829 C121.572,99.0198 124.67,99.9951 127.596,99.9951 L130.732,99.9951 Z M100.785,83.4727 C100.771,80.6486 100.032,77.8754 98.6391,75.4189 C97.2458,72.9625 95.2448,70.9052 92.8279,69.4443 C90.4111,67.9835 87.6594,67.168 84.8368,67.0762 C82.0143,66.9843 79.2154,67.6192 76.7087,68.9199 C74.9128,69.7893 73.3102,71.0109 71.9961,72.5123 C70.682,74.0137 69.6833,75.764 69.0594,77.6592 C68.2216,79.9061 67.8872,82.3096 68.0796,84.6999 C68.2721,87.0902 68.9867,89.4091 70.1732,91.4931 C71.3597,93.5771 72.989,95.3753 74.9462,96.7609 C76.9035,98.1464 79.141,99.0856 81.5008,99.512 C83.8606,99.9385 86.2852,99.8419 88.6036,99.2291 C90.9221,98.6162 93.0778,97.502 94.9186,95.9651 C96.7594,94.4283 98.2405,92.5062 99.2574,90.3344 C100.274,88.1627 100.802,85.7942 100.804,83.3961 L100.785,83.4727 Z" id="Shape" fill="#231F20" fill-rule="nonzero"/>
<path d="M140.612,117.895 L140.612,49.0486 L162.641,49.0486 L162.641,56.7958 C164.992,53.9589 167.937,51.6718 171.267,50.0958 C174.598,48.5198 178.233,47.6932 181.918,47.6741 C191.344,47.6741 199.432,52.946 203.683,61.0366 C204.639,59.6404 205.802,58.0306 206.871,56.7958 C209.242,54.0553 212.166,51.6718 215.497,50.0958 C218.827,48.5198 222.462,47.6932 226.147,47.6741 C240.088,47.6741 251.103,59.2054 251.103,74.3318 L251.103,117.875 L229.073,117.875 L229.073,77.9652 C229.133,76.5544 228.904,75.1462 228.402,73.8264 C227.9,72.5066 227.135,71.3028 226.153,70.2884 C225.171,69.274 223.992,68.4701 222.689,67.9258 C221.386,67.3815 219.986,67.1082 218.574,67.1224 C215.462,67.1919 212.5,68.4707 210.315,70.6877 C208.172,72.862 206.943,75.7711 206.873,78.8186 L206.873,117.875 L184.844,117.875 L184.844,77.9652 C184.903,76.5544 184.675,75.1462 184.173,73.8264 C183.671,72.5066 182.906,71.3028 181.923,70.2884 C180.941,69.274 179.763,68.4701 178.46,67.9258 C177.157,67.3815 175.757,67.1082 174.345,67.1224 C171.233,67.1919 168.271,68.4707 166.086,70.6877 C163.901,72.9047 162.666,75.8854 162.641,78.9979 L162.641,117.895 L140.612,117.895 Z" id="Path" fill="#231F20" fill-rule="nonzero"/>
<path d="M284.925,111.959 L284.925,117.895 L262.895,117.895 L262.895,0 L284.925,0 L284.925,55.0664 C290.286,50.2021 297.297,47.5592 304.536,47.6741 C323.659,47.6741 338.613,63.5081 338.613,83.4728 C338.613,103.437 323.659,119.271 304.536,119.271 C299.953,119.337 295.42,118.309 291.313,116.274 C288.992,115.124 286.845,113.671 284.925,111.959 Z M285.99,75.4191 C284.597,77.8755 283.858,80.6487 283.845,83.4728 L283.825,83.3963 C283.827,85.7943 284.355,88.1628 285.372,90.3346 C286.389,92.5063 287.87,94.4284 289.711,95.9652 C291.552,97.5021 293.707,98.6163 296.026,99.2292 C298.344,99.842 300.769,99.9387 303.129,99.5122 C305.488,99.0857 307.726,98.1465 309.683,96.761 C311.64,95.3754 313.27,93.5772 314.456,91.4932 C315.643,89.4093 316.357,87.0903 316.55,84.7 C316.742,82.3097 316.408,79.9063 315.57,77.6593 C314.946,75.7641 313.947,74.0138 312.633,72.5124 C311.319,71.0111 309.717,69.7894 307.921,68.92 C305.414,67.6194 302.615,66.9845 299.793,67.0763 C296.97,67.1681 294.218,67.9836 291.801,69.4445 C289.385,70.9053 287.384,72.9626 285.99,75.4191 Z" id="Shape" fill="#231F20"/>
<path d="M346.581,83.4727 C346.581,62.8197 361.879,47.6741 384.961,47.6741 C408.043,47.6741 423.207,62.8197 423.207,83.4727 C423.207,104.126 408.062,119.271 384.961,119.271 C361.86,119.271 346.581,104.126 346.581,83.4727 Z M401.311,83.4727 C401.311,80.2389 400.352,77.0778 398.556,74.389 C396.759,71.7002 394.206,69.6045 391.218,68.367 C388.23,67.1295 384.943,66.8057 381.771,67.4366 C378.6,68.0674 375.686,69.6246 373.4,71.9113 C371.113,74.1979 369.556,77.1113 368.925,80.2829 C368.294,83.4546 368.618,86.7421 369.855,89.7297 C371.093,92.7174 373.188,95.271 375.877,97.0676 C378.566,98.8641 381.727,99.823 384.961,99.823 C387.112,99.8462 389.246,99.4388 391.237,98.6246 C393.228,97.8104 395.036,96.606 396.554,95.0823 C398.072,93.5586 399.27,91.7464 400.078,89.7526 C400.885,87.7588 401.285,85.6235 401.254,83.4727 L401.311,83.4727 Z" id="Shape" fill="#231F20" fill-rule="nonzero"/>
<path d="M431.175,49.0431 L431.175,117.895 L453.205,117.895 L453.205,78.9979 C453.229,75.8854 454.464,72.9047 456.649,70.6877 C458.834,68.4707 461.796,67.1919 464.908,67.1224 C466.32,67.1082 467.72,67.3815 469.023,67.9258 C470.326,68.4701 471.505,69.274 472.487,70.2884 C473.469,71.3028 474.235,72.5066 474.737,73.8264 C475.239,75.1462 475.467,76.5544 475.407,77.9652 L475.407,117.875 L497.437,117.875 L497.437,74.3318 C497.437,59.2054 486.422,47.6741 472.481,47.6741 C468.797,47.6932 465.161,48.5198 461.831,50.0958 C458.5,51.6718 455.556,53.9589 453.205,56.7958 L453.205,49.0431 L431.175,49.0431 Z" id="Path" fill="#231F20" fill-rule="nonzero"/>
<path d="M552.288,67.2054 C550.533,65.2934 547.308,63.3419 541.476,63.5469 C529.761,63.9587 523.766,75.0885 525.135,81.5759 C526.226,86.7428 522.921,91.8154 517.754,92.9059 C512.587,93.9964 507.515,90.6918 506.424,85.5249 C502.712,67.9368 516.694,45.283 540.804,44.4355 C551.73,44.0515 560.519,47.8936 566.376,54.2733 C572.026,60.4288 574.505,68.4947 573.837,75.8471 C572.514,90.4014 561.294,98.132 553.314,103.169 L552.947,103.401 C560.751,105.477 567.732,109.3 573.654,114.473 C581.83,121.615 587.724,131.083 591.132,141.515 C602.593,146.006 613.885,152.148 624.164,160.113 C625.588,154.886 629.044,147.961 632.213,145.361 C640.793,161.753 646.346,185.684 648.305,203.786 C636.364,198.042 613.993,190.787 598.981,188.666 C601.859,183.086 607.729,177.682 612.267,175.088 C606.644,170.752 600.635,167.037 594.441,163.914 C593.768,176.812 587.773,188.12 579.654,196.456 C570.419,205.94 557.705,212.351 545.014,212.939 C533.206,213.486 522.067,210.344 513.76,203.503 C505.292,196.53 500.444,186.23 500.745,174.206 C501.035,162.596 505.809,151.435 514.635,143.457 C523.54,135.409 535.977,131.123 550.61,132.187 C555.615,132.551 560.807,133.234 566.097,134.255 C564.577,132.272 562.896,130.467 561.073,128.875 C553.309,122.093 542.394,118.61 528.39,122.148 C528.065,122.23 527.739,122.295 527.412,122.342 C522.518,123.318 518.103,123.096 516.388,120.409 C514.27,117.092 514.163,112.712 519.024,106.168 C522.272,101.538 526.394,98.0112 530.331,95.1763 C533.391,92.973 536.851,90.8513 539.885,88.9914 C541.035,88.2863 542.124,87.6186 543.106,86.9986 C551.38,81.7756 554.396,78.4765 554.792,74.1157 C554.978,72.0653 554.249,69.3415 552.288,67.2054 Z M574.952,156.124 C566.16,153.452 557.408,151.855 549.223,151.26 C539.229,150.533 532.089,153.458 527.458,157.644 C522.749,161.9 520.029,167.996 519.862,174.684 C519.705,180.957 522.088,185.588 525.917,188.741 C529.907,192.027 536.131,194.207 544.128,193.836 C551.243,193.507 559.559,189.682 565.954,183.114 C572.254,176.645 575.96,168.286 575.305,159.444 C575.222,158.326 575.104,157.219 574.952,156.124 Z" id="Shape" fill="#DA1C5C"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

@@ -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(
<React.StrictMode>
<ChakraProvider>
<ChakraProvider theme={mainTheme}>
<App />
</ChakraProvider>
</React.StrictMode>

View File

@@ -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();
}

View File

@@ -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();

View File

@@ -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);
};

50
src/theme.tsx Normal file
View File

@@ -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;

View File

@@ -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",

View File

@@ -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<CallHistory[]>([]);
const tabsSettings = [
{
title: "Phone",
@@ -24,6 +41,10 @@ export const WindowApp = () => {
/>
),
},
{
title: "Recent",
content: <CallHistories calls={callHistories} />,
},
{
title: "Settings",
content: <Settings />,
@@ -36,6 +57,7 @@ export const WindowApp = () => {
const onTabsChange = () => {
loadSettings();
setCallHistories(getCallHistories(sipUsername));
};
const loadSettings = () => {
@@ -60,26 +82,39 @@ export const WindowApp = () => {
}
};
return (
<Box p={4}>
<Tabs
isFitted
variant="enclosed"
colorScheme={DEFAULT_COLOR_SCHEME}
onChange={onTabsChange}
>
<TabList mb="1em">
{tabsSettings.map((s) => (
<Tab>{s.title}</Tab>
))}
</TabList>
<Grid h="100vh" templateRows="1fr auto">
<Box p={2}>
<Tabs
isFitted
variant="enclosed"
colorScheme={DEFAULT_COLOR_SCHEME}
onChange={onTabsChange}
>
<TabList mb="1em">
{tabsSettings.map((s) => (
<Tab
_selected={{ color: "white", bg: "jambonz.500" }}
bg="grey.500"
>
{s.title}
</Tab>
))}
</TabList>
<TabPanels>
{tabsSettings.map((s) => (
<TabPanel>{s.content}</TabPanel>
))}
</TabPanels>
</Tabs>
</Box>
<TabPanels>
{tabsSettings.map((s) => (
<TabPanel>{s.content}</TabPanel>
))}
</TabPanels>
</Tabs>
</Box>
<Center>
<HStack spacing={1} mb={2} align="start">
<Text fontSize="14px">Powered by</Text>
<Image src={jambonz} alt="Jambonz Logo" w="91px" h="31px" />
</HStack>
</Center>
</Grid>
);
};

View File

@@ -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 (
<VStack w="full" bg="black" spacing={0.5}>
{buttons.map((row, rowIndex) => (
<HStack key={rowIndex} justifyContent="space-between" spacing={0.5}>
{row.map((num) => (
<Button
key={num}
onClick={() => handleDigitPress(num)}
size="lg"
p={0}
width="70px"
height="70px"
variant="unstyled"
bg="white"
borderRadius={0}
>
{num}
</Button>
))}
</HStack>
))}
</VStack>
);
};
export default DialPad;

View File

@@ -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 (
<VStack alignItems="center" spacing={4} mt={5}>
{!showDialPad && (
<>
<Text fontSize="2xl" fontWeight="bold">
Incoming call from
</Text>
<Text fontSize="2xl" fontWeight="bold">
{formatPhoneNumber(number)}
</Text>
</>
)}
{isSipClientAnswered(callStatus) && (
<Tooltip label="Dial pad">
<IconButton
colorScheme={DEFAULT_COLOR_SCHEME}
aria-label="Toggle Dialpad"
icon={<Smartphone />}
onClick={() => setShowDialPad((prev) => !prev)}
/>
</Tooltip>
)}
<Collapse in={showDialPad}>
<DialPad handleDigitPress={handleDialPadClick} />
</Collapse>
<Button
w="full"
colorScheme={DEFAULT_COLOR_SCHEME}
onClick={isSipClientRinging(callStatus) ? answer : hangup}
>
{isSipClientRinging(callStatus) ? "Answer" : "Hang up"}
</Button>
{isSipClientRinging(callStatus) && (
<Button w="full" colorScheme={DEFAULT_COLOR_SCHEME} onClick={decline}>
Decline
</Button>
)}
</VStack>
);
};
export default IncommingCall;

View File

@@ -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(
<React.StrictMode>
<ChakraProvider>
<ChakraProvider theme={mainTheme}>
<WindownApp />
</ChakraProvider>
</React.StrictMode>

View File

@@ -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<NodeJS.Timer | null>(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 (
<VStack alignItems="center" spacing={4} mt={5}>
{!showDialPad && (
<>
<Text fontSize="2xl" fontWeight="bold">
Talking to
</Text>
<Text fontSize="2xl" fontWeight="bold">
{formatPhoneNumber(number)}
</Text>
<Text fontSize="xl" fontWeight="bold">
{new Date(seconds * 1000).toISOString().substr(11, 8)}
</Text>
</>
)}
{isSipClientAnswered(callStatus) && (
<Tooltip label="Dial pad">
<IconButton
colorScheme={DEFAULT_COLOR_SCHEME}
aria-label="Toggle Dialpad"
icon={<Smartphone />}
onClick={() => setShowDialPad((prev) => !prev)}
/>
</Tooltip>
)}
<Collapse in={showDialPad}>
<DialPad handleDigitPress={handleDialPadClick} />
</Collapse>
<VStack spacing={5}>
<Button w="full" colorScheme={DEFAULT_COLOR_SCHEME} onClick={hangup}>
Hang up
</Button>
{isSipClientAnswered(callStatus) && (
<Button
w="full"
colorScheme={DEFAULT_COLOR_SCHEME}
onClick={callOnHold}
>
{callHold ? "Unhold " : "Place call on hold"}
</Button>
)}
</VStack>
</VStack>
);
};
export default OutgoingCall;

View File

@@ -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<SipClientStatus>("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<SipUA | null>(null);
useEffect(() => {
if (sipDomain && sipUsername && sipPassword) {
createSipClient();
}
}, [sipDomain, sipUsername, sipPassword, sipServerAddress, sipDisplayName]);
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)) {
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 (
<Center flexDirection="column">
{status === "online" || goOffline || backtoOnline ? (
<VStack mb={2}>
<Text fontWeight="bold" maxW="full">
{`${sipUsername}@${sipDomain}`}
</Text>
<Flex alignItems="center" justifyContent="space-between" w="full">
<Text>
<strong>Status:</strong> {status}
</Text>
<Spacer />
{isSipClientIdle(callStatus) && (
<Button
size="sm"
onClick={handleGoOffline}
colorScheme={DEFAULT_COLOR_SCHEME}
ml={5}
>
{goOffline ? "Go online" : "Go offline"}
</Button>
)}
</Flex>
</VStack>
) : (
<Heading size="md" mb={2}>
Go to Settings to configure your account
</Heading>
)}
{!isSipClientIdle(callStatus) ? (
sessionDirection === "outgoing" ? (
<OutgoingCall
callHold={callHold}
callStatus={callStatus}
number={inputNumber}
hangup={handleHangup}
callOnHold={handleCallOnHold}
handleDialPadClick={handleDialPadClick}
/>
) : (
<IncommingCall
number={inputNumber}
callStatus={callStatus}
answer={handleAnswer}
hangup={handleHangup}
decline={handleDecline}
handleDialPadClick={handleDialPadClick}
/>
)
) : (
<VStack w="200px" spacing={4}>
<Center flexDirection="column">
<InputGroup>
<Input
value={inputNumber}
shadow="md"
fontWeight="bold"
onChange={(e) => setInputNumber(e.target.value)}
/>
<InputRightElement
children={
<IconButton
aria-label="Delete text"
icon={<Delete />}
variant="ghost"
colorScheme={DEFAULT_COLOR_SCHEME}
_hover={{ bg: "none" }}
_active={{ bg: "none" }}
onClick={() => setInputNumber((prev) => prev.slice(0, -1))}
/>
}
/>
</InputGroup>
</Center>
<DialPad handleDigitPress={handleDialPadClick} />
<HStack alignItems="center" w="80%">
<Button
w="full"
onClick={handleCallButtion}
isDisabled={status === "offline"}
colorScheme={DEFAULT_COLOR_SCHEME}
>
Call
</Button>
</HStack>
</VStack>
)}
</Center>
);
};
export default Phone;

View File

@@ -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 (
<Box p={2} w="full">
<VStack w="full" bg="grey.500" spacing={0.5}>
{buttons.map((row, rowIndex) => (
<HStack key={rowIndex} justifyContent="space-between" spacing={0.5}>
{row.map((num) => (
<Button
key={num}
onClick={() => handleDigitPress(num)}
size="lg"
p={0}
width="124px"
height="70px"
variant="unstyled"
bg="white"
borderRadius={0}
>
{num}
</Button>
))}
</HStack>
))}
</VStack>
</Box>
);
};
export default DialPad;

View File

@@ -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 (
<VStack alignItems="center" spacing={4} mt="130px" w="full">
<Icon as={PhoneCall} color="jambonz.500" w="60px" h="60px" />
<Text fontSize="15px">Incoming call from</Text>
<Text fontSize="24px" fontWeight="bold">
{formatPhoneNumber(number)}
</Text>
<HStack w="full">
<Button w="full" colorScheme="jambonz" onClick={decline}>
Decline
</Button>
<Spacer />
<Button w="full" colorScheme="green" onClick={answer}>
Answer
</Button>
</HStack>
</VStack>
);
};
export default IncommingCall;

View File

@@ -0,0 +1,4 @@
.blurred {
filter: blur(5px);
pointer-events: none;
}

View File

@@ -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 (
<ChakraProvider theme={settingsThem}>
<form onSubmit={handleSubmit}>
<VStack spacing={2}>
<FormControl id="jambonz_sip_domain">
<FormLabel>Jambonz SIP Domain</FormLabel>
<Input
type="text"
placeholder="Domain"
isRequired
value={sipDomain}
onChange={(e) => setSipDomain(e.target.value)}
/>
</FormControl>
<FormControl id="jambonz_server_address">
<FormLabel>Jambonz Server Address</FormLabel>
<Input
type="text"
placeholder="Server address"
isRequired
value={sipServerAddress}
onChange={(e) => setSipServerAddress(e.target.value)}
/>
</FormControl>
<FormControl id="username">
<FormLabel>SIP Username</FormLabel>
<Input
type="text"
placeholder="Username"
isRequired
value={sipUsername}
onChange={(e) => setSipUsername(e.target.value)}
/>
</FormControl>
<FormControl id="password">
<FormLabel>SIP Password</FormLabel>
<Input
type="password"
placeholder="Enter your password"
isRequired
value={sipPassword}
onChange={(e) => setSipPassword(e.target.value)}
/>
</FormControl>
<FormControl id="sip_display_name">
<FormLabel>SIP Display Name</FormLabel>
<Input
type="text"
placeholder="Display name"
isRequired
value={sipDisplayName}
onChange={(e) => setSipDisplayName(e.target.value)}
/>
</FormControl>
<FormControl id="api_key">
<FormLabel>API Key (optional)</FormLabel>
<Input
type="password"
placeholder="API Key"
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
/>
</FormControl>
<Button colorScheme="pink" type="submit" w="80%">
Save
</Button>
</VStack>
</form>
</ChakraProvider>
);
};
export default Settings;

View File

@@ -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 (
<form onSubmit={handleSubmit}>
<VStack spacing={2}>
<FormControl id="jambonz_sip_domain">
<FormLabel>Jambonz SIP Domain</FormLabel>
<Input
type="text"
placeholder="Domain"
isRequired
value={sipDomain}
onChange={(e) => setSipDomain(e.target.value)}
/>
</FormControl>
<FormControl id="jambonz_server_address">
<FormLabel>Jambonz Server Address</FormLabel>
<Input
type="text"
placeholder="Server address"
isRequired
value={sipServerAddress}
onChange={(e) => setSipServerAddress(e.target.value)}
/>
</FormControl>
<FormControl id="username">
<FormLabel>SIP Username</FormLabel>
<Input
type="text"
placeholder="Username"
isRequired
value={sipUsername}
onChange={(e) => setSipUsername(e.target.value)}
/>
</FormControl>
<FormControl id="password">
<FormLabel fontWeight="">SIP Password</FormLabel>
<PasswordInput
password={[sipPassword, setSipPassword]}
placeHolder="Enter your password"
/>
</FormControl>
<FormControl id="sip_display_name">
<FormLabel>SIP Display Name</FormLabel>
<Input
type="text"
placeholder="Display name"
isRequired
value={sipDisplayName}
onChange={(e) => setSipDisplayName(e.target.value)}
/>
</FormControl>
<FormControl id="api_key">
<FormLabel>API Key (optional)</FormLabel>
<PasswordInput password={[apiKey, setApiKey]} />
</FormControl>
<Button colorScheme="jambonz" type="submit" w="full">
Save
</Button>
<HStack w="full">
<HStack spacing={1}>
<Image src={InfoIcon} w="30px" h="30px" />
<Text fontSize="14px">Get help</Text>
</HStack>
<Spacer />
<HStack spacing={1}>
<Image src={ResetIcon} w="30px" h="30px" />
<Text fontSize="14px" onClick={resetSetting} cursor="pointer">
Reset settings
</Text>
</HStack>
</HStack>
</VStack>
</form>
);
};
export default Settings;

View File

@@ -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",
},
],
},