├── .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 | 81 | 82 |
83 |
84 |
85 |
    86 |
    87 |
    88 | 89 |
    90 |
    91 |
    92 |
    93 |
    94 |
    95 |

    Members

    96 | 97 | 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 = $('