mirror of
https://github.com/jambonz/chrome-extension-dialer.git
synced 2026-05-06 08:47:01 +00:00
use @jambonz/client-sdk-web and enable noiseIsolation feature
This commit is contained in:
Generated
+31
-24
@@ -14,12 +14,12 @@
|
||||
"@fortawesome/fontawesome-svg-core": "^6.5.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.5.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@jambonz/client-sdk-web": "^0.1.4",
|
||||
"buffer": "^6.0.3",
|
||||
"dayjs": "^1.11.10",
|
||||
"framer-motion": "^10.16.4",
|
||||
"fuse.js": "^6.6.2",
|
||||
"google-libphonenumber": "^3.2.33",
|
||||
"jssip": "^3.10.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"uuid": "^9.0.1",
|
||||
@@ -4038,6 +4038,32 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@jambonz/client-sdk-core": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@jambonz/client-sdk-core/-/client-sdk-core-0.1.4.tgz",
|
||||
"integrity": "sha512-D8IomTUJMboCGjqgFrun4OSZJffFP1Z4B6ggXe8HemcZTnownTJSaZpMyTzQyf9AL1C7vd+jb3LHXe1Sb4qtxw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jssip": "^3.10.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=17.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@jambonz/client-sdk-web": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@jambonz/client-sdk-web/-/client-sdk-web-0.1.4.tgz",
|
||||
"integrity": "sha512-vKM/WcXr1VCNg/RFiF+idMgYAmmntzf7c7xoR/q3ANdhB2NQxZvitbmUBFbgevblX+hIPwUeWCXRr9Ncw6zwnA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jambonz/client-sdk-core": "0.1.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@jest/console": {
|
||||
"version": "27.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@jest/console/-/console-27.5.1.tgz",
|
||||
@@ -5638,14 +5664,6 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/debug": {
|
||||
"version": "4.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.8.tgz",
|
||||
"integrity": "sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==",
|
||||
"dependencies": {
|
||||
"@types/ms": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/eslint": {
|
||||
"version": "8.44.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.2.tgz",
|
||||
@@ -5672,11 +5690,6 @@
|
||||
"integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/events": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz",
|
||||
"integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g=="
|
||||
},
|
||||
"node_modules/@types/express": {
|
||||
"version": "4.17.17",
|
||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz",
|
||||
@@ -5823,11 +5836,6 @@
|
||||
"integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/ms": {
|
||||
"version": "0.7.31",
|
||||
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz",
|
||||
"integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA=="
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "16.18.53",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.53.tgz",
|
||||
@@ -14938,12 +14946,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/jssip": {
|
||||
"version": "3.10.0",
|
||||
"resolved": "https://registry.npmjs.org/jssip/-/jssip-3.10.0.tgz",
|
||||
"integrity": "sha512-iJj+bhnNl0S296sUDc2ZjIbAetnelzZ92aWARyW01oKZ0X8t+5aGrYfJdMFliLFm8hxMcnkep3vmSRGe/yRjsA==",
|
||||
"version": "3.13.6",
|
||||
"resolved": "https://registry.npmjs.org/jssip/-/jssip-3.13.6.tgz",
|
||||
"integrity": "sha512-Bf1ndrSuqpO87/AG56WACR7kKcCvKOzaIQROu7JUMh0qFaGOV4NuR+wsnaXa7f3/d6xhwVczczFyt1ywJmTjPg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/debug": "^4.1.7",
|
||||
"@types/events": "^3.0.0",
|
||||
"debug": "^4.3.1",
|
||||
"events": "^3.3.0",
|
||||
"sdp-transform": "^2.14.1"
|
||||
|
||||
+1
-1
@@ -9,12 +9,12 @@
|
||||
"@fortawesome/fontawesome-svg-core": "^6.5.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.5.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@jambonz/client-sdk-web": "^0.1.4",
|
||||
"buffer": "^6.0.3",
|
||||
"dayjs": "^1.11.10",
|
||||
"framer-motion": "^10.16.4",
|
||||
"fuse.js": "^6.6.2",
|
||||
"google-libphonenumber": "^3.2.33",
|
||||
"jssip": "^3.10.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"uuid": "^9.0.1",
|
||||
|
||||
Binary file not shown.
Binary file not shown.
+3
-9
@@ -1,4 +1,5 @@
|
||||
import { ConferenceModes } from "src/api/types";
|
||||
import type { ClientState } from "@jambonz/client-sdk-web";
|
||||
|
||||
export interface LoginCredential {
|
||||
name?: string;
|
||||
@@ -97,12 +98,5 @@ export interface CallHistory {
|
||||
isSaved?: boolean;
|
||||
}
|
||||
|
||||
export type SipClientStatus =
|
||||
| "start"
|
||||
| "stop"
|
||||
| "connecting"
|
||||
| "connected"
|
||||
| "disconnected"
|
||||
| "registered"
|
||||
| "unregistered";
|
||||
export type SipCallDirection = "" | "outgoing" | "incoming";
|
||||
export type SipClientStatus = ClientState;
|
||||
export type SipCallDirection = "" | "outbound" | "inbound";
|
||||
|
||||
@@ -2,8 +2,6 @@ export default class SipAudioElements {
|
||||
#ringing: HTMLAudioElement;
|
||||
#ringBack: HTMLAudioElement;
|
||||
#failed: HTMLAudioElement;
|
||||
#busy: HTMLAudioElement;
|
||||
#remote: HTMLAudioElement;
|
||||
#hungup: HTMLAudioElement;
|
||||
#localHungup: HTMLAudioElement;
|
||||
|
||||
@@ -16,13 +14,10 @@ export default class SipAudioElements {
|
||||
this.#ringBack.volume = 0.8;
|
||||
this.#failed = this.getAudio("audios/call-failed.mp3");
|
||||
this.#failed.volume = 0.3;
|
||||
this.#busy = this.getAudio("audios/us-busy-signal.mp3");
|
||||
this.#busy.volume = 0.3;
|
||||
this.#hungup = this.getAudio("audios/remote-party-hungup-tone.mp3");
|
||||
this.#hungup.volume = 0.3;
|
||||
this.#localHungup = this.getAudio("audios/local-party-hungup-tone.mp3");
|
||||
this.#localHungup.volume = 0.3;
|
||||
this.#remote = new Audio();
|
||||
}
|
||||
|
||||
private getAudio(path: string) {
|
||||
@@ -43,19 +38,13 @@ export default class SipAudioElements {
|
||||
return new Audio(audioURL);
|
||||
}
|
||||
|
||||
playLocalHungup(volume: number | undefined) {
|
||||
playLocalHungup() {
|
||||
this.pauseRingback();
|
||||
this.pauseRinging();
|
||||
if (volume) {
|
||||
this.#localHungup.volume = volume;
|
||||
}
|
||||
this.#localHungup.play();
|
||||
}
|
||||
|
||||
playRinging(volume: number | undefined): void {
|
||||
if (volume) {
|
||||
this.#ringing.volume = volume;
|
||||
}
|
||||
playRinging(): void {
|
||||
this.#ringing.play();
|
||||
}
|
||||
|
||||
@@ -65,10 +54,7 @@ export default class SipAudioElements {
|
||||
}
|
||||
}
|
||||
|
||||
playRingback(volume: number | undefined): void {
|
||||
if (volume) {
|
||||
this.#ringBack.volume = volume;
|
||||
}
|
||||
playRingback(): void {
|
||||
this.#ringBack.play();
|
||||
}
|
||||
|
||||
@@ -78,47 +64,25 @@ export default class SipAudioElements {
|
||||
}
|
||||
}
|
||||
|
||||
playFailed(volume: number | undefined): void {
|
||||
playFailed(): 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;
|
||||
}
|
||||
playRemotePartyHungup(): void {
|
||||
this.#hungup.play();
|
||||
}
|
||||
|
||||
playAnswer(volume: number | undefined): void {
|
||||
playAnswer(): void {
|
||||
this.pauseRinging();
|
||||
this.pauseRingback();
|
||||
}
|
||||
|
||||
isRemoteAudioPaused(): boolean {
|
||||
return this.#remote.paused;
|
||||
}
|
||||
|
||||
playRemote(stream: MediaStream) {
|
||||
this.#remote.srcObject = stream;
|
||||
this.#remote.play();
|
||||
}
|
||||
|
||||
stopAll() {
|
||||
this.pauseRinging();
|
||||
this.pauseRingback();
|
||||
}
|
||||
|
||||
isPLaying(audio: HTMLAudioElement) {
|
||||
return (
|
||||
audio.currentTime > 0 &&
|
||||
!audio.paused &&
|
||||
!audio.ended &&
|
||||
audio.readyState > audio.HAVE_CURRENT_DATA
|
||||
);
|
||||
this.#ringing.currentTime = 0;
|
||||
this.#ringBack.currentTime = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,311 +0,0 @@
|
||||
import {
|
||||
EndEvent,
|
||||
HoldEvent,
|
||||
IceCandidateEvent,
|
||||
PeerConnectionEvent,
|
||||
ReferEvent,
|
||||
RTCPeerConnectionDeprecated,
|
||||
RTCSession,
|
||||
} from "jssip/lib/RTCSession";
|
||||
import { SipConstants, SipAudioElements, randomId } from "./index";
|
||||
import { DTMF_TRANSPORT } from "jssip/lib/Constants";
|
||||
import { IncomingResponse } from "jssip/lib/SIPMessage";
|
||||
import * as events from "events";
|
||||
import { C, Grammar } from "jssip";
|
||||
|
||||
export default class SipSession extends events.EventEmitter {
|
||||
#id: string;
|
||||
#rtcOptions: any;
|
||||
#audio: SipAudioElements;
|
||||
#rtcSession: RTCSession;
|
||||
#active: boolean;
|
||||
|
||||
constructor(
|
||||
rtcSession: RTCSession,
|
||||
rtcConfig: RTCConfiguration,
|
||||
audio: SipAudioElements
|
||||
) {
|
||||
super();
|
||||
this.setMaxListeners(Infinity);
|
||||
this.#id = randomId("");
|
||||
this.#rtcOptions = {
|
||||
mediaConstraints: { audio: true, video: false },
|
||||
pcConfig: rtcConfig,
|
||||
};
|
||||
this.#rtcSession = rtcSession;
|
||||
this.#audio = audio;
|
||||
this.#active = false;
|
||||
this.addListeners();
|
||||
}
|
||||
|
||||
addListeners(): void {
|
||||
if (this.#rtcSession.connection) {
|
||||
this.addPeerConnectionListener(this.#rtcSession.connection);
|
||||
} else {
|
||||
this.#rtcSession.on(
|
||||
"peerconnection",
|
||||
(data: PeerConnectionEvent): void => {
|
||||
let pc: RTCPeerConnectionDeprecated = data.peerconnection;
|
||||
this.addPeerConnectionListener(pc);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
this.#rtcSession.on("progress", (): void => {
|
||||
this.emit(SipConstants.SESSION_RINGING, {
|
||||
status: SipConstants.SESSION_RINGING,
|
||||
});
|
||||
if (this.#audio.isRemoteAudioPaused() && !this.replaces) {
|
||||
this.#audio.playRinging(undefined);
|
||||
} else {
|
||||
this.#audio.playRingback(undefined);
|
||||
}
|
||||
});
|
||||
|
||||
this.#rtcSession.on(
|
||||
"accepted",
|
||||
({ response }: { response: IncomingResponse }) => {
|
||||
this.emit(SipConstants.SESSION_ANSWERED, {
|
||||
status: SipConstants.SESSION_ANSWERED,
|
||||
callSid: response?.hasHeader("X-Call-Sid")
|
||||
? response.getHeader("X-Call-Sid")
|
||||
: null,
|
||||
});
|
||||
this.#audio.playAnswer(undefined);
|
||||
}
|
||||
);
|
||||
|
||||
this.#rtcSession.on("failed", (data: EndEvent): void => {
|
||||
let { originator, cause, message } = data;
|
||||
let description;
|
||||
if (
|
||||
message &&
|
||||
originator === "remote" &&
|
||||
message instanceof IncomingResponse &&
|
||||
message.status_code
|
||||
) {
|
||||
description = `${message.status_code}`.trim();
|
||||
}
|
||||
if (originator === "local" && cause === C.causes.CANCELED) {
|
||||
description = "Cancelled by user";
|
||||
}
|
||||
if (originator === "local" && cause === C.causes.REJECTED) {
|
||||
description = "Rejected by user";
|
||||
}
|
||||
this.emit(SipConstants.SESSION_FAILED, {
|
||||
cause: cause,
|
||||
status: SipConstants.SESSION_FAILED,
|
||||
originator: originator,
|
||||
description: description,
|
||||
});
|
||||
if (originator === "remote") {
|
||||
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);
|
||||
} else {
|
||||
this.#audio.playLocalHungup(undefined);
|
||||
}
|
||||
if (message && originator === "remote" && message.hasHeader("Reason")) {
|
||||
const reason = Grammar.parse(message.getHeader("Reason"), "Reason");
|
||||
if (reason) {
|
||||
description = `${reason.cause}`.trim();
|
||||
}
|
||||
}
|
||||
this.emit(SipConstants.SESSION_ENDED, {
|
||||
cause: cause,
|
||||
status: SipConstants.SESSION_ENDED,
|
||||
originator: originator,
|
||||
description: description,
|
||||
});
|
||||
});
|
||||
|
||||
this.#rtcSession.on("muted", (): void => {
|
||||
this.emit(SipConstants.SESSION_MUTED, {
|
||||
status: "muted",
|
||||
});
|
||||
});
|
||||
|
||||
this.#rtcSession.on("unmuted", (): void => {
|
||||
this.emit(SipConstants.SESSION_MUTED, {
|
||||
status: "unmuted",
|
||||
});
|
||||
});
|
||||
|
||||
this.#rtcSession.on("hold", (data: HoldEvent): void => {
|
||||
this.emit(SipConstants.SESSION_HOLD, {
|
||||
status: "hold",
|
||||
originator: data.originator,
|
||||
});
|
||||
});
|
||||
|
||||
this.#rtcSession.on("unhold", (data: HoldEvent): void => {
|
||||
this.emit(SipConstants.SESSION_HOLD, {
|
||||
status: "unhold",
|
||||
originator: data.originator,
|
||||
});
|
||||
});
|
||||
|
||||
this.#rtcSession.on("refer", (data: ReferEvent): void => {
|
||||
let { accept } = data;
|
||||
accept((rtcSession: RTCSession): void => {
|
||||
rtcSession.data.replaces = true;
|
||||
this.emit(SipConstants.SESSION_REFER, {
|
||||
session: rtcSession,
|
||||
type: "refer",
|
||||
});
|
||||
}, this.#rtcOptions);
|
||||
});
|
||||
|
||||
this.#rtcSession.on("replaces", (data: ReferEvent): void => {
|
||||
data.accept((rtcSession: RTCSession): void => {
|
||||
rtcSession.data.replaces = true;
|
||||
if (!rtcSession.isEstablished()) {
|
||||
rtcSession.answer(this.#rtcOptions);
|
||||
this.emit(SipConstants.SESSION_REPLACES, {
|
||||
session: rtcSession,
|
||||
type: "replaces",
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.#rtcSession.on("icecandidate", (evt: IceCandidateEvent): void => {
|
||||
let type: string[] = evt.candidate.candidate.split(" ");
|
||||
let candidate: string = type[7];
|
||||
if (["srflx", "relay"].indexOf(candidate) > -1) {
|
||||
evt.ready();
|
||||
this.emit(SipConstants.SESSION_ICE_READY, {
|
||||
candidate: candidate,
|
||||
status: "ready",
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addPeerConnectionListener(pc: RTCPeerConnection): void {
|
||||
pc.addEventListener("addstream", (event: any): void => {
|
||||
if (this.#rtcSession.direction === "outgoing") {
|
||||
this.#audio.pauseRinging();
|
||||
}
|
||||
this.#audio.playRemote(event.stream);
|
||||
this.emit(SipConstants.SESSION_ADD_STREAM, {
|
||||
direction: this.#rtcSession.direction,
|
||||
});
|
||||
});
|
||||
// pc.addEventListener(
|
||||
// "track",
|
||||
// (event: RTCPeerConnectionEventMap["track"]): void => {
|
||||
// const stream: MediaStream = new MediaStream([event.track]);
|
||||
// if (this.#rtcSession.direction === "outgoing") {
|
||||
// this.#audio.pauseRinging();
|
||||
// }
|
||||
// this.#audio.playRemote(stream);
|
||||
// this.emit(SipConstants.SESSION_TRACK, {
|
||||
// direction: this.#rtcSession.direction,
|
||||
// });
|
||||
// }
|
||||
// );
|
||||
}
|
||||
|
||||
get rtcSession() {
|
||||
return this.#rtcSession;
|
||||
}
|
||||
|
||||
get direction() {
|
||||
return this.#rtcSession.direction;
|
||||
}
|
||||
|
||||
get id() {
|
||||
return this.#id;
|
||||
}
|
||||
|
||||
get user() {
|
||||
return this.#rtcSession.remote_identity.uri.user;
|
||||
}
|
||||
|
||||
get active() {
|
||||
return this.#active;
|
||||
}
|
||||
|
||||
get answerTime(): Date {
|
||||
return this.#rtcSession.start_time;
|
||||
}
|
||||
|
||||
get duration() {
|
||||
if (!this.answerTime) {
|
||||
return 0;
|
||||
}
|
||||
let now: number = new Date().getUTCMilliseconds();
|
||||
return Math.floor((now - this.answerTime.getUTCMilliseconds()) / 1000);
|
||||
}
|
||||
|
||||
get replaces() {
|
||||
return Boolean(this.#rtcSession.data.replaces);
|
||||
}
|
||||
|
||||
setActive(flag: boolean): void {
|
||||
let wasActive: boolean = this.#active;
|
||||
this.#active = flag;
|
||||
if (this.#rtcSession.isEstablished()) {
|
||||
if (this.replaces) {
|
||||
return;
|
||||
}
|
||||
if (this.#active) {
|
||||
this.unhold();
|
||||
} else {
|
||||
this.hold();
|
||||
}
|
||||
}
|
||||
if (this.#active && !wasActive) {
|
||||
this.emit(SipConstants.SESSION_ACTIVE);
|
||||
}
|
||||
}
|
||||
|
||||
answer() {
|
||||
this.#rtcSession.answer(this.#rtcOptions);
|
||||
}
|
||||
|
||||
terminate(sipCode: number, sipReason: string): void {
|
||||
this.#rtcSession.terminate({
|
||||
status_code: sipCode,
|
||||
reason_phrase: sipReason,
|
||||
});
|
||||
}
|
||||
|
||||
isMuted(): boolean {
|
||||
return this.#rtcSession.isMuted().audio?.valueOf() || false;
|
||||
}
|
||||
|
||||
mute(): void {
|
||||
this.#rtcSession.mute({ audio: true, video: true });
|
||||
}
|
||||
|
||||
unmute(): void {
|
||||
this.#rtcSession.unmute({ audio: true, video: true });
|
||||
}
|
||||
|
||||
isHolded(): boolean {
|
||||
return this.#rtcSession.isOnHold().local.valueOf() || false;
|
||||
}
|
||||
|
||||
hold(): void {
|
||||
this.#rtcSession.hold();
|
||||
}
|
||||
|
||||
unhold(): void {
|
||||
this.#rtcSession.unhold();
|
||||
}
|
||||
|
||||
sendDtmf(tone: number | string): void {
|
||||
this.#rtcSession.sendDTMF(tone, { transportType: DTMF_TRANSPORT.RFC2833 });
|
||||
}
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
import { SipSession, SipModel, SipConstants } from "./index";
|
||||
|
||||
export default class SipSessionManager {
|
||||
#sessions: Map<string, SipModel.SipSessionState>;
|
||||
|
||||
constructor() {
|
||||
this.#sessions = new Map();
|
||||
}
|
||||
|
||||
activate(session: SipSession) {
|
||||
this.#sessions.forEach((v, k) => {
|
||||
if (k !== session.id) {
|
||||
v.active = false;
|
||||
session.setActive(false);
|
||||
} else {
|
||||
v.active = true;
|
||||
session.setActive(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateSession(field: string, session: SipSession, args: any): void {
|
||||
const state: SipModel.SipSessionState = this.getSessionState(session.id);
|
||||
if (state) {
|
||||
switch (field) {
|
||||
case SipConstants.SESSION_RINGING:
|
||||
state.status = args.status;
|
||||
break;
|
||||
case SipConstants.SESSION_ANSWERED:
|
||||
state.status = args.status;
|
||||
break;
|
||||
case SipConstants.SESSION_FAILED:
|
||||
case SipConstants.SESSION_ENDED:
|
||||
state.status = args.status;
|
||||
state.endState = {
|
||||
cause: args.cause,
|
||||
status: args.status,
|
||||
originator: args.endState,
|
||||
description: args.description,
|
||||
};
|
||||
this.#sessions.delete(session.id);
|
||||
break;
|
||||
case SipConstants.SESSION_MUTED:
|
||||
state.muteStatus = args.status;
|
||||
break;
|
||||
case SipConstants.SESSION_HOLD:
|
||||
state.holdState = {
|
||||
originator: args.originator,
|
||||
status: args.status,
|
||||
};
|
||||
break;
|
||||
case SipConstants.SESSION_ICE_READY:
|
||||
state.iceReady = true;
|
||||
break;
|
||||
case SipConstants.SESSION_ACTIVE:
|
||||
state.active = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getSessionState(id: string): SipModel.SipSessionState {
|
||||
const state = this.#sessions.get(id);
|
||||
if (!state) {
|
||||
throw new Error("Session not found");
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
getSession(id: string): SipSession {
|
||||
return this.getSessionState(id).sipSession;
|
||||
}
|
||||
|
||||
newSession(session: SipSession): void {
|
||||
this.#sessions.set(session.id, {
|
||||
id: session.id,
|
||||
sipSession: session,
|
||||
startDateTime: new Date(),
|
||||
active: true,
|
||||
status: "init",
|
||||
});
|
||||
}
|
||||
|
||||
get activeSession(): SipSession {
|
||||
if (this.#sessions.size === 0) {
|
||||
throw new Error("No sessions");
|
||||
}
|
||||
|
||||
const state = [...this.#sessions.values()].filter((s) => s.active);
|
||||
if (state.length) {
|
||||
return state[0].sipSession;
|
||||
} else {
|
||||
throw new Error("No Active sessions");
|
||||
}
|
||||
}
|
||||
|
||||
get count() {
|
||||
return this.#sessions.size;
|
||||
}
|
||||
}
|
||||
@@ -1,219 +0,0 @@
|
||||
import * as events from "events";
|
||||
import { UA, WebSocketInterface, debug } from "jssip";
|
||||
import { DisconnectEvent } from "jssip/lib/WebSocketInterface";
|
||||
import { RTCSession } from "jssip/lib/RTCSession";
|
||||
import {
|
||||
ConnectedEvent,
|
||||
IncomingRTCSessionEvent,
|
||||
RegisteredEvent,
|
||||
UAConnectingEvent,
|
||||
UnRegisteredEvent,
|
||||
} from "jssip/lib/UA";
|
||||
import {
|
||||
SessionManager,
|
||||
SipAudioElements,
|
||||
SipSession,
|
||||
SipConstants,
|
||||
SipModel,
|
||||
normalizeNumber,
|
||||
} from "./index";
|
||||
|
||||
export default class SipUA extends events.EventEmitter {
|
||||
#ua: UA;
|
||||
#rtcConfig: RTCConfiguration;
|
||||
#sessionManager: SessionManager;
|
||||
|
||||
constructor(client: SipModel.ClientAuth, settings: SipModel.ClientOptions) {
|
||||
super();
|
||||
debug.enable("JsSIP:*");
|
||||
this.#sessionManager = new SessionManager();
|
||||
this.#rtcConfig = settings.pcConfig;
|
||||
this.#ua = new UA({
|
||||
uri: `sip:${client.username}`,
|
||||
password: client.password,
|
||||
display_name: client.name,
|
||||
sockets: [new WebSocketInterface(settings.wsUri)],
|
||||
register: settings.register,
|
||||
register_expires: 600,
|
||||
});
|
||||
this.#ua.on("connecting", (data: UAConnectingEvent) =>
|
||||
this.emit(SipConstants.UA_CONNECTING, { ...data, client })
|
||||
);
|
||||
this.#ua.on("connected", (data: ConnectedEvent) =>
|
||||
this.emit(SipConstants.UA_CONNECTED, { ...data, client })
|
||||
);
|
||||
this.#ua.on("disconnected", (data: DisconnectEvent) =>
|
||||
this.emit(SipConstants.UA_DISCONNECTED, {
|
||||
...data,
|
||||
client,
|
||||
})
|
||||
);
|
||||
this.#ua.on("registered", (data: RegisteredEvent) =>
|
||||
this.emit(SipConstants.UA_REGISTERED, { ...data, client })
|
||||
);
|
||||
this.#ua.on("unregistered", (data: UnRegisteredEvent) =>
|
||||
this.emit(SipConstants.UA_UNREGISTERED, {
|
||||
...data,
|
||||
client,
|
||||
})
|
||||
);
|
||||
this.#ua.on("registrationFailed", (data: UnRegisteredEvent) =>
|
||||
this.emit(SipConstants.UA_UNREGISTERED, {
|
||||
...data,
|
||||
client,
|
||||
})
|
||||
);
|
||||
this.#ua.on("newRTCSession", (data: IncomingRTCSessionEvent) => {
|
||||
const rtcSession: RTCSession = data.session;
|
||||
const session: SipSession = new SipSession(
|
||||
rtcSession,
|
||||
this.#rtcConfig,
|
||||
new SipAudioElements()
|
||||
);
|
||||
this.#sessionManager.newSession(session);
|
||||
session.on(SipConstants.SESSION_RINGING, (args) =>
|
||||
this.updateSession(SipConstants.SESSION_RINGING, session, args, client)
|
||||
);
|
||||
session.on(SipConstants.SESSION_ANSWERED, (args) =>
|
||||
this.updateSession(SipConstants.SESSION_ANSWERED, session, args, client)
|
||||
);
|
||||
session.on(SipConstants.SESSION_FAILED, (args) =>
|
||||
this.updateSession(SipConstants.SESSION_FAILED, session, args, client)
|
||||
);
|
||||
session.on(SipConstants.SESSION_ENDED, (args) =>
|
||||
this.updateSession(SipConstants.SESSION_ENDED, session, args, client)
|
||||
);
|
||||
session.on(SipConstants.SESSION_MUTED, (args) =>
|
||||
this.updateSession(SipConstants.SESSION_MUTED, session, args, client)
|
||||
);
|
||||
session.on(SipConstants.SESSION_HOLD, (args) =>
|
||||
this.updateSession(SipConstants.SESSION_HOLD, session, args, client)
|
||||
);
|
||||
session.on(SipConstants.SESSION_UNHOLD, (args) =>
|
||||
this.updateSession(SipConstants.SESSION_UNHOLD, session, args, client)
|
||||
);
|
||||
session.on(SipConstants.SESSION_ICE_READY, (args) =>
|
||||
this.updateSession(
|
||||
SipConstants.SESSION_ICE_READY,
|
||||
session,
|
||||
args,
|
||||
client
|
||||
)
|
||||
);
|
||||
session.on(SipConstants.SESSION_ACTIVE, (args) => {
|
||||
this.updateSession(SipConstants.SESSION_ACTIVE, session, args, client);
|
||||
});
|
||||
session.setActive(true);
|
||||
});
|
||||
}
|
||||
|
||||
updateSession(
|
||||
field: string,
|
||||
session: SipSession,
|
||||
args: any,
|
||||
client: SipModel.ClientAuth
|
||||
) {
|
||||
this.emit(field, { ...args, client, session });
|
||||
this.#sessionManager.updateSession(field, session, args);
|
||||
}
|
||||
|
||||
start(): void {
|
||||
this.#ua.start();
|
||||
this.emit(SipConstants.UA_START);
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
this.#ua.stop();
|
||||
this.emit(SipConstants.UA_STOP);
|
||||
}
|
||||
|
||||
call(number: string, customHeaders: string[] = []): void {
|
||||
let normalizedNumber: string = normalizeNumber(number);
|
||||
this.#ua.call(normalizedNumber, {
|
||||
extraHeaders: [`X-Original-Number:${number}`].concat(customHeaders),
|
||||
mediaConstraints: { audio: true, video: false },
|
||||
pcConfig: this.#rtcConfig,
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
} else {
|
||||
this.#sessionManager.activeSession.mute();
|
||||
}
|
||||
}
|
||||
|
||||
unmute(id: string | undefined): void {
|
||||
if (id) {
|
||||
this.#sessionManager.getSession(id).unmute();
|
||||
} else {
|
||||
this.#sessionManager.activeSession.unmute();
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
} else {
|
||||
this.#sessionManager.activeSession.hold();
|
||||
}
|
||||
}
|
||||
|
||||
unhold(id: string | undefined): void {
|
||||
if (id) {
|
||||
this.#sessionManager.getSession(id).unhold();
|
||||
} else {
|
||||
this.#sessionManager.activeSession.unhold();
|
||||
}
|
||||
}
|
||||
|
||||
dtmf(tone: number | string, id: string | undefined): void {
|
||||
if (id) {
|
||||
this.#sessionManager.getSession(id).sendDtmf(tone);
|
||||
} else {
|
||||
this.#sessionManager.activeSession.sendDtmf(tone);
|
||||
}
|
||||
}
|
||||
|
||||
terminate(sipCode: number, reason: string, id: string | undefined): void {
|
||||
if (id) {
|
||||
this.#sessionManager.getSession(id).terminate(sipCode, reason);
|
||||
} else {
|
||||
this.#sessionManager.activeSession.terminate(sipCode, reason);
|
||||
}
|
||||
}
|
||||
|
||||
answer(id: string | undefined): void {
|
||||
if (id) {
|
||||
this.#sessionManager.getSession(id).answer();
|
||||
} else {
|
||||
this.#sessionManager.activeSession.answer();
|
||||
}
|
||||
}
|
||||
|
||||
activate(id: string) {
|
||||
const session: SipSession = this.#sessionManager.getSession(id);
|
||||
session.setActive(true);
|
||||
}
|
||||
|
||||
isConnected() {
|
||||
return this.#ua.isConnected();
|
||||
}
|
||||
}
|
||||
+1
-16
@@ -1,18 +1,3 @@
|
||||
import SipSession from "./SipSession";
|
||||
import SessionManager from "./SipSessionManager";
|
||||
import SipAudioElements from "./SipAudioElements";
|
||||
import SipUA from "./SipUA";
|
||||
import {normalizeNumber, randomId} from "./sip-utils";
|
||||
import * as SipModel from "./sip-models";
|
||||
import * as SipConstants from "./sip-constants";
|
||||
|
||||
export {
|
||||
SipUA,
|
||||
SipSession,
|
||||
SessionManager,
|
||||
SipAudioElements,
|
||||
randomId,
|
||||
normalizeNumber,
|
||||
SipConstants,
|
||||
SipModel
|
||||
}
|
||||
export { SipAudioElements };
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
export const SESSION_RINGING: string = "ringing";
|
||||
export const SESSION_ANSWERED: string = "answered";
|
||||
export const SESSION_FAILED: string = "failed";
|
||||
export const SESSION_ENDED: string = "ended";
|
||||
export const SESSION_MUTED: string = "muted";
|
||||
export const SESSION_HOLD: string = "hold";
|
||||
export const SESSION_UNHOLD: string = "unhold";
|
||||
export const SESSION_REFER: string = "refer";
|
||||
export const SESSION_REPLACES: string = "replaces";
|
||||
export const SESSION_ICE_READY: string = "ice-ready";
|
||||
export const SESSION_ADD_STREAM: string = "add-stream";
|
||||
export const SESSION_TRACK: string = "track";
|
||||
export const SESSION_ACTIVE: string = "active";
|
||||
export const UA_CONNECTING: string = "connecting";
|
||||
export const UA_CONNECTED: string = "connected";
|
||||
export const UA_DISCONNECTED: string = "disconnected";
|
||||
export const UA_REGISTERED: string = "registered";
|
||||
export const UA_UNREGISTERED: string = "unregistered";
|
||||
export const UA_START: string = "start";
|
||||
export const UA_STOP: string = "stop";
|
||||
@@ -1,45 +0,0 @@
|
||||
import SipSession from "./SipSession";
|
||||
|
||||
export interface SipSessionState {
|
||||
id: string;
|
||||
sipSession: SipSession;
|
||||
startDateTime: Date;
|
||||
active: boolean;
|
||||
status: string;
|
||||
muteStatus?: string;
|
||||
iceReady?: boolean;
|
||||
endState?: EndState;
|
||||
holdState?: HoldState;
|
||||
}
|
||||
|
||||
export interface EndState {
|
||||
cause: string;
|
||||
status: string;
|
||||
originator: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface HoldState {
|
||||
status: string;
|
||||
originator: string;
|
||||
}
|
||||
|
||||
export interface ClientAuth {
|
||||
username: string;
|
||||
password: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface ClientOptions {
|
||||
pcConfig: PeerConnectionConfig;
|
||||
wsUri: string;
|
||||
register: boolean;
|
||||
}
|
||||
|
||||
export interface PeerConnectionConfig {
|
||||
iceServers: IceServer[];
|
||||
}
|
||||
|
||||
export interface IceServer {
|
||||
urls: string[];
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
function normalizeNumber(number: string): string {
|
||||
if (/^(sips?|tel):/i.test(number)) {
|
||||
return number;
|
||||
} else if (/@/i.test(number)) {
|
||||
return number;
|
||||
} else if (
|
||||
number.startsWith("app-") ||
|
||||
number.startsWith("queue-") ||
|
||||
number.startsWith("conference-")
|
||||
) {
|
||||
return number;
|
||||
} else {
|
||||
return number.replace(/[()\-. ]*/g, "");
|
||||
}
|
||||
}
|
||||
|
||||
function randomId(prefix: string): string {
|
||||
const id: string = [...Array(16)]
|
||||
.map(() => Math.floor(Math.random() * 16).toString(16))
|
||||
.join("");
|
||||
if (prefix) {
|
||||
return `${prefix}-${id}`;
|
||||
} else {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
export { normalizeNumber, randomId };
|
||||
+7
-10
@@ -1,4 +1,4 @@
|
||||
import { SipConstants } from "./../lib";
|
||||
import { CallState } from "@jambonz/client-sdk-web";
|
||||
import { deleteWindowIdKey, getWindowIdKey, saveWindowIdKey } from "./storage";
|
||||
import { PhoneNumberFormat, PhoneNumberUtil } from "google-libphonenumber";
|
||||
|
||||
@@ -44,19 +44,16 @@ const initiateNewPhonePopup = (callback: (v: unknown) => void) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const isSipClientRinging = (callStatus: string) => {
|
||||
return callStatus === SipConstants.SESSION_RINGING;
|
||||
export const isSipClientRinging = (callState: CallState | null) => {
|
||||
return callState === CallState.Ringing || callState === CallState.Connecting;
|
||||
};
|
||||
|
||||
export const isSipClientAnswered = (callStatus: string) => {
|
||||
return callStatus === SipConstants.SESSION_ANSWERED;
|
||||
export const isSipClientAnswered = (callState: CallState | null) => {
|
||||
return callState === CallState.Connected;
|
||||
};
|
||||
|
||||
export const isSipClientIdle = (callStatus: string) => {
|
||||
return (
|
||||
callStatus === SipConstants.SESSION_ENDED ||
|
||||
callStatus === SipConstants.SESSION_FAILED
|
||||
);
|
||||
export const isSipClientIdle = (callState: CallState | null) => {
|
||||
return callState === null || callState === CallState.Idle || callState === CallState.Ended;
|
||||
};
|
||||
|
||||
export const normalizeUrl = (input: string): string => {
|
||||
|
||||
+8
-15
@@ -10,11 +10,12 @@ import {
|
||||
import Phone from "./phone";
|
||||
import Settings from "./settings";
|
||||
import { DEFAULT_COLOR_SCHEME } from "src/common/constants";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { getActiveSettings, getCallHistories, getSettings } from "src/storage";
|
||||
|
||||
import CallHistories from "./history";
|
||||
import { CallHistory, IAppSettings, SipClientStatus } from "src/common/types";
|
||||
import { ClientState } from "@jambonz/client-sdk-web";
|
||||
import Footer from "./footer/footer";
|
||||
|
||||
export const WindowApp = () => {
|
||||
@@ -27,33 +28,25 @@ export const WindowApp = () => {
|
||||
const [calledNumber, setCalledNumber] = useState("");
|
||||
const [calledName, setCalledName] = useState("");
|
||||
const [tabIndex, setTabIndex] = useState(0);
|
||||
const [status, setStatus] = useState<SipClientStatus>("stop");
|
||||
const [status, setStatus] = useState<SipClientStatus>(ClientState.Disconnected);
|
||||
const [allSettings, setAllSettings] = useState<IAppSettings[]>([]);
|
||||
const [advancedSettings, setAdvancedSettings] = useState<IAppSettings | null>(
|
||||
null
|
||||
);
|
||||
const [isSwitchingUserStatus, setIsSwitchingUserStatus] = useState(false);
|
||||
const [isOnline, setIsOnline] = useState(false);
|
||||
const phoneSipAschildRef = useRef<{
|
||||
updateGoOffline: (x: string) => void;
|
||||
} | null>(null);
|
||||
const [isUserOffline, setIsUserOffline] = useState(false);
|
||||
|
||||
const handleGoOffline = (s: SipClientStatus) => {
|
||||
if (s === status) {
|
||||
return;
|
||||
}
|
||||
if (phoneSipAschildRef.current) {
|
||||
if (s === "unregistered") {
|
||||
phoneSipAschildRef.current.updateGoOffline("stop");
|
||||
if (s === ClientState.Unregistered) {
|
||||
setIsUserOffline(true);
|
||||
} else {
|
||||
phoneSipAschildRef.current.updateGoOffline("start");
|
||||
}
|
||||
setIsUserOffline(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loadSettings = () => {
|
||||
const settings = getSettings();
|
||||
|
||||
const activeSettings = settings.find((el) => el.active);
|
||||
|
||||
setAllSettings(getSettings());
|
||||
@@ -70,7 +63,6 @@ export const WindowApp = () => {
|
||||
title: "Dialer",
|
||||
content: (
|
||||
<Phone
|
||||
ref={phoneSipAschildRef}
|
||||
sipUsername={sipUsername}
|
||||
sipPassword={sipPassword}
|
||||
sipDomain={sipDomain}
|
||||
@@ -84,6 +76,7 @@ export const WindowApp = () => {
|
||||
reload={loadSettings}
|
||||
setIsSwitchingUserStatus={setIsSwitchingUserStatus}
|
||||
setIsOnline={setIsOnline}
|
||||
isUserOffline={isUserOffline}
|
||||
/>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -2,6 +2,7 @@ import { HStack, Image, Text } from "@chakra-ui/react";
|
||||
import jambonz from "src/imgs/jambonz.svg";
|
||||
import { Dispatch, SetStateAction, useEffect, useState } from "react";
|
||||
import { SipClientStatus } from "src/common/types";
|
||||
import { ClientState } from "@jambonz/client-sdk-web";
|
||||
import JambonzSwitch from "src/components/switch";
|
||||
import "./styles.scss";
|
||||
|
||||
@@ -35,9 +36,12 @@ function Footer({
|
||||
const [isConfigured, setIsConfigured] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (status === "registered" || status === "disconnected") {
|
||||
if (
|
||||
status === ClientState.Registered ||
|
||||
status === ClientState.Disconnected
|
||||
) {
|
||||
setIsSwitchingUserStatus(false);
|
||||
setIsOnline(status === "registered");
|
||||
setIsOnline(status === ClientState.Registered);
|
||||
}
|
||||
}, [status, setIsSwitchingUserStatus, setIsOnline]);
|
||||
|
||||
@@ -63,7 +67,9 @@ function Footer({
|
||||
checked={[isOnline, setIsOnline]}
|
||||
onChange={(v) => {
|
||||
setIsSwitchingUserStatus(true);
|
||||
onHandleGoOffline(v ? "registered" : "unregistered");
|
||||
onHandleGoOffline(
|
||||
v ? ClientState.Registered : ClientState.Unregistered
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Text>You are {isOnline ? "online" : "offline"}</Text>
|
||||
|
||||
@@ -35,9 +35,9 @@ export const CallHistoryItem = ({
|
||||
}: CallHistoryItemProbs) => {
|
||||
const [callEnable, setCallEnable] = useState(false);
|
||||
const getDirectionIcon = (direction: SipCallDirection) => {
|
||||
if (direction === "outgoing") {
|
||||
if (direction === "outbound") {
|
||||
return faArrowRightFromBracket;
|
||||
} else if (direction === "incoming") {
|
||||
} else if (direction === "inbound") {
|
||||
return faArrowRightToBracket;
|
||||
} else {
|
||||
return faPhone;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Text, UnorderedList, VStack } from "@chakra-ui/react";
|
||||
import CallHistoryItem from "./call-history-item";
|
||||
import { CallHistory } from "src/common/types";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import Fuse from "fuse.js";
|
||||
|
||||
type RecentsProbs = {
|
||||
@@ -19,27 +19,22 @@ export const Recents = ({
|
||||
onDataChange,
|
||||
onCallNumber,
|
||||
}: RecentsProbs) => {
|
||||
const [callHistories, setCallHistories] = useState<CallHistory[]>(calls);
|
||||
const baseCalls = useMemo(
|
||||
() => (isSaved ? calls.filter((c) => c.isSaved === true) : calls),
|
||||
[calls, isSaved]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const fuseInstance = useMemo(
|
||||
() => new Fuse(baseCalls, { keys: ["number"] }),
|
||||
[baseCalls]
|
||||
);
|
||||
|
||||
const callHistories = useMemo(() => {
|
||||
if (search) {
|
||||
setCallHistories((prev) =>
|
||||
new Fuse(prev, {
|
||||
keys: ["number"],
|
||||
})
|
||||
.search(search)
|
||||
.map(({ item }) => item)
|
||||
);
|
||||
} else {
|
||||
setCallHistories(
|
||||
isSaved ? calls.filter((c) => c.isSaved === true) : calls
|
||||
);
|
||||
return fuseInstance.search(search).map(({ item }) => item);
|
||||
}
|
||||
}, [search]);
|
||||
|
||||
useEffect(() => {
|
||||
setCallHistories(isSaved ? calls.filter((c) => c.isSaved === true) : calls);
|
||||
}, [calls]);
|
||||
return baseCalls;
|
||||
}, [search, fuseInstance, baseCalls]);
|
||||
|
||||
return (
|
||||
<VStack spacing={2}>
|
||||
|
||||
@@ -2,8 +2,14 @@ export default class DialPadAudioElements {
|
||||
private keySounds: { [key: string]: HTMLAudioElement | undefined } = {};
|
||||
|
||||
constructor() {
|
||||
const arr = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "*", "#"];
|
||||
for (const i of arr) {
|
||||
const keys = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "*", "#"];
|
||||
const fileMap: Record<string, string> = {
|
||||
"*": "star",
|
||||
"#": "hash",
|
||||
};
|
||||
|
||||
for (const key of keys) {
|
||||
const fileName = fileMap[key] || key;
|
||||
let audioURL;
|
||||
|
||||
// Check if we're in a Chrome extension
|
||||
@@ -12,16 +18,14 @@ export default class DialPadAudioElements {
|
||||
chrome.runtime &&
|
||||
chrome.runtime.getURL
|
||||
) {
|
||||
audioURL = chrome.runtime.getURL(
|
||||
`audios/dtmf-${encodeURIComponent(i)}.mp3`
|
||||
);
|
||||
audioURL = chrome.runtime.getURL(`audios/dtmf-${fileName}.mp3`);
|
||||
} else {
|
||||
// We're in a web context, adjust this path as necessary
|
||||
audioURL = `/audios/dtmf-${encodeURIComponent(i)}.mp3`;
|
||||
audioURL = `/audios/dtmf-${fileName}.mp3`;
|
||||
}
|
||||
|
||||
this.keySounds[i] = new Audio(audioURL);
|
||||
const audio = this.keySounds[i];
|
||||
this.keySounds[key] = new Audio(audioURL);
|
||||
const audio = this.keySounds[key];
|
||||
if (audio) {
|
||||
audio.volume = 0.5;
|
||||
}
|
||||
|
||||
@@ -16,12 +16,12 @@ import { updateConferenceParticipantAction } from "src/api";
|
||||
import { ConferenceModes } from "src/api/types";
|
||||
import { DEFAULT_TOAST_DURATION } from "src/common/constants";
|
||||
import OutlineBox from "src/components/outline-box";
|
||||
import { SipConstants } from "src/lib";
|
||||
import {
|
||||
deleteConferenceSettings,
|
||||
getConferenceSettings,
|
||||
saveConferenceSettings,
|
||||
} from "src/storage";
|
||||
import { CallState } from "@jambonz/client-sdk-web";
|
||||
|
||||
type JoinConferenceProbs = {
|
||||
conferenceId?: string;
|
||||
@@ -60,9 +60,21 @@ export const JoinConference = ({
|
||||
);
|
||||
const [participantState, setParticipantState] = useState("Join as");
|
||||
|
||||
const updateSettings = (updates: Partial<{ mode: ConferenceModes; speakOnlyTo: string; tags: string }>) => {
|
||||
const next = {
|
||||
mode: updates.mode ?? mode,
|
||||
speakOnlyTo: updates.speakOnlyTo ?? speakOnlyTo,
|
||||
tags: updates.tags ?? tags,
|
||||
};
|
||||
if (updates.mode !== undefined) setMode(updates.mode);
|
||||
if (updates.speakOnlyTo !== undefined) setSpeakOnlyTo(updates.speakOnlyTo);
|
||||
if (updates.tags !== undefined) setTags(updates.tags);
|
||||
saveConferenceSettings(next);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
switch (callStatus) {
|
||||
case SipConstants.SESSION_ANSWERED:
|
||||
case CallState.Connected:
|
||||
setAppTitle("Conference");
|
||||
setSubmitTitle("Update");
|
||||
setCancelTitle("Hangup");
|
||||
@@ -70,8 +82,8 @@ export const JoinConference = ({
|
||||
setIsLoading(false);
|
||||
configureConferenceSession();
|
||||
break;
|
||||
case SipConstants.SESSION_ENDED:
|
||||
case SipConstants.SESSION_FAILED:
|
||||
case CallState.Ended:
|
||||
case CallState.Idle:
|
||||
setIsLoading(false);
|
||||
deleteConferenceSettings();
|
||||
break;
|
||||
@@ -80,7 +92,7 @@ export const JoinConference = ({
|
||||
|
||||
const handleSubmit = (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (callStatus !== SipConstants.SESSION_ANSWERED) {
|
||||
if (callStatus !== CallState.Connected) {
|
||||
call(conferenceName);
|
||||
if (!callSid) {
|
||||
setIsLoading(true);
|
||||
@@ -145,7 +157,7 @@ export const JoinConference = ({
|
||||
</Text>
|
||||
{callDuration > 0 && (
|
||||
<Text fontSize="15px">
|
||||
{new Date(callDuration * 1000).toISOString().substr(11, 8)}
|
||||
{new Date(callDuration * 1000).toISOString().substring(11, 19)}
|
||||
</Text>
|
||||
)}
|
||||
<FormControl id="conference_name">
|
||||
@@ -163,12 +175,7 @@ export const JoinConference = ({
|
||||
<OutlineBox title={participantState}>
|
||||
<RadioGroup
|
||||
onChange={(e) => {
|
||||
setMode(e as ConferenceModes);
|
||||
saveConferenceSettings({
|
||||
mode: e as ConferenceModes,
|
||||
speakOnlyTo,
|
||||
tags,
|
||||
});
|
||||
updateSettings({ mode: e as ConferenceModes });
|
||||
}}
|
||||
value={mode}
|
||||
colorScheme="jambonz"
|
||||
@@ -189,12 +196,7 @@ export const JoinConference = ({
|
||||
placeholder="tag"
|
||||
value={speakOnlyTo}
|
||||
onChange={(e) => {
|
||||
setSpeakOnlyTo(e.target.value);
|
||||
saveConferenceSettings({
|
||||
mode,
|
||||
speakOnlyTo: e.target.value,
|
||||
tags,
|
||||
});
|
||||
updateSettings({ speakOnlyTo: e.target.value });
|
||||
}}
|
||||
disabled={mode !== "coach"}
|
||||
required={mode === "coach"}
|
||||
@@ -208,12 +210,7 @@ export const JoinConference = ({
|
||||
placeholder="tag"
|
||||
value={tags}
|
||||
onChange={(e) => {
|
||||
setTags(e.target.value);
|
||||
saveConferenceSettings({
|
||||
mode,
|
||||
speakOnlyTo,
|
||||
tags: e.target.value,
|
||||
});
|
||||
updateSettings({ tags: e.target.value });
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Box, Button, HStack, VStack } from "@chakra-ui/react";
|
||||
import DialPadAudioElements from "./DialPadSoundElement";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
|
||||
type DialPadProbs = {
|
||||
handleDigitPress: (digit: string, fromKeyboard: boolean) => void;
|
||||
@@ -8,17 +8,24 @@ type DialPadProbs = {
|
||||
|
||||
const keySounds = new DialPadAudioElements();
|
||||
|
||||
export const DialPad = ({ handleDigitPress }: DialPadProbs) => {
|
||||
const selfRef = useRef<HTMLDivElement | null>(null);
|
||||
const isVisibleRef = useRef(false);
|
||||
const buttons = [
|
||||
const buttons = [
|
||||
["1", "2", "3"],
|
||||
["4", "5", "6"],
|
||||
["7", "8", "9"],
|
||||
["*", "0", "#"],
|
||||
];
|
||||
];
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
export const DialPad = ({ handleDigitPress }: DialPadProbs) => {
|
||||
const selfRef = useRef<HTMLDivElement | null>(null);
|
||||
const isVisibleRef = useRef(false);
|
||||
const handleDigitPressRef = useRef(handleDigitPress);
|
||||
|
||||
// Keep ref in sync so the keydown listener always uses the latest callback
|
||||
useEffect(() => {
|
||||
handleDigitPressRef.current = handleDigitPress;
|
||||
}, [handleDigitPress]);
|
||||
|
||||
const handleKeyDown = useCallback((e: KeyboardEvent) => {
|
||||
if (
|
||||
["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "*", "#"].includes(
|
||||
e.key
|
||||
@@ -26,10 +33,10 @@ export const DialPad = ({ handleDigitPress }: DialPadProbs) => {
|
||||
) {
|
||||
if (isVisibleRef.current) {
|
||||
keySounds?.playKeyTone(e.key);
|
||||
handleDigitPress(e.key, true);
|
||||
handleDigitPressRef.current(e.key, true);
|
||||
}
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
@@ -50,7 +57,7 @@ export const DialPad = ({ handleDigitPress }: DialPadProbs) => {
|
||||
observer.unobserve(selfRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
}, [handleKeyDown]);
|
||||
|
||||
return (
|
||||
<Box p={2} w="full" h="280px" ref={selfRef}>
|
||||
|
||||
+301
-301
@@ -15,27 +15,30 @@ import {
|
||||
} from "@chakra-ui/react";
|
||||
import {
|
||||
Dispatch,
|
||||
forwardRef,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import {
|
||||
IAppSettings,
|
||||
SipCallDirection,
|
||||
SipClientStatus,
|
||||
} from "src/common/types";
|
||||
import { SipConstants, SipUA } from "src/lib";
|
||||
import {
|
||||
useJambonzClient,
|
||||
useCall,
|
||||
ClientState,
|
||||
CallState as JambonzCallState,
|
||||
} from "@jambonz/client-sdk-web";
|
||||
import IncommingCall from "./incoming-call";
|
||||
import DialPad from "./dial-pad";
|
||||
import {
|
||||
isSipClientAnswered,
|
||||
isSipClientIdle,
|
||||
isSipClientRinging,
|
||||
} from "src/utils";
|
||||
import { SipAudioElements } from "src/lib";
|
||||
|
||||
import Avatar from "src/imgs/icons/Avatar.svg";
|
||||
import GreenAvatar from "src/imgs/icons/Avatar-Green.svg";
|
||||
@@ -70,6 +73,8 @@ import {
|
||||
faPhoneSlash,
|
||||
faPlay,
|
||||
faUserGroup,
|
||||
faEarListen,
|
||||
faEarDeaf,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import JoinConference from "./conference";
|
||||
@@ -90,6 +95,7 @@ type PhoneProbs = {
|
||||
reload: () => void;
|
||||
setIsSwitchingUserStatus: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setIsOnline: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
isUserOffline: boolean;
|
||||
};
|
||||
|
||||
enum PAGE_VIEW {
|
||||
@@ -99,15 +105,13 @@ enum PAGE_VIEW {
|
||||
JOIN_CONFERENCE,
|
||||
}
|
||||
|
||||
export const Phone = forwardRef(
|
||||
(
|
||||
{
|
||||
export const Phone = ({
|
||||
sipDomain,
|
||||
sipServerAddress,
|
||||
sipUsername,
|
||||
sipPassword,
|
||||
sipDisplayName,
|
||||
stat: [status, setStatus],
|
||||
stat: [, setStatus],
|
||||
calledNumber: [calledANumber, setCalledANumber],
|
||||
calledName: [calledAName, setCalledAName],
|
||||
advancedSettings,
|
||||
@@ -115,14 +119,10 @@ export const Phone = forwardRef(
|
||||
reload,
|
||||
setIsSwitchingUserStatus,
|
||||
setIsOnline,
|
||||
}: PhoneProbs,
|
||||
ref: any
|
||||
) => {
|
||||
isUserOffline,
|
||||
}: PhoneProbs) => {
|
||||
const [inputNumber, setInputNumber] = useState("");
|
||||
const [appName, setAppName] = useState("");
|
||||
const [callStatus, setCallStatus] = useState(SipConstants.SESSION_ENDED);
|
||||
const [sessionDirection, setSessionDirection] =
|
||||
useState<SipCallDirection>("");
|
||||
const [seconds, setSeconds] = useState(0);
|
||||
const [isCallButtonLoading, setIsCallButtonLoading] = useState(false);
|
||||
const [isAdvanceMode, setIsAdvancedMode] = useState(false);
|
||||
@@ -135,240 +135,221 @@ export const Phone = forwardRef(
|
||||
allow_direct_user_calling: false,
|
||||
});
|
||||
const [selectedConference, setSelectedConference] = useState("");
|
||||
const [callSid, setCallSid] = useState("");
|
||||
const [callSidState, setCallSidState] = useState("");
|
||||
const [showConference, setShowConference] = useState(false);
|
||||
|
||||
const [showAccounts, setShowAccounts] = useState(false);
|
||||
const [isNoiseIsolation, setIsNoiseIsolation] = useState(false);
|
||||
|
||||
const inputNumberRef = useRef(inputNumber);
|
||||
const sessionDirectionRef = useRef(sessionDirection);
|
||||
const sipUA = useRef<SipUA | null>(null);
|
||||
const timerRef = useRef<NodeJS.Timer | null>(null);
|
||||
const FetchUsertimerRef = useRef<NodeJS.Timer | null>(null);
|
||||
const isRestartRef = useRef(false);
|
||||
const sipDomainRef = useRef("");
|
||||
const sipUsernameRef = useRef("");
|
||||
const sipPasswordRef = useRef("");
|
||||
const sipServerAddressRef = useRef("");
|
||||
const sipDisplayNameRef = useRef("");
|
||||
const unregisteredReasonRef = useRef("");
|
||||
const isInputNumberFocusRef = useRef(false);
|
||||
const secondsRef = useRef(seconds);
|
||||
const accountsCardRef = useRef<HTMLDivElement | null>(null);
|
||||
const audioRef = useRef<SipAudioElements>(new SipAudioElements());
|
||||
const prevIsRegisteredRef = useRef(false);
|
||||
const sipUsernameRef = useRef(sipUsername);
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
updateGoOffline(newState: string) {
|
||||
if (newState === "stop") {
|
||||
sipUA.current?.stop();
|
||||
} else {
|
||||
sipUA.current?.start();
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
const addCallHistory = useCallback(() => {
|
||||
const call = getCurrentCall();
|
||||
if (call) {
|
||||
saveCallHistory(sipUsername, {
|
||||
number: call.number,
|
||||
direction: call.direction,
|
||||
duration: transform(Date.now(), call.timeStamp),
|
||||
timeStamp: call.timeStamp,
|
||||
callSid: call.callSid,
|
||||
name: call.name,
|
||||
});
|
||||
}
|
||||
deleteCurrentCall();
|
||||
// Keep sipUsername ref up to date
|
||||
useEffect(() => {
|
||||
sipUsernameRef.current = sipUsername;
|
||||
}, [sipUsername]);
|
||||
|
||||
const startCallDurationCounter = useCallback(() => {
|
||||
stopCallDurationCounter();
|
||||
timerRef.current = setInterval(() => {
|
||||
setSeconds((seconds) => seconds + 1);
|
||||
}, 1000);
|
||||
}, []);
|
||||
// Build stable options for useJambonzClient
|
||||
const clientOptions = useMemo(() => {
|
||||
if (!sipDomain || !sipUsername || !sipPassword || !sipServerAddress) {
|
||||
return { server: "", username: "", password: "" };
|
||||
}
|
||||
return {
|
||||
server: sipServerAddress,
|
||||
username: sipUsername,
|
||||
password: sipPassword,
|
||||
displayName: sipDisplayName || sipUsername,
|
||||
realm: sipDomain,
|
||||
};
|
||||
}, [sipDomain, sipServerAddress, sipUsername, sipPassword, sipDisplayName]);
|
||||
|
||||
const createSipClient = useCallback(() => {
|
||||
const hasCredentials = !!(sipDomain && sipUsername && sipPassword && sipServerAddress);
|
||||
|
||||
const {
|
||||
client,
|
||||
state: clientState,
|
||||
isRegistered,
|
||||
connect,
|
||||
disconnect,
|
||||
error: clientError,
|
||||
} = useJambonzClient(clientOptions);
|
||||
|
||||
const {
|
||||
call: activeCall,
|
||||
state: callState,
|
||||
isMuted,
|
||||
isHeld,
|
||||
incomingCaller,
|
||||
makeCall,
|
||||
answerIncoming,
|
||||
declineIncoming,
|
||||
hangup,
|
||||
toggleMute,
|
||||
toggleHold,
|
||||
sendDtmf,
|
||||
} = useCall(client);
|
||||
|
||||
// Sync client state to parent
|
||||
useEffect(() => {
|
||||
setStatus(clientState);
|
||||
}, [clientState, setStatus]);
|
||||
|
||||
// Auto-connect/disconnect based on credentials and user offline toggle
|
||||
useEffect(() => {
|
||||
if (hasCredentials && !isUserOffline) {
|
||||
setIsSwitchingUserStatus(true);
|
||||
const client = {
|
||||
username: `${sipUsernameRef.current}@${sipDomainRef.current}`,
|
||||
password: sipPasswordRef.current,
|
||||
name: sipDisplayNameRef.current ?? sipUsernameRef.current,
|
||||
};
|
||||
|
||||
const settings = {
|
||||
pcConfig: {
|
||||
iceServers: [{ urls: ["stun:stun.l.google.com:19302"] }],
|
||||
},
|
||||
wsUri: sipServerAddressRef.current,
|
||||
register: true,
|
||||
};
|
||||
|
||||
const sipClient = new SipUA(client, settings);
|
||||
|
||||
// UA Status
|
||||
sipClient.on(SipConstants.UA_REGISTERED, (args) => {
|
||||
setStatus("registered");
|
||||
});
|
||||
sipClient.on(SipConstants.UA_UNREGISTERED, (args) => {
|
||||
setStatus("unregistered");
|
||||
if (sipUA.current) {
|
||||
sipUA.current.stop();
|
||||
}
|
||||
unregisteredReasonRef.current = `User is not registered${
|
||||
args.cause ? `, ${args.cause}` : ""
|
||||
}`;
|
||||
});
|
||||
|
||||
sipClient.on(SipConstants.UA_DISCONNECTED, (args) => {
|
||||
if (unregisteredReasonRef.current) {
|
||||
toast({
|
||||
title: unregisteredReasonRef.current,
|
||||
status: "warning",
|
||||
duration: DEFAULT_TOAST_DURATION,
|
||||
isClosable: true,
|
||||
});
|
||||
unregisteredReasonRef.current = "";
|
||||
}
|
||||
setStatus("disconnected");
|
||||
setIsOnline(false);
|
||||
connect().catch((err) => {
|
||||
setIsSwitchingUserStatus(false);
|
||||
if (sipUA.current) {
|
||||
sipUA.current.stop();
|
||||
}
|
||||
|
||||
if (args.error) {
|
||||
setIsOnline(false);
|
||||
toast({
|
||||
title: `Cannot connect to ${sipServerAddressRef.current}${
|
||||
args.reason ? `, ${args.reason}` : ""
|
||||
}`,
|
||||
title: `Cannot connect to ${sipServerAddress}${err?.message ? `: ${err.message}` : ""}`,
|
||||
status: "warning",
|
||||
duration: DEFAULT_TOAST_DURATION,
|
||||
isClosable: true,
|
||||
});
|
||||
} else if (isRestartRef.current) {
|
||||
createSipClient();
|
||||
isRestartRef.current = false;
|
||||
}
|
||||
});
|
||||
// Call Status
|
||||
sipClient.on(SipConstants.SESSION_RINGING, (args) => {
|
||||
if (args.session.direction === "incoming") {
|
||||
} else {
|
||||
disconnect();
|
||||
}
|
||||
}, [hasCredentials, isUserOffline, sipDomain, sipUsername, sipPassword, sipServerAddress, sipDisplayName]);
|
||||
|
||||
// Cleanup on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
disconnect();
|
||||
if (timerRef.current) {
|
||||
clearInterval(timerRef.current);
|
||||
timerRef.current = null;
|
||||
}
|
||||
clearFetchUserTimer();
|
||||
};
|
||||
}, [disconnect]);
|
||||
|
||||
// Handle registration state changes
|
||||
useEffect(() => {
|
||||
if (isRegistered !== prevIsRegisteredRef.current) {
|
||||
prevIsRegisteredRef.current = isRegistered;
|
||||
setIsSwitchingUserStatus(false);
|
||||
setIsOnline(isRegistered);
|
||||
}
|
||||
}, [isRegistered, setIsSwitchingUserStatus, setIsOnline]);
|
||||
|
||||
// Handle client errors
|
||||
useEffect(() => {
|
||||
if (clientError) {
|
||||
toast({
|
||||
title: clientError,
|
||||
status: "warning",
|
||||
duration: DEFAULT_TOAST_DURATION,
|
||||
isClosable: true,
|
||||
});
|
||||
}
|
||||
}, [clientError, toast]);
|
||||
|
||||
// Handle disconnected state
|
||||
useEffect(() => {
|
||||
if (clientState === ClientState.Disconnected || clientState === ClientState.Error) {
|
||||
setIsSwitchingUserStatus(false);
|
||||
setIsOnline(false);
|
||||
}
|
||||
}, [clientState, setIsSwitchingUserStatus, setIsOnline]);
|
||||
|
||||
// Audio feedback and page view based on call state
|
||||
useEffect(() => {
|
||||
if (isSipClientIdle(callState) && isCallButtonLoading) {
|
||||
setIsCallButtonLoading(false);
|
||||
}
|
||||
|
||||
if (callState === JambonzCallState.Ringing || callState === JambonzCallState.Connecting) {
|
||||
if (incomingCaller) {
|
||||
// Incoming call
|
||||
setInputNumber(incomingCaller);
|
||||
setPageView(PAGE_VIEW.INCOMING_CALL);
|
||||
audioRef.current.playRinging();
|
||||
saveCurrentCall({
|
||||
number: args.session.user,
|
||||
direction: args.session.direction,
|
||||
number: incomingCaller,
|
||||
direction: "inbound",
|
||||
timeStamp: Date.now(),
|
||||
duration: "0",
|
||||
callSid: uuidv4(),
|
||||
});
|
||||
} else {
|
||||
// Outgoing call ringing
|
||||
setPageView(PAGE_VIEW.OUTGOING_CALL);
|
||||
audioRef.current.playRingback();
|
||||
}
|
||||
setCallStatus(SipConstants.SESSION_RINGING);
|
||||
setSessionDirection(args.session.direction);
|
||||
setInputNumber(args.session.user);
|
||||
});
|
||||
sipClient.on(SipConstants.SESSION_ANSWERED, (args) => {
|
||||
setCallSid(args.callSid);
|
||||
} else if (callState === JambonzCallState.Connected) {
|
||||
audioRef.current.playAnswer();
|
||||
const currentCall = getCurrentCall();
|
||||
if (currentCall) {
|
||||
currentCall.timeStamp = Date.now();
|
||||
saveCurrentCall(currentCall);
|
||||
}
|
||||
setCallStatus(SipConstants.SESSION_ANSWERED);
|
||||
if (selectedConference) {
|
||||
setPageView(PAGE_VIEW.JOIN_CONFERENCE);
|
||||
} else {
|
||||
setPageView(PAGE_VIEW.DIAL_PAD);
|
||||
}
|
||||
startCallDurationCounter();
|
||||
});
|
||||
sipClient.on(SipConstants.SESSION_ENDED, (args) => {
|
||||
} else if (callState === JambonzCallState.Ended || callState === null) {
|
||||
// Call ended or no call
|
||||
if (timerRef.current) {
|
||||
// There was an active call that just ended
|
||||
audioRef.current.playLocalHungup();
|
||||
addCallHistory();
|
||||
setCallStatus(SipConstants.SESSION_ENDED);
|
||||
setSessionDirection("");
|
||||
stopCallDurationCounter();
|
||||
});
|
||||
sipClient.on(SipConstants.SESSION_FAILED, (args) => {
|
||||
addCallHistory();
|
||||
setCallStatus(SipConstants.SESSION_FAILED);
|
||||
setSessionDirection("");
|
||||
stopCallDurationCounter();
|
||||
});
|
||||
|
||||
sipClient.start();
|
||||
sipUA.current = sipClient;
|
||||
}, [
|
||||
addCallHistory,
|
||||
setIsSwitchingUserStatus,
|
||||
setStatus,
|
||||
startCallDurationCounter,
|
||||
toast,
|
||||
setIsOnline,
|
||||
]);
|
||||
setSelectedConference("");
|
||||
setIsNoiseIsolation(false);
|
||||
setPageView(PAGE_VIEW.DIAL_PAD);
|
||||
}
|
||||
}
|
||||
}, [callState, incomingCaller, selectedConference]);
|
||||
|
||||
// Handle call failed event from the call object
|
||||
useEffect(() => {
|
||||
sipDomainRef.current = sipDomain;
|
||||
sipUsernameRef.current = sipUsername;
|
||||
sipPasswordRef.current = sipPassword;
|
||||
sipServerAddressRef.current = sipServerAddress;
|
||||
sipDisplayNameRef.current = sipDisplayName;
|
||||
if (sipDomain && sipUsername && sipPassword && sipServerAddress) {
|
||||
if (sipUA.current) {
|
||||
if (sipUA.current.isConnected()) {
|
||||
clientGoOffline();
|
||||
isRestartRef.current = true;
|
||||
} else {
|
||||
createSipClient();
|
||||
}
|
||||
} else {
|
||||
createSipClient();
|
||||
}
|
||||
} else {
|
||||
clientGoOffline();
|
||||
}
|
||||
}, [
|
||||
sipDomain,
|
||||
sipUsername,
|
||||
sipPassword,
|
||||
sipServerAddress,
|
||||
sipDisplayName,
|
||||
createSipClient,
|
||||
]);
|
||||
if (!activeCall) return;
|
||||
|
||||
const onFailed = () => {
|
||||
audioRef.current.playFailed();
|
||||
addCallHistory();
|
||||
stopCallDurationCounter();
|
||||
setSelectedConference("");
|
||||
setPageView(PAGE_VIEW.DIAL_PAD);
|
||||
};
|
||||
|
||||
activeCall.on("failed", onFailed);
|
||||
return () => {
|
||||
activeCall.off("failed", onFailed);
|
||||
};
|
||||
}, [activeCall]);
|
||||
|
||||
// Get callSid from call headers when accepted
|
||||
useEffect(() => {
|
||||
if (!activeCall) return;
|
||||
|
||||
const onAccepted = () => {
|
||||
// The SDK doesn't expose response headers directly,
|
||||
// use the call id as callSid
|
||||
setCallSidState(activeCall.id);
|
||||
};
|
||||
|
||||
activeCall.on("accepted", onAccepted);
|
||||
return () => {
|
||||
activeCall.off("accepted", onAccepted);
|
||||
};
|
||||
}, [activeCall]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsAdvancedMode(!!advancedSettings?.decoded?.accountSid);
|
||||
fetchRegisterUser();
|
||||
}, [advancedSettings]);
|
||||
|
||||
useEffect(() => {
|
||||
inputNumberRef.current = inputNumber;
|
||||
sessionDirectionRef.current = sessionDirection;
|
||||
secondsRef.current = seconds;
|
||||
}, [inputNumber, seconds, sessionDirection]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSipClientIdle(callStatus) && isCallButtonLoading) {
|
||||
setIsCallButtonLoading(false);
|
||||
}
|
||||
switch (callStatus) {
|
||||
case SipConstants.SESSION_RINGING:
|
||||
if (sessionDirection === "incoming") {
|
||||
setPageView(PAGE_VIEW.INCOMING_CALL);
|
||||
} else {
|
||||
setPageView(PAGE_VIEW.OUTGOING_CALL);
|
||||
}
|
||||
break;
|
||||
case SipConstants.SESSION_ANSWERED:
|
||||
if (!!selectedConference) {
|
||||
setPageView(PAGE_VIEW.JOIN_CONFERENCE);
|
||||
} else {
|
||||
setPageView(PAGE_VIEW.DIAL_PAD);
|
||||
}
|
||||
break;
|
||||
case SipConstants.SESSION_ENDED:
|
||||
case SipConstants.SESSION_FAILED:
|
||||
setSelectedConference("");
|
||||
setPageView(PAGE_VIEW.DIAL_PAD);
|
||||
break;
|
||||
}
|
||||
}, [callStatus, isCallButtonLoading, selectedConference, sessionDirection]);
|
||||
|
||||
useEffect(() => {
|
||||
if (calledANumber) {
|
||||
if (
|
||||
@@ -388,13 +369,6 @@ export const Phone = forwardRef(
|
||||
}
|
||||
}, [calledANumber, calledAName, setCalledAName, setCalledANumber]);
|
||||
|
||||
useEffect(() => {
|
||||
if (status === "registered" || status === "disconnected") {
|
||||
setIsSwitchingUserStatus(false);
|
||||
setIsOnline(status === "registered");
|
||||
}
|
||||
}, [status, setIsOnline, setIsSwitchingUserStatus]);
|
||||
|
||||
const clearFetchUserTimer = () => {
|
||||
if (FetchUsertimerRef.current) {
|
||||
clearInterval(FetchUsertimerRef.current);
|
||||
@@ -404,7 +378,6 @@ export const Phone = forwardRef(
|
||||
|
||||
useEffect(() => {
|
||||
if (isAdvanceMode) {
|
||||
// check conference aibility
|
||||
getConferences()
|
||||
.then(() => {
|
||||
setShowConference(true);
|
||||
@@ -412,6 +385,7 @@ export const Phone = forwardRef(
|
||||
.catch(() => {
|
||||
setShowConference(false);
|
||||
});
|
||||
clearFetchUserTimer();
|
||||
FetchUsertimerRef.current = setInterval(() => {
|
||||
fetchRegisterUser();
|
||||
}, 10_000);
|
||||
@@ -419,6 +393,7 @@ export const Phone = forwardRef(
|
||||
clearFetchUserTimer();
|
||||
setShowConference(false);
|
||||
}
|
||||
return () => clearFetchUserTimer();
|
||||
}, [isAdvanceMode]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -438,7 +413,7 @@ export const Phone = forwardRef(
|
||||
.then(({ json }) => {
|
||||
setRegisteredUser(json);
|
||||
})
|
||||
.catch((err) => {
|
||||
.catch(() => {
|
||||
setRegisteredUser({
|
||||
allow_direct_app_calling: false,
|
||||
allow_direct_queue_calling: false,
|
||||
@@ -447,6 +422,28 @@ export const Phone = forwardRef(
|
||||
});
|
||||
};
|
||||
|
||||
const addCallHistory = useCallback(() => {
|
||||
const call = getCurrentCall();
|
||||
if (call) {
|
||||
saveCallHistory(sipUsernameRef.current, {
|
||||
number: call.number,
|
||||
direction: call.direction,
|
||||
duration: transform(Date.now(), call.timeStamp),
|
||||
timeStamp: call.timeStamp,
|
||||
callSid: call.callSid,
|
||||
name: call.name,
|
||||
});
|
||||
}
|
||||
deleteCurrentCall();
|
||||
}, []);
|
||||
|
||||
const startCallDurationCounter = useCallback(() => {
|
||||
stopCallDurationCounter();
|
||||
timerRef.current = setInterval(() => {
|
||||
setSeconds((seconds) => seconds + 1);
|
||||
}, 1000);
|
||||
}, []);
|
||||
|
||||
function stopCallDurationCounter() {
|
||||
if (timerRef.current) {
|
||||
clearInterval(timerRef.current);
|
||||
@@ -456,105 +453,95 @@ export const Phone = forwardRef(
|
||||
}
|
||||
|
||||
function transform(t1: number, t2: number) {
|
||||
const diff = Math.abs(t1 - t2) / 1000; // Get the difference in seconds
|
||||
|
||||
const diff = Math.abs(t1 - t2) / 1000;
|
||||
const hours = Math.floor(diff / 3600);
|
||||
const minutes = Math.floor((diff % 3600) / 60);
|
||||
const seconds = Math.floor(diff % 60);
|
||||
|
||||
// Pad the values with a leading zero if they are less than 10
|
||||
const secs = Math.floor(diff % 60);
|
||||
const hours1 = hours < 10 ? "0" + hours : hours;
|
||||
const minutes1 = minutes < 10 ? "0" + minutes : minutes;
|
||||
const seconds1 = seconds < 10 ? "0" + seconds : seconds;
|
||||
|
||||
const seconds1 = secs < 10 ? "0" + secs : secs;
|
||||
return `${hours1}:${minutes1}:${seconds1}`;
|
||||
}
|
||||
|
||||
const handleDialPadClick = (value: string, fromKeyboad: boolean) => {
|
||||
if (!(isInputNumberFocusRef.current && fromKeyboad)) {
|
||||
const handleDialPadClick = (value: string, fromKeyboard: boolean) => {
|
||||
if (!(isInputNumberFocusRef.current && fromKeyboard)) {
|
||||
setInputNumber((prev) => prev + value);
|
||||
}
|
||||
|
||||
if (isSipClientAnswered(callStatus)) {
|
||||
sipUA.current?.dtmf(value, undefined);
|
||||
if (isSipClientAnswered(callState)) {
|
||||
sendDtmf(value);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCallButtion = () => {
|
||||
const handleCallButton = () => {
|
||||
makeOutboundCall(inputNumber);
|
||||
};
|
||||
|
||||
const makeOutboundCall = (number: string, name: string = "") => {
|
||||
if (sipUA.current && number) {
|
||||
if (client && isRegistered && number) {
|
||||
setIsCallButtonLoading(true);
|
||||
setCallStatus(SipConstants.SESSION_RINGING);
|
||||
setSessionDirection("outgoing");
|
||||
saveCurrentCall({
|
||||
number: number,
|
||||
name,
|
||||
direction: "outgoing",
|
||||
direction: "outbound",
|
||||
timeStamp: Date.now(),
|
||||
duration: "0",
|
||||
callSid: uuidv4(),
|
||||
});
|
||||
// Add custom header if this is special jambonz call
|
||||
let customHeaders: string[] = [];
|
||||
if (number.startsWith("app-")) {
|
||||
customHeaders = [
|
||||
`X-Application-Sid: ${number.substring(4, number.length)}`,
|
||||
];
|
||||
}
|
||||
sipUA.current.call(number, customHeaders);
|
||||
}
|
||||
};
|
||||
|
||||
const clientGoOffline = () => {
|
||||
if (sipUA.current) {
|
||||
sipUA.current.stop();
|
||||
sipUA.current = null;
|
||||
// Use SDK-specific call methods based on the target type
|
||||
if (number.startsWith("app-")) {
|
||||
const appSid = number.substring(4);
|
||||
makeCall(number, {
|
||||
headers: { "X-Application-Sid": appSid },
|
||||
});
|
||||
} else if (number.startsWith("queue-")) {
|
||||
makeCall(number);
|
||||
} else if (number.startsWith("conference-")) {
|
||||
makeCall(number);
|
||||
} else {
|
||||
makeCall(number);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleHangup = () => {
|
||||
if (isSipClientAnswered(callStatus) || isSipClientRinging(callStatus)) {
|
||||
sipUA.current?.terminate(480, "Call Finished", undefined);
|
||||
}
|
||||
audioRef.current.stopAll();
|
||||
hangup();
|
||||
};
|
||||
|
||||
const handleCallOnHold = () => {
|
||||
if (isSipClientAnswered(callStatus)) {
|
||||
if (sipUA.current?.isHolded(undefined)) {
|
||||
sipUA.current?.unhold(undefined);
|
||||
} else {
|
||||
sipUA.current?.hold(undefined);
|
||||
}
|
||||
if (isSipClientAnswered(callState)) {
|
||||
toggleHold();
|
||||
}
|
||||
};
|
||||
|
||||
const handleCallMute = () => {
|
||||
if (isSipClientAnswered(callStatus)) {
|
||||
if (sipUA.current?.isMuted(undefined)) {
|
||||
sipUA.current?.unmute(undefined);
|
||||
if (isSipClientAnswered(callState)) {
|
||||
toggleMute();
|
||||
}
|
||||
};
|
||||
|
||||
const handleNoiseIsolation = () => {
|
||||
if (isSipClientAnswered(callState) && activeCall) {
|
||||
if (isNoiseIsolation) {
|
||||
activeCall.disableNoiseIsolation();
|
||||
setIsNoiseIsolation(false);
|
||||
} else {
|
||||
sipUA.current?.mute(undefined);
|
||||
activeCall.enableNoiseIsolation();
|
||||
setIsNoiseIsolation(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleAnswer = () => {
|
||||
if (isSipClientRinging(callStatus)) {
|
||||
sipUA.current?.answer(undefined);
|
||||
}
|
||||
audioRef.current.pauseRinging();
|
||||
answerIncoming();
|
||||
};
|
||||
|
||||
const handleDecline = () => {
|
||||
if (isSipClientRinging(callStatus)) {
|
||||
sipUA.current?.terminate(486, "Busy here", undefined);
|
||||
}
|
||||
};
|
||||
|
||||
const isStatusRegistered = () => {
|
||||
return status === "registered";
|
||||
audioRef.current.stopAll();
|
||||
declineIncoming();
|
||||
};
|
||||
|
||||
const handleSetActive = (id: number) => {
|
||||
@@ -597,7 +584,7 @@ export const Phone = forwardRef(
|
||||
{sipUsername && sipDomain ? (
|
||||
<>
|
||||
<Image
|
||||
src={isStatusRegistered() ? GreenAvatar : Avatar}
|
||||
src={isRegistered ? GreenAvatar : Avatar}
|
||||
boxSize="35px"
|
||||
/>
|
||||
<VStack alignItems="start" w="full" spacing={0}>
|
||||
@@ -607,7 +594,7 @@ export const Phone = forwardRef(
|
||||
</Text>
|
||||
<Circle
|
||||
size="8px"
|
||||
bg={isStatusRegistered() ? "green.500" : "gray.500"}
|
||||
bg={isRegistered ? "green.500" : "gray.500"}
|
||||
/>
|
||||
</HStack>
|
||||
<Text fontWeight="bold" w="full">
|
||||
@@ -646,9 +633,9 @@ export const Phone = forwardRef(
|
||||
spacing={2}
|
||||
w="full"
|
||||
mt={5}
|
||||
className={isStatusRegistered() ? "" : "blurred"}
|
||||
className={isRegistered ? "" : "blurred"}
|
||||
>
|
||||
{isAdvanceMode && isSipClientIdle(callStatus) && (
|
||||
{isAdvanceMode && isSipClientIdle(callState) && (
|
||||
<HStack spacing={2} align="start" w="full">
|
||||
{registeredUser.allow_direct_user_calling && (
|
||||
<IconButtonMenu
|
||||
@@ -807,22 +794,22 @@ export const Phone = forwardRef(
|
||||
isInputNumberFocusRef.current = false;
|
||||
}}
|
||||
textAlign="center"
|
||||
isReadOnly={!isSipClientIdle(callStatus)}
|
||||
isReadOnly={!isSipClientIdle(callState)}
|
||||
/>
|
||||
|
||||
{!isSipClientIdle(callStatus) && seconds >= 0 && (
|
||||
{!isSipClientIdle(callState) && seconds >= 0 && (
|
||||
<Text fontSize="15px">
|
||||
{new Date(seconds * 1000).toISOString().substr(11, 8)}
|
||||
{new Date(seconds * 1000).toISOString().substring(11, 19)}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<DialPad handleDigitPress={handleDialPadClick} />
|
||||
|
||||
{isSipClientIdle(callStatus) ? (
|
||||
{isSipClientIdle(callState) ? (
|
||||
<Button
|
||||
w="full"
|
||||
onClick={handleCallButtion}
|
||||
isDisabled={!isStatusRegistered()}
|
||||
onClick={handleCallButton}
|
||||
isDisabled={!isRegistered}
|
||||
colorScheme="jambonz"
|
||||
alignContent="center"
|
||||
isLoading={isCallButtonLoading}
|
||||
@@ -830,18 +817,13 @@ export const Phone = forwardRef(
|
||||
Call
|
||||
</Button>
|
||||
) : (
|
||||
<VStack w="full" spacing={2}>
|
||||
<HStack w="full">
|
||||
<Tooltip
|
||||
label={sipUA.current?.isHolded(undefined) ? "UnHold" : "Hold"}
|
||||
>
|
||||
<Tooltip label={isHeld ? "UnHold" : "Hold"}>
|
||||
<IconButton
|
||||
aria-label="Place call onhold"
|
||||
icon={
|
||||
<FontAwesomeIcon
|
||||
icon={
|
||||
sipUA.current?.isHolded(undefined) ? faPlay : faPause
|
||||
}
|
||||
/>
|
||||
<FontAwesomeIcon icon={isHeld ? faPlay : faPause} />
|
||||
}
|
||||
w="33%"
|
||||
variant="unstyled"
|
||||
@@ -863,18 +845,12 @@ export const Phone = forwardRef(
|
||||
onClick={handleHangup}
|
||||
/>
|
||||
<Spacer />
|
||||
<Tooltip
|
||||
label={sipUA.current?.isMuted(undefined) ? "Unmute" : "Mute"}
|
||||
>
|
||||
<Tooltip label={isMuted ? "Unmute" : "Mute"}>
|
||||
<IconButton
|
||||
aria-label="Mute"
|
||||
icon={
|
||||
<FontAwesomeIcon
|
||||
icon={
|
||||
sipUA.current?.isMuted(undefined)
|
||||
? faMicrophone
|
||||
: faMicrophoneSlash
|
||||
}
|
||||
icon={isMuted ? faMicrophone : faMicrophoneSlash}
|
||||
/>
|
||||
}
|
||||
w="33%"
|
||||
@@ -886,6 +862,31 @@ export const Phone = forwardRef(
|
||||
/>
|
||||
</Tooltip>
|
||||
</HStack>
|
||||
{isSipClientAnswered(callState) && (
|
||||
<Tooltip
|
||||
label={
|
||||
isNoiseIsolation
|
||||
? "Disable Noise Isolation"
|
||||
: "Enable Noise Isolation"
|
||||
}
|
||||
>
|
||||
<IconButton
|
||||
aria-label="Toggle noise isolation"
|
||||
icon={
|
||||
<FontAwesomeIcon
|
||||
icon={isNoiseIsolation ? faEarDeaf : faEarListen}
|
||||
/>
|
||||
}
|
||||
variant="unstyled"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
color={isNoiseIsolation ? "jambonz.500" : undefined}
|
||||
onClick={handleNoiseIsolation}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</VStack>
|
||||
)}
|
||||
</VStack>
|
||||
)}
|
||||
@@ -899,18 +900,18 @@ export const Phone = forwardRef(
|
||||
{pageView === PAGE_VIEW.OUTGOING_CALL && (
|
||||
<OutGoingCall
|
||||
number={inputNumber || appName}
|
||||
cancelCall={handleDecline}
|
||||
cancelCall={handleHangup}
|
||||
/>
|
||||
)}
|
||||
{pageView === PAGE_VIEW.JOIN_CONFERENCE && (
|
||||
<JoinConference
|
||||
conferenceId={selectedConference}
|
||||
callSid={callSid}
|
||||
callSid={callSidState}
|
||||
callDuration={seconds}
|
||||
callStatus={callStatus}
|
||||
callStatus={callState ?? JambonzCallState.Idle}
|
||||
handleCancel={() => {
|
||||
if (isSipClientAnswered(callStatus)) {
|
||||
sipUA.current?.terminate(480, "Call Finished", undefined);
|
||||
if (isSipClientAnswered(callState)) {
|
||||
hangup();
|
||||
}
|
||||
setPageView(PAGE_VIEW.DIAL_PAD);
|
||||
}}
|
||||
@@ -924,7 +925,6 @@ export const Phone = forwardRef(
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default Phone;
|
||||
|
||||
Reference in New Issue
Block a user