mirror of
https://github.com/jambonz/chrome-extension-dialer.git
synced 2025-12-19 04:47:45 +00:00
add new audios and fix review comment
This commit is contained in:
BIN
public/audios/call-failed.mp3
Normal file
BIN
public/audios/call-failed.mp3
Normal file
Binary file not shown.
Binary file not shown.
BIN
public/audios/remote-party-hungup-tone.mp3
Normal file
BIN
public/audios/remote-party-hungup-tone.mp3
Normal file
Binary file not shown.
BIN
public/audios/us-busy-signal.mp3
Normal file
BIN
public/audios/us-busy-signal.mp3
Normal file
Binary file not shown.
BIN
public/audios/us-ringback.mp3
Normal file
BIN
public/audios/us-ringback.mp3
Normal file
Binary file not shown.
@@ -16,7 +16,7 @@ export interface IconButtonMenuItems {
|
||||
type IconButtonMenuProbs = {
|
||||
icon: React.ReactElement;
|
||||
onOpen: () => Promise<IconButtonMenuItems[]>;
|
||||
onClick: (value: string) => void;
|
||||
onClick: (name: string, value: string) => void;
|
||||
tooltip: string;
|
||||
};
|
||||
|
||||
@@ -53,7 +53,7 @@ export const IconButtonMenu = ({
|
||||
</MenuItem>
|
||||
) : (
|
||||
items.map((i, idx) => (
|
||||
<MenuItem key={idx} onClick={() => onClick(i.value)}>
|
||||
<MenuItem key={idx} onClick={() => onClick(i.name, i.value)}>
|
||||
{i.name}
|
||||
</MenuItem>
|
||||
))
|
||||
|
||||
@@ -1,17 +1,26 @@
|
||||
export default class SipAudioElements {
|
||||
#ringing: HTMLAudioElement;
|
||||
#ringBack: HTMLAudioElement;
|
||||
#failed: HTMLAudioElement;
|
||||
#answer: HTMLAudioElement;
|
||||
#busy: HTMLAudioElement;
|
||||
#remote: HTMLAudioElement;
|
||||
#hungup: HTMLAudioElement;
|
||||
|
||||
constructor() {
|
||||
this.#ringing = new Audio(chrome.runtime.getURL("audios/ringing.mp3"));
|
||||
this.#ringing.loop = true;
|
||||
this.#ringing.volume = 0.8;
|
||||
this.#failed = new Audio(chrome.runtime.getURL("audios/failed.mp3"));
|
||||
this.#ringBack = new Audio(chrome.runtime.getURL("audios/us-ringback.mp3"));
|
||||
this.#ringBack.loop = true;
|
||||
this.#ringBack.volume = 0.8;
|
||||
this.#failed = new Audio(chrome.runtime.getURL("audios/call-failed.mp3"));
|
||||
this.#failed.volume = 0.3;
|
||||
this.#answer = new Audio(chrome.runtime.getURL("audios/failed.mp3"));
|
||||
this.#answer.volume = 0.3;
|
||||
this.#busy = new Audio(chrome.runtime.getURL("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.volume = 0.3;
|
||||
this.#remote = new Audio();
|
||||
}
|
||||
|
||||
@@ -28,20 +37,38 @@ export default class SipAudioElements {
|
||||
}
|
||||
}
|
||||
|
||||
playRingback(volume: number | undefined): void {
|
||||
if (volume) {
|
||||
this.#ringBack.volume = volume;
|
||||
}
|
||||
this.#ringBack.play();
|
||||
}
|
||||
|
||||
pauseRingback(): void {
|
||||
if (!this.#ringBack.paused) {
|
||||
this.#ringBack.pause();
|
||||
}
|
||||
}
|
||||
|
||||
playFailed(volume: number | undefined): void {
|
||||
this.pauseRinging();
|
||||
this.pauseRingback();
|
||||
if (volume) {
|
||||
this.#failed.volume = volume;
|
||||
}
|
||||
this.#failed.play();
|
||||
}
|
||||
|
||||
playRemotePartyHungup(volume: number | undefined): void {
|
||||
if (volume) {
|
||||
this.#hungup.volume = volume;
|
||||
}
|
||||
this.#hungup.play();
|
||||
}
|
||||
|
||||
playAnswer(volume: number | undefined): void {
|
||||
this.pauseRinging();
|
||||
// if (volume) {
|
||||
// this.#answer.volume = volume;
|
||||
// }
|
||||
// this.#answer.play();
|
||||
this.pauseRingback();
|
||||
}
|
||||
|
||||
isRemoteAudioPaused(): boolean {
|
||||
@@ -53,6 +80,11 @@ export default class SipAudioElements {
|
||||
this.#remote.play();
|
||||
}
|
||||
|
||||
stopAll() {
|
||||
this.pauseRinging();
|
||||
this.pauseRingback();
|
||||
}
|
||||
|
||||
isPLaying(audio: HTMLAudioElement) {
|
||||
return (
|
||||
audio.currentTime > 0 &&
|
||||
|
||||
@@ -57,6 +57,8 @@ export default class SipSession extends events.EventEmitter {
|
||||
});
|
||||
if (this.#audio.isRemoteAudioPaused() && !this.replaces) {
|
||||
this.#audio.playRinging(undefined);
|
||||
} else {
|
||||
this.#audio.playRingback(undefined);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -94,12 +96,16 @@ export default class SipSession extends events.EventEmitter {
|
||||
this.#audio.playFailed(undefined);
|
||||
} else {
|
||||
this.#audio.pauseRinging();
|
||||
this.#audio.pauseRingback();
|
||||
}
|
||||
});
|
||||
|
||||
this.#rtcSession.on("ended", (data: EndEvent): void => {
|
||||
const { originator, cause, message } = data;
|
||||
let description;
|
||||
if (originator === "remote") {
|
||||
this.#audio.playRemotePartyHungup(undefined);
|
||||
}
|
||||
if (message && originator === "remote" && message.hasHeader("Reason")) {
|
||||
const reason = Grammar.parse(message.getHeader("Reason"), "Reason");
|
||||
if (reason) {
|
||||
|
||||
@@ -58,3 +58,11 @@ export const isSipClientIdle = (callStatus: string) => {
|
||||
callStatus === SipConstants.SESSION_FAILED
|
||||
);
|
||||
};
|
||||
|
||||
export const normalizeUrl = (input: string): string => {
|
||||
// Extract the domain name
|
||||
const url = new URL(input.startsWith("http") ? input : `https://${input}`);
|
||||
|
||||
// Return the fully formed URL
|
||||
return `${url.protocol}//${url.hostname}/api/v1`;
|
||||
};
|
||||
|
||||
@@ -15,11 +15,15 @@ import Phone from "./phone";
|
||||
import Settings from "./settings";
|
||||
import { DEFAULT_COLOR_SCHEME } from "src/common/constants";
|
||||
import { useEffect, useState } from "react";
|
||||
import { getCallHistories, getSettings } from "src/storage";
|
||||
import {
|
||||
getAdvancedSettings,
|
||||
getCallHistories,
|
||||
getSettings,
|
||||
} from "src/storage";
|
||||
|
||||
import jambonz from "src/imgs/jambonz.svg";
|
||||
import CallHistories from "./history";
|
||||
import { CallHistory } from "src/common/types";
|
||||
import { AdvancedAppSettings, CallHistory } from "src/common/types";
|
||||
|
||||
export const WindowApp = () => {
|
||||
const [sipDomain, setSipDomain] = useState("");
|
||||
@@ -30,6 +34,9 @@ export const WindowApp = () => {
|
||||
const [callHistories, setCallHistories] = useState<CallHistory[]>([]);
|
||||
const [calledNumber, setCalledNumber] = useState("");
|
||||
const [tabIndex, setTabIndex] = useState(0);
|
||||
const [advancedSettings, setAdvancedSettings] = useState<AdvancedAppSettings>(
|
||||
getAdvancedSettings()
|
||||
);
|
||||
const tabsSettings = [
|
||||
{
|
||||
title: "Dialer",
|
||||
@@ -41,6 +48,7 @@ export const WindowApp = () => {
|
||||
sipDisplayName={sipDisplayName}
|
||||
sipServerAddress={sipServerAddress}
|
||||
calledNumber={[calledNumber, setCalledNumber]}
|
||||
advancedSettings={advancedSettings}
|
||||
/>
|
||||
),
|
||||
},
|
||||
@@ -75,6 +83,7 @@ export const WindowApp = () => {
|
||||
|
||||
const loadSettings = () => {
|
||||
const settings = getSettings();
|
||||
setAdvancedSettings(getAdvancedSettings());
|
||||
setSipDomain(settings.sipDomain);
|
||||
setSipServerAddress(settings.sipServerAddress);
|
||||
setSipUsername(settings.sipUsername);
|
||||
|
||||
@@ -24,12 +24,15 @@ import {
|
||||
Play,
|
||||
Users,
|
||||
} from "react-feather";
|
||||
import { Call, SipCallDirection, SipClientStatus } from "src/common/types";
|
||||
import {
|
||||
AdvancedAppSettings,
|
||||
SipCallDirection,
|
||||
SipClientStatus,
|
||||
} from "src/common/types";
|
||||
import { SipConstants, SipUA } from "src/lib";
|
||||
import IncommingCall from "./incomming-call";
|
||||
import DialPad from "./dial-pad";
|
||||
import {
|
||||
formatPhoneNumber,
|
||||
isSipClientAnswered,
|
||||
isSipClientIdle,
|
||||
isSipClientRinging,
|
||||
@@ -48,7 +51,6 @@ import {
|
||||
import { OutGoingCall } from "./outgoing-call";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import IconButtonMenu, { IconButtonMenuItems } from "src/components/menu";
|
||||
import { Application } from "src/api/types";
|
||||
import { getApplications, getQueues, getRegisteredUser } from "src/api";
|
||||
|
||||
type PhoneProbs = {
|
||||
@@ -58,6 +60,7 @@ type PhoneProbs = {
|
||||
sipPassword: string;
|
||||
sipDisplayName: string;
|
||||
calledNumber: [string, React.Dispatch<React.SetStateAction<string>>];
|
||||
advancedSettings: AdvancedAppSettings;
|
||||
};
|
||||
|
||||
export const Phone = ({
|
||||
@@ -67,8 +70,10 @@ export const Phone = ({
|
||||
sipPassword,
|
||||
sipDisplayName,
|
||||
calledNumber: [calledANumber, setCalledANumber],
|
||||
advancedSettings,
|
||||
}: PhoneProbs) => {
|
||||
const [inputNumber, setInputNumber] = useState("");
|
||||
const [appName, setAppName] = useState("");
|
||||
const inputNumberRef = useRef(inputNumber);
|
||||
const [status, setStatus] = useState<SipClientStatus>("offline");
|
||||
const [isConfigured, setIsConfigured] = useState(false);
|
||||
@@ -93,9 +98,12 @@ export const Phone = ({
|
||||
setIsConfigured(false);
|
||||
clientGoOffline();
|
||||
}
|
||||
}, [sipDomain, sipUsername, sipPassword, sipServerAddress, sipDisplayName]);
|
||||
|
||||
useEffect(() => {
|
||||
const advancedSettings = getAdvancedSettings();
|
||||
setIsAdvancedMode(!!advancedSettings.accountSid);
|
||||
}, [sipDomain, sipUsername, sipPassword, sipServerAddress, sipDisplayName]);
|
||||
}, [advancedSettings]);
|
||||
|
||||
useEffect(() => {
|
||||
inputNumberRef.current = inputNumber;
|
||||
@@ -400,7 +408,10 @@ export const Phone = ({
|
||||
decline={handleDecline}
|
||||
/>
|
||||
) : (
|
||||
<OutGoingCall number={inputNumber} cancelCall={handleDecline} />
|
||||
<OutGoingCall
|
||||
number={inputNumber || appName}
|
||||
cancelCall={handleDecline}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<VStack
|
||||
@@ -414,7 +425,7 @@ export const Phone = ({
|
||||
<IconButtonMenu
|
||||
icon={<Users />}
|
||||
tooltip="Call to user"
|
||||
onClick={(value) => {
|
||||
onClick={(_, value) => {
|
||||
setInputNumber(value);
|
||||
makeOutboundCall(value);
|
||||
}}
|
||||
@@ -443,9 +454,10 @@ export const Phone = ({
|
||||
<IconButtonMenu
|
||||
icon={<List />}
|
||||
tooltip="Call to a queue"
|
||||
onClick={(value) => {
|
||||
onClick={(name, value) => {
|
||||
setAppName(`Queue ${name}`);
|
||||
const calledQueue = `queue-${value}`;
|
||||
setInputNumber(calledQueue);
|
||||
setInputNumber("");
|
||||
makeOutboundCall(calledQueue);
|
||||
}}
|
||||
onOpen={() => {
|
||||
@@ -469,9 +481,10 @@ export const Phone = ({
|
||||
<IconButtonMenu
|
||||
icon={<GitMerge />}
|
||||
tooltip="Call to an application"
|
||||
onClick={(value) => {
|
||||
onClick={(name, value) => {
|
||||
setAppName(`App ${name}`);
|
||||
const calledAppId = `app-${value}`;
|
||||
setInputNumber(calledAppId);
|
||||
setInputNumber("");
|
||||
makeOutboundCall(calledAppId);
|
||||
}}
|
||||
onOpen={() => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
FormControl,
|
||||
FormLabel,
|
||||
HStack,
|
||||
Icon,
|
||||
Image,
|
||||
Input,
|
||||
Spacer,
|
||||
@@ -10,20 +11,27 @@ import {
|
||||
VStack,
|
||||
} from "@chakra-ui/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { CheckCircle, XCircle } from "react-feather";
|
||||
import { getApplications } from "src/api";
|
||||
import { AdvancedAppSettings } from "src/common/types";
|
||||
import PasswordInput from "src/components/password-input";
|
||||
import InfoIcon from "src/imgs/icons/Info.svg";
|
||||
import ResetIcon from "src/imgs/icons/Reset.svg";
|
||||
import { getAdvancedSettings, saveAddvancedSettings } from "src/storage";
|
||||
import { normalizeUrl } from "src/utils";
|
||||
|
||||
export const AdvancedSettings = () => {
|
||||
const [apiKey, setApiKey] = useState("");
|
||||
const [apiServer, setApiServer] = useState("");
|
||||
const [accountSid, setAccountSid] = useState("");
|
||||
const [isCredentialOk, setIsCredentialOk] = useState(false);
|
||||
const [isAdvancedMode, setIsAdvancedMode] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const settings = getAdvancedSettings();
|
||||
if (settings.apiServer) {
|
||||
setIsAdvancedMode(true);
|
||||
checkCredential();
|
||||
setApiServer(settings.apiServer);
|
||||
}
|
||||
if (settings.apiKey) {
|
||||
@@ -34,20 +42,35 @@ export const AdvancedSettings = () => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleSubmit = () => {
|
||||
const checkCredential = () => {
|
||||
getApplications()
|
||||
.then(() => {
|
||||
setIsCredentialOk(true);
|
||||
})
|
||||
.catch(() => {
|
||||
setIsCredentialOk(false);
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setApiServer(normalizeUrl(apiServer));
|
||||
const settings: AdvancedAppSettings = {
|
||||
accountSid,
|
||||
apiKey,
|
||||
apiServer,
|
||||
apiServer: normalizeUrl(apiServer),
|
||||
};
|
||||
|
||||
saveAddvancedSettings(settings);
|
||||
setIsAdvancedMode(true);
|
||||
checkCredential();
|
||||
};
|
||||
const resetSetting = () => {
|
||||
saveAddvancedSettings({} as AdvancedAppSettings);
|
||||
setApiKey("");
|
||||
setApiServer("");
|
||||
setAccountSid("");
|
||||
setIsAdvancedMode(false);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -85,6 +108,22 @@ export const AdvancedSettings = () => {
|
||||
<PasswordInput password={[apiKey, setApiKey]} isRequired />
|
||||
</FormControl>
|
||||
</VStack>
|
||||
{isAdvancedMode && (
|
||||
<HStack w="full" mt={2} mb={2}>
|
||||
<Icon
|
||||
as={isCredentialOk ? CheckCircle : XCircle}
|
||||
color={isCredentialOk ? "green.500" : "red.500"}
|
||||
boxSize={6}
|
||||
/>
|
||||
<Text
|
||||
fontSize="14px"
|
||||
color={isCredentialOk ? "green.500" : "red.500"}
|
||||
>
|
||||
Credential is {isCredentialOk ? "valid" : "invalid"}
|
||||
</Text>
|
||||
</HStack>
|
||||
)}
|
||||
|
||||
<Button colorScheme="jambonz" type="submit" w="full">
|
||||
Save
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user