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

Chat with C# and ASP.NET MVC


(warning)

Warning

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

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

(error)

Danger

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

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

Ready to implement a chat application using Twilio 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.

Properati built a web and mobile messaging app to help real estate buyers and sellers connect in real time. Learn more here.(link takes you to an external page)

For your convenience, we consolidated the source code for this tutorial in a single GitHub repository(link takes you to an external page). Feel free to clone it and tweak as required.


Token Generation

token-generation page anchor

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.

Generate an Access Token

generate-an-access-token page anchor

TwilioChat.Web/Domain/TokenGenerator.cs


_30
using System.Collections.Generic;
_30
using Twilio.Jwt.AccessToken;
_30
_30
namespace 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.


Token Generation Controller

token-generation-controller page anchor

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


_27
using System.Web.Mvc;
_27
using TwilioChat.Web.Domain;
_27
_27
namespace 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.


Initialize the Chat Client

initialize-the-chat-client page anchor

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


_381
var twiliochat = (function () {
_381
var tc = {};
_381
_381
var GENERAL_CHANNEL_UNIQUE_NAME = 'general';
_381
var GENERAL_CHANNEL_NAME = 'General Channel';
_381
var MESSAGES_HISTORY_LIMIT = 50;
_381
_381
var $channelList;
_381
var $inputText;
_381
var $usernameInput;
_381
var $statusRow;
_381
var $connectPanel;
_381
var $newChannelInputRow;
_381
var $newChannelInput;
_381
var $typingRow;
_381
var $typingPlaceholder;
_381
_381
$(document).ready(function () {
_381
tc.init();
_381
});
_381
_381
tc.init = function () {
_381
tc.$messageList = $('#message-list');
_381
$channelList = $('#channel-list');
_381
$inputText = $('#input-text');
_381
$usernameInput = $('#username-input');
_381
$statusRow = $('#status-row');
_381
$connectPanel = $('#connect-panel');
_381
$newChannelInputRow = $('#new-channel-input-row');
_381
$newChannelInput = $('#new-channel-input');
_381
$typingRow = $('#typing-row');
_381
$typingPlaceholder = $('#typing-placeholder');
_381
$usernameInput.focus();
_381
$usernameInput.on('keypress', handleUsernameInputKeypress);
_381
$inputText.on('keypress', handleInputTextKeypress);
_381
$newChannelInput.on('keypress', tc.handleNewChannelInputKeypress);
_381
$('#connect-image').on('click', connectClientWithUsername);
_381
$('#add-channel-image').on('click', showAddChannelInput);
_381
$('#leave-span').on('click', disconnectClient);
_381
$('#delete-channel-span').on('click', deleteCurrentChannel);
_381
};
_381
_381
function handleUsernameInputKeypress(event) {
_381
if (event.keyCode === 13) {
_381
connectClientWithUsername();
_381
}
_381
}
_381
_381
function handleInputTextKeypress(event) {
_381
if (event.keyCode === 13) {
_381
tc.currentChannel.sendMessage($(this).val());
_381
event.preventDefault();
_381
$(this).val('');
_381
}
_381
else {
_381
notifyTyping();
_381
}
_381
}
_381
_381
var notifyTyping = $.throttle(function () {
_381
tc.currentChannel.typing();
_381
}, 1000);
_381
_381
tc.handleNewChannelInputKeypress = function (event) {
_381
if (event.keyCode === 13) {
_381
tc.messagingClient
_381
.createChannel({
_381
friendlyName: $newChannelInput.val(),
_381
})
_381
.then(hideAddChannelInput);
_381
_381
$(this).val('');
_381
event.preventDefault();
_381
}
_381
};
_381
_381
function connectClientWithUsername() {
_381
var usernameText = $usernameInput.val();
_381
$usernameInput.val('');
_381
if (usernameText == '') {
_381
alert('Username cannot be empty');
_381
return;
_381
}
_381
tc.username = usernameText;
_381
fetchAccessToken(tc.username, connectMessagingClient);
_381
}
_381
_381
function fetchAccessToken(username, handler) {
_381
$.post('/token', { identity: username}, null, 'json')
_381
.done(function (response) {
_381
handler(response.token);
_381
})
_381
.fail(function (error) {
_381
console.log('Failed to fetch the Access Token with error: ' + error);
_381
});
_381
}
_381
_381
function connectMessagingClient(token) {
_381
// Initialize the Chat messaging client
_381
Twilio.Chat.Client.create(token).then(function (client) {
_381
tc.messagingClient = client;
_381
updateConnectedUI();
_381
tc.loadChannelList(tc.joinGeneralChannel);
_381
tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));
_381
tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));
_381
tc.messagingClient.on('tokenExpired', refreshToken);
_381
});
_381
}
_381
_381
function refreshToken() {
_381
fetchAccessToken(tc.username, setNewToken);
_381
}
_381
_381
function setNewToken(token) {
_381
tc.messagingClient.updateToken(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(link takes you to an external page). 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


_337
var 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.


Join the General Channel

join-the-general-channel page anchor

This application will try to join a channel called "General Channel" when it starts. If the channel doesn't exist, we'll create one with that name. The scope of this example application will show you how to work only with public channels, but the 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


_337
var 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.


Listen to Channel Events

listen-to-channel-events page anchor

With access to the channel objects we can use them to listen to a series of events(link takes you to an external page). 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


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


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

TwilioChat.Web/Scripts/twiliochat.js


_337
var 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


_337
var 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


_337
var 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:

SMS and MMS Notifications

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.

Workflow Automation

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.

Masked Phone Numbers

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.

Did this help?

did-this-help page anchor

Thanks for checking out this tutorial! If you have any feedback to share with us, we'd love to hear it. Tweet @twilio(link takes you to an external page) to let us know what you think.


Rate this page: