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.

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