diff --git a/src/api/index.ts b/src/api/index.ts index 813d28d..8b33495 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,5 +1,7 @@ import { Application, + ConferenceParticipantAction, + ConferenceParticipantActions, FetchError, FetchTransport, Queue, @@ -8,6 +10,7 @@ import { } from "./types"; import { MSG_SOMETHING_WRONG } from "./constants"; import { getAdvancedSettings } from "src/storage"; +import { EmptyData } from "src/common/types"; const fetchTransport = ( url: string, @@ -152,3 +155,21 @@ export const getSelfRegisteredUser = (username: string) => { `${advancedSettings.apiServer}/Accounts/${advancedSettings.accountSid}/RegisteredSipUsers/${username}` ); }; + +export const getConferences = () => { + const advancedSettings = getAdvancedSettings(); + return getFetch( + `${advancedSettings.apiServer}/Accounts/${advancedSettings.accountSid}/Conferences` + ); +}; + +export const updateConferenceParticipantAction = ( + callSid: string, + payload: ConferenceParticipantAction +) => { + const advancedSettings = getAdvancedSettings(); + return putFetch( + `${advancedSettings.apiServer}/Accounts/${advancedSettings.accountSid}/Calls/${callSid}`, + payload + ); +}; diff --git a/src/api/types.ts b/src/api/types.ts index a7fc7b1..8827ced 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -45,3 +45,20 @@ export interface RegisteredUser { allow_direct_user_calling: boolean; registered_status: string; } + +export type ConferenceParticipantActions = + | "tag" + | "untag" + | "coach" + | "mute" + | "unmute" + | "hold" + | "unhold"; + +export interface ConferenceParticipantAction { + action: ConferenceParticipantActions; + tag: string; +} +export interface UpdateCall { + conferenceParticipantAction: ConferenceParticipantAction; +} diff --git a/src/lib/SipSession.ts b/src/lib/SipSession.ts index 2210487..b88930d 100644 --- a/src/lib/SipSession.ts +++ b/src/lib/SipSession.ts @@ -62,12 +62,18 @@ export default class SipSession extends events.EventEmitter { } }); - this.#rtcSession.on("accepted", () => { - this.emit(SipConstants.SESSION_ANSWERED, { - status: SipConstants.SESSION_ANSWERED, - }); - this.#audio.playAnswer(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; @@ -195,16 +201,19 @@ export default class SipSession extends events.EventEmitter { 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, "track"); - // this.emit(SipConstants.SESSION_TRACK, { - // 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() { diff --git a/src/lib/SipSessionManager.ts b/src/lib/SipSessionManager.ts index 8d08166..4e96afd 100644 --- a/src/lib/SipSessionManager.ts +++ b/src/lib/SipSessionManager.ts @@ -1,108 +1,100 @@ -import { - SipSession, SipModel, SipConstants -} from "./index"; - +import { SipSession, SipModel, SipConstants } from "./index"; export default class SipSessionManager { + #sessions: Map; - #sessions: Map; + constructor() { + this.#sessions = new Map(); + } - 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"); } - 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); - } - }); + const state = [...this.#sessions.values()].filter((s) => s.active); + if (state.length) { + return state[0].sipSession; + } else { + throw new Error("No Active sessions"); } + } - 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; - } -} \ No newline at end of file + get count() { + return this.#sessions.size; + } +} diff --git a/src/window/phone/incoming-call.tsx b/src/window/phone/incoming-call.tsx index 2effc7d..91d954e 100644 --- a/src/window/phone/incoming-call.tsx +++ b/src/window/phone/incoming-call.tsx @@ -28,8 +28,8 @@ export const IncommingCall = ({ as={FontAwesomeIcon} icon={faPhone} color="jambonz.500" - width="60px" - height="60px" + width="30px" + height="30px" /> Incoming call from diff --git a/src/window/phone/index.tsx b/src/window/phone/index.tsx index b742a84..61c0fc7 100644 --- a/src/window/phone/index.tsx +++ b/src/window/phone/index.tsx @@ -42,6 +42,7 @@ import { v4 as uuidv4 } from "uuid"; import IconButtonMenu, { IconButtonMenuItems } from "src/components/menu"; import { getApplications, + getConferences, getQueues, getRegisteredUser, getSelfRegisteredUser, @@ -126,6 +127,7 @@ export const Phone = ({ allow_direct_user_calling: false, } ); + const callSidRef = useRef(""); const toast = useToast(); useEffect(() => { @@ -204,9 +206,12 @@ export const Phone = ({ }, [status]); useEffect(() => { - setInterval(() => { + const timer = setInterval(() => { fetchRegisterUser(); - }, 10_000); + }, 10000); + return () => { + clearInterval(timer); + }; }, []); const fetchRegisterUser = () => { @@ -311,6 +316,7 @@ export const Phone = ({ setInputNumber(args.session.user); }); sipClient.on(SipConstants.SESSION_ANSWERED, (args) => { + callSidRef.current = args.callSid; const currentCall = getCurrentCall(); if (currentCall) { currentCall.timeStamp = Date.now(); @@ -628,17 +634,34 @@ export const Phone = ({ tooltip="Join a conference" noResultLabel="No conference" onClick={(name, value) => { - setPageView(PAGE_VIEW.START_NEW_CONFERENCE); + if (value === PAGE_VIEW.START_NEW_CONFERENCE.toString()) { + setPageView(PAGE_VIEW.START_NEW_CONFERENCE); + } else { + setPageView(PAGE_VIEW.JOIN_CONFERENCE); + } }} onOpen={() => { - return new Promise((resolve) => { - resolve([ - { - name: "Start New Conference", - value: "start_new_conference", - }, - ]); - }); + return new Promise( + (resolve, reject) => { + getConferences() + .then(({ json }) => { + const sortedApps = json.sort((a, b) => + a.localeCompare(b) + ); + resolve([ + { + name: "Start new conference", + value: PAGE_VIEW.START_NEW_CONFERENCE.toString(), + }, + ...sortedApps.map((a) => ({ + name: a, + value: a, + })), + ]); + }) + .catch((err) => reject(err)); + } + ); }} />