use @jambonz/client-sdk-web and enable noiseIsolation feature

This commit is contained in:
Hoan HL
2026-04-02 15:23:58 +07:00
parent 70f6eb5817
commit 9fee6fb787
22 changed files with 881 additions and 1655 deletions
+31 -24
View File
@@ -14,12 +14,12 @@
"@fortawesome/fontawesome-svg-core": "^6.5.2", "@fortawesome/fontawesome-svg-core": "^6.5.2",
"@fortawesome/free-solid-svg-icons": "^6.5.2", "@fortawesome/free-solid-svg-icons": "^6.5.2",
"@fortawesome/react-fontawesome": "^0.2.0", "@fortawesome/react-fontawesome": "^0.2.0",
"@jambonz/client-sdk-web": "^0.1.4",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"framer-motion": "^10.16.4", "framer-motion": "^10.16.4",
"fuse.js": "^6.6.2", "fuse.js": "^6.6.2",
"google-libphonenumber": "^3.2.33", "google-libphonenumber": "^3.2.33",
"jssip": "^3.10.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"uuid": "^9.0.1", "uuid": "^9.0.1",
@@ -4038,6 +4038,32 @@
"node": ">=8" "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": { "node_modules/@jest/console": {
"version": "27.5.1", "version": "27.5.1",
"resolved": "https://registry.npmjs.org/@jest/console/-/console-27.5.1.tgz", "resolved": "https://registry.npmjs.org/@jest/console/-/console-27.5.1.tgz",
@@ -5638,14 +5664,6 @@
"@types/node": "*" "@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": { "node_modules/@types/eslint": {
"version": "8.44.2", "version": "8.44.2",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.2.tgz", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.2.tgz",
@@ -5672,11 +5690,6 @@
"integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==", "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==",
"dev": true "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": { "node_modules/@types/express": {
"version": "4.17.17", "version": "4.17.17",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz",
@@ -5823,11 +5836,6 @@
"integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==", "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==",
"dev": true "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": { "node_modules/@types/node": {
"version": "16.18.53", "version": "16.18.53",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.53.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.53.tgz",
@@ -14938,12 +14946,11 @@
} }
}, },
"node_modules/jssip": { "node_modules/jssip": {
"version": "3.10.0", "version": "3.13.6",
"resolved": "https://registry.npmjs.org/jssip/-/jssip-3.10.0.tgz", "resolved": "https://registry.npmjs.org/jssip/-/jssip-3.13.6.tgz",
"integrity": "sha512-iJj+bhnNl0S296sUDc2ZjIbAetnelzZ92aWARyW01oKZ0X8t+5aGrYfJdMFliLFm8hxMcnkep3vmSRGe/yRjsA==", "integrity": "sha512-Bf1ndrSuqpO87/AG56WACR7kKcCvKOzaIQROu7JUMh0qFaGOV4NuR+wsnaXa7f3/d6xhwVczczFyt1ywJmTjPg==",
"license": "MIT",
"dependencies": { "dependencies": {
"@types/debug": "^4.1.7",
"@types/events": "^3.0.0",
"debug": "^4.3.1", "debug": "^4.3.1",
"events": "^3.3.0", "events": "^3.3.0",
"sdp-transform": "^2.14.1" "sdp-transform": "^2.14.1"
+1 -1
View File
@@ -9,12 +9,12 @@
"@fortawesome/fontawesome-svg-core": "^6.5.2", "@fortawesome/fontawesome-svg-core": "^6.5.2",
"@fortawesome/free-solid-svg-icons": "^6.5.2", "@fortawesome/free-solid-svg-icons": "^6.5.2",
"@fortawesome/react-fontawesome": "^0.2.0", "@fortawesome/react-fontawesome": "^0.2.0",
"@jambonz/client-sdk-web": "^0.1.4",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"framer-motion": "^10.16.4", "framer-motion": "^10.16.4",
"fuse.js": "^6.6.2", "fuse.js": "^6.6.2",
"google-libphonenumber": "^3.2.33", "google-libphonenumber": "^3.2.33",
"jssip": "^3.10.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"uuid": "^9.0.1", "uuid": "^9.0.1",
Binary file not shown.
Binary file not shown.
+3 -9
View File
@@ -1,4 +1,5 @@
import { ConferenceModes } from "src/api/types"; import { ConferenceModes } from "src/api/types";
import type { ClientState } from "@jambonz/client-sdk-web";
export interface LoginCredential { export interface LoginCredential {
name?: string; name?: string;
@@ -97,12 +98,5 @@ export interface CallHistory {
isSaved?: boolean; isSaved?: boolean;
} }
export type SipClientStatus = export type SipClientStatus = ClientState;
| "start" export type SipCallDirection = "" | "outbound" | "inbound";
| "stop"
| "connecting"
| "connected"
| "disconnected"
| "registered"
| "unregistered";
export type SipCallDirection = "" | "outgoing" | "incoming";
+8 -44
View File
@@ -2,8 +2,6 @@ export default class SipAudioElements {
#ringing: HTMLAudioElement; #ringing: HTMLAudioElement;
#ringBack: HTMLAudioElement; #ringBack: HTMLAudioElement;
#failed: HTMLAudioElement; #failed: HTMLAudioElement;
#busy: HTMLAudioElement;
#remote: HTMLAudioElement;
#hungup: HTMLAudioElement; #hungup: HTMLAudioElement;
#localHungup: HTMLAudioElement; #localHungup: HTMLAudioElement;
@@ -16,13 +14,10 @@ export default class SipAudioElements {
this.#ringBack.volume = 0.8; this.#ringBack.volume = 0.8;
this.#failed = this.getAudio("audios/call-failed.mp3"); this.#failed = this.getAudio("audios/call-failed.mp3");
this.#failed.volume = 0.3; 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 = this.getAudio("audios/remote-party-hungup-tone.mp3");
this.#hungup.volume = 0.3; this.#hungup.volume = 0.3;
this.#localHungup = this.getAudio("audios/local-party-hungup-tone.mp3"); this.#localHungup = this.getAudio("audios/local-party-hungup-tone.mp3");
this.#localHungup.volume = 0.3; this.#localHungup.volume = 0.3;
this.#remote = new Audio();
} }
private getAudio(path: string) { private getAudio(path: string) {
@@ -43,19 +38,13 @@ export default class SipAudioElements {
return new Audio(audioURL); return new Audio(audioURL);
} }
playLocalHungup(volume: number | undefined) { playLocalHungup() {
this.pauseRingback(); this.pauseRingback();
this.pauseRinging(); this.pauseRinging();
if (volume) {
this.#localHungup.volume = volume;
}
this.#localHungup.play(); this.#localHungup.play();
} }
playRinging(volume: number | undefined): void { playRinging(): void {
if (volume) {
this.#ringing.volume = volume;
}
this.#ringing.play(); this.#ringing.play();
} }
@@ -65,10 +54,7 @@ export default class SipAudioElements {
} }
} }
playRingback(volume: number | undefined): void { playRingback(): void {
if (volume) {
this.#ringBack.volume = volume;
}
this.#ringBack.play(); this.#ringBack.play();
} }
@@ -78,47 +64,25 @@ export default class SipAudioElements {
} }
} }
playFailed(volume: number | undefined): void { playFailed(): void {
this.pauseRinging(); this.pauseRinging();
this.pauseRingback(); this.pauseRingback();
if (volume) {
this.#failed.volume = volume;
}
this.#failed.play(); this.#failed.play();
} }
playRemotePartyHungup(volume: number | undefined): void { playRemotePartyHungup(): void {
if (volume) {
this.#hungup.volume = volume;
}
this.#hungup.play(); this.#hungup.play();
} }
playAnswer(volume: number | undefined): void { playAnswer(): void {
this.pauseRinging(); this.pauseRinging();
this.pauseRingback(); this.pauseRingback();
} }
isRemoteAudioPaused(): boolean {
return this.#remote.paused;
}
playRemote(stream: MediaStream) {
this.#remote.srcObject = stream;
this.#remote.play();
}
stopAll() { stopAll() {
this.pauseRinging(); this.pauseRinging();
this.pauseRingback(); this.pauseRingback();
} this.#ringing.currentTime = 0;
this.#ringBack.currentTime = 0;
isPLaying(audio: HTMLAudioElement) {
return (
audio.currentTime > 0 &&
!audio.paused &&
!audio.ended &&
audio.readyState > audio.HAVE_CURRENT_DATA
);
} }
} }
-311
View File
@@ -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 });
}
}
-100
View File
@@ -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;
}
}
-219
View File
@@ -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
View File
@@ -1,18 +1,3 @@
import SipSession from "./SipSession";
import SessionManager from "./SipSessionManager";
import SipAudioElements from "./SipAudioElements"; 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 { export { SipAudioElements };
SipUA,
SipSession,
SessionManager,
SipAudioElements,
randomId,
normalizeNumber,
SipConstants,
SipModel
}
-20
View File
@@ -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";
-45
View File
@@ -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[];
}
-28
View File
@@ -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
View File
@@ -1,4 +1,4 @@
import { SipConstants } from "./../lib"; import { CallState } from "@jambonz/client-sdk-web";
import { deleteWindowIdKey, getWindowIdKey, saveWindowIdKey } from "./storage"; import { deleteWindowIdKey, getWindowIdKey, saveWindowIdKey } from "./storage";
import { PhoneNumberFormat, PhoneNumberUtil } from "google-libphonenumber"; import { PhoneNumberFormat, PhoneNumberUtil } from "google-libphonenumber";
@@ -44,19 +44,16 @@ const initiateNewPhonePopup = (callback: (v: unknown) => void) => {
}); });
}; };
export const isSipClientRinging = (callStatus: string) => { export const isSipClientRinging = (callState: CallState | null) => {
return callStatus === SipConstants.SESSION_RINGING; return callState === CallState.Ringing || callState === CallState.Connecting;
}; };
export const isSipClientAnswered = (callStatus: string) => { export const isSipClientAnswered = (callState: CallState | null) => {
return callStatus === SipConstants.SESSION_ANSWERED; return callState === CallState.Connected;
}; };
export const isSipClientIdle = (callStatus: string) => { export const isSipClientIdle = (callState: CallState | null) => {
return ( return callState === null || callState === CallState.Idle || callState === CallState.Ended;
callStatus === SipConstants.SESSION_ENDED ||
callStatus === SipConstants.SESSION_FAILED
);
}; };
export const normalizeUrl = (input: string): string => { export const normalizeUrl = (input: string): string => {
+9 -16
View File
@@ -10,11 +10,12 @@ import {
import Phone from "./phone"; import Phone from "./phone";
import Settings from "./settings"; import Settings from "./settings";
import { DEFAULT_COLOR_SCHEME } from "src/common/constants"; 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 { getActiveSettings, getCallHistories, getSettings } from "src/storage";
import CallHistories from "./history"; import CallHistories from "./history";
import { CallHistory, IAppSettings, SipClientStatus } from "src/common/types"; import { CallHistory, IAppSettings, SipClientStatus } from "src/common/types";
import { ClientState } from "@jambonz/client-sdk-web";
import Footer from "./footer/footer"; import Footer from "./footer/footer";
export const WindowApp = () => { export const WindowApp = () => {
@@ -27,33 +28,25 @@ export const WindowApp = () => {
const [calledNumber, setCalledNumber] = useState(""); const [calledNumber, setCalledNumber] = useState("");
const [calledName, setCalledName] = useState(""); const [calledName, setCalledName] = useState("");
const [tabIndex, setTabIndex] = useState(0); const [tabIndex, setTabIndex] = useState(0);
const [status, setStatus] = useState<SipClientStatus>("stop"); const [status, setStatus] = useState<SipClientStatus>(ClientState.Disconnected);
const [allSettings, setAllSettings] = useState<IAppSettings[]>([]); const [allSettings, setAllSettings] = useState<IAppSettings[]>([]);
const [advancedSettings, setAdvancedSettings] = useState<IAppSettings | null>( const [advancedSettings, setAdvancedSettings] = useState<IAppSettings | null>(
null null
); );
const [isSwitchingUserStatus, setIsSwitchingUserStatus] = useState(false); const [isSwitchingUserStatus, setIsSwitchingUserStatus] = useState(false);
const [isOnline, setIsOnline] = useState(false); const [isOnline, setIsOnline] = useState(false);
const phoneSipAschildRef = useRef<{ const [isUserOffline, setIsUserOffline] = useState(false);
updateGoOffline: (x: string) => void;
} | null>(null);
const handleGoOffline = (s: SipClientStatus) => { const handleGoOffline = (s: SipClientStatus) => {
if (s === status) { if (s === ClientState.Unregistered) {
return; setIsUserOffline(true);
} } else {
if (phoneSipAschildRef.current) { setIsUserOffline(false);
if (s === "unregistered") {
phoneSipAschildRef.current.updateGoOffline("stop");
} else {
phoneSipAschildRef.current.updateGoOffline("start");
}
} }
}; };
const loadSettings = () => { const loadSettings = () => {
const settings = getSettings(); const settings = getSettings();
const activeSettings = settings.find((el) => el.active); const activeSettings = settings.find((el) => el.active);
setAllSettings(getSettings()); setAllSettings(getSettings());
@@ -70,7 +63,6 @@ export const WindowApp = () => {
title: "Dialer", title: "Dialer",
content: ( content: (
<Phone <Phone
ref={phoneSipAschildRef}
sipUsername={sipUsername} sipUsername={sipUsername}
sipPassword={sipPassword} sipPassword={sipPassword}
sipDomain={sipDomain} sipDomain={sipDomain}
@@ -84,6 +76,7 @@ export const WindowApp = () => {
reload={loadSettings} reload={loadSettings}
setIsSwitchingUserStatus={setIsSwitchingUserStatus} setIsSwitchingUserStatus={setIsSwitchingUserStatus}
setIsOnline={setIsOnline} setIsOnline={setIsOnline}
isUserOffline={isUserOffline}
/> />
), ),
}, },
+9 -3
View File
@@ -2,6 +2,7 @@ import { HStack, Image, Text } from "@chakra-ui/react";
import jambonz from "src/imgs/jambonz.svg"; import jambonz from "src/imgs/jambonz.svg";
import { Dispatch, SetStateAction, useEffect, useState } from "react"; import { Dispatch, SetStateAction, useEffect, useState } from "react";
import { SipClientStatus } from "src/common/types"; import { SipClientStatus } from "src/common/types";
import { ClientState } from "@jambonz/client-sdk-web";
import JambonzSwitch from "src/components/switch"; import JambonzSwitch from "src/components/switch";
import "./styles.scss"; import "./styles.scss";
@@ -35,9 +36,12 @@ function Footer({
const [isConfigured, setIsConfigured] = useState(false); const [isConfigured, setIsConfigured] = useState(false);
useEffect(() => { useEffect(() => {
if (status === "registered" || status === "disconnected") { if (
status === ClientState.Registered ||
status === ClientState.Disconnected
) {
setIsSwitchingUserStatus(false); setIsSwitchingUserStatus(false);
setIsOnline(status === "registered"); setIsOnline(status === ClientState.Registered);
} }
}, [status, setIsSwitchingUserStatus, setIsOnline]); }, [status, setIsSwitchingUserStatus, setIsOnline]);
@@ -63,7 +67,9 @@ function Footer({
checked={[isOnline, setIsOnline]} checked={[isOnline, setIsOnline]}
onChange={(v) => { onChange={(v) => {
setIsSwitchingUserStatus(true); setIsSwitchingUserStatus(true);
onHandleGoOffline(v ? "registered" : "unregistered"); onHandleGoOffline(
v ? ClientState.Registered : ClientState.Unregistered
);
}} }}
/> />
<Text>You are {isOnline ? "online" : "offline"}</Text> <Text>You are {isOnline ? "online" : "offline"}</Text>
+2 -2
View File
@@ -35,9 +35,9 @@ export const CallHistoryItem = ({
}: CallHistoryItemProbs) => { }: CallHistoryItemProbs) => {
const [callEnable, setCallEnable] = useState(false); const [callEnable, setCallEnable] = useState(false);
const getDirectionIcon = (direction: SipCallDirection) => { const getDirectionIcon = (direction: SipCallDirection) => {
if (direction === "outgoing") { if (direction === "outbound") {
return faArrowRightFromBracket; return faArrowRightFromBracket;
} else if (direction === "incoming") { } else if (direction === "inbound") {
return faArrowRightToBracket; return faArrowRightToBracket;
} else { } else {
return faPhone; return faPhone;
+14 -19
View File
@@ -1,7 +1,7 @@
import { Text, UnorderedList, VStack } from "@chakra-ui/react"; import { Text, UnorderedList, VStack } from "@chakra-ui/react";
import CallHistoryItem from "./call-history-item"; import CallHistoryItem from "./call-history-item";
import { CallHistory } from "src/common/types"; import { CallHistory } from "src/common/types";
import { useEffect, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import Fuse from "fuse.js"; import Fuse from "fuse.js";
type RecentsProbs = { type RecentsProbs = {
@@ -19,27 +19,22 @@ export const Recents = ({
onDataChange, onDataChange,
onCallNumber, onCallNumber,
}: RecentsProbs) => { }: 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) { if (search) {
setCallHistories((prev) => return fuseInstance.search(search).map(({ item }) => item);
new Fuse(prev, {
keys: ["number"],
})
.search(search)
.map(({ item }) => item)
);
} else {
setCallHistories(
isSaved ? calls.filter((c) => c.isSaved === true) : calls
);
} }
}, [search]); return baseCalls;
}, [search, fuseInstance, baseCalls]);
useEffect(() => {
setCallHistories(isSaved ? calls.filter((c) => c.isSaved === true) : calls);
}, [calls]);
return ( return (
<VStack spacing={2}> <VStack spacing={2}>
+12 -8
View File
@@ -2,8 +2,14 @@ export default class DialPadAudioElements {
private keySounds: { [key: string]: HTMLAudioElement | undefined } = {}; private keySounds: { [key: string]: HTMLAudioElement | undefined } = {};
constructor() { constructor() {
const arr = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "*", "#"]; const keys = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "*", "#"];
for (const i of arr) { const fileMap: Record<string, string> = {
"*": "star",
"#": "hash",
};
for (const key of keys) {
const fileName = fileMap[key] || key;
let audioURL; let audioURL;
// Check if we're in a Chrome extension // Check if we're in a Chrome extension
@@ -12,16 +18,14 @@ export default class DialPadAudioElements {
chrome.runtime && chrome.runtime &&
chrome.runtime.getURL chrome.runtime.getURL
) { ) {
audioURL = chrome.runtime.getURL( audioURL = chrome.runtime.getURL(`audios/dtmf-${fileName}.mp3`);
`audios/dtmf-${encodeURIComponent(i)}.mp3`
);
} else { } else {
// We're in a web context, adjust this path as necessary // 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); this.keySounds[key] = new Audio(audioURL);
const audio = this.keySounds[i]; const audio = this.keySounds[key];
if (audio) { if (audio) {
audio.volume = 0.5; audio.volume = 0.5;
} }
+21 -24
View File
@@ -16,12 +16,12 @@ import { updateConferenceParticipantAction } from "src/api";
import { ConferenceModes } from "src/api/types"; import { ConferenceModes } from "src/api/types";
import { DEFAULT_TOAST_DURATION } from "src/common/constants"; import { DEFAULT_TOAST_DURATION } from "src/common/constants";
import OutlineBox from "src/components/outline-box"; import OutlineBox from "src/components/outline-box";
import { SipConstants } from "src/lib";
import { import {
deleteConferenceSettings, deleteConferenceSettings,
getConferenceSettings, getConferenceSettings,
saveConferenceSettings, saveConferenceSettings,
} from "src/storage"; } from "src/storage";
import { CallState } from "@jambonz/client-sdk-web";
type JoinConferenceProbs = { type JoinConferenceProbs = {
conferenceId?: string; conferenceId?: string;
@@ -60,9 +60,21 @@ export const JoinConference = ({
); );
const [participantState, setParticipantState] = useState("Join as"); 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(() => { useEffect(() => {
switch (callStatus) { switch (callStatus) {
case SipConstants.SESSION_ANSWERED: case CallState.Connected:
setAppTitle("Conference"); setAppTitle("Conference");
setSubmitTitle("Update"); setSubmitTitle("Update");
setCancelTitle("Hangup"); setCancelTitle("Hangup");
@@ -70,8 +82,8 @@ export const JoinConference = ({
setIsLoading(false); setIsLoading(false);
configureConferenceSession(); configureConferenceSession();
break; break;
case SipConstants.SESSION_ENDED: case CallState.Ended:
case SipConstants.SESSION_FAILED: case CallState.Idle:
setIsLoading(false); setIsLoading(false);
deleteConferenceSettings(); deleteConferenceSettings();
break; break;
@@ -80,7 +92,7 @@ export const JoinConference = ({
const handleSubmit = (e: FormEvent) => { const handleSubmit = (e: FormEvent) => {
e.preventDefault(); e.preventDefault();
if (callStatus !== SipConstants.SESSION_ANSWERED) { if (callStatus !== CallState.Connected) {
call(conferenceName); call(conferenceName);
if (!callSid) { if (!callSid) {
setIsLoading(true); setIsLoading(true);
@@ -145,7 +157,7 @@ export const JoinConference = ({
</Text> </Text>
{callDuration > 0 && ( {callDuration > 0 && (
<Text fontSize="15px"> <Text fontSize="15px">
{new Date(callDuration * 1000).toISOString().substr(11, 8)} {new Date(callDuration * 1000).toISOString().substring(11, 19)}
</Text> </Text>
)} )}
<FormControl id="conference_name"> <FormControl id="conference_name">
@@ -163,12 +175,7 @@ export const JoinConference = ({
<OutlineBox title={participantState}> <OutlineBox title={participantState}>
<RadioGroup <RadioGroup
onChange={(e) => { onChange={(e) => {
setMode(e as ConferenceModes); updateSettings({ mode: e as ConferenceModes });
saveConferenceSettings({
mode: e as ConferenceModes,
speakOnlyTo,
tags,
});
}} }}
value={mode} value={mode}
colorScheme="jambonz" colorScheme="jambonz"
@@ -189,12 +196,7 @@ export const JoinConference = ({
placeholder="tag" placeholder="tag"
value={speakOnlyTo} value={speakOnlyTo}
onChange={(e) => { onChange={(e) => {
setSpeakOnlyTo(e.target.value); updateSettings({ speakOnlyTo: e.target.value });
saveConferenceSettings({
mode,
speakOnlyTo: e.target.value,
tags,
});
}} }}
disabled={mode !== "coach"} disabled={mode !== "coach"}
required={mode === "coach"} required={mode === "coach"}
@@ -208,12 +210,7 @@ export const JoinConference = ({
placeholder="tag" placeholder="tag"
value={tags} value={tags}
onChange={(e) => { onChange={(e) => {
setTags(e.target.value); updateSettings({ tags: e.target.value });
saveConferenceSettings({
mode,
speakOnlyTo,
tags: e.target.value,
});
}} }}
/> />
</FormControl> </FormControl>
+18 -11
View File
@@ -1,6 +1,6 @@
import { Box, Button, HStack, VStack } from "@chakra-ui/react"; import { Box, Button, HStack, VStack } from "@chakra-ui/react";
import DialPadAudioElements from "./DialPadSoundElement"; import DialPadAudioElements from "./DialPadSoundElement";
import { useEffect, useRef } from "react"; import { useCallback, useEffect, useRef } from "react";
type DialPadProbs = { type DialPadProbs = {
handleDigitPress: (digit: string, fromKeyboard: boolean) => void; handleDigitPress: (digit: string, fromKeyboard: boolean) => void;
@@ -8,17 +8,24 @@ type DialPadProbs = {
const keySounds = new DialPadAudioElements(); const keySounds = new DialPadAudioElements();
const buttons = [
["1", "2", "3"],
["4", "5", "6"],
["7", "8", "9"],
["*", "0", "#"],
];
export const DialPad = ({ handleDigitPress }: DialPadProbs) => { export const DialPad = ({ handleDigitPress }: DialPadProbs) => {
const selfRef = useRef<HTMLDivElement | null>(null); const selfRef = useRef<HTMLDivElement | null>(null);
const isVisibleRef = useRef(false); const isVisibleRef = useRef(false);
const buttons = [ const handleDigitPressRef = useRef(handleDigitPress);
["1", "2", "3"],
["4", "5", "6"],
["7", "8", "9"],
["*", "0", "#"],
];
const handleKeyDown = (e: KeyboardEvent) => { // Keep ref in sync so the keydown listener always uses the latest callback
useEffect(() => {
handleDigitPressRef.current = handleDigitPress;
}, [handleDigitPress]);
const handleKeyDown = useCallback((e: KeyboardEvent) => {
if ( if (
["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "*", "#"].includes( ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "*", "#"].includes(
e.key e.key
@@ -26,10 +33,10 @@ export const DialPad = ({ handleDigitPress }: DialPadProbs) => {
) { ) {
if (isVisibleRef.current) { if (isVisibleRef.current) {
keySounds?.playKeyTone(e.key); keySounds?.playKeyTone(e.key);
handleDigitPress(e.key, true); handleDigitPressRef.current(e.key, true);
} }
} }
}; }, []);
useEffect(() => { useEffect(() => {
const observer = new IntersectionObserver( const observer = new IntersectionObserver(
@@ -50,7 +57,7 @@ export const DialPad = ({ handleDigitPress }: DialPadProbs) => {
observer.unobserve(selfRef.current); observer.unobserve(selfRef.current);
} }
}; };
}, []); }, [handleKeyDown]);
return ( return (
<Box p={2} w="full" h="280px" ref={selfRef}> <Box p={2} w="full" h="280px" ref={selfRef}>
+745 -745
View File
File diff suppressed because it is too large Load Diff