git clone -b twilio https://github.com/philnash/react-express-starter.git twilio-chat-kendo cd twilio-chat-kendo npm install
.env.example
file to .env
then fill in the blanks with your Twilio account SID, the chat service, and API keys you generated earlier.cp .env.example .env
npm run dev
twilio-chat
module to connect with Twilio Chat and then a few KendoReact modules that will provide the components we're going to use:npm install twilio-chat @progress/kendo-react-conversational-ui @progress/kendo-react-inputs @progress/kendo-react-buttons @progress/kendo-react-intl @progress/kendo-theme-material
src/App.js
back to the basics, including the CSS for the KendoReact Material theme:import React, { Component } from 'react'; import '@progress/kendo-theme-material/dist/all.css'; class App extends Component { constructor(props) { super(props); } render() { return <p>Hello world</p>; } } export default App;
<head>
of public/index.html
:<!DOCTYPE html> <html lang="en"> <head> <!-- rest of the head --> <title>React App</title> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous" /> </head>
src/Login.js
, and open it up. We'll make this a functional component as the login form itself doesn't need to store any state. Start with the following boilerplate:import React from 'react'; const Login = props => { return; }; export default Login;
Button
and Input
components:import React from 'react'; import { Button } from '@progress/kendo-react-buttons'; import { Input } from '@progress/kendo-react-inputs';
Login
function to return the following JSX:const Login = props => { return ( <form className="k-form" onSubmit={props.handleLogin}> <fieldset> <legend>Log in</legend> <div className="mb-3"> <Input name="username" label="Username" required={true} style={{ width: '100%' }} value={props.username} onChange={props.handleUsernameChange} /> </div> <div> <Button type="submit" primary={true}> Sign in </Button> </div> </fieldset> </form> ); };
<form>
containing a <fieldset>
and <legend>
. Then inside there is an <Input>
component and a <Button>
component. These are the KendoReact components that we imported. They act like regular <input>
and <button>
elements but fit in with the KendoReact style.<App>
component so we can pass them in as properties.src/App.js
and start by importing the new <Login>
component.import React, { Component } from 'react'; import '@progress/kendo-theme-material/dist/all.css'; import Login from './Login';
<Login>
component. One function needs to handle the user typing in the input and update the username stored in the state. The other handles the form being submitted and will set the state to show that the user is logged in. Add these below the <App>
component's constructor in src/App.js
:handleLogin(event) { event.preventDefault(); this.setState({ loggedIn: true }); } handleUsernameChange(event) { this.setState({ username: event.target.value }); }
constructor(props) { super(props); this.state = { username: '', loggedIn: false }; this.handleLogin = this.handleLogin.bind(this); this.handleUsernameChange = this.handleUsernameChange.bind(this); }
render
function to show the username if the state says the user is logged in, and the <Login>
component otherwise.render() { let loginOrChat; if (this.state.loggedIn) { loginOrChat = <p>Logged in as {this.state.username}</p>; } else { loginOrChat = ( <Login handleLogin={this.handleLogin} handleUsernameChange={this.handleUsernameChange} username={this.state.username} /> ); } return ( <div className="container"> <div className="row mt-3 justify-content-center">{loginOrChat}</div> </div> ); }
npm run dev
and open localhost:3000
. Enter your name in the form and press enter or click "Sign in".src/ChatApp.js
and open it up. We'll create a class based component for the chat app, so add the following boilerplate:import React, { Component } from 'react'; class ChatApp extends Component { } export default ChatApp;
src/ChatApp.js
add:import React, { Component } from 'react'; import Chat from 'twilio-chat'; import { Chat as ChatUI } from '@progress/kendo-react-conversational-ui';
true
.class ChatApp extends Component { constructor(props) { super(props); this.state = { error: null, isLoading: true, messages: [] }; } }
/chat/token
endpoint. We'll use the fetch
API to make the request as part of the componentDidMount
lifecycle event. We use componentDidMount
here as the React documentation tells us that this is a good place to load external data.json
method then once it's parsed, we can use the token to initialise the Chat client.catch
method.ChatApp
class below the constructor:componentDidMount() { fetch('/chat/token', { headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, method: 'POST', body: `identity=${encodeURIComponent(this.props.username)}` }) .then(res => res.json()) .then(data => Chat.create(data.token)) .then(this.setupChatClient) .catch(this.handleError); }
handleError(error) { console.error(error); this.setState({ error: 'Could not load chat.' }); }
setupChatClient
method to the class:setupChatClient(client) { this.client = client; this.client .getChannelByUniqueName('general') .then(channel => channel) .catch(error => { if (error.body.code === 50300) { return this.client.createChannel({ uniqueName: 'general' }); } else { this.handleError(error); } }) .then(channel => { this.channel = channel; return this.channel.join().catch(() => {}); }) .then(() => { // Success! }) .catch(this.handleError); }
isLoading
variable to false
. We also need to load existing messages and set up a listener for new messages.// Success!
comment above with:.then(() => { this.setState({ isLoading: false }); this.channel.getMessages().then(this.messagesLoaded); this.channel.on('messageAdded', this.messageAdded); })
messagesLoaded
and messageAdded
methods we just referenced above, but before we do we need to consider the format that the KendoReact conversational UI wants the messages. We need to translate the message object from the format Twilio provides it to that which can be used by the conversational UI component.twilioMessageToKendoMessage(message) { return { text: message.body, author: { id: message.author, name: message.author }, timestamp: message.timestamp }; }
messagesLoaded
and messageAdded
methods. messagesLoaded
runs when we first load the existing messages to a channel so we fill up state.messages
with all the messages we receive.messagesLoaded(messagePage) { this.setState({ messages: messagePage.items.map(this.twilioMessageToKendoMessage) }); }
messageAdded
will receive one message as its argument so we use the callback version of setState
to add the message to the list. Note we also use the spread operator (...
) to copy the existing messages into the new state.messageAdded(message) { this.setState(prevState => ({ messages: [ ...prevState.messages, this.twilioMessageToKendoMessage(message) ] })); }
messageAdded
event we are listening to on the channel.ChatApp
class:sendMessage(event) { this.channel.sendMessage(event.message.text); }
componentWillUnmount() { this.client.shutdown(); }
constructor(props) { super(props); this.state = { error: null, isLoading: true, messages: [] }; this.user = { id: props.username, name: props.username }; this.setupChatClient = this.setupChatClient.bind(this); this.messagesLoaded = this.messagesLoaded.bind(this); this.messageAdded = this.messageAdded.bind(this); this.sendMessage = this.sendMessage.bind(this); this.handleError = this.handleError.bind(this); }
render
method in src/ChatApp.js
that handles the various states of the component. If there are errors or if the chat is still loading, we will render a message, otherwise we will render the KendoReact Conversational UI component, passing the user object, the messages and the callback method to be run when the user sends a message.render() { if (this.state.error) { return <p>{this.state.error}</p>; } else if (this.state.isLoading) { return <p>Loading chat...</p>; } return ( <ChatUI user={this.user} messages={this.state.messages} onMessageSend={this.sendMessage} width={500} /> ); }
<App>
component. Import the <ChatApp>
component at the top of src/App.js
.import React, { Component } from 'react'; import Login from './Login'; import ChatApp from './ChatApp'; import '@progress/kendo-theme-material/dist/all.css';
render
function of the <App> component
to return the <ChatApp>
component when the user is logged in.render() { let loginOrChat; if (this.state.loggedIn) { loginOrChat = <ChatApp username={this.state.username} />; } else { loginOrChat = ( <Login handleLogin={this.handleLogin} handleUsernameChange={this.handleUsernameChange} username={this.state.username} /> ); } return ( <div className="container"> <div className="row mt-3">{loginOrChat}</div> </div> );