Skip to contentSkip to navigationSkip to topbar
Rate this page:
On this page

Chat with Java and Servlets


(warning)

Warning

As the Programmable Chat API is set to sunset in 2022(link takes you to an external page), we will no longer maintain these chat tutorials.

Please see our Conversations API QuickStart to start building robust virtual spaces for conversation.

(error)

Danger

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(link takes you to an external page).

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.

Properati built a web and mobile messaging app to help real estate buyers and sellers connect in real time. Learn more here.(link takes you to an external page)

For your convenience, we consolidated the source code for this tutorial in a single GitHub repository(link takes you to an external page). Feel free to clone it and tweak it as required.


Generate the Token

generate-the-token page anchor

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.

Generate an Access Token

generate-an-access-token page anchor

src/main/java/com/twilio/chat/TwilioTokenCreator.java


_32
package com.twilio.chat;
_32
_32
import javax.inject.Inject;
_32
_32
import com.twilio.jwt.accesstoken.AccessToken;
_32
import com.twilio.jwt.accesstoken.ChatGrant;
_32
_32
public 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.


Token Generation Controller

token-generation-controller page anchor

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


_52
package com.twilio.chat;
_52
_52
import java.io.BufferedWriter;
_52
import java.io.IOException;
_52
import java.util.HashMap;
_52
import java.util.Map;
_52
_52
import javax.inject.Inject;
_52
import javax.servlet.http.HttpServlet;
_52
import javax.servlet.http.HttpServletRequest;
_52
import javax.servlet.http.HttpServletResponse;
_52
_52
import com.google.gson.Gson;
_52
import com.google.inject.Singleton;
_52
_52
@Singleton
_52
public 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.


Initialize the Programmable Chat Client

initialize-the-programmable-chat-client page anchor

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.

Initialize the Chat Client

initialize-the-chat-client page anchor

src/main/webapp/js/twiliochat.js


_363
var 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


_363
var 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.


Join the General Channel

join-the-general-channel page anchor

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


_363
var 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.


Listen to Channel Events

listen-to-channel-events page anchor

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


_363
var 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


_363
var 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


_363
var 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


_363
var 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(link takes you to an external page) 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


_363
var 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:

SMS and MMS Notifications(link takes you to an external page)

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.

Workflow Automation

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.

Masked Phone Numbers

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.

Did this help?

did-this-help page anchor

Thanks for checking out this tutorial! If you have any feedback to share with us, we'd love to hear it. Tweet @twilio(link takes you to an external page) to let us know what you think.


Rate this page: