A unf. social network done poorly.
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.

1639 lines
62KB

  1. AjaxIM = function(options, actions) {
  2. if(this instanceof AjaxIM) {
  3. var self = this;
  4. // === {{{ defaults }}} ===
  5. //
  6. // These are the available settings for Ajax IM, and the associated
  7. // defaults:
  8. //
  9. // * {{{pollServer}}} is the default URL to which all actions refer. It is
  10. // possible to specify certain action URLs separately (as is used with the
  11. // NodeJS server).
  12. // * {{{theme}}} is the name of the theme folder that defines the HTML and
  13. // CSS of the IM bar and chat boxes. Usually, themes are deposited in the
  14. // provided "themes" folder and specified by that path, e.g. {{{themes/default}}}.
  15. // Theme files within the theme folder must be named {{{theme.html}}} and
  16. // {{{theme.css}}}.
  17. var defaults = {
  18. pollServer: '',
  19. theme: 'themes/default'
  20. };
  21. // === {{{AjaxIM.}}}**{{{settings}}}** ===
  22. //
  23. // These are the settings for the IM. If particular options are not specified,
  24. // the defaults (see above) will be used. //These options will be defined
  25. // upon calling the initialization function, and not set directly.//
  26. this.settings = $.extend(defaults, options);
  27. // === {{{AjaxIM.}}}**{{{actions}}}** ===
  28. //
  29. // Each individual action that the IM engine can execute is predefined here.
  30. // By default, it merely appends the action onto the end of the {{{pollServer}}} url,
  31. // however, it is possible to define actions individually. //The alternative actions
  32. // will be defined upon calling the initialization function, and not set directly.//
  33. //
  34. // Should you define an action at a different URL, Ajax IM will determine whether
  35. // or not this URL is within the current domain. If it is within a subdomain of
  36. // the current domain, it will set the document.domain variable for you,
  37. // to match a broader hostname scope; the action will continue to use {{{$.post}}}
  38. // (the default AJAX method for Ajax IM).
  39. //
  40. // On the other hand, should you choose a URL outside the current domain
  41. // Ajax IM will switch to {{{$.getJSON}}} (a get request) to avoid
  42. // cross-domain scripting issues. This means that a server on a different
  43. // port or at a different address will need to be able to handle GET
  44. // requests rather than POST requests (such as how the Node.JS Ajax IM
  45. // server works).
  46. this.actions = $.extend({
  47. noop: this.settings.pollServer + '/app/noop',
  48. listen: this.settings.pollServer + '/app/listen',
  49. send: this.settings.pollServer + '/app/message',
  50. status: this.settings.pollServer + '/app/status',
  51. signoff: this.settings.pollServer + '/app/signoff'
  52. }, actions);
  53. // If Socket.IO is available, create a socket
  54. self.socket = null;
  55. $.getScript(this.settings.pollServer+'/socket.io/socket.io.js', function(){
  56. self.socket = io(self.settings.pollServer);
  57. self.socket.on('client', function(event) {
  58. event = $.extend(true, {}, event);
  59. self.dispatchEvent(event);
  60. });
  61. var event = {type: 'hello', from: this.username, sessionID: cookies.get('sessionid')};
  62. self.sendEvent(event);
  63. });
  64. // We load the theme dynamically based on the passed
  65. // settings. If the theme is set to false, we assume
  66. // that the user is going to load it himself.
  67. this.themeLoaded = false;
  68. if(this.settings.theme) {
  69. if(typeof document.createStyleSheet == 'function')
  70. document.createStyleSheet(this.settings.theme + '/theme.css');
  71. else
  72. $('body').append('<link rel="stylesheet" href="' +
  73. this.settings.theme + '/theme.css" />');
  74. $('<div>').appendTo('body').load(this.settings.theme + '/theme.html #imjs-bar, .imjs-tooltip',
  75. function() {
  76. self.themeLoaded = true;
  77. self.setup();
  78. }
  79. );
  80. } else {
  81. this.themeLoaded = true;
  82. this.setup();
  83. }
  84. // Allow a chatbox to be minimized
  85. $(document).on('click', '.imjs-chatbox', function(e) {
  86. e.preventDefault();
  87. return false;
  88. });
  89. $(document).on('click', '.imjs-chatbox .imjs-minimize', function() {
  90. $(this).parents('.imjs-selected').click();
  91. });
  92. // Setup message sending for all chatboxes
  93. $(document).on('keydown', '.imjs-chatbox .imjs-input', function(event) {
  94. var obj = $(this);
  95. // if(event.keyCode == 13 && !($.browser.msie && $.browser.version < 8)) {
  96. if(event.keyCode == 13) {
  97. self.send(obj.parents('.imjs-chatbox').data('username'), obj.val());
  98. }
  99. }).on('keyup', '.imjs-chatbox .imjs-input', function(event) {
  100. if(event.keyCode == 13) {
  101. // if($.browser.msie && $.browser.version < 8) {
  102. if(false) {
  103. var obj = $(this);
  104. self.send(obj.parents('.imjs-chatbox').data('username'), obj.val());
  105. }
  106. var obj = $(this);
  107. obj.val('');
  108. obj.height(obj.data('height'));
  109. }
  110. }).on('keypress', '.imjs-chatbox .imjs-input', function(e) {
  111. var obj = $(this);
  112. obj.height(0);
  113. if(true) obj.height(0);
  114. if(this.scrollHeight > obj.height() || this.scrollHeight < obj.height()) {
  115. obj.height(this.scrollHeight);
  116. }
  117. });
  118. $(document).on('click', '.imjs-msglog', function() {
  119. var chatbox = $(this).parents('.imjs-chatbox');
  120. chatbox.find('.imjs-input').focus();
  121. });
  122. // Create a chatbox when a buddylist item is clicked
  123. $(document).on('click', '.imjs-friend', function() {
  124. var chatbox = self._createChatbox($(this).data('friend'));
  125. if(chatbox.parents('.imjs-tab').data('state') != 'active') {
  126. chatbox.parents('.imjs-tab').click();
  127. store.set(self.username + '-activeTab', $(this).data('friend'));
  128. }
  129. chatbox.find('.imjs-input').focus();
  130. if(!(input = chatbox.find('.imjs-input')).data('height')) {
  131. // store the height for resizing later
  132. if (!input.height()) {
  133. input.height(16);
  134. }
  135. input.data('height', input.height());
  136. }
  137. });
  138. // Setup and hide the scrollers
  139. $('.imjs-scroll').css('display', 'none');
  140. $(document).on('click', '#imjs-scroll-right', function() {
  141. var hiddenTab = $(this)
  142. .prevAll('#imjs-bar li.imjs-tab:hidden')
  143. .filter(function() {
  144. return (
  145. $(this).data('state') != 'closed' &&
  146. $(this).prev('#imjs-bar li.imjs-tab:visible').length
  147. );
  148. })
  149. .not('.imjs-default')
  150. .slice(-1)
  151. .css('display', '');
  152. if(hiddenTab.length) {
  153. $('#imjs-bar li.imjs-tab:visible').eq(0).css('display', 'none');
  154. $(this).html(parseInt($(this).html()) - 1);
  155. $('#imjs-scroll-left').html(parseInt($('#imjs-scroll-left').html()) + 1);
  156. }
  157. return false;
  158. });
  159. $(document).on('click', '#imjs-scroll-left', function() {
  160. var hiddenTab = $(this)
  161. .nextAll('#imjs-bar li.imjs-tab:hidden')
  162. .filter(function() {
  163. return (
  164. $(this).data('state') != 'closed' &&
  165. $(this).next('#imjs-bar li.imjs-tab:visible').length
  166. );
  167. })
  168. .not('.imjs-default')
  169. .slice(-1)
  170. .css('display', '');
  171. if(hiddenTab.length) {
  172. $('#imjs-bar li.imjs-tab:visible').slice(-1).css('display', 'none');
  173. $(this).html(parseInt($(this).html()) - 1);
  174. $('#imjs-scroll-right').html(parseInt($('#imjs-scroll-right').html()) + 1);
  175. }
  176. return false;
  177. });
  178. // Setup status buttons
  179. $(document).on('click', '#imjs-status-panel .imjs-button', function() {
  180. var status = this.id.split('-')[2];
  181. $('#imjs-away-message-text, #imjs-away-message-text-arrow').animate({
  182. opacity: (status == 'away' ? 'show' : 'hide'),
  183. height: (status == 'away' ? 'show' : 'hide')
  184. }, 50);
  185. $('#imjs-status-panel .imjs-button').removeClass('imjs-toggled');
  186. $(this).addClass('imjs-toggled');
  187. if(self.current_status[0] == 'away')
  188. self._last_status_message = $('#imjs-away-message-text').val();
  189. $('#imjs-away-message-text').val(status == 'away'
  190. ? self._last_status_message ||
  191. AjaxIM.l10n.defaultAway
  192. : '');
  193. self.status(status, $('#imjs-away-message-text').val());
  194. return false;
  195. });
  196. // Allow status message to be changed
  197. $(document)
  198. .on('keyup', '#imjs-away-message-text', (function() {
  199. var msg_type_timer = null;
  200. return function() {
  201. if(msg_type_timer) clearTimeout(msg_type_timer);
  202. msg_type_timer = setTimeout(function() {
  203. self._last_status_message =
  204. self.current_status[1] = $('#imjs-away-message-text')
  205. .addClass('imjs-loading').val();
  206. self.status.apply(self, self.current_status);
  207. }, 250);
  208. };
  209. })());
  210. $(this).bind('changeStatusSuccessful changeStatusFailed', function() {
  211. $('#imjs-away-message-text').removeClass('imjs-loading');
  212. });
  213. // Setup reconnect button
  214. $(document).on('click', '#imjs-reconnect', function() {
  215. self.offline = false;
  216. store.remove(self.username + '-offline');
  217. $('#imjs-reconnect').hide();
  218. $('.imjs-input').attr('disabled', false);
  219. // Restore status to available
  220. $('#imjs-status-panel .imjs-button').removeClass('imjs-toggled');
  221. $('#imjs-button-available').addClass('imjs-toggled');
  222. $(self.statuses).each(function() {
  223. $('#imjs-friends').removeClass('imjs-' + this);
  224. });
  225. $('#imjs-friends').addClass('imjs-available');
  226. $('#imjs-away-message-text, #imjs-away-message-text-arrow')
  227. .css('display', 'none');
  228. // Set status
  229. self.current_status = ['available', ''];
  230. store.set(self.username + '-status', ['available', '']);
  231. self.status('available', '');
  232. // Reconnect
  233. self.storage();
  234. self.listen();
  235. });
  236. // Initialize the chatbox hash
  237. this.chats = {};
  238. // On window resize, check scroller visibility
  239. $(window).resize(function() {
  240. try {
  241. self._scrollers();
  242. } catch(e) {}
  243. });
  244. // Set up event handling
  245. this.onEvent('hello', this.onHello);
  246. this.onEvent('message', this.onMessage);
  247. this.onEvent('status', this.onStatus);
  248. this.onEvent('notice', this.onNotice);
  249. this.onEvent('goodbye', this.onGoodbye);
  250. } else {
  251. return AjaxIM.init(options);
  252. }
  253. };
  254. $.extend(AjaxIM.prototype, {
  255. // == Main ==
  256. setup: function() {
  257. var self = this;
  258. $(this).trigger('loadComplete');
  259. $('.imjs-scroll').css('display', 'none');
  260. this.initTabBar();
  261. this._scrollers();
  262. this.username = store.get('user');
  263. this._lastReconnect = 0;
  264. if(this.username && store.get(this.username + '-offline') == true) {
  265. this.offline = true;
  266. setTimeout(function() { self._showReconnect(); }, 0);
  267. return;
  268. }
  269. if(this.username)
  270. this.storage();
  271. setTimeout(function() { if (!self.socket) self.listen(); }, 2000);
  272. },
  273. // === {{{AjaxIM.}}}**{{{storage()}}}** ===
  274. //
  275. // Retrieves chat session data from whatever storage engine is enabled
  276. // (provided that one is enabled at all). If a page reloads, this function
  277. // is called to restore the user's chat state (existing conversations, active tab).
  278. // This function is called //automatically//, upon initialization of the IM engine.
  279. storage: function() {
  280. var self = this,
  281. chatstore = store.get(this.username + '-chats'),
  282. friends = store.get(this.username + '-friends'),
  283. status = store.get(this.username + '-status') || ['available', ''];
  284. this.chatstore = chatstore || {};
  285. this.friends = {};
  286. this.current_status = status;
  287. if(friends) {
  288. $.each(friends, function(friend, data) {
  289. self.addFriend(friend, data.status, data.group);
  290. });
  291. $('#imjs-friends').removeClass('imjs-not-connected')
  292. .addClass('imjs-' + status[0]);
  293. $('#imjs-button-' + status[0]).addClass('imjs-toggled');
  294. if(status[0] == 'away') {
  295. setTimeout(function() {
  296. $('#imjs-away-message-text, #imjs-away-message-text-arrow').show();
  297. }, 250);
  298. $('#imjs-away-message-text').val(this.current_status[1]);
  299. }
  300. }
  301. $.each(this.chatstore, function(username, convo) {
  302. if(!convo.length) return;
  303. var chatbox = self._createChatbox(username, true),
  304. msglog = chatbox.find('.imjs-msglog').empty();
  305. chatbox.data('lastDateStamp', null).css('display', 'none');
  306. if(typeof convo == 'string')
  307. convo = self.chatstore[username] = JSON.parse(convo);
  308. // Restore all messages, date stamps, and errors
  309. msglog.html(convo.join(''));
  310. $(self).trigger('chatRestored', [username, chatbox]);
  311. });
  312. var activeTab = store.get(this.username + '-activeTab');
  313. if(activeTab && activeTab in this.chats) {
  314. this.chats[activeTab].parents('.imjs-tab').click();
  315. var msglog = this.chats[activeTab].find('.imjs-msglog');
  316. msglog[0].scrollTop = msglog[0].scrollHeight;
  317. }
  318. // Set username in Friends list
  319. var header = $('#imjs-friends-panel .imjs-header');
  320. header.html(header.html().replace('{username}', this.username));
  321. },
  322. // === //private// {{{AjaxIM.}}}**{{{_clearSession()}}}** ===
  323. //
  324. // Clears all session data from the last known user.
  325. _clearSession: function() {
  326. var last_user = store.get('user');
  327. $.each(['friends', 'activeTab', 'chats', 'status', 'connected'],
  328. function(i, key) {
  329. store.remove(last_user + '-' + key);
  330. });
  331. store.set('user', '');
  332. this.chats = {};
  333. this.friends = {};
  334. this.chatstore = {};
  335. this.current_status = ['available', ''];
  336. $('.imjs-tab').not('.imjs-tab.imjs-default').remove();
  337. $('.imjs-friend-group').not('.imjs-friend-group.imjs-default').remove();
  338. delete this.username;
  339. },
  340. // === {{{AjaxIM.}}}**{{{listen()}}}** ===
  341. //
  342. // Queries the server for new messages.
  343. listen: function() {
  344. if(this.offline) return;
  345. var self = this;
  346. AjaxIM.get(
  347. this.actions.listen,
  348. {},
  349. function(response) {
  350. if($.isArray(response)) {
  351. $.each(response, function(key, value) {
  352. self._parseMessage(value);
  353. });
  354. }
  355. else if($.isPlainObject(response)) {
  356. self._parseMessage(response);
  357. }
  358. if (!self.socket) {
  359. setTimeout(function() { self.listen(); }, 0);
  360. }
  361. },
  362. function(error) {
  363. self._notConnected();
  364. $(self).trigger('pollFailed', ['not connected']);
  365. // Try reconnecting in n*2 seconds (max 16)
  366. self._reconnectIn = (self._lastReconnect < (new Date()) - 60000)
  367. ? 1000
  368. : Math.min(self._reconnectIn * 2, 16000);
  369. self._lastReconnect = new Date();
  370. if (!self.socket) {
  371. setTimeout(function() { self.listen(); }, self._reconnectIn);
  372. }
  373. },
  374. this.actions.noop
  375. );
  376. },
  377. // === //private// {{{AjaxIM.}}}**{{{_parseMessages(messages)}}}** ===
  378. //
  379. _parseMessage: function(message) {
  380. this.triggerEvent(message);
  381. },
  382. onHello: function(message) {
  383. var self = this;
  384. this._clearSession();
  385. this.username = message.username;
  386. this.current_status = ['available', ''];
  387. store.set('user', message.username);
  388. store.set(this.username + '-status', this.current_status);
  389. $('#imjs-friends').attr('class', 'imjs-available');
  390. $.each(message.friends, function() {
  391. var friend;
  392. if(this.length == 2)
  393. friend = this;
  394. else
  395. friend = [this.toString(), ['offline', '']];
  396. self.addFriend(friend[0], friend[1], 'Friends');
  397. });
  398. store.set(this.username + '-friends', this.friends);
  399. // Set username in Friends list
  400. var header = $('#imjs-friends-panel .imjs-header');
  401. header.html(header.html().replace('{username}', this.username));
  402. // Set status available
  403. $('#imjs-away-message-text, #imjs-away-message-text-arrow').hide();
  404. $('#imjs-status-panel .imjs-button').removeClass('imjs-toggled');
  405. $('#imjs-button-available').addClass('imjs-toggled');
  406. },
  407. onMessage: function(event) {
  408. this.incoming(event.from, event.body);
  409. },
  410. onStatus: function(event) {
  411. this._friendUpdate(event.from, event.status, event.message);
  412. this._storeFriends();
  413. },
  414. onNotice: function(event) {
  415. },
  416. onGoodbye: function(event) {
  417. this._notConnected();
  418. },
  419. // === {{{AjaxIM.}}}**{{{incoming(from, message)}}}** ===
  420. //
  421. // Handles a new message from another user. If a chatbox for that
  422. // user does not yet exist, one is created. If it does exist, but
  423. // is minimized, the user is notified but the chatbox is not brought
  424. // to the front. This function also stores the message, if a storage
  425. // method is set.
  426. //
  427. // ==== Parameters ====
  428. // * {{{from}}} is the username of the sender.
  429. // * {{{message}}} is the body.
  430. incoming: function(from, message) {
  431. // check if IM exists, otherwise create new window
  432. // TODO: If friend is not on the buddylist,
  433. // should add them to a temp list?
  434. var chatbox = this._createChatbox(from),
  435. tab = chatbox.parents('.imjs-tab');
  436. if(!$('#imjs-bar .imjs-selected').length) {
  437. tab.click();
  438. } else if(tab.data('state') != 'active') {
  439. this.notification(tab);
  440. }
  441. this._store(from, this._addMessage('b', chatbox, from, message));
  442. },
  443. // === {{{AjaxIM.}}}**{{{addFriend(username, group)}}}** ===
  444. //
  445. // Inserts a new friend into the friends list. If the group specified
  446. // doesn't exist, it is created. If the friend is already in this group,
  447. // they aren't added again, however, the friend item is returned.
  448. //
  449. // ==== Parameters ====
  450. // * {{{username}}} is the username of the new friend.
  451. // * {{{status}}} is the current status of the friend.
  452. // * {{{group}}} is the user group to which the friend should be added.
  453. addFriend: function(username, status, group) {
  454. var group_id = 'imjs-group-' + md5.hex(group);
  455. if(!(group_item = $('#' + group_id)).length) {
  456. var group_item = $('.imjs-friend-group.imjs-default').clone()
  457. .removeClass('imjs-default')
  458. .attr('id', group_id)
  459. .data('group', group)
  460. .appendTo('#imjs-friends-list');
  461. var group_header = group_item.find('.imjs-friend-group-header');
  462. group_header.html(group_header.html().replace('{group}', group));
  463. }
  464. var user_id = 'imjs-friend-' + md5.hex(username + group);
  465. if(!$('#' + user_id).length) {
  466. var user_item = group_item.find('ul li.imjs-default').clone()
  467. .removeClass('imjs-default')
  468. .addClass('imjs-' + status[0])
  469. .attr('id', user_id)
  470. .data('friend', username)
  471. .appendTo(group_item.find('ul'));
  472. // if(status[0] == 'offline')
  473. // user_item.hide();
  474. user_item.html(
  475. user_item.html()
  476. .replace('{username}', username)
  477. .replace('{status}', status[1])
  478. );
  479. user_item.find('.imjs-friend-status')
  480. .attr('title', status[1]);
  481. }
  482. this.friends[username] = {status: status, group: group};
  483. this._updateFriendCount();
  484. return this.friends[username];
  485. },
  486. // === //private// {{{AjaxIM.}}}**{{{_updateFriendCount()}}}** ===
  487. //
  488. // Counts the number of online friends and updates the friends count
  489. // in the friend tab.
  490. _updateFriendCount: function() {
  491. var friendsLength = 0;
  492. $.each(this.friends, function(u, f) {
  493. if(f.status[0] != 'offline') friendsLength++;
  494. });
  495. $('#imjs-friends .imjs-tab-text span span').html(friendsLength);
  496. },
  497. // === //private// {{{AjaxIM.}}}**{{{_storeFriends()}}}** ===
  498. //
  499. // If a storage method is enabled, the current state of the
  500. // user's friends list is stored.
  501. _storeFriends: function() {
  502. store.set(this.username + '-friends', this.friends);
  503. },
  504. // === //private// {{{AjaxIM.}}}**{{{_createChatbox(username)}}}** ===
  505. //
  506. // Builds a chatbox based on the default chatbox HTML and CSS defined
  507. // in the current theme. Should a chatbox for this user already exist,
  508. // a new one is not created. Instead, it is either given focus (should
  509. // no other windows already have focus), or a notification is issued.
  510. //
  511. // As well, if the chatbox does not exist, an associated tab will be
  512. // created.
  513. //
  514. // ==== Parameters ====
  515. // * {{{username}}} is the name of the user for whom the chatbox is intended
  516. // for.
  517. // * {{{no_stamp}}} sets whther or not to add a date stamp to the chatbox
  518. // upon creation.
  519. //
  520. // //Note:// New chatboxes are given an automatically generated ID in the
  521. // format of {{{#imjs-[md5 of username]}}}.
  522. _createChatbox: function(username, no_stamp) {
  523. var self = this,
  524. chatbox_id = 'imjs-' + md5.hex(username);
  525. if(!(chatbox = $('#' + chatbox_id)).length) {
  526. // add a tab
  527. var tab = this.addTab(username, '#' + chatbox_id);
  528. var chatbox = tab.find('.imjs-chatbox');
  529. chatbox.attr('id', chatbox_id);
  530. // remove default items from the message log
  531. var message_log = chatbox.find('.imjs-msglog').empty();
  532. // setup the chatbox header
  533. var cb_header = chatbox.find('.imjs-header');
  534. cb_header.html(cb_header.html().replace('{username}', username));
  535. if(!no_stamp) {
  536. // add a date stamp
  537. this._store(username, this._addDateStamp(chatbox));
  538. }
  539. // associate the username with the object and vice-versa
  540. this.chats[username] = chatbox;
  541. chatbox.data('username', username);
  542. if(username in this.friends) {
  543. status = this.friends[username].status;
  544. tab.addClass('imjs-' + status);
  545. }
  546. setTimeout(function() { self._scrollers(); }, 0);
  547. } else if(chatbox.parents('.imjs-tab').data('state') == 'closed') {
  548. chatbox.find('.imjs-msglog > *').addClass('imjs-msg-old');
  549. var tab = chatbox.parents('.imjs-tab');
  550. if(tab.css('display') == 'none')
  551. tab.css('display', '').removeClass('imjs-selected')
  552. .insertAfter('#imjs-scroll-left')
  553. .data('state', 'minimized');
  554. if(!no_stamp) {
  555. // possibly add a date stamp
  556. this._store(username, this._addDateStamp(chatbox));
  557. }
  558. if(!$('#imjs-bar .imjs-selected').length) {
  559. tab.click();
  560. } else {
  561. this.notification(tab);
  562. }
  563. setTimeout(function() { self._scrollers() }, 0);
  564. }
  565. return chatbox;
  566. },
  567. // === //private// {{{AjaxIM.}}}**{{{_addDateStamp(chatbox)}}}** //
  568. //
  569. // Adds a date/time notifier to a chatbox. These are generally
  570. // inserted upon creation of a chatbox, or upon the date changing
  571. // since the last time a date stamp was added. If a date stamp for
  572. // the current date already exists, a new one will not be added.
  573. //
  574. // ==== Parameters ====
  575. // * {{{chatbox}}} refers to the jQuery-selected chatbox DOM element.
  576. // * {{{time}}} is the date/time the date stamp will show. It is specified
  577. // in milliseconds since the Unix Epoch. This is //only// defined when
  578. // date stamps are being restored from storage; if not specified, the
  579. // current computer time will be used.
  580. _addDateStamp: function(chatbox, time) {
  581. var message_log = $(chatbox).find('.imjs-msglog');
  582. if(!time)
  583. time = (new Date()).getTime();
  584. var date_stamp = $('.imjs-tab.imjs-default .imjs-chatbox .imjs-msglog .imjs-date').clone();
  585. var date_stamp_time = date_stamp.find('.imjs-msg-time');
  586. if(date_stamp_time.length)
  587. date_stamp_time.html(dateFormat(time, date_stamp_time.html()));
  588. var date_stamp_date = date_stamp.find('.imjs-date-date');
  589. var formatted_date = dateFormat(time, date_stamp_date.html());
  590. if(chatbox.data('lastDateStamp') != formatted_date) {
  591. if(date_stamp_date.length)
  592. date_stamp_date.html(dateFormat(time, date_stamp_date.html()));
  593. chatbox.data('lastDateStamp', formatted_date);
  594. date_stamp.appendTo(message_log);
  595. return {
  596. replace_last: false,
  597. html: jQuery('<div>').append(date_stamp.clone()).html()
  598. };
  599. } else {
  600. //$('<div></div>').appendTo(message_log);
  601. return {replace_last: false, html: ''};
  602. }
  603. },
  604. // === //private// {{{AjaxIM.}}}**{{{_addError(chatbox, error)}}}** //
  605. //
  606. // Adds an error to a chatbox. These are generally inserted after
  607. // a user sends a message unsuccessfully. If an error message
  608. // was already added, another one will be added anyway.
  609. //
  610. // ==== Parameters ====
  611. // * {{{chatbox}}} refers to the jQuery-selected chatbox DOM element.
  612. // * {{{error}}} is the error message string.
  613. // * {{{time}}} is the date/time the error occurred. It is specified in
  614. // milliseconds since the Unix Epoch. This is //only// defined when
  615. // errors are being restored from storage; if not specified, the current
  616. // computer time will be used.
  617. _addError: function(chatbox, error, time) {
  618. var message_log = $(chatbox).find('.imjs-msglog');
  619. var error_item =
  620. $('.imjs-tab.imjs-default .imjs-chatbox .imjs-msglog .imjs-error').clone();
  621. var error_item_time = error_item.find('.imjs-msg-time');
  622. if(error_item_time.length) {
  623. if(!time)
  624. time = (new Date()).getTime();
  625. error_item_time.html(dateFormat(time, error_item_time.html()));
  626. }
  627. error_item.find('.imjs-error-error').html(error);
  628. error_item.appendTo(message_log);
  629. message_log[0].scrollTop = message_log[0].scrollHeight;
  630. return {
  631. replace_last: false,
  632. html: jQuery('<div>').append(error_item.clone()).html()
  633. };
  634. },
  635. // === //private// {{{AjaxIM.}}}**{{{_addMessage(ab, chatbox, username, message, time)}}}** //
  636. //
  637. // Adds a message to a chatbox. Depending on the {{{ab}}} value,
  638. // the color of the username may change as a way of visually
  639. // identifying users (however, this depends on the theme's CSS).
  640. // A timestamp is added to the message, and the chatbox is scrolled
  641. // to the bottom, such that the new message is visible.
  642. //
  643. // Messages will be automatically tag-escaped, so as to prevent
  644. // any potential cross-site scripting problems. Additionally,
  645. // URLs will be automatically linked.
  646. //
  647. // ==== Parameters ====
  648. // * {{{ab}}} refers to whether the user is "a" or "b" in a conversation.
  649. // For the general case, "you" are "a" and "they" are "b".
  650. // * {{{chatbox}}} refers to the jQuery-selected chatbox DOM element.
  651. // * {{{username}}} is the username of the user who sent the message.
  652. // * {{{time}}} is the time the message was sent in milliseconds since
  653. // the Unix Epoch. This is //only// defined when messages are being
  654. // restored from storage. For new messages, the current computer
  655. // time is automatically used.
  656. _addMessage: function(ab, chatbox, username, message, time) {
  657. var last_message = chatbox.find('.imjs-msglog > *:last-child');
  658. if(last_message.hasClass('imjs-msg-' + ab)) {
  659. // Last message was from the same person, so let's just add another imjs-msg-*-msg
  660. var message_container = (last_message.hasClass('imjs-msg-' + ab + '-container')
  661. ? last_message
  662. : last_message.find('.imjs-msg-' + ab + '-container'));
  663. var single_message =
  664. $('.imjs-tab.imjs-default .imjs-chatbox .imjs-msglog .imjs-msg-' + ab + '-msg')
  665. .clone().appendTo(message_container);
  666. single_message.html(single_message.html().replace('{username}', username));
  667. } else if(!last_message.length || !last_message.hasClass('imjs-msg-' + ab)) {
  668. var message_group = $('.imjs-tab.imjs-default .imjs-chatbox .imjs-msg-' + ab)
  669. .clone().appendTo(chatbox.find('.imjs-msglog'));
  670. message_group.html(message_group.html().replace('{username}', username));
  671. var single_message = message_group.find('.imjs-msg-' + ab + '-msg');
  672. }
  673. // clean up the message
  674. message = message.toString().replace(/</g, '&lt;').replace(/>/g, '&gt;')
  675. .replace(/(^|.*)\*([^*]+)\*(.*|$)/, '$1<strong>$2</strong>$3');
  676. // autolink URLs
  677. message = message.replace(
  678. new RegExp('([A-Za-z][A-Za-z0-9+.-]{1,120}:[A-Za-z0-9/]' +
  679. '(([A-Za-z0-9$_.+!*,;/?:@&~=-])|%[A-Fa-f0-9]{2}){1,333}' +
  680. '(#([a-zA-Z0-9][a-zA-Z0-9$_.+!*,;/?:@&~=%-]{0,1000}))?)', 'g'),
  681. '<a href="$1" target="_blank">$1</a>');
  682. // insert the message
  683. single_message.html(single_message.html().replace('{message}', message));
  684. // set the message time
  685. var msgtime = single_message.find('.imjs-msg-time');
  686. if(!time)
  687. time = new Date();
  688. if(typeof time != 'string')
  689. time = dateFormat(time, msgtime.html());
  690. msgtime.html(time);
  691. var msglog = chatbox.find('.imjs-msglog');
  692. msglog[0].scrollTop = msglog[0].scrollHeight;
  693. return {
  694. replace_last : !!message_container,
  695. html: jQuery('<div>').append(
  696. message_container
  697. ? last_message.clone()
  698. : message_group.clone()
  699. ).html()
  700. };
  701. },
  702. _store: function(username, msg) {
  703. if(!msg.html.length) return;
  704. if(!this.chatstore) this.chatstore = {};
  705. if(!(username in this.chatstore)) {
  706. this.chatstore[username] = [];
  707. } else if(this.chatstore[username].length > 300) {
  708. // If the chat store gets too long, it becomes slow to load.
  709. this.chatstore[username].shift();
  710. }
  711. if(msg.replace_last)
  712. this.chatstore[username].pop();
  713. this.chatstore[username].push(msg.html);
  714. store.set(this.username + '-chats', this.chatstore);
  715. },
  716. // === //private// {{{AjaxIM.}}}**{{{_friendUpdate(friend, status, statusMessage)}}}** ===
  717. //
  718. // Called when a friend's status is updated. This function will update all locations
  719. // where a status icon is displayed (chat tab, friends list), as well as insert
  720. // a notification, should a chatbox be open.
  721. //
  722. // ==== Parameters ====
  723. // * {{{friend}}} is the username of the friend.
  724. // * {{{status}}} is the new status code. See {{{AjaxIM.statuses}}} for a list of available
  725. // codes. //Note: If an invalid status is specified, no action will be taken.//
  726. // * {{{statusMessage}}} is a message that was, optionally, specified by the user. It will be
  727. // used should "you" send the user an IM while they are away, or if their status is viewed
  728. // in another way (such as via the friends list [**not yet implemented**]).
  729. _friendUpdate: function(friend, status, statusMessage) {
  730. if(this.chats[friend]) {
  731. var tab = this.chats[friend].parents('.imjs-tab');
  732. var tab_class = 'imjs-tab';
  733. if(tab.data('state') == 'active') tab_class += ' imjs-selected';
  734. tab_class += ' imjs-' + status;
  735. tab.attr('class', tab_class);
  736. // display the status in the chatbox
  737. var date_stamp =
  738. $('.imjs-tab.imjs-default .imjs-chatbox .imjs-msglog .imjs-date').clone();
  739. var date_stamp_time = date_stamp.find('.imjs-msg-time');
  740. if(date_stamp_time.length)
  741. date_stamp_time.html(dateFormat(date_stamp_time.html()));
  742. var date_stamp_date = date_stamp.find('.imjs-date-date').html(
  743. AjaxIM.l10n[
  744. 'chat' + status.toUpperCase().slice(0, 1) + status.slice(1)
  745. ].replace(/%s/g, friend));
  746. var msglog = this.chats[friend].find('.imjs-msglog');
  747. date_stamp.appendTo(msglog);
  748. msglog[0].scrollTop = msglog[0].scrollHeight;
  749. }
  750. if (!this.friends[friend]) {
  751. this.addFriend(friend, [status, statusMessage], 'Friends');
  752. }
  753. if(this.friends[friend]) {
  754. var friend_id = 'imjs-friend-' + md5.hex(friend + this.friends[friend].group);
  755. $('#' + friend_id).attr('class', 'imjs-friend imjs-' + status);
  756. $('#' + friend_id).find('.imjs-friend-status')
  757. .html(statusMessage)
  758. .attr('status', statusMessage);
  759. if(status == 'offline') {
  760. $('#' + friend_id + ':visible').slideUp();
  761. $('#' + friend_id + ':hidden').hide();
  762. } else if(!$('#' + friend_id + ':visible').length) {
  763. $('#' + friend_id).slideDown();
  764. }
  765. this.friends[friend].status = [status, statusMessage];
  766. this._updateFriendCount();
  767. }
  768. },
  769. // === //private// {{{AjaxIM.}}}**{{{_notConnected()}}}** ===
  770. //
  771. // Puts the user into a visible state of disconnection. Sets the
  772. // friends list to "not connected" and empties it; disallows new messages
  773. // to be sent.
  774. _notConnected: function() {
  775. $('#imjs-friends')
  776. .addClass('imjs-not-connected')
  777. .unbind('click', this.activateTab);
  778. if($('#imjs-friends').hasClass('imjs-selected'))
  779. this.activateTab($('#imjs-friends'));
  780. },
  781. _showReconnect: function() {
  782. $('#imjs-reconnect').show();
  783. },
  784. // === {{{AjaxIM.}}}**{{{send(to, message)}}}** ===
  785. //
  786. // Sends a message to another user. The message will be added
  787. // to the chatbox before it is actually sent, however, if an
  788. // error occurs during sending, that will be indicated immediately
  789. // afterward.
  790. //
  791. // After sending the message, one of three status codes should be
  792. // returned as a JSON object, e.g. {{{{r: 'code'}}}}:
  793. // * {{{ok}}} &mdash; Message was sent successfully.
  794. // * {{{offline}}} &mdash; The user is offline or unavailable to
  795. // receive messages.
  796. // * {{{error}}} &mdash; a problem occurred, unrelated to the user
  797. // being unavailable.
  798. //
  799. // ==== Parameters ====
  800. // * {{{to}}} is the username of the recipient.
  801. // * {{{message}}} is the content to be sent.
  802. send: function(username, body) {
  803. if(!body) return;
  804. var self = this;
  805. if(this.chats[username]) {
  806. // possibly add a datestamp
  807. this._store(username, this._addDateStamp(this.chats[username]));
  808. this._store(username,
  809. this._addMessage('a', this.chats[username],
  810. this.username, body));
  811. }
  812. $(this).trigger('sendingMessage', [username, body]);
  813. var event = {type: 'message', from: this.username, to: username, body: body};
  814. this.sendEvent(event, function(result) {
  815. if(result._status.sent) {
  816. $(self).trigger('sendMessageSuccessful', [username, body]);
  817. } else if(result.type == 'error') {
  818. if(result.error == 'not online')
  819. $(self).trigger('sendMessageFailed', ['offline', username, body]);
  820. else
  821. $(self).trigger('sendMessageFailed', [result.error, username, body]);
  822. }
  823. }, function(error) {
  824. self._notConnected();
  825. var error = self._addError(
  826. self.chats[username],
  827. 'You are currently not connected or the ' +
  828. 'server is not available. Please ensure ' +
  829. 'that you are signed in and try again.');
  830. self._store(error);
  831. $(self).trigger('sendMessageFailed',
  832. ['not connected', username, body]);
  833. });
  834. },
  835. // === {{{AjaxIM.}}}**{{{status(s, message)}}}** ===
  836. //
  837. // Sets the user's status and status message. It is possible to not
  838. // set a status message by setting it to an empty string. The status
  839. // will be sent to the server, where upon the server will broadcast
  840. // the update to all individuals with "you" on their friends list.
  841. //
  842. // ==== Parameters ====
  843. // * {{{s}}} is the status code, as defined by {{{AjaxIM.statuses}}}.
  844. // * {{{message}}} is the custom status message.
  845. status: function(value, message) {
  846. var self = this;
  847. // update status icon(s)
  848. if(!~this.statuses.indexOf(value))
  849. return;
  850. // check if selected before writing over the class!
  851. $(this.statuses).each(function() {
  852. $('#imjs-friends').removeClass('imjs-' + this);
  853. });
  854. $('#imjs-friends').addClass('imjs-' + value);
  855. $(this).trigger('changingStatus', [value, message]);
  856. if(value == 'offline') {
  857. self._notConnected();
  858. self._showReconnect();
  859. store.set(this.username + '-offline', true);
  860. self.offline = true;
  861. $('.imjs-input').attr('disabled', true);
  862. AjaxIM.post(
  863. this.actions.signoff,
  864. {},
  865. function(result) {
  866. if(result.type == 'success')
  867. $(self).trigger('changeStatusSuccessful',
  868. [value, null]);
  869. },
  870. function(error) {
  871. $(self).trigger('changeStatusFailed',
  872. ['not connected', value, null]);
  873. }
  874. );
  875. } else {
  876. var event = {type: 'status', status: value, message: message};
  877. this.sendEvent(event, function(result) {
  878. if(result._status.send) {
  879. $(self).trigger('sendMessageSuccessful', [username, body]);
  880. } else if(result.type == 'error') {
  881. if(result.error == 'not online')
  882. $(self).trigger('sendMessageFailed', ['offline', username, body]);
  883. else
  884. $(self).trigger('sendMessageFailed', [result.error, username, body]);
  885. }
  886. }, function(error) {
  887. self._notConnected();
  888. var error = self._addError(
  889. self.chats[username],
  890. 'You are currently not connected or the ' +
  891. 'server is not available. Please ensure ' +
  892. 'that you are signed in and try again.');
  893. self._store(error);
  894. $(self).trigger('sendMessageFailed',
  895. ['not connected', username, body]);
  896. });
  897. }
  898. },
  899. // === {{{AjaxIM.}}}**{{{statuses}}}** ===
  900. //
  901. // These are the available status codes and their associated identities:
  902. // * {{{offline}}} (0) &mdash; Only used when signing out/when another
  903. // user has signed out, as once this status is set, the user is removed
  904. // from the server and friends will be unable to contact the user.
  905. // * {{{available}}} (1) &mdash; The user is online and ready to be messaged.
  906. // * {{{away}}} (2) &mdash; The user is online but is not available. Others
  907. // may still contact this user, however, the user may not respond. Anyone
  908. // contacting an away user will receive a notice stating that the user is away,
  909. // and (if one is set) their custom status message.
  910. // * {{{invisible}}} (3; **not yet implemented**) &mdash; The user is online,
  911. // but other users are made unaware, and the user will be represented
  912. // as being offline. It is still possible to contact this user, and for this
  913. // user to contact others; no status message or notice will be sent to others
  914. // messaging this user.
  915. statuses: ['offline', 'available', 'away'],
  916. // === {{{AjaxIM.}}}**{{{initTabs()}}}** ===
  917. //
  918. // Setup the footer bar and enable tab actions. This function
  919. // uses {{{jQuery.live}}} to set hooks on any bar tabs created
  920. // in the future.
  921. initTabBar: function() {
  922. var self = this;
  923. // Set up your standard tab actions
  924. $(document)
  925. .on('click', '.imjs-tab', function() {
  926. return self.activateTab.call(self, $(this));
  927. });
  928. $(document)
  929. .on('click', '.imjs-tab .imjs-close', function() {
  930. return self.closeTab.call(self, $(this));
  931. });
  932. // Set up the friends list actions
  933. $(document).click(function(e) {
  934. if(~['imjs-friends'].indexOf(e.target.id) ||
  935. $(e.target).parents('#imjs-friends').length) {
  936. return;
  937. }
  938. if($('#imjs-friends').data('state') == 'active')
  939. self.activateTab.call(self, $('#imjs-friends'));
  940. else if($('#imjs-status').data('state') == 'active')
  941. self.activateTab.call(self, $('#imjs-status'));
  942. });
  943. $('#imjs-friends')
  944. .data('state', 'minimized')
  945. .click(function(e) {
  946. if(!$(this).hasClass('imjs-not-connected') &&
  947. e.target.id != 'imjs-friends-panel' &&
  948. !$(e.target).parents('#imjs-friends-panel').length)
  949. self.activateTab.call(self, $(this));
  950. })
  951. .mouseenter(function() {
  952. if($(this).hasClass('imjs-not-connected')) {
  953. $('.imjs-tooltip')
  954. .css('display', 'block')
  955. .find('p')
  956. .html(AjaxIM.l10n.notConnectedTip);
  957. var tip_left = $(this).offset().left -
  958. $('.imjs-tooltip').outerWidth() +
  959. ($(this).outerWidth() / 2);
  960. var tip_top = $(this).offset().top -
  961. $('.imjs-tooltip').outerHeight(true);
  962. $('.imjs-tooltip').css({
  963. left: tip_left,
  964. top: tip_top
  965. });
  966. }
  967. })
  968. .mouseleave(function() {
  969. if($(this).hasClass('imjs-not-connected')) {
  970. $('.imjs-tooltip').css('display', '');
  971. }
  972. });
  973. $('#imjs-friends-panel').css('display', 'none');
  974. },
  975. // === {{{AjaxIM.}}}**{{{activateTab()}}}** ===
  976. //
  977. // Activate a tab by setting it to the 'active' state and
  978. // showing any related chatbox. If a chatbox is available
  979. // for this tab, also focus the input box.
  980. //
  981. // //Note:// {{{this}}}, here, refers to the tab DOM element.
  982. activateTab: function(tab) {
  983. var chatbox = tab.find('.imjs-chatbox') || false,
  984. input;
  985. if(tab.data('state') != 'active') {
  986. if(tab.attr('id') != 'imjs-friends') {
  987. $('#imjs-bar > li')
  988. .not(tab)
  989. .not('#imjs-friends, .imjs-scroll, .imjs-default')
  990. .add(tab.attr('id') == 'imjs-status' ? '#imjs-friends' : '')
  991. .removeClass('imjs-selected')
  992. .each(function() {
  993. var self = $(this);
  994. if(self.data('state') != 'closed') {
  995. self.data('state', 'minimized');
  996. var chatbox = self.find('.imjs-chatbox');
  997. if(chatbox.length)
  998. chatbox.css('display', 'none');
  999. }
  1000. });
  1001. } else {
  1002. $('#imjs-status')
  1003. .removeClass('imjs-selected')
  1004. .data('state', 'minimized')
  1005. .find('.imjs-chatbox')
  1006. .css('display', 'none');
  1007. }
  1008. if(chatbox && chatbox.css('display') == 'none')
  1009. chatbox.css('display', '');
  1010. // set the tab to active...
  1011. tab.addClass('imjs-selected').data('state', 'active');
  1012. // ...and hide and reset the notification icon
  1013. tab.find('.imjs-notification').css('display', 'none')
  1014. .data('count', 0);
  1015. if(chatbox && (username = chatbox.data('username')))
  1016. store.set(this.username + '-activeTab', username);
  1017. $(this).trigger('tabToggled', ['activated', tab]);
  1018. } else {
  1019. tab.removeClass('imjs-selected').data('state', 'minimized');
  1020. if(chatbox && chatbox.css('display') != 'none')
  1021. chatbox.css('display', 'none');
  1022. store.set(this.username + '-activeTab', '');
  1023. $(this).trigger('tabToggled', ['minimized', tab]);
  1024. }
  1025. if(chatbox) {
  1026. if((input = chatbox.find('.imjs-input')).length &&
  1027. !input.data('height')) {
  1028. input.height(0);
  1029. if(input[0].scrollHeight > input.height() ||
  1030. input[0].scrollHeight < input.height()) {
  1031. input.height(input[0].scrollHeight);
  1032. }
  1033. // store the height for resizing later
  1034. if (!input.height()) {
  1035. input.height(16);
  1036. }
  1037. input.data('height', input.height());
  1038. }
  1039. try {
  1040. var msglog = chatbox.find('.imjs-msglog');
  1041. msglog[0].scrollTop = msglog[0].scrollHeight;
  1042. } catch(e) {}
  1043. try { chatbox.find('.imjs-input').focus(); } catch(e) {}
  1044. }
  1045. },
  1046. // === {{{AjaxIM.}}}**{{{closeTab()}}}** ===
  1047. //
  1048. // Close a tab and hide any related chatbox, such that
  1049. // the chatbox can not be reopened without reinitializing
  1050. // the tab.
  1051. //
  1052. // //Note:// {{{this}}}, here, refers to the tab DOM element.
  1053. closeTab: function(tab) {
  1054. tab = tab.parents('.imjs-tab');
  1055. tab.css('display', 'none')
  1056. .removeClass('imjs-selected')
  1057. .data('state', 'closed');
  1058. delete this.chatstore[tab.find('.imjs-chatbox').data('username')];
  1059. store.set(this.username + '-chats', this.chatstore);
  1060. $(this).trigger('tabToggled', ['closed', tab]);
  1061. this._scrollers();
  1062. return false;
  1063. },
  1064. // === {{{AjaxIM.}}}**{{{addTab(label, action, closable)}}}** ===
  1065. //
  1066. // Adds a tab to the tab bar, with the label {{{label}}}. When
  1067. // clicked, it will call a callback function, {{{action}}}. If
  1068. // {{{action}}} is a string, it is assumed that the string is
  1069. // referring to a chatbox ID.
  1070. //
  1071. // ==== Parameters ====
  1072. // * {{{label}}} is the text that will be displayed on the tab.\\
  1073. // * {{{action}}} is the callback function, if it is a non-chatbox
  1074. // tab, or a string if it //is// a chatbox tab.\\
  1075. // * {{{closable}}} is a boolean value that determines whether or not
  1076. // it is possible for a user to close this tab.
  1077. //
  1078. // //Note:// New tabs are given an automatically generated ID
  1079. // in the format of {{{#imjs-tab-[md5 of label]}}}.
  1080. addTab: function(label, action, closable) {
  1081. var tab = $('.imjs-tab.imjs-default').clone().insertAfter('#imjs-scroll-left');
  1082. tab.removeClass('imjs-default')
  1083. .attr('id', 'imjs-tab-' + md5.hex(label))
  1084. .html(tab.html().replace('{label}', label))
  1085. .data('state', 'minimized');
  1086. var notification = tab.find('.imjs-notification');
  1087. notification.css('display', 'none')
  1088. .data('count', 0)
  1089. .data('default-text', notification.html())
  1090. .html(notification.html().replace('{count}', '0'));
  1091. if(closable === false)
  1092. tab.find('.imjs-close').eq(0).remove();
  1093. if(typeof action != 'string') {
  1094. tab.find('.imjs-chatbox').remove();
  1095. tab.click(action);
  1096. }
  1097. return tab;
  1098. },
  1099. // === {{{AjaxIM.}}}**{{{notification(tab)}}}** ===
  1100. //
  1101. // Displays a notification on a tab. Generally, this is called when
  1102. // a tab is minimized to let the user know that there is an update
  1103. // for them. The way the notification is displayed depends on the
  1104. // theme CSS.
  1105. //
  1106. // ==== Parameters ====
  1107. // * {{{tab}}} is the jQuery-selected tab DOM element.
  1108. notification: function(tab) {
  1109. var notify = tab.find('.imjs-notification');
  1110. var notify_count = notify.data('count') + 1;
  1111. notify.data('count', notify_count)
  1112. .html(notify.data('default-text').replace('{count}', notify_count))
  1113. .css('display', '');
  1114. },
  1115. // === //private// {{{AjaxIM.}}}**{{{_scrollers()}}}** ===
  1116. //
  1117. // Document me!
  1118. _scrollers: function() {
  1119. var needScrollers = false;
  1120. $('#imjs-scroll-left').nextAll('.imjs-tab')
  1121. .filter(function() {
  1122. return $(this).data('state') != 'closed';
  1123. })
  1124. .each(function(i, tab) {
  1125. tab = $(tab).css('display', '');
  1126. var tab_pos = tab.position();
  1127. if(tab_pos.top >= $('#imjs-bar').height() ||
  1128. tab_pos.left < 0 ||
  1129. tab_pos.right > $(document).width()) {
  1130. $('.imjs-scroll').css('display', '');
  1131. tab.css('display', 'none');
  1132. needScrollers = true;
  1133. }
  1134. });
  1135. if(!needScrollers) {
  1136. $('.imjs-scroll').css('display', 'none');
  1137. }
  1138. if($('#imjs-scroll-left').css('display') != 'none' &&
  1139. $('#imjs-scroll-right').position().top >= $('#imjs-bar').height()) {
  1140. $('#imjs-bar li.imjs-tab:visible').slice(-1).css('display', 'none');
  1141. }
  1142. if($('#imjs-bar li.imjs-tab:visible').length) {
  1143. while($('.imjs-selected').css('display') == 'none')
  1144. $('#imjs-scroll-right').click();
  1145. }
  1146. this._scrollerIndex();
  1147. },
  1148. _scrollerIndex: function() {
  1149. var hiddenRight = $('#imjs-bar li.imjs-tab:visible').slice(-1)
  1150. .nextAll('#imjs-bar li.imjs-tab:hidden')
  1151. .not('.imjs-default')
  1152. .filter(function() {
  1153. return $(this).data('state') != 'closed'
  1154. }).length;
  1155. var hiddenLeft = $('#imjs-bar li.imjs-tab:visible').eq(0)
  1156. .prevAll('#imjs-bar li.imjs-tab:hidden')
  1157. .not('.imjs-default')
  1158. .filter(function() {
  1159. return $(this).data('state') != 'closed'
  1160. }).length;
  1161. $('#imjs-scroll-left').html(hiddenLeft);
  1162. $('#imjs-scroll-right').html(hiddenRight);
  1163. },
  1164. unconfirmedEvents: {},
  1165. eventId: 1,
  1166. createEvent: function() {
  1167. var event = {};
  1168. event.id = this.eventId++;;
  1169. this.unconfirmedEvents[event.id] = evt;
  1170. },
  1171. sendEvent: function(event, successFunc, failureFunc) {
  1172. event.id = this.eventId++;
  1173. var evt = $.extend({}, event);
  1174. evt['_status'] = {
  1175. successFunc: successFunc,
  1176. failureFunc: failureFunc
  1177. };
  1178. this.unconfirmedEvents[event.id] = evt;
  1179. if (this.socket) {
  1180. this.socket.emit('server', event);
  1181. } else {
  1182. var self = this;
  1183. var url = null;
  1184. switch (event.type) {
  1185. case 'message':
  1186. url = this.actions.send;
  1187. break;
  1188. case 'status':
  1189. url = this.actions.status;
  1190. break;
  1191. case 'signoff':
  1192. url = this.actions.signoff;
  1193. break;
  1194. default:
  1195. break;
  1196. }
  1197. AjaxIM.post(url, event,
  1198. function(result) {
  1199. if (result) {
  1200. for (var e=0; e < result.length; ++e) {
  1201. self.dispatchEvent(events[e]);
  1202. }
  1203. }
  1204. },
  1205. function(error) {
  1206. if (self.unconfirmedEvents[event.id]) {
  1207. event = self.unconfirmedEvents[event.id];
  1208. event['_status']['sent'] = false;
  1209. self.dispatchEvent(event);
  1210. }
  1211. }
  1212. );
  1213. }
  1214. },
  1215. dispatchEvent: function(event) {
  1216. if (event.id && this.unconfirmedEvents[event.id]) {
  1217. event['_status'] = $.extend({}, this.unconfirmedEvents[event.id]['_status'], event['_status']);
  1218. delete this.unconfirmedEvents[event.id];
  1219. if (event['_status']['sent']) {
  1220. event['_status']['successFunc'](event);
  1221. } else {
  1222. event['_status']['failureFunc'](event);
  1223. }
  1224. } else {
  1225. this.triggerEvent(event);
  1226. }
  1227. },
  1228. // poor man's Backbone.js Events
  1229. eventHandlers: {},
  1230. /**
  1231. * Add a callback to listen for an event type.
  1232. */
  1233. onEvent: function(eventType, callback) {
  1234. if (!this.eventHandlers[eventType]) {
  1235. this.eventHandlers[eventType] = [];
  1236. }
  1237. this.eventHandlers[eventType].push(callback);
  1238. },
  1239. /**
  1240. * Trigger an event on all interested callbacks.
  1241. */
  1242. triggerEvent: function(event) {
  1243. if (this.eventHandlers[event.type]) {
  1244. for (var e=0; e < this.eventHandlers[event.type].length; ++e) {
  1245. this.eventHandlers[event.type][e].call(this, event);
  1246. }
  1247. }
  1248. }
  1249. })
  1250. // == Static functions and variables ==
  1251. //
  1252. // The following functions and variables are available outside of an initialized
  1253. // {{{AjaxIM}}} object.
  1254. // === {{{AjaxIM.}}}**{{{client}}}** ===
  1255. //
  1256. // Once {{{AjaxIM.init()}}} is called, this will be set to the active AjaxIM
  1257. // object. Only one AjaxIM object instance can exist at a time. This variable
  1258. // can and should be accessed directly.
  1259. AjaxIM.client = null;
  1260. // === {{{AjaxIM.}}}**{{{init(options, actions)}}}** ===
  1261. //
  1262. // Initialize the AjaxIM client object and engine. Here, you can define your
  1263. // options and actions as outlined at the top of this documentation.
  1264. //
  1265. // ==== Parameters ====
  1266. // * {{{options}}} is the hash of custom settings to initialize Ajax IM with.
  1267. // * {{{actions}}} is the hash of any custom action URLs.
  1268. AjaxIM.init = function(options, actions) {
  1269. if(!AjaxIM.client)
  1270. AjaxIM.client = new AjaxIM(options, actions);
  1271. return AjaxIM.client;
  1272. };
  1273. // === {{{AjaxIM.}}}**{{{request(url, data, successFunc, failureFunc)}}}** ===
  1274. //
  1275. // Wrapper around {{{$.jsonp}}}, the JSON-P library for jQuery, and {{{$.ajax}}},
  1276. // jQuery's ajax library. Allows either function to be called, automatically,
  1277. // depending on the request's URL array (see {{{AjaxIM.actions}}}).
  1278. //
  1279. // ==== Parameters ====
  1280. // {{{url}}} is the URL of the request.
  1281. // {{{data}}} are any arguments that go along with the request.
  1282. // {{{success}}} is a callback function called when a request has completed
  1283. // without issue.
  1284. // {{{_ignore_}}} is simply to provide compatability with {{{$.post}}}.
  1285. // {{{failure}}} is a callback function called when a request hasn't not
  1286. // completed successfully.
  1287. AjaxIM.post = function(url, data, successFunc, failureFunc, urlnoop) {
  1288. AjaxIM.request(url, 'POST', data, successFunc, failureFunc, urlnoop);
  1289. };
  1290. AjaxIM.get = function(url, data, successFunc, failureFunc, urlnoop) {
  1291. AjaxIM.request(url, 'GET', data, successFunc, failureFunc, urlnoop);
  1292. };
  1293. AjaxIM.request = function(url, type, data, successFunc, failureFunc, noopurl) {
  1294. var errorTypes = ['timeout', 'error', 'notmodified', 'parseerror'];
  1295. if(typeof failureFunc != 'function')
  1296. failureFunc = function(){};
  1297. var jsonp = (url.substring(0, 1) !== '/');
  1298. var success = false;
  1299. data['sessionid'] = cookies.get('sessionid');
  1300. $.ajax({
  1301. url: url,
  1302. data: data,
  1303. dataType: jsonp? 'jsonp': 'json',
  1304. type: type,
  1305. cache: false,
  1306. timeout: 299000
  1307. }).done(function(data) {
  1308. success = true;
  1309. _dbg(JSON.stringify(data));
  1310. successFunc(data);
  1311. }).fail(function(jqXHR, textStatus) {
  1312. _dbg(textStatus);
  1313. failureFunc(textStatus);
  1314. });
  1315. if (jsonp) {
  1316. setTimeout(function() {
  1317. var failfn = function() {
  1318. if (!success) {
  1319. var textStatus = 'error';
  1320. _dbg(textStatus);
  1321. failureFunc(textStatus);
  1322. }
  1323. };
  1324. if (noopurl) {
  1325. var noopfn = function() {
  1326. var noopdone = false;
  1327. var event = {type: 'noop'};
  1328. $.ajax({
  1329. url: noopurl,
  1330. data: event,
  1331. dataType: 'jsonp',
  1332. type: type,
  1333. cache: false,
  1334. timeout: 299000
  1335. }).done(function(data) {
  1336. noopdone = true;
  1337. if (!success) {
  1338. setTimeout(noopfn, 3000);
  1339. }
  1340. }).fail(function(jqXHR, textStatus) {
  1341. // since JSONP, never called
  1342. });
  1343. setTimeout(function() {
  1344. if (!noopdone) {
  1345. failfn();
  1346. }
  1347. }, 3000);
  1348. };
  1349. noopfn();
  1350. } else {
  1351. failfn();
  1352. }
  1353. }, 3000);
  1354. }
  1355. // This prevents Firefox from spinning indefinitely
  1356. // while it waits for a response.
  1357. /*
  1358. if(url == 'jsonp' && $.browser.mozilla) {
  1359. $.jsonp({
  1360. 'url': 'about:',
  1361. timeout: 0
  1362. });
  1363. }
  1364. */
  1365. };
  1366. // === {{{AjaxIM.}}}**{{{incoming(data)}}}** ===
  1367. //
  1368. // Never call this directly. It is used as a connecting function between
  1369. // client and server for Comet.
  1370. //
  1371. // //Note:// There are two {{{AjaxIM.incoming()}}} functions. This one is a
  1372. // static function called outside of the initialized AjaxIM object; the other
  1373. // is only called within the initalized AjaxIM object.
  1374. AjaxIM.incoming = function(data) {
  1375. if(!AjaxIM.client)
  1376. return false;
  1377. if(data.length)
  1378. AjaxIM.client._parseMessages(data);
  1379. };
  1380. AjaxIM.eventID = 1;
  1381. // === {{{AjaxIM.}}}**{{{l10n}}}** ===
  1382. //
  1383. // Text strings used by Ajax IM. Should you want to translate Ajax IM into
  1384. // another language, merely change these strings.
  1385. //
  1386. // {{{%s}}} denotes text that will be automatically replaced when the string is
  1387. // used.
  1388. AjaxIM._ = function(str) {
  1389. if(str in AjaxIM.l10n) return AjaxIM.l10n[str];
  1390. return str;
  1391. };
  1392. AjaxIM.l10n = {
  1393. dayNames: [
  1394. "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat",
  1395. "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
  1396. ],
  1397. monthNames: [
  1398. "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
  1399. "January", "February", "March", "April", "May", "June", "July", "August", "September",
  1400. "October", "November", "December"
  1401. ],
  1402. chatOffline: '%s signed off.',
  1403. chatAvailable: '%s became available.',
  1404. chatAway: '%s went away.',
  1405. notConnected: 'You are currently not connected or the server is not available. ' +
  1406. 'Please ensure that you are signed in and try again.',
  1407. notConnectedTip: 'You are currently not connected.',
  1408. defaultAway: 'I\'m away.'
  1409. };
  1410. AjaxIM.debug = true;
  1411. function _dbg(msg) {
  1412. if(AjaxIM.debug && window.console) console.log(msg);
  1413. }
  1414. function uid(n){
  1415. var chars='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', nn='';
  1416. for(var c=0; c < n; c++){
  1417. nn += chars.substr(0|Math.random() * chars.length, 1);
  1418. }
  1419. return nn;
  1420. }