Skip to contentSkip to navigationSkip to topbar
On this page

Chat with Node.js and Express


(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 Programmable Chat Client, Node.js and Express(link takes you to an external page)?

This application allows users to exchange messages through different channels, using the Twilio Programmable Chat API. In this example, we'll show how to use this API capabilities 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 it as required.


Token Generation

token-generation page anchor

In order to create a Twilio Programmable Chat client, you will need an access token. This token holds information about your Twilio Account and Programmable Chat API keys.

We generate this token by creating a new AccessToken and providing it with a ChatGrant. The new AccessToken object is created using your Twilio credentials.

Generate an Access Token

generate-an-access-token page anchor

services/tokenService.js

1
const twilio = require('twilio');
2
3
const AccessToken = twilio.jwt.AccessToken;
4
const ChatGrant = AccessToken.ChatGrant;
5
6
function TokenGenerator(identity) {
7
const appName = 'TwilioChat';
8
9
// Create a "grant" which enables a client to use Chat as a given user
10
const chatGrant = new ChatGrant({
11
serviceSid: process.env.TWILIO_CHAT_SERVICE_SID,
12
});
13
14
// Create an access token which we will sign and return to the client,
15
// containing the grant we just created
16
const token = new AccessToken(
17
process.env.TWILIO_ACCOUNT_SID,
18
process.env.TWILIO_API_KEY,
19
process.env.TWILIO_API_SECRET
20
);
21
22
token.addGrant(chatGrant);
23
token.identity = identity;
24
25
return token;
26
}
27
28
module.exports = { generate: TokenGenerator };
29

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 the endpoint for token generation. This endpoint is responsible for providing a valid token when passed this parameter:

  • identity: identifies the user itself.

Once we have used the tokenService object to generate a token we can use the AccessToken's method as token.toJwt() to get the token as a String. Then we just return the token as a JSON encoded string.

routes/token.js

1
var express = require('express');
2
var router = express.Router();
3
var TokenService = require('../services/tokenService');
4
5
// POST /token
6
router.post('/', function(req, res) {
7
var identity = req.body.identity;
8
9
var token = TokenService.generate(identity)
10
11
res.json({
12
identity: identity,
13
token: token.toJwt(),
14
});
15
});
16
17
// GET /token
18
router.get('/', function(req, res) {
19
var identity = req.query.identity;
20
21
var token = TokenService.generate(identity)
22
23
res.json({
24
identity: identity,
25
token: token.toJwt(),
26
});
27
});
28
29
30
module.exports = router;
31

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 Programmable Chat Client

initialize-the-programmable-chat-client page anchor

Our client fetches a new Token by making a POST request to our endpoint when it calls the fetchAccessToken method.

With the token, we can then instantiate Twilio.Chat.Client in connectMessagingClient.

Initialize the Chat Client

initialize-the-chat-client page anchor

public/js/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.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(tokenResponse.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 instantiated our Chat Client, let's see how we can get a list of channels.


After initializing the client we can use its getPublicChannelDescriptors method to retrieve all visible channels. The method returns a promise which we use to show the list of channels retrieved on the UI.

public/js/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.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(tokenResponse.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

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 Programmable 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.

public/js/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.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(tokenResponse.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 let's listen for some channel events.


Listen to Channel Events

listen-to-channel-events page anchor

Next, we listen for channel events. In our case, we're setting listeners to the following events:

  • messageAdded: When another member sends a message to the channel you are connected to.
  • typingStarted: When another member is typing a message on the channel that you are connected to.
  • typingEnded: When another member stops typing a message on the channel that you are connected to.
  • memberJoined: When another member joins the channel that you are connected to.
  • memberLeft: When another member leaves the channel that you are connected to.

We register a different function to handle each particular event.

public/js/twiliochat.js

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(tokenResponse.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

The client emits events as well. Let's see how we can listen to those events as well.


Just like with channels, we can register handlers for events on the Client:

  • channelAdded: When a channel becomes visible to the Client.
  • channelRemoved: When a channel is no longer visible to the Client.
  • tokenExpired: When the supplied token expires.

For a complete list of client events

public/js/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.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(tokenResponse.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.