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.

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