As the Programmable Chat API is set to sunset in 2022, we will no longer maintain these chat tutorials.
Please see our Conversations API QuickStart to start building robust virtual spaces for conversation.
Programmable Chat has been deprecated and is no longer supported. Instead, we'll be focusing on the next generation of chat: Twilio Conversations. Find out more about the EOL process here.
If you're starting a new project, please visit the Conversations Docs to begin. If you've already built on Programmable Chat, please visit our Migration Guide to learn about how to switch.
Ready to implement a chat application using Twilio Programmable Chat Client, Node.js and Express?
This application allows users to exchange messages through different channels, using the Twilio Programmable Chat API. In this example, we'll show how to use this API capabilities to manage channels and their usages.
For your convenience, we consolidated the source code for this tutorial in a single GitHub repository. Feel free to clone it and tweak it as required.
In order to create a Twilio Programmable Chat client, you will need an access token. This token holds information about your Twilio Account and Programmable Chat API keys.
We generate this token by creating a new AccessToken
and providing it with a ChatGrant
. The new AccessToken object is created using your Twilio credentials.
services/tokenService.js
1const twilio = require('twilio');23const AccessToken = twilio.jwt.AccessToken;4const ChatGrant = AccessToken.ChatGrant;56function TokenGenerator(identity) {7const appName = 'TwilioChat';89// Create a "grant" which enables a client to use Chat as a given user10const chatGrant = new ChatGrant({11serviceSid: process.env.TWILIO_CHAT_SERVICE_SID,12});1314// Create an access token which we will sign and return to the client,15// containing the grant we just created16const token = new AccessToken(17process.env.TWILIO_ACCOUNT_SID,18process.env.TWILIO_API_KEY,19process.env.TWILIO_API_SECRET20);2122token.addGrant(chatGrant);23token.identity = identity;2425return token;26}2728module.exports = { generate: TokenGenerator };29
We can generate a token, now we need a way for the chat app to get it.
On our controller we expose the endpoint for token generation. This endpoint is responsible for providing a valid token when passed this parameter:
identity
: identifies the user itself.Once we have used the tokenService
object to generate a token we can use the AccessToken's method as token.toJwt()
to get the token as a String. Then we just return the token as a JSON encoded string.
routes/token.js
1var express = require('express');2var router = express.Router();3var TokenService = require('../services/tokenService');45// POST /token6router.post('/', function(req, res) {7var identity = req.body.identity;89var token = TokenService.generate(identity)1011res.json({12identity: identity,13token: token.toJwt(),14});15});1617// GET /token18router.get('/', function(req, res) {19var identity = req.query.identity;2021var token = TokenService.generate(identity)2223res.json({24identity: identity,25token: token.toJwt(),26});27});282930module.exports = router;31
Now that we have a route that generates JWT tokens on demand, let's use this route to initialize our Twilio Chat Client.
Our client fetches a new Token by making a POST
request to our endpoint when it calls the fetchAccessToken
method.
With the token, we can then instantiate Twilio.Chat.Client
in connectMessagingClient
.
public/js/twiliochat.js
1var twiliochat = (function() {2var tc = {};34var GENERAL_CHANNEL_UNIQUE_NAME = 'general';5var GENERAL_CHANNEL_NAME = 'General Channel';6var MESSAGES_HISTORY_LIMIT = 50;78var $channelList;9var $inputText;10var $usernameInput;11var $statusRow;12var $connectPanel;13var $newChannelInputRow;14var $newChannelInput;15var $typingRow;16var $typingPlaceholder;1718$(document).ready(function() {19tc.init();20});2122tc.init = function() {23tc.$messageList = $('#message-list');24$channelList = $('#channel-list');25$inputText = $('#input-text');26$usernameInput = $('#username-input');27$statusRow = $('#status-row');28$connectPanel = $('#connect-panel');29$newChannelInputRow = $('#new-channel-input-row');30$newChannelInput = $('#new-channel-input');31$typingRow = $('#typing-row');32$typingPlaceholder = $('#typing-placeholder');33$usernameInput.focus();34$usernameInput.on('keypress', handleUsernameInputKeypress);35$inputText.on('keypress', handleInputTextKeypress);36$newChannelInput.on('keypress', tc.handleNewChannelInputKeypress);37$('#connect-image').on('click', connectClientWithUsername);38$('#add-channel-image').on('click', showAddChannelInput);39$('#leave-span').on('click', disconnectClient);40$('#delete-channel-span').on('click', deleteCurrentChannel);41};4243function handleUsernameInputKeypress(event) {44if (event.keyCode === 13){45connectClientWithUsername();46}47}4849function handleInputTextKeypress(event) {50if (event.keyCode === 13) {51tc.currentChannel.sendMessage($(this).val());52event.preventDefault();53$(this).val('');54}55else {56notifyTyping();57}58}5960var notifyTyping = $.throttle(function() {61tc.currentChannel.typing();62}, 1000);6364tc.handleNewChannelInputKeypress = function(event) {65if (event.keyCode === 13) {66tc.messagingClient67.createChannel({68friendlyName: $newChannelInput.val(),69})70.then(hideAddChannelInput);7172$(this).val('');73event.preventDefault();74}75};7677function connectClientWithUsername() {78var usernameText = $usernameInput.val();79$usernameInput.val('');80if (usernameText == '') {81alert('Username cannot be empty');82return;83}84tc.username = usernameText;85fetchAccessToken(tc.username, connectMessagingClient);86}8788function fetchAccessToken(username, handler) {89$.post('/token', {identity: username}, null, 'json')90.done(function(response) {91handler(response.token);92})93.fail(function(error) {94console.log('Failed to fetch the Access Token with error: ' + error);95});96}9798function connectMessagingClient(token) {99// Initialize the Chat messaging client100Twilio.Chat.Client.create(token).then(function(client) {101tc.messagingClient = client;102updateConnectedUI();103tc.loadChannelList(tc.joinGeneralChannel);104tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));105tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));106tc.messagingClient.on('tokenExpired', refreshToken);107});108}109110function refreshToken() {111fetchAccessToken(tc.username, setNewToken);112}113114function setNewToken(token) {115tc.messagingClient.updateToken(tokenResponse.token);116}117118function updateConnectedUI() {119$('#username-span').text(tc.username);120$statusRow.addClass('connected').removeClass('disconnected');121tc.$messageList.addClass('connected').removeClass('disconnected');122$connectPanel.addClass('connected').removeClass('disconnected');123$inputText.addClass('with-shadow');124$typingRow.addClass('connected').removeClass('disconnected');125}126127tc.loadChannelList = function(handler) {128if (tc.messagingClient === undefined) {129console.log('Client is not initialized');130return;131}132133tc.messagingClient.getPublicChannelDescriptors().then(function(channels) {134tc.channelArray = tc.sortChannelsByName(channels.items);135$channelList.text('');136tc.channelArray.forEach(addChannel);137if (typeof handler === 'function') {138handler();139}140});141};142143tc.joinGeneralChannel = function() {144console.log('Attempting to join "general" chat channel...');145if (!tc.generalChannel) {146// If it doesn't exist, let's create it147tc.messagingClient.createChannel({148uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,149friendlyName: GENERAL_CHANNEL_NAME150}).then(function(channel) {151console.log('Created general channel');152tc.generalChannel = channel;153tc.loadChannelList(tc.joinGeneralChannel);154});155}156else {157console.log('Found general channel:');158setupChannel(tc.generalChannel);159}160};161162function initChannel(channel) {163console.log('Initialized channel ' + channel.friendlyName);164return tc.messagingClient.getChannelBySid(channel.sid);165}166167function joinChannel(_channel) {168return _channel.join()169.then(function(joinedChannel) {170console.log('Joined channel ' + joinedChannel.friendlyName);171updateChannelUI(_channel);172173return joinedChannel;174})175.catch(function(err) {176if (_channel.status == 'joined') {177updateChannelUI(_channel);178return _channel;179}180console.error(181"Couldn't join channel " + _channel.friendlyName + ' because -> ' + err182);183});184}185186function initChannelEvents() {187console.log(tc.currentChannel.friendlyName + ' ready.');188tc.currentChannel.on('messageAdded', tc.addMessageToList);189tc.currentChannel.on('typingStarted', showTypingStarted);190tc.currentChannel.on('typingEnded', hideTypingStarted);191tc.currentChannel.on('memberJoined', notifyMemberJoined);192tc.currentChannel.on('memberLeft', notifyMemberLeft);193$inputText.prop('disabled', false).focus();194}195196function setupChannel(channel) {197return leaveCurrentChannel()198.then(function() {199return initChannel(channel);200})201.then(function(_channel) {202return joinChannel(_channel);203})204.then(initChannelEvents);205}206207tc.loadMessages = function() {208tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT).then(function (messages) {209messages.items.forEach(tc.addMessageToList);210});211};212213function leaveCurrentChannel() {214if (tc.currentChannel) {215return tc.currentChannel.leave().then(function(leftChannel) {216console.log('left ' + leftChannel.friendlyName);217leftChannel.removeListener('messageAdded', tc.addMessageToList);218leftChannel.removeListener('typingStarted', showTypingStarted);219leftChannel.removeListener('typingEnded', hideTypingStarted);220leftChannel.removeListener('memberJoined', notifyMemberJoined);221leftChannel.removeListener('memberLeft', notifyMemberLeft);222});223} else {224return Promise.resolve();225}226}227228tc.addMessageToList = function(message) {229var rowDiv = $('<div>').addClass('row no-margin');230rowDiv.loadTemplate($('#message-template'), {231username: message.author,232date: dateFormatter.getTodayDate(message.dateCreated),233body: message.body234});235if (message.author === tc.username) {236rowDiv.addClass('own-message');237}238239tc.$messageList.append(rowDiv);240scrollToMessageListBottom();241};242243function notifyMemberJoined(member) {244notify(member.identity + ' joined the channel')245}246247function notifyMemberLeft(member) {248notify(member.identity + ' left the channel');249}250251function notify(message) {252var row = $('<div>').addClass('col-md-12');253row.loadTemplate('#member-notification-template', {254status: message255});256tc.$messageList.append(row);257scrollToMessageListBottom();258}259260function showTypingStarted(member) {261$typingPlaceholder.text(member.identity + ' is typing...');262}263264function hideTypingStarted(member) {265$typingPlaceholder.text('');266}267268function scrollToMessageListBottom() {269tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);270}271272function updateChannelUI(selectedChannel) {273var channelElements = $('.channel-element').toArray();274var channelElement = channelElements.filter(function(element) {275return $(element).data().sid === selectedChannel.sid;276});277channelElement = $(channelElement);278if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {279tc.currentChannelContainer = channelElement;280}281tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');282channelElement.removeClass('unselected-channel').addClass('selected-channel');283tc.currentChannelContainer = channelElement;284tc.currentChannel = selectedChannel;285tc.loadMessages();286}287288function showAddChannelInput() {289if (tc.messagingClient) {290$newChannelInputRow.addClass('showing').removeClass('not-showing');291$channelList.addClass('showing').removeClass('not-showing');292$newChannelInput.focus();293}294}295296function hideAddChannelInput() {297$newChannelInputRow.addClass('not-showing').removeClass('showing');298$channelList.addClass('not-showing').removeClass('showing');299$newChannelInput.val('');300}301302function addChannel(channel) {303if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {304tc.generalChannel = channel;305}306var rowDiv = $('<div>').addClass('row channel-row');307rowDiv.loadTemplate('#channel-template', {308channelName: channel.friendlyName309});310311var channelP = rowDiv.children().children().first();312313rowDiv.on('click', selectChannel);314channelP.data('sid', channel.sid);315if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {316tc.currentChannelContainer = channelP;317channelP.addClass('selected-channel');318}319else {320channelP.addClass('unselected-channel')321}322323$channelList.append(rowDiv);324}325326function deleteCurrentChannel() {327if (!tc.currentChannel) {328return;329}330331if (tc.currentChannel.sid === tc.generalChannel.sid) {332alert('You cannot delete the general channel');333return;334}335336tc.currentChannel337.delete()338.then(function(channel) {339console.log('channel: '+ channel.friendlyName + ' deleted');340setupChannel(tc.generalChannel);341});342}343344function selectChannel(event) {345var target = $(event.target);346var channelSid = target.data().sid;347var selectedChannel = tc.channelArray.filter(function(channel) {348return channel.sid === channelSid;349})[0];350if (selectedChannel === tc.currentChannel) {351return;352}353setupChannel(selectedChannel);354};355356function disconnectClient() {357leaveCurrentChannel();358$channelList.text('');359tc.$messageList.text('');360channels = undefined;361$statusRow.addClass('disconnected').removeClass('connected');362tc.$messageList.addClass('disconnected').removeClass('connected');363$connectPanel.addClass('disconnected').removeClass('connected');364$inputText.removeClass('with-shadow');365$typingRow.addClass('disconnected').removeClass('connected');366}367368tc.sortChannelsByName = function(channels) {369return channels.sort(function(a, b) {370if (a.friendlyName === GENERAL_CHANNEL_NAME) {371return -1;372}373if (b.friendlyName === GENERAL_CHANNEL_NAME) {374return 1;375}376return a.friendlyName.localeCompare(b.friendlyName);377});378};379380return tc;381})();382
Now that we've instantiated our Chat Client, let's see how we can get a list of channels.
After initializing the client we can use its getPublicChannelDescriptors
method to retrieve all visible channels. The method returns a promise which we use to show the list of channels retrieved on the UI.
public/js/twiliochat.js
1var twiliochat = (function() {2var tc = {};34var GENERAL_CHANNEL_UNIQUE_NAME = 'general';5var GENERAL_CHANNEL_NAME = 'General Channel';6var MESSAGES_HISTORY_LIMIT = 50;78var $channelList;9var $inputText;10var $usernameInput;11var $statusRow;12var $connectPanel;13var $newChannelInputRow;14var $newChannelInput;15var $typingRow;16var $typingPlaceholder;1718$(document).ready(function() {19tc.init();20});2122tc.init = function() {23tc.$messageList = $('#message-list');24$channelList = $('#channel-list');25$inputText = $('#input-text');26$usernameInput = $('#username-input');27$statusRow = $('#status-row');28$connectPanel = $('#connect-panel');29$newChannelInputRow = $('#new-channel-input-row');30$newChannelInput = $('#new-channel-input');31$typingRow = $('#typing-row');32$typingPlaceholder = $('#typing-placeholder');33$usernameInput.focus();34$usernameInput.on('keypress', handleUsernameInputKeypress);35$inputText.on('keypress', handleInputTextKeypress);36$newChannelInput.on('keypress', tc.handleNewChannelInputKeypress);37$('#connect-image').on('click', connectClientWithUsername);38$('#add-channel-image').on('click', showAddChannelInput);39$('#leave-span').on('click', disconnectClient);40$('#delete-channel-span').on('click', deleteCurrentChannel);41};4243function handleUsernameInputKeypress(event) {44if (event.keyCode === 13){45connectClientWithUsername();46}47}4849function handleInputTextKeypress(event) {50if (event.keyCode === 13) {51tc.currentChannel.sendMessage($(this).val());52event.preventDefault();53$(this).val('');54}55else {56notifyTyping();57}58}5960var notifyTyping = $.throttle(function() {61tc.currentChannel.typing();62}, 1000);6364tc.handleNewChannelInputKeypress = function(event) {65if (event.keyCode === 13) {66tc.messagingClient67.createChannel({68friendlyName: $newChannelInput.val(),69})70.then(hideAddChannelInput);7172$(this).val('');73event.preventDefault();74}75};7677function connectClientWithUsername() {78var usernameText = $usernameInput.val();79$usernameInput.val('');80if (usernameText == '') {81alert('Username cannot be empty');82return;83}84tc.username = usernameText;85fetchAccessToken(tc.username, connectMessagingClient);86}8788function fetchAccessToken(username, handler) {89$.post('/token', {identity: username}, null, 'json')90.done(function(response) {91handler(response.token);92})93.fail(function(error) {94console.log('Failed to fetch the Access Token with error: ' + error);95});96}9798function connectMessagingClient(token) {99// Initialize the Chat messaging client100Twilio.Chat.Client.create(token).then(function(client) {101tc.messagingClient = client;102updateConnectedUI();103tc.loadChannelList(tc.joinGeneralChannel);104tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));105tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));106tc.messagingClient.on('tokenExpired', refreshToken);107});108}109110function refreshToken() {111fetchAccessToken(tc.username, setNewToken);112}113114function setNewToken(token) {115tc.messagingClient.updateToken(tokenResponse.token);116}117118function updateConnectedUI() {119$('#username-span').text(tc.username);120$statusRow.addClass('connected').removeClass('disconnected');121tc.$messageList.addClass('connected').removeClass('disconnected');122$connectPanel.addClass('connected').removeClass('disconnected');123$inputText.addClass('with-shadow');124$typingRow.addClass('connected').removeClass('disconnected');125}126127tc.loadChannelList = function(handler) {128if (tc.messagingClient === undefined) {129console.log('Client is not initialized');130return;131}132133tc.messagingClient.getPublicChannelDescriptors().then(function(channels) {134tc.channelArray = tc.sortChannelsByName(channels.items);135$channelList.text('');136tc.channelArray.forEach(addChannel);137if (typeof handler === 'function') {138handler();139}140});141};142143tc.joinGeneralChannel = function() {144console.log('Attempting to join "general" chat channel...');145if (!tc.generalChannel) {146// If it doesn't exist, let's create it147tc.messagingClient.createChannel({148uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,149friendlyName: GENERAL_CHANNEL_NAME150}).then(function(channel) {151console.log('Created general channel');152tc.generalChannel = channel;153tc.loadChannelList(tc.joinGeneralChannel);154});155}156else {157console.log('Found general channel:');158setupChannel(tc.generalChannel);159}160};161162function initChannel(channel) {163console.log('Initialized channel ' + channel.friendlyName);164return tc.messagingClient.getChannelBySid(channel.sid);165}166167function joinChannel(_channel) {168return _channel.join()169.then(function(joinedChannel) {170console.log('Joined channel ' + joinedChannel.friendlyName);171updateChannelUI(_channel);172173return joinedChannel;174})175.catch(function(err) {176if (_channel.status == 'joined') {177updateChannelUI(_channel);178return _channel;179}180console.error(181"Couldn't join channel " + _channel.friendlyName + ' because -> ' + err182);183});184}185186function initChannelEvents() {187console.log(tc.currentChannel.friendlyName + ' ready.');188tc.currentChannel.on('messageAdded', tc.addMessageToList);189tc.currentChannel.on('typingStarted', showTypingStarted);190tc.currentChannel.on('typingEnded', hideTypingStarted);191tc.currentChannel.on('memberJoined', notifyMemberJoined);192tc.currentChannel.on('memberLeft', notifyMemberLeft);193$inputText.prop('disabled', false).focus();194}195196function setupChannel(channel) {197return leaveCurrentChannel()198.then(function() {199return initChannel(channel);200})201.then(function(_channel) {202return joinChannel(_channel);203})204.then(initChannelEvents);205}206207tc.loadMessages = function() {208tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT).then(function (messages) {209messages.items.forEach(tc.addMessageToList);210});211};212213function leaveCurrentChannel() {214if (tc.currentChannel) {215return tc.currentChannel.leave().then(function(leftChannel) {216console.log('left ' + leftChannel.friendlyName);217leftChannel.removeListener('messageAdded', tc.addMessageToList);218leftChannel.removeListener('typingStarted', showTypingStarted);219leftChannel.removeListener('typingEnded', hideTypingStarted);220leftChannel.removeListener('memberJoined', notifyMemberJoined);221leftChannel.removeListener('memberLeft', notifyMemberLeft);222});223} else {224return Promise.resolve();225}226}227228tc.addMessageToList = function(message) {229var rowDiv = $('<div>').addClass('row no-margin');230rowDiv.loadTemplate($('#message-template'), {231username: message.author,232date: dateFormatter.getTodayDate(message.dateCreated),233body: message.body234});235if (message.author === tc.username) {236rowDiv.addClass('own-message');237}238239tc.$messageList.append(rowDiv);240scrollToMessageListBottom();241};242243function notifyMemberJoined(member) {244notify(member.identity + ' joined the channel')245}246247function notifyMemberLeft(member) {248notify(member.identity + ' left the channel');249}250251function notify(message) {252var row = $('<div>').addClass('col-md-12');253row.loadTemplate('#member-notification-template', {254status: message255});256tc.$messageList.append(row);257scrollToMessageListBottom();258}259260function showTypingStarted(member) {261$typingPlaceholder.text(member.identity + ' is typing...');262}263264function hideTypingStarted(member) {265$typingPlaceholder.text('');266}267268function scrollToMessageListBottom() {269tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);270}271272function updateChannelUI(selectedChannel) {273var channelElements = $('.channel-element').toArray();274var channelElement = channelElements.filter(function(element) {275return $(element).data().sid === selectedChannel.sid;276});277channelElement = $(channelElement);278if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {279tc.currentChannelContainer = channelElement;280}281tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');282channelElement.removeClass('unselected-channel').addClass('selected-channel');283tc.currentChannelContainer = channelElement;284tc.currentChannel = selectedChannel;285tc.loadMessages();286}287288function showAddChannelInput() {289if (tc.messagingClient) {290$newChannelInputRow.addClass('showing').removeClass('not-showing');291$channelList.addClass('showing').removeClass('not-showing');292$newChannelInput.focus();293}294}295296function hideAddChannelInput() {297$newChannelInputRow.addClass('not-showing').removeClass('showing');298$channelList.addClass('not-showing').removeClass('showing');299$newChannelInput.val('');300}301302function addChannel(channel) {303if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {304tc.generalChannel = channel;305}306var rowDiv = $('<div>').addClass('row channel-row');307rowDiv.loadTemplate('#channel-template', {308channelName: channel.friendlyName309});310311var channelP = rowDiv.children().children().first();312313rowDiv.on('click', selectChannel);314channelP.data('sid', channel.sid);315if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {316tc.currentChannelContainer = channelP;317channelP.addClass('selected-channel');318}319else {320channelP.addClass('unselected-channel')321}322323$channelList.append(rowDiv);324}325326function deleteCurrentChannel() {327if (!tc.currentChannel) {328return;329}330331if (tc.currentChannel.sid === tc.generalChannel.sid) {332alert('You cannot delete the general channel');333return;334}335336tc.currentChannel337.delete()338.then(function(channel) {339console.log('channel: '+ channel.friendlyName + ' deleted');340setupChannel(tc.generalChannel);341});342}343344function selectChannel(event) {345var target = $(event.target);346var channelSid = target.data().sid;347var selectedChannel = tc.channelArray.filter(function(channel) {348return channel.sid === channelSid;349})[0];350if (selectedChannel === tc.currentChannel) {351return;352}353setupChannel(selectedChannel);354};355356function disconnectClient() {357leaveCurrentChannel();358$channelList.text('');359tc.$messageList.text('');360channels = undefined;361$statusRow.addClass('disconnected').removeClass('connected');362tc.$messageList.addClass('disconnected').removeClass('connected');363$connectPanel.addClass('disconnected').removeClass('connected');364$inputText.removeClass('with-shadow');365$typingRow.addClass('disconnected').removeClass('connected');366}367368tc.sortChannelsByName = function(channels) {369return channels.sort(function(a, b) {370if (a.friendlyName === GENERAL_CHANNEL_NAME) {371return -1;372}373if (b.friendlyName === GENERAL_CHANNEL_NAME) {374return 1;375}376return a.friendlyName.localeCompare(b.friendlyName);377});378};379380return tc;381})();382
Next, we need a default channel.
This application will try to join a channel called "General Channel" when it starts. If the channel doesn't exist, we'll create one with that name. The scope of this example application will show you how to work only with public channels, but the Programmable Chat client allows you to create private channels and handle invitations.
Notice we set a unique name for the general channel as we don't want to create a new general channel every time we start the application.
public/js/twiliochat.js
1var twiliochat = (function() {2var tc = {};34var GENERAL_CHANNEL_UNIQUE_NAME = 'general';5var GENERAL_CHANNEL_NAME = 'General Channel';6var MESSAGES_HISTORY_LIMIT = 50;78var $channelList;9var $inputText;10var $usernameInput;11var $statusRow;12var $connectPanel;13var $newChannelInputRow;14var $newChannelInput;15var $typingRow;16var $typingPlaceholder;1718$(document).ready(function() {19tc.init();20});2122tc.init = function() {23tc.$messageList = $('#message-list');24$channelList = $('#channel-list');25$inputText = $('#input-text');26$usernameInput = $('#username-input');27$statusRow = $('#status-row');28$connectPanel = $('#connect-panel');29$newChannelInputRow = $('#new-channel-input-row');30$newChannelInput = $('#new-channel-input');31$typingRow = $('#typing-row');32$typingPlaceholder = $('#typing-placeholder');33$usernameInput.focus();34$usernameInput.on('keypress', handleUsernameInputKeypress);35$inputText.on('keypress', handleInputTextKeypress);36$newChannelInput.on('keypress', tc.handleNewChannelInputKeypress);37$('#connect-image').on('click', connectClientWithUsername);38$('#add-channel-image').on('click', showAddChannelInput);39$('#leave-span').on('click', disconnectClient);40$('#delete-channel-span').on('click', deleteCurrentChannel);41};4243function handleUsernameInputKeypress(event) {44if (event.keyCode === 13){45connectClientWithUsername();46}47}4849function handleInputTextKeypress(event) {50if (event.keyCode === 13) {51tc.currentChannel.sendMessage($(this).val());52event.preventDefault();53$(this).val('');54}55else {56notifyTyping();57}58}5960var notifyTyping = $.throttle(function() {61tc.currentChannel.typing();62}, 1000);6364tc.handleNewChannelInputKeypress = function(event) {65if (event.keyCode === 13) {66tc.messagingClient67.createChannel({68friendlyName: $newChannelInput.val(),69})70.then(hideAddChannelInput);7172$(this).val('');73event.preventDefault();74}75};7677function connectClientWithUsername() {78var usernameText = $usernameInput.val();79$usernameInput.val('');80if (usernameText == '') {81alert('Username cannot be empty');82return;83}84tc.username = usernameText;85fetchAccessToken(tc.username, connectMessagingClient);86}8788function fetchAccessToken(username, handler) {89$.post('/token', {identity: username}, null, 'json')90.done(function(response) {91handler(response.token);92})93.fail(function(error) {94console.log('Failed to fetch the Access Token with error: ' + error);95});96}9798function connectMessagingClient(token) {99// Initialize the Chat messaging client100Twilio.Chat.Client.create(token).then(function(client) {101tc.messagingClient = client;102updateConnectedUI();103tc.loadChannelList(tc.joinGeneralChannel);104tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));105tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));106tc.messagingClient.on('tokenExpired', refreshToken);107});108}109110function refreshToken() {111fetchAccessToken(tc.username, setNewToken);112}113114function setNewToken(token) {115tc.messagingClient.updateToken(tokenResponse.token);116}117118function updateConnectedUI() {119$('#username-span').text(tc.username);120$statusRow.addClass('connected').removeClass('disconnected');121tc.$messageList.addClass('connected').removeClass('disconnected');122$connectPanel.addClass('connected').removeClass('disconnected');123$inputText.addClass('with-shadow');124$typingRow.addClass('connected').removeClass('disconnected');125}126127tc.loadChannelList = function(handler) {128if (tc.messagingClient === undefined) {129console.log('Client is not initialized');130return;131}132133tc.messagingClient.getPublicChannelDescriptors().then(function(channels) {134tc.channelArray = tc.sortChannelsByName(channels.items);135$channelList.text('');136tc.channelArray.forEach(addChannel);137if (typeof handler === 'function') {138handler();139}140});141};142143tc.joinGeneralChannel = function() {144console.log('Attempting to join "general" chat channel...');145if (!tc.generalChannel) {146// If it doesn't exist, let's create it147tc.messagingClient.createChannel({148uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,149friendlyName: GENERAL_CHANNEL_NAME150}).then(function(channel) {151console.log('Created general channel');152tc.generalChannel = channel;153tc.loadChannelList(tc.joinGeneralChannel);154});155}156else {157console.log('Found general channel:');158setupChannel(tc.generalChannel);159}160};161162function initChannel(channel) {163console.log('Initialized channel ' + channel.friendlyName);164return tc.messagingClient.getChannelBySid(channel.sid);165}166167function joinChannel(_channel) {168return _channel.join()169.then(function(joinedChannel) {170console.log('Joined channel ' + joinedChannel.friendlyName);171updateChannelUI(_channel);172173return joinedChannel;174})175.catch(function(err) {176if (_channel.status == 'joined') {177updateChannelUI(_channel);178return _channel;179}180console.error(181"Couldn't join channel " + _channel.friendlyName + ' because -> ' + err182);183});184}185186function initChannelEvents() {187console.log(tc.currentChannel.friendlyName + ' ready.');188tc.currentChannel.on('messageAdded', tc.addMessageToList);189tc.currentChannel.on('typingStarted', showTypingStarted);190tc.currentChannel.on('typingEnded', hideTypingStarted);191tc.currentChannel.on('memberJoined', notifyMemberJoined);192tc.currentChannel.on('memberLeft', notifyMemberLeft);193$inputText.prop('disabled', false).focus();194}195196function setupChannel(channel) {197return leaveCurrentChannel()198.then(function() {199return initChannel(channel);200})201.then(function(_channel) {202return joinChannel(_channel);203})204.then(initChannelEvents);205}206207tc.loadMessages = function() {208tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT).then(function (messages) {209messages.items.forEach(tc.addMessageToList);210});211};212213function leaveCurrentChannel() {214if (tc.currentChannel) {215return tc.currentChannel.leave().then(function(leftChannel) {216console.log('left ' + leftChannel.friendlyName);217leftChannel.removeListener('messageAdded', tc.addMessageToList);218leftChannel.removeListener('typingStarted', showTypingStarted);219leftChannel.removeListener('typingEnded', hideTypingStarted);220leftChannel.removeListener('memberJoined', notifyMemberJoined);221leftChannel.removeListener('memberLeft', notifyMemberLeft);222});223} else {224return Promise.resolve();225}226}227228tc.addMessageToList = function(message) {229var rowDiv = $('<div>').addClass('row no-margin');230rowDiv.loadTemplate($('#message-template'), {231username: message.author,232date: dateFormatter.getTodayDate(message.dateCreated),233body: message.body234});235if (message.author === tc.username) {236rowDiv.addClass('own-message');237}238239tc.$messageList.append(rowDiv);240scrollToMessageListBottom();241};242243function notifyMemberJoined(member) {244notify(member.identity + ' joined the channel')245}246247function notifyMemberLeft(member) {248notify(member.identity + ' left the channel');249}250251function notify(message) {252var row = $('<div>').addClass('col-md-12');253row.loadTemplate('#member-notification-template', {254status: message255});256tc.$messageList.append(row);257scrollToMessageListBottom();258}259260function showTypingStarted(member) {261$typingPlaceholder.text(member.identity + ' is typing...');262}263264function hideTypingStarted(member) {265$typingPlaceholder.text('');266}267268function scrollToMessageListBottom() {269tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);270}271272function updateChannelUI(selectedChannel) {273var channelElements = $('.channel-element').toArray();274var channelElement = channelElements.filter(function(element) {275return $(element).data().sid === selectedChannel.sid;276});277channelElement = $(channelElement);278if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {279tc.currentChannelContainer = channelElement;280}281tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');282channelElement.removeClass('unselected-channel').addClass('selected-channel');283tc.currentChannelContainer = channelElement;284tc.currentChannel = selectedChannel;285tc.loadMessages();286}287288function showAddChannelInput() {289if (tc.messagingClient) {290$newChannelInputRow.addClass('showing').removeClass('not-showing');291$channelList.addClass('showing').removeClass('not-showing');292$newChannelInput.focus();293}294}295296function hideAddChannelInput() {297$newChannelInputRow.addClass('not-showing').removeClass('showing');298$channelList.addClass('not-showing').removeClass('showing');299$newChannelInput.val('');300}301302function addChannel(channel) {303if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {304tc.generalChannel = channel;305}306var rowDiv = $('<div>').addClass('row channel-row');307rowDiv.loadTemplate('#channel-template', {308channelName: channel.friendlyName309});310311var channelP = rowDiv.children().children().first();312313rowDiv.on('click', selectChannel);314channelP.data('sid', channel.sid);315if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {316tc.currentChannelContainer = channelP;317channelP.addClass('selected-channel');318}319else {320channelP.addClass('unselected-channel')321}322323$channelList.append(rowDiv);324}325326function deleteCurrentChannel() {327if (!tc.currentChannel) {328return;329}330331if (tc.currentChannel.sid === tc.generalChannel.sid) {332alert('You cannot delete the general channel');333return;334}335336tc.currentChannel337.delete()338.then(function(channel) {339console.log('channel: '+ channel.friendlyName + ' deleted');340setupChannel(tc.generalChannel);341});342}343344function selectChannel(event) {345var target = $(event.target);346var channelSid = target.data().sid;347var selectedChannel = tc.channelArray.filter(function(channel) {348return channel.sid === channelSid;349})[0];350if (selectedChannel === tc.currentChannel) {351return;352}353setupChannel(selectedChannel);354};355356function disconnectClient() {357leaveCurrentChannel();358$channelList.text('');359tc.$messageList.text('');360channels = undefined;361$statusRow.addClass('disconnected').removeClass('connected');362tc.$messageList.addClass('disconnected').removeClass('connected');363$connectPanel.addClass('disconnected').removeClass('connected');364$inputText.removeClass('with-shadow');365$typingRow.addClass('disconnected').removeClass('connected');366}367368tc.sortChannelsByName = function(channels) {369return channels.sort(function(a, b) {370if (a.friendlyName === GENERAL_CHANNEL_NAME) {371return -1;372}373if (b.friendlyName === GENERAL_CHANNEL_NAME) {374return 1;375}376return a.friendlyName.localeCompare(b.friendlyName);377});378};379380return tc;381})();382
Now let's listen for some channel events.
Next, we listen for channel events. In our case, we're setting listeners to the following events:
messageAdded
: When another member sends a message to the channel you are connected to.typingStarted
: When another member is typing a message on the channel that you are connected to.typingEnded
: When another member stops typing a message on the channel that you are connected to.memberJoined
: When another member joins the channel that you are connected to.memberLeft
: When another member leaves the channel that you are connected to.We register a different function to handle each particular event.
public/js/twiliochat.js
1var twiliochat = (function() {2var tc = {};34var GENERAL_CHANNEL_UNIQUE_NAME = 'general';5var GENERAL_CHANNEL_NAME = 'General Channel';6var MESSAGES_HISTORY_LIMIT = 50;78var $channelList;9var $inputText;10var $usernameInput;11var $statusRow;12var $connectPanel;13var $newChannelInputRow;14var $newChannelInput;15var $typingRow;16var $typingPlaceholder;1718$(document).ready(function() {19tc.init();20});2122tc.init = function() {23tc.$messageList = $('#message-list');24$channelList = $('#channel-list');25$inputText = $('#input-text');26$usernameInput = $('#username-input');27$statusRow = $('#status-row');28$connectPanel = $('#connect-panel');29$newChannelInputRow = $('#new-channel-input-row');30$newChannelInput = $('#new-channel-input');31$typingRow = $('#typing-row');32$typingPlaceholder = $('#typing-placeholder');33$usernameInput.focus();34$usernameInput.on('keypress', handleUsernameInputKeypress);35$inputText.on('keypress', handleInputTextKeypress);36$newChannelInput.on('keypress', tc.handleNewChannelInputKeypress);37$('#connect-image').on('click', connectClientWithUsername);38$('#add-channel-image').on('click', showAddChannelInput);39$('#leave-span').on('click', disconnectClient);40$('#delete-channel-span').on('click', deleteCurrentChannel);41};4243function handleUsernameInputKeypress(event) {44if (event.keyCode === 13){45connectClientWithUsername();46}47}4849function handleInputTextKeypress(event) {50if (event.keyCode === 13) {51tc.currentChannel.sendMessage($(this).val());52event.preventDefault();53$(this).val('');54}55else {56notifyTyping();57}58}5960var notifyTyping = $.throttle(function() {61tc.currentChannel.typing();62}, 1000);6364tc.handleNewChannelInputKeypress = function(event) {65if (event.keyCode === 13) {66tc.messagingClient67.createChannel({68friendlyName: $newChannelInput.val(),69})70.then(hideAddChannelInput);7172$(this).val('');73event.preventDefault();74}75};7677function connectClientWithUsername() {78var usernameText = $usernameInput.val();79$usernameInput.val('');80if (usernameText == '') {81alert('Username cannot be empty');82return;83}84tc.username = usernameText;85fetchAccessToken(tc.username, connectMessagingClient);86}8788function fetchAccessToken(username, handler) {89$.post('/token', {identity: username}, null, 'json')90.done(function(response) {91handler(response.token);92})93.fail(function(error) {94console.log('Failed to fetch the Access Token with error: ' + error);95});96}9798function connectMessagingClient(token) {99// Initialize the Chat messaging client100Twilio.Chat.Client.create(token).then(function(client) {101tc.messagingClient = client;102updateConnectedUI();103tc.loadChannelList(tc.joinGeneralChannel);104tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));105tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));106tc.messagingClient.on('tokenExpired', refreshToken);107});108}109110function refreshToken() {111fetchAccessToken(tc.username, setNewToken);112}113114function setNewToken(token) {115tc.messagingClient.updateToken(tokenResponse.token);116}117118function updateConnectedUI() {119$('#username-span').text(tc.username);120$statusRow.addClass('connected').removeClass('disconnected');121tc.$messageList.addClass('connected').removeClass('disconnected');122$connectPanel.addClass('connected').removeClass('disconnected');123$inputText.addClass('with-shadow');124$typingRow.addClass('connected').removeClass('disconnected');125}126127tc.loadChannelList = function(handler) {128if (tc.messagingClient === undefined) {129console.log('Client is not initialized');130return;131}132133tc.messagingClient.getPublicChannelDescriptors().then(function(channels) {134tc.channelArray = tc.sortChannelsByName(channels.items);135$channelList.text('');136tc.channelArray.forEach(addChannel);137if (typeof handler === 'function') {138handler();139}140});141};142143tc.joinGeneralChannel = function() {144console.log('Attempting to join "general" chat channel...');145if (!tc.generalChannel) {146// If it doesn't exist, let's create it147tc.messagingClient.createChannel({148uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,149friendlyName: GENERAL_CHANNEL_NAME150}).then(function(channel) {151console.log('Created general channel');152tc.generalChannel = channel;153tc.loadChannelList(tc.joinGeneralChannel);154});155}156else {157console.log('Found general channel:');158setupChannel(tc.generalChannel);159}160};161162function initChannel(channel) {163console.log('Initialized channel ' + channel.friendlyName);164return tc.messagingClient.getChannelBySid(channel.sid);165}166167function joinChannel(_channel) {168return _channel.join()169.then(function(joinedChannel) {170console.log('Joined channel ' + joinedChannel.friendlyName);171updateChannelUI(_channel);172173return joinedChannel;174})175.catch(function(err) {176if (_channel.status == 'joined') {177updateChannelUI(_channel);178return _channel;179}180console.error(181"Couldn't join channel " + _channel.friendlyName + ' because -> ' + err182);183});184}185186function initChannelEvents() {187console.log(tc.currentChannel.friendlyName + ' ready.');188tc.currentChannel.on('messageAdded', tc.addMessageToList);189tc.currentChannel.on('typingStarted', showTypingStarted);190tc.currentChannel.on('typingEnded', hideTypingStarted);191tc.currentChannel.on('memberJoined', notifyMemberJoined);192tc.currentChannel.on('memberLeft', notifyMemberLeft);193$inputText.prop('disabled', false).focus();194}195196function setupChannel(channel) {197return leaveCurrentChannel()198.then(function() {199return initChannel(channel);200})201.then(function(_channel) {202return joinChannel(_channel);203})204.then(initChannelEvents);205}206207tc.loadMessages = function() {208tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT).then(function (messages) {209messages.items.forEach(tc.addMessageToList);210});211};212213function leaveCurrentChannel() {214if (tc.currentChannel) {215return tc.currentChannel.leave().then(function(leftChannel) {216console.log('left ' + leftChannel.friendlyName);217leftChannel.removeListener('messageAdded', tc.addMessageToList);218leftChannel.removeListener('typingStarted', showTypingStarted);219leftChannel.removeListener('typingEnded', hideTypingStarted);220leftChannel.removeListener('memberJoined', notifyMemberJoined);221leftChannel.removeListener('memberLeft', notifyMemberLeft);222});223} else {224return Promise.resolve();225}226}227228tc.addMessageToList = function(message) {229var rowDiv = $('<div>').addClass('row no-margin');230rowDiv.loadTemplate($('#message-template'), {231username: message.author,232date: dateFormatter.getTodayDate(message.dateCreated),233body: message.body234});235if (message.author === tc.username) {236rowDiv.addClass('own-message');237}238239tc.$messageList.append(rowDiv);240scrollToMessageListBottom();241};242243function notifyMemberJoined(member) {244notify(member.identity + ' joined the channel')245}246247function notifyMemberLeft(member) {248notify(member.identity + ' left the channel');249}250251function notify(message) {252var row = $('<div>').addClass('col-md-12');253row.loadTemplate('#member-notification-template', {254status: message255});256tc.$messageList.append(row);257scrollToMessageListBottom();258}259260function showTypingStarted(member) {261$typingPlaceholder.text(member.identity + ' is typing...');262}263264function hideTypingStarted(member) {265$typingPlaceholder.text('');266}267268function scrollToMessageListBottom() {269tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);270}271272function updateChannelUI(selectedChannel) {273var channelElements = $('.channel-element').toArray();274var channelElement = channelElements.filter(function(element) {275return $(element).data().sid === selectedChannel.sid;276});277channelElement = $(channelElement);278if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {279tc.currentChannelContainer = channelElement;280}281tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');282channelElement.removeClass('unselected-channel').addClass('selected-channel');283tc.currentChannelContainer = channelElement;284tc.currentChannel = selectedChannel;285tc.loadMessages();286}287288function showAddChannelInput() {289if (tc.messagingClient) {290$newChannelInputRow.addClass('showing').removeClass('not-showing');291$channelList.addClass('showing').removeClass('not-showing');292$newChannelInput.focus();293}294}295296function hideAddChannelInput() {297$newChannelInputRow.addClass('not-showing').removeClass('showing');298$channelList.addClass('not-showing').removeClass('showing');299$newChannelInput.val('');300}301302function addChannel(channel) {303if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {304tc.generalChannel = channel;305}306var rowDiv = $('<div>').addClass('row channel-row');307rowDiv.loadTemplate('#channel-template', {308channelName: channel.friendlyName309});310311var channelP = rowDiv.children().children().first();312313rowDiv.on('click', selectChannel);314channelP.data('sid', channel.sid);315if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {316tc.currentChannelContainer = channelP;317channelP.addClass('selected-channel');318}319else {320channelP.addClass('unselected-channel')321}322323$channelList.append(rowDiv);324}325326function deleteCurrentChannel() {327if (!tc.currentChannel) {328return;329}330331if (tc.currentChannel.sid === tc.generalChannel.sid) {332alert('You cannot delete the general channel');333return;334}335336tc.currentChannel337.delete()338.then(function(channel) {339console.log('channel: '+ channel.friendlyName + ' deleted');340setupChannel(tc.generalChannel);341});342}343344function selectChannel(event) {345var target = $(event.target);346var channelSid = target.data().sid;347var selectedChannel = tc.channelArray.filter(function(channel) {348return channel.sid === channelSid;349})[0];350if (selectedChannel === tc.currentChannel) {351return;352}353setupChannel(selectedChannel);354};355356function disconnectClient() {357leaveCurrentChannel();358$channelList.text('');359tc.$messageList.text('');360channels = undefined;361$statusRow.addClass('disconnected').removeClass('connected');362tc.$messageList.addClass('disconnected').removeClass('connected');363$connectPanel.addClass('disconnected').removeClass('connected');364$inputText.removeClass('with-shadow');365$typingRow.addClass('disconnected').removeClass('connected');366}367368tc.sortChannelsByName = function(channels) {369return channels.sort(function(a, b) {370if (a.friendlyName === GENERAL_CHANNEL_NAME) {371return -1;372}373if (b.friendlyName === GENERAL_CHANNEL_NAME) {374return 1;375}376return a.friendlyName.localeCompare(b.friendlyName);377});378};379380return tc;381})();382
The client emits events as well. Let's see how we can listen to those events as well.
Just like with channels, we can register handlers for events on the Client:
channelAdded
: When a channel becomes visible to the Client.channelRemoved
: When a channel is no longer visible to the Client.tokenExpired
: When the supplied token expires.For a complete list of client events
public/js/twiliochat.js
1var twiliochat = (function() {2var tc = {};34var GENERAL_CHANNEL_UNIQUE_NAME = 'general';5var GENERAL_CHANNEL_NAME = 'General Channel';6var MESSAGES_HISTORY_LIMIT = 50;78var $channelList;9var $inputText;10var $usernameInput;11var $statusRow;12var $connectPanel;13var $newChannelInputRow;14var $newChannelInput;15var $typingRow;16var $typingPlaceholder;1718$(document).ready(function() {19tc.init();20});2122tc.init = function() {23tc.$messageList = $('#message-list');24$channelList = $('#channel-list');25$inputText = $('#input-text');26$usernameInput = $('#username-input');27$statusRow = $('#status-row');28$connectPanel = $('#connect-panel');29$newChannelInputRow = $('#new-channel-input-row');30$newChannelInput = $('#new-channel-input');31$typingRow = $('#typing-row');32$typingPlaceholder = $('#typing-placeholder');33$usernameInput.focus();34$usernameInput.on('keypress', handleUsernameInputKeypress);35$inputText.on('keypress', handleInputTextKeypress);36$newChannelInput.on('keypress', tc.handleNewChannelInputKeypress);37$('#connect-image').on('click', connectClientWithUsername);38$('#add-channel-image').on('click', showAddChannelInput);39$('#leave-span').on('click', disconnectClient);40$('#delete-channel-span').on('click', deleteCurrentChannel);41};4243function handleUsernameInputKeypress(event) {44if (event.keyCode === 13){45connectClientWithUsername();46}47}4849function handleInputTextKeypress(event) {50if (event.keyCode === 13) {51tc.currentChannel.sendMessage($(this).val());52event.preventDefault();53$(this).val('');54}55else {56notifyTyping();57}58}5960var notifyTyping = $.throttle(function() {61tc.currentChannel.typing();62}, 1000);6364tc.handleNewChannelInputKeypress = function(event) {65if (event.keyCode === 13) {66tc.messagingClient67.createChannel({68friendlyName: $newChannelInput.val(),69})70.then(hideAddChannelInput);7172$(this).val('');73event.preventDefault();74}75};7677function connectClientWithUsername() {78var usernameText = $usernameInput.val();79$usernameInput.val('');80if (usernameText == '') {81alert('Username cannot be empty');82return;83}84tc.username = usernameText;85fetchAccessToken(tc.username, connectMessagingClient);86}8788function fetchAccessToken(username, handler) {89$.post('/token', {identity: username}, null, 'json')90.done(function(response) {91handler(response.token);92})93.fail(function(error) {94console.log('Failed to fetch the Access Token with error: ' + error);95});96}9798function connectMessagingClient(token) {99// Initialize the Chat messaging client100Twilio.Chat.Client.create(token).then(function(client) {101tc.messagingClient = client;102updateConnectedUI();103tc.loadChannelList(tc.joinGeneralChannel);104tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));105tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));106tc.messagingClient.on('tokenExpired', refreshToken);107});108}109110function refreshToken() {111fetchAccessToken(tc.username, setNewToken);112}113114function setNewToken(token) {115tc.messagingClient.updateToken(tokenResponse.token);116}117118function updateConnectedUI() {119$('#username-span').text(tc.username);120$statusRow.addClass('connected').removeClass('disconnected');121tc.$messageList.addClass('connected').removeClass('disconnected');122$connectPanel.addClass('connected').removeClass('disconnected');123$inputText.addClass('with-shadow');124$typingRow.addClass('connected').removeClass('disconnected');125}126127tc.loadChannelList = function(handler) {128if (tc.messagingClient === undefined) {129console.log('Client is not initialized');130return;131}132133tc.messagingClient.getPublicChannelDescriptors().then(function(channels) {134tc.channelArray = tc.sortChannelsByName(channels.items);135$channelList.text('');136tc.channelArray.forEach(addChannel);137if (typeof handler === 'function') {138handler();139}140});141};142143tc.joinGeneralChannel = function() {144console.log('Attempting to join "general" chat channel...');145if (!tc.generalChannel) {146// If it doesn't exist, let's create it147tc.messagingClient.createChannel({148uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,149friendlyName: GENERAL_CHANNEL_NAME150}).then(function(channel) {151console.log('Created general channel');152tc.generalChannel = channel;153tc.loadChannelList(tc.joinGeneralChannel);154});155}156else {157console.log('Found general channel:');158setupChannel(tc.generalChannel);159}160};161162function initChannel(channel) {163console.log('Initialized channel ' + channel.friendlyName);164return tc.messagingClient.getChannelBySid(channel.sid);165}166167function joinChannel(_channel) {168return _channel.join()169.then(function(joinedChannel) {170console.log('Joined channel ' + joinedChannel.friendlyName);171updateChannelUI(_channel);172173return joinedChannel;174})175.catch(function(err) {176if (_channel.status == 'joined') {177updateChannelUI(_channel);178return _channel;179}180console.error(181"Couldn't join channel " + _channel.friendlyName + ' because -> ' + err182);183});184}185186function initChannelEvents() {187console.log(tc.currentChannel.friendlyName + ' ready.');188tc.currentChannel.on('messageAdded', tc.addMessageToList);189tc.currentChannel.on('typingStarted', showTypingStarted);190tc.currentChannel.on('typingEnded', hideTypingStarted);191tc.currentChannel.on('memberJoined', notifyMemberJoined);192tc.currentChannel.on('memberLeft', notifyMemberLeft);193$inputText.prop('disabled', false).focus();194}195196function setupChannel(channel) {197return leaveCurrentChannel()198.then(function() {199return initChannel(channel);200})201.then(function(_channel) {202return joinChannel(_channel);203})204.then(initChannelEvents);205}206207tc.loadMessages = function() {208tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT).then(function (messages) {209messages.items.forEach(tc.addMessageToList);210});211};212213function leaveCurrentChannel() {214if (tc.currentChannel) {215return tc.currentChannel.leave().then(function(leftChannel) {216console.log('left ' + leftChannel.friendlyName);217leftChannel.removeListener('messageAdded', tc.addMessageToList);218leftChannel.removeListener('typingStarted', showTypingStarted);219leftChannel.removeListener('typingEnded', hideTypingStarted);220leftChannel.removeListener('memberJoined', notifyMemberJoined);221leftChannel.removeListener('memberLeft', notifyMemberLeft);222});223} else {224return Promise.resolve();225}226}227228tc.addMessageToList = function(message) {229var rowDiv = $('<div>').addClass('row no-margin');230rowDiv.loadTemplate($('#message-template'), {231username: message.author,232date: dateFormatter.getTodayDate(message.dateCreated),233body: message.body234});235if (message.author === tc.username) {236rowDiv.addClass('own-message');237}238239tc.$messageList.append(rowDiv);240scrollToMessageListBottom();241};242243function notifyMemberJoined(member) {244notify(member.identity + ' joined the channel')245}246247function notifyMemberLeft(member) {248notify(member.identity + ' left the channel');249}250251function notify(message) {252var row = $('<div>').addClass('col-md-12');253row.loadTemplate('#member-notification-template', {254status: message255});256tc.$messageList.append(row);257scrollToMessageListBottom();258}259260function showTypingStarted(member) {261$typingPlaceholder.text(member.identity + ' is typing...');262}263264function hideTypingStarted(member) {265$typingPlaceholder.text('');266}267268function scrollToMessageListBottom() {269tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);270}271272function updateChannelUI(selectedChannel) {273var channelElements = $('.channel-element').toArray();274var channelElement = channelElements.filter(function(element) {275return $(element).data().sid === selectedChannel.sid;276});277channelElement = $(channelElement);278if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {279tc.currentChannelContainer = channelElement;280}281tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');282channelElement.removeClass('unselected-channel').addClass('selected-channel');283tc.currentChannelContainer = channelElement;284tc.currentChannel = selectedChannel;285tc.loadMessages();286}287288function showAddChannelInput() {289if (tc.messagingClient) {290$newChannelInputRow.addClass('showing').removeClass('not-showing');291$channelList.addClass('showing').removeClass('not-showing');292$newChannelInput.focus();293}294}295296function hideAddChannelInput() {297$newChannelInputRow.addClass('not-showing').removeClass('showing');298$channelList.addClass('not-showing').removeClass('showing');299$newChannelInput.val('');300}301302function addChannel(channel) {303if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {304tc.generalChannel = channel;305}306var rowDiv = $('<div>').addClass('row channel-row');307rowDiv.loadTemplate('#channel-template', {308channelName: channel.friendlyName309});310311var channelP = rowDiv.children().children().first();312313rowDiv.on('click', selectChannel);314channelP.data('sid', channel.sid);315if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {316tc.currentChannelContainer = channelP;317channelP.addClass('selected-channel');318}319else {320channelP.addClass('unselected-channel')321}322323$channelList.append(rowDiv);324}325326function deleteCurrentChannel() {327if (!tc.currentChannel) {328return;329}330331if (tc.currentChannel.sid === tc.generalChannel.sid) {332alert('You cannot delete the general channel');333return;334}335336tc.currentChannel337.delete()338.then(function(channel) {339console.log('channel: '+ channel.friendlyName + ' deleted');340setupChannel(tc.generalChannel);341});342}343344function selectChannel(event) {345var target = $(event.target);346var channelSid = target.data().sid;347var selectedChannel = tc.channelArray.filter(function(channel) {348return channel.sid === channelSid;349})[0];350if (selectedChannel === tc.currentChannel) {351return;352}353setupChannel(selectedChannel);354};355356function disconnectClient() {357leaveCurrentChannel();358$channelList.text('');359tc.$messageList.text('');360channels = undefined;361$statusRow.addClass('disconnected').removeClass('connected');362tc.$messageList.addClass('disconnected').removeClass('connected');363$connectPanel.addClass('disconnected').removeClass('connected');364$inputText.removeClass('with-shadow');365$typingRow.addClass('disconnected').removeClass('connected');366}367368tc.sortChannelsByName = function(channels) {369return channels.sort(function(a, b) {370if (a.friendlyName === GENERAL_CHANNEL_NAME) {371return -1;372}373if (b.friendlyName === GENERAL_CHANNEL_NAME) {374return 1;375}376return a.friendlyName.localeCompare(b.friendlyName);377});378};379380return tc;381})();382
We've actually got a real chat app going here, but let's make it more interesting with multiple channels.
When a user clicks on the "+ Channel" link we'll show an input text field where it's possible to type the name of the new channel. Creating a channel involves calling createChannel
with an object that has the friendlyName
key. You can create a channel with more options listed on the Channels section of the Programmable Chat documentation.
public/js/twiliochat.js
1var twiliochat = (function() {2var tc = {};34var GENERAL_CHANNEL_UNIQUE_NAME = 'general';5var GENERAL_CHANNEL_NAME = 'General Channel';6var MESSAGES_HISTORY_LIMIT = 50;78var $channelList;9var $inputText;10var $usernameInput;11var $statusRow;12var $connectPanel;13var $newChannelInputRow;14var $newChannelInput;15var $typingRow;16var $typingPlaceholder;1718$(document).ready(function() {19tc.init();20});2122tc.init = function() {23tc.$messageList = $('#message-list');24$channelList = $('#channel-list');25$inputText = $('#input-text');26$usernameInput = $('#username-input');27$statusRow = $('#status-row');28$connectPanel = $('#connect-panel');29$newChannelInputRow = $('#new-channel-input-row');30$newChannelInput = $('#new-channel-input');31$typingRow = $('#typing-row');32$typingPlaceholder = $('#typing-placeholder');33$usernameInput.focus();34$usernameInput.on('keypress', handleUsernameInputKeypress);35$inputText.on('keypress', handleInputTextKeypress);36$newChannelInput.on('keypress', tc.handleNewChannelInputKeypress);37$('#connect-image').on('click', connectClientWithUsername);38$('#add-channel-image').on('click', showAddChannelInput);39$('#leave-span').on('click', disconnectClient);40$('#delete-channel-span').on('click', deleteCurrentChannel);41};4243function handleUsernameInputKeypress(event) {44if (event.keyCode === 13){45connectClientWithUsername();46}47}4849function handleInputTextKeypress(event) {50if (event.keyCode === 13) {51tc.currentChannel.sendMessage($(this).val());52event.preventDefault();53$(this).val('');54}55else {56notifyTyping();57}58}5960var notifyTyping = $.throttle(function() {61tc.currentChannel.typing();62}, 1000);6364tc.handleNewChannelInputKeypress = function(event) {65if (event.keyCode === 13) {66tc.messagingClient67.createChannel({68friendlyName: $newChannelInput.val(),69})70.then(hideAddChannelInput);7172$(this).val('');73event.preventDefault();74}75};7677function connectClientWithUsername() {78var usernameText = $usernameInput.val();79$usernameInput.val('');80if (usernameText == '') {81alert('Username cannot be empty');82return;83}84tc.username = usernameText;85fetchAccessToken(tc.username, connectMessagingClient);86}8788function fetchAccessToken(username, handler) {89$.post('/token', {identity: username}, null, 'json')90.done(function(response) {91handler(response.token);92})93.fail(function(error) {94console.log('Failed to fetch the Access Token with error: ' + error);95});96}9798function connectMessagingClient(token) {99// Initialize the Chat messaging client100Twilio.Chat.Client.create(token).then(function(client) {101tc.messagingClient = client;102updateConnectedUI();103tc.loadChannelList(tc.joinGeneralChannel);104tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));105tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));106tc.messagingClient.on('tokenExpired', refreshToken);107});108}109110function refreshToken() {111fetchAccessToken(tc.username, setNewToken);112}113114function setNewToken(token) {115tc.messagingClient.updateToken(tokenResponse.token);116}117118function updateConnectedUI() {119$('#username-span').text(tc.username);120$statusRow.addClass('connected').removeClass('disconnected');121tc.$messageList.addClass('connected').removeClass('disconnected');122$connectPanel.addClass('connected').removeClass('disconnected');123$inputText.addClass('with-shadow');124$typingRow.addClass('connected').removeClass('disconnected');125}126127tc.loadChannelList = function(handler) {128if (tc.messagingClient === undefined) {129console.log('Client is not initialized');130return;131}132133tc.messagingClient.getPublicChannelDescriptors().then(function(channels) {134tc.channelArray = tc.sortChannelsByName(channels.items);135$channelList.text('');136tc.channelArray.forEach(addChannel);137if (typeof handler === 'function') {138handler();139}140});141};142143tc.joinGeneralChannel = function() {144console.log('Attempting to join "general" chat channel...');145if (!tc.generalChannel) {146// If it doesn't exist, let's create it147tc.messagingClient.createChannel({148uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,149friendlyName: GENERAL_CHANNEL_NAME150}).then(function(channel) {151console.log('Created general channel');152tc.generalChannel = channel;153tc.loadChannelList(tc.joinGeneralChannel);154});155}156else {157console.log('Found general channel:');158setupChannel(tc.generalChannel);159}160};161162function initChannel(channel) {163console.log('Initialized channel ' + channel.friendlyName);164return tc.messagingClient.getChannelBySid(channel.sid);165}166167function joinChannel(_channel) {168return _channel.join()169.then(function(joinedChannel) {170console.log('Joined channel ' + joinedChannel.friendlyName);171updateChannelUI(_channel);172173return joinedChannel;174})175.catch(function(err) {176if (_channel.status == 'joined') {177updateChannelUI(_channel);178return _channel;179}180console.error(181"Couldn't join channel " + _channel.friendlyName + ' because -> ' + err182);183});184}185186function initChannelEvents() {187console.log(tc.currentChannel.friendlyName + ' ready.');188tc.currentChannel.on('messageAdded', tc.addMessageToList);189tc.currentChannel.on('typingStarted', showTypingStarted);190tc.currentChannel.on('typingEnded', hideTypingStarted);191tc.currentChannel.on('memberJoined', notifyMemberJoined);192tc.currentChannel.on('memberLeft', notifyMemberLeft);193$inputText.prop('disabled', false).focus();194}195196function setupChannel(channel) {197return leaveCurrentChannel()198.then(function() {199return initChannel(channel);200})201.then(function(_channel) {202return joinChannel(_channel);203})204.then(initChannelEvents);205}206207tc.loadMessages = function() {208tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT).then(function (messages) {209messages.items.forEach(tc.addMessageToList);210});211};212213function leaveCurrentChannel() {214if (tc.currentChannel) {215return tc.currentChannel.leave().then(function(leftChannel) {216console.log('left ' + leftChannel.friendlyName);217leftChannel.removeListener('messageAdded', tc.addMessageToList);218leftChannel.removeListener('typingStarted', showTypingStarted);219leftChannel.removeListener('typingEnded', hideTypingStarted);220leftChannel.removeListener('memberJoined', notifyMemberJoined);221leftChannel.removeListener('memberLeft', notifyMemberLeft);222});223} else {224return Promise.resolve();225}226}227228tc.addMessageToList = function(message) {229var rowDiv = $('<div>').addClass('row no-margin');230rowDiv.loadTemplate($('#message-template'), {231username: message.author,232date: dateFormatter.getTodayDate(message.dateCreated),233body: message.body234});235if (message.author === tc.username) {236rowDiv.addClass('own-message');237}238239tc.$messageList.append(rowDiv);240scrollToMessageListBottom();241};242243function notifyMemberJoined(member) {244notify(member.identity + ' joined the channel')245}246247function notifyMemberLeft(member) {248notify(member.identity + ' left the channel');249}250251function notify(message) {252var row = $('<div>').addClass('col-md-12');253row.loadTemplate('#member-notification-template', {254status: message255});256tc.$messageList.append(row);257scrollToMessageListBottom();258}259260function showTypingStarted(member) {261$typingPlaceholder.text(member.identity + ' is typing...');262}263264function hideTypingStarted(member) {265$typingPlaceholder.text('');266}267268function scrollToMessageListBottom() {269tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);270}271272function updateChannelUI(selectedChannel) {273var channelElements = $('.channel-element').toArray();274var channelElement = channelElements.filter(function(element) {275return $(element).data().sid === selectedChannel.sid;276});277channelElement = $(channelElement);278if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {279tc.currentChannelContainer = channelElement;280}281tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');282channelElement.removeClass('unselected-channel').addClass('selected-channel');283tc.currentChannelContainer = channelElement;284tc.currentChannel = selectedChannel;285tc.loadMessages();286}287288function showAddChannelInput() {289if (tc.messagingClient) {290$newChannelInputRow.addClass('showing').removeClass('not-showing');291$channelList.addClass('showing').removeClass('not-showing');292$newChannelInput.focus();293}294}295296function hideAddChannelInput() {297$newChannelInputRow.addClass('not-showing').removeClass('showing');298$channelList.addClass('not-showing').removeClass('showing');299$newChannelInput.val('');300}301302function addChannel(channel) {303if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {304tc.generalChannel = channel;305}306var rowDiv = $('<div>').addClass('row channel-row');307rowDiv.loadTemplate('#channel-template', {308channelName: channel.friendlyName309});310311var channelP = rowDiv.children().children().first();312313rowDiv.on('click', selectChannel);314channelP.data('sid', channel.sid);315if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {316tc.currentChannelContainer = channelP;317channelP.addClass('selected-channel');318}319else {320channelP.addClass('unselected-channel')321}322323$channelList.append(rowDiv);324}325326function deleteCurrentChannel() {327if (!tc.currentChannel) {328return;329}330331if (tc.currentChannel.sid === tc.generalChannel.sid) {332alert('You cannot delete the general channel');333return;334}335336tc.currentChannel337.delete()338.then(function(channel) {339console.log('channel: '+ channel.friendlyName + ' deleted');340setupChannel(tc.generalChannel);341});342}343344function selectChannel(event) {345var target = $(event.target);346var channelSid = target.data().sid;347var selectedChannel = tc.channelArray.filter(function(channel) {348return channel.sid === channelSid;349})[0];350if (selectedChannel === tc.currentChannel) {351return;352}353setupChannel(selectedChannel);354};355356function disconnectClient() {357leaveCurrentChannel();358$channelList.text('');359tc.$messageList.text('');360channels = undefined;361$statusRow.addClass('disconnected').removeClass('connected');362tc.$messageList.addClass('disconnected').removeClass('connected');363$connectPanel.addClass('disconnected').removeClass('connected');364$inputText.removeClass('with-shadow');365$typingRow.addClass('disconnected').removeClass('connected');366}367368tc.sortChannelsByName = function(channels) {369return channels.sort(function(a, b) {370if (a.friendlyName === GENERAL_CHANNEL_NAME) {371return -1;372}373if (b.friendlyName === GENERAL_CHANNEL_NAME) {374return 1;375}376return a.friendlyName.localeCompare(b.friendlyName);377});378};379380return tc;381})();382
Next, we will see how we can switch between channels.
When you tap on the name of a channel from the sidebar, that channel is set as the selectedChannel
. The selectChannel
method takes care of joining to the selected channel and setting up the selectedChannel
.
public/js/twiliochat.js
1var twiliochat = (function() {2var tc = {};34var GENERAL_CHANNEL_UNIQUE_NAME = 'general';5var GENERAL_CHANNEL_NAME = 'General Channel';6var MESSAGES_HISTORY_LIMIT = 50;78var $channelList;9var $inputText;10var $usernameInput;11var $statusRow;12var $connectPanel;13var $newChannelInputRow;14var $newChannelInput;15var $typingRow;16var $typingPlaceholder;1718$(document).ready(function() {19tc.init();20});2122tc.init = function() {23tc.$messageList = $('#message-list');24$channelList = $('#channel-list');25$inputText = $('#input-text');26$usernameInput = $('#username-input');27$statusRow = $('#status-row');28$connectPanel = $('#connect-panel');29$newChannelInputRow = $('#new-channel-input-row');30$newChannelInput = $('#new-channel-input');31$typingRow = $('#typing-row');32$typingPlaceholder = $('#typing-placeholder');33$usernameInput.focus();34$usernameInput.on('keypress', handleUsernameInputKeypress);35$inputText.on('keypress', handleInputTextKeypress);36$newChannelInput.on('keypress', tc.handleNewChannelInputKeypress);37$('#connect-image').on('click', connectClientWithUsername);38$('#add-channel-image').on('click', showAddChannelInput);39$('#leave-span').on('click', disconnectClient);40$('#delete-channel-span').on('click', deleteCurrentChannel);41};4243function handleUsernameInputKeypress(event) {44if (event.keyCode === 13){45connectClientWithUsername();46}47}4849function handleInputTextKeypress(event) {50if (event.keyCode === 13) {51tc.currentChannel.sendMessage($(this).val());52event.preventDefault();53$(this).val('');54}55else {56notifyTyping();57}58}5960var notifyTyping = $.throttle(function() {61tc.currentChannel.typing();62}, 1000);6364tc.handleNewChannelInputKeypress = function(event) {65if (event.keyCode === 13) {66tc.messagingClient67.createChannel({68friendlyName: $newChannelInput.val(),69})70.then(hideAddChannelInput);7172$(this).val('');73event.preventDefault();74}75};7677function connectClientWithUsername() {78var usernameText = $usernameInput.val();79$usernameInput.val('');80if (usernameText == '') {81alert('Username cannot be empty');82return;83}84tc.username = usernameText;85fetchAccessToken(tc.username, connectMessagingClient);86}8788function fetchAccessToken(username, handler) {89$.post('/token', {identity: username}, null, 'json')90.done(function(response) {91handler(response.token);92})93.fail(function(error) {94console.log('Failed to fetch the Access Token with error: ' + error);95});96}9798function connectMessagingClient(token) {99// Initialize the Chat messaging client100Twilio.Chat.Client.create(token).then(function(client) {101tc.messagingClient = client;102updateConnectedUI();103tc.loadChannelList(tc.joinGeneralChannel);104tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));105tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));106tc.messagingClient.on('tokenExpired', refreshToken);107});108}109110function refreshToken() {111fetchAccessToken(tc.username, setNewToken);112}113114function setNewToken(token) {115tc.messagingClient.updateToken(tokenResponse.token);116}117118function updateConnectedUI() {119$('#username-span').text(tc.username);120$statusRow.addClass('connected').removeClass('disconnected');121tc.$messageList.addClass('connected').removeClass('disconnected');122$connectPanel.addClass('connected').removeClass('disconnected');123$inputText.addClass('with-shadow');124$typingRow.addClass('connected').removeClass('disconnected');125}126127tc.loadChannelList = function(handler) {128if (tc.messagingClient === undefined) {129console.log('Client is not initialized');130return;131}132133tc.messagingClient.getPublicChannelDescriptors().then(function(channels) {134tc.channelArray = tc.sortChannelsByName(channels.items);135$channelList.text('');136tc.channelArray.forEach(addChannel);137if (typeof handler === 'function') {138handler();139}140});141};142143tc.joinGeneralChannel = function() {144console.log('Attempting to join "general" chat channel...');145if (!tc.generalChannel) {146// If it doesn't exist, let's create it147tc.messagingClient.createChannel({148uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,149friendlyName: GENERAL_CHANNEL_NAME150}).then(function(channel) {151console.log('Created general channel');152tc.generalChannel = channel;153tc.loadChannelList(tc.joinGeneralChannel);154});155}156else {157console.log('Found general channel:');158setupChannel(tc.generalChannel);159}160};161162function initChannel(channel) {163console.log('Initialized channel ' + channel.friendlyName);164return tc.messagingClient.getChannelBySid(channel.sid);165}166167function joinChannel(_channel) {168return _channel.join()169.then(function(joinedChannel) {170console.log('Joined channel ' + joinedChannel.friendlyName);171updateChannelUI(_channel);172173return joinedChannel;174})175.catch(function(err) {176if (_channel.status == 'joined') {177updateChannelUI(_channel);178return _channel;179}180console.error(181"Couldn't join channel " + _channel.friendlyName + ' because -> ' + err182);183});184}185186function initChannelEvents() {187console.log(tc.currentChannel.friendlyName + ' ready.');188tc.currentChannel.on('messageAdded', tc.addMessageToList);189tc.currentChannel.on('typingStarted', showTypingStarted);190tc.currentChannel.on('typingEnded', hideTypingStarted);191tc.currentChannel.on('memberJoined', notifyMemberJoined);192tc.currentChannel.on('memberLeft', notifyMemberLeft);193$inputText.prop('disabled', false).focus();194}195196function setupChannel(channel) {197return leaveCurrentChannel()198.then(function() {199return initChannel(channel);200})201.then(function(_channel) {202return joinChannel(_channel);203})204.then(initChannelEvents);205}206207tc.loadMessages = function() {208tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT).then(function (messages) {209messages.items.forEach(tc.addMessageToList);210});211};212213function leaveCurrentChannel() {214if (tc.currentChannel) {215return tc.currentChannel.leave().then(function(leftChannel) {216console.log('left ' + leftChannel.friendlyName);217leftChannel.removeListener('messageAdded', tc.addMessageToList);218leftChannel.removeListener('typingStarted', showTypingStarted);219leftChannel.removeListener('typingEnded', hideTypingStarted);220leftChannel.removeListener('memberJoined', notifyMemberJoined);221leftChannel.removeListener('memberLeft', notifyMemberLeft);222});223} else {224return Promise.resolve();225}226}227228tc.addMessageToList = function(message) {229var rowDiv = $('<div>').addClass('row no-margin');230rowDiv.loadTemplate($('#message-template'), {231username: message.author,232date: dateFormatter.getTodayDate(message.dateCreated),233body: message.body234});235if (message.author === tc.username) {236rowDiv.addClass('own-message');237}238239tc.$messageList.append(rowDiv);240scrollToMessageListBottom();241};242243function notifyMemberJoined(member) {244notify(member.identity + ' joined the channel')245}246247function notifyMemberLeft(member) {248notify(member.identity + ' left the channel');249}250251function notify(message) {252var row = $('<div>').addClass('col-md-12');253row.loadTemplate('#member-notification-template', {254status: message255});256tc.$messageList.append(row);257scrollToMessageListBottom();258}259260function showTypingStarted(member) {261$typingPlaceholder.text(member.identity + ' is typing...');262}263264function hideTypingStarted(member) {265$typingPlaceholder.text('');266}267268function scrollToMessageListBottom() {269tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);270}271272function updateChannelUI(selectedChannel) {273var channelElements = $('.channel-element').toArray();274var channelElement = channelElements.filter(function(element) {275return $(element).data().sid === selectedChannel.sid;276});277channelElement = $(channelElement);278if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {279tc.currentChannelContainer = channelElement;280}281tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');282channelElement.removeClass('unselected-channel').addClass('selected-channel');283tc.currentChannelContainer = channelElement;284tc.currentChannel = selectedChannel;285tc.loadMessages();286}287288function showAddChannelInput() {289if (tc.messagingClient) {290$newChannelInputRow.addClass('showing').removeClass('not-showing');291$channelList.addClass('showing').removeClass('not-showing');292$newChannelInput.focus();293}294}295296function hideAddChannelInput() {297$newChannelInputRow.addClass('not-showing').removeClass('showing');298$channelList.addClass('not-showing').removeClass('showing');299$newChannelInput.val('');300}301302function addChannel(channel) {303if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {304tc.generalChannel = channel;305}306var rowDiv = $('<div>').addClass('row channel-row');307rowDiv.loadTemplate('#channel-template', {308channelName: channel.friendlyName309});310311var channelP = rowDiv.children().children().first();312313rowDiv.on('click', selectChannel);314channelP.data('sid', channel.sid);315if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {316tc.currentChannelContainer = channelP;317channelP.addClass('selected-channel');318}319else {320channelP.addClass('unselected-channel')321}322323$channelList.append(rowDiv);324}325326function deleteCurrentChannel() {327if (!tc.currentChannel) {328return;329}330331if (tc.currentChannel.sid === tc.generalChannel.sid) {332alert('You cannot delete the general channel');333return;334}335336tc.currentChannel337.delete()338.then(function(channel) {339console.log('channel: '+ channel.friendlyName + ' deleted');340setupChannel(tc.generalChannel);341});342}343344function selectChannel(event) {345var target = $(event.target);346var channelSid = target.data().sid;347var selectedChannel = tc.channelArray.filter(function(channel) {348return channel.sid === channelSid;349})[0];350if (selectedChannel === tc.currentChannel) {351return;352}353setupChannel(selectedChannel);354};355356function disconnectClient() {357leaveCurrentChannel();358$channelList.text('');359tc.$messageList.text('');360channels = undefined;361$statusRow.addClass('disconnected').removeClass('connected');362tc.$messageList.addClass('disconnected').removeClass('connected');363$connectPanel.addClass('disconnected').removeClass('connected');364$inputText.removeClass('with-shadow');365$typingRow.addClass('disconnected').removeClass('connected');366}367368tc.sortChannelsByName = function(channels) {369return channels.sort(function(a, b) {370if (a.friendlyName === GENERAL_CHANNEL_NAME) {371return -1;372}373if (b.friendlyName === GENERAL_CHANNEL_NAME) {374return 1;375}376return a.friendlyName.localeCompare(b.friendlyName);377});378};379380return tc;381})();382
At some point, your users will want to delete a channel. Let's have a look at how that can be done.
Deleting a channel is easier than creating one. The application lets the user delete the channel they are currently on through the "delete current channel" link. The only thing you need to do to actually delete the channel from Twilio, is call the delete
method on the channel you are trying to delete. Like other methods on the Channel
object, it'll return a promise where you can set the success handler.
public/js/twiliochat.js
1var twiliochat = (function() {2var tc = {};34var GENERAL_CHANNEL_UNIQUE_NAME = 'general';5var GENERAL_CHANNEL_NAME = 'General Channel';6var MESSAGES_HISTORY_LIMIT = 50;78var $channelList;9var $inputText;10var $usernameInput;11var $statusRow;12var $connectPanel;13var $newChannelInputRow;14var $newChannelInput;15var $typingRow;16var $typingPlaceholder;1718$(document).ready(function() {19tc.init();20});2122tc.init = function() {23tc.$messageList = $('#message-list');24$channelList = $('#channel-list');25$inputText = $('#input-text');26$usernameInput = $('#username-input');27$statusRow = $('#status-row');28$connectPanel = $('#connect-panel');29$newChannelInputRow = $('#new-channel-input-row');30$newChannelInput = $('#new-channel-input');31$typingRow = $('#typing-row');32$typingPlaceholder = $('#typing-placeholder');33$usernameInput.focus();34$usernameInput.on('keypress', handleUsernameInputKeypress);35$inputText.on('keypress', handleInputTextKeypress);36$newChannelInput.on('keypress', tc.handleNewChannelInputKeypress);37$('#connect-image').on('click', connectClientWithUsername);38$('#add-channel-image').on('click', showAddChannelInput);39$('#leave-span').on('click', disconnectClient);40$('#delete-channel-span').on('click', deleteCurrentChannel);41};4243function handleUsernameInputKeypress(event) {44if (event.keyCode === 13){45connectClientWithUsername();46}47}4849function handleInputTextKeypress(event) {50if (event.keyCode === 13) {51tc.currentChannel.sendMessage($(this).val());52event.preventDefault();53$(this).val('');54}55else {56notifyTyping();57}58}5960var notifyTyping = $.throttle(function() {61tc.currentChannel.typing();62}, 1000);6364tc.handleNewChannelInputKeypress = function(event) {65if (event.keyCode === 13) {66tc.messagingClient67.createChannel({68friendlyName: $newChannelInput.val(),69})70.then(hideAddChannelInput);7172$(this).val('');73event.preventDefault();74}75};7677function connectClientWithUsername() {78var usernameText = $usernameInput.val();79$usernameInput.val('');80if (usernameText == '') {81alert('Username cannot be empty');82return;83}84tc.username = usernameText;85fetchAccessToken(tc.username, connectMessagingClient);86}8788function fetchAccessToken(username, handler) {89$.post('/token', {identity: username}, null, 'json')90.done(function(response) {91handler(response.token);92})93.fail(function(error) {94console.log('Failed to fetch the Access Token with error: ' + error);95});96}9798function connectMessagingClient(token) {99// Initialize the Chat messaging client100Twilio.Chat.Client.create(token).then(function(client) {101tc.messagingClient = client;102updateConnectedUI();103tc.loadChannelList(tc.joinGeneralChannel);104tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));105tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));106tc.messagingClient.on('tokenExpired', refreshToken);107});108}109110function refreshToken() {111fetchAccessToken(tc.username, setNewToken);112}113114function setNewToken(token) {115tc.messagingClient.updateToken(tokenResponse.token);116}117118function updateConnectedUI() {119$('#username-span').text(tc.username);120$statusRow.addClass('connected').removeClass('disconnected');121tc.$messageList.addClass('connected').removeClass('disconnected');122$connectPanel.addClass('connected').removeClass('disconnected');123$inputText.addClass('with-shadow');124$typingRow.addClass('connected').removeClass('disconnected');125}126127tc.loadChannelList = function(handler) {128if (tc.messagingClient === undefined) {129console.log('Client is not initialized');130return;131}132133tc.messagingClient.getPublicChannelDescriptors().then(function(channels) {134tc.channelArray = tc.sortChannelsByName(channels.items);135$channelList.text('');136tc.channelArray.forEach(addChannel);137if (typeof handler === 'function') {138handler();139}140});141};142143tc.joinGeneralChannel = function() {144console.log('Attempting to join "general" chat channel...');145if (!tc.generalChannel) {146// If it doesn't exist, let's create it147tc.messagingClient.createChannel({148uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,149friendlyName: GENERAL_CHANNEL_NAME150}).then(function(channel) {151console.log('Created general channel');152tc.generalChannel = channel;153tc.loadChannelList(tc.joinGeneralChannel);154});155}156else {157console.log('Found general channel:');158setupChannel(tc.generalChannel);159}160};161162function initChannel(channel) {163console.log('Initialized channel ' + channel.friendlyName);164return tc.messagingClient.getChannelBySid(channel.sid);165}166167function joinChannel(_channel) {168return _channel.join()169.then(function(joinedChannel) {170console.log('Joined channel ' + joinedChannel.friendlyName);171updateChannelUI(_channel);172173return joinedChannel;174})175.catch(function(err) {176if (_channel.status == 'joined') {177updateChannelUI(_channel);178return _channel;179}180console.error(181"Couldn't join channel " + _channel.friendlyName + ' because -> ' + err182);183});184}185186function initChannelEvents() {187console.log(tc.currentChannel.friendlyName + ' ready.');188tc.currentChannel.on('messageAdded', tc.addMessageToList);189tc.currentChannel.on('typingStarted', showTypingStarted);190tc.currentChannel.on('typingEnded', hideTypingStarted);191tc.currentChannel.on('memberJoined', notifyMemberJoined);192tc.currentChannel.on('memberLeft', notifyMemberLeft);193$inputText.prop('disabled', false).focus();194}195196function setupChannel(channel) {197return leaveCurrentChannel()198.then(function() {199return initChannel(channel);200})201.then(function(_channel) {202return joinChannel(_channel);203})204.then(initChannelEvents);205}206207tc.loadMessages = function() {208tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT).then(function (messages) {209messages.items.forEach(tc.addMessageToList);210});211};212213function leaveCurrentChannel() {214if (tc.currentChannel) {215return tc.currentChannel.leave().then(function(leftChannel) {216console.log('left ' + leftChannel.friendlyName);217leftChannel.removeListener('messageAdded', tc.addMessageToList);218leftChannel.removeListener('typingStarted', showTypingStarted);219leftChannel.removeListener('typingEnded', hideTypingStarted);220leftChannel.removeListener('memberJoined', notifyMemberJoined);221leftChannel.removeListener('memberLeft', notifyMemberLeft);222});223} else {224return Promise.resolve();225}226}227228tc.addMessageToList = function(message) {229var rowDiv = $('<div>').addClass('row no-margin');230rowDiv.loadTemplate($('#message-template'), {231username: message.author,232date: dateFormatter.getTodayDate(message.dateCreated),233body: message.body234});235if (message.author === tc.username) {236rowDiv.addClass('own-message');237}238239tc.$messageList.append(rowDiv);240scrollToMessageListBottom();241};242243function notifyMemberJoined(member) {244notify(member.identity + ' joined the channel')245}246247function notifyMemberLeft(member) {248notify(member.identity + ' left the channel');249}250251function notify(message) {252var row = $('<div>').addClass('col-md-12');253row.loadTemplate('#member-notification-template', {254status: message255});256tc.$messageList.append(row);257scrollToMessageListBottom();258}259260function showTypingStarted(member) {261$typingPlaceholder.text(member.identity + ' is typing...');262}263264function hideTypingStarted(member) {265$typingPlaceholder.text('');266}267268function scrollToMessageListBottom() {269tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);270}271272function updateChannelUI(selectedChannel) {273var channelElements = $('.channel-element').toArray();274var channelElement = channelElements.filter(function(element) {275return $(element).data().sid === selectedChannel.sid;276});277channelElement = $(channelElement);278if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {279tc.currentChannelContainer = channelElement;280}281tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');282channelElement.removeClass('unselected-channel').addClass('selected-channel');283tc.currentChannelContainer = channelElement;284tc.currentChannel = selectedChannel;285tc.loadMessages();286}287288function showAddChannelInput() {289if (tc.messagingClient) {290$newChannelInputRow.addClass('showing').removeClass('not-showing');291$channelList.addClass('showing').removeClass('not-showing');292$newChannelInput.focus();293}294}295296function hideAddChannelInput() {297$newChannelInputRow.addClass('not-showing').removeClass('showing');298$channelList.addClass('not-showing').removeClass('showing');299$newChannelInput.val('');300}301302function addChannel(channel) {303if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {304tc.generalChannel = channel;305}306var rowDiv = $('<div>').addClass('row channel-row');307rowDiv.loadTemplate('#channel-template', {308channelName: channel.friendlyName309});310311var channelP = rowDiv.children().children().first();312313rowDiv.on('click', selectChannel);314channelP.data('sid', channel.sid);315if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {316tc.currentChannelContainer = channelP;317channelP.addClass('selected-channel');318}319else {320channelP.addClass('unselected-channel')321}322323$channelList.append(rowDiv);324}325326function deleteCurrentChannel() {327if (!tc.currentChannel) {328return;329}330331if (tc.currentChannel.sid === tc.generalChannel.sid) {332alert('You cannot delete the general channel');333return;334}335336tc.currentChannel337.delete()338.then(function(channel) {339console.log('channel: '+ channel.friendlyName + ' deleted');340setupChannel(tc.generalChannel);341});342}343344function selectChannel(event) {345var target = $(event.target);346var channelSid = target.data().sid;347var selectedChannel = tc.channelArray.filter(function(channel) {348return channel.sid === channelSid;349})[0];350if (selectedChannel === tc.currentChannel) {351return;352}353setupChannel(selectedChannel);354};355356function disconnectClient() {357leaveCurrentChannel();358$channelList.text('');359tc.$messageList.text('');360channels = undefined;361$statusRow.addClass('disconnected').removeClass('connected');362tc.$messageList.addClass('disconnected').removeClass('connected');363$connectPanel.addClass('disconnected').removeClass('connected');364$inputText.removeClass('with-shadow');365$typingRow.addClass('disconnected').removeClass('connected');366}367368tc.sortChannelsByName = function(channels) {369return channels.sort(function(a, b) {370if (a.friendlyName === GENERAL_CHANNEL_NAME) {371return -1;372}373if (b.friendlyName === GENERAL_CHANNEL_NAME) {374return 1;375}376return a.friendlyName.localeCompare(b.friendlyName);377});378};379380return tc;381})();382
That's it! We've just implemented a chat application for Node.js using Express.
If you are a Node developer working with Twilio, you might want to check out these other tutorials:
Put a button on your web page that connects visitors to live support or sales people via telephone.
Instantly collect structured data from your users with a survey conducted over a voice call or SMS text messages.
Thanks for checking out this tutorial! If you have any feedback to share with us, we'd love to hear it. Tweet @twilio to let us know what you think.