Skip to contentSkip to navigationSkip to topbar
On this page

Chat with Java and Servlets


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


Generate the Token

generate-the-token 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 Chat API keys.

We generate this token by creating a new AccessToken and providing it with a ChatGrant. With the AccessToken at hand, we can use its method ToJWT() to return its string representation.

Generate an Access Token

generate-an-access-token page anchor

src/main/java/com/twilio/chat/TwilioTokenCreator.java

1
package com.twilio.chat;
2
3
import javax.inject.Inject;
4
5
import com.twilio.jwt.accesstoken.AccessToken;
6
import com.twilio.jwt.accesstoken.ChatGrant;
7
8
public class TwilioTokenCreator {
9
10
private final AppConfig appConfig;
11
12
@Inject
13
public TwilioTokenCreator(AppConfig appConfig) {
14
this.appConfig = appConfig;
15
if (appConfig.isIncomplete()) {
16
throw new IncompleteConfigException(appConfig);
17
}
18
}
19
20
String generateToken(String identity) {
21
ChatGrant grant = new ChatGrant();
22
grant.setServiceSid(appConfig.getTwilioChatServiceSID());
23
24
AccessToken token = new AccessToken.Builder(
25
appConfig.getTwilioAccountSID(),
26
appConfig.getTwilioAPIKey(),
27
appConfig.getTwilioAPISecret()
28
).identity(identity).grant(grant).build();
29
30
return token.toJwt();
31
}
32
}

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.

It uses tokenGenerator.Generate method to get hold of a new token and return it in a JSON format to be used for our client.

src/main/java/com/twilio/chat/TokenServlet.java

1
package com.twilio.chat;
2
3
import java.io.BufferedWriter;
4
import java.io.IOException;
5
import java.util.HashMap;
6
import java.util.Map;
7
8
import javax.inject.Inject;
9
import javax.servlet.http.HttpServlet;
10
import javax.servlet.http.HttpServletRequest;
11
import javax.servlet.http.HttpServletResponse;
12
13
import com.google.gson.Gson;
14
import com.google.inject.Singleton;
15
16
@Singleton
17
public class TokenServlet extends HttpServlet {
18
19
private final TwilioTokenCreator tokenCreator;
20
21
@Inject
22
public TokenServlet(TwilioTokenCreator tokenCreator) {
23
this.tokenCreator = tokenCreator;
24
}
25
26
@Override
27
public void doPost(HttpServletRequest request, HttpServletResponse response) {
28
String identity = request.getParameter("identity");
29
30
if (identity != null) {
31
32
String generatedToken = tokenCreator.generateToken(identity);
33
34
Map<String, String> json = new HashMap<>();
35
json.put("identity", identity);
36
json.put("token", generatedToken);
37
renderJson(response, json);
38
}
39
40
}
41
42
private void renderJson(HttpServletResponse response, Map<String, String> json) {
43
Gson gson = new Gson();
44
response.setContentType("application/json");
45
try (BufferedWriter responseWriter = new BufferedWriter(response.getWriter())) {
46
responseWriter.write(gson.toJson(json));
47
responseWriter.flush();
48
} catch (IOException e) {
49
e.printStackTrace();
50
}
51
}
52
}
53

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

On our client, we fetch a new Token using 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

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

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


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

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

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

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

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.

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

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.

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