This commit is contained in:
Quan HL
2024-05-08 19:27:25 +07:00
parent 7d6efbfbaf
commit 7046813a4e
6 changed files with 251 additions and 125 deletions

View File

@@ -7,6 +7,7 @@ import {
Queue, Queue,
RegisteredUser, RegisteredUser,
StatusCodes, StatusCodes,
UpdateCall,
} from "./types"; } from "./types";
import { MSG_SOMETHING_WRONG } from "./constants"; import { MSG_SOMETHING_WRONG } from "./constants";
import { getAdvancedSettings } from "src/storage"; import { getAdvancedSettings } from "src/storage";
@@ -168,8 +169,10 @@ export const updateConferenceParticipantAction = (
payload: ConferenceParticipantAction payload: ConferenceParticipantAction
) => { ) => {
const advancedSettings = getAdvancedSettings(); const advancedSettings = getAdvancedSettings();
return putFetch<EmptyData, ConferenceParticipantAction>( return putFetch<EmptyData, UpdateCall>(
`${advancedSettings.apiServer}/Accounts/${advancedSettings.accountSid}/Calls/${callSid}`, `${advancedSettings.apiServer}/Accounts/${advancedSettings.accountSid}/Calls/${callSid}`,
payload {
conferenceParticipantAction: payload,
}
); );
}; };

View File

@@ -50,11 +50,14 @@ export type ConferenceParticipantActions =
| "tag" | "tag"
| "untag" | "untag"
| "coach" | "coach"
| "uncoach"
| "mute" | "mute"
| "unmute" | "unmute"
| "hold" | "hold"
| "unhold"; | "unhold";
export type ConferenceModes = "full_participant" | "muted" | "coach";
export interface ConferenceParticipantAction { export interface ConferenceParticipantAction {
action: ConferenceParticipantActions; action: ConferenceParticipantActions;
tag: string; tag: string;

View File

@@ -3,7 +3,11 @@ function normalizeNumber(number: string): string {
return number; return number;
} else if (/@/i.test(number)) { } else if (/@/i.test(number)) {
return number; return number;
} else if (number.startsWith("app-") || number.startsWith("queue-")) { } else if (
number.startsWith("app-") ||
number.startsWith("queue-") ||
number.startsWith("conference-")
) {
return number; return number;
} else { } else {
return number.replace(/[()\-. ]*/g, ""); return number.replace(/[()\-. ]*/g, "");

View File

@@ -0,0 +1,190 @@
import {
Box,
Button,
Checkbox,
FormControl,
FormLabel,
HStack,
Input,
Radio,
RadioGroup,
Text,
VStack,
} from "@chakra-ui/react";
import { FormEvent, useEffect, useState } from "react";
import { updateConferenceParticipantAction } from "src/api";
import { ConferenceModes } from "src/api/types";
import OutlineBox from "src/components/outline-box";
import { SipConstants } from "src/lib";
type JoinConferenceProbs = {
conferenceId?: string;
callSid: string;
callDuration: number;
callStatus: string;
handleCancel: () => void;
call: (conference: string) => void;
};
export const JoinConference = ({
conferenceId,
callSid,
callDuration,
callStatus,
handleCancel,
call,
}: JoinConferenceProbs) => {
const [conferenceName, setConferenceName] = useState(conferenceId || "");
const [appTitle, setAppTitle] = useState(
!!conferenceId ? "Joining Conference" : "Start Conference"
);
const [submitTitle, setSubmitTitle] = useState(
!!conferenceId ? "Joining Conference" : "Start Conference"
);
const [cancelTitle, setCancelTitle] = useState("Cancel");
const [isLoading, setIsLoading] = useState(false);
const [speakOnlyTo, setSpeakOnlyTo] = useState("");
const [tags, setTags] = useState("");
const [mode, setMode] = useState<ConferenceModes>("full_participant");
useEffect(() => {
switch (callStatus) {
case SipConstants.SESSION_ANSWERED:
setAppTitle("Conference");
setSubmitTitle("Update");
setCancelTitle("Hangup");
setIsLoading(false);
configureConferenceSession();
break;
case SipConstants.SESSION_ENDED:
case SipConstants.SESSION_FAILED:
setIsLoading(false);
break;
}
}, [callStatus]);
useEffect(() => {
switch (mode) {
case "full_participant":
break;
case "muted":
break;
case "coach":
break;
}
}, [mode]);
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
if (callStatus !== SipConstants.SESSION_ANSWERED) {
call(conferenceName);
if (!callSid) {
setIsLoading(true);
}
} else {
configureConferenceSession();
}
};
const configureConferenceSession = async () => {
if (callSid) {
await updateConferenceParticipantAction(callSid, {
action: mode === "muted" ? "mute" : "unmute",
tag: "",
});
await updateConferenceParticipantAction(callSid, {
action: tags ? "tag" : "untag",
tag: tags,
});
await updateConferenceParticipantAction(callSid, {
action: mode === "coach" ? "coach" : "uncoach",
tag: speakOnlyTo,
});
}
};
return (
<Box as="form" onSubmit={handleSubmit} w="full">
<VStack spacing={4} mt="20px" w="full">
<Text fontWeight="bold">{appTitle}</Text>
{callDuration > 0 && (
<Text fontSize="15px">
{new Date(callDuration * 1000).toISOString().substr(11, 8)}
</Text>
)}
<FormControl id="conference_name">
<FormLabel>Conference name</FormLabel>
<Input
type="text"
placeholder="Name"
isRequired
value={conferenceName}
onChange={(e) => setConferenceName(e.target.value)}
disabled={!!conferenceId}
/>
</FormControl>
<OutlineBox title="Join as">
<RadioGroup
onChange={(e) => setMode(e as ConferenceModes)}
value={mode}
colorScheme="jambonz"
>
<VStack align="start">
<Radio value="full_participant" variant="">
Full participant
</Radio>
<Radio value="muted">Muted</Radio>
<Radio value="coach">Coach mode</Radio>
</VStack>
</RadioGroup>
<FormControl id="speak_only_to">
<FormLabel>Speak only to</FormLabel>
<Input
type="text"
placeholder="tag"
value={speakOnlyTo}
onChange={(e) => setSpeakOnlyTo(e.target.value)}
disabled={mode !== "coach"}
required={mode === "coach"}
/>
</FormControl>
<FormControl id="tag">
<FormLabel>Tag</FormLabel>
<Input
type="text"
placeholder="tag"
value={tags}
onChange={(e) => setTags(e.target.value)}
/>
</FormControl>
</OutlineBox>
<HStack w="full">
<Button
colorScheme="jambonz"
type="submit"
w="full"
isLoading={isLoading}
>
{submitTitle}
</Button>
<Button
colorScheme="grey"
type="button"
w="full"
textColor="black"
onClick={handleCancel}
>
{cancelTitle}
</Button>
</HStack>
</VStack>
</Box>
);
};
export default JoinConference;

View File

@@ -62,7 +62,7 @@ import {
faUserGroup, faUserGroup,
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import JoinConference from "./join-conference"; import JoinConference from "./conference";
type PhoneProbs = { type PhoneProbs = {
sipDomain: string; sipDomain: string;
@@ -198,7 +198,9 @@ export const Phone = ({
if (calledANumber) { if (calledANumber) {
if ( if (
!( !(
calledANumber.startsWith("app-") || calledANumber.startsWith("queue-") calledANumber.startsWith("app-") ||
calledANumber.startsWith("queue-") ||
calledANumber.startsWith("conference-")
) )
) { ) {
setInputNumber(calledANumber); setInputNumber(calledANumber);
@@ -641,41 +643,44 @@ export const Phone = ({
}} }}
/> />
)} )}
{registeredUser.allow_direct_app_calling && (
<IconButtonMenu <IconButtonMenu
icon={<FontAwesomeIcon icon={faPeopleGroup} />} icon={<FontAwesomeIcon icon={faPeopleGroup} />}
tooltip="Join a conference" tooltip="Join a conference"
noResultLabel="No conference" noResultLabel="No conference"
onClick={(name, value) => { onClick={(name, value) => {
setPageView(PAGE_VIEW.JOIN_CONFERENCE); setPageView(PAGE_VIEW.JOIN_CONFERENCE);
setSelectedConference( setSelectedConference(
value === PAGE_VIEW.JOIN_CONFERENCE.toString() ? "" : value value === PAGE_VIEW.JOIN_CONFERENCE.toString()
); ? ""
}} : value
onOpen={() => { );
return new Promise<IconButtonMenuItems[]>( }}
(resolve, reject) => { onOpen={() => {
getConferences() return new Promise<IconButtonMenuItems[]>(
.then(({ json }) => { (resolve, reject) => {
const sortedApps = json.sort((a, b) => getConferences()
a.localeCompare(b) .then(({ json }) => {
); const sortedApps = json.sort((a, b) =>
resolve([ a.localeCompare(b)
{ );
name: "Start new conference", resolve([
value: PAGE_VIEW.JOIN_CONFERENCE.toString(), {
}, name: "Start new conference",
...sortedApps.map((a) => ({ value: PAGE_VIEW.JOIN_CONFERENCE.toString(),
name: a, },
value: a, ...sortedApps.map((a) => ({
})), name: a,
]); value: a,
}) })),
.catch((err) => reject(err)); ]);
} })
); .catch((err) => reject(err));
}} }
/> );
}}
/>
)}
</HStack> </HStack>
)} )}
@@ -793,12 +798,17 @@ export const Phone = ({
<JoinConference <JoinConference
conferenceId={selectedConference} conferenceId={selectedConference}
callSid={callSid} callSid={callSid}
callDuration={seconds}
callStatus={callStatus}
handleCancel={() => { handleCancel={() => {
if (isSipClientAnswered(callStatus)) {
sipUA.current?.terminate(480, "Call Finished", undefined);
}
setPageView(PAGE_VIEW.DIAL_PAD); setPageView(PAGE_VIEW.DIAL_PAD);
}} }}
call={(name) => { call={(name) => {
setSelectedConference(name); setSelectedConference(name);
sipUA.current?.call(`conf:${name}`); sipUA.current?.call(`conference-${name}`);
}} }}
/> />
)} )}

View File

@@ -1,84 +0,0 @@
import {
Box,
Button,
Checkbox,
FormControl,
FormLabel,
HStack,
Input,
Text,
VStack,
} from "@chakra-ui/react";
import { FormEvent, useState } from "react";
import OutlineBox from "src/components/outline-box";
type JoinConferenceProbs = {
conferenceId?: string;
callSid: string;
handleCancel: () => void;
call: (conference: string) => void;
};
export const JoinConference = ({
conferenceId,
handleCancel,
call,
}: JoinConferenceProbs) => {
const [conferenceName, setConferenceName] = useState(conferenceId || "");
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
call(conferenceName);
};
return (
<Box as="form" onSubmit={handleSubmit} w="full">
<VStack spacing={4} mt="20px" w="full">
<Text fontWeight="bold">
{!!conferenceId ? "Joining" : "Start"} conference
</Text>
<FormControl id="conference_name">
<FormLabel>Conference name</FormLabel>
<Input
type="text"
placeholder="Name"
isRequired
value={conferenceName}
onChange={(e) => setConferenceName(e.target.value)}
disabled={!!conferenceId}
/>
</FormControl>
<OutlineBox title="Join as">
<Checkbox colorScheme="jambonz">Full participant</Checkbox>
<Checkbox colorScheme="jambonz">Muted</Checkbox>
<Checkbox colorScheme="jambonz">Coach mode</Checkbox>
<FormControl id="speak_only_to">
<FormLabel>Speak only to</FormLabel>
<Input type="text" placeholder="agent" />
</FormControl>
<FormControl id="tag">
<FormLabel>Tag</FormLabel>
<Input type="text" placeholder="tags" />
</FormControl>
</OutlineBox>
<HStack w="full">
<Button colorScheme="jambonz" type="submit" w="full">
{!!conferenceId ? "Join conference" : "Start conference"}
</Button>
<Button
colorScheme="grey"
type="button"
w="full"
textColor="black"
onClick={handleCancel}
>
Cancel
</Button>
</HStack>
</VStack>
</Box>
);
};
export default JoinConference;