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?
This application allows users to exchange messages through different channels, using the Twilio Programmable Chat API. On this example, we'll show how to use this API features to manage channels and to show it's 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.
With Laravel we must create a provider that will inject the AccessToken
object in the controller, the same goes for ChatMessagingGrant
inside TwilioChatGrantProvider.php
. We'll see how to use these objects in the next step.
app/Providers/TwilioAccessTokenProvider.php
1<?php2namespace App\Providers;3use Illuminate\Support\ServiceProvider;4use Twilio\Jwt\AccessToken;56class TwilioAccessTokenProvider extends ServiceProvider7{8/**9* Register the application services.10*11* @return void12*/13public function register()14{15$this->app->bind(16AccessToken::class, function ($app) {17$TWILIO_ACCOUNT_SID = config('services.twilio')['accountSid'];18$TWILIO_API_KEY = config('services.twilio')['apiKey'];19$TWILIO_API_SECRET = config('services.twilio')['apiSecret'];2021$token = new AccessToken(22$TWILIO_ACCOUNT_SID,23$TWILIO_API_KEY,24$TWILIO_API_SECRET,25360026);2728return $token;29}30);31}32}33
We can generate a token, now we need a way for the chat app to get it.
On our controller, we expose the endpoint responsible for providing a valid token using this parameter:
identity
: identifies the user itselfOnce we have used the AccessToken
object to generate a token we can use the AccessToken's method token.toJWT()
to get the token as a String. Then we just return the token as a JSON encoded string.
app/Http/Controllers/TokenController.php
1<?php2namespace App\Http\Controllers;3use Illuminate\Http\Request;4use App\Http\Requests;5use App\Http\Controllers\Controller;6use Twilio\Jwt\AccessToken;7use Twilio\Jwt\Grants\ChatGrant;89class TokenController extends Controller10{11public function generate(Request $request, AccessToken $accessToken, ChatGrant $chatGrant)12{13$appName = "TwilioChat";14$identity = $request->input("identity");1516$TWILIO_CHAT_SERVICE_SID = config('services.twilio')['chatServiceSid'];1718$accessToken->setIdentity($identity);1920$chatGrant->setServiceSid($TWILIO_CHAT_SERVICE_SID);2122$accessToken->addGrant($chatGrant);2324$response = array(25'identity' => $identity,26'token' => $accessToken->toJWT()27);2829return response()->json($response);30}31}
Now that we have a route that generates JWT tokens on demand, let's use this route to initialize our Twilio Chat Client.
Our client fetches a new Token by making a POST
request to our endpoint.
With the token we can create a new Twilio.AccessManager
, and initialize our Twilio.Chat.Client
.
public/js/twiliochat.js
1var twiliochat = (function() {2var tc = {};34var GENERAL_CHANNEL_UNIQUE_NAME = 'general';5var GENERAL_CHANNEL_NAME = 'General Channel';6var MESSAGES_HISTORY_LIMIT = 50;78var $channelList;9var $inputText;10var $usernameInput;11var $statusRow;12var $connectPanel;13var $newChannelInputRow;14var $newChannelInput;15var $typingRow;16var $typingPlaceholder;1718$(document).ready(function() {19tc.$messageList = $('#message-list');20$channelList = $('#channel-list');21$inputText = $('#input-text');22$usernameInput = $('#username-input');23$statusRow = $('#status-row');24$connectPanel = $('#connect-panel');25$newChannelInputRow = $('#new-channel-input-row');26$newChannelInput = $('#new-channel-input');27$typingRow = $('#typing-row');28$typingPlaceholder = $('#typing-placeholder');29$usernameInput.focus();30$usernameInput.on('keypress', handleUsernameInputKeypress);31$inputText.on('keypress', handleInputTextKeypress);32$newChannelInput.on('keypress', tc.handleNewChannelInputKeypress);33$('#connect-image').on('click', connectClientWithUsername);34$('#add-channel-image').on('click', showAddChannelInput);35$('#leave-span').on('click', disconnectClient);36$('#delete-channel-span').on('click', deleteCurrentChannel);37});3839function handleUsernameInputKeypress(event) {40if (event.keyCode === 13){41connectClientWithUsername();42}43}4445function handleInputTextKeypress(event) {46if (event.keyCode === 13) {47tc.currentChannel.sendMessage($(this).val());48event.preventDefault();49$(this).val('');50}51else {52notifyTyping();53}54}5556var notifyTyping = $.throttle(function() {57tc.currentChannel.typing();58}, 1000);5960tc.handleNewChannelInputKeypress = function(event) {61if (event.keyCode === 13) {62tc.messagingClient.createChannel({63friendlyName: $newChannelInput.val()64}).then(hideAddChannelInput);65$(this).val('');66event.preventDefault();67}68};6970function connectClientWithUsername() {71var usernameText = $usernameInput.val();72$usernameInput.val('');73if (usernameText == '') {74alert('Username cannot be empty');75return;76}77tc.username = usernameText;78fetchAccessToken(tc.username, connectMessagingClient);79}8081function fetchAccessToken(username, handler) {82$.post('/token', {identity: username}, null, 'json')83.done(function(response) {84handler(response.token);85})86.fail(function(error) {87console.log('Failed to fetch the Access Token with error: ' + error);88});89}9091function connectMessagingClient(token) {92// Initialize the Chat messaging client93tc.accessManager = new Twilio.AccessManager(token);94Twilio.Chat.Client.create(token).then(function(client) {95tc.messagingClient = client;96updateConnectedUI();97tc.loadChannelList(tc.joinGeneralChannel);98tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));99tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));100tc.messagingClient.on('tokenExpired', refreshToken);101});102}103104function refreshToken() {105fetchAccessToken(tc.username, setNewToken);106}107108function setNewToken(tokenResponse) {109tc.accessManager.updateToken(tokenResponse.token);110}111112function updateConnectedUI() {113$('#username-span').text(tc.username);114$statusRow.addClass('connected').removeClass('disconnected');115tc.$messageList.addClass('connected').removeClass('disconnected');116$connectPanel.addClass('connected').removeClass('disconnected');117$inputText.addClass('with-shadow');118$typingRow.addClass('connected').removeClass('disconnected');119}120121tc.loadChannelList = function(handler) {122if (tc.messagingClient === undefined) {123console.log('Client is not initialized');124return;125}126127tc.messagingClient.getPublicChannelDescriptors().then(function(channels) {128tc.channelArray = tc.sortChannelsByName(channels.items);129$channelList.text('');130tc.channelArray.forEach(addChannel);131if (typeof handler === 'function') {132handler();133}134});135};136137tc.joinGeneralChannel = function() {138console.log('Attempting to join "general" chat channel...');139if (!tc.generalChannel) {140// If it doesn't exist, let's create it141tc.messagingClient.createChannel({142uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,143friendlyName: GENERAL_CHANNEL_NAME144}).then(function(channel) {145console.log('Created general channel');146tc.generalChannel = channel;147tc.loadChannelList(tc.joinGeneralChannel);148});149}150else {151console.log('Found general channel:');152setupChannel(tc.generalChannel);153}154};155156function initChannel(channel) {157console.log('Initialized channel ' + channel.friendlyName);158return tc.messagingClient.getChannelBySid(channel.sid);159}160161function joinChannel(_channel) {162return _channel.join()163.then(function(joinedChannel) {164console.log('Joined channel ' + joinedChannel.friendlyName);165updateChannelUI(_channel);166tc.currentChannel = _channel;167tc.loadMessages();168return joinedChannel;169});170}171172function initChannelEvents() {173console.log(tc.currentChannel.friendlyName + ' ready.');174tc.currentChannel.on('messageAdded', tc.addMessageToList);175tc.currentChannel.on('typingStarted', showTypingStarted);176tc.currentChannel.on('typingEnded', hideTypingStarted);177tc.currentChannel.on('memberJoined', notifyMemberJoined);178tc.currentChannel.on('memberLeft', notifyMemberLeft);179$inputText.prop('disabled', false).focus();180}181182function setupChannel(channel) {183return leaveCurrentChannel()184.then(function() {185return initChannel(channel);186})187.then(function(_channel) {188return joinChannel(_channel);189})190.then(initChannelEvents);191}192193tc.loadMessages = function() {194tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT).then(function (messages) {195messages.items.forEach(tc.addMessageToList);196});197};198199function leaveCurrentChannel() {200if (tc.currentChannel) {201return tc.currentChannel.leave().then(function(leftChannel) {202console.log('left ' + leftChannel.friendlyName);203leftChannel.removeListener('messageAdded', tc.addMessageToList);204leftChannel.removeListener('typingStarted', showTypingStarted);205leftChannel.removeListener('typingEnded', hideTypingStarted);206leftChannel.removeListener('memberJoined', notifyMemberJoined);207leftChannel.removeListener('memberLeft', notifyMemberLeft);208});209} else {210return Promise.resolve();211}212}213214tc.addMessageToList = function(message) {215var rowDiv = $('<div>').addClass('row no-margin');216rowDiv.loadTemplate($('#message-template'), {217username: message.author,218date: dateFormatter.getTodayDate(message.dateCreated),219body: message.body220});221if (message.author === tc.username) {222rowDiv.addClass('own-message');223}224225tc.$messageList.append(rowDiv);226scrollToMessageListBottom();227};228229function notifyMemberJoined(member) {230notify(member.identity + ' joined the channel')231}232233function notifyMemberLeft(member) {234notify(member.identity + ' left the channel');235}236237function notify(message) {238var row = $('<div>').addClass('col-md-12');239row.loadTemplate('#member-notification-template', {240status: message241});242tc.$messageList.append(row);243scrollToMessageListBottom();244}245246function showTypingStarted(member) {247$typingPlaceholder.text(member.identity + ' is typing...');248}249250function hideTypingStarted(member) {251$typingPlaceholder.text('');252}253254function scrollToMessageListBottom() {255tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);256}257258function updateChannelUI(selectedChannel) {259var channelElements = $('.channel-element').toArray();260var channelElement = channelElements.filter(function(element) {261return $(element).data().sid === selectedChannel.sid;262});263channelElement = $(channelElement);264if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {265tc.currentChannelContainer = channelElement;266}267tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');268channelElement.removeClass('unselected-channel').addClass('selected-channel');269tc.currentChannelContainer = channelElement;270}271272function showAddChannelInput() {273if (tc.messagingClient) {274$newChannelInputRow.addClass('showing').removeClass('not-showing');275$channelList.addClass('showing').removeClass('not-showing');276$newChannelInput.focus();277}278}279280function hideAddChannelInput() {281$newChannelInputRow.addClass('not-showing').removeClass('showing');282$channelList.addClass('not-showing').removeClass('showing');283$newChannelInput.val('');284}285286function addChannel(channel) {287if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {288tc.generalChannel = channel;289}290var rowDiv = $('<div>').addClass('row channel-row');291rowDiv.loadTemplate('#channel-template', {292channelName: channel.friendlyName293});294295var channelP = rowDiv.children().children().first();296297rowDiv.on('click', selectChannel);298channelP.data('sid', channel.sid);299if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {300tc.currentChannelContainer = channelP;301channelP.addClass('selected-channel');302}303else {304channelP.addClass('unselected-channel')305}306307$channelList.append(rowDiv);308}309310function deleteCurrentChannel() {311if (!tc.currentChannel) {312return;313}314if (tc.currentChannel.sid === tc.generalChannel.sid) {315alert('You cannot delete the general channel');316return;317}318tc.currentChannel.delete().then(function(channel) {319console.log('channel: '+ channel.friendlyName + ' deleted');320setupChannel(tc.generalChannel);321});322}323324function selectChannel(event) {325var target = $(event.target);326var channelSid = target.data().sid;327var selectedChannel = tc.channelArray.filter(function(channel) {328return channel.sid === channelSid;329})[0];330if (selectedChannel === tc.currentChannel) {331return;332}333setupChannel(selectedChannel);334};335336function disconnectClient() {337leaveCurrentChannel();338$channelList.text('');339tc.$messageList.text('');340channels = undefined;341$statusRow.addClass('disconnected').removeClass('connected');342tc.$messageList.addClass('disconnected').removeClass('connected');343$connectPanel.addClass('disconnected').removeClass('connected');344$inputText.removeClass('with-shadow');345$typingRow.addClass('disconnected').removeClass('connected');346}347348tc.sortChannelsByName = function(channels) {349return channels.sort(function(a, b) {350if (a.friendlyName === GENERAL_CHANNEL_NAME) {351return -1;352}353if (b.friendlyName === GENERAL_CHANNEL_NAME) {354return 1;355}356return a.friendlyName.localeCompare(b.friendlyName);357});358};359360return tc;361})();362
Now that we've initialized our Chat Client, let's see how we can get a list of channels.
After initializing the client we can call the getPublicChannelDescriptors
method to retrieve all visible channels. This method returns a promise which we use to show the list of channels retrieved on the UI.
public/js/twiliochat.js
1var twiliochat = (function() {2var tc = {};34var GENERAL_CHANNEL_UNIQUE_NAME = 'general';5var GENERAL_CHANNEL_NAME = 'General Channel';6var MESSAGES_HISTORY_LIMIT = 50;78var $channelList;9var $inputText;10var $usernameInput;11var $statusRow;12var $connectPanel;13var $newChannelInputRow;14var $newChannelInput;15var $typingRow;16var $typingPlaceholder;1718$(document).ready(function() {19tc.$messageList = $('#message-list');20$channelList = $('#channel-list');21$inputText = $('#input-text');22$usernameInput = $('#username-input');23$statusRow = $('#status-row');24$connectPanel = $('#connect-panel');25$newChannelInputRow = $('#new-channel-input-row');26$newChannelInput = $('#new-channel-input');27$typingRow = $('#typing-row');28$typingPlaceholder = $('#typing-placeholder');29$usernameInput.focus();30$usernameInput.on('keypress', handleUsernameInputKeypress);31$inputText.on('keypress', handleInputTextKeypress);32$newChannelInput.on('keypress', tc.handleNewChannelInputKeypress);33$('#connect-image').on('click', connectClientWithUsername);34$('#add-channel-image').on('click', showAddChannelInput);35$('#leave-span').on('click', disconnectClient);36$('#delete-channel-span').on('click', deleteCurrentChannel);37});3839function handleUsernameInputKeypress(event) {40if (event.keyCode === 13){41connectClientWithUsername();42}43}4445function handleInputTextKeypress(event) {46if (event.keyCode === 13) {47tc.currentChannel.sendMessage($(this).val());48event.preventDefault();49$(this).val('');50}51else {52notifyTyping();53}54}5556var notifyTyping = $.throttle(function() {57tc.currentChannel.typing();58}, 1000);5960tc.handleNewChannelInputKeypress = function(event) {61if (event.keyCode === 13) {62tc.messagingClient.createChannel({63friendlyName: $newChannelInput.val()64}).then(hideAddChannelInput);65$(this).val('');66event.preventDefault();67}68};6970function connectClientWithUsername() {71var usernameText = $usernameInput.val();72$usernameInput.val('');73if (usernameText == '') {74alert('Username cannot be empty');75return;76}77tc.username = usernameText;78fetchAccessToken(tc.username, connectMessagingClient);79}8081function fetchAccessToken(username, handler) {82$.post('/token', {identity: username}, null, 'json')83.done(function(response) {84handler(response.token);85})86.fail(function(error) {87console.log('Failed to fetch the Access Token with error: ' + error);88});89}9091function connectMessagingClient(token) {92// Initialize the Chat messaging client93tc.accessManager = new Twilio.AccessManager(token);94Twilio.Chat.Client.create(token).then(function(client) {95tc.messagingClient = client;96updateConnectedUI();97tc.loadChannelList(tc.joinGeneralChannel);98tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));99tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));100tc.messagingClient.on('tokenExpired', refreshToken);101});102}103104function refreshToken() {105fetchAccessToken(tc.username, setNewToken);106}107108function setNewToken(tokenResponse) {109tc.accessManager.updateToken(tokenResponse.token);110}111112function updateConnectedUI() {113$('#username-span').text(tc.username);114$statusRow.addClass('connected').removeClass('disconnected');115tc.$messageList.addClass('connected').removeClass('disconnected');116$connectPanel.addClass('connected').removeClass('disconnected');117$inputText.addClass('with-shadow');118$typingRow.addClass('connected').removeClass('disconnected');119}120121tc.loadChannelList = function(handler) {122if (tc.messagingClient === undefined) {123console.log('Client is not initialized');124return;125}126127tc.messagingClient.getPublicChannelDescriptors().then(function(channels) {128tc.channelArray = tc.sortChannelsByName(channels.items);129$channelList.text('');130tc.channelArray.forEach(addChannel);131if (typeof handler === 'function') {132handler();133}134});135};136137tc.joinGeneralChannel = function() {138console.log('Attempting to join "general" chat channel...');139if (!tc.generalChannel) {140// If it doesn't exist, let's create it141tc.messagingClient.createChannel({142uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,143friendlyName: GENERAL_CHANNEL_NAME144}).then(function(channel) {145console.log('Created general channel');146tc.generalChannel = channel;147tc.loadChannelList(tc.joinGeneralChannel);148});149}150else {151console.log('Found general channel:');152setupChannel(tc.generalChannel);153}154};155156function initChannel(channel) {157console.log('Initialized channel ' + channel.friendlyName);158return tc.messagingClient.getChannelBySid(channel.sid);159}160161function joinChannel(_channel) {162return _channel.join()163.then(function(joinedChannel) {164console.log('Joined channel ' + joinedChannel.friendlyName);165updateChannelUI(_channel);166tc.currentChannel = _channel;167tc.loadMessages();168return joinedChannel;169});170}171172function initChannelEvents() {173console.log(tc.currentChannel.friendlyName + ' ready.');174tc.currentChannel.on('messageAdded', tc.addMessageToList);175tc.currentChannel.on('typingStarted', showTypingStarted);176tc.currentChannel.on('typingEnded', hideTypingStarted);177tc.currentChannel.on('memberJoined', notifyMemberJoined);178tc.currentChannel.on('memberLeft', notifyMemberLeft);179$inputText.prop('disabled', false).focus();180}181182function setupChannel(channel) {183return leaveCurrentChannel()184.then(function() {185return initChannel(channel);186})187.then(function(_channel) {188return joinChannel(_channel);189})190.then(initChannelEvents);191}192193tc.loadMessages = function() {194tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT).then(function (messages) {195messages.items.forEach(tc.addMessageToList);196});197};198199function leaveCurrentChannel() {200if (tc.currentChannel) {201return tc.currentChannel.leave().then(function(leftChannel) {202console.log('left ' + leftChannel.friendlyName);203leftChannel.removeListener('messageAdded', tc.addMessageToList);204leftChannel.removeListener('typingStarted', showTypingStarted);205leftChannel.removeListener('typingEnded', hideTypingStarted);206leftChannel.removeListener('memberJoined', notifyMemberJoined);207leftChannel.removeListener('memberLeft', notifyMemberLeft);208});209} else {210return Promise.resolve();211}212}213214tc.addMessageToList = function(message) {215var rowDiv = $('<div>').addClass('row no-margin');216rowDiv.loadTemplate($('#message-template'), {217username: message.author,218date: dateFormatter.getTodayDate(message.dateCreated),219body: message.body220});221if (message.author === tc.username) {222rowDiv.addClass('own-message');223}224225tc.$messageList.append(rowDiv);226scrollToMessageListBottom();227};228229function notifyMemberJoined(member) {230notify(member.identity + ' joined the channel')231}232233function notifyMemberLeft(member) {234notify(member.identity + ' left the channel');235}236237function notify(message) {238var row = $('<div>').addClass('col-md-12');239row.loadTemplate('#member-notification-template', {240status: message241});242tc.$messageList.append(row);243scrollToMessageListBottom();244}245246function showTypingStarted(member) {247$typingPlaceholder.text(member.identity + ' is typing...');248}249250function hideTypingStarted(member) {251$typingPlaceholder.text('');252}253254function scrollToMessageListBottom() {255tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);256}257258function updateChannelUI(selectedChannel) {259var channelElements = $('.channel-element').toArray();260var channelElement = channelElements.filter(function(element) {261return $(element).data().sid === selectedChannel.sid;262});263channelElement = $(channelElement);264if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {265tc.currentChannelContainer = channelElement;266}267tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');268channelElement.removeClass('unselected-channel').addClass('selected-channel');269tc.currentChannelContainer = channelElement;270}271272function showAddChannelInput() {273if (tc.messagingClient) {274$newChannelInputRow.addClass('showing').removeClass('not-showing');275$channelList.addClass('showing').removeClass('not-showing');276$newChannelInput.focus();277}278}279280function hideAddChannelInput() {281$newChannelInputRow.addClass('not-showing').removeClass('showing');282$channelList.addClass('not-showing').removeClass('showing');283$newChannelInput.val('');284}285286function addChannel(channel) {287if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {288tc.generalChannel = channel;289}290var rowDiv = $('<div>').addClass('row channel-row');291rowDiv.loadTemplate('#channel-template', {292channelName: channel.friendlyName293});294295var channelP = rowDiv.children().children().first();296297rowDiv.on('click', selectChannel);298channelP.data('sid', channel.sid);299if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {300tc.currentChannelContainer = channelP;301channelP.addClass('selected-channel');302}303else {304channelP.addClass('unselected-channel')305}306307$channelList.append(rowDiv);308}309310function deleteCurrentChannel() {311if (!tc.currentChannel) {312return;313}314if (tc.currentChannel.sid === tc.generalChannel.sid) {315alert('You cannot delete the general channel');316return;317}318tc.currentChannel.delete().then(function(channel) {319console.log('channel: '+ channel.friendlyName + ' deleted');320setupChannel(tc.generalChannel);321});322}323324function selectChannel(event) {325var target = $(event.target);326var channelSid = target.data().sid;327var selectedChannel = tc.channelArray.filter(function(channel) {328return channel.sid === channelSid;329})[0];330if (selectedChannel === tc.currentChannel) {331return;332}333setupChannel(selectedChannel);334};335336function disconnectClient() {337leaveCurrentChannel();338$channelList.text('');339tc.$messageList.text('');340channels = undefined;341$statusRow.addClass('disconnected').removeClass('connected');342tc.$messageList.addClass('disconnected').removeClass('connected');343$connectPanel.addClass('disconnected').removeClass('connected');344$inputText.removeClass('with-shadow');345$typingRow.addClass('disconnected').removeClass('connected');346}347348tc.sortChannelsByName = function(channels) {349return channels.sort(function(a, b) {350if (a.friendlyName === GENERAL_CHANNEL_NAME) {351return -1;352}353if (b.friendlyName === GENERAL_CHANNEL_NAME) {354return 1;355}356return a.friendlyName.localeCompare(b.friendlyName);357});358};359360return tc;361})();362
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 since we don't want to create a new general channel every time we start the application.
public/js/twiliochat.js
1var twiliochat = (function() {2var tc = {};34var GENERAL_CHANNEL_UNIQUE_NAME = 'general';5var GENERAL_CHANNEL_NAME = 'General Channel';6var MESSAGES_HISTORY_LIMIT = 50;78var $channelList;9var $inputText;10var $usernameInput;11var $statusRow;12var $connectPanel;13var $newChannelInputRow;14var $newChannelInput;15var $typingRow;16var $typingPlaceholder;1718$(document).ready(function() {19tc.$messageList = $('#message-list');20$channelList = $('#channel-list');21$inputText = $('#input-text');22$usernameInput = $('#username-input');23$statusRow = $('#status-row');24$connectPanel = $('#connect-panel');25$newChannelInputRow = $('#new-channel-input-row');26$newChannelInput = $('#new-channel-input');27$typingRow = $('#typing-row');28$typingPlaceholder = $('#typing-placeholder');29$usernameInput.focus();30$usernameInput.on('keypress', handleUsernameInputKeypress);31$inputText.on('keypress', handleInputTextKeypress);32$newChannelInput.on('keypress', tc.handleNewChannelInputKeypress);33$('#connect-image').on('click', connectClientWithUsername);34$('#add-channel-image').on('click', showAddChannelInput);35$('#leave-span').on('click', disconnectClient);36$('#delete-channel-span').on('click', deleteCurrentChannel);37});3839function handleUsernameInputKeypress(event) {40if (event.keyCode === 13){41connectClientWithUsername();42}43}4445function handleInputTextKeypress(event) {46if (event.keyCode === 13) {47tc.currentChannel.sendMessage($(this).val());48event.preventDefault();49$(this).val('');50}51else {52notifyTyping();53}54}5556var notifyTyping = $.throttle(function() {57tc.currentChannel.typing();58}, 1000);5960tc.handleNewChannelInputKeypress = function(event) {61if (event.keyCode === 13) {62tc.messagingClient.createChannel({63friendlyName: $newChannelInput.val()64}).then(hideAddChannelInput);65$(this).val('');66event.preventDefault();67}68};6970function connectClientWithUsername() {71var usernameText = $usernameInput.val();72$usernameInput.val('');73if (usernameText == '') {74alert('Username cannot be empty');75return;76}77tc.username = usernameText;78fetchAccessToken(tc.username, connectMessagingClient);79}8081function fetchAccessToken(username, handler) {82$.post('/token', {identity: username}, null, 'json')83.done(function(response) {84handler(response.token);85})86.fail(function(error) {87console.log('Failed to fetch the Access Token with error: ' + error);88});89}9091function connectMessagingClient(token) {92// Initialize the Chat messaging client93tc.accessManager = new Twilio.AccessManager(token);94Twilio.Chat.Client.create(token).then(function(client) {95tc.messagingClient = client;96updateConnectedUI();97tc.loadChannelList(tc.joinGeneralChannel);98tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));99tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));100tc.messagingClient.on('tokenExpired', refreshToken);101});102}103104function refreshToken() {105fetchAccessToken(tc.username, setNewToken);106}107108function setNewToken(tokenResponse) {109tc.accessManager.updateToken(tokenResponse.token);110}111112function updateConnectedUI() {113$('#username-span').text(tc.username);114$statusRow.addClass('connected').removeClass('disconnected');115tc.$messageList.addClass('connected').removeClass('disconnected');116$connectPanel.addClass('connected').removeClass('disconnected');117$inputText.addClass('with-shadow');118$typingRow.addClass('connected').removeClass('disconnected');119}120121tc.loadChannelList = function(handler) {122if (tc.messagingClient === undefined) {123console.log('Client is not initialized');124return;125}126127tc.messagingClient.getPublicChannelDescriptors().then(function(channels) {128tc.channelArray = tc.sortChannelsByName(channels.items);129$channelList.text('');130tc.channelArray.forEach(addChannel);131if (typeof handler === 'function') {132handler();133}134});135};136137tc.joinGeneralChannel = function() {138console.log('Attempting to join "general" chat channel...');139if (!tc.generalChannel) {140// If it doesn't exist, let's create it141tc.messagingClient.createChannel({142uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,143friendlyName: GENERAL_CHANNEL_NAME144}).then(function(channel) {145console.log('Created general channel');146tc.generalChannel = channel;147tc.loadChannelList(tc.joinGeneralChannel);148});149}150else {151console.log('Found general channel:');152setupChannel(tc.generalChannel);153}154};155156function initChannel(channel) {157console.log('Initialized channel ' + channel.friendlyName);158return tc.messagingClient.getChannelBySid(channel.sid);159}160161function joinChannel(_channel) {162return _channel.join()163.then(function(joinedChannel) {164console.log('Joined channel ' + joinedChannel.friendlyName);165updateChannelUI(_channel);166tc.currentChannel = _channel;167tc.loadMessages();168return joinedChannel;169});170}171172function initChannelEvents() {173console.log(tc.currentChannel.friendlyName + ' ready.');174tc.currentChannel.on('messageAdded', tc.addMessageToList);175tc.currentChannel.on('typingStarted', showTypingStarted);176tc.currentChannel.on('typingEnded', hideTypingStarted);177tc.currentChannel.on('memberJoined', notifyMemberJoined);178tc.currentChannel.on('memberLeft', notifyMemberLeft);179$inputText.prop('disabled', false).focus();180}181182function setupChannel(channel) {183return leaveCurrentChannel()184.then(function() {185return initChannel(channel);186})187.then(function(_channel) {188return joinChannel(_channel);189})190.then(initChannelEvents);191}192193tc.loadMessages = function() {194tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT).then(function (messages) {195messages.items.forEach(tc.addMessageToList);196});197};198199function leaveCurrentChannel() {200if (tc.currentChannel) {201return tc.currentChannel.leave().then(function(leftChannel) {202console.log('left ' + leftChannel.friendlyName);203leftChannel.removeListener('messageAdded', tc.addMessageToList);204leftChannel.removeListener('typingStarted', showTypingStarted);205leftChannel.removeListener('typingEnded', hideTypingStarted);206leftChannel.removeListener('memberJoined', notifyMemberJoined);207leftChannel.removeListener('memberLeft', notifyMemberLeft);208});209} else {210return Promise.resolve();211}212}213214tc.addMessageToList = function(message) {215var rowDiv = $('<div>').addClass('row no-margin');216rowDiv.loadTemplate($('#message-template'), {217username: message.author,218date: dateFormatter.getTodayDate(message.dateCreated),219body: message.body220});221if (message.author === tc.username) {222rowDiv.addClass('own-message');223}224225tc.$messageList.append(rowDiv);226scrollToMessageListBottom();227};228229function notifyMemberJoined(member) {230notify(member.identity + ' joined the channel')231}232233function notifyMemberLeft(member) {234notify(member.identity + ' left the channel');235}236237function notify(message) {238var row = $('<div>').addClass('col-md-12');239row.loadTemplate('#member-notification-template', {240status: message241});242tc.$messageList.append(row);243scrollToMessageListBottom();244}245246function showTypingStarted(member) {247$typingPlaceholder.text(member.identity + ' is typing...');248}249250function hideTypingStarted(member) {251$typingPlaceholder.text('');252}253254function scrollToMessageListBottom() {255tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);256}257258function updateChannelUI(selectedChannel) {259var channelElements = $('.channel-element').toArray();260var channelElement = channelElements.filter(function(element) {261return $(element).data().sid === selectedChannel.sid;262});263channelElement = $(channelElement);264if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {265tc.currentChannelContainer = channelElement;266}267tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');268channelElement.removeClass('unselected-channel').addClass('selected-channel');269tc.currentChannelContainer = channelElement;270}271272function showAddChannelInput() {273if (tc.messagingClient) {274$newChannelInputRow.addClass('showing').removeClass('not-showing');275$channelList.addClass('showing').removeClass('not-showing');276$newChannelInput.focus();277}278}279280function hideAddChannelInput() {281$newChannelInputRow.addClass('not-showing').removeClass('showing');282$channelList.addClass('not-showing').removeClass('showing');283$newChannelInput.val('');284}285286function addChannel(channel) {287if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {288tc.generalChannel = channel;289}290var rowDiv = $('<div>').addClass('row channel-row');291rowDiv.loadTemplate('#channel-template', {292channelName: channel.friendlyName293});294295var channelP = rowDiv.children().children().first();296297rowDiv.on('click', selectChannel);298channelP.data('sid', channel.sid);299if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {300tc.currentChannelContainer = channelP;301channelP.addClass('selected-channel');302}303else {304channelP.addClass('unselected-channel')305}306307$channelList.append(rowDiv);308}309310function deleteCurrentChannel() {311if (!tc.currentChannel) {312return;313}314if (tc.currentChannel.sid === tc.generalChannel.sid) {315alert('You cannot delete the general channel');316return;317}318tc.currentChannel.delete().then(function(channel) {319console.log('channel: '+ channel.friendlyName + ' deleted');320setupChannel(tc.generalChannel);321});322}323324function selectChannel(event) {325var target = $(event.target);326var channelSid = target.data().sid;327var selectedChannel = tc.channelArray.filter(function(channel) {328return channel.sid === channelSid;329})[0];330if (selectedChannel === tc.currentChannel) {331return;332}333setupChannel(selectedChannel);334};335336function disconnectClient() {337leaveCurrentChannel();338$channelList.text('');339tc.$messageList.text('');340channels = undefined;341$statusRow.addClass('disconnected').removeClass('connected');342tc.$messageList.addClass('disconnected').removeClass('connected');343$connectPanel.addClass('disconnected').removeClass('connected');344$inputText.removeClass('with-shadow');345$typingRow.addClass('disconnected').removeClass('connected');346}347348tc.sortChannelsByName = function(channels) {349return channels.sort(function(a, b) {350if (a.friendlyName === GENERAL_CHANNEL_NAME) {351return -1;352}353if (b.friendlyName === GENERAL_CHANNEL_NAME) {354return 1;355}356return a.friendlyName.localeCompare(b.friendlyName);357});358};359360return tc;361})();362
Now let's listen for some channel events.
Next, we listen for channel events. In our case we're setting listeners to the following events:
messageAdded
: When another member sends a message to the channel you are connected to.typingStarted
: When another member is typing a message on the channel that you are connected to.typingEnded
: When another member stops typing a message on the channel that you are connected to.memberJoined
: When another member joins the channel that you are connected to.memberLeft
: When another member leaves the channel that you are connected to.We register a different function to handle each particular event.
public/js/twiliochat.js
1var twiliochat = (function() {2var tc = {};34var GENERAL_CHANNEL_UNIQUE_NAME = 'general';5var GENERAL_CHANNEL_NAME = 'General Channel';6var MESSAGES_HISTORY_LIMIT = 50;78var $channelList;9var $inputText;10var $usernameInput;11var $statusRow;12var $connectPanel;13var $newChannelInputRow;14var $newChannelInput;15var $typingRow;16var $typingPlaceholder;1718$(document).ready(function() {19tc.$messageList = $('#message-list');20$channelList = $('#channel-list');21$inputText = $('#input-text');22$usernameInput = $('#username-input');23$statusRow = $('#status-row');24$connectPanel = $('#connect-panel');25$newChannelInputRow = $('#new-channel-input-row');26$newChannelInput = $('#new-channel-input');27$typingRow = $('#typing-row');28$typingPlaceholder = $('#typing-placeholder');29$usernameInput.focus();30$usernameInput.on('keypress', handleUsernameInputKeypress);31$inputText.on('keypress', handleInputTextKeypress);32$newChannelInput.on('keypress', tc.handleNewChannelInputKeypress);33$('#connect-image').on('click', connectClientWithUsername);34$('#add-channel-image').on('click', showAddChannelInput);35$('#leave-span').on('click', disconnectClient);36$('#delete-channel-span').on('click', deleteCurrentChannel);37});3839function handleUsernameInputKeypress(event) {40if (event.keyCode === 13){41connectClientWithUsername();42}43}4445function handleInputTextKeypress(event) {46if (event.keyCode === 13) {47tc.currentChannel.sendMessage($(this).val());48event.preventDefault();49$(this).val('');50}51else {52notifyTyping();53}54}5556var notifyTyping = $.throttle(function() {57tc.currentChannel.typing();58}, 1000);5960tc.handleNewChannelInputKeypress = function(event) {61if (event.keyCode === 13) {62tc.messagingClient.createChannel({63friendlyName: $newChannelInput.val()64}).then(hideAddChannelInput);65$(this).val('');66event.preventDefault();67}68};6970function connectClientWithUsername() {71var usernameText = $usernameInput.val();72$usernameInput.val('');73if (usernameText == '') {74alert('Username cannot be empty');75return;76}77tc.username = usernameText;78fetchAccessToken(tc.username, connectMessagingClient);79}8081function fetchAccessToken(username, handler) {82$.post('/token', {identity: username}, null, 'json')83.done(function(response) {84handler(response.token);85})86.fail(function(error) {87console.log('Failed to fetch the Access Token with error: ' + error);88});89}9091function connectMessagingClient(token) {92// Initialize the Chat messaging client93tc.accessManager = new Twilio.AccessManager(token);94Twilio.Chat.Client.create(token).then(function(client) {95tc.messagingClient = client;96updateConnectedUI();97tc.loadChannelList(tc.joinGeneralChannel);98tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));99tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));100tc.messagingClient.on('tokenExpired', refreshToken);101});102}103104function refreshToken() {105fetchAccessToken(tc.username, setNewToken);106}107108function setNewToken(tokenResponse) {109tc.accessManager.updateToken(tokenResponse.token);110}111112function updateConnectedUI() {113$('#username-span').text(tc.username);114$statusRow.addClass('connected').removeClass('disconnected');115tc.$messageList.addClass('connected').removeClass('disconnected');116$connectPanel.addClass('connected').removeClass('disconnected');117$inputText.addClass('with-shadow');118$typingRow.addClass('connected').removeClass('disconnected');119}120121tc.loadChannelList = function(handler) {122if (tc.messagingClient === undefined) {123console.log('Client is not initialized');124return;125}126127tc.messagingClient.getPublicChannelDescriptors().then(function(channels) {128tc.channelArray = tc.sortChannelsByName(channels.items);129$channelList.text('');130tc.channelArray.forEach(addChannel);131if (typeof handler === 'function') {132handler();133}134});135};136137tc.joinGeneralChannel = function() {138console.log('Attempting to join "general" chat channel...');139if (!tc.generalChannel) {140// If it doesn't exist, let's create it141tc.messagingClient.createChannel({142uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,143friendlyName: GENERAL_CHANNEL_NAME144}).then(function(channel) {145console.log('Created general channel');146tc.generalChannel = channel;147tc.loadChannelList(tc.joinGeneralChannel);148});149}150else {151console.log('Found general channel:');152setupChannel(tc.generalChannel);153}154};155156function initChannel(channel) {157console.log('Initialized channel ' + channel.friendlyName);158return tc.messagingClient.getChannelBySid(channel.sid);159}160161function joinChannel(_channel) {162return _channel.join()163.then(function(joinedChannel) {164console.log('Joined channel ' + joinedChannel.friendlyName);165updateChannelUI(_channel);166tc.currentChannel = _channel;167tc.loadMessages();168return joinedChannel;169});170}171172function initChannelEvents() {173console.log(tc.currentChannel.friendlyName + ' ready.');174tc.currentChannel.on('messageAdded', tc.addMessageToList);175tc.currentChannel.on('typingStarted', showTypingStarted);176tc.currentChannel.on('typingEnded', hideTypingStarted);177tc.currentChannel.on('memberJoined', notifyMemberJoined);178tc.currentChannel.on('memberLeft', notifyMemberLeft);179$inputText.prop('disabled', false).focus();180}181182function setupChannel(channel) {183return leaveCurrentChannel()184.then(function() {185return initChannel(channel);186})187.then(function(_channel) {188return joinChannel(_channel);189})190.then(initChannelEvents);191}192193tc.loadMessages = function() {194tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT).then(function (messages) {195messages.items.forEach(tc.addMessageToList);196});197};198199function leaveCurrentChannel() {200if (tc.currentChannel) {201return tc.currentChannel.leave().then(function(leftChannel) {202console.log('left ' + leftChannel.friendlyName);203leftChannel.removeListener('messageAdded', tc.addMessageToList);204leftChannel.removeListener('typingStarted', showTypingStarted);205leftChannel.removeListener('typingEnded', hideTypingStarted);206leftChannel.removeListener('memberJoined', notifyMemberJoined);207leftChannel.removeListener('memberLeft', notifyMemberLeft);208});209} else {210return Promise.resolve();211}212}213214tc.addMessageToList = function(message) {215var rowDiv = $('<div>').addClass('row no-margin');216rowDiv.loadTemplate($('#message-template'), {217username: message.author,218date: dateFormatter.getTodayDate(message.dateCreated),219body: message.body220});221if (message.author === tc.username) {222rowDiv.addClass('own-message');223}224225tc.$messageList.append(rowDiv);226scrollToMessageListBottom();227};228229function notifyMemberJoined(member) {230notify(member.identity + ' joined the channel')231}232233function notifyMemberLeft(member) {234notify(member.identity + ' left the channel');235}236237function notify(message) {238var row = $('<div>').addClass('col-md-12');239row.loadTemplate('#member-notification-template', {240status: message241});242tc.$messageList.append(row);243scrollToMessageListBottom();244}245246function showTypingStarted(member) {247$typingPlaceholder.text(member.identity + ' is typing...');248}249250function hideTypingStarted(member) {251$typingPlaceholder.text('');252}253254function scrollToMessageListBottom() {255tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);256}257258function updateChannelUI(selectedChannel) {259var channelElements = $('.channel-element').toArray();260var channelElement = channelElements.filter(function(element) {261return $(element).data().sid === selectedChannel.sid;262});263channelElement = $(channelElement);264if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {265tc.currentChannelContainer = channelElement;266}267tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');268channelElement.removeClass('unselected-channel').addClass('selected-channel');269tc.currentChannelContainer = channelElement;270}271272function showAddChannelInput() {273if (tc.messagingClient) {274$newChannelInputRow.addClass('showing').removeClass('not-showing');275$channelList.addClass('showing').removeClass('not-showing');276$newChannelInput.focus();277}278}279280function hideAddChannelInput() {281$newChannelInputRow.addClass('not-showing').removeClass('showing');282$channelList.addClass('not-showing').removeClass('showing');283$newChannelInput.val('');284}285286function addChannel(channel) {287if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {288tc.generalChannel = channel;289}290var rowDiv = $('<div>').addClass('row channel-row');291rowDiv.loadTemplate('#channel-template', {292channelName: channel.friendlyName293});294295var channelP = rowDiv.children().children().first();296297rowDiv.on('click', selectChannel);298channelP.data('sid', channel.sid);299if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {300tc.currentChannelContainer = channelP;301channelP.addClass('selected-channel');302}303else {304channelP.addClass('unselected-channel')305}306307$channelList.append(rowDiv);308}309310function deleteCurrentChannel() {311if (!tc.currentChannel) {312return;313}314if (tc.currentChannel.sid === tc.generalChannel.sid) {315alert('You cannot delete the general channel');316return;317}318tc.currentChannel.delete().then(function(channel) {319console.log('channel: '+ channel.friendlyName + ' deleted');320setupChannel(tc.generalChannel);321});322}323324function selectChannel(event) {325var target = $(event.target);326var channelSid = target.data().sid;327var selectedChannel = tc.channelArray.filter(function(channel) {328return channel.sid === channelSid;329})[0];330if (selectedChannel === tc.currentChannel) {331return;332}333setupChannel(selectedChannel);334};335336function disconnectClient() {337leaveCurrentChannel();338$channelList.text('');339tc.$messageList.text('');340channels = undefined;341$statusRow.addClass('disconnected').removeClass('connected');342tc.$messageList.addClass('disconnected').removeClass('connected');343$connectPanel.addClass('disconnected').removeClass('connected');344$inputText.removeClass('with-shadow');345$typingRow.addClass('disconnected').removeClass('connected');346}347348tc.sortChannelsByName = function(channels) {349return channels.sort(function(a, b) {350if (a.friendlyName === GENERAL_CHANNEL_NAME) {351return -1;352}353if (b.friendlyName === GENERAL_CHANNEL_NAME) {354return 1;355}356return a.friendlyName.localeCompare(b.friendlyName);357});358};359360return tc;361})();362
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.public/js/twiliochat.js
1var twiliochat = (function() {2var tc = {};34var GENERAL_CHANNEL_UNIQUE_NAME = 'general';5var GENERAL_CHANNEL_NAME = 'General Channel';6var MESSAGES_HISTORY_LIMIT = 50;78var $channelList;9var $inputText;10var $usernameInput;11var $statusRow;12var $connectPanel;13var $newChannelInputRow;14var $newChannelInput;15var $typingRow;16var $typingPlaceholder;1718$(document).ready(function() {19tc.$messageList = $('#message-list');20$channelList = $('#channel-list');21$inputText = $('#input-text');22$usernameInput = $('#username-input');23$statusRow = $('#status-row');24$connectPanel = $('#connect-panel');25$newChannelInputRow = $('#new-channel-input-row');26$newChannelInput = $('#new-channel-input');27$typingRow = $('#typing-row');28$typingPlaceholder = $('#typing-placeholder');29$usernameInput.focus();30$usernameInput.on('keypress', handleUsernameInputKeypress);31$inputText.on('keypress', handleInputTextKeypress);32$newChannelInput.on('keypress', tc.handleNewChannelInputKeypress);33$('#connect-image').on('click', connectClientWithUsername);34$('#add-channel-image').on('click', showAddChannelInput);35$('#leave-span').on('click', disconnectClient);36$('#delete-channel-span').on('click', deleteCurrentChannel);37});3839function handleUsernameInputKeypress(event) {40if (event.keyCode === 13){41connectClientWithUsername();42}43}4445function handleInputTextKeypress(event) {46if (event.keyCode === 13) {47tc.currentChannel.sendMessage($(this).val());48event.preventDefault();49$(this).val('');50}51else {52notifyTyping();53}54}5556var notifyTyping = $.throttle(function() {57tc.currentChannel.typing();58}, 1000);5960tc.handleNewChannelInputKeypress = function(event) {61if (event.keyCode === 13) {62tc.messagingClient.createChannel({63friendlyName: $newChannelInput.val()64}).then(hideAddChannelInput);65$(this).val('');66event.preventDefault();67}68};6970function connectClientWithUsername() {71var usernameText = $usernameInput.val();72$usernameInput.val('');73if (usernameText == '') {74alert('Username cannot be empty');75return;76}77tc.username = usernameText;78fetchAccessToken(tc.username, connectMessagingClient);79}8081function fetchAccessToken(username, handler) {82$.post('/token', {identity: username}, null, 'json')83.done(function(response) {84handler(response.token);85})86.fail(function(error) {87console.log('Failed to fetch the Access Token with error: ' + error);88});89}9091function connectMessagingClient(token) {92// Initialize the Chat messaging client93tc.accessManager = new Twilio.AccessManager(token);94Twilio.Chat.Client.create(token).then(function(client) {95tc.messagingClient = client;96updateConnectedUI();97tc.loadChannelList(tc.joinGeneralChannel);98tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));99tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));100tc.messagingClient.on('tokenExpired', refreshToken);101});102}103104function refreshToken() {105fetchAccessToken(tc.username, setNewToken);106}107108function setNewToken(tokenResponse) {109tc.accessManager.updateToken(tokenResponse.token);110}111112function updateConnectedUI() {113$('#username-span').text(tc.username);114$statusRow.addClass('connected').removeClass('disconnected');115tc.$messageList.addClass('connected').removeClass('disconnected');116$connectPanel.addClass('connected').removeClass('disconnected');117$inputText.addClass('with-shadow');118$typingRow.addClass('connected').removeClass('disconnected');119}120121tc.loadChannelList = function(handler) {122if (tc.messagingClient === undefined) {123console.log('Client is not initialized');124return;125}126127tc.messagingClient.getPublicChannelDescriptors().then(function(channels) {128tc.channelArray = tc.sortChannelsByName(channels.items);129$channelList.text('');130tc.channelArray.forEach(addChannel);131if (typeof handler === 'function') {132handler();133}134});135};136137tc.joinGeneralChannel = function() {138console.log('Attempting to join "general" chat channel...');139if (!tc.generalChannel) {140// If it doesn't exist, let's create it141tc.messagingClient.createChannel({142uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,143friendlyName: GENERAL_CHANNEL_NAME144}).then(function(channel) {145console.log('Created general channel');146tc.generalChannel = channel;147tc.loadChannelList(tc.joinGeneralChannel);148});149}150else {151console.log('Found general channel:');152setupChannel(tc.generalChannel);153}154};155156function initChannel(channel) {157console.log('Initialized channel ' + channel.friendlyName);158return tc.messagingClient.getChannelBySid(channel.sid);159}160161function joinChannel(_channel) {162return _channel.join()163.then(function(joinedChannel) {164console.log('Joined channel ' + joinedChannel.friendlyName);165updateChannelUI(_channel);166tc.currentChannel = _channel;167tc.loadMessages();168return joinedChannel;169});170}171172function initChannelEvents() {173console.log(tc.currentChannel.friendlyName + ' ready.');174tc.currentChannel.on('messageAdded', tc.addMessageToList);175tc.currentChannel.on('typingStarted', showTypingStarted);176tc.currentChannel.on('typingEnded', hideTypingStarted);177tc.currentChannel.on('memberJoined', notifyMemberJoined);178tc.currentChannel.on('memberLeft', notifyMemberLeft);179$inputText.prop('disabled', false).focus();180}181182function setupChannel(channel) {183return leaveCurrentChannel()184.then(function() {185return initChannel(channel);186})187.then(function(_channel) {188return joinChannel(_channel);189})190.then(initChannelEvents);191}192193tc.loadMessages = function() {194tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT).then(function (messages) {195messages.items.forEach(tc.addMessageToList);196});197};198199function leaveCurrentChannel() {200if (tc.currentChannel) {201return tc.currentChannel.leave().then(function(leftChannel) {202console.log('left ' + leftChannel.friendlyName);203leftChannel.removeListener('messageAdded', tc.addMessageToList);204leftChannel.removeListener('typingStarted', showTypingStarted);205leftChannel.removeListener('typingEnded', hideTypingStarted);206leftChannel.removeListener('memberJoined', notifyMemberJoined);207leftChannel.removeListener('memberLeft', notifyMemberLeft);208});209} else {210return Promise.resolve();211}212}213214tc.addMessageToList = function(message) {215var rowDiv = $('<div>').addClass('row no-margin');216rowDiv.loadTemplate($('#message-template'), {217username: message.author,218date: dateFormatter.getTodayDate(message.dateCreated),219body: message.body220});221if (message.author === tc.username) {222rowDiv.addClass('own-message');223}224225tc.$messageList.append(rowDiv);226scrollToMessageListBottom();227};228229function notifyMemberJoined(member) {230notify(member.identity + ' joined the channel')231}232233function notifyMemberLeft(member) {234notify(member.identity + ' left the channel');235}236237function notify(message) {238var row = $('<div>').addClass('col-md-12');239row.loadTemplate('#member-notification-template', {240status: message241});242tc.$messageList.append(row);243scrollToMessageListBottom();244}245246function showTypingStarted(member) {247$typingPlaceholder.text(member.identity + ' is typing...');248}249250function hideTypingStarted(member) {251$typingPlaceholder.text('');252}253254function scrollToMessageListBottom() {255tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);256}257258function updateChannelUI(selectedChannel) {259var channelElements = $('.channel-element').toArray();260var channelElement = channelElements.filter(function(element) {261return $(element).data().sid === selectedChannel.sid;262});263channelElement = $(channelElement);264if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {265tc.currentChannelContainer = channelElement;266}267tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');268channelElement.removeClass('unselected-channel').addClass('selected-channel');269tc.currentChannelContainer = channelElement;270}271272function showAddChannelInput() {273if (tc.messagingClient) {274$newChannelInputRow.addClass('showing').removeClass('not-showing');275$channelList.addClass('showing').removeClass('not-showing');276$newChannelInput.focus();277}278}279280function hideAddChannelInput() {281$newChannelInputRow.addClass('not-showing').removeClass('showing');282$channelList.addClass('not-showing').removeClass('showing');283$newChannelInput.val('');284}285286function addChannel(channel) {287if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {288tc.generalChannel = channel;289}290var rowDiv = $('<div>').addClass('row channel-row');291rowDiv.loadTemplate('#channel-template', {292channelName: channel.friendlyName293});294295var channelP = rowDiv.children().children().first();296297rowDiv.on('click', selectChannel);298channelP.data('sid', channel.sid);299if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {300tc.currentChannelContainer = channelP;301channelP.addClass('selected-channel');302}303else {304channelP.addClass('unselected-channel')305}306307$channelList.append(rowDiv);308}309310function deleteCurrentChannel() {311if (!tc.currentChannel) {312return;313}314if (tc.currentChannel.sid === tc.generalChannel.sid) {315alert('You cannot delete the general channel');316return;317}318tc.currentChannel.delete().then(function(channel) {319console.log('channel: '+ channel.friendlyName + ' deleted');320setupChannel(tc.generalChannel);321});322}323324function selectChannel(event) {325var target = $(event.target);326var channelSid = target.data().sid;327var selectedChannel = tc.channelArray.filter(function(channel) {328return channel.sid === channelSid;329})[0];330if (selectedChannel === tc.currentChannel) {331return;332}333setupChannel(selectedChannel);334};335336function disconnectClient() {337leaveCurrentChannel();338$channelList.text('');339tc.$messageList.text('');340channels = undefined;341$statusRow.addClass('disconnected').removeClass('connected');342tc.$messageList.addClass('disconnected').removeClass('connected');343$connectPanel.addClass('disconnected').removeClass('connected');344$inputText.removeClass('with-shadow');345$typingRow.addClass('disconnected').removeClass('connected');346}347348tc.sortChannelsByName = function(channels) {349return channels.sort(function(a, b) {350if (a.friendlyName === GENERAL_CHANNEL_NAME) {351return -1;352}353if (b.friendlyName === GENERAL_CHANNEL_NAME) {354return 1;355}356return a.friendlyName.localeCompare(b.friendlyName);357});358};359360return tc;361})();362
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