Skip to contentSkip to navigationSkip to topbar
Rate this Page:

Chat with Node.js and Express


(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, Node.js and Express(link takes you to an external page)?

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.


Token Generation

token-generation 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 Programmable Chat API keys.

We generate this token by creating a new AccessToken and providing it with a ChatGrant. The new AccessToken object is created using your Twilio credentials.

Generate an Access Token

generate-an-access-token page anchor

services/tokenService.js


_28
const twilio = require('twilio');
_28
_28
const AccessToken = twilio.jwt.AccessToken;
_28
const ChatGrant = AccessToken.ChatGrant;
_28
_28
function TokenGenerator(identity) {
_28
const appName = 'TwilioChat';
_28
_28
// Create a "grant" which enables a client to use Chat as a given user
_28
const chatGrant = new ChatGrant({
_28
serviceSid: process.env.TWILIO_CHAT_SERVICE_SID,
_28
});
_28
_28
// Create an access token which we will sign and return to the client,
_28
// containing the grant we just created
_28
const token = new AccessToken(
_28
process.env.TWILIO_ACCOUNT_SID,
_28
process.env.TWILIO_API_KEY,
_28
process.env.TWILIO_API_SECRET
_28
);
_28
_28
token.addGrant(chatGrant);
_28
token.identity = identity;
_28
_28
return token;
_28
}
_28
_28
module.exports = { generate: TokenGenerator };

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 for token generation. This endpoint is responsible for providing a valid token when passed this parameter:

  • identity : identifies the user itself.

Once we have used the tokenService object to generate a token we can use the AccessToken's method as token.toJwt() to get the token as a String. Then we just return the token as a JSON encoded string.

routes/token.js


_30
var express = require('express');
_30
var router = express.Router();
_30
var TokenService = require('../services/tokenService');
_30
_30
// POST /token
_30
router.post('/', function(req, res) {
_30
var identity = req.body.identity;
_30
_30
var token = TokenService.generate(identity)
_30
_30
res.json({
_30
identity: identity,
_30
token: token.toJwt(),
_30
});
_30
});
_30
_30
// GET /token
_30
router.get('/', function(req, res) {
_30
var identity = req.query.identity;
_30
_30
var token = TokenService.generate(identity)
_30
_30
res.json({
_30
identity: identity,
_30
token: token.toJwt(),
_30
});
_30
});
_30
_30
_30
module.exports = router;

Now that we have a route that generates JWT tokens on demand, let's use this route to initialize our Twilio Chat Client.


Initialize the Programmable Chat Client

initialize-the-programmable-chat-client page anchor

Our client fetches a new Token by making a POST request to our endpoint when it calls the fetchAccessToken method.

With the token, we can then instantiate Twilio.Chat.Client in connectMessagingClient.

Initialize the Chat Client

initialize-the-chat-client page anchor

public/js/twiliochat.js


_381
var twiliochat = (function() {
_381
var tc = {};
_381
_381
var GENERAL_CHANNEL_UNIQUE_NAME = 'general';
_381
var GENERAL_CHANNEL_NAME = 'General Channel';
_381
var MESSAGES_HISTORY_LIMIT = 50;
_381
_381
var $channelList;
_381
var $inputText;
_381
var $usernameInput;
_381
var $statusRow;
_381
var $connectPanel;
_381
var $newChannelInputRow;
_381
var $newChannelInput;
_381
var $typingRow;
_381
var $typingPlaceholder;
_381
_381
$(document).ready(function() {
_381
tc.init();
_381
});
_381
_381
tc.init = function() {
_381
tc.$messageList = $('#message-list');
_381
$channelList = $('#channel-list');
_381
$inputText = $('#input-text');
_381
$usernameInput = $('#username-input');
_381
$statusRow = $('#status-row');
_381
$connectPanel = $('#connect-panel');
_381
$newChannelInputRow = $('#new-channel-input-row');
_381
$newChannelInput = $('#new-channel-input');
_381
$typingRow = $('#typing-row');
_381
$typingPlaceholder = $('#typing-placeholder');
_381
$usernameInput.focus();
_381
$usernameInput.on('keypress', handleUsernameInputKeypress);
_381
$inputText.on('keypress', handleInputTextKeypress);
_381
$newChannelInput.on('keypress', tc.handleNewChannelInputKeypress);
_381
$('#connect-image').on('click', connectClientWithUsername);
_381
$('#add-channel-image').on('click', showAddChannelInput);
_381
$('#leave-span').on('click', disconnectClient);
_381
$('#delete-channel-span').on('click', deleteCurrentChannel);
_381
};
_381
_381
function handleUsernameInputKeypress(event) {
_381
if (event.keyCode === 13){
_381
connectClientWithUsername();
_381
}
_381
}
_381
_381
function handleInputTextKeypress(event) {
_381
if (event.keyCode === 13) {
_381
tc.currentChannel.sendMessage($(this).val());
_381
event.preventDefault();
_381
$(this).val('');
_381
}
_381
else {
_381
notifyTyping();
_381
}
_381
}
_381
_381
var notifyTyping = $.throttle(function() {
_381
tc.currentChannel.typing();
_381
}, 1000);
_381
_381
tc.handleNewChannelInputKeypress = function(event) {
_381
if (event.keyCode === 13) {
_381
tc.messagingClient
_381
.createChannel({
_381
friendlyName: $newChannelInput.val(),
_381
})
_381
.then(hideAddChannelInput);
_381
_381
$(this).val('');
_381
event.preventDefault();
_381
}
_381
};
_381
_381
function connectClientWithUsername() {
_381
var usernameText = $usernameInput.val();
_381
$usernameInput.val('');
_381
if (usernameText == '') {
_381
alert('Username cannot be empty');
_381
return;
_381
}
_381
tc.username = usernameText;
_381
fetchAccessToken(tc.username, connectMessagingClient);
_381
}
_381
_381
function fetchAccessToken(username, handler) {
_381
$.post('/token', {identity: username}, null, 'json')
_381
.done(function(response) {
_381
handler(response.token);
_381
})
_381
.fail(function(error) {
_381
console.log('Failed to fetch the Access Token with error: ' + error);
_381
});
_381
}
_381
_381
function connectMessagingClient(token) {
_381
// Initialize the Chat messaging client
_381
Twilio.Chat.Client.create(token).then(function(client) {
_381
tc.messagingClient = client;
_381
updateConnectedUI();
_381
tc.loadChannelList(tc.joinGeneralChannel);
_381
tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));
_381
tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));
_381
tc.messagingClient.on('tokenExpired', refreshToken);
_381
});
_381
}
_381
_381
function refreshToken() {
_381
fetchAccessToken(tc.username, setNewToken);
_381
}
_381
_381
function setNewToken(token) {
_381
tc.messagingClient.updateToken(tokenResponse.token);
_381
}
_381
_381
function updateConnectedUI() {
_381
$('#username-span').text(tc.username);
_381
$statusRow.addClass('connected').removeClass('disconnected');
_381
tc.$messageList.addClass('connected').removeClass('disconnected');
_381
$connectPanel.addClass('connected').removeClass('disconnected');
_381
$inputText.addClass('with-shadow');
_381
$typingRow.addClass('connected').removeClass('disconnected');
_381
}
_381
_381
tc.loadChannelList = function(handler) {
_381
if (tc.messagingClient === undefined) {
_381
console.log('Client is not initialized');
_381
return;
_381
}
_381
_381
tc.messagingClient.getPublicChannelDescriptors().then(function(channels) {
_381
tc.channelArray = tc.sortChannelsByName(channels.items);
_381
$channelList.text('');
_381
tc.channelArray.forEach(addChannel);
_381
if (typeof handler === 'function') {
_381
handler();
_381
}
_381
});
_381
};
_381
_381
tc.joinGeneralChannel = function() {
_381
console.log('Attempting to join "general" chat channel...');
_381
if (!tc.generalChannel) {
_381
// If it doesn't exist, let's create it
_381
tc.messagingClient.createChannel({
_381
uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,
_381
friendlyName: GENERAL_CHANNEL_NAME
_381
}).then(function(channel) {
_381
console.log('Created general channel');
_381
tc.generalChannel = channel;
_381
tc.loadChannelList(tc.joinGeneralChannel);
_381
});
_381
}
_381
else {
_381
console.log('Found general channel:');
_381
setupChannel(tc.generalChannel);
_381
}
_381
};
_381
_381
function initChannel(channel) {
_381
console.log('Initialized channel ' + channel.friendlyName);
_381
return tc.messagingClient.getChannelBySid(channel.sid);
_381
}
_381
_381
function joinChannel(_channel) {
_381
return _channel.join()
_381
.then(function(joinedChannel) {
_381
console.log('Joined channel ' + joinedChannel.friendlyName);
_381
updateChannelUI(_channel);
_381
_381
return joinedChannel;
_381
})
_381
.catch(function(err) {
_381
if (_channel.status == 'joined') {
_381
updateChannelUI(_channel);
_381
return _channel;
_381
}
_381
console.error(
_381
"Couldn't join channel " + _channel.friendlyName + ' because -> ' + err
_381
);
_381
});
_381
}
_381
_381
function initChannelEvents() {
_381
console.log(tc.currentChannel.friendlyName + ' ready.');
_381
tc.currentChannel.on('messageAdded', tc.addMessageToList);
_381
tc.currentChannel.on('typingStarted', showTypingStarted);
_381
tc.currentChannel.on('typingEnded', hideTypingStarted);
_381
tc.currentChannel.on('memberJoined', notifyMemberJoined);
_381
tc.currentChannel.on('memberLeft', notifyMemberLeft);
_381
$inputText.prop('disabled', false).focus();
_381
}
_381
_381
function setupChannel(channel) {
_381
return leaveCurrentChannel()
_381
.then(function() {
_381
return initChannel(channel);
_381
})
_381
.then(function(_channel) {
_381
return joinChannel(_channel);
_381
})
_381
.then(initChannelEvents);
_381
}
_381
_381
tc.loadMessages = function() {
_381
tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT).then(function (messages) {
_381
messages.items.forEach(tc.addMessageToList);
_381
});
_381
};
_381
_381
function leaveCurrentChannel() {
_381
if (tc.currentChannel) {
_381
return tc.currentChannel.leave().then(function(leftChannel) {
_381
console.log('left ' + leftChannel.friendlyName);
_381
leftChannel.removeListener('messageAdded', tc.addMessageToList);
_381
leftChannel.removeListener('typingStarted', showTypingStarted);
_381
leftChannel.removeListener('typingEnded', hideTypingStarted);
_381
leftChannel.removeListener('memberJoined', notifyMemberJoined);
_381
leftChannel.removeListener('memberLeft', notifyMemberLeft);
_381
});
_381
} else {
_381
return Promise.resolve();
_381
}
_381
}
_381
_381
tc.addMessageToList = function(message) {
_381
var rowDiv = $('<div>').addClass('row no-margin');
_381
rowDiv.loadTemplate($('#message-template'), {
_381
username: message.author,
_381
date: dateFormatter.getTodayDate(message.dateCreated),
_381
body: message.body
_381
});
_381
if (message.author === tc.username) {
_381
rowDiv.addClass('own-message');
_381
}
_381
_381
tc.$messageList.append(rowDiv);
_381
scrollToMessageListBottom();
_381
};
_381
_381
function notifyMemberJoined(member) {
_381
notify(member.identity + ' joined the channel')
_381
}
_381
_381
function notifyMemberLeft(member) {
_381
notify(member.identity + ' left the channel');
_381
}
_381
_381
function notify(message) {
_381
var row = $('<div>').addClass('col-md-12');
_381
row.loadTemplate('#member-notification-template', {
_381
status: message
_381
});
_381
tc.$messageList.append(row);
_381
scrollToMessageListBottom();
_381
}
_381
_381
function showTypingStarted(member) {
_381
$typingPlaceholder.text(member.identity + ' is typing...');
_381
}
_381
_381
function hideTypingStarted(member) {
_381
$typingPlaceholder.text('');
_381
}
_381
_381
function scrollToMessageListBottom() {
_381
tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);
_381
}
_381
_381
function updateChannelUI(selectedChannel) {
_381
var channelElements = $('.channel-element').toArray();
_381
var channelElement = channelElements.filter(function(element) {
_381
return $(element).data().sid === selectedChannel.sid;
_381
});
_381
channelElement = $(channelElement);
_381
if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {
_381
tc.currentChannelContainer = channelElement;
_381
}
_381
tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');
_381
channelElement.removeClass('unselected-channel').addClass('selected-channel');
_381
tc.currentChannelContainer = channelElement;
_381
tc.currentChannel = selectedChannel;
_381
tc.loadMessages();
_381
}
_381
_381
function showAddChannelInput() {
_381
if (tc.messagingClient) {
_381
$newChannelInputRow.addClass('showing').removeClass('not-showing');
_381
$channelList.addClass('showing').removeClass('not-showing');
_381
$newChannelInput.focus();
_381
}
_381
}
_381
_381
function hideAddChannelInput() {
_381
$newChannelInputRow.addClass('not-showing').removeClass('showing');
_381
$channelList.addClass('not-showing').removeClass('showing');
_381
$newChannelInput.val('');
_381
}
_381
_381
function addChannel(channel) {
_381
if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {
_381
tc.generalChannel = channel;
_381
}
_381
var rowDiv = $('<div>').addClass('row channel-row');
_381
rowDiv.loadTemplate('#channel-template', {
_381
channelName: channel.friendlyName
_381
});
_381
_381
var channelP = rowDiv.children().children().first();
_381
_381
rowDiv.on('click', selectChannel);
_381
channelP.data('sid', channel.sid);
_381
if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {
_381
tc.currentChannelContainer = channelP;
_381
channelP.addClass('selected-channel');
_381
}
_381
else {
_381
channelP.addClass('unselected-channel')
_381
}
_381
_381
$channelList.append(rowDiv);
_381
}
_381
_381
function deleteCurrentChannel() {
_381
if (!tc.currentChannel) {
_381
return;
_381
}
_381
_381
if (tc.currentChannel.sid === tc.generalChannel.sid) {
_381
alert('You cannot delete the general channel');
_381
return;
_381
}
_381
_381
tc.currentChannel
_381
.delete()
_381
.then(function(channel) {
_381
console.log('channel: '+ channel.friendlyName + ' deleted');
_381
setupChannel(tc.generalChannel);
_381
});
_381
}
_381
_381
function selectChannel(event) {
_381
var target = $(event.target);
_381
var channelSid = target.data().sid;
_381
var selectedChannel = tc.channelArray.filter(function(channel) {
_381
return channel.sid === channelSid;
_381
})[0];
_381
if (selectedChannel === tc.currentChannel) {
_381
return;
_381
}
_381
setupChannel(selectedChannel);
_381
};
_381
_381
function disconnectClient() {
_381
leaveCurrentChannel();
_381
$channelList.text('');
_381
tc.$messageList.text('');
_381
channels = undefined;
_381
$statusRow.addClass('disconnected').removeClass('connected');
_381
tc.$messageList.addClass('disconnected').removeClass('connected');
_381
$connectPanel.addClass('disconnected').removeClass('connected');
_381
$inputText.removeClass('with-shadow');
_381
$typingRow.addClass('disconnected').removeClass('connected');
_381
}
_381
_381
tc.sortChannelsByName = function(channels) {
_381
return channels.sort(function(a, b) {
_381
if (a.friendlyName === GENERAL_CHANNEL_NAME) {
_381
return -1;
_381
}
_381
if (b.friendlyName === GENERAL_CHANNEL_NAME) {
_381
return 1;
_381
}
_381
return a.friendlyName.localeCompare(b.friendlyName);
_381
});
_381
};
_381
_381
return tc;
_381
})();

Now that we've instantiated our Chat Client, let's see how we can get a list of channels.


After initializing the client we can use its getPublicChannelDescriptors method to retrieve all visible channels. The method returns a promise which we use to show the list of channels retrieved on the UI.

public/js/twiliochat.js


_381
var twiliochat = (function() {
_381
var tc = {};
_381
_381
var GENERAL_CHANNEL_UNIQUE_NAME = 'general';
_381
var GENERAL_CHANNEL_NAME = 'General Channel';
_381
var MESSAGES_HISTORY_LIMIT = 50;
_381
_381
var $channelList;
_381
var $inputText;
_381
var $usernameInput;
_381
var $statusRow;
_381
var $connectPanel;
_381
var $newChannelInputRow;
_381
var $newChannelInput;
_381
var $typingRow;
_381
var $typingPlaceholder;
_381
_381
$(document).ready(function() {
_381
tc.init();
_381
});
_381
_381
tc.init = function() {
_381
tc.$messageList = $('#message-list');
_381
$channelList = $('#channel-list');
_381
$inputText = $('#input-text');
_381
$usernameInput = $('#username-input');
_381
$statusRow = $('#status-row');
_381
$connectPanel = $('#connect-panel');
_381
$newChannelInputRow = $('#new-channel-input-row');
_381
$newChannelInput = $('#new-channel-input');
_381
$typingRow = $('#typing-row');
_381
$typingPlaceholder = $('#typing-placeholder');
_381
$usernameInput.focus();
_381
$usernameInput.on('keypress', handleUsernameInputKeypress);
_381
$inputText.on('keypress', handleInputTextKeypress);
_381
$newChannelInput.on('keypress', tc.handleNewChannelInputKeypress);
_381
$('#connect-image').on('click', connectClientWithUsername);
_381
$('#add-channel-image').on('click', showAddChannelInput);
_381
$('#leave-span').on('click', disconnectClient);
_381
$('#delete-channel-span').on('click', deleteCurrentChannel);
_381
};
_381
_381
function handleUsernameInputKeypress(event) {
_381
if (event.keyCode === 13){
_381
connectClientWithUsername();
_381
}
_381
}
_381
_381
function handleInputTextKeypress(event) {
_381
if (event.keyCode === 13) {
_381
tc.currentChannel.sendMessage($(this).val());
_381
event.preventDefault();
_381
$(this).val('');
_381
}
_381
else {
_381
notifyTyping();
_381
}
_381
}
_381
_381
var notifyTyping = $.throttle(function() {
_381
tc.currentChannel.typing();
_381
}, 1000);
_381
_381
tc.handleNewChannelInputKeypress = function(event) {
_381
if (event.keyCode === 13) {
_381
tc.messagingClient
_381
.createChannel({
_381
friendlyName: $newChannelInput.val(),
_381
})
_381
.then(hideAddChannelInput);
_381
_381
$(this).val('');
_381
event.preventDefault();
_381
}
_381
};
_381
_381
function connectClientWithUsername() {
_381
var usernameText = $usernameInput.val();
_381
$usernameInput.val('');
_381
if (usernameText == '') {
_381
alert('Username cannot be empty');
_381
return;
_381
}
_381
tc.username = usernameText;
_381
fetchAccessToken(tc.username, connectMessagingClient);
_381
}
_381
_381
function fetchAccessToken(username, handler) {
_381
$.post('/token', {identity: username}, null, 'json')
_381
.done(function(response) {
_381
handler(response.token);
_381
})
_381
.fail(function(error) {
_381
console.log('Failed to fetch the Access Token with error: ' + error);
_381
});
_381
}
_381
_381
function connectMessagingClient(token) {
_381
// Initialize the Chat messaging client
_381
Twilio.Chat.Client.create(token).then(function(client) {
_381
tc.messagingClient = client;
_381
updateConnectedUI();
_381
tc.loadChannelList(tc.joinGeneralChannel);
_381
tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));
_381
tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));
_381
tc.messagingClient.on('tokenExpired', refreshToken);
_381
});
_381
}
_381
_381
function refreshToken() {
_381
fetchAccessToken(tc.username, setNewToken);
_381
}
_381
_381
function setNewToken(token) {
_381
tc.messagingClient.updateToken(tokenResponse.token);
_381
}
_381
_381
function updateConnectedUI() {
_381
$('#username-span').text(tc.username);
_381
$statusRow.addClass('connected').removeClass('disconnected');
_381
tc.$messageList.addClass('connected').removeClass('disconnected');
_381
$connectPanel.addClass('connected').removeClass('disconnected');
_381
$inputText.addClass('with-shadow');
_381
$typingRow.addClass('connected').removeClass('disconnected');
_381
}
_381
_381
tc.loadChannelList = function(handler) {
_381
if (tc.messagingClient === undefined) {
_381
console.log('Client is not initialized');
_381
return;
_381
}
_381
_381
tc.messagingClient.getPublicChannelDescriptors().then(function(channels) {
_381
tc.channelArray = tc.sortChannelsByName(channels.items);
_381
$channelList.text('');
_381
tc.channelArray.forEach(addChannel);
_381
if (typeof handler === 'function') {
_381
handler();
_381
}
_381
});
_381
};
_381
_381
tc.joinGeneralChannel = function() {
_381
console.log('Attempting to join "general" chat channel...');
_381
if (!tc.generalChannel) {
_381
// If it doesn't exist, let's create it
_381
tc.messagingClient.createChannel({
_381
uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,
_381
friendlyName: GENERAL_CHANNEL_NAME
_381
}).then(function(channel) {
_381
console.log('Created general channel');
_381
tc.generalChannel = channel;
_381
tc.loadChannelList(tc.joinGeneralChannel);
_381
});
_381
}
_381
else {
_381
console.log('Found general channel:');
_381
setupChannel(tc.generalChannel);
_381
}
_381
};
_381
_381
function initChannel(channel) {
_381
console.log('Initialized channel ' + channel.friendlyName);
_381
return tc.messagingClient.getChannelBySid(channel.sid);
_381
}
_381
_381
function joinChannel(_channel) {
_381
return _channel.join()
_381
.then(function(joinedChannel) {
_381
console.log('Joined channel ' + joinedChannel.friendlyName);
_381
updateChannelUI(_channel);
_381
_381
return joinedChannel;
_381
})
_381
.catch(function(err) {
_381
if (_channel.status == 'joined') {
_381
updateChannelUI(_channel);
_381
return _channel;
_381
}
_381
console.error(
_381
"Couldn't join channel " + _channel.friendlyName + ' because -> ' + err
_381
);
_381
});
_381
}
_381
_381
function initChannelEvents() {
_381
console.log(tc.currentChannel.friendlyName + ' ready.');
_381
tc.currentChannel.on('messageAdded', tc.addMessageToList);
_381
tc.currentChannel.on('typingStarted', showTypingStarted);
_381
tc.currentChannel.on('typingEnded', hideTypingStarted);
_381
tc.currentChannel.on('memberJoined', notifyMemberJoined);
_381
tc.currentChannel.on('memberLeft', notifyMemberLeft);
_381
$inputText.prop('disabled', false).focus();
_381
}
_381
_381
function setupChannel(channel) {
_381
return leaveCurrentChannel()
_381
.then(function() {
_381
return initChannel(channel);
_381
})
_381
.then(function(_channel) {
_381
return joinChannel(_channel);
_381
})
_381
.then(initChannelEvents);
_381
}
_381
_381
tc.loadMessages = function() {
_381
tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT).then(function (messages) {
_381
messages.items.forEach(tc.addMessageToList);
_381
});
_381
};
_381
_381
function leaveCurrentChannel() {
_381
if (tc.currentChannel) {
_381
return tc.currentChannel.leave().then(function(leftChannel) {
_381
console.log('left ' + leftChannel.friendlyName);
_381
leftChannel.removeListener('messageAdded', tc.addMessageToList);
_381
leftChannel.removeListener('typingStarted', showTypingStarted);
_381
leftChannel.removeListener('typingEnded', hideTypingStarted);
_381
leftChannel.removeListener('memberJoined', notifyMemberJoined);
_381
leftChannel.removeListener('memberLeft', notifyMemberLeft);
_381
});
_381
} else {
_381
return Promise.resolve();
_381
}
_381
}
_381
_381
tc.addMessageToList = function(message) {
_381
var rowDiv = $('<div>').addClass('row no-margin');
_381
rowDiv.loadTemplate($('#message-template'), {
_381
username: message.author,
_381
date: dateFormatter.getTodayDate(message.dateCreated),
_381
body: message.body
_381
});
_381
if (message.author === tc.username) {
_381
rowDiv.addClass('own-message');
_381
}
_381
_381
tc.$messageList.append(rowDiv);
_381
scrollToMessageListBottom();
_381
};
_381
_381
function notifyMemberJoined(member) {
_381
notify(member.identity + ' joined the channel')
_381
}
_381
_381
function notifyMemberLeft(member) {
_381
notify(member.identity + ' left the channel');
_381
}
_381
_381
function notify(message) {
_381
var row = $('<div>').addClass('col-md-12');
_381
row.loadTemplate('#member-notification-template', {
_381
status: message
_381
});
_381
tc.$messageList.append(row);
_381
scrollToMessageListBottom();
_381
}
_381
_381
function showTypingStarted(member) {
_381
$typingPlaceholder.text(member.identity + ' is typing...');
_381
}
_381
_381
function hideTypingStarted(member) {
_381
$typingPlaceholder.text('');
_381
}
_381
_381
function scrollToMessageListBottom() {
_381
tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);
_381
}
_381
_381
function updateChannelUI(selectedChannel) {
_381
var channelElements = $('.channel-element').toArray();
_381
var channelElement = channelElements.filter(function(element) {
_381
return $(element).data().sid === selectedChannel.sid;
_381
});
_381
channelElement = $(channelElement);
_381
if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {
_381
tc.currentChannelContainer = channelElement;
_381
}
_381
tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');
_381
channelElement.removeClass('unselected-channel').addClass('selected-channel');
_381
tc.currentChannelContainer = channelElement;
_381
tc.currentChannel = selectedChannel;
_381
tc.loadMessages();
_381
}
_381
_381
function showAddChannelInput() {
_381
if (tc.messagingClient) {
_381
$newChannelInputRow.addClass('showing').removeClass('not-showing');
_381
$channelList.addClass('showing').removeClass('not-showing');
_381
$newChannelInput.focus();
_381
}
_381
}
_381
_381
function hideAddChannelInput() {
_381
$newChannelInputRow.addClass('not-showing').removeClass('showing');
_381
$channelList.addClass('not-showing').removeClass('showing');
_381
$newChannelInput.val('');
_381
}
_381
_381
function addChannel(channel) {
_381
if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {
_381
tc.generalChannel = channel;
_381
}
_381
var rowDiv = $('<div>').addClass('row channel-row');
_381
rowDiv.loadTemplate('#channel-template', {
_381
channelName: channel.friendlyName
_381
});
_381
_381
var channelP = rowDiv.children().children().first();
_381
_381
rowDiv.on('click', selectChannel);
_381
channelP.data('sid', channel.sid);
_381
if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {
_381
tc.currentChannelContainer = channelP;
_381
channelP.addClass('selected-channel');
_381
}
_381
else {
_381
channelP.addClass('unselected-channel')
_381
}
_381
_381
$channelList.append(rowDiv);
_381
}
_381
_381
function deleteCurrentChannel() {
_381
if (!tc.currentChannel) {
_381
return;
_381
}
_381
_381
if (tc.currentChannel.sid === tc.generalChannel.sid) {
_381
alert('You cannot delete the general channel');
_381
return;
_381
}
_381
_381
tc.currentChannel
_381
.delete()
_381
.then(function(channel) {
_381
console.log('channel: '+ channel.friendlyName + ' deleted');
_381
setupChannel(tc.generalChannel);
_381
});
_381
}
_381
_381
function selectChannel(event) {
_381
var target = $(event.target);
_381
var channelSid = target.data().sid;
_381
var selectedChannel = tc.channelArray.filter(function(channel) {
_381
return channel.sid === channelSid;
_381
})[0];
_381
if (selectedChannel === tc.currentChannel) {
_381
return;
_381
}
_381
setupChannel(selectedChannel);
_381
};
_381
_381
function disconnectClient() {
_381
leaveCurrentChannel();
_381
$channelList.text('');
_381
tc.$messageList.text('');
_381
channels = undefined;
_381
$statusRow.addClass('disconnected').removeClass('connected');
_381
tc.$messageList.addClass('disconnected').removeClass('connected');
_381
$connectPanel.addClass('disconnected').removeClass('connected');
_381
$inputText.removeClass('with-shadow');
_381
$typingRow.addClass('disconnected').removeClass('connected');
_381
}
_381
_381
tc.sortChannelsByName = function(channels) {
_381
return channels.sort(function(a, b) {
_381
if (a.friendlyName === GENERAL_CHANNEL_NAME) {
_381
return -1;
_381
}
_381
if (b.friendlyName === GENERAL_CHANNEL_NAME) {
_381
return 1;
_381
}
_381
return a.friendlyName.localeCompare(b.friendlyName);
_381
});
_381
};
_381
_381
return tc;
_381
})();

Next, we need a default channel.


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 handle invitations.

Notice we set a unique name for the general channel as we don't want to create a new general channel every time we start the application.

public/js/twiliochat.js


_381
var twiliochat = (function() {
_381
var tc = {};
_381
_381
var GENERAL_CHANNEL_UNIQUE_NAME = 'general';
_381
var GENERAL_CHANNEL_NAME = 'General Channel';
_381
var MESSAGES_HISTORY_LIMIT = 50;
_381
_381
var $channelList;
_381
var $inputText;
_381
var $usernameInput;
_381
var $statusRow;
_381
var $connectPanel;
_381
var $newChannelInputRow;
_381
var $newChannelInput;
_381
var $typingRow;
_381
var $typingPlaceholder;
_381
_381
$(document).ready(function() {
_381
tc.init();
_381
});
_381
_381
tc.init = function() {
_381
tc.$messageList = $('#message-list');
_381
$channelList = $('#channel-list');
_381
$inputText = $('#input-text');
_381
$usernameInput = $('#username-input');
_381
$statusRow = $('#status-row');
_381
$connectPanel = $('#connect-panel');
_381
$newChannelInputRow = $('#new-channel-input-row');
_381
$newChannelInput = $('#new-channel-input');
_381
$typingRow = $('#typing-row');
_381
$typingPlaceholder = $('#typing-placeholder');
_381
$usernameInput.focus();
_381
$usernameInput.on('keypress', handleUsernameInputKeypress);
_381
$inputText.on('keypress', handleInputTextKeypress);
_381
$newChannelInput.on('keypress', tc.handleNewChannelInputKeypress);
_381
$('#connect-image').on('click', connectClientWithUsername);
_381
$('#add-channel-image').on('click', showAddChannelInput);
_381
$('#leave-span').on('click', disconnectClient);
_381
$('#delete-channel-span').on('click', deleteCurrentChannel);
_381
};
_381
_381
function handleUsernameInputKeypress(event) {
_381
if (event.keyCode === 13){
_381
connectClientWithUsername();
_381
}
_381
}
_381
_381
function handleInputTextKeypress(event) {
_381
if (event.keyCode === 13) {
_381
tc.currentChannel.sendMessage($(this).val());
_381
event.preventDefault();
_381
$(this).val('');
_381
}
_381
else {
_381
notifyTyping();
_381
}
_381
}
_381
_381
var notifyTyping = $.throttle(function() {
_381
tc.currentChannel.typing();
_381
}, 1000);
_381
_381
tc.handleNewChannelInputKeypress = function(event) {
_381
if (event.keyCode === 13) {
_381
tc.messagingClient
_381
.createChannel({
_381
friendlyName: $newChannelInput.val(),
_381
})
_381
.then(hideAddChannelInput);
_381
_381
$(this).val('');
_381
event.preventDefault();
_381
}
_381
};
_381
_381
function connectClientWithUsername() {
_381
var usernameText = $usernameInput.val();
_381
$usernameInput.val('');
_381
if (usernameText == '') {
_381
alert('Username cannot be empty');
_381
return;
_381
}
_381
tc.username = usernameText;
_381
fetchAccessToken(tc.username, connectMessagingClient);
_381
}
_381
_381
function fetchAccessToken(username, handler) {
_381
$.post('/token', {identity: username}, null, 'json')
_381
.done(function(response) {
_381
handler(response.token);
_381
})
_381
.fail(function(error) {
_381
console.log('Failed to fetch the Access Token with error: ' + error);
_381
});
_381
}
_381
_381
function connectMessagingClient(token) {
_381
// Initialize the Chat messaging client
_381
Twilio.Chat.Client.create(token).then(function(client) {
_381
tc.messagingClient = client;
_381
updateConnectedUI();
_381
tc.loadChannelList(tc.joinGeneralChannel);
_381
tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));
_381
tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));
_381
tc.messagingClient.on('tokenExpired', refreshToken);
_381
});
_381
}
_381
_381
function refreshToken() {
_381
fetchAccessToken(tc.username, setNewToken);
_381
}
_381
_381
function setNewToken(token) {
_381
tc.messagingClient.updateToken(tokenResponse.token);
_381
}
_381
_381
function updateConnectedUI() {
_381
$('#username-span').text(tc.username);
_381
$statusRow.addClass('connected').removeClass('disconnected');
_381
tc.$messageList.addClass('connected').removeClass('disconnected');
_381
$connectPanel.addClass('connected').removeClass('disconnected');
_381
$inputText.addClass('with-shadow');
_381
$typingRow.addClass('connected').removeClass('disconnected');
_381
}
_381
_381
tc.loadChannelList = function(handler) {
_381
if (tc.messagingClient === undefined) {
_381
console.log('Client is not initialized');
_381
return;
_381
}
_381
_381
tc.messagingClient.getPublicChannelDescriptors().then(function(channels) {
_381
tc.channelArray = tc.sortChannelsByName(channels.items);
_381
$channelList.text('');
_381
tc.channelArray.forEach(addChannel);
_381
if (typeof handler === 'function') {
_381
handler();
_381
}
_381
});
_381
};
_381
_381
tc.joinGeneralChannel = function() {
_381
console.log('Attempting to join "general" chat channel...');
_381
if (!tc.generalChannel) {
_381
// If it doesn't exist, let's create it
_381
tc.messagingClient.createChannel({
_381
uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,
_381
friendlyName: GENERAL_CHANNEL_NAME
_381
}).then(function(channel) {
_381
console.log('Created general channel');
_381
tc.generalChannel = channel;
_381
tc.loadChannelList(tc.joinGeneralChannel);
_381
});
_381
}
_381
else {
_381
console.log('Found general channel:');
_381
setupChannel(tc.generalChannel);
_381
}
_381
};
_381
_381
function initChannel(channel) {
_381
console.log('Initialized channel ' + channel.friendlyName);
_381
return tc.messagingClient.getChannelBySid(channel.sid);
_381
}
_381
_381
function joinChannel(_channel) {
_381
return _channel.join()
_381
.then(function(joinedChannel) {
_381
console.log('Joined channel ' + joinedChannel.friendlyName);
_381
updateChannelUI(_channel);
_381
_381
return joinedChannel;
_381
})
_381
.catch(function(err) {
_381
if (_channel.status == 'joined') {
_381
updateChannelUI(_channel);
_381
return _channel;
_381
}
_381
console.error(
_381
"Couldn't join channel " + _channel.friendlyName + ' because -> ' + err
_381
);
_381
});
_381
}
_381
_381
function initChannelEvents() {
_381
console.log(tc.currentChannel.friendlyName + ' ready.');
_381
tc.currentChannel.on('messageAdded', tc.addMessageToList);
_381
tc.currentChannel.on('typingStarted', showTypingStarted);
_381
tc.currentChannel.on('typingEnded', hideTypingStarted);
_381
tc.currentChannel.on('memberJoined', notifyMemberJoined);
_381
tc.currentChannel.on('memberLeft', notifyMemberLeft);
_381
$inputText.prop('disabled', false).focus();
_381
}
_381
_381
function setupChannel(channel) {
_381
return leaveCurrentChannel()
_381
.then(function() {
_381
return initChannel(channel);
_381
})
_381
.then(function(_channel) {
_381
return joinChannel(_channel);
_381
})
_381
.then(initChannelEvents);
_381
}
_381
_381
tc.loadMessages = function() {
_381
tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT).then(function (messages) {
_381
messages.items.forEach(tc.addMessageToList);
_381
});
_381
};
_381
_381
function leaveCurrentChannel() {
_381
if (tc.currentChannel) {
_381
return tc.currentChannel.leave().then(function(leftChannel) {
_381
console.log('left ' + leftChannel.friendlyName);
_381
leftChannel.removeListener('messageAdded', tc.addMessageToList);
_381
leftChannel.removeListener('typingStarted', showTypingStarted);
_381
leftChannel.removeListener('typingEnded', hideTypingStarted);
_381
leftChannel.removeListener('memberJoined', notifyMemberJoined);
_381
leftChannel.removeListener('memberLeft', notifyMemberLeft);
_381
});
_381
} else {
_381
return Promise.resolve();
_381
}
_381
}
_381
_381
tc.addMessageToList = function(message) {
_381
var rowDiv = $('<div>').addClass('row no-margin');
_381
rowDiv.loadTemplate($('#message-template'), {
_381
username: message.author,
_381
date: dateFormatter.getTodayDate(message.dateCreated),
_381
body: message.body
_381
});
_381
if (message.author === tc.username) {
_381
rowDiv.addClass('own-message');
_381
}
_381
_381
tc.$messageList.append(rowDiv);
_381
scrollToMessageListBottom();
_381
};
_381
_381
function notifyMemberJoined(member) {
_381
notify(member.identity + ' joined the channel')
_381
}
_381
_381
function notifyMemberLeft(member) {
_381
notify(member.identity + ' left the channel');
_381
}
_381
_381
function notify(message) {
_381
var row = $('<div>').addClass('col-md-12');
_381
row.loadTemplate('#member-notification-template', {
_381
status: message
_381
});
_381
tc.$messageList.append(row);
_381
scrollToMessageListBottom();
_381
}
_381
_381
function showTypingStarted(member) {
_381
$typingPlaceholder.text(member.identity + ' is typing...');
_381
}
_381
_381
function hideTypingStarted(member) {
_381
$typingPlaceholder.text('');
_381
}
_381
_381
function scrollToMessageListBottom() {
_381
tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);
_381
}
_381
_381
function updateChannelUI(selectedChannel) {
_381
var channelElements = $('.channel-element').toArray();
_381
var channelElement = channelElements.filter(function(element) {
_381
return $(element).data().sid === selectedChannel.sid;
_381
});
_381
channelElement = $(channelElement);
_381
if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {
_381
tc.currentChannelContainer = channelElement;
_381
}
_381
tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');
_381
channelElement.removeClass('unselected-channel').addClass('selected-channel');
_381
tc.currentChannelContainer = channelElement;
_381
tc.currentChannel = selectedChannel;
_381
tc.loadMessages();
_381
}
_381
_381
function showAddChannelInput() {
_381
if (tc.messagingClient) {
_381
$newChannelInputRow.addClass('showing').removeClass('not-showing');
_381
$channelList.addClass('showing').removeClass('not-showing');
_381
$newChannelInput.focus();
_381
}
_381
}
_381
_381
function hideAddChannelInput() {
_381
$newChannelInputRow.addClass('not-showing').removeClass('showing');
_381
$channelList.addClass('not-showing').removeClass('showing');
_381
$newChannelInput.val('');
_381
}
_381
_381
function addChannel(channel) {
_381
if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {
_381
tc.generalChannel = channel;
_381
}
_381
var rowDiv = $('<div>').addClass('row channel-row');
_381
rowDiv.loadTemplate('#channel-template', {
_381
channelName: channel.friendlyName
_381
});
_381
_381
var channelP = rowDiv.children().children().first();
_381
_381
rowDiv.on('click', selectChannel);
_381
channelP.data('sid', channel.sid);
_381
if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {
_381
tc.currentChannelContainer = channelP;
_381
channelP.addClass('selected-channel');
_381
}
_381
else {
_381
channelP.addClass('unselected-channel')
_381
}
_381
_381
$channelList.append(rowDiv);
_381
}
_381
_381
function deleteCurrentChannel() {
_381
if (!tc.currentChannel) {
_381
return;
_381
}
_381
_381
if (tc.currentChannel.sid === tc.generalChannel.sid) {
_381
alert('You cannot delete the general channel');
_381
return;
_381
}
_381
_381
tc.currentChannel
_381
.delete()
_381
.then(function(channel) {
_381
console.log('channel: '+ channel.friendlyName + ' deleted');
_381
setupChannel(tc.generalChannel);
_381
});
_381
}
_381
_381
function selectChannel(event) {
_381
var target = $(event.target);
_381
var channelSid = target.data().sid;
_381
var selectedChannel = tc.channelArray.filter(function(channel) {
_381
return channel.sid === channelSid;
_381
})[0];
_381
if (selectedChannel === tc.currentChannel) {
_381
return;
_381
}
_381
setupChannel(selectedChannel);
_381
};
_381
_381
function disconnectClient() {
_381
leaveCurrentChannel();
_381
$channelList.text('');
_381
tc.$messageList.text('');
_381
channels = undefined;
_381
$statusRow.addClass('disconnected').removeClass('connected');
_381
tc.$messageList.addClass('disconnected').removeClass('connected');
_381
$connectPanel.addClass('disconnected').removeClass('connected');
_381
$inputText.removeClass('with-shadow');
_381
$typingRow.addClass('disconnected').removeClass('connected');
_381
}
_381
_381
tc.sortChannelsByName = function(channels) {
_381
return channels.sort(function(a, b) {
_381
if (a.friendlyName === GENERAL_CHANNEL_NAME) {
_381
return -1;
_381
}
_381
if (b.friendlyName === GENERAL_CHANNEL_NAME) {
_381
return 1;
_381
}
_381
return a.friendlyName.localeCompare(b.friendlyName);
_381
});
_381
};
_381
_381
return tc;
_381
})();

Now let's listen for some channel events.


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.

public/js/twiliochat.js


_381
var twiliochat = (function() {
_381
var tc = {};
_381
_381
var GENERAL_CHANNEL_UNIQUE_NAME = 'general';
_381
var GENERAL_CHANNEL_NAME = 'General Channel';
_381
var MESSAGES_HISTORY_LIMIT = 50;
_381
_381
var $channelList;
_381
var $inputText;
_381
var $usernameInput;
_381
var $statusRow;
_381
var $connectPanel;
_381
var $newChannelInputRow;
_381
var $newChannelInput;
_381
var $typingRow;
_381
var $typingPlaceholder;
_381
_381
$(document).ready(function() {
_381
tc.init();
_381
});
_381
_381
tc.init = function() {
_381
tc.$messageList = $('#message-list');
_381
$channelList = $('#channel-list');
_381
$inputText = $('#input-text');
_381
$usernameInput = $('#username-input');
_381
$statusRow = $('#status-row');
_381
$connectPanel = $('#connect-panel');
_381
$newChannelInputRow = $('#new-channel-input-row');
_381
$newChannelInput = $('#new-channel-input');
_381
$typingRow = $('#typing-row');
_381
$typingPlaceholder = $('#typing-placeholder');
_381
$usernameInput.focus();
_381
$usernameInput.on('keypress', handleUsernameInputKeypress);
_381
$inputText.on('keypress', handleInputTextKeypress);
_381
$newChannelInput.on('keypress', tc.handleNewChannelInputKeypress);
_381
$('#connect-image').on('click', connectClientWithUsername);
_381
$('#add-channel-image').on('click', showAddChannelInput);
_381
$('#leave-span').on('click', disconnectClient);
_381
$('#delete-channel-span').on('click', deleteCurrentChannel);
_381
};
_381
_381
function handleUsernameInputKeypress(event) {
_381
if (event.keyCode === 13){
_381
connectClientWithUsername();
_381
}
_381
}
_381
_381
function handleInputTextKeypress(event) {
_381
if (event.keyCode === 13) {
_381
tc.currentChannel.sendMessage($(this).val());
_381
event.preventDefault();
_381
$(this).val('');
_381
}
_381
else {
_381
notifyTyping();
_381
}
_381
}
_381
_381
var notifyTyping = $.throttle(function() {
_381
tc.currentChannel.typing();
_381
}, 1000);
_381
_381
tc.handleNewChannelInputKeypress = function(event) {
_381
if (event.keyCode === 13) {
_381
tc.messagingClient
_381
.createChannel({
_381
friendlyName: $newChannelInput.val(),
_381
})
_381
.then(hideAddChannelInput);
_381
_381
$(this).val('');
_381
event.preventDefault();
_381
}
_381
};
_381
_381
function connectClientWithUsername() {
_381
var usernameText = $usernameInput.val();
_381
$usernameInput.val('');
_381
if (usernameText == '') {
_381
alert('Username cannot be empty');
_381
return;
_381
}
_381
tc.username = usernameText;
_381
fetchAccessToken(tc.username, connectMessagingClient);
_381
}
_381
_381
function fetchAccessToken(username, handler) {
_381
$.post('/token', {identity: username}, null, 'json')
_381
.done(function(response) {
_381
handler(response.token);
_381
})
_381
.fail(function(error) {
_381
console.log('Failed to fetch the Access Token with error: ' + error);
_381
});
_381
}
_381
_381
function connectMessagingClient(token) {
_381
// Initialize the Chat messaging client
_381
Twilio.Chat.Client.create(token).then(function(client) {
_381
tc.messagingClient = client;
_381
updateConnectedUI();
_381
tc.loadChannelList(tc.joinGeneralChannel);
_381
tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));
_381
tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));
_381
tc.messagingClient.on('tokenExpired', refreshToken);
_381
});
_381
}
_381
_381
function refreshToken() {
_381
fetchAccessToken(tc.username, setNewToken);
_381
}
_381
_381
function setNewToken(token) {
_381
tc.messagingClient.updateToken(tokenResponse.token);
_381
}
_381
_381
function updateConnectedUI() {
_381
$('#username-span').text(tc.username);
_381
$statusRow.addClass('connected').removeClass('disconnected');
_381
tc.$messageList.addClass('connected').removeClass('disconnected');
_381
$connectPanel.addClass('connected').removeClass('disconnected');
_381
$inputText.addClass('with-shadow');
_381
$typingRow.addClass('connected').removeClass('disconnected');
_381
}
_381
_381
tc.loadChannelList = function(handler) {
_381
if (tc.messagingClient === undefined) {
_381
console.log('Client is not initialized');
_381
return;
_381
}
_381
_381
tc.messagingClient.getPublicChannelDescriptors().then(function(channels) {
_381
tc.channelArray = tc.sortChannelsByName(channels.items);
_381
$channelList.text('');
_381
tc.channelArray.forEach(addChannel);
_381
if (typeof handler === 'function') {
_381
handler();
_381
}
_381
});
_381
};
_381
_381
tc.joinGeneralChannel = function() {
_381
console.log('Attempting to join "general" chat channel...');
_381
if (!tc.generalChannel) {
_381
// If it doesn't exist, let's create it
_381
tc.messagingClient.createChannel({
_381
uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,
_381
friendlyName: GENERAL_CHANNEL_NAME
_381
}).then(function(channel) {
_381
console.log('Created general channel');
_381
tc.generalChannel = channel;
_381
tc.loadChannelList(tc.joinGeneralChannel);
_381
});
_381
}
_381
else {
_381
console.log('Found general channel:');
_381
setupChannel(tc.generalChannel);
_381
}
_381
};
_381
_381
function initChannel(channel) {
_381
console.log('Initialized channel ' + channel.friendlyName);
_381
return tc.messagingClient.getChannelBySid(channel.sid);
_381
}
_381
_381
function joinChannel(_channel) {
_381
return _channel.join()
_381
.then(function(joinedChannel) {
_381
console.log('Joined channel ' + joinedChannel.friendlyName);
_381
updateChannelUI(_channel);
_381
_381
return joinedChannel;
_381
})
_381
.catch(function(err) {
_381
if (_channel.status == 'joined') {
_381
updateChannelUI(_channel);
_381
return _channel;
_381
}
_381
console.error(
_381
"Couldn't join channel " + _channel.friendlyName + ' because -> ' + err
_381
);
_381
});
_381
}
_381
_381
function initChannelEvents() {
_381
console.log(tc.currentChannel.friendlyName + ' ready.');
_381
tc.currentChannel.on('messageAdded', tc.addMessageToList);
_381
tc.currentChannel.on('typingStarted', showTypingStarted);
_381
tc.currentChannel.on('typingEnded', hideTypingStarted);
_381
tc.currentChannel.on('memberJoined', notifyMemberJoined);
_381
tc.currentChannel.on('memberLeft', notifyMemberLeft);
_381
$inputText.prop('disabled', false).focus();
_381
}
_381
_381
function setupChannel(channel) {
_381
return leaveCurrentChannel()
_381
.then(function() {
_381
return initChannel(channel);
_381
})
_381
.then(function(_channel) {
_381
return joinChannel(_channel);
_381
})
_381
.then(initChannelEvents);
_381
}
_381
_381
tc.loadMessages = function() {
_381
tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT).then(function (messages) {
_381
messages.items.forEach(tc.addMessageToList);
_381
});
_381
};
_381
_381
function leaveCurrentChannel() {
_381
if (tc.currentChannel) {
_381
return tc.currentChannel.leave().then(function(leftChannel) {
_381
console.log('left ' + leftChannel.friendlyName);
_381
leftChannel.removeListener('messageAdded', tc.addMessageToList);
_381
leftChannel.removeListener('typingStarted', showTypingStarted);
_381
leftChannel.removeListener('typingEnded', hideTypingStarted);
_381
leftChannel.removeListener('memberJoined', notifyMemberJoined);
_381
leftChannel.removeListener('memberLeft', notifyMemberLeft);
_381
});
_381
} else {
_381
return Promise.resolve();
_381
}
_381
}
_381
_381
tc.addMessageToList = function(message) {
_381
var rowDiv = $('<div>').addClass('row no-margin');
_381
rowDiv.loadTemplate($('#message-template'), {
_381
username: message.author,
_381
date: dateFormatter.getTodayDate(message.dateCreated),
_381
body: message.body
_381
});
_381
if (message.author === tc.username) {
_381
rowDiv.addClass('own-message');
_381
}
_381
_381
tc.$messageList.append(rowDiv);
_381
scrollToMessageListBottom();
_381
};
_381
_381
function notifyMemberJoined(member) {
_381
notify(member.identity + ' joined the channel')
_381
}
_381
_381
function notifyMemberLeft(member) {
_381
notify(member.identity + ' left the channel');
_381
}
_381
_381
function notify(message) {
_381
var row = $('<div>').addClass('col-md-12');
_381
row.loadTemplate('#member-notification-template', {
_381
status: message
_381
});
_381
tc.$messageList.append(row);
_381
scrollToMessageListBottom();
_381
}
_381
_381
function showTypingStarted(member) {
_381
$typingPlaceholder.text(member.identity + ' is typing...');
_381
}
_381
_381
function hideTypingStarted(member) {
_381
$typingPlaceholder.text('');
_381
}
_381
_381
function scrollToMessageListBottom() {
_381
tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);
_381
}
_381
_381
function updateChannelUI(selectedChannel) {
_381
var channelElements = $('.channel-element').toArray();
_381
var channelElement = channelElements.filter(function(element) {
_381
return $(element).data().sid === selectedChannel.sid;
_381
});
_381
channelElement = $(channelElement);
_381
if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {
_381
tc.currentChannelContainer = channelElement;
_381
}
_381
tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');
_381
channelElement.removeClass('unselected-channel').addClass('selected-channel');
_381
tc.currentChannelContainer = channelElement;
_381
tc.currentChannel = selectedChannel;
_381
tc.loadMessages();
_381
}
_381
_381
function showAddChannelInput() {
_381
if (tc.messagingClient) {
_381
$newChannelInputRow.addClass('showing').removeClass('not-showing');
_381
$channelList.addClass('showing').removeClass('not-showing');
_381
$newChannelInput.focus();
_381
}
_381
}
_381
_381
function hideAddChannelInput() {
_381
$newChannelInputRow.addClass('not-showing').removeClass('showing');
_381
$channelList.addClass('not-showing').removeClass('showing');
_381
$newChannelInput.val('');
_381
}
_381
_381
function addChannel(channel) {
_381
if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {
_381
tc.generalChannel = channel;
_381
}
_381
var rowDiv = $('<div>').addClass('row channel-row');
_381
rowDiv.loadTemplate('#channel-template', {
_381
channelName: channel.friendlyName
_381
});
_381
_381
var channelP = rowDiv.children().children().first();
_381
_381
rowDiv.on('click', selectChannel);
_381
channelP.data('sid', channel.sid);
_381
if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {
_381
tc.currentChannelContainer = channelP;
_381
channelP.addClass('selected-channel');
_381
}
_381
else {
_381
channelP.addClass('unselected-channel')
_381
}
_381
_381
$channelList.append(rowDiv);
_381
}
_381
_381
function deleteCurrentChannel() {
_381
if (!tc.currentChannel) {
_381
return;
_381
}
_381
_381
if (tc.currentChannel.sid === tc.generalChannel.sid) {
_381
alert('You cannot delete the general channel');
_381
return;
_381
}
_381
_381
tc.currentChannel
_381
.delete()
_381
.then(function(channel) {
_381
console.log('channel: '+ channel.friendlyName + ' deleted');
_381
setupChannel(tc.generalChannel);
_381
});
_381
}
_381
_381
function selectChannel(event) {
_381
var target = $(event.target);
_381
var channelSid = target.data().sid;
_381
var selectedChannel = tc.channelArray.filter(function(channel) {
_381
return channel.sid === channelSid;
_381
})[0];
_381
if (selectedChannel === tc.currentChannel) {
_381
return;
_381
}
_381
setupChannel(selectedChannel);
_381
};
_381
_381
function disconnectClient() {
_381
leaveCurrentChannel();
_381
$channelList.text('');
_381
tc.$messageList.text('');
_381
channels = undefined;
_381
$statusRow.addClass('disconnected').removeClass('connected');
_381
tc.$messageList.addClass('disconnected').removeClass('connected');
_381
$connectPanel.addClass('disconnected').removeClass('connected');
_381
$inputText.removeClass('with-shadow');
_381
$typingRow.addClass('disconnected').removeClass('connected');
_381
}
_381
_381
tc.sortChannelsByName = function(channels) {
_381
return channels.sort(function(a, b) {
_381
if (a.friendlyName === GENERAL_CHANNEL_NAME) {
_381
return -1;
_381
}
_381
if (b.friendlyName === GENERAL_CHANNEL_NAME) {
_381
return 1;
_381
}
_381
return a.friendlyName.localeCompare(b.friendlyName);
_381
});
_381
};
_381
_381
return tc;
_381
})();

The client emits events as well. Let's see how we can listen to those events as well.


Just like with channels, we can register handlers for events on the Client:

  • channelAdded : When a channel becomes visible to the Client.
  • channelRemoved : When a channel is no longer visible to the Client.
  • tokenExpired : When the supplied token expires.

For a complete list of client events

public/js/twiliochat.js


_381
var twiliochat = (function() {
_381
var tc = {};
_381
_381
var GENERAL_CHANNEL_UNIQUE_NAME = 'general';
_381
var GENERAL_CHANNEL_NAME = 'General Channel';
_381
var MESSAGES_HISTORY_LIMIT = 50;
_381
_381
var $channelList;
_381
var $inputText;
_381
var $usernameInput;
_381
var $statusRow;
_381
var $connectPanel;
_381
var $newChannelInputRow;
_381
var $newChannelInput;
_381
var $typingRow;
_381
var $typingPlaceholder;
_381
_381
$(document).ready(function() {
_381
tc.init();
_381
});
_381
_381
tc.init = function() {
_381
tc.$messageList = $('#message-list');
_381
$channelList = $('#channel-list');
_381
$inputText = $('#input-text');
_381
$usernameInput = $('#username-input');
_381
$statusRow = $('#status-row');
_381
$connectPanel = $('#connect-panel');
_381
$newChannelInputRow = $('#new-channel-input-row');
_381
$newChannelInput = $('#new-channel-input');
_381
$typingRow = $('#typing-row');
_381
$typingPlaceholder = $('#typing-placeholder');
_381
$usernameInput.focus();
_381
$usernameInput.on('keypress', handleUsernameInputKeypress);
_381
$inputText.on('keypress', handleInputTextKeypress);
_381
$newChannelInput.on('keypress', tc.handleNewChannelInputKeypress);
_381
$('#connect-image').on('click', connectClientWithUsername);
_381
$('#add-channel-image').on('click', showAddChannelInput);
_381
$('#leave-span').on('click', disconnectClient);
_381
$('#delete-channel-span').on('click', deleteCurrentChannel);
_381
};
_381
_381
function handleUsernameInputKeypress(event) {
_381
if (event.keyCode === 13){
_381
connectClientWithUsername();
_381
}
_381
}
_381
_381
function handleInputTextKeypress(event) {
_381
if (event.keyCode === 13) {
_381
tc.currentChannel.sendMessage($(this).val());
_381
event.preventDefault();
_381
$(this).val('');
_381
}
_381
else {
_381
notifyTyping();
_381
}
_381
}
_381
_381
var notifyTyping = $.throttle(function() {
_381
tc.currentChannel.typing();
_381
}, 1000);
_381
_381
tc.handleNewChannelInputKeypress = function(event) {
_381
if (event.keyCode === 13) {
_381
tc.messagingClient
_381
.createChannel({
_381
friendlyName: $newChannelInput.val(),
_381
})
_381
.then(hideAddChannelInput);
_381
_381
$(this).val('');
_381
event.preventDefault();
_381
}
_381
};
_381
_381
function connectClientWithUsername() {
_381
var usernameText = $usernameInput.val();
_381
$usernameInput.val('');
_381
if (usernameText == '') {
_381
alert('Username cannot be empty');
_381
return;
_381
}
_381
tc.username = usernameText;
_381
fetchAccessToken(tc.username, connectMessagingClient);
_381
}
_381
_381
function fetchAccessToken(username, handler) {
_381
$.post('/token', {identity: username}, null, 'json')
_381
.done(function(response) {
_381
handler(response.token);
_381
})
_381
.fail(function(error) {
_381
console.log('Failed to fetch the Access Token with error: ' + error);
_381
});
_381
}
_381
_381
function connectMessagingClient(token) {
_381
// Initialize the Chat messaging client
_381
Twilio.Chat.Client.create(token).then(function(client) {
_381
tc.messagingClient = client;
_381
updateConnectedUI();
_381
tc.loadChannelList(tc.joinGeneralChannel);
_381
tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));
_381
tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));
_381
tc.messagingClient.on('tokenExpired', refreshToken);
_381
});
_381
}
_381
_381
function refreshToken() {
_381
fetchAccessToken(tc.username, setNewToken);
_381
}
_381
_381
function setNewToken(token) {
_381
tc.messagingClient.updateToken(tokenResponse.token);
_381
}
_381
_381
function updateConnectedUI() {
_381
$('#username-span').text(tc.username);
_381
$statusRow.addClass('connected').removeClass('disconnected');
_381
tc.$messageList.addClass('connected').removeClass('disconnected');
_381
$connectPanel.addClass('connected').removeClass('disconnected');
_381
$inputText.addClass('with-shadow');
_381
$typingRow.addClass('connected').removeClass('disconnected');
_381
}
_381
_381
tc.loadChannelList = function(handler) {
_381
if (tc.messagingClient === undefined) {
_381
console.log('Client is not initialized');
_381
return;
_381
}
_381
_381
tc.messagingClient.getPublicChannelDescriptors().then(function(channels) {
_381
tc.channelArray = tc.sortChannelsByName(channels.items);
_381
$channelList.text('');
_381
tc.channelArray.forEach(addChannel);
_381
if (typeof handler === 'function') {
_381
handler();
_381
}
_381
});
_381
};
_381
_381
tc.joinGeneralChannel = function() {
_381
console.log('Attempting to join "general" chat channel...');
_381
if (!tc.generalChannel) {
_381
// If it doesn't exist, let's create it
_381
tc.messagingClient.createChannel({
_381
uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,
_381
friendlyName: GENERAL_CHANNEL_NAME
_381
}).then(function(channel) {
_381
console.log('Created general channel');
_381
tc.generalChannel = channel;
_381
tc.loadChannelList(tc.joinGeneralChannel);
_381
});
_381
}
_381
else {
_381
console.log('Found general channel:');
_381
setupChannel(tc.generalChannel);
_381
}
_381
};
_381
_381
function initChannel(channel) {
_381
console.log('Initialized channel ' + channel.friendlyName);
_381
return tc.messagingClient.getChannelBySid(channel.sid);
_381
}
_381
_381
function joinChannel(_channel) {
_381
return _channel.join()
_381
.then(function(joinedChannel) {
_381
console.log('Joined channel ' + joinedChannel.friendlyName);
_381
updateChannelUI(_channel);
_381
_381
return joinedChannel;
_381
})
_381
.catch(function(err) {
_381
if (_channel.status == 'joined') {
_381
updateChannelUI(_channel);
_381
return _channel;
_381
}
_381
console.error(
_381
"Couldn't join channel " + _channel.friendlyName + ' because -> ' + err
_381
);
_381
});
_381
}
_381
_381
function initChannelEvents() {
_381
console.log(tc.currentChannel.friendlyName + ' ready.');
_381
tc.currentChannel.on('messageAdded', tc.addMessageToList);
_381
tc.currentChannel.on('typingStarted', showTypingStarted);
_381
tc.currentChannel.on('typingEnded', hideTypingStarted);
_381
tc.currentChannel.on('memberJoined', notifyMemberJoined);
_381
tc.currentChannel.on('memberLeft', notifyMemberLeft);
_381
$inputText.prop('disabled', false).focus();
_381
}
_381
_381
function setupChannel(channel) {
_381
return leaveCurrentChannel()
_381
.then(function() {
_381
return initChannel(channel);
_381
})
_381
.then(function(_channel) {
_381
return joinChannel(_channel);
_381
})
_381
.then(initChannelEvents);
_381
}
_381
_381
tc.loadMessages = function() {
_381
tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT).then(function (messages) {
_381
messages.items.forEach(tc.addMessageToList);
_381
});
_381
};
_381
_381
function leaveCurrentChannel() {
_381
if (tc.currentChannel) {
_381
return tc.currentChannel.leave().then(function(leftChannel) {
_381
console.log('left ' + leftChannel.friendlyName);
_381
leftChannel.removeListener('messageAdded', tc.addMessageToList);
_381
leftChannel.removeListener('typingStarted', showTypingStarted);
_381
leftChannel.removeListener('typingEnded', hideTypingStarted);
_381
leftChannel.removeListener('memberJoined', notifyMemberJoined);
_381
leftChannel.removeListener('memberLeft', notifyMemberLeft);
_381
});
_381
} else {
_381
return Promise.resolve();
_381
}
_381
}
_381
_381
tc.addMessageToList = function(message) {
_381
var rowDiv = $('<div>').addClass('row no-margin');
_381
rowDiv.loadTemplate($('#message-template'), {
_381
username: message.author,
_381
date: dateFormatter.getTodayDate(message.dateCreated),
_381
body: message.body
_381
});
_381
if (message.author === tc.username) {
_381
rowDiv.addClass('own-message');
_381
}
_381
_381
tc.$messageList.append(rowDiv);
_381
scrollToMessageListBottom();
_381
};
_381
_381
function notifyMemberJoined(member) {
_381
notify(member.identity + ' joined the channel')
_381
}
_381
_381
function notifyMemberLeft(member) {
_381
notify(member.identity + ' left the channel');
_381
}
_381
_381
function notify(message) {
_381
var row = $('<div>').addClass('col-md-12');
_381
row.loadTemplate('#member-notification-template', {
_381
status: message
_381
});
_381
tc.$messageList.append(row);
_381
scrollToMessageListBottom();
_381
}
_381
_381
function showTypingStarted(member) {
_381
$typingPlaceholder.text(member.identity + ' is typing...');
_381
}
_381
_381
function hideTypingStarted(member) {
_381
$typingPlaceholder.text('');
_381
}
_381
_381
function scrollToMessageListBottom() {
_381
tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);
_381
}
_381
_381
function updateChannelUI(selectedChannel) {
_381
var channelElements = $('.channel-element').toArray();
_381
var channelElement = channelElements.filter(function(element) {
_381
return $(element).data().sid === selectedChannel.sid;
_381
});
_381
channelElement = $(channelElement);
_381
if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {
_381
tc.currentChannelContainer = channelElement;
_381
}
_381
tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');
_381
channelElement.removeClass('unselected-channel').addClass('selected-channel');
_381
tc.currentChannelContainer = channelElement;
_381
tc.currentChannel = selectedChannel;
_381
tc.loadMessages();
_381
}
_381
_381
function showAddChannelInput() {
_381
if (tc.messagingClient) {
_381
$newChannelInputRow.addClass('showing').removeClass('not-showing');
_381
$channelList.addClass('showing').removeClass('not-showing');
_381
$newChannelInput.focus();
_381
}
_381
}
_381
_381
function hideAddChannelInput() {
_381
$newChannelInputRow.addClass('not-showing').removeClass('showing');
_381
$channelList.addClass('not-showing').removeClass('showing');
_381
$newChannelInput.val('');
_381
}
_381
_381
function addChannel(channel) {
_381
if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {
_381
tc.generalChannel = channel;
_381
}
_381
var rowDiv = $('<div>').addClass('row channel-row');
_381
rowDiv.loadTemplate('#channel-template', {
_381
channelName: channel.friendlyName
_381
});
_381
_381
var channelP = rowDiv.children().children().first();
_381
_381
rowDiv.on('click', selectChannel);
_381
channelP.data('sid', channel.sid);
_381
if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {
_381
tc.currentChannelContainer = channelP;
_381
channelP.addClass('selected-channel');
_381
}
_381
else {
_381
channelP.addClass('unselected-channel')
_381
}
_381
_381
$channelList.append(rowDiv);
_381
}
_381
_381
function deleteCurrentChannel() {
_381
if (!tc.currentChannel) {
_381
return;
_381
}
_381
_381
if (tc.currentChannel.sid === tc.generalChannel.sid) {
_381
alert('You cannot delete the general channel');
_381
return;
_381
}
_381
_381
tc.currentChannel
_381
.delete()
_381
.then(function(channel) {
_381
console.log('channel: '+ channel.friendlyName + ' deleted');
_381
setupChannel(tc.generalChannel);
_381
});
_381
}
_381
_381
function selectChannel(event) {
_381
var target = $(event.target);
_381
var channelSid = target.data().sid;
_381
var selectedChannel = tc.channelArray.filter(function(channel) {
_381
return channel.sid === channelSid;
_381
})[0];
_381
if (selectedChannel === tc.currentChannel) {
_381
return;
_381
}
_381
setupChannel(selectedChannel);
_381
};
_381
_381
function disconnectClient() {
_381
leaveCurrentChannel();
_381
$channelList.text('');
_381
tc.$messageList.text('');
_381
channels = undefined;
_381
$statusRow.addClass('disconnected').removeClass('connected');
_381
tc.$messageList.addClass('disconnected').removeClass('connected');
_381
$connectPanel.addClass('disconnected').removeClass('connected');
_381
$inputText.removeClass('with-shadow');
_381
$typingRow.addClass('disconnected').removeClass('connected');
_381
}
_381
_381
tc.sortChannelsByName = function(channels) {
_381
return channels.sort(function(a, b) {
_381
if (a.friendlyName === GENERAL_CHANNEL_NAME) {
_381
return -1;
_381
}
_381
if (b.friendlyName === GENERAL_CHANNEL_NAME) {
_381
return 1;
_381
}
_381
return a.friendlyName.localeCompare(b.friendlyName);
_381
});
_381
};
_381
_381
return tc;
_381
})();

We've actually got a real chat app going here, but let's make it more interesting with multiple channels.


When a user clicks on the "+ Channel" link we'll show an input text field where it's possible to type the name of the new channel. Creating a channel 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.

public/js/twiliochat.js


_381
var twiliochat = (function() {
_381
var tc = {};
_381
_381
var GENERAL_CHANNEL_UNIQUE_NAME = 'general';
_381
var GENERAL_CHANNEL_NAME = 'General Channel';
_381
var MESSAGES_HISTORY_LIMIT = 50;
_381
_381
var $channelList;
_381
var $inputText;
_381
var $usernameInput;
_381
var $statusRow;
_381
var $connectPanel;
_381
var $newChannelInputRow;
_381
var $newChannelInput;
_381
var $typingRow;
_381
var $typingPlaceholder;
_381
_381
$(document).ready(function() {
_381
tc.init();
_381
});
_381
_381
tc.init = function() {
_381
tc.$messageList = $('#message-list');
_381
$channelList = $('#channel-list');
_381
$inputText = $('#input-text');
_381
$usernameInput = $('#username-input');
_381
$statusRow = $('#status-row');
_381
$connectPanel = $('#connect-panel');
_381
$newChannelInputRow = $('#new-channel-input-row');
_381
$newChannelInput = $('#new-channel-input');
_381
$typingRow = $('#typing-row');
_381
$typingPlaceholder = $('#typing-placeholder');
_381
$usernameInput.focus();
_381
$usernameInput.on('keypress', handleUsernameInputKeypress);
_381
$inputText.on('keypress', handleInputTextKeypress);
_381
$newChannelInput.on('keypress', tc.handleNewChannelInputKeypress);
_381
$('#connect-image').on('click', connectClientWithUsername);
_381
$('#add-channel-image').on('click', showAddChannelInput);
_381
$('#leave-span').on('click', disconnectClient);
_381
$('#delete-channel-span').on('click', deleteCurrentChannel);
_381
};
_381
_381
function handleUsernameInputKeypress(event) {
_381
if (event.keyCode === 13){
_381
connectClientWithUsername();
_381
}
_381
}
_381
_381
function handleInputTextKeypress(event) {
_381
if (event.keyCode === 13) {
_381
tc.currentChannel.sendMessage($(this).val());
_381
event.preventDefault();
_381
$(this).val('');
_381
}
_381
else {
_381
notifyTyping();
_381
}
_381
}
_381
_381
var notifyTyping = $.throttle(function() {
_381
tc.currentChannel.typing();
_381
}, 1000);
_381
_381
tc.handleNewChannelInputKeypress = function(event) {
_381
if (event.keyCode === 13) {
_381
tc.messagingClient
_381
.createChannel({
_381
friendlyName: $newChannelInput.val(),
_381
})
_381
.then(hideAddChannelInput);
_381
_381
$(this).val('');
_381
event.preventDefault();
_381
}
_381
};
_381
_381
function connectClientWithUsername() {
_381
var usernameText = $usernameInput.val();
_381
$usernameInput.val('');
_381
if (usernameText == '') {
_381
alert('Username cannot be empty');
_381
return;
_381
}
_381
tc.username = usernameText;
_381
fetchAccessToken(tc.username, connectMessagingClient);
_381
}
_381
_381
function fetchAccessToken(username, handler) {
_381
$.post('/token', {identity: username}, null, 'json')
_381
.done(function(response) {
_381
handler(response.token);
_381
})
_381
.fail(function(error) {
_381
console.log('Failed to fetch the Access Token with error: ' + error);
_381
});
_381
}
_381
_381
function connectMessagingClient(token) {
_381
// Initialize the Chat messaging client
_381
Twilio.Chat.Client.create(token).then(function(client) {
_381
tc.messagingClient = client;
_381
updateConnectedUI();
_381
tc.loadChannelList(tc.joinGeneralChannel);
_381
tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));
_381
tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));
_381
tc.messagingClient.on('tokenExpired', refreshToken);
_381
});
_381
}
_381
_381
function refreshToken() {
_381
fetchAccessToken(tc.username, setNewToken);
_381
}
_381
_381
function setNewToken(token) {
_381
tc.messagingClient.updateToken(tokenResponse.token);
_381
}
_381
_381
function updateConnectedUI() {
_381
$('#username-span').text(tc.username);
_381
$statusRow.addClass('connected').removeClass('disconnected');
_381
tc.$messageList.addClass('connected').removeClass('disconnected');
_381
$connectPanel.addClass('connected').removeClass('disconnected');
_381
$inputText.addClass('with-shadow');
_381
$typingRow.addClass('connected').removeClass('disconnected');
_381
}
_381
_381
tc.loadChannelList = function(handler) {
_381
if (tc.messagingClient === undefined) {
_381
console.log('Client is not initialized');
_381
return;
_381
}
_381
_381
tc.messagingClient.getPublicChannelDescriptors().then(function(channels) {
_381
tc.channelArray = tc.sortChannelsByName(channels.items);
_381
$channelList.text('');
_381
tc.channelArray.forEach(addChannel);
_381
if (typeof handler === 'function') {
_381
handler();
_381
}
_381
});
_381
};
_381
_381
tc.joinGeneralChannel = function() {
_381
console.log('Attempting to join "general" chat channel...');
_381
if (!tc.generalChannel) {
_381
// If it doesn't exist, let's create it
_381
tc.messagingClient.createChannel({
_381
uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,
_381
friendlyName: GENERAL_CHANNEL_NAME
_381
}).then(function(channel) {
_381
console.log('Created general channel');
_381
tc.generalChannel = channel;
_381
tc.loadChannelList(tc.joinGeneralChannel);
_381
});
_381
}
_381
else {
_381
console.log('Found general channel:');
_381
setupChannel(tc.generalChannel);
_381
}
_381
};
_381
_381
function initChannel(channel) {
_381
console.log('Initialized channel ' + channel.friendlyName);
_381
return tc.messagingClient.getChannelBySid(channel.sid);
_381
}
_381
_381
function joinChannel(_channel) {
_381
return _channel.join()
_381
.then(function(joinedChannel) {
_381
console.log('Joined channel ' + joinedChannel.friendlyName);
_381
updateChannelUI(_channel);
_381
_381
return joinedChannel;
_381
})
_381
.catch(function(err) {
_381
if (_channel.status == 'joined') {
_381
updateChannelUI(_channel);
_381
return _channel;
_381
}
_381
console.error(
_381
"Couldn't join channel " + _channel.friendlyName + ' because -> ' + err
_381
);
_381
});
_381
}
_381
_381
function initChannelEvents() {
_381
console.log(tc.currentChannel.friendlyName + ' ready.');
_381
tc.currentChannel.on('messageAdded', tc.addMessageToList);
_381
tc.currentChannel.on('typingStarted', showTypingStarted);
_381
tc.currentChannel.on('typingEnded', hideTypingStarted);
_381
tc.currentChannel.on('memberJoined', notifyMemberJoined);
_381
tc.currentChannel.on('memberLeft', notifyMemberLeft);
_381
$inputText.prop('disabled', false).focus();
_381
}
_381
_381
function setupChannel(channel) {
_381
return leaveCurrentChannel()
_381
.then(function() {
_381
return initChannel(channel);
_381
})
_381
.then(function(_channel) {
_381
return joinChannel(_channel);
_381
})
_381
.then(initChannelEvents);
_381
}
_381
_381
tc.loadMessages = function() {
_381
tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT).then(function (messages) {
_381
messages.items.forEach(tc.addMessageToList);
_381
});
_381
};
_381
_381
function leaveCurrentChannel() {
_381
if (tc.currentChannel) {
_381
return tc.currentChannel.leave().then(function(leftChannel) {
_381
console.log('left ' + leftChannel.friendlyName);
_381
leftChannel.removeListener('messageAdded', tc.addMessageToList);
_381
leftChannel.removeListener('typingStarted', showTypingStarted);
_381
leftChannel.removeListener('typingEnded', hideTypingStarted);
_381
leftChannel.removeListener('memberJoined', notifyMemberJoined);
_381
leftChannel.removeListener('memberLeft', notifyMemberLeft);
_381
});
_381
} else {
_381
return Promise.resolve();
_381
}
_381
}
_381
_381
tc.addMessageToList = function(message) {
_381
var rowDiv = $('<div>').addClass('row no-margin');
_381
rowDiv.loadTemplate($('#message-template'), {
_381
username: message.author,
_381
date: dateFormatter.getTodayDate(message.dateCreated),
_381
body: message.body
_381
});
_381
if (message.author === tc.username) {
_381
rowDiv.addClass('own-message');
_381
}
_381
_381
tc.$messageList.append(rowDiv);
_381
scrollToMessageListBottom();
_381
};
_381
_381
function notifyMemberJoined(member) {
_381
notify(member.identity + ' joined the channel')
_381
}
_381
_381
function notifyMemberLeft(member) {
_381
notify(member.identity + ' left the channel');
_381
}
_381
_381
function notify(message) {
_381
var row = $('<div>').addClass('col-md-12');
_381
row.loadTemplate('#member-notification-template', {
_381
status: message
_381
});
_381
tc.$messageList.append(row);
_381
scrollToMessageListBottom();
_381
}
_381
_381
function showTypingStarted(member) {
_381
$typingPlaceholder.text(member.identity + ' is typing...');
_381
}
_381
_381
function hideTypingStarted(member) {
_381
$typingPlaceholder.text('');
_381
}
_381
_381
function scrollToMessageListBottom() {
_381
tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);
_381
}
_381
_381
function updateChannelUI(selectedChannel) {
_381
var channelElements = $('.channel-element').toArray();
_381
var channelElement = channelElements.filter(function(element) {
_381
return $(element).data().sid === selectedChannel.sid;
_381
});
_381
channelElement = $(channelElement);
_381
if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {
_381
tc.currentChannelContainer = channelElement;
_381
}
_381
tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');
_381
channelElement.removeClass('unselected-channel').addClass('selected-channel');
_381
tc.currentChannelContainer = channelElement;
_381
tc.currentChannel = selectedChannel;
_381
tc.loadMessages();
_381
}
_381
_381
function showAddChannelInput() {
_381
if (tc.messagingClient) {
_381
$newChannelInputRow.addClass('showing').removeClass('not-showing');
_381
$channelList.addClass('showing').removeClass('not-showing');
_381
$newChannelInput.focus();
_381
}
_381
}
_381
_381
function hideAddChannelInput() {
_381
$newChannelInputRow.addClass('not-showing').removeClass('showing');
_381
$channelList.addClass('not-showing').removeClass('showing');
_381
$newChannelInput.val('');
_381
}
_381
_381
function addChannel(channel) {
_381
if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {
_381
tc.generalChannel = channel;
_381
}
_381
var rowDiv = $('<div>').addClass('row channel-row');
_381
rowDiv.loadTemplate('#channel-template', {
_381
channelName: channel.friendlyName
_381
});
_381
_381
var channelP = rowDiv.children().children().first();
_381
_381
rowDiv.on('click', selectChannel);
_381
channelP.data('sid', channel.sid);
_381
if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {
_381
tc.currentChannelContainer = channelP;
_381
channelP.addClass('selected-channel');
_381
}
_381
else {
_381
channelP.addClass('unselected-channel')
_381
}
_381
_381
$channelList.append(rowDiv);
_381
}
_381
_381
function deleteCurrentChannel() {
_381
if (!tc.currentChannel) {
_381
return;
_381
}
_381
_381
if (tc.currentChannel.sid === tc.generalChannel.sid) {
_381
alert('You cannot delete the general channel');
_381
return;
_381
}
_381
_381
tc.currentChannel
_381
.delete()
_381
.then(function(channel) {
_381
console.log('channel: '+ channel.friendlyName + ' deleted');
_381
setupChannel(tc.generalChannel);
_381
});
_381
}
_381
_381
function selectChannel(event) {
_381
var target = $(event.target);
_381
var channelSid = target.data().sid;
_381
var selectedChannel = tc.channelArray.filter(function(channel) {
_381
return channel.sid === channelSid;
_381
})[0];
_381
if (selectedChannel === tc.currentChannel) {
_381
return;
_381
}
_381
setupChannel(selectedChannel);
_381
};
_381
_381
function disconnectClient() {
_381
leaveCurrentChannel();
_381
$channelList.text('');
_381
tc.$messageList.text('');
_381
channels = undefined;
_381
$statusRow.addClass('disconnected').removeClass('connected');
_381
tc.$messageList.addClass('disconnected').removeClass('connected');
_381
$connectPanel.addClass('disconnected').removeClass('connected');
_381
$inputText.removeClass('with-shadow');
_381
$typingRow.addClass('disconnected').removeClass('connected');
_381
}
_381
_381
tc.sortChannelsByName = function(channels) {
_381
return channels.sort(function(a, b) {
_381
if (a.friendlyName === GENERAL_CHANNEL_NAME) {
_381
return -1;
_381
}
_381
if (b.friendlyName === GENERAL_CHANNEL_NAME) {
_381
return 1;
_381
}
_381
return a.friendlyName.localeCompare(b.friendlyName);
_381
});
_381
};
_381
_381
return tc;
_381
})();

Next, we will see how we can switch between channels.


When you tap on the name of a channel from the sidebar, that channel is set as the selectedChannel. The selectChannel method takes care of joining to the selected channel and setting up the selectedChannel.

public/js/twiliochat.js


_381
var twiliochat = (function() {
_381
var tc = {};
_381
_381
var GENERAL_CHANNEL_UNIQUE_NAME = 'general';
_381
var GENERAL_CHANNEL_NAME = 'General Channel';
_381
var MESSAGES_HISTORY_LIMIT = 50;
_381
_381
var $channelList;
_381
var $inputText;
_381
var $usernameInput;
_381
var $statusRow;
_381
var $connectPanel;
_381
var $newChannelInputRow;
_381
var $newChannelInput;
_381
var $typingRow;
_381
var $typingPlaceholder;
_381
_381
$(document).ready(function() {
_381
tc.init();
_381
});
_381
_381
tc.init = function() {
_381
tc.$messageList = $('#message-list');
_381
$channelList = $('#channel-list');
_381
$inputText = $('#input-text');
_381
$usernameInput = $('#username-input');
_381
$statusRow = $('#status-row');
_381
$connectPanel = $('#connect-panel');
_381
$newChannelInputRow = $('#new-channel-input-row');
_381
$newChannelInput = $('#new-channel-input');
_381
$typingRow = $('#typing-row');
_381
$typingPlaceholder = $('#typing-placeholder');
_381
$usernameInput.focus();
_381
$usernameInput.on('keypress', handleUsernameInputKeypress);
_381
$inputText.on('keypress', handleInputTextKeypress);
_381
$newChannelInput.on('keypress', tc.handleNewChannelInputKeypress);
_381
$('#connect-image').on('click', connectClientWithUsername);
_381
$('#add-channel-image').on('click', showAddChannelInput);
_381
$('#leave-span').on('click', disconnectClient);
_381
$('#delete-channel-span').on('click', deleteCurrentChannel);
_381
};
_381
_381
function handleUsernameInputKeypress(event) {
_381
if (event.keyCode === 13){
_381
connectClientWithUsername();
_381
}
_381
}
_381
_381
function handleInputTextKeypress(event) {
_381
if (event.keyCode === 13) {
_381
tc.currentChannel.sendMessage($(this).val());
_381
event.preventDefault();
_381
$(this).val('');
_381
}
_381
else {
_381
notifyTyping();
_381
}
_381
}
_381
_381
var notifyTyping = $.throttle(function() {
_381
tc.currentChannel.typing();
_381
}, 1000);
_381
_381
tc.handleNewChannelInputKeypress = function(event) {
_381
if (event.keyCode === 13) {
_381
tc.messagingClient
_381
.createChannel({
_381
friendlyName: $newChannelInput.val(),
_381
})
_381
.then(hideAddChannelInput);
_381
_381
$(this).val('');
_381
event.preventDefault();
_381
}
_381
};
_381
_381
function connectClientWithUsername() {
_381
var usernameText = $usernameInput.val();
_381
$usernameInput.val('');
_381
if (usernameText == '') {
_381
alert('Username cannot be empty');
_381
return;
_381
}
_381
tc.username = usernameText;
_381
fetchAccessToken(tc.username, connectMessagingClient);
_381
}
_381
_381
function fetchAccessToken(username, handler) {
_381
$.post('/token', {identity: username}, null, 'json')
_381
.done(function(response) {
_381
handler(response.token);
_381
})
_381
.fail(function(error) {
_381
console.log('Failed to fetch the Access Token with error: ' + error);
_381
});
_381
}
_381
_381
function connectMessagingClient(token) {
_381
// Initialize the Chat messaging client
_381
Twilio.Chat.Client.create(token).then(function(client) {
_381
tc.messagingClient = client;
_381
updateConnectedUI();
_381
tc.loadChannelList(tc.joinGeneralChannel);
_381
tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));
_381
tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));
_381
tc.messagingClient.on('tokenExpired', refreshToken);
_381
});
_381
}
_381
_381
function refreshToken() {
_381
fetchAccessToken(tc.username, setNewToken);
_381
}
_381
_381
function setNewToken(token) {
_381
tc.messagingClient.updateToken(tokenResponse.token);
_381
}
_381
_381
function updateConnectedUI() {
_381
$('#username-span').text(tc.username);
_381
$statusRow.addClass('connected').removeClass('disconnected');
_381
tc.$messageList.addClass('connected').removeClass('disconnected');
_381
$connectPanel.addClass('connected').removeClass('disconnected');
_381
$inputText.addClass('with-shadow');
_381
$typingRow.addClass('connected').removeClass('disconnected');
_381
}
_381
_381
tc.loadChannelList = function(handler) {
_381
if (tc.messagingClient === undefined) {
_381
console.log('Client is not initialized');
_381
return;
_381
}
_381
_381
tc.messagingClient.getPublicChannelDescriptors().then(function(channels) {
_381
tc.channelArray = tc.sortChannelsByName(channels.items);
_381
$channelList.text('');
_381
tc.channelArray.forEach(addChannel);
_381
if (typeof handler === 'function') {
_381
handler();
_381
}
_381
});
_381
};
_381
_381
tc.joinGeneralChannel = function() {
_381
console.log('Attempting to join "general" chat channel...');
_381
if (!tc.generalChannel) {
_381
// If it doesn't exist, let's create it
_381
tc.messagingClient.createChannel({
_381
uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,
_381
friendlyName: GENERAL_CHANNEL_NAME
_381
}).then(function(channel) {
_381
console.log('Created general channel');
_381
tc.generalChannel = channel;
_381
tc.loadChannelList(tc.joinGeneralChannel);
_381
});
_381
}
_381
else {
_381
console.log('Found general channel:');
_381
setupChannel(tc.generalChannel);
_381
}
_381
};
_381
_381
function initChannel(channel) {
_381
console.log('Initialized channel ' + channel.friendlyName);
_381
return tc.messagingClient.getChannelBySid(channel.sid);
_381
}
_381
_381
function joinChannel(_channel) {
_381
return _channel.join()
_381
.then(function(joinedChannel) {
_381
console.log('Joined channel ' + joinedChannel.friendlyName);
_381
updateChannelUI(_channel);
_381
_381
return joinedChannel;
_381
})
_381
.catch(function(err) {
_381
if (_channel.status == 'joined') {
_381
updateChannelUI(_channel);
_381
return _channel;
_381
}
_381
console.error(
_381
"Couldn't join channel " + _channel.friendlyName + ' because -> ' + err
_381
);
_381
});
_381
}
_381
_381
function initChannelEvents() {
_381
console.log(tc.currentChannel.friendlyName + ' ready.');
_381
tc.currentChannel.on('messageAdded', tc.addMessageToList);
_381
tc.currentChannel.on('typingStarted', showTypingStarted);
_381
tc.currentChannel.on('typingEnded', hideTypingStarted);
_381
tc.currentChannel.on('memberJoined', notifyMemberJoined);
_381
tc.currentChannel.on('memberLeft', notifyMemberLeft);
_381
$inputText.prop('disabled', false).focus();
_381
}
_381
_381
function setupChannel(channel) {
_381
return leaveCurrentChannel()
_381
.then(function() {
_381
return initChannel(channel);
_381
})
_381
.then(function(_channel) {
_381
return joinChannel(_channel);
_381
})
_381
.then(initChannelEvents);
_381
}
_381
_381
tc.loadMessages = function() {
_381
tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT).then(function (messages) {
_381
messages.items.forEach(tc.addMessageToList);
_381
});
_381
};
_381
_381
function leaveCurrentChannel() {
_381
if (tc.currentChannel) {
_381
return tc.currentChannel.leave().then(function(leftChannel) {
_381
console.log('left ' + leftChannel.friendlyName);
_381
leftChannel.removeListener('messageAdded', tc.addMessageToList);
_381
leftChannel.removeListener('typingStarted', showTypingStarted);
_381
leftChannel.removeListener('typingEnded', hideTypingStarted);
_381
leftChannel.removeListener('memberJoined', notifyMemberJoined);
_381
leftChannel.removeListener('memberLeft', notifyMemberLeft);
_381
});
_381
} else {
_381
return Promise.resolve();
_381
}
_381
}
_381
_381
tc.addMessageToList = function(message) {
_381
var rowDiv = $('<div>').addClass('row no-margin');
_381
rowDiv.loadTemplate($('#message-template'), {
_381
username: message.author,
_381
date: dateFormatter.getTodayDate(message.dateCreated),
_381
body: message.body
_381
});
_381
if (message.author === tc.username) {
_381
rowDiv.addClass('own-message');
_381
}
_381
_381
tc.$messageList.append(rowDiv);
_381
scrollToMessageListBottom();
_381
};
_381
_381
function notifyMemberJoined(member) {
_381
notify(member.identity + ' joined the channel')
_381
}
_381
_381
function notifyMemberLeft(member) {
_381
notify(member.identity + ' left the channel');
_381
}
_381
_381
function notify(message) {
_381
var row = $('<div>').addClass('col-md-12');
_381
row.loadTemplate('#member-notification-template', {
_381
status: message
_381
});
_381
tc.$messageList.append(row);
_381
scrollToMessageListBottom();
_381
}
_381
_381
function showTypingStarted(member) {
_381
$typingPlaceholder.text(member.identity + ' is typing...');
_381
}
_381
_381
function hideTypingStarted(member) {
_381
$typingPlaceholder.text('');
_381
}
_381
_381
function scrollToMessageListBottom() {
_381
tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);
_381
}
_381
_381
function updateChannelUI(selectedChannel) {
_381
var channelElements = $('.channel-element').toArray();
_381
var channelElement = channelElements.filter(function(element) {
_381
return $(element).data().sid === selectedChannel.sid;
_381
});
_381
channelElement = $(channelElement);
_381
if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {
_381
tc.currentChannelContainer = channelElement;
_381
}
_381
tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');
_381
channelElement.removeClass('unselected-channel').addClass('selected-channel');
_381
tc.currentChannelContainer = channelElement;
_381
tc.currentChannel = selectedChannel;
_381
tc.loadMessages();
_381
}
_381
_381
function showAddChannelInput() {
_381
if (tc.messagingClient) {
_381
$newChannelInputRow.addClass('showing').removeClass('not-showing');
_381
$channelList.addClass('showing').removeClass('not-showing');
_381
$newChannelInput.focus();
_381
}
_381
}
_381
_381
function hideAddChannelInput() {
_381
$newChannelInputRow.addClass('not-showing').removeClass('showing');
_381
$channelList.addClass('not-showing').removeClass('showing');
_381
$newChannelInput.val('');
_381
}
_381
_381
function addChannel(channel) {
_381
if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {
_381
tc.generalChannel = channel;
_381
}
_381
var rowDiv = $('<div>').addClass('row channel-row');
_381
rowDiv.loadTemplate('#channel-template', {
_381
channelName: channel.friendlyName
_381
});
_381
_381
var channelP = rowDiv.children().children().first();
_381
_381
rowDiv.on('click', selectChannel);
_381
channelP.data('sid', channel.sid);
_381
if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {
_381
tc.currentChannelContainer = channelP;
_381
channelP.addClass('selected-channel');
_381
}
_381
else {
_381
channelP.addClass('unselected-channel')
_381
}
_381
_381
$channelList.append(rowDiv);
_381
}
_381
_381
function deleteCurrentChannel() {
_381
if (!tc.currentChannel) {
_381
return;
_381
}
_381
_381
if (tc.currentChannel.sid === tc.generalChannel.sid) {
_381
alert('You cannot delete the general channel');
_381
return;
_381
}
_381
_381
tc.currentChannel
_381
.delete()
_381
.then(function(channel) {
_381
console.log('channel: '+ channel.friendlyName + ' deleted');
_381
setupChannel(tc.generalChannel);
_381
});
_381
}
_381
_381
function selectChannel(event) {
_381
var target = $(event.target);
_381
var channelSid = target.data().sid;
_381
var selectedChannel = tc.channelArray.filter(function(channel) {
_381
return channel.sid === channelSid;
_381
})[0];
_381
if (selectedChannel === tc.currentChannel) {
_381
return;
_381
}
_381
setupChannel(selectedChannel);
_381
};
_381
_381
function disconnectClient() {
_381
leaveCurrentChannel();
_381
$channelList.text('');
_381
tc.$messageList.text('');
_381
channels = undefined;
_381
$statusRow.addClass('disconnected').removeClass('connected');
_381
tc.$messageList.addClass('disconnected').removeClass('connected');
_381
$connectPanel.addClass('disconnected').removeClass('connected');
_381
$inputText.removeClass('with-shadow');
_381
$typingRow.addClass('disconnected').removeClass('connected');
_381
}
_381
_381
tc.sortChannelsByName = function(channels) {
_381
return channels.sort(function(a, b) {
_381
if (a.friendlyName === GENERAL_CHANNEL_NAME) {
_381
return -1;
_381
}
_381
if (b.friendlyName === GENERAL_CHANNEL_NAME) {
_381
return 1;
_381
}
_381
return a.friendlyName.localeCompare(b.friendlyName);
_381
});
_381
};
_381
_381
return tc;
_381
})();

At some point, your users will want to delete a channel. Let's have a look at how that can be done.


Deleting a channel is easier than creating one. The application lets the user delete the channel they are currently on through the "delete current channel" link. The only thing you need to do to actually delete the channel from Twilio, is call the delete(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.

public/js/twiliochat.js


_381
var twiliochat = (function() {
_381
var tc = {};
_381
_381
var GENERAL_CHANNEL_UNIQUE_NAME = 'general';
_381
var GENERAL_CHANNEL_NAME = 'General Channel';
_381
var MESSAGES_HISTORY_LIMIT = 50;
_381
_381
var $channelList;
_381
var $inputText;
_381
var $usernameInput;
_381
var $statusRow;
_381
var $connectPanel;
_381
var $newChannelInputRow;
_381
var $newChannelInput;
_381
var $typingRow;
_381
var $typingPlaceholder;
_381
_381
$(document).ready(function() {
_381
tc.init();
_381
});
_381
_381
tc.init = function() {
_381
tc.$messageList = $('#message-list');
_381
$channelList = $('#channel-list');
_381
$inputText = $('#input-text');
_381
$usernameInput = $('#username-input');
_381
$statusRow = $('#status-row');
_381
$connectPanel = $('#connect-panel');
_381
$newChannelInputRow = $('#new-channel-input-row');
_381
$newChannelInput = $('#new-channel-input');
_381
$typingRow = $('#typing-row');
_381
$typingPlaceholder = $('#typing-placeholder');
_381
$usernameInput.focus();
_381
$usernameInput.on('keypress', handleUsernameInputKeypress);
_381
$inputText.on('keypress', handleInputTextKeypress);
_381
$newChannelInput.on('keypress', tc.handleNewChannelInputKeypress);
_381
$('#connect-image').on('click', connectClientWithUsername);
_381
$('#add-channel-image').on('click', showAddChannelInput);
_381
$('#leave-span').on('click', disconnectClient);
_381
$('#delete-channel-span').on('click', deleteCurrentChannel);
_381
};
_381
_381
function handleUsernameInputKeypress(event) {
_381
if (event.keyCode === 13){
_381
connectClientWithUsername();
_381
}
_381
}
_381
_381
function handleInputTextKeypress(event) {
_381
if (event.keyCode === 13) {
_381
tc.currentChannel.sendMessage($(this).val());
_381
event.preventDefault();
_381
$(this).val('');
_381
}
_381
else {
_381
notifyTyping();
_381
}
_381
}
_381
_381
var notifyTyping = $.throttle(function() {
_381
tc.currentChannel.typing();
_381
}, 1000);
_381
_381
tc.handleNewChannelInputKeypress = function(event) {
_381
if (event.keyCode === 13) {
_381
tc.messagingClient
_381
.createChannel({
_381
friendlyName: $newChannelInput.val(),
_381
})
_381
.then(hideAddChannelInput);
_381
_381
$(this).val('');
_381
event.preventDefault();
_381
}
_381
};
_381
_381
function connectClientWithUsername() {
_381
var usernameText = $usernameInput.val();
_381
$usernameInput.val('');
_381
if (usernameText == '') {
_381
alert('Username cannot be empty');
_381
return;
_381
}
_381
tc.username = usernameText;
_381
fetchAccessToken(tc.username, connectMessagingClient);
_381
}
_381
_381
function fetchAccessToken(username, handler) {
_381
$.post('/token', {identity: username}, null, 'json')
_381
.done(function(response) {
_381
handler(response.token);
_381
})
_381
.fail(function(error) {
_381
console.log('Failed to fetch the Access Token with error: ' + error);
_381
});
_381
}
_381
_381
function connectMessagingClient(token) {
_381
// Initialize the Chat messaging client
_381
Twilio.Chat.Client.create(token).then(function(client) {
_381
tc.messagingClient = client;
_381
updateConnectedUI();
_381
tc.loadChannelList(tc.joinGeneralChannel);
_381
tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));
_381
tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));
_381
tc.messagingClient.on('tokenExpired', refreshToken);
_381
});
_381
}
_381
_381
function refreshToken() {
_381
fetchAccessToken(tc.username, setNewToken);
_381
}
_381
_381
function setNewToken(token) {
_381
tc.messagingClient.updateToken(tokenResponse.token);
_381
}
_381
_381
function updateConnectedUI() {
_381
$('#username-span').text(tc.username);
_381
$statusRow.addClass('connected').removeClass('disconnected');
_381
tc.$messageList.addClass('connected').removeClass('disconnected');
_381
$connectPanel.addClass('connected').removeClass('disconnected');
_381
$inputText.addClass('with-shadow');
_381
$typingRow.addClass('connected').removeClass('disconnected');
_381
}
_381
_381
tc.loadChannelList = function(handler) {
_381
if (tc.messagingClient === undefined) {
_381
console.log('Client is not initialized');
_381
return;
_381
}
_381
_381
tc.messagingClient.getPublicChannelDescriptors().then(function(channels) {
_381
tc.channelArray = tc.sortChannelsByName(channels.items);
_381
$channelList.text('');
_381
tc.channelArray.forEach(addChannel);
_381
if (typeof handler === 'function') {
_381
handler();
_381
}
_381
});
_381
};
_381
_381
tc.joinGeneralChannel = function() {
_381
console.log('Attempting to join "general" chat channel...');
_381
if (!tc.generalChannel) {
_381
// If it doesn't exist, let's create it
_381
tc.messagingClient.createChannel({
_381
uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,
_381
friendlyName: GENERAL_CHANNEL_NAME
_381
}).then(function(channel) {
_381
console.log('Created general channel');
_381
tc.generalChannel = channel;
_381
tc.loadChannelList(tc.joinGeneralChannel);
_381
});
_381
}
_381
else {
_381
console.log('Found general channel:');
_381
setupChannel(tc.generalChannel);
_381
}
_381
};
_381
_381
function initChannel(channel) {
_381
console.log('Initialized channel ' + channel.friendlyName);
_381
return tc.messagingClient.getChannelBySid(channel.sid);
_381
}
_381
_381
function joinChannel(_channel) {
_381
return _channel.join()
_381
.then(function(joinedChannel) {
_381
console.log('Joined channel ' + joinedChannel.friendlyName);
_381
updateChannelUI(_channel);
_381
_381
return joinedChannel;
_381
})
_381
.catch(function(err) {
_381
if (_channel.status == 'joined') {
_381
updateChannelUI(_channel);
_381
return _channel;
_381
}
_381
console.error(
_381
"Couldn't join channel " + _channel.friendlyName + ' because -> ' + err
_381
);
_381
});
_381
}
_381
_381
function initChannelEvents() {
_381
console.log(tc.currentChannel.friendlyName + ' ready.');
_381
tc.currentChannel.on('messageAdded', tc.addMessageToList);
_381
tc.currentChannel.on('typingStarted', showTypingStarted);
_381
tc.currentChannel.on('typingEnded', hideTypingStarted);
_381
tc.currentChannel.on('memberJoined', notifyMemberJoined);
_381
tc.currentChannel.on('memberLeft', notifyMemberLeft);
_381
$inputText.prop('disabled', false).focus();
_381
}
_381
_381
function setupChannel(channel) {
_381
return leaveCurrentChannel()
_381
.then(function() {
_381
return initChannel(channel);
_381
})
_381
.then(function(_channel) {
_381
return joinChannel(_channel);
_381
})
_381
.then(initChannelEvents);
_381
}
_381
_381
tc.loadMessages = function() {
_381
tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT).then(function (messages) {
_381
messages.items.forEach(tc.addMessageToList);
_381
});
_381
};
_381
_381
function leaveCurrentChannel() {
_381
if (tc.currentChannel) {
_381
return tc.currentChannel.leave().then(function(leftChannel) {
_381
console.log('left ' + leftChannel.friendlyName);
_381
leftChannel.removeListener('messageAdded', tc.addMessageToList);
_381
leftChannel.removeListener('typingStarted', showTypingStarted);
_381
leftChannel.removeListener('typingEnded', hideTypingStarted);
_381
leftChannel.removeListener('memberJoined', notifyMemberJoined);
_381
leftChannel.removeListener('memberLeft', notifyMemberLeft);
_381
});
_381
} else {
_381
return Promise.resolve();
_381
}
_381
}
_381
_381
tc.addMessageToList = function(message) {
_381
var rowDiv = $('<div>').addClass('row no-margin');
_381
rowDiv.loadTemplate($('#message-template'), {
_381
username: message.author,
_381
date: dateFormatter.getTodayDate(message.dateCreated),
_381
body: message.body
_381
});
_381
if (message.author === tc.username) {
_381
rowDiv.addClass('own-message');
_381
}
_381
_381
tc.$messageList.append(rowDiv);
_381
scrollToMessageListBottom();
_381
};
_381
_381
function notifyMemberJoined(member) {
_381
notify(member.identity + ' joined the channel')
_381
}
_381
_381
function notifyMemberLeft(member) {
_381
notify(member.identity + ' left the channel');
_381
}
_381
_381
function notify(message) {
_381
var row = $('<div>').addClass('col-md-12');
_381
row.loadTemplate('#member-notification-template', {
_381
status: message
_381
});
_381
tc.$messageList.append(row);
_381
scrollToMessageListBottom();
_381
}
_381
_381
function showTypingStarted(member) {
_381
$typingPlaceholder.text(member.identity + ' is typing...');
_381
}
_381
_381
function hideTypingStarted(member) {
_381
$typingPlaceholder.text('');
_381
}
_381
_381
function scrollToMessageListBottom() {
_381
tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);
_381
}
_381
_381
function updateChannelUI(selectedChannel) {
_381
var channelElements = $('.channel-element').toArray();
_381
var channelElement = channelElements.filter(function(element) {
_381
return $(element).data().sid === selectedChannel.sid;
_381
});
_381
channelElement = $(channelElement);
_381
if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {
_381
tc.currentChannelContainer = channelElement;
_381
}
_381
tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');
_381
channelElement.removeClass('unselected-channel').addClass('selected-channel');
_381
tc.currentChannelContainer = channelElement;
_381
tc.currentChannel = selectedChannel;
_381
tc.loadMessages();
_381
}
_381
_381
function showAddChannelInput() {
_381
if (tc.messagingClient) {
_381
$newChannelInputRow.addClass('showing').removeClass('not-showing');
_381
$channelList.addClass('showing').removeClass('not-showing');
_381
$newChannelInput.focus();
_381
}
_381
}
_381
_381
function hideAddChannelInput() {
_381
$newChannelInputRow.addClass('not-showing').removeClass('showing');
_381
$channelList.addClass('not-showing').removeClass('showing');
_381
$newChannelInput.val('');
_381
}
_381
_381
function addChannel(channel) {
_381
if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {
_381
tc.generalChannel = channel;
_381
}
_381
var rowDiv = $('<div>').addClass('row channel-row');
_381
rowDiv.loadTemplate('#channel-template', {
_381
channelName: channel.friendlyName
_381
});
_381
_381
var channelP = rowDiv.children().children().first();
_381
_381
rowDiv.on('click', selectChannel);
_381
channelP.data('sid', channel.sid);
_381
if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {
_381
tc.currentChannelContainer = channelP;
_381
channelP.addClass('selected-channel');
_381
}
_381
else {
_381
channelP.addClass('unselected-channel')
_381
}
_381
_381
$channelList.append(rowDiv);
_381
}
_381
_381
function deleteCurrentChannel() {
_381
if (!tc.currentChannel) {
_381
return;
_381
}
_381
_381
if (tc.currentChannel.sid === tc.generalChannel.sid) {
_381
alert('You cannot delete the general channel');
_381
return;
_381
}
_381
_381
tc.currentChannel
_381
.delete()
_381
.then(function(channel) {
_381
console.log('channel: '+ channel.friendlyName + ' deleted');
_381
setupChannel(tc.generalChannel);
_381
});
_381
}
_381
_381
function selectChannel(event) {
_381
var target = $(event.target);
_381
var channelSid = target.data().sid;
_381
var selectedChannel = tc.channelArray.filter(function(channel) {
_381
return channel.sid === channelSid;
_381
})[0];
_381
if (selectedChannel === tc.currentChannel) {
_381
return;
_381
}
_381
setupChannel(selectedChannel);
_381
};
_381
_381
function disconnectClient() {
_381
leaveCurrentChannel();
_381
$channelList.text('');
_381
tc.$messageList.text('');
_381
channels = undefined;
_381
$statusRow.addClass('disconnected').removeClass('connected');
_381
tc.$messageList.addClass('disconnected').removeClass('connected');
_381
$connectPanel.addClass('disconnected').removeClass('connected');
_381
$inputText.removeClass('with-shadow');
_381
$typingRow.addClass('disconnected').removeClass('connected');
_381
}
_381
_381
tc.sortChannelsByName = function(channels) {
_381
return channels.sort(function(a, b) {
_381
if (a.friendlyName === GENERAL_CHANNEL_NAME) {
_381
return -1;
_381
}
_381
if (b.friendlyName === GENERAL_CHANNEL_NAME) {
_381
return 1;
_381
}
_381
return a.friendlyName.localeCompare(b.friendlyName);
_381
});
_381
};
_381
_381
return tc;
_381
})();

That's it! We've just implemented a simple chat application for Node.js using Express.


If you are a Node developer working with Twilio, you might want to check out these other tutorials:

Click-To-Call

Put a button on your web page that connects visitors to live support or sales people via telephone.

Automated Survey

Instantly collect structured data from your users with a survey conducted over a voice call or SMS text messages.

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: