From b2fe2ba2020728e786995375ee030c635419a8f2 Mon Sep 17 00:00:00 2001 From: spencerpincott Date: Fri, 7 Feb 2020 22:20:27 -0500 Subject: [PATCH] Add phone components Add mocks --- package-lock.json | 67 ++++++++++++++++++ src/App.tsx | 13 +--- src/Components/Phone/button.tsx | 15 ++++ src/Components/Phone/dial-box.tsx | 6 ++ src/Components/Phone/display-device-state.tsx | 11 +++ src/Components/Phone/phone.tsx | 46 +++++++++++++ src/Mocks/mock-connection.ts | 68 +++++++++++++++++++ src/Mocks/mock-device.ts | 51 ++++++++++++++ src/Wrappers/default-config.ts | 2 +- src/Wrappers/voice.ts | 59 +++++++++++++--- 10 files changed, 318 insertions(+), 20 deletions(-) create mode 100644 src/Components/Phone/button.tsx create mode 100644 src/Components/Phone/dial-box.tsx create mode 100644 src/Components/Phone/display-device-state.tsx create mode 100644 src/Components/Phone/phone.tsx create mode 100644 src/Mocks/mock-connection.ts create mode 100644 src/Mocks/mock-device.ts diff --git a/package-lock.json b/package-lock.json index a6a0e6c..2dcefb2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1441,6 +1441,19 @@ "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-7.2.1.tgz", "integrity": "sha512-oZ0Ib5I4Z2pUEcoo95cT1cr6slco9WY7yiPpG+RGNkj8YcYgJnM7pXmYmorNOReh8MIGcKSqXyeGjxnr8YiZbA==" }, + "@twilio/audioplayer": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@twilio/audioplayer/-/audioplayer-1.0.6.tgz", + "integrity": "sha512-c9cjX/ifICgXqShtyAQdVMqfe7odnxougiuRMXBJtn3dZ320mFdt7kmuKedpNnc3ZJ6irOZ9M9MZi9/vuEqHiw==", + "requires": { + "babel-runtime": "^6.26.0" + } + }, + "@twilio/voice-errors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@twilio/voice-errors/-/voice-errors-1.0.1.tgz", + "integrity": "sha512-iXzCuiOhNMhrr8DVjRRzI14YwGUIBM83kWSWcDktxmXim0Tz9xoCth4QFAQcMkNL2h9DlfXlob6noH+3h2iA4A==" + }, "@types/babel__core": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.3.tgz", @@ -2445,6 +2458,14 @@ "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==" }, + "backoff": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/backoff/-/backoff-2.5.0.tgz", + "integrity": "sha1-9hbtqdPktmuMp/ynn2lXIsX44m8=", + "requires": { + "precond": "0.2" + } + }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -10012,6 +10033,11 @@ "uniq": "^1.0.1" } }, + "precond": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz", + "integrity": "sha1-qpWRvKokkj8eD0hJ0kD0fvwQdaw=" + }, "prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", @@ -10952,6 +10978,14 @@ "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==" }, + "rtcpeerconnection-shim": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rtcpeerconnection-shim/-/rtcpeerconnection-shim-1.2.8.tgz", + "integrity": "sha512-5Sx90FGru1sQw9aGOM+kHU4i6mbP8eJPgxliu2X3Syhg8qgDybx8dpDTxUwfJvPnubXFnZeRNl59DWr4AttJKQ==", + "requires": { + "sdp": "^2.6.0" + } + }, "run-async": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", @@ -11083,6 +11117,11 @@ "ajv-keywords": "^3.4.1" } }, + "sdp": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/sdp/-/sdp-2.12.0.tgz", + "integrity": "sha512-jhXqQAQVM+8Xj5EjJGVweuEzgtGWb3tmEEpl3CLP3cStInSbVHSg0QWOGQzNq8pSID4JkpeV2mPqlMDLrm0/Vw==" + }, "select-hose": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", @@ -12314,6 +12353,29 @@ "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" }, + "twilio-client": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/twilio-client/-/twilio-client-1.9.7.tgz", + "integrity": "sha512-od55ewdYRBk7JIxaMQ3Df1So7vSTIGrOmilL047Fb/axSQHG2hENjgvEfY0QYk72tK5s7avoDgG8DshfnHCzjw==", + "requires": { + "@twilio/audioplayer": "1.0.6", + "@twilio/voice-errors": "1.0.1", + "backoff": "2.5.0", + "rtcpeerconnection-shim": "1.2.8", + "ws": "6.1.3", + "xmlhttprequest": "1.8.0" + }, + "dependencies": { + "ws": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.3.tgz", + "integrity": "sha512-tbSxiT+qJI223AP4iLfQbkbxkwdFcneYinM2+x46Gx2wgvbaOMO36czfdfVUBRTHvzAMRhDd98sA5d/BuWbQdg==", + "requires": { + "async-limiter": "~1.0.0" + } + } + } + }, "type": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", @@ -14508,6 +14570,11 @@ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" }, + "xmlhttprequest": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz", + "integrity": "sha1-Z/4HXFwk/vOfnWX197f+dRcZaPw=" + }, "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/src/App.tsx b/src/App.tsx index 6a3ef60..2655692 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,22 +1,15 @@ import React from 'react'; -import logo from './logo.svg'; import './App.css'; +import Phone from './Components/Phone/phone'; import useVoice from './Wrappers/voice'; import { voiceToken } from './Wrappers/default-config'; const App = () => { - const { startDevice, answer, hangup, voiceState } = useVoice(); - - window.setTimeout(() => startDevice(voiceToken), 5000); + const voice = useVoice(); return (
-
- logo -

- Device is currently {voiceState.deviceState}. -

-
+
); } diff --git a/src/Components/Phone/button.tsx b/src/Components/Phone/button.tsx new file mode 100644 index 0000000..2ffb285 --- /dev/null +++ b/src/Components/Phone/button.tsx @@ -0,0 +1,15 @@ +import React from 'react'; + +interface ButtonSettings +{ + title: string; + onClick: () => void; +} + +export default function Button({title, onClick} : ButtonSettings) { + return ( + + ); +} \ No newline at end of file diff --git a/src/Components/Phone/dial-box.tsx b/src/Components/Phone/dial-box.tsx new file mode 100644 index 0000000..1b449c9 --- /dev/null +++ b/src/Components/Phone/dial-box.tsx @@ -0,0 +1,6 @@ +import React from 'react'; +const DialBox = () => { + return; +} + +export default DialBox; \ No newline at end of file diff --git a/src/Components/Phone/display-device-state.tsx b/src/Components/Phone/display-device-state.tsx new file mode 100644 index 0000000..1db7f51 --- /dev/null +++ b/src/Components/Phone/display-device-state.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { DeviceState } from '../../Hooks/voice-state'; + +export default function DisplayDeviceState({deviceState}: {deviceState: DeviceState}) +{ + return( +
+ Device is currently {deviceState}. +
+ ); +} \ No newline at end of file diff --git a/src/Components/Phone/phone.tsx b/src/Components/Phone/phone.tsx new file mode 100644 index 0000000..2569901 --- /dev/null +++ b/src/Components/Phone/phone.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import useVoice, { Voice } from '../../Wrappers/voice'; +import { voiceToken } from '../../Wrappers/default-config'; +import Button from './button'; +import DeviceState from './display-device-state'; + +export default function Phone({voice:{ startDevice, answer, reject, hangup, dial, voiceState} }: {voice: Voice}) +{ + if (voiceState.deviceState === "Offline") + { + return ( +
+ +
+ ); + } + else if(voiceState.deviceState === "Ringing") + { + return ( +
+ +
+ ); + } + else if(voiceState.deviceState === "Connected") + { + return ( +
+ +
+ ); + } + else + { + return ( +
+ +
+ ); + } + +} \ No newline at end of file diff --git a/src/Mocks/mock-connection.ts b/src/Mocks/mock-connection.ts new file mode 100644 index 0000000..267785f --- /dev/null +++ b/src/Mocks/mock-connection.ts @@ -0,0 +1,68 @@ +import { Connection, ConnectionParameters, ConnectionStatus, ConnectionEvent } from "twilio-client"; + +export class MockConnection implements Connection +{ + private from = "+16133713909"; + private to = "+123456789"; + + public parameters: ConnectionParameters = { From: this.from, To: this.to }; + public customParameters: Map = new Map(); + public options: object = {}; + + private connectionStatus: ConnectionStatus = "ringing"; + private readonly handlers = <{[key in ConnectionEvent]: ((mute?: boolean) => void)[]}>{}; + private muted: boolean = false; + + + public accept(audioConstraints?: any) + { + setTimeout(() => this.handlers["accept"].forEach(handler => handler()), 500) + } + + public reject() + { + setTimeout(() => this.handlers["disconnect"].forEach(handler => handler()), 500) + } + + public ignore() + { + setTimeout(() => this.handlers["disconnect"].forEach(handler => handler()), 500) + } + + public disconnect() + { + setTimeout(() => this.handlers["disconnect"].forEach(handler => handler()), 500) + } + + public mute(mute: boolean) + { + this.muted = mute + setTimeout(() => this.handlers["mute"].forEach(handler => handler(this.muted)), 500) + } + + public isMuted() + { + return this.muted; + } + + public sendDigits(digits: string) + { + + } + + public status(): ConnectionStatus + { + return this.connectionStatus; + } + + public on(event: ConnectionEvent, handler: (...args: any[]) => void) + { + if (!this.handlers[event]) + { + this.handlers[event] = []; + } + this.handlers[event].push(handler); + } + + getRemoteStream() { } +} \ No newline at end of file diff --git a/src/Mocks/mock-device.ts b/src/Mocks/mock-device.ts new file mode 100644 index 0000000..0ce766f --- /dev/null +++ b/src/Mocks/mock-device.ts @@ -0,0 +1,51 @@ +import { Device, DeviceAudio, SetupParams, Connection, DeviceStatus, DeviceEvent } from "twilio-client"; +import { MockConnection } from "./mock-connection"; + +export class MockDevice implements Device +{ + public audio: DeviceAudio = {}; + private deviceStatus: DeviceStatus = "offline"; + private readonly handlers = <{[key in DeviceEvent]: ((connection?: Connection) => void)[]}>{}; + + constructor(token: string, params?: SetupParams) + { + setTimeout(() => this.handlers["ready"].forEach(handler => handler()), 1000); + setTimeout(() => this.handlers["incoming"].forEach(handler => handler(new MockConnection())), 5000); + } + + public setup(token: string, params?: SetupParams): void {} + + public connect(params: any, audioConstraints?: any): Connection + { + return {}; + } + + public activeConnection(): Connection + { + return {}; + } + + public destroy(): void + { + this.handlers["offline"].forEach(handler => handler()); + } + + public disconnectAll(): void + { + this.handlers["disconnect"].forEach(handler => handler()); + } + + public status() + { + return this.deviceStatus; + } + + public on(event: DeviceEvent, handler: (...args: any[]) => void): void + { + if (!this.handlers[event]) + { + this.handlers[event] = []; + } + this.handlers[event].push(handler); + } +} diff --git a/src/Wrappers/default-config.ts b/src/Wrappers/default-config.ts index 6362fae..075fe7c 100644 --- a/src/Wrappers/default-config.ts +++ b/src/Wrappers/default-config.ts @@ -1 +1 @@ -export const voiceToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzY29wZSI6InNjb3BlOmNsaWVudDppbmNvbWluZz9jbGllbnROYW1lPXNwZW5jZXIgc2NvcGU6Y2xpZW50Om91dGdvaW5nP2FwcFNpZD1BUGJmNzMyOTg4YjcxYTMzZDFiOGY1ZjY1MmI1NDU4MmQ0JmNsaWVudE5hbWU9c3BlbmNlciIsImlzcyI6IkFDOWQyYzcxOTc0NmIxZTg5MWU2ZjVlZmQwMmE5Yjg0MzAiLCJleHAiOjE1ODA5NjA0NTAsImlhdCI6MTU4MDk1Njg1MH0.LOlWmq52LjPLM4ONlMdnUybZf6NDk1Xeye-qjVRI_O4"; +export const voiceToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzY29wZSI6InNjb3BlOmNsaWVudDppbmNvbWluZz9jbGllbnROYW1lPXNwZW5jZXIgc2NvcGU6Y2xpZW50Om91dGdvaW5nP2FwcFNpZD1BUGJmNzMyOTg4YjcxYTMzZDFiOGY1ZjY1MmI1NDU4MmQ0JmNsaWVudE5hbWU9c3BlbmNlciIsImlzcyI6IkFDOWQyYzcxOTc0NmIxZTg5MWU2ZjVlZmQwMmE5Yjg0MzAiLCJleHAiOjE1ODExMjcyNzEsImlhdCI6MTU4MTEyMzY3MX0.-78SDqG6a-KVJTOfSPuJB7dLZkJTzDUjojTkJ8oxZ3c"; diff --git a/src/Wrappers/voice.ts b/src/Wrappers/voice.ts index ad7ae97..2645ec4 100644 --- a/src/Wrappers/voice.ts +++ b/src/Wrappers/voice.ts @@ -1,53 +1,94 @@ import { Device, Connection } from "twilio-client"; import useVoiceState, { VoiceState } from "../Hooks/voice-state"; +import { useState } from "react"; +import { MockDevice } from "../Mocks/mock-device"; export interface Voice { startDevice(token: string): void; answer(): void; + reject(): void; hangup(): void; + dial(phoneNumber: string): void; voiceState: VoiceState; } export default function useVoice() { const state = useVoiceState(); - let device: Device; - let connection: Connection; + console.log("Creating new voice"); + + const [device, setDevice] = useState(); + const [connection, setConnection] = useState(); const startDevice = (token: string) => { - device = new Device(token); + // const device = new Device(token) + const device = new MockDevice(token); + + console.log("Creating new device", device); device.on("ready", () => state.onDeviceStateUpdate("Disconnected")); device.on("offline", () => state.onDeviceStateUpdate("Offline")); - device.on("disconnect", () => state.onDeviceStateUpdate("Disconnected")); - device.on("cancel", () => state.onDeviceStateUpdate("Disconnected")); + + device.on("disconnect", () => + { + setConnection(undefined); + state.onDeviceStateUpdate("Disconnected") + }); + + device.on("cancel", () => + { + setConnection(undefined); + state.onDeviceStateUpdate("Disconnected") + }); device.on("incoming", (incomingConnection: Connection) => { state.onDeviceStateUpdate("Ringing", incomingConnection.parameters.From); incomingConnection.on("accept", () => state.onDeviceStateUpdate("Connected")); + incomingConnection.on("disconnect", () => state.onDeviceStateUpdate("Disconnected")); incomingConnection.on("mute", muted => state.onDeviceStateUpdate(muted ? "Muted" : "Connected")); - connection = incomingConnection; + + setConnection(incomingConnection); }); + + setDevice(device); } const answer = () => { - connection.accept(); + connection?.accept(); + } + + const reject = () => + { + connection?.reject(); } const hangup = () => { - device.disconnectAll(); + device?.disconnectAll(); + } + + const dial = (phoneNumber: string) => + { + const connection = device?.connect({To: phoneNumber}); + + connection?.on("accept", () => state.onDeviceStateUpdate("Connected")); + connection?.on("disconnect", () => state.onDeviceStateUpdate("Disconnected")); + connection?.on("mute", muted => state.onDeviceStateUpdate(muted ? "Muted" : "Connected")); + + setConnection(connection); } return { startDevice, answer, + reject, hangup, + dial, voiceState: state.voiceState, - } + } as Voice; } \ No newline at end of file