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
_30using System.Collections.Generic;_30using Twilio.Jwt.AccessToken;_30_30namespace TwilioChat.Web.Domain_30{_30 public interface ITokenGenerator_30 {_30 string Generate(string identity);_30 }_30_30 public class TokenGenerator : ITokenGenerator_30 {_30 public string Generate(string identity)_30 {_30 var grants = new HashSet<IGrant>_30 {_30 new ChatGrant {ServiceSid = Configuration.ChatServiceSID}_30 };_30_30 var token = new Token(_30 Configuration.AccountSID,_30 Configuration.ApiKey,_30 Configuration.ApiSecret,_30 identity,_30 grants: grants);_30_30 return token.ToJwt();_30 }_30 }_30}
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
_27using System.Web.Mvc;_27using TwilioChat.Web.Domain;_27_27namespace TwilioChat.Web.Controllers_27{_27 public class TokenController : Controller_27 {_27 private readonly ITokenGenerator _tokenGenerator;_27_27 public TokenController() : this(new TokenGenerator()) { }_27_27 public TokenController(ITokenGenerator tokenGenerator)_27 {_27 _tokenGenerator = tokenGenerator;_27 }_27_27 // POST: Token_27 [HttpPost]_27 public ActionResult Index(string identity)_27 {_27 if (identity == null) return null;_27_27 var token = _tokenGenerator.Generate(identity);_27 return Json(new {identity, token});_27 }_27 }_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
_381var twiliochat = (function () {_381 var tc = {};_381_381 var GENERAL_CHANNEL_UNIQUE_NAME = 'general';_381 var GENERAL_CHANNEL_NAME = 'General Channel';_381 var MESSAGES_HISTORY_LIMIT = 50;_381_381 var $channelList;_381 var $inputText;_381 var $usernameInput;_381 var $statusRow;_381 var $connectPanel;_381 var $newChannelInputRow;_381 var $newChannelInput;_381 var $typingRow;_381 var $typingPlaceholder;_381_381 $(document).ready(function () {_381 tc.init();_381 });_381_381 tc.init = function () {_381 tc.$messageList = $('#message-list');_381 $channelList = $('#channel-list');_381 $inputText = $('#input-text');_381 $usernameInput = $('#username-input');_381 $statusRow = $('#status-row');_381 $connectPanel = $('#connect-panel');_381 $newChannelInputRow = $('#new-channel-input-row');_381 $newChannelInput = $('#new-channel-input');_381 $typingRow = $('#typing-row');_381 $typingPlaceholder = $('#typing-placeholder');_381 $usernameInput.focus();_381 $usernameInput.on('keypress', handleUsernameInputKeypress);_381 $inputText.on('keypress', handleInputTextKeypress);_381 $newChannelInput.on('keypress', tc.handleNewChannelInputKeypress);_381 $('#connect-image').on('click', connectClientWithUsername);_381 $('#add-channel-image').on('click', showAddChannelInput);_381 $('#leave-span').on('click', disconnectClient);_381 $('#delete-channel-span').on('click', deleteCurrentChannel);_381 };_381_381 function handleUsernameInputKeypress(event) {_381 if (event.keyCode === 13) {_381 connectClientWithUsername();_381 }_381 }_381_381 function handleInputTextKeypress(event) {_381 if (event.keyCode === 13) {_381 tc.currentChannel.sendMessage($(this).val());_381 event.preventDefault();_381 $(this).val('');_381 }_381 else {_381 notifyTyping();_381 }_381 }_381_381 var notifyTyping = $.throttle(function () {_381 tc.currentChannel.typing();_381 }, 1000);_381_381 tc.handleNewChannelInputKeypress = function (event) {_381 if (event.keyCode === 13) {_381 tc.messagingClient_381 .createChannel({_381 friendlyName: $newChannelInput.val(),_381 })_381 .then(hideAddChannelInput);_381_381 $(this).val('');_381 event.preventDefault();_381 }_381 };_381_381 function connectClientWithUsername() {_381 var usernameText = $usernameInput.val();_381 $usernameInput.val('');_381 if (usernameText == '') {_381 alert('Username cannot be empty');_381 return;_381 }_381 tc.username = usernameText;_381 fetchAccessToken(tc.username, connectMessagingClient);_381 }_381_381 function fetchAccessToken(username, handler) {_381 $.post('/token', { identity: username}, null, 'json')_381 .done(function (response) {_381 handler(response.token);_381 })_381 .fail(function (error) {_381 console.log('Failed to fetch the Access Token with error: ' + error);_381 });_381 }_381_381 function connectMessagingClient(token) {_381 // Initialize the Chat messaging client_381 Twilio.Chat.Client.create(token).then(function (client) {_381 tc.messagingClient = client;_381 updateConnectedUI();_381 tc.loadChannelList(tc.joinGeneralChannel);_381 tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));_381 tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));_381 tc.messagingClient.on('tokenExpired', refreshToken);_381 });_381 }_381_381 function refreshToken() {_381 fetchAccessToken(tc.username, setNewToken);_381 }_381_381 function setNewToken(token) {_381 tc.messagingClient.updateToken(token);_381 }_381_381 function updateConnectedUI() {_381 $('#username-span').text(tc.username);_381 $statusRow.addClass('connected').removeClass('disconnected');_381 tc.$messageList.addClass('connected').removeClass('disconnected');_381 $connectPanel.addClass('connected').removeClass('disconnected');_381 $inputText.addClass('with-shadow');_381 $typingRow.addClass('connected').removeClass('disconnected');_381 }_381_381 tc.loadChannelList = function (handler) {_381 if (tc.messagingClient === undefined) {_381 console.log('Client is not initialized');_381 return;_381 }_381_381 tc.messagingClient.getPublicChannelDescriptors().then(function (channels) {_381 tc.channelArray = tc.sortChannelsByName(channels.items);_381 $channelList.text('');_381 tc.channelArray.forEach(addChannel);_381 if (typeof handler === 'function') {_381 handler();_381 }_381 });_381 };_381_381 tc.joinGeneralChannel = function () {_381 console.log('Attempting to join "general" chat channel...');_381 if (!tc.generalChannel) {_381 // If it doesn't exist, let's create it_381 tc.messagingClient.createChannel({_381 uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,_381 friendlyName: GENERAL_CHANNEL_NAME_381 }).then(function (channel) {_381 console.log('Created general channel');_381 tc.generalChannel = channel;_381 tc.loadChannelList(tc.joinGeneralChannel);_381 });_381 }_381 else {_381 console.log('Found general channel:');_381 setupChannel(tc.generalChannel);_381 }_381 };_381_381 function initChannel(channel) {_381 console.log('Initialized channel ' + channel.friendlyName);_381 return tc.messagingClient.getChannelBySid(channel.sid);_381 }_381_381 function joinChannel(_channel) {_381 return _channel.join()_381 .then(function (joinedChannel) {_381 console.log('Joined channel ' + joinedChannel.friendlyName);_381 updateChannelUI(_channel);_381_381 return joinedChannel;_381 })_381 .catch(function (err) {_381 if (_channel.status == 'joined') {_381 updateChannelUI(_channel);_381 return _channel;_381 }_381 console.error(_381 "Couldn't join channel " + _channel.friendlyName + ' because -> ' + err_381 );_381 });_381 }_381_381 function initChannelEvents() {_381 console.log(tc.currentChannel.friendlyName + ' ready.');_381 tc.currentChannel.on('messageAdded', tc.addMessageToList);_381 tc.currentChannel.on('typingStarted', showTypingStarted);_381 tc.currentChannel.on('typingEnded', hideTypingStarted);_381 tc.currentChannel.on('memberJoined', notifyMemberJoined);_381 tc.currentChannel.on('memberLeft', notifyMemberLeft);_381 $inputText.prop('disabled', false).focus();_381 }_381_381 function setupChannel(channel) {_381 return leaveCurrentChannel()_381 .then(function () {_381 return initChannel(channel);_381 })_381 .then(function (_channel) {_381 return joinChannel(_channel);_381 })_381 .then(initChannelEvents);_381 }_381_381 tc.loadMessages = function () {_381 tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT).then(function (messages) {_381 messages.items.forEach(tc.addMessageToList);_381 });_381 };_381_381 function leaveCurrentChannel() {_381 if (tc.currentChannel) {_381 return tc.currentChannel.leave().then(function (leftChannel) {_381 console.log('left ' + leftChannel.friendlyName);_381 leftChannel.removeListener('messageAdded', tc.addMessageToList);_381 leftChannel.removeListener('typingStarted', showTypingStarted);_381 leftChannel.removeListener('typingEnded', hideTypingStarted);_381 leftChannel.removeListener('memberJoined', notifyMemberJoined);_381 leftChannel.removeListener('memberLeft', notifyMemberLeft);_381 });_381 } else {_381 return Promise.resolve();_381 }_381 }_381_381 tc.addMessageToList = function (message) {_381 var rowDiv = $('<div>').addClass('row no-margin');_381 rowDiv.loadTemplate($('#message-template'), {_381 username: message.author,_381 date: dateFormatter.getTodayDate(message.dateCreated),_381 body: message.body_381 });_381 if (message.author === tc.username) {_381 rowDiv.addClass('own-message');_381 }_381_381 tc.$messageList.append(rowDiv);_381 scrollToMessageListBottom();_381 };_381_381 function notifyMemberJoined(member) {_381 notify(member.identity + ' joined the channel')_381 }_381_381 function notifyMemberLeft(member) {_381 notify(member.identity + ' left the channel');_381 }_381_381 function notify(message) {_381 var row = $('<div>').addClass('col-md-12');_381 row.loadTemplate('#member-notification-template', {_381 status: message_381 });_381 tc.$messageList.append(row);_381 scrollToMessageListBottom();_381 }_381_381 function showTypingStarted(member) {_381 $typingPlaceholder.text(member.identity + ' is typing...');_381 }_381_381 function hideTypingStarted(member) {_381 $typingPlaceholder.text('');_381 }_381_381 function scrollToMessageListBottom() {_381 tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);_381 }_381_381 function updateChannelUI(selectedChannel) {_381 var channelElements = $('.channel-element').toArray();_381 var channelElement = channelElements.filter(function (element) {_381 return $(element).data().sid === selectedChannel.sid;_381 });_381 channelElement = $(channelElement);_381 if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {_381 tc.currentChannelContainer = channelElement;_381 }_381 tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');_381 channelElement.removeClass('unselected-channel').addClass('selected-channel');_381 tc.currentChannelContainer = channelElement;_381 tc.currentChannel = selectedChannel;_381 tc.loadMessages();_381 }_381_381 function showAddChannelInput() {_381 if (tc.messagingClient) {_381 $newChannelInputRow.addClass('showing').removeClass('not-showing');_381 $channelList.addClass('showing').removeClass('not-showing');_381 $newChannelInput.focus();_381 }_381 }_381_381 function hideAddChannelInput() {_381 $newChannelInputRow.addClass('not-showing').removeClass('showing');_381 $channelList.addClass('not-showing').removeClass('showing');_381 $newChannelInput.val('');_381 }_381_381 function addChannel(channel) {_381 if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {_381 tc.generalChannel = channel;_381 }_381 var rowDiv = $('<div>').addClass('row channel-row');_381 rowDiv.loadTemplate('#channel-template', {_381 channelName: channel.friendlyName_381 });_381_381 var channelP = rowDiv.children().children().first();_381_381 rowDiv.on('click', selectChannel);_381 channelP.data('sid', channel.sid);_381 if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {_381 tc.currentChannelContainer = channelP;_381 channelP.addClass('selected-channel');_381 }_381 else {_381 channelP.addClass('unselected-channel')_381 }_381_381 $channelList.append(rowDiv);_381 }_381_381 function deleteCurrentChannel() {_381 if (!tc.currentChannel) {_381 return;_381 }_381_381 if (tc.currentChannel.sid === tc.generalChannel.sid) {_381 alert('You cannot delete the general channel');_381 return;_381 }_381_381 tc.currentChannel_381 .delete()_381 .then(function (channel) {_381 console.log('channel: ' + channel.friendlyName + ' deleted');_381 setupChannel(tc.generalChannel);_381 });_381 }_381_381 function selectChannel(event) {_381 var target = $(event.target);_381 var channelSid = target.data().sid;_381 var selectedChannel = tc.channelArray.filter(function (channel) {_381 return channel.sid === channelSid;_381 })[0];_381 if (selectedChannel === tc.currentChannel) {_381 return;_381 }_381 setupChannel(selectedChannel);_381 };_381_381 function disconnectClient() {_381 leaveCurrentChannel();_381 $channelList.text('');_381 tc.$messageList.text('');_381 channels = undefined;_381 $statusRow.addClass('disconnected').removeClass('connected');_381 tc.$messageList.addClass('disconnected').removeClass('connected');_381 $connectPanel.addClass('disconnected').removeClass('connected');_381 $inputText.removeClass('with-shadow');_381 $typingRow.addClass('disconnected').removeClass('connected');_381 }_381_381 tc.sortChannelsByName = function (channels) {_381 return channels.sort(function (a, b) {_381 if (a.friendlyName === GENERAL_CHANNEL_NAME) {_381 return -1;_381 }_381 if (b.friendlyName === GENERAL_CHANNEL_NAME) {_381 return 1;_381 }_381 return a.friendlyName.localeCompare(b.friendlyName);_381 });_381 };_381_381 return tc;_381})();
Now that we've 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
_337var twiliochat = (function () {_337 var tc = {};_337_337 var GENERAL_CHANNEL_UNIQUE_NAME = 'general';_337 var GENERAL_CHANNEL_NAME = 'General Channel';_337 var MESSAGES_HISTORY_LIMIT = 50;_337_337 var $channelList;_337 var $inputText;_337 var $usernameInput;_337 var $statusRow;_337 var $connectPanel;_337 var $newChannelInputRow;_337 var $newChannelInput;_337 var $typingRow;_337 var $typingPlaceholder;_337_337 $(document).ready(function () {_337 tc.$messageList = $('#message-list');_337 $channelList = $('#channel-list');_337 $inputText = $('#input-text');_337 $usernameInput = $('#username-input');_337 $statusRow = $('#status-row');_337 $connectPanel = $('#connect-panel');_337 $newChannelInputRow = $('#new-channel-input-row');_337 $newChannelInput = $('#new-channel-input');_337 $typingRow = $('#typing-row');_337 $typingPlaceholder = $('#typing-placeholder');_337 $usernameInput.focus();_337 $usernameInput.on('keypress', handleUsernameInputKeypress);_337 $inputText.on('keypress', handleInputTextKeypress);_337 $newChannelInput.on('keypress', tc.handleNewChannelInputKeypress);_337 $('#connect-image').on('click', connectClientWithUsername);_337 $('#add-channel-image').on('click', showAddChannelInput);_337 $('#leave-span').on('click', disconnectClient);_337 $('#delete-channel-span').on('click', deleteCurrentChannel);_337 });_337_337 function handleUsernameInputKeypress(event) {_337 if (event.keyCode === 13) {_337 connectClientWithUsername();_337 }_337 }_337_337 function handleInputTextKeypress(event) {_337 if (event.keyCode === 13) {_337 tc.currentChannel.sendMessage($(this).val());_337 event.preventDefault();_337 $(this).val('');_337 }_337 else {_337 notifyTyping();_337 }_337 }_337_337 var notifyTyping = $.throttle(function () {_337 tc.currentChannel.typing();_337 }, 1000);_337_337 tc.handleNewChannelInputKeypress = function (event) {_337 if (event.keyCode === 13) {_337 tc.messagingClient.createChannel({_337 friendlyName: $newChannelInput.val()_337 }).then(hideAddChannelInput);_337 $(this).val('');_337 event.preventDefault();_337 }_337 };_337_337 function connectClientWithUsername() {_337 var usernameText = $usernameInput.val();_337 $usernameInput.val('');_337 if (usernameText == '') {_337 alert('Username cannot be empty');_337 return;_337 }_337 tc.username = usernameText;_337 fetchAccessToken(tc.username, connectMessagingClient);_337 }_337_337 function fetchAccessToken(username, handler) {_337 $.post('/token', {_337 identity: username,_337 device: 'browser'_337 }, function (data) {_337 handler(data);_337 }, 'json');_337 }_337_337 function connectMessagingClient(tokenResponse) {_337 // Initialize the IP messaging client_337 tc.accessManager = new Twilio.AccessManager(tokenResponse.token);_337 tc.messagingClient = new Twilio.IPMessaging.Client(tc.accessManager);_337 updateConnectedUI();_337 tc.loadChannelList(tc.joinGeneralChannel);_337 tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));_337 tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));_337 tc.messagingClient.on('tokenExpired', refreshToken);_337 }_337_337 function refreshToken() {_337 fetchAccessToken(tc.username, setNewToken);_337 }_337_337 function setNewToken(tokenResponse) {_337 tc.accessManager.updateToken(tokenResponse.token);_337 }_337_337 function updateConnectedUI() {_337 $('#username-span').text(tc.username);_337 $statusRow.addClass('connected').removeClass('disconnected');_337 tc.$messageList.addClass('connected').removeClass('disconnected');_337 $connectPanel.addClass('connected').removeClass('disconnected');_337 $inputText.addClass('with-shadow');_337 $typingRow.addClass('connected').removeClass('disconnected');_337 }_337_337 tc.loadChannelList = function (handler) {_337 if (tc.messagingClient === undefined) {_337 console.log('Client is not initialized');_337 return;_337 }_337_337 tc.messagingClient.getChannels().then(function (channels) {_337 tc.channelArray = tc.sortChannelsByName(channels);_337 $channelList.text('');_337 tc.channelArray.forEach(addChannel);_337 if (typeof handler === 'function') {_337 handler();_337 }_337 });_337 };_337_337 tc.joinGeneralChannel = function () {_337 console.log('Attempting to join "general" chat channel...');_337 if (!tc.generalChannel) {_337 // If it doesn't exist, let's create it_337 tc.messagingClient.createChannel({_337 uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,_337 friendlyName: GENERAL_CHANNEL_NAME_337 }).then(function (channel) {_337 console.log('Created general channel');_337 tc.generalChannel = channel;_337 tc.loadChannelList(tc.joinGeneralChannel);_337 });_337 }_337 else {_337 console.log('Found general channel:');_337 setupChannel(tc.generalChannel);_337 }_337 };_337_337 function setupChannel(channel) {_337 // Join the channel_337 channel.join().then(function (joinedChannel) {_337 console.log('Joined channel ' + joinedChannel.friendlyName);_337 leaveCurrentChannel();_337 updateChannelUI(channel);_337 tc.currentChannel = channel;_337 tc.loadMessages();_337 channel.on('messageAdded', tc.addMessageToList);_337 channel.on('typingStarted', showTypingStarted);_337 channel.on('typingEnded', hideTypingStarted);_337 channel.on('memberJoined', notifyMemberJoined);_337 channel.on('memberLeft', notifyMemberLeft);_337 $inputText.prop('disabled', false).focus();_337 tc.$messageList.text('');_337 });_337 }_337_337 tc.loadMessages = function () {_337 tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT).then(function (messages) {_337 messages.forEach(tc.addMessageToList);_337 });_337 };_337_337 function leaveCurrentChannel() {_337 if (tc.currentChannel) {_337 tc.currentChannel.leave().then(function (leftChannel) {_337 console.log('left ' + leftChannel.friendlyName);_337 leftChannel.removeListener('messageAdded', tc.addMessageToList);_337 leftChannel.removeListener('typingStarted', showTypingStarted);_337 leftChannel.removeListener('typingEnded', hideTypingStarted);_337 leftChannel.removeListener('memberJoined', notifyMemberJoined);_337 leftChannel.removeListener('memberLeft', notifyMemberLeft);_337 });_337 }_337 }_337_337 tc.addMessageToList = function (message) {_337 var rowDiv = $('<div>').addClass('row no-margin');_337 rowDiv.loadTemplate($('#message-template'), {_337 username: message.author,_337 date: dateFormatter.getTodayDate(message.timestamp),_337 body: message.body_337 });_337 if (message.author === tc.username) {_337 rowDiv.addClass('own-message');_337 }_337_337 tc.$messageList.append(rowDiv);_337 scrollToMessageListBottom();_337 };_337_337 function notifyMemberJoined(member) {_337 notify(member.identity + ' joined the channel')_337 }_337_337 function notifyMemberLeft(member) {_337 notify(member.identity + ' left the channel');_337 }_337_337 function notify(message) {_337 var row = $('<div>').addClass('col-md-12');_337 row.loadTemplate('#member-notification-template', {_337 status: message_337 });_337 tc.$messageList.append(row);_337 scrollToMessageListBottom();_337 }_337_337 function showTypingStarted(member) {_337 $typingPlaceholder.text(member.identity + ' is typing...');_337 }_337_337 function hideTypingStarted(member) {_337 $typingPlaceholder.text('');_337 }_337_337 function scrollToMessageListBottom() {_337 tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);_337 }_337_337 function updateChannelUI(selectedChannel) {_337 var channelElements = $('.channel-element').toArray();_337 var channelElement = channelElements.filter(function (element) {_337 return $(element).data().sid === selectedChannel.sid;_337 });_337 channelElement = $(channelElement);_337 if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {_337 tc.currentChannelContainer = channelElement;_337 }_337 tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');_337 channelElement.removeClass('unselected-channel').addClass('selected-channel');_337 tc.currentChannelContainer = channelElement;_337 }_337_337 function showAddChannelInput() {_337 if (tc.messagingClient) {_337 $newChannelInputRow.addClass('showing').removeClass('not-showing');_337 $channelList.addClass('showing').removeClass('not-showing');_337 $newChannelInput.focus();_337 }_337 }_337_337 function hideAddChannelInput() {_337 $newChannelInputRow.addClass('not-showing').removeClass('showing');_337 $channelList.addClass('not-showing').removeClass('showing');_337 $newChannelInput.val('');_337 }_337_337 function addChannel(channel) {_337 if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {_337 tc.generalChannel = channel;_337 }_337 var rowDiv = $('<div>').addClass('row channel-row');_337 rowDiv.loadTemplate('#channel-template', {_337 channelName: channel.friendlyName_337 });_337_337 var channelP = rowDiv.children().children().first();_337_337 rowDiv.on('click', selectChannel);_337 channelP.data('sid', channel.sid);_337 if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {_337 tc.currentChannelContainer = channelP;_337 channelP.addClass('selected-channel');_337 }_337 else {_337 channelP.addClass('unselected-channel')_337 }_337_337 $channelList.append(rowDiv);_337 }_337_337 function deleteCurrentChannel() {_337 if (!tc.currentChannel) {_337 return;_337 }_337 if (tc.currentChannel.sid === tc.generalChannel.sid) {_337 alert('You cannot delete the general channel');_337 return;_337 }_337 tc.currentChannel.delete().then(function (channel) {_337 console.log('channel: ' + channel.friendlyName + ' deleted');_337 setupChannel(tc.generalChannel);_337 });_337 }_337_337 function selectChannel(event) {_337 var target = $(event.target);_337 var channelSid = target.data().sid;_337 var selectedChannel = tc.channelArray.filter(function (channel) {_337 return channel.sid === channelSid;_337 })[0];_337 if (selectedChannel === tc.currentChannel) {_337 return;_337 }_337 setupChannel(selectedChannel);_337 };_337_337 function disconnectClient() {_337 leaveCurrentChannel();_337 $channelList.text('');_337 tc.$messageList.text('');_337 channels = undefined;_337 $statusRow.addClass('disconnected').removeClass('connected');_337 tc.$messageList.addClass('disconnected').removeClass('connected');_337 $connectPanel.addClass('disconnected').removeClass('connected');_337 $inputText.removeClass('with-shadow');_337 $typingRow.addClass('disconnected').removeClass('connected');_337 }_337_337 tc.sortChannelsByName = function (channels) {_337 return channels.sort(function (a, b) {_337 if (a.friendlyName === GENERAL_CHANNEL_NAME) {_337 return -1;_337 }_337 if (b.friendlyName === GENERAL_CHANNEL_NAME) {_337 return 1;_337 }_337 return a.friendlyName.localeCompare(b.friendlyName);_337 });_337 };_337_337 return 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
_337var twiliochat = (function () {_337 var tc = {};_337_337 var GENERAL_CHANNEL_UNIQUE_NAME = 'general';_337 var GENERAL_CHANNEL_NAME = 'General Channel';_337 var MESSAGES_HISTORY_LIMIT = 50;_337_337 var $channelList;_337 var $inputText;_337 var $usernameInput;_337 var $statusRow;_337 var $connectPanel;_337 var $newChannelInputRow;_337 var $newChannelInput;_337 var $typingRow;_337 var $typingPlaceholder;_337_337 $(document).ready(function () {_337 tc.$messageList = $('#message-list');_337 $channelList = $('#channel-list');_337 $inputText = $('#input-text');_337 $usernameInput = $('#username-input');_337 $statusRow = $('#status-row');_337 $connectPanel = $('#connect-panel');_337 $newChannelInputRow = $('#new-channel-input-row');_337 $newChannelInput = $('#new-channel-input');_337 $typingRow = $('#typing-row');_337 $typingPlaceholder = $('#typing-placeholder');_337 $usernameInput.focus();_337 $usernameInput.on('keypress', handleUsernameInputKeypress);_337 $inputText.on('keypress', handleInputTextKeypress);_337 $newChannelInput.on('keypress', tc.handleNewChannelInputKeypress);_337 $('#connect-image').on('click', connectClientWithUsername);_337 $('#add-channel-image').on('click', showAddChannelInput);_337 $('#leave-span').on('click', disconnectClient);_337 $('#delete-channel-span').on('click', deleteCurrentChannel);_337 });_337_337 function handleUsernameInputKeypress(event) {_337 if (event.keyCode === 13) {_337 connectClientWithUsername();_337 }_337 }_337_337 function handleInputTextKeypress(event) {_337 if (event.keyCode === 13) {_337 tc.currentChannel.sendMessage($(this).val());_337 event.preventDefault();_337 $(this).val('');_337 }_337 else {_337 notifyTyping();_337 }_337 }_337_337 var notifyTyping = $.throttle(function () {_337 tc.currentChannel.typing();_337 }, 1000);_337_337 tc.handleNewChannelInputKeypress = function (event) {_337 if (event.keyCode === 13) {_337 tc.messagingClient.createChannel({_337 friendlyName: $newChannelInput.val()_337 }).then(hideAddChannelInput);_337 $(this).val('');_337 event.preventDefault();_337 }_337 };_337_337 function connectClientWithUsername() {_337 var usernameText = $usernameInput.val();_337 $usernameInput.val('');_337 if (usernameText == '') {_337 alert('Username cannot be empty');_337 return;_337 }_337 tc.username = usernameText;_337 fetchAccessToken(tc.username, connectMessagingClient);_337 }_337_337 function fetchAccessToken(username, handler) {_337 $.post('/token', {_337 identity: username,_337 device: 'browser'_337 }, function (data) {_337 handler(data);_337 }, 'json');_337 }_337_337 function connectMessagingClient(tokenResponse) {_337 // Initialize the IP messaging client_337 tc.accessManager = new Twilio.AccessManager(tokenResponse.token);_337 tc.messagingClient = new Twilio.IPMessaging.Client(tc.accessManager);_337 updateConnectedUI();_337 tc.loadChannelList(tc.joinGeneralChannel);_337 tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));_337 tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));_337 tc.messagingClient.on('tokenExpired', refreshToken);_337 }_337_337 function refreshToken() {_337 fetchAccessToken(tc.username, setNewToken);_337 }_337_337 function setNewToken(tokenResponse) {_337 tc.accessManager.updateToken(tokenResponse.token);_337 }_337_337 function updateConnectedUI() {_337 $('#username-span').text(tc.username);_337 $statusRow.addClass('connected').removeClass('disconnected');_337 tc.$messageList.addClass('connected').removeClass('disconnected');_337 $connectPanel.addClass('connected').removeClass('disconnected');_337 $inputText.addClass('with-shadow');_337 $typingRow.addClass('connected').removeClass('disconnected');_337 }_337_337 tc.loadChannelList = function (handler) {_337 if (tc.messagingClient === undefined) {_337 console.log('Client is not initialized');_337 return;_337 }_337_337 tc.messagingClient.getChannels().then(function (channels) {_337 tc.channelArray = tc.sortChannelsByName(channels);_337 $channelList.text('');_337 tc.channelArray.forEach(addChannel);_337 if (typeof handler === 'function') {_337 handler();_337 }_337 });_337 };_337_337 tc.joinGeneralChannel = function () {_337 console.log('Attempting to join "general" chat channel...');_337 if (!tc.generalChannel) {_337 // If it doesn't exist, let's create it_337 tc.messagingClient.createChannel({_337 uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,_337 friendlyName: GENERAL_CHANNEL_NAME_337 }).then(function (channel) {_337 console.log('Created general channel');_337 tc.generalChannel = channel;_337 tc.loadChannelList(tc.joinGeneralChannel);_337 });_337 }_337 else {_337 console.log('Found general channel:');_337 setupChannel(tc.generalChannel);_337 }_337 };_337_337 function setupChannel(channel) {_337 // Join the channel_337 channel.join().then(function (joinedChannel) {_337 console.log('Joined channel ' + joinedChannel.friendlyName);_337 leaveCurrentChannel();_337 updateChannelUI(channel);_337 tc.currentChannel = channel;_337 tc.loadMessages();_337 channel.on('messageAdded', tc.addMessageToList);_337 channel.on('typingStarted', showTypingStarted);_337 channel.on('typingEnded', hideTypingStarted);_337 channel.on('memberJoined', notifyMemberJoined);_337 channel.on('memberLeft', notifyMemberLeft);_337 $inputText.prop('disabled', false).focus();_337 tc.$messageList.text('');_337 });_337 }_337_337 tc.loadMessages = function () {_337 tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT).then(function (messages) {_337 messages.forEach(tc.addMessageToList);_337 });_337 };_337_337 function leaveCurrentChannel() {_337 if (tc.currentChannel) {_337 tc.currentChannel.leave().then(function (leftChannel) {_337 console.log('left ' + leftChannel.friendlyName);_337 leftChannel.removeListener('messageAdded', tc.addMessageToList);_337 leftChannel.removeListener('typingStarted', showTypingStarted);_337 leftChannel.removeListener('typingEnded', hideTypingStarted);_337 leftChannel.removeListener('memberJoined', notifyMemberJoined);_337 leftChannel.removeListener('memberLeft', notifyMemberLeft);_337 });_337 }_337 }_337_337 tc.addMessageToList = function (message) {_337 var rowDiv = $('<div>').addClass('row no-margin');_337 rowDiv.loadTemplate($('#message-template'), {_337 username: message.author,_337 date: dateFormatter.getTodayDate(message.timestamp),_337 body: message.body_337 });_337 if (message.author === tc.username) {_337 rowDiv.addClass('own-message');_337 }_337_337 tc.$messageList.append(rowDiv);_337 scrollToMessageListBottom();_337 };_337_337 function notifyMemberJoined(member) {_337 notify(member.identity + ' joined the channel')_337 }_337_337 function notifyMemberLeft(member) {_337 notify(member.identity + ' left the channel');_337 }_337_337 function notify(message) {_337 var row = $('<div>').addClass('col-md-12');_337 row.loadTemplate('#member-notification-template', {_337 status: message_337 });_337 tc.$messageList.append(row);_337 scrollToMessageListBottom();_337 }_337_337 function showTypingStarted(member) {_337 $typingPlaceholder.text(member.identity + ' is typing...');_337 }_337_337 function hideTypingStarted(member) {_337 $typingPlaceholder.text('');_337 }_337_337 function scrollToMessageListBottom() {_337 tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);_337 }_337_337 function updateChannelUI(selectedChannel) {_337 var channelElements = $('.channel-element').toArray();_337 var channelElement = channelElements.filter(function (element) {_337 return $(element).data().sid === selectedChannel.sid;_337 });_337 channelElement = $(channelElement);_337 if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {_337 tc.currentChannelContainer = channelElement;_337 }_337 tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');_337 channelElement.removeClass('unselected-channel').addClass('selected-channel');_337 tc.currentChannelContainer = channelElement;_337 }_337_337 function showAddChannelInput() {_337 if (tc.messagingClient) {_337 $newChannelInputRow.addClass('showing').removeClass('not-showing');_337 $channelList.addClass('showing').removeClass('not-showing');_337 $newChannelInput.focus();_337 }_337 }_337_337 function hideAddChannelInput() {_337 $newChannelInputRow.addClass('not-showing').removeClass('showing');_337 $channelList.addClass('not-showing').removeClass('showing');_337 $newChannelInput.val('');_337 }_337_337 function addChannel(channel) {_337 if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {_337 tc.generalChannel = channel;_337 }_337 var rowDiv = $('<div>').addClass('row channel-row');_337 rowDiv.loadTemplate('#channel-template', {_337 channelName: channel.friendlyName_337 });_337_337 var channelP = rowDiv.children().children().first();_337_337 rowDiv.on('click', selectChannel);_337 channelP.data('sid', channel.sid);_337 if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {_337 tc.currentChannelContainer = channelP;_337 channelP.addClass('selected-channel');_337 }_337 else {_337 channelP.addClass('unselected-channel')_337 }_337_337 $channelList.append(rowDiv);_337 }_337_337 function deleteCurrentChannel() {_337 if (!tc.currentChannel) {_337 return;_337 }_337 if (tc.currentChannel.sid === tc.generalChannel.sid) {_337 alert('You cannot delete the general channel');_337 return;_337 }_337 tc.currentChannel.delete().then(function (channel) {_337 console.log('channel: ' + channel.friendlyName + ' deleted');_337 setupChannel(tc.generalChannel);_337 });_337 }_337_337 function selectChannel(event) {_337 var target = $(event.target);_337 var channelSid = target.data().sid;_337 var selectedChannel = tc.channelArray.filter(function (channel) {_337 return channel.sid === channelSid;_337 })[0];_337 if (selectedChannel === tc.currentChannel) {_337 return;_337 }_337 setupChannel(selectedChannel);_337 };_337_337 function disconnectClient() {_337 leaveCurrentChannel();_337 $channelList.text('');_337 tc.$messageList.text('');_337 channels = undefined;_337 $statusRow.addClass('disconnected').removeClass('connected');_337 tc.$messageList.addClass('disconnected').removeClass('connected');_337 $connectPanel.addClass('disconnected').removeClass('connected');_337 $inputText.removeClass('with-shadow');_337 $typingRow.addClass('disconnected').removeClass('connected');_337 }_337_337 tc.sortChannelsByName = function (channels) {_337 return channels.sort(function (a, b) {_337 if (a.friendlyName === GENERAL_CHANNEL_NAME) {_337 return -1;_337 }_337 if (b.friendlyName === GENERAL_CHANNEL_NAME) {_337 return 1;_337 }_337 return a.friendlyName.localeCompare(b.friendlyName);_337 });_337 };_337_337 return 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
_337var twiliochat = (function () {_337 var tc = {};_337_337 var GENERAL_CHANNEL_UNIQUE_NAME = 'general';_337 var GENERAL_CHANNEL_NAME = 'General Channel';_337 var MESSAGES_HISTORY_LIMIT = 50;_337_337 var $channelList;_337 var $inputText;_337 var $usernameInput;_337 var $statusRow;_337 var $connectPanel;_337 var $newChannelInputRow;_337 var $newChannelInput;_337 var $typingRow;_337 var $typingPlaceholder;_337_337 $(document).ready(function () {_337 tc.$messageList = $('#message-list');_337 $channelList = $('#channel-list');_337 $inputText = $('#input-text');_337 $usernameInput = $('#username-input');_337 $statusRow = $('#status-row');_337 $connectPanel = $('#connect-panel');_337 $newChannelInputRow = $('#new-channel-input-row');_337 $newChannelInput = $('#new-channel-input');_337 $typingRow = $('#typing-row');_337 $typingPlaceholder = $('#typing-placeholder');_337 $usernameInput.focus();_337 $usernameInput.on('keypress', handleUsernameInputKeypress);_337 $inputText.on('keypress', handleInputTextKeypress);_337 $newChannelInput.on('keypress', tc.handleNewChannelInputKeypress);_337 $('#connect-image').on('click', connectClientWithUsername);_337 $('#add-channel-image').on('click', showAddChannelInput);_337 $('#leave-span').on('click', disconnectClient);_337 $('#delete-channel-span').on('click', deleteCurrentChannel);_337 });_337_337 function handleUsernameInputKeypress(event) {_337 if (event.keyCode === 13) {_337 connectClientWithUsername();_337 }_337 }_337_337 function handleInputTextKeypress(event) {_337 if (event.keyCode === 13) {_337 tc.currentChannel.sendMessage($(this).val());_337 event.preventDefault();_337 $(this).val('');_337 }_337 else {_337 notifyTyping();_337 }_337 }_337_337 var notifyTyping = $.throttle(function () {_337 tc.currentChannel.typing();_337 }, 1000);_337_337 tc.handleNewChannelInputKeypress = function (event) {_337 if (event.keyCode === 13) {_337 tc.messagingClient.createChannel({_337 friendlyName: $newChannelInput.val()_337 }).then(hideAddChannelInput);_337 $(this).val('');_337 event.preventDefault();_337 }_337 };_337_337 function connectClientWithUsername() {_337 var usernameText = $usernameInput.val();_337 $usernameInput.val('');_337 if (usernameText == '') {_337 alert('Username cannot be empty');_337 return;_337 }_337 tc.username = usernameText;_337 fetchAccessToken(tc.username, connectMessagingClient);_337 }_337_337 function fetchAccessToken(username, handler) {_337 $.post('/token', {_337 identity: username,_337 device: 'browser'_337 }, function (data) {_337 handler(data);_337 }, 'json');_337 }_337_337 function connectMessagingClient(tokenResponse) {_337 // Initialize the IP messaging client_337 tc.accessManager = new Twilio.AccessManager(tokenResponse.token);_337 tc.messagingClient = new Twilio.IPMessaging.Client(tc.accessManager);_337 updateConnectedUI();_337 tc.loadChannelList(tc.joinGeneralChannel);_337 tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));_337 tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));_337 tc.messagingClient.on('tokenExpired', refreshToken);_337 }_337_337 function refreshToken() {_337 fetchAccessToken(tc.username, setNewToken);_337 }_337_337 function setNewToken(tokenResponse) {_337 tc.accessManager.updateToken(tokenResponse.token);_337 }_337_337 function updateConnectedUI() {_337 $('#username-span').text(tc.username);_337 $statusRow.addClass('connected').removeClass('disconnected');_337 tc.$messageList.addClass('connected').removeClass('disconnected');_337 $connectPanel.addClass('connected').removeClass('disconnected');_337 $inputText.addClass('with-shadow');_337 $typingRow.addClass('connected').removeClass('disconnected');_337 }_337_337 tc.loadChannelList = function (handler) {_337 if (tc.messagingClient === undefined) {_337 console.log('Client is not initialized');_337 return;_337 }_337_337 tc.messagingClient.getChannels().then(function (channels) {_337 tc.channelArray = tc.sortChannelsByName(channels);_337 $channelList.text('');_337 tc.channelArray.forEach(addChannel);_337 if (typeof handler === 'function') {_337 handler();_337 }_337 });_337 };_337_337 tc.joinGeneralChannel = function () {_337 console.log('Attempting to join "general" chat channel...');_337 if (!tc.generalChannel) {_337 // If it doesn't exist, let's create it_337 tc.messagingClient.createChannel({_337 uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,_337 friendlyName: GENERAL_CHANNEL_NAME_337 }).then(function (channel) {_337 console.log('Created general channel');_337 tc.generalChannel = channel;_337 tc.loadChannelList(tc.joinGeneralChannel);_337 });_337 }_337 else {_337 console.log('Found general channel:');_337 setupChannel(tc.generalChannel);_337 }_337 };_337_337 function setupChannel(channel) {_337 // Join the channel_337 channel.join().then(function (joinedChannel) {_337 console.log('Joined channel ' + joinedChannel.friendlyName);_337 leaveCurrentChannel();_337 updateChannelUI(channel);_337 tc.currentChannel = channel;_337 tc.loadMessages();_337 channel.on('messageAdded', tc.addMessageToList);_337 channel.on('typingStarted', showTypingStarted);_337 channel.on('typingEnded', hideTypingStarted);_337 channel.on('memberJoined', notifyMemberJoined);_337 channel.on('memberLeft', notifyMemberLeft);_337 $inputText.prop('disabled', false).focus();_337 tc.$messageList.text('');_337 });_337 }_337_337 tc.loadMessages = function () {_337 tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT).then(function (messages) {_337 messages.forEach(tc.addMessageToList);_337 });_337 };_337_337 function leaveCurrentChannel() {_337 if (tc.currentChannel) {_337 tc.currentChannel.leave().then(function (leftChannel) {_337 console.log('left ' + leftChannel.friendlyName);_337 leftChannel.removeListener('messageAdded', tc.addMessageToList);_337 leftChannel.removeListener('typingStarted', showTypingStarted);_337 leftChannel.removeListener('typingEnded', hideTypingStarted);_337 leftChannel.removeListener('memberJoined', notifyMemberJoined);_337 leftChannel.removeListener('memberLeft', notifyMemberLeft);_337 });_337 }_337 }_337_337 tc.addMessageToList = function (message) {_337 var rowDiv = $('<div>').addClass('row no-margin');_337 rowDiv.loadTemplate($('#message-template'), {_337 username: message.author,_337 date: dateFormatter.getTodayDate(message.timestamp),_337 body: message.body_337 });_337 if (message.author === tc.username) {_337 rowDiv.addClass('own-message');_337 }_337_337 tc.$messageList.append(rowDiv);_337 scrollToMessageListBottom();_337 };_337_337 function notifyMemberJoined(member) {_337 notify(member.identity + ' joined the channel')_337 }_337_337 function notifyMemberLeft(member) {_337 notify(member.identity + ' left the channel');_337 }_337_337 function notify(message) {_337 var row = $('<div>').addClass('col-md-12');_337 row.loadTemplate('#member-notification-template', {_337 status: message_337 });_337 tc.$messageList.append(row);_337 scrollToMessageListBottom();_337 }_337_337 function showTypingStarted(member) {_337 $typingPlaceholder.text(member.identity + ' is typing...');_337 }_337_337 function hideTypingStarted(member) {_337 $typingPlaceholder.text('');_337 }_337_337 function scrollToMessageListBottom() {_337 tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);_337 }_337_337 function updateChannelUI(selectedChannel) {_337 var channelElements = $('.channel-element').toArray();_337 var channelElement = channelElements.filter(function (element) {_337 return $(element).data().sid === selectedChannel.sid;_337 });_337 channelElement = $(channelElement);_337 if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {_337 tc.currentChannelContainer = channelElement;_337 }_337 tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');_337 channelElement.removeClass('unselected-channel').addClass('selected-channel');_337 tc.currentChannelContainer = channelElement;_337 }_337_337 function showAddChannelInput() {_337 if (tc.messagingClient) {_337 $newChannelInputRow.addClass('showing').removeClass('not-showing');_337 $channelList.addClass('showing').removeClass('not-showing');_337 $newChannelInput.focus();_337 }_337 }_337_337 function hideAddChannelInput() {_337 $newChannelInputRow.addClass('not-showing').removeClass('showing');_337 $channelList.addClass('not-showing').removeClass('showing');_337 $newChannelInput.val('');_337 }_337_337 function addChannel(channel) {_337 if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {_337 tc.generalChannel = channel;_337 }_337 var rowDiv = $('<div>').addClass('row channel-row');_337 rowDiv.loadTemplate('#channel-template', {_337 channelName: channel.friendlyName_337 });_337_337 var channelP = rowDiv.children().children().first();_337_337 rowDiv.on('click', selectChannel);_337 channelP.data('sid', channel.sid);_337 if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {_337 tc.currentChannelContainer = channelP;_337 channelP.addClass('selected-channel');_337 }_337 else {_337 channelP.addClass('unselected-channel')_337 }_337_337 $channelList.append(rowDiv);_337 }_337_337 function deleteCurrentChannel() {_337 if (!tc.currentChannel) {_337 return;_337 }_337 if (tc.currentChannel.sid === tc.generalChannel.sid) {_337 alert('You cannot delete the general channel');_337 return;_337 }_337 tc.currentChannel.delete().then(function (channel) {_337 console.log('channel: ' + channel.friendlyName + ' deleted');_337 setupChannel(tc.generalChannel);_337 });_337 }_337_337 function selectChannel(event) {_337 var target = $(event.target);_337 var channelSid = target.data().sid;_337 var selectedChannel = tc.channelArray.filter(function (channel) {_337 return channel.sid === channelSid;_337 })[0];_337 if (selectedChannel === tc.currentChannel) {_337 return;_337 }_337 setupChannel(selectedChannel);_337 };_337_337 function disconnectClient() {_337 leaveCurrentChannel();_337 $channelList.text('');_337 tc.$messageList.text('');_337 channels = undefined;_337 $statusRow.addClass('disconnected').removeClass('connected');_337 tc.$messageList.addClass('disconnected').removeClass('connected');_337 $connectPanel.addClass('disconnected').removeClass('connected');_337 $inputText.removeClass('with-shadow');_337 $typingRow.addClass('disconnected').removeClass('connected');_337 }_337_337 tc.sortChannelsByName = function (channels) {_337 return channels.sort(function (a, b) {_337 if (a.friendlyName === GENERAL_CHANNEL_NAME) {_337 return -1;_337 }_337 if (b.friendlyName === GENERAL_CHANNEL_NAME) {_337 return 1;_337 }_337 return a.friendlyName.localeCompare(b.friendlyName);_337 });_337 };_337_337 return 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
_337var twiliochat = (function () {_337 var tc = {};_337_337 var GENERAL_CHANNEL_UNIQUE_NAME = 'general';_337 var GENERAL_CHANNEL_NAME = 'General Channel';_337 var MESSAGES_HISTORY_LIMIT = 50;_337_337 var $channelList;_337 var $inputText;_337 var $usernameInput;_337 var $statusRow;_337 var $connectPanel;_337 var $newChannelInputRow;_337 var $newChannelInput;_337 var $typingRow;_337 var $typingPlaceholder;_337_337 $(document).ready(function () {_337 tc.$messageList = $('#message-list');_337 $channelList = $('#channel-list');_337 $inputText = $('#input-text');_337 $usernameInput = $('#username-input');_337 $statusRow = $('#status-row');_337 $connectPanel = $('#connect-panel');_337 $newChannelInputRow = $('#new-channel-input-row');_337 $newChannelInput = $('#new-channel-input');_337 $typingRow = $('#typing-row');_337 $typingPlaceholder = $('#typing-placeholder');_337 $usernameInput.focus();_337 $usernameInput.on('keypress', handleUsernameInputKeypress);_337 $inputText.on('keypress', handleInputTextKeypress);_337 $newChannelInput.on('keypress', tc.handleNewChannelInputKeypress);_337 $('#connect-image').on('click', connectClientWithUsername);_337 $('#add-channel-image').on('click', showAddChannelInput);_337 $('#leave-span').on('click', disconnectClient);_337 $('#delete-channel-span').on('click', deleteCurrentChannel);_337 });_337_337 function handleUsernameInputKeypress(event) {_337 if (event.keyCode === 13) {_337 connectClientWithUsername();_337 }_337 }_337_337 function handleInputTextKeypress(event) {_337 if (event.keyCode === 13) {_337 tc.currentChannel.sendMessage($(this).val());_337 event.preventDefault();_337 $(this).val('');_337 }_337 else {_337 notifyTyping();_337 }_337 }_337_337 var notifyTyping = $.throttle(function () {_337 tc.currentChannel.typing();_337 }, 1000);_337_337 tc.handleNewChannelInputKeypress = function (event) {_337 if (event.keyCode === 13) {_337 tc.messagingClient.createChannel({_337 friendlyName: $newChannelInput.val()_337 }).then(hideAddChannelInput);_337 $(this).val('');_337 event.preventDefault();_337 }_337 };_337_337 function connectClientWithUsername() {_337 var usernameText = $usernameInput.val();_337 $usernameInput.val('');_337 if (usernameText == '') {_337 alert('Username cannot be empty');_337 return;_337 }_337 tc.username = usernameText;_337 fetchAccessToken(tc.username, connectMessagingClient);_337 }_337_337 function fetchAccessToken(username, handler) {_337 $.post('/token', {_337 identity: username,_337 device: 'browser'_337 }, function (data) {_337 handler(data);_337 }, 'json');_337 }_337_337 function connectMessagingClient(tokenResponse) {_337 // Initialize the IP messaging client_337 tc.accessManager = new Twilio.AccessManager(tokenResponse.token);_337 tc.messagingClient = new Twilio.IPMessaging.Client(tc.accessManager);_337 updateConnectedUI();_337 tc.loadChannelList(tc.joinGeneralChannel);_337 tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));_337 tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));_337 tc.messagingClient.on('tokenExpired', refreshToken);_337 }_337_337 function refreshToken() {_337 fetchAccessToken(tc.username, setNewToken);_337 }_337_337 function setNewToken(tokenResponse) {_337 tc.accessManager.updateToken(tokenResponse.token);_337 }_337_337 function updateConnectedUI() {_337 $('#username-span').text(tc.username);_337 $statusRow.addClass('connected').removeClass('disconnected');_337 tc.$messageList.addClass('connected').removeClass('disconnected');_337 $connectPanel.addClass('connected').removeClass('disconnected');_337 $inputText.addClass('with-shadow');_337 $typingRow.addClass('connected').removeClass('disconnected');_337 }_337_337 tc.loadChannelList = function (handler) {_337 if (tc.messagingClient === undefined) {_337 console.log('Client is not initialized');_337 return;_337 }_337_337 tc.messagingClient.getChannels().then(function (channels) {_337 tc.channelArray = tc.sortChannelsByName(channels);_337 $channelList.text('');_337 tc.channelArray.forEach(addChannel);_337 if (typeof handler === 'function') {_337 handler();_337 }_337 });_337 };_337_337 tc.joinGeneralChannel = function () {_337 console.log('Attempting to join "general" chat channel...');_337 if (!tc.generalChannel) {_337 // If it doesn't exist, let's create it_337 tc.messagingClient.createChannel({_337 uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,_337 friendlyName: GENERAL_CHANNEL_NAME_337 }).then(function (channel) {_337 console.log('Created general channel');_337 tc.generalChannel = channel;_337 tc.loadChannelList(tc.joinGeneralChannel);_337 });_337 }_337 else {_337 console.log('Found general channel:');_337 setupChannel(tc.generalChannel);_337 }_337 };_337_337 function setupChannel(channel) {_337 // Join the channel_337 channel.join().then(function (joinedChannel) {_337 console.log('Joined channel ' + joinedChannel.friendlyName);_337 leaveCurrentChannel();_337 updateChannelUI(channel);_337 tc.currentChannel = channel;_337 tc.loadMessages();_337 channel.on('messageAdded', tc.addMessageToList);_337 channel.on('typingStarted', showTypingStarted);_337 channel.on('typingEnded', hideTypingStarted);_337 channel.on('memberJoined', notifyMemberJoined);_337 channel.on('memberLeft', notifyMemberLeft);_337 $inputText.prop('disabled', false).focus();_337 tc.$messageList.text('');_337 });_337 }_337_337 tc.loadMessages = function () {_337 tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT).then(function (messages) {_337 messages.forEach(tc.addMessageToList);_337 });_337 };_337_337 function leaveCurrentChannel() {_337 if (tc.currentChannel) {_337 tc.currentChannel.leave().then(function (leftChannel) {_337 console.log('left ' + leftChannel.friendlyName);_337 leftChannel.removeListener('messageAdded', tc.addMessageToList);_337 leftChannel.removeListener('typingStarted', showTypingStarted);_337 leftChannel.removeListener('typingEnded', hideTypingStarted);_337 leftChannel.removeListener('memberJoined', notifyMemberJoined);_337 leftChannel.removeListener('memberLeft', notifyMemberLeft);_337 });_337 }_337 }_337_337 tc.addMessageToList = function (message) {_337 var rowDiv = $('<div>').addClass('row no-margin');_337 rowDiv.loadTemplate($('#message-template'), {_337 username: message.author,_337 date: dateFormatter.getTodayDate(message.timestamp),_337 body: message.body_337 });_337 if (message.author === tc.username) {_337 rowDiv.addClass('own-message');_337 }_337_337 tc.$messageList.append(rowDiv);_337 scrollToMessageListBottom();_337 };_337_337 function notifyMemberJoined(member) {_337 notify(member.identity + ' joined the channel')_337 }_337_337 function notifyMemberLeft(member) {_337 notify(member.identity + ' left the channel');_337 }_337_337 function notify(message) {_337 var row = $('<div>').addClass('col-md-12');_337 row.loadTemplate('#member-notification-template', {_337 status: message_337 });_337 tc.$messageList.append(row);_337 scrollToMessageListBottom();_337 }_337_337 function showTypingStarted(member) {_337 $typingPlaceholder.text(member.identity + ' is typing...');_337 }_337_337 function hideTypingStarted(member) {_337 $typingPlaceholder.text('');_337 }_337_337 function scrollToMessageListBottom() {_337 tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);_337 }_337_337 function updateChannelUI(selectedChannel) {_337 var channelElements = $('.channel-element').toArray();_337 var channelElement = channelElements.filter(function (element) {_337 return $(element).data().sid === selectedChannel.sid;_337 });_337 channelElement = $(channelElement);_337 if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {_337 tc.currentChannelContainer = channelElement;_337 }_337 tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');_337 channelElement.removeClass('unselected-channel').addClass('selected-channel');_337 tc.currentChannelContainer = channelElement;_337 }_337_337 function showAddChannelInput() {_337 if (tc.messagingClient) {_337 $newChannelInputRow.addClass('showing').removeClass('not-showing');_337 $channelList.addClass('showing').removeClass('not-showing');_337 $newChannelInput.focus();_337 }_337 }_337_337 function hideAddChannelInput() {_337 $newChannelInputRow.addClass('not-showing').removeClass('showing');_337 $channelList.addClass('not-showing').removeClass('showing');_337 $newChannelInput.val('');_337 }_337_337 function addChannel(channel) {_337 if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {_337 tc.generalChannel = channel;_337 }_337 var rowDiv = $('<div>').addClass('row channel-row');_337 rowDiv.loadTemplate('#channel-template', {_337 channelName: channel.friendlyName_337 });_337_337 var channelP = rowDiv.children().children().first();_337_337 rowDiv.on('click', selectChannel);_337 channelP.data('sid', channel.sid);_337 if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {_337 tc.currentChannelContainer = channelP;_337 channelP.addClass('selected-channel');_337 }_337 else {_337 channelP.addClass('unselected-channel')_337 }_337_337 $channelList.append(rowDiv);_337 }_337_337 function deleteCurrentChannel() {_337 if (!tc.currentChannel) {_337 return;_337 }_337 if (tc.currentChannel.sid === tc.generalChannel.sid) {_337 alert('You cannot delete the general channel');_337 return;_337 }_337 tc.currentChannel.delete().then(function (channel) {_337 console.log('channel: ' + channel.friendlyName + ' deleted');_337 setupChannel(tc.generalChannel);_337 });_337 }_337_337 function selectChannel(event) {_337 var target = $(event.target);_337 var channelSid = target.data().sid;_337 var selectedChannel = tc.channelArray.filter(function (channel) {_337 return channel.sid === channelSid;_337 })[0];_337 if (selectedChannel === tc.currentChannel) {_337 return;_337 }_337 setupChannel(selectedChannel);_337 };_337_337 function disconnectClient() {_337 leaveCurrentChannel();_337 $channelList.text('');_337 tc.$messageList.text('');_337 channels = undefined;_337 $statusRow.addClass('disconnected').removeClass('connected');_337 tc.$messageList.addClass('disconnected').removeClass('connected');_337 $connectPanel.addClass('disconnected').removeClass('connected');_337 $inputText.removeClass('with-shadow');_337 $typingRow.addClass('disconnected').removeClass('connected');_337 }_337_337 tc.sortChannelsByName = function (channels) {_337 return channels.sort(function (a, b) {_337 if (a.friendlyName === GENERAL_CHANNEL_NAME) {_337 return -1;_337 }_337 if (b.friendlyName === GENERAL_CHANNEL_NAME) {_337 return 1;_337 }_337 return a.friendlyName.localeCompare(b.friendlyName);_337 });_337 };_337_337 return 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 is as simple as 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
_337var twiliochat = (function () {_337 var tc = {};_337_337 var GENERAL_CHANNEL_UNIQUE_NAME = 'general';_337 var GENERAL_CHANNEL_NAME = 'General Channel';_337 var MESSAGES_HISTORY_LIMIT = 50;_337_337 var $channelList;_337 var $inputText;_337 var $usernameInput;_337 var $statusRow;_337 var $connectPanel;_337 var $newChannelInputRow;_337 var $newChannelInput;_337 var $typingRow;_337 var $typingPlaceholder;_337_337 $(document).ready(function () {_337 tc.$messageList = $('#message-list');_337 $channelList = $('#channel-list');_337 $inputText = $('#input-text');_337 $usernameInput = $('#username-input');_337 $statusRow = $('#status-row');_337 $connectPanel = $('#connect-panel');_337 $newChannelInputRow = $('#new-channel-input-row');_337 $newChannelInput = $('#new-channel-input');_337 $typingRow = $('#typing-row');_337 $typingPlaceholder = $('#typing-placeholder');_337 $usernameInput.focus();_337 $usernameInput.on('keypress', handleUsernameInputKeypress);_337 $inputText.on('keypress', handleInputTextKeypress);_337 $newChannelInput.on('keypress', tc.handleNewChannelInputKeypress);_337 $('#connect-image').on('click', connectClientWithUsername);_337 $('#add-channel-image').on('click', showAddChannelInput);_337 $('#leave-span').on('click', disconnectClient);_337 $('#delete-channel-span').on('click', deleteCurrentChannel);_337 });_337_337 function handleUsernameInputKeypress(event) {_337 if (event.keyCode === 13) {_337 connectClientWithUsername();_337 }_337 }_337_337 function handleInputTextKeypress(event) {_337 if (event.keyCode === 13) {_337 tc.currentChannel.sendMessage($(this).val());_337 event.preventDefault();_337 $(this).val('');_337 }_337 else {_337 notifyTyping();_337 }_337 }_337_337 var notifyTyping = $.throttle(function () {_337 tc.currentChannel.typing();_337 }, 1000);_337_337 tc.handleNewChannelInputKeypress = function (event) {_337 if (event.keyCode === 13) {_337 tc.messagingClient.createChannel({_337 friendlyName: $newChannelInput.val()_337 }).then(hideAddChannelInput);_337 $(this).val('');_337 event.preventDefault();_337 }_337 };_337_337 function connectClientWithUsername() {_337 var usernameText = $usernameInput.val();_337 $usernameInput.val('');_337 if (usernameText == '') {_337 alert('Username cannot be empty');_337 return;_337 }_337 tc.username = usernameText;_337 fetchAccessToken(tc.username, connectMessagingClient);_337 }_337_337 function fetchAccessToken(username, handler) {_337 $.post('/token', {_337 identity: username,_337 device: 'browser'_337 }, function (data) {_337 handler(data);_337 }, 'json');_337 }_337_337 function connectMessagingClient(tokenResponse) {_337 // Initialize the IP messaging client_337 tc.accessManager = new Twilio.AccessManager(tokenResponse.token);_337 tc.messagingClient = new Twilio.IPMessaging.Client(tc.accessManager);_337 updateConnectedUI();_337 tc.loadChannelList(tc.joinGeneralChannel);_337 tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));_337 tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));_337 tc.messagingClient.on('tokenExpired', refreshToken);_337 }_337_337 function refreshToken() {_337 fetchAccessToken(tc.username, setNewToken);_337 }_337_337 function setNewToken(tokenResponse) {_337 tc.accessManager.updateToken(tokenResponse.token);_337 }_337_337 function updateConnectedUI() {_337 $('#username-span').text(tc.username);_337 $statusRow.addClass('connected').removeClass('disconnected');_337 tc.$messageList.addClass('connected').removeClass('disconnected');_337 $connectPanel.addClass('connected').removeClass('disconnected');_337 $inputText.addClass('with-shadow');_337 $typingRow.addClass('connected').removeClass('disconnected');_337 }_337_337 tc.loadChannelList = function (handler) {_337 if (tc.messagingClient === undefined) {_337 console.log('Client is not initialized');_337 return;_337 }_337_337 tc.messagingClient.getChannels().then(function (channels) {_337 tc.channelArray = tc.sortChannelsByName(channels);_337 $channelList.text('');_337 tc.channelArray.forEach(addChannel);_337 if (typeof handler === 'function') {_337 handler();_337 }_337 });_337 };_337_337 tc.joinGeneralChannel = function () {_337 console.log('Attempting to join "general" chat channel...');_337 if (!tc.generalChannel) {_337 // If it doesn't exist, let's create it_337 tc.messagingClient.createChannel({_337 uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,_337 friendlyName: GENERAL_CHANNEL_NAME_337 }).then(function (channel) {_337 console.log('Created general channel');_337 tc.generalChannel = channel;_337 tc.loadChannelList(tc.joinGeneralChannel);_337 });_337 }_337 else {_337 console.log('Found general channel:');_337 setupChannel(tc.generalChannel);_337 }_337 };_337_337 function setupChannel(channel) {_337 // Join the channel_337 channel.join().then(function (joinedChannel) {_337 console.log('Joined channel ' + joinedChannel.friendlyName);_337 leaveCurrentChannel();_337 updateChannelUI(channel);_337 tc.currentChannel = channel;_337 tc.loadMessages();_337 channel.on('messageAdded', tc.addMessageToList);_337 channel.on('typingStarted', showTypingStarted);_337 channel.on('typingEnded', hideTypingStarted);_337 channel.on('memberJoined', notifyMemberJoined);_337 channel.on('memberLeft', notifyMemberLeft);_337 $inputText.prop('disabled', false).focus();_337 tc.$messageList.text('');_337 });_337 }_337_337 tc.loadMessages = function () {_337 tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT).then(function (messages) {_337 messages.forEach(tc.addMessageToList);_337 });_337 };_337_337 function leaveCurrentChannel() {_337 if (tc.currentChannel) {_337 tc.currentChannel.leave().then(function (leftChannel) {_337 console.log('left ' + leftChannel.friendlyName);_337 leftChannel.removeListener('messageAdded', tc.addMessageToList);_337 leftChannel.removeListener('typingStarted', showTypingStarted);_337 leftChannel.removeListener('typingEnded', hideTypingStarted);_337 leftChannel.removeListener('memberJoined', notifyMemberJoined);_337 leftChannel.removeListener('memberLeft', notifyMemberLeft);_337 });_337 }_337 }_337_337 tc.addMessageToList = function (message) {_337 var rowDiv = $('<div>').addClass('row no-margin');_337 rowDiv.loadTemplate($('#message-template'), {_337 username: message.author,_337 date: dateFormatter.getTodayDate(message.timestamp),_337 body: message.body_337 });_337 if (message.author === tc.username) {_337 rowDiv.addClass('own-message');_337 }_337_337 tc.$messageList.append(rowDiv);_337 scrollToMessageListBottom();_337 };_337_337 function notifyMemberJoined(member) {_337 notify(member.identity + ' joined the channel')_337 }_337_337 function notifyMemberLeft(member) {_337 notify(member.identity + ' left the channel');_337 }_337_337 function notify(message) {_337 var row = $('<div>').addClass('col-md-12');_337 row.loadTemplate('#member-notification-template', {_337 status: message_337 });_337 tc.$messageList.append(row);_337 scrollToMessageListBottom();_337 }_337_337 function showTypingStarted(member) {_337 $typingPlaceholder.text(member.identity + ' is typing...');_337 }_337_337 function hideTypingStarted(member) {_337 $typingPlaceholder.text('');_337 }_337_337 function scrollToMessageListBottom() {_337 tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);_337 }_337_337 function updateChannelUI(selectedChannel) {_337 var channelElements = $('.channel-element').toArray();_337 var channelElement = channelElements.filter(function (element) {_337 return $(element).data().sid === selectedChannel.sid;_337 });_337 channelElement = $(channelElement);_337 if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {_337 tc.currentChannelContainer = channelElement;_337 }_337 tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');_337 channelElement.removeClass('unselected-channel').addClass('selected-channel');_337 tc.currentChannelContainer = channelElement;_337 }_337_337 function showAddChannelInput() {_337 if (tc.messagingClient) {_337 $newChannelInputRow.addClass('showing').removeClass('not-showing');_337 $channelList.addClass('showing').removeClass('not-showing');_337 $newChannelInput.focus();_337 }_337 }_337_337 function hideAddChannelInput() {_337 $newChannelInputRow.addClass('not-showing').removeClass('showing');_337 $channelList.addClass('not-showing').removeClass('showing');_337 $newChannelInput.val('');_337 }_337_337 function addChannel(channel) {_337 if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {_337 tc.generalChannel = channel;_337 }_337 var rowDiv = $('<div>').addClass('row channel-row');_337 rowDiv.loadTemplate('#channel-template', {_337 channelName: channel.friendlyName_337 });_337_337 var channelP = rowDiv.children().children().first();_337_337 rowDiv.on('click', selectChannel);_337 channelP.data('sid', channel.sid);_337 if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {_337 tc.currentChannelContainer = channelP;_337 channelP.addClass('selected-channel');_337 }_337 else {_337 channelP.addClass('unselected-channel')_337 }_337_337 $channelList.append(rowDiv);_337 }_337_337 function deleteCurrentChannel() {_337 if (!tc.currentChannel) {_337 return;_337 }_337 if (tc.currentChannel.sid === tc.generalChannel.sid) {_337 alert('You cannot delete the general channel');_337 return;_337 }_337 tc.currentChannel.delete().then(function (channel) {_337 console.log('channel: ' + channel.friendlyName + ' deleted');_337 setupChannel(tc.generalChannel);_337 });_337 }_337_337 function selectChannel(event) {_337 var target = $(event.target);_337 var channelSid = target.data().sid;_337 var selectedChannel = tc.channelArray.filter(function (channel) {_337 return channel.sid === channelSid;_337 })[0];_337 if (selectedChannel === tc.currentChannel) {_337 return;_337 }_337 setupChannel(selectedChannel);_337 };_337_337 function disconnectClient() {_337 leaveCurrentChannel();_337 $channelList.text('');_337 tc.$messageList.text('');_337 channels = undefined;_337 $statusRow.addClass('disconnected').removeClass('connected');_337 tc.$messageList.addClass('disconnected').removeClass('connected');_337 $connectPanel.addClass('disconnected').removeClass('connected');_337 $inputText.removeClass('with-shadow');_337 $typingRow.addClass('disconnected').removeClass('connected');_337 }_337_337 tc.sortChannelsByName = function (channels) {_337 return channels.sort(function (a, b) {_337 if (a.friendlyName === GENERAL_CHANNEL_NAME) {_337 return -1;_337 }_337 if (b.friendlyName === GENERAL_CHANNEL_NAME) {_337 return 1;_337 }_337 return a.friendlyName.localeCompare(b.friendlyName);_337 });_337 };_337_337 return tc;_337})();
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
.
TwilioChat.Web/Scripts/twiliochat.js
_337var twiliochat = (function () {_337 var tc = {};_337_337 var GENERAL_CHANNEL_UNIQUE_NAME = 'general';_337 var GENERAL_CHANNEL_NAME = 'General Channel';_337 var MESSAGES_HISTORY_LIMIT = 50;_337_337 var $channelList;_337 var $inputText;_337 var $usernameInput;_337 var $statusRow;_337 var $connectPanel;_337 var $newChannelInputRow;_337 var $newChannelInput;_337 var $typingRow;_337 var $typingPlaceholder;_337_337 $(document).ready(function () {_337 tc.$messageList = $('#message-list');_337 $channelList = $('#channel-list');_337 $inputText = $('#input-text');_337 $usernameInput = $('#username-input');_337 $statusRow = $('#status-row');_337 $connectPanel = $('#connect-panel');_337 $newChannelInputRow = $('#new-channel-input-row');_337 $newChannelInput = $('#new-channel-input');_337 $typingRow = $('#typing-row');_337 $typingPlaceholder = $('#typing-placeholder');_337 $usernameInput.focus();_337 $usernameInput.on('keypress', handleUsernameInputKeypress);_337 $inputText.on('keypress', handleInputTextKeypress);_337 $newChannelInput.on('keypress', tc.handleNewChannelInputKeypress);_337 $('#connect-image').on('click', connectClientWithUsername);_337 $('#add-channel-image').on('click', showAddChannelInput);_337 $('#leave-span').on('click', disconnectClient);_337 $('#delete-channel-span').on('click', deleteCurrentChannel);_337 });_337_337 function handleUsernameInputKeypress(event) {_337 if (event.keyCode === 13) {_337 connectClientWithUsername();_337 }_337 }_337_337 function handleInputTextKeypress(event) {_337 if (event.keyCode === 13) {_337 tc.currentChannel.sendMessage($(this).val());_337 event.preventDefault();_337 $(this).val('');_337 }_337 else {_337 notifyTyping();_337 }_337 }_337_337 var notifyTyping = $.throttle(function () {_337 tc.currentChannel.typing();_337 }, 1000);_337_337 tc.handleNewChannelInputKeypress = function (event) {_337 if (event.keyCode === 13) {_337 tc.messagingClient.createChannel({_337 friendlyName: $newChannelInput.val()_337 }).then(hideAddChannelInput);_337 $(this).val('');_337 event.preventDefault();_337 }_337 };_337_337 function connectClientWithUsername() {_337 var usernameText = $usernameInput.val();_337 $usernameInput.val('');_337 if (usernameText == '') {_337 alert('Username cannot be empty');_337 return;_337 }_337 tc.username = usernameText;_337 fetchAccessToken(tc.username, connectMessagingClient);_337 }_337_337 function fetchAccessToken(username, handler) {_337 $.post('/token', {_337 identity: username,_337 device: 'browser'_337 }, function (data) {_337 handler(data);_337 }, 'json');_337 }_337_337 function connectMessagingClient(tokenResponse) {_337 // Initialize the IP messaging client_337 tc.accessManager = new Twilio.AccessManager(tokenResponse.token);_337 tc.messagingClient = new Twilio.IPMessaging.Client(tc.accessManager);_337 updateConnectedUI();_337 tc.loadChannelList(tc.joinGeneralChannel);_337 tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));_337 tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));_337 tc.messagingClient.on('tokenExpired', refreshToken);_337 }_337_337 function refreshToken() {_337 fetchAccessToken(tc.username, setNewToken);_337 }_337_337 function setNewToken(tokenResponse) {_337 tc.accessManager.updateToken(tokenResponse.token);_337 }_337_337 function updateConnectedUI() {_337 $('#username-span').text(tc.username);_337 $statusRow.addClass('connected').removeClass('disconnected');_337 tc.$messageList.addClass('connected').removeClass('disconnected');_337 $connectPanel.addClass('connected').removeClass('disconnected');_337 $inputText.addClass('with-shadow');_337 $typingRow.addClass('connected').removeClass('disconnected');_337 }_337_337 tc.loadChannelList = function (handler) {_337 if (tc.messagingClient === undefined) {_337 console.log('Client is not initialized');_337 return;_337 }_337_337 tc.messagingClient.getChannels().then(function (channels) {_337 tc.channelArray = tc.sortChannelsByName(channels);_337 $channelList.text('');_337 tc.channelArray.forEach(addChannel);_337 if (typeof handler === 'function') {_337 handler();_337 }_337 });_337 };_337_337 tc.joinGeneralChannel = function () {_337 console.log('Attempting to join "general" chat channel...');_337 if (!tc.generalChannel) {_337 // If it doesn't exist, let's create it_337 tc.messagingClient.createChannel({_337 uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,_337 friendlyName: GENERAL_CHANNEL_NAME_337 }).then(function (channel) {_337 console.log('Created general channel');_337 tc.generalChannel = channel;_337 tc.loadChannelList(tc.joinGeneralChannel);_337 });_337 }_337 else {_337 console.log('Found general channel:');_337 setupChannel(tc.generalChannel);_337 }_337 };_337_337 function setupChannel(channel) {_337 // Join the channel_337 channel.join().then(function (joinedChannel) {_337 console.log('Joined channel ' + joinedChannel.friendlyName);_337 leaveCurrentChannel();_337 updateChannelUI(channel);_337 tc.currentChannel = channel;_337 tc.loadMessages();_337 channel.on('messageAdded', tc.addMessageToList);_337 channel.on('typingStarted', showTypingStarted);_337 channel.on('typingEnded', hideTypingStarted);_337 channel.on('memberJoined', notifyMemberJoined);_337 channel.on('memberLeft', notifyMemberLeft);_337 $inputText.prop('disabled', false).focus();_337 tc.$messageList.text('');_337 });_337 }_337_337 tc.loadMessages = function () {_337 tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT).then(function (messages) {_337 messages.forEach(tc.addMessageToList);_337 });_337 };_337_337 function leaveCurrentChannel() {_337 if (tc.currentChannel) {_337 tc.currentChannel.leave().then(function (leftChannel) {_337 console.log('left ' + leftChannel.friendlyName);_337 leftChannel.removeListener('messageAdded', tc.addMessageToList);_337 leftChannel.removeListener('typingStarted', showTypingStarted);_337 leftChannel.removeListener('typingEnded', hideTypingStarted);_337 leftChannel.removeListener('memberJoined', notifyMemberJoined);_337 leftChannel.removeListener('memberLeft', notifyMemberLeft);_337 });_337 }_337 }_337_337 tc.addMessageToList = function (message) {_337 var rowDiv = $('<div>').addClass('row no-margin');_337 rowDiv.loadTemplate($('#message-template'), {_337 username: message.author,_337 date: dateFormatter.getTodayDate(message.timestamp),_337 body: message.body_337 });_337 if (message.author === tc.username) {_337 rowDiv.addClass('own-message');_337 }_337_337 tc.$messageList.append(rowDiv);_337 scrollToMessageListBottom();_337 };_337_337 function notifyMemberJoined(member) {_337 notify(member.identity + ' joined the channel')_337 }_337_337 function notifyMemberLeft(member) {_337 notify(member.identity + ' left the channel');_337 }_337_337 function notify(message) {_337 var row = $('<div>').addClass('col-md-12');_337 row.loadTemplate('#member-notification-template', {_337 status: message_337 });_337 tc.$messageList.append(row);_337 scrollToMessageListBottom();_337 }_337_337 function showTypingStarted(member) {_337 $typingPlaceholder.text(member.identity + ' is typing...');_337 }_337_337 function hideTypingStarted(member) {_337 $typingPlaceholder.text('');_337 }_337_337 function scrollToMessageListBottom() {_337 tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);_337 }_337_337 function updateChannelUI(selectedChannel) {_337 var channelElements = $('.channel-element').toArray();_337 var channelElement = channelElements.filter(function (element) {_337 return $(element).data().sid === selectedChannel.sid;_337 });_337 channelElement = $(channelElement);_337 if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {_337 tc.currentChannelContainer = channelElement;_337 }_337 tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');_337 channelElement.removeClass('unselected-channel').addClass('selected-channel');_337 tc.currentChannelContainer = channelElement;_337 }_337_337 function showAddChannelInput() {_337 if (tc.messagingClient) {_337 $newChannelInputRow.addClass('showing').removeClass('not-showing');_337 $channelList.addClass('showing').removeClass('not-showing');_337 $newChannelInput.focus();_337 }_337 }_337_337 function hideAddChannelInput() {_337 $newChannelInputRow.addClass('not-showing').removeClass('showing');_337 $channelList.addClass('not-showing').removeClass('showing');_337 $newChannelInput.val('');_337 }_337_337 function addChannel(channel) {_337 if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {_337 tc.generalChannel = channel;_337 }_337 var rowDiv = $('<div>').addClass('row channel-row');_337 rowDiv.loadTemplate('#channel-template', {_337 channelName: channel.friendlyName_337 });_337_337 var channelP = rowDiv.children().children().first();_337_337 rowDiv.on('click', selectChannel);_337 channelP.data('sid', channel.sid);_337 if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {_337 tc.currentChannelContainer = channelP;_337 channelP.addClass('selected-channel');_337 }_337 else {_337 channelP.addClass('unselected-channel')_337 }_337_337 $channelList.append(rowDiv);_337 }_337_337 function deleteCurrentChannel() {_337 if (!tc.currentChannel) {_337 return;_337 }_337 if (tc.currentChannel.sid === tc.generalChannel.sid) {_337 alert('You cannot delete the general channel');_337 return;_337 }_337 tc.currentChannel.delete().then(function (channel) {_337 console.log('channel: ' + channel.friendlyName + ' deleted');_337 setupChannel(tc.generalChannel);_337 });_337 }_337_337 function selectChannel(event) {_337 var target = $(event.target);_337 var channelSid = target.data().sid;_337 var selectedChannel = tc.channelArray.filter(function (channel) {_337 return channel.sid === channelSid;_337 })[0];_337 if (selectedChannel === tc.currentChannel) {_337 return;_337 }_337 setupChannel(selectedChannel);_337 };_337_337 function disconnectClient() {_337 leaveCurrentChannel();_337 $channelList.text('');_337 tc.$messageList.text('');_337 channels = undefined;_337 $statusRow.addClass('disconnected').removeClass('connected');_337 tc.$messageList.addClass('disconnected').removeClass('connected');_337 $connectPanel.addClass('disconnected').removeClass('connected');_337 $inputText.removeClass('with-shadow');_337 $typingRow.addClass('disconnected').removeClass('connected');_337 }_337_337 tc.sortChannelsByName = function (channels) {_337 return channels.sort(function (a, b) {_337 if (a.friendlyName === GENERAL_CHANNEL_NAME) {_337 return -1;_337 }_337 if (b.friendlyName === GENERAL_CHANNEL_NAME) {_337 return 1;_337 }_337 return a.friendlyName.localeCompare(b.friendlyName);_337 });_337 };_337_337 return tc;_337})();
At some point your users will want to delete a channel. Let's have a look at how that can be done.
Deleting a channel is even more simple than creating one. The application lets the user delete the channel they are currently joined to 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. As other methods on the `Channel' object, It'll return a promise where you can set function that is going to handle successes.
TwilioChat.Web/Scripts/twiliochat.js
_337var twiliochat = (function () {_337 var tc = {};_337_337 var GENERAL_CHANNEL_UNIQUE_NAME = 'general';_337 var GENERAL_CHANNEL_NAME = 'General Channel';_337 var MESSAGES_HISTORY_LIMIT = 50;_337_337 var $channelList;_337 var $inputText;_337 var $usernameInput;_337 var $statusRow;_337 var $connectPanel;_337 var $newChannelInputRow;_337 var $newChannelInput;_337 var $typingRow;_337 var $typingPlaceholder;_337_337 $(document).ready(function () {_337 tc.$messageList = $('#message-list');_337 $channelList = $('#channel-list');_337 $inputText = $('#input-text');_337 $usernameInput = $('#username-input');_337 $statusRow = $('#status-row');_337 $connectPanel = $('#connect-panel');_337 $newChannelInputRow = $('#new-channel-input-row');_337 $newChannelInput = $('#new-channel-input');_337 $typingRow = $('#typing-row');_337 $typingPlaceholder = $('#typing-placeholder');_337 $usernameInput.focus();_337 $usernameInput.on('keypress', handleUsernameInputKeypress);_337 $inputText.on('keypress', handleInputTextKeypress);_337 $newChannelInput.on('keypress', tc.handleNewChannelInputKeypress);_337 $('#connect-image').on('click', connectClientWithUsername);_337 $('#add-channel-image').on('click', showAddChannelInput);_337 $('#leave-span').on('click', disconnectClient);_337 $('#delete-channel-span').on('click', deleteCurrentChannel);_337 });_337_337 function handleUsernameInputKeypress(event) {_337 if (event.keyCode === 13) {_337 connectClientWithUsername();_337 }_337 }_337_337 function handleInputTextKeypress(event) {_337 if (event.keyCode === 13) {_337 tc.currentChannel.sendMessage($(this).val());_337 event.preventDefault();_337 $(this).val('');_337 }_337 else {_337 notifyTyping();_337 }_337 }_337_337 var notifyTyping = $.throttle(function () {_337 tc.currentChannel.typing();_337 }, 1000);_337_337 tc.handleNewChannelInputKeypress = function (event) {_337 if (event.keyCode === 13) {_337 tc.messagingClient.createChannel({_337 friendlyName: $newChannelInput.val()_337 }).then(hideAddChannelInput);_337 $(this).val('');_337 event.preventDefault();_337 }_337 };_337_337 function connectClientWithUsername() {_337 var usernameText = $usernameInput.val();_337 $usernameInput.val('');_337 if (usernameText == '') {_337 alert('Username cannot be empty');_337 return;_337 }_337 tc.username = usernameText;_337 fetchAccessToken(tc.username, connectMessagingClient);_337 }_337_337 function fetchAccessToken(username, handler) {_337 $.post('/token', {_337 identity: username,_337 device: 'browser'_337 }, function (data) {_337 handler(data);_337 }, 'json');_337 }_337_337 function connectMessagingClient(tokenResponse) {_337 // Initialize the IP messaging client_337 tc.accessManager = new Twilio.AccessManager(tokenResponse.token);_337 tc.messagingClient = new Twilio.IPMessaging.Client(tc.accessManager);_337 updateConnectedUI();_337 tc.loadChannelList(tc.joinGeneralChannel);_337 tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));_337 tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));_337 tc.messagingClient.on('tokenExpired', refreshToken);_337 }_337_337 function refreshToken() {_337 fetchAccessToken(tc.username, setNewToken);_337 }_337_337 function setNewToken(tokenResponse) {_337 tc.accessManager.updateToken(tokenResponse.token);_337 }_337_337 function updateConnectedUI() {_337 $('#username-span').text(tc.username);_337 $statusRow.addClass('connected').removeClass('disconnected');_337 tc.$messageList.addClass('connected').removeClass('disconnected');_337 $connectPanel.addClass('connected').removeClass('disconnected');_337 $inputText.addClass('with-shadow');_337 $typingRow.addClass('connected').removeClass('disconnected');_337 }_337_337 tc.loadChannelList = function (handler) {_337 if (tc.messagingClient === undefined) {_337 console.log('Client is not initialized');_337 return;_337 }_337_337 tc.messagingClient.getChannels().then(function (channels) {_337 tc.channelArray = tc.sortChannelsByName(channels);_337 $channelList.text('');_337 tc.channelArray.forEach(addChannel);_337 if (typeof handler === 'function') {_337 handler();_337 }_337 });_337 };_337_337 tc.joinGeneralChannel = function () {_337 console.log('Attempting to join "general" chat channel...');_337 if (!tc.generalChannel) {_337 // If it doesn't exist, let's create it_337 tc.messagingClient.createChannel({_337 uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,_337 friendlyName: GENERAL_CHANNEL_NAME_337 }).then(function (channel) {_337 console.log('Created general channel');_337 tc.generalChannel = channel;_337 tc.loadChannelList(tc.joinGeneralChannel);_337 });_337 }_337 else {_337 console.log('Found general channel:');_337 setupChannel(tc.generalChannel);_337 }_337 };_337_337 function setupChannel(channel) {_337 // Join the channel_337 channel.join().then(function (joinedChannel) {_337 console.log('Joined channel ' + joinedChannel.friendlyName);_337 leaveCurrentChannel();_337 updateChannelUI(channel);_337 tc.currentChannel = channel;_337 tc.loadMessages();_337 channel.on('messageAdded', tc.addMessageToList);_337 channel.on('typingStarted', showTypingStarted);_337 channel.on('typingEnded', hideTypingStarted);_337 channel.on('memberJoined', notifyMemberJoined);_337 channel.on('memberLeft', notifyMemberLeft);_337 $inputText.prop('disabled', false).focus();_337 tc.$messageList.text('');_337 });_337 }_337_337 tc.loadMessages = function () {_337 tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT).then(function (messages) {_337 messages.forEach(tc.addMessageToList);_337 });_337 };_337_337 function leaveCurrentChannel() {_337 if (tc.currentChannel) {_337 tc.currentChannel.leave().then(function (leftChannel) {_337 console.log('left ' + leftChannel.friendlyName);_337 leftChannel.removeListener('messageAdded', tc.addMessageToList);_337 leftChannel.removeListener('typingStarted', showTypingStarted);_337 leftChannel.removeListener('typingEnded', hideTypingStarted);_337 leftChannel.removeListener('memberJoined', notifyMemberJoined);_337 leftChannel.removeListener('memberLeft', notifyMemberLeft);_337 });_337 }_337 }_337_337 tc.addMessageToList = function (message) {_337 var rowDiv = $('<div>').addClass('row no-margin');_337 rowDiv.loadTemplate($('#message-template'), {_337 username: message.author,_337 date: dateFormatter.getTodayDate(message.timestamp),_337 body: message.body_337 });_337 if (message.author === tc.username) {_337 rowDiv.addClass('own-message');_337 }_337_337 tc.$messageList.append(rowDiv);_337 scrollToMessageListBottom();_337 };_337_337 function notifyMemberJoined(member) {_337 notify(member.identity + ' joined the channel')_337 }_337_337 function notifyMemberLeft(member) {_337 notify(member.identity + ' left the channel');_337 }_337_337 function notify(message) {_337 var row = $('<div>').addClass('col-md-12');_337 row.loadTemplate('#member-notification-template', {_337 status: message_337 });_337 tc.$messageList.append(row);_337 scrollToMessageListBottom();_337 }_337_337 function showTypingStarted(member) {_337 $typingPlaceholder.text(member.identity + ' is typing...');_337 }_337_337 function hideTypingStarted(member) {_337 $typingPlaceholder.text('');_337 }_337_337 function scrollToMessageListBottom() {_337 tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);_337 }_337_337 function updateChannelUI(selectedChannel) {_337 var channelElements = $('.channel-element').toArray();_337 var channelElement = channelElements.filter(function (element) {_337 return $(element).data().sid === selectedChannel.sid;_337 });_337 channelElement = $(channelElement);_337 if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {_337 tc.currentChannelContainer = channelElement;_337 }_337 tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');_337 channelElement.removeClass('unselected-channel').addClass('selected-channel');_337 tc.currentChannelContainer = channelElement;_337 }_337_337 function showAddChannelInput() {_337 if (tc.messagingClient) {_337 $newChannelInputRow.addClass('showing').removeClass('not-showing');_337 $channelList.addClass('showing').removeClass('not-showing');_337 $newChannelInput.focus();_337 }_337 }_337_337 function hideAddChannelInput() {_337 $newChannelInputRow.addClass('not-showing').removeClass('showing');_337 $channelList.addClass('not-showing').removeClass('showing');_337 $newChannelInput.val('');_337 }_337_337 function addChannel(channel) {_337 if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {_337 tc.generalChannel = channel;_337 }_337 var rowDiv = $('<div>').addClass('row channel-row');_337 rowDiv.loadTemplate('#channel-template', {_337 channelName: channel.friendlyName_337 });_337_337 var channelP = rowDiv.children().children().first();_337_337 rowDiv.on('click', selectChannel);_337 channelP.data('sid', channel.sid);_337 if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {_337 tc.currentChannelContainer = channelP;_337 channelP.addClass('selected-channel');_337 }_337 else {_337 channelP.addClass('unselected-channel')_337 }_337_337 $channelList.append(rowDiv);_337 }_337_337 function deleteCurrentChannel() {_337 if (!tc.currentChannel) {_337 return;_337 }_337 if (tc.currentChannel.sid === tc.generalChannel.sid) {_337 alert('You cannot delete the general channel');_337 return;_337 }_337 tc.currentChannel.delete().then(function (channel) {_337 console.log('channel: ' + channel.friendlyName + ' deleted');_337 setupChannel(tc.generalChannel);_337 });_337 }_337_337 function selectChannel(event) {_337 var target = $(event.target);_337 var channelSid = target.data().sid;_337 var selectedChannel = tc.channelArray.filter(function (channel) {_337 return channel.sid === channelSid;_337 })[0];_337 if (selectedChannel === tc.currentChannel) {_337 return;_337 }_337 setupChannel(selectedChannel);_337 };_337_337 function disconnectClient() {_337 leaveCurrentChannel();_337 $channelList.text('');_337 tc.$messageList.text('');_337 channels = undefined;_337 $statusRow.addClass('disconnected').removeClass('connected');_337 tc.$messageList.addClass('disconnected').removeClass('connected');_337 $connectPanel.addClass('disconnected').removeClass('connected');_337 $inputText.removeClass('with-shadow');_337 $typingRow.addClass('disconnected').removeClass('connected');_337 }_337_337 tc.sortChannelsByName = function (channels) {_337 return channels.sort(function (a, b) {_337 if (a.friendlyName === GENERAL_CHANNEL_NAME) {_337 return -1;_337 }_337 if (b.friendlyName === GENERAL_CHANNEL_NAME) {_337 return 1;_337 }_337 return a.friendlyName.localeCompare(b.friendlyName);_337 });_337 };_337_337 return tc;_337})();
That's it! We've just implemented a simple chat application for C# using ASP.NET MVC.
If you are a C# developer working with Twilio, you might want to check out these other tutorials:
Never miss another server outage. Learn how to build a server notification system that will alert all administrators via SMS when a server outage occurs.
Increase your rate of response by automating the workflows that are key to your business. In this tutorial, learn how to build a ready-for-scale automated SMS workflow, for a vacation rental company.
Protect your users' privacy by anonymously connecting them with Twilio Voice and SMS. Learn how to create disposable phone numbers on-demand, so two users can communicate without exchanging personal information.
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.