├── .gitignore
├── credentials.example.json
├── package.json
├── server.js
├── lib
└── tokenprovider.js
├── LICENSE
├── README.md
└── public
├── index.html
├── js
├── md5.js
├── index.js
└── vendor
│ └── superagent.js
└── css
└── main.css
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | credentials.json
3 | *.swp
4 | node_modules
5 | package-lock.json
6 |
--------------------------------------------------------------------------------
/credentials.example.json:
--------------------------------------------------------------------------------
1 | {
2 | "accountSid": "",
3 | "signingKeySid": "",
4 | "signingKeySecret": "",
5 | "serviceSid": "",
6 | "pushCredentialSid": ""
7 | }
8 |
9 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "twilio-chat-demo-js",
3 | "version": "0.11.0",
4 | "description": "Twilio Chat JS SDK Demo",
5 | "main": "server.js",
6 | "scripts": {
7 | "start": "node server.js"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "https://github.com/twilio/ip-demo-js"
12 | },
13 | "author": "Twilio",
14 | "license": "ISC",
15 | "bugs": {
16 | "url": "https://github.com/twilio/twilio-chat-demo-js/issues"
17 | },
18 | "homepage": "https://github.com/twilio/twilio-chat-demo-js",
19 | "dependencies": {
20 | "express": "^4.14.0",
21 | "twilio": "^3.6.5"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | var credentials = require('./credentials.json');
2 | var express = require('express');
3 | var TokenProvider = require('./lib/tokenprovider');
4 |
5 | var app = new express();
6 | var tokenProvider = new TokenProvider(credentials);
7 |
8 | if (credentials.authToken) {
9 | console.warn('WARNING: The "authToken" field is deprecated. Please use "signingKeySecret".');
10 | }
11 |
12 | if (credentials.instanceSid) {
13 | console.warn('WARNING: The "instanceSid" field is deprecated. Please use "serviceSid".');
14 | }
15 |
16 | app.get('/getToken', function(req, res) {
17 | var identity = req.query && req.query.identity;
18 | if (!identity) {
19 | res.status(400).send('getToken requires an Identity to be provided');
20 | }
21 |
22 | var token = tokenProvider.getToken(identity);
23 | res.send(token);
24 | });
25 |
26 | app.use(express.static(__dirname + '/public'));
27 |
28 | app.listen(8080);
29 |
--------------------------------------------------------------------------------
/lib/tokenprovider.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var twilio = require('twilio');
4 | var AccessToken = twilio.jwt.AccessToken;
5 | var ChatGrant = AccessToken.ChatGrant;
6 |
7 | function TokenProvider(credentials) {
8 | Object.defineProperties(this, {
9 | accountSid: {
10 | enumerable: true,
11 | value: credentials.accountSid
12 | },
13 | signingKeySid: {
14 | enumerable: true,
15 | value: credentials.signingKeySid
16 | },
17 | signingKeySecret: {
18 | enumerable: true,
19 | value: credentials.signingKeySecret || credentials.authToken
20 | },
21 | serviceSid: {
22 | enumerable: true,
23 | value: credentials.serviceSid || credentials.instanceSid
24 | },
25 | pushCredentialSid: {
26 | enumerable: true,
27 | value: credentials.pushCredentialSid
28 | }
29 | });
30 | }
31 |
32 | TokenProvider.prototype.getToken = function(identity) {
33 | var token = new AccessToken(this.accountSid, this.signingKeySid, this.signingKeySecret, {
34 | identity: identity,
35 | ttl: 40000
36 | });
37 |
38 | var grant = new ChatGrant({ pushCredentialSid: this.pushCredentialSid });
39 |
40 | grant.serviceSid = this.serviceSid;
41 | token.addGrant(grant);
42 |
43 | return token.toJwt();
44 | };
45 |
46 | module.exports = TokenProvider;
47 |
48 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2016, Twilio, inc.
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions are
6 | met:
7 |
8 | 1. Redistributions of source code must retain the above copyright
9 | notice, this list of conditions and the following disclaimer.
10 |
11 | 2. Redistributions in binary form must reproduce the above copyright
12 | notice, this list of conditions and the following disclaimer in
13 | the documentation and/or other materials provided with the
14 | distribution.
15 |
16 | 3. Neither the name of Twilio nor the names of its contributors may
17 | be used to endorse or promote products derived from this software
18 | without specific prior written permission.
19 |
20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Deprecated
2 | ======================
3 |
4 | Please use the new [conversations-demo-react](https://github.com/twilio/twilio-conversations-demo-react).
5 |
6 | ### Running the demo
7 |
8 | ##### Set credentials
9 |
10 | 1. Copy `credentials.example.json` to `credentials.json`
11 | 2. Plug your credentials into `credentials.json`
12 |
13 | You can find the following credentials in your Twilio Console:
14 |
15 | | Config Value | Description |
16 | | :------------- |:------------- |
17 | `accountSid` | Your primary Twilio account identifier - find this [in the console here](https://www.twilio.com/console).
18 | `signingKeySid` | The SID for your API Key, used to authenticate - [generate one here](https://www.twilio.com/console/runtime/api-keys).
19 | `signingKeySecret` | The secret for your API Key, used to authenticate - [you'll get this when you create your API key, as above](https://www.twilio.com/console/runtime/api-keys).
20 | `serviceSid` | Like a database for your Chat data - [generate one in the console here](https://www.twilio.com/console/chat/services).
21 | `pushCredentialSid` | Credentials are records for push notification channels for APN and FCM - [generate them in the console here](https://www.twilio.com/console/chat/credentials) and [read more about configuring push here](https://www.twilio.com/docs/api/chat/guides/push-notification-configuration).
22 |
23 | ##### Install dependencies
24 |
25 | ```
26 | $ npm install
27 | ```
28 |
29 | ##### Run server
30 |
31 | ```
32 | $ npm start
33 | ```
34 |
35 | ##### Connect
36 |
37 | Connect via `http://localhost:8080`
38 |
39 | ### Using another version
40 |
41 | This demo defaults to using the latest build of the Chat JS SDK.
42 | To change to a different version, just open `public/index.html` and change the
43 | following string to point to the URL of the version you'd like to use, for example to use v3.2.1:
44 |
45 | ```
46 |
47 | ```
48 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Twilio Chat
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
20 |
25 |
30 |
38 |
46 |
47 |
71 |
72 |
73 |
You are not currently viewing a Channel.
74 |
75 |
76 |
77 |
78 |
79 |
80 | Edit Channel
81 | Delete Channel
82 |
83 |
84 |
91 |
92 | Join this Channel
93 |
94 |
95 |
Members
96 |
Add
97 |
Invite
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
--------------------------------------------------------------------------------
/public/js/md5.js:
--------------------------------------------------------------------------------
1 | var MD5 = function (string) {
2 |
3 | function RotateLeft(lValue, iShiftBits) {
4 | return (lValue<>>(32-iShiftBits));
5 | }
6 |
7 | function AddUnsigned(lX,lY) {
8 | var lX4,lY4,lX8,lY8,lResult;
9 | lX8 = (lX & 0x80000000);
10 | lY8 = (lY & 0x80000000);
11 | lX4 = (lX & 0x40000000);
12 | lY4 = (lY & 0x40000000);
13 | lResult = (lX & 0x3FFFFFFF)+(lY & 0x3FFFFFFF);
14 | if (lX4 & lY4) {
15 | return (lResult ^ 0x80000000 ^ lX8 ^ lY8);
16 | }
17 | if (lX4 | lY4) {
18 | if (lResult & 0x40000000) {
19 | return (lResult ^ 0xC0000000 ^ lX8 ^ lY8);
20 | } else {
21 | return (lResult ^ 0x40000000 ^ lX8 ^ lY8);
22 | }
23 | } else {
24 | return (lResult ^ lX8 ^ lY8);
25 | }
26 | }
27 |
28 | function F(x,y,z) { return (x & y) | ((~x) & z); }
29 | function G(x,y,z) { return (x & z) | (y & (~z)); }
30 | function H(x,y,z) { return (x ^ y ^ z); }
31 | function I(x,y,z) { return (y ^ (x | (~z))); }
32 |
33 | function FF(a,b,c,d,x,s,ac) {
34 | a = AddUnsigned(a, AddUnsigned(AddUnsigned(F(b, c, d), x), ac));
35 | return AddUnsigned(RotateLeft(a, s), b);
36 | };
37 |
38 | function GG(a,b,c,d,x,s,ac) {
39 | a = AddUnsigned(a, AddUnsigned(AddUnsigned(G(b, c, d), x), ac));
40 | return AddUnsigned(RotateLeft(a, s), b);
41 | };
42 |
43 | function HH(a,b,c,d,x,s,ac) {
44 | a = AddUnsigned(a, AddUnsigned(AddUnsigned(H(b, c, d), x), ac));
45 | return AddUnsigned(RotateLeft(a, s), b);
46 | };
47 |
48 | function II(a,b,c,d,x,s,ac) {
49 | a = AddUnsigned(a, AddUnsigned(AddUnsigned(I(b, c, d), x), ac));
50 | return AddUnsigned(RotateLeft(a, s), b);
51 | };
52 |
53 | function ConvertToWordArray(string) {
54 | var lWordCount;
55 | var lMessageLength = string.length;
56 | var lNumberOfWords_temp1=lMessageLength + 8;
57 | var lNumberOfWords_temp2=(lNumberOfWords_temp1-(lNumberOfWords_temp1 % 64))/64;
58 | var lNumberOfWords = (lNumberOfWords_temp2+1)*16;
59 | var lWordArray=Array(lNumberOfWords-1);
60 | var lBytePosition = 0;
61 | var lByteCount = 0;
62 | while ( lByteCount < lMessageLength ) {
63 | lWordCount = (lByteCount-(lByteCount % 4))/4;
64 | lBytePosition = (lByteCount % 4)*8;
65 | lWordArray[lWordCount] = (lWordArray[lWordCount] | (string.charCodeAt(lByteCount)<>>29;
73 | return lWordArray;
74 | };
75 |
76 | function WordToHex(lValue) {
77 | var WordToHexValue="",WordToHexValue_temp="",lByte,lCount;
78 | for (lCount = 0;lCount<=3;lCount++) {
79 | lByte = (lValue>>>(lCount*8)) & 255;
80 | WordToHexValue_temp = "0" + lByte.toString(16);
81 | WordToHexValue = WordToHexValue + WordToHexValue_temp.substr(WordToHexValue_temp.length-2,2);
82 | }
83 | return WordToHexValue;
84 | };
85 |
86 | function Utf8Encode(string) {
87 | string = string.replace(/\r\n/g,"\n");
88 | var utftext = "";
89 |
90 | for (var n = 0; n < string.length; n++) {
91 |
92 | var c = string.charCodeAt(n);
93 |
94 | if (c < 128) {
95 | utftext += String.fromCharCode(c);
96 | }
97 | else if((c > 127) && (c < 2048)) {
98 | utftext += String.fromCharCode((c >> 6) | 192);
99 | utftext += String.fromCharCode((c & 63) | 128);
100 | }
101 | else {
102 | utftext += String.fromCharCode((c >> 12) | 224);
103 | utftext += String.fromCharCode(((c >> 6) & 63) | 128);
104 | utftext += String.fromCharCode((c & 63) | 128);
105 | }
106 |
107 | }
108 |
109 | return utftext;
110 | };
111 |
112 | var x=Array();
113 | var k,AA,BB,CC,DD,a,b,c,d;
114 | var S11=7, S12=12, S13=17, S14=22;
115 | var S21=5, S22=9 , S23=14, S24=20;
116 | var S31=4, S32=11, S33=16, S34=23;
117 | var S41=6, S42=10, S43=15, S44=21;
118 |
119 | string = Utf8Encode(string);
120 |
121 | x = ConvertToWordArray(string);
122 |
123 | a = 0x67452301; b = 0xEFCDAB89; c = 0x98BADCFE; d = 0x10325476;
124 |
125 | for (k=0;k img {
149 | width: 40px;
150 | height: 40px;
151 | border-radius: 20px;
152 | border: 2px solid white;
153 | }
154 |
155 | #profile > label {
156 | width: 170px;
157 | white-space: nowrap;
158 | overflow: hidden;
159 | text-overflow: ellipsis;
160 | vertical-align: middle;
161 | font-size: 1.2em;
162 | margin-left: 6px;
163 | }
164 |
165 | #profile > #presence {
166 | display: inline-block;
167 | height: 20px;
168 | width: 20px;
169 | position: absolute;
170 | border-radius: 20px;
171 | top: 20px;
172 | right: 15px;
173 | }
174 |
175 | #profile > #presence.connected { background: green; }
176 | #profile > #presence.disconnected { background: gray; }
177 | #profile > #presence.connecting { background: yellow; }
178 | #profile > #presence.denied { background: red; }
179 |
180 | #channels {
181 | margin-top: 80px;
182 | }
183 |
184 | #sidebar h3 {
185 | margin-left: 20px;
186 | }
187 |
188 | #sidebar ul {
189 | list-style-type: none;
190 | padding: 0 10px;
191 | }
192 |
193 | #sidebar li {
194 | margin: 5px 0;
195 | cursor: pointer;
196 | padding: 5px 5px 5px 15px;
197 | width: 100%;
198 | white-space: nowrap;
199 | overflow: hidden;
200 | text-overflow: ellipsis;
201 | }
202 |
203 | #sidebar li span {
204 | color: #555;
205 | }
206 |
207 | #sidebar li span.joined {
208 | color: white;
209 | }
210 |
211 | #sidebar li.new-messages span {
212 | font-weight: bold;
213 | }
214 |
215 | #sidebar li span.invited {
216 | font-style: italic;
217 | color: white;
218 | }
219 |
220 | #sidebar li.new-messages span.messages-count {
221 | background: #f5e9a5;
222 | padding: 2px;
223 | border-radius: 4px;
224 | font-size: 8pt;
225 | margin-left: 4px;
226 | }
227 |
228 |
229 | #sidebar li div {
230 | display: none;
231 | }
232 |
233 | #sidebar li:hover {
234 | background: #E30000;
235 | border-radius: 5px;
236 | }
237 |
238 | #sidebar li:hover div {
239 | display: block;
240 | }
241 |
242 | #sidebar-footer {
243 | position: fixed;
244 | bottom: 0;
245 | width: 250px;
246 | height: 80px;
247 | padding: 20px 0;
248 | text-align: center;
249 | background-color: #222;
250 | }
251 |
252 | #create-channel-button {
253 | margin: 0 auto;
254 | padding: 12px;
255 | width: 90%;
256 | }
257 |
258 | #channel, #no-channel {
259 | position: absolute;
260 | top: 0;
261 | left: 250px;
262 | bottom: 0;
263 | right: 0;
264 | background: #FAFAFF;
265 | }
266 |
267 | #channel {
268 | display: none;
269 | }
270 |
271 | #channel.view-only div.edit-button,
272 | #channel.view-only div.remove-button,
273 | #channel.view-only #channel-info button,
274 | #channel.view-only #channel-members,
275 | #channel.view-only #channel-message-send,
276 | #channel.view-only #typing-indicator {
277 | display: none;
278 | }
279 |
280 | #channel.view-only #channel-chat {
281 | right: 0;
282 | }
283 |
284 | #channel.view-only #channel-join-panel {
285 | display: block;
286 | }
287 |
288 | #no-channel p {
289 | padding: 50px;
290 | font-size: 2em;
291 | color: #555;
292 | }
293 |
294 | #channel-info {
295 | position: absolute;
296 | top: 0;
297 | left: 0;
298 | right: 0;
299 | height: 60px;
300 | padding: 5px 10px;
301 | background: #FFF;
302 | border-bottom: 2px solid #666;
303 | }
304 |
305 | #channel-info h1 {
306 | font-size: 25px;
307 | line-height: 30px;
308 | margin: 0;
309 | padding: 0;
310 | font-weight: bold;
311 | }
312 |
313 | div.remove-button {
314 | width: 16px;
315 | height: 16px;
316 | vertical-align: middle;
317 | margin-left: 4px;
318 | cursor: pointer;
319 | display: inline-block;
320 | float: right;
321 | }
322 |
323 | div.edit-button {
324 | width: 16px;
325 | height: 16px;
326 | vertical-align: middle;
327 | margin-left: 4px;
328 | cursor: pointer;
329 | }
330 |
331 | #channel-info h2 {
332 | font-size: 16px;
333 | line-height: 20px;
334 | font-style: italic;
335 | color: #444;
336 | margin: 0;
337 | padding: 0;
338 | display: inline-block;
339 | }
340 |
341 | .red-button {
342 | border-radius: 5px;
343 | border: 1px solid black;
344 | color: white;
345 | box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.4);
346 | text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.75);
347 | background-color: #E30000;
348 | }
349 |
350 | .red-button:hover {
351 | background-color: #B30000;
352 | }
353 |
354 | .white-button {
355 | border-radius: 5px;
356 | border: 1px solid black;
357 | color: white;
358 | box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.4);
359 | text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.75);
360 | background-color: #E3E3E3;
361 | }
362 |
363 | .white-button:hover {
364 | background-color: #B3B3B3;
365 | }
366 |
367 | #edit-channel {
368 | position: absolute;
369 | right: 170px;
370 | top: 10px;
371 | padding: 10px;
372 | line-height: 20px;
373 | font-size: 16px;
374 | }
375 |
376 | #delete-channel {
377 | position: absolute;
378 | right: 10px;
379 | top: 10px;
380 | padding: 10px;
381 | line-height: 20px;
382 | font-size: 16px;
383 | }
384 |
385 | #channel-body {
386 | position: absolute;
387 | top: 60px;
388 | left: 0;
389 | right: 0;
390 | bottom: 0;
391 | }
392 |
393 | #channel-chat {
394 | position: absolute;
395 | left: 0;
396 | top: 0;
397 | bottom: 0;
398 | right: 200px;
399 | overflow: hidden;
400 | }
401 |
402 | #channel-messages {
403 | margin-bottom: 70px;
404 | position: relative;
405 | width: 100%;
406 | height: calc(100% - 70px);
407 | overflow-y: auto;
408 | }
409 |
410 |
411 | #channel-messages ul {
412 | list-style-type: none;
413 | margin: 0;
414 | padding: 0;
415 | }
416 |
417 | #channel-messages li {
418 | padding: 10px;
419 | position: relative;
420 | }
421 |
422 | #channel-messages li > img {
423 | width: 30px;
424 | height: 30px;
425 | border-radius: 15px;
426 | border: 2px solid #333;
427 | margin-right: 5px;
428 | }
429 |
430 | #channel-messages p.members-read {
431 | position: absolute;
432 | bottom: 2px;
433 | right: 10px;
434 | margin: 0;
435 | padding: 0;
436 | width: 100%;
437 | }
438 |
439 | #channel-messages p.members-read img {
440 | height: 16px;
441 | width: 16px;
442 | border-radius: 8px;
443 | border: solid 1px #333;
444 | float: right;
445 | margin-left: 4px;
446 | }
447 |
448 | #channel-messages p.last-read {
449 | position: absolute;
450 | bottom: 2px;
451 | right: 10px;
452 | margin: 0;
453 | font-size: 12px;
454 | color: red;
455 | font-weight: bold;
456 | display: none;
457 | }
458 |
459 | #channel-messages li:nth-child(even) {
460 | background: #F0F0FA;
461 | }
462 |
463 | #channel-messages li.last-read {
464 | border-bottom: 1px solid red;
465 | }
466 |
467 | #channel-messages li.last-read p.last-read {
468 | display: block;
469 | }
470 |
471 | #channel-messages li > div {
472 | display: none;
473 | }
474 |
475 | #channel-messages li:hover > div {
476 | display: block;
477 | }
478 |
479 | #channel-messages li.editing:hover > div {
480 | display: none;
481 | }
482 |
483 | #channel-messages li:hover {
484 | background: #EBEBF0;
485 | }
486 |
487 | #channel-messages p.author {
488 | display: inline-block;
489 | font-weight: bold;
490 | margin: 0;
491 | padding: 0;
492 | width: calc(100% - 90px);
493 | }
494 |
495 | #channel-messages .timestamp {
496 | font-style: italic;
497 | color: #333;
498 | margin-left: 4px;
499 | }
500 |
501 | #channel-messages p.body {
502 | margin: 0 0 0 35px;
503 | padding: 0;
504 | width: calc(100% - 90px);
505 | }
506 |
507 | #channel-messages textarea.edit-body {
508 | width: calc(100% - 90px);
509 | margin: 5px 0 0 35px;
510 | padding: 0;
511 | display: none;
512 | }
513 |
514 | #channel-messages button {
515 | display: none;
516 | margin: 0 5px;
517 | }
518 |
519 | #channel-messages span.edited {
520 | font-style: italic;
521 | color: #444;
522 | margin-left: 8px;
523 | }
524 |
525 | #channel-messages div {
526 | float: right;
527 | }
528 |
529 | #channel-message-send {
530 | position: absolute;
531 | left: 0;
532 | right: 0;
533 | bottom: 0;
534 | padding-bottom: 10px;
535 | border-top: 1px solid #AAA;
536 | background: #F5F5F8;
537 | }
538 |
539 | #typing-indicator {
540 | padding: 5px 15px;
541 | font-style: italic;
542 | color: #444;
543 | }
544 |
545 | #typing-indicator span {
546 | display: block;
547 | min-height: 18px;
548 | }
549 |
550 | #channel-message-send input {
551 | display: inline-block;
552 | width: calc(100% - 90px);
553 | margin-left: 10px;
554 | height: 30px;
555 | right: 60px;
556 | padding: 0 10px;
557 | border-radius: 5px;
558 | border: 1px solid black;
559 | }
560 |
561 | #channel-message-send button {
562 | margin: 0 10px;
563 | height: 30px;
564 | padding: 5px 10px;
565 | display: inline-block;
566 | }
567 |
568 | #channel-join-panel {
569 | position: absolute;
570 | bottom: 0;
571 | left: 0;
572 | right: 0;
573 | display: none;
574 | font-size: 20px;
575 | border-top: 1px solid #AAA;
576 | background: #F5F5F8;
577 | padding: 10px;
578 | text-align: center;
579 | }
580 |
581 | #channel-join-panel button {
582 | margin: 0 auto;
583 | padding: 5px 10px;
584 | }
585 |
586 | #channel-members {
587 | position: absolute;
588 | right: 0;
589 | top: 60px;
590 | bottom: 0;
591 | width: 200px;
592 | border-left: 2px solid #667;
593 | background-color: #333;
594 | color: white;
595 | }
596 |
597 | #channel-members h3 {
598 | margin: 5px;
599 | padding: 10px;
600 | }
601 |
602 | #channel-members button {
603 | width: 80px;
604 | margin-left: 10px;
605 | display: inline-block;
606 | }
607 |
608 | #channel-members ul {
609 | list-style-type: none;
610 | margin: 20px 0 0 0;
611 | padding: 0 10px;
612 | }
613 |
614 | #channel-members li {
615 | margin: 5px 0;
616 | cursor: pointer;
617 | padding: 5px 5px 5px 15px;
618 | width: 100%;
619 | white-space: nowrap;
620 | overflow: hidden;
621 | text-overflow: ellipsis;
622 | }
623 |
624 | #channel-members li img {
625 | width: 20px;
626 | height: 20px;
627 | border-radius: 10px;
628 | border: 1px solid black;
629 | margin-right: 5px;
630 | }
631 |
632 | #channel-members li div {
633 | display: none;
634 | }
635 |
636 | #channel-members li:hover {
637 | background: #E30000;
638 | border-radius: 5px;
639 | }
640 |
641 | #channel-members li:hover div {
642 | display: block;
643 | }
644 |
645 | #channel-members li span {
646 | display: inline-block;
647 | width: 110px;
648 | vertical-align: top;
649 | white-space: nowrap;
650 | overflow: hidden;
651 | text-overflow: ellipsis;
652 | }
653 |
654 | #channel-members .member-online {
655 | font-weight: bold;
656 | }
657 |
658 | #channel-members .member-offline {
659 | color: gray;
660 | }
661 |
662 | .loader {
663 | -webkit-filter: grayscale(100%) blur(2px);
664 | -moz-filter: grayscale(100%) blur(2px);
665 | -o-filter: grayscale(100%) blur(2px);
666 | -ms-filter: grayscale(100%) blur(2px);
667 | filter: grayscale(100%) blur(2px);
668 | }
669 |
670 |
--------------------------------------------------------------------------------
/public/js/index.js:
--------------------------------------------------------------------------------
1 | var request = window.superagent;
2 |
3 | var activeChannel;
4 | var client;
5 | var typingMembers = new Set();
6 |
7 | var activeChannelPage;
8 |
9 | var userContext = { identity: null};
10 |
11 | $(document).ready(function() {
12 | $('#login-name').focus();
13 |
14 | $('#login-button').on('click', function() {
15 | var identity = $('#login-name').val();
16 | if (!identity) { return; }
17 |
18 | userContext.identity = identity;
19 |
20 | logIn(identity, identity);
21 | });
22 |
23 | $('#login-name').on('keydown', function(e) {
24 | if (e.keyCode === 13) { $('#login-button').click(); }
25 | });
26 |
27 | $('#message-body-input').on('keydown', function(e) {
28 | if (e.keyCode === 13) { $('#send-message').click(); }
29 | else if (activeChannel) { activeChannel.typing(); }
30 | });
31 |
32 | $('#edit-channel').on('click', function() {
33 | $('#update-channel-display-name').val(activeChannel.friendlyName || '');
34 | $('#update-channel-unique-name').val(activeChannel.uniqueName || '');
35 | $('#update-channel-desc').val(activeChannel.attributes.description || '');
36 | $('#update-channel-private').prop('checked', activeChannel.isPrivate);
37 | $('#update-channel').show();
38 | $('#overlay').show();
39 | });
40 |
41 | var isUpdatingConsumption = false;
42 | $('#channel-messages').on('scroll', function(e) {
43 | var $messages = $('#channel-messages');
44 |
45 | if ($('#channel-messages ul').height() - 50 < $messages.scrollTop() + $messages.height()) {
46 | activeChannel.getMessages(1).then(messages => {
47 | var newestMessageIndex = messages.length ? messages[0].index : 0;
48 | if (!isUpdatingConsumption && activeChannel.lastConsumedMessageIndex !== newestMessageIndex) {
49 | isUpdatingConsumption = true;
50 | activeChannel.updateLastConsumedMessageIndex(newestMessageIndex).then(function() {
51 | isUpdatingConsumption = false;
52 | });
53 | }
54 | });
55 | }
56 |
57 | var self = $(this);
58 | if($messages.scrollTop() < 50 && activeChannelPage && activeChannelPage.hasPrevPage && !self.hasClass('loader')) {
59 | self.addClass('loader');
60 | var initialHeight = $('ul', self).height();
61 | activeChannelPage.prevPage().then(page => {
62 | page.items.reverse().forEach(prependMessage);
63 | activeChannelPage = page;
64 | var difference = $('ul', self).height() - initialHeight;
65 | self.scrollTop(difference);
66 | self.removeClass('loader');
67 | });
68 | }
69 | });
70 |
71 | $('#update-channel .remove-button').on('click', function() {
72 | $('#update-channel').hide();
73 | $('#overlay').hide();
74 | });
75 |
76 | $('#delete-channel').on('click', function() {
77 | activeChannel && activeChannel.delete();
78 | });
79 |
80 | $('#join-channel').on('click', function() {
81 | activeChannel.join().then(setActiveChannel);
82 | });
83 |
84 | $('#invite-user').on('click', function() {
85 | $('#invite-member').show();
86 | $('#overlay').show();
87 | });
88 |
89 | $('#add-user').on('click', function() {
90 | $('#add-member').show();
91 | $('#overlay').show();
92 | });
93 |
94 | $('#invite-button').on('click', function() {
95 | var identity = $('#invite-identity').val();
96 | identity && activeChannel.invite(identity).then(function() {
97 | $('#invite-member').hide();
98 | $('#overlay').hide();
99 | $('#invite-identity').val('');
100 | });
101 | });
102 |
103 | $('#add-button').on('click', function() {
104 | var identity = $('#add-identity').val();
105 | identity && activeChannel.add(identity).then(function() {
106 | $('#add-member').hide();
107 | $('#overlay').hide();
108 | $('#add-identity').val('');
109 | });
110 | });
111 |
112 | $('#invite-member .remove-button').on('click', function() {
113 | $('#invite-member').hide();
114 | $('#overlay').hide();
115 | });
116 |
117 | $('#add-member .remove-button').on('click', function() {
118 | $('#add-member').hide();
119 | $('#overlay').hide();
120 | });
121 |
122 | $('#create-channel .remove-button').on('click', function() {
123 | $('#create-channel').hide();
124 | $('#overlay').hide();
125 | });
126 |
127 | $('#create-channel-button').on('click', function() {
128 | $('#create-channel').show();
129 | $('#overlay').show();
130 | });
131 |
132 | $('#create-new-channel').on('click', function() {
133 | var attributes = {
134 | description: $('#create-channel-desc').val()
135 | };
136 |
137 | var isPrivate = $('#create-channel-private').is(':checked');
138 | var friendlyName = $('#create-channel-display-name').val();
139 | var uniqueName = $('#create-channel-unique-name').val();
140 |
141 | client.createChannel({
142 | attributes: attributes,
143 | friendlyName: friendlyName,
144 | isPrivate: isPrivate,
145 | uniqueName: uniqueName
146 | }).then(function joinChannel(channel) {
147 | $('#create-channel').hide();
148 | $('#overlay').hide();
149 | return channel.join();
150 | }).then(setActiveChannel);
151 | });
152 |
153 | $('#update-channel-submit').on('click', function() {
154 | var desc = $('#update-channel-desc').val();
155 | var friendlyName = $('#update-channel-display-name').val();
156 | var uniqueName = $('#update-channel-unique-name').val();
157 |
158 | var promises = [];
159 | if (desc !== activeChannel.attributes.description) {
160 | promises.push(activeChannel.updateAttributes({ description: desc }));
161 | }
162 |
163 | if (friendlyName !== activeChannel.friendlyName) {
164 | promises.push(activeChannel.updateFriendlyName(friendlyName));
165 | }
166 |
167 | if (uniqueName !== activeChannel.uniqueName) {
168 | promises.push(activeChannel.updateUniqueName(uniqueName));
169 | }
170 |
171 | Promise.all(promises).then(function() {
172 | $('#update-channel').hide();
173 | $('#overlay').hide();
174 | });
175 | });
176 | });
177 |
178 | function googleLogIn(googleUser) {
179 | var profile = googleUser.getBasicProfile();
180 | var identity = profile.getEmail().toLowerCase();
181 | var fullName = profile.getName();
182 | logIn(identity, fullName);
183 | }
184 |
185 | function logIn(identity, displayName) {
186 | request('/getToken?identity=' + identity, function(err, res) {
187 | if (err) { throw new Error(res.text); }
188 |
189 | var token = res.text;
190 |
191 | userContext.identity = identity;
192 |
193 | Twilio.Chat.Client.create(token, { logLevel: 'info' })
194 | .then(function(createdClient) {
195 | $('#login').hide();
196 | $('#overlay').hide();
197 | client = createdClient;
198 | client.on('tokenAboutToExpire', () => {
199 | request('/getToken?identity=' + identity, function(err, res) {
200 | if (err) {
201 | console.error('Failed to get a token ', res.text);
202 | throw new Error(res.text);
203 | }
204 | console.log('Got new token!', res.text);
205 | client.updateToken(res.text);
206 | });
207 | });
208 |
209 | $('#profile label').text(client.user.friendlyName || client.user.identity);
210 | $('#profile img').attr('src', 'http://gravatar.com/avatar/' + MD5(identity) + '?s=40&d=mm&r=g');
211 |
212 | client.user.on('updated', function() {
213 | $('#profile label').text(client.user.friendlyName || client.user.identity);
214 | });
215 |
216 | var connectionInfo = $('#profile #presence');
217 | connectionInfo
218 | .removeClass('online offline connecting denied')
219 | .addClass(client.connectionState);
220 | client.on('connectionStateChanged', function(state) {
221 | connectionInfo
222 | .removeClass('online offline connecting denied')
223 | .addClass(client.connectionState);
224 | });
225 |
226 | client.getSubscribedChannels().then(updateChannels);
227 |
228 | client.on('channelJoined', function(channel) {
229 | channel.on('messageAdded', updateUnreadMessages);
230 | channel.on('messageAdded', updateChannels);
231 | updateChannels();
232 | });
233 |
234 | client.on('channelInvited', updateChannels);
235 | client.on('channelAdded', updateChannels);
236 | client.on('channelUpdated', updateChannels);
237 | client.on('channelLeft', leaveChannel);
238 | client.on('channelRemoved', leaveChannel);
239 | })
240 | .catch(function(err) {
241 | throw err;
242 | })
243 | });
244 | }
245 |
246 | function updateUnreadMessages(message) {
247 | var channel = message.channel;
248 | if (channel !== activeChannel) {
249 | $('#sidebar li[data-sid="' + channel.sid + '"] span').addClass('new-messages');
250 | }
251 | }
252 |
253 | function leaveChannel(channel) {
254 | if (channel == activeChannel && channel.status !== 'joined') {
255 | clearActiveChannel();
256 | }
257 |
258 | channel.removeListener('messageAdded', updateUnreadMessages);
259 |
260 | updateChannels();
261 | }
262 |
263 | function addKnownChannel(channel) {
264 | var $el = $(' ')
265 | .attr('data-sid', channel.sid)
266 | .on('click', function() {
267 | setActiveChannel(channel);
268 | });
269 |
270 | var $title = $(' ')
271 | .text(channel.friendlyName)
272 | .appendTo($el);
273 |
274 | $('#known-channels ul').append($el);
275 | }
276 |
277 | function addPublicChannel(channel) {
278 | var $el = $(' ')
279 | .attr('data-sid', channel.sid)
280 | .attr('id', channel.sid)
281 | .on('click', function() {
282 | channel.getChannel().then(channel => {
283 | channel.join().then(channel => {
284 | setActiveChannel(channel);
285 | removePublicChannel(channel);
286 | });
287 | });
288 | });
289 |
290 | var $title = $(' ')
291 | .text(channel.friendlyName)
292 | .appendTo($el);
293 |
294 | $('#public-channels ul').append($el);
295 | }
296 |
297 | function addInvitedChannel(channel) {
298 | var $el = $(' ')
299 | .attr('data-sid', channel.sid)
300 | .on('click', function() {
301 | setActiveChannel(channel);
302 | });
303 |
304 | var $title = $(' ')
305 | .text(channel.friendlyName)
306 | .appendTo($el);
307 |
308 | var $decline = $('
')
309 | .on('click', function(e) {
310 | e.stopPropagation();
311 | channel.decline();
312 | }).appendTo($el);
313 |
314 | $('#invited-channels ul').append($el);
315 | }
316 |
317 | function addJoinedChannel(channel) {
318 | var $el = $(' ')
319 | .attr('data-sid', channel.sid)
320 | .on('click', function() {
321 | setActiveChannel(channel);
322 | });
323 |
324 | var $title = $(' ')
325 | .text(channel.friendlyName)
326 | .appendTo($el);
327 |
328 | var $count = $(' ')
329 | .appendTo($el);
330 |
331 | /*
332 | channel.getUnreadMessagesCount().then(count => {
333 | if (count > 0) {
334 | $el.addClass('new-messages');
335 | $count.text(count);
336 | }
337 | });
338 | */
339 |
340 | var $leave = $('
')
341 | .on('click', function(e) {
342 | e.stopPropagation();
343 | channel.leave();
344 | }).appendTo($el);
345 |
346 | $('#my-channels ul').append($el);
347 | }
348 |
349 | function removeLeftChannel(channel) {
350 | $('#my-channels li[data-sid=' + channel.sid + ']').remove();
351 |
352 | if (channel === activeChannel) {
353 | clearActiveChannel();
354 | }
355 | }
356 |
357 | function removePublicChannel(channel) {
358 | $('#public-channels li[data-sid=' + channel.sid + ']').remove();
359 | }
360 |
361 | function updateMessages() {
362 | $('#channel-messages ul').empty();
363 | activeChannel.getMessages(30).then(function(page) {
364 | page.items.forEach(addMessage);
365 | });
366 | }
367 |
368 | function removeMessage(message) {
369 | $('#channel-messages li[data-index=' + message.index + ']').remove();
370 | }
371 |
372 | function updateMessage(args) {
373 | var $el = $('#channel-messages li[data-index=' + args.message.index + ']');
374 | $el.empty();
375 | createMessage(args.message, $el);
376 | }
377 |
378 | function createMessage(message, $el) {
379 | var $remove = $('
')
380 | .on('click', function(e) {
381 | e.preventDefault();
382 | message.remove();
383 | }).appendTo($el);
384 |
385 | var $edit = $('
')
386 | .on('click', function(e) {
387 | e.preventDefault();
388 | $('.body', $el).hide();
389 | $('.edit-body', $el).show();
390 | $('button', $el).show();
391 | $el.addClass('editing');
392 | }).appendTo($el);
393 |
394 | var $img = $(' ')
395 | .attr('src', 'http://gravatar.com/avatar/' + MD5(message.author) + '?s=30&d=mm&r=g')
396 | .appendTo($el);
397 |
398 | var $author = $('
')
399 | .text(message.author)
400 | .appendTo($el);
401 |
402 | var time = message.timestamp;
403 | var minutes = time.getMinutes();
404 | var ampm = Math.floor(time.getHours()/12) ? 'PM' : 'AM';
405 |
406 | if (minutes < 10) { minutes = '0' + minutes; }
407 |
408 | var $timestamp = $(' ')
409 | .text('(' + (time.getHours()%12) + ':' + minutes + ' ' + ampm + ')')
410 | .appendTo($author);
411 |
412 | if (message.lastUpdatedBy) {
413 | time = message.dateUpdated;
414 | minutes = time.getMinutes();
415 | ampm = Math.floor(time.getHours()/12) ? 'PM' : 'AM';
416 |
417 | if (minutes < 10) { minutes = '0' + minutes; }
418 |
419 | $(' ')
420 | .text('(Edited by ' + message.lastUpdatedBy + ' at ' +
421 | (time.getHours()%12) + ':' + minutes + ' ' + ampm + ')')
422 | .appendTo($author)
423 | }
424 |
425 | var $body = $('
')
426 | .text(message.body)
427 | .appendTo($el);
428 |
429 | var $editBody = $('')
430 | .text(message.body)
431 | .appendTo($el);
432 |
433 | var $cancel = $(' ')
434 | .text('Cancel')
435 | .on('click', function(e) {
436 | e.preventDefault();
437 | $('.edit-body', $el).hide();
438 | $('button', $el).hide();
439 | $('.body', $el).show();
440 | $el.removeClass('editing');
441 | }).appendTo($el);
442 |
443 | var $edit = $(' ')
444 | .text('Make Change')
445 | .on('click', function(e) {
446 | message.updateBody($editBody.val());
447 | }).appendTo($el);
448 |
449 | var $lastRead = $('
')
450 | .text('New messages')
451 | .appendTo($el);
452 |
453 | var $membersRead = $('
')
454 | .appendTo($el);
455 | }
456 |
457 | function prependMessage(message) {
458 | var $messages = $('#channel-messages');
459 | var $el = $(' ').attr('data-index', message.index);
460 | createMessage(message, $el);
461 | $('#channel-messages ul').prepend($el);
462 | }
463 |
464 | function addMessage(message) {
465 | var $messages = $('#channel-messages');
466 | var initHeight = $('#channel-messages ul').height();
467 | var $el = $(' ').attr('data-index', message.index);
468 | createMessage(message, $el);
469 |
470 | $('#channel-messages ul').append($el);
471 |
472 | if (initHeight - 50 < $messages.scrollTop() + $messages.height()) {
473 | $messages.scrollTop($('#channel-messages ul').height());
474 | }
475 |
476 | if ($('#channel-messages ul').height() <= $messages.height() &&
477 | message.index > message.channel.lastConsumedMessageIndex) {
478 | message.channel.updateLastConsumedMessageIndex(message.index);
479 | }
480 | }
481 |
482 | function addMember(member) {
483 | member.getUser().then(user => {
484 | var $el = $(' ')
485 | .attr('data-identity', member.identity);
486 |
487 | var $img = $(' ')
488 | .attr('src', 'http://gravatar.com/avatar/' + MD5(member.identity.toLowerCase()) + '?s=20&d=mm&r=g')
489 | .appendTo($el);
490 |
491 |
492 | let hasReachability = (user.online !== null) && (typeof user.online !== 'undefined');
493 | var $span = $(' ')
494 | .text(user.friendlyName || user.identity)
495 | .addClass(hasReachability ? ( user.online ? 'member-online' : 'member-offline' ) : '')
496 | .appendTo($el);
497 |
498 | var $remove = $('
')
499 | .on('click', member.remove.bind(member))
500 | .appendTo($el);
501 |
502 | updateMember(member, user);
503 |
504 | $('#channel-members ul').append($el);
505 | });
506 | }
507 |
508 | function updateMembers() {
509 | $('#channel-members ul').empty();
510 |
511 | activeChannel.getMembers()
512 | .then(members => members
513 | .sort(function(a, b) { return a.identity > b.identity; })
514 | .sort(function(a, b) { return a.getUser().then(user => user.online) < b.getUser().then(user => user.online); })
515 | .forEach(addMember));
516 |
517 | }
518 |
519 | function updateChannels() {
520 | client.getSubscribedChannels()
521 | .then(page => {
522 | subscribedChannels = page.items.sort(function(a, b) {
523 | return a.friendlyName > b.friendlyName;
524 | });
525 | $('#known-channels ul').empty();
526 | $('#invited-channels ul').empty();
527 | $('#my-channels ul').empty()
528 | subscribedChannels.forEach(function(channel) {
529 | switch (channel.status) {
530 | case 'joined':
531 | addJoinedChannel(channel);
532 | break;
533 | case 'invited':
534 | addInvitedChannel(channel);
535 | break;
536 | default:
537 | addKnownChannel(channel);
538 | break;
539 | }
540 | });
541 | client.getPublicChannelDescriptors()
542 | .then(page => {
543 | publicChannels = page.items.sort(function(a, b) {
544 | return a.friendlyName > b.friendlyName;
545 | });
546 | $('#public-channels ul').empty();
547 | publicChannels.forEach(function(channel) {
548 | var result = subscribedChannels.find(item => item.sid === channel.sid);
549 | console.log('Adding public channel ' + channel.sid + ' ' + channel.status + ', result=' + result);
550 | if (result === undefined) {
551 | addPublicChannel(channel);
552 | }
553 | });
554 | });
555 | });
556 | }
557 |
558 | function updateMember(member, user) {
559 | if (user === undefined) { return; }
560 | if (member.identity === decodeURIComponent(client.identity)) { return; }
561 |
562 | var $lastRead = $('#channel-messages p.members-read img[data-identity="' + member.identity + '"]');
563 |
564 | if (!$lastRead.length) {
565 | $lastRead = $(' ')
566 | .attr('src', 'http://gravatar.com/avatar/' + MD5(member.identity) + '?s=20&d=mm&r=g')
567 | .attr('title', user.friendlyName || member.identity)
568 | .attr('data-identity', member.identity);
569 | }
570 |
571 | var lastIndex = member.lastConsumedMessageIndex;
572 | if (lastIndex) {
573 | $('#channel-messages li[data-index=' + lastIndex + '] p.members-read').append($lastRead);
574 | }
575 | }
576 |
577 | function setActiveChannel(channel) {
578 | if (activeChannel) {
579 | activeChannel.removeListener('messageAdded', addMessage);
580 | activeChannel.removeListener('messageRemoved', removeMessage);
581 | activeChannel.removeListener('messageUpdated', updateMessage);
582 | activeChannel.removeListener('updated', updateActiveChannel);
583 | activeChannel.removeListener('memberUpdated', updateMember);
584 | }
585 |
586 | activeChannel = channel;
587 |
588 | $('#channel-title').text(channel.friendlyName);
589 | $('#channel-messages ul').empty();
590 | $('#channel-members ul').empty();
591 | activeChannel.getAttributes().then(function(attributes) {
592 | $('#channel-desc').text(attributes.description);
593 | });
594 |
595 | $('#send-message').off('click');
596 | $('#send-message').on('click', function() {
597 | var body = $('#message-body-input').val();
598 | channel.sendMessage(body).then(function() {
599 | $('#message-body-input').val('').focus();
600 | $('#channel-messages').scrollTop($('#channel-messages ul').height());
601 | $('#channel-messages li.last-read').removeClass('last-read');
602 | });
603 | });
604 |
605 | activeChannel.on('updated', updateActiveChannel);
606 |
607 | $('#no-channel').hide();
608 | $('#channel').show();
609 |
610 | if (channel.status !== 'joined') {
611 | $('#channel').addClass('view-only');
612 | return;
613 | } else {
614 | $('#channel').removeClass('view-only');
615 | }
616 |
617 | channel.getMessages(30).then(function(page) {
618 | activeChannelPage = page;
619 | page.items.forEach(addMessage);
620 |
621 | channel.on('messageAdded', addMessage);
622 | channel.on('messageUpdated', updateMessage);
623 | channel.on('messageRemoved', removeMessage);
624 |
625 | var newestMessageIndex = page.items.length ? page.items[page.items.length - 1].index : 0;
626 | var lastIndex = channel.lastConsumedMessageIndex;
627 | if (lastIndex && lastIndex !== newestMessageIndex) {
628 | var $li = $('li[data-index='+ lastIndex + ']');
629 | var top = $li.position() && $li.position().top;
630 | $li.addClass('last-read');
631 | $('#channel-messages').scrollTop(top + $('#channel-messages').scrollTop());
632 | }
633 |
634 | if ($('#channel-messages ul').height() <= $('#channel-messages').height()) {
635 | channel.updateLastConsumedMessageIndex(newestMessageIndex).then(updateChannels);
636 | }
637 |
638 | return channel.getMembers();
639 | }).then(function(members) {
640 | updateMembers();
641 |
642 | channel.on('memberJoined', updateMembers);
643 | channel.on('memberLeft', updateMembers);
644 | channel.on('memberUpdated', updateMember);
645 |
646 | members.forEach(member => {
647 | member.getUser().then(user => {
648 | user.on('updated', () => {
649 | updateMember.bind(null, member, user);
650 | updateMembers();
651 | });
652 | });
653 | });
654 | });
655 |
656 | channel.on('typingStarted', function(member) {
657 | member.getUser().then(user => {
658 | typingMembers.add(user.friendlyName || member.identity);
659 | updateTypingIndicator();
660 | });
661 | });
662 |
663 | channel.on('typingEnded', function(member) {
664 | member.getUser().then(user => {
665 | typingMembers.delete(user.friendlyName || member.identity);
666 | updateTypingIndicator();
667 | });
668 | });
669 |
670 | $('#message-body-input').focus();
671 | }
672 |
673 | function clearActiveChannel() {
674 | $('#channel').hide();
675 | $('#no-channel').show();
676 | }
677 |
678 | function updateActiveChannel() {
679 | $('#channel-title').text(activeChannel.friendlyName);
680 | $('#channel-desc').text(activeChannel.attributes.description);
681 | }
682 |
683 | function updateTypingIndicator() {
684 | var message = 'Typing: ';
685 | var names = Array.from(typingMembers).slice(0,3);
686 |
687 | if (typingMembers.size) {
688 | message += names.join(', ');
689 | }
690 |
691 | if (typingMembers.size > 3) {
692 | message += ', and ' + (typingMembers.size-3) + 'more';
693 | }
694 |
695 | if (typingMembers.size) {
696 | message += '...';
697 | } else {
698 | message = '';
699 | }
700 | $('#typing-indicator span').text(message);
701 | }
702 |
703 |
--------------------------------------------------------------------------------
/public/js/vendor/superagent.js:
--------------------------------------------------------------------------------
1 | !function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.superagent=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o= 200 && res.status < 300) {
674 | return self.callback(err, res);
675 | }
676 |
677 | var new_err = new Error(res.statusText || 'Unsuccessful HTTP response');
678 | new_err.original = err;
679 | new_err.response = res;
680 | new_err.status = res.status;
681 |
682 | self.callback(new_err, res);
683 | });
684 | }
685 |
686 | /**
687 | * Mixin `Emitter`.
688 | */
689 |
690 | Emitter(Request.prototype);
691 |
692 | /**
693 | * Allow for extension
694 | */
695 |
696 | Request.prototype.use = function(fn) {
697 | fn(this);
698 | return this;
699 | }
700 |
701 | /**
702 | * Set timeout to `ms`.
703 | *
704 | * @param {Number} ms
705 | * @return {Request} for chaining
706 | * @api public
707 | */
708 |
709 | Request.prototype.timeout = function(ms){
710 | this._timeout = ms;
711 | return this;
712 | };
713 |
714 | /**
715 | * Clear previous timeout.
716 | *
717 | * @return {Request} for chaining
718 | * @api public
719 | */
720 |
721 | Request.prototype.clearTimeout = function(){
722 | this._timeout = 0;
723 | clearTimeout(this._timer);
724 | return this;
725 | };
726 |
727 | /**
728 | * Abort the request, and clear potential timeout.
729 | *
730 | * @return {Request}
731 | * @api public
732 | */
733 |
734 | Request.prototype.abort = function(){
735 | if (this.aborted) return;
736 | this.aborted = true;
737 | this.xhr.abort();
738 | this.clearTimeout();
739 | this.emit('abort');
740 | return this;
741 | };
742 |
743 | /**
744 | * Set header `field` to `val`, or multiple fields with one object.
745 | *
746 | * Examples:
747 | *
748 | * req.get('/')
749 | * .set('Accept', 'application/json')
750 | * .set('X-API-Key', 'foobar')
751 | * .end(callback);
752 | *
753 | * req.get('/')
754 | * .set({ Accept: 'application/json', 'X-API-Key': 'foobar' })
755 | * .end(callback);
756 | *
757 | * @param {String|Object} field
758 | * @param {String} val
759 | * @return {Request} for chaining
760 | * @api public
761 | */
762 |
763 | Request.prototype.set = function(field, val){
764 | if (isObject(field)) {
765 | for (var key in field) {
766 | this.set(key, field[key]);
767 | }
768 | return this;
769 | }
770 | this._header[field.toLowerCase()] = val;
771 | this.header[field] = val;
772 | return this;
773 | };
774 |
775 | /**
776 | * Remove header `field`.
777 | *
778 | * Example:
779 | *
780 | * req.get('/')
781 | * .unset('User-Agent')
782 | * .end(callback);
783 | *
784 | * @param {String} field
785 | * @return {Request} for chaining
786 | * @api public
787 | */
788 |
789 | Request.prototype.unset = function(field){
790 | delete this._header[field.toLowerCase()];
791 | delete this.header[field];
792 | return this;
793 | };
794 |
795 | /**
796 | * Get case-insensitive header `field` value.
797 | *
798 | * @param {String} field
799 | * @return {String}
800 | * @api private
801 | */
802 |
803 | Request.prototype.getHeader = function(field){
804 | return this._header[field.toLowerCase()];
805 | };
806 |
807 | /**
808 | * Set Content-Type to `type`, mapping values from `request.types`.
809 | *
810 | * Examples:
811 | *
812 | * superagent.types.xml = 'application/xml';
813 | *
814 | * request.post('/')
815 | * .type('xml')
816 | * .send(xmlstring)
817 | * .end(callback);
818 | *
819 | * request.post('/')
820 | * .type('application/xml')
821 | * .send(xmlstring)
822 | * .end(callback);
823 | *
824 | * @param {String} type
825 | * @return {Request} for chaining
826 | * @api public
827 | */
828 |
829 | Request.prototype.type = function(type){
830 | this.set('Content-Type', request.types[type] || type);
831 | return this;
832 | };
833 |
834 | /**
835 | * Set Accept to `type`, mapping values from `request.types`.
836 | *
837 | * Examples:
838 | *
839 | * superagent.types.json = 'application/json';
840 | *
841 | * request.get('/agent')
842 | * .accept('json')
843 | * .end(callback);
844 | *
845 | * request.get('/agent')
846 | * .accept('application/json')
847 | * .end(callback);
848 | *
849 | * @param {String} accept
850 | * @return {Request} for chaining
851 | * @api public
852 | */
853 |
854 | Request.prototype.accept = function(type){
855 | this.set('Accept', request.types[type] || type);
856 | return this;
857 | };
858 |
859 | /**
860 | * Set Authorization field value with `user` and `pass`.
861 | *
862 | * @param {String} user
863 | * @param {String} pass
864 | * @return {Request} for chaining
865 | * @api public
866 | */
867 |
868 | Request.prototype.auth = function(user, pass){
869 | var str = btoa(user + ':' + pass);
870 | this.set('Authorization', 'Basic ' + str);
871 | return this;
872 | };
873 |
874 | /**
875 | * Add query-string `val`.
876 | *
877 | * Examples:
878 | *
879 | * request.get('/shoes')
880 | * .query('size=10')
881 | * .query({ color: 'blue' })
882 | *
883 | * @param {Object|String} val
884 | * @return {Request} for chaining
885 | * @api public
886 | */
887 |
888 | Request.prototype.query = function(val){
889 | if ('string' != typeof val) val = serialize(val);
890 | if (val) this._query.push(val);
891 | return this;
892 | };
893 |
894 | /**
895 | * Write the field `name` and `val` for "multipart/form-data"
896 | * request bodies.
897 | *
898 | * ``` js
899 | * request.post('/upload')
900 | * .field('foo', 'bar')
901 | * .end(callback);
902 | * ```
903 | *
904 | * @param {String} name
905 | * @param {String|Blob|File} val
906 | * @return {Request} for chaining
907 | * @api public
908 | */
909 |
910 | Request.prototype.field = function(name, val){
911 | if (!this._formData) this._formData = new root.FormData();
912 | this._formData.append(name, val);
913 | return this;
914 | };
915 |
916 | /**
917 | * Queue the given `file` as an attachment to the specified `field`,
918 | * with optional `filename`.
919 | *
920 | * ``` js
921 | * request.post('/upload')
922 | * .attach(new Blob(['hey! '], { type: "text/html"}))
923 | * .end(callback);
924 | * ```
925 | *
926 | * @param {String} field
927 | * @param {Blob|File} file
928 | * @param {String} filename
929 | * @return {Request} for chaining
930 | * @api public
931 | */
932 |
933 | Request.prototype.attach = function(field, file, filename){
934 | if (!this._formData) this._formData = new root.FormData();
935 | this._formData.append(field, file, filename);
936 | return this;
937 | };
938 |
939 | /**
940 | * Send `data`, defaulting the `.type()` to "json" when
941 | * an object is given.
942 | *
943 | * Examples:
944 | *
945 | * // querystring
946 | * request.get('/search')
947 | * .end(callback)
948 | *
949 | * // multiple data "writes"
950 | * request.get('/search')
951 | * .send({ search: 'query' })
952 | * .send({ range: '1..5' })
953 | * .send({ order: 'desc' })
954 | * .end(callback)
955 | *
956 | * // manual json
957 | * request.post('/user')
958 | * .type('json')
959 | * .send('{"name":"tj"})
960 | * .end(callback)
961 | *
962 | * // auto json
963 | * request.post('/user')
964 | * .send({ name: 'tj' })
965 | * .end(callback)
966 | *
967 | * // manual x-www-form-urlencoded
968 | * request.post('/user')
969 | * .type('form')
970 | * .send('name=tj')
971 | * .end(callback)
972 | *
973 | * // auto x-www-form-urlencoded
974 | * request.post('/user')
975 | * .type('form')
976 | * .send({ name: 'tj' })
977 | * .end(callback)
978 | *
979 | * // defaults to x-www-form-urlencoded
980 | * request.post('/user')
981 | * .send('name=tobi')
982 | * .send('species=ferret')
983 | * .end(callback)
984 | *
985 | * @param {String|Object} data
986 | * @return {Request} for chaining
987 | * @api public
988 | */
989 |
990 | Request.prototype.send = function(data){
991 | var obj = isObject(data);
992 | var type = this.getHeader('Content-Type');
993 |
994 | // merge
995 | if (obj && isObject(this._data)) {
996 | for (var key in data) {
997 | this._data[key] = data[key];
998 | }
999 | } else if ('string' == typeof data) {
1000 | if (!type) this.type('form');
1001 | type = this.getHeader('Content-Type');
1002 | if ('application/x-www-form-urlencoded' == type) {
1003 | this._data = this._data
1004 | ? this._data + '&' + data
1005 | : data;
1006 | } else {
1007 | this._data = (this._data || '') + data;
1008 | }
1009 | } else {
1010 | this._data = data;
1011 | }
1012 |
1013 | if (!obj || isHost(data)) return this;
1014 | if (!type) this.type('json');
1015 | return this;
1016 | };
1017 |
1018 | /**
1019 | * Invoke the callback with `err` and `res`
1020 | * and handle arity check.
1021 | *
1022 | * @param {Error} err
1023 | * @param {Response} res
1024 | * @api private
1025 | */
1026 |
1027 | Request.prototype.callback = function(err, res){
1028 | var fn = this._callback;
1029 | this.clearTimeout();
1030 | fn(err, res);
1031 | };
1032 |
1033 | /**
1034 | * Invoke callback with x-domain error.
1035 | *
1036 | * @api private
1037 | */
1038 |
1039 | Request.prototype.crossDomainError = function(){
1040 | var err = new Error('Origin is not allowed by Access-Control-Allow-Origin');
1041 | err.crossDomain = true;
1042 | this.callback(err);
1043 | };
1044 |
1045 | /**
1046 | * Invoke callback with timeout error.
1047 | *
1048 | * @api private
1049 | */
1050 |
1051 | Request.prototype.timeoutError = function(){
1052 | var timeout = this._timeout;
1053 | var err = new Error('timeout of ' + timeout + 'ms exceeded');
1054 | err.timeout = timeout;
1055 | this.callback(err);
1056 | };
1057 |
1058 | /**
1059 | * Enable transmission of cookies with x-domain requests.
1060 | *
1061 | * Note that for this to work the origin must not be
1062 | * using "Access-Control-Allow-Origin" with a wildcard,
1063 | * and also must set "Access-Control-Allow-Credentials"
1064 | * to "true".
1065 | *
1066 | * @api public
1067 | */
1068 |
1069 | Request.prototype.withCredentials = function(){
1070 | this._withCredentials = true;
1071 | return this;
1072 | };
1073 |
1074 | /**
1075 | * Initiate request, invoking callback `fn(res)`
1076 | * with an instanceof `Response`.
1077 | *
1078 | * @param {Function} fn
1079 | * @return {Request} for chaining
1080 | * @api public
1081 | */
1082 |
1083 | Request.prototype.end = function(fn){
1084 | var self = this;
1085 | var xhr = this.xhr = request.getXHR();
1086 | var query = this._query.join('&');
1087 | var timeout = this._timeout;
1088 | var data = this._formData || this._data;
1089 |
1090 | // store callback
1091 | this._callback = fn || noop;
1092 |
1093 | // state change
1094 | xhr.onreadystatechange = function(){
1095 | if (4 != xhr.readyState) return;
1096 |
1097 | // In IE9, reads to any property (e.g. status) off of an aborted XHR will
1098 | // result in the error "Could not complete the operation due to error c00c023f"
1099 | var status;
1100 | try { status = xhr.status } catch(e) { status = 0; }
1101 |
1102 | if (0 == status) {
1103 | if (self.timedout) return self.timeoutError();
1104 | if (self.aborted) return;
1105 | return self.crossDomainError();
1106 | }
1107 | self.emit('end');
1108 | };
1109 |
1110 | // progress
1111 | var handleProgress = function(e){
1112 | if (e.total > 0) {
1113 | e.percent = e.loaded / e.total * 100;
1114 | }
1115 | self.emit('progress', e);
1116 | };
1117 | if (this.hasListeners('progress')) {
1118 | xhr.onprogress = handleProgress;
1119 | }
1120 | try {
1121 | if (xhr.upload && this.hasListeners('progress')) {
1122 | xhr.upload.onprogress = handleProgress;
1123 | }
1124 | } catch(e) {
1125 | // Accessing xhr.upload fails in IE from a web worker, so just pretend it doesn't exist.
1126 | // Reported here:
1127 | // https://connect.microsoft.com/IE/feedback/details/837245/xmlhttprequest-upload-throws-invalid-argument-when-used-from-web-worker-context
1128 | }
1129 |
1130 | // timeout
1131 | if (timeout && !this._timer) {
1132 | this._timer = setTimeout(function(){
1133 | self.timedout = true;
1134 | self.abort();
1135 | }, timeout);
1136 | }
1137 |
1138 | // querystring
1139 | if (query) {
1140 | query = request.serializeObject(query);
1141 | this.url += ~this.url.indexOf('?')
1142 | ? '&' + query
1143 | : '?' + query;
1144 | }
1145 |
1146 | // initiate request
1147 | xhr.open(this.method, this.url, true);
1148 |
1149 | // CORS
1150 | if (this._withCredentials) xhr.withCredentials = true;
1151 |
1152 | // body
1153 | if ('GET' != this.method && 'HEAD' != this.method && 'string' != typeof data && !isHost(data)) {
1154 | // serialize stuff
1155 | var contentType = this.getHeader('Content-Type');
1156 | var serialize = request.serialize[contentType ? contentType.split(';')[0] : ''];
1157 | if (serialize) data = serialize(data);
1158 | }
1159 |
1160 | // set header fields
1161 | for (var field in this.header) {
1162 | if (null == this.header[field]) continue;
1163 | xhr.setRequestHeader(field, this.header[field]);
1164 | }
1165 |
1166 | // send stuff
1167 | this.emit('request', this);
1168 | xhr.send(data);
1169 | return this;
1170 | };
1171 |
1172 | /**
1173 | * Faux promise support
1174 | *
1175 | * @param {Function} fulfill
1176 | * @param {Function} reject
1177 | * @return {Request}
1178 | */
1179 |
1180 | Request.prototype.then = function (fulfill, reject) {
1181 | return this.end(function(err, res) {
1182 | err ? reject(err) : fulfill(res);
1183 | });
1184 | }
1185 |
1186 | /**
1187 | * Expose `Request`.
1188 | */
1189 |
1190 | request.Request = Request;
1191 |
1192 | /**
1193 | * Issue a request:
1194 | *
1195 | * Examples:
1196 | *
1197 | * request('GET', '/users').end(callback)
1198 | * request('/users').end(callback)
1199 | * request('/users', callback)
1200 | *
1201 | * @param {String} method
1202 | * @param {String|Function} url or callback
1203 | * @return {Request}
1204 | * @api public
1205 | */
1206 |
1207 | function request(method, url) {
1208 | // callback
1209 | if ('function' == typeof url) {
1210 | return new Request('GET', method).end(url);
1211 | }
1212 |
1213 | // url first
1214 | if (1 == arguments.length) {
1215 | return new Request('GET', method);
1216 | }
1217 |
1218 | return new Request(method, url);
1219 | }
1220 |
1221 | /**
1222 | * GET `url` with optional callback `fn(res)`.
1223 | *
1224 | * @param {String} url
1225 | * @param {Mixed|Function} data or fn
1226 | * @param {Function} fn
1227 | * @return {Request}
1228 | * @api public
1229 | */
1230 |
1231 | request.get = function(url, data, fn){
1232 | var req = request('GET', url);
1233 | if ('function' == typeof data) fn = data, data = null;
1234 | if (data) req.query(data);
1235 | if (fn) req.end(fn);
1236 | return req;
1237 | };
1238 |
1239 | /**
1240 | * HEAD `url` with optional callback `fn(res)`.
1241 | *
1242 | * @param {String} url
1243 | * @param {Mixed|Function} data or fn
1244 | * @param {Function} fn
1245 | * @return {Request}
1246 | * @api public
1247 | */
1248 |
1249 | request.head = function(url, data, fn){
1250 | var req = request('HEAD', url);
1251 | if ('function' == typeof data) fn = data, data = null;
1252 | if (data) req.send(data);
1253 | if (fn) req.end(fn);
1254 | return req;
1255 | };
1256 |
1257 | /**
1258 | * DELETE `url` with optional callback `fn(res)`.
1259 | *
1260 | * @param {String} url
1261 | * @param {Function} fn
1262 | * @return {Request}
1263 | * @api public
1264 | */
1265 |
1266 | request.del = function(url, fn){
1267 | var req = request('DELETE', url);
1268 | if (fn) req.end(fn);
1269 | return req;
1270 | };
1271 |
1272 | /**
1273 | * PATCH `url` with optional `data` and callback `fn(res)`.
1274 | *
1275 | * @param {String} url
1276 | * @param {Mixed} data
1277 | * @param {Function} fn
1278 | * @return {Request}
1279 | * @api public
1280 | */
1281 |
1282 | request.patch = function(url, data, fn){
1283 | var req = request('PATCH', url);
1284 | if ('function' == typeof data) fn = data, data = null;
1285 | if (data) req.send(data);
1286 | if (fn) req.end(fn);
1287 | return req;
1288 | };
1289 |
1290 | /**
1291 | * POST `url` with optional `data` and callback `fn(res)`.
1292 | *
1293 | * @param {String} url
1294 | * @param {Mixed} data
1295 | * @param {Function} fn
1296 | * @return {Request}
1297 | * @api public
1298 | */
1299 |
1300 | request.post = function(url, data, fn){
1301 | var req = request('POST', url);
1302 | if ('function' == typeof data) fn = data, data = null;
1303 | if (data) req.send(data);
1304 | if (fn) req.end(fn);
1305 | return req;
1306 | };
1307 |
1308 | /**
1309 | * PUT `url` with optional `data` and callback `fn(res)`.
1310 | *
1311 | * @param {String} url
1312 | * @param {Mixed|Function} data or fn
1313 | * @param {Function} fn
1314 | * @return {Request}
1315 | * @api public
1316 | */
1317 |
1318 | request.put = function(url, data, fn){
1319 | var req = request('PUT', url);
1320 | if ('function' == typeof data) fn = data, data = null;
1321 | if (data) req.send(data);
1322 | if (fn) req.end(fn);
1323 | return req;
1324 | };
1325 |
1326 | /**
1327 | * Expose `request`.
1328 | */
1329 |
1330 | module.exports = request;
1331 |
1332 | },{"emitter":1,"reduce":2}]},{},[3])(3)
1333 | });
1334 |
--------------------------------------------------------------------------------