// Copyright 2006 37signals <info@37signals.com>
// This file is part of Campfire <http://campfirenow.com/terms.html> and 
// may not be used outside of Campfire without express written permission.  

var Campfire = {};
Campfire.UnreadMessageCounter = /^\((\d+)\) /;
Campfire.Responders = [
  'TimestampManager',
  'Transcript',
  'Poller',
  'Speaker',
  'WindowManager',
  'SoundManager',
  'Addresser',
  'Uploader'
];

/*--------------------------------------------------------------------------*/

Campfire.Chat = Class.create();
Campfire.Chat.prototype = {
  initialize: function(options) {
    Object.extend(this, options);
    if (/MSIE/.test(navigator.userAgent)) this.IE  = true;
    this.register.apply(this, Campfire.Responders);
  },
  
  register: function() {
    this.events = {};
    this.listeners = $A(arguments).map(function(klass) {
      return this[klass.toLowerCase()] = new Campfire[klass](this);
    }.bind(this));
  },
  
  cacheEventsFor: function(event) {
    var callback = ('on-' + event).camelize();
    this.events[event] = this.listeners.inject([], 
      function(callbacks, listener) {
        if (listener[callback]) {
          var __method = listener[callback].bind(listener);
          __method.listener = listener;
          callbacks.push(__method);
        }
        return callbacks;
      });
  },
  
  dispatch: function() {
    var args = $A(arguments), event = args.shift();
    
    if (!this.events[event])
      this.cacheEventsFor(event);

    try {
      this.events[event].each(function(callback) {
        callback.apply(callback.listener, args);
      });
    } catch (e) {
      /* alert('Error in event ' + event + ': ' + e); */
    }
  },
  
  redirectTo: function(url) {
    this.poller.stop();
    window.location.href = url;
  }
}

/*--------------------------------------------------------------------------*/

Campfire.WindowManager = Class.create();
Campfire.WindowManager.prototype = {
  initialize: function(chat) {
    this.chat = chat;
    this.reset();
    
    if (this.chat.IE) {
      Event.observe(window, 'load', this.reset.bind(this));
    } else {
      this.startForcefullyAutoscrolling();
    }
    
    Event.observe(window, 'load', this.registerCallbacks.bind(this));
  },
  
  registerCallbacks: function() {
    Event.observe(window, 'scroll', this.onScroll.bind(this));
    Event.observe(window, 'resize', this.onResize.bind(this));
    Event.observe(window, 'blur',   this.onBlur.bind(this));
    Event.observe(window, 'focus',  this.onFocus.bind(this));
  },
  
  startForcefullyAutoscrolling: function() {
    if (this.chat.scrollToBottom)
      this.interval = window.setInterval(this.scrollToBottom.bind(this), 50);
  },
  
  stopForcefullyAutoscrolling: function() {
    if (this.interval) {
      window.clearInterval(this.interval);
      delete this.interval;
    }
  },
    
  reset: function() {
    this.layout();
    if (this.chat.scrollToBottom)
      this.scrollToBottom();
    if (this.chat.speaker)
      window.setTimeout(this.chat.speaker.focus.bind(this.chat.speaker), 10);
  },
  
  getPageHeight: function() {
    return Math.max(document.documentElement.offsetHeight, 
      document.body.scrollHeight);
  },
  
  getWindowHeight: function() {
    return window.innerHeight || document.body.clientHeight;
  },
  
  getScrollOffset: function() {
    return Math.max(document.documentElement.scrollTop,
      document.body.scrollTop);
  },
  
  isScrolledToBottom: function() {
    return this.getScrollOffset() + this.getWindowHeight() >= 
      this.getPageHeight();
  },
  
  getChatViewportWidth: function() {
    var element = this.chat.transcript.element.parentNode.parentNode;
    return Element.getDimensions(element).width;
  },
  
  getChatAuthorColumnWidth: function() {
    var element = this.chat.transcript.element.getElementsByTagName('td')[1];
    if (!element) return 0;
    return Position.cumulativeOffset(element)[0] -
      Position.cumulativeOffset(this.chat.transcript.element)[0];
  },
  
  adjustChatMessageColumnWidth: function() {
    if (this.chat.IE) return false;
    var viewportWidth = this.getChatViewportWidth();
    var authorColumnWidth = this.getChatAuthorColumnWidth();
    var messageColumnWidth = viewportWidth - authorColumnWidth - 10;
    
    var stylesheet = $A(document.styleSheets).last();
    var rules = stylesheet.cssRules || stylesheet.rules;
    var style = rules[rules.length - 1].style;
    if (style) style.width = messageColumnWidth + 'px';
  },
  
  adjustChatControls: function() {
    if (this.chat.IE || !this.chat.speaker) return false;
    var controlsWidth = Element.getDimensions(this.chat.speaker.controls).width;
    this.chat.speaker.input.style.width = 
      this.getChatViewportWidth() - controlsWidth - 10 + 'px';
  },
  
  layout: function() {
    this.adjustChatMessageColumnWidth();
    this.adjustChatControls();
  },
  
  scrollToBottom: function() {
    if (this.scrollingToBottom) return;
    this.scrollingToBottom = true;
    
    if (this.chat.IE) {
      $('last_message').scrollIntoView(true);
    } else {
      window.scrollTo(0, this.getPageHeight() + this.getWindowHeight() + 100);
    }
    
    this.scrollingToBottom = false;
    this.scrolledToBottom = true;
  },

  onScroll: function(event) {
    if (this.scrollingToBottom) return;
    this.stopForcefullyAutoscrolling();
    this.scrolledToBottom = this.isScrolledToBottom();
  },
  
  onResize: function(event) {
    this.layout();
    if (!this.chat.IE && this.scrolledToBottom && !this.isScrolledToBottom())
      this.scrollToBottom();
  },
  
  onBlur: function(event) {
    this.blurred = true;
  },
  
  onFocus: function(event) {
    this.blurred = false;
    document.title = document.title.replace(Campfire.UnreadMessageCounter, '');
  },
  
  onMessagesInsertedBeforeDisplay: function() {
    this.stopForcefullyAutoscrolling();
    this.scrolledToBottom = this.isScrolledToBottom();
  },
  
  onMessagesInserted: function(messages) {
    if (this.blurred) {
      var match, count = 0;
      for (var i = 0; i < messages.length; i++)
        if (messages[i].actsLikeTextMessage()) count++;
        
      if (count) {
        if (match = document.title.match(Campfire.UnreadMessageCounter))
          document.title = document.title.replace(Campfire.UnreadMessageCounter,
            '(' + (parseInt(match[1]) + count).toString() + ') ');
        else
          document.title = '(' + count + ') ' + document.title;
      }
    }
    
    this.stopForcefullyAutoscrolling();
    this.adjustChatMessageColumnWidth();
    if (this.scrolledToBottom)
      this.scrollToBottom();
  },
  
  onMessageSpoken: function() {
    this.scrollToBottom();
    this.chat.speaker.focus();
  },
  
  onMessageBodyUpdated: function() {
    this.scrollToBottom();
  }
}

/*--------------------------------------------------------------------------*/

Campfire.Transcript = Class.create();
Campfire.Transcript.prototype = {
  initialize: function(chat) {
    this.chat     = chat;
    this.element  = $(chat.transcriptElement);
    this.template = new Template(chat.messageTemplate || '');
    this.findMessages();
  },
  
  findMessages: function() {
    var elements = document.getElementsByClassName('message', this.element);
    this.messages = elements.map(function(element) {
      var message = new Campfire.Message(element);
      if (message.kind == 'timestamp') this.lastTimestampMessage = message;
      return message;
    }.bind(this));
    this.updateTranscriptLink();
  },
  
  bodyForPendingMessage: function(message) {
    return Autolink.all(message.escapeHTML(), {target: '_blank'}, 
      function(text) { return text.truncate(50, '&hellip;') });
  },
  
  insertPendingMessage: function(message) {
    var html = this.template.evaluate({id: 'pending', 
      body: this.bodyForPendingMessage(message)});
    var element = this.insertMessages(html, 'pending').first();
    element.element.id = '';
    return element;
  },
  
  insertMessages: function() {
    var ids = $A(arguments), html = ids.shift();
    new Insertion.Bottom(this.element, html);
    var messages = ids.map(this.getMessageById.bind(this));
    this.chat.dispatch('messagesInsertedBeforeDisplay', messages);
    Element.show.apply(null, messages.pluck('element'));
    this.chat.dispatch('messagesInserted', messages);
    return messages;
  },
  
  queueMessage: function(message, id) {
    this.messageQueue = this.messageQueue || [];
    this.messageQueue.push([message, id]);
  },
  
  trimHistoryBy: function(size) {
    var adjustment = this.messages.length - this.chat.messageHistory + size;
    if (adjustment > 0) {
      adjustment.times(function() {
        Element.remove(this.messages.shift().element);
      }.bind(this));
      for (var i = 0, message; i < this.messages.length; i++)
        if ((message = this.messages[i]).actsLikeTextMessage())
          return message.setAuthorVisibilityInRelationTo(null);
    }
  },
  
  updateTranscriptLink: function() {
    if (this.messages.length >= this.chat.messageHistory) {
      if (!$('todays_transcript')) return;
      Element.show('todays_transcript');
      var link = $('todays_transcript_link');
      link.href = link.href.replace(/\/\d+$/, 
        '/' + this.messages.first().id());
    }
  },
  
  onMessagesInsertedBeforeDisplay: function(messages) {
    for (var i = 0; i < messages.length; i++) {
      var message = messages[i];
      if (message.kind == 'timestamp') {
        if (this.lastTimestampMessage)
          message.setAuthorVisibilityInRelationTo(this.lastTimestampMessage);
        this.lastTimestampMessage = message;
      } else {
        message.setAuthorVisibilityInRelationTo(this.messages.last());
      }
      if (Element.hasClassName(message.element, 'user_' + this.chat.userID))
        Element.addClassName(message.element, 'you');
      this.messages.push(message);
    }

    this.updateTranscriptLink();
    this.trimHistoryBy(messages.length);
  },
  
  onMessageAccepted: function(message, id) {
    var element = message.element;
    element.id = 'message_' + id;
    Element.removeClassName(element, 'pending');
  },
  
  onMessageBodyUpdated: function(message, body) {
    message.updateBody(body);
  },
  
  onPollCompleted: function() {
    if (!this.messageQueue) return;
    var args = [''], message;
    for (var i = 0; i < this.messageQueue.length; i++) {
      message = this.messageQueue[i];
      args[0] += message[0];
      args.push(message[1]);
    }
    delete this.messageQueue;
    this.insertMessages.apply(this, args);
  },
  
  getRows: function() {
    return this.element.getElementsByTagName('tr');
  },
  
  getMessage: function(element) {
    return new Campfire.Message(element);
  },
  
  getMessageById: function(id) {
    return this.getMessage($('message_' + id));
  }
}

/*--------------------------------------------------------------------------*/

Campfire.TimestampManager = Class.create();
Campfire.TimestampManager.prototype = {
  initialize: function(chat) {
    this.chat       = chat;
    this.transcript = chat.transcript;
  },
  
  removePendingTimestamp: function() {
    if (this.pendingTimestamp) {
      this.chat.transcript.messages = 
        this.chat.transcript.messages.without(this.pendingTimestamp);
      Element.remove(this.pendingTimestamp.element);
      this.pendingTimestamp = null;
    }
  },
  
  hidePendingTimestamp: function() {
    if (this.pendingTimestamp)
      Element.addClassName(this.pendingTimestamp.element, 'hidden');
  },
  
  showPendingTimestamp: function() {
    if (this.pendingTimestamp)
      Element.removeClassName(this.pendingTimestamp.element, 'hidden');
  },
  
  onMessagesInserted: function(messages) {
    var firstMessage = messages[0];
    if (messages.length == 1 && firstMessage.kind == 'timestamp') {
      this.removePendingTimestamp();
      this.pendingTimestamp = firstMessage;
      this.hidePendingTimestamp();
    } else if (this.pendingTimestamp) {
      if (firstMessage.kind == 'timestamp') {
        this.removePendingTimestamp();
      } else {
        this.showPendingTimestamp();
        this.pendingTimestamp = null;
      }
    }
  }
}

/*--------------------------------------------------------------------------*/

Campfire.Message = Class.create();
Campfire.Message.prototype = {
  initialize: function(element) {
    this.element = element;
    
    var children    = element.getElementsByTagName('td');
    this.authorCell = children[0];
    this.bodyCell   = children[1];

    this.kind = (element.className.match(/\s*(\w+)_message\s*/) || [])[1];
  },
  
  id: function() {
    return parseInt(this.element.id.match(/\d+/)) || 0;
  },
  
  pending: function() {
    return Element.hasClassName(this.element, 'pending');
  },
  
  innerElement: function(name) {
    var node = this[name + 'Cell'];
    if (this.actsLikeTextMessage() || this.kind == 'timestamp')
      return node.childNodes[0];
    return node;    
  },
  
  authorElement: function() {
    return this.innerElement('author');
  },
  
  bodyElement: function() {
    return this.innerElement('body');
  },
  
  author: function() {
    return this.authorElement().innerHTML;
  },
  
  actsLikeTextMessage: function() {
    return this.kind == 'text' || this.kind == 'upload' || this.kind == 'paste';
  },

  hideDateForTimestamp: function(lastTimestamp) {
    return !(!lastTimestamp || this.author() != lastTimestamp.author());
  },
  
  hideAuthorForMessage: function(lastMessage) {
    if (!lastMessage) return;
    if (lastMessage.kind == 'timestamp')
      return this.hideDateForTimestamp(lastMessage);
    return !(this.author() != lastMessage.author() ||
      !this.actsLikeTextMessage() || !lastMessage.actsLikeTextMessage());
  },
  
  setKind: function(kind) {
    Element.removeClassName(this.element, this.kind + '_message');
    this.kind = kind;
    Element.addClassName(this.element, this.kind + '_message');
  },
  
  setAuthorVisibilityInRelationTo: function(message) {
    Element[this.hideAuthorForMessage(message) ? 'hide' : 'show'](this.authorElement());
  },
  
  updateBody: function(body) {
    Element.update(this.bodyElement(), body);
  },
  
  inspect: function() {
    return $H(this).merge({id: this.id(), pending: this.pending()}).inspect();
  }
}

/*--------------------------------------------------------------------------*/

Campfire.Poller = Class.create();
Campfire.Poller.prototype = {
  initialize: function(chat) {
    this.chat     = chat;
    this.url      = chat.pollURL;
    this.interval = chat.pollInterval || 3;
    this.lastCacheID = this.chat.lastCacheID;
    this.timestamp = chat.timestamp;
    this.start();
  },
  
  start: function() {
    this.stopped = this.request = false;
    this.registerTimer();
  },
  
  stop: function() {
    this.stopped = true;
  },
  
  registerTimer: function() {
    window.setTimeout(this.poll.bind(this), this.interval * 1000);
  },
  
  parametersForRequest: function() {
    return $H({
      l: this.lastCacheID,          // l: the last cache fragment id
      m: this.chat.membershipKey,   // m: the user's membership key
      t: $T(),                      // t: the timestamp of the current request, 
                                    //    used only to defeat caching
      s: this.timestamp             // s: the server timestamp of the last 
                                    //    refresh from this client
    });
  },
  
  poll: function() {
    if (this.stopped || this.request) return;
    this.request = new Ajax.Request(this.url, {
      parameters: this.parametersForRequest().toQueryString(),
      onComplete: this.onComplete.bind(this)
    });
    this.chat.dispatch('pollStarted');
  },
    
  onComplete: function() {
    this.chat.dispatch('pollCompleted');
  },
  
  onPollCompleted: function() {
    this.request = false;
    this.registerTimer();
  }
}

/*--------------------------------------------------------------------------*/

Campfire.Speaker = Class.create();
Campfire.Speaker.prototype = {
  initialize: function(chat) {
    this.chat     = chat;
    this.url      = chat.speakURL;
    this.input    = $(chat.speakElement);
    this.form     = Form.forElement(this.input);
    this.controls = $('chat_controls');
    this.filters  = Campfire.Speaker.Filters.toArray();
    this.registerCallbacks();
    this.focus();
  },
  
  registerCallbacks: function() {
    Event.observe(this.input, 'keypress', this.onKeyPress.bind(this));
    Event.observe(this.input, 'keyup', this.onKeyUp.bind(this));
    Event.observe(this.form, 'submit', this.onSubmit.bind(this));
  },
  
  focus: function() {
    window.setTimeout(Field.focus.bind(Field, this.input), 10);
  },
  
  speak: function(message, sendAsPaste) {
    if (!(message = this.filterMessage(message))) return;
    
    this.chat.dispatch('preparationForMessageSpoken', message);
    var parameters = {message: message, t: $T()}, element;
    
    if (sendAsPaste) {
      parameters.paste = 'true';
      element = this.chat.transcript.insertPendingMessage('');
      element.setKind('paste');
      element.updateBody('<span class="pasting">Pasting...</span>');
    } else {
      element = this.chat.transcript.insertPendingMessage(message);
    }
    
    new Ajax.Request(this.url, {
      parameters: $H(parameters).toQueryString(),
      onComplete: this.onComplete.bind(this, element)
    });
    
    this.chat.dispatch('messageSpoken', element);
  },

  send: function(forcePaste) {
    var value = $F(this.input);
    if (value.blank()) return;
    var pasting = forcePaste || value.match(/\r|\n/);
    this.speak(value, pasting);
    this.input.value = '';
  },
  
  onSubmit: function(event) {
    this.send();
    Event.stop(event);
    this.focus();
  },
  
  onKeyPress: function(event) {
    this.chat.dispatch('preparationForKeyPress', event);
    switch (event.keyCode) {
      case Event.KEY_RETURN:
        if (event.shiftKey) {
          return;
        } else if (event.ctrlKey || event.metaKey) {
          this.send(true);
        } else {
          this.send();
        }
        Event.stop(event);
    }
  },
  
  onKeyUp: function(event) {
    this.chat.dispatch('keyPressed', event);
  },
  
  onComplete: function(element, request, messageID) {
    this.chat.dispatch('messageAccepted', element, messageID);
    var body = request.responseText.strip();
    if (body) this.chat.dispatch('messageBodyUpdated', element, body);
  },
  
  filterMessage: function(message) {
    for (var i = 0; i < this.filters.length; i++)
      if (!(message = this.filters[i](message)))
        return false;
    return message;
  }
}

Campfire.Speaker.Filters = [
  function(message) {
    var match;
    if (match = message.match(/^\/me *(.*)/)) {
      if ((match = match[1].toString()).blank()) return false;
      return '*' + match + '*';
    } else {
      return message;
    }
  }
]

/*--------------------------------------------------------------------------*/

Campfire.Addresser = Class.create();
Campfire.Addresser.Pattern = /^\/\/([a-z]+)(?:\s+|$)/i;
Campfire.Addresser.prototype = {
  initialize: function(chat) {
    this.chat = chat;
    this.element = $('tooltip');
  },
  
  onMessagesInserted: function() {
    this._participants = null;
  },
  
  onPreparationForKeyPress: function(event) {
    if (this.addressee && event.keyCode == Event.KEY_RETURN)
      this.complete(Event.element(event));
  },
  
  onKeyPressed: function(event) {
    var input;
    if (input = this.input(Event.element(event)))
      if (this.addressee = this.match(input)) return;

    this.cancel();
  },
  
  input: function(element) {
    return ($F(element).match(Campfire.Addresser.Pattern) || [])[1];
  },
  
  match: function(string) {
    var pattern = new RegExp(string.split('').join('.*'), 'i'), match;
    if (match = this.participants().grep(pattern))
      if (match.length == 1)
        return this.show(match[0].replace(/ \(guest\)$/, ''));
  },
  
  complete: function(element) {
    element.value = element.value.replace(Campfire.Addresser.Pattern,
      this.addressee + ': ');
    this.cancel();
  },
  
  show: function(match) {
    if (!Element.visible(this.element))
      new Effect.Appear(this.element, {duration: 0.15});
    this.element.innerHTML = match.escapeHTML() + ':';
    return match;
  },

  cancel: function() {
    if (Element.visible(this.element))
      new Effect.Fade(this.element, {duration: 0.15});
    this.addressee = null;
  },
  
  participants: function() {
    var elements = $A($(this.chat.participantList).getElementsByTagName('li'));
    return this._participants = this._participants ||
      elements.pluck('innerHTML').invoke('unescapeHTML');
  }
}

/*--------------------------------------------------------------------------*/

Campfire.Uploader = Class.create();
Campfire.Uploader.prototype = {
  initialize: function(chat) {
    this.chat = chat;
  },
  
  chooseFile: function() {
    Element.show('upload_form_contents');
    Element.hide('upload_form_progress');
  },
  
  start: function() {
    if (this.value()) {
      this.showProgress();
      $('upload_form_tag').submit();
      this.chat.speaker.focus();
    }
  },
  
  reset: function() {
    Element.update('upload_form_tag', $('upload_form_tag').innerHTML);
  },
  
  showProgress: function() {
    Element.show('upload_form_progress');
    Element.hide('upload_form_contents');
    Element.update('upload_form_status', 
      'Uploading <strong>' + this.filename() + '</strong>&hellip;');
  },
  
  waitForMessage: function(id) {
    this.pending = id;
    if (this.messageExists(id))
      this.finish();
    else
      Element.update('upload_form_status', 'Finishing upload&hellip;');
  },
  
  messageExists: function() {
    return !!$('message_' + this.pending);
  },

  finish: function() {
    if (!this.pending) return;
    this.reset();
    this.chooseFile();
    $('uploader').showHide.hide();
    delete this.pending;
  },
  
  value: function() {
    return $('upload').value;
  },
  
  filename: function() {
    var value = this.value();
    return (value.match(/([^:\\\/]+)$/) || [null, value])[1];
  },
  
  onMessagesInserted: function(messages) {
    if (!this.messageExists(this.pending)) return;
    this.finish();
  },

  enableUploads: function(flag) {
    Element[flag ? 'show' : 'hide']('upload_file_link');
  }
}

/*--------------------------------------------------------------------------*/

Campfire.SoundManager = Class.create();
Campfire.SoundManager.prototype = {
  url: '/sounds/incoming.mp3',
  
  initialize: function(chat) {
    this.chat    = chat;
    this.enabled = chat.soundsEnabled;
    if (this.hasFlashInstalled()) {
      this.createFlashProxy();
      this.createFlashObject();
    } else {
      if ($('sounds')) Element.hide('sounds');
    }
  },
  
  hasFlashInstalled: function() {
    try {
      new ActiveXObject('ShockwaveFlash.ShockwaveFlash');
      return true;
    } catch (e) {
      var type = navigator.mimeTypes['application/x-shockwave-flash'];
      return !!(type && type.enabledPlugin);
    }
  },
  
  createFlashProxy: function() {
    this.uid = 'flash-' + $T();
    this.flashProxy = new FlashProxy(this.uid, 
      '/movies/javascript_flash_gateway.swf');
  },
  
  createFlashObject: function() {
    var tag = new FlashTag('/movies/sound_player.swf', 1, 1);
    tag.setFlashvars($H({lcId: this.uid, mp3Url: this.url}).toQueryString());
    tag.write(document);
  },
  
  call: function() {
    if (this.flashProxy)
      return this.flashProxy.call.apply(this.flashProxy, arguments);
  },
  
  playSound: function() {
    this.call('playSound', 100);
  },
  
  onMessagesInserted: function(messages) {
    if (!this.enabled || this.speaking) return;
    for (var i = 0; i < messages.length; i++)
      if (messages[i].actsLikeTextMessage()) 
        return this.playSound();
  },
  
  onPreparationForMessageSpoken: function() {
    this.speaking = true;
  },
  
  onMessageSpoken: function() {
    this.speaking = false;
  }
}

/*--------------------------------------------------------------------------*/

if (/Safari/.test(navigator.userAgent)) {
  // Safari doesn't fire window.onfocus/window.onblur events for tabs, but
  // it *does* pause Flash movies in background tabs.  Sneaky!
  
  Campfire.Responders.push('TabFocusDetector');

  Campfire.TabFocusDetector = Class.create();
  Campfire.TabFocusDetector.prototype = {
    initialize: function(chat) {
      this.chat = chat;
      Event.observe(window, 'load', this.start.bind(this));
    },
  
    start: function() {
      if (!this.interval && this.chat.soundmanager.flashProxy) {
        this.registerCallback();
        this.interval = window.setInterval(this.tick.bind(this), 100);
        this.ping();
      }
    },
  
    stop: function() {
      if (this.interval)
        window.clearInterval(this.interval);
    },
  
    ping: function() {
      this.time = $T();
      if (this.paused) {
        this.paused = false;
        this.chat.windowmanager.onFocus();
      }
    },
    
    tick: function() {
      var paused = $T() - this.time > 1250;
      if (paused && !this.paused) {
        this.paused = true;
        this.chat.windowmanager.onBlur();
      }
    },
    
    registerCallback: function() {
      this.callbackName = 'tabFocusDetectorCallback_' + $T();
      window[this.callbackName] = this.ping.bind(this);
      this.chat.soundmanager.call('startTimer', this.callbackName);
    }
  }
}

