├── .gitignore ├── README.md ├── build ├── assets │ └── app.js └── index.html ├── package.json ├── server.js ├── server └── routes │ ├── calls.js │ ├── chat.js │ ├── sms.js │ ├── taskrouter.js │ └── tokens.js ├── src ├── actions │ └── index.js ├── components │ ├── common │ │ └── Button.js │ ├── messages │ │ ├── MessageBox.js │ │ ├── MessageControl.js │ │ ├── MessageEntry.js │ │ ├── Messenger.js │ │ └── MessengerContainer.js │ ├── phone │ │ ├── CallControl.js │ │ ├── KeyPad.js │ │ ├── NumberEntry.js │ │ ├── PhoneContainer.js │ │ └── phone.js │ ├── taskrouter │ │ ├── QueueStats.js │ │ ├── ReservationControls.js │ │ ├── ReservationControlsContainer.js │ │ ├── SimpleAgentStatusControls.js │ │ ├── SimpleAgentStatusControlsContainer.js │ │ ├── StatBox.js │ │ └── TaskControls.js │ └── workspace │ │ ├── AgentWorkSpace.js │ │ └── AgentWorkSpaceContainer.js ├── configureStore.js ├── configureUrls_SAMPLE.js ├── index.html ├── main.js ├── reducers │ ├── chat.js │ ├── index.js │ ├── phone.js │ └── taskrouter.js └── styles │ ├── App.css │ └── Reset.css ├── styles ├── App.css └── Reset.css ├── twilio.config.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .env_bcoyle 3 | node_modules 4 | npm-debug.log 5 | .DS_Store 6 | src/configureUrls.js 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-twilio-starter 2 | This application will serve as a getting started application for building a Twilio powered multi channel contact center using React and Redux on the front end and Twilio Functions for the backend. The backend can be swapped out to use whichever server side technology or framework you choose. 3 | 4 | NOTE: This includes a node server backend which includes the functionality currently being migrated to Twilio functions. This server side node code has been left in as an example for an option not using Twilio functions. 5 | 6 | ## Setup 7 | 8 | ### Twilio Product Setup 9 | * Turn on Agent Conference - https://www.twilio.com/console/voice/settings/conferences 10 | * Create a TaskRouter Workspace - https://www.twilio.com/console/taskrouter/workspaces 11 | * On the workspace setting page enable **Multitasking** 12 | * Buy a new phone number 13 | * Create a new worker and add the following attributes **be sure to name your worker all lowercase with no spaces** 14 | ```sh 15 | {"contact_uri":"client:YOUR WORKER FRIENDLY NAME", "agent_name":"YOUR WORKER FRIENDLY NAME", "phone_number":"A PHONE NUMBER ON YOUR ACCOUNT"} 16 | ``` 17 | *note: these attributes are used by the code and workflow in this example. Your production attributes will depend on your own routing rules* 18 | * Create a new TaskQueue 19 | * Name - name your queue 20 | * Target Workers - 1==1 *This ensures all workers are available for this queue and is useful for testing* 21 | * Keep the rest of the defaults 22 | * Create a new Workflow 23 | * Name - name your queue 24 | * Leave Assignment Callback blank. For this example we will handle assignments in the browser 25 | * Choose your queue above as Default Queue 26 | * Add a Filter to your Workflow 27 | * EXPRESSION = direction == 'outbound' 28 | * TARGET WORKERS EXPRESSION = task.agent_id==worker.agent_name 29 | * **Note-These expressions are for use with this sample app. Your production app can use any matching criteria you choose.** 30 | 31 | 32 | ### Twilio Functions Backend 33 | * In your Twilio console go to Runtime->Functions->Config - https://www.twilio.com/console/runtime/functions/configure 34 | * Under Credentials Enable ACCOUNT_SID and AUTH_TOKEN 35 | * Add the following environment variables 36 | ```sh 37 | TWILIO_WORKSPACE_SID=[your workspace sid] 38 | TWILIO_WORKFLOW_SID=[your default workflow sid] 39 | ``` 40 | * Under Dependencies add jsonwebtoken v8.1.0 41 | * For each function in this repo: https://github.com/tonyv/twilio-functions create a new Twilio function on the manage page of functions: https://www.twilio.com/console/runtime/functions/manage. 42 | * You can name the Twilio function whatever name you choose and the URL to whatever you choose. The front end app will default to the file name of each of the functions in the repo but this is configurable 43 | * Unless noted in the comments of the function you will not check the **Check for valid Twilio signature** 44 | * You do not need to choose the **Event** for each function 45 | * Click Save for each function and the function will deploy 46 | * Go to the number you purchased and configure the Voice URL to point to the function you created named **enqueue-call** 47 | 48 | ### React Frontend 49 | * Clone this front end repo 50 | * run **npm install** 51 | * All server side URLs are defined in single file to make it easy to configure custom urls for any Twilio functions you have copied or created yourself. Configure your Function URLs in the /src/configureUrls.js file 52 | * Copy src/configureUrls_SAMPLE.js to configureUrls.js 53 | * Set base_url to your Twilio Runtime URL: https://www.twilio.com/console/runtime/overview 54 | * Set taskRouterToken to path you defined 55 | * Set clientToken to path you defined 56 | * Set conferenceTerminate to path you defined 57 | * Run the webpack dev server with **npm start** 58 | * Go to http://localhost:8080/?worker=[YOUR WORKER SID] 59 | 60 | ## Components 61 | Components are organized into Container components and functional presentation components. Container components contain all of the Twilio specific code along with the actions and reducers. The presentation componets are functional and only handle layout. Any front end framework can be used with these components. Funcitonal components that need a container handler are named the same with the Container component having the Container at the end of the name. 62 | -------------------------------------------------------------------------------- /build/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Twilio Starter 6 | 7 | 8 |
9 | 10 | 11 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "twilio_react_starter", 3 | "version": "1.0.0", 4 | "description": "Starter app for Twilio projects using react and webpack", 5 | "main": "app.js", 6 | "scripts": { 7 | "start": "webpack-dev-server --inline --progress --colors", 8 | "dist": "npm run copy & webpack", 9 | "copy": "copyfiles -f ./src/index.html ./build", 10 | "test": "jest" 11 | }, 12 | "keywords": [ 13 | "react", 14 | "twilio", 15 | "webpack" 16 | ], 17 | "author": "Brian Coyle @bcoyle73", 18 | "license": "ISC", 19 | "dependencies": { 20 | "babel-polyfill": "^6.9.1", 21 | "body-parser": "^1.15.0", 22 | "cookie-parser": "^1.4.3", 23 | "copyfiles": "^1.2.0", 24 | "express": "^4.13.4", 25 | "isomorphic-fetch": "^2.2.1", 26 | "react": "^0.14.7", 27 | "react-dom": "0.14", 28 | "react-redux": "^4.4.5", 29 | "redux": "^3.5.2", 30 | "redux-logger": "^2.8.1", 31 | "redux-thunk": "^2.2.0", 32 | "twilio": "^3.6.5", 33 | "twilio-video": "^1.0.0" 34 | }, 35 | "devDependencies": { 36 | "babel-core": "^6.5.2", 37 | "babel-jest": "^6.0.1", 38 | "babel-loader": "^6.2.3", 39 | "babel-preset-es2015": "^6.5.0", 40 | "babel-preset-react": "^6.5.0", 41 | "css-loader": "^0.23.1", 42 | "jest-cli": "^0.8.2", 43 | "react-addons-test-utils": "^0.14.7", 44 | "style-loader": "^0.13.0", 45 | "webpack": "^1.12.13", 46 | "webpack-dev-server": "^1.14.1" 47 | }, 48 | "jest": { 49 | "scriptPreprocessor": "/node_modules/babel-jest", 50 | "unmockedModulePathPatterns": [ 51 | "/node_modules/react", 52 | "/node_modules/react-dom", 53 | "/node_modules/react-addons-test-utils", 54 | "/node_modules/fbjs" 55 | ] 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var bodyParser = require('body-parser'); 3 | var cookieParser = require('cookie-parser'); 4 | var path = require('path'); 5 | var fs = require('fs'); 6 | var app = express(); 7 | 8 | var tokens = require('./server/routes/tokens'); 9 | var calls = require('./server/routes/calls'); 10 | var sms = require('./server/routes/sms'); 11 | var taskrouter = require('./server/routes/taskrouter'); 12 | //var chat = require('./server/routes/chat'); 13 | 14 | var port = process.env.PORT || 3000; 15 | app.use(bodyParser.urlencoded({ 16 | extended: true 17 | })); 18 | app.use( bodyParser.json() ); 19 | app.use(express.static(path.join(__dirname, 'build'))); 20 | app.use(cookieParser()); 21 | 22 | app.use(function timeLog(req, res, next) { 23 | console.log('Main Request - Time: ', Date.now()); 24 | console.log(req.originalUrl); 25 | next(); 26 | }); 27 | 28 | //setup routes all scoped to /api 29 | app.use('/api/tokens', tokens); 30 | app.use('/api/calls', calls); 31 | app.use('/api/sms', sms); 32 | app.use('/api/taskrouter', taskrouter); 33 | //app.use('/api/chat', chat); 34 | 35 | 36 | app.listen(port, function (err){ 37 | if (err) throw err; 38 | console.log("Server running on port " + port); 39 | }); 40 | -------------------------------------------------------------------------------- /server/routes/calls.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | 4 | var VoiceResponse = require('twilio').twiml.VoiceResponse; 5 | var config = require('../../twilio.config'); 6 | 7 | router.post('/', function(req, res) { 8 | const resp = new VoiceResponse(); 9 | const type = {skill: 'skill_1', accelerator: 'false'}; 10 | const json = JSON.stringify(type); 11 | 12 | resp.enqueueTask({ 13 | workflowSid: config.workflowSid, 14 | }).task({priority: '1'}, json) 15 | 16 | res.send(resp.toString()); 17 | 18 | }); 19 | 20 | router.post('/events', function(req, res) { 21 | console.log('*********************************************************') 22 | console.log('*********************************************************') 23 | console.log('********************* CALL EVENT ************************') 24 | console.log(req.body) 25 | 26 | res.send({}) 27 | 28 | }); 29 | 30 | router.post('/conference/events/:call_sid', function(req, res) { 31 | console.log('*********************************************************') 32 | console.log('*********************************************************') 33 | console.log('*************** CONFERENCE EVENT ************************') 34 | console.log(req.body) 35 | const client = require('twilio')(config.accountSid, config.authToken); 36 | const callerSid = req.params.call_sid 37 | 38 | // This method of putting call sid in the URI does not allow you to reconnect a calls 39 | // to customer if customer drops 40 | if (callerSid === req.body.CallSid && req.body.StatusCallbackEvent == 'participant-leave') { 41 | console.log("CALLER HUNG UP. KILL CONFERENCE") 42 | client.api.accounts(config.accountSid) 43 | .conferences(req.body.ConferenceSid) 44 | .fetch() 45 | .then((conference) => { 46 | console.log(conference) 47 | if (conference) { 48 | conference.update({status: "completed"}) 49 | .then((conference) => console.log("closed conf")) 50 | } 51 | }) 52 | .catch((error) => { 53 | console.log(error) 54 | }) 55 | } 56 | 57 | res.send({}) 58 | 59 | }); 60 | 61 | router.post('/conference/:conference_sid/hold/:call_sid/:toggle', function(req, res) { 62 | const client = require('twilio')(config.accountSid, config.authToken); 63 | const confSid = req.params.conference_sid 64 | const callSid = req.params.call_sid 65 | const toggle = req.params.toggle 66 | client.api.accounts(config.accountSid) 67 | .conferences(confSid) 68 | .participants(callSid) 69 | .update({hold: toggle}) 70 | .then((participant) => console.log(participant.hold)) 71 | .done(); 72 | }); 73 | 74 | router.post('/conference/:conference_sid/terminate', function(req, res) { 75 | const client = require('twilio')(config.accountSid, config.authToken); 76 | const confSid = req.params.conference_sid 77 | 78 | console.log(confSid) 79 | 80 | client.api.accounts(config.accountSid) 81 | .conferences(confSid) 82 | .fetch() 83 | .then((conference) => { 84 | console.log(conference) 85 | if (conference) { 86 | conference.update({status: "completed"}) 87 | .then((conference) => console.log("closed conf")) 88 | } 89 | }) 90 | .catch((error) => { 91 | console.log(error) 92 | res.send({}); 93 | }) 94 | }); 95 | 96 | // This endpoint dials out to a number and places that call into a conference 97 | // it also responds with Twiml to place the call accessing this endpoint into the same conference 98 | // This is called primarly when workers accept a reservation with call method 99 | router.post('/outbound/dial/:to/from/:from/conf/:conference_name', function(req, res) { 100 | console.log(req.body) 101 | const to = req.params.to 102 | const from = req.params.from 103 | const conferenceName = req.params.conference_name 104 | const client = require('twilio')(config.accountSid, config.authToken); 105 | 106 | client 107 | .conferences(conferenceName) 108 | .participants.create({to: to, from: from, earlyMedia: "true", statusCallback: "http://bcoyle.ngrok.io" + "/api/taskrouter/event"}) 109 | .then((participant) => { 110 | const resp = new VoiceResponse(); 111 | const dial = resp.dial(); 112 | dial.conference({ 113 | beep: false, 114 | waitUrl: '', 115 | startConferenceOnEnter: true, 116 | endConferenceOnExit: false 117 | }, conferenceName); 118 | console.log(resp.toString()) 119 | res.send(resp.toString()); 120 | 121 | // Now update the task with a conference attribute with Agent Call Sid 122 | client.taskrouter.v1 123 | .workspaces(config.workspaceSid) 124 | .tasks(conferenceName) 125 | .update({ 126 | attributes: JSON.stringify({conference: {sid: participant.conferenceSid, participants: {worker: req.body.CallSid, customer: participant.callSid}}}), 127 | }).then((task) => { 128 | console.log(task) 129 | }) 130 | res.send({}); 131 | }) 132 | .catch((error) => { 133 | console.log(error) 134 | }) 135 | }); 136 | 137 | router.post('/record/:conference_id/', function(req, res) { 138 | // This endpoint is set when accepting the task with the call method 139 | const conferenceSid = req.params.conference_id 140 | const client = require('twilio')(config.accountSid, config.authToken); 141 | 142 | client 143 | .conferences(conferenceSid) 144 | .participants.create({to: "+12162083661", from: "2146438999", earlyMedia: "true", record: "true"}) 145 | .then((participant) => { 146 | console.log(participant.callSid) 147 | res.send({callSid: participant.callSid}); 148 | }) 149 | .catch((error) => { 150 | console.log(error) 151 | }) 152 | }); 153 | 154 | router.post('/outbound/agent', function(req, res) { 155 | // The endpoint is configure in Twiml App that Twilio client referenced when token was created 156 | const from = req.body.From 157 | const to = req.body.To 158 | const agent = req.body.Agent 159 | const client = require('twilio')(config.accountSid, config.authToken); 160 | 161 | const dial = resp.dial(); 162 | dial.conference({ 163 | beep: false, 164 | waitUrl: '', 165 | startConferenceOnEnter: true, 166 | endConferenceOnExit: false 167 | }, task.sid); 168 | console.log(resp.toString()) 169 | res.send(resp.toString()); 170 | }); 171 | 172 | 173 | module.exports = router; 174 | -------------------------------------------------------------------------------- /server/routes/chat.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | 4 | var twilio = require('twilio'); 5 | var config = require('../../twilio.config'); 6 | 7 | 8 | router.post('/event', function(req, res) { 9 | console.log(req.body) 10 | }) 11 | 12 | router.post('/pre-event', function(req, res) { 13 | console.log(req.body) 14 | 15 | }) 16 | -------------------------------------------------------------------------------- /server/routes/sms.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | 4 | var twilioLibrary = require('twilio'); 5 | 6 | //var IpMessagingClient = require('twilio').IpMessagingClient; 7 | 8 | var config = require('../../twilio.config'); 9 | 10 | router.post('/', function(req, res) { 11 | var client = new twilioLibrary.Twilio(config.accountSid, config.authToken); 12 | //var client = new IpMessagingClient(config.accountSid, config.authToken); 13 | var service = client.chat.services(config.chatServiceSid) 14 | 15 | var cookie = req.cookies.convo; 16 | 17 | if (cookie) { 18 | // Part of existing conversation add this to room 19 | console.log("cookie found"); 20 | service.channels(cookie).messages.create({ 21 | body: req.body.body, 22 | from: req.body.from 23 | }).then(function(response) { 24 | console.log(response); 25 | }).catch(function(error) { 26 | console.log(error); 27 | }); 28 | } else { 29 | service.channels.create({ 30 | friendlyName: req.body.sid 31 | }).then(function(response) { 32 | console.log(response); 33 | //res.cookie('convo', response.sid, { maxAge: 300000, httpOnly: true }) 34 | }).catch(function(error) { 35 | console.log(error); 36 | }); 37 | } 38 | resp = new twilioLibrary.twiml.MessagingResponse(); 39 | console.log(resp.toString()); 40 | //var resp = client.MessagingResponse.toString(); 41 | // expire in 5 mins 42 | 43 | res.send(resp.toString()); 44 | }); 45 | 46 | 47 | 48 | module.exports = router; 49 | -------------------------------------------------------------------------------- /server/routes/taskrouter.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | 4 | var twilio = require('twilio'); 5 | var config = require('../../twilio.config'); 6 | 7 | router.post('/assignment', function(req, res) { 8 | 9 | var taskAttributes = JSON.parse(req.body.TaskAttributes); 10 | var workerAttributes = JSON.parse(req.body.WorkerAttributes); 11 | var instructions = {} 12 | 13 | 14 | //console.log("Assignment called. Ignore this for now. Accept client side"); 15 | console.log(req.body) 16 | 17 | if (taskAttributes.type == "outbound_transfer") { 18 | instructions = { 19 | "instruction": "call", 20 | "accept": "true", 21 | "from": workerAttributes.phone_number, 22 | "url": config.baseUrl + "/api/calls/outbound/dial", 23 | "status_callback_url": config.baseUrl + "/api/taskrouter/event" 24 | } 25 | } 26 | 27 | 28 | }); 29 | 30 | router.post('/event', function(req, res) { 31 | console.log('*********************************************************') 32 | console.log('*********************************************************') 33 | console.log('*************** TASKROUTER EVENT ************************') 34 | console.log(`${req.body.EventType} --- ${req.body.EventDescription}`) 35 | console.log(req.body) 36 | 37 | if (req.body.ResourceType == 'worker') { 38 | const client = require('twilio')(config.accountSid, config.authToken); 39 | const service = client.sync.services(config.syncServiceSid); 40 | const worker = req.body 41 | 42 | service.syncMaps('current_workers') 43 | .syncMapItems(worker.WorkerSid).update({ 44 | data: { 45 | name: worker.Workername, 46 | activity: worker.WorkerActivityName, 47 | timestamp: worker.Timestamp 48 | } 49 | }).then(function(response) { 50 | console.log("worker updated"); 51 | }).catch(function(error) { 52 | console.log(error); 53 | }); 54 | } 55 | res.send({}) 56 | }) 57 | 58 | 59 | router.post('/outbound', function(req, res) { 60 | //const resp = new VoiceResponse(); 61 | console.log(req.body) 62 | const from = req.body.From 63 | const to = req.body.To 64 | const agent = req.body.Agent 65 | const client = require('twilio')(config.accountSid, config.authToken); 66 | // Create a Task on a custom channel 67 | // with the Task sid that was returned place this agent leg of the call into 68 | // a conference named by the task sid 69 | 70 | client.taskrouter.v1 71 | .workspaces(config.workspaceSid) 72 | .tasks 73 | .create({ 74 | workflowSid: config.workflowSid, 75 | taskChannel: 'custom1', 76 | attributes: JSON.stringify({direction:"outbound", agent_name: 'bcoyle', from: from, to: to}), 77 | }).then((task) => { 78 | console.log(task) 79 | }) 80 | res.send({}); 81 | }); 82 | 83 | router.get('/initialize', function(req, res) { 84 | const client = require('twilio')(config.accountSid, config.authToken); 85 | const service = client.sync.services(config.syncServiceSid); 86 | service.syncMaps 87 | .create({ 88 | uniqueName: 'current_workers', 89 | }) 90 | .then(response => { 91 | console.log(response); 92 | client.taskrouter.v1 93 | .workspaces(config.workspaceSid) 94 | .workers 95 | .list() 96 | .then((workers) => { 97 | workers.forEach((worker) => { 98 | service.syncMaps('current_workers') 99 | .syncMapItems.create({ 100 | key: worker.sid, 101 | data: { 102 | name: worker.FriendlyName, 103 | activity: worker.Activity, 104 | } 105 | }).then(function(response) { 106 | console.log(response); 107 | }).catch(function(error) { 108 | console.log(error); 109 | }); 110 | }); 111 | }); 112 | }) 113 | .catch(error => { 114 | console.log(error); 115 | }); 116 | res.send({status: "ok"}) 117 | }) 118 | 119 | 120 | 121 | module.exports = router; 122 | -------------------------------------------------------------------------------- /server/routes/tokens.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const _ = require('lodash'); 4 | 5 | const ClientCapability = require('twilio').jwt.ClientCapability; 6 | const AccessToken = require('twilio').jwt.AccessToken; 7 | const TaskRouterCapability = require('twilio').jwt.taskrouter.TaskRouterCapability; 8 | const Policy = TaskRouterCapability.Policy; 9 | const util = require('twilio').jwt.taskrouter.util; 10 | 11 | const TASKROUTER_BASE_URL = 'https://taskrouter.twilio.com'; 12 | const version = 'v1'; 13 | 14 | //const SyncGrant = AccessToken.SyncGrant; 15 | 16 | 17 | const config = require('../../twilio.config'); 18 | 19 | router.get('/worker/:id', function(req, res) { 20 | const worker = req.params.id; 21 | 22 | const capability = new TaskRouterCapability({ 23 | accountSid: config.accountSid, 24 | authToken: config.authToken, 25 | workspaceSid: config.workspaceSid, 26 | channelId: worker, 27 | ttl: 3600}); // 28 | 29 | // Event Bridge Policies 30 | var eventBridgePolicies = util.defaultEventBridgePolicies(config.accountSid, worker); 31 | 32 | var workspacePolicies = [ 33 | // Workspace fetch Policy 34 | buildWorkspacePolicy(), 35 | // Workspace Activities Update Policy 36 | buildWorkspacePolicy({ resources: ['Activities'], method: 'POST' }), 37 | buildWorkspacePolicy({ resources: ['Activities'], method: 'GET' }), 38 | // 39 | buildWorkspacePolicy({ resources: ['Tasks', '**'], method: 'POST' }), 40 | buildWorkspacePolicy({ resources: ['Tasks', '**'], method: 'GET' }), 41 | // Workspace Activities Worker Reserations Policy 42 | buildWorkspacePolicy({ resources: ['Workers', worker, 'Reservations', '**'], method: 'POST' }), 43 | buildWorkspacePolicy({ resources: ['Workers', worker, 'Reservations', '**'], method: 'GET' }), 44 | // 45 | 46 | // Workspace Activities Worker Policy 47 | buildWorkspacePolicy({ resources: ['Workers', worker], method: 'GET' }), 48 | buildWorkspacePolicy({ resources: ['Workers', worker], method: 'POST' }), 49 | ]; 50 | 51 | eventBridgePolicies.concat(workspacePolicies).forEach(function (policy) { 52 | capability.addPolicy(policy); 53 | }); 54 | 55 | res.send(capability.toJwt()); 56 | }); 57 | 58 | // Helper function to create Policy for TaskRouter token 59 | function buildWorkspacePolicy(options) { 60 | options = options || {}; 61 | var resources = options.resources || []; 62 | var urlComponents = [TASKROUTER_BASE_URL, version, 'Workspaces', config.workspaceSid] 63 | 64 | return new Policy({ 65 | url: urlComponents.concat(resources).join('/'), 66 | method: options.method || 'GET', 67 | allow: true 68 | }); 69 | } 70 | 71 | router.get('/phone/:name', function(req, res) { 72 | const clientName = req.params.name; 73 | console.log("register phone", clientName); 74 | const capability = new ClientCapability({ 75 | accountSid: config.accountSid, 76 | authToken: config.authToken, 77 | }); 78 | capability.addScope(new ClientCapability.IncomingClientScope(clientName)); 79 | capability.addScope( 80 | new ClientCapability.OutgoingClientScope({applicationSid: config.twimlApp}) 81 | ); 82 | res.send(capability.toJwt()); 83 | }); 84 | 85 | 86 | 87 | router.get('/chat/:name/:endpoint', function(req, res) { 88 | const clientName = req.params.name; 89 | const endpoint = req.params.endpoint; 90 | console.log("Service SID", config.chatServiceSid); 91 | const chatGrant = new AccessToken.IpMessagingGrant({ 92 | serviceSid: config.chatServiceSid, 93 | endpointId: clientName + endpoint 94 | }) 95 | 96 | const videoGrant = new AccessToken.VideoGrant() 97 | 98 | // Create an access token which we will sign and return to the client 99 | const accessToken = new AccessToken( 100 | config.accountSid, 101 | config.keySid, 102 | config.keySecret, 103 | ''); 104 | 105 | //accessToken.identity = clientName; 106 | accessToken.identity = clientName; 107 | 108 | 109 | 110 | accessToken.addGrant(chatGrant); 111 | accessToken.addGrant(videoGrant); 112 | 113 | //return token.toJwt(); 114 | 115 | // Serialize the token to a JWT string and include it in a JSON response 116 | res.send({ 117 | identity: clientName, 118 | token: accessToken.toJwt() 119 | }); 120 | }) 121 | 122 | module.exports = router; 123 | -------------------------------------------------------------------------------- /src/actions/index.js: -------------------------------------------------------------------------------- 1 | import fetch from 'isomorphic-fetch' 2 | import urls from '../configureUrls' 3 | 4 | // Actions to register the worker 5 | function registerWorker() { 6 | return { 7 | type: 'REGISTER_WORKER', 8 | } 9 | } 10 | 11 | function errorTaskRouter(message) { 12 | return { 13 | type: 'ERROR_TASKROUTER', 14 | message: message 15 | } 16 | } 17 | 18 | function workerConnectionUpdate(status) { 19 | return { 20 | type: 'CONNECTION_UPDATED', 21 | status: status 22 | } 23 | } 24 | 25 | export function workerUpdated(worker) { 26 | return { 27 | type: 'WORKER_UPDATED', 28 | worker: worker 29 | } 30 | } 31 | 32 | export function workerClientUpdated(worker) { 33 | return { 34 | type: 'WORKER_CLIENT_UPDATED', 35 | worker: worker 36 | } 37 | } 38 | 39 | export function reservationsFetch(worker) { 40 | return ( dispatch, getState ) => { 41 | worker.fetchReservations((error, reservations) => { 42 | dispatch(reservationsUpdated(reservations.data)) 43 | }) 44 | } 45 | } 46 | 47 | function taskUpdated(task) { 48 | return { 49 | type: 'TASK_UPDATED', 50 | task: task 51 | } 52 | } 53 | 54 | function taskCompleted(task) { 55 | return { 56 | type: 'TASK_COMPLETED', 57 | task: task 58 | } 59 | } 60 | 61 | function reservationsUpdated(data) { 62 | return { 63 | type: 'RESERVATIONS_UPDATED', 64 | reservations: data 65 | } 66 | } 67 | 68 | export function requestTaskComplete(task) { 69 | return (dispatch) => { 70 | console.log("COMPLETE TASK") 71 | task.complete((error, task) => { 72 | if (error) { 73 | console.log(error); 74 | } 75 | dispatch(taskCompleted(task)) 76 | }) 77 | } 78 | } 79 | 80 | export function requestAcceptReservation() { 81 | return (dispatch, getState) => { 82 | 83 | } 84 | } 85 | 86 | // We have a generic action to refresh reservations as we 87 | // will need that after calls drop 88 | export function requestRefreshReservations() { 89 | return (dispatch, getState) => { 90 | const { taskrouter } = getState() 91 | const { worker } = taskrouter 92 | console.log(worker) 93 | taskrouter.workerClient.fetchReservations((error, reservations) => { 94 | if (error) { 95 | dispatch(errorTaskRouter("Fetching Reservations: " + error.message + " check your TaskRouter token policies")) 96 | } else { 97 | console.log(reservations.data, "RESERVATIONS") 98 | if (reservations.data.length > 0) { 99 | console.log("Your worker has reservations currently assigned to them") 100 | for (let reservation of reservations.data) { 101 | // dont display tasks arleady completed 102 | if (reservation.task.assignmentStatus != "completed") { 103 | dispatch(taskUpdated(reservation.task)) 104 | } 105 | } 106 | } 107 | } 108 | }) 109 | } 110 | } 111 | 112 | export function requestStateChange(newStateName) { 113 | return (dispatch, getState) => { 114 | const { taskrouter } = getState() 115 | let requestedActivitySid = getActivitySid(taskrouter.activities, newStateName) 116 | taskrouter.worker.update("ActivitySid", requestedActivitySid, (error, worker) => { 117 | if (error) { 118 | console.log(error); 119 | dispatch(errorTaskRouter("Updating Worker Activity Sid: " + error.message)) 120 | } else { 121 | console.log("STATE CHANGE", worker) 122 | dispatch(workerUpdated(worker)) 123 | } 124 | }) 125 | } 126 | } 127 | 128 | export function requestWorker(workerSid) { 129 | return (dispatch, getState) => { 130 | dispatch(registerWorker()) 131 | return fetch(urls.taskRouterToken, { 132 | method: "POST", 133 | headers: { 134 | "Content-Type": "application/x-www-form-urlencoded" 135 | }, 136 | body: "workerSid="+workerSid 137 | }) 138 | .then(response => response.json()) 139 | .then(json => { 140 | console.log(json) 141 | // Register your TaskRouter Worker 142 | // --params token, debug, connectActivitySid, disconnectActivitySid, closeExistingSession 143 | // --see https://www.twilio.com/docs/api/taskrouter/worker-js#parameters 144 | let worker = new Twilio.TaskRouter.Worker(json.token, true, null, null, true ) 145 | dispatch(workerClientUpdated(worker)) 146 | console.log(worker) 147 | worker.activities.fetch((error, activityList) => { 148 | if (error) { 149 | console.log(error, "Activity Fetch Error") 150 | dispatch(errorTaskRouter("Fetching Activites: " + error.message)) 151 | } else { 152 | console.log(activityList) 153 | dispatch(activitiesUpdated(activityList.data)) 154 | } 155 | }) 156 | worker.fetchChannels((error, channels) => { 157 | if (error) { 158 | console.log(error, "Channels Fetch Error") 159 | } else { 160 | console.log(channels) 161 | dispatch(channelsUpdated(channels.data)) 162 | } 163 | 164 | }) 165 | 166 | dispatch(requestRefreshReservations()) 167 | 168 | worker.on("ready", (worker) => { 169 | dispatch(workerConnectionUpdate("ready")) 170 | dispatch(workerUpdated(worker)) 171 | dispatch(requestPhone(worker.attributes.contact_uri.split(":").pop())) 172 | //dispatch(requestChat(worker.friendlyName)) 173 | console.log("worker obj", worker) 174 | }) 175 | worker.on('activity.update', (worker) => { 176 | dispatch(workerUpdated(worker)) 177 | }) 178 | worker.on('token.expired', () => { 179 | console.log('EXPIRED') 180 | dispatch(requestWorker(workerSid)) 181 | }) 182 | worker.on('error', (error) => { 183 | // You would want to provide the agent a notication of the error 184 | console.log("Websocket had an error: "+ error.response + " with message: "+error.message) 185 | console.log(error) 186 | dispatch(errorTaskRouter("Error: " + error.message)) 187 | }) 188 | worker.on("disconnected", function() { 189 | // You would want to provide the agent a notication of the error 190 | dispatch(workerConnectionUpdate("disconnected")) 191 | dispatch(errorTaskRouter("Web socket disconnection: " + error.message)) 192 | console.log("Websocket has disconnected"); 193 | 194 | }) 195 | worker.on('reservation.timeout', (reservation) => { 196 | console.log("Reservation Timed Out") 197 | }) 198 | // Another worker has accepted the task 199 | worker.on('reservation.rescinded', (reservation) => { 200 | console.log("Reservation Rescinded") 201 | }) 202 | worker.on('reservation.rejected', (reservation) => { 203 | console.log("Reservation Rejected") 204 | }) 205 | worker.on('reservation.cancelled', (reservation) => { 206 | console.log("Reservation Cancelled") 207 | }) 208 | worker.on('reservation.accepted', (reservation) => { 209 | console.log("Reservation Accepted") 210 | console.log(reservation, "RESERVATION ACCEPTED RESV") 211 | dispatch(taskUpdated(reservation.task)) 212 | // Phone record is a demo of stop/start recording with ghost legs 213 | //dispatch(phoneRecord(reservation.task.attributes.conference.sid)) 214 | }) 215 | worker.on("attributes.update", function(channel) { 216 | 217 | console.log("Worker attributes updated", channel); 218 | }) 219 | worker.on("channel.availability.update", function(channel) { 220 | 221 | console.log("Channel availability updated", channel); 222 | }) 223 | 224 | worker.on("channel.capacity.update", function(channel) { 225 | 226 | console.log("Channel capacity updated", channel); 227 | }) 228 | 229 | worker.on('reservation.created', (reservation) => { 230 | console.log("Incoming reservation") 231 | console.log(reservation) 232 | 233 | switch (reservation.task.taskChannelUniqueName) { 234 | case 'voice': 235 | const customerLeg = reservation.task.attributes.call_sid 236 | console.log(customerLeg, "customer call sid") 237 | console.log("Create a conference for agent and customer") 238 | var options = { 239 | "ConferenceStatusCallback": urls.conferenceEvents + "?customer_sid=" + customerLeg, 240 | "ConferenceStatusCallbackEvent": "start,leave,join,end", 241 | "EndConferenceOnExit": "false", 242 | "Beep": "false" 243 | } 244 | reservation.conference(null, null, null, null, null, options) 245 | break 246 | case 'sms': 247 | reservation.accept() 248 | dispatch(chatNewRequest(reservation.task)) 249 | break 250 | case 'video': 251 | reservation.accept() 252 | dispatch(videoRequest(reservation.task)) 253 | break 254 | case 'custom1': 255 | const taskSid = reservation.task.sid 256 | const to = reservation.task.attributes.to 257 | const from = reservation.task.attributes.from 258 | console.log(reservation, "OUTBOUND") 259 | reservation.call( 260 | from, 261 | urls.callOutboundCallback + "?ToPhone="+to+"&FromPhone="+from+"&Sid="+taskSid, 262 | null, 263 | "true", 264 | "", 265 | "", 266 | function(error, reservation) { 267 | if (error) { 268 | console.log(error) 269 | console.log(error.message) 270 | } 271 | console.log(reservation) 272 | } 273 | ) 274 | 275 | break 276 | default: 277 | reservation.reject() 278 | } 279 | 280 | }) 281 | }) 282 | .then(() => console.log("ih")) 283 | 284 | } 285 | } 286 | 287 | function activitiesUpdated(activities) { 288 | return { 289 | type: 'ACTIVITIES_UPDATED', 290 | activities: activities 291 | } 292 | } 293 | 294 | function channelsUpdated(channels) { 295 | return { 296 | type: 'CHANNELS_UPDATED', 297 | channels: channels 298 | } 299 | } 300 | 301 | export function dialPadUpdated(number) { 302 | return { 303 | type: 'DIAL_PAD_UPDATED', 304 | number: number 305 | } 306 | } 307 | 308 | function registerPhoneDevice() { 309 | return { 310 | type: 'REGISTER_PHONE' 311 | } 312 | } 313 | 314 | function phoneMuted(boolean) { 315 | return { 316 | type: 'PHONE_MUTED', 317 | boolean: boolean 318 | } 319 | } 320 | 321 | function phoneHeld(boolean) { 322 | return { 323 | type: 'PHONE_HELD', 324 | boolean: boolean 325 | } 326 | } 327 | 328 | function phoneWarning(warning) { 329 | return { 330 | type: 'PHONE_WARNING', 331 | warning: warning 332 | } 333 | } 334 | 335 | export function phoneDeviceUpdated(device) { 336 | return { 337 | type: 'PHONE_DEVICE_UPDATED', 338 | device: device 339 | } 340 | } 341 | 342 | export function phoneConnectionUpdated(conn) { 343 | return { 344 | type: "PHONE_CONN_UPDATED", 345 | connection: conn 346 | } 347 | } 348 | 349 | export function requestPhone(clientName) { 350 | return (dispatch, getState) => { 351 | dispatch(registerPhoneDevice()) 352 | const { taskrouter } = getState() 353 | return fetch(urls.clientToken, { 354 | method: "POST", 355 | headers: { 356 | "Content-Type": "application/x-www-form-urlencoded" 357 | }, 358 | body: "clientName="+clientName+"&token="+taskrouter.worker.token, 359 | }) 360 | .then(response => response.json()) 361 | .then(json => { 362 | Twilio.Device.setup(json.token) 363 | Twilio.Device.ready((device) => { 364 | console.log("phone is ready"); 365 | dispatch(phoneDeviceUpdated(device)) 366 | }) 367 | Twilio.Device.incoming(function(connection) { 368 | // Accept the phone call automatically 369 | connection.accept(); 370 | }) 371 | Twilio.Device.connect((conn) => { 372 | console.log("incoming call") 373 | console.log(conn._direction) 374 | // Call is connected. Register callback for events to make sure UI is updated 375 | conn.mute((boolean, connection) => { 376 | dispatch(phoneMuted(boolean)) 377 | }) 378 | conn.disconnect((conn) => { 379 | // Phone disconnected. Refresh Reservations to capture wrapping 380 | //dispatch(requestRefreshReservations()) 381 | }) 382 | // Twilio Client Insights feature. Warning are received here 383 | conn.on('warning', (warning) => { 384 | dispatch(phoneWarning(warning)) 385 | }) 386 | // Twilio Client Insights feature. Warning are cleared here 387 | conn.on('warning-cleared', (warning) => { 388 | dispatch(phoneWarning(" ")) 389 | }) 390 | dispatch(phoneConnectionUpdated(conn)) 391 | }) 392 | Twilio.Device.disconnect((conn) => { 393 | dispatch(phoneConnectionUpdated(null)) 394 | }) 395 | }) 396 | .then(console.log("error")) 397 | } 398 | } 399 | 400 | export function phoneHold(confSid, callSid) { 401 | return(dispatch, getState) => { 402 | const { taskrouter, phone } = getState() 403 | const newHoldState = !phone.isHeld 404 | return fetch(urls.callHold, { 405 | method: "POST", 406 | headers: { 407 | "Content-Type": "application/x-www-form-urlencoded" 408 | }, 409 | body: "conference_sid="+confSid+"&call_sid="+callSid+"&toggle="+newHoldState+"&token="+taskrouter.worker.token, 410 | }) 411 | .then(response => response.json()) 412 | .then(json => { 413 | console.log(json) 414 | dispatch(phoneHeld(json.result)) 415 | }) 416 | 417 | } 418 | } 419 | 420 | export function phoneRecord(confSid, currentState) { 421 | return(dispatch, getState) => { 422 | return fetch(`/api/calls/record/${confSid}`,{method: "POST"}) 423 | .then(response => response.json()) 424 | .then( json => { 425 | console.log(json) 426 | dispatch(phoneRecordOn(json.callSid)) 427 | }) 428 | } 429 | } 430 | 431 | export function phoneRecordOn(callSid) { 432 | return { 433 | type: 'PHONE_RECORD_ON', 434 | callSid: callSid 435 | } 436 | } 437 | 438 | export function phoneRecordOff() { 439 | return { 440 | type: 'PHONE_RECORD_OFF' 441 | } 442 | } 443 | 444 | // This action is tied to the hangup phone phone button 445 | // - this action will call down to server which complete's the conference 446 | // - which then terminates all participant's calls 447 | // - after getting a response from the server this will update the task as complete 448 | export function requestConfTerminate(confSid) { 449 | return(dispatch, getState) => { 450 | const { taskrouter } = getState() 451 | return fetch(urls.conferenceTerminate, { 452 | method: "POST", 453 | headers: { 454 | "Content-Type": "application/x-www-form-urlencoded" 455 | }, 456 | body: "conferenceSid="+confSid+"&token="+taskrouter.worker.token, 457 | }) 458 | .then(response => response.json()) 459 | .then( json => { 460 | console.log(json, "Terminate conf response") 461 | }) 462 | } 463 | } 464 | 465 | // Phone Mute will use the Twilio Device to Mute the call 466 | // -- After the phone is muted a callback will fired to update 467 | // -- the redux store. 468 | export function phoneMute() { 469 | return (dispatch, getState) => { 470 | const { phone } = getState() 471 | console.log("mute clicked") 472 | console.log("Current call is muted? " + phone.currentCall.isMuted()) 473 | phone.currentCall.mute(!phone.currentCall.isMuted()) 474 | } 475 | } 476 | 477 | export function phoneButtonPushed(digit) { 478 | return (dispatch, getState) => { 479 | const { phone } = getState() 480 | console.log("dial pad clicked ", digit) 481 | 482 | phone.currentCall.sendDigits(digit) 483 | } 484 | } 485 | 486 | export function phoneCall() { 487 | return (dispatch, getState) => { 488 | // Call the number that is currently in the number box 489 | // pass the from and to number for the phone call as well as agent name 490 | const { phone, taskrouter } = getState() 491 | console.log("call clicked to " + phone.dialPadNumber) 492 | 493 | return fetch(urls.callOutbound, 494 | { 495 | method: "POST", 496 | headers: { 497 | 'Accept': 'application/json', 498 | 'Content-Type': 'application/x-www-form-urlencoded' 499 | }, 500 | body: 501 | "To=" + phone.dialPadNumber + "&" + 502 | "From=" + taskrouter.worker.attributes.phone_number + "&" + 503 | "Agent=" + taskrouter.worker.friendlyName + "&" + 504 | "Token=" + taskrouter.worker.token 505 | }) 506 | .then(response => response.json()) 507 | .then( json => { 508 | console.log(json) 509 | }) 510 | 511 | } 512 | } 513 | 514 | export function phoneDialCustomer(number) { 515 | return(dispatch, getState) => { 516 | return fetch(`/api/calls/confin`) 517 | .then(response => response.json()) 518 | .then( json => { 519 | console.log(json) 520 | }) 521 | } 522 | 523 | } 524 | 525 | export function phoneHangup() { 526 | return (dispatch, getState) => { 527 | const { phone } = getState() 528 | phone.currentCall.disconnect() 529 | } 530 | } 531 | 532 | export function chatClientUpdated(client) { 533 | return { 534 | type: 'CHAT_CLIENT_UPDATED', 535 | client: client 536 | } 537 | } 538 | 539 | export function requestChat(identity) { 540 | return (dispatch, getState) => { 541 | return fetch(`/api/tokens/chat/${identity}/browser`) 542 | .then(response => response.json()) 543 | .then(json => { 544 | try { 545 | let chatClient = new Twilio.Chat.Client(json.token, {logLevel: 'debug'}) 546 | dispatch(chatClientUpdated(chatClient)) 547 | chatClient.on('channelJoined', (channel) => { 548 | console.log("joined chat channel") 549 | channel.on('messageAdded', (message) => { 550 | console.log("message added") 551 | }) 552 | }) 553 | } 554 | catch (e) { 555 | console.log(e) 556 | } 557 | }) 558 | } 559 | } 560 | 561 | function chatUpdateChannel(channel) { 562 | return { 563 | type: 'CHAT_UPDATE_CHANNEL', 564 | channel: channel 565 | } 566 | } 567 | 568 | function chatAddMessage(message) { 569 | return { 570 | type: 'CHAT_ADD_MESSAGE', 571 | message: message 572 | } 573 | } 574 | 575 | export function chatNewRequest(task) { 576 | return (dispatch, getState) => { 577 | const currState = getState() 578 | console.log(currState) 579 | currState.chat.client.getChannelBySid(task.attributes.chat_channel) 580 | .then(channel => { 581 | console.log(channel) 582 | channel.on('memberJoined', (member) => { 583 | console.log("JOINED") 584 | }) 585 | channel.on('messageAdded', (message) => { 586 | console.log(message) 587 | dispatch(chatAddMessage({channel: message.channel.sid, author: message.author, body: message.body})) 588 | }) 589 | channel.add(currState.taskrouter.worker.friendlyName) 590 | .then(error => { 591 | console.log(error) 592 | channel.sendMessage("Brian is in the house") 593 | }) 594 | .catch(error => { 595 | console.log(error) 596 | }) 597 | dispatch(chatUpdateChannel(channel)) 598 | 599 | }) 600 | 601 | } 602 | } 603 | 604 | export function videoRequest(task) { 605 | return (dispatch, getState) => { 606 | let worker = task.workerName 607 | return fetch(`/api/tokens/chat/${worker}/browser`) 608 | .then(response => response.json()) 609 | .then(json => { 610 | try { 611 | let videoClient = new Twilio.Video.connect(json.token, {name:'brian-test'}) 612 | .then(room => { 613 | console.log(room, "VIDEO CREATED") 614 | let participant = room.participants.values().next().value 615 | dispatch(videoParticipantConnected(participant)) 616 | room.on('participantConnected', (participant) => { 617 | console.log('A remote Participant connected: ', participant) 618 | //dispatch(videoParticipantConnected(participant)) 619 | }) 620 | }) 621 | 622 | } 623 | catch (e) { 624 | console.log(e) 625 | } 626 | }) 627 | 628 | } 629 | } 630 | 631 | function videoParticipantConnected(participant) { 632 | return { 633 | type: 'VIDEO_PARTICIPANT_CONNECTED', 634 | participant: participant 635 | } 636 | } 637 | 638 | const getActivitySid = (activities, activityName) => { 639 | let activity = activities.find((activity) => 640 | activity.friendlyName == activityName) 641 | if (activity) { 642 | return activity.sid 643 | } else { 644 | return "no-activity-found" 645 | } 646 | } 647 | -------------------------------------------------------------------------------- /src/components/common/Button.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | 5 | class Button extends React.Component { 6 | 7 | constructor(props) { 8 | super(props); 9 | this.onClick = this.onClick.bind(this); 10 | } 11 | 12 | onClick() { 13 | this.props.onClick(); 14 | } 15 | 16 | 17 | render() { 18 | var classes = this.props.classes.join(" "); 19 | return ( 20 | 21 | ); 22 | } 23 | } 24 | 25 | Button.propTypes = { 26 | buttonText: React.PropTypes.string.isRequired, 27 | classes: React.PropTypes.array, 28 | disabled: React.PropTypes.bool 29 | } 30 | Button.defaultProps = { 31 | disabled: false 32 | } 33 | 34 | export default Button; 35 | -------------------------------------------------------------------------------- /src/components/messages/MessageBox.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | const MessageBox = ({messages}) => { 4 | const thread = messages.map((message) => 5 |
6 | message.body 7 |
8 | ) 9 | return ( 10 |
11 | { thread } 12 |
13 | ) 14 | } 15 | 16 | MessageBox.propTypes = { 17 | 18 | } 19 | 20 | MessageBox.defaultProps = { messages: [] }; 21 | 22 | export default MessageBox; 23 | -------------------------------------------------------------------------------- /src/components/messages/MessageControl.js: -------------------------------------------------------------------------------- 1 | import Button from '../common/Button.js' 2 | import React, { PropTypes } from 'react'; 3 | 4 | const MessageControl = () => ( 5 |
6 |
7 |
9 |
10 | ) 11 | 12 | MessageControl.propTypes = { 13 | 14 | } 15 | 16 | export default MessageControl; 17 | -------------------------------------------------------------------------------- /src/components/messages/MessageEntry.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | const MessageEntry = () => ( 4 |
5 | 6 |
7 | ) 8 | 9 | MessageEntry.propTypes = { 10 | 11 | } 12 | 13 | export default MessageEntry; 14 | -------------------------------------------------------------------------------- /src/components/messages/Messenger.js: -------------------------------------------------------------------------------- 1 | import MessageBox from './MessageBox'; 2 | import MessageEntry from './MessageEntry'; 3 | import MessageControl from './MessageControl' 4 | 5 | import React, { PropTypes } from 'react'; 6 | 7 | const Messenger = ({messages}) => ( 8 |
9 | 10 | 11 | 12 |
13 | ) 14 | 15 | Messenger.propTypes = { 16 | 17 | } 18 | 19 | export default Messenger; 20 | -------------------------------------------------------------------------------- /src/components/messages/MessengerContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Messenger from './Messenger'; 4 | 5 | import { connect } from 'react-redux' 6 | import {requestChat} from '../../actions' 7 | 8 | 9 | const mapStateToProps = (state) => { 10 | const { chat } = state 11 | 12 | return { 13 | messages: chat.messages, 14 | } 15 | } 16 | 17 | const MessageBoxContainer = connect(mapStateToProps)(Messenger) 18 | 19 | export default MessageBoxContainer 20 | -------------------------------------------------------------------------------- /src/components/phone/CallControl.js: -------------------------------------------------------------------------------- 1 | import Button from '../common/Button.js'; 2 | import React, { PropTypes } from 'react'; 3 | 4 | const CallControl = ({ 5 | status, 6 | mute, 7 | hangup, 8 | call, 9 | hold, 10 | record, 11 | isMuted, 12 | isHeld, 13 | isRecording, 14 | recordingCallSid, 15 | task 16 | }) => { 17 | let buttons, callSid, confSid 18 | // Make sure the conference attributes are there 19 | 20 | if (task && typeof task.attributes.conference != "undefined") { 21 | callSid = task.attributes.conference.participants.customer 22 | confSid = task.attributes.conference.sid 23 | } 24 | // if status is open then we are on a call 25 | if (status == "open") { 26 | buttons = 27 |
28 |
34 | } else { 35 | buttons =