Skip to contentSkip to navigationSkip to topbar
On this page

Chat with PHP and Laravel


(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?

This application allows users to exchange messages through different channels, using the Twilio Programmable Chat API. On this example, we'll show how to use this API features to manage channels and to show it's 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.

With Laravel we must create a provider that will inject the AccessToken object in the controller, the same goes for ChatMessagingGrant inside TwilioChatGrantProvider.php. We'll see how to use these objects in the next step.

Generate an Access Token

generate-an-access-token page anchor

app/Providers/TwilioAccessTokenProvider.php

1
<?php
2
namespace App\Providers;
3
use Illuminate\Support\ServiceProvider;
4
use Twilio\Jwt\AccessToken;
5
6
class TwilioAccessTokenProvider extends ServiceProvider
7
{
8
/**
9
* Register the application services.
10
*
11
* @return void
12
*/
13
public function register()
14
{
15
$this->app->bind(
16
AccessToken::class, function ($app) {
17
$TWILIO_ACCOUNT_SID = config('services.twilio')['accountSid'];
18
$TWILIO_API_KEY = config('services.twilio')['apiKey'];
19
$TWILIO_API_SECRET = config('services.twilio')['apiSecret'];
20
21
$token = new AccessToken(
22
$TWILIO_ACCOUNT_SID,
23
$TWILIO_API_KEY,
24
$TWILIO_API_SECRET,
25
3600
26
);
27
28
return $token;
29
}
30
);
31
}
32
}
33

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 responsible for providing a valid token using this parameter:

  • identity: identifies the user itself

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

app/Http/Controllers/TokenController.php

1
<?php
2
namespace App\Http\Controllers;
3
use Illuminate\Http\Request;
4
use App\Http\Requests;
5
use App\Http\Controllers\Controller;
6
use Twilio\Jwt\AccessToken;
7
use Twilio\Jwt\Grants\ChatGrant;
8
9
class TokenController extends Controller
10
{
11
public function generate(Request $request, AccessToken $accessToken, ChatGrant $chatGrant)
12
{
13
$appName = "TwilioChat";
14
$identity = $request->input("identity");
15
16
$TWILIO_CHAT_SERVICE_SID = config('services.twilio')['chatServiceSid'];
17
18
$accessToken->setIdentity($identity);
19
20
$chatGrant->setServiceSid($TWILIO_CHAT_SERVICE_SID);
21
22
$accessToken->addGrant($chatGrant);
23
24
$response = array(
25
'identity' => $identity,
26
'token' => $accessToken->toJWT()
27
);
28
29
return response()->json($response);
30
}
31
}

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


Initializing the Programmable Chat Client

initializing-the-programmable-chat-client page anchor

Our client fetches a new Token by making a POST request to our endpoint.

With the token we can create a new Twilio.AccessManager, and initialize our Twilio.Chat.Client.

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.$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', {identity: username}, null, 'json')
83
.done(function(response) {
84
handler(response.token);
85
})
86
.fail(function(error) {
87
console.log('Failed to fetch the Access Token with error: ' + error);
88
});
89
}
90
91
function connectMessagingClient(token) {
92
// Initialize the Chat messaging client
93
tc.accessManager = new Twilio.AccessManager(token);
94
Twilio.Chat.Client.create(token).then(function(client) {
95
tc.messagingClient = client;
96
updateConnectedUI();
97
tc.loadChannelList(tc.joinGeneralChannel);
98
tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));
99
tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));
100
tc.messagingClient.on('tokenExpired', refreshToken);
101
});
102
}
103
104
function refreshToken() {
105
fetchAccessToken(tc.username, setNewToken);
106
}
107
108
function setNewToken(tokenResponse) {
109
tc.accessManager.updateToken(tokenResponse.token);
110
}
111
112
function updateConnectedUI() {
113
$('#username-span').text(tc.username);
114
$statusRow.addClass('connected').removeClass('disconnected');
115
tc.$messageList.addClass('connected').removeClass('disconnected');
116
$connectPanel.addClass('connected').removeClass('disconnected');
117
$inputText.addClass('with-shadow');
118
$typingRow.addClass('connected').removeClass('disconnected');
119
}
120
121
tc.loadChannelList = function(handler) {
122
if (tc.messagingClient === undefined) {
123
console.log('Client is not initialized');
124
return;
125
}
126
127
tc.messagingClient.getPublicChannelDescriptors().then(function(channels) {
128
tc.channelArray = tc.sortChannelsByName(channels.items);
129
$channelList.text('');
130
tc.channelArray.forEach(addChannel);
131
if (typeof handler === 'function') {
132
handler();
133
}
134
});
135
};
136
137
tc.joinGeneralChannel = function() {
138
console.log('Attempting to join "general" chat channel...');
139
if (!tc.generalChannel) {
140
// If it doesn't exist, let's create it
141
tc.messagingClient.createChannel({
142
uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,
143
friendlyName: GENERAL_CHANNEL_NAME
144
}).then(function(channel) {
145
console.log('Created general channel');
146
tc.generalChannel = channel;
147
tc.loadChannelList(tc.joinGeneralChannel);
148
});
149
}
150
else {
151
console.log('Found general channel:');
152
setupChannel(tc.generalChannel);
153
}
154
};
155
156
function initChannel(channel) {
157
console.log('Initialized channel ' + channel.friendlyName);
158
return tc.messagingClient.getChannelBySid(channel.sid);
159
}
160
161
function joinChannel(_channel) {
162
return _channel.join()
163
.then(function(joinedChannel) {
164
console.log('Joined channel ' + joinedChannel.friendlyName);
165
updateChannelUI(_channel);
166
tc.currentChannel = _channel;
167
tc.loadMessages();
168
return joinedChannel;
169
});
170
}
171
172
function initChannelEvents() {
173
console.log(tc.currentChannel.friendlyName + ' ready.');
174
tc.currentChannel.on('messageAdded', tc.addMessageToList);
175
tc.currentChannel.on('typingStarted', showTypingStarted);
176
tc.currentChannel.on('typingEnded', hideTypingStarted);
177
tc.currentChannel.on('memberJoined', notifyMemberJoined);
178
tc.currentChannel.on('memberLeft', notifyMemberLeft);
179
$inputText.prop('disabled', false).focus();
180
}
181
182
function setupChannel(channel) {
183
return leaveCurrentChannel()
184
.then(function() {
185
return initChannel(channel);
186
})
187
.then(function(_channel) {
188
return joinChannel(_channel);
189
})
190
.then(initChannelEvents);
191
}
192
193
tc.loadMessages = function() {
194
tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT).then(function (messages) {
195
messages.items.forEach(tc.addMessageToList);
196
});
197
};
198
199
function leaveCurrentChannel() {
200
if (tc.currentChannel) {
201
return tc.currentChannel.leave().then(function(leftChannel) {
202
console.log('left ' + leftChannel.friendlyName);
203
leftChannel.removeListener('messageAdded', tc.addMessageToList);
204
leftChannel.removeListener('typingStarted', showTypingStarted);
205
leftChannel.removeListener('typingEnded', hideTypingStarted);
206
leftChannel.removeListener('memberJoined', notifyMemberJoined);
207
leftChannel.removeListener('memberLeft', notifyMemberLeft);
208
});
209
} else {
210
return Promise.resolve();
211
}
212
}
213
214
tc.addMessageToList = function(message) {
215
var rowDiv = $('<div>').addClass('row no-margin');
216
rowDiv.loadTemplate($('#message-template'), {
217
username: message.author,
218
date: dateFormatter.getTodayDate(message.dateCreated),
219
body: message.body
220
});
221
if (message.author === tc.username) {
222
rowDiv.addClass('own-message');
223
}
224
225
tc.$messageList.append(rowDiv);
226
scrollToMessageListBottom();
227
};
228
229
function notifyMemberJoined(member) {
230
notify(member.identity + ' joined the channel')
231
}
232
233
function notifyMemberLeft(member) {
234
notify(member.identity + ' left the channel');
235
}
236
237
function notify(message) {
238
var row = $('<div>').addClass('col-md-12');
239
row.loadTemplate('#member-notification-template', {
240
status: message
241
});
242
tc.$messageList.append(row);
243
scrollToMessageListBottom();
244
}
245
246
function showTypingStarted(member) {
247
$typingPlaceholder.text(member.identity + ' is typing...');
248
}
249
250
function hideTypingStarted(member) {
251
$typingPlaceholder.text('');
252
}
253
254
function scrollToMessageListBottom() {
255
tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);
256
}
257
258
function updateChannelUI(selectedChannel) {
259
var channelElements = $('.channel-element').toArray();
260
var channelElement = channelElements.filter(function(element) {
261
return $(element).data().sid === selectedChannel.sid;
262
});
263
channelElement = $(channelElement);
264
if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {
265
tc.currentChannelContainer = channelElement;
266
}
267
tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');
268
channelElement.removeClass('unselected-channel').addClass('selected-channel');
269
tc.currentChannelContainer = channelElement;
270
}
271
272
function showAddChannelInput() {
273
if (tc.messagingClient) {
274
$newChannelInputRow.addClass('showing').removeClass('not-showing');
275
$channelList.addClass('showing').removeClass('not-showing');
276
$newChannelInput.focus();
277
}
278
}
279
280
function hideAddChannelInput() {
281
$newChannelInputRow.addClass('not-showing').removeClass('showing');
282
$channelList.addClass('not-showing').removeClass('showing');
283
$newChannelInput.val('');
284
}
285
286
function addChannel(channel) {
287
if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {
288
tc.generalChannel = channel;
289
}
290
var rowDiv = $('<div>').addClass('row channel-row');
291
rowDiv.loadTemplate('#channel-template', {
292
channelName: channel.friendlyName
293
});
294
295
var channelP = rowDiv.children().children().first();
296
297
rowDiv.on('click', selectChannel);
298
channelP.data('sid', channel.sid);
299
if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {
300
tc.currentChannelContainer = channelP;
301
channelP.addClass('selected-channel');
302
}
303
else {
304
channelP.addClass('unselected-channel')
305
}
306
307
$channelList.append(rowDiv);
308
}
309
310
function deleteCurrentChannel() {
311
if (!tc.currentChannel) {
312
return;
313
}
314
if (tc.currentChannel.sid === tc.generalChannel.sid) {
315
alert('You cannot delete the general channel');
316
return;
317
}
318
tc.currentChannel.delete().then(function(channel) {
319
console.log('channel: '+ channel.friendlyName + ' deleted');
320
setupChannel(tc.generalChannel);
321
});
322
}
323
324
function selectChannel(event) {
325
var target = $(event.target);
326
var channelSid = target.data().sid;
327
var selectedChannel = tc.channelArray.filter(function(channel) {
328
return channel.sid === channelSid;
329
})[0];
330
if (selectedChannel === tc.currentChannel) {
331
return;
332
}
333
setupChannel(selectedChannel);
334
};
335
336
function disconnectClient() {
337
leaveCurrentChannel();
338
$channelList.text('');
339
tc.$messageList.text('');
340
channels = undefined;
341
$statusRow.addClass('disconnected').removeClass('connected');
342
tc.$messageList.addClass('disconnected').removeClass('connected');
343
$connectPanel.addClass('disconnected').removeClass('connected');
344
$inputText.removeClass('with-shadow');
345
$typingRow.addClass('disconnected').removeClass('connected');
346
}
347
348
tc.sortChannelsByName = function(channels) {
349
return channels.sort(function(a, b) {
350
if (a.friendlyName === GENERAL_CHANNEL_NAME) {
351
return -1;
352
}
353
if (b.friendlyName === GENERAL_CHANNEL_NAME) {
354
return 1;
355
}
356
return a.friendlyName.localeCompare(b.friendlyName);
357
});
358
};
359
360
return tc;
361
})();
362

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


Getting the Channel List

getting-the-channel-list page anchor

After initializing the client we can call the getPublicChannelDescriptors method to retrieve all visible channels. This 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.$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', {identity: username}, null, 'json')
83
.done(function(response) {
84
handler(response.token);
85
})
86
.fail(function(error) {
87
console.log('Failed to fetch the Access Token with error: ' + error);
88
});
89
}
90
91
function connectMessagingClient(token) {
92
// Initialize the Chat messaging client
93
tc.accessManager = new Twilio.AccessManager(token);
94
Twilio.Chat.Client.create(token).then(function(client) {
95
tc.messagingClient = client;
96
updateConnectedUI();
97
tc.loadChannelList(tc.joinGeneralChannel);
98
tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));
99
tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));
100
tc.messagingClient.on('tokenExpired', refreshToken);
101
});
102
}
103
104
function refreshToken() {
105
fetchAccessToken(tc.username, setNewToken);
106
}
107
108
function setNewToken(tokenResponse) {
109
tc.accessManager.updateToken(tokenResponse.token);
110
}
111
112
function updateConnectedUI() {
113
$('#username-span').text(tc.username);
114
$statusRow.addClass('connected').removeClass('disconnected');
115
tc.$messageList.addClass('connected').removeClass('disconnected');
116
$connectPanel.addClass('connected').removeClass('disconnected');
117
$inputText.addClass('with-shadow');
118
$typingRow.addClass('connected').removeClass('disconnected');
119
}
120
121
tc.loadChannelList = function(handler) {
122
if (tc.messagingClient === undefined) {
123
console.log('Client is not initialized');
124
return;
125
}
126
127
tc.messagingClient.getPublicChannelDescriptors().then(function(channels) {
128
tc.channelArray = tc.sortChannelsByName(channels.items);
129
$channelList.text('');
130
tc.channelArray.forEach(addChannel);
131
if (typeof handler === 'function') {
132
handler();
133
}
134
});
135
};
136
137
tc.joinGeneralChannel = function() {
138
console.log('Attempting to join "general" chat channel...');
139
if (!tc.generalChannel) {
140
// If it doesn't exist, let's create it
141
tc.messagingClient.createChannel({
142
uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,
143
friendlyName: GENERAL_CHANNEL_NAME
144
}).then(function(channel) {
145
console.log('Created general channel');
146
tc.generalChannel = channel;
147
tc.loadChannelList(tc.joinGeneralChannel);
148
});
149
}
150
else {
151
console.log('Found general channel:');
152
setupChannel(tc.generalChannel);
153
}
154
};
155
156
function initChannel(channel) {
157
console.log('Initialized channel ' + channel.friendlyName);
158
return tc.messagingClient.getChannelBySid(channel.sid);
159
}
160
161
function joinChannel(_channel) {
162
return _channel.join()
163
.then(function(joinedChannel) {
164
console.log('Joined channel ' + joinedChannel.friendlyName);
165
updateChannelUI(_channel);
166
tc.currentChannel = _channel;
167
tc.loadMessages();
168
return joinedChannel;
169
});
170
}
171
172
function initChannelEvents() {
173
console.log(tc.currentChannel.friendlyName + ' ready.');
174
tc.currentChannel.on('messageAdded', tc.addMessageToList);
175
tc.currentChannel.on('typingStarted', showTypingStarted);
176
tc.currentChannel.on('typingEnded', hideTypingStarted);
177
tc.currentChannel.on('memberJoined', notifyMemberJoined);
178
tc.currentChannel.on('memberLeft', notifyMemberLeft);
179
$inputText.prop('disabled', false).focus();
180
}
181
182
function setupChannel(channel) {
183
return leaveCurrentChannel()
184
.then(function() {
185
return initChannel(channel);
186
})
187
.then(function(_channel) {
188
return joinChannel(_channel);
189
})
190
.then(initChannelEvents);
191
}
192
193
tc.loadMessages = function() {
194
tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT).then(function (messages) {
195
messages.items.forEach(tc.addMessageToList);
196
});
197
};
198
199
function leaveCurrentChannel() {
200
if (tc.currentChannel) {
201
return tc.currentChannel.leave().then(function(leftChannel) {
202
console.log('left ' + leftChannel.friendlyName);
203
leftChannel.removeListener('messageAdded', tc.addMessageToList);
204
leftChannel.removeListener('typingStarted', showTypingStarted);
205
leftChannel.removeListener('typingEnded', hideTypingStarted);
206
leftChannel.removeListener('memberJoined', notifyMemberJoined);
207
leftChannel.removeListener('memberLeft', notifyMemberLeft);
208
});
209
} else {
210
return Promise.resolve();
211
}
212
}
213
214
tc.addMessageToList = function(message) {
215
var rowDiv = $('<div>').addClass('row no-margin');
216
rowDiv.loadTemplate($('#message-template'), {
217
username: message.author,
218
date: dateFormatter.getTodayDate(message.dateCreated),
219
body: message.body
220
});
221
if (message.author === tc.username) {
222
rowDiv.addClass('own-message');
223
}
224
225
tc.$messageList.append(rowDiv);
226
scrollToMessageListBottom();
227
};
228
229
function notifyMemberJoined(member) {
230
notify(member.identity + ' joined the channel')
231
}
232
233
function notifyMemberLeft(member) {
234
notify(member.identity + ' left the channel');
235
}
236
237
function notify(message) {
238
var row = $('<div>').addClass('col-md-12');
239
row.loadTemplate('#member-notification-template', {
240
status: message
241
});
242
tc.$messageList.append(row);
243
scrollToMessageListBottom();
244
}
245
246
function showTypingStarted(member) {
247
$typingPlaceholder.text(member.identity + ' is typing...');
248
}
249
250
function hideTypingStarted(member) {
251
$typingPlaceholder.text('');
252
}
253
254
function scrollToMessageListBottom() {
255
tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);
256
}
257
258
function updateChannelUI(selectedChannel) {
259
var channelElements = $('.channel-element').toArray();
260
var channelElement = channelElements.filter(function(element) {
261
return $(element).data().sid === selectedChannel.sid;
262
});
263
channelElement = $(channelElement);
264
if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {
265
tc.currentChannelContainer = channelElement;
266
}
267
tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');
268
channelElement.removeClass('unselected-channel').addClass('selected-channel');
269
tc.currentChannelContainer = channelElement;
270
}
271
272
function showAddChannelInput() {
273
if (tc.messagingClient) {
274
$newChannelInputRow.addClass('showing').removeClass('not-showing');
275
$channelList.addClass('showing').removeClass('not-showing');
276
$newChannelInput.focus();
277
}
278
}
279
280
function hideAddChannelInput() {
281
$newChannelInputRow.addClass('not-showing').removeClass('showing');
282
$channelList.addClass('not-showing').removeClass('showing');
283
$newChannelInput.val('');
284
}
285
286
function addChannel(channel) {
287
if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {
288
tc.generalChannel = channel;
289
}
290
var rowDiv = $('<div>').addClass('row channel-row');
291
rowDiv.loadTemplate('#channel-template', {
292
channelName: channel.friendlyName
293
});
294
295
var channelP = rowDiv.children().children().first();
296
297
rowDiv.on('click', selectChannel);
298
channelP.data('sid', channel.sid);
299
if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {
300
tc.currentChannelContainer = channelP;
301
channelP.addClass('selected-channel');
302
}
303
else {
304
channelP.addClass('unselected-channel')
305
}
306
307
$channelList.append(rowDiv);
308
}
309
310
function deleteCurrentChannel() {
311
if (!tc.currentChannel) {
312
return;
313
}
314
if (tc.currentChannel.sid === tc.generalChannel.sid) {
315
alert('You cannot delete the general channel');
316
return;
317
}
318
tc.currentChannel.delete().then(function(channel) {
319
console.log('channel: '+ channel.friendlyName + ' deleted');
320
setupChannel(tc.generalChannel);
321
});
322
}
323
324
function selectChannel(event) {
325
var target = $(event.target);
326
var channelSid = target.data().sid;
327
var selectedChannel = tc.channelArray.filter(function(channel) {
328
return channel.sid === channelSid;
329
})[0];
330
if (selectedChannel === tc.currentChannel) {
331
return;
332
}
333
setupChannel(selectedChannel);
334
};
335
336
function disconnectClient() {
337
leaveCurrentChannel();
338
$channelList.text('');
339
tc.$messageList.text('');
340
channels = undefined;
341
$statusRow.addClass('disconnected').removeClass('connected');
342
tc.$messageList.addClass('disconnected').removeClass('connected');
343
$connectPanel.addClass('disconnected').removeClass('connected');
344
$inputText.removeClass('with-shadow');
345
$typingRow.addClass('disconnected').removeClass('connected');
346
}
347
348
tc.sortChannelsByName = function(channels) {
349
return channels.sort(function(a, b) {
350
if (a.friendlyName === GENERAL_CHANNEL_NAME) {
351
return -1;
352
}
353
if (b.friendlyName === GENERAL_CHANNEL_NAME) {
354
return 1;
355
}
356
return a.friendlyName.localeCompare(b.friendlyName);
357
});
358
};
359
360
return tc;
361
})();
362

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 since 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.$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', {identity: username}, null, 'json')
83
.done(function(response) {
84
handler(response.token);
85
})
86
.fail(function(error) {
87
console.log('Failed to fetch the Access Token with error: ' + error);
88
});
89
}
90
91
function connectMessagingClient(token) {
92
// Initialize the Chat messaging client
93
tc.accessManager = new Twilio.AccessManager(token);
94
Twilio.Chat.Client.create(token).then(function(client) {
95
tc.messagingClient = client;
96
updateConnectedUI();
97
tc.loadChannelList(tc.joinGeneralChannel);
98
tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));
99
tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));
100
tc.messagingClient.on('tokenExpired', refreshToken);
101
});
102
}
103
104
function refreshToken() {
105
fetchAccessToken(tc.username, setNewToken);
106
}
107
108
function setNewToken(tokenResponse) {
109
tc.accessManager.updateToken(tokenResponse.token);
110
}
111
112
function updateConnectedUI() {
113
$('#username-span').text(tc.username);
114
$statusRow.addClass('connected').removeClass('disconnected');
115
tc.$messageList.addClass('connected').removeClass('disconnected');
116
$connectPanel.addClass('connected').removeClass('disconnected');
117
$inputText.addClass('with-shadow');
118
$typingRow.addClass('connected').removeClass('disconnected');
119
}
120
121
tc.loadChannelList = function(handler) {
122
if (tc.messagingClient === undefined) {
123
console.log('Client is not initialized');
124
return;
125
}
126
127
tc.messagingClient.getPublicChannelDescriptors().then(function(channels) {
128
tc.channelArray = tc.sortChannelsByName(channels.items);
129
$channelList.text('');
130
tc.channelArray.forEach(addChannel);
131
if (typeof handler === 'function') {
132
handler();
133
}
134
});
135
};
136
137
tc.joinGeneralChannel = function() {
138
console.log('Attempting to join "general" chat channel...');
139
if (!tc.generalChannel) {
140
// If it doesn't exist, let's create it
141
tc.messagingClient.createChannel({
142
uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,
143
friendlyName: GENERAL_CHANNEL_NAME
144
}).then(function(channel) {
145
console.log('Created general channel');
146
tc.generalChannel = channel;
147
tc.loadChannelList(tc.joinGeneralChannel);
148
});
149
}
150
else {
151
console.log('Found general channel:');
152
setupChannel(tc.generalChannel);
153
}
154
};
155
156
function initChannel(channel) {
157
console.log('Initialized channel ' + channel.friendlyName);
158
return tc.messagingClient.getChannelBySid(channel.sid);
159
}
160
161
function joinChannel(_channel) {
162
return _channel.join()
163
.then(function(joinedChannel) {
164
console.log('Joined channel ' + joinedChannel.friendlyName);
165
updateChannelUI(_channel);
166
tc.currentChannel = _channel;
167
tc.loadMessages();
168
return joinedChannel;
169
});
170
}
171
172
function initChannelEvents() {
173
console.log(tc.currentChannel.friendlyName + ' ready.');
174
tc.currentChannel.on('messageAdded', tc.addMessageToList);
175
tc.currentChannel.on('typingStarted', showTypingStarted);
176
tc.currentChannel.on('typingEnded', hideTypingStarted);
177
tc.currentChannel.on('memberJoined', notifyMemberJoined);
178
tc.currentChannel.on('memberLeft', notifyMemberLeft);
179
$inputText.prop('disabled', false).focus();
180
}
181
182
function setupChannel(channel) {
183
return leaveCurrentChannel()
184
.then(function() {
185
return initChannel(channel);
186
})
187
.then(function(_channel) {
188
return joinChannel(_channel);
189
})
190
.then(initChannelEvents);
191
}
192
193
tc.loadMessages = function() {
194
tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT).then(function (messages) {
195
messages.items.forEach(tc.addMessageToList);
196
});
197
};
198
199
function leaveCurrentChannel() {
200
if (tc.currentChannel) {
201
return tc.currentChannel.leave().then(function(leftChannel) {
202
console.log('left ' + leftChannel.friendlyName);
203
leftChannel.removeListener('messageAdded', tc.addMessageToList);
204
leftChannel.removeListener('typingStarted', showTypingStarted);
205
leftChannel.removeListener('typingEnded', hideTypingStarted);
206
leftChannel.removeListener('memberJoined', notifyMemberJoined);
207
leftChannel.removeListener('memberLeft', notifyMemberLeft);
208
});
209
} else {
210
return Promise.resolve();
211
}
212
}
213
214
tc.addMessageToList = function(message) {
215
var rowDiv = $('<div>').addClass('row no-margin');
216
rowDiv.loadTemplate($('#message-template'), {
217
username: message.author,
218
date: dateFormatter.getTodayDate(message.dateCreated),
219
body: message.body
220
});
221
if (message.author === tc.username) {
222
rowDiv.addClass('own-message');
223
}
224
225
tc.$messageList.append(rowDiv);
226
scrollToMessageListBottom();
227
};
228
229
function notifyMemberJoined(member) {
230
notify(member.identity + ' joined the channel')
231
}
232
233
function notifyMemberLeft(member) {
234
notify(member.identity + ' left the channel');
235
}
236
237
function notify(message) {
238
var row = $('<div>').addClass('col-md-12');
239
row.loadTemplate('#member-notification-template', {
240
status: message
241
});
242
tc.$messageList.append(row);
243
scrollToMessageListBottom();
244
}
245
246
function showTypingStarted(member) {
247
$typingPlaceholder.text(member.identity + ' is typing...');
248
}
249
250
function hideTypingStarted(member) {
251
$typingPlaceholder.text('');
252
}
253
254
function scrollToMessageListBottom() {
255
tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);
256
}
257
258
function updateChannelUI(selectedChannel) {
259
var channelElements = $('.channel-element').toArray();
260
var channelElement = channelElements.filter(function(element) {
261
return $(element).data().sid === selectedChannel.sid;
262
});
263
channelElement = $(channelElement);
264
if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {
265
tc.currentChannelContainer = channelElement;
266
}
267
tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');
268
channelElement.removeClass('unselected-channel').addClass('selected-channel');
269
tc.currentChannelContainer = channelElement;
270
}
271
272
function showAddChannelInput() {
273
if (tc.messagingClient) {
274
$newChannelInputRow.addClass('showing').removeClass('not-showing');
275
$channelList.addClass('showing').removeClass('not-showing');
276
$newChannelInput.focus();
277
}
278
}
279
280
function hideAddChannelInput() {
281
$newChannelInputRow.addClass('not-showing').removeClass('showing');
282
$channelList.addClass('not-showing').removeClass('showing');
283
$newChannelInput.val('');
284
}
285
286
function addChannel(channel) {
287
if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {
288
tc.generalChannel = channel;
289
}
290
var rowDiv = $('<div>').addClass('row channel-row');
291
rowDiv.loadTemplate('#channel-template', {
292
channelName: channel.friendlyName
293
});
294
295
var channelP = rowDiv.children().children().first();
296
297
rowDiv.on('click', selectChannel);
298
channelP.data('sid', channel.sid);
299
if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {
300
tc.currentChannelContainer = channelP;
301
channelP.addClass('selected-channel');
302
}
303
else {
304
channelP.addClass('unselected-channel')
305
}
306
307
$channelList.append(rowDiv);
308
}
309
310
function deleteCurrentChannel() {
311
if (!tc.currentChannel) {
312
return;
313
}
314
if (tc.currentChannel.sid === tc.generalChannel.sid) {
315
alert('You cannot delete the general channel');
316
return;
317
}
318
tc.currentChannel.delete().then(function(channel) {
319
console.log('channel: '+ channel.friendlyName + ' deleted');
320
setupChannel(tc.generalChannel);
321
});
322
}
323
324
function selectChannel(event) {
325
var target = $(event.target);
326
var channelSid = target.data().sid;
327
var selectedChannel = tc.channelArray.filter(function(channel) {
328
return channel.sid === channelSid;
329
})[0];
330
if (selectedChannel === tc.currentChannel) {
331
return;
332
}
333
setupChannel(selectedChannel);
334
};
335
336
function disconnectClient() {
337
leaveCurrentChannel();
338
$channelList.text('');
339
tc.$messageList.text('');
340
channels = undefined;
341
$statusRow.addClass('disconnected').removeClass('connected');
342
tc.$messageList.addClass('disconnected').removeClass('connected');
343
$connectPanel.addClass('disconnected').removeClass('connected');
344
$inputText.removeClass('with-shadow');
345
$typingRow.addClass('disconnected').removeClass('connected');
346
}
347
348
tc.sortChannelsByName = function(channels) {
349
return channels.sort(function(a, b) {
350
if (a.friendlyName === GENERAL_CHANNEL_NAME) {
351
return -1;
352
}
353
if (b.friendlyName === GENERAL_CHANNEL_NAME) {
354
return 1;
355
}
356
return a.friendlyName.localeCompare(b.friendlyName);
357
});
358
};
359
360
return tc;
361
})();
362

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.$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', {identity: username}, null, 'json')
83
.done(function(response) {
84
handler(response.token);
85
})
86
.fail(function(error) {
87
console.log('Failed to fetch the Access Token with error: ' + error);
88
});
89
}
90
91
function connectMessagingClient(token) {
92
// Initialize the Chat messaging client
93
tc.accessManager = new Twilio.AccessManager(token);
94
Twilio.Chat.Client.create(token).then(function(client) {
95
tc.messagingClient = client;
96
updateConnectedUI();
97
tc.loadChannelList(tc.joinGeneralChannel);
98
tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));
99
tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));
100
tc.messagingClient.on('tokenExpired', refreshToken);
101
});
102
}
103
104
function refreshToken() {
105
fetchAccessToken(tc.username, setNewToken);
106
}
107
108
function setNewToken(tokenResponse) {
109
tc.accessManager.updateToken(tokenResponse.token);
110
}
111
112
function updateConnectedUI() {
113
$('#username-span').text(tc.username);
114
$statusRow.addClass('connected').removeClass('disconnected');
115
tc.$messageList.addClass('connected').removeClass('disconnected');
116
$connectPanel.addClass('connected').removeClass('disconnected');
117
$inputText.addClass('with-shadow');
118
$typingRow.addClass('connected').removeClass('disconnected');
119
}
120
121
tc.loadChannelList = function(handler) {
122
if (tc.messagingClient === undefined) {
123
console.log('Client is not initialized');
124
return;
125
}
126
127
tc.messagingClient.getPublicChannelDescriptors().then(function(channels) {
128
tc.channelArray = tc.sortChannelsByName(channels.items);
129
$channelList.text('');
130
tc.channelArray.forEach(addChannel);
131
if (typeof handler === 'function') {
132
handler();
133
}
134
});
135
};
136
137
tc.joinGeneralChannel = function() {
138
console.log('Attempting to join "general" chat channel...');
139
if (!tc.generalChannel) {
140
// If it doesn't exist, let's create it
141
tc.messagingClient.createChannel({
142
uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,
143
friendlyName: GENERAL_CHANNEL_NAME
144
}).then(function(channel) {
145
console.log('Created general channel');
146
tc.generalChannel = channel;
147
tc.loadChannelList(tc.joinGeneralChannel);
148
});
149
}
150
else {
151
console.log('Found general channel:');
152
setupChannel(tc.generalChannel);
153
}
154
};
155
156
function initChannel(channel) {
157
console.log('Initialized channel ' + channel.friendlyName);
158
return tc.messagingClient.getChannelBySid(channel.sid);
159
}
160
161
function joinChannel(_channel) {
162
return _channel.join()
163
.then(function(joinedChannel) {
164
console.log('Joined channel ' + joinedChannel.friendlyName);
165
updateChannelUI(_channel);
166
tc.currentChannel = _channel;
167
tc.loadMessages();
168
return joinedChannel;
169
});
170
}
171
172
function initChannelEvents() {
173
console.log(tc.currentChannel.friendlyName + ' ready.');
174
tc.currentChannel.on('messageAdded', tc.addMessageToList);
175
tc.currentChannel.on('typingStarted', showTypingStarted);
176
tc.currentChannel.on('typingEnded', hideTypingStarted);
177
tc.currentChannel.on('memberJoined', notifyMemberJoined);
178
tc.currentChannel.on('memberLeft', notifyMemberLeft);
179
$inputText.prop('disabled', false).focus();
180
}
181
182
function setupChannel(channel) {
183
return leaveCurrentChannel()
184
.then(function() {
185
return initChannel(channel);
186
})
187
.then(function(_channel) {
188
return joinChannel(_channel);
189
})
190
.then(initChannelEvents);
191
}
192
193
tc.loadMessages = function() {
194
tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT).then(function (messages) {
195
messages.items.forEach(tc.addMessageToList);
196
});
197
};
198
199
function leaveCurrentChannel() {
200
if (tc.currentChannel) {
201
return tc.currentChannel.leave().then(function(leftChannel) {
202
console.log('left ' + leftChannel.friendlyName);
203
leftChannel.removeListener('messageAdded', tc.addMessageToList);
204
leftChannel.removeListener('typingStarted', showTypingStarted);
205
leftChannel.removeListener('typingEnded', hideTypingStarted);
206
leftChannel.removeListener('memberJoined', notifyMemberJoined);
207
leftChannel.removeListener('memberLeft', notifyMemberLeft);
208
});
209
} else {
210
return Promise.resolve();
211
}
212
}
213
214
tc.addMessageToList = function(message) {
215
var rowDiv = $('<div>').addClass('row no-margin');
216
rowDiv.loadTemplate($('#message-template'), {
217
username: message.author,
218
date: dateFormatter.getTodayDate(message.dateCreated),
219
body: message.body
220
});
221
if (message.author === tc.username) {
222
rowDiv.addClass('own-message');
223
}
224
225
tc.$messageList.append(rowDiv);
226
scrollToMessageListBottom();
227
};
228
229
function notifyMemberJoined(member) {
230
notify(member.identity + ' joined the channel')
231
}
232
233
function notifyMemberLeft(member) {
234
notify(member.identity + ' left the channel');
235
}
236
237
function notify(message) {
238
var row = $('<div>').addClass('col-md-12');
239
row.loadTemplate('#member-notification-template', {
240
status: message
241
});
242
tc.$messageList.append(row);
243
scrollToMessageListBottom();
244
}
245
246
function showTypingStarted(member) {
247
$typingPlaceholder.text(member.identity + ' is typing...');
248
}
249
250
function hideTypingStarted(member) {
251
$typingPlaceholder.text('');
252
}
253
254
function scrollToMessageListBottom() {
255
tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);
256
}
257
258
function updateChannelUI(selectedChannel) {
259
var channelElements = $('.channel-element').toArray();
260
var channelElement = channelElements.filter(function(element) {
261
return $(element).data().sid === selectedChannel.sid;
262
});
263
channelElement = $(channelElement);
264
if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {
265
tc.currentChannelContainer = channelElement;
266
}
267
tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');
268
channelElement.removeClass('unselected-channel').addClass('selected-channel');
269
tc.currentChannelContainer = channelElement;
270
}
271
272
function showAddChannelInput() {
273
if (tc.messagingClient) {
274
$newChannelInputRow.addClass('showing').removeClass('not-showing');
275
$channelList.addClass('showing').removeClass('not-showing');
276
$newChannelInput.focus();
277
}
278
}
279
280
function hideAddChannelInput() {
281
$newChannelInputRow.addClass('not-showing').removeClass('showing');
282
$channelList.addClass('not-showing').removeClass('showing');
283
$newChannelInput.val('');
284
}
285
286
function addChannel(channel) {
287
if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {
288
tc.generalChannel = channel;
289
}
290
var rowDiv = $('<div>').addClass('row channel-row');
291
rowDiv.loadTemplate('#channel-template', {
292
channelName: channel.friendlyName
293
});
294
295
var channelP = rowDiv.children().children().first();
296
297
rowDiv.on('click', selectChannel);
298
channelP.data('sid', channel.sid);
299
if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {
300
tc.currentChannelContainer = channelP;
301
channelP.addClass('selected-channel');
302
}
303
else {
304
channelP.addClass('unselected-channel')
305
}
306
307
$channelList.append(rowDiv);
308
}
309
310
function deleteCurrentChannel() {
311
if (!tc.currentChannel) {
312
return;
313
}
314
if (tc.currentChannel.sid === tc.generalChannel.sid) {
315
alert('You cannot delete the general channel');
316
return;
317
}
318
tc.currentChannel.delete().then(function(channel) {
319
console.log('channel: '+ channel.friendlyName + ' deleted');
320
setupChannel(tc.generalChannel);
321
});
322
}
323
324
function selectChannel(event) {
325
var target = $(event.target);
326
var channelSid = target.data().sid;
327
var selectedChannel = tc.channelArray.filter(function(channel) {
328
return channel.sid === channelSid;
329
})[0];
330
if (selectedChannel === tc.currentChannel) {
331
return;
332
}
333
setupChannel(selectedChannel);
334
};
335
336
function disconnectClient() {
337
leaveCurrentChannel();
338
$channelList.text('');
339
tc.$messageList.text('');
340
channels = undefined;
341
$statusRow.addClass('disconnected').removeClass('connected');
342
tc.$messageList.addClass('disconnected').removeClass('connected');
343
$connectPanel.addClass('disconnected').removeClass('connected');
344
$inputText.removeClass('with-shadow');
345
$typingRow.addClass('disconnected').removeClass('connected');
346
}
347
348
tc.sortChannelsByName = function(channels) {
349
return channels.sort(function(a, b) {
350
if (a.friendlyName === GENERAL_CHANNEL_NAME) {
351
return -1;
352
}
353
if (b.friendlyName === GENERAL_CHANNEL_NAME) {
354
return 1;
355
}
356
return a.friendlyName.localeCompare(b.friendlyName);
357
});
358
};
359
360
return tc;
361
})();
362

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.

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.$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', {identity: username}, null, 'json')
83
.done(function(response) {
84
handler(response.token);
85
})
86
.fail(function(error) {
87
console.log('Failed to fetch the Access Token with error: ' + error);
88
});
89
}
90
91
function connectMessagingClient(token) {
92
// Initialize the Chat messaging client
93
tc.accessManager = new Twilio.AccessManager(token);
94
Twilio.Chat.Client.create(token).then(function(client) {
95
tc.messagingClient = client;
96
updateConnectedUI();
97
tc.loadChannelList(tc.joinGeneralChannel);
98
tc.messagingClient.on('channelAdded', $.throttle(tc.loadChannelList));
99
tc.messagingClient.on('channelRemoved', $.throttle(tc.loadChannelList));
100
tc.messagingClient.on('tokenExpired', refreshToken);
101
});
102
}
103
104
function refreshToken() {
105
fetchAccessToken(tc.username, setNewToken);
106
}
107
108
function setNewToken(tokenResponse) {
109
tc.accessManager.updateToken(tokenResponse.token);
110
}
111
112
function updateConnectedUI() {
113
$('#username-span').text(tc.username);
114
$statusRow.addClass('connected').removeClass('disconnected');
115
tc.$messageList.addClass('connected').removeClass('disconnected');
116
$connectPanel.addClass('connected').removeClass('disconnected');
117
$inputText.addClass('with-shadow');
118
$typingRow.addClass('connected').removeClass('disconnected');
119
}
120
121
tc.loadChannelList = function(handler) {
122
if (tc.messagingClient === undefined) {
123
console.log('Client is not initialized');
124
return;
125
}
126
127
tc.messagingClient.getPublicChannelDescriptors().then(function(channels) {
128
tc.channelArray = tc.sortChannelsByName(channels.items);
129
$channelList.text('');
130
tc.channelArray.forEach(addChannel);
131
if (typeof handler === 'function') {
132
handler();
133
}
134
});
135
};
136
137
tc.joinGeneralChannel = function() {
138
console.log('Attempting to join "general" chat channel...');
139
if (!tc.generalChannel) {
140
// If it doesn't exist, let's create it
141
tc.messagingClient.createChannel({
142
uniqueName: GENERAL_CHANNEL_UNIQUE_NAME,
143
friendlyName: GENERAL_CHANNEL_NAME
144
}).then(function(channel) {
145
console.log('Created general channel');
146
tc.generalChannel = channel;
147
tc.loadChannelList(tc.joinGeneralChannel);
148
});
149
}
150
else {
151
console.log('Found general channel:');
152
setupChannel(tc.generalChannel);
153
}
154
};
155
156
function initChannel(channel) {
157
console.log('Initialized channel ' + channel.friendlyName);
158
return tc.messagingClient.getChannelBySid(channel.sid);
159
}
160
161
function joinChannel(_channel) {
162
return _channel.join()
163
.then(function(joinedChannel) {
164
console.log('Joined channel ' + joinedChannel.friendlyName);
165
updateChannelUI(_channel);
166
tc.currentChannel = _channel;
167
tc.loadMessages();
168
return joinedChannel;
169
});
170
}
171
172
function initChannelEvents() {
173
console.log(tc.currentChannel.friendlyName + ' ready.');
174
tc.currentChannel.on('messageAdded', tc.addMessageToList);
175
tc.currentChannel.on('typingStarted', showTypingStarted);
176
tc.currentChannel.on('typingEnded', hideTypingStarted);
177
tc.currentChannel.on('memberJoined', notifyMemberJoined);
178
tc.currentChannel.on('memberLeft', notifyMemberLeft);
179
$inputText.prop('disabled', false).focus();
180
}
181
182
function setupChannel(channel) {
183
return leaveCurrentChannel()
184
.then(function() {
185
return initChannel(channel);
186
})
187
.then(function(_channel) {
188
return joinChannel(_channel);
189
})
190
.then(initChannelEvents);
191
}
192
193
tc.loadMessages = function() {
194
tc.currentChannel.getMessages(MESSAGES_HISTORY_LIMIT).then(function (messages) {
195
messages.items.forEach(tc.addMessageToList);
196
});
197
};
198
199
function leaveCurrentChannel() {
200
if (tc.currentChannel) {
201
return tc.currentChannel.leave().then(function(leftChannel) {
202
console.log('left ' + leftChannel.friendlyName);
203
leftChannel.removeListener('messageAdded', tc.addMessageToList);
204
leftChannel.removeListener('typingStarted', showTypingStarted);
205
leftChannel.removeListener('typingEnded', hideTypingStarted);
206
leftChannel.removeListener('memberJoined', notifyMemberJoined);
207
leftChannel.removeListener('memberLeft', notifyMemberLeft);
208
});
209
} else {
210
return Promise.resolve();
211
}
212
}
213
214
tc.addMessageToList = function(message) {
215
var rowDiv = $('<div>').addClass('row no-margin');
216
rowDiv.loadTemplate($('#message-template'), {
217
username: message.author,
218
date: dateFormatter.getTodayDate(message.dateCreated),
219
body: message.body
220
});
221
if (message.author === tc.username) {
222
rowDiv.addClass('own-message');
223
}
224
225
tc.$messageList.append(rowDiv);
226
scrollToMessageListBottom();
227
};
228
229
function notifyMemberJoined(member) {
230
notify(member.identity + ' joined the channel')
231
}
232
233
function notifyMemberLeft(member) {
234
notify(member.identity + ' left the channel');
235
}
236
237
function notify(message) {
238
var row = $('<div>').addClass('col-md-12');
239
row.loadTemplate('#member-notification-template', {
240
status: message
241
});
242
tc.$messageList.append(row);
243
scrollToMessageListBottom();
244
}
245
246
function showTypingStarted(member) {
247
$typingPlaceholder.text(member.identity + ' is typing...');
248
}
249
250
function hideTypingStarted(member) {
251
$typingPlaceholder.text('');
252
}
253
254
function scrollToMessageListBottom() {
255
tc.$messageList.scrollTop(tc.$messageList[0].scrollHeight);
256
}
257
258
function updateChannelUI(selectedChannel) {
259
var channelElements = $('.channel-element').toArray();
260
var channelElement = channelElements.filter(function(element) {
261
return $(element).data().sid === selectedChannel.sid;
262
});
263
channelElement = $(channelElement);
264
if (tc.currentChannelContainer === undefined && selectedChannel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {
265
tc.currentChannelContainer = channelElement;
266
}
267
tc.currentChannelContainer.removeClass('selected-channel').addClass('unselected-channel');
268
channelElement.removeClass('unselected-channel').addClass('selected-channel');
269
tc.currentChannelContainer = channelElement;
270
}
271
272
function showAddChannelInput() {
273
if (tc.messagingClient) {
274
$newChannelInputRow.addClass('showing').removeClass('not-showing');
275
$channelList.addClass('showing').removeClass('not-showing');
276
$newChannelInput.focus();
277
}
278
}
279
280
function hideAddChannelInput() {
281
$newChannelInputRow.addClass('not-showing').removeClass('showing');
282
$channelList.addClass('not-showing').removeClass('showing');
283
$newChannelInput.val('');
284
}
285
286
function addChannel(channel) {
287
if (channel.uniqueName === GENERAL_CHANNEL_UNIQUE_NAME) {
288
tc.generalChannel = channel;
289
}
290
var rowDiv = $('<div>').addClass('row channel-row');
291
rowDiv.loadTemplate('#channel-template', {
292
channelName: channel.friendlyName
293
});
294
295
var channelP = rowDiv.children().children().first();
296
297
rowDiv.on('click', selectChannel);
298
channelP.data('sid', channel.sid);
299
if (tc.currentChannel && channel.sid === tc.currentChannel.sid) {
300
tc.currentChannelContainer = channelP;
301
channelP.addClass('selected-channel');
302
}
303
else {
304
channelP.addClass('unselected-channel')
305
}
306
307
$channelList.append(rowDiv);
308
}
309
310
function deleteCurrentChannel() {
311
if (!tc.currentChannel) {
312
return;
313
}
314
if (tc.currentChannel.sid === tc.generalChannel.sid) {
315
alert('You cannot delete the general channel');
316
return;
317
}
318
tc.currentChannel.delete().then(function(channel) {
319
console.log('channel: '+ channel.friendlyName + ' deleted');
320
setupChannel(tc.generalChannel);
321
});
322
}
323
324
function selectChannel(event) {
325
var target = $(event.target);
326
var channelSid = target.data().sid;
327
var selectedChannel = tc.channelArray.filter(function(channel) {
328
return channel.sid === channelSid;
329
})[0];
330
if (selectedChannel === tc.currentChannel) {
331
return;
332
}
333
setupChannel(selectedChannel);
334
};
335
336
function disconnectClient() {
337
leaveCurrentChannel();
338
$channelList.text('');
339
tc.$messageList.text('');
340
channels = undefined;
341
$statusRow.addClass('disconnected').removeClass('connected');
342
tc.$messageList.addClass('disconnected').removeClass('connected');
343
$connectPanel.addClass('disconnected').removeClass('connected');
344
$inputText.removeClass('with-shadow');
345
$typingRow.addClass('disconnected').removeClass('connected');
346
}
347
348
tc.sortChannelsByName = function(channels) {
349
return channels.sort(function(a, b) {
350
if (a.friendlyName === GENERAL_CHANNEL_NAME) {
351
return -1;
352
}
353
if (b.friendlyName === GENERAL_CHANNEL_NAME) {
354
return 1;
355
}
356
return a.friendlyName.localeCompare(b.friendlyName);
357
});
358
};
359
360
return tc;
361
})();
362

We've actually got a real chat app going here, but let's make it more interesting with multiple channels.


When a user clicks on the "+ Channel" link we'll show an input text field where it's possible to type the name of the new channel. Creating a channel involves calling createChannel(link takes you to an external page)