As the Programmable Chat API is set to sunset in 2022, we will no longer maintain these chat tutorials.
Please see our Conversations API QuickStart to start building robust virtual spaces for conversation.
Programmable Chat has been deprecated and is no longer supported. Instead, we'll be focusing on the next generation of chat: Twilio Conversations. Find out more about the EOL process here.
If you're starting a new project, please visit the Conversations Docs to begin. If you've already built on Programmable Chat, please visit our Migration Guide to learn about how to switch.
Ready to implement a chat application using Twilio Programmable Chat Client?
This application allows users to exchange messages through different channels, using the Twilio Programmable Chat API. On this example, we'll show how to use this API features to manage channels and to show it's usages.
For your convenience, we consolidated the source code for this tutorial in a single GitHub repository. Feel free to clone it and tweak it as required.
In order to create a Twilio Programmable Chat client, you will need an access token. This token holds information about your Twilio Account and 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.
app/Providers/TwilioAccessTokenProvider.php
_32<?php_32namespace App\Providers;_32use Illuminate\Support\ServiceProvider;_32use Twilio\Jwt\AccessToken;_32_32class 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.
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_31namespace App\Http\Controllers;_31use Illuminate\Http\Request;_31use App\Http\Requests;_31use App\Http\Controllers\Controller;_31use Twilio\Jwt\AccessToken;_31use Twilio\Jwt\Grants\ChatGrant;_31_31class 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.
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
.
public/js/twiliochat.js
_361var 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.
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
_361var 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.
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
_361var 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.
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
_361var 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
_361var 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
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
_361var 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
_361var 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
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
_361var 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:
Put a button on your web page that connects visitors to live support or salespeople via telephone.
Instantly collect structured data from your users with a survey conducted over a voice call or SMS text messages.
Thanks for checking out this tutorial! If you have any feedback to share with us, we'd love to hear it. Tweet @twilio to let us know what you think.