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
_28const twilio = require('twilio');_28_28const AccessToken = twilio.jwt.AccessToken;_28const ChatGrant = AccessToken.ChatGrant;_28_28function TokenGenerator(identity) {_28 const appName = 'TwilioChat';_28_28 // Create a "grant" which enables a client to use Chat as a given user_28 const chatGrant = new ChatGrant({_28 serviceSid: process.env.TWILIO_CHAT_SERVICE_SID,_28 });_28_28 // Create an access token which we will sign and return to the client,_28 // containing the grant we just created_28 const token = new AccessToken(_28 process.env.TWILIO_ACCOUNT_SID,_28 process.env.TWILIO_API_KEY,_28 process.env.TWILIO_API_SECRET_28 );_28_28 token.addGrant(chatGrant);_28 token.identity = identity;_28_28 return token;_28}_28_28module.exports = { generate: TokenGenerator };
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
_30var express = require('express');_30var router = express.Router();_30var TokenService = require('../services/tokenService');_30_30// POST /token_30router.post('/', function(req, res) {_30 var identity = req.body.identity;_30_30 var token = TokenService.generate(identity)_30_30 res.json({_30 identity: identity,_30 token: token.toJwt(),_30 });_30});_30_30// GET /token_30router.get('/', function(req, res) {_30 var identity = req.query.identity;_30_30 var token = TokenService.generate(identity)_30_30 res.json({_30 identity: identity,_30 token: token.toJwt(),_30 });_30});_30_30_30module.exports = router;
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
_381var twiliochat = (function() {_381 var tc = {};_381_381 var GENERAL_CHANNEL_UNIQUE_NAME = 'general';_381 var GENERAL_CHANNEL_NAME = 'General Channel';_381 var MESSAGES_HISTORY_LIMIT = 50;_381_381 var $channelList;_381 var $inputText;_381 var $usernameInput;_381 var $statusRow;_381 var $connectPanel;_381 var $newChannelInputRow;_381 var $newChannelInput;_381 var $typingRow;_381 var $typingPlaceholder;_381_381 $(document).ready(function() {_381 tc.init();_381 });_381_381 tc.init = function() {_381 tc.$messageList = $('#message-list');_381 $channelList = $('#channel-list');_381 $inputText = $('#input-text');_381 $usernameInput = $('#username-input');_381 $statusRow = $('#status-row');_381 $connectPanel = $('#connect-panel');_381 $newChannelInputRow = $('#new-channel-input-row');_381 $newChannelInput = $('#new-channel-input');_381 $typingRow = $('#typing-row');_381 $typingPlaceholder = $('#typing-placeholder');_381 $usernameInput.focus();_381 $usernameInput.on('keypress', handleUsernameInputKeypress);_381 $inputText.on('keypress', handleInputTextKeypress);_381 $newChannelInput.on('keypress', tc.handleNewChannelInputKeypress);_381 $('#connect-image').on('click', connectClientWithUsername);_381 $('#add-channel-image').on('click', showAddChannelInput);_381 $('#leave-span').on('click', disconnectClient);_381 $('#delete-channel-span').on('click', deleteCurrentChannel);_381 };_381_381 function handleUsernameInputKeypress(event) {_381 if (event.keyCode === 13){_381 connectClientWithUsername();_381 }_381 }_381_381 function handleInputTextKeypress(event) {_381 if (event.keyCode === 13) {_381 tc.currentChannel.sendMessage($(this).val());_381 event.preventDefault();_381 $(this).val('');_381 }_381 else {_381 notifyTyping();_381 }_381 }_381_381 var notifyTyping = $.throttle(function() {_381 tc.currentChannel.typing();_381 }, 1000);_381_381 tc.handleNewChannelInputKeypress = function(event) {_381 if (event.keyCode === 13) {_381 tc.messagingClient_381 .createChannel({_381 friendlyName: $newChannelInput.val(),_381 })_381 .then(hideAddChannelInput);_381_381 $(this).val('');_381 event.preventDefault();_381 }_381 };_381_381 function connectClientWithUsername() {_381 var usernameText = $usernameInput.val();_381 $usernameInput.val('');_381 if (usernameText == '') {_381 alert('Username cannot be empty');_381 return;_381 }_381 tc.username = usernameText;_381 fetchAccessToken(tc.username, connectMessagingClient);_381 }_381_381 function fetchAccessToken(username, handler) {_381 $.post('/token', {identity: username}, null, 'json')_381 .done(function(response) {_381 handler(response.token);_381 })_381 .fail(function(error) {_381 console.log('Failed to fetch the Access Token with error: ' + error);_381 });_381 }_381_381 function connectMessagingClient(token) {_381 // Initialize the Chat messaging client_381 Twilio.Chat.Client.create(token).then(function(client) {_381 tc.messagingClient = client;_381 updateConnectedUI();_381 tc.loadChannelList(tc.joinGeneralChannel);_381 tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));_381 tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));_381 tc.messagingClient.on('tokenExpired', refreshToken);_381 });_381 }_381_381 function refreshToken() {_381 fetchAccessToken(tc.username, setNewToken);_381 }_381_381 function setNewToken(token) {_381 tc.messagingClient.updateToken(tokenResponse.token);_381 }_381_381 function updateConnectedUI() {_381 $('#username-span').text(tc.username);_381 $statusRow.addClass('connected').removeClass('disconnected');_381 tc.$messageList.addClass('connected').removeClass('disconnected');_381 $connectPanel.addClass('connected').removeClass('disconnected');_381 $inputText.addClass('with-shadow');_381 $typingRow.addClass('connected').removeClass('disconnected');_381 }_381_381 tc.loadChannelList = function(handler) {_381 if (tc.messagingClient === undefined) {_381 console.log('Client is not initialized');_381 return;_381 }_381_381 tc.messagingClient.getPublicChannelDescriptors().then(function(channels) {_381 tc.channelArray = tc.sortChannelsByName(channels.items);_381 $channelList.text('');_381 tc.channelArray.forEach(addChannel);_381 if (typeof handler === 'function') {_381 handler();_381 }_381 });_381 };_381_381 tc.joinGeneralChannel = function() {_381 console.log('Attempting to join "general" chat channel...');_381 if (!tc.generalChannel) {_381 // If it doesn't exist, let's create it_381 tc.messagingClient.createChannel({_381 uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,_381 friendlyName: GENERAL_CHANNEL_NAME_381 }).then(function(channel) {_381 console.log('Created general channel');_381 tc.generalChannel = channel;_381 tc.loadChannelList(tc.joinGeneralChannel);_381 });_381 }_381 else {_381 console.log('Found general channel:');_381 setupChannel(tc.generalChannel);_381 }_381 };_381_381 function initChannel(channel) {_381 console.log('Initialized channel ' + channel.friendlyName);_381 return tc.messagingClient.getChannelBySid(channel.sid);_381 }_381_381 function joinChannel(_channel) {_381 return _channel.join()_381 .then(function(joinedChannel) {_381 console.log('Joined channel ' + joinedChannel.friendlyName);_381 updateChannelUI(_channel);_381_381 return joinedChannel;_381 })_381 .catch(function(err) {_381 if (_channel.status == 'joined') {_381 updateChannelUI(_channel);_381 return _channel;_381 } _381 console.error(_381 "Couldn't join channel " + _channel.friendlyName + ' because -> ' + err_381 );_381 });_381 }_381_381 function initChannelEvents() {_381 console.log(tc.currentChannel.friendlyName + ' ready.');_381 tc.currentChannel.on('messageAdded', tc.addMessageToList);_381 tc.currentChannel.on('typingStarted', showTypingStarted);_381 tc.currentChannel.on('typingEnded', hideTypingStarted);_381 tc.currentChannel.on('memberJoined', notifyMemberJoined);_381 tc.currentChannel.on('memberLeft', notifyMemberLeft);_381 $inputText.prop('disabled', false).focus();_381 }_381_381 function setupChannel(channel) {_381 return leaveCurrentChannel()_381 .then(function() {_381 return initChannel(channel);_381 })_381 .then(function(_channel) {_381 return joinChannel(_channel);_381 })_381 .then(initChannelEvents);_381 }_381_381 tc.loadMessages = function() {_381 tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT).then(function (messages) {_381 messages.items.forEach(tc.addMessageToList);_381 });_381 };_381_381 function leaveCurrentChannel() {_381 if (tc.currentChannel) {_381 return tc.currentChannel.leave().then(function(leftChannel) {_381 console.log('left ' + leftChannel.friendlyName);_381 leftChannel.removeListener('messageAdded', tc.addMessageToList);_381 leftChannel.removeListener('typingStarted', showTypingStarted);_381 leftChannel.removeListener('typingEnded', hideTypingStarted);_381 leftChannel.removeListener('memberJoined', notifyMemberJoined);_381 leftChannel.removeListener('memberLeft', notifyMemberLeft);_381 });_381 } else {_381 return Promise.resolve();_381 }_381 }_381_381 tc.addMessageToList = function(message) {_381 var rowDiv = $('<div>').addClass('row no-margin');_381 rowDiv.loadTemplate($('#message-template'), {_381 username: message.author,_381 date: dateFormatter.getTodayDate(message.dateCreated),_381 body: message.body_381 });_381 if (message.author === tc.username) {_381 rowDiv.addClass('own-message');_381 }_381_381 tc.$messageList.append(rowDiv);_381 scrollToMessageListBottom();_381 };_381_381 function notifyMemberJoined(member) {_381 notify(member.identity + ' joined the channel')_381 }_381_381 function notifyMemberLeft(member) {_381 notify(member.identity + ' left the channel');_381 }_381_381 function notify(message) {_381 var row = $('<div>').addClass('col-md-12');_381 row.loadTemplate('#member-notification-template', {_381 status: message_381 });_381 tc.$messageList.append(row);_381 scrollToMessageListBottom();_381 }_381_381 function showTypingStarted(member) {_381 $typingPlaceholder.text(member.identity + ' is typing...');_381 }_381_381 function hideTypingStarted(member) {_381 $typingPlaceholder.text('');_381 }_381_381 function scrollToMessageListBottom() {_381 tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);_381 }_381_381 function updateChannelUI(selectedChannel) {_381 var channelElements = $('.channel-element').toArray();_381 var channelElement = channelElements.filter(function(element) {_381 return $(element).data().sid === selectedChannel.sid;_381 });_381 channelElement = $(channelElement);_381 if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {_381 tc.currentChannelContainer = channelElement;_381 }_381 tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');_381 channelElement.removeClass('unselected-channel').addClass('selected-channel');_381 tc.currentChannelContainer = channelElement;_381 tc.currentChannel = selectedChannel;_381 tc.loadMessages();_381 }_381_381 function showAddChannelInput() {_381 if (tc.messagingClient) {_381 $newChannelInputRow.addClass('showing').removeClass('not-showing');_381 $channelList.addClass('showing').removeClass('not-showing');_381 $newChannelInput.focus();_381 }_381 }_381_381 function hideAddChannelInput() {_381 $newChannelInputRow.addClass('not-showing').removeClass('showing');_381 $channelList.addClass('not-showing').removeClass('showing');_381 $newChannelInput.val('');_381 }_381_381 function addChannel(channel) {_381 if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {_381 tc.generalChannel = channel;_381 }_381 var rowDiv = $('<div>').addClass('row channel-row');_381 rowDiv.loadTemplate('#channel-template', {_381 channelName: channel.friendlyName_381 });_381_381 var channelP = rowDiv.children().children().first();_381_381 rowDiv.on('click', selectChannel);_381 channelP.data('sid', channel.sid);_381 if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {_381 tc.currentChannelContainer = channelP;_381 channelP.addClass('selected-channel');_381 }_381 else {_381 channelP.addClass('unselected-channel')_381 }_381_381 $channelList.append(rowDiv);_381 }_381_381 function deleteCurrentChannel() {_381 if (!tc.currentChannel) {_381 return;_381 }_381_381 if (tc.currentChannel.sid === tc.generalChannel.sid) {_381 alert('You cannot delete the general channel');_381 return;_381 }_381_381 tc.currentChannel_381 .delete()_381 .then(function(channel) {_381 console.log('channel: '+ channel.friendlyName + ' deleted');_381 setupChannel(tc.generalChannel);_381 });_381 }_381_381 function selectChannel(event) {_381 var target = $(event.target);_381 var channelSid = target.data().sid;_381 var selectedChannel = tc.channelArray.filter(function(channel) {_381 return channel.sid === channelSid;_381 })[0];_381 if (selectedChannel === tc.currentChannel) {_381 return;_381 }_381 setupChannel(selectedChannel);_381 };_381_381 function disconnectClient() {_381 leaveCurrentChannel();_381 $channelList.text('');_381 tc.$messageList.text('');_381 channels = undefined;_381 $statusRow.addClass('disconnected').removeClass('connected');_381 tc.$messageList.addClass('disconnected').removeClass('connected');_381 $connectPanel.addClass('disconnected').removeClass('connected');_381 $inputText.removeClass('with-shadow');_381 $typingRow.addClass('disconnected').removeClass('connected');_381 }_381_381 tc.sortChannelsByName = function(channels) {_381 return channels.sort(function(a, b) {_381 if (a.friendlyName === GENERAL_CHANNEL_NAME) {_381 return -1;_381 }_381 if (b.friendlyName === GENERAL_CHANNEL_NAME) {_381 return 1;_381 }_381 return a.friendlyName.localeCompare(b.friendlyName);_381 });_381 };_381_381 return tc;_381})();
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
_381var twiliochat = (function() {_381 var tc = {};_381_381 var GENERAL_CHANNEL_UNIQUE_NAME = 'general';_381 var GENERAL_CHANNEL_NAME = 'General Channel';_381 var MESSAGES_HISTORY_LIMIT = 50;_381_381 var $channelList;_381 var $inputText;_381 var $usernameInput;_381 var $statusRow;_381 var $connectPanel;_381 var $newChannelInputRow;_381 var $newChannelInput;_381 var $typingRow;_381 var $typingPlaceholder;_381_381 $(document).ready(function() {_381 tc.init();_381 });_381_381 tc.init = function() {_381 tc.$messageList = $('#message-list');_381 $channelList = $('#channel-list');_381 $inputText = $('#input-text');_381 $usernameInput = $('#username-input');_381 $statusRow = $('#status-row');_381 $connectPanel = $('#connect-panel');_381 $newChannelInputRow = $('#new-channel-input-row');_381 $newChannelInput = $('#new-channel-input');_381 $typingRow = $('#typing-row');_381 $typingPlaceholder = $('#typing-placeholder');_381 $usernameInput.focus();_381 $usernameInput.on('keypress', handleUsernameInputKeypress);_381 $inputText.on('keypress', handleInputTextKeypress);_381 $newChannelInput.on('keypress', tc.handleNewChannelInputKeypress);_381 $('#connect-image').on('click', connectClientWithUsername);_381 $('#add-channel-image').on('click', showAddChannelInput);_381 $('#leave-span').on('click', disconnectClient);_381 $('#delete-channel-span').on('click', deleteCurrentChannel);_381 };_381_381 function handleUsernameInputKeypress(event) {_381 if (event.keyCode === 13){_381 connectClientWithUsername();_381 }_381 }_381_381 function handleInputTextKeypress(event) {_381 if (event.keyCode === 13) {_381 tc.currentChannel.sendMessage($(this).val());_381 event.preventDefault();_381 $(this).val('');_381 }_381 else {_381 notifyTyping();_381 }_381 }_381_381 var notifyTyping = $.throttle(function() {_381 tc.currentChannel.typing();_381 }, 1000);_381_381 tc.handleNewChannelInputKeypress = function(event) {_381 if (event.keyCode === 13) {_381 tc.messagingClient_381 .createChannel({_381 friendlyName: $newChannelInput.val(),_381 })_381 .then(hideAddChannelInput);_381_381 $(this).val('');_381 event.preventDefault();_381 }_381 };_381_381 function connectClientWithUsername() {_381 var usernameText = $usernameInput.val();_381 $usernameInput.val('');_381 if (usernameText == '') {_381 alert('Username cannot be empty');_381 return;_381 }_381 tc.username = usernameText;_381 fetchAccessToken(tc.username, connectMessagingClient);_381 }_381_381 function fetchAccessToken(username, handler) {_381 $.post('/token', {identity: username}, null, 'json')_381 .done(function(response) {_381 handler(response.token);_381 })_381 .fail(function(error) {_381 console.log('Failed to fetch the Access Token with error: ' + error);_381 });_381 }_381_381 function connectMessagingClient(token) {_381 // Initialize the Chat messaging client_381 Twilio.Chat.Client.create(token).then(function(client) {_381 tc.messagingClient = client;_381 updateConnectedUI();_381 tc.loadChannelList(tc.joinGeneralChannel);_381 tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));_381 tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));_381 tc.messagingClient.on('tokenExpired', refreshToken);_381 });_381 }_381_381 function refreshToken() {_381 fetchAccessToken(tc.username, setNewToken);_381 }_381_381 function setNewToken(token) {_381 tc.messagingClient.updateToken(tokenResponse.token);_381 }_381_381 function updateConnectedUI() {_381 $('#username-span').text(tc.username);_381 $statusRow.addClass('connected').removeClass('disconnected');_381 tc.$messageList.addClass('connected').removeClass('disconnected');_381 $connectPanel.addClass('connected').removeClass('disconnected');_381 $inputText.addClass('with-shadow');_381 $typingRow.addClass('connected').removeClass('disconnected');_381 }_381_381 tc.loadChannelList = function(handler) {_381 if (tc.messagingClient === undefined) {_381 console.log('Client is not initialized');_381 return;_381 }_381_381 tc.messagingClient.getPublicChannelDescriptors().then(function(channels) {_381 tc.channelArray = tc.sortChannelsByName(channels.items);_381 $channelList.text('');_381 tc.channelArray.forEach(addChannel);_381 if (typeof handler === 'function') {_381 handler();_381 }_381 });_381 };_381_381 tc.joinGeneralChannel = function() {_381 console.log('Attempting to join "general" chat channel...');_381 if (!tc.generalChannel) {_381 // If it doesn't exist, let's create it_381 tc.messagingClient.createChannel({_381 uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,_381 friendlyName: GENERAL_CHANNEL_NAME_381 }).then(function(channel) {_381 console.log('Created general channel');_381 tc.generalChannel = channel;_381 tc.loadChannelList(tc.joinGeneralChannel);_381 });_381 }_381 else {_381 console.log('Found general channel:');_381 setupChannel(tc.generalChannel);_381 }_381 };_381_381 function initChannel(channel) {_381 console.log('Initialized channel ' + channel.friendlyName);_381 return tc.messagingClient.getChannelBySid(channel.sid);_381 }_381_381 function joinChannel(_channel) {_381 return _channel.join()_381 .then(function(joinedChannel) {_381 console.log('Joined channel ' + joinedChannel.friendlyName);_381 updateChannelUI(_channel);_381_381 return joinedChannel;_381 })_381 .catch(function(err) {_381 if (_channel.status == 'joined') {_381 updateChannelUI(_channel);_381 return _channel;_381 } _381 console.error(_381 "Couldn't join channel " + _channel.friendlyName + ' because -> ' + err_381 );_381 });_381 }_381_381 function initChannelEvents() {_381 console.log(tc.currentChannel.friendlyName + ' ready.');_381 tc.currentChannel.on('messageAdded', tc.addMessageToList);_381 tc.currentChannel.on('typingStarted', showTypingStarted);_381 tc.currentChannel.on('typingEnded', hideTypingStarted);_381 tc.currentChannel.on('memberJoined', notifyMemberJoined);_381 tc.currentChannel.on('memberLeft', notifyMemberLeft);_381 $inputText.prop('disabled', false).focus();_381 }_381_381 function setupChannel(channel) {_381 return leaveCurrentChannel()_381 .then(function() {_381 return initChannel(channel);_381 })_381 .then(function(_channel) {_381 return joinChannel(_channel);_381 })_381 .then(initChannelEvents);_381 }_381_381 tc.loadMessages = function() {_381 tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT).then(function (messages) {_381 messages.items.forEach(tc.addMessageToList);_381 });_381 };_381_381 function leaveCurrentChannel() {_381 if (tc.currentChannel) {_381 return tc.currentChannel.leave().then(function(leftChannel) {_381 console.log('left ' + leftChannel.friendlyName);_381 leftChannel.removeListener('messageAdded', tc.addMessageToList);_381 leftChannel.removeListener('typingStarted', showTypingStarted);_381 leftChannel.removeListener('typingEnded', hideTypingStarted);_381 leftChannel.removeListener('memberJoined', notifyMemberJoined);_381 leftChannel.removeListener('memberLeft', notifyMemberLeft);_381 });_381 } else {_381 return Promise.resolve();_381 }_381 }_381_381 tc.addMessageToList = function(message) {_381 var rowDiv = $('<div>').addClass('row no-margin');_381 rowDiv.loadTemplate($('#message-template'), {_381 username: message.author,_381 date: dateFormatter.getTodayDate(message.dateCreated),_381 body: message.body_381 });_381 if (message.author === tc.username) {_381 rowDiv.addClass('own-message');_381 }_381_381 tc.$messageList.append(rowDiv);_381 scrollToMessageListBottom();_381 };_381_381 function notifyMemberJoined(member) {_381 notify(member.identity + ' joined the channel')_381 }_381_381 function notifyMemberLeft(member) {_381 notify(member.identity + ' left the channel');_381 }_381_381 function notify(message) {_381 var row = $('<div>').addClass('col-md-12');_381 row.loadTemplate('#member-notification-template', {_381 status: message_381 });_381 tc.$messageList.append(row);_381 scrollToMessageListBottom();_381 }_381_381 function showTypingStarted(member) {_381 $typingPlaceholder.text(member.identity + ' is typing...');_381 }_381_381 function hideTypingStarted(member) {_381 $typingPlaceholder.text('');_381 }_381_381 function scrollToMessageListBottom() {_381 tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);_381 }_381_381 function updateChannelUI(selectedChannel) {_381 var channelElements = $('.channel-element').toArray();_381 var channelElement = channelElements.filter(function(element) {_381 return $(element).data().sid === selectedChannel.sid;_381 });_381 channelElement = $(channelElement);_381 if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {_381 tc.currentChannelContainer = channelElement;_381 }_381 tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');_381 channelElement.removeClass('unselected-channel').addClass('selected-channel');_381 tc.currentChannelContainer = channelElement;_381 tc.currentChannel = selectedChannel;_381 tc.loadMessages();_381 }_381_381 function showAddChannelInput() {_381 if (tc.messagingClient) {_381 $newChannelInputRow.addClass('showing').removeClass('not-showing');_381 $channelList.addClass('showing').removeClass('not-showing');_381 $newChannelInput.focus();_381 }_381 }_381_381 function hideAddChannelInput() {_381 $newChannelInputRow.addClass('not-showing').removeClass('showing');_381 $channelList.addClass('not-showing').removeClass('showing');_381 $newChannelInput.val('');_381 }_381_381 function addChannel(channel) {_381 if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {_381 tc.generalChannel = channel;_381 }_381 var rowDiv = $('<div>').addClass('row channel-row');_381 rowDiv.loadTemplate('#channel-template', {_381 channelName: channel.friendlyName_381 });_381_381 var channelP = rowDiv.children().children().first();_381_381 rowDiv.on('click', selectChannel);_381 channelP.data('sid', channel.sid);_381 if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {_381 tc.currentChannelContainer = channelP;_381 channelP.addClass('selected-channel');_381 }_381 else {_381 channelP.addClass('unselected-channel')_381 }_381_381 $channelList.append(rowDiv);_381 }_381_381 function deleteCurrentChannel() {_381 if (!tc.currentChannel) {_381 return;_381 }_381_381 if (tc.currentChannel.sid === tc.generalChannel.sid) {_381 alert('You cannot delete the general channel');_381 return;_381 }_381_381 tc.currentChannel_381 .delete()_381 .then(function(channel) {_381 console.log('channel: '+ channel.friendlyName + ' deleted');_381 setupChannel(tc.generalChannel);_381 });_381 }_381_381 function selectChannel(event) {_381 var target = $(event.target);_381 var channelSid = target.data().sid;_381 var selectedChannel = tc.channelArray.filter(function(channel) {_381 return channel.sid === channelSid;_381 })[0];_381 if (selectedChannel === tc.currentChannel) {_381 return;_381 }_381 setupChannel(selectedChannel);_381 };_381_381 function disconnectClient() {_381 leaveCurrentChannel();_381 $channelList.text('');_381 tc.$messageList.text('');_381 channels = undefined;_381 $statusRow.addClass('disconnected').removeClass('connected');_381 tc.$messageList.addClass('disconnected').removeClass('connected');_381 $connectPanel.addClass('disconnected').removeClass('connected');_381 $inputText.removeClass('with-shadow');_381 $typingRow.addClass('disconnected').removeClass('connected');_381 }_381_381 tc.sortChannelsByName = function(channels) {_381 return channels.sort(function(a, b) {_381 if (a.friendlyName === GENERAL_CHANNEL_NAME) {_381 return -1;_381 }_381 if (b.friendlyName === GENERAL_CHANNEL_NAME) {_381 return 1;_381 }_381 return a.friendlyName.localeCompare(b.friendlyName);_381 });_381 };_381_381 return tc;_381})();
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
_381var twiliochat = (function() {_381 var tc = {};_381_381 var GENERAL_CHANNEL_UNIQUE_NAME = 'general';_381 var GENERAL_CHANNEL_NAME = 'General Channel';_381 var MESSAGES_HISTORY_LIMIT = 50;_381_381 var $channelList;_381 var $inputText;_381 var $usernameInput;_381 var $statusRow;_381 var $connectPanel;_381 var $newChannelInputRow;_381 var $newChannelInput;_381 var $typingRow;_381 var $typingPlaceholder;_381_381 $(document).ready(function() {_381 tc.init();_381 });_381_381 tc.init = function() {_381 tc.$messageList = $('#message-list');_381 $channelList = $('#channel-list');_381 $inputText = $('#input-text');_381 $usernameInput = $('#username-input');_381 $statusRow = $('#status-row');_381 $connectPanel = $('#connect-panel');_381 $newChannelInputRow = $('#new-channel-input-row');_381 $newChannelInput = $('#new-channel-input');_381 $typingRow = $('#typing-row');_381 $typingPlaceholder = $('#typing-placeholder');_381 $usernameInput.focus();_381 $usernameInput.on('keypress', handleUsernameInputKeypress);_381 $inputText.on('keypress', handleInputTextKeypress);_381 $newChannelInput.on('keypress', tc.handleNewChannelInputKeypress);_381 $('#connect-image').on('click', connectClientWithUsername);_381 $('#add-channel-image').on('click', showAddChannelInput);_381 $('#leave-span').on('click', disconnectClient);_381 $('#delete-channel-span').on('click', deleteCurrentChannel);_381 };_381_381 function handleUsernameInputKeypress(event) {_381 if (event.keyCode === 13){_381 connectClientWithUsername();_381 }_381 }_381_381 function handleInputTextKeypress(event) {_381 if (event.keyCode === 13) {_381 tc.currentChannel.sendMessage($(this).val());_381 event.preventDefault();_381 $(this).val('');_381 }_381 else {_381 notifyTyping();_381 }_381 }_381_381 var notifyTyping = $.throttle(function() {_381 tc.currentChannel.typing();_381 }, 1000);_381_381 tc.handleNewChannelInputKeypress = function(event) {_381 if (event.keyCode === 13) {_381 tc.messagingClient_381 .createChannel({_381 friendlyName: $newChannelInput.val(),_381 })_381 .then(hideAddChannelInput);_381_381 $(this).val('');_381 event.preventDefault();_381 }_381 };_381_381 function connectClientWithUsername() {_381 var usernameText = $usernameInput.val();_381 $usernameInput.val('');_381 if (usernameText == '') {_381 alert('Username cannot be empty');_381 return;_381 }_381 tc.username = usernameText;_381 fetchAccessToken(tc.username, connectMessagingClient);_381 }_381_381 function fetchAccessToken(username, handler) {_381 $.post('/token', {identity: username}, null, 'json')_381 .done(function(response) {_381 handler(response.token);_381 })_381 .fail(function(error) {_381 console.log('Failed to fetch the Access Token with error: ' + error);_381 });_381 }_381_381 function connectMessagingClient(token) {_381 // Initialize the Chat messaging client_381 Twilio.Chat.Client.create(token).then(function(client) {_381 tc.messagingClient = client;_381 updateConnectedUI();_381 tc.loadChannelList(tc.joinGeneralChannel);_381 tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));_381 tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));_381 tc.messagingClient.on('tokenExpired', refreshToken);_381 });_381 }_381_381 function refreshToken() {_381 fetchAccessToken(tc.username, setNewToken);_381 }_381_381 function setNewToken(token) {_381 tc.messagingClient.updateToken(tokenResponse.token);_381 }_381_381 function updateConnectedUI() {_381 $('#username-span').text(tc.username);_381 $statusRow.addClass('connected').removeClass('disconnected');_381 tc.$messageList.addClass('connected').removeClass('disconnected');_381 $connectPanel.addClass('connected').removeClass('disconnected');_381 $inputText.addClass('with-shadow');_381 $typingRow.addClass('connected').removeClass('disconnected');_381 }_381_381 tc.loadChannelList = function(handler) {_381 if (tc.messagingClient === undefined) {_381 console.log('Client is not initialized');_381 return;_381 }_381_381 tc.messagingClient.getPublicChannelDescriptors().then(function(channels) {_381 tc.channelArray = tc.sortChannelsByName(channels.items);_381 $channelList.text('');_381 tc.channelArray.forEach(addChannel);_381 if (typeof handler === 'function') {_381 handler();_381 }_381 });_381 };_381_381 tc.joinGeneralChannel = function() {_381 console.log('Attempting to join "general" chat channel...');_381 if (!tc.generalChannel) {_381 // If it doesn't exist, let's create it_381 tc.messagingClient.createChannel({_381 uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,_381 friendlyName: GENERAL_CHANNEL_NAME_381 }).then(function(channel) {_381 console.log('Created general channel');_381 tc.generalChannel = channel;_381 tc.loadChannelList(tc.joinGeneralChannel);_381 });_381 }_381 else {_381 console.log('Found general channel:');_381 setupChannel(tc.generalChannel);_381 }_381 };_381_381 function initChannel(channel) {_381 console.log('Initialized channel ' + channel.friendlyName);_381 return tc.messagingClient.getChannelBySid(channel.sid);_381 }_381_381 function joinChannel(_channel) {_381 return _channel.join()_381 .then(function(joinedChannel) {_381 console.log('Joined channel ' + joinedChannel.friendlyName);_381 updateChannelUI(_channel);_381_381 return joinedChannel;_381 })_381 .catch(function(err) {_381 if (_channel.status == 'joined') {_381 updateChannelUI(_channel);_381 return _channel;_381 } _381 console.error(_381 "Couldn't join channel " + _channel.friendlyName + ' because -> ' + err_381 );_381 });_381 }_381_381 function initChannelEvents() {_381 console.log(tc.currentChannel.friendlyName + ' ready.');_381 tc.currentChannel.on('messageAdded', tc.addMessageToList);_381 tc.currentChannel.on('typingStarted', showTypingStarted);_381 tc.currentChannel.on('typingEnded', hideTypingStarted);_381 tc.currentChannel.on('memberJoined', notifyMemberJoined);_381 tc.currentChannel.on('memberLeft', notifyMemberLeft);_381 $inputText.prop('disabled', false).focus();_381 }_381_381 function setupChannel(channel) {_381 return leaveCurrentChannel()_381 .then(function() {_381 return initChannel(channel);_381 })_381 .then(function(_channel) {_381 return joinChannel(_channel);_381 })_381 .then(initChannelEvents);_381 }_381_381 tc.loadMessages = function() {_381 tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT).then(function (messages) {_381 messages.items.forEach(tc.addMessageToList);_381 });_381 };_381_381 function leaveCurrentChannel() {_381 if (tc.currentChannel) {_381 return tc.currentChannel.leave().then(function(leftChannel) {_381 console.log('left ' + leftChannel.friendlyName);_381 leftChannel.removeListener('messageAdded', tc.addMessageToList);_381 leftChannel.removeListener('typingStarted', showTypingStarted);_381 leftChannel.removeListener('typingEnded', hideTypingStarted);_381 leftChannel.removeListener('memberJoined', notifyMemberJoined);_381 leftChannel.removeListener('memberLeft', notifyMemberLeft);_381 });_381 } else {_381 return Promise.resolve();_381 }_381 }_381_381 tc.addMessageToList = function(message) {_381 var rowDiv = $('<div>').addClass('row no-margin');_381 rowDiv.loadTemplate($('#message-template'), {_381 username: message.author,_381 date: dateFormatter.getTodayDate(message.dateCreated),_381 body: message.body_381 });_381 if (message.author === tc.username) {_381 rowDiv.addClass('own-message');_381 }_381_381 tc.$messageList.append(rowDiv);_381 scrollToMessageListBottom();_381 };_381_381 function notifyMemberJoined(member) {_381 notify(member.identity + ' joined the channel')_381 }_381_381 function notifyMemberLeft(member) {_381 notify(member.identity + ' left the channel');_381 }_381_381 function notify(message) {_381 var row = $('<div>').addClass('col-md-12');_381 row.loadTemplate('#member-notification-template', {_381 status: message_381 });_381 tc.$messageList.append(row);_381 scrollToMessageListBottom();_381 }_381_381 function showTypingStarted(member) {_381 $typingPlaceholder.text(member.identity + ' is typing...');_381 }_381_381 function hideTypingStarted(member) {_381 $typingPlaceholder.text('');_381 }_381_381 function scrollToMessageListBottom() {_381 tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);_381 }_381_381 function updateChannelUI(selectedChannel) {_381 var channelElements = $('.channel-element').toArray();_381 var channelElement = channelElements.filter(function(element) {_381 return $(element).data().sid === selectedChannel.sid;_381 });_381 channelElement = $(channelElement);_381 if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {_381 tc.currentChannelContainer = channelElement;_381 }_381 tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');_381 channelElement.removeClass('unselected-channel').addClass('selected-channel');_381 tc.currentChannelContainer = channelElement;_381 tc.currentChannel = selectedChannel;_381 tc.loadMessages();_381 }_381_381 function showAddChannelInput() {_381 if (tc.messagingClient) {_381 $newChannelInputRow.addClass('showing').removeClass('not-showing');_381 $channelList.addClass('showing').removeClass('not-showing');_381 $newChannelInput.focus();_381 }_381 }_381_381 function hideAddChannelInput() {_381 $newChannelInputRow.addClass('not-showing').removeClass('showing');_381 $channelList.addClass('not-showing').removeClass('showing');_381 $newChannelInput.val('');_381 }_381_381 function addChannel(channel) {_381 if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {_381 tc.generalChannel = channel;_381 }_381 var rowDiv = $('<div>').addClass('row channel-row');_381 rowDiv.loadTemplate('#channel-template', {_381 channelName: channel.friendlyName_381 });_381_381 var channelP = rowDiv.children().children().first();_381_381 rowDiv.on('click', selectChannel);_381 channelP.data('sid', channel.sid);_381 if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {_381 tc.currentChannelContainer = channelP;_381 channelP.addClass('selected-channel');_381 }_381 else {_381 channelP.addClass('unselected-channel')_381 }_381_381 $channelList.append(rowDiv);_381 }_381_381 function deleteCurrentChannel() {_381 if (!tc.currentChannel) {_381 return;_381 }_381_381 if (tc.currentChannel.sid === tc.generalChannel.sid) {_381 alert('You cannot delete the general channel');_381 return;_381 }_381_381 tc.currentChannel_381 .delete()_381 .then(function(channel) {_381 console.log('channel: '+ channel.friendlyName + ' deleted');_381 setupChannel(tc.generalChannel);_381 });_381 }_381_381 function selectChannel(event) {_381 var target = $(event.target);_381 var channelSid = target.data().sid;_381 var selectedChannel = tc.channelArray.filter(function(channel) {_381 return channel.sid === channelSid;_381 })[0];_381 if (selectedChannel === tc.currentChannel) {_381 return;_381 }_381 setupChannel(selectedChannel);_381 };_381_381 function disconnectClient() {_381 leaveCurrentChannel();_381 $channelList.text('');_381 tc.$messageList.text('');_381 channels = undefined;_381 $statusRow.addClass('disconnected').removeClass('connected');_381 tc.$messageList.addClass('disconnected').removeClass('connected');_381 $connectPanel.addClass('disconnected').removeClass('connected');_381 $inputText.removeClass('with-shadow');_381 $typingRow.addClass('disconnected').removeClass('connected');_381 }_381_381 tc.sortChannelsByName = function(channels) {_381 return channels.sort(function(a, b) {_381 if (a.friendlyName === GENERAL_CHANNEL_NAME) {_381 return -1;_381 }_381 if (b.friendlyName === GENERAL_CHANNEL_NAME) {_381 return 1;_381 }_381 return a.friendlyName.localeCompare(b.friendlyName);_381 });_381 };_381_381 return tc;_381})();
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
_381var twiliochat = (function() {_381 var tc = {};_381_381 var GENERAL_CHANNEL_UNIQUE_NAME = 'general';_381 var GENERAL_CHANNEL_NAME = 'General Channel';_381 var MESSAGES_HISTORY_LIMIT = 50;_381_381 var $channelList;_381 var $inputText;_381 var $usernameInput;_381 var $statusRow;_381 var $connectPanel;_381 var $newChannelInputRow;_381 var $newChannelInput;_381 var $typingRow;_381 var $typingPlaceholder;_381_381 $(document).ready(function() {_381 tc.init();_381 });_381_381 tc.init = function() {_381 tc.$messageList = $('#message-list');_381 $channelList = $('#channel-list');_381 $inputText = $('#input-text');_381 $usernameInput = $('#username-input');_381 $statusRow = $('#status-row');_381 $connectPanel = $('#connect-panel');_381 $newChannelInputRow = $('#new-channel-input-row');_381 $newChannelInput = $('#new-channel-input');_381 $typingRow = $('#typing-row');_381 $typingPlaceholder = $('#typing-placeholder');_381 $usernameInput.focus();_381 $usernameInput.on('keypress', handleUsernameInputKeypress);_381 $inputText.on('keypress', handleInputTextKeypress);_381 $newChannelInput.on('keypress', tc.handleNewChannelInputKeypress);_381 $('#connect-image').on('click', connectClientWithUsername);_381 $('#add-channel-image').on('click', showAddChannelInput);_381 $('#leave-span').on('click', disconnectClient);_381 $('#delete-channel-span').on('click', deleteCurrentChannel);_381 };_381_381 function handleUsernameInputKeypress(event) {_381 if (event.keyCode === 13){_381 connectClientWithUsername();_381 }_381 }_381_381 function handleInputTextKeypress(event) {_381 if (event.keyCode === 13) {_381 tc.currentChannel.sendMessage($(this).val());_381 event.preventDefault();_381 $(this).val('');_381 }_381 else {_381 notifyTyping();_381 }_381 }_381_381 var notifyTyping = $.throttle(function() {_381 tc.currentChannel.typing();_381 }, 1000);_381_381 tc.handleNewChannelInputKeypress = function(event) {_381 if (event.keyCode === 13) {_381 tc.messagingClient_381 .createChannel({_381 friendlyName: $newChannelInput.val(),_381 })_381 .then(hideAddChannelInput);_381_381 $(this).val('');_381 event.preventDefault();_381 }_381 };_381_381 function connectClientWithUsername() {_381 var usernameText = $usernameInput.val();_381 $usernameInput.val('');_381 if (usernameText == '') {_381 alert('Username cannot be empty');_381 return;_381 }_381 tc.username = usernameText;_381 fetchAccessToken(tc.username, connectMessagingClient);_381 }_381_381 function fetchAccessToken(username, handler) {_381 $.post('/token', {identity: username}, null, 'json')_381 .done(function(response) {_381 handler(response.token);_381 })_381 .fail(function(error) {_381 console.log('Failed to fetch the Access Token with error: ' + error);_381 });_381 }_381_381 function connectMessagingClient(token) {_381 // Initialize the Chat messaging client_381 Twilio.Chat.Client.create(token).then(function(client) {_381 tc.messagingClient = client;_381 updateConnectedUI();_381 tc.loadChannelList(tc.joinGeneralChannel);_381 tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));_381 tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));_381 tc.messagingClient.on('tokenExpired', refreshToken);_381 });_381 }_381_381 function refreshToken() {_381 fetchAccessToken(tc.username, setNewToken);_381 }_381_381 function setNewToken(token) {_381 tc.messagingClient.updateToken(tokenResponse.token);_381 }_381_381 function updateConnectedUI() {_381 $('#username-span').text(tc.username);_381 $statusRow.addClass('connected').removeClass('disconnected');_381 tc.$messageList.addClass('connected').removeClass('disconnected');_381 $connectPanel.addClass('connected').removeClass('disconnected');_381 $inputText.addClass('with-shadow');_381 $typingRow.addClass('connected').removeClass('disconnected');_381 }_381_381 tc.loadChannelList = function(handler) {_381 if (tc.messagingClient === undefined) {_381 console.log('Client is not initialized');_381 return;_381 }_381_381 tc.messagingClient.getPublicChannelDescriptors().then(function(channels) {_381 tc.channelArray = tc.sortChannelsByName(channels.items);_381 $channelList.text('');_381 tc.channelArray.forEach(addChannel);_381 if (typeof handler === 'function') {_381 handler();_381 }_381 });_381 };_381_381 tc.joinGeneralChannel = function() {_381 console.log('Attempting to join "general" chat channel...');_381 if (!tc.generalChannel) {_381 // If it doesn't exist, let's create it_381 tc.messagingClient.createChannel({_381 uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,_381 friendlyName: GENERAL_CHANNEL_NAME_381 }).then(function(channel) {_381 console.log('Created general channel');_381 tc.generalChannel = channel;_381 tc.loadChannelList(tc.joinGeneralChannel);_381 });_381 }_381 else {_381 console.log('Found general channel:');_381 setupChannel(tc.generalChannel);_381 }_381 };_381_381 function initChannel(channel) {_381 console.log('Initialized channel ' + channel.friendlyName);_381 return tc.messagingClient.getChannelBySid(channel.sid);_381 }_381_381 function joinChannel(_channel) {_381 return _channel.join()_381 .then(function(joinedChannel) {_381 console.log('Joined channel ' + joinedChannel.friendlyName);_381 updateChannelUI(_channel);_381_381 return joinedChannel;_381 })_381 .catch(function(err) {_381 if (_channel.status == 'joined') {_381 updateChannelUI(_channel);_381 return _channel;_381 } _381 console.error(_381 "Couldn't join channel " + _channel.friendlyName + ' because -> ' + err_381 );_381 });_381 }_381_381 function initChannelEvents() {_381 console.log(tc.currentChannel.friendlyName + ' ready.');_381 tc.currentChannel.on('messageAdded', tc.addMessageToList);_381 tc.currentChannel.on('typingStarted', showTypingStarted);_381 tc.currentChannel.on('typingEnded', hideTypingStarted);_381 tc.currentChannel.on('memberJoined', notifyMemberJoined);_381 tc.currentChannel.on('memberLeft', notifyMemberLeft);_381 $inputText.prop('disabled', false).focus();_381 }_381_381 function setupChannel(channel) {_381 return leaveCurrentChannel()_381 .then(function() {_381 return initChannel(channel);_381 })_381 .then(function(_channel) {_381 return joinChannel(_channel);_381 })_381 .then(initChannelEvents);_381 }_381_381 tc.loadMessages = function() {_381 tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT).then(function (messages) {_381 messages.items.forEach(tc.addMessageToList);_381 });_381 };_381_381 function leaveCurrentChannel() {_381 if (tc.currentChannel) {_381 return tc.currentChannel.leave().then(function(leftChannel) {_381 console.log('left ' + leftChannel.friendlyName);_381 leftChannel.removeListener('messageAdded', tc.addMessageToList);_381 leftChannel.removeListener('typingStarted', showTypingStarted);_381 leftChannel.removeListener('typingEnded', hideTypingStarted);_381 leftChannel.removeListener('memberJoined', notifyMemberJoined);_381 leftChannel.removeListener('memberLeft', notifyMemberLeft);_381 });_381 } else {_381 return Promise.resolve();_381 }_381 }_381_381 tc.addMessageToList = function(message) {_381 var rowDiv = $('<div>').addClass('row no-margin');_381 rowDiv.loadTemplate($('#message-template'), {_381 username: message.author,_381 date: dateFormatter.getTodayDate(message.dateCreated),_381 body: message.body_381 });_381 if (message.author === tc.username) {_381 rowDiv.addClass('own-message');_381 }_381_381 tc.$messageList.append(rowDiv);_381 scrollToMessageListBottom();_381 };_381_381 function notifyMemberJoined(member) {_381 notify(member.identity + ' joined the channel')_381 }_381_381 function notifyMemberLeft(member) {_381 notify(member.identity + ' left the channel');_381 }_381_381 function notify(message) {_381 var row = $('<div>').addClass('col-md-12');_381 row.loadTemplate('#member-notification-template', {_381 status: message_381 });_381 tc.$messageList.append(row);_381 scrollToMessageListBottom();_381 }_381_381 function showTypingStarted(member) {_381 $typingPlaceholder.text(member.identity + ' is typing...');_381 }_381_381 function hideTypingStarted(member) {_381 $typingPlaceholder.text('');_381 }_381_381 function scrollToMessageListBottom() {_381 tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);_381 }_381_381 function updateChannelUI(selectedChannel) {_381 var channelElements = $('.channel-element').toArray();_381 var channelElement = channelElements.filter(function(element) {_381 return $(element).data().sid === selectedChannel.sid;_381 });_381 channelElement = $(channelElement);_381 if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {_381 tc.currentChannelContainer = channelElement;_381 }_381 tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');_381 channelElement.removeClass('unselected-channel').addClass('selected-channel');_381 tc.currentChannelContainer = channelElement;_381 tc.currentChannel = selectedChannel;_381 tc.loadMessages();_381 }_381_381 function showAddChannelInput() {_381 if (tc.messagingClient) {_381 $newChannelInputRow.addClass('showing').removeClass('not-showing');_381 $channelList.addClass('showing').removeClass('not-showing');_381 $newChannelInput.focus();_381 }_381 }_381_381 function hideAddChannelInput() {_381 $newChannelInputRow.addClass('not-showing').removeClass('showing');_381 $channelList.addClass('not-showing').removeClass('showing');_381 $newChannelInput.val('');_381 }_381_381 function addChannel(channel) {_381 if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {_381 tc.generalChannel = channel;_381 }_381 var rowDiv = $('<div>').addClass('row channel-row');_381 rowDiv.loadTemplate('#channel-template', {_381 channelName: channel.friendlyName_381 });_381_381 var channelP = rowDiv.children().children().first();_381_381 rowDiv.on('click', selectChannel);_381 channelP.data('sid', channel.sid);_381 if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {_381 tc.currentChannelContainer = channelP;_381 channelP.addClass('selected-channel');_381 }_381 else {_381 channelP.addClass('unselected-channel')_381 }_381_381 $channelList.append(rowDiv);_381 }_381_381 function deleteCurrentChannel() {_381 if (!tc.currentChannel) {_381 return;_381 }_381_381 if (tc.currentChannel.sid === tc.generalChannel.sid) {_381 alert('You cannot delete the general channel');_381 return;_381 }_381_381 tc.currentChannel_381 .delete()_381 .then(function(channel) {_381 console.log('channel: '+ channel.friendlyName + ' deleted');_381 setupChannel(tc.generalChannel);_381 });_381 }_381_381 function selectChannel(event) {_381 var target = $(event.target);_381 var channelSid = target.data().sid;_381 var selectedChannel = tc.channelArray.filter(function(channel) {_381 return channel.sid === channelSid;_381 })[0];_381 if (selectedChannel === tc.currentChannel) {_381 return;_381 }_381 setupChannel(selectedChannel);_381 };_381_381 function disconnectClient() {_381 leaveCurrentChannel();_381 $channelList.text('');_381 tc.$messageList.text('');_381 channels = undefined;_381 $statusRow.addClass('disconnected').removeClass('connected');_381 tc.$messageList.addClass('disconnected').removeClass('connected');_381 $connectPanel.addClass('disconnected').removeClass('connected');_381 $inputText.removeClass('with-shadow');_381 $typingRow.addClass('disconnected').removeClass('connected');_381 }_381_381 tc.sortChannelsByName = function(channels) {_381 return channels.sort(function(a, b) {_381 if (a.friendlyName === GENERAL_CHANNEL_NAME) {_381 return -1;_381 }_381 if (b.friendlyName === GENERAL_CHANNEL_NAME) {_381 return 1;_381 }_381 return a.friendlyName.localeCompare(b.friendlyName);_381 });_381 };_381_381 return tc;_381})();
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
_381var twiliochat = (function() {_381 var tc = {};_381_381 var GENERAL_CHANNEL_UNIQUE_NAME = 'general';_381 var GENERAL_CHANNEL_NAME = 'General Channel';_381 var MESSAGES_HISTORY_LIMIT = 50;_381_381 var $channelList;_381 var $inputText;_381 var $usernameInput;_381 var $statusRow;_381 var $connectPanel;_381 var $newChannelInputRow;_381 var $newChannelInput;_381 var $typingRow;_381 var $typingPlaceholder;_381_381 $(document).ready(function() {_381 tc.init();_381 });_381_381 tc.init = function() {_381 tc.$messageList = $('#message-list');_381 $channelList = $('#channel-list');_381 $inputText = $('#input-text');_381 $usernameInput = $('#username-input');_381 $statusRow = $('#status-row');_381 $connectPanel = $('#connect-panel');_381 $newChannelInputRow = $('#new-channel-input-row');_381 $newChannelInput = $('#new-channel-input');_381 $typingRow = $('#typing-row');_381 $typingPlaceholder = $('#typing-placeholder');_381 $usernameInput.focus();_381 $usernameInput.on('keypress', handleUsernameInputKeypress);_381 $inputText.on('keypress', handleInputTextKeypress);_381 $newChannelInput.on('keypress', tc.handleNewChannelInputKeypress);_381 $('#connect-image').on('click', connectClientWithUsername);_381 $('#add-channel-image').on('click', showAddChannelInput);_381 $('#leave-span').on('click', disconnectClient);_381 $('#delete-channel-span').on('click', deleteCurrentChannel);_381 };_381_381 function handleUsernameInputKeypress(event) {_381 if (event.keyCode === 13){_381 connectClientWithUsername();_381 }_381 }_381_381 function handleInputTextKeypress(event) {_381 if (event.keyCode === 13) {_381 tc.currentChannel.sendMessage($(this).val());_381 event.preventDefault();_381 $(this).val('');_381 }_381 else {_381 notifyTyping();_381 }_381 }_381_381 var notifyTyping = $.throttle(function() {_381 tc.currentChannel.typing();_381 }, 1000);_381_381 tc.handleNewChannelInputKeypress = function(event) {_381 if (event.keyCode === 13) {_381 tc.messagingClient_381 .createChannel({_381 friendlyName: $newChannelInput.val(),_381 })_381 .then(hideAddChannelInput);_381_381 $(this).val('');_381 event.preventDefault();_381 }_381 };_381_381 function connectClientWithUsername() {_381 var usernameText = $usernameInput.val();_381 $usernameInput.val('');_381 if (usernameText == '') {_381 alert('Username cannot be empty');_381 return;_381 }_381 tc.username = usernameText;_381 fetchAccessToken(tc.username, connectMessagingClient);_381 }_381_381 function fetchAccessToken(username, handler) {_381 $.post('/token', {identity: username}, null, 'json')_381 .done(function(response) {_381 handler(response.token);_381 })_381 .fail(function(error) {_381 console.log('Failed to fetch the Access Token with error: ' + error);_381 });_381 }_381_381 function connectMessagingClient(token) {_381 // Initialize the Chat messaging client_381 Twilio.Chat.Client.create(token).then(function(client) {_381 tc.messagingClient = client;_381 updateConnectedUI();_381 tc.loadChannelList(tc.joinGeneralChannel);_381 tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));_381 tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));_381 tc.messagingClient.on('tokenExpired', refreshToken);_381 });_381 }_381_381 function refreshToken() {_381 fetchAccessToken(tc.username, setNewToken);_381 }_381_381 function setNewToken(token) {_381 tc.messagingClient.updateToken(tokenResponse.token);_381 }_381_381 function updateConnectedUI() {_381 $('#username-span').text(tc.username);_381 $statusRow.addClass('connected').removeClass('disconnected');_381 tc.$messageList.addClass('connected').removeClass('disconnected');_381 $connectPanel.addClass('connected').removeClass('disconnected');_381 $inputText.addClass('with-shadow');_381 $typingRow.addClass('connected').removeClass('disconnected');_381 }_381_381 tc.loadChannelList = function(handler) {_381 if (tc.messagingClient === undefined) {_381 console.log('Client is not initialized');_381 return;_381 }_381_381 tc.messagingClient.getPublicChannelDescriptors().then(function(channels) {_381 tc.channelArray = tc.sortChannelsByName(channels.items);_381 $channelList.text('');_381 tc.channelArray.forEach(addChannel);_381 if (typeof handler === 'function') {_381 handler();_381 }_381 });_381 };_381_381 tc.joinGeneralChannel = function() {_381 console.log('Attempting to join "general" chat channel...');_381 if (!tc.generalChannel) {_381 // If it doesn't exist, let's create it_381 tc.messagingClient.createChannel({_381 uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,_381 friendlyName: GENERAL_CHANNEL_NAME_381 }).then(function(channel) {_381 console.log('Created general channel');_381 tc.generalChannel = channel;_381 tc.loadChannelList(tc.joinGeneralChannel);_381 });_381 }_381 else {_381 console.log('Found general channel:');_381 setupChannel(tc.generalChannel);_381 }_381 };_381_381 function initChannel(channel) {_381 console.log('Initialized channel ' + channel.friendlyName);_381 return tc.messagingClient.getChannelBySid(channel.sid);_381 }_381_381 function joinChannel(_channel) {_381 return _channel.join()_381 .then(function(joinedChannel) {_381 console.log('Joined channel ' + joinedChannel.friendlyName);_381 updateChannelUI(_channel);_381_381 return joinedChannel;_381 })_381 .catch(function(err) {_381 if (_channel.status == 'joined') {_381 updateChannelUI(_channel);_381 return _channel;_381 } _381 console.error(_381 "Couldn't join channel " + _channel.friendlyName + ' because -> ' + err_381 );_381 });_381 }_381_381 function initChannelEvents() {_381 console.log(tc.currentChannel.friendlyName + ' ready.');_381 tc.currentChannel.on('messageAdded', tc.addMessageToList);_381 tc.currentChannel.on('typingStarted', showTypingStarted);_381 tc.currentChannel.on('typingEnded', hideTypingStarted);_381 tc.currentChannel.on('memberJoined', notifyMemberJoined);_381 tc.currentChannel.on('memberLeft', notifyMemberLeft);_381 $inputText.prop('disabled', false).focus();_381 }_381_381 function setupChannel(channel) {_381 return leaveCurrentChannel()_381 .then(function() {_381 return initChannel(channel);_381 })_381 .then(function(_channel) {_381 return joinChannel(_channel);_381 })_381 .then(initChannelEvents);_381 }_381_381 tc.loadMessages = function() {_381 tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT).then(function (messages) {_381 messages.items.forEach(tc.addMessageToList);_381 });_381 };_381_381 function leaveCurrentChannel() {_381 if (tc.currentChannel) {_381 return tc.currentChannel.leave().then(function(leftChannel) {_381 console.log('left ' + leftChannel.friendlyName);_381 leftChannel.removeListener('messageAdded', tc.addMessageToList);_381 leftChannel.removeListener('typingStarted', showTypingStarted);_381 leftChannel.removeListener('typingEnded', hideTypingStarted);_381 leftChannel.removeListener('memberJoined', notifyMemberJoined);_381 leftChannel.removeListener('memberLeft', notifyMemberLeft);_381 });_381 } else {_381 return Promise.resolve();_381 }_381 }_381_381 tc.addMessageToList = function(message) {_381 var rowDiv = $('<div>').addClass('row no-margin');_381 rowDiv.loadTemplate($('#message-template'), {_381 username: message.author,_381 date: dateFormatter.getTodayDate(message.dateCreated),_381 body: message.body_381 });_381 if (message.author === tc.username) {_381 rowDiv.addClass('own-message');_381 }_381_381 tc.$messageList.append(rowDiv);_381 scrollToMessageListBottom();_381 };_381_381 function notifyMemberJoined(member) {_381 notify(member.identity + ' joined the channel')_381 }_381_381 function notifyMemberLeft(member) {_381 notify(member.identity + ' left the channel');_381 }_381_381 function notify(message) {_381 var row = $('<div>').addClass('col-md-12');_381 row.loadTemplate('#member-notification-template', {_381 status: message_381 });_381 tc.$messageList.append(row);_381 scrollToMessageListBottom();_381 }_381_381 function showTypingStarted(member) {_381 $typingPlaceholder.text(member.identity + ' is typing...');_381 }_381_381 function hideTypingStarted(member) {_381 $typingPlaceholder.text('');_381 }_381_381 function scrollToMessageListBottom() {_381 tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);_381 }_381_381 function updateChannelUI(selectedChannel) {_381 var channelElements = $('.channel-element').toArray();_381 var channelElement = channelElements.filter(function(element) {_381 return $(element).data().sid === selectedChannel.sid;_381 });_381 channelElement = $(channelElement);_381 if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {_381 tc.currentChannelContainer = channelElement;_381 }_381 tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');_381 channelElement.removeClass('unselected-channel').addClass('selected-channel');_381 tc.currentChannelContainer = channelElement;_381 tc.currentChannel = selectedChannel;_381 tc.loadMessages();_381 }_381_381 function showAddChannelInput() {_381 if (tc.messagingClient) {_381 $newChannelInputRow.addClass('showing').removeClass('not-showing');_381 $channelList.addClass('showing').removeClass('not-showing');_381 $newChannelInput.focus();_381 }_381 }_381_381 function hideAddChannelInput() {_381 $newChannelInputRow.addClass('not-showing').removeClass('showing');_381 $channelList.addClass('not-showing').removeClass('showing');_381 $newChannelInput.val('');_381 }_381_381 function addChannel(channel) {_381 if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {_381 tc.generalChannel = channel;_381 }_381 var rowDiv = $('<div>').addClass('row channel-row');_381 rowDiv.loadTemplate('#channel-template', {_381 channelName: channel.friendlyName_381 });_381_381 var channelP = rowDiv.children().children().first();_381_381 rowDiv.on('click', selectChannel);_381 channelP.data('sid', channel.sid);_381 if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {_381 tc.currentChannelContainer = channelP;_381 channelP.addClass('selected-channel');_381 }_381 else {_381 channelP.addClass('unselected-channel')_381 }_381_381 $channelList.append(rowDiv);_381 }_381_381 function deleteCurrentChannel() {_381 if (!tc.currentChannel) {_381 return;_381 }_381_381 if (tc.currentChannel.sid === tc.generalChannel.sid) {_381 alert('You cannot delete the general channel');_381 return;_381 }_381_381 tc.currentChannel_381 .delete()_381 .then(function(channel) {_381 console.log('channel: '+ channel.friendlyName + ' deleted');_381 setupChannel(tc.generalChannel);_381 });_381 }_381_381 function selectChannel(event) {_381 var target = $(event.target);_381 var channelSid = target.data().sid;_381 var selectedChannel = tc.channelArray.filter(function(channel) {_381 return channel.sid === channelSid;_381 })[0];_381 if (selectedChannel === tc.currentChannel) {_381 return;_381 }_381 setupChannel(selectedChannel);_381 };_381_381 function disconnectClient() {_381 leaveCurrentChannel();_381 $channelList.text('');_381 tc.$messageList.text('');_381 channels = undefined;_381 $statusRow.addClass('disconnected').removeClass('connected');_381 tc.$messageList.addClass('disconnected').removeClass('connected');_381 $connectPanel.addClass('disconnected').removeClass('connected');_381 $inputText.removeClass('with-shadow');_381 $typingRow.addClass('disconnected').removeClass('connected');_381 }_381_381 tc.sortChannelsByName = function(channels) {_381 return channels.sort(function(a, b) {_381 if (a.friendlyName === GENERAL_CHANNEL_NAME) {_381 return -1;_381 }_381 if (b.friendlyName === GENERAL_CHANNEL_NAME) {_381 return 1;_381 }_381 return a.friendlyName.localeCompare(b.friendlyName);_381 });_381 };_381_381 return tc;_381})();
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
_381var twiliochat = (function() {_381 var tc = {};_381_381 var GENERAL_CHANNEL_UNIQUE_NAME = 'general';_381 var GENERAL_CHANNEL_NAME = 'General Channel';_381 var MESSAGES_HISTORY_LIMIT = 50;_381_381 var $channelList;_381 var $inputText;_381 var $usernameInput;_381 var $statusRow;_381 var $connectPanel;_381 var $newChannelInputRow;_381 var $newChannelInput;_381 var $typingRow;_381 var $typingPlaceholder;_381_381 $(document).ready(function() {_381 tc.init();_381 });_381_381 tc.init = function() {_381 tc.$messageList = $('#message-list');_381 $channelList = $('#channel-list');_381 $inputText = $('#input-text');_381 $usernameInput = $('#username-input');_381 $statusRow = $('#status-row');_381 $connectPanel = $('#connect-panel');_381 $newChannelInputRow = $('#new-channel-input-row');_381 $newChannelInput = $('#new-channel-input');_381 $typingRow = $('#typing-row');_381 $typingPlaceholder = $('#typing-placeholder');_381 $usernameInput.focus();_381 $usernameInput.on('keypress', handleUsernameInputKeypress);_381 $inputText.on('keypress', handleInputTextKeypress);_381 $newChannelInput.on('keypress', tc.handleNewChannelInputKeypress);_381 $('#connect-image').on('click', connectClientWithUsername);_381 $('#add-channel-image').on('click', showAddChannelInput);_381 $('#leave-span').on('click', disconnectClient);_381 $('#delete-channel-span').on('click', deleteCurrentChannel);_381 };_381_381 function handleUsernameInputKeypress(event) {_381 if (event.keyCode === 13){_381 connectClientWithUsername();_381 }_381 }_381_381 function handleInputTextKeypress(event) {_381 if (event.keyCode === 13) {_381 tc.currentChannel.sendMessage($(this).val());_381 event.preventDefault();_381 $(this).val('');_381 }_381 else {_381 notifyTyping();_381 }_381 }_381_381 var notifyTyping = $.throttle(function() {_381 tc.currentChannel.typing();_381 }, 1000);_381_381 tc.handleNewChannelInputKeypress = function(event) {_381 if (event.keyCode === 13) {_381 tc.messagingClient_381 .createChannel({_381 friendlyName: $newChannelInput.val(),_381 })_381 .then(hideAddChannelInput);_381_381 $(this).val('');_381 event.preventDefault();_381 }_381 };_381_381 function connectClientWithUsername() {_381 var usernameText = $usernameInput.val();_381 $usernameInput.val('');_381 if (usernameText == '') {_381 alert('Username cannot be empty');_381 return;_381 }_381 tc.username = usernameText;_381 fetchAccessToken(tc.username, connectMessagingClient);_381 }_381_381 function fetchAccessToken(username, handler) {_381 $.post('/token', {identity: username}, null, 'json')_381 .done(function(response) {_381 handler(response.token);_381 })_381 .fail(function(error) {_381 console.log('Failed to fetch the Access Token with error: ' + error);_381 });_381 }_381_381 function connectMessagingClient(token) {_381 // Initialize the Chat messaging client_381 Twilio.Chat.Client.create(token).then(function(client) {_381 tc.messagingClient = client;_381 updateConnectedUI();_381 tc.loadChannelList(tc.joinGeneralChannel);_381 tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));_381 tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));_381 tc.messagingClient.on('tokenExpired', refreshToken);_381 });_381 }_381_381 function refreshToken() {_381 fetchAccessToken(tc.username, setNewToken);_381 }_381_381 function setNewToken(token) {_381 tc.messagingClient.updateToken(tokenResponse.token);_381 }_381_381 function updateConnectedUI() {_381 $('#username-span').text(tc.username);_381 $statusRow.addClass('connected').removeClass('disconnected');_381 tc.$messageList.addClass('connected').removeClass('disconnected');_381 $connectPanel.addClass('connected').removeClass('disconnected');_381 $inputText.addClass('with-shadow');_381 $typingRow.addClass('connected').removeClass('disconnected');_381 }_381_381 tc.loadChannelList = function(handler) {_381 if (tc.messagingClient === undefined) {_381 console.log('Client is not initialized');_381 return;_381 }_381_381 tc.messagingClient.getPublicChannelDescriptors().then(function(channels) {_381 tc.channelArray = tc.sortChannelsByName(channels.items);_381 $channelList.text('');_381 tc.channelArray.forEach(addChannel);_381 if (typeof handler === 'function') {_381 handler();_381 }_381 });_381 };_381_381 tc.joinGeneralChannel = function() {_381 console.log('Attempting to join "general" chat channel...');_381 if (!tc.generalChannel) {_381 // If it doesn't exist, let's create it_381 tc.messagingClient.createChannel({_381 uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,_381 friendlyName: GENERAL_CHANNEL_NAME_381 }).then(function(channel) {_381 console.log('Created general channel');_381 tc.generalChannel = channel;_381 tc.loadChannelList(tc.joinGeneralChannel);_381 });_381 }_381 else {_381 console.log('Found general channel:');_381 setupChannel(tc.generalChannel);_381 }_381 };_381_381 function initChannel(channel) {_381 console.log('Initialized channel ' + channel.friendlyName);_381 return tc.messagingClient.getChannelBySid(channel.sid);_381 }_381_381 function joinChannel(_channel) {_381 return _channel.join()_381 .then(function(joinedChannel) {_381 console.log('Joined channel ' + joinedChannel.friendlyName);_381 updateChannelUI(_channel);_381_381 return joinedChannel;_381 })_381 .catch(function(err) {_381 if (_channel.status == 'joined') {_381 updateChannelUI(_channel);_381 return _channel;_381 } _381 console.error(_381 "Couldn't join channel " + _channel.friendlyName + ' because -> ' + err_381 );_381 });_381 }_381_381 function initChannelEvents() {_381 console.log(tc.currentChannel.friendlyName + ' ready.');_381 tc.currentChannel.on('messageAdded', tc.addMessageToList);_381 tc.currentChannel.on('typingStarted', showTypingStarted);_381 tc.currentChannel.on('typingEnded', hideTypingStarted);_381 tc.currentChannel.on('memberJoined', notifyMemberJoined);_381 tc.currentChannel.on('memberLeft', notifyMemberLeft);_381 $inputText.prop('disabled', false).focus();_381 }_381_381 function setupChannel(channel) {_381 return leaveCurrentChannel()_381 .then(function() {_381 return initChannel(channel);_381 })_381 .then(function(_channel) {_381 return joinChannel(_channel);_381 })_381 .then(initChannelEvents);_381 }_381_381 tc.loadMessages = function() {_381 tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT).then(function (messages) {_381 messages.items.forEach(tc.addMessageToList);_381 });_381 };_381_381 function leaveCurrentChannel() {_381 if (tc.currentChannel) {_381 return tc.currentChannel.leave().then(function(leftChannel) {_381 console.log('left ' + leftChannel.friendlyName);_381 leftChannel.removeListener('messageAdded', tc.addMessageToList);_381 leftChannel.removeListener('typingStarted', showTypingStarted);_381 leftChannel.removeListener('typingEnded', hideTypingStarted);_381 leftChannel.removeListener('memberJoined', notifyMemberJoined);_381 leftChannel.removeListener('memberLeft', notifyMemberLeft);_381 });_381 } else {_381 return Promise.resolve();_381 }_381 }_381_381 tc.addMessageToList = function(message) {_381 var rowDiv = $('<div>').addClass('row no-margin');_381 rowDiv.loadTemplate($('#message-template'), {_381 username: message.author,_381 date: dateFormatter.getTodayDate(message.dateCreated),_381 body: message.body_381 });_381 if (message.author === tc.username) {_381 rowDiv.addClass('own-message');_381 }_381_381 tc.$messageList.append(rowDiv);_381 scrollToMessageListBottom();_381 };_381_381 function notifyMemberJoined(member) {_381 notify(member.identity + ' joined the channel')_381 }_381_381 function notifyMemberLeft(member) {_381 notify(member.identity + ' left the channel');_381 }_381_381 function notify(message) {_381 var row = $('<div>').addClass('col-md-12');_381 row.loadTemplate('#member-notification-template', {_381 status: message_381 });_381 tc.$messageList.append(row);_381 scrollToMessageListBottom();_381 }_381_381 function showTypingStarted(member) {_381 $typingPlaceholder.text(member.identity + ' is typing...');_381 }_381_381 function hideTypingStarted(member) {_381 $typingPlaceholder.text('');_381 }_381_381 function scrollToMessageListBottom() {_381 tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);_381 }_381_381 function updateChannelUI(selectedChannel) {_381 var channelElements = $('.channel-element').toArray();_381 var channelElement = channelElements.filter(function(element) {_381 return $(element).data().sid === selectedChannel.sid;_381 });_381 channelElement = $(channelElement);_381 if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {_381 tc.currentChannelContainer = channelElement;_381 }_381 tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');_381 channelElement.removeClass('unselected-channel').addClass('selected-channel');_381 tc.currentChannelContainer = channelElement;_381 tc.currentChannel = selectedChannel;_381 tc.loadMessages();_381 }_381_381 function showAddChannelInput() {_381 if (tc.messagingClient) {_381 $newChannelInputRow.addClass('showing').removeClass('not-showing');_381 $channelList.addClass('showing').removeClass('not-showing');_381 $newChannelInput.focus();_381 }_381 }_381_381 function hideAddChannelInput() {_381 $newChannelInputRow.addClass('not-showing').removeClass('showing');_381 $channelList.addClass('not-showing').removeClass('showing');_381 $newChannelInput.val('');_381 }_381_381 function addChannel(channel) {_381 if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {_381 tc.generalChannel = channel;_381 }_381 var rowDiv = $('<div>').addClass('row channel-row');_381 rowDiv.loadTemplate('#channel-template', {_381 channelName: channel.friendlyName_381 });_381_381 var channelP = rowDiv.children().children().first();_381_381 rowDiv.on('click', selectChannel);_381 channelP.data('sid', channel.sid);_381 if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {_381 tc.currentChannelContainer = channelP;_381 channelP.addClass('selected-channel');_381 }_381 else {_381 channelP.addClass('unselected-channel')_381 }_381_381 $channelList.append(rowDiv);_381 }_381_381 function deleteCurrentChannel() {_381 if (!tc.currentChannel) {_381 return;_381 }_381_381 if (tc.currentChannel.sid === tc.generalChannel.sid) {_381 alert('You cannot delete the general channel');_381 return;_381 }_381_381 tc.currentChannel_381 .delete()_381 .then(function(channel) {_381 console.log('channel: '+ channel.friendlyName + ' deleted');_381 setupChannel(tc.generalChannel);_381 });_381 }_381_381 function selectChannel(event) {_381 var target = $(event.target);_381 var channelSid = target.data().sid;_381 var selectedChannel = tc.channelArray.filter(function(channel) {_381 return channel.sid === channelSid;_381 })[0];_381 if (selectedChannel === tc.currentChannel) {_381 return;_381 }_381 setupChannel(selectedChannel);_381 };_381_381 function disconnectClient() {_381 leaveCurrentChannel();_381 $channelList.text('');_381 tc.$messageList.text('');_381 channels = undefined;_381 $statusRow.addClass('disconnected').removeClass('connected');_381 tc.$messageList.addClass('disconnected').removeClass('connected');_381 $connectPanel.addClass('disconnected').removeClass('connected');_381 $inputText.removeClass('with-shadow');_381 $typingRow.addClass('disconnected').removeClass('connected');_381 }_381_381 tc.sortChannelsByName = function(channels) {_381 return channels.sort(function(a, b) {_381 if (a.friendlyName === GENERAL_CHANNEL_NAME) {_381 return -1;_381 }_381 if (b.friendlyName === GENERAL_CHANNEL_NAME) {_381 return 1;_381 }_381 return a.friendlyName.localeCompare(b.friendlyName);_381 });_381 };_381_381 return tc;_381})();
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
_381var twiliochat = (function() {_381 var tc = {};_381_381 var GENERAL_CHANNEL_UNIQUE_NAME = 'general';_381 var GENERAL_CHANNEL_NAME = 'General Channel';_381 var MESSAGES_HISTORY_LIMIT = 50;_381_381 var $channelList;_381 var $inputText;_381 var $usernameInput;_381 var $statusRow;_381 var $connectPanel;_381 var $newChannelInputRow;_381 var $newChannelInput;_381 var $typingRow;_381 var $typingPlaceholder;_381_381 $(document).ready(function() {_381 tc.init();_381 });_381_381 tc.init = function() {_381 tc.$messageList = $('#message-list');_381 $channelList = $('#channel-list');_381 $inputText = $('#input-text');_381 $usernameInput = $('#username-input');_381 $statusRow = $('#status-row');_381 $connectPanel = $('#connect-panel');_381 $newChannelInputRow = $('#new-channel-input-row');_381 $newChannelInput = $('#new-channel-input');_381 $typingRow = $('#typing-row');_381 $typingPlaceholder = $('#typing-placeholder');_381 $usernameInput.focus();_381 $usernameInput.on('keypress', handleUsernameInputKeypress);_381 $inputText.on('keypress', handleInputTextKeypress);_381 $newChannelInput.on('keypress', tc.handleNewChannelInputKeypress);_381 $('#connect-image').on('click', connectClientWithUsername);_381 $('#add-channel-image').on('click', showAddChannelInput);_381 $('#leave-span').on('click', disconnectClient);_381 $('#delete-channel-span').on('click', deleteCurrentChannel);_381 };_381_381 function handleUsernameInputKeypress(event) {_381 if (event.keyCode === 13){_381 connectClientWithUsername();_381 }_381 }_381_381 function handleInputTextKeypress(event) {_381 if (event.keyCode === 13) {_381 tc.currentChannel.sendMessage($(this).val());_381 event.preventDefault();_381 $(this).val('');_381 }_381 else {_381 notifyTyping();_381 }_381 }_381_381 var notifyTyping = $.throttle(function() {_381 tc.currentChannel.typing();_381 }, 1000);_381_381 tc.handleNewChannelInputKeypress = function(event) {_381 if (event.keyCode === 13) {_381 tc.messagingClient_381 .createChannel({_381 friendlyName: $newChannelInput.val(),_381 })_381 .then(hideAddChannelInput);_381_381 $(this).val('');_381 event.preventDefault();_381 }_381 };_381_381 function connectClientWithUsername() {_381 var usernameText = $usernameInput.val();_381 $usernameInput.val('');_381 if (usernameText == '') {_381 alert('Username cannot be empty');_381 return;_381 }_381 tc.username = usernameText;_381 fetchAccessToken(tc.username, connectMessagingClient);_381 }_381_381 function fetchAccessToken(username, handler) {_381 $.post('/token', {identity: username}, null, 'json')_381 .done(function(response) {_381 handler(response.token);_381 })_381 .fail(function(error) {_381 console.log('Failed to fetch the Access Token with error: ' + error);_381 });_381 }_381_381 function connectMessagingClient(token) {_381 // Initialize the Chat messaging client_381 Twilio.Chat.Client.create(token).then(function(client) {_381 tc.messagingClient = client;_381 updateConnectedUI();_381 tc.loadChannelList(tc.joinGeneralChannel);_381 tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));_381 tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));_381 tc.messagingClient.on('tokenExpired', refreshToken);_381 });_381 }_381_381 function refreshToken() {_381 fetchAccessToken(tc.username, setNewToken);_381 }_381_381 function setNewToken(token) {_381 tc.messagingClient.updateToken(tokenResponse.token);_381 }_381_381 function updateConnectedUI() {_381 $('#username-span').text(tc.username);_381 $statusRow.addClass('connected').removeClass('disconnected');_381 tc.$messageList.addClass('connected').removeClass('disconnected');_381 $connectPanel.addClass('connected').removeClass('disconnected');_381 $inputText.addClass('with-shadow');_381 $typingRow.addClass('connected').removeClass('disconnected');_381 }_381_381 tc.loadChannelList = function(handler) {_381 if (tc.messagingClient === undefined) {_381 console.log('Client is not initialized');_381 return;_381 }_381_381 tc.messagingClient.getPublicChannelDescriptors().then(function(channels) {_381 tc.channelArray = tc.sortChannelsByName(channels.items);_381 $channelList.text('');_381 tc.channelArray.forEach(addChannel);_381 if (typeof handler === 'function') {_381 handler();_381 }_381 });_381 };_381_381 tc.joinGeneralChannel = function() {_381 console.log('Attempting to join "general" chat channel...');_381 if (!tc.generalChannel) {_381 // If it doesn't exist, let's create it_381 tc.messagingClient.createChannel({_381 uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,_381 friendlyName: GENERAL_CHANNEL_NAME_381 }).then(function(channel) {_381 console.log('Created general channel');_381 tc.generalChannel = channel;_381 tc.loadChannelList(tc.joinGeneralChannel);_381 });_381 }_381 else {_381 console.log('Found general channel:');_381 setupChannel(tc.generalChannel);_381 }_381 };_381_381 function initChannel(channel) {_381 console.log('Initialized channel ' + channel.friendlyName);_381 return tc.messagingClient.getChannelBySid(channel.sid);_381 }_381_381 function joinChannel(_channel) {_381 return _channel.join()_381 .then(function(joinedChannel) {_381 console.log('Joined channel ' + joinedChannel.friendlyName);_381 updateChannelUI(_channel);_381_381 return joinedChannel;_381 })_381 .catch(function(err) {_381 if (_channel.status == 'joined') {_381 updateChannelUI(_channel);_381 return _channel;_381 } _381 console.error(_381 "Couldn't join channel " + _channel.friendlyName + ' because -> ' + err_381 );_381 });_381 }_381_381 function initChannelEvents() {_381 console.log(tc.currentChannel.friendlyName + ' ready.');_381 tc.currentChannel.on('messageAdded', tc.addMessageToList);_381 tc.currentChannel.on('typingStarted', showTypingStarted);_381 tc.currentChannel.on('typingEnded', hideTypingStarted);_381 tc.currentChannel.on('memberJoined', notifyMemberJoined);_381 tc.currentChannel.on('memberLeft', notifyMemberLeft);_381 $inputText.prop('disabled', false).focus();_381 }_381_381 function setupChannel(channel) {_381 return leaveCurrentChannel()_381 .then(function() {_381 return initChannel(channel);_381 })_381 .then(function(_channel) {_381 return joinChannel(_channel);_381 })_381 .then(initChannelEvents);_381 }_381_381 tc.loadMessages = function() {_381 tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT).then(function (messages) {_381 messages.items.forEach(tc.addMessageToList);_381 });_381 };_381_381 function leaveCurrentChannel() {_381 if (tc.currentChannel) {_381 return tc.currentChannel.leave().then(function(leftChannel) {_381 console.log('left ' + leftChannel.friendlyName);_381 leftChannel.removeListener('messageAdded', tc.addMessageToList);_381 leftChannel.removeListener('typingStarted', showTypingStarted);_381 leftChannel.removeListener('typingEnded', hideTypingStarted);_381 leftChannel.removeListener('memberJoined', notifyMemberJoined);_381 leftChannel.removeListener('memberLeft', notifyMemberLeft);_381 });_381 } else {_381 return Promise.resolve();_381 }_381 }_381_381 tc.addMessageToList = function(message) {_381 var rowDiv = $('<div>').addClass('row no-margin');_381 rowDiv.loadTemplate($('#message-template'), {_381 username: message.author,_381 date: dateFormatter.getTodayDate(message.dateCreated),_381 body: message.body_381 });_381 if (message.author === tc.username) {_381 rowDiv.addClass('own-message');_381 }_381_381 tc.$messageList.append(rowDiv);_381 scrollToMessageListBottom();_381 };_381_381 function notifyMemberJoined(member) {_381 notify(member.identity + ' joined the channel')_381 }_381_381 function notifyMemberLeft(member) {_381 notify(member.identity + ' left the channel');_381 }_381_381 function notify(message) {_381 var row = $('<div>').addClass('col-md-12');_381 row.loadTemplate('#member-notification-template', {_381 status: message_381 });_381 tc.$messageList.append(row);_381 scrollToMessageListBottom();_381 }_381_381 function showTypingStarted(member) {_381 $typingPlaceholder.text(member.identity + ' is typing...');_381 }_381_381 function hideTypingStarted(member) {_381 $typingPlaceholder.text('');_381 }_381_381 function scrollToMessageListBottom() {_381 tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);_381 }_381_381 function updateChannelUI(selectedChannel) {_381 var channelElements = $('.channel-element').toArray();_381 var channelElement = channelElements.filter(function(element) {_381 return $(element).data().sid === selectedChannel.sid;_381 });_381 channelElement = $(channelElement);_381 if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {_381 tc.currentChannelContainer = channelElement;_381 }_381 tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');_381 channelElement.removeClass('unselected-channel').addClass('selected-channel');_381 tc.currentChannelContainer = channelElement;_381 tc.currentChannel = selectedChannel;_381 tc.loadMessages();_381 }_381_381 function showAddChannelInput() {_381 if (tc.messagingClient) {_381 $newChannelInputRow.addClass('showing').removeClass('not-showing');_381 $channelList.addClass('showing').removeClass('not-showing');_381 $newChannelInput.focus();_381 }_381 }_381_381 function hideAddChannelInput() {_381 $newChannelInputRow.addClass('not-showing').removeClass('showing');_381 $channelList.addClass('not-showing').removeClass('showing');_381 $newChannelInput.val('');_381 }_381_381 function addChannel(channel) {_381 if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {_381 tc.generalChannel = channel;_381 }_381 var rowDiv = $('<div>').addClass('row channel-row');_381 rowDiv.loadTemplate('#channel-template', {_381 channelName: channel.friendlyName_381 });_381_381 var channelP = rowDiv.children().children().first();_381_381 rowDiv.on('click', selectChannel);_381 channelP.data('sid', channel.sid);_381 if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {_381 tc.currentChannelContainer = channelP;_381 channelP.addClass('selected-channel');_381 }_381 else {_381 channelP.addClass('unselected-channel')_381 }_381_381 $channelList.append(rowDiv);_381 }_381_381 function deleteCurrentChannel() {_381 if (!tc.currentChannel) {_381 return;_381 }_381_381 if (tc.currentChannel.sid === tc.generalChannel.sid) {_381 alert('You cannot delete the general channel');_381 return;_381 }_381_381 tc.currentChannel_381 .delete()_381 .then(function(channel) {_381 console.log('channel: '+ channel.friendlyName + ' deleted');_381 setupChannel(tc.generalChannel);_381 });_381 }_381_381 function selectChannel(event) {_381 var target = $(event.target);_381 var channelSid = target.data().sid;_381 var selectedChannel = tc.channelArray.filter(function(channel) {_381 return channel.sid === channelSid;_381 })[0];_381 if (selectedChannel === tc.currentChannel) {_381 return;_381 }_381 setupChannel(selectedChannel);_381 };_381_381 function disconnectClient() {_381 leaveCurrentChannel();_381 $channelList.text('');_381 tc.$messageList.text('');_381 channels = undefined;_381 $statusRow.addClass('disconnected').removeClass('connected');_381 tc.$messageList.addClass('disconnected').removeClass('connected');_381 $connectPanel.addClass('disconnected').removeClass('connected');_381 $inputText.removeClass('with-shadow');_381 $typingRow.addClass('disconnected').removeClass('connected');_381 }_381_381 tc.sortChannelsByName = function(channels) {_381 return channels.sort(function(a, b) {_381 if (a.friendlyName === GENERAL_CHANNEL_NAME) {_381 return -1;_381 }_381 if (b.friendlyName === GENERAL_CHANNEL_NAME) {_381 return 1;_381 }_381 return a.friendlyName.localeCompare(b.friendlyName);_381 });_381 };_381_381 return tc;_381})();
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
_381var twiliochat = (function() {_381 var tc = {};_381_381 var GENERAL_CHANNEL_UNIQUE_NAME = 'general';_381 var GENERAL_CHANNEL_NAME = 'General Channel';_381 var MESSAGES_HISTORY_LIMIT = 50;_381_381 var $channelList;_381 var $inputText;_381 var $usernameInput;_381 var $statusRow;_381 var $connectPanel;_381 var $newChannelInputRow;_381 var $newChannelInput;_381 var $typingRow;_381 var $typingPlaceholder;_381_381 $(document).ready(function() {_381 tc.init();_381 });_381_381 tc.init = function() {_381 tc.$messageList = $('#message-list');_381 $channelList = $('#channel-list');_381 $inputText = $('#input-text');_381 $usernameInput = $('#username-input');_381 $statusRow = $('#status-row');_381 $connectPanel = $('#connect-panel');_381 $newChannelInputRow = $('#new-channel-input-row');_381 $newChannelInput = $('#new-channel-input');_381 $typingRow = $('#typing-row');_381 $typingPlaceholder = $('#typing-placeholder');_381 $usernameInput.focus();_381 $usernameInput.on('keypress', handleUsernameInputKeypress);_381 $inputText.on('keypress', handleInputTextKeypress);_381 $newChannelInput.on('keypress', tc.handleNewChannelInputKeypress);_381 $('#connect-image').on('click', connectClientWithUsername);_381 $('#add-channel-image').on('click', showAddChannelInput);_381 $('#leave-span').on('click', disconnectClient);_381 $('#delete-channel-span').on('click', deleteCurrentChannel);_381 };_381_381 function handleUsernameInputKeypress(event) {_381 if (event.keyCode === 13){_381 connectClientWithUsername();_381 }_381 }_381_381 function handleInputTextKeypress(event) {_381 if (event.keyCode === 13) {_381 tc.currentChannel.sendMessage($(this).val());_381 event.preventDefault();_381 $(this).val('');_381 }_381 else {_381 notifyTyping();_381 }_381 }_381_381 var notifyTyping = $.throttle(function() {_381 tc.currentChannel.typing();_381 }, 1000);_381_381 tc.handleNewChannelInputKeypress = function(event) {_381 if (event.keyCode === 13) {_381 tc.messagingClient_381 .createChannel({_381 friendlyName: $newChannelInput.val(),_381 })_381 .then(hideAddChannelInput);_381_381 $(this).val('');_381 event.preventDefault();_381 }_381 };_381_381 function connectClientWithUsername() {_381 var usernameText = $usernameInput.val();_381 $usernameInput.val('');_381 if (usernameText == '') {_381 alert('Username cannot be empty');_381 return;_381 }_381 tc.username = usernameText;_381 fetchAccessToken(tc.username, connectMessagingClient);_381 }_381_381 function fetchAccessToken(username, handler) {_381 $.post('/token', {identity: username}, null, 'json')_381 .done(function(response) {_381 handler(response.token);_381 })_381 .fail(function(error) {_381 console.log('Failed to fetch the Access Token with error: ' + error);_381 });_381 }_381_381 function connectMessagingClient(token) {_381 // Initialize the Chat messaging client_381 Twilio.Chat.Client.create(token).then(function(client) {_381 tc.messagingClient = client;_381 updateConnectedUI();_381 tc.loadChannelList(tc.joinGeneralChannel);_381 tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));_381 tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));_381 tc.messagingClient.on('tokenExpired', refreshToken);_381 });_381 }_381_381 function refreshToken() {_381 fetchAccessToken(tc.username, setNewToken);_381 }_381_381 function setNewToken(token) {_381 tc.messagingClient.updateToken(tokenResponse.token);_381 }_381_381 function updateConnectedUI() {_381 $('#username-span').text(tc.username);_381 $statusRow.addClass('connected').removeClass('disconnected');_381 tc.$messageList.addClass('connected').removeClass('disconnected');_381 $connectPanel.addClass('connected').removeClass('disconnected');_381 $inputText.addClass('with-shadow');_381 $typingRow.addClass('connected').removeClass('disconnected');_381 }_381_381 tc.loadChannelList = function(handler) {_381 if (tc.messagingClient === undefined) {_381 console.log('Client is not initialized');_381 return;_381 }_381_381 tc.messagingClient.getPublicChannelDescriptors().then(function(channels) {_381 tc.channelArray = tc.sortChannelsByName(channels.items);_381 $channelList.text('');_381 tc.channelArray.forEach(addChannel);_381 if (typeof handler === 'function') {_381 handler();_381 }_381 });_381 };_381_381 tc.joinGeneralChannel = function() {_381 console.log('Attempting to join "general" chat channel...');_381 if (!tc.generalChannel) {_381 // If it doesn't exist, let's create it_381 tc.messagingClient.createChannel({_381 uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,_381 friendlyName: GENERAL_CHANNEL_NAME_381 }).then(function(channel) {_381 console.log('Created general channel');_381 tc.generalChannel = channel;_381 tc.loadChannelList(tc.joinGeneralChannel);_381 });_381 }_381 else {_381 console.log('Found general channel:');_381 setupChannel(tc.generalChannel);_381 }_381 };_381_381 function initChannel(channel) {_381 console.log('Initialized channel ' + channel.friendlyName);_381 return tc.messagingClient.getChannelBySid(channel.sid);_381 }_381_381 function joinChannel(_channel) {_381 return _channel.join()_381 .then(function(joinedChannel) {_381 console.log('Joined channel ' + joinedChannel.friendlyName);_381 updateChannelUI(_channel);_381_381 return joinedChannel;_381 })_381 .catch(function(err) {_381 if (_channel.status == 'joined') {_381 updateChannelUI(_channel);_381 return _channel;_381 } _381 console.error(_381 "Couldn't join channel " + _channel.friendlyName + ' because -> ' + err_381 );_381 });_381 }_381_381 function initChannelEvents() {_381 console.log(tc.currentChannel.friendlyName + ' ready.');_381 tc.currentChannel.on('messageAdded', tc.addMessageToList);_381 tc.currentChannel.on('typingStarted', showTypingStarted);_381 tc.currentChannel.on('typingEnded', hideTypingStarted);_381 tc.currentChannel.on('memberJoined', notifyMemberJoined);_381 tc.currentChannel.on('memberLeft', notifyMemberLeft);_381 $inputText.prop('disabled', false).focus();_381 }_381_381 function setupChannel(channel) {_381 return leaveCurrentChannel()_381 .then(function() {_381 return initChannel(channel);_381 })_381 .then(function(_channel) {_381 return joinChannel(_channel);_381 })_381 .then(initChannelEvents);_381 }_381_381 tc.loadMessages = function() {_381 tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT).then(function (messages) {_381 messages.items.forEach(tc.addMessageToList);_381 });_381 };_381_381 function leaveCurrentChannel() {_381 if (tc.currentChannel) {_381 return tc.currentChannel.leave().then(function(leftChannel) {_381 console.log('left ' + leftChannel.friendlyName);_381 leftChannel.removeListener('messageAdded', tc.addMessageToList);_381 leftChannel.removeListener('typingStarted', showTypingStarted);_381 leftChannel.removeListener('typingEnded', hideTypingStarted);_381 leftChannel.removeListener('memberJoined', notifyMemberJoined);_381 leftChannel.removeListener('memberLeft', notifyMemberLeft);_381 });_381 } else {_381 return Promise.resolve();_381 }_381 }_381_381 tc.addMessageToList = function(message) {_381 var rowDiv = $('<div>').addClass('row no-margin');_381 rowDiv.loadTemplate($('#message-template'), {_381 username: message.author,_381 date: dateFormatter.getTodayDate(message.dateCreated),_381 body: message.body_381 });_381 if (message.author === tc.username) {_381 rowDiv.addClass('own-message');_381 }_381_381 tc.$messageList.append(rowDiv);_381 scrollToMessageListBottom();_381 };_381_381 function notifyMemberJoined(member) {_381 notify(member.identity + ' joined the channel')_381 }_381_381 function notifyMemberLeft(member) {_381 notify(member.identity + ' left the channel');_381 }_381_381 function notify(message) {_381 var row = $('<div>').addClass('col-md-12');_381 row.loadTemplate('#member-notification-template', {_381 status: message_381 });_381 tc.$messageList.append(row);_381 scrollToMessageListBottom();_381 }_381_381 function showTypingStarted(member) {_381 $typingPlaceholder.text(member.identity + ' is typing...');_381 }_381_381 function hideTypingStarted(member) {_381 $typingPlaceholder.text('');_381 }_381_381 function scrollToMessageListBottom() {_381 tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);_381 }_381_381 function updateChannelUI(selectedChannel) {_381 var channelElements = $('.channel-element').toArray();_381 var channelElement = channelElements.filter(function(element) {_381 return $(element).data().sid === selectedChannel.sid;_381 });_381 channelElement = $(channelElement);_381 if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {_381 tc.currentChannelContainer = channelElement;_381 }_381 tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');_381 channelElement.removeClass('unselected-channel').addClass('selected-channel');_381 tc.currentChannelContainer = channelElement;_381 tc.currentChannel = selectedChannel;_381 tc.loadMessages();_381 }_381_381 function showAddChannelInput() {_381 if (tc.messagingClient) {_381 $newChannelInputRow.addClass('showing').removeClass('not-showing');_381 $channelList.addClass('showing').removeClass('not-showing');_381 $newChannelInput.focus();_381 }_381 }_381_381 function hideAddChannelInput() {_381 $newChannelInputRow.addClass('not-showing').removeClass('showing');_381 $channelList.addClass('not-showing').removeClass('showing');_381 $newChannelInput.val('');_381 }_381_381 function addChannel(channel) {_381 if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {_381 tc.generalChannel = channel;_381 }_381 var rowDiv = $('<div>').addClass('row channel-row');_381 rowDiv.loadTemplate('#channel-template', {_381 channelName: channel.friendlyName_381 });_381_381 var channelP = rowDiv.children().children().first();_381_381 rowDiv.on('click', selectChannel);_381 channelP.data('sid', channel.sid);_381 if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {_381 tc.currentChannelContainer = channelP;_381 channelP.addClass('selected-channel');_381 }_381 else {_381 channelP.addClass('unselected-channel')_381 }_381_381 $channelList.append(rowDiv);_381 }_381_381 function deleteCurrentChannel() {_381 if (!tc.currentChannel) {_381 return;_381 }_381_381 if (tc.currentChannel.sid === tc.generalChannel.sid) {_381 alert('You cannot delete the general channel');_381 return;_381 }_381_381 tc.currentChannel_381 .delete()_381 .then(function(channel) {_381 console.log('channel: '+ channel.friendlyName + ' deleted');_381 setupChannel(tc.generalChannel);_381 });_381 }_381_381 function selectChannel(event) {_381 var target = $(event.target);_381 var channelSid = target.data().sid;_381 var selectedChannel = tc.channelArray.filter(function(channel) {_381 return channel.sid === channelSid;_381 })[0];_381 if (selectedChannel === tc.currentChannel) {_381 return;_381 }_381 setupChannel(selectedChannel);_381 };_381_381 function disconnectClient() {_381 leaveCurrentChannel();_381 $channelList.text('');_381 tc.$messageList.text('');_381 channels = undefined;_381 $statusRow.addClass('disconnected').removeClass('connected');_381 tc.$messageList.addClass('disconnected').removeClass('connected');_381 $connectPanel.addClass('disconnected').removeClass('connected');_381 $inputText.removeClass('with-shadow');_381 $typingRow.addClass('disconnected').removeClass('connected');_381 }_381_381 tc.sortChannelsByName = function(channels) {_381 return channels.sort(function(a, b) {_381 if (a.friendlyName === GENERAL_CHANNEL_NAME) {_381 return -1;_381 }_381 if (b.friendlyName === GENERAL_CHANNEL_NAME) {_381 return 1;_381 }_381 return a.friendlyName.localeCompare(b.friendlyName);_381 });_381 };_381_381 return tc;_381})();
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.