You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

670 lines
20 KiB

  1. /* ******************************
  2. Module - Chat Room
  3. Author: Jack Lukic
  4. Notes: First Commit Aug 8, 2012
  5. Designed as a simple modular chat component
  6. ****************************** */
  7. ;(function ($, window, document, undefined) {
  8. $.fn.chat = function(key, channelName, parameters) {
  9. var
  10. settings = $.extend(true, {}, $.fn.chat.settings, parameters),
  11. // hoist arguments
  12. moduleArguments = arguments || false
  13. ;
  14. $(this)
  15. .each(function() {
  16. var
  17. $module = $(this),
  18. $expandButton = $module.find(settings.selector.expandButton),
  19. $userListButton = $module.find(settings.selector.userListButton),
  20. $userList = $module.find(settings.selector.userList),
  21. $room = $module.find(settings.selector.room),
  22. $userCount = $module.find(settings.selector.userCount),
  23. $log = $module.find(settings.selector.log),
  24. $message = $module.find(settings.selector.message),
  25. $messageInput = $module.find(settings.selector.messageInput),
  26. $messageButton = $module.find(settings.selector.messageButton),
  27. instance = $module.data('module'),
  28. className = settings.className,
  29. namespace = settings.namespace,
  30. html = '',
  31. users = {},
  32. loggedInUser,
  33. message,
  34. count,
  35. height,
  36. pusher,
  37. module
  38. ;
  39. module = {
  40. channel: false,
  41. width: {
  42. log : $log.width(),
  43. userList : $userList.outerWidth()
  44. },
  45. initialize: function() {
  46. // check error conditions
  47. if(Pusher === undefined) {
  48. module.error(settings.errors.pusher);
  49. }
  50. if(key === undefined || channelName === undefined) {
  51. module.error(settings.errors.key);
  52. return false;
  53. }
  54. else if( !(settings.endpoint.message || settings.endpoint.authentication) ) {
  55. module.error(settings.errors.endpoint);
  56. return false;
  57. }
  58. // define pusher
  59. pusher = new Pusher(key);
  60. Pusher.channel_auth_endpoint = settings.endpoint.authentication;
  61. module.channel = pusher.subscribe(channelName);
  62. module.channel.bind('pusher:subscription_succeeded', module.user.list.create);
  63. module.channel.bind('pusher:subscription_error', module.error);
  64. module.channel.bind('pusher:member_added', module.user.joined);
  65. module.channel.bind('pusher:member_removed', module.user.left);
  66. module.channel.bind('update_messages', module.message.receive);
  67. $.each(settings.customEvents, function(label, value) {
  68. module.channel.bind(label, value);
  69. });
  70. // expandable with states
  71. if( $.fn.hoverClass !== undefined && $.fn.downClass !== undefined ) {
  72. $expandButton
  73. .hoverClass()
  74. .downClass()
  75. ;
  76. $userListButton
  77. .hoverClass()
  78. .downClass()
  79. ;
  80. $messageButton
  81. .hoverClass()
  82. .downClass()
  83. ;
  84. }
  85. // bind module events
  86. $userListButton
  87. .on('click.' + namespace, module.event.toggleUserList)
  88. ;
  89. $expandButton
  90. .on('click.' + namespace, module.event.toggleExpand)
  91. ;
  92. $messageInput
  93. .on('keydown.' + namespace, module.event.input.keydown)
  94. .on('keyup.' + namespace, module.event.input.keyup)
  95. ;
  96. $messageButton
  97. .on('mouseenter.' + namespace, module.event.hover)
  98. .on('mouseleave.' + namespace, module.event.hover)
  99. .on('click.' + namespace, module.event.submit)
  100. ;
  101. // scroll to bottom of chat log
  102. $log
  103. .animate({
  104. scrollTop: $log.prop('scrollHeight')
  105. }, 400)
  106. ;
  107. $module
  108. .data('module', module)
  109. .addClass(className.loading)
  110. ;
  111. },
  112. // refresh module
  113. refresh: function() {
  114. // reset width calculations
  115. $userListButton
  116. .removeClass(className.active)
  117. ;
  118. module.width = {
  119. log : $log.width(),
  120. userList : $userList.outerWidth()
  121. };
  122. if( $userListButton.hasClass(className.active) ) {
  123. module.user.list.hide();
  124. }
  125. $module.data('module', module);
  126. },
  127. user: {
  128. updateCount: function() {
  129. if(settings.userCount) {
  130. users = $module.data('users');
  131. count = 0;
  132. $.each(users, function(index){
  133. count++;
  134. });
  135. $userCount
  136. .html( settings.templates.userCount(count) )
  137. ;
  138. }
  139. },
  140. // add user to user list
  141. joined: function(member) {
  142. users = $module.data('users');
  143. if(member.id != 'anonymous' && users[ member.id ] === undefined ) {
  144. users[ member.id ] = member.info;
  145. if(settings.randomColor && member.info.color === undefined) {
  146. member.info.color = settings.templates.color(member.id);
  147. }
  148. html = settings.templates.userList(member.info);
  149. if(member.info.isAdmin) {
  150. $(html)
  151. .prependTo($userList)
  152. ;
  153. }
  154. else {
  155. $(html)
  156. .appendTo($userList)
  157. ;
  158. }
  159. if(settings.partingMessages) {
  160. $log
  161. .append( settings.templates.joined(member.info) )
  162. ;
  163. module.message.scroll.test();
  164. }
  165. module.user.updateCount();
  166. }
  167. },
  168. // remove user from user list
  169. left: function(member) {
  170. users = $module.data('users');
  171. if(member !== undefined && member.id !== 'anonymous') {
  172. delete users[ member.id ];
  173. $module
  174. .data('users', users)
  175. ;
  176. $userList
  177. .find('[data-id='+ member.id + ']')
  178. .remove()
  179. ;
  180. if(settings.partingMessages) {
  181. $log
  182. .append( settings.templates.left(member.info) )
  183. ;
  184. module.message.scroll.test();
  185. }
  186. module.user.updateCount();
  187. }
  188. },
  189. list: {
  190. // receives list of members and generates user list
  191. create: function(members) {
  192. users = {};
  193. members.each(function(member) {
  194. if(member.id !== 'anonymous' && member.id !== 'undefined') {
  195. if(settings.randomColor && member.info.color === undefined) {
  196. member.info.color = settings.templates.color(member.id);
  197. }
  198. // sort list with admin first
  199. html = (member.info.isAdmin)
  200. ? settings.templates.userList(member.info) + html
  201. : html + settings.templates.userList(member.info)
  202. ;
  203. users[ member.id ] = member.info;
  204. }
  205. });
  206. $module
  207. .data('users', users)
  208. .data('user', users[members.me.id] )
  209. .removeClass(className.loading)
  210. ;
  211. $userList
  212. .html(html)
  213. ;
  214. module.user.updateCount();
  215. $.proxy(settings.onJoin, $userList.children())();
  216. },
  217. // shows user list
  218. show: function() {
  219. $log
  220. .animate({
  221. width: (module.width.log - module.width.userList)
  222. }, {
  223. duration : settings.speed,
  224. easing : settings.easing,
  225. complete : module.message.scroll.move
  226. })
  227. ;
  228. },
  229. // hides user list
  230. hide: function() {
  231. $log
  232. .stop()
  233. .animate({
  234. width: (module.width.log)
  235. }, {
  236. duration : settings.speed,
  237. easing : settings.easing,
  238. complete : module.message.scroll.move
  239. })
  240. ;
  241. }
  242. }
  243. },
  244. message: {
  245. // handles scrolling of chat log
  246. scroll: {
  247. test: function() {
  248. height = $log.prop('scrollHeight') - $log.height();
  249. if( Math.abs($log.scrollTop() - height) < settings.scrollArea) {
  250. module.message.scroll.move();
  251. }
  252. },
  253. move: function() {
  254. height = $log.prop('scrollHeight') - $log.height();
  255. $log
  256. .scrollTop(height)
  257. ;
  258. }
  259. },
  260. // sends chat message
  261. send: function(message) {
  262. if( !module.utils.emptyString(message) ) {
  263. $.api({
  264. url : settings.endpoint.message,
  265. method : 'POST',
  266. data : {
  267. 'chat_message': {
  268. content : message,
  269. timestamp : new Date().getTime()
  270. }
  271. }
  272. });
  273. }
  274. },
  275. // receives chat response and processes
  276. receive: function(response) {
  277. message = response.data;
  278. users = $module.data('users');
  279. loggedInUser = $module.data('user');
  280. if(users[ message.userID] !== undefined) {
  281. // logged in user's messages already pushed instantly
  282. if(loggedInUser === undefined || loggedInUser.id != message.userID) {
  283. message.user = users[ message.userID ];
  284. module.message.display(message);
  285. }
  286. }
  287. },
  288. // displays message in chat log
  289. display: function(message) {
  290. $log
  291. .append( settings.templates.message(message) )
  292. ;
  293. module.message.scroll.test();
  294. $.proxy(settings.onMessage, $log.children().last() )();
  295. }
  296. },
  297. expand: function() {
  298. $module
  299. .addClass(className.expand)
  300. ;
  301. $.proxy(settings.onExpand, $module )();
  302. module.refresh();
  303. },
  304. contract: function() {
  305. $module
  306. .removeClass(className.expand)
  307. ;
  308. $.proxy(settings.onContract, $module )();
  309. module.refresh();
  310. },
  311. event: {
  312. input: {
  313. keydown: function(event) {
  314. if(event.which == 13) {
  315. $messageButton
  316. .addClass(className.down)
  317. ;
  318. }
  319. },
  320. keyup: function(event) {
  321. if(event.which == 13) {
  322. $messageButton
  323. .removeClass(className.down)
  324. ;
  325. module.event.submit();
  326. }
  327. }
  328. },
  329. // handles message form submit
  330. submit: function() {
  331. var
  332. message = $messageInput.val(),
  333. loggedInUser = $module.data('user')
  334. ;
  335. if(loggedInUser !== undefined && !module.utils.emptyString(message)) {
  336. module.message.send(message);
  337. // display immediately
  338. module.message.display({
  339. user: loggedInUser,
  340. text: message
  341. });
  342. module.message.scroll.move();
  343. $messageInput
  344. .val('')
  345. ;
  346. }
  347. },
  348. // handles button click on expand button
  349. toggleExpand: function() {
  350. if( !$module.hasClass(className.expand) ) {
  351. $expandButton
  352. .addClass(className.active)
  353. ;
  354. module.expand();
  355. }
  356. else {
  357. $expandButton
  358. .removeClass(className.active)
  359. ;
  360. module.contract();
  361. }
  362. },
  363. // handles button click on user list button
  364. toggleUserList: function() {
  365. if( !$log.is(':animated') ) {
  366. if( !$userListButton.hasClass(className.active) ) {
  367. $userListButton
  368. .addClass(className.active)
  369. ;
  370. module.user.list.show();
  371. }
  372. else {
  373. $userListButton
  374. .removeClass('active')
  375. ;
  376. module.user.list.hide();
  377. }
  378. }
  379. }
  380. },
  381. utils: {
  382. emptyString: function(string) {
  383. if(typeof string == 'string') {
  384. return (string.search(/\S/) == -1);
  385. }
  386. return false;
  387. }
  388. },
  389. // standard methods
  390. debug: function(message) {
  391. if(settings.debug) {
  392. console.info(settings.moduleName + ': ' + message);
  393. }
  394. },
  395. error: function(errorMessage) {
  396. console.warn(settings.moduleName + ': ' + errorMessage);
  397. },
  398. invoke: function(methodName, context, methodArguments) {
  399. var
  400. method
  401. ;
  402. methodArguments = methodArguments || Array.prototype.slice.call( arguments, 2 );
  403. if(typeof methodName == 'string' && instance !== undefined) {
  404. methodName = methodName.split('.');
  405. $.each(methodName, function(index, name) {
  406. if( $.isPlainObject( instance[name] ) ) {
  407. instance = instance[name];
  408. return true;
  409. }
  410. else if( $.isFunction( instance[name] ) ) {
  411. method = instance[name];
  412. return true;
  413. }
  414. module.error(settings.errors.method);
  415. return false;
  416. });
  417. }
  418. return ( $.isFunction( method ) )
  419. ? method.apply(context, methodArguments)
  420. : false
  421. ;
  422. }
  423. };
  424. if(instance !== undefined && moduleArguments) {
  425. // simpler than invoke realizing to invoke itself (and losing scope due prototype.call()
  426. if(moduleArguments[0] == 'invoke') {
  427. moduleArguments = Array.prototype.slice.call( moduleArguments, 1 );
  428. }
  429. return module.invoke(moduleArguments[0], this, Array.prototype.slice.call( moduleArguments, 1 ) );
  430. }
  431. // initializing
  432. module.initialize();
  433. })
  434. ;
  435. return this;
  436. };
  437. $.fn.chat.settings = {
  438. moduleName : 'Chat Module',
  439. debug : false,
  440. namespace : 'chat',
  441. onJoin : function(){},
  442. onMessage : function(){},
  443. onExpand : function(){},
  444. onContract : function(){},
  445. customEvents : {},
  446. partingMessages : false,
  447. userCount : true,
  448. randomColor : true,
  449. speed : 300,
  450. easing : 'easeOutQuint',
  451. // pixels from bottom of chat log that should trigger auto scroll to bottom
  452. scrollArea : 9999,
  453. endpoint : {
  454. message : false,
  455. authentication : false
  456. },
  457. errors: {
  458. method : 'The method you called is not defined',
  459. endpoint : 'Please define a message and authentication endpoint.',
  460. key : 'You must specify a pusher key and channel.',
  461. pusher : 'You must include the Pusher library.'
  462. },
  463. className : {
  464. expand : 'expand',
  465. active : 'active',
  466. hover : 'hover',
  467. down : 'down',
  468. loading : 'loading'
  469. },
  470. selector : {
  471. userCount : '.actions .message',
  472. userListButton : '.actions .button.user-list',
  473. expandButton : '.actions .button.expand',
  474. room : '.room',
  475. userList : '.room .user-list',
  476. log : '.room .log',
  477. message : '.room .log .message',
  478. author : '.room log .message .author',
  479. messageInput : '.talk input',
  480. messageButton : '.talk .send.button'
  481. },
  482. templates: {
  483. userCount: function(number) {
  484. return number + ' users in chat';
  485. },
  486. color: function(userID) {
  487. var
  488. colors = [
  489. '#000000',
  490. '#333333',
  491. '#666666',
  492. '#999999',
  493. '#CC9999',
  494. '#CC6666',
  495. '#CC3333',
  496. '#993333',
  497. '#663333',
  498. '#CC6633',
  499. '#CC9966',
  500. '#CC9933',
  501. '#999966',
  502. '#CCCC66',
  503. '#99CC66',
  504. '#669933',
  505. '#669966',
  506. '#33A3CC',
  507. '#336633',
  508. '#33CCCC',
  509. '#339999',
  510. '#336666',
  511. '#336699',
  512. '#6666CC',
  513. '#9966CC',
  514. '#333399',
  515. '#663366',
  516. '#996699',
  517. '#993366',
  518. '#CC6699'
  519. ]
  520. ;
  521. return colors[ Math.floor( Math.random() * colors.length) ];
  522. },
  523. message: function(message) {
  524. var
  525. html = ''
  526. ;
  527. if(message.user.isAdmin) {
  528. message.user.color = '#55356A';
  529. html += '<div class="admin message">';
  530. html += '<span class="quirky ui flag team"></span>';
  531. }
  532. /*
  533. else if(message.user.isPro) {
  534. html += '<div class="indent message">';
  535. html += '<span class="quirky ui flag pro"></span>';
  536. }
  537. */
  538. else {
  539. html += '<div class="message">';
  540. }
  541. html += '<p>';
  542. if(message.user.color !== undefined) {
  543. html += '<span class="author" style="color: ' + message.user.color + ';">' + message.user.name + '</span>: ';
  544. }
  545. else {
  546. html += '<span class="author">' + message.user.name + '</span>: ';
  547. }
  548. html += ''
  549. + message.text
  550. + ' </p>'
  551. + '</div>'
  552. ;
  553. return html;
  554. },
  555. joined: function(member) {
  556. return (typeof member.name !== undefined)
  557. ? '<div class="status">' + member.name + ' has joined the chat.</div>'
  558. : false
  559. ;
  560. },
  561. left: function(member) {
  562. return (typeof member.name !== undefined)
  563. ? '<div class="status">' + member.name + ' has left the chat.</div>'
  564. : false
  565. ;
  566. },
  567. userList: function(member) {
  568. var
  569. html = ''
  570. ;
  571. if(member.isAdmin) {
  572. member.color = '#55356A';
  573. }
  574. html += ''
  575. + '<div class="user" data-id="' + member.id + '">'
  576. + ' <div class="image">'
  577. + ' <img src="' + member.avatarURL + '">'
  578. + ' </div>'
  579. ;
  580. if(member.color !== undefined) {
  581. html += ' <p><a href="/users/' + member.id + '" target="_blank" style="color: ' + member.color + ';">' + member.name + '</a></p>';
  582. }
  583. else {
  584. html += ' <p><a href="/users/' + member.id + '" target="_blank">' + member.name + '</a></p>';
  585. }
  586. html += '</div>';
  587. return html;
  588. }
  589. }
  590. };
  591. })( jQuery, window , document );