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
_32package com.twilio.chat;_32_32import javax.inject.Inject;_32_32import com.twilio.jwt.accesstoken.AccessToken;_32import com.twilio.jwt.accesstoken.ChatGrant;_32_32public class TwilioTokenCreator {_32_32 private final AppConfig appConfig;_32_32 @Inject_32 public TwilioTokenCreator(AppConfig appConfig) {_32 this.appConfig = appConfig;_32 if (appConfig.isIncomplete()) {_32 throw new IncompleteConfigException(appConfig);_32 }_32 }_32_32 String generateToken(String identity) {_32 ChatGrant grant = new ChatGrant();_32 grant.setServiceSid(appConfig.getTwilioChatServiceSID());_32_32 AccessToken token = new AccessToken.Builder(_32 appConfig.getTwilioAccountSID(),_32 appConfig.getTwilioAPIKey(),_32 appConfig.getTwilioAPISecret()_32 ).identity(identity).grant(grant).build();_32_32 return token.toJwt();_32 }_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
_52package com.twilio.chat;_52_52import java.io.BufferedWriter;_52import java.io.IOException;_52import java.util.HashMap;_52import java.util.Map;_52_52import javax.inject.Inject;_52import javax.servlet.http.HttpServlet;_52import javax.servlet.http.HttpServletRequest;_52import javax.servlet.http.HttpServletResponse;_52_52import com.google.gson.Gson;_52import com.google.inject.Singleton;_52_52@Singleton_52public class TokenServlet extends HttpServlet {_52_52 private final TwilioTokenCreator tokenCreator;_52_52 @Inject_52 public TokenServlet(TwilioTokenCreator tokenCreator) {_52 this.tokenCreator = tokenCreator;_52 }_52_52 @Override_52 public void doPost(HttpServletRequest request, HttpServletResponse response) {_52 String identity = request.getParameter("identity");_52_52 if (identity != null) {_52_52 String generatedToken = tokenCreator.generateToken(identity);_52_52 Map<String, String> json = new HashMap<>();_52 json.put("identity", identity);_52 json.put("token", generatedToken);_52 renderJson(response, json);_52 }_52_52 }_52_52 private void renderJson(HttpServletResponse response, Map<String, String> json) {_52 Gson gson = new Gson();_52 response.setContentType("application/json");_52 try (BufferedWriter responseWriter = new BufferedWriter(response.getWriter())) {_52 responseWriter.write(gson.toJson(json));_52 responseWriter.flush();_52 } catch (IOException e) {_52 e.printStackTrace();_52 }_52 }_52}
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
_363var twiliochat = (function() {_363 var tc = {};_363_363 var GENERAL_CHANNEL_UNIQUE_NAME = 'general';_363 var GENERAL_CHANNEL_NAME = 'General Channel';_363 var MESSAGES_HISTORY_LIMIT = 50;_363_363 var $channelList;_363 var $inputText;_363 var $usernameInput;_363 var $statusRow;_363 var $connectPanel;_363 var $newChannelInputRow;_363 var $newChannelInput;_363 var $typingRow;_363 var $typingPlaceholder;_363_363 $(document).ready(function() {_363 tc.$messageList = $('#message-list');_363 $channelList = $('#channel-list');_363 $inputText = $('#input-text');_363 $usernameInput = $('#username-input');_363 $statusRow = $('#status-row');_363 $connectPanel = $('#connect-panel');_363 $newChannelInputRow = $('#new-channel-input-row');_363 $newChannelInput = $('#new-channel-input');_363 $typingRow = $('#typing-row');_363 $typingPlaceholder = $('#typing-placeholder');_363 $usernameInput.focus();_363 $usernameInput.on('keypress', handleUsernameInputKeypress);_363 $inputText.on('keypress', handleInputTextKeypress);_363 $newChannelInput.on('keypress', tc.handleNewChannelInputKeypress);_363 $('#connect-image').on('click', connectClientWithUsername);_363 $('#add-channel-image').on('click', showAddChannelInput);_363 $('#leave-span').on('click', disconnectClient);_363 $('#delete-channel-span').on('click', deleteCurrentChannel);_363 });_363_363 function handleUsernameInputKeypress(event) {_363 if (event.keyCode === 13){_363 connectClientWithUsername();_363 }_363 }_363_363 function handleInputTextKeypress(event) {_363 if (event.keyCode === 13) {_363 tc.currentChannel.sendMessage($(this).val());_363 event.preventDefault();_363 $(this).val('');_363 }_363 else {_363 notifyTyping();_363 }_363 }_363_363 var notifyTyping = $.throttle(function() {_363 tc.currentChannel.typing();_363 }, 1000);_363_363 tc.handleNewChannelInputKeypress = function(event) {_363 if (event.keyCode === 13) {_363 tc.messagingClient.createChannel({_363 friendlyName: $newChannelInput.val()_363 }).then(hideAddChannelInput);_363 $(this).val('');_363 event.preventDefault();_363 }_363 };_363_363 function connectClientWithUsername() {_363 var usernameText = $usernameInput.val();_363 $usernameInput.val('');_363 if (usernameText == '') {_363 alert('Username cannot be empty');_363 return;_363 }_363 tc.username = usernameText;_363 fetchAccessToken(tc.username, connectMessagingClient);_363 }_363_363 function fetchAccessToken(username, handler) {_363 $.post('/twiliochat-servlets/token', {identity: username}, null, 'json')_363 .done(function(response) {_363 console.log('Successfully finished fetch of the Access Token.');_363 handler(response.token);_363 })_363 .fail(function(error) {_363 console.log('Failed to fetch the Access Token with error: ' + error);_363 });_363 }_363_363 function connectMessagingClient(token) {_363 // Initialize the Chat messaging client_363 tc.accessManager = new Twilio.AccessManager(token);_363 Twilio.Chat.Client.create(token).then(function(client) {_363 tc.messagingClient = client;_363 updateConnectedUI();_363 tc.loadChannelList(tc.joinGeneralChannel);_363 tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));_363 tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));_363 tc.messagingClient.on('tokenExpired', refreshToken);_363 });_363 }_363_363 function refreshToken() {_363 fetchAccessToken(tc.username, setNewToken);_363 }_363_363 function setNewToken(tokenResponse) {_363 tc.accessManager.updateToken(tokenResponse.token);_363 }_363_363 function updateConnectedUI() {_363 $('#username-span').text(tc.username);_363 $statusRow.addClass('connected').removeClass('disconnected');_363 tc.$messageList.addClass('connected').removeClass('disconnected');_363 $connectPanel.addClass('connected').removeClass('disconnected');_363 $inputText.addClass('with-shadow');_363 $typingRow.addClass('connected').removeClass('disconnected');_363 }_363_363 tc.loadChannelList = function(handler) {_363 if (tc.messagingClient === undefined) {_363 console.log('Client is not initialized');_363 return;_363 }_363_363 tc.messagingClient.getPublicChannelDescriptors().then(function(channels) {_363 tc.channelArray = tc.sortChannelsByName(channels.items);_363 $channelList.text('');_363 tc.channelArray.forEach(addChannel);_363 if (typeof handler === 'function') {_363 handler();_363 }_363 });_363 };_363_363 tc.joinGeneralChannel = function() {_363 console.log('Attempting to join "general" chat channel...');_363 if (!tc.generalChannel) {_363 // If it doesn't exist, let's create it_363 tc.messagingClient.createChannel({_363 uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,_363 friendlyName: GENERAL_CHANNEL_NAME_363 }).then(function(channel) {_363 console.log('Created general channel');_363 tc.generalChannel = channel;_363 tc.loadChannelList(tc.joinGeneralChannel);_363 });_363 }_363 else {_363 console.log('Found general channel:');_363 setupChannel(tc.generalChannel);_363 }_363 };_363_363 function initChannel(channel) {_363 console.log('Initialized channel ' + channel.friendlyName);_363 return tc.messagingClient.getChannelBySid(channel.sid);_363 }_363_363 function joinChannel(_channel) {_363 return _channel.join()_363 .then(function(joinedChannel) {_363 console.log('Joined channel ' + joinedChannel.friendlyName);_363 updateChannelUI(_channel);_363 tc.currentChannel = _channel;_363 tc.loadMessages();_363 return joinedChannel;_363 });_363 }_363_363 function initChannelEvents() {_363 console.log(tc.currentChannel.friendlyName + ' ready.');_363 tc.currentChannel.on('messageAdded', tc.addMessageToList);_363 tc.currentChannel.on('typingStarted', showTypingStarted);_363 tc.currentChannel.on('typingEnded', hideTypingStarted);_363 tc.currentChannel.on('memberJoined', notifyMemberJoined);_363 tc.currentChannel.on('memberLeft', notifyMemberLeft);_363 $inputText.prop('disabled', false).focus();_363 }_363_363 function setupChannel(channel) {_363 return leaveCurrentChannel()_363 .then(function() {_363 return initChannel(channel);_363 })_363 .then(function(_channel) {_363 return joinChannel(_channel);_363 })_363 .then(initChannelEvents);_363 }_363_363 tc.loadMessages = function() {_363 tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT)_363 .then(function(messages) {_363 messages.items.forEach(tc.addMessageToList);_363 });_363 };_363_363 function leaveCurrentChannel() {_363 if (tc.currentChannel) {_363 return tc.currentChannel.leave().then(function(leftChannel) {_363 console.log('left ' + leftChannel.friendlyName);_363 leftChannel.removeListener('messageAdded', tc.addMessageToList);_363 leftChannel.removeListener('typingStarted', showTypingStarted);_363 leftChannel.removeListener('typingEnded', hideTypingStarted);_363 leftChannel.removeListener('memberJoined', notifyMemberJoined);_363 leftChannel.removeListener('memberLeft', notifyMemberLeft);_363 });_363 } else {_363 return Promise.resolve();_363 }_363 }_363_363 tc.addMessageToList = function(message) {_363 var rowDiv = $('<div>').addClass('row no-margin');_363 rowDiv.loadTemplate($('#message-template'), {_363 username: message.author,_363 date: dateFormatter.getTodayDate(message.dateCreated),_363 body: message.body_363 });_363 if (message.author === tc.username) {_363 rowDiv.addClass('own-message');_363 }_363_363 tc.$messageList.append(rowDiv);_363 scrollToMessageListBottom();_363 };_363_363 function notifyMemberJoined(member) {_363 notify(member.identity + ' joined the channel')_363 }_363_363 function notifyMemberLeft(member) {_363 notify(member.identity + ' left the channel');_363 }_363_363 function notify(message) {_363 var row = $('<div>').addClass('col-md-12');_363 row.loadTemplate('#member-notification-template', {_363 status: message_363 });_363 tc.$messageList.append(row);_363 scrollToMessageListBottom();_363 }_363_363 function showTypingStarted(member) {_363 $typingPlaceholder.text(member.identity + ' is typing...');_363 }_363_363 function hideTypingStarted(member) {_363 $typingPlaceholder.text('');_363 }_363_363 function scrollToMessageListBottom() {_363 tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);_363 }_363_363 function updateChannelUI(selectedChannel) {_363 var channelElements = $('.channel-element').toArray();_363 var channelElement = channelElements.filter(function(element) {_363 return $(element).data().sid === selectedChannel.sid;_363 });_363 channelElement = $(channelElement);_363 if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {_363 tc.currentChannelContainer = channelElement;_363 }_363 tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');_363 channelElement.removeClass('unselected-channel').addClass('selected-channel');_363 tc.currentChannelContainer = channelElement;_363 }_363_363 function showAddChannelInput() {_363 if (tc.messagingClient) {_363 $newChannelInputRow.addClass('showing').removeClass('not-showing');_363 $channelList.addClass('showing').removeClass('not-showing');_363 $newChannelInput.focus();_363 }_363 }_363_363 function hideAddChannelInput() {_363 $newChannelInputRow.addClass('not-showing').removeClass('showing');_363 $channelList.addClass('not-showing').removeClass('showing');_363 $newChannelInput.val('');_363 }_363_363 function addChannel(channel) {_363 if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {_363 tc.generalChannel = channel;_363 }_363 var rowDiv = $('<div>').addClass('row channel-row');_363 rowDiv.loadTemplate('#channel-template', {_363 channelName: channel.friendlyName_363 });_363_363 var channelP = rowDiv.children().children().first();_363_363 rowDiv.on('click', selectChannel);_363 channelP.data('sid', channel.sid);_363 if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {_363 tc.currentChannelContainer = channelP;_363 channelP.addClass('selected-channel');_363 }_363 else {_363 channelP.addClass('unselected-channel')_363 }_363_363 $channelList.append(rowDiv);_363 }_363_363 function deleteCurrentChannel() {_363 if (!tc.currentChannel) {_363 return;_363 }_363 if (tc.currentChannel.sid === tc.generalChannel.sid) {_363 alert('You cannot delete the general channel');_363 return;_363 }_363 tc.currentChannel.delete().then(function(channel) {_363 console.log('channel: '+ channel.friendlyName + ' deleted');_363 setupChannel(tc.generalChannel);_363 });_363 }_363_363 function selectChannel(event) {_363 var target = $(event.target);_363 var channelSid = target.data().sid;_363 var selectedChannel = tc.channelArray.filter(function(channel) {_363 return channel.sid === channelSid;_363 })[0];_363 if (selectedChannel === tc.currentChannel) {_363 return;_363 }_363 setupChannel(selectedChannel);_363 };_363_363 function disconnectClient() {_363 leaveCurrentChannel();_363 $channelList.text('');_363 tc.$messageList.text('');_363 channels = undefined;_363 $statusRow.addClass('disconnected').removeClass('connected');_363 tc.$messageList.addClass('disconnected').removeClass('connected');_363 $connectPanel.addClass('disconnected').removeClass('connected');_363 $inputText.removeClass('with-shadow');_363 $typingRow.addClass('disconnected').removeClass('connected');_363 }_363_363 tc.sortChannelsByName = function(channels) {_363 return channels.sort(function(a, b) {_363 if (a.friendlyName === GENERAL_CHANNEL_NAME) {_363 return -1;_363 }_363 if (b.friendlyName === GENERAL_CHANNEL_NAME) {_363 return 1;_363 }_363 return a.friendlyName.localeCompare(b.friendlyName);_363 });_363 };_363_363 return tc;_363})();
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
_363var twiliochat = (function() {_363 var tc = {};_363_363 var GENERAL_CHANNEL_UNIQUE_NAME = 'general';_363 var GENERAL_CHANNEL_NAME = 'General Channel';_363 var MESSAGES_HISTORY_LIMIT = 50;_363_363 var $channelList;_363 var $inputText;_363 var $usernameInput;_363 var $statusRow;_363 var $connectPanel;_363 var $newChannelInputRow;_363 var $newChannelInput;_363 var $typingRow;_363 var $typingPlaceholder;_363_363 $(document).ready(function() {_363 tc.$messageList = $('#message-list');_363 $channelList = $('#channel-list');_363 $inputText = $('#input-text');_363 $usernameInput = $('#username-input');_363 $statusRow = $('#status-row');_363 $connectPanel = $('#connect-panel');_363 $newChannelInputRow = $('#new-channel-input-row');_363 $newChannelInput = $('#new-channel-input');_363 $typingRow = $('#typing-row');_363 $typingPlaceholder = $('#typing-placeholder');_363 $usernameInput.focus();_363 $usernameInput.on('keypress', handleUsernameInputKeypress);_363 $inputText.on('keypress', handleInputTextKeypress);_363 $newChannelInput.on('keypress', tc.handleNewChannelInputKeypress);_363 $('#connect-image').on('click', connectClientWithUsername);_363 $('#add-channel-image').on('click', showAddChannelInput);_363 $('#leave-span').on('click', disconnectClient);_363 $('#delete-channel-span').on('click', deleteCurrentChannel);_363 });_363_363 function handleUsernameInputKeypress(event) {_363 if (event.keyCode === 13){_363 connectClientWithUsername();_363 }_363 }_363_363 function handleInputTextKeypress(event) {_363 if (event.keyCode === 13) {_363 tc.currentChannel.sendMessage($(this).val());_363 event.preventDefault();_363 $(this).val('');_363 }_363 else {_363 notifyTyping();_363 }_363 }_363_363 var notifyTyping = $.throttle(function() {_363 tc.currentChannel.typing();_363 }, 1000);_363_363 tc.handleNewChannelInputKeypress = function(event) {_363 if (event.keyCode === 13) {_363 tc.messagingClient.createChannel({_363 friendlyName: $newChannelInput.val()_363 }).then(hideAddChannelInput);_363 $(this).val('');_363 event.preventDefault();_363 }_363 };_363_363 function connectClientWithUsername() {_363 var usernameText = $usernameInput.val();_363 $usernameInput.val('');_363 if (usernameText == '') {_363 alert('Username cannot be empty');_363 return;_363 }_363 tc.username = usernameText;_363 fetchAccessToken(tc.username, connectMessagingClient);_363 }_363_363 function fetchAccessToken(username, handler) {_363 $.post('/twiliochat-servlets/token', {identity: username}, null, 'json')_363 .done(function(response) {_363 console.log('Successfully finished fetch of the Access Token.');_363 handler(response.token);_363 })_363 .fail(function(error) {_363 console.log('Failed to fetch the Access Token with error: ' + error);_363 });_363 }_363_363 function connectMessagingClient(token) {_363 // Initialize the Chat messaging client_363 tc.accessManager = new Twilio.AccessManager(token);_363 Twilio.Chat.Client.create(token).then(function(client) {_363 tc.messagingClient = client;_363 updateConnectedUI();_363 tc.loadChannelList(tc.joinGeneralChannel);_363 tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));_363 tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));_363 tc.messagingClient.on('tokenExpired', refreshToken);_363 });_363 }_363_363 function refreshToken() {_363 fetchAccessToken(tc.username, setNewToken);_363 }_363_363 function setNewToken(tokenResponse) {_363 tc.accessManager.updateToken(tokenResponse.token);_363 }_363_363 function updateConnectedUI() {_363 $('#username-span').text(tc.username);_363 $statusRow.addClass('connected').removeClass('disconnected');_363 tc.$messageList.addClass('connected').removeClass('disconnected');_363 $connectPanel.addClass('connected').removeClass('disconnected');_363 $inputText.addClass('with-shadow');_363 $typingRow.addClass('connected').removeClass('disconnected');_363 }_363_363 tc.loadChannelList = function(handler) {_363 if (tc.messagingClient === undefined) {_363 console.log('Client is not initialized');_363 return;_363 }_363_363 tc.messagingClient.getPublicChannelDescriptors().then(function(channels) {_363 tc.channelArray = tc.sortChannelsByName(channels.items);_363 $channelList.text('');_363 tc.channelArray.forEach(addChannel);_363 if (typeof handler === 'function') {_363 handler();_363 }_363 });_363 };_363_363 tc.joinGeneralChannel = function() {_363 console.log('Attempting to join "general" chat channel...');_363 if (!tc.generalChannel) {_363 // If it doesn't exist, let's create it_363 tc.messagingClient.createChannel({_363 uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,_363 friendlyName: GENERAL_CHANNEL_NAME_363 }).then(function(channel) {_363 console.log('Created general channel');_363 tc.generalChannel = channel;_363 tc.loadChannelList(tc.joinGeneralChannel);_363 });_363 }_363 else {_363 console.log('Found general channel:');_363 setupChannel(tc.generalChannel);_363 }_363 };_363_363 function initChannel(channel) {_363 console.log('Initialized channel ' + channel.friendlyName);_363 return tc.messagingClient.getChannelBySid(channel.sid);_363 }_363_363 function joinChannel(_channel) {_363 return _channel.join()_363 .then(function(joinedChannel) {_363 console.log('Joined channel ' + joinedChannel.friendlyName);_363 updateChannelUI(_channel);_363 tc.currentChannel = _channel;_363 tc.loadMessages();_363 return joinedChannel;_363 });_363 }_363_363 function initChannelEvents() {_363 console.log(tc.currentChannel.friendlyName + ' ready.');_363 tc.currentChannel.on('messageAdded', tc.addMessageToList);_363 tc.currentChannel.on('typingStarted', showTypingStarted);_363 tc.currentChannel.on('typingEnded', hideTypingStarted);_363 tc.currentChannel.on('memberJoined', notifyMemberJoined);_363 tc.currentChannel.on('memberLeft', notifyMemberLeft);_363 $inputText.prop('disabled', false).focus();_363 }_363_363 function setupChannel(channel) {_363 return leaveCurrentChannel()_363 .then(function() {_363 return initChannel(channel);_363 })_363 .then(function(_channel) {_363 return joinChannel(_channel);_363 })_363 .then(initChannelEvents);_363 }_363_363 tc.loadMessages = function() {_363 tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT)_363 .then(function(messages) {_363 messages.items.forEach(tc.addMessageToList);_363 });_363 };_363_363 function leaveCurrentChannel() {_363 if (tc.currentChannel) {_363 return tc.currentChannel.leave().then(function(leftChannel) {_363 console.log('left ' + leftChannel.friendlyName);_363 leftChannel.removeListener('messageAdded', tc.addMessageToList);_363 leftChannel.removeListener('typingStarted', showTypingStarted);_363 leftChannel.removeListener('typingEnded', hideTypingStarted);_363 leftChannel.removeListener('memberJoined', notifyMemberJoined);_363 leftChannel.removeListener('memberLeft', notifyMemberLeft);_363 });_363 } else {_363 return Promise.resolve();_363 }_363 }_363_363 tc.addMessageToList = function(message) {_363 var rowDiv = $('<div>').addClass('row no-margin');_363 rowDiv.loadTemplate($('#message-template'), {_363 username: message.author,_363 date: dateFormatter.getTodayDate(message.dateCreated),_363 body: message.body_363 });_363 if (message.author === tc.username) {_363 rowDiv.addClass('own-message');_363 }_363_363 tc.$messageList.append(rowDiv);_363 scrollToMessageListBottom();_363 };_363_363 function notifyMemberJoined(member) {_363 notify(member.identity + ' joined the channel')_363 }_363_363 function notifyMemberLeft(member) {_363 notify(member.identity + ' left the channel');_363 }_363_363 function notify(message) {_363 var row = $('<div>').addClass('col-md-12');_363 row.loadTemplate('#member-notification-template', {_363 status: message_363 });_363 tc.$messageList.append(row);_363 scrollToMessageListBottom();_363 }_363_363 function showTypingStarted(member) {_363 $typingPlaceholder.text(member.identity + ' is typing...');_363 }_363_363 function hideTypingStarted(member) {_363 $typingPlaceholder.text('');_363 }_363_363 function scrollToMessageListBottom() {_363 tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);_363 }_363_363 function updateChannelUI(selectedChannel) {_363 var channelElements = $('.channel-element').toArray();_363 var channelElement = channelElements.filter(function(element) {_363 return $(element).data().sid === selectedChannel.sid;_363 });_363 channelElement = $(channelElement);_363 if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {_363 tc.currentChannelContainer = channelElement;_363 }_363 tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');_363 channelElement.removeClass('unselected-channel').addClass('selected-channel');_363 tc.currentChannelContainer = channelElement;_363 }_363_363 function showAddChannelInput() {_363 if (tc.messagingClient) {_363 $newChannelInputRow.addClass('showing').removeClass('not-showing');_363 $channelList.addClass('showing').removeClass('not-showing');_363 $newChannelInput.focus();_363 }_363 }_363_363 function hideAddChannelInput() {_363 $newChannelInputRow.addClass('not-showing').removeClass('showing');_363 $channelList.addClass('not-showing').removeClass('showing');_363 $newChannelInput.val('');_363 }_363_363 function addChannel(channel) {_363 if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {_363 tc.generalChannel = channel;_363 }_363 var rowDiv = $('<div>').addClass('row channel-row');_363 rowDiv.loadTemplate('#channel-template', {_363 channelName: channel.friendlyName_363 });_363_363 var channelP = rowDiv.children().children().first();_363_363 rowDiv.on('click', selectChannel);_363 channelP.data('sid', channel.sid);_363 if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {_363 tc.currentChannelContainer = channelP;_363 channelP.addClass('selected-channel');_363 }_363 else {_363 channelP.addClass('unselected-channel')_363 }_363_363 $channelList.append(rowDiv);_363 }_363_363 function deleteCurrentChannel() {_363 if (!tc.currentChannel) {_363 return;_363 }_363 if (tc.currentChannel.sid === tc.generalChannel.sid) {_363 alert('You cannot delete the general channel');_363 return;_363 }_363 tc.currentChannel.delete().then(function(channel) {_363 console.log('channel: '+ channel.friendlyName + ' deleted');_363 setupChannel(tc.generalChannel);_363 });_363 }_363_363 function selectChannel(event) {_363 var target = $(event.target);_363 var channelSid = target.data().sid;_363 var selectedChannel = tc.channelArray.filter(function(channel) {_363 return channel.sid === channelSid;_363 })[0];_363 if (selectedChannel === tc.currentChannel) {_363 return;_363 }_363 setupChannel(selectedChannel);_363 };_363_363 function disconnectClient() {_363 leaveCurrentChannel();_363 $channelList.text('');_363 tc.$messageList.text('');_363 channels = undefined;_363 $statusRow.addClass('disconnected').removeClass('connected');_363 tc.$messageList.addClass('disconnected').removeClass('connected');_363 $connectPanel.addClass('disconnected').removeClass('connected');_363 $inputText.removeClass('with-shadow');_363 $typingRow.addClass('disconnected').removeClass('connected');_363 }_363_363 tc.sortChannelsByName = function(channels) {_363 return channels.sort(function(a, b) {_363 if (a.friendlyName === GENERAL_CHANNEL_NAME) {_363 return -1;_363 }_363 if (b.friendlyName === GENERAL_CHANNEL_NAME) {_363 return 1;_363 }_363 return a.friendlyName.localeCompare(b.friendlyName);_363 });_363 };_363_363 return tc;_363})();
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
_363var twiliochat = (function() {_363 var tc = {};_363_363 var GENERAL_CHANNEL_UNIQUE_NAME = 'general';_363 var GENERAL_CHANNEL_NAME = 'General Channel';_363 var MESSAGES_HISTORY_LIMIT = 50;_363_363 var $channelList;_363 var $inputText;_363 var $usernameInput;_363 var $statusRow;_363 var $connectPanel;_363 var $newChannelInputRow;_363 var $newChannelInput;_363 var $typingRow;_363 var $typingPlaceholder;_363_363 $(document).ready(function() {_363 tc.$messageList = $('#message-list');_363 $channelList = $('#channel-list');_363 $inputText = $('#input-text');_363 $usernameInput = $('#username-input');_363 $statusRow = $('#status-row');_363 $connectPanel = $('#connect-panel');_363 $newChannelInputRow = $('#new-channel-input-row');_363 $newChannelInput = $('#new-channel-input');_363 $typingRow = $('#typing-row');_363 $typingPlaceholder = $('#typing-placeholder');_363 $usernameInput.focus();_363 $usernameInput.on('keypress', handleUsernameInputKeypress);_363 $inputText.on('keypress', handleInputTextKeypress);_363 $newChannelInput.on('keypress', tc.handleNewChannelInputKeypress);_363 $('#connect-image').on('click', connectClientWithUsername);_363 $('#add-channel-image').on('click', showAddChannelInput);_363 $('#leave-span').on('click', disconnectClient);_363 $('#delete-channel-span').on('click', deleteCurrentChannel);_363 });_363_363 function handleUsernameInputKeypress(event) {_363 if (event.keyCode === 13){_363 connectClientWithUsername();_363 }_363 }_363_363 function handleInputTextKeypress(event) {_363 if (event.keyCode === 13) {_363 tc.currentChannel.sendMessage($(this).val());_363 event.preventDefault();_363 $(this).val('');_363 }_363 else {_363 notifyTyping();_363 }_363 }_363_363 var notifyTyping = $.throttle(function() {_363 tc.currentChannel.typing();_363 }, 1000);_363_363 tc.handleNewChannelInputKeypress = function(event) {_363 if (event.keyCode === 13) {_363 tc.messagingClient.createChannel({_363 friendlyName: $newChannelInput.val()_363 }).then(hideAddChannelInput);_363 $(this).val('');_363 event.preventDefault();_363 }_363 };_363_363 function connectClientWithUsername() {_363 var usernameText = $usernameInput.val();_363 $usernameInput.val('');_363 if (usernameText == '') {_363 alert('Username cannot be empty');_363 return;_363 }_363 tc.username = usernameText;_363 fetchAccessToken(tc.username, connectMessagingClient);_363 }_363_363 function fetchAccessToken(username, handler) {_363 $.post('/twiliochat-servlets/token', {identity: username}, null, 'json')_363 .done(function(response) {_363 console.log('Successfully finished fetch of the Access Token.');_363 handler(response.token);_363 })_363 .fail(function(error) {_363 console.log('Failed to fetch the Access Token with error: ' + error);_363 });_363 }_363_363 function connectMessagingClient(token) {_363 // Initialize the Chat messaging client_363 tc.accessManager = new Twilio.AccessManager(token);_363 Twilio.Chat.Client.create(token).then(function(client) {_363 tc.messagingClient = client;_363 updateConnectedUI();_363 tc.loadChannelList(tc.joinGeneralChannel);_363 tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));_363 tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));_363 tc.messagingClient.on('tokenExpired', refreshToken);_363 });_363 }_363_363 function refreshToken() {_363 fetchAccessToken(tc.username, setNewToken);_363 }_363_363 function setNewToken(tokenResponse) {_363 tc.accessManager.updateToken(tokenResponse.token);_363 }_363_363 function updateConnectedUI() {_363 $('#username-span').text(tc.username);_363 $statusRow.addClass('connected').removeClass('disconnected');_363 tc.$messageList.addClass('connected').removeClass('disconnected');_363 $connectPanel.addClass('connected').removeClass('disconnected');_363 $inputText.addClass('with-shadow');_363 $typingRow.addClass('connected').removeClass('disconnected');_363 }_363_363 tc.loadChannelList = function(handler) {_363 if (tc.messagingClient === undefined) {_363 console.log('Client is not initialized');_363 return;_363 }_363_363 tc.messagingClient.getPublicChannelDescriptors().then(function(channels) {_363 tc.channelArray = tc.sortChannelsByName(channels.items);_363 $channelList.text('');_363 tc.channelArray.forEach(addChannel);_363 if (typeof handler === 'function') {_363 handler();_363 }_363 });_363 };_363_363 tc.joinGeneralChannel = function() {_363 console.log('Attempting to join "general" chat channel...');_363 if (!tc.generalChannel) {_363 // If it doesn't exist, let's create it_363 tc.messagingClient.createChannel({_363 uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,_363 friendlyName: GENERAL_CHANNEL_NAME_363 }).then(function(channel) {_363 console.log('Created general channel');_363 tc.generalChannel = channel;_363 tc.loadChannelList(tc.joinGeneralChannel);_363 });_363 }_363 else {_363 console.log('Found general channel:');_363 setupChannel(tc.generalChannel);_363 }_363 };_363_363 function initChannel(channel) {_363 console.log('Initialized channel ' + channel.friendlyName);_363 return tc.messagingClient.getChannelBySid(channel.sid);_363 }_363_363 function joinChannel(_channel) {_363 return _channel.join()_363 .then(function(joinedChannel) {_363 console.log('Joined channel ' + joinedChannel.friendlyName);_363 updateChannelUI(_channel);_363 tc.currentChannel = _channel;_363 tc.loadMessages();_363 return joinedChannel;_363 });_363 }_363_363 function initChannelEvents() {_363 console.log(tc.currentChannel.friendlyName + ' ready.');_363 tc.currentChannel.on('messageAdded', tc.addMessageToList);_363 tc.currentChannel.on('typingStarted', showTypingStarted);_363 tc.currentChannel.on('typingEnded', hideTypingStarted);_363 tc.currentChannel.on('memberJoined', notifyMemberJoined);_363 tc.currentChannel.on('memberLeft', notifyMemberLeft);_363 $inputText.prop('disabled', false).focus();_363 }_363_363 function setupChannel(channel) {_363 return leaveCurrentChannel()_363 .then(function() {_363 return initChannel(channel);_363 })_363 .then(function(_channel) {_363 return joinChannel(_channel);_363 })_363 .then(initChannelEvents);_363 }_363_363 tc.loadMessages = function() {_363 tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT)_363 .then(function(messages) {_363 messages.items.forEach(tc.addMessageToList);_363 });_363 };_363_363 function leaveCurrentChannel() {_363 if (tc.currentChannel) {_363 return tc.currentChannel.leave().then(function(leftChannel) {_363 console.log('left ' + leftChannel.friendlyName);_363 leftChannel.removeListener('messageAdded', tc.addMessageToList);_363 leftChannel.removeListener('typingStarted', showTypingStarted);_363 leftChannel.removeListener('typingEnded', hideTypingStarted);_363 leftChannel.removeListener('memberJoined', notifyMemberJoined);_363 leftChannel.removeListener('memberLeft', notifyMemberLeft);_363 });_363 } else {_363 return Promise.resolve();_363 }_363 }_363_363 tc.addMessageToList = function(message) {_363 var rowDiv = $('<div>').addClass('row no-margin');_363 rowDiv.loadTemplate($('#message-template'), {_363 username: message.author,_363 date: dateFormatter.getTodayDate(message.dateCreated),_363 body: message.body_363 });_363 if (message.author === tc.username) {_363 rowDiv.addClass('own-message');_363 }_363_363 tc.$messageList.append(rowDiv);_363 scrollToMessageListBottom();_363 };_363_363 function notifyMemberJoined(member) {_363 notify(member.identity + ' joined the channel')_363 }_363_363 function notifyMemberLeft(member) {_363 notify(member.identity + ' left the channel');_363 }_363_363 function notify(message) {_363 var row = $('<div>').addClass('col-md-12');_363 row.loadTemplate('#member-notification-template', {_363 status: message_363 });_363 tc.$messageList.append(row);_363 scrollToMessageListBottom();_363 }_363_363 function showTypingStarted(member) {_363 $typingPlaceholder.text(member.identity + ' is typing...');_363 }_363_363 function hideTypingStarted(member) {_363 $typingPlaceholder.text('');_363 }_363_363 function scrollToMessageListBottom() {_363 tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);_363 }_363_363 function updateChannelUI(selectedChannel) {_363 var channelElements = $('.channel-element').toArray();_363 var channelElement = channelElements.filter(function(element) {_363 return $(element).data().sid === selectedChannel.sid;_363 });_363 channelElement = $(channelElement);_363 if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {_363 tc.currentChannelContainer = channelElement;_363 }_363 tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');_363 channelElement.removeClass('unselected-channel').addClass('selected-channel');_363 tc.currentChannelContainer = channelElement;_363 }_363_363 function showAddChannelInput() {_363 if (tc.messagingClient) {_363 $newChannelInputRow.addClass('showing').removeClass('not-showing');_363 $channelList.addClass('showing').removeClass('not-showing');_363 $newChannelInput.focus();_363 }_363 }_363_363 function hideAddChannelInput() {_363 $newChannelInputRow.addClass('not-showing').removeClass('showing');_363 $channelList.addClass('not-showing').removeClass('showing');_363 $newChannelInput.val('');_363 }_363_363 function addChannel(channel) {_363 if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {_363 tc.generalChannel = channel;_363 }_363 var rowDiv = $('<div>').addClass('row channel-row');_363 rowDiv.loadTemplate('#channel-template', {_363 channelName: channel.friendlyName_363 });_363_363 var channelP = rowDiv.children().children().first();_363_363 rowDiv.on('click', selectChannel);_363 channelP.data('sid', channel.sid);_363 if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {_363 tc.currentChannelContainer = channelP;_363 channelP.addClass('selected-channel');_363 }_363 else {_363 channelP.addClass('unselected-channel')_363 }_363_363 $channelList.append(rowDiv);_363 }_363_363 function deleteCurrentChannel() {_363 if (!tc.currentChannel) {_363 return;_363 }_363 if (tc.currentChannel.sid === tc.generalChannel.sid) {_363 alert('You cannot delete the general channel');_363 return;_363 }_363 tc.currentChannel.delete().then(function(channel) {_363 console.log('channel: '+ channel.friendlyName + ' deleted');_363 setupChannel(tc.generalChannel);_363 });_363 }_363_363 function selectChannel(event) {_363 var target = $(event.target);_363 var channelSid = target.data().sid;_363 var selectedChannel = tc.channelArray.filter(function(channel) {_363 return channel.sid === channelSid;_363 })[0];_363 if (selectedChannel === tc.currentChannel) {_363 return;_363 }_363 setupChannel(selectedChannel);_363 };_363_363 function disconnectClient() {_363 leaveCurrentChannel();_363 $channelList.text('');_363 tc.$messageList.text('');_363 channels = undefined;_363 $statusRow.addClass('disconnected').removeClass('connected');_363 tc.$messageList.addClass('disconnected').removeClass('connected');_363 $connectPanel.addClass('disconnected').removeClass('connected');_363 $inputText.removeClass('with-shadow');_363 $typingRow.addClass('disconnected').removeClass('connected');_363 }_363_363 tc.sortChannelsByName = function(channels) {_363 return channels.sort(function(a, b) {_363 if (a.friendlyName === GENERAL_CHANNEL_NAME) {_363 return -1;_363 }_363 if (b.friendlyName === GENERAL_CHANNEL_NAME) {_363 return 1;_363 }_363 return a.friendlyName.localeCompare(b.friendlyName);_363 });_363 };_363_363 return tc;_363})();
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
_363var twiliochat = (function() {_363 var tc = {};_363_363 var GENERAL_CHANNEL_UNIQUE_NAME = 'general';_363 var GENERAL_CHANNEL_NAME = 'General Channel';_363 var MESSAGES_HISTORY_LIMIT = 50;_363_363 var $channelList;_363 var $inputText;_363 var $usernameInput;_363 var $statusRow;_363 var $connectPanel;_363 var $newChannelInputRow;_363 var $newChannelInput;_363 var $typingRow;_363 var $typingPlaceholder;_363_363 $(document).ready(function() {_363 tc.$messageList = $('#message-list');_363 $channelList = $('#channel-list');_363 $inputText = $('#input-text');_363 $usernameInput = $('#username-input');_363 $statusRow = $('#status-row');_363 $connectPanel = $('#connect-panel');_363 $newChannelInputRow = $('#new-channel-input-row');_363 $newChannelInput = $('#new-channel-input');_363 $typingRow = $('#typing-row');_363 $typingPlaceholder = $('#typing-placeholder');_363 $usernameInput.focus();_363 $usernameInput.on('keypress', handleUsernameInputKeypress);_363 $inputText.on('keypress', handleInputTextKeypress);_363 $newChannelInput.on('keypress', tc.handleNewChannelInputKeypress);_363 $('#connect-image').on('click', connectClientWithUsername);_363 $('#add-channel-image').on('click', showAddChannelInput);_363 $('#leave-span').on('click', disconnectClient);_363 $('#delete-channel-span').on('click', deleteCurrentChannel);_363 });_363_363 function handleUsernameInputKeypress(event) {_363 if (event.keyCode === 13){_363 connectClientWithUsername();_363 }_363 }_363_363 function handleInputTextKeypress(event) {_363 if (event.keyCode === 13) {_363 tc.currentChannel.sendMessage($(this).val());_363 event.preventDefault();_363 $(this).val('');_363 }_363 else {_363 notifyTyping();_363 }_363 }_363_363 var notifyTyping = $.throttle(function() {_363 tc.currentChannel.typing();_363 }, 1000);_363_363 tc.handleNewChannelInputKeypress = function(event) {_363 if (event.keyCode === 13) {_363 tc.messagingClient.createChannel({_363 friendlyName: $newChannelInput.val()_363 }).then(hideAddChannelInput);_363 $(this).val('');_363 event.preventDefault();_363 }_363 };_363_363 function connectClientWithUsername() {_363 var usernameText = $usernameInput.val();_363 $usernameInput.val('');_363 if (usernameText == '') {_363 alert('Username cannot be empty');_363 return;_363 }_363 tc.username = usernameText;_363 fetchAccessToken(tc.username, connectMessagingClient);_363 }_363_363 function fetchAccessToken(username, handler) {_363 $.post('/twiliochat-servlets/token', {identity: username}, null, 'json')_363 .done(function(response) {_363 console.log('Successfully finished fetch of the Access Token.');_363 handler(response.token);_363 })_363 .fail(function(error) {_363 console.log('Failed to fetch the Access Token with error: ' + error);_363 });_363 }_363_363 function connectMessagingClient(token) {_363 // Initialize the Chat messaging client_363 tc.accessManager = new Twilio.AccessManager(token);_363 Twilio.Chat.Client.create(token).then(function(client) {_363 tc.messagingClient = client;_363 updateConnectedUI();_363 tc.loadChannelList(tc.joinGeneralChannel);_363 tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));_363 tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));_363 tc.messagingClient.on('tokenExpired', refreshToken);_363 });_363 }_363_363 function refreshToken() {_363 fetchAccessToken(tc.username, setNewToken);_363 }_363_363 function setNewToken(tokenResponse) {_363 tc.accessManager.updateToken(tokenResponse.token);_363 }_363_363 function updateConnectedUI() {_363 $('#username-span').text(tc.username);_363 $statusRow.addClass('connected').removeClass('disconnected');_363 tc.$messageList.addClass('connected').removeClass('disconnected');_363 $connectPanel.addClass('connected').removeClass('disconnected');_363 $inputText.addClass('with-shadow');_363 $typingRow.addClass('connected').removeClass('disconnected');_363 }_363_363 tc.loadChannelList = function(handler) {_363 if (tc.messagingClient === undefined) {_363 console.log('Client is not initialized');_363 return;_363 }_363_363 tc.messagingClient.getPublicChannelDescriptors().then(function(channels) {_363 tc.channelArray = tc.sortChannelsByName(channels.items);_363 $channelList.text('');_363 tc.channelArray.forEach(addChannel);_363 if (typeof handler === 'function') {_363 handler();_363 }_363 });_363 };_363_363 tc.joinGeneralChannel = function() {_363 console.log('Attempting to join "general" chat channel...');_363 if (!tc.generalChannel) {_363 // If it doesn't exist, let's create it_363 tc.messagingClient.createChannel({_363 uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,_363 friendlyName: GENERAL_CHANNEL_NAME_363 }).then(function(channel) {_363 console.log('Created general channel');_363 tc.generalChannel = channel;_363 tc.loadChannelList(tc.joinGeneralChannel);_363 });_363 }_363 else {_363 console.log('Found general channel:');_363 setupChannel(tc.generalChannel);_363 }_363 };_363_363 function initChannel(channel) {_363 console.log('Initialized channel ' + channel.friendlyName);_363 return tc.messagingClient.getChannelBySid(channel.sid);_363 }_363_363 function joinChannel(_channel) {_363 return _channel.join()_363 .then(function(joinedChannel) {_363 console.log('Joined channel ' + joinedChannel.friendlyName);_363 updateChannelUI(_channel);_363 tc.currentChannel = _channel;_363 tc.loadMessages();_363 return joinedChannel;_363 });_363 }_363_363 function initChannelEvents() {_363 console.log(tc.currentChannel.friendlyName + ' ready.');_363 tc.currentChannel.on('messageAdded', tc.addMessageToList);_363 tc.currentChannel.on('typingStarted', showTypingStarted);_363 tc.currentChannel.on('typingEnded', hideTypingStarted);_363 tc.currentChannel.on('memberJoined', notifyMemberJoined);_363 tc.currentChannel.on('memberLeft', notifyMemberLeft);_363 $inputText.prop('disabled', false).focus();_363 }_363_363 function setupChannel(channel) {_363 return leaveCurrentChannel()_363 .then(function() {_363 return initChannel(channel);_363 })_363 .then(function(_channel) {_363 return joinChannel(_channel);_363 })_363 .then(initChannelEvents);_363 }_363_363 tc.loadMessages = function() {_363 tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT)_363 .then(function(messages) {_363 messages.items.forEach(tc.addMessageToList);_363 });_363 };_363_363 function leaveCurrentChannel() {_363 if (tc.currentChannel) {_363 return tc.currentChannel.leave().then(function(leftChannel) {_363 console.log('left ' + leftChannel.friendlyName);_363 leftChannel.removeListener('messageAdded', tc.addMessageToList);_363 leftChannel.removeListener('typingStarted', showTypingStarted);_363 leftChannel.removeListener('typingEnded', hideTypingStarted);_363 leftChannel.removeListener('memberJoined', notifyMemberJoined);_363 leftChannel.removeListener('memberLeft', notifyMemberLeft);_363 });_363 } else {_363 return Promise.resolve();_363 }_363 }_363_363 tc.addMessageToList = function(message) {_363 var rowDiv = $('<div>').addClass('row no-margin');_363 rowDiv.loadTemplate($('#message-template'), {_363 username: message.author,_363 date: dateFormatter.getTodayDate(message.dateCreated),_363 body: message.body_363 });_363 if (message.author === tc.username) {_363 rowDiv.addClass('own-message');_363 }_363_363 tc.$messageList.append(rowDiv);_363 scrollToMessageListBottom();_363 };_363_363 function notifyMemberJoined(member) {_363 notify(member.identity + ' joined the channel')_363 }_363_363 function notifyMemberLeft(member) {_363 notify(member.identity + ' left the channel');_363 }_363_363 function notify(message) {_363 var row = $('<div>').addClass('col-md-12');_363 row.loadTemplate('#member-notification-template', {_363 status: message_363 });_363 tc.$messageList.append(row);_363 scrollToMessageListBottom();_363 }_363_363 function showTypingStarted(member) {_363 $typingPlaceholder.text(member.identity + ' is typing...');_363 }_363_363 function hideTypingStarted(member) {_363 $typingPlaceholder.text('');_363 }_363_363 function scrollToMessageListBottom() {_363 tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);_363 }_363_363 function updateChannelUI(selectedChannel) {_363 var channelElements = $('.channel-element').toArray();_363 var channelElement = channelElements.filter(function(element) {_363 return $(element).data().sid === selectedChannel.sid;_363 });_363 channelElement = $(channelElement);_363 if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {_363 tc.currentChannelContainer = channelElement;_363 }_363 tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');_363 channelElement.removeClass('unselected-channel').addClass('selected-channel');_363 tc.currentChannelContainer = channelElement;_363 }_363_363 function showAddChannelInput() {_363 if (tc.messagingClient) {_363 $newChannelInputRow.addClass('showing').removeClass('not-showing');_363 $channelList.addClass('showing').removeClass('not-showing');_363 $newChannelInput.focus();_363 }_363 }_363_363 function hideAddChannelInput() {_363 $newChannelInputRow.addClass('not-showing').removeClass('showing');_363 $channelList.addClass('not-showing').removeClass('showing');_363 $newChannelInput.val('');_363 }_363_363 function addChannel(channel) {_363 if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {_363 tc.generalChannel = channel;_363 }_363 var rowDiv = $('<div>').addClass('row channel-row');_363 rowDiv.loadTemplate('#channel-template', {_363 channelName: channel.friendlyName_363 });_363_363 var channelP = rowDiv.children().children().first();_363_363 rowDiv.on('click', selectChannel);_363 channelP.data('sid', channel.sid);_363 if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {_363 tc.currentChannelContainer = channelP;_363 channelP.addClass('selected-channel');_363 }_363 else {_363 channelP.addClass('unselected-channel')_363 }_363_363 $channelList.append(rowDiv);_363 }_363_363 function deleteCurrentChannel() {_363 if (!tc.currentChannel) {_363 return;_363 }_363 if (tc.currentChannel.sid === tc.generalChannel.sid) {_363 alert('You cannot delete the general channel');_363 return;_363 }_363 tc.currentChannel.delete().then(function(channel) {_363 console.log('channel: '+ channel.friendlyName + ' deleted');_363 setupChannel(tc.generalChannel);_363 });_363 }_363_363 function selectChannel(event) {_363 var target = $(event.target);_363 var channelSid = target.data().sid;_363 var selectedChannel = tc.channelArray.filter(function(channel) {_363 return channel.sid === channelSid;_363 })[0];_363 if (selectedChannel === tc.currentChannel) {_363 return;_363 }_363 setupChannel(selectedChannel);_363 };_363_363 function disconnectClient() {_363 leaveCurrentChannel();_363 $channelList.text('');_363 tc.$messageList.text('');_363 channels = undefined;_363 $statusRow.addClass('disconnected').removeClass('connected');_363 tc.$messageList.addClass('disconnected').removeClass('connected');_363 $connectPanel.addClass('disconnected').removeClass('connected');_363 $inputText.removeClass('with-shadow');_363 $typingRow.addClass('disconnected').removeClass('connected');_363 }_363_363 tc.sortChannelsByName = function(channels) {_363 return channels.sort(function(a, b) {_363 if (a.friendlyName === GENERAL_CHANNEL_NAME) {_363 return -1;_363 }_363 if (b.friendlyName === GENERAL_CHANNEL_NAME) {_363 return 1;_363 }_363 return a.friendlyName.localeCompare(b.friendlyName);_363 });_363 };_363_363 return tc;_363})();
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
_363var twiliochat = (function() {_363 var tc = {};_363_363 var GENERAL_CHANNEL_UNIQUE_NAME = 'general';_363 var GENERAL_CHANNEL_NAME = 'General Channel';_363 var MESSAGES_HISTORY_LIMIT = 50;_363_363 var $channelList;_363 var $inputText;_363 var $usernameInput;_363 var $statusRow;_363 var $connectPanel;_363 var $newChannelInputRow;_363 var $newChannelInput;_363 var $typingRow;_363 var $typingPlaceholder;_363_363 $(document).ready(function() {_363 tc.$messageList = $('#message-list');_363 $channelList = $('#channel-list');_363 $inputText = $('#input-text');_363 $usernameInput = $('#username-input');_363 $statusRow = $('#status-row');_363 $connectPanel = $('#connect-panel');_363 $newChannelInputRow = $('#new-channel-input-row');_363 $newChannelInput = $('#new-channel-input');_363 $typingRow = $('#typing-row');_363 $typingPlaceholder = $('#typing-placeholder');_363 $usernameInput.focus();_363 $usernameInput.on('keypress', handleUsernameInputKeypress);_363 $inputText.on('keypress', handleInputTextKeypress);_363 $newChannelInput.on('keypress', tc.handleNewChannelInputKeypress);_363 $('#connect-image').on('click', connectClientWithUsername);_363 $('#add-channel-image').on('click', showAddChannelInput);_363 $('#leave-span').on('click', disconnectClient);_363 $('#delete-channel-span').on('click', deleteCurrentChannel);_363 });_363_363 function handleUsernameInputKeypress(event) {_363 if (event.keyCode === 13){_363 connectClientWithUsername();_363 }_363 }_363_363 function handleInputTextKeypress(event) {_363 if (event.keyCode === 13) {_363 tc.currentChannel.sendMessage($(this).val());_363 event.preventDefault();_363 $(this).val('');_363 }_363 else {_363 notifyTyping();_363 }_363 }_363_363 var notifyTyping = $.throttle(function() {_363 tc.currentChannel.typing();_363 }, 1000);_363_363 tc.handleNewChannelInputKeypress = function(event) {_363 if (event.keyCode === 13) {_363 tc.messagingClient.createChannel({_363 friendlyName: $newChannelInput.val()_363 }).then(hideAddChannelInput);_363 $(this).val('');_363 event.preventDefault();_363 }_363 };_363_363 function connectClientWithUsername() {_363 var usernameText = $usernameInput.val();_363 $usernameInput.val('');_363 if (usernameText == '') {_363 alert('Username cannot be empty');_363 return;_363 }_363 tc.username = usernameText;_363 fetchAccessToken(tc.username, connectMessagingClient);_363 }_363_363 function fetchAccessToken(username, handler) {_363 $.post('/twiliochat-servlets/token', {identity: username}, null, 'json')_363 .done(function(response) {_363 console.log('Successfully finished fetch of the Access Token.');_363 handler(response.token);_363 })_363 .fail(function(error) {_363 console.log('Failed to fetch the Access Token with error: ' + error);_363 });_363 }_363_363 function connectMessagingClient(token) {_363 // Initialize the Chat messaging client_363 tc.accessManager = new Twilio.AccessManager(token);_363 Twilio.Chat.Client.create(token).then(function(client) {_363 tc.messagingClient = client;_363 updateConnectedUI();_363 tc.loadChannelList(tc.joinGeneralChannel);_363 tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));_363 tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));_363 tc.messagingClient.on('tokenExpired', refreshToken);_363 });_363 }_363_363 function refreshToken() {_363 fetchAccessToken(tc.username, setNewToken);_363 }_363_363 function setNewToken(tokenResponse) {_363 tc.accessManager.updateToken(tokenResponse.token);_363 }_363_363 function updateConnectedUI() {_363 $('#username-span').text(tc.username);_363 $statusRow.addClass('connected').removeClass('disconnected');_363 tc.$messageList.addClass('connected').removeClass('disconnected');_363 $connectPanel.addClass('connected').removeClass('disconnected');_363 $inputText.addClass('with-shadow');_363 $typingRow.addClass('connected').removeClass('disconnected');_363 }_363_363 tc.loadChannelList = function(handler) {_363 if (tc.messagingClient === undefined) {_363 console.log('Client is not initialized');_363 return;_363 }_363_363 tc.messagingClient.getPublicChannelDescriptors().then(function(channels) {_363 tc.channelArray = tc.sortChannelsByName(channels.items);_363 $channelList.text('');_363 tc.channelArray.forEach(addChannel);_363 if (typeof handler === 'function') {_363 handler();_363 }_363 });_363 };_363_363 tc.joinGeneralChannel = function() {_363 console.log('Attempting to join "general" chat channel...');_363 if (!tc.generalChannel) {_363 // If it doesn't exist, let's create it_363 tc.messagingClient.createChannel({_363 uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,_363 friendlyName: GENERAL_CHANNEL_NAME_363 }).then(function(channel) {_363 console.log('Created general channel');_363 tc.generalChannel = channel;_363 tc.loadChannelList(tc.joinGeneralChannel);_363 });_363 }_363 else {_363 console.log('Found general channel:');_363 setupChannel(tc.generalChannel);_363 }_363 };_363_363 function initChannel(channel) {_363 console.log('Initialized channel ' + channel.friendlyName);_363 return tc.messagingClient.getChannelBySid(channel.sid);_363 }_363_363 function joinChannel(_channel) {_363 return _channel.join()_363 .then(function(joinedChannel) {_363 console.log('Joined channel ' + joinedChannel.friendlyName);_363 updateChannelUI(_channel);_363 tc.currentChannel = _channel;_363 tc.loadMessages();_363 return joinedChannel;_363 });_363 }_363_363 function initChannelEvents() {_363 console.log(tc.currentChannel.friendlyName + ' ready.');_363 tc.currentChannel.on('messageAdded', tc.addMessageToList);_363 tc.currentChannel.on('typingStarted', showTypingStarted);_363 tc.currentChannel.on('typingEnded', hideTypingStarted);_363 tc.currentChannel.on('memberJoined', notifyMemberJoined);_363 tc.currentChannel.on('memberLeft', notifyMemberLeft);_363 $inputText.prop('disabled', false).focus();_363 }_363_363 function setupChannel(channel) {_363 return leaveCurrentChannel()_363 .then(function() {_363 return initChannel(channel);_363 })_363 .then(function(_channel) {_363 return joinChannel(_channel);_363 })_363 .then(initChannelEvents);_363 }_363_363 tc.loadMessages = function() {_363 tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT)_363 .then(function(messages) {_363 messages.items.forEach(tc.addMessageToList);_363 });_363 };_363_363 function leaveCurrentChannel() {_363 if (tc.currentChannel) {_363 return tc.currentChannel.leave().then(function(leftChannel) {_363 console.log('left ' + leftChannel.friendlyName);_363 leftChannel.removeListener('messageAdded', tc.addMessageToList);_363 leftChannel.removeListener('typingStarted', showTypingStarted);_363 leftChannel.removeListener('typingEnded', hideTypingStarted);_363 leftChannel.removeListener('memberJoined', notifyMemberJoined);_363 leftChannel.removeListener('memberLeft', notifyMemberLeft);_363 });_363 } else {_363 return Promise.resolve();_363 }_363 }_363_363 tc.addMessageToList = function(message) {_363 var rowDiv = $('<div>').addClass('row no-margin');_363 rowDiv.loadTemplate($('#message-template'), {_363 username: message.author,_363 date: dateFormatter.getTodayDate(message.dateCreated),_363 body: message.body_363 });_363 if (message.author === tc.username) {_363 rowDiv.addClass('own-message');_363 }_363_363 tc.$messageList.append(rowDiv);_363 scrollToMessageListBottom();_363 };_363_363 function notifyMemberJoined(member) {_363 notify(member.identity + ' joined the channel')_363 }_363_363 function notifyMemberLeft(member) {_363 notify(member.identity + ' left the channel');_363 }_363_363 function notify(message) {_363 var row = $('<div>').addClass('col-md-12');_363 row.loadTemplate('#member-notification-template', {_363 status: message_363 });_363 tc.$messageList.append(row);_363 scrollToMessageListBottom();_363 }_363_363 function showTypingStarted(member) {_363 $typingPlaceholder.text(member.identity + ' is typing...');_363 }_363_363 function hideTypingStarted(member) {_363 $typingPlaceholder.text('');_363 }_363_363 function scrollToMessageListBottom() {_363 tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);_363 }_363_363 function updateChannelUI(selectedChannel) {_363 var channelElements = $('.channel-element').toArray();_363 var channelElement = channelElements.filter(function(element) {_363 return $(element).data().sid === selectedChannel.sid;_363 });_363 channelElement = $(channelElement);_363 if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {_363 tc.currentChannelContainer = channelElement;_363 }_363 tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');_363 channelElement.removeClass('unselected-channel').addClass('selected-channel');_363 tc.currentChannelContainer = channelElement;_363 }_363_363 function showAddChannelInput() {_363 if (tc.messagingClient) {_363 $newChannelInputRow.addClass('showing').removeClass('not-showing');_363 $channelList.addClass('showing').removeClass('not-showing');_363 $newChannelInput.focus();_363 }_363 }_363_363 function hideAddChannelInput() {_363 $newChannelInputRow.addClass('not-showing').removeClass('showing');_363 $channelList.addClass('not-showing').removeClass('showing');_363 $newChannelInput.val('');_363 }_363_363 function addChannel(channel) {_363 if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {_363 tc.generalChannel = channel;_363 }_363 var rowDiv = $('<div>').addClass('row channel-row');_363 rowDiv.loadTemplate('#channel-template', {_363 channelName: channel.friendlyName_363 });_363_363 var channelP = rowDiv.children().children().first();_363_363 rowDiv.on('click', selectChannel);_363 channelP.data('sid', channel.sid);_363 if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {_363 tc.currentChannelContainer = channelP;_363 channelP.addClass('selected-channel');_363 }_363 else {_363 channelP.addClass('unselected-channel')_363 }_363_363 $channelList.append(rowDiv);_363 }_363_363 function deleteCurrentChannel() {_363 if (!tc.currentChannel) {_363 return;_363 }_363 if (tc.currentChannel.sid === tc.generalChannel.sid) {_363 alert('You cannot delete the general channel');_363 return;_363 }_363 tc.currentChannel.delete().then(function(channel) {_363 console.log('channel: '+ channel.friendlyName + ' deleted');_363 setupChannel(tc.generalChannel);_363 });_363 }_363_363 function selectChannel(event) {_363 var target = $(event.target);_363 var channelSid = target.data().sid;_363 var selectedChannel = tc.channelArray.filter(function(channel) {_363 return channel.sid === channelSid;_363 })[0];_363 if (selectedChannel === tc.currentChannel) {_363 return;_363 }_363 setupChannel(selectedChannel);_363 };_363_363 function disconnectClient() {_363 leaveCurrentChannel();_363 $channelList.text('');_363 tc.$messageList.text('');_363 channels = undefined;_363 $statusRow.addClass('disconnected').removeClass('connected');_363 tc.$messageList.addClass('disconnected').removeClass('connected');_363 $connectPanel.addClass('disconnected').removeClass('connected');_363 $inputText.removeClass('with-shadow');_363 $typingRow.addClass('disconnected').removeClass('connected');_363 }_363_363 tc.sortChannelsByName = function(channels) {_363 return channels.sort(function(a, b) {_363 if (a.friendlyName === GENERAL_CHANNEL_NAME) {_363 return -1;_363 }_363 if (b.friendlyName === GENERAL_CHANNEL_NAME) {_363 return 1;_363 }_363 return a.friendlyName.localeCompare(b.friendlyName);_363 });_363 };_363_363 return tc;_363})();
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 is as simple as calling createChannel
with an object that has the friendlyName
key. You can create a channel with more options listed on the Channels section of the Programmable Chat documentation.
src/main/webapp/js/twiliochat.js
_363var twiliochat = (function() {_363 var tc = {};_363_363 var GENERAL_CHANNEL_UNIQUE_NAME = 'general';_363 var GENERAL_CHANNEL_NAME = 'General Channel';_363 var MESSAGES_HISTORY_LIMIT = 50;_363_363 var $channelList;_363 var $inputText;_363 var $usernameInput;_363 var $statusRow;_363 var $connectPanel;_363 var $newChannelInputRow;_363 var $newChannelInput;_363 var $typingRow;_363 var $typingPlaceholder;_363_363 $(document).ready(function() {_363 tc.$messageList = $('#message-list');_363 $channelList = $('#channel-list');_363 $inputText = $('#input-text');_363 $usernameInput = $('#username-input');_363 $statusRow = $('#status-row');_363 $connectPanel = $('#connect-panel');_363 $newChannelInputRow = $('#new-channel-input-row');_363 $newChannelInput = $('#new-channel-input');_363 $typingRow = $('#typing-row');_363 $typingPlaceholder = $('#typing-placeholder');_363 $usernameInput.focus();_363 $usernameInput.on('keypress', handleUsernameInputKeypress);_363 $inputText.on('keypress', handleInputTextKeypress);_363 $newChannelInput.on('keypress', tc.handleNewChannelInputKeypress);_363 $('#connect-image').on('click', connectClientWithUsername);_363 $('#add-channel-image').on('click', showAddChannelInput);_363 $('#leave-span').on('click', disconnectClient);_363 $('#delete-channel-span').on('click', deleteCurrentChannel);_363 });_363_363 function handleUsernameInputKeypress(event) {_363 if (event.keyCode === 13){_363 connectClientWithUsername();_363 }_363 }_363_363 function handleInputTextKeypress(event) {_363 if (event.keyCode === 13) {_363 tc.currentChannel.sendMessage($(this).val());_363 event.preventDefault();_363 $(this).val('');_363 }_363 else {_363 notifyTyping();_363 }_363 }_363_363 var notifyTyping = $.throttle(function() {_363 tc.currentChannel.typing();_363 }, 1000);_363_363 tc.handleNewChannelInputKeypress = function(event) {_363 if (event.keyCode === 13) {_363 tc.messagingClient.createChannel({_363 friendlyName: $newChannelInput.val()_363 }).then(hideAddChannelInput);_363 $(this).val('');_363 event.preventDefault();_363 }_363 };_363_363 function connectClientWithUsername() {_363 var usernameText = $usernameInput.val();_363 $usernameInput.val('');_363 if (usernameText == '') {_363 alert('Username cannot be empty');_363 return;_363 }_363 tc.username = usernameText;_363 fetchAccessToken(tc.username, connectMessagingClient);_363 }_363_363 function fetchAccessToken(username, handler) {_363 $.post('/twiliochat-servlets/token', {identity: username}, null, 'json')_363 .done(function(response) {_363 console.log('Successfully finished fetch of the Access Token.');_363 handler(response.token);_363 })_363 .fail(function(error) {_363 console.log('Failed to fetch the Access Token with error: ' + error);_363 });_363 }_363_363 function connectMessagingClient(token) {_363 // Initialize the Chat messaging client_363 tc.accessManager = new Twilio.AccessManager(token);_363 Twilio.Chat.Client.create(token).then(function(client) {_363 tc.messagingClient = client;_363 updateConnectedUI();_363 tc.loadChannelList(tc.joinGeneralChannel);_363 tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));_363 tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));_363 tc.messagingClient.on('tokenExpired', refreshToken);_363 });_363 }_363_363 function refreshToken() {_363 fetchAccessToken(tc.username, setNewToken);_363 }_363_363 function setNewToken(tokenResponse) {_363 tc.accessManager.updateToken(tokenResponse.token);_363 }_363_363 function updateConnectedUI() {_363 $('#username-span').text(tc.username);_363 $statusRow.addClass('connected').removeClass('disconnected');_363 tc.$messageList.addClass('connected').removeClass('disconnected');_363 $connectPanel.addClass('connected').removeClass('disconnected');_363 $inputText.addClass('with-shadow');_363 $typingRow.addClass('connected').removeClass('disconnected');_363 }_363_363 tc.loadChannelList = function(handler) {_363 if (tc.messagingClient === undefined) {_363 console.log('Client is not initialized');_363 return;_363 }_363_363 tc.messagingClient.getPublicChannelDescriptors().then(function(channels) {_363 tc.channelArray = tc.sortChannelsByName(channels.items);_363 $channelList.text('');_363 tc.channelArray.forEach(addChannel);_363 if (typeof handler === 'function') {_363 handler();_363 }_363 });_363 };_363_363 tc.joinGeneralChannel = function() {_363 console.log('Attempting to join "general" chat channel...');_363 if (!tc.generalChannel) {_363 // If it doesn't exist, let's create it_363 tc.messagingClient.createChannel({_363 uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,_363 friendlyName: GENERAL_CHANNEL_NAME_363 }).then(function(channel) {_363 console.log('Created general channel');_363 tc.generalChannel = channel;_363 tc.loadChannelList(tc.joinGeneralChannel);_363 });_363 }_363 else {_363 console.log('Found general channel:');_363 setupChannel(tc.generalChannel);_363 }_363 };_363_363 function initChannel(channel) {_363 console.log('Initialized channel ' + channel.friendlyName);_363 return tc.messagingClient.getChannelBySid(channel.sid);_363 }_363_363 function joinChannel(_channel) {_363 return _channel.join()_363 .then(function(joinedChannel) {_363 console.log('Joined channel ' + joinedChannel.friendlyName);_363 updateChannelUI(_channel);_363 tc.currentChannel = _channel;_363 tc.loadMessages();_363 return joinedChannel;_363 });_363 }_363_363 function initChannelEvents() {_363 console.log(tc.currentChannel.friendlyName + ' ready.');_363 tc.currentChannel.on('messageAdded', tc.addMessageToList);_363 tc.currentChannel.on('typingStarted', showTypingStarted);_363 tc.currentChannel.on('typingEnded', hideTypingStarted);_363 tc.currentChannel.on('memberJoined', notifyMemberJoined);_363 tc.currentChannel.on('memberLeft', notifyMemberLeft);_363 $inputText.prop('disabled', false).focus();_363 }_363_363 function setupChannel(channel) {_363 return leaveCurrentChannel()_363 .then(function() {_363 return initChannel(channel);_363 })_363 .then(function(_channel) {_363 return joinChannel(_channel);_363 })_363 .then(initChannelEvents);_363 }_363_363 tc.loadMessages = function() {_363 tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT)_363 .then(function(messages) {_363 messages.items.forEach(tc.addMessageToList);_363 });_363 };_363_363 function leaveCurrentChannel() {_363 if (tc.currentChannel) {_363 return tc.currentChannel.leave().then(function(leftChannel) {_363 console.log('left ' + leftChannel.friendlyName);_363 leftChannel.removeListener('messageAdded', tc.addMessageToList);_363 leftChannel.removeListener('typingStarted', showTypingStarted);_363 leftChannel.removeListener('typingEnded', hideTypingStarted);_363 leftChannel.removeListener('memberJoined', notifyMemberJoined);_363 leftChannel.removeListener('memberLeft', notifyMemberLeft);_363 });_363 } else {_363 return Promise.resolve();_363 }_363 }_363_363 tc.addMessageToList = function(message) {_363 var rowDiv = $('<div>').addClass('row no-margin');_363 rowDiv.loadTemplate($('#message-template'), {_363 username: message.author,_363 date: dateFormatter.getTodayDate(message.dateCreated),_363 body: message.body_363 });_363 if (message.author === tc.username) {_363 rowDiv.addClass('own-message');_363 }_363_363 tc.$messageList.append(rowDiv);_363 scrollToMessageListBottom();_363 };_363_363 function notifyMemberJoined(member) {_363 notify(member.identity + ' joined the channel')_363 }_363_363 function notifyMemberLeft(member) {_363 notify(member.identity + ' left the channel');_363 }_363_363 function notify(message) {_363 var row = $('<div>').addClass('col-md-12');_363 row.loadTemplate('#member-notification-template', {_363 status: message_363 });_363 tc.$messageList.append(row);_363 scrollToMessageListBottom();_363 }_363_363 function showTypingStarted(member) {_363 $typingPlaceholder.text(member.identity + ' is typing...');_363 }_363_363 function hideTypingStarted(member) {_363 $typingPlaceholder.text('');_363 }_363_363 function scrollToMessageListBottom() {_363 tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);_363 }_363_363 function updateChannelUI(selectedChannel) {_363 var channelElements = $('.channel-element').toArray();_363 var channelElement = channelElements.filter(function(element) {_363 return $(element).data().sid === selectedChannel.sid;_363 });_363 channelElement = $(channelElement);_363 if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {_363 tc.currentChannelContainer = channelElement;_363 }_363 tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');_363 channelElement.removeClass('unselected-channel').addClass('selected-channel');_363 tc.currentChannelContainer = channelElement;_363 }_363_363 function showAddChannelInput() {_363 if (tc.messagingClient) {_363 $newChannelInputRow.addClass('showing').removeClass('not-showing');_363 $channelList.addClass('showing').removeClass('not-showing');_363 $newChannelInput.focus();_363 }_363 }_363_363 function hideAddChannelInput() {_363 $newChannelInputRow.addClass('not-showing').removeClass('showing');_363 $channelList.addClass('not-showing').removeClass('showing');_363 $newChannelInput.val('');_363 }_363_363 function addChannel(channel) {_363 if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {_363 tc.generalChannel = channel;_363 }_363 var rowDiv = $('<div>').addClass('row channel-row');_363 rowDiv.loadTemplate('#channel-template', {_363 channelName: channel.friendlyName_363 });_363_363 var channelP = rowDiv.children().children().first();_363_363 rowDiv.on('click', selectChannel);_363 channelP.data('sid', channel.sid);_363 if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {_363 tc.currentChannelContainer = channelP;_363 channelP.addClass('selected-channel');_363 }_363 else {_363 channelP.addClass('unselected-channel')_363 }_363_363 $channelList.append(rowDiv);_363 }_363_363 function deleteCurrentChannel() {_363 if (!tc.currentChannel) {_363 return;_363 }_363 if (tc.currentChannel.sid === tc.generalChannel.sid) {_363 alert('You cannot delete the general channel');_363 return;_363 }_363 tc.currentChannel.delete().then(function(channel) {_363 console.log('channel: '+ channel.friendlyName + ' deleted');_363 setupChannel(tc.generalChannel);_363 });_363 }_363_363 function selectChannel(event) {_363 var target = $(event.target);_363 var channelSid = target.data().sid;_363 var selectedChannel = tc.channelArray.filter(function(channel) {_363 return channel.sid === channelSid;_363 })[0];_363 if (selectedChannel === tc.currentChannel) {_363 return;_363 }_363 setupChannel(selectedChannel);_363 };_363_363 function disconnectClient() {_363 leaveCurrentChannel();_363 $channelList.text('');_363 tc.$messageList.text('');_363 channels = undefined;_363 $statusRow.addClass('disconnected').removeClass('connected');_363 tc.$messageList.addClass('disconnected').removeClass('connected');_363 $connectPanel.addClass('disconnected').removeClass('connected');_363 $inputText.removeClass('with-shadow');_363 $typingRow.addClass('disconnected').removeClass('connected');_363 }_363_363 tc.sortChannelsByName = function(channels) {_363 return channels.sort(function(a, b) {_363 if (a.friendlyName === GENERAL_CHANNEL_NAME) {_363 return -1;_363 }_363 if (b.friendlyName === GENERAL_CHANNEL_NAME) {_363 return 1;_363 }_363 return a.friendlyName.localeCompare(b.friendlyName);_363 });_363 };_363_363 return tc;_363})();
Next, we will see how we can switch between channels.
When you tap on the name of a channel from the sidebar, that channel is set as the selectedChannel
. The selectChannel
method takes care of joining to the selected channel and setting up the selectedChannel
.
src/main/webapp/js/twiliochat.js
_363var twiliochat = (function() {_363 var tc = {};_363_363 var GENERAL_CHANNEL_UNIQUE_NAME = 'general';_363 var GENERAL_CHANNEL_NAME = 'General Channel';_363 var MESSAGES_HISTORY_LIMIT = 50;_363_363 var $channelList;_363 var $inputText;_363 var $usernameInput;_363 var $statusRow;_363 var $connectPanel;_363 var $newChannelInputRow;_363 var $newChannelInput;_363 var $typingRow;_363 var $typingPlaceholder;_363_363 $(document).ready(function() {_363 tc.$messageList = $('#message-list');_363 $channelList = $('#channel-list');_363 $inputText = $('#input-text');_363 $usernameInput = $('#username-input');_363 $statusRow = $('#status-row');_363 $connectPanel = $('#connect-panel');_363 $newChannelInputRow = $('#new-channel-input-row');_363 $newChannelInput = $('#new-channel-input');_363 $typingRow = $('#typing-row');_363 $typingPlaceholder = $('#typing-placeholder');_363 $usernameInput.focus();_363 $usernameInput.on('keypress', handleUsernameInputKeypress);_363 $inputText.on('keypress', handleInputTextKeypress);_363 $newChannelInput.on('keypress', tc.handleNewChannelInputKeypress);_363 $('#connect-image').on('click', connectClientWithUsername);_363 $('#add-channel-image').on('click', showAddChannelInput);_363 $('#leave-span').on('click', disconnectClient);_363 $('#delete-channel-span').on('click', deleteCurrentChannel);_363 });_363_363 function handleUsernameInputKeypress(event) {_363 if (event.keyCode === 13){_363 connectClientWithUsername();_363 }_363 }_363_363 function handleInputTextKeypress(event) {_363 if (event.keyCode === 13) {_363 tc.currentChannel.sendMessage($(this).val());_363 event.preventDefault();_363 $(this).val('');_363 }_363 else {_363 notifyTyping();_363 }_363 }_363_363 var notifyTyping = $.throttle(function() {_363 tc.currentChannel.typing();_363 }, 1000);_363_363 tc.handleNewChannelInputKeypress = function(event) {_363 if (event.keyCode === 13) {_363 tc.messagingClient.createChannel({_363 friendlyName: $newChannelInput.val()_363 }).then(hideAddChannelInput);_363 $(this).val('');_363 event.preventDefault();_363 }_363 };_363_363 function connectClientWithUsername() {_363 var usernameText = $usernameInput.val();_363 $usernameInput.val('');_363 if (usernameText == '') {_363 alert('Username cannot be empty');_363 return;_363 }_363 tc.username = usernameText;_363 fetchAccessToken(tc.username, connectMessagingClient);_363 }_363_363 function fetchAccessToken(username, handler) {_363 $.post('/twiliochat-servlets/token', {identity: username}, null, 'json')_363 .done(function(response) {_363 console.log('Successfully finished fetch of the Access Token.');_363 handler(response.token);_363 })_363 .fail(function(error) {_363 console.log('Failed to fetch the Access Token with error: ' + error);_363 });_363 }_363_363 function connectMessagingClient(token) {_363 // Initialize the Chat messaging client_363 tc.accessManager = new Twilio.AccessManager(token);_363 Twilio.Chat.Client.create(token).then(function(client) {_363 tc.messagingClient = client;_363 updateConnectedUI();_363 tc.loadChannelList(tc.joinGeneralChannel);_363 tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));_363 tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));_363 tc.messagingClient.on('tokenExpired', refreshToken);_363 });_363 }_363_363 function refreshToken() {_363 fetchAccessToken(tc.username, setNewToken);_363 }_363_363 function setNewToken(tokenResponse) {_363 tc.accessManager.updateToken(tokenResponse.token);_363 }_363_363 function updateConnectedUI() {_363 $('#username-span').text(tc.username);_363 $statusRow.addClass('connected').removeClass('disconnected');_363 tc.$messageList.addClass('connected').removeClass('disconnected');_363 $connectPanel.addClass('connected').removeClass('disconnected');_363 $inputText.addClass('with-shadow');_363 $typingRow.addClass('connected').removeClass('disconnected');_363 }_363_363 tc.loadChannelList = function(handler) {_363 if (tc.messagingClient === undefined) {_363 console.log('Client is not initialized');_363 return;_363 }_363_363 tc.messagingClient.getPublicChannelDescriptors().then(function(channels) {_363 tc.channelArray = tc.sortChannelsByName(channels.items);_363 $channelList.text('');_363 tc.channelArray.forEach(addChannel);_363 if (typeof handler === 'function') {_363 handler();_363 }_363 });_363 };_363_363 tc.joinGeneralChannel = function() {_363 console.log('Attempting to join "general" chat channel...');_363 if (!tc.generalChannel) {_363 // If it doesn't exist, let's create it_363 tc.messagingClient.createChannel({_363 uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,_363 friendlyName: GENERAL_CHANNEL_NAME_363 }).then(function(channel) {_363 console.log('Created general channel');_363 tc.generalChannel = channel;_363 tc.loadChannelList(tc.joinGeneralChannel);_363 });_363 }_363 else {_363 console.log('Found general channel:');_363 setupChannel(tc.generalChannel);_363 }_363 };_363_363 function initChannel(channel) {_363 console.log('Initialized channel ' + channel.friendlyName);_363 return tc.messagingClient.getChannelBySid(channel.sid);_363 }_363_363 function joinChannel(_channel) {_363 return _channel.join()_363 .then(function(joinedChannel) {_363 console.log('Joined channel ' + joinedChannel.friendlyName);_363 updateChannelUI(_channel);_363 tc.currentChannel = _channel;_363 tc.loadMessages();_363 return joinedChannel;_363 });_363 }_363_363 function initChannelEvents() {_363 console.log(tc.currentChannel.friendlyName + ' ready.');_363 tc.currentChannel.on('messageAdded', tc.addMessageToList);_363 tc.currentChannel.on('typingStarted', showTypingStarted);_363 tc.currentChannel.on('typingEnded', hideTypingStarted);_363 tc.currentChannel.on('memberJoined', notifyMemberJoined);_363 tc.currentChannel.on('memberLeft', notifyMemberLeft);_363 $inputText.prop('disabled', false).focus();_363 }_363_363 function setupChannel(channel) {_363 return leaveCurrentChannel()_363 .then(function() {_363 return initChannel(channel);_363 })_363 .then(function(_channel) {_363 return joinChannel(_channel);_363 })_363 .then(initChannelEvents);_363 }_363_363 tc.loadMessages = function() {_363 tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT)_363 .then(function(messages) {_363 messages.items.forEach(tc.addMessageToList);_363 });_363 };_363_363 function leaveCurrentChannel() {_363 if (tc.currentChannel) {_363 return tc.currentChannel.leave().then(function(leftChannel) {_363 console.log('left ' + leftChannel.friendlyName);_363 leftChannel.removeListener('messageAdded', tc.addMessageToList);_363 leftChannel.removeListener('typingStarted', showTypingStarted);_363 leftChannel.removeListener('typingEnded', hideTypingStarted);_363 leftChannel.removeListener('memberJoined', notifyMemberJoined);_363 leftChannel.removeListener('memberLeft', notifyMemberLeft);_363 });_363 } else {_363 return Promise.resolve();_363 }_363 }_363_363 tc.addMessageToList = function(message) {_363 var rowDiv = $('<div>').addClass('row no-margin');_363 rowDiv.loadTemplate($('#message-template'), {_363 username: message.author,_363 date: dateFormatter.getTodayDate(message.dateCreated),_363 body: message.body_363 });_363 if (message.author === tc.username) {_363 rowDiv.addClass('own-message');_363 }_363_363 tc.$messageList.append(rowDiv);_363 scrollToMessageListBottom();_363 };_363_363 function notifyMemberJoined(member) {_363 notify(member.identity + ' joined the channel')_363 }_363_363 function notifyMemberLeft(member) {_363 notify(member.identity + ' left the channel');_363 }_363_363 function notify(message) {_363 var row = $('<div>').addClass('col-md-12');_363 row.loadTemplate('#member-notification-template', {_363 status: message_363 });_363 tc.$messageList.append(row);_363 scrollToMessageListBottom();_363 }_363_363 function showTypingStarted(member) {_363 $typingPlaceholder.text(member.identity + ' is typing...');_363 }_363_363 function hideTypingStarted(member) {_363 $typingPlaceholder.text('');_363 }_363_363 function scrollToMessageListBottom() {_363 tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);_363 }_363_363 function updateChannelUI(selectedChannel) {_363 var channelElements = $('.channel-element').toArray();_363 var channelElement = channelElements.filter(function(element) {_363 return $(element).data().sid === selectedChannel.sid;_363 });_363 channelElement = $(channelElement);_363 if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {_363 tc.currentChannelContainer = channelElement;_363 }_363 tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');_363 channelElement.removeClass('unselected-channel').addClass('selected-channel');_363 tc.currentChannelContainer = channelElement;_363 }_363_363 function showAddChannelInput() {_363 if (tc.messagingClient) {_363 $newChannelInputRow.addClass('showing').removeClass('not-showing');_363 $channelList.addClass('showing').removeClass('not-showing');_363 $newChannelInput.focus();_363 }_363 }_363_363 function hideAddChannelInput() {_363 $newChannelInputRow.addClass('not-showing').removeClass('showing');_363 $channelList.addClass('not-showing').removeClass('showing');_363 $newChannelInput.val('');_363 }_363_363 function addChannel(channel) {_363 if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {_363 tc.generalChannel = channel;_363 }_363 var rowDiv = $('<div>').addClass('row channel-row');_363 rowDiv.loadTemplate('#channel-template', {_363 channelName: channel.friendlyName_363 });_363_363 var channelP = rowDiv.children().children().first();_363_363 rowDiv.on('click', selectChannel);_363 channelP.data('sid', channel.sid);_363 if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {_363 tc.currentChannelContainer = channelP;_363 channelP.addClass('selected-channel');_363 }_363 else {_363 channelP.addClass('unselected-channel')_363 }_363_363 $channelList.append(rowDiv);_363 }_363_363 function deleteCurrentChannel() {_363 if (!tc.currentChannel) {_363 return;_363 }_363 if (tc.currentChannel.sid === tc.generalChannel.sid) {_363 alert('You cannot delete the general channel');_363 return;_363 }_363 tc.currentChannel.delete().then(function(channel) {_363 console.log('channel: '+ channel.friendlyName + ' deleted');_363 setupChannel(tc.generalChannel);_363 });_363 }_363_363 function selectChannel(event) {_363 var target = $(event.target);_363 var channelSid = target.data().sid;_363 var selectedChannel = tc.channelArray.filter(function(channel) {_363 return channel.sid === channelSid;_363 })[0];_363 if (selectedChannel === tc.currentChannel) {_363 return;_363 }_363 setupChannel(selectedChannel);_363 };_363_363 function disconnectClient() {_363 leaveCurrentChannel();_363 $channelList.text('');_363 tc.$messageList.text('');_363 channels = undefined;_363 $statusRow.addClass('disconnected').removeClass('connected');_363 tc.$messageList.addClass('disconnected').removeClass('connected');_363 $connectPanel.addClass('disconnected').removeClass('connected');_363 $inputText.removeClass('with-shadow');_363 $typingRow.addClass('disconnected').removeClass('connected');_363 }_363_363 tc.sortChannelsByName = function(channels) {_363 return channels.sort(function(a, b) {_363 if (a.friendlyName === GENERAL_CHANNEL_NAME) {_363 return -1;_363 }_363 if (b.friendlyName === GENERAL_CHANNEL_NAME) {_363 return 1;_363 }_363 return a.friendlyName.localeCompare(b.friendlyName);_363 });_363 };_363_363 return tc;_363})();
At some point, your users will want to delete a channel. Let's have a look at how that can be done.
Deleting a channel is even more simple than creating one. The application lets the user delete the channel they are currently joining through the "delete current channel" link. The only thing you need to do to actually delete the channel from Twilio is call the delete
method on the channel you are trying to delete. Like other methods on the Channel
object, it'll return a promise where you can set the success handler.
src/main/webapp/js/twiliochat.js
_363var twiliochat = (function() {_363 var tc = {};_363_363 var GENERAL_CHANNEL_UNIQUE_NAME = 'general';_363 var GENERAL_CHANNEL_NAME = 'General Channel';_363 var MESSAGES_HISTORY_LIMIT = 50;_363_363 var $channelList;_363 var $inputText;_363 var $usernameInput;_363 var $statusRow;_363 var $connectPanel;_363 var $newChannelInputRow;_363 var $newChannelInput;_363 var $typingRow;_363 var $typingPlaceholder;_363_363 $(document).ready(function() {_363 tc.$messageList = $('#message-list');_363 $channelList = $('#channel-list');_363 $inputText = $('#input-text');_363 $usernameInput = $('#username-input');_363 $statusRow = $('#status-row');_363 $connectPanel = $('#connect-panel');_363 $newChannelInputRow = $('#new-channel-input-row');_363 $newChannelInput = $('#new-channel-input');_363 $typingRow = $('#typing-row');_363 $typingPlaceholder = $('#typing-placeholder');_363 $usernameInput.focus();_363 $usernameInput.on('keypress', handleUsernameInputKeypress);_363 $inputText.on('keypress', handleInputTextKeypress);_363 $newChannelInput.on('keypress', tc.handleNewChannelInputKeypress);_363 $('#connect-image').on('click', connectClientWithUsername);_363 $('#add-channel-image').on('click', showAddChannelInput);_363 $('#leave-span').on('click', disconnectClient);_363 $('#delete-channel-span').on('click', deleteCurrentChannel);_363 });_363_363 function handleUsernameInputKeypress(event) {_363 if (event.keyCode === 13){_363 connectClientWithUsername();_363 }_363 }_363_363 function handleInputTextKeypress(event) {_363 if (event.keyCode === 13) {_363 tc.currentChannel.sendMessage($(this).val());_363 event.preventDefault();_363 $(this).val('');_363 }_363 else {_363 notifyTyping();_363 }_363 }_363_363 var notifyTyping = $.throttle(function() {_363 tc.currentChannel.typing();_363 }, 1000);_363_363 tc.handleNewChannelInputKeypress = function(event) {_363 if (event.keyCode === 13) {_363 tc.messagingClient.createChannel({_363 friendlyName: $newChannelInput.val()_363 }).then(hideAddChannelInput);_363 $(this).val('');_363 event.preventDefault();_363 }_363 };_363_363 function connectClientWithUsername() {_363 var usernameText = $usernameInput.val();_363 $usernameInput.val('');_363 if (usernameText == '') {_363 alert('Username cannot be empty');_363 return;_363 }_363 tc.username = usernameText;_363 fetchAccessToken(tc.username, connectMessagingClient);_363 }_363_363 function fetchAccessToken(username, handler) {_363 $.post('/twiliochat-servlets/token', {identity: username}, null, 'json')_363 .done(function(response) {_363 console.log('Successfully finished fetch of the Access Token.');_363 handler(response.token);_363 })_363 .fail(function(error) {_363 console.log('Failed to fetch the Access Token with error: ' + error);_363 });_363 }_363_363 function connectMessagingClient(token) {_363 // Initialize the Chat messaging client_363 tc.accessManager = new Twilio.AccessManager(token);_363 Twilio.Chat.Client.create(token).then(function(client) {_363 tc.messagingClient = client;_363 updateConnectedUI();_363 tc.loadChannelList(tc.joinGeneralChannel);_363 tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));_363 tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));_363 tc.messagingClient.on('tokenExpired', refreshToken);_363 });_363 }_363_363 function refreshToken() {_363 fetchAccessToken(tc.username, setNewToken);_363 }_363_363 function setNewToken(tokenResponse) {_363 tc.accessManager.updateToken(tokenResponse.token);_363 }_363_363 function updateConnectedUI() {_363 $('#username-span').text(tc.username);_363 $statusRow.addClass('connected').removeClass('disconnected');_363 tc.$messageList.addClass('connected').removeClass('disconnected');_363 $connectPanel.addClass('connected').removeClass('disconnected');_363 $inputText.addClass('with-shadow');_363 $typingRow.addClass('connected').removeClass('disconnected');_363 }_363_363 tc.loadChannelList = function(handler) {_363 if (tc.messagingClient === undefined) {_363 console.log('Client is not initialized');_363 return;_363 }_363_363 tc.messagingClient.getPublicChannelDescriptors().then(function(channels) {_363 tc.channelArray = tc.sortChannelsByName(channels.items);_363 $channelList.text('');_363 tc.channelArray.forEach(addChannel);_363 if (typeof handler === 'function') {_363 handler();_363 }_363 });_363 };_363_363 tc.joinGeneralChannel = function() {_363 console.log('Attempting to join "general" chat channel...');_363 if (!tc.generalChannel) {_363 // If it doesn't exist, let's create it_363 tc.messagingClient.createChannel({_363 uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,_363 friendlyName: GENERAL_CHANNEL_NAME_363 }).then(function(channel) {_363 console.log('Created general channel');_363 tc.generalChannel = channel;_363 tc.loadChannelList(tc.joinGeneralChannel);_363 });_363 }_363 else {_363 console.log('Found general channel:');_363 setupChannel(tc.generalChannel);_363 }_363 };_363_363 function initChannel(channel) {_363 console.log('Initialized channel ' + channel.friendlyName);_363 return tc.messagingClient.getChannelBySid(channel.sid);_363 }_363_363 function joinChannel(_channel) {_363 return _channel.join()_363 .then(function(joinedChannel) {_363 console.log('Joined channel ' + joinedChannel.friendlyName);_363 updateChannelUI(_channel);_363 tc.currentChannel = _channel;_363 tc.loadMessages();_363 return joinedChannel;_363 });_363 }_363_363 function initChannelEvents() {_363 console.log(tc.currentChannel.friendlyName + ' ready.');_363 tc.currentChannel.on('messageAdded', tc.addMessageToList);_363 tc.currentChannel.on('typingStarted', showTypingStarted);_363 tc.currentChannel.on('typingEnded', hideTypingStarted);_363 tc.currentChannel.on('memberJoined', notifyMemberJoined);_363 tc.currentChannel.on('memberLeft', notifyMemberLeft);_363 $inputText.prop('disabled', false).focus();_363 }_363_363 function setupChannel(channel) {_363 return leaveCurrentChannel()_363 .then(function() {_363 return initChannel(channel);_363 })_363 .then(function(_channel) {_363 return joinChannel(_channel);_363 })_363 .then(initChannelEvents);_363 }_363_363 tc.loadMessages = function() {_363 tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT)_363 .then(function(messages) {_363 messages.items.forEach(tc.addMessageToList);_363 });_363 };_363_363 function leaveCurrentChannel() {_363 if (tc.currentChannel) {_363 return tc.currentChannel.leave().then(function(leftChannel) {_363 console.log('left ' + leftChannel.friendlyName);_363 leftChannel.removeListener('messageAdded', tc.addMessageToList);_363 leftChannel.removeListener('typingStarted', showTypingStarted);_363 leftChannel.removeListener('typingEnded', hideTypingStarted);_363 leftChannel.removeListener('memberJoined', notifyMemberJoined);_363 leftChannel.removeListener('memberLeft', notifyMemberLeft);_363 });_363 } else {_363 return Promise.resolve();_363 }_363 }_363_363 tc.addMessageToList = function(message) {_363 var rowDiv = $('<div>').addClass('row no-margin');_363 rowDiv.loadTemplate($('#message-template'), {_363 username: message.author,_363 date: dateFormatter.getTodayDate(message.dateCreated),_363 body: message.body_363 });_363 if (message.author === tc.username) {_363 rowDiv.addClass('own-message');_363 }_363_363 tc.$messageList.append(rowDiv);_363 scrollToMessageListBottom();_363 };_363_363 function notifyMemberJoined(member) {_363 notify(member.identity + ' joined the channel')_363 }_363_363 function notifyMemberLeft(member) {_363 notify(member.identity + ' left the channel');_363 }_363_363 function notify(message) {_363 var row = $('<div>').addClass('col-md-12');_363 row.loadTemplate('#member-notification-template', {_363 status: message_363 });_363 tc.$messageList.append(row);_363 scrollToMessageListBottom();_363 }_363_363 function showTypingStarted(member) {_363 $typingPlaceholder.text(member.identity + ' is typing...');_363 }_363_363 function hideTypingStarted(member) {_363 $typingPlaceholder.text('');_363 }_363_363 function scrollToMessageListBottom() {_363 tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);_363 }_363_363 function updateChannelUI(selectedChannel) {_363 var channelElements = $('.channel-element').toArray();_363 var channelElement = channelElements.filter(function(element) {_363 return $(element).data().sid === selectedChannel.sid;_363 });_363 channelElement = $(channelElement);_363 if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {_363 tc.currentChannelContainer = channelElement;_363 }_363 tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');_363 channelElement.removeClass('unselected-channel').addClass('selected-channel');_363 tc.currentChannelContainer = channelElement;_363 }_363_363 function showAddChannelInput() {_363 if (tc.messagingClient) {_363 $newChannelInputRow.addClass('showing').removeClass('not-showing');_363 $channelList.addClass('showing').removeClass('not-showing');_363 $newChannelInput.focus();_363 }_363 }_363_363 function hideAddChannelInput() {_363 $newChannelInputRow.addClass('not-showing').removeClass('showing');_363 $channelList.addClass('not-showing').removeClass('showing');_363 $newChannelInput.val('');_363 }_363_363 function addChannel(channel) {_363 if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {_363 tc.generalChannel = channel;_363 }_363 var rowDiv = $('<div>').addClass('row channel-row');_363 rowDiv.loadTemplate('#channel-template', {_363 channelName: channel.friendlyName_363 });_363_363 var channelP = rowDiv.children().children().first();_363_363 rowDiv.on('click', selectChannel);_363 channelP.data('sid', channel.sid);_363 if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {_363 tc.currentChannelContainer = channelP;_363 channelP.addClass('selected-channel');_363 }_363 else {_363 channelP.addClass('unselected-channel')_363 }_363_363 $channelList.append(rowDiv);_363 }_363_363 function deleteCurrentChannel() {_363 if (!tc.currentChannel) {_363 return;_363 }_363 if (tc.currentChannel.sid === tc.generalChannel.sid) {_363 alert('You cannot delete the general channel');_363 return;_363 }_363 tc.currentChannel.delete().then(function(channel) {_363 console.log('channel: '+ channel.friendlyName + ' deleted');_363 setupChannel(tc.generalChannel);_363 });_363 }_363_363 function selectChannel(event) {_363 var target = $(event.target);_363 var channelSid = target.data().sid;_363 var selectedChannel = tc.channelArray.filter(function(channel) {_363 return channel.sid === channelSid;_363 })[0];_363 if (selectedChannel === tc.currentChannel) {_363 return;_363 }_363 setupChannel(selectedChannel);_363 };_363_363 function disconnectClient() {_363 leaveCurrentChannel();_363 $channelList.text('');_363 tc.$messageList.text('');_363 channels = undefined;_363 $statusRow.addClass('disconnected').removeClass('connected');_363 tc.$messageList.addClass('disconnected').removeClass('connected');_363 $connectPanel.addClass('disconnected').removeClass('connected');_363 $inputText.removeClass('with-shadow');_363 $typingRow.addClass('disconnected').removeClass('connected');_363 }_363_363 tc.sortChannelsByName = function(channels) {_363 return channels.sort(function(a, b) {_363 if (a.friendlyName === GENERAL_CHANNEL_NAME) {_363 return -1;_363 }_363 if (b.friendlyName === GENERAL_CHANNEL_NAME) {_363 return 1;_363 }_363 return a.friendlyName.localeCompare(b.friendlyName);_363 });_363 };_363_363 return tc;_363})();
That's it! We've just implemented a simple chat application for Java using the Servlet API.
If you are a Java developer working with Twilio, you might want to check out these other tutorials:
Never miss another server outage. Learn how to build a server notification system that will alert all administrators via SMS when a server outage occurs.
Increase your rate of response by automating the workflows that are key to your business. In this tutorial, learn how to build a ready-for-scale automated SMS workflow, for a vacation rental company.
Protect your users' privacy by anonymously connecting them with Twilio Voice and SMS. Learn how to create disposable phone numbers on-demand, so two users can communicate without exchanging personal information.
Thanks for checking out this tutorial! If you have any feedback to share with us, we'd love to hear it. Tweet @twilio to let us know what you think.