Add ability to make new call

This commit is contained in:
2020-03-14 23:18:27 -04:00
parent 70aaa79e69
commit 44e8489742
15 changed files with 132 additions and 91 deletions

View File

@@ -25,14 +25,14 @@
left: 2px; left: 2px;
} }
.answer { .answer, .makeCall {
composes: button; composes: button;
background-color: #45bd57; background-color: #45bd57;
} }
.answer:hover { .answer:hover, .makeCall:hover {
background-image: linear-gradient(#84b68c, #5fb86c); background-image: linear-gradient(#84b68c, #5fb86c);
} }
.answer:after { .answer:after, .makeCall:after{
content: url(/phone-solid.svg); content: url(/phone-solid.svg);
transform: scale(0.65); transform: scale(0.65);
} }

View File

@@ -1,6 +0,0 @@
import React from 'react';
const DialBox = () => {
return;
}
export default DialBox;

View File

@@ -1,11 +0,0 @@
import React from 'react';
import { DeviceState } from '../../hooks/voice-state';
export default function DisplayDeviceState({deviceState}: {deviceState: DeviceState})
{
return(
<div>
Device is currently {deviceState}.
</div>
);
}

View File

@@ -2,47 +2,47 @@ import React from 'react';
import { Voice } from '../../wrappers/voice'; import { Voice } from '../../wrappers/voice';
import { voiceToken } from '../../wrappers/default-config'; import { voiceToken } from '../../wrappers/default-config';
import Button from './Button/button'; import Button from './Button/button';
import DeviceState from './display-device-state'; import DeviceState from './DeviceState/display-device-state';
import DtmfButtons from './DtmfButtons/dtmf-buttons'; import DialButtons from './DialButtons/dial-buttons';
import DialBox from './DialBox/dial-box';
export default function Phone({voice:{ startDevice, answer, reject, hangup, dial, dtmf, voiceState} }: {voice: Voice}) export default function Phone({voice:{ startDevice, answer, reject, hangup, dialDigit, makeCall, voiceState} }: {voice: Voice})
{ {
if (voiceState.deviceState === "Offline")
{
return (
<div className="Phone">
<DeviceState deviceState = {voiceState.deviceState}/>
<Button title = "connect" onClick = {() => startDevice(voiceToken)}/>
</div>
);
}
else if(voiceState.deviceState === "Ringing")
{
return (
<div className="Phone">
<DeviceState deviceState = {voiceState.deviceState}/>
<Button title = "reject" onClick = {() => reject()}/>
<Button title = "answer" onClick = {() => answer()}/>
</div>
);
}
else if(voiceState.deviceState === "Connected")
{
return (
<div className="Phone">
<DeviceState deviceState = {voiceState.deviceState}/>
<DtmfButtons dtmf = {dtmf} />
<Button title = "hangup" onClick = {() => hangup()}/>
</div>
);
}
else
{
return (
<div className="Phone">
<DeviceState deviceState = {voiceState.deviceState}/>
</div>
);
}
switch (voiceState.deviceState)
{
case "Offline":
return (
<div className="Phone">
<DeviceState deviceState = {voiceState.deviceState} phoneNumber = {voiceState.callPhoneNumber}/>
<Button title = "connect" onClick = {() => startDevice(voiceToken)}/>
</div>
);
case "Ringing":
return (
<div className="Phone">
<DeviceState deviceState = {voiceState.deviceState} phoneNumber = {voiceState.callPhoneNumber}/>
<Button title = "reject" onClick = {() => reject()}/>
<Button title = "answer" onClick = {() => answer()}/>
</div>
);
case "Connected":
return (
<div className="Phone">
<DeviceState deviceState = {voiceState.deviceState} phoneNumber = {voiceState.callPhoneNumber}/>
<DialBox dialDigits = {voiceState.dialDigits} />
<DialButtons action = {dialDigit} />
<Button title = "hangup" onClick = {() => hangup()}/>
</div>
);
default:
return (
<div className="Phone">
<DeviceState deviceState = {voiceState.deviceState} phoneNumber = {voiceState.callPhoneNumber}/>
<DialBox dialDigits = {voiceState.dialDigits} />
<DialButtons action = {dialDigit} />
<Button title = "makeCall" onClick = {() => makeCall()}/>
</div>
);
}
} }

View File

@@ -8,11 +8,13 @@ export interface VoiceState
callCreatedTime: number; callCreatedTime: number;
callEstablishedTime: number; callEstablishedTime: number;
callPhoneNumber: string; callPhoneNumber: string;
dialDigits: string;
} }
export interface VoiceStateManager export interface VoiceStateManager
{ {
onDeviceStateUpdate(state: DeviceState, phoneNumber?: string): void; onDeviceStateUpdate(state: DeviceState, phoneNumber?: string): void;
setDialDigits(digits: string): void;
voiceState: VoiceState; voiceState: VoiceState;
} }
@@ -23,6 +25,9 @@ export default function useVoiceState(): VoiceStateManager
const [callEstablishedTime, setCallEstablishedTime] = useState<number>(0); const [callEstablishedTime, setCallEstablishedTime] = useState<number>(0);
const [callPhoneNumber, setCallPhoneNumber] = useState<string>(""); const [callPhoneNumber, setCallPhoneNumber] = useState<string>("");
// Used for DTMF and number to dial
const [dialDigits, setDialDigits] = useState<string>("");
const onDeviceStateUpdate = (state: DeviceState, phoneNumber?: string) => const onDeviceStateUpdate = (state: DeviceState, phoneNumber?: string) =>
{ {
@@ -39,15 +44,20 @@ export default function useVoiceState(): VoiceStateManager
setDeviceState(state); setDeviceState(state);
if (phoneNumber) { setCallPhoneNumber(phoneNumber); } if (phoneNumber) { setCallPhoneNumber(phoneNumber); }
// Wipe of the dialing digits on every state change.
setDialDigits("");
} }
return { return {
onDeviceStateUpdate, onDeviceStateUpdate,
setDialDigits,
voiceState: { voiceState: {
deviceState, deviceState,
callCreatedTime, callCreatedTime,
callEstablishedTime, callEstablishedTime,
callPhoneNumber callPhoneNumber,
dialDigits
} }
} }
} }

View File

@@ -3,10 +3,10 @@ import { defaultDelay } from "./mock-device";
export class MockConnection implements Connection export class MockConnection implements Connection
{ {
private from = "+16133713909"; private from: string = "+16133713909";
private to = "+123456789"; private to: string = "+123456789";
public parameters: ConnectionParameters = { From: this.from, To: this.to } as ConnectionParameters; public get parameters() { return { From: this.from, To: this.to } as ConnectionParameters; }
public customParameters: Map<string, string> = new Map(); public customParameters: Map<string, string> = new Map();
public options: object = {}; public options: object = {};
@@ -14,6 +14,16 @@ export class MockConnection implements Connection
private readonly handlers = {} as {[key in ConnectionEvent]: ((mute?: boolean) => void)[]}; private readonly handlers = {} as {[key in ConnectionEvent]: ((mute?: boolean) => void)[]};
private muted: boolean = false; private muted: boolean = false;
constructor(phoneNumber?: string)
{
if (phoneNumber)
{
this.to = phoneNumber;
this.from = "";
this.executeHandler("accept");
}
}
private executeHandler(handlerName: ConnectionEvent) private executeHandler(handlerName: ConnectionEvent)
{ {
setTimeout(() => this.handlers[handlerName]?.forEach(handler => handler()), defaultDelay); setTimeout(() => this.handlers[handlerName]?.forEach(handler => handler()), defaultDelay);

View File

@@ -8,23 +8,31 @@ export class MockDevice implements Device
public audio: DeviceAudio = {} as DeviceAudio; public audio: DeviceAudio = {} as DeviceAudio;
private deviceStatus: DeviceStatus = "offline"; private deviceStatus: DeviceStatus = "offline";
private readonly handlers = {} as {[key in DeviceEvent]: ((connection?: Connection) => void)[]}; private readonly handlers = {} as {[key in DeviceEvent]: ((connection?: Connection) => void)[]};
private currentConnection: Connection | undefined;
constructor(token: string, params?: SetupParams) constructor(token: string, params?: SetupParams)
{ {
setTimeout(() => this.handlers["ready"].forEach(handler => handler()), defaultDelay); setTimeout(() => this.handlers["ready"].forEach(handler => handler()), defaultDelay);
setTimeout(() => this.handlers["incoming"].forEach(handler => handler(new MockConnection())), 2000); setTimeout(() =>
{
this.currentConnection = new MockConnection();
this.handlers["incoming"].forEach(handler => handler(this.currentConnection));
},
2000);
} }
public setup(token: string, params?: SetupParams): void {} public setup(token: string, params?: SetupParams): void {}
public connect(params: any, audioConstraints?: any): Connection public connect(params: any, audioConstraints?: any): Connection
{ {
return {} as Connection; this.currentConnection = new MockConnection(params.To);
return this.currentConnection;
} }
public activeConnection(): Connection public activeConnection(): Connection | undefined
{ {
return {} as Connection; return this.currentConnection;
} }
public destroy(): void public destroy(): void

View File

@@ -9,8 +9,8 @@ export interface Voice
answer(): void; answer(): void;
reject(): void; reject(): void;
hangup(): void; hangup(): void;
dial(phoneNumber: string): void; dialDigit(digit: string): void;
dtmf(digit: string): void; makeCall(): void;
voiceState: VoiceState; voiceState: VoiceState;
} }
@@ -72,20 +72,27 @@ export default function useVoice()
device?.disconnectAll(); device?.disconnectAll();
} }
const dial = (phoneNumber: string) => const dialDigit = (digit: string) =>
{ {
const connection = device?.connect({To: phoneNumber}); if (state.voiceState.deviceState === "Connected")
{
connection?.on("accept", () => state.onDeviceStateUpdate("Connected")); connection?.sendDigits(digit);
connection?.on("disconnect", () => state.onDeviceStateUpdate("Disconnected")); }
connection?.on("mute", muted => state.onDeviceStateUpdate(muted ? "Muted" : "Connected")); state.setDialDigits(state.voiceState.dialDigits + digit);
setConnection(connection);
} }
const dtmf = (digit: string) => const makeCall = () =>
{ {
connection?.sendDigits(digit); if (state.voiceState.dialDigits)
{
const connection = device?.connect({To: state.voiceState.dialDigits});
connection?.on("accept", () => state.onDeviceStateUpdate("Connected", connection.parameters.To));
connection?.on("disconnect", () => state.onDeviceStateUpdate("Disconnected"));
connection?.on("mute", muted => state.onDeviceStateUpdate(muted ? "Muted" : "Connected"));
setConnection(connection);
}
} }
return { return {
@@ -93,8 +100,8 @@ export default function useVoice()
answer, answer,
reject, reject,
hangup, hangup,
dial, dialDigit,
dtmf, makeCall,
voiceState: state.voiceState, voiceState: state.voiceState,
} as Voice; } as Voice;
} }

View File

@@ -0,0 +1,4 @@
.phoneNumber {
font-size: 15px;
font-family: Arial, Helvetica, sans-serif;
}

View File

@@ -0,0 +1,13 @@
import React from 'react';
import { DeviceState } from '../../../hooks/voice-state';
import styles from './display-device-state.module.css';
export default function DisplayDeviceState({deviceState, phoneNumber}: {deviceState: DeviceState, phoneNumber: string})
{
return(
<div>
Device is currently {deviceState}.
<p className={styles.phoneNumber} >{phoneNumber}</p>
</div>
);
}

View File

@@ -0,0 +1,7 @@
import React from 'react';
import style from './dial-box.module.css';
export default function DialBox({dialDigits} : {dialDigits: string})
{
return <div>{dialDigits}</div>;
}

View File

@@ -1,4 +1,4 @@
.dtmf-button { .dial-button {
border-radius: 50%; border-radius: 50%;
border: 1px solid black; border: 1px solid black;
width: 70px; width: 70px;
@@ -7,7 +7,7 @@
background-color: white; background-color: white;
} }
.dtmf-button:hover { .dial-button:hover {
background-color: lightgray; background-color: lightgray;
} }

View File

@@ -1,15 +1,14 @@
import React from 'react'; import React from 'react';
import { chunk } from 'lodash'; import { chunk } from 'lodash';
import { Voice } from '../../../wrappers/voice'; import styles from './dial-buttons.module.css';
import styles from './dtmf-buttons.module.css';
interface DtmfButton interface DialButton
{ {
title: string; title: string;
subTitle:string; subTitle:string;
} }
const buttons: DtmfButton[] = [ const buttons: DialButton[] = [
{title: "1", subTitle: " "}, {title: "1", subTitle: " "},
{title: "2", subTitle: "abc"}, {title: "2", subTitle: "abc"},
{title: "3", subTitle: "efg"}, {title: "3", subTitle: "efg"},
@@ -24,13 +23,13 @@ const buttons: DtmfButton[] = [
{title: "#", subTitle: " "}, {title: "#", subTitle: " "},
]; ];
export default function DtmfButtons({dtmf}: {dtmf: Voice["dtmf"]}) { export default function DialButtons({action}: {action: (digit: string) => void}) {
return ( return (
<div> <div>
{ chunk(buttons, 3).map((buttonRow, rowIndex) => { chunk(buttons, 3).map((buttonRow, rowIndex) =>
<div key={rowIndex}> <div key={rowIndex}>
{ buttonRow.map(({title, subTitle}, index) => { buttonRow.map(({title, subTitle}, index) =>
<button key={title} className={styles["dtmf-button"]} onClick = {() => dtmf(title)}> <button key={title} className={styles["dial-button"]} onClick = {() => action(title)}>
<div className={styles["title"]}>{ title }</div> <div className={styles["title"]}>{ title }</div>
<div className={styles["sub-title"]}>{ subTitle }</div> <div className={styles["sub-title"]}>{ subTitle }</div>
</button>) } </button>) }

View File

@@ -91,7 +91,7 @@ declare module "twilio-client"
constructor(token: string, params?: SetupParams); constructor(token: string, params?: SetupParams);
public setup(token: string, params?: SetupParams): void; public setup(token: string, params?: SetupParams): void;
public connect(params: any, audioConstraints?: any): Connection; public connect(params: any, audioConstraints?: any): Connection;
public activeConnection(): Connection; public activeConnection(): Connection | undefined;
public destroy(): void; public destroy(): void;
public disconnectAll(): void; public disconnectAll(): void;
public status(): DeviceStatus; public status(): DeviceStatus;