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 Chat?
This application allows users to exchange messages through different channels, using the Twilio Chat API. With this example, we'll show you how to use this API to manage channels and their usages.
For your convenience, we consolidated the source code for this tutorial in a single GitHub repository. Feel free to clone it and tweak as required.
In order to create a Twilio Chat client, you will need an access token. This token provides access for a client (such as a JavaScript front end web application) to talk to the Twilio Chat API.
We generate this token by creating a new Token
and providing it with a ChatGrant
. With the Token
at hand, we can use its method ToJwt()
to return its string representation.
TwilioChat.Web/Domain/TokenGenerator.cs
1using System.Collections.Generic;2using Twilio.Jwt.AccessToken;34namespace TwilioChat.Web.Domain5{6public interface ITokenGenerator7{8string Generate(string identity);9}1011public class TokenGenerator : ITokenGenerator12{13public string Generate(string identity)14{15var grants = new HashSet<IGrant>16{17new ChatGrant {ServiceSid = Configuration.ChatServiceSID}18};1920var token = new Token(21Configuration.AccountSID,22Configuration.ApiKey,23Configuration.ApiSecret,24identity,25grants: grants);2627return token.ToJwt();28}29}30}31
We can generate a token, now we need a way for the chat app to get it.
On our controller we expose an endpoint that provides a valid token. Using the parameter:
identity
: identifies the user itself.It uses tokenGenerator.Generate
method to get hold of a new token and return it in a JSON format to be used for our client.
TwilioChat.Web/Controllers/TokenController.cs
1using System.Web.Mvc;2using TwilioChat.Web.Domain;34namespace TwilioChat.Web.Controllers5{6public class TokenController : Controller7{8private readonly ITokenGenerator _tokenGenerator;910public TokenController() : this(new TokenGenerator()) { }1112public TokenController(ITokenGenerator tokenGenerator)13{14_tokenGenerator = tokenGenerator;15}1617// POST: Token18[HttpPost]19public ActionResult Index(string identity)20{21if (identity == null) return null;2223var token = _tokenGenerator.Generate(identity);24return Json(new {identity, token});25}26}27}
Now that we have a route that generates JWT tokens on demand, let's use this route to initialize our Twilio Chat Client.
On our client, we fetch a new Token using a POST
request to our endpoint.
And with the token we can instantiate a new Twilio.AccessManager
that is used to initialize our Twilio.Chat.Client
.
Twilio Chat Client Initialization in JS
1var twiliochat = (function () {2var tc = {};34var GENERAL_CHANNEL_UNIQUE_NAME = 'general';5var GENERAL_CHANNEL_NAME = 'General Channel';6var MESSAGES_HISTORY_LIMIT = 50;78var $channelList;9var $inputText;10var $usernameInput;11var $statusRow;12var $connectPanel;13var $newChannelInputRow;14var $newChannelInput;15var $typingRow;16var $typingPlaceholder;1718$(document).ready(function () {19tc.init();20});2122tc.init = function () {23tc.$messageList = $('#message-list');24$channelList = $('#channel-list');25$inputText = $('#input-text');26$usernameInput = $('#username-input');27$statusRow = $('#status-row');28$connectPanel = $('#connect-panel');29$newChannelInputRow = $('#new-channel-input-row');30$newChannelInput = $('#new-channel-input');31$typingRow = $('#typing-row');32$typingPlaceholder = $('#typing-placeholder');33$usernameInput.focus();34$usernameInput.on('keypress', handleUsernameInputKeypress);35$inputText.on('keypress', handleInputTextKeypress);36$newChannelInput.on('keypress', tc.handleNewChannelInputKeypress);37$('#connect-image').on('click', connectClientWithUsername);38$('#add-channel-image').on('click', showAddChannelInput);39$('#leave-span').on('click', disconnectClient);40$('#delete-channel-span').on('click', deleteCurrentChannel);41};4243function handleUsernameInputKeypress(event) {44if (event.keyCode === 13) {45connectClientWithUsername();46}47}4849function handleInputTextKeypress(event) {50if (event.keyCode === 13) {51tc.currentChannel.sendMessage($(this).val());52event.preventDefault();53$(this).val('');54}55else {56notifyTyping();57}58}5960var notifyTyping = $.throttle(function () {61tc.currentChannel.typing();62}, 1000);6364tc.handleNewChannelInputKeypress = function (event) {65if (event.keyCode === 13) {66tc.messagingClient67.createChannel({68friendlyName: $newChannelInput.val(),69})70.then(hideAddChannelInput);7172$(this).val('');73event.preventDefault();74}75};7677function connectClientWithUsername() {78var usernameText = $usernameInput.val();79$usernameInput.val('');80if (usernameText == '') {81alert('Username cannot be empty');82return;83}84tc.username = usernameText;85fetchAccessToken(tc.username, connectMessagingClient);86}8788function fetchAccessToken(username, handler) {89$.post('/token', { identity: username}, null, 'json')90.done(function (response) {91handler(response.token);92})93.fail(function (error) {94console.log('Failed to fetch the Access Token with error: ' + error);95});96}9798function connectMessagingClient(token) {99// Initialize the Chat messaging client100Twilio.Chat.Client.create(token).then(function (client) {101tc.messagingClient = client;102updateConnectedUI();103tc.loadChannelList(tc.joinGeneralChannel);104tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));105tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));106tc.messagingClient.on('tokenExpired', refreshToken);107});108}109110function refreshToken() {111fetchAccessToken(tc.username, setNewToken);112}113114function setNewToken(token) {115tc.messagingClient.updateToken(token);116}117118function updateConnectedUI() {119$('#username-span').text(tc.username);120$statusRow.addClass('connected').removeClass('disconnected');121tc.$messageList.addClass('connected').removeClass('disconnected');122$connectPanel.addClass('connected').removeClass('disconnected');123$inputText.addClass('with-shadow');124$typingRow.addClass('connected').removeClass('disconnected');125}126127tc.loadChannelList = function (handler) {128if (tc.messagingClient === undefined) {129console.log('Client is not initialized');130return;131}132133tc.messagingClient.getPublicChannelDescriptors().then(function (channels) {134tc.channelArray = tc.sortChannelsByName(channels.items);135$channelList.text('');136tc.channelArray.forEach(addChannel);137if (typeof handler === 'function') {138handler();139}140});141};142143tc.joinGeneralChannel = function () {144console.log('Attempting to join "general" chat channel...');145if (!tc.generalChannel) {146// If it doesn't exist, let's create it147tc.messagingClient.createChannel({148uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,149friendlyName: GENERAL_CHANNEL_NAME150}).then(function (channel) {151console.log('Created general channel');152tc.generalChannel = channel;153tc.loadChannelList(tc.joinGeneralChannel);154});155}156else {157console.log('Found general channel:');158setupChannel(tc.generalChannel);159}160};161162function initChannel(channel) {163console.log('Initialized channel ' + channel.friendlyName);164return tc.messagingClient.getChannelBySid(channel.sid);165}166167function joinChannel(_channel) {168return _channel.join()169.then(function (joinedChannel) {170console.log('Joined channel ' + joinedChannel.friendlyName);171updateChannelUI(_channel);172173return joinedChannel;174})175.catch(function (err) {176if (_channel.status == 'joined') {177updateChannelUI(_channel);178return _channel;179}180console.error(181"Couldn't join channel " + _channel.friendlyName + ' because -> ' + err182);183});184}185186function initChannelEvents() {187console.log(tc.currentChannel.friendlyName + ' ready.');188tc.currentChannel.on('messageAdded', tc.addMessageToList);189tc.currentChannel.on('typingStarted', showTypingStarted);190tc.currentChannel.on('typingEnded', hideTypingStarted);191tc.currentChannel.on('memberJoined', notifyMemberJoined);192tc.currentChannel.on('memberLeft', notifyMemberLeft);193$inputText.prop('disabled', false).focus();194}195196function setupChannel(channel) {197return leaveCurrentChannel()198.then(function () {199return initChannel(channel);200})201.then(function (_channel) {202return joinChannel(_channel);203})204.then(initChannelEvents);205}206207tc.loadMessages = function () {208tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT).then(function (messages) {209messages.items.forEach(tc.addMessageToList);210});211};212213function leaveCurrentChannel() {214if (tc.currentChannel) {215return tc.currentChannel.leave().then(function (leftChannel) {216console.log('left ' + leftChannel.friendlyName);217leftChannel.removeListener('messageAdded', tc.addMessageToList);218leftChannel.removeListener('typingStarted', showTypingStarted);219leftChannel.removeListener('typingEnded', hideTypingStarted);220leftChannel.removeListener('memberJoined', notifyMemberJoined);221leftChannel.removeListener('memberLeft', notifyMemberLeft);222});223} else {224return Promise.resolve();225}226}227228tc.addMessageToList = function (message) {229var rowDiv = $('<div>').addClass('row no-margin');230rowDiv.loadTemplate($('#message-template'), {231username: message.author,232date: dateFormatter.getTodayDate(message.dateCreated),233body: message.body234});235if (message.author === tc.username) {236rowDiv.addClass('own-message');237}238239tc.$messageList.append(rowDiv);240scrollToMessageListBottom();241};242243function notifyMemberJoined(member) {244notify(member.identity + ' joined the channel')245}246247function notifyMemberLeft(member) {248notify(member.identity + ' left the channel');249}250251function notify(message) {252var row = $('<div>').addClass('col-md-12');253row.loadTemplate('#member-notification-template', {254status: message255});256tc.$messageList.append(row);257scrollToMessageListBottom();258}259260function showTypingStarted(member) {261$typingPlaceholder.text(member.identity + ' is typing...');262}263264function hideTypingStarted(member) {265$typingPlaceholder.text('');266}267268function scrollToMessageListBottom() {269tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);270}271272function updateChannelUI(selectedChannel) {273var channelElements = $('.channel-element').toArray();274var channelElement = channelElements.filter(function (element) {275return $(element).data().sid === selectedChannel.sid;276});277channelElement = $(channelElement);278if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {279tc.currentChannelContainer = channelElement;280}281tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');282channelElement.removeClass('unselected-channel').addClass('selected-channel');283tc.currentChannelContainer = channelElement;284tc.currentChannel = selectedChannel;285tc.loadMessages();286}287288function showAddChannelInput() {289if (tc.messagingClient) {290$newChannelInputRow.addClass('showing').removeClass('not-showing');291$channelList.addClass('showing').removeClass('not-showing');292$newChannelInput.focus();293}294}295296function hideAddChannelInput() {297$newChannelInputRow.addClass('not-showing').removeClass('showing');298$channelList.addClass('not-showing').removeClass('showing');299$newChannelInput.val('');300}301302function addChannel(channel) {303if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {304tc.generalChannel = channel;305}306var rowDiv = $('<div>').addClass('row channel-row');307rowDiv.loadTemplate('#channel-template', {308channelName: channel.friendlyName309});310311var channelP = rowDiv.children().children().first();312313rowDiv.on('click', selectChannel);314channelP.data('sid', channel.sid);315if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {316tc.currentChannelContainer = channelP;317channelP.addClass('selected-channel');318}319else {320channelP.addClass('unselected-channel')321}322323$channelList.append(rowDiv);324}325326function deleteCurrentChannel() {327if (!tc.currentChannel) {328return;329}330331if (tc.currentChannel.sid === tc.generalChannel.sid) {332alert('You cannot delete the general channel');333return;334}335336tc.currentChannel337.delete()338.then(function (channel) {339console.log('channel: ' + channel.friendlyName + ' deleted');340setupChannel(tc.generalChannel);341});342}343344function selectChannel(event) {345var target = $(event.target);346var channelSid = target.data().sid;347var selectedChannel = tc.channelArray.filter(function (channel) {348return channel.sid === channelSid;349})[0];350if (selectedChannel === tc.currentChannel) {351return;352}353setupChannel(selectedChannel);354};355356function disconnectClient() {357leaveCurrentChannel();358$channelList.text('');359tc.$messageList.text('');360channels = undefined;361$statusRow.addClass('disconnected').removeClass('connected');362tc.$messageList.addClass('disconnected').removeClass('connected');363$connectPanel.addClass('disconnected').removeClass('connected');364$inputText.removeClass('with-shadow');365$typingRow.addClass('disconnected').removeClass('connected');366}367368tc.sortChannelsByName = function (channels) {369return channels.sort(function (a, b) {370if (a.friendlyName === GENERAL_CHANNEL_NAME) {371return -1;372}373if (b.friendlyName === GENERAL_CHANNEL_NAME) {374return 1;375}376return a.friendlyName.localeCompare(b.friendlyName);377});378};379380return tc;381})();382
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 now call it's method getChannels
to retrieve all visible channels. The method returns a promise as a result that we use to show the list of channels retrieved on the UI.
TwilioChat.Web/Scripts/twiliochat.js
1var twiliochat = (function () {2var tc = {};34var GENERAL_CHANNEL_UNIQUE_NAME = 'general';5var GENERAL_CHANNEL_NAME = 'General Channel';6var MESSAGES_HISTORY_LIMIT = 50;78var $channelList;9var $inputText;10var $usernameInput;11var $statusRow;12var $connectPanel;13var $newChannelInputRow;14var $newChannelInput;15var $typingRow;16var $typingPlaceholder;1718$(document).ready(function () {19tc.$messageList = $('#message-list');20$channelList = $('#channel-list');21$inputText = $('#input-text');22$usernameInput = $('#username-input');23$statusRow = $('#status-row');24$connectPanel = $('#connect-panel');25$newChannelInputRow = $('#new-channel-input-row');26$newChannelInput = $('#new-channel-input');27$typingRow = $('#typing-row');28$typingPlaceholder = $('#typing-placeholder');29$usernameInput.focus();30$usernameInput.on('keypress', handleUsernameInputKeypress);31$inputText.on('keypress', handleInputTextKeypress);32$newChannelInput.on('keypress', tc.handleNewChannelInputKeypress);33$('#connect-image').on('click', connectClientWithUsername);34$('#add-channel-image').on('click', showAddChannelInput);35$('#leave-span').on('click', disconnectClient);36$('#delete-channel-span').on('click', deleteCurrentChannel);37});3839function handleUsernameInputKeypress(event) {40if (event.keyCode === 13) {41connectClientWithUsername();42}43}4445function handleInputTextKeypress(event) {46if (event.keyCode === 13) {47tc.currentChannel.sendMessage($(this).val());48event.preventDefault();49$(this).val('');50}51else {52notifyTyping();53}54}5556var notifyTyping = $.throttle(function () {57tc.currentChannel.typing();58}, 1000);5960tc.handleNewChannelInputKeypress = function (event) {61if (event.keyCode === 13) {62tc.messagingClient.createChannel({63friendlyName: $newChannelInput.val()64}).then(hideAddChannelInput);65$(this).val('');66event.preventDefault();67}68};6970function connectClientWithUsername() {71var usernameText = $usernameInput.val();72$usernameInput.val('');73if (usernameText == '') {74alert('Username cannot be empty');75return;76}77tc.username = usernameText;78fetchAccessToken(tc.username, connectMessagingClient);79}8081function fetchAccessToken(username, handler) {82$.post('/token', {83identity: username,84device: 'browser'85}, function (data) {86handler(data);87}, 'json');88}8990function connectMessagingClient(tokenResponse) {91// Initialize the IP messaging client92tc.accessManager = new Twilio.AccessManager(tokenResponse.token);93tc.messagingClient = new Twilio.IPMessaging.Client(tc.accessManager);94updateConnectedUI();95tc.loadChannelList(tc.joinGeneralChannel);96tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));97tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));98tc.messagingClient.on('tokenExpired', refreshToken);99}100101function refreshToken() {102fetchAccessToken(tc.username, setNewToken);103}104105function setNewToken(tokenResponse) {106tc.accessManager.updateToken(tokenResponse.token);107}108109function updateConnectedUI() {110$('#username-span').text(tc.username);111$statusRow.addClass('connected').removeClass('disconnected');112tc.$messageList.addClass('connected').removeClass('disconnected');113$connectPanel.addClass('connected').removeClass('disconnected');114$inputText.addClass('with-shadow');115$typingRow.addClass('connected').removeClass('disconnected');116}117118tc.loadChannelList = function (handler) {119if (tc.messagingClient === undefined) {120console.log('Client is not initialized');121return;122}123124tc.messagingClient.getChannels().then(function (channels) {125tc.channelArray = tc.sortChannelsByName(channels);126$channelList.text('');127tc.channelArray.forEach(addChannel);128if (typeof handler === 'function') {129handler();130}131});132};133134tc.joinGeneralChannel = function () {135console.log('Attempting to join "general" chat channel...');136if (!tc.generalChannel) {137// If it doesn't exist, let's create it138tc.messagingClient.createChannel({139uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,140friendlyName: GENERAL_CHANNEL_NAME141}).then(function (channel) {142console.log('Created general channel');143tc.generalChannel = channel;144tc.loadChannelList(tc.joinGeneralChannel);145});146}147else {148console.log('Found general channel:');149setupChannel(tc.generalChannel);150}151};152153function setupChannel(channel) {154// Join the channel155channel.join().then(function (joinedChannel) {156console.log('Joined channel ' + joinedChannel.friendlyName);157leaveCurrentChannel();158updateChannelUI(channel);159tc.currentChannel = channel;160tc.loadMessages();161channel.on('messageAdded', tc.addMessageToList);162channel.on('typingStarted', showTypingStarted);163channel.on('typingEnded', hideTypingStarted);164channel.on('memberJoined', notifyMemberJoined);165channel.on('memberLeft', notifyMemberLeft);166$inputText.prop('disabled', false).focus();167tc.$messageList.text('');168});169}170171tc.loadMessages = function () {172tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT).then(function (messages) {173messages.forEach(tc.addMessageToList);174});175};176177function leaveCurrentChannel() {178if (tc.currentChannel) {179tc.currentChannel.leave().then(function (leftChannel) {180console.log('left ' + leftChannel.friendlyName);181leftChannel.removeListener('messageAdded', tc.addMessageToList);182leftChannel.removeListener('typingStarted', showTypingStarted);183leftChannel.removeListener('typingEnded', hideTypingStarted);184leftChannel.removeListener('memberJoined', notifyMemberJoined);185leftChannel.removeListener('memberLeft', notifyMemberLeft);186});187}188}189190tc.addMessageToList = function (message) {191var rowDiv = $('<div>').addClass('row no-margin');192rowDiv.loadTemplate($('#message-template'), {193username: message.author,194date: dateFormatter.getTodayDate(message.timestamp),195body: message.body196});197if (message.author === tc.username) {198rowDiv.addClass('own-message');199}200201tc.$messageList.append(rowDiv);202scrollToMessageListBottom();203};204205function notifyMemberJoined(member) {206notify(member.identity + ' joined the channel')207}208209function notifyMemberLeft(member) {210notify(member.identity + ' left the channel');211}212213function notify(message) {214var row = $('<div>').addClass('col-md-12');215row.loadTemplate('#member-notification-template', {216status: message217});218tc.$messageList.append(row);219scrollToMessageListBottom();220}221222function showTypingStarted(member) {223$typingPlaceholder.text(member.identity + ' is typing...');224}225226function hideTypingStarted(member) {227$typingPlaceholder.text('');228}229230function scrollToMessageListBottom() {231tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);232}233234function updateChannelUI(selectedChannel) {235var channelElements = $('.channel-element').toArray();236var channelElement = channelElements.filter(function (element) {237return $(element).data().sid === selectedChannel.sid;238});239channelElement = $(channelElement);240if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {241tc.currentChannelContainer = channelElement;242}243tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');244channelElement.removeClass('unselected-channel').addClass('selected-channel');245tc.currentChannelContainer = channelElement;246}247248function showAddChannelInput() {249if (tc.messagingClient) {250$newChannelInputRow.addClass('showing').removeClass('not-showing');251$channelList.addClass('showing').removeClass('not-showing');252$newChannelInput.focus();253}254}255256function hideAddChannelInput() {257$newChannelInputRow.addClass('not-showing').removeClass('showing');258$channelList.addClass('not-showing').removeClass('showing');259$newChannelInput.val('');260}261262function addChannel(channel) {263if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {264tc.generalChannel = channel;265}266var rowDiv = $('<div>').addClass('row channel-row');267rowDiv.loadTemplate('#channel-template', {268channelName: channel.friendlyName269});270271var channelP = rowDiv.children().children().first();272273rowDiv.on('click', selectChannel);274channelP.data('sid', channel.sid);275if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {276tc.currentChannelContainer = channelP;277channelP.addClass('selected-channel');278}279else {280channelP.addClass('unselected-channel')281}282283$channelList.append(rowDiv);284}285286function deleteCurrentChannel() {287if (!tc.currentChannel) {288return;289}290if (tc.currentChannel.sid === tc.generalChannel.sid) {291alert('You cannot delete the general channel');292return;293}294tc.currentChannel.delete().then(function (channel) {295console.log('channel: ' + channel.friendlyName + ' deleted');296setupChannel(tc.generalChannel);297});298}299300function selectChannel(event) {301var target = $(event.target);302var channelSid = target.data().sid;303var selectedChannel = tc.channelArray.filter(function (channel) {304return channel.sid === channelSid;305})[0];306if (selectedChannel === tc.currentChannel) {307return;308}309setupChannel(selectedChannel);310};311312function disconnectClient() {313leaveCurrentChannel();314$channelList.text('');315tc.$messageList.text('');316channels = undefined;317$statusRow.addClass('disconnected').removeClass('connected');318tc.$messageList.addClass('disconnected').removeClass('connected');319$connectPanel.addClass('disconnected').removeClass('connected');320$inputText.removeClass('with-shadow');321$typingRow.addClass('disconnected').removeClass('connected');322}323324tc.sortChannelsByName = function (channels) {325return channels.sort(function (a, b) {326if (a.friendlyName === GENERAL_CHANNEL_NAME) {327return -1;328}329if (b.friendlyName === GENERAL_CHANNEL_NAME) {330return 1;331}332return a.friendlyName.localeCompare(b.friendlyName);333});334};335336return tc;337})();
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 Chat client allows you to create private channels and handle invitations.
Notice we set a unique name for the general channel as we don't want to create a new general channel every time we start the application.
TwilioChat.Web/Scripts/twiliochat.js
1var twiliochat = (function () {2var tc = {};34var GENERAL_CHANNEL_UNIQUE_NAME = 'general';5var GENERAL_CHANNEL_NAME = 'General Channel';6var MESSAGES_HISTORY_LIMIT = 50;78var $channelList;9var $inputText;10var $usernameInput;11var $statusRow;12var $connectPanel;13var $newChannelInputRow;14var $newChannelInput;15var $typingRow;16var $typingPlaceholder;1718$(document).ready(function () {19tc.$messageList = $('#message-list');20$channelList = $('#channel-list');21$inputText = $('#input-text');22$usernameInput = $('#username-input');23$statusRow = $('#status-row');24$connectPanel = $('#connect-panel');25$newChannelInputRow = $('#new-channel-input-row');26$newChannelInput = $('#new-channel-input');27$typingRow = $('#typing-row');28$typingPlaceholder = $('#typing-placeholder');29$usernameInput.focus();30$usernameInput.on('keypress', handleUsernameInputKeypress);31$inputText.on('keypress', handleInputTextKeypress);32$newChannelInput.on('keypress', tc.handleNewChannelInputKeypress);33$('#connect-image').on('click', connectClientWithUsername);34$('#add-channel-image').on('click', showAddChannelInput);35$('#leave-span').on('click', disconnectClient);36$('#delete-channel-span').on('click', deleteCurrentChannel);37});3839function handleUsernameInputKeypress(event) {40if (event.keyCode === 13) {41connectClientWithUsername();42}43}4445function handleInputTextKeypress(event) {46if (event.keyCode === 13) {47tc.currentChannel.sendMessage($(this).val());48event.preventDefault();49$(this).val('');50}51else {52notifyTyping();53}54}5556var notifyTyping = $.throttle(function () {57tc.currentChannel.typing();58}, 1000);5960tc.handleNewChannelInputKeypress = function (event) {61if (event.keyCode === 13) {62tc.messagingClient.createChannel({63friendlyName: $newChannelInput.val()64}).then(hideAddChannelInput);65$(this).val('');66event.preventDefault();67}68};6970function connectClientWithUsername() {71var usernameText = $usernameInput.val();72$usernameInput.val('');73if (usernameText == '') {74alert('Username cannot be empty');75return;76}77tc.username = usernameText;78fetchAccessToken(tc.username, connectMessagingClient);79}8081function fetchAccessToken(username, handler) {82$.post('/token', {83identity: username,84device: 'browser'85}, function (data) {86handler(data);87}, 'json');88}8990function connectMessagingClient(tokenResponse) {91// Initialize the IP messaging client92tc.accessManager = new Twilio.AccessManager(tokenResponse.token);93tc.messagingClient = new Twilio.IPMessaging.Client(tc.accessManager);94updateConnectedUI();95tc.loadChannelList(tc.joinGeneralChannel);96tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));97tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));98tc.messagingClient.on('tokenExpired', refreshToken);99}100101function refreshToken() {102fetchAccessToken(tc.username, setNewToken);103}104105function setNewToken(tokenResponse) {106tc.accessManager.updateToken(tokenResponse.token);107}108109function updateConnectedUI() {110$('#username-span').text(tc.username);111$statusRow.addClass('connected').removeClass('disconnected');112tc.$messageList.addClass('connected').removeClass('disconnected');113$connectPanel.addClass('connected').removeClass('disconnected');114$inputText.addClass('with-shadow');115$typingRow.addClass('connected').removeClass('disconnected');116}117118tc.loadChannelList = function (handler) {119if (tc.messagingClient === undefined) {120console.log('Client is not initialized');121return;122}123124tc.messagingClient.getChannels().then(function (channels) {125tc.channelArray = tc.sortChannelsByName(channels);126$channelList.text('');127tc.channelArray.forEach(addChannel);128if (typeof handler === 'function') {129handler();130}131});132};133134tc.joinGeneralChannel = function () {135console.log('Attempting to join "general" chat channel...');136if (!tc.generalChannel) {137// If it doesn't exist, let's create it138tc.messagingClient.createChannel({139uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,140friendlyName: GENERAL_CHANNEL_NAME141}).then(function (channel) {142console.log('Created general channel');143tc.generalChannel = channel;144tc.loadChannelList(tc.joinGeneralChannel);145});146}147else {148console.log('Found general channel:');149setupChannel(tc.generalChannel);150}151};152153function setupChannel(channel) {154// Join the channel155channel.join().then(function (joinedChannel) {156console.log('Joined channel ' + joinedChannel.friendlyName);157leaveCurrentChannel();158updateChannelUI(channel);159tc.currentChannel = channel;160tc.loadMessages();161channel.on('messageAdded', tc.addMessageToList);162channel.on('typingStarted', showTypingStarted);163channel.on('typingEnded', hideTypingStarted);164channel.on('memberJoined', notifyMemberJoined);165channel.on('memberLeft', notifyMemberLeft);166$inputText.prop('disabled', false).focus();167tc.$messageList.text('');168});169}170171tc.loadMessages = function () {172tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT).then(function (messages) {173messages.forEach(tc.addMessageToList);174});175};176177function leaveCurrentChannel() {178if (tc.currentChannel) {179tc.currentChannel.leave().then(function (leftChannel) {180console.log('left ' + leftChannel.friendlyName);181leftChannel.removeListener('messageAdded', tc.addMessageToList);182leftChannel.removeListener('typingStarted', showTypingStarted);183leftChannel.removeListener('typingEnded', hideTypingStarted);184leftChannel.removeListener('memberJoined', notifyMemberJoined);185leftChannel.removeListener('memberLeft', notifyMemberLeft);186});187}188}189190tc.addMessageToList = function (message) {191var rowDiv = $('<div>').addClass('row no-margin');192rowDiv.loadTemplate($('#message-template'), {193username: message.author,194date: dateFormatter.getTodayDate(message.timestamp),195body: message.body196});197if (message.author === tc.username) {198rowDiv.addClass('own-message');199}200201tc.$messageList.append(rowDiv);202scrollToMessageListBottom();203};204205function notifyMemberJoined(member) {206notify(member.identity + ' joined the channel')207}208209function notifyMemberLeft(member) {210notify(member.identity + ' left the channel');211}212213function notify(message) {214var row = $('<div>').addClass('col-md-12');215row.loadTemplate('#member-notification-template', {216status: message217});218tc.$messageList.append(row);219scrollToMessageListBottom();220}221222function showTypingStarted(member) {223$typingPlaceholder.text(member.identity + ' is typing...');224}225226function hideTypingStarted(member) {227$typingPlaceholder.text('');228}229230function scrollToMessageListBottom() {231tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);232}233234function updateChannelUI(selectedChannel) {235var channelElements = $('.channel-element').toArray();236var channelElement = channelElements.filter(function (element) {237return $(element).data().sid === selectedChannel.sid;238});239channelElement = $(channelElement);240if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {241tc.currentChannelContainer = channelElement;242}243tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');244channelElement.removeClass('unselected-channel').addClass('selected-channel');245tc.currentChannelContainer = channelElement;246}247248function showAddChannelInput() {249if (tc.messagingClient) {250$newChannelInputRow.addClass('showing').removeClass('not-showing');251$channelList.addClass('showing').removeClass('not-showing');252$newChannelInput.focus();253}254}255256function hideAddChannelInput() {257$newChannelInputRow.addClass('not-showing').removeClass('showing');258$channelList.addClass('not-showing').removeClass('showing');259$newChannelInput.val('');260}261262function addChannel(channel) {263if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {264tc.generalChannel = channel;265}266var rowDiv = $('<div>').addClass('row channel-row');267rowDiv.loadTemplate('#channel-template', {268channelName: channel.friendlyName269});270271var channelP = rowDiv.children().children().first();272273rowDiv.on('click', selectChannel);274channelP.data('sid', channel.sid);275if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {276tc.currentChannelContainer = channelP;277channelP.addClass('selected-channel');278}279else {280channelP.addClass('unselected-channel')281}282283$channelList.append(rowDiv);284}285286function deleteCurrentChannel() {287if (!tc.currentChannel) {288return;289}290if (tc.currentChannel.sid === tc.generalChannel.sid) {291alert('You cannot delete the general channel');292return;293}294tc.currentChannel.delete().then(function (channel) {295console.log('channel: ' + channel.friendlyName + ' deleted');296setupChannel(tc.generalChannel);297});298}299300function selectChannel(event) {301var target = $(event.target);302var channelSid = target.data().sid;303var selectedChannel = tc.channelArray.filter(function (channel) {304return channel.sid === channelSid;305})[0];306if (selectedChannel === tc.currentChannel) {307return;308}309setupChannel(selectedChannel);310};311312function disconnectClient() {313leaveCurrentChannel();314$channelList.text('');315tc.$messageList.text('');316channels = undefined;317$statusRow.addClass('disconnected').removeClass('connected');318tc.$messageList.addClass('disconnected').removeClass('connected');319$connectPanel.addClass('disconnected').removeClass('connected');320$inputText.removeClass('with-shadow');321$typingRow.addClass('disconnected').removeClass('connected');322}323324tc.sortChannelsByName = function (channels) {325return channels.sort(function (a, b) {326if (a.friendlyName === GENERAL_CHANNEL_NAME) {327return -1;328}329if (b.friendlyName === GENERAL_CHANNEL_NAME) {330return 1;331}332return a.friendlyName.localeCompare(b.friendlyName);333});334};335336return tc;337})();
Now let's listen for some channel events.
With access to the channel objects we can use them to listen to a series of 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.Here, we just register a different function to handle each particular event.
TwilioChat.Web/Scripts/twiliochat.js
1var twiliochat = (function () {2var tc = {};34var GENERAL_CHANNEL_UNIQUE_NAME = 'general';5var GENERAL_CHANNEL_NAME = 'General Channel';6var MESSAGES_HISTORY_LIMIT = 50;78var $channelList;9var $inputText;10var $usernameInput;11var $statusRow;12var $connectPanel;13var $newChannelInputRow;14var $newChannelInput;15var $typingRow;16var $typingPlaceholder;1718$(document).ready(function () {19tc.$messageList = $('#message-list');20$channelList = $('#channel-list');21$inputText = $('#input-text');22$usernameInput = $('#username-input');23$statusRow = $('#status-row');24$connectPanel = $('#connect-panel');25$newChannelInputRow = $('#new-channel-input-row');26$newChannelInput = $('#new-channel-input');27$typingRow = $('#typing-row');28$typingPlaceholder = $('#typing-placeholder');29$usernameInput.focus();30$usernameInput.on('keypress', handleUsernameInputKeypress);31$inputText.on('keypress', handleInputTextKeypress);32$newChannelInput.on('keypress', tc.handleNewChannelInputKeypress);33$('#connect-image').on('click', connectClientWithUsername);34$('#add-channel-image').on('click', showAddChannelInput);35$('#leave-span').on('click', disconnectClient);36$('#delete-channel-span').on('click', deleteCurrentChannel);37});3839function handleUsernameInputKeypress(event) {40if (event.keyCode === 13) {41connectClientWithUsername();42}43}4445function handleInputTextKeypress(event) {46if (event.keyCode === 13) {47tc.currentChannel.sendMessage($(this).val());48event.preventDefault();49$(this).val('');50}51else {52notifyTyping();53}54}5556var notifyTyping = $.throttle(function () {57tc.currentChannel.typing();58}, 1000);5960tc.handleNewChannelInputKeypress = function (event) {61if (event.keyCode === 13) {62tc.messagingClient.createChannel({63friendlyName: $newChannelInput.val()64}).then(hideAddChannelInput);65$(this).val('');66event.preventDefault();67}68};6970function connectClientWithUsername() {71var usernameText = $usernameInput.val();72$usernameInput.val('');73if (usernameText == '') {74alert('Username cannot be empty');75return;76}77tc.username = usernameText;78fetchAccessToken(tc.username, connectMessagingClient);79}8081function fetchAccessToken(username, handler) {82$.post('/token', {83identity: username,84device: 'browser'85}, function (data) {86handler(data);87}, 'json');88}8990function connectMessagingClient(tokenResponse) {91// Initialize the IP messaging client92tc.accessManager = new Twilio.AccessManager(tokenResponse.token);93tc.messagingClient = new Twilio.IPMessaging.Client(tc.accessManager);94updateConnectedUI();95tc.loadChannelList(tc.joinGeneralChannel);96tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));97tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));98tc.messagingClient.on('tokenExpired', refreshToken);99}100101function refreshToken() {102fetchAccessToken(tc.username, setNewToken);103}104105function setNewToken(tokenResponse) {106tc.accessManager.updateToken(tokenResponse.token);107}108109function updateConnectedUI() {110$('#username-span').text(tc.username);111$statusRow.addClass('connected').removeClass('disconnected');112tc.$messageList.addClass('connected').removeClass('disconnected');113$connectPanel.addClass('connected').removeClass('disconnected');114$inputText.addClass('with-shadow');115$typingRow.addClass('connected').removeClass('disconnected');116}117118tc.loadChannelList = function (handler) {119if (tc.messagingClient === undefined) {120console.log('Client is not initialized');121return;122}123124tc.messagingClient.getChannels().then(function (channels) {125tc.channelArray = tc.sortChannelsByName(channels);126$channelList.text('');127tc.channelArray.forEach(addChannel);128if (typeof handler === 'function') {129handler();130}131});132};133134tc.joinGeneralChannel = function () {135console.log('Attempting to join "general" chat channel...');136if (!tc.generalChannel) {137// If it doesn't exist, let's create it138tc.messagingClient.createChannel({139uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,140friendlyName: GENERAL_CHANNEL_NAME141}).then(function (channel) {142console.log('Created general channel');143tc.generalChannel = channel;144tc.loadChannelList(tc.joinGeneralChannel);145});146}147else {148console.log('Found general channel:');149setupChannel(tc.generalChannel);150}151};152153function setupChannel(channel) {154// Join the channel155channel.join().then(function (joinedChannel) {156console.log('Joined channel ' + joinedChannel.friendlyName);157leaveCurrentChannel();158updateChannelUI(channel);159tc.currentChannel = channel;160tc.loadMessages();161channel.on('messageAdded', tc.addMessageToList);162channel.on('typingStarted', showTypingStarted);163channel.on('typingEnded', hideTypingStarted);164channel.on('memberJoined', notifyMemberJoined);165channel.on('memberLeft', notifyMemberLeft);166$inputText.prop('disabled', false).focus();167tc.$messageList.text('');168});169}170171tc.loadMessages = function () {172tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT).then(function (messages) {173messages.forEach(tc.addMessageToList);174});175};176177function leaveCurrentChannel() {178if (tc.currentChannel) {179tc.currentChannel.leave().then(function (leftChannel) {180console.log('left ' + leftChannel.friendlyName);181leftChannel.removeListener('messageAdded', tc.addMessageToList);182leftChannel.removeListener('typingStarted', showTypingStarted);183leftChannel.removeListener('typingEnded', hideTypingStarted);184leftChannel.removeListener('memberJoined', notifyMemberJoined);185leftChannel.removeListener('memberLeft', notifyMemberLeft);186});187}188}189190tc.addMessageToList = function (message) {191var rowDiv = $('<div>').addClass('row no-margin');192rowDiv.loadTemplate($('#message-template'), {193username: message.author,194date: dateFormatter.getTodayDate(message.timestamp),195body: message.body196});197if (message.author === tc.username) {198rowDiv.addClass('own-message');199}200201tc.$messageList.append(rowDiv);202scrollToMessageListBottom();203};204205function notifyMemberJoined(member) {206notify(member.identity + ' joined the channel')207}208209function notifyMemberLeft(member) {210notify(member.identity + ' left the channel');211}212213function notify(message) {214var row = $('<div>').addClass('col-md-12');215row.loadTemplate('#member-notification-template', {216status: message217});218tc.$messageList.append(row);219scrollToMessageListBottom();220}221222function showTypingStarted(member) {223$typingPlaceholder.text(member.identity + ' is typing...');224}225226function hideTypingStarted(member) {227$typingPlaceholder.text('');228}229230function scrollToMessageListBottom() {231tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);232}233234function updateChannelUI(selectedChannel) {235var channelElements = $('.channel-element').toArray();236var channelElement = channelElements.filter(function (element) {237return $(element).data().sid === selectedChannel.sid;238});239channelElement = $(channelElement);240if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {241tc.currentChannelContainer = channelElement;242}243tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');244channelElement.removeClass('unselected-channel').addClass('selected-channel');245tc.currentChannelContainer = channelElement;246}247248function showAddChannelInput() {249if (tc.messagingClient) {250$newChannelInputRow.addClass('showing').removeClass('not-showing');251$channelList.addClass('showing').removeClass('not-showing');252$newChannelInput.focus();253}254}255256function hideAddChannelInput() {257$newChannelInputRow.addClass('not-showing').removeClass('showing');258$channelList.addClass('not-showing').removeClass('showing');259$newChannelInput.val('');260}261262function addChannel(channel) {263if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {264tc.generalChannel = channel;265}266var rowDiv = $('<div>').addClass('row channel-row');267rowDiv.loadTemplate('#channel-template', {268channelName: channel.friendlyName269});270271var channelP = rowDiv.children().children().first();272273rowDiv.on('click', selectChannel);274channelP.data('sid', channel.sid);275if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {276tc.currentChannelContainer = channelP;277channelP.addClass('selected-channel');278}279else {280channelP.addClass('unselected-channel')281}282283$channelList.append(rowDiv);284}285286function deleteCurrentChannel() {287if (!tc.currentChannel) {288return;289}290if (tc.currentChannel.sid === tc.generalChannel.sid) {291alert('You cannot delete the general channel');292return;293}294tc.currentChannel.delete().then(function (channel) {295console.log('channel: ' + channel.friendlyName + ' deleted');296setupChannel(tc.generalChannel);297});298}299300function selectChannel(event) {301var target = $(event.target);302var channelSid = target.data().sid;303var selectedChannel = tc.channelArray.filter(function (channel) {304return channel.sid === channelSid;305})[0];306if (selectedChannel === tc.currentChannel) {307return;308}309setupChannel(selectedChannel);310};311312function disconnectClient() {313leaveCurrentChannel();314$channelList.text('');315tc.$messageList.text('');316channels = undefined;317$statusRow.addClass('disconnected').removeClass('connected');318tc.$messageList.addClass('disconnected').removeClass('connected');319$connectPanel.addClass('disconnected').removeClass('connected');320$inputText.removeClass('with-shadow');321$typingRow.addClass('disconnected').removeClass('connected');322}323324tc.sortChannelsByName = function (channels) {325return channels.sort(function (a, b) {326if (a.friendlyName === GENERAL_CHANNEL_NAME) {327return -1;328}329if (b.friendlyName === GENERAL_CHANNEL_NAME) {330return 1;331}332return a.friendlyName.localeCompare(b.friendlyName);333});334};335336return tc;337})();
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.TwilioChat.Web/Scripts/twiliochat.js
1var twiliochat = (function () {2var tc = {};34var GENERAL_CHANNEL_UNIQUE_NAME = 'general';5var GENERAL_CHANNEL_NAME = 'General Channel';6var MESSAGES_HISTORY_LIMIT = 50;78var $channelList;9var $inputText;10var $usernameInput;11var $statusRow;12var $connectPanel;13var $newChannelInputRow;14var $newChannelInput;15var $typingRow;16var $typingPlaceholder;1718$(document).ready(function () {19tc.$messageList = $('#message-list');20$channelList = $('#channel-list');21$inputText = $('#input-text');22$usernameInput = $('#username-input');23$statusRow = $('#status-row');24$connectPanel = $('#connect-panel');25$newChannelInputRow = $('#new-channel-input-row');26$newChannelInput = $('#new-channel-input');27$typingRow = $('#typing-row');28$typingPlaceholder = $('#typing-placeholder');29$usernameInput.focus();30$usernameInput.on('keypress', handleUsernameInputKeypress);31$inputText.on('keypress', handleInputTextKeypress);32$newChannelInput.on('keypress', tc.handleNewChannelInputKeypress);33$('#connect-image').on('click', connectClientWithUsername);34$('#add-channel-image').on('click', showAddChannelInput);35$('#leave-span').on('click', disconnectClient);36$('#delete-channel-span').on('click', deleteCurrentChannel);37});3839function handleUsernameInputKeypress(event) {40if (event.keyCode === 13) {41connectClientWithUsername();42}43}4445function handleInputTextKeypress(event) {46if (event.keyCode === 13) {47tc.currentChannel.sendMessage($(this).val());48event.preventDefault();49$(this).val('');50}51else {52notifyTyping();53}54}5556var notifyTyping = $.throttle(function () {57tc.currentChannel.typing();58}, 1000);5960tc.handleNewChannelInputKeypress = function (event) {61if (event.keyCode === 13) {62tc.messagingClient.createChannel({63friendlyName: $newChannelInput.val()64}).then(hideAddChannelInput);65$(this).val('');66event.preventDefault();67}68};6970function connectClientWithUsername() {71var usernameText = $usernameInput.val();72$usernameInput.val('');73if (usernameText == '') {74alert('Username cannot be empty');75return;76}77tc.username = usernameText;78fetchAccessToken(tc.username, connectMessagingClient);79}8081function fetchAccessToken(username, handler) {82$.post('/token', {83identity: username,84device: 'browser'85}, function (data) {86handler(data);87}, 'json');88}8990function connectMessagingClient(tokenResponse) {91// Initialize the IP messaging client92tc.accessManager = new Twilio.AccessManager(tokenResponse.token);93tc.messagingClient = new Twilio.IPMessaging.Client(tc.accessManager);94updateConnectedUI();95tc.loadChannelList(tc.joinGeneralChannel);96tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));97tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));98tc.messagingClient.on('tokenExpired', refreshToken);99}100101function refreshToken() {102fetchAccessToken(tc.username, setNewToken);103}104105function setNewToken(tokenResponse) {106tc.accessManager.updateToken(tokenResponse.token);107}108109function updateConnectedUI() {110$('#username-span').text(tc.username);111$statusRow.addClass('connected').removeClass('disconnected');112tc.$messageList.addClass('connected').removeClass('disconnected');113$connectPanel.addClass('connected').removeClass('disconnected');114$inputText.addClass('with-shadow');115$typingRow.addClass('connected').removeClass('disconnected');116}117118tc.loadChannelList = function (handler) {119if (tc.messagingClient === undefined) {120console.log('Client is not initialized');121return;122}123124tc.messagingClient.getChannels().then(function (channels) {125tc.channelArray = tc.sortChannelsByName(channels);126$channelList.text('');127tc.channelArray.forEach(addChannel);128if (typeof handler === 'function') {129handler();130}131});132};133134tc.joinGeneralChannel = function () {135console.log('Attempting to join "general" chat channel...');136if (!tc.generalChannel) {137// If it doesn't exist, let's create it138tc.messagingClient.createChannel({139uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,140friendlyName: GENERAL_CHANNEL_NAME141}).then(function (channel) {142console.log('Created general channel');143tc.generalChannel = channel;144tc.loadChannelList(tc.joinGeneralChannel);145});146}147else {148console.log('Found general channel:');149setupChannel(tc.generalChannel);150}151};152153function setupChannel(channel) {154// Join the channel155channel.join().then(function (joinedChannel) {156console.log('Joined channel ' + joinedChannel.friendlyName);157leaveCurrentChannel();158updateChannelUI(channel);159tc.currentChannel = channel;160tc.loadMessages();161channel.on('messageAdded', tc.addMessageToList);162channel.on('typingStarted', showTypingStarted);163channel.on('typingEnded', hideTypingStarted);164channel.on('memberJoined', notifyMemberJoined);165channel.on('memberLeft', notifyMemberLeft);166$inputText.prop('disabled', false).focus();167tc.$messageList.text('');168});169}170171tc.loadMessages = function () {172tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT).then(function (messages) {173messages.forEach(tc.addMessageToList);174});175};176177function leaveCurrentChannel() {178if (tc.currentChannel) {179tc.currentChannel.leave().then(function (leftChannel) {180console.log('left ' + leftChannel.friendlyName);181leftChannel.removeListener('messageAdded', tc.addMessageToList);182leftChannel.removeListener('typingStarted', showTypingStarted);183leftChannel.removeListener('typingEnded', hideTypingStarted);184leftChannel.removeListener('memberJoined', notifyMemberJoined);185leftChannel.removeListener('memberLeft', notifyMemberLeft);186});187}188}189190tc.addMessageToList = function (message) {191var rowDiv = $('<div>').addClass('row no-margin');192rowDiv.loadTemplate($('#message-template'), {193username: message.author,194date: dateFormatter.getTodayDate(message.timestamp),195body: message.body196});197if (message.author === tc.username) {198rowDiv.addClass('own-message');199}200201tc.$messageList.append(rowDiv);202scrollToMessageListBottom();203};204205function notifyMemberJoined(member) {206notify(member.identity + ' joined the channel')207}208209function notifyMemberLeft(member) {210notify(member.identity + ' left the channel');211}212213function notify(message) {214var row = $('<div>').addClass('col-md-12');215row.loadTemplate('#member-notification-template', {216status: message217});218tc.$messageList.append(row);219scrollToMessageListBottom();220}221222function showTypingStarted(member) {223$typingPlaceholder.text(member.identity + ' is typing...');224}225226function hideTypingStarted(member) {227$typingPlaceholder.text('');228}229230function scrollToMessageListBottom() {231tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);232}233234function updateChannelUI(selectedChannel) {235var channelElements = $('.channel-element').toArray();236var channelElement = channelElements.filter(function (element) {237return $(element).data().sid === selectedChannel.sid;238});239channelElement = $(channelElement);240if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {241tc.currentChannelContainer = channelElement;242}243tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');244channelElement.removeClass('unselected-channel').addClass('selected-channel');245tc.currentChannelContainer = channelElement;246}247248function showAddChannelInput() {249if (tc.messagingClient) {250$newChannelInputRow.addClass('showing').removeClass('not-showing');251$channelList.addClass('showing').removeClass('not-showing');252$newChannelInput.focus();253}254}255256function hideAddChannelInput() {257$newChannelInputRow.addClass('not-showing').removeClass('showing');258$channelList.addClass('not-showing').removeClass('showing');259$newChannelInput.val('');260}261262function addChannel(channel) {263if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {264tc.generalChannel = channel;265}266var rowDiv = $('<div>').addClass('row channel-row');267rowDiv.loadTemplate('#channel-template', {268channelName: channel.friendlyName269});270271var channelP = rowDiv.children().children().first();272273rowDiv.on('click', selectChannel);274channelP.data('sid', channel.sid);275if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {276tc.currentChannelContainer = channelP;277channelP.addClass('selected-channel');278}279else {280channelP.addClass('unselected-channel')281}282283$channelList.append(rowDiv);284}285286function deleteCurrentChannel() {287if (!tc.currentChannel) {288return;289}290if (tc.currentChannel.sid === tc.generalChannel.sid) {291alert('You cannot delete the general channel');292return;293}294tc.currentChannel.delete().then(function (channel) {295console.log('channel: ' + channel.friendlyName + ' deleted');296setupChannel(tc.generalChannel);297});298}299300function selectChannel(event) {301var target = $(event.target);302var channelSid = target.data().sid;303var selectedChannel = tc.channelArray.filter(function (channel) {304return channel.sid === channelSid;305})[0];306if (selectedChannel === tc.currentChannel) {307return;308}309setupChannel(selectedChannel);310};311312function disconnectClient() {313leaveCurrentChannel();314$channelList.text('');315tc.$messageList.text('');316channels = undefined;317$statusRow.addClass('disconnected').removeClass('connected');318tc.$messageList.addClass('disconnected').removeClass('connected');319$connectPanel.addClass('disconnected').removeClass('connected');320$inputText.removeClass('with-shadow');321$typingRow.addClass('disconnected').removeClass('connected');322}323324tc.sortChannelsByName = function (channels) {325return channels.sort(function (a, b) {326if (a.friendlyName === GENERAL_CHANNEL_NAME) {327return -1;328}329if (b.friendlyName === GENERAL_CHANNEL_NAME) {330return 1;331}332return a.friendlyName.localeCompare(b.friendlyName);333});334};335336return tc;337})();
We've actually got a real chat app going here, but let's make it more interesting with multiple channels.
To create a new channel, the user clicks on the "+ Channel" link. That we'll show an input text field where it's possible to type the name of the new channel. The only restriction here, is that the user can't create a channel called "General Channel". Other than that, creating a channel involves calling createChannel
with an object that has the friendlyName
key. You can create a channel with more options though, see a list of the options here.
TwilioChat.Web/Scripts/twiliochat.js
1var twiliochat = (function () {2var tc = {};34var GENERAL_CHANNEL_UNIQUE_NAME = 'general';5var GENERAL_CHANNEL_NAME = 'General Channel';6var MESSAGES_HISTORY_LIMIT = 50;78var $channelList;9var $inputText;10var $usernameInput;11var $statusRow;12var $connectPanel;13var $newChannelInputRow;14var $newChannelInput;15var $typingRow;16var $typingPlaceholder;1718$(document).ready(function () {19tc.$messageList = $('#message-list');20$channelList = $('#channel-list');21$inputText = $('#input-text');22$usernameInput = $('#username-input');23$statusRow = $('#status-row');24$connectPanel = $('#connect-panel');25$newChannelInputRow = $('#new-channel-input-row');26$newChannelInput = $('#new-channel-input');27$typingRow = $('#typing-row');28$typingPlaceholder = $('#typing-placeholder');29$usernameInput.focus();30$usernameInput.on('keypress', handleUsernameInputKeypress);31$inputText.on('keypress', handleInputTextKeypress);32$newChannelInput.on('keypress', tc.handleNewChannelInputKeypress);33$('#connect-image').on('click', connectClientWithUsername);34$('#add-channel-image').on('click', showAddChannelInput);35$('#leave-span').on('click', disconnectClient);36$('#delete-channel-span').on('click', deleteCurrentChannel);37});3839function handleUsernameInputKeypress(event) {40if (event.keyCode === 13) {41connectClientWithUsername();42}43}4445function handleInputTextKeypress(event) {46if (event.keyCode === 13) {47tc.currentChannel.sendMessage($(this).val());48event.preventDefault();49$(this).val('');50}51else {52notifyTyping();53}54<}