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. 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 Chat API keys.
We generate this token by creating a new AccessToken
and providing it with a ChatGrant
. With the AccessToken
at hand, we can use its method ToJWT()
to return its string representation.
src/main/java/com/twilio/chat/TwilioTokenCreator.java
1package com.twilio.chat;23import javax.inject.Inject;45import com.twilio.jwt.accesstoken.AccessToken;6import com.twilio.jwt.accesstoken.ChatGrant;78public class TwilioTokenCreator {910private final AppConfig appConfig;1112@Inject13public TwilioTokenCreator(AppConfig appConfig) {14this.appConfig = appConfig;15if (appConfig.isIncomplete()) {16throw new IncompleteConfigException(appConfig);17}18}1920String generateToken(String identity) {21ChatGrant grant = new ChatGrant();22grant.setServiceSid(appConfig.getTwilioChatServiceSID());2324AccessToken token = new AccessToken.Builder(25appConfig.getTwilioAccountSID(),26appConfig.getTwilioAPIKey(),27appConfig.getTwilioAPISecret()28).identity(identity).grant(grant).build();2930return token.toJwt();31}32}
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 itself.
It uses tokenGenerator.Generate
method to get hold of a new token and return it in a JSON format to be used for our client.
src/main/java/com/twilio/chat/TokenServlet.java
1package com.twilio.chat;23import java.io.BufferedWriter;4import java.io.IOException;5import java.util.HashMap;6import java.util.Map;78import javax.inject.Inject;9import javax.servlet.http.HttpServlet;10import javax.servlet.http.HttpServletRequest;11import javax.servlet.http.HttpServletResponse;1213import com.google.gson.Gson;14import com.google.inject.Singleton;1516@Singleton17public class TokenServlet extends HttpServlet {1819private final TwilioTokenCreator tokenCreator;2021@Inject22public TokenServlet(TwilioTokenCreator tokenCreator) {23this.tokenCreator = tokenCreator;24}2526@Override27public void doPost(HttpServletRequest request, HttpServletResponse response) {28String identity = request.getParameter("identity");2930if (identity != null) {3132String generatedToken = tokenCreator.generateToken(identity);3334Map<String, String> json = new HashMap<>();35json.put("identity", identity);36json.put("token", generatedToken);37renderJson(response, json);38}3940}4142private void renderJson(HttpServletResponse response, Map<String, String> json) {43Gson gson = new Gson();44response.setContentType("application/json");45try (BufferedWriter responseWriter = new BufferedWriter(response.getWriter())) {46responseWriter.write(gson.toJson(json));47responseWriter.flush();48} catch (IOException e) {49e.printStackTrace();50}51}52}53
Now that we have a route that generates JWT tokens on demand, let's use this route to initialize our Twilio Chat Client.
On our client, we fetch a new Token using a POST
request to our endpoint.
With the token, we can create a new Twilio.AccessManager
, and initialize our Twilio.Chat.Client
.
src/main/webapp/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('/twiliochat-servlets/token', {identity: username}, null, 'json')83.done(function(response) {84console.log('Successfully finished fetch of the Access Token.');85handler(response.token);86})87.fail(function(error) {88console.log('Failed to fetch the Access Token with error: ' + error);89});90}9192function connectMessagingClient(token) {93// Initialize the Chat messaging client94tc.accessManager = new Twilio.AccessManager(token);95Twilio.Chat.Client.create(token).then(function(client) {96tc.messagingClient = client;97updateConnectedUI();98tc.loadChannelList(tc.joinGeneralChannel);99tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));100tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));101tc.messagingClient.on('tokenExpired', refreshToken);102});103}104105function refreshToken() {106fetchAccessToken(tc.username, setNewToken);107}108109function setNewToken(tokenResponse) {110tc.accessManager.updateToken(tokenResponse.token);111}112113function updateConnectedUI() {114$('#username-span').text(tc.username);115$statusRow.addClass('connected').removeClass('disconnected');116tc.$messageList.addClass('connected').removeClass('disconnected');117$connectPanel.addClass('connected').removeClass('disconnected');118$inputText.addClass('with-shadow');119$typingRow.addClass('connected').removeClass('disconnected');120}121122tc.loadChannelList = function(handler) {123if (tc.messagingClient === undefined) {124console.log('Client is not initialized');125return;126}127128tc.messagingClient.getPublicChannelDescriptors().then(function(channels) {129tc.channelArray = tc.sortChannelsByName(channels.items);130$channelList.text('');131tc.channelArray.forEach(addChannel);132if (typeof handler === 'function') {133handler();134}135});136};137138tc.joinGeneralChannel = function() {139console.log('Attempting to join "general" chat channel...');140if (!tc.generalChannel) {141// If it doesn't exist, let's create it142tc.messagingClient.createChannel({143uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,144friendlyName: GENERAL_CHANNEL_NAME145}).then(function(channel) {146console.log('Created general channel');147tc.generalChannel = channel;148tc.loadChannelList(tc.joinGeneralChannel);149});150}151else {152console.log('Found general channel:');153setupChannel(tc.generalChannel);154}155};156157function initChannel(channel) {158console.log('Initialized channel ' + channel.friendlyName);159return tc.messagingClient.getChannelBySid(channel.sid);160}161162function joinChannel(_channel) {163return _channel.join()164.then(function(joinedChannel) {165console.log('Joined channel ' + joinedChannel.friendlyName);166updateChannelUI(_channel);167tc.currentChannel = _channel;168tc.loadMessages();169return joinedChannel;170});171}172173function initChannelEvents() {174console.log(tc.currentChannel.friendlyName + ' ready.');175tc.currentChannel.on('messageAdded', tc.addMessageToList);176tc.currentChannel.on('typingStarted', showTypingStarted);177tc.currentChannel.on('typingEnded', hideTypingStarted);178tc.currentChannel.on('memberJoined', notifyMemberJoined);179tc.currentChannel.on('memberLeft', notifyMemberLeft);180$inputText.prop('disabled', false).focus();181}182183function setupChannel(channel) {184return leaveCurrentChannel()185.then(function() {186return initChannel(channel);187})188.then(function(_channel) {189return joinChannel(_channel);190})191.then(initChannelEvents);192}193194tc.loadMessages = function() {195tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT)196.then(function(messages) {197messages.items.forEach(tc.addMessageToList);198});199};200201function leaveCurrentChannel() {202if (tc.currentChannel) {203return tc.currentChannel.leave().then(function(leftChannel) {204console.log('left ' + leftChannel.friendlyName);205leftChannel.removeListener('messageAdded', tc.addMessageToList);206leftChannel.removeListener('typingStarted', showTypingStarted);207leftChannel.removeListener('typingEnded', hideTypingStarted);208leftChannel.removeListener('memberJoined', notifyMemberJoined);209leftChannel.removeListener('memberLeft', notifyMemberLeft);210});211} else {212return Promise.resolve();213}214}215216tc.addMessageToList = function(message) {217var rowDiv = $('<div>').addClass('row no-margin');218rowDiv.loadTemplate($('#message-template'), {219username: message.author,220date: dateFormatter.getTodayDate(message.dateCreated),221body: message.body222});223if (message.author === tc.username) {224rowDiv.addClass('own-message');225}226227tc.$messageList.append(rowDiv);228scrollToMessageListBottom();229};230231function notifyMemberJoined(member) {232notify(member.identity + ' joined the channel')233}234235function notifyMemberLeft(member) {236notify(member.identity + ' left the channel');237}238239function notify(message) {240var row = $('<div>').addClass('col-md-12');241row.loadTemplate('#member-notification-template', {242status: message243});244tc.$messageList.append(row);245scrollToMessageListBottom();246}247248function showTypingStarted(member) {249$typingPlaceholder.text(member.identity + ' is typing...');250}251252function hideTypingStarted(member) {253$typingPlaceholder.text('');254}255256function scrollToMessageListBottom() {257tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);258}259260function updateChannelUI(selectedChannel) {261var channelElements = $('.channel-element').toArray();262var channelElement = channelElements.filter(function(element) {263return $(element).data().sid === selectedChannel.sid;264});265channelElement = $(channelElement);266if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {267tc.currentChannelContainer = channelElement;268}269tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');270channelElement.removeClass('unselected-channel').addClass('selected-channel');271tc.currentChannelContainer = channelElement;272}273274function showAddChannelInput() {275if (tc.messagingClient) {276$newChannelInputRow.addClass('showing').removeClass('not-showing');277$channelList.addClass('showing').removeClass('not-showing');278$newChannelInput.focus();279}280}281282function hideAddChannelInput() {283$newChannelInputRow.addClass('not-showing').removeClass('showing');284$channelList.addClass('not-showing').removeClass('showing');285$newChannelInput.val('');286}287288function addChannel(channel) {289if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {290tc.generalChannel = channel;291}292var rowDiv = $('<div>').addClass('row channel-row');293rowDiv.loadTemplate('#channel-template', {294channelName: channel.friendlyName295});296297var channelP = rowDiv.children().children().first();298299rowDiv.on('click', selectChannel);300channelP.data('sid', channel.sid);301if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {302tc.currentChannelContainer = channelP;303channelP.addClass('selected-channel');304}305else {306channelP.addClass('unselected-channel')307}308309$channelList.append(rowDiv);310}311312function deleteCurrentChannel() {313if (!tc.currentChannel) {314return;315}316if (tc.currentChannel.sid === tc.generalChannel.sid) {317alert('You cannot delete the general channel');318return;319}320tc.currentChannel.delete().then(function(channel) {321console.log('channel: '+ channel.friendlyName + ' deleted');322setupChannel(tc.generalChannel);323});324}325326function selectChannel(event) {327var target = $(event.target);328var channelSid = target.data().sid;329var selectedChannel = tc.channelArray.filter(function(channel) {330return channel.sid === channelSid;331})[0];332if (selectedChannel === tc.currentChannel) {333return;334}335setupChannel(selectedChannel);336};337338function disconnectClient() {339leaveCurrentChannel();340$channelList.text('');341tc.$messageList.text('');342channels = undefined;343$statusRow.addClass('disconnected').removeClass('connected');344tc.$messageList.addClass('disconnected').removeClass('connected');345$connectPanel.addClass('disconnected').removeClass('connected');346$inputText.removeClass('with-shadow');347$typingRow.addClass('disconnected').removeClass('connected');348}349350tc.sortChannelsByName = function(channels) {351return channels.sort(function(a, b) {352if (a.friendlyName === GENERAL_CHANNEL_NAME) {353return -1;354}355if (b.friendlyName === GENERAL_CHANNEL_NAME) {356return 1;357}358return a.friendlyName.localeCompare(b.friendlyName);359});360};361362return tc;363})();364
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 its method getPublicChannelDescriptors
to retrieve all visible channels. The method returns a promise which we use to show the list of channels retrieved on the UI.
src/main/webapp/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('/twiliochat-servlets/token', {identity: username}, null, 'json')83.done(function(response) {84console.log('Successfully finished fetch of the Access Token.');85handler(response.token);86})87.fail(function(error) {88console.log('Failed to fetch the Access Token with error: ' + error);89});90}9192function connectMessagingClient(token) {93// Initialize the Chat messaging client94tc.accessManager = new Twilio.AccessManager(token);95Twilio.Chat.Client.create(token).then(function(client) {96tc.messagingClient = client;97updateConnectedUI();98tc.loadChannelList(tc.joinGeneralChannel);99tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));100tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));101tc.messagingClient.on('tokenExpired', refreshToken);102});103}104105function refreshToken() {106fetchAccessToken(tc.username, setNewToken);107}108109function setNewToken(tokenResponse) {110tc.accessManager.updateToken(tokenResponse.token);111}112113function updateConnectedUI() {114$('#username-span').text(tc.username);115$statusRow.addClass('connected').removeClass('disconnected');116tc.$messageList.addClass('connected').removeClass('disconnected');117$connectPanel.addClass('connected').removeClass('disconnected');118$inputText.addClass('with-shadow');119$typingRow.addClass('connected').removeClass('disconnected');120}121122tc.loadChannelList = function(handler) {123if (tc.messagingClient === undefined) {124console.log('Client is not initialized');125return;126}127128tc.messagingClient.getPublicChannelDescriptors().then(function(channels) {129tc.channelArray = tc.sortChannelsByName(channels.items);130$channelList.text('');131tc.channelArray.forEach(addChannel);132if (typeof handler === 'function') {133handler();134}135});136};137138tc.joinGeneralChannel = function() {139console.log('Attempting to join "general" chat channel...');140if (!tc.generalChannel) {141// If it doesn't exist, let's create it142tc.messagingClient.createChannel({143uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,144friendlyName: GENERAL_CHANNEL_NAME145}).then(function(channel) {146console.log('Created general channel');147tc.generalChannel = channel;148tc.loadChannelList(tc.joinGeneralChannel);149});150}151else {152console.log('Found general channel:');153setupChannel(tc.generalChannel);154}155};156157function initChannel(channel) {158console.log('Initialized channel ' + channel.friendlyName);159return tc.messagingClient.getChannelBySid(channel.sid);160}161162function joinChannel(_channel) {163return _channel.join()164.then(function(joinedChannel) {165console.log('Joined channel ' + joinedChannel.friendlyName);166updateChannelUI(_channel);167tc.currentChannel = _channel;168tc.loadMessages();169return joinedChannel;170});171}172173function initChannelEvents() {174console.log(tc.currentChannel.friendlyName + ' ready.');175tc.currentChannel.on('messageAdded', tc.addMessageToList);176tc.currentChannel.on('typingStarted', showTypingStarted);177tc.currentChannel.on('typingEnded', hideTypingStarted);178tc.currentChannel.on('memberJoined', notifyMemberJoined);179tc.currentChannel.on('memberLeft', notifyMemberLeft);180$inputText.prop('disabled', false).focus();181}182183function setupChannel(channel) {184return leaveCurrentChannel()185.then(function() {186return initChannel(channel);187})188.then(function(_channel) {189return joinChannel(_channel);190})191.then(initChannelEvents);192}193194tc.loadMessages = function() {195tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT)196.then(function(messages) {197messages.items.forEach(tc.addMessageToList);198});199};200201function leaveCurrentChannel() {202if (tc.currentChannel) {203return tc.currentChannel.leave().then(function(leftChannel) {204console.log('left ' + leftChannel.friendlyName);205leftChannel.removeListener('messageAdded', tc.addMessageToList);206leftChannel.removeListener('typingStarted', showTypingStarted);207leftChannel.removeListener('typingEnded', hideTypingStarted);208leftChannel.removeListener('memberJoined', notifyMemberJoined);209leftChannel.removeListener('memberLeft', notifyMemberLeft);210});211} else {212return Promise.resolve();213}214}215216tc.addMessageToList = function(message) {217var rowDiv = $('<div>').addClass('row no-margin');218rowDiv.loadTemplate($('#message-template'), {219username: message.author,220date: dateFormatter.getTodayDate(message.dateCreated),221body: message.body222});223if (message.author === tc.username) {224rowDiv.addClass('own-message');225}226227tc.$messageList.append(rowDiv);228scrollToMessageListBottom();229};230231function notifyMemberJoined(member) {232notify(member.identity + ' joined the channel')233}234235function notifyMemberLeft(member) {236notify(member.identity + ' left the channel');237}238239function notify(message) {240var row = $('<div>').addClass('col-md-12');241row.loadTemplate('#member-notification-template', {242status: message243});244tc.$messageList.append(row);245scrollToMessageListBottom();246}247248function showTypingStarted(member) {249$typingPlaceholder.text(member.identity + ' is typing...');250}251252function hideTypingStarted(member) {253$typingPlaceholder.text('');254}255256function scrollToMessageListBottom() {257tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);258}259260function updateChannelUI(selectedChannel) {261var channelElements = $('.channel-element').toArray();262var channelElement = channelElements.filter(function(element) {263return $(element).data().sid === selectedChannel.sid;264});265channelElement = $(channelElement);266if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {267tc.currentChannelContainer = channelElement;268}269tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');270channelElement.removeClass('unselected-channel').addClass('selected-channel');271tc.currentChannelContainer = channelElement;272}273274function showAddChannelInput() {275if (tc.messagingClient) {276$newChannelInputRow.addClass('showing').removeClass('not-showing');277$channelList.addClass('showing').removeClass('not-showing');278$newChannelInput.focus();279}280}281282function hideAddChannelInput() {283$newChannelInputRow.addClass('not-showing').removeClass('showing');284$channelList.addClass('not-showing').removeClass('showing');285$newChannelInput.val('');286}287288function addChannel(channel) {289if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {290tc.generalChannel = channel;291}292var rowDiv = $('<div>').addClass('row channel-row');293rowDiv.loadTemplate('#channel-template', {294channelName: channel.friendlyName295});296297var channelP = rowDiv.children().children().first();298299rowDiv.on('click', selectChannel);300channelP.data('sid', channel.sid);301if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {302tc.currentChannelContainer = channelP;303channelP.addClass('selected-channel');304}305else {306channelP.addClass('unselected-channel')307}308309$channelList.append(rowDiv);310}311312function deleteCurrentChannel() {313if (!tc.currentChannel) {314return;315}316if (tc.currentChannel.sid === tc.generalChannel.sid) {317alert('You cannot delete the general channel');318return;319}320tc.currentChannel.delete().then(function(channel) {321console.log('channel: '+ channel.friendlyName + ' deleted');322setupChannel(tc.generalChannel);323});324}325326function selectChannel(event) {327var target = $(event.target);328var channelSid = target.data().sid;329var selectedChannel = tc.channelArray.filter(function(channel) {330return channel.sid === channelSid;331})[0];332if (selectedChannel === tc.currentChannel) {333return;334}335setupChannel(selectedChannel);336};337338function disconnectClient() {339leaveCurrentChannel();340$channelList.text('');341tc.$messageList.text('');342channels = undefined;343$statusRow.addClass('disconnected').removeClass('connected');344tc.$messageList.addClass('disconnected').removeClass('connected');345$connectPanel.addClass('disconnected').removeClass('connected');346$inputText.removeClass('with-shadow');347$typingRow.addClass('disconnected').removeClass('connected');348}349350tc.sortChannelsByName = function(channels) {351return channels.sort(function(a, b) {352if (a.friendlyName === GENERAL_CHANNEL_NAME) {353return -1;354}355if (b.friendlyName === GENERAL_CHANNEL_NAME) {356return 1;357}358return a.friendlyName.localeCompare(b.friendlyName);359});360};361362return tc;363})();364
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 handles 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.
src/main/webapp/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('/twiliochat-servlets/token', {identity: username}, null, 'json')83.done(function(response) {84console.log('Successfully finished fetch of the Access Token.');85handler(response.token);86})87.fail(function(error) {88console.log('Failed to fetch the Access Token with error: ' + error);89});90}9192function connectMessagingClient(token) {93// Initialize the Chat messaging client94tc.accessManager = new Twilio.AccessManager(token);95Twilio.Chat.Client.create(token).then(function(client) {96tc.messagingClient = client;97updateConnectedUI();98tc.loadChannelList(tc.joinGeneralChannel);99tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));100tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));101tc.messagingClient.on('tokenExpired', refreshToken);102});103}104105function refreshToken() {106fetchAccessToken(tc.username, setNewToken);107}108109function setNewToken(tokenResponse) {110tc.accessManager.updateToken(tokenResponse.token);111}112113function updateConnectedUI() {114$('#username-span').text(tc.username);115$statusRow.addClass('connected').removeClass('disconnected');116tc.$messageList.addClass('connected').removeClass('disconnected');117$connectPanel.addClass('connected').removeClass('disconnected');118$inputText.addClass('with-shadow');119$typingRow.addClass('connected').removeClass('disconnected');120}121122tc.loadChannelList = function(handler) {123if (tc.messagingClient === undefined) {124console.log('Client is not initialized');125return;126}127128tc.messagingClient.getPublicChannelDescriptors().then(function(channels) {129tc.channelArray = tc.sortChannelsByName(channels.items);130$channelList.text('');131tc.channelArray.forEach(addChannel);132if (typeof handler === 'function') {133handler();134}135});136};137138tc.joinGeneralChannel = function() {139console.log('Attempting to join "general" chat channel...');140if (!tc.generalChannel) {141// If it doesn't exist, let's create it142tc.messagingClient.createChannel({143uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,144friendlyName: GENERAL_CHANNEL_NAME145}).then(function(channel) {146console.log('Created general channel');147tc.generalChannel = channel;148tc.loadChannelList(tc.joinGeneralChannel);149});150}151else {152console.log('Found general channel:');153setupChannel(tc.generalChannel);154}155};156157function initChannel(channel) {158console.log('Initialized channel ' + channel.friendlyName);159return tc.messagingClient.getChannelBySid(channel.sid);160}161162function joinChannel(_channel) {163return _channel.join()164.then(function(joinedChannel) {165console.log('Joined channel ' + joinedChannel.friendlyName);166updateChannelUI(_channel);167tc.currentChannel = _channel;168tc.loadMessages();169return joinedChannel;170});171}172173function initChannelEvents() {174console.log(tc.currentChannel.friendlyName + ' ready.');175tc.currentChannel.on('messageAdded', tc.addMessageToList);176tc.currentChannel.on('typingStarted', showTypingStarted);177tc.currentChannel.on('typingEnded', hideTypingStarted);178tc.currentChannel.on('memberJoined', notifyMemberJoined);179tc.currentChannel.on('memberLeft', notifyMemberLeft);180$inputText.prop('disabled', false).focus();181}182183function setupChannel(channel) {184return leaveCurrentChannel()185.then(function() {186return initChannel(channel);187})188.then(function(_channel) {189return joinChannel(_channel);190})191.then(initChannelEvents);192}193194tc.loadMessages = function() {195tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT)196.then(function(messages) {197messages.items.forEach(tc.addMessageToList);198});199};200201function leaveCurrentChannel() {202if (tc.currentChannel) {203return tc.currentChannel.leave().then(function(leftChannel) {204console.log('left ' + leftChannel.friendlyName);205leftChannel.removeListener('messageAdded', tc.addMessageToList);206leftChannel.removeListener('typingStarted', showTypingStarted);207leftChannel.removeListener('typingEnded', hideTypingStarted);208leftChannel.removeListener('memberJoined', notifyMemberJoined);209leftChannel.removeListener('memberLeft', notifyMemberLeft);210});211} else {212return Promise.resolve();213}214}215216tc.addMessageToList = function(message) {217var rowDiv = $('<div>').addClass('row no-margin');218rowDiv.loadTemplate($('#message-template'), {219username: message.author,220date: dateFormatter.getTodayDate(message.dateCreated),221body: message.body222});223if (message.author === tc.username) {224rowDiv.addClass('own-message');225}226227tc.$messageList.append(rowDiv);228scrollToMessageListBottom();229};230231function notifyMemberJoined(member) {232notify(member.identity + ' joined the channel')233}234235function notifyMemberLeft(member) {236notify(member.identity + ' left the channel');237}238239function notify(message) {240var row = $('<div>').addClass('col-md-12');241row.loadTemplate('#member-notification-template', {242status: message243});244tc.$messageList.append(row);245scrollToMessageListBottom();246}247248function showTypingStarted(member) {249$typingPlaceholder.text(member.identity + ' is typing...');250}251252function hideTypingStarted(member) {253$typingPlaceholder.text('');254}255256function scrollToMessageListBottom() {257tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);258}259260function updateChannelUI(selectedChannel) {261var channelElements = $('.channel-element').toArray();262var channelElement = channelElements.filter(function(element) {263return $(element).data().sid === selectedChannel.sid;264});265channelElement = $(channelElement);266if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {267tc.currentChannelContainer = channelElement;268}269tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');270channelElement.removeClass('unselected-channel').addClass('selected-channel');271tc.currentChannelContainer = channelElement;272}273274function showAddChannelInput() {275if (tc.messagingClient) {276$newChannelInputRow.addClass('showing').removeClass('not-showing');277$channelList.addClass('showing').removeClass('not-showing');278$newChannelInput.focus();279}280}281282function hideAddChannelInput() {283$newChannelInputRow.addClass('not-showing').removeClass('showing');284$channelList.addClass('not-showing').removeClass('showing');285$newChannelInput.val('');286}287288function addChannel(channel) {289if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {290tc.generalChannel = channel;291}292var rowDiv = $('<div>').addClass('row channel-row');293rowDiv.loadTemplate('#channel-template', {294channelName: channel.friendlyName295});296297var channelP = rowDiv.children().children().first();298299rowDiv.on('click', selectChannel);300channelP.data('sid', channel.sid);301if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {302tc.currentChannelContainer = channelP;303channelP.addClass('selected-channel');304}305else {306channelP.addClass('unselected-channel')307}308309$channelList.append(rowDiv);310}311312function deleteCurrentChannel() {313if (!tc.currentChannel) {314return;315}316if (tc.currentChannel.sid === tc.generalChannel.sid) {317alert('You cannot delete the general channel');318return;319}320tc.currentChannel.delete().then(function(channel) {321console.log('channel: '+ channel.friendlyName + ' deleted');322setupChannel(tc.generalChannel);323});324}325326function selectChannel(event) {327var target = $(event.target);328var channelSid = target.data().sid;329var selectedChannel = tc.channelArray.filter(function(channel) {330return channel.sid === channelSid;331})[0];332if (selectedChannel === tc.currentChannel) {333return;334}335setupChannel(selectedChannel);336};337338function disconnectClient() {339leaveCurrentChannel();340$channelList.text('');341tc.$messageList.text('');342channels = undefined;343$statusRow.addClass('disconnected').removeClass('connected');344tc.$messageList.addClass('disconnected').removeClass('connected');345$connectPanel.addClass('disconnected').removeClass('connected');346$inputText.removeClass('with-shadow');347$typingRow.addClass('disconnected').removeClass('connected');348}349350tc.sortChannelsByName = function(channels) {351return channels.sort(function(a, b) {352if (a.friendlyName === GENERAL_CHANNEL_NAME) {353return -1;354}355if (b.friendlyName === GENERAL_CHANNEL_NAME) {356return 1;357}358return a.friendlyName.localeCompare(b.friendlyName);359});360};361362return tc;363})();364
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.
src/main/webapp/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('/twiliochat-servlets/token', {identity: username}, null, 'json')83.done(function(response) {84console.log('Successfully finished fetch of the Access Token.');85handler(response.token);86})87.fail(function(error) {88console.log('Failed to fetch the Access Token with error: ' + error);89});90}9192function connectMessagingClient(token) {93// Initialize the Chat messaging client94tc.accessManager = new Twilio.AccessManager(token);95Twilio.Chat.Client.create(token).then(function(client) {96tc.messagingClient = client;97updateConnectedUI();98tc.loadChannelList(tc.joinGeneralChannel);99tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));100tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));101tc.messagingClient.on('tokenExpired', refreshToken);102});103}104105function refreshToken() {106fetchAccessToken(tc.username, setNewToken);107}108109function setNewToken(tokenResponse) {110tc.accessManager.updateToken(tokenResponse.token);111}112113function updateConnectedUI() {114$('#username-span').text(tc.username);115$statusRow.addClass('connected').removeClass('disconnected');116tc.$messageList.addClass('connected').removeClass('disconnected');117$connectPanel.addClass('connected').removeClass('disconnected');118$inputText.addClass('with-shadow');119$typingRow.addClass('connected').removeClass('disconnected');120}121122tc.loadChannelList = function(handler) {123if (tc.messagingClient === undefined) {124console.log('Client is not initialized');125return;126}127128tc.messagingClient.getPublicChannelDescriptors().then(function(channels) {129tc.channelArray = tc.sortChannelsByName(channels.items);130$channelList.text('');131tc.channelArray.forEach(addChannel);132if (typeof handler === 'function') {133handler();134}135});136};137138tc.joinGeneralChannel = function() {139console.log('Attempting to join "general" chat channel...');140if (!tc.generalChannel) {141// If it doesn't exist, let's create it142tc.messagingClient.createChannel({143uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,144friendlyName: GENERAL_CHANNEL_NAME145}).then(function(channel) {146console.log('Created general channel');147tc.generalChannel = channel;148tc.loadChannelList(tc.joinGeneralChannel);149});150}151else {152console.log('Found general channel:');153setupChannel(tc.generalChannel);154}155};156157function initChannel(channel) {158console.log('Initialized channel ' + channel.friendlyName);159return tc.messagingClient.getChannelBySid(channel.sid);160}161162function joinChannel(_channel) {163return _channel.join()164.then(function(joinedChannel) {165console.log('Joined channel ' + joinedChannel.friendlyName);166updateChannelUI(_channel);167tc.currentChannel = _channel;168tc.loadMessages();169return joinedChannel;170});171}172173function initChannelEvents() {174console.log(tc.currentChannel.friendlyName + ' ready.');175tc.currentChannel.on('messageAdded', tc.addMessageToList);176tc.currentChannel.on('typingStarted', showTypingStarted);177tc.currentChannel.on('typingEnded', hideTypingStarted);178tc.currentChannel.on('memberJoined', notifyMemberJoined);179tc.currentChannel.on('memberLeft', notifyMemberLeft);180$inputText.prop('disabled', false).focus();181}182183function setupChannel(channel) {184return leaveCurrentChannel()185.then(function() {186return initChannel(channel);187})188.then(function(_channel) {189return joinChannel(_channel);190})191.then(initChannelEvents);192}193194tc.loadMessages = function() {195tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT)196.then(function(messages) {197messages.items.forEach(tc.addMessageToList);198});199};200201function leaveCurrentChannel() {202if (tc.currentChannel) {203return tc.currentChannel.leave().then(function(leftChannel) {204console.log('left ' + leftChannel.friendlyName);205leftChannel.removeListener('messageAdded', tc.addMessageToList);206leftChannel.removeListener('typingStarted', showTypingStarted);207leftChannel.removeListener('typingEnded', hideTypingStarted);208leftChannel.removeListener('memberJoined', notifyMemberJoined);209leftChannel.removeListener('memberLeft', notifyMemberLeft);210});211} else {212return Promise.resolve();213}214}215216tc.addMessageToList = function(message) {217var rowDiv = $('<div>').addClass('row no-margin');218rowDiv.loadTemplate($('#message-template'), {219username: message.author,220date: dateFormatter.getTodayDate(message.dateCreated),221body: message.body222});223if (message.author === tc.username) {224rowDiv.addClass('own-message');225}226227tc.$messageList.append(rowDiv);228scrollToMessageListBottom();229};230231function notifyMemberJoined(member) {232notify(member.identity + ' joined the channel')233}234235function notifyMemberLeft(member) {236notify(member.identity + ' left the channel');237}238239function notify(message) {240var row = $('<div>').addClass('col-md-12');241row.loadTemplate('#member-notification-template', {242status: message243});244tc.$messageList.append(row);245scrollToMessageListBottom();246}247248function showTypingStarted(member) {249$typingPlaceholder.text(member.identity + ' is typing...');250}251252function hideTypingStarted(member) {253$typingPlaceholder.text('');254}255256function scrollToMessageListBottom() {257tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);258}259260function updateChannelUI(selectedChannel) {261var channelElements = $('.channel-element').toArray();262var channelElement = channelElements.filter(function(element) {263return $(element).data().sid === selectedChannel.sid;264});265channelElement = $(channelElement);266if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {267tc.currentChannelContainer = channelElement;268}269tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');270channelElement.removeClass('unselected-channel').addClass('selected-channel');271tc.currentChannelContainer = channelElement;272}273274function showAddChannelInput() {275if (tc.messagingClient) {276$newChannelInputRow.addClass('showing').removeClass('not-showing');277$channelList.addClass('showing').removeClass('not-showing');278$newChannelInput.focus();279}280}281282function hideAddChannelInput() {283$newChannelInputRow.addClass('not-showing').removeClass('showing');284$channelList.addClass('not-showing').removeClass('showing');285$newChannelInput.val('');286}287288function addChannel(channel) {289if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {290tc.generalChannel = channel;291}292var rowDiv = $('<div>').addClass('row channel-row');293rowDiv.loadTemplate('#channel-template', {294channelName: channel.friendlyName295});296297var channelP = rowDiv.children().children().first();298299rowDiv.on('click', selectChannel);300channelP.data('sid', channel.sid);301if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {302tc.currentChannelContainer = channelP;303channelP.addClass('selected-channel');304}305else {306channelP.addClass('unselected-channel')307}308309$channelList.append(rowDiv);310}311312function deleteCurrentChannel() {313if (!tc.currentChannel) {314return;315}316if (tc.currentChannel.sid === tc.generalChannel.sid) {317alert('You cannot delete the general channel');318return;319}320tc.currentChannel.delete().then(function(channel) {321console.log('channel: '+ channel.friendlyName + ' deleted');322setupChannel(tc.generalChannel);323});324}325326function selectChannel(event) {327var target = $(event.target);328var channelSid = target.data().sid;329var selectedChannel = tc.channelArray.filter(function(channel) {330return channel.sid === channelSid;331})[0];332if (selectedChannel === tc.currentChannel) {333return;334}335setupChannel(selectedChannel);336};337338function disconnectClient() {339leaveCurrentChannel();340$channelList.text('');341tc.$messageList.text('');342channels = undefined;343$statusRow.addClass('disconnected').removeClass('connected');344tc.$messageList.addClass('disconnected').removeClass('connected');345$connectPanel.addClass('disconnected').removeClass('connected');346$inputText.removeClass('with-shadow');347$typingRow.addClass('disconnected').removeClass('connected');348}349350tc.sortChannelsByName = function(channels) {351return channels.sort(function(a, b) {352if (a.friendlyName === GENERAL_CHANNEL_NAME) {353return -1;354}355if (b.friendlyName === GENERAL_CHANNEL_NAME) {356return 1;357}358return a.friendlyName.localeCompare(b.friendlyName);359});360};361362return tc;363})();364
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.
src/main/webapp/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('/twiliochat-servlets/token', {identity: username}, null, 'json')83.done(function(response) {84console.log('Successfully finished fetch of the Access Token.');85handler(response.token);86})87.fail(function(error) {88console.log('Failed to fetch the Access Token with error: ' + error);89});90}9192function connectMessagingClient(token) {93// Initialize the Chat messaging client94tc.accessManager = new Twilio.AccessManager(token);95Twilio.Chat.Client.create(token).then(function(client) {96tc.messagingClient = client;97updateConnectedUI();98tc.loadChannelList(tc.joinGeneralChannel);99tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));100tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));101tc.messagingClient.on('tokenExpired', refreshToken);102});103}104105function refreshToken() {106fetchAccessToken(tc.username, setNewToken);107}108109function setNewToken(tokenResponse) {110tc.accessManager.updateToken(tokenResponse.token);111}112113function updateConnectedUI() {114$('#username-span').text(tc.username);115$statusRow.addClass('connected').removeClass('disconnected');116tc.$messageList.addClass('connected').removeClass('disconnected');117$connectPanel.addClass('connected').removeClass('disconnected');118$inputText.addClass('with-shadow');119$typingRow.addClass('connected').removeClass('disconnected');120}121122tc.loadChannelList = function(handler) {123if (tc.messagingClient === undefined) {124console.log('Client is not initialized');125return;126}127128tc.messagingClient.getPublicChannelDescriptors().then(function(channels) {129tc.channelArray = tc.sortChannelsByName(channels.items);130$channelList.text('');131tc.channelArray.forEach(addChannel);132if (typeof handler === 'function') {133handler();134}135});136};137138tc.joinGeneralChannel = function() {139console.log('Attempting to join "general" chat channel...');140if (!tc.generalChannel) {141// If it doesn't exist, let's create it142tc.messagingClient.createChannel({143uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,144friendlyName: GENERAL_CHANNEL_NAME145}).then(function(channel) {146console.log('Created general channel');147tc.generalChannel = channel;148tc.loadChannelList(tc.joinGeneralChannel);149});150}151else {152console.log('Found general channel:');153setupChannel(tc.generalChannel);154}155};156157function initChannel(channel) {158console.log('Initialized channel ' + channel.friendlyName);159return tc.messagingClient.getChannelBySid(channel.sid);160}161162function joinChannel(_channel) {163return _channel.join()164.then(function(joinedChannel) {165console.log('Joined channel ' + joinedChannel.friendlyName);166updateChannelUI(_channel);167tc.currentChannel = _channel;168tc.loadMessages();169return joinedChannel;170});171}172173function initChannelEvents() {174console.log(tc.currentChannel.friendlyName + ' ready.');175tc.currentChannel.on('messageAdded', tc.addMessageToList);176tc.currentChannel.on('typingStarted', showTypingStarted);177tc.currentChannel.on('typingEnded', hideTypingStarted);178tc.currentChannel.on('memberJoined', notifyMemberJoined);179tc.currentChannel.on('memberLeft', notifyMemberLeft);180$inputText.prop('disabled', false).focus();181}182183function setupChannel(channel) {184return leaveCurrentChannel()185.then(function() {186return initChannel(channel);187})188.then(function(_channel) {189return joinChannel(_channel);190})191.then(initChannelEvents);192}193194tc.loadMessages = function() {195tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT)196.then(function(messages) {197messages.items.forEach(tc.addMessageToList);198});199};200201function leaveCurrentChannel() {202if (tc.currentChannel) {203return tc.currentChannel.leave().then(function(leftChannel) {204console.log('left ' + leftChannel.friendlyName);205leftChannel.removeListener('messageAdded', tc.addMessageToList);206leftChannel.removeListener('typingStarted', showTypingStarted);207leftChannel.removeListener('typingEnded', hideTypingStarted);208leftChannel.removeListener('memberJoined', notifyMemberJoined);209leftChannel.removeListener('memberLeft', notifyMemberLeft);210});211} else {212return Promise.resolve();213}214}215216tc.addMessageToList = function(message) {217var rowDiv = $('<div>').addClass('row no-margin');218rowDiv.loadTemplate($('#message-template'), {219username: message.author,220date: dateFormatter.getTodayDate(message.dateCreated),221body: message.body222});223if (message.author === tc.username) {224rowDiv.addClass('own-message');225}226227tc.$messageList.append(rowDiv);228scrollToMessageListBottom();229};230231function notifyMemberJoined(member) {232notify(member.identity + ' joined the channel')233}234235function notifyMemberLeft(member) {236notify(member.identity + ' left the channel');237}238239function notify(message) {240var row = $('<div>').addClass('col-md-12');241row.loadTemplate('#member-notification-template', {242status: message243});244tc.$messageList.append(row);245scrollToMessageListBottom();246}247248function showTypingStarted(member) {249$typingPlaceholder.text(member.identity + ' is typing...');250}251252function hideTypingStarted(member) {253$typingPlaceholder.text('');254}255256function scrollToMessageListBottom() {257tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);258}259260function updateChannelUI(selectedChannel) {261var channelElements = $('.channel-element').toArray();262var channelElement = channelElements.filter(function(element) {263return $(element).data().sid === selectedChannel.sid;264});265channelElement = $(channelElement);266if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {267tc.currentChannelContainer = channelElement;268}269tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');270channelElement.removeClass('unselected-channel').addClass('selected-channel');271tc.currentChannelContainer = channelElement;272}273274function showAddChannelInput() {275if (tc.messagingClient) {276$newChannelInputRow.addClass('showing').removeClass('not-showing');277$channelList.addClass('showing').removeClass('not-showing');278$newChannelInput.focus();279}280}281282function hideAddChannelInput() {283$newChannelInputRow.addClass('not-showing').removeClass('showing');284$channelList.addClass('not-showing').removeClass('showing');285$newChannelInput.val('');286}287288function addChannel(channel) {289if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {290tc.generalChannel = channel;291}292var rowDiv = $('<div>').addClass('row channel-row');293rowDiv.loadTemplate('#channel-template', {294channelName: channel.friendlyName295});296297var channelP = rowDiv.children().children().first();298299rowDiv.on('click', selectChannel);300channelP.data('sid', channel.sid);301if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {302tc.currentChannelContainer = channelP;303channelP.addClass('selected-channel');304}305else {306channelP.addClass('unselected-channel')307}308309$channelList.append(rowDiv);310}311312function deleteCurrentChannel() {313if (!tc.currentChannel) {314return;315}316if (tc.currentChannel.sid === tc.generalChannel.sid) {317alert('You cannot delete the general channel');318return;319}320tc.currentChannel.delete().then(function(channel) {321console.log('channel: '+ channel.friendlyName + ' deleted');322setupChannel(tc.generalChannel);323});324}325326function selectChannel(event) {327var target = $(event.target);328var channelSid = target.data().sid;329var selectedChannel = tc.channelArray.filter(function(channel) {330return channel.sid === channelSid;331})[0];332if (selectedChannel === tc.currentChannel) {333return;334}335setupChannel(selectedChannel);336};337338function disconnectClient() {339leaveCurrentChannel();340$channelList.text('');341tc.$messageList.text('');342channels = undefined;343$statusRow.addClass('disconnected').removeClass('connected');344tc.$messageList.addClass('disconnected').removeClass('connected');345$connectPanel.addClass('disconnected').removeClass('connected');346$inputText.removeClass('with-shadow');347$typingRow.addClass('disconnected').removeClass('connected');348}349350tc.sortChannelsByName = function(channels) {351return channels.sort(function(a, b) {352if (a.friendlyName === GENERAL_CH