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

Chat with PHP and Laravel


(warning)

Warning

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

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

(error)

Danger

Programmable Chat has been deprecated and is no longer supported. Instead, we'll be focusing on the next generation of chat: Twilio Conversations. Find out more about the EOL process here(link takes you to an external page).

If you're starting a new project, please visit the Conversations Docs to begin. If you've already built on Programmable Chat, please visit our Migration Guide to learn about how to switch.

Ready to implement a chat application using Twilio Programmable Chat Client?

This application allows users to exchange messages through different channels, using the Twilio Programmable Chat API. On this example, we'll show how to use this API features to manage channels and to show it's 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.

With Laravel we must create a provider that will inject the AccessToken object in the controller, the same goes for ChatMessagingGrant inside TwilioChatGrantProvider.php. We'll see how to use these objects in the next step.

Generate an Access Token

generate-an-access-token page anchor

app/Providers/TwilioAccessTokenProvider.php


_32
<?php
_32
namespace App\Providers;
_32
use Illuminate\Support\ServiceProvider;
_32
use Twilio\Jwt\AccessToken;
_32
_32
class TwilioAccessTokenProvider extends ServiceProvider
_32
{
_32
/**
_32
* Register the application services.
_32
*
_32
* @return void
_32
*/
_32
public function register()
_32
{
_32
$this->app->bind(
_32
AccessToken::class, function ($app) {
_32
$TWILIO_ACCOUNT_SID = config('services.twilio')['accountSid'];
_32
$TWILIO_API_KEY = config('services.twilio')['apiKey'];
_32
$TWILIO_API_SECRET = config('services.twilio')['apiSecret'];
_32
_32
$token = new AccessToken(
_32
$TWILIO_ACCOUNT_SID,
_32
$TWILIO_API_KEY,
_32
$TWILIO_API_SECRET,
_32
3600
_32
);
_32
_32
return $token;
_32
}
_32
);
_32
}
_32
}

We can generate a token, now we need a way for the chat app to get it.


Token Generation Controller

token-generation-controller page anchor

On our controller, we expose the endpoint responsible for providing a valid token using this parameter:

  • identity : identifies the user itself

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

app/Http/Controllers/TokenController.php


_31
<?php
_31
namespace App\Http\Controllers;
_31
use Illuminate\Http\Request;
_31
use App\Http\Requests;
_31
use App\Http\Controllers\Controller;
_31
use Twilio\Jwt\AccessToken;
_31
use Twilio\Jwt\Grants\ChatGrant;
_31
_31
class TokenController extends Controller
_31
{
_31
public function generate(Request $request, AccessToken $accessToken, ChatGrant $chatGrant)
_31
{
_31
$appName = "TwilioChat";
_31
$identity = $request->input("identity");
_31
_31
$TWILIO_CHAT_SERVICE_SID = config('services.twilio')['chatServiceSid'];
_31
_31
$accessToken->setIdentity($identity);
_31
_31
$chatGrant->setServiceSid($TWILIO_CHAT_SERVICE_SID);
_31
_31
$accessToken->addGrant($chatGrant);
_31
_31
$response = array(
_31
'identity' => $identity,
_31
'token' => $accessToken->toJWT()
_31
);
_31
_31
return response()->json($response);
_31
}
_31
}

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


Initializing the Programmable Chat Client

initializing-the-programmable-chat-client page anchor

Our client fetches a new Token by making a POST request to our endpoint.

With the token we can create a new Twilio.AccessManager, and initialize our Twilio.Chat.Client.

Initialize the Chat Client

initialize-the-chat-client page anchor

public/js/twiliochat.js


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

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


Getting the Channel List

getting-the-channel-list page anchor

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

public/js/twiliochat.js


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

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 since we don't want to create a new general channel every time we start the application.

public/js/twiliochat.js


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

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


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

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


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

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

public/js/twiliochat.js


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

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


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

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


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

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


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

That's it! We've just implemented a simple chat application for PHP using Laravel.


If you are a PHP 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 salespeople via telephone.

Automated Survey(link takes you to an external page)

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: