Skip to contentSkip to navigationSkip to topbar
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

1
using System.Collections.Generic;
2
using Twilio.Jwt.AccessToken;
3
4
namespace TwilioChat.Web.Domain
5
{
6
public interface ITokenGenerator
7
{
8
string Generate(string identity);
9
}
10
11
public class TokenGenerator : ITokenGenerator
12
{
13
public string Generate(string identity)
14
{
15
var grants = new HashSet<IGrant>
16
{
17
new ChatGrant {ServiceSid = Configuration.ChatServiceSID}
18
};
19
20
var token = new Token(
21
Configuration.AccountSID,
22
Configuration.ApiKey,
23
Configuration.ApiSecret,
24
identity,
25
grants: grants);
26
27
return token.ToJwt();
28
}
29
}
30
}
31

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

1
using System.Web.Mvc;
2
using TwilioChat.Web.Domain;
3
4
namespace TwilioChat.Web.Controllers
5
{
6
public class TokenController : Controller
7
{
8
private readonly ITokenGenerator _tokenGenerator;
9
10
public TokenController() : this(new TokenGenerator()) { }
11
12
public TokenController(ITokenGenerator tokenGenerator)
13
{
14
_tokenGenerator = tokenGenerator;
15
}
16
17
// POST: Token
18
[HttpPost]
19
public ActionResult Index(string identity)
20
{
21
if (identity == null) return null;
22
23
var token = _tokenGenerator.Generate(identity);
24
return Json(new {identity, token});
25
}
26
}
27
}

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


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

1
var twiliochat = (function () {
2
var tc = {};
3
4
var GENERAL_CHANNEL_UNIQUE_NAME = 'general';
5
var GENERAL_CHANNEL_NAME = 'General Channel';
6
var MESSAGES_HISTORY_LIMIT = 50;
7
8
var $channelList;
9
var $inputText;
10
var $usernameInput;
11
var $statusRow;
12
var $connectPanel;
13
var $newChannelInputRow;
14
var $newChannelInput;
15
var $typingRow;
16
var $typingPlaceholder;
17
18
$(document).ready(function () {
19
tc.init();
20
});
21
22
tc.init = function () {
23
tc.$messageList = $('#message-list');
24
$channelList = $('#channel-list');
25
$inputText = $('#input-text');
26
$usernameInput = $('#username-input');
27
$statusRow = $('#status-row');
28
$connectPanel = $('#connect-panel');
29
$newChannelInputRow = $('#new-channel-input-row');
30
$newChannelInput = $('#new-channel-input');
31
$typingRow = $('#typing-row');
32
$typingPlaceholder = $('#typing-placeholder');
33
$usernameInput.focus();
34
$usernameInput.on('keypress', handleUsernameInputKeypress);
35
$inputText.on('keypress', handleInputTextKeypress);
36
$newChannelInput.on('keypress', tc.handleNewChannelInputKeypress);
37
$('#connect-image').on('click', connectClientWithUsername);
38
$('#add-channel-image').on('click', showAddChannelInput);
39
$('#leave-span').on('click', disconnectClient);
40
$('#delete-channel-span').on('click', deleteCurrentChannel);
41
};
42
43
function handleUsernameInputKeypress(event) {
44
if (event.keyCode === 13) {
45
connectClientWithUsername();
46
}
47
}
48
49
function handleInputTextKeypress(event) {
50
if (event.keyCode === 13) {
51
tc.currentChannel.sendMessage($(this).val());
52
event.preventDefault();
53
$(this).val('');
54
}
55
else {
56
notifyTyping();
57
}
58
}
59
60
var notifyTyping = $.throttle(function () {
61
tc.currentChannel.typing();
62
}, 1000);
63
64
tc.handleNewChannelInputKeypress = function (event) {
65
if (event.keyCode === 13) {
66
tc.messagingClient
67
.createChannel({
68
friendlyName: $newChannelInput.val(),
69
})
70
.then(hideAddChannelInput);
71
72
$(this).val('');
73
event.preventDefault();
74
}
75
};
76
77
function connectClientWithUsername() {
78
var usernameText = $usernameInput.val();
79
$usernameInput.val('');
80
if (usernameText == '') {
81
alert('Username cannot be empty');
82
return;
83
}
84
tc.username = usernameText;
85
fetchAccessToken(tc.username, connectMessagingClient);
86
}
87
88
function fetchAccessToken(username, handler) {
89
$.post('/token', { identity: username}, null, 'json')
90
.done(function (response) {
91
handler(response.token);
92
})
93
.fail(function (error) {
94
console.log('Failed to fetch the Access Token with error: ' + error);
95
});
96
}
97
98
function connectMessagingClient(token) {
99
// Initialize the Chat messaging client
100
Twilio.Chat.Client.create(token).then(function (client) {
101
tc.messagingClient = client;
102
updateConnectedUI();
103
tc.loadChannelList(tc.joinGeneralChannel);
104
tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));
105
tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));
106
tc.messagingClient.on('tokenExpired', refreshToken);
107
});
108
}
109
110
function refreshToken() {
111
fetchAccessToken(tc.username, setNewToken);
112
}
113
114
function setNewToken(token) {
115
tc.messagingClient.updateToken(token);
116
}
117
118
function updateConnectedUI() {
119
$('#username-span').text(tc.username);
120
$statusRow.addClass('connected').removeClass('disconnected');
121
tc.$messageList.addClass('connected').removeClass('disconnected');
122
$connectPanel.addClass('connected').removeClass('disconnected');
123
$inputText.addClass('with-shadow');
124
$typingRow.addClass('connected').removeClass('disconnected');
125
}
126
127
tc.loadChannelList = function (handler) {
128
if (tc.messagingClient === undefined) {
129
console.log('Client is not initialized');
130
return;
131
}
132
133
tc.messagingClient.getPublicChannelDescriptors().then(function (channels) {
134
tc.channelArray = tc.sortChannelsByName(channels.items);
135
$channelList.text('');
136
tc.channelArray.forEach(addChannel);
137
if (typeof handler === 'function') {
138
handler();
139
}
140
});
141
};
142
143
tc.joinGeneralChannel = function () {
144
console.log('Attempting to join "general" chat channel...');
145
if (!tc.generalChannel) {
146
// If it doesn't exist, let's create it
147
tc.messagingClient.createChannel({
148
uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,
149
friendlyName: GENERAL_CHANNEL_NAME
150
}).then(function (channel) {
151
console.log('Created general channel');
152
tc.generalChannel = channel;
153
tc.loadChannelList(tc.joinGeneralChannel);
154
});
155
}
156
else {
157
console.log('Found general channel:');
158
setupChannel(tc.generalChannel);
159
}
160
};
161
162
function initChannel(channel) {
163
console.log('Initialized channel ' + channel.friendlyName);
164
return tc.messagingClient.getChannelBySid(channel.sid);
165
}
166
167
function joinChannel(_channel) {
168
return _channel.join()
169
.then(function (joinedChannel) {
170
console.log('Joined channel ' + joinedChannel.friendlyName);
171
updateChannelUI(_channel);
172
173
return joinedChannel;
174
})
175
.catch(function (err) {
176
if (_channel.status == 'joined') {
177
updateChannelUI(_channel);
178
return _channel;
179
}
180
console.error(
181
"Couldn't join channel " + _channel.friendlyName + ' because -> ' + err
182
);
183
});
184
}
185
186
function initChannelEvents() {
187
console.log(tc.currentChannel.friendlyName + ' ready.');
188
tc.currentChannel.on('messageAdded', tc.addMessageToList);
189
tc.currentChannel.on('typingStarted', showTypingStarted);
190
tc.currentChannel.on('typingEnded', hideTypingStarted);
191
tc.currentChannel.on('memberJoined', notifyMemberJoined);
192
tc.currentChannel.on('memberLeft', notifyMemberLeft);
193
$inputText.prop('disabled', false).focus();
194
}
195
196
function setupChannel(channel) {
197
return leaveCurrentChannel()
198
.then(function () {
199
return initChannel(channel);
200
})
201
.then(function (_channel) {
202
return joinChannel(_channel);
203
})
204
.then(initChannelEvents);
205
}
206
207
tc.loadMessages = function () {
208
tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT).then(function (messages) {
209
messages.items.forEach(tc.addMessageToList);
210
});
211
};
212
213
function leaveCurrentChannel() {
214
if (tc.currentChannel) {
215
return tc.currentChannel.leave().then(function (leftChannel) {
216
console.log('left ' + leftChannel.friendlyName);
217
leftChannel.removeListener('messageAdded', tc.addMessageToList);
218
leftChannel.removeListener('typingStarted', showTypingStarted);
219
leftChannel.removeListener('typingEnded', hideTypingStarted);
220
leftChannel.removeListener('memberJoined', notifyMemberJoined);
221
leftChannel.removeListener('memberLeft', notifyMemberLeft);
222
});
223
} else {
224
return Promise.resolve();
225
}
226
}
227
228
tc.addMessageToList = function (message) {
229
var rowDiv = $('<div>').addClass('row no-margin');
230
rowDiv.loadTemplate($('#message-template'), {
231
username: message.author,
232
date: dateFormatter.getTodayDate(message.dateCreated),
233
body: message.body
234
});
235
if (message.author === tc.username) {
236
rowDiv.addClass('own-message');
237
}
238
239
tc.$messageList.append(rowDiv);
240
scrollToMessageListBottom();
241
};
242
243
function notifyMemberJoined(member) {
244
notify(member.identity + ' joined the channel')
245
}
246
247
function notifyMemberLeft(member) {
248
notify(member.identity + ' left the channel');
249
}
250
251
function notify(message) {
252
var row = $('<div>').addClass('col-md-12');
253
row.loadTemplate('#member-notification-template', {
254
status: message
255
});
256
tc.$messageList.append(row);
257
scrollToMessageListBottom();
258
}
259
260
function showTypingStarted(member) {
261
$typingPlaceholder.text(member.identity + ' is typing...');
262
}
263
264
function hideTypingStarted(member) {
265
$typingPlaceholder.text('');
266
}
267
268
function scrollToMessageListBottom() {
269
tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);
270
}
271
272
function updateChannelUI(selectedChannel) {
273
var channelElements = $('.channel-element').toArray();
274
var channelElement = channelElements.filter(function (element) {
275
return $(element).data().sid === selectedChannel.sid;
276
});
277
channelElement = $(channelElement);
278
if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {
279
tc.currentChannelContainer = channelElement;
280
}
281
tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');
282
channelElement.removeClass('unselected-channel').addClass('selected-channel');
283
tc.currentChannelContainer = channelElement;
284
tc.currentChannel = selectedChannel;
285
tc.loadMessages();
286
}
287
288
function showAddChannelInput() {
289
if (tc.messagingClient) {
290
$newChannelInputRow.addClass('showing').removeClass('not-showing');
291
$channelList.addClass('showing').removeClass('not-showing');
292
$newChannelInput.focus();
293
}
294
}
295
296
function hideAddChannelInput() {
297
$newChannelInputRow.addClass('not-showing').removeClass('showing');
298
$channelList.addClass('not-showing').removeClass('showing');
299
$newChannelInput.val('');
300
}
301
302
function addChannel(channel) {
303
if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {
304
tc.generalChannel = channel;
305
}
306
var rowDiv = $('<div>').addClass('row channel-row');
307
rowDiv.loadTemplate('#channel-template', {
308
channelName: channel.friendlyName
309
});
310
311
var channelP = rowDiv.children().children().first();
312
313
rowDiv.on('click', selectChannel);
314
channelP.data('sid', channel.sid);
315
if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {
316
tc.currentChannelContainer = channelP;
317
channelP.addClass('selected-channel');
318
}
319
else {
320
channelP.addClass('unselected-channel')
321
}
322
323
$channelList.append(rowDiv);
324
}
325
326
function deleteCurrentChannel() {
327
if (!tc.currentChannel) {
328
return;
329
}
330
331
if (tc.currentChannel.sid === tc.generalChannel.sid) {
332
alert('You cannot delete the general channel');
333
return;
334
}
335
336
tc.currentChannel
337
.delete()
338
.then(function (channel) {
339
console.log('channel: ' + channel.friendlyName + ' deleted');
340
setupChannel(tc.generalChannel);
341
});
342
}
343
344
function selectChannel(event) {
345
var target = $(event.target);
346
var channelSid = target.data().sid;
347
var selectedChannel = tc.channelArray.filter(function (channel) {
348
return channel.sid === channelSid;
349
})[0];
350
if (selectedChannel === tc.currentChannel) {
351
return;
352
}
353
setupChannel(selectedChannel);
354
};
355
356
function disconnectClient() {
357
leaveCurrentChannel();
358
$channelList.text('');
359
tc.$messageList.text('');
360
channels = undefined;
361
$statusRow.addClass('disconnected').removeClass('connected');
362
tc.$messageList.addClass('disconnected').removeClass('connected');
363
$connectPanel.addClass('disconnected').removeClass('connected');
364
$inputText.removeClass('with-shadow');
365
$typingRow.addClass('disconnected').removeClass('connected');
366
}
367
368
tc.sortChannelsByName = function (channels) {
369
return channels.sort(function (a, b) {
370
if (a.friendlyName === GENERAL_CHANNEL_NAME) {
371
return -1;
372
}
373
if (b.friendlyName === GENERAL_CHANNEL_NAME) {
374
return 1;
375
}
376
return a.friendlyName.localeCompare(b.friendlyName);
377
});
378
};
379
380
return tc;
381
})();
382

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


After initializing the client, we can now call it's method getChannels to retrieve all visible channels(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

1
var twiliochat = (function () {
2
var tc = {};
3
4
var GENERAL_CHANNEL_UNIQUE_NAME = 'general';
5
var GENERAL_CHANNEL_NAME = 'General Channel';
6
var MESSAGES_HISTORY_LIMIT = 50;
7
8
var $channelList;
9
var $inputText;
10
var $usernameInput;
11
var $statusRow;
12
var $connectPanel;
13
var $newChannelInputRow;
14
var $newChannelInput;
15
var $typingRow;
16
var $typingPlaceholder;
17
18
$(document).ready(function () {
19
tc.$messageList = $('#message-list');
20
$channelList = $('#channel-list');
21
$inputText = $('#input-text');
22
$usernameInput = $('#username-input');
23
$statusRow = $('#status-row');
24
$connectPanel = $('#connect-panel');
25
$newChannelInputRow = $('#new-channel-input-row');
26
$newChannelInput = $('#new-channel-input');
27
$typingRow = $('#typing-row');
28
$typingPlaceholder = $('#typing-placeholder');
29
$usernameInput.focus();
30
$usernameInput.on('keypress', handleUsernameInputKeypress);
31
$inputText.on('keypress', handleInputTextKeypress);
32
$newChannelInput.on('keypress', tc.handleNewChannelInputKeypress);
33
$('#connect-image').on('click', connectClientWithUsername);
34
$('#add-channel-image').on('click', showAddChannelInput);
35
$('#leave-span').on('click', disconnectClient);
36
$('#delete-channel-span').on('click', deleteCurrentChannel);
37
});
38
39
function handleUsernameInputKeypress(event) {
40
if (event.keyCode === 13) {
41
connectClientWithUsername();
42
}
43
}
44
45
function handleInputTextKeypress(event) {
46
if (event.keyCode === 13) {
47
tc.currentChannel.sendMessage($(this).val());
48
event.preventDefault();
49
$(this).val('');
50
}
51
else {
52
notifyTyping();
53
}
54
}
55
56
var notifyTyping = $.throttle(function () {
57
tc.currentChannel.typing();
58
}, 1000);
59
60
tc.handleNewChannelInputKeypress = function (event) {
61
if (event.keyCode === 13) {
62
tc.messagingClient.createChannel({
63
friendlyName: $newChannelInput.val()
64
}).then(hideAddChannelInput);
65
$(this).val('');
66
event.preventDefault();
67
}
68
};
69
70
function connectClientWithUsername() {
71
var usernameText = $usernameInput.val();
72
$usernameInput.val('');
73
if (usernameText == '') {
74
alert('Username cannot be empty');
75
return;
76
}
77
tc.username = usernameText;
78
fetchAccessToken(tc.username, connectMessagingClient);
79
}
80
81
function fetchAccessToken(username, handler) {
82
$.post('/token', {
83
identity: username,
84
device: 'browser'
85
}, function (data) {
86
handler(data);
87
}, 'json');
88
}
89
90
function connectMessagingClient(tokenResponse) {
91
// Initialize the IP messaging client
92
tc.accessManager = new Twilio.AccessManager(tokenResponse.token);
93
tc.messagingClient = new Twilio.IPMessaging.Client(tc.accessManager);
94
updateConnectedUI();
95
tc.loadChannelList(tc.joinGeneralChannel);
96
tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));
97
tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));
98
tc.messagingClient.on('tokenExpired', refreshToken);
99
}
100
101
function refreshToken() {
102
fetchAccessToken(tc.username, setNewToken);
103
}
104
105
function setNewToken(tokenResponse) {
106
tc.accessManager.updateToken(tokenResponse.token);
107
}
108
109
function updateConnectedUI() {
110
$('#username-span').text(tc.username);
111
$statusRow.addClass('connected').removeClass('disconnected');
112
tc.$messageList.addClass('connected').removeClass('disconnected');
113
$connectPanel.addClass('connected').removeClass('disconnected');
114
$inputText.addClass('with-shadow');
115
$typingRow.addClass('connected').removeClass('disconnected');
116
}
117
118
tc.loadChannelList = function (handler) {
119
if (tc.messagingClient === undefined) {
120
console.log('Client is not initialized');
121
return;
122
}
123
124
tc.messagingClient.getChannels().then(function (channels) {
125
tc.channelArray = tc.sortChannelsByName(channels);
126
$channelList.text('');
127
tc.channelArray.forEach(addChannel);
128
if (typeof handler === 'function') {
129
handler();
130
}
131
});
132
};
133
134
tc.joinGeneralChannel = function () {
135
console.log('Attempting to join "general" chat channel...');
136
if (!tc.generalChannel) {
137
// If it doesn't exist, let's create it
138
tc.messagingClient.createChannel({
139
uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,
140
friendlyName: GENERAL_CHANNEL_NAME
141
}).then(function (channel) {
142
console.log('Created general channel');
143
tc.generalChannel = channel;
144
tc.loadChannelList(tc.joinGeneralChannel);
145
});
146
}
147
else {
148
console.log('Found general channel:');
149
setupChannel(tc.generalChannel);
150
}
151
};
152
153
function setupChannel(channel) {
154
// Join the channel
155
channel.join().then(function (joinedChannel) {
156
console.log('Joined channel ' + joinedChannel.friendlyName);
157
leaveCurrentChannel();
158
updateChannelUI(channel);
159
tc.currentChannel = channel;
160
tc.loadMessages();
161
channel.on('messageAdded', tc.addMessageToList);
162
channel.on('typingStarted', showTypingStarted);
163
channel.on('typingEnded', hideTypingStarted);
164
channel.on('memberJoined', notifyMemberJoined);
165
channel.on('memberLeft', notifyMemberLeft);
166
$inputText.prop('disabled', false).focus();
167
tc.$messageList.text('');
168
});
169
}
170
171
tc.loadMessages = function () {
172
tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT).then(function (messages) {
173
messages.forEach(tc.addMessageToList);
174
});
175
};
176
177
function leaveCurrentChannel() {
178
if (tc.currentChannel) {
179
tc.currentChannel.leave().then(function (leftChannel) {
180
console.log('left ' + leftChannel.friendlyName);
181
leftChannel.removeListener('messageAdded', tc.addMessageToList);
182
leftChannel.removeListener('typingStarted', showTypingStarted);
183
leftChannel.removeListener('typingEnded', hideTypingStarted);
184
leftChannel.removeListener('memberJoined', notifyMemberJoined);
185
leftChannel.removeListener('memberLeft', notifyMemberLeft);
186
});
187
}
188
}
189
190
tc.addMessageToList = function (message) {
191
var rowDiv = $('<div>').addClass('row no-margin');
192
rowDiv.loadTemplate($('#message-template'), {
193
username: message.author,
194
date: dateFormatter.getTodayDate(message.timestamp),
195
body: message.body
196
});
197
if (message.author === tc.username) {
198
rowDiv.addClass('own-message');
199
}
200
201
tc.$messageList.append(rowDiv);
202
scrollToMessageListBottom();
203
};
204
205
function notifyMemberJoined(member) {
206
notify(member.identity + ' joined the channel')
207
}
208
209
function notifyMemberLeft(member) {
210
notify(member.identity + ' left the channel');
211
}
212
213
function notify(message) {
214
var row = $('<div>').addClass('col-md-12');
215
row.loadTemplate('#member-notification-template', {
216
status: message
217
});
218
tc.$messageList.append(row);
219
scrollToMessageListBottom();
220
}
221
222
function showTypingStarted(member) {
223
$typingPlaceholder.text(member.identity + ' is typing...');
224
}
225
226
function hideTypingStarted(member) {
227
$typingPlaceholder.text('');
228
}
229
230
function scrollToMessageListBottom() {
231
tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);
232
}
233
234
function updateChannelUI(selectedChannel) {
235
var channelElements = $('.channel-element').toArray();
236
var channelElement = channelElements.filter(function (element) {
237
return $(element).data().sid === selectedChannel.sid;
238
});
239
channelElement = $(channelElement);
240
if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {
241
tc.currentChannelContainer = channelElement;
242
}
243
tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');
244
channelElement.removeClass('unselected-channel').addClass('selected-channel');
245
tc.currentChannelContainer = channelElement;
246
}
247
248
function showAddChannelInput() {
249
if (tc.messagingClient) {
250
$newChannelInputRow.addClass('showing').removeClass('not-showing');
251
$channelList.addClass('showing').removeClass('not-showing');
252
$newChannelInput.focus();
253
}
254
}
255
256
function hideAddChannelInput() {
257
$newChannelInputRow.addClass('not-showing').removeClass('showing');
258
$channelList.addClass('not-showing').removeClass('showing');
259
$newChannelInput.val('');
260
}
261
262
function addChannel(channel) {
263
if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {
264
tc.generalChannel = channel;
265
}
266
var rowDiv = $('<div>').addClass('row channel-row');
267
rowDiv.loadTemplate('#channel-template', {
268
channelName: channel.friendlyName
269
});
270
271
var channelP = rowDiv.children().children().first();
272
273
rowDiv.on('click', selectChannel);
274
channelP.data('sid', channel.sid);
275
if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {
276
tc.currentChannelContainer = channelP;
277
channelP.addClass('selected-channel');
278
}
279
else {
280
channelP.addClass('unselected-channel')
281
}
282
283
$channelList.append(rowDiv);
284
}
285
286
function deleteCurrentChannel() {
287
if (!tc.currentChannel) {
288
return;
289
}
290
if (tc.currentChannel.sid === tc.generalChannel.sid) {
291
alert('You cannot delete the general channel');
292
return;
293
}
294
tc.currentChannel.delete().then(function (channel) {
295
console.log('channel: ' + channel.friendlyName + ' deleted');
296
setupChannel(tc.generalChannel);
297
});
298
}
299
300
function selectChannel(event) {
301
var target = $(event.target);
302
var channelSid = target.data().sid;
303
var selectedChannel = tc.channelArray.filter(function (channel) {
304
return channel.sid === channelSid;
305
})[0];
306
if (selectedChannel === tc.currentChannel) {
307
return;
308
}
309
setupChannel(selectedChannel);
310
};
311
312
function disconnectClient() {
313
leaveCurrentChannel();
314
$channelList.text('');
315
tc.$messageList.text('');
316
channels = undefined;
317
$statusRow.addClass('disconnected').removeClass('connected');
318
tc.$messageList.addClass('disconnected').removeClass('connected');
319
$connectPanel.addClass('disconnected').removeClass('connected');
320
$inputText.removeClass('with-shadow');
321
$typingRow.addClass('disconnected').removeClass('connected');
322
}
323
324
tc.sortChannelsByName = function (channels) {
325
return channels.sort(function (a, b) {
326
if (a.friendlyName === GENERAL_CHANNEL_NAME) {
327
return -1;
328
}
329
if (b.friendlyName === GENERAL_CHANNEL_NAME) {
330
return 1;
331
}
332
return a.friendlyName.localeCompare(b.friendlyName);
333
});
334
};
335
336
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

1
var twiliochat = (function () {
2
var tc = {};
3
4
var GENERAL_CHANNEL_UNIQUE_NAME = 'general';
5
var GENERAL_CHANNEL_NAME = 'General Channel';
6
var MESSAGES_HISTORY_LIMIT = 50;
7
8
var $channelList;
9
var $inputText;
10
var $usernameInput;
11
var $statusRow;
12
var $connectPanel;
13
var $newChannelInputRow;
14
var $newChannelInput;
15
var $typingRow;
16
var $typingPlaceholder;
17
18
$(document).ready(function () {
19
tc.$messageList = $('#message-list');
20
$channelList = $('#channel-list');
21
$inputText = $('#input-text');
22
$usernameInput = $('#username-input');
23
$statusRow = $('#status-row');
24
$connectPanel = $('#connect-panel');
25
$newChannelInputRow = $('#new-channel-input-row');
26
$newChannelInput = $('#new-channel-input');
27
$typingRow = $('#typing-row');
28
$typingPlaceholder = $('#typing-placeholder');
29
$usernameInput.focus();
30
$usernameInput.on('keypress', handleUsernameInputKeypress);
31
$inputText.on('keypress', handleInputTextKeypress);
32
$newChannelInput.on('keypress', tc.handleNewChannelInputKeypress);
33
$('#connect-image').on('click', connectClientWithUsername);
34
$('#add-channel-image').on('click', showAddChannelInput);
35
$('#leave-span').on('click', disconnectClient);
36
$('#delete-channel-span').on('click', deleteCurrentChannel);
37
});
38
39
function handleUsernameInputKeypress(event) {
40
if (event.keyCode === 13) {
41
connectClientWithUsername();
42
}
43
}
44
45
function handleInputTextKeypress(event) {
46
if (event.keyCode === 13) {
47
tc.currentChannel.sendMessage($(this).val());
48
event.preventDefault();
49
$(this).val('');
50
}
51
else {
52
notifyTyping();
53
}
54
}
55
56
var notifyTyping = $.throttle(function () {
57
tc.currentChannel.typing();
58
}, 1000);
59
60
tc.handleNewChannelInputKeypress = function (event) {
61
if (event.keyCode === 13) {
62
tc.messagingClient.createChannel({
63
friendlyName: $newChannelInput.val()
64
}).then(hideAddChannelInput);
65
$(this).val('');
66
event.preventDefault();
67
}
68
};
69
70
function connectClientWithUsername() {
71
var usernameText = $usernameInput.val();
72
$usernameInput.val('');
73
if (usernameText == '') {
74
alert('Username cannot be empty');
75
return;
76
}
77
tc.username = usernameText;
78
fetchAccessToken(tc.username, connectMessagingClient);
79
}
80
81
function fetchAccessToken(username, handler) {
82
$.post('/token', {
83
identity: username,
84
device: 'browser'
85
}, function (data) {
86
handler(data);
87
}, 'json');
88
}
89
90
function connectMessagingClient(tokenResponse) {
91
// Initialize the IP messaging client
92
tc.accessManager = new Twilio.AccessManager(tokenResponse.token);
93
tc.messagingClient = new Twilio.IPMessaging.Client(tc.accessManager);
94
updateConnectedUI();
95
tc.loadChannelList(tc.joinGeneralChannel);
96
tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));
97
tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));
98
tc.messagingClient.on('tokenExpired', refreshToken);
99
}
100
101
function refreshToken() {
102
fetchAccessToken(tc.username, setNewToken);
103
}
104
105
function setNewToken(tokenResponse) {
106
tc.accessManager.updateToken(tokenResponse.token);
107
}
108
109
function updateConnectedUI() {
110
$('#username-span').text(tc.username);
111
$statusRow.addClass('connected').removeClass('disconnected');
112
tc.$messageList.addClass('connected').removeClass('disconnected');
113
$connectPanel.addClass('connected').removeClass('disconnected');
114
$inputText.addClass('with-shadow');
115
$typingRow.addClass('connected').removeClass('disconnected');
116
}
117
118
tc.loadChannelList = function (handler) {
119
if (tc.messagingClient === undefined) {
120
console.log('Client is not initialized');
121
return;
122
}
123
124
tc.messagingClient.getChannels().then(function (channels) {
125
tc.channelArray = tc.sortChannelsByName(channels);
126
$channelList.text('');
127
tc.channelArray.forEach(addChannel);
128
if (typeof handler === 'function') {
129
handler();
130
}
131
});
132
};
133
134
tc.joinGeneralChannel = function () {
135
console.log('Attempting to join "general" chat channel...');
136
if (!tc.generalChannel) {
137
// If it doesn't exist, let's create it
138
tc.messagingClient.createChannel({
139
uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,
140
friendlyName: GENERAL_CHANNEL_NAME
141
}).then(function (channel) {
142
console.log('Created general channel');
143
tc.generalChannel = channel;
144
tc.loadChannelList(tc.joinGeneralChannel);
145
});
146
}
147
else {
148
console.log('Found general channel:');
149
setupChannel(tc.generalChannel);
150
}
151
};
152
153
function setupChannel(channel) {
154
// Join the channel
155
channel.join().then(function (joinedChannel) {
156
console.log('Joined channel ' + joinedChannel.friendlyName);
157
leaveCurrentChannel();
158
updateChannelUI(channel);
159
tc.currentChannel = channel;
160
tc.loadMessages();
161
channel.on('messageAdded', tc.addMessageToList);
162
channel.on('typingStarted', showTypingStarted);
163
channel.on('typingEnded', hideTypingStarted);
164
channel.on('memberJoined', notifyMemberJoined);
165
channel.on('memberLeft', notifyMemberLeft);
166
$inputText.prop('disabled', false).focus();
167
tc.$messageList.text('');
168
});
169
}
170
171
tc.loadMessages = function () {
172
tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT).then(function (messages) {
173
messages.forEach(tc.addMessageToList);
174
});
175
};
176
177
function leaveCurrentChannel() {
178
if (tc.currentChannel) {
179
tc.currentChannel.leave().then(function (leftChannel) {
180
console.log('left ' + leftChannel.friendlyName);
181
leftChannel.removeListener('messageAdded', tc.addMessageToList);
182
leftChannel.removeListener('typingStarted', showTypingStarted);
183
leftChannel.removeListener('typingEnded', hideTypingStarted);
184
leftChannel.removeListener('memberJoined', notifyMemberJoined);
185
leftChannel.removeListener('memberLeft', notifyMemberLeft);
186
});
187
}
188
}
189
190
tc.addMessageToList = function (message) {
191
var rowDiv = $('<div>').addClass('row no-margin');
192
rowDiv.loadTemplate($('#message-template'), {
193
username: message.author,
194
date: dateFormatter.getTodayDate(message.timestamp),
195
body: message.body
196
});
197
if (message.author === tc.username) {
198
rowDiv.addClass('own-message');
199
}
200
201
tc.$messageList.append(rowDiv);
202
scrollToMessageListBottom();
203
};
204
205
function notifyMemberJoined(member) {
206
notify(member.identity + ' joined the channel')
207
}
208
209
function notifyMemberLeft(member) {
210
notify(member.identity + ' left the channel');
211
}
212
213
function notify(message) {
214
var row = $('<div>').addClass('col-md-12');
215
row.loadTemplate('#member-notification-template', {
216
status: message
217
});
218
tc.$messageList.append(row);
219
scrollToMessageListBottom();
220
}
221
222
function showTypingStarted(member) {
223
$typingPlaceholder.text(member.identity + ' is typing...');
224
}
225
226
function hideTypingStarted(member) {
227
$typingPlaceholder.text('');
228
}
229
230
function scrollToMessageListBottom() {
231
tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);
232
}
233
234
function updateChannelUI(selectedChannel) {
235
var channelElements = $('.channel-element').toArray();
236
var channelElement = channelElements.filter(function (element) {
237
return $(element).data().sid === selectedChannel.sid;
238
});
239
channelElement = $(channelElement);
240
if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {
241
tc.currentChannelContainer = channelElement;
242
}
243
tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');
244
channelElement.removeClass('unselected-channel').addClass('selected-channel');
245
tc.currentChannelContainer = channelElement;
246
}
247
248
function showAddChannelInput() {
249
if (tc.messagingClient) {
250
$newChannelInputRow.addClass('showing').removeClass('not-showing');
251
$channelList.addClass('showing').removeClass('not-showing');
252
$newChannelInput.focus();
253
}
254
}
255
256
function hideAddChannelInput() {
257
$newChannelInputRow.addClass('not-showing').removeClass('showing');
258
$channelList.addClass('not-showing').removeClass('showing');
259
$newChannelInput.val('');
260
}
261
262
function addChannel(channel) {
263
if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {
264
tc.generalChannel = channel;
265
}
266
var rowDiv = $('<div>').addClass('row channel-row');
267
rowDiv.loadTemplate('#channel-template', {
268
channelName: channel.friendlyName
269
});
270
271
var channelP = rowDiv.children().children().first();
272
273
rowDiv.on('click', selectChannel);
274
channelP.data('sid', channel.sid);
275
if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {
276
tc.currentChannelContainer = channelP;
277
channelP.addClass('selected-channel');
278
}
279
else {
280
channelP.addClass('unselected-channel')
281
}
282
283
$channelList.append(rowDiv);
284
}
285
286
function deleteCurrentChannel() {
287
if (!tc.currentChannel) {
288
return;
289
}
290
if (tc.currentChannel.sid === tc.generalChannel.sid) {
291
alert('You cannot delete the general channel');
292
return;
293
}
294
tc.currentChannel.delete().then(function (channel) {
295
console.log('channel: ' + channel.friendlyName + ' deleted');
296
setupChannel(tc.generalChannel);
297
});
298
}
299
300
function selectChannel(event) {
301
var target = $(event.target);
302
var channelSid = target.data().sid;
303
var selectedChannel = tc.channelArray.filter(function (channel) {
304
return channel.sid === channelSid;
305
})[0];
306
if (selectedChannel === tc.currentChannel) {
307
return;
308
}
309
setupChannel(selectedChannel);
310
};
311
312
function disconnectClient() {
313
leaveCurrentChannel();
314
$channelList.text('');
315
tc.$messageList.text('');
316
channels = undefined;
317
$statusRow.addClass('disconnected').removeClass('connected');
318
tc.$messageList.addClass('disconnected').removeClass('connected');
319
$connectPanel.addClass('disconnected').removeClass('connected');
320
$inputText.removeClass('with-shadow');
321
$typingRow.addClass('disconnected').removeClass('connected');
322
}
323
324
tc.sortChannelsByName = function (channels) {
325
return channels.sort(function (a, b) {
326
if (a.friendlyName === GENERAL_CHANNEL_NAME) {
327
return -1;
328
}
329
if (b.friendlyName === GENERAL_CHANNEL_NAME) {
330
return 1;
331
}
332
return a.friendlyName.localeCompare(b.friendlyName);
333
});
334
};
335
336
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

1
var twiliochat = (function () {
2
var tc = {};
3
4
var GENERAL_CHANNEL_UNIQUE_NAME = 'general';
5
var GENERAL_CHANNEL_NAME = 'General Channel';
6
var MESSAGES_HISTORY_LIMIT = 50;
7
8
var $channelList;
9
var $inputText;
10
var $usernameInput;
11
var $statusRow;
12
var $connectPanel;
13
var $newChannelInputRow;
14
var $newChannelInput;
15
var $typingRow;
16
var $typingPlaceholder;
17
18
$(document).ready(function () {
19
tc.$messageList = $('#message-list');
20
$channelList = $('#channel-list');
21
$inputText = $('#input-text');
22
$usernameInput = $('#username-input');
23
$statusRow = $('#status-row');
24
$connectPanel = $('#connect-panel');
25
$newChannelInputRow = $('#new-channel-input-row');
26
$newChannelInput = $('#new-channel-input');
27
$typingRow = $('#typing-row');
28
$typingPlaceholder = $('#typing-placeholder');
29
$usernameInput.focus();
30
$usernameInput.on('keypress', handleUsernameInputKeypress);
31
$inputText.on('keypress', handleInputTextKeypress);
32
$newChannelInput.on('keypress', tc.handleNewChannelInputKeypress);
33
$('#connect-image').on('click', connectClientWithUsername);
34
$('#add-channel-image').on('click', showAddChannelInput);
35
$('#leave-span').on('click', disconnectClient);
36
$('#delete-channel-span').on('click', deleteCurrentChannel);
37
});
38
39
function handleUsernameInputKeypress(event) {
40
if (event.keyCode === 13) {
41
connectClientWithUsername();
42
}
43
}
44
45
function handleInputTextKeypress(event) {
46
if (event.keyCode === 13) {
47
tc.currentChannel.sendMessage($(this).val());
48
event.preventDefault();
49
$(this).val('');
50
}
51
else {
52
notifyTyping();
53
}
54
}
55
56
var notifyTyping = $.throttle(function () {
57
tc.currentChannel.typing();
58
}, 1000);
59
60
tc.handleNewChannelInputKeypress = function (event) {
61
if (event.keyCode === 13) {
62
tc.messagingClient.createChannel({
63
friendlyName: $newChannelInput.val()
64
}).then(hideAddChannelInput);
65
$(this).val('');
66
event.preventDefault();
67
}
68
};
69
70
function connectClientWithUsername() {
71
var usernameText = $usernameInput.val();
72
$usernameInput.val('');
73
if (usernameText == '') {
74
alert('Username cannot be empty');
75
return;
76
}
77
tc.username = usernameText;
78
fetchAccessToken(tc.username, connectMessagingClient);
79
}
80
81
function fetchAccessToken(username, handler) {
82
$.post('/token', {
83
identity: username,
84
device: 'browser'
85
}, function (data) {
86
handler(data);
87
}, 'json');
88
}
89
90
function connectMessagingClient(tokenResponse) {
91
// Initialize the IP messaging client
92
tc.accessManager = new Twilio.AccessManager(tokenResponse.token);
93
tc.messagingClient = new Twilio.IPMessaging.Client(tc.accessManager);
94
updateConnectedUI();
95
tc.loadChannelList(tc.joinGeneralChannel);
96
tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));
97
tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));
98
tc.messagingClient.on('tokenExpired', refreshToken);
99
}
100
101
function refreshToken() {
102
fetchAccessToken(tc.username, setNewToken);
103
}
104
105
function setNewToken(tokenResponse) {
106
tc.accessManager.updateToken(tokenResponse.token);
107
}
108
109
function updateConnectedUI() {
110
$('#username-span').text(tc.username);
111
$statusRow.addClass('connected').removeClass('disconnected');
112
tc.$messageList.addClass('connected').removeClass('disconnected');
113
$connectPanel.addClass('connected').removeClass('disconnected');
114
$inputText.addClass('with-shadow');
115
$typingRow.addClass('connected').removeClass('disconnected');
116
}
117
118
tc.loadChannelList = function (handler) {
119
if (tc.messagingClient === undefined) {
120
console.log('Client is not initialized');
121
return;
122
}
123
124
tc.messagingClient.getChannels().then(function (channels) {
125
tc.channelArray = tc.sortChannelsByName(channels);
126
$channelList.text('');
127
tc.channelArray.forEach(addChannel);
128
if (typeof handler === 'function') {
129
handler();
130
}
131
});
132
};
133
134
tc.joinGeneralChannel = function () {
135
console.log('Attempting to join "general" chat channel...');
136
if (!tc.generalChannel) {
137
// If it doesn't exist, let's create it
138
tc.messagingClient.createChannel({
139
uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,
140
friendlyName: GENERAL_CHANNEL_NAME
141
}).then(function (channel) {
142
console.log('Created general channel');
143
tc.generalChannel = channel;
144
tc.loadChannelList(tc.joinGeneralChannel);
145
});
146
}
147
else {
148
console.log('Found general channel:');
149
setupChannel(tc.generalChannel);
150
}
151
};
152
153
function setupChannel(channel) {
154
// Join the channel
155
channel.join().then(function (joinedChannel) {
156
console.log('Joined channel ' + joinedChannel.friendlyName);
157
leaveCurrentChannel();
158
updateChannelUI(channel);
159
tc.currentChannel = channel;
160
tc.loadMessages();
161
channel.on('messageAdded', tc.addMessageToList);
162
channel.on('typingStarted', showTypingStarted);
163
channel.on('typingEnded', hideTypingStarted);
164
channel.on('memberJoined', notifyMemberJoined);
165
channel.on('memberLeft', notifyMemberLeft);
166
$inputText.prop('disabled', false).focus();
167
tc.$messageList.text('');
168
});
169
}
170
171
tc.loadMessages = function () {
172
tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT).then(function (messages) {
173
messages.forEach(tc.addMessageToList);
174
});
175
};
176
177
function leaveCurrentChannel() {
178
if (tc.currentChannel) {
179
tc.currentChannel.leave().then(function (leftChannel) {
180
console.log('left ' + leftChannel.friendlyName);
181
leftChannel.removeListener('messageAdded', tc.addMessageToList);
182
leftChannel.removeListener('typingStarted', showTypingStarted);
183
leftChannel.removeListener('typingEnded', hideTypingStarted);
184
leftChannel.removeListener('memberJoined', notifyMemberJoined);
185
leftChannel.removeListener('memberLeft', notifyMemberLeft);
186
});
187
}
188
}
189
190
tc.addMessageToList = function (message) {
191
var rowDiv = $('<div>').addClass('row no-margin');
192
rowDiv.loadTemplate($('#message-template'), {
193
username: message.author,
194
date: dateFormatter.getTodayDate(message.timestamp),
195
body: message.body
196
});
197
if (message.author === tc.username) {
198
rowDiv.addClass('own-message');
199
}
200
201
tc.$messageList.append(rowDiv);
202
scrollToMessageListBottom();
203
};
204
205
function notifyMemberJoined(member) {
206
notify(member.identity + ' joined the channel')
207
}
208
209
function notifyMemberLeft(member) {
210
notify(member.identity + ' left the channel');
211
}
212
213
function notify(message) {
214
var row = $('<div>').addClass('col-md-12');
215
row.loadTemplate('#member-notification-template', {
216
status: message
217
});
218
tc.$messageList.append(row);
219
scrollToMessageListBottom();
220
}
221
222
function showTypingStarted(member) {
223
$typingPlaceholder.text(member.identity + ' is typing...');
224
}
225
226
function hideTypingStarted(member) {
227
$typingPlaceholder.text('');
228
}
229
230
function scrollToMessageListBottom() {
231
tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);
232
}
233
234
function updateChannelUI(selectedChannel) {
235
var channelElements = $('.channel-element').toArray();
236
var channelElement = channelElements.filter(function (element) {
237
return $(element).data().sid === selectedChannel.sid;
238
});
239
channelElement = $(channelElement);
240
if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {
241
tc.currentChannelContainer = channelElement;
242
}
243
tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');
244
channelElement.removeClass('unselected-channel').addClass('selected-channel');
245
tc.currentChannelContainer = channelElement;
246
}
247
248
function showAddChannelInput() {
249
if (tc.messagingClient) {
250
$newChannelInputRow.addClass('showing').removeClass('not-showing');
251
$channelList.addClass('showing').removeClass('not-showing');
252
$newChannelInput.focus();
253
}
254
}
255
256
function hideAddChannelInput() {
257
$newChannelInputRow.addClass('not-showing').removeClass('showing');
258
$channelList.addClass('not-showing').removeClass('showing');
259
$newChannelInput.val('');
260
}
261
262
function addChannel(channel) {
263
if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {
264
tc.generalChannel = channel;
265
}
266
var rowDiv = $('<div>').addClass('row channel-row');
267
rowDiv.loadTemplate('#channel-template', {
268
channelName: channel.friendlyName
269
});
270
271
var channelP = rowDiv.children().children().first();
272
273
rowDiv.on('click', selectChannel);
274
channelP.data('sid', channel.sid);
275
if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {
276
tc.currentChannelContainer = channelP;
277
channelP.addClass('selected-channel');
278
}
279
else {
280
channelP.addClass('unselected-channel')
281
}
282
283
$channelList.append(rowDiv);
284
}
285
286
function deleteCurrentChannel() {
287
if (!tc.currentChannel) {
288
return;
289
}
290
if (tc.currentChannel.sid === tc.generalChannel.sid) {
291
alert('You cannot delete the general channel');
292
return;
293
}
294
tc.currentChannel.delete().then(function (channel) {
295
console.log('channel: ' + channel.friendlyName + ' deleted');
296
setupChannel(tc.generalChannel);
297
});
298
}
299
300
function selectChannel(event) {
301
var target = $(event.target);
302
var channelSid = target.data().sid;
303
var selectedChannel = tc.channelArray.filter(function (channel) {
304
return channel.sid === channelSid;
305
})[0];
306
if (selectedChannel === tc.currentChannel) {
307
return;
308
}
309
setupChannel(selectedChannel);
310
};
311
312
function disconnectClient() {
313
leaveCurrentChannel();
314
$channelList.text('');
315
tc.$messageList.text('');
316
channels = undefined;
317
$statusRow.addClass('disconnected').removeClass('connected');
318
tc.$messageList.addClass('disconnected').removeClass('connected');
319
$connectPanel.addClass('disconnected').removeClass('connected');
320
$inputText.removeClass('with-shadow');
321
$typingRow.addClass('disconnected').removeClass('connected');
322
}
323
324
tc.sortChannelsByName = function (channels) {
325
return channels.sort(function (a, b) {
326
if (a.friendlyName === GENERAL_CHANNEL_NAME) {
327
return -1;
328
}
329
if (b.friendlyName === GENERAL_CHANNEL_NAME) {
330
return 1;
331
}
332
return a.friendlyName.localeCompare(b.friendlyName);
333
});
334
};
335
336
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

1
var twiliochat = (function () {
2
var tc = {};
3
4
var GENERAL_CHANNEL_UNIQUE_NAME = 'general';
5
var GENERAL_CHANNEL_NAME = 'General Channel';
6
var MESSAGES_HISTORY_LIMIT = 50;
7
8
var $channelList;
9
var $inputText;
10
var $usernameInput;
11
var $statusRow;
12
var $connectPanel;
13
var $newChannelInputRow;
14
var $newChannelInput;
15
var $typingRow;
16
var $typingPlaceholder;
17
18
$(document).ready(function () {
19
tc.$messageList = $('#message-list');
20
$channelList = $('#channel-list');
21
$inputText = $('#input-text');
22
$usernameInput = $('#username-input');
23
$statusRow = $('#status-row');
24
$connectPanel = $('#connect-panel');
25
$newChannelInputRow = $('#new-channel-input-row');
26
$newChannelInput = $('#new-channel-input');
27
$typingRow = $('#typing-row');
28
$typingPlaceholder = $('#typing-placeholder');
29
$usernameInput.focus();
30
$usernameInput.on('keypress', handleUsernameInputKeypress);
31
$inputText.on('keypress', handleInputTextKeypress);
32
$newChannelInput.on('keypress', tc.handleNewChannelInputKeypress);
33
$('#connect-image').on('click', connectClientWithUsername);
34
$('#add-channel-image').on('click', showAddChannelInput);
35
$('#leave-span').on('click', disconnectClient);
36
$('#delete-channel-span').on('click', deleteCurrentChannel);
37
});
38
39
function handleUsernameInputKeypress(event) {
40
if (event.keyCode === 13) {
41
connectClientWithUsername();
42
}
43
}
44
45
function handleInputTextKeypress(event) {
46
if (event.keyCode === 13) {
47
tc.currentChannel.sendMessage($(this).val());
48
event.preventDefault();
49
$(this).val('');
50
}
51
else {
52
notifyTyping();
53
}
54
}
55
56
var notifyTyping = $.throttle(function () {
57
tc.currentChannel.typing();
58
}, 1000);
59
60
tc.handleNewChannelInputKeypress = function (event) {
61
if (event.keyCode === 13) {
62
tc.messagingClient.createChannel({
63
friendlyName: $newChannelInput.val()
64
}).then(hideAddChannelInput);
65
$(this).val('');
66
event.preventDefault();
67
}
68
};
69
70
function connectClientWithUsername() {
71
var usernameText = $usernameInput.val();
72
$usernameInput.val('');
73
if (usernameText == '') {
74
alert('Username cannot be empty');
75
return;
76
}
77
tc.username = usernameText;
78
fetchAccessToken(tc.username, connectMessagingClient);
79
}
80
81
function fetchAccessToken(username, handler) {
82
$.post('/token', {
83
identity: username,
84
device: 'browser'
85
}, function (data) {
86
handler(data);
87
}, 'json');
88
}
89
90
function connectMessagingClient(tokenResponse) {
91
// Initialize the IP messaging client
92
tc.accessManager = new Twilio.AccessManager(tokenResponse.token);
93
tc.messagingClient = new Twilio.IPMessaging.Client(tc.accessManager);
94
updateConnectedUI();
95
tc.loadChannelList(tc.joinGeneralChannel);
96
tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));
97
tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));
98
tc.messagingClient.on('tokenExpired', refreshToken);
99
}
100
101
function refreshToken() {
102
fetchAccessToken(tc.username, setNewToken);
103
}
104
105
function setNewToken(tokenResponse) {
106
tc.accessManager.updateToken(tokenResponse.token);
107
}
108
109
function updateConnectedUI() {
110
$('#username-span').text(tc.username);
111
$statusRow.addClass('connected').removeClass('disconnected');
112
tc.$messageList.addClass('connected').removeClass('disconnected');
113
$connectPanel.addClass('connected').removeClass('disconnected');
114
$inputText.addClass('with-shadow');
115
$typingRow.addClass('connected').removeClass('disconnected');
116
}
117
118
tc.loadChannelList = function (handler) {
119
if (tc.messagingClient === undefined) {
120
console.log('Client is not initialized');
121
return;
122
}
123
124
tc.messagingClient.getChannels().then(function (channels) {
125
tc.channelArray = tc.sortChannelsByName(channels);
126
$channelList.text('');
127
tc.channelArray.forEach(addChannel);
128
if (typeof handler === 'function') {
129
handler();
130
}
131
});
132
};
133
134
tc.joinGeneralChannel = function () {
135
console.log('Attempting to join "general" chat channel...');
136
if (!tc.generalChannel) {
137
// If it doesn't exist, let's create it
138
tc.messagingClient.createChannel({
139
uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,
140
friendlyName: GENERAL_CHANNEL_NAME
141
}).then(function (channel) {
142
console.log('Created general channel');
143
tc.generalChannel = channel;
144
tc.loadChannelList(tc.joinGeneralChannel);
145
});
146
}
147
else {
148
console.log('Found general channel:');
149
setupChannel(tc.generalChannel);
150
}
151
};
152
153
function setupChannel(channel) {
154
// Join the channel
155
channel.join().then(function (joinedChannel) {
156
console.log('Joined channel ' + joinedChannel.friendlyName);
157
leaveCurrentChannel();
158
updateChannelUI(channel);
159
tc.currentChannel = channel;
160
tc.loadMessages();
161
channel.on('messageAdded', tc.addMessageToList);
162
channel.on('typingStarted', showTypingStarted);
163
channel.on('typingEnded', hideTypingStarted);
164
channel.on('memberJoined', notifyMemberJoined);
165
channel.on('memberLeft', notifyMemberLeft);
166
$inputText.prop('disabled', false).focus();
167
tc.$messageList.text('');
168
});
169
}
170
171
tc.loadMessages = function () {
172
tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT).then(function (messages) {
173
messages.forEach(tc.addMessageToList);
174
});
175
};
176
177
function leaveCurrentChannel() {
178
if (tc.currentChannel) {
179
tc.currentChannel.leave().then(function (leftChannel) {
180
console.log('left ' + leftChannel.friendlyName);
181
leftChannel.removeListener('messageAdded', tc.addMessageToList);
182
leftChannel.removeListener('typingStarted', showTypingStarted);
183
leftChannel.removeListener('typingEnded', hideTypingStarted);
184
leftChannel.removeListener('memberJoined', notifyMemberJoined);
185
leftChannel.removeListener('memberLeft', notifyMemberLeft);
186
});
187
}
188
}
189
190
tc.addMessageToList = function (message) {
191
var rowDiv = $('<div>').addClass('row no-margin');
192
rowDiv.loadTemplate($('#message-template'), {
193
username: message.author,
194
date: dateFormatter.getTodayDate(message.timestamp),
195
body: message.body
196
});
197
if (message.author === tc.username) {
198
rowDiv.addClass('own-message');
199
}
200
201
tc.$messageList.append(rowDiv);
202
scrollToMessageListBottom();
203
};
204
205
function notifyMemberJoined(member) {
206
notify(member.identity + ' joined the channel')
207
}
208
209
function notifyMemberLeft(member) {
210
notify(member.identity + ' left the channel');
211
}
212
213
function notify(message) {
214
var row = $('<div>').addClass('col-md-12');
215
row.loadTemplate('#member-notification-template', {
216
status: message
217
});
218
tc.$messageList.append(row);
219
scrollToMessageListBottom();
220
}
221
222
function showTypingStarted(member) {
223
$typingPlaceholder.text(member.identity + ' is typing...');
224
}
225
226
function hideTypingStarted(member) {
227
$typingPlaceholder.text('');
228
}
229
230
function scrollToMessageListBottom() {
231
tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);
232
}
233
234
function updateChannelUI(selectedChannel) {
235
var channelElements = $('.channel-element').toArray();
236
var channelElement = channelElements.filter(function (element) {
237
return $(element).data().sid === selectedChannel.sid;
238
});
239
channelElement = $(channelElement);
240
if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {
241
tc.currentChannelContainer = channelElement;
242
}
243
tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');
244
channelElement.removeClass('unselected-channel').addClass('selected-channel');
245
tc.currentChannelContainer = channelElement;
246
}
247
248
function showAddChannelInput() {
249
if (tc.messagingClient) {
250
$newChannelInputRow.addClass('showing').removeClass('not-showing');
251
$channelList.addClass('showing').removeClass('not-showing');
252
$newChannelInput.focus();
253
}
254
}
255
256
function hideAddChannelInput() {
257
$newChannelInputRow.addClass('not-showing').removeClass('showing');
258
$channelList.addClass('not-showing').removeClass('showing');
259
$newChannelInput.val('');
260
}
261
262
function addChannel(channel) {
263
if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {
264
tc.generalChannel = channel;
265
}
266
var rowDiv = $('<div>').addClass('row channel-row');
267
rowDiv.loadTemplate('#channel-template', {
268
channelName: channel.friendlyName
269
});
270
271
var channelP = rowDiv.children().children().first();
272
273
rowDiv.on('click', selectChannel);
274
channelP.data('sid', channel.sid);
275
if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {
276
tc.currentChannelContainer = channelP;
277
channelP.addClass('selected-channel');
278
}
279
else {
280
channelP.addClass('unselected-channel')
281
}
282
283
$channelList.append(rowDiv);
284
}
285
286
function deleteCurrentChannel() {
287
if (!tc.currentChannel) {
288
return;
289
}
290
if (tc.currentChannel.sid === tc.generalChannel.sid) {
291
alert('You cannot delete the general channel');
292
return;
293
}
294
tc.currentChannel.delete().then(function (channel) {
295
console.log('channel: ' + channel.friendlyName + ' deleted');
296
setupChannel(tc.generalChannel);
297
});
298
}
299
300
function selectChannel(event) {
301
var target = $(event.target);
302
var channelSid = target.data().sid;
303
var selectedChannel = tc.channelArray.filter(function (channel) {
304
return channel.sid === channelSid;
305
})[0];
306
if (selectedChannel === tc.currentChannel) {
307
return;
308
}
309
setupChannel(selectedChannel);
310
};
311
312
function disconnectClient() {
313
leaveCurrentChannel();
314
$channelList.text('');
315
tc.$messageList.text('');
316
channels = undefined;
317
$statusRow.addClass('disconnected').removeClass('connected');
318
tc.$messageList.addClass('disconnected').removeClass('connected');
319
$connectPanel.addClass('disconnected').removeClass('connected');
320
$inputText.removeClass('with-shadow');
321
$typingRow.addClass('disconnected').removeClass('connected');
322
}
323
324
tc.sortChannelsByName = function (channels) {
325
return channels.sort(function (a, b) {
326
if (a.friendlyName === GENERAL_CHANNEL_NAME) {
327
return -1;
328
}
329
if (b.friendlyName === GENERAL_CHANNEL_NAME) {
330
return 1;
331
}
332
return a.friendlyName.localeCompare(b.friendlyName);
333
});
334
};
335
336
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 involves calling createChannel with an object that has the friendlyName key. You can create a channel with more options though, see a list of the options here(link takes you to an external page).

TwilioChat.Web/Scripts/twiliochat.js

1
var twiliochat = (function () {
2
var tc = {};
3
4
var GENERAL_CHANNEL_UNIQUE_NAME = 'general';
5
var GENERAL_CHANNEL_NAME = 'General Channel';
6
var MESSAGES_HISTORY_LIMIT = 50;
7
8
var $channelList;
9
var $inputText;
10
var $usernameInput;
11
var $statusRow;
12
var $connectPanel;
13
var $newChannelInputRow;
14
var $newChannelInput;
15
var $typingRow;
16
var $typingPlaceholder;
17
18
$(document).ready(function () {
19
tc.$messageList = $('#message-list');
20
$channelList = $('#channel-list');
21
$inputText = $('#input-text');
22
$usernameInput = $('#username-input');
23
$statusRow = $('#status-row');
24
$connectPanel = $('#connect-panel');
25
$newChannelInputRow = $('#new-channel-input-row');
26
$newChannelInput = $('#new-channel-input');
27
$typingRow = $('#typing-row');
28
$typingPlaceholder = $('#typing-placeholder');
29
$usernameInput.focus();
30
$usernameInput.on('keypress', handleUsernameInputKeypress);
31
$inputText.on('keypress', handleInputTextKeypress);
32
$newChannelInput.on('keypress', tc.handleNewChannelInputKeypress);
33
$('#connect-image').on('click', connectClientWithUsername);
34
$('#add-channel-image').on('click', showAddChannelInput);
35
$('#leave-span').on('click', disconnectClient);
36
$('#delete-channel-span').on('click', deleteCurrentChannel);
37
});
38
39
function handleUsernameInputKeypress(event) {
40
if (event.keyCode === 13) {
41
connectClientWithUsername();
42
}
43
}
44
45
function handleInputTextKeypress(event) {
46
if (event.keyCode === 13) {
47
tc.currentChannel.sendMessage($(this).val());
48
event.preventDefault();
49
$(this).val('');
50
}
51
else {
52
notifyTyping();
53
}
54
}
<