Вход Регистрация
Файл: js/al/notifier.js
Строк: 4634
<?php
if (!window.curNotifier) {
  
curNotifier = {
    
addQueues: {},
    
recvClbks: {}
  };
}

Notifier = {
  
debug: false,//vk && vk.id == 45944258,
  
init: function (options) {
    
curNotifier = extend({
      
q_events: [],
      
q_shown: [],
      
q_closed: [],
      
q_max: 3,
      
q_idle_max: 5,
      
done_events: {},
      
addQueues: curNotifier.addQueues || {},
      
recvClbks: curNotifier.recvClbks || {},
      
error_timeout: 1,
      
sound: new Sound('mp3/bb1'),
      
sound_im: new Sound('mp3/bb2')
    }, 
options);

    if (!
this.initFrameTransport() && !this.initFlashTransport(options)) {
      return 
false;
    }
    
this.initIdleMan();

    if (!(
curNotifier.cont = ge('notifiers_wrap'))) {
      
bodyNode.insertBefore(curNotifier.cont = ce('div', {id: 'notifiers_wrap', className: 'fixed'}), ge('page_wrap'));
    }
  },
  
destroy: function () {
    
Notifier.hideAllEvents();
    
curNotifier.idle_manager.stop();
    
curNotifier = {};
    
re('notifiers_wrap');
    
re('queue_transport_wrap');
  },
  
reinit: function () {
    
ajax.post('notifier.php?act=a_get_params', {}, {
      
onDone: function (options) {
        if (
options) {
          
curNotifier.error_timeout = 1;
          
this.init(options);
        } else {
          
curNotifier.error_timeout = curNotifier.error_timeout || 1;
          
setTimeout(this.reinit.bind(this), curNotifier.error_timeout * 1000);
          if (
curNotifier.error_timeout < 256) {
            
curNotifier.error_timeout *= 2;
          }
        }
      }.
bind(this),
      
onFail: function () {
        
curNotifier.error_timeout = curNotifier.error_timeout || 1;
        
setTimeout(this.reinit.bind(this), curNotifier.error_timeout * 1000);
        if (
curNotifier.error_timeout < 256) {
          
curNotifier.error_timeout *= 2;
        }
        return 
true;
      }.
bind(this)
    });
  },
  
standby: function (nextTO) {
    
this.destroy();
    
curNotifier.error_timeout = nextTO || 1;
    
setTimeout(this.reinit.bind(this), curNotifier.error_timeout * 1000);
  },
  
freezeEvents: function () {
    
curNotifier.frozen = true;
    
each (curNotifier.q_shown, function () {
      
clearTimeout(this.fadeTO);
      if (
getStyle(this.baloonEl, 'opacity') < 1) {
        
animate(this.baloonEl, {opacity: 1}, 100);
      }
    });
  },
  
unfreezeEvents: function () {
    
curNotifier.frozen = false;
    
each (curNotifier.q_shown, function () {
      
this.fadeTO = setTimeout(this.startFading, 5000);
    });
  },
  
getTransportWrap: function () {
    return 
ge('queue_transport_wrap') || utilsNode.appendChild(ce('div', {id: 'queue_transport_wrap'}));
  },
  
setFocus: function (val) {
    var 
instance = (val ? '1' : '0') + curNotifier.instance_id;
    if (
curNotifier.transport == 'flash' && curNotifier.flash_transport) {
      
curNotifier.flash_transport.setInstanceFocused(instance);
    } else if (
curNotifier.transport == 'frame') {
      
Notifier.lcSend('focus', {instance_id: instance});
      
this.onInstanceFocus(instance);
    }
  },
  
initIdleMan: function () {
    if (
curNotifier.idle_manager && curNotifier.idle_manager.started) return;

    
curNotifier.idle_manager = (function (onIdleCb, onUnIdleCb) {
      var 
setIdleTo, checkIdleCb, checkIdleCbTo, sendCbTO, cb_active, cb_inactive, params = {
        
started: false,
        
is_idle: false,
        
onIdle: onIdleCb || null,
        
onUnIdle: onUnIdleCb || null,
        
stop: function () {
          
params.started = false;
          
removeEvent(document, 'mousemove keydown', cb_active);
          
removeEvent(window, 'focus', cb_active);
          
removeEvent(window, 'blur', cb_inactive);
          
clearTimeout(setIdleTo);
          
clearTimeout(checkIdleCbTo);
          
clearTimeout(sendCbTO);
        },
        
start: function () {
          
params.started = true;
          if (
browser.mobile) return;
          
checkIdleCb = function () {
            if (!(
window.curNotifier && curNotifier.idle_manager)) return;
            
addEvent(document, 'mousemove keydown', cb_active);
            
clearTimeout(setIdleTo);
            
setIdleTo = setTimeout(cb_inactive, 30000); // tab becomes idle in 30 secs without moving mouse or typing
          
};
          
cb_active = function (e) {
            if (!(
window.curNotifier && curNotifier.idle_manager)) return;
            
clearTimeout(setIdleTo);
            if (
params.is_idle) {
              
params.is_idle = false;
              
clearTimeout(sendCbTO);
              
sendCbTO = setTimeout(function () {
                if (
params.onUnIdle) {
                  
params.onUnIdle();
                }
                
//debugLog('unidle');
              
}, 100);
            }
            
removeEvent(document, 'mousemove keydown', cb_active);
            
clearTimeout(checkIdleCbTo);
            
checkIdleCbTo = setTimeout(checkIdleCb, 30000);
          };
          
cb_inactive = function (e) {
            if (!(
window.curNotifier && curNotifier.idle_manager)) return;
            if (!
params.is_idle) {
              
params.is_idle = true;
              
clearTimeout(sendCbTO);
              
sendCbTO = setTimeout(function () {
                if (
params.onIdle) {
                  
params.onIdle();
                }
                
//debugLog('idle');
              
}, 100);
            }
            
removeEvent(document, 'mousemove keydown', cb_active);
            
clearTimeout(checkIdleCbTo);
            
checkIdleCbTo = setTimeout(checkIdleCb, 30000);
          };
          
addEvent(window, 'focus', cb_active);
          
addEvent(window, 'blur', cb_inactive);
          
clearTimeout(checkIdleCbTo);
          
checkIdleCbTo = setTimeout(checkIdleCb, 30000);
        }
      };
      return 
params;
    })(function () { 
// on IDLE
      
Notifier.freezeEvents();
      
Notifier.setFocus(0);
      
cur.onIdle && each(cur.onIdle, function (k, cb) {cb();});
    }, function () { 
// on ACTIVE
      
Notifier.unfreezeEvents();
      
Notifier.setFocus(1);
      
cur.onUnidle && each(cur.onUnidle, function (k, cb) {cb()});
      
FastChat && FastChat.onUnidle();
    });
    
curNotifier.idle_manager.start();
  },
  
initFlashTransport: function (options) {
    return 
false;
    var 
flashVars = extend({
      
onConnectionInit: 'Notifier.onConnectionInit',
      
onConnectionFailed: 'Notifier.onConnectionFailed',
      
onRelogin: 'Notifier.onRelogin',
      
onMessageReceive: 'Notifier.onMessage',
      
onInstanceFocus: 'Notifier.onInstanceFocus',
      
onInstanceServer: 'Notifier.onInstanceServer'
    
}, options);
    if (
vk.id == 13033) {
      
flashVars.onDebug = 'debugLog';
    }
    var 
queueCont = Notifier.getTransportWrap();
    if (!
renderFlash(queueCont, {url: curNotifier.flash_url, id: 'queue_transport', name: 'queue_transport'}, {}, flashVars)) {
      return 
false;
    }
    
curNotifier.flash_transport = ge('queue_transport') || false;
    
curNotifier.transport = 'flash';
    return 
true;
  },
  
initFrameTransport: function () {
    if (!
ls.checkVersion() || browser.msie8 || !('onmessage' in window || 'postMessage' in window)) return false;

    
curNotifier.connection_id = 'queue_connection_' + curNotifier.queue_id;
    
curNotifier.lc_prev_value = '';
    
curNotifier.is_server = false;
    
curNotifier.lp_connected = false;
    
curNotifier.error_timeout = 1;
    
curNotifier.post_message = Notifier.debug || !(browser.opera || browser.msie);
    
curNotifier.transport = 'frame';

    
this.lcInit();

    return 
true;
  },
  
onConnectionInit: function () {
    (!
curNotifier.idle_manager || !curNotifier.idle_manager.is_idle) && Notifier.setFocus(1);
  },
  
onConnectionFailed: function () { },
  
onRelogin: function () {
    
setTimeout(function () {
      
Notifier.standby();
    }, 
0);
  },
  
onMessage: function (msg) {
    if (
curNotifier.focus_instance && curNotifier.focus_instance != curNotifier.instance_id) { // Process only events, when no active tab or current window is focused
      
return;
    }
    try {
      var 
events = eval('(' + msg + ')'), pushed = false;
      
Notifier.pushEvents(events);
    } catch (
e) {debugLog(e.message);}
  },
  
onInstanceFocus: function (instance) {
    var 
focused = instance.charAt(0);
    
instance = instance.substr(1);
    if (
focused == '1') {
      
curNotifier.focus_instance = instance;
    } else {
      if (
curNotifier.focus_instance == instance) {
        
curNotifier.focus_instance = '';
      }
      return;
    }
    if (
instance != curNotifier.instance_id) {
      if (!
curNotifier.idle_manager.is_idle) {
        
curNotifier.idle_manager.is_idle = true;
        
curNotifier.idle_manager.onIdle();
      }
      
Notifier.hideAllEvents();
    }
  },
  
onInstanceServer: function (isServer) {
    
curNotifier.is_server = !!intval(isServer);
  },
  
pushEvents: function (evs, cnt) {
    var 
pushed = 0;
    
each (evs, function (k, v) {
      
pushed |= Notifier.pushEvent(v, cnt);
    });
    if (
pushed && !ls.get('sound_notify_off') && curNotifier.is_server) {
      if (
pushed & 2) {
        
curNotifier.sound_im.play();
      } else {
        
curNotifier.sound.play();
      }
    }
  },
  
pushEvent: function (msg, cnt) {
    if (
msg == 'nop') {
      return;
    }
    
msg = msg.split('<!>');
    if (
msg[0] != curNotifier.version) {
      
debugLog('Notifier old version');
      return 
false;
    }
    if (
msg[1] == 'update_cnt') { // msg[2] - section
      
handlePageCount(msg[3], msg[4], msg[5], msg[6]);
      return 
0;
    }
    var 
ev = {
      
type: msg[1],
      
title: msg[2],
      
author_photo: psr(msg[3] || ''),
      
author_link: msg[4] || '',
      
text: psr(msg[5]),
      
add_photo: psr(msg[6]) || '',
      
link: msg[7],
      
onclick: msg[8],
      
add: msg[9],
      
id: msg[10],
      
author_id: msg[11]
    }, 
push = !cnt ? 1 : 0;

    if (
msg[12]) {
      
ev.custom = eval('('+msg[12]+')');
    }

    if (
curNotifier.done_events[ev.id]) return;
    
curNotifier.done_events[ev.id] = 1;
    
// debugLog(ev.type, ev.add, !!cnt);

    
switch (ev.type) {
      case 
'mail':
        
handlePageCount('msg', ev.add);
        if (
window.Call && Call.params.call_id && intval(ev['author_id']) == intval(Call.params['far_uid'])) {
          
Call.showChat();
        }
        break;

      case 
'post_reply':
      case 
'reply_reply':
      case 
'post_mention':
      case 
'reply_mention':
      case 
'wall_post':
      case 
'comment_photo':
      case 
'comment_photo_reply':
      case 
'comment_photo_mention':
      case 
'comment_video':
      case 
'comment_video_reply':
      case 
'comment_video_mention':
      case 
'board_mention':
        
handlePageCount('nws', ev.add, 'feed', 'section=notifications');
        break;

      case 
'mail_failed':
        var 
peer = intval(ev.author_id);
        if (
nav.objLoc[0] == 'im' && cur.tabs[peer]) {
          var 
msg = ge('mess'+ev.add);
          if (
msg && hasClass(msg, 'im_new_msg')) {
            
removeClass(msg, 'im_new_msg');
            
addClass(msg, 'im_failed');
            var 
n = geByClass1('im_log_author_chat_name', msg);
            if (
n) {
              
n.innerHTML += ' &nbsp;<span>'+cur.lang['mail_send_failed']+'</span>';
            }
            
push = 2; // only sound
          
}
        }
        break;

      case 
'friend_request':
        
handlePageCount('fr', ev.add);
        break;

      case 
'mail_cnt':
        
handlePageCount('msg', ev.add);
        
push = 0;
        break;

      case 
'clear_notify':
        
Notifier.hideAllEvents();
        
push = 0;
        break;

      case 
'support_reply':
        
handlePageCount ('spr', ev.add, 'support', (ev.author_id ? 'act=show&id=' + ev.author_id : 'act=show'));
        
toggle('l_spr', ev.add > 0);
        break;

      case 
'support_cnt':
        
handlePageCount ('spr', ev.add, 'support', (ev.author_id ? 'act=show&id=' + ev.author_id : 'act=show'));
        
toggle('l_spr', ev.add > 0);
        
push = 0;
        break;

      case 
'balance_changed':
        
updateMoney(ev.add);
        if (
ev.custom && ev.custom[0] == 'app' && cur.app) {
          if (
cur.app.params.api_id == ev.custom[1]) {
            
cur.app.balanceUpdated(ev.custom[2]);
          }
        }
        break;

      case 
'gift_sent':
        
re('left_block10_0');
        var 
left_block = ev.add;
        if (
left_block) {
          var 
leftBlocksElem = ge('left_blocks'),
              
left_unpaid_gifts = se(left_block);
          if (
leftBlocksElem) {
            if (
leftBlocksElem.firstChild) {
              
leftBlocksElem.insertBefore(left_unpaid_gifts, leftBlocksElem.firstChild);
            } else {
              
leftBlocksElem.appendChild(left_unpaid_gifts);
            }
          }
        }
        break;

      case 
'call_start':
        if (
window.Call) {
          
Call.incomingReceive(ev);
        } else {
          
stManager.add(['call.js', 'call.css', 'notifier.css'], function() {
            
Call.incomingReceive(ev);
          });
        }
        
push = 0;
        break;

      case 
'call':
        if (
window.Call) {
          
Call.processNotify(ev);
        } else {
          
debugLog('wnd Call event without call obj');
        }
        
push = 0;
        break;
    }
    
// debugLog(ev);
    
if (push && ev.type == 'mail') {
      var 
lastImTime = intval(ls.get('im_opened' + vk.id));
      if (
vkNow() - lastImTime < 2000) {
        
// So, currently IM is active
        
push = 0;
      } else {
        
push |= 2;
      }
      if (
window.curFastChat && curFastChat.inited && ev.author_id) {
        if (
curFastChat.tabs && curFastChat.tabs[ev.author_id] ||
            
curFastChat.clistBox && curFastChat.clistBox.visible) {
          
push = 2; // Only sound notification
        
} else {
          
ev.onclick = 'FastChat.addPeer('' + ev.author_id + '', false, true);';
        }
      }
    }
    if (
push & 1) {
      
curNotifier.q_events.push(ev);
      if (
curNotifier.q_events.length > 30) {
        
curNotifier.q_events.splice(0, curNotifier.q_events.length - 30);
      }
      
this.checkEvents();
    }
    return 
push;
  },
  
checkEvents: function () {
    if (!
curNotifier.q_events.length ||
        
curNotifier.q_shown.length >= (curNotifier.idle_manager.is_idle ? curNotifier.q_idle_max : curNotifier.q_max) ||
        !
curNotifier.idle_manager.is_idle && curNotifier.frozen
    
) {
      
// debugLog(['check failed', !curNotifier.q_events.length, curNotifier.q_shown.length >= (curNotifier.idle_manager.is_idle ? curNotifier.q_idle_max : curNotifier.q_max), !curNotifier.idle_manager.is_idle && curNotifier.frozen, curNotifier.q_events.length, curNotifier.q_shown.length, (curNotifier.idle_manager.is_idle ? curNotifier.q_idle_max : curNotifier.q_max), curNotifier.idle_manager.is_idle, curNotifier.frozen]);
      // debugLog(curNotifier.q_shown);
      
return;
    }
    var 
ev = curNotifier.q_events.shift();
    
// if (nav.objLoc[0] == 'im' && ev.type == 'mail') return;
    // debugLog(this.canNotifyUi(), curNotifier.focus_instance, curNotifier.instance_id, curNotifier.is_server);
    
if (curNotifier.idle_manager.is_idle && curNotifier.is_server && this.canNotifyUi()) {
      
this.showEventUi(ev);
    } else {
      
this.showEvent(ev);
    }
  },

  
showEvent: function (ev) {
    
curNotifier.q_shown.push(ev);
    var 
imgClass = '';
    if (
ev.type == 'gift') {
      
imgClass = ' notifier_image_gift';
    }
    
ev.baloonWrapEl = ce('div', {
      
className: 'notifier_baloon_wrap',
      
innerHTML: '<div class="notifier_baloon clear_fix"><div class="notifier_baloon_head clear_fix"><div class="notifier_baloon_title fl_l">' + ev.title + '</div><div class="notifier_close_wrap fl_r"><a class="notifier_close" title="' + getLang('global_close') + '" href=""></a></div></div><div class="notifier_baloon_body"><table cellpadding="0" cellspacing="0" width="100%"><tr>' + (ev.author_photo && ('<td class="notifier_image_wrap"><div class="notifier_image_wrap">' + (ev.author_link && ('<a href="' + ev.author_link + '" onclick="return nav.go(this, event);">')) + '<img src="' + Notifier.fixPhoto(ev.author_photo) + '" class="notifier_image" />' + (ev.author_link && '</a>') + '</div></td>')) + '<td class="notifier_baloon_msg"><div class="notifier_baloon_msg wrapped" style="width: ' + (300 - 60 * ((ev.author_photo && ev.add_photo) ? 2 : ((ev.add_photo || ev.author_photo) ? 1 : 0))) + 'px;">' + ev.text + '</div></td>' + (ev.add_photo && ('<td class="notifier_add_image_wrap"><div class="notifier_image_wrap'+imgClass+'"><img src="' + ev.add_photo + '" class="notifier_image" /></div></td>')) + '</tr></table></div></div>'
    
});
    
ev.baloonEl = ev.baloonWrapEl.firstChild;
    
ev.closeEl = geByClass1('notifier_close_wrap', ev.baloonEl);
    
addEvent(ev.closeEl, 'mouseover mouseout', function (e) {
      
e = (e.originalEvent || e) || window.event;
      if ((
e.target || e.srcElement) != ev.closeEl) {
        return;
      }
      if (
e.type == 'mouseover') {
        
addClass(ev.closeEl, 'notifier_close_over');
      } else {
        
removeClass(ev.closeEl, 'notifier_close_over');
      }
    });

    
addEvent(ev.baloonEl, 'mouseover mouseout', function (e) {
      
ev.over = (e.type == 'mouseover');
      if (
ev.over) {
        
Notifier.freezeEvents();
        
addClass(ev.baloonEl, 'notifier_baloon_over');
      } else {
        
Notifier.unfreezeEvents();
        
removeClass(ev.baloonEl, 'notifier_baloon_over');
      }
    });
    
addEvent(ev.baloonEl, 'mousedown', function (e) {
      
e = (e.originalEvent || e) || window.event;
      var 
btn = e.which, nohide = false;
      if (
browser.msie) {
        
btn = e.button == 1 ? 1 : (e.button == 2 ? 3 : 2)
      }
      if (
btn == 1 && (e.ctrlKey || browser.mac && e.metaKey)) {
        
btn = 2;
        if (
browser.mac) nohide = true;
      }
      if ((
e.target || e.srcElement).tagName == 'A') {
        switch (
btn) {
          case 
1: // left button
            // setTimeout(function () {Notifier.hideEvent(ev);}, 100);
            
break;

          case 
3: // right
            
break;
        }
        return;
      }
      
// debugLog(e.button, e.which, btn, nohide);
      
switch (btn) {
        case 
1: //left button
          
eval(ev.onclick);
          
Notifier.hideEvent(ev);
          break;
        case 
2: // middle
          
var wnd = window.open(ev.link, '_blank');
          try {
wnd.blur(); window.focus();} catch (e) {}
          if (!
nohide) Notifier.hideEvent(ev); // else it will be  hidden by context menu
          
break;
        case 
3: // right
          
if (browser.mozilla) {
            return;
          }
      }
      return 
cancelEvent(e);
    });
    
addEvent(ev.baloonEl, 'contextmenu', function (e) {
      
// debugLog('contextmenu');
      
setTimeout(function () {
        
Notifier.hideEvent(ev, false, false, true);
      }, 
10);
      return 
cancelEvent(e);
    });
    
addEvent(ev.closeEl, 'mousedown', function (e) {
      
Notifier.hideEvent(ev, false, false, true);
      return 
cancelEvent(e);
    });
    
ev.startFading = function () {
      
ev.fading = animate(ev.baloonEl, {opacity: 0}, 1000, Notifier.hideEvent.bind(Notifier).pbind(ev, false));
      if (
ev.over) {
        
ev.fading.stop();
      }
    }
    
curNotifier.cont.insertBefore(ev.baloonWrapEl, curNotifier.cont.firstChild);
    var 
h = ev.baloonWrapEl.offsetHeight;
    
re(ev.baloonWrapEl);
    
curNotifier.cont.appendChild(ev.baloonWrapEl);
    
setStyle(curNotifier.cont, {bottom: -h});
    
setStyle(ev.baloonWrapEl, {visibility: 'visible'});
    
animate(curNotifier.cont, {bottom: 0}, 200);
    if (!
curNotifier.idle_manager.is_idle) {
      
ev.fadeTO = setTimeout(ev.startFading, 7000);
    }
  },

  
canNotifyUi: function () {
    
// if (vk.id == 13033) return false;
    
return window.webkitNotifications && (webkitNotifications.checkPermission() <= 0 && !ls.get('im_ui_notify_off'));
  },

  
showEventUi: function (ev) {
    if (!
this.canNotifyUi()) return false;

    
// curNotifier.q_shown.push(ev);
    
var title = stripHTML(replaceEntities(ev.title));
    var 
text = stripHTML(replaceEntities(ev.text).replace(/<br>/g, "n").replace(/(<span class='notifier_author_quote'.*</span>)/, '$1:')).replace(/&laquo;|&raquo;/gi, '"');
    var 
notification = ev.uiNotification = webkitNotifications.createNotification(ev.author_photo, title, text);
    
notification.onclick = function (e) {
      
window.focus();
      eval(
ev.onclick);
      
Notifier.hideEvent(ev);
    };
    
notification.onclose = function () {
      
Notifier.hideEvent(ev, true);
    };
    
notification.show();
    
ev.closeTO = setTimeout(Notifier.hideEvent.bind(Notifier).pbind(ev), 5000);
    return 
true;
  },

  
hideEvent: function (ev, already, broadcasted, forced) {
    
clearTimeout(ev.closeTO);
    
clearTimeout(ev.fadeTO);
    
ev.fading && ev.fading.stop();
    var 
pos = indexOf(curNotifier.q_shown, ev), closedLen;
    
// debugLog('hide event', ev, already, broadcasted, forced, pos);
    
if (pos != -1) {
      
curNotifier.q_shown.splice(pos, 1);
    } else if (!
ev.uiNotification && vk.id == 13033) {
      
// window.console && isFunction(console.trace) && console.trace();
      // debugLog('trying to splice event not from q_shown', ev, curNotifier.q_shown);
    
}
    
Notifier.unfreezeEvents();
    if (!
already) {
      if (
ev.baloonWrapEl) {
        
cleanElems(ev.closeEl, ev.baloonEl);
        
re(ev.baloonWrapEl);
      } else if (
ev.uiNotification) {
        
ev.uiNotification.cancel();
      }
    }
    if (
forced === true && isArray(curNotifier.q_closed)) {
      
curNotifier.q_closed.unshift(vkNow());
      if ((
closedLen = curNotifier.q_closed.length) > 3) {
        
curNotifier.q_closed.splice(3, closedLen - 3);
        
closedLen = 3;
      }
      
// debugLog('q_closed', clone(curNotifier.q_closed));
      // closedLen == 3 && debugLog('interval', curNotifier.q_closed[0] - curNotifier.q_closed[2]);
      
if (closedLen == 3 && curNotifier.q_closed[0] - curNotifier.q_closed[2] < 700) {
        
Notifier.hideAllEvents();
      }
    }
    if (
forced != -1) {
      
this.checkEvents();
    }
    if (
curNotifier.transport == 'frame' && !broadcasted) {
      
this.lcSend('hide', {event_id: ev.id});
    }
    if ((
forced === true || !curNotifier.idle_manager.is_idle) && !curNotifier.q_events.length && !curNotifier.q_shown.length) {
      
ajax.post('notifier.php', {act: 'a_clear_notifier'});
    }
  },
  
hideAllEvents: function () {
    
curNotifier.q_events = [];
    
each (clone(curNotifier.q_shown), function () {
      
Notifier.hideEvent(this, false, true, -1);
    });
    
curNotifier.q_shown = [];
    
curNotifier.q_closed = [];
  },
  
onEventHide: function (event_id) {
    if (!
event_id) return;
    
// debugLog('ev.id',  event_id);
    
each(curNotifier.q_shown, function () {
      if (
this.id == event_id) {
        
// debugLog('found in q_shown', this);
        
Notifier.hideEvent(this, false, true);
        return 
false;
      }
    });
    
each(curNotifier.q_events, function (k) {
      if (
this.id == event_id) {
        
// debugLog('found in q_events', this);
        
curNotifier.q_events.splice(k, 1);
        return 
false;
      }
    });
  },

  
/* Fake localConnection methods (based on localStorage and onstorage events) */
  
lcInit: function () {
    if (!
curNotifier.post_message) {
      if (
browser.msie && (intval(browser.version) < 9)) {
        
// document.onstorage = this.lcOnStorage.bind(this);
        
addEvent(document, 'storage', this.lcOnStorage.bind(this));
      } else {
        
addEvent(window, 'storage', this.lcOnStorage.bind(this));
      }
      
this.lcStart();
    } else { 
// localStorage through extra-iframe, because of vk document.domain problems
      
addEvent(window, 'message', this.lcOnMessage.bind(this));
      var 
el = curNotifier.storage_el = ce('iframe', {
        
id: 'queue_storage_frame',
        
name: 'queue_storage_frame',
        
src: /*location.protocol + '//' + locHost + */'/notifier.php?act=storage_frame&from=' + location.host + (Notifier.debug ? '&debug=' + vkNow() : '&4') + '#' + curNotifier.connection_id
      
});
      
Notifier.getTransportWrap().appendChild(el);
      
curNotifier.storage_frame = el.contentWindow;
      
curNotifier.storage_frame_origin = location.protocol + '//' + locHost;
    }
  },
  
lcStart: function () {
    if (
Notifier.lcCheckServer()) {
      
this.lcServer();
    } else {
      
this.lcSend('check');
      
clearTimeout(curNotifier.becomeServerTO);
      
curNotifier.becomeServerTO = setTimeout(this.lcServer.bind(this).pbind(true), 500);
    }

    
curNotifier.checkServerInt = setInterval(function () {
      if (
curNotifier.is_server) return;
      if (
vkNow() - curNotifier.last_succ > 8000 && Notifier.lcCheckServer()) {
        
debugLog('timeout');
        
this.lcServer(true);
      }
    }.
bind(this), 1000 + intval(rand(-100, 100)));
    
curNotifier.isServerBroadcastInt = setInterval(function () {
      if (!
curNotifier.is_server) return;
      if (
Notifier.lcCheckServer()) {
        
this.lcSend('check_ok');
      } else {
        
debugLog('no server from server broadcast');
        
this.lcNoServer();
      }
    }.
bind(this), 5000 + intval(rand(-100, 100)));
    
curNotifier.playlistTimeInt = setInterval(function () {
      if (!
curNotifier.is_server) return;
      var 
plData = ls.get('pad_pldata');
      
// if (!plData)  {
      //   var aid = ls.get('audio_id');
      //   if (aid) ls.remove('audio_id');
      //   return;
      // }
      
if (plData && plData.instance == curNotifier.instance_id) {
        
ls.set('pad_pltime', vkNow());
      } else {
        
this.lcSend('check_playlist');
      }
      var 
plTime = ls.get('pad_pltime') || 0;
      if (
vkNow() - plTime > 3000 && !(window._pads && _pads.shown == 'mus')) {
        
ls.remove('pad_pltime');
        
ls.remove('pad_pldata');
        
ls.remove('pad_playlist');
        
ls.remove('pad_lastsong');
        
ls.remove('audio_id');
      }
    }.
bind(this), 1000 + intval(rand(-100, 100)));

    if (
curNotifier.fc !== undefined) {
      
FastChat.init(curNotifier.fc);
    }
  },
  
lcStop: function () {
    
clearInterval(curNotifier.isServerBroadcastInt);
    
clearInterval(curNotifier.checkServerInt);
    
clearTimeout(curNotifier.becomeServerTO);
  },
  
lcSend: function (act, data) {
    
Notifier.debug && debugLog(curNotifier.instance_id + ': sending', act, data || '');
    
// debugLog(act);
    
var sendObj = extend({__client: curNotifier.instance_id, __act: act, __rnd: Math.random()}, data || {});
    if (!
curNotifier.post_message) {
      
ls.set(curNotifier.connection_id, sendObj);
    } else {
      try {
        
curNotifier.storage_frame.postMessage(curNotifier.connection_id + ':' + JSON.stringify(sendObj), curNotifier.storage_frame_origin);
      } catch (
e) {debugLog(e, e.message, e.stack);}
    }
  },
  
lcRecv: function (data) {
    if (
isEmpty(data) || data.__client == curNotifier.instance_id) return;
    var 
act = data.__act;
    
delete data.__client;
    
delete data.__act;
    
delete data.__rnd;

    
Notifier.debug && debugLog(curNotifier.instance_id + ': recv', act, data);
    
// debugLog(act);
    
switch (act) {
      case 
'new_server':
        
curNotifier.last_succ = vkNow() + 1000; // extra 1 sec for iframe init
        
break;

      case 
'feed':
        
curNotifier.timestamp = data.ts;
        
curNotifier.key = data.key;
        
Notifier.pushEvents(data.events, !data.full);
        break;

      case 
'addfeed':
        
Notifier.addFeed(data[0], data[1]);
        break;

      case 
'new_key':
        
debugLog('new key', data);
        
curNotifier.timestamp = data.ts;
        
curNotifier.key = data.key;
        break;

      case 
'new_addkey':
        
// debugLog('add key', data);
        
var queue = data.queue || data.key, addq = curNotifier.addQueues[queue], to_reset = !addq && curNotifier.is_server;
        if (
addq) {
          
addq[0] = vkNow();
        } else {
          
curNotifier.addQueues[queue] = [vkNow(), data.ts, data.key];
        }
        if (
to_reset) {
          
Notifier.lpReset();
        }
        break;

      case 
'clear_addkeys':
        
curNotifier.addQueues = {};
        break;

      case 
'check_ok':
        
curNotifier.last_succ = vkNow();
        if (
curNotifier.becomeServerTO) {
          
clearTimeout(curNotifier.becomeServerTO);
          
curNotifier.becomeServerTO = false;
        }
        if (!
curNotifier.lp_connected) {
          
curNotifier.lp_connected = true;
          
Notifier.onConnectionInit();
        }
        break;

      case 
'focus':
        
// debugLog('focus from lc');
        
Notifier.onInstanceFocus(data.instance_id);
        break;

      case 
'hide':
        
// debugLog('hide from lc');
        
Notifier.onEventHide(data.event_id);
        break;

      case 
'check_playlist':
        var 
pl = ls.get('pad_playlist');
        if (
pl && pl.instance == curNotifier.instance_id) {
          
ls.set('pad_pltime', vkNow());
        }
        break;

      default:
        if (
curNotifier.recvClbks && curNotifier.recvClbks[act]) {
          for (var 
i in curNotifier.recvClbks[act]) curNotifier.recvClbks[act][i](data);
        }
        break;
    }
    if (!
curNotifier.is_server) return;

    
// acts, processed only while instance is server
    
switch (act) {
      case 
'new_server':
      case 
'new_key':
      case 
'check_ok':
        
debugLog('no server from lcRecv', act);
        
Notifier.lcNoServer();
        break;

      case 
'check':
        
this.lcSend('check_ok');
        break;
    }
  },
  
lcOnStorage: function (e) { // receiving messages from native onstorage event
    
e = e || window.event;
    
Notifier.debug && debugLog('onstorage', e.key, e.newValue, e);
    var 
key = e.key, val = e.newValue;
    if (!
val) {
      return;
    }
    if (!
key) {
      
key = curNotifier.connection_id;
      
val = localStorage.getItem(key);
      if (
val == curNotifier.lc_prev_value) return;
      
curNotifier.lc_prev_value = val;
    } else {
      if (
e.key != curNotifier.connection_id) return;
    }
    
this.lcRecv(JSON.parse(val) || {});
  },
  
lcOnMessage: function (e) { // receiving messages from storage iframe via postMessage
    
e = e || window.event;
    
Notifier.debug && debugLog('onmessage', e.data, e.origin, e);
    if (
e.origin && e.origin != curNotifier.storage_frame_origin) {
      
// vk.id == 13033 && debugLog('wrong origin', e.origin);
      
return;
    }
    if (
typeof e.data != 'string' || e.data.indexOf('q_st')) return;
    var 
msg = e.data.substr(4), pos, key;
    if (
msg == 'ready') {
      
curNotifier.storage_frame = e.source;
      
this.lcStart();
    } else {
      if ((
pos = msg.indexOf(':')) == -1 || (key = msg.substr(0, pos)) != curNotifier.connection_id || !msg.substr(pos + 1)) return;
      
this.lcRecv(JSON.parse(msg.substr(pos + 1)));
    }
  },
  
lcServer: function (changed) {
    
debugLog('becoming server');
    
this.lpInit();
    
this.lcSend('new_server');
    
Notifier.lcCheckServer(true);
    
curNotifier.is_server = true;
    
Notifier.onInstanceServer(1);
    if (!
curNotifier.lp_connected) {
      
curNotifier.lp_connected = true;
      
Notifier.onConnectionInit();
    }
    if (
window.curFastChat && curFastChat.inited) {
      
FastChat.becameServer();
    }
    
this.lpStop();
    if (!
changed) {
      
this.lpStart();
    } else {
      
this.lpReset(this.lpStart.bind(this));
    }
  },
  
lcNoServer: function () {
    
this.lpStop();
    if (!
curNotifier.is_server) {
      return;
    }
    
debugLog('not server now');
    
this.onInstanceServer(0);
    
curNotifier.is_server = false;
  },
  
lcCheckServer: function (nocheck) {
    var 
key = 'server_' + curNotifier.connection_id,
        
prev, ts = vkNow();

    if (!
nocheck && isArray(prev = ls.get(key)) && prev[0] != curNotifier.instance_id && ts - prev[1] < 8000) {
      return 
false;
    }
    
ls.set(key, [curNotifier.instance_id, ts]);
    return 
true;
  },

  
/* Long-poll methods */
  
lpInit: function () {
    if (
curNotifier.lpMakeRequest) return;
    
delete curNotifier.lpMakeRequest;
    
re('queue_transport_frame');
    
Notifier.getTransportWrap().appendChild(
      
ce('iframe', {
        
id: 'queue_transport_frame',
        
name: 'queue_transport_frame',
        
src: curNotifier.frame_path
      
})
    );
  },
  
lpStart: function () {
    
curNotifier.lp_started = true;
    
Notifier.lpCheck();
  },
  
lpStop: function () {
    
curNotifier.lp_started = false;
    
clearTimeout(curNotifier.lp_check_to);
    
clearTimeout(curNotifier.lp_error_to);
    
clearTimeout(curNotifier.lp_req_check_to);
  },
  
lpCheck: function () {
    if (!
curNotifier.lp_started || curNotifier.lpActive || curNotifier.lpInvalid) return;
    if (!
curNotifier.lpMakeRequest) {
      
clearTimeout(curNotifier.lp_check_to);
      
curNotifier.lp_check_to = setTimeout(this.lpCheck.bind(this), 1000);
      return;
    }
    if (!
Notifier.lcCheckServer()) {
      
debugLog('no server from check');
      
this.lcNoServer();
      return;
    }

    var 
now = vkNow(),
        
add_queues = [],
        
completed = false,
        
params = {
      
act: 'a_check',
      
ts: curNotifier.timestamp,
      
key: curNotifier.key,
      
id: curNotifier.uid,
      
wait: 25
    
};

    
each (curNotifier.addQueues, function (queue, data) {
      if (
now - data[0] > 30000) {
        
// old queue
        
debugLog('drop key', queue, now - data[0]);
        
delete curNotifier.addQueues[queue];
        return;
      }
      
add_queues.push(queue);
      
params.ts += '_' + data[1];
      
params.key += data[2];
    });

    var 
onFail = function (msg) {
      if (
completed) return;
      
completed = true;
      
curNotifier.lpActive = false;
      
clearTimeout(curNotifier.lp_req_check_to);
      
// topError('Notify error: ' + msg);

      
curNotifier.error_timeout = curNotifier.error_timeout || 1;
      
clearTimeout(curNotifier.lp_error_to);
      
curNotifier.lp_error_to = setTimeout(this.lpCheck.bind(this), curNotifier.error_timeout * 1000 + irand(1000, 10000));
      if (
curNotifier.error_timeout < 64) {
        
curNotifier.error_timeout *= 2;
      }
    }.
bind(this);

    
// debugLog('query', params.ts, params.key);
    
curNotifier.lpActive = true;
    
clearTimeout(curNotifier.lp_req_check_to);
    
curNotifier.lp_req_check_to = setTimeout(onFail, (params.wait + 5) * 1000);
    
curNotifier.lpMakeRequest(curNotifier.frame_url, params, function (text) {
      if (
completed) return;
      
completed = true;
      
curNotifier.lpActive = false;
      if (!
curNotifier.lp_started) return;
      
this.lcSend('check_ok');
      try {
        var 
response = eval('(' + text + ')'), main_response = response, add_response, add_queue, busy = 0;
        
// debugLog('response', clone(response), clone(add_queues));
        
if (isArray(response)) {
          
main_response = response.shift();
          while (
add_response = response.shift()) {
            
add_queue = add_queues.shift();
            if (!
add_queue) break;
            if (
add_response.failed == 2 && add_response.err == 4) {
              
debugLog('!!notifier key busy!! ' + curNotifier.instance_id);
              
busy |= 1;
              continue;
            }
            
this.lcSend('addfeed', [add_queue, add_response]);
            
this.addFeed(add_queue, add_response);
            if (
add_response.failed) {
              
delete curNotifier.addQueues[add_queue];
            }
          }
        } else if (
response.failed) {
          while (
add_queue = add_queues.shift()) {
            
this.lcSend('addfeed', [add_queue, response]);
            
this.addFeed(add_queue, response);
            
delete curNotifier.addQueues[add_queue];
          }
          
this.lcSend('clear_addkeys');
        }
        switch (
this.lpChecked(main_response)) {
          case 
0: break; // ok

          
case 1:
            
// topError('Notifier key real error', {dt: -1, type: 5, answer: text + 'nnbusy:' + busy + 'nserver:' + curNotifier.is_server + 'ninstance:' + curNotifier.instance_id, url: curNotifier.frame_url, query: params && ajx2q(params)});
            
return;

          case 
2:
            
busy |= 2; break;

          default: return;
        }
        if (!
busy) {
          
this.lpCheck();
          
curNotifier.error_timeout = Math.max(1, (curNotifier.error_timeout || 1) / 1.5);
        } else {
          
// topError('Notifier key busy', {dt: -1, type: 5, answer: text + 'nnbusy:' + busy + 'nserver:' + curNotifier.is_server + 'ninstance:' + curNotifier.instance_id, url: curNotifier.frame_url, query: params && ajx2q(params)});
          
this.lcNoServer();
        }
      } catch (
e) {
        if (
text && text.indexOf('Ad Muncher') == -1) {
          
topError('Notifier error: ' + e.message, {dt: -1, type: 5, stack: e.stack, answer: text + 'nnbusy:' + busy + 'nserver:' + curNotifier.is_server + 'ninstance:' + curNotifier.instance_id, url: curNotifier.frame_url, query: params && ajx2q(params)});
          
debugLog(e.message, e.stack, e);
        }

        
curNotifier.error_timeout = curNotifier.error_timeout || 1;
        
clearTimeout(curNotifier.lp_error_to);
        
curNotifier.lp_error_to = setTimeout(this.lpCheck.bind(this), curNotifier.error_timeout * 1000);
        if (
curNotifier.error_timeout < 64) {
          
curNotifier.error_timeout *= 2;
        }
      }
    }.
bind(this), onFail);
  },
  
lpChecked: function(response) {
    
// debugLog('response', response);
    
var failed = response.failed;
    if (
failed == 2) {
      if (
response.err == 4) {
        return 
2;
      }
      
curNotifier.lpInvalid = true;
      
clearTimeout(curNotifier.lp_error_to);
      
curNotifier.lp_error_to = setTimeout(this.lpGetKey.bind(this), curNotifier.error_timeout * 1000);
      if (
curNotifier.error_timeout < 64) {
        
curNotifier.error_timeout *= 2;
      }
      return (
response.err == 1) ? 1 : 3;
    } else if (
failed) {
      throw 
getLang('global_unknown_error');
    }
    
this.lcSend('feed', extend({full: curNotifier.idle_manager && curNotifier.idle_manager.is_idle && !this.canNotifyUi(), key: curNotifier.key}, response));

    
curNotifier.timestamp = response.ts;
    
Notifier.pushEvents(response.events);
    return 
0;
  },
  
lpOnReset: function () {
    
curNotifier.lpOnReset && curNotifier.lpOnReset();
  },
  
lpReset: function (cb) {
    if (
cb) {
      
curNotifier.lpOnReset = cb;
    }
    
clearTimeout(curNotifier.resetTO);
    
curNotifier.resetTO = setTimeout(function () {
      if (
curNotifier.is_server && !curNotifier.lp_started) {
        
Notifier.lpStart();
        return
      }
      if (
curNotifier.lpMakeRequest && !curNotifier.lpInvalid) {
        var 
key = curNotifier.key, ts = curNotifier.timestamp;
        
each (curNotifier.addQueues, function (queue, data) {
          
key += data[2];
          
ts += '_' + data[1];
        });
        
curNotifier.lpMakeRequest(curNotifier.frame_url, {
          
act: 'a_release',
          
key: key,
          
ts: ts,
          
id: curNotifier.uid,
          
wait: 25
        
}, Notifier.lpOnReset, Notifier.lpOnReset);
      } else {
        
ajax.post('notifier.php?act=a_reset', false, {
          
onDone: Notifier.lpOnReset,
          
onFail: function () {Notifier.lpOnReset(); return true;}
        });
      }
    }, 
100);
  },
  
lpGetKey: function () {
    var 
stNow = vkNow();
    
ajax.post('notifier.php?act=a_get_key', {id: curNotifier.uid}, {
      
onDone: function (key, ts) {
        
curNotifier.timestamp = ts;
        
curNotifier.key = key;
        
curNotifier.lpInvalid = false;
        
this.lcSend('new_key', {ts: ts, key: key});
        
this.lpCheck();
      }.
bind(this),
      
onFail: function (code) {
        switch (
code) {
          case 
1: // non auth
          
case 3: // disabled
            
Notifier.standby();
            return;
            break

          case 
4: // dynamic IP address
            
Notifier.standby(300);
            return;
            break;

          case 
2: // wrong auth
            
Notifier.onRelogin();
            return;
            break
        }
        
curNotifier.error_timeout = 64;
        
clearTimeout(this.lp_error_to);
        
this.lp_error_to = setTimeout(this.lpGetKey.bind(this), curNotifier.error_timeout * 1000);
        
// if (curNotifier.error_timeout < 64) {
        //   curNotifier.error_timeout *= 2;
        // }
        
return true;
      }.
bind(this)
    });
  },

  
addKey: function (data, cb, local) {
    if (
curNotifier.flash_transport) {
      return 
false;
    }
    
// debugLog(data);
    
var queue = (data && data.queue) ? data.queue : data.key;
    var 
addq = curNotifier.addQueues[queue];
    var 
to_reset = !addq && curNotifier.is_server;
    if (
addq) {
      
addq[0] = vkNow();
      
addq[3] = cb;
      
addq[4] = local;
    } else {
      
curNotifier.addQueues[queue] = [vkNow(), data.ts, data.key, cb, local];
    }
    if (!
local) {
      
Notifier.lcSend('new_addkey', data);
    }
    if (
to_reset) {
      
Notifier.lpReset();
    }
    return 
true;
  },
  
addFeed: function (queue, data) {
    var 
addq = curNotifier.addQueues[queue];
    if (!
isArray(addq) || !addq.length) return;

    
addq[1] = data.ts;
    if (
isFunction(addq[3])) {
      
// debugLog('addfeed', queue, data);
      
addq[3](queue, data);
    }
  },
  
addRecvClbk: function(act, type, clbk, force) {
    if (!
curNotifier.recvClbks) curNotifier.recvClbks = {};
    if (!
curNotifier.recvClbks[act]) curNotifier.recvClbks[act] = {};
    if (!
curNotifier.recvClbks[act][type] || force) {
      
curNotifier.recvClbks[act][type] = clbk;
    }
  },

  
fixPhoto: function (src, smaller) {
    
src = clean(src);
    if (
src.indexOf('question_c.gif') == -1) {
      return 
src;
    }
    return 
smaller ? '/images/question_inv_xc.png' : '/images/question_inv_c.png';
  }
}

function 
Sound(filename) {
  var 
audioObjSupport = false, audioTagSupport = false, self = this, ext;
  if (!
filename) throw 'Undefined filename';

  try {
    var 
audioObj = ce('audio');
    
audioObjSupport = !!(audioObj.canPlayType);

    if ((
'no' != audioObj.canPlayType('audio/mpeg')) && ('' != audioObj.canPlayType('audio/mpeg')))
      
ext = '.mp3?1';
    else if ((
'no' != audioObj.canPlayType('audio/ogg; codecs="vorbis"')) && ('' != audioObj.canPlayType('audio/ogg; codecs="vorbis"')))
      
ext = '.ogg?1';
    else
      
audioObjSupport = false;
  } catch (
e) {}
  
// audioObjSupport = false;

  
if (audioObjSupport) {
    
audioObj.src = '/' + filename + ext;
    var 
ended = false;
    
audioObj.addEventListener('ended', function(){ended = true;}, true);
    
audioObj.load();
    
this.playSound = function() {
      if (
ended) {
        
audioObj.load();
      }
      
audioObj.play();
      
ended = false;
    };
    
this.pauseSound = function() {
      
audioObj.pause();
    };
  } else {
    
cur.__sound_guid = cur.__sound_guid || 0;
    var 
wrap = ge('flash_sounds_wrap') || utilsNode.appendChild(ce('span', {id: 'flash_sounds_wrap'})),
        
guid = 'flash_sound_' + (cur.__sound_guid++);

    var 
opts = {
      
url: '/swf/audio_lite.swf?4',
      
id: guid
    
}
    var 
params = {
      
swliveconnect: 'true',
      
allowscriptaccess: 'always',
      
wmode: 'opaque'
    
}
    if (
renderFlash(wrap, opts, params, {})) {
      var 
swfObj = browser.msie ? window[guid] : document[guid],
          
inited = false,
          
checkLoadInt = setInterval(function () {
        if (
swfObj && swfObj.paused) {
          try {
            
swfObj.setVolume(1);
            
swfObj.loadAudio('/' + filename + ext);
            
swfObj.pauseAudio();
          } catch (
e) {debugLog(e);}
        }
        
inited = true;
        
clearInterval(checkLoadInt);
      }, 
300);
      
self.playSound = function() {
        if (!
inited) return;
        
swfObj.playAudio(0);
      };
      
self.pauseSound = function() {
        if (!
inited) return;
        
swfObj.pauseAudio();
      };
    }
  }
}
Sound.prototype = {
  
play: function() {
    try {
this.playSound();} catch(e){}
  },
  
pause: function() {
    try {
this.pauseSound();} catch(e){}
  }
};


function 
getWndInner() {
  var 
w = lastWindowWidth, h = lastWindowHeight, sb = sbWidth();
  if (
lastWndScroll[0] !== false ? lastWndScroll[0] :
      (
browser.msie6 ? pageNode.scrollHeight > pageNode.clientHeight : !browser.msie6 && htmlNode.scrollHeight > htmlNode.clientHeight)) {
    
w -= sb + (sb ? 1 : 0);
  }
  return [
h, w];
}

window.lastWndScroll = [false, false];
function 
updateWndVScroll() {
  var 
w = window, wndInner = getWndInner(), vScroll = false;
  if (
w.boxLayerWrap && isVisible(boxLayerWrap)) {
    
vScroll = (boxLayerWrap.scrollHeight > boxLayerWrap.clientHeight) ? 1 : 0;
  } else if (
w.layerWrap && isVisible(layerWrap)) {
    
vScroll = (layerWrap.scrollHeight > layerWrap.clientHeight) ? 1 : 0;
  } else if (
w.mvLayerWrap && isVisible(mvLayerWrap)) {
    
vScroll = (mvLayerWrap.scrollHeight > mvLayerWrap.clientHeight) ? 1 : 0;
  } else {
    
vScroll = false;
  }
  if (
vScroll === lastWndScroll[0]) {
    return;
  }
  
lastWndScroll[0] = vScroll;

  
each (curRBox.boxes, function (id) {
    if (
this.toRight) {
      
setStyle(this.wrap, {marginRight: vScroll ? sbWidth() + 1 : 0});
    }
  });
}

function 
defBox(options, callback) {
  var 
boxC = '<div><div class="fc_clist_inner"><div class="fc_tab_head"><a class="fc_tab_close_wrap fl_r"><div class="fc_tab_close"></div></a><div class="fc_tab_title noselect">%title%</div></div>%content%</div></div></div>';

  if (
options.content) {
    var 
cont = '<div class="fc_content_wrap"><div class="fc_content">'+options.content+'</div></div>';
  } else {
    var 
cont = options.innerHTML;
  }
  var 
wrap = se(rs(boxC, {
    
title: options.title,
    
content: cont
  
}));
  var 
cont = geByClass1('fc_content', wrap, 'div');
  var 
opts = {
    
movable: geByClass1('fc_tab_head', wrap),
    
hider: geByClass1('fc_tab_close_wrap', wrap, 'a'),
    
startLeft: options.x,
    
startTop: options.y,
    
startHeight: options.height,
    
startWidth: options.width,
    
resizeableH: cont,
    
resize: false,
    
minH: options.minH,
    
onBeforeHide: options.onBeforeHide || function() {},
    
onHide: options.onHide || function () {},
    
onDragEnd: function (y, x) {},
    
onResize: function (h, w) {}
  },
  
box = new RBox(wrap, extend(opts, options));

  if (
options.content) {
    var 
scroll = new Scrollbar(cont, {
      
prefix: 'fc_',
      
more: debugLog,
      
nomargin: true,
      global: 
true,
      
nokeys: true,
      
right: vk.rtl ? 'auto' : 10,
      
left: !vk.rtl ? 'auto' : 10,
      
onHold: options.onHold
    
});
  }

  
callback({
    
id: box.id,
    
cont: cont,
    
update: function() {
      
scroll && scroll.update();
    }
  });
  return 
box;
}

if (!
window.curRBox) {
  
curRBox = {
    
guid: 0,
    
active: false,
    
focused: [],
    
boxes: {}
  };
}
function 
RBox(content, options) {
  var 
t = this, defaultOptions = {
    
minH: 50,
    
minW: 50
  
};
  
t.options = options = extend(defaultOptions, options);
  
t.content = content;
  var 
id = t.id = 'rb_box_' + (options.id || curRBox.guid++);

  
t.wrap = ce('div', {id: id, className: 'rb_box_wrap fixed'});
  var 
pos = {};
  
t.toBottom = t.toRight = false;
  if (
options.startTop !== undefined) pos.top = options.startTop;
  else if (
options.startBottom !== undefined) pos.bottom = options.startBottom;
  if (
options.startLeft !== undefined) pos.left = options.startLeft;
  else if (
options.startRight !== undefined) pos.right = options.startRight;
  
setStyle(t.wrap, pos);

  if (
options.movable) {
    
addEvent(options.movable, 'mousedown', t._head_mdown.bind(t));
  }
  
t.resizeableH = options.resizeableH || content;
  if (
options.startHeight) {
    
setStyle(t.resizeableH, 'height', options.startHeight);
  }
  
t.resizeableW = options.resizeableW || content;
  if (
options.startWidth) {
    
setStyle(t.resizeableW, 'width', options.startWidth);
  }
  
addEvent(content, 'mousedown', t._cont_mdown.bind(t));
  if (
options.closer) {
    
addEvent(options.closer, 'mousedown', t._close_mdown.bind(t));
    
addEvent(options.closer, 'click', t._close_click.bind(t));
  }
  if (
options.hider) {
    
addEvent(options.hider, 'mousedown', t._close_mdown.bind(t));
    
addEvent(options.hider, 'click', t._hide_click.bind(t));
  }
  if (
options.minimizer && options.minimizer !== true) {
    
addEvent(options.minimizer, 'mousedown', t._close_mdown.bind(t));
    
addEvent(options.minimizer, 'click', t._min_toggle.bind(t));
  }
    
// debugLog(options.closer);

  
t.wrap.appendChild(content);

  if (
options.resize !== false) {
    
t.resizeWrap = ce('div', {className: 'rb_resize_wrap', innerHTML: '<div class="rb_resize"></div>'});
    
t.wrap.appendChild(t.resizeWrap);
    
addEvent(t.resizeWrap, 'mousedown', t._resize_mdown.bind(t));
  }
  if (
options.minimized) {
    
addClass(t.wrap, 'rb_minimized');
    
t.minimized = true;
  }
  
bodyNode.insertBefore(t.wrap, ge('page_wrap'));

  var 
st = getStyle(t.wrap, 'top'),
      
sb = getStyle(t.wrap, 'bottom'),
      
sl = getStyle(t.wrap, 'left'),
      
sr = getStyle(t.wrap, 'right');
  
this.toBottom = (st === 'auto' || st === '' || browser.msie && st === 0) && sb != 'auto' && sb !== '' && !(browser.msie && sb === 0);
  
this.toRight = (sl === 'auto' || sl === '' || browser.msie && sl === 0) && sr != 'auto' && sr !== '' && !(browser.msie && sr === 0);

  if (
this.toRight) {
    
setStyle(t.wrap, {marginRight: lastWndScroll[0] ? sbWidth() + 1 : 0});
  }
  if (
options.nofocus || options.noshow) {
    
addClass(t.wrap, 'rb_inactive');
  }

  
// console.log(['s', st,st === '0',st === 0, sb, sl,sl==='0',sl===0, sr, this.toBottom, this.toRight]);
  
curRBox.boxes[id] = t;
  
t.pos = false;
  if (!
options.noshow) {
    
t.show(false, options.nofocus);
  } else {
    
setStyle(t.wrap, {visibility: 'hidden', display: 'block'});
    
t._update_pos();
    
setStyle(t.wrap, {visibility: '', display: ''});
  }
};
extend(RBox.prototype, {
  
show: function (ts, nofocus) {
    var 
t = this;
    if (
ts === undefined) ts = 0;
    if (
ts) {
      
setStyle(t.wrap, {opacity: 0, display: 'block'});
      
t.visible = true;
      !
nofocus && t.focus();
      
animate(t.wrap, {opacity: 1}, ts, function () {
        
setStyle(t.wrap, browser.msie ? {filter: 'none'} : {opacity: ''});
        
t._update_pos();
      });
    } else {
      
show(t.wrap);
      
t.visible = true;
      !
nofocus && t.focus();
      
t._update_pos();
    }
  },
  
hide: function (ts, nofire) {
    var 
t = this;
    if (!
nofire && t.options.onBeforeHide && t.options.onBeforeHide()) {
      return 
true;
    }
    if (
ts === undefined) ts = 0;
    if (
ts) {
      
setStyle(t.wrap, {opacity: 1, display: 'block'});
      
animate(t.wrap, {opacity: 0}, ts, function () {
        
hide(t.wrap);
        
setStyle(t.wrap, browser.msie ? {filter: 'none'} : {opacity: ''});
      });
    } else {
      
hide(t.wrap);
    }
    
t.visible = false;
    if (!
nofire && t.options.onHide) t.options.onHide();
  },
  
_head_mdown: function (e) {
    if (
checkEvent(e)) return;
    (
e.originalEvent || e).cancelBubble = true;

    var 
t = this, handler = e.target,
        
wndInner = getWndInner(),
        
focused = curRBox.active == t.id,
        
startY = e.pageY,
        
startX = e.pageX,
        
wrapH = t.wrap.offsetHeight,
        
wrapW = t.wrap.offsetWidth,
        
startTop, startLeft, lastTop = 0, lastLeft = 0,
        
maxTop = wndInner[0] - wrapH,
        
maxLeft = wndInner[1] - wrapW,
        
selectEvent = browser.msie ? 'selectstart' : 'mousedown';

    if (!
focused) {
      
t.focus();
    }

    if (
t.toBottom) {
      
t.toBottom = false;
      
startTop = wndInner[0] - intval(getStyle(t.wrap, 'bottom')) - wrapH;
      
setStyle(t.wrap, {top: startTop, bottom: 'auto'});
    } else 
startTop = intval(getStyle(t.wrap, 'top'));
    if (
t.toRight) {
      
t.toRight = false;
      
// console.log(['to r',startLeft, getStyle(t.wrap, 'right'), intval(getStyle(t.wrap, 'right')), wrapW]);
      
startLeft = wndInner[1] - intval(getStyle(t.wrap, 'right')) - wrapW;
      
setStyle(t.wrap, {left: startLeft, right: 'auto'});
    } else 
startLeft = intval(getStyle(t.wrap, 'left'));

    
lastTop = startTop;
    
lastLeft = startLeft;

    
cur._fcdrag = 1;
    var 
_temp = function (e) {
      
lastTop = Math.max(0, Math.min(maxTop, startTop + e.pageY - startY));
      if (
maxTop - lastTop < 10) lastTop = maxTop;
      else if (
lastTop < 10) lastTop = 0;
      
t.wrap.style.top = lastTop + 'px';

      
lastLeft = Math.max(0, Math.min(maxLeft, startLeft + e.pageX - startX));
      if (
maxLeft - lastLeft < 10) lastLeft = maxLeft;
      else if (
lastLeft < 10) lastLeft = 0;
      
t.wrap.style.left = lastLeft + 'px';
      return 
cancelEvent(e);
    }, 
_temp2 = function (e) {
      
cur._fcdrag = 0;
      
removeEvent(document, 'mousemove', _temp);
      
removeEvent(document, 'mouseup', _temp2);
      
removeEvent(document, selectEvent, cancelEvent);
      
setStyle(bodyNode, 'cursor', '');
      
setStyle(handler, 'cursor', '');
      if (
t.toBottom = (lastTop >= maxTop - 5)) {
        
setStyle(t.wrap, {top: 'auto', bottom: 0});
      }
      if (
t.toRight = (lastLeft >= maxLeft - 5)) {
        
setStyle(t.wrap, {left: 'auto', right: 0, marginRight: lastWndScroll[0] ? sbWidth() + 1 : 0});
      }
      
// debugLog('mup', t.toBottom, lastTop, maxTop);
      
t._update_pos();
      var 
dlittle = Math.abs(e.pageY - startY) < 3 && Math.abs(e.pageX - startX) < 3;
      if (
cur._fcpromo > 0) {
        
cur._fcpromo = dlittle ? 0 : -1;
      } else if (
t.options.minimizer && dlittle) {
        if (!
t.minimized && focused) {
          
t.minimize(true);
        } else if (
t.minimized) {
          
t.unminimize(true);
        }
      } else {
        
t.options.onDragEnd && t.options.onDragEnd(t.toBottom ? -1 : lastTop / wndInner[0], t.toRight ? -1 : lastLeft / wndInner[1]);
      }
    };

    
addEvent(document, 'mousemove', _temp);
    
addEvent(document, 'mouseup', _temp2);
    
addEvent(document, selectEvent, cancelEvent);
    
setStyle(bodyNode, 'cursor', 'move');
    
setStyle(handler, 'cursor', 'move');
    return 
false;
  },
  
_resize_mdown: function (e) {
    if (
checkEvent(e)) return;
    
this.focus();

    var 
t = this, handler = e.target,
        
wndInner = getWndInner(),
        
startY = e.pageY,
        
startX = e.pageX,
        
wrapH = t.wrap.offsetHeight,
        
wrapW = t.wrap.offsetWidth,
        
startTop, startLeft, lastH = 0, lastW = 0,
        
startH = t.resizeableH.clientHeight - intval(getStyle(t.resizeableH, 'paddingBottom')) - intval(getStyle(t.resizeableH, 'paddingTop')),
        
startW = t.resizeableW.clientWidth - intval(getStyle(t.resizeableW, 'paddingRight')) - intval(getStyle(t.resizeableW, 'paddingLeft')),
        
selectEvent = browser.msie ? 'selectstart' : 'mousedown',
        
onresize = !browser.msie && t.options.onResize || false;
        
// debugLog(startH, startW);

    
if (t.toBottom) {
      
t.toBottom = false;
      
startTop = wndInner[0] - intval(getStyle(t.wrap, 'bottom')) - wrapH;
      
setStyle(t.wrap, {top: startTop, bottom: 'auto'});
      
// debugLog('rst', wndInner[0], intval(getStyle(t.wrap, 'bottom')), wrapH, startTop);
    
} else startTop = intval(getStyle(t.wrap, 'top'));
    if (
t.toRight) {
      
t.toRight = false;
      
startLeft = wndInner[1] - intval(getStyle(t.wrap, 'right')) - wrapW;
      
setStyle(t.wrap, {left: startLeft, right: 'auto'});
      
// debugLog('to right -> left', startLeft, 'wnd', wndInner[1]);
    
} else startLeft = intval(getStyle(t.wrap, 'left'));

    
t.options.onResizeStart && t.options.onResizeStart(startH, startW);

    var 
maxH = startH + wndInner[0] - startTop - wrapH,
        
maxW = startW + wndInner[1] - startLeft - wrapW;
    
// debugLog(maxH, startH, wndInner[0], startTop, wrapH);
    // debugLog('wndW', wndInner[1], 'maxW', maxW, '');

    
var _temp = function (e) {
      
lastH = Math.max(t.options.minH, Math.min(maxH, startH + e.pageY - startY));
      
// debugLog(maxH, lastH, maxH - lastH);
      
if (maxH - lastH < 10) lastH = maxH;
      
t.resizeableH.style.height = lastH + 'px';
      
// debugLog(lastH + 'px');

      
lastW = Math.max(t.options.minW, Math.min(maxW, startW + e.pageX - startX));
      if (
maxW - lastW < 10) lastW = maxW;
      
t.resizeableW.style.width = lastW + 'px';
      
onresize && onresize(lastH, lastW)

      return 
cancelEvent(e);
    }, 
_temp2 = function (e) {
      
removeEvent(document, 'mousemove', _temp);
      
removeEvent(document, 'mouseup', _temp2);
      
removeEvent(document, selectEvent, cancelEvent);
      
setStyle(bodyNode, 'cursor', '');
      
setStyle(handler, 'cursor', '');
      if (
t.toBottom = (lastH == maxH)) {
        
setStyle(t.wrap, {top: 'auto', bottom: 0});
      }
      if (
t.toRight = (lastW == maxW)) {
        
setStyle(t.wrap, {left: 'auto', right: 0, marginRight: lastWndScroll[0] ? sbWidth() + 1 : 0});
      }
      
t._update_pos();
      
t.options.onResizeEnd && t.options.onResizeEnd(lastH, lastW, wndInner[0], wndInner[1], t.toBottom, t.toRight);
    };

    
addEvent(document, 'mousemove', _temp);
    
addEvent(document, 'mouseup', _temp2);
    
addEvent(document, selectEvent, cancelEvent);
    
setStyle(bodyNode, 'cursor', 'move');
    
setStyle(handler, 'cursor', 'move');
    return 
false;
  },
  
_update_pos: function() {
    var 
t = this, wrap = t.wrap
    t
.pos = [t.wrap.offsetTop, t.wrap.offsetLeft, t.wrap.offsetHeight, t.wrap.offsetWidth];
  },
  
_wnd_resize: function (wndH, wndW, check) {
    
// debugLog('---- resize -----');
    
var t = this;
    if (
t.toBottom) {
      
t.pos[0] = t.wrap.offsetTop;
    }
    if (
t.toRight) {
      
t.pos[1] = t.wrap.offsetLeft;
    }
    var 
s = {}, sh = false, sw = false,
        
needH = t.pos[0] + t.pos[2] - wndH,
        
diffT = t.pos[0],
        
// diffH = t.options.resize !== false ? t.resizeableH.clientHeight - t.options.minH : 0,
        
diffH = t.resizeableH.clientHeight - t.options.minH,
        
needW = t.pos[1] + t.pos[3] - wndW,
        
diffL = t.pos[1],
        
diffW = t.options.resize !== false ? t.resizeableW.clientWidth - t.options.minW : 0;

    if (
check) {
      if (
diffW < 0) {
        
setStyle(t.resizeableW, t.options.minW);
      }
      if (
diffH < 0) {
        
setStyle(t.resizeableH, t.options.minH);
      }
    }

    if ((
needH <= 0 || diffT <= 0  && diffH <= 0) &&
        (
needW <= 0 || diffL <= 0  && diffW <= 0)) return;

    
// debugLog('needH', needH, 'diffT', diffT, 'diffH', diffH);
    // debugLog('needW', needW, 'diffL', diffL, 'diffW', diffW);

    
if (needH > 0 && diffT > 0) {
      
diffT = Math.min(needH, diffT);
      
needH -= diffT;
      
s.top = t.pos[0] - diffT;
      
s.bottom = '';
      
// debugLog('moving top', t.pos[0], 'by', diffT, 'now need', needH);
    
}
    if (
needH > 0 && diffH > 0) {
      
diffH = Math.min(needH, diffH);
      
sh = t.resizeableH.clientHeight - diffH;
      
// debugLog('change height', t.resizeableH.clientHeight, 'by', diffH);
    
}
    if (
needW > 0 && diffL > 0) {
      
diffL = Math.min(needW, diffL);
      
needW -= diffL;
      
s.left = t.pos[1] - diffL;
      
s.right = '';
      
// debugLog('moving left', t.pos[1], 'by', diffL, 'now need', needW);
    
}
    if (
needW > 0 && diffW > 0) {
      
diffW = Math.min(needW, diffW);
      
sw = t.resizeableW.clientWidth - diffW;
      
// debugLog('change width', t.resizeableW.clientWidth, 'by', diffW);
    
}
    if (
sw !== false) {
      
setStyle(t.resizeableW, 'width', sw);
    }
    if (
sh !== false) {
      
setStyle(t.resizeableH, 'height', sh);
    }
    
setStyle(t.wrap, s);
    
t._update_pos();
    
t.options.onResize && t.options.onResize(t.resizeableH.clientHeight, t.resizeableW.clientWidth);
  },
  
_cont_mdown: function (e) {
    var 
stop = (curRBox.active != this.id);
    if (
stop) {
      
this.focus();
      return 
cancelEvent(e);
    }
  },

  
_focus: function () {
    var 
t = this, pos = indexOf(curRBox.focused, t.id), prev = curRBox.active, prevBox = prev && curRBox.boxes[prev];
    if (
prev == t.id) {
      return;
    }
    if (
prevBox && isFunction(prevBox.options.onBlur)) {
      
prevBox.options.onBlur();
    }
    if (
pos != -1) {
      
curRBox.focused.splice(pos, 1);
    }
    
curRBox.focused.unshift(t.id);

    var 
zIndex = 700 + curRBox.focused.length, first = true;
    
each(curRBox.focused, function (k, id) {
      var 
wrap = curRBox.boxes[id].wrap;
      if (
first) {
        
addClass(wrap, 'rb_active');
        
removeClass(wrap, 'rb_inactive');
        
curRBox.active = id;
        
first = false;
      } else {
        
removeClass(wrap, 'rb_active');
        
addClass(wrap, 'rb_inactive');
      }
      
setStyle(wrap, 'zIndex', zIndex);
      
zIndex--;
    });
  },
  
_hide_click: function () {
    
this.hide();
  },
  
minimize: function (fire) {
    var 
t = this, wrap = t.wrap;
    
addClass(wrap, 'rb_minimized');
    
t.minimized = true;
    
t._update_pos();
    if (
fire && t.options.onMinimize) {
      
t.options.onMinimize(0);
    }
  },
  
unminimize: function (fire) {
    var 
t = this, wrap = t.wrap, wndInner = getWndInner();
    
removeClass(wrap, 'rb_minimized');
    
t.minimized = false;
    
t._update_pos();
    
t._wnd_resize(wndInner[0], wndInner[1], true);
    
curRBox.active = false;
    
t.focus();
    if (
fire && t.options.onMinimize) {
      
t.options.onMinimize(1);
    }
  },
  
_min_toggle: function (e) {
    var 
t = this;
    
setTimeout(function () {
      if (!
t.minimized) {
        
t.minimize(true);
      } else {
        
t.unminimize(true);
      }
    }, 
50);
  },
  
destroy: function () {
    var 
t = this,
        
pos = indexOf(curRBox.focused, t.id);
    if (
pos != -1) {
      
curRBox.focused.splice(pos, 1);
    }
    
cleanElems(t.wrap, t.resizeWrap, t.content, t.options.movable, t.options.closer, t.options.hider);
    
re(t.wrap);
    
delete curRBox.boxes[t.id];
    
delete t;
  },
  
_close_mdown: function (e) {
    (
e.originalEvent || e).cancelBubble = true;
  },
  
_close_click: function (e) {
    
this.close();
  },
  
_close: function () {
    var 
t = this;
    
this.destroy();
    if (
curRBox.focused[0]) {
      
curRBox.boxes[curRBox.focused[0]].focus();
    }
  },
  
focus: function () {
    var 
t = this, cb = (curRBox.active != t.id) || true;
    
t._focus();
    if (
cb && isFunction(t.options.onFocus)) {
      
t.options.onFocus();
    }
    return 
cb;
  },
  
close: function () {
    var 
t = this, pos = t.pos;
    
t._close();
    if (
isFunction(t.options.onClose)) {
      
t.options.onClose(pos);
    }
  }
});

if (!
window.curFastChat) {
  
curFastChat = {};
}
FastChat = {
  
init: function (options) {
    
//debugLog('fastchat start');
    
extend(curFastChat, {
      
tabs: {},
      
needPeers: {},
      
gotPeers: {},
      
needMedia: {},
      
gotMedia: {},
      
myTypingEvents: {},
      
typingEvents: {},
      
inited: true,
      
options: options,
      
posSeq: 0,
      
error_timeout: 1
    
});
    
delete curFastChat.standby;
    
delete curFastChat.standbyTO;
    
Notifier.addRecvClbk('fastchat', 0, FastChat.lcRecv, true);
    
FastChat.lcSend('needSettings', {version: options.version, lang_id: langConfig.id});
    
clearTimeout(curFastChat.getSettingsTO);
    
curFastChat.getSettingsTO = setTimeout(FastChat.getSettings, 300);
  },
  
getSettings: function () {
    var 
friends = ls.get('fcFriends' + vk.id);
    
ajax.post('al_im.php', {
      
act: 'a_get_fast_chat',
      
friends: friends && friends.version
    
}, {
      
onDone: function (data) {
        if (
data.friends == -1) {
          
data.friends_version = friends.version;
          
data.friends = friends.list;
        } else {
          
ls.set('fcFriends' + vk.id, {version: data.friends_version, list: data.friends});
        }
        
FastChat.gotSettings(data);
        
FastChat.sendSettings();
      },
      
onFail: function () {
        return 
true;
      }
    });
  },
  
gotSettings: function (data) {
    
clearTimeout(curFastChat.getSettingsTO);
    
window.lang = extend(window.lang || {}, data.lang);
    
extend(curFastChat, data, {lang_id: langConfig.id});
    if (
curNotifier.is_server) {
      if (!
data.im_queue) {
        
clearTimeout(curFastChat.lp_error_to);
        
curFastChat.lp_error_to = setTimeout(FastChat.updateQueueKeys.bind(FastChat), (curNotifier.error_timeout || 1) * 1000);
      } else if (!
curFastChat.lpInited) {
        
FastChat.initLp();
      }
    }
    
curFastChat.friendsCnt = 0;
    for (var 
i in (curFastChat.friends || {})) {
      
curFastChat.friendsCnt++;
    }
    
setTimeout(FastChat.clistCache.pbind(false), 10);
    
FastChat.initUI();
  },
  
sendSettings: function () {
    
clearTimeout(curFastChat.sendSettingsTO);
    var 
settings = {}, k = ['friends', 'friends_version', 'onlines', 'tpl', 'lang', 'me', 'version', 'im_queue', 'cl_queue'], i;
    for (
i in k) {
      if (
k[i] != 'cl_queue' && curFastChat[k[i]] === undefined) {
        return;
      }
      
settings[k[i]] = curFastChat[k[i]];
    }
    
clearTimeout(curFastChat.sendSettingsTO);
    
curFastChat.sendSettingsTO = setTimeout(function () {
      
FastChat.lcSend('settings', {settings: settings})
    }, 
curNotifier.is_server ? 0 : irand(50, 100));
  },
  
becameServer: function () {
    if (
curFastChat.lpInited || !curFastChat.version) {
      return;
    }
    
delete curNotifier.addQueues['fastchat' + vk.id];
    
delete curNotifier.addQueues['contacts' + vk.id];
    if (!
curFastChat.im_queue) {
      
clearTimeout(curFastChat.lp_error_to);
      
curFastChat.lp_error_to = setTimeout(FastChat.updateQueueKeys.bind(FastChat), (curNotifier.error_timeout || 1) * 1000);
    } else if (!
curFastChat.lpInited) {
      
FastChat.initLp();
    }
  },
  
destroy: function () {
    if (!
curFastChat.inited) {
      return 
false;
    }
    var 
topLink;
    
FastChat.stopLp();
    
each(curFastChat.tabs || {}, function (peer, tab) {
      
tab.box.destroy();
    });
    
curFastChat.clistBox && curFastChat.clistBox.destroy();
    
each (curFastChat.el || {}, function () {
      
cleanElems(this);
    });
    
clearInterval(curFastChat.updateFriendsInt);
    
clearInterval(curFastChat.updateTypingsInt);
    
clearTimeout(curFastChat.correspondentsTO);
    
clearTimeout(curFastChat.lp_error_to);
    
curFastChat = {inited: false};
    return 
true;
  },
  
standby: function (version) {
    
FastChat.destroy();
    
curFastChat.standby = true;
    var 
to = 1, cb = function () {
      if (!
curNotifier.is_server) {
        
clearTimeout(curFastChat.standbyTO);
        
curFastChat.standbyTO = setTimeout(cb, to * 1000);
        return;
      }
      
ajax.post('notifier.php?act=a_get_reload', {version: version}, {
        
onDone: function (navVersion, config) {
          
FastChat.lcSend('gotConfig', {navVersion: navVersion, config: config});
          
FastChat.gotConfig(navVersion, config);
        },
        
onFail: function () {
          
to *= 2;
          
clearTimeout(curFastChat.standbyTO);
          
curFastChat.standbyTO = setTimeout(cb, to * 1000);
          return 
true;
        }
      });
    };
    
cb();
  },
  
gotConfig: function (navVersion, config) {
    
clearTimeout(curFastChat.standbyTO);
    if (!
curFastChat.standby) {
      return;
    }
    
setTimeout(function () {
      if (
navVersion > stVersions['nav']) {
        
debugLog('appending al loader');
        
headNode.appendChild(ce('script', {
          
type: 'text/javascript',
          
src: '/js/loader_nav' + navVersion + '_' + vk.lang + '.js'
        
}));
      }
      
setTimeout(function() {
        if (
navVersion <= stVersions['nav']) {
          
stManager.add(['notifier.js', 'notifier.css'], function () {
            
FastChat.init(config);
          })
          return;
        }
        
setTimeout(arguments.callee, 100);
      }, 
0);
    }, 
curNotifier.is_server ? 0 : irand(1000, 2000));
  },
  
updateVersion: function (version) {
    
FastChat.lcSend('standby', {version: version});
    
FastChat.standby(version);
  },

  
// Local connection: communication between tabs in one browser instanse
  
lcSend: function (act, data) {
    
// debugLog('fc::lcSend', act, clone(data));
    
Notifier.lcSend('fastchat', extend({act: act, __id: curFastChat.me && curFastChat.me.id || vk.id}, data));
  },
  
lcRecv: function (data) {
    if (
isEmpty(data)) return;
    var 
act = data.act;
    if (
data.__id != (curFastChat.me && curFastChat.me.id || vk.id)) {
      
debugLog('drop foreign event', data);
      return;
    }
    
delete data.act;
    
delete data.__id;
    
FastChat.lcFeed(act, data);
  },
  
lcFeed: function (act, data) {
    
// debugLog('fc::lcFeed', act, data);
    
switch (act) {
      case 
'needSettings':
        if (
curFastChat.version < data.version) {
          
// May be update version here
        
} else if (data.lang_id == curFastChat.lang_id) {
          
FastChat.sendSettings();
        }
        break;

      case 
'settings':
        if (!
curFastChat.version && curFastChat.options && data.settings.version == curFastChat.options.version) {
          
FastChat.gotSettings(data.settings);
        }
        
clearTimeout(curFastChat.sendSettingsTO);
        break;

      case 
'standby':
        if (!
curFastChat.version) break;
        
FastChat.standby(data.version);
        break;

      case 
'gotConfig':
        
FastChat.gotConfig(data.navVersion, data.config);
        break;

      case 
'clFeed':
        if (!
curFastChat.version) break;
        
FastChat.clFeed(data.events);
        break;

      case 
'clistOnlines':
        if (!
curFastChat.version) break;
        
FastChat.clistGotOnlines(data);
        break;

      case 
'imFeeds':
        if (!
curFastChat.version) break;
        
FastChat.imFeeds(data);
        break;

      case 
'needPeer':
        if (!
curFastChat.version) break;
        var 
peer = data.id, tab = curFastChat.tabs[peer], i, peerData = false, mem;
        if (
tab !== undefined) {
          
peerData = {
            
name: tab.name,
            
photo: tab.photo,
            
fname: tab.fname,
            
hash: tab.hash,
            
sex: tab.sex,
            
data: tab.data,
            
online: tab.online
          
};
          for (
i in tab.msgs) {
            
peerData.history = [tab.log.innerHTML, tab.msgs];
            break;
          }
        } else if (
mem = curFastChat.friends[peer + '_']) {
          
peerData = {name: mem[0], photo: mem[1], fname: mem[2], hash: mem[3], data: mem[4], online: curFastChat.onlines[peer]};
        }
        
// debugLog(data.id, peer, tab, mem, peerData);
        
if (peerData === false) {
          break;
        }
        
curFastChat.gotPeers[peer] = setTimeout(function () {
          var 
response = {};
          
response[peer] = peerData;
          
FastChat.lcSend('gotPeers', response);
        }, 
curNotifier.is_server ? 0 : irand(50, 100));

        break;

      case 
'fetchingPeers':
        if (!
curFastChat.version) break;
        
each (data, function (peer, flags) {
          var 
needPeer = curFastChat.needPeers[peer];
          if (
needPeer && (flags & needPeer[0]) == needPeer[0]) {
            
clearTimeout(needPeer[2]);
          }
        });
        break;

      case 
'gotPeers':
        if (!
curFastChat.version) break;
        
FastChat.gotPeers(data);
        break;

      case 
'stateChange':
        if (!
curFastChat.version) break;
        
FastChat.onStateChanged(data);
        break;

      case 
'queueSet':
        
// debugLog('q set', data);
        
extend(curFastChat, data);
        break;

      case 
'queueClean':
        if (!
curNotifier.is_server) {
          
// debugLog('q clean');
          
delete curFastChat.im_queue;
          
delete curFastChat.cl_queue;
        }
        break;

      case 
'needMedia':
        var 
msgId = data.msgId, msgMedia = curFastChat.gotMedia[msgId];
        if (
msgMedia === undefined || msgMedia === 0) {
          break;
        }
        
curFastChat.gotMedia[msgId][2] = setTimeout(function () {
          
FastChat.lcSend('gotMedia', {msgId: msgId, peer: msgMedia[0], text: msgMedia[1]});
        }, 
curNotifier.is_server ? 0 : irand(50, 100));
        break;

      case 
'fetchingMedia':
        
// if (!curFastChat.version) break;
        
var msgId = data.msgId, msgNeed = curFastChat.needMedia[msgId];
        if (
msgNeed === undefined || curFastChat.gotMedia[msgId] === 0) {
          break;
        }
        
clearTimeout(msgNeed[1]);
        
msgNeed[1] = setTimeout(FastChat.loadMsgMedia.pbind(msgNeed[0], msgId), 1000);
        break;

      case 
'gotMedia':
        var 
msgId = data.msgId, msgMedia = curFastChat.gotMedia[msgId];
        if (
isArray(msgMedia)) {
          
clearTimeout(msgMedia[2]);
        }
        
FastChat.gotMsgMedia(data.peer, msgId, data.text);
        break;
    }
  },

  
// Long poll
  
initLp: function () {
    
curFastChat.lpInited = true;
    
FastChat.checkLp();
    
curFastChat.checkLpInt = setInterval(FastChat.checkLp, 20000);
  },
  
stopLp: function () {
    
curFastChat.lpInited = false;
    
clearInterval(curFastChat.checkLpInt);
    
delete curFastChat.im_queue;
    
delete curFastChat.cl_queue;
  },
  
checkLp: function () {
    if (!
curNotifier.is_server || !curFastChat.im_queue/* || !curFastChat.cl_queue*/) {
      return;
    }
    
Notifier.addKey({
      
queue: curFastChat.im_queue.id,
      
key: curFastChat.im_queue.key,
      
ts: curFastChat.im_queue.ts
    
}, FastChat.imChecked, true);

    if (
curFastChat.cl_queue) {
      
Notifier.addKey({
        
queue: curFastChat.cl_queue.id,
        
key: curFastChat.cl_queue.key,
        
ts: curFastChat.cl_queue.ts
      
}, FastChat.clChecked, true);
    }
    
FastChat.lcSend('queueSet', {
      
im_queue: curFastChat.im_queue,
      
cl_queue: curFastChat.cl_queue
    
});
  },
  
updateQueueKeys: function () {
    if (
curFastChat.updatingQueues) {
      return;
    }
    
curFastChat.updatingQueues = 1;
    
FastChat.lcSend('queueClean');
    
FastChat.stopLp();
    
ajax.post('al_im.php', {act: 'a_get_fc_queue'}, {
      
onDone: function (data) {
        if (
data.version > curFastChat.version) {
          
FastChat.updateVersion(data.version);
          return;
        }
        
delete curFastChat.updatingQueues;
        
extend(curFastChat, data);
        
FastChat.lcSend('queueSet', data);
        if (
curNotifier.is_server) {
          
FastChat.initLp();
          
FastChat.clistUpdate();
        }
      },
      
onFail: function () {
        
delete curFastChat.updatingQueues;
        
FastChat.destroy();
        return 
true;
      }
    });
  },

  
// Checked function (recv long-poll response)
  
clChecked: function (queue, response) {
    if (!
curFastChat.inited || !curFastChat.ready || !curFastChat.cl_queue) return;
    if (
response.failed) {
      
clearTimeout(curFastChat.lp_error_to);
      
curFastChat.lp_error_to = setTimeout(FastChat.updateQueueKeys.bind(FastChat), (curNotifier.error_timeout || 1) * 1000);
      return;
    }
    if (
response.ts) {
      if (
response.key) {
        
curFastChat.cl_queue.key = response.key;
      }
      
curFastChat.cl_queue.ts = response.ts;
      
FastChat.lcSend('queueSet', {cl_queue: curFastChat.cl_queue});
    }
    if (!
isArray(response.events) || !response.events.length) {
      return;
    }
    
FastChat.clFeed(response.events);
    
FastChat.lcSend('clFeed', {events: response.events});
  },
  
clFeed: function (events) {
    if (!
curFastChat.inited || !curFastChat.ready || !curFastChat.tabs) return;
    var 
clistUpdated = false, failed = false;
    
each (events, function () {
      var 
ev = this.split('<!>'), evVer = ev[0], evType = ev[1], peer = ev[2], onltype = ev[3] ? ev[3] : 1, tab = curFastChat.tabs[peer], wasOnline = curFastChat.onlines[peer];
      if (
evVer != curFastChat.version) {
        
FastChat.updateVersion(evVer);
        
failed = true;
        return 
false;
      }
      if (!
curFastChat.friends[peer + '_'] && !tab) {
        return;
      }

      switch (
evType) {
        case 
'online':
          if (
wasOnline == onltype) break;
          
curFastChat.onlines[peer] = onltype;
          
FastChat.tabNotify(peer, 'online', onltype);
          
clistUpdated = true;
          break;

        case 
'offline':
          if (!
wasOnline) break;
          
delete curFastChat.onlines[peer];
          if (
re('fc_contact' + peer) && curFastChat.clistBox.visible) {
            
FastChat.clistShowMore();
          }
          
FastChat.tabNotify(peer, 'offline');
          break;
      }
    });
    if (
failed) {
      return;
    }
    if (
clistUpdated &&
        
curFastChat.clistBox.visible &&
        
curNotifier.idle_manager && !curNotifier.idle_manager.is_idle &&
        (
curFastChat.el.clist.scrollTop < 100 || curRBox.active != curFastChat.clistBox.id)) {
      
FastChat.clistRender(); // Title is also updated here
    
} else {
      
FastChat.clistUpdateTitle();
    }
  },
  
imChecked: function (queue, response) {
    if (!
curFastChat.inited || !curFastChat.ready || !curFastChat.im_queue) return;
    if (
response.failed) {
      
clearTimeout(curFastChat.lp_error_to);
      
curFastChat.lp_error_to = setTimeout(FastChat.updateQueueKeys.bind(FastChat), (curNotifier.error_timeout || 1) * 1000);
      return;
    }
    if (
response.ts && curFastChat.im_queue) {
      if (
response.key) {
        
curFastChat.im_queue.key = response.key;
      }
      
curFastChat.im_queue.ts = response.ts;
      
FastChat.lcSend('queueSet', {im_queue: curFastChat.im_queue});
    }
    if (!
isArray(response.events) || !response.events.length) {
      return;
    }
    var 
feeds = {}, failed = false;
    
each (response.events, function () {
      var 
ev = this.split('<!>'),
          
evVer = ev[0],
          
evType = ev[1],
          
peer = ev[2],
          
flags = 0,
          
tab = curFastChat.tabs[peer];

      if (
evVer != curFastChat.version) {
        
FastChat.updateVersion(evVer);
        
failed = true;
        return 
false;
      }

      switch (
evType) {
        case 
'read':
          break;

        case 
'typing':
          
flags = 1;
          break;

        case 
'new':
          
flags = (ev[4] & 2) ? 0 : 2;
          break;

        default: return;
      }

      if (!
feeds[peer]) {
        
feeds[peer] = [0];
      }
      
feeds[peer][0] |= flags;
      
feeds[peer].push(ev);
    });
    if (
failed || isEmpty(feeds)) {
      return;
    }
    
FastChat.lcSend('imFeeds', feeds);
    
FastChat.imFeeds(feeds);
  },
  
imFeeds: function (feeds) {
    if (!
curFastChat.inited || !curFastChat.ready) return;
    
// debugLog('imFeeds', feeds);
    
each (feeds, function (peer, events) {
      var 
flags = events.shift();
      if (
curFastChat.tabs[peer]) {
        
FastChat.imFeed(peer, events);
      } else if (
curFastChat.clistBox.visible && (flags & 2)) {
        
FastChat.addPeer(peer, events);
      }
    });
  },
  
imFeed: function (peer, events) {
    var 
tab = curFastChat.tabs[peer],
        
ts = vkNow();
    if (!
tab) return false;
    
each (events, function (k, ev) {
      switch (
ev[1]) {
        case 
'new':
          
each (tab.sentmsgs, function (k, msgId) {
            var 
row = ge('fc_msg' + msgId), parent = row && row.parentNode;
            if (
re(row) && parent && !parent.childNodes.length) {
              
re(parent.parentNode);
            }
          });
          if (!
ge('fc_msg' + ev[3])) {
            
stManager.add(['im.js'], function() {
              
ev[5] = IM.emojiToHTML(ev[5], true);
              
FastChat.addMsg(FastChat.prepareMsgData(ev.slice(2)));
            });
            
tab.msgs[ev[3]] = [ev[4] & 2 ? 1 : 0, ev[4] & 1];
            if ((
ev[4] & 3) === 1) tab.unread++;
            
FastChat.scroll(peer);
          }
          
FastChat.blinkTab(peer);
          break;

        case 
'read':
          
each(ev[3].split(','), function (k, msgId) {
            var 
row = ge('fc_msg' + msgId), parent = row && row.parentNode;
            
// debugLog('feed read', row, clone(tab.msgs[msgId]), parent, hasClass(parent.parentNode, 'fc_msgs_unread'));
            
if (!row) return;
            if (
tab.msgs[msgId] && tab.msgs[msgId][1]) {
              
tab.msgs[msgId][1] = 0;
              if (!
tab.msgs[msgId][0]) {
                
tab.unread--;
              }
            }
            
removeClass(row, 'fc_msg_unread');
            if (
hasClass(parent.parentNode, 'fc_msgs_unread')) {
              
each (parent.childNodes, function () {
                if (!
hasClass(this, 'fc_msg_unread')) {
                  
removeClass(parent.parentNode, 'fc_msgs_unread');
                  return 
false;
                }
              });
            }
          });
          break;

        case 
'typing':
          if (
peer > 2e9) {
            if (!
curFastChat.typingEvents[peer]) {
              
curFastChat.typingEvents[peer] = {};
            }
            
curFastChat.typingEvents[peer][ev[3]] = ts;
          } else {
            
curFastChat.typingEvents[peer] = ts;
          }
          
FastChat.updateTyping(peer);
          break;
      }
    });
    if (
tab.unread > 0) {
      
tab.unread = 0;
      
each (tab.msgs, function () {
        if (!
this[0] && this[1]) tab.unread++;
      });
    }
    if (
tab.auto && !tab.unread) {
      
tab.box._close();
      
delete curFastChat.tabs[peer];
    }
    
FastChat.updateUnreadTab(peer);
  },
  
tabNotify: function(peer, evType, evData) {
    var 
tab = curFastChat.tabs[peer];
    if (
peer > 0 && peer < 2e9 && isFunction(cur.onPeerStatusChanged)) {
      
cur.onPeerStatusChanged(peer, evType, evData);
    }
    if (
peer <= 0 || !tab || !tab.box || tab.box.minimized) return;
    var 
addClassTo = geByClass1('fc_tab', tab.wrap, 'div'), mob = (evData != 1), cls = mob ? 'fc_tab_mobile' : 'fc_tab_online';
    if (
evType == 'online') {
      
addClassTo.className = addClassTo.className.replace(mob ? 'fc_tab_online' : 'fc_tab_mobile', cls);
      if (
hasClass(addClassTo, cls)) return;
    }

    
clearTimeout(tab.hideNotifyTO);
    switch (
evType) {
      case 
'online':
        
text = langSex(tab.sex, lang.mail_im_user_became_online);
        
FastChat.blinkTab(peer);
        
addClass(addClassTo, cls);
        break;

      case 
'offline':
        
text = langSex(tab.sex, lang.mail_im_user_became_offline);
        
FastChat.blinkTab(peer);
        
removeClass(addClassTo, 'fc_tab_online');
        
removeClass(addClassTo, 'fc_tab_mobile');
        break;

      case 
'unavail':
        
text = langSex(tab.sex, lang.mail_im_user_unavail);
        break;
    }
    
text = text.replace('{user}', tab.fname);
    
val(tab.notify, '<div class="fc_tab_notify fc_tab_notify_' + evType + '">' + text + '</div>');
    
// debugLog('<div class="fc_tab_notify fc_tab_notify_' + evType + '">' + text + '</div>');
    
var notify = tab.notify.firstChild;
    
setStyle(notify, {width: tab.logWrap.clientWidth - 8/*, zIndex: 400*/});
    
// debugLog(notify, tab.log.clientWidth - 8);
    
clearTimeout(tab.hideNotifyTO);
    
tab.hideNotifyTO = setTimeout(function () {
      
fadeOut(notify, 200, function () {
        
val(tab.notify, '');
      });
    }, 
5000);
    
// debugLog('tab notify', peer, evType);
  
},

  
initUI: function () {
    var 
el = curFastChat.el = {},
        
wndInner = getWndInner();
    
re('rb_box_fc_clist');
    
el.clistWrap = se(curFastChat.tpl.clist);
    
el.clist = geByClass1('fc_contacts', el.clistWrap, 'div');
    
el.clistTitle = geByClass1('fc_tab_title', el.clistWrap, 'div');
    
el.clistOnline = geByClass1('fc_clist_online', el.clistWrap, 'div');

    var 
state = curFastChat.options.state || false,
        
clistMin = !curFastChat.friendsCnt || (!(state && state.clist.min !== undefined) ? wndInner[1] < 1200 || curFastChat.friendsCnt < 5 : state.clist.min),
        
opts = {
      
id: 'fc_clist',
      
movable: geByClass1('fc_tab_head', el.clistWrap),
      
hider: geByClass1('fc_tab_close_wrap', el.clistWrap, 'a'),
      
startHeight: Math.max(300, Math.min(2000, wndInner[0] * 0.5)),
      
startWidth: 220,
      
resizeableH: el.clist,
      
resize: false,
      
minH: 200,
      
onHide: function () {
        
FastChat.stateChange({op: 'clist_toggled', val: 0});
        
show(el.topLink)
      },
      
onDragEnd: function (y, x) {
        
FastChat.stateChange({op: 'clist_moved', y: y, x: x});
      },
      
onResize: function (h, w) {
        
curFastChat.clistBoxScroll && curFastChat.clistBoxScroll.update(false, true);
      }
    };
    if (
state) {
      if (
state.clist.x !== false) {
        if (
state.clist.x == -1) {
          
opts.startRight = 0;
        } else {
          
opts.startLeft = wndInner[1] * state.clist.x;
        }
      }
      if (
state.clist.y !== false) {
        if (
state.clist.y == -1) {
          
opts.startBottom = 0;
        } else {
          
opts.startTop = wndInner[0] * state.clist.y;
        }
      }
    }
    if (
clistMin) {
      
opts.noshow = true;
    }
    if (
opts.startTop === undefined && opts.startBottom === undefined) {
      
opts.startTop = wndInner[0] < 800 ? 0 : wndInner[0] * 0.10;
    }
    if (
opts.startLeft === undefined && opts.startRight === undefined) {
      
opts.startRight = 0;
    }
    
curFastChat.clistBox = new RBox(el.clistWrap, opts);
    if (!
opts.noshow && (opts.startLeft !== undefined || opts.startTop !== undefined)) {
      
curFastChat.clistBox._wnd_resize(wndInner[0], wndInner[1], true);
    }
    
// debugLog('cList done');

    // Top link
    // re('top_fc_link');
    // var topTable = ge('top_links'), topBackLink = geByClass1('top_back_link_td', topTable), topTd;
    // if (topBackLink) {
    //   topBackLink.parentNode.insertBefore(topTd = ce('td', {innerHTML: '<nobr><a class="top_nav_link" id="top_fc_link" href="/im?sel=0" onmousedown="event.cancelBubble = true;"></a></nobr>'}), topBackLink.nextSibling);
    //   el.topLink = ge('top_fc_link');
    //   addEvent(el.topLink, 'click', function (e) {
    //     if (checkEvent(e)) return;
    //     FastChat.clistShow();
    //     return false;
    //   });
    //   if (!clistMin) {
    //     hide(el.topLink);
    //   }
    // }

    // Friends list
    
curFastChat.clistBoxScroll = new Scrollbar(el.clist, {
      
prefix: 'fc_',
      
more: FastChat.clistShowMore,
      
nomargin: true,
      global: 
true,
      
nokeys: true,
      
right: vk.rtl ? 'auto' : 10,
      
left: !vk.rtl ? 'auto' : 10
    
});
    
curFastChat.updateFriendsInt = setInterval(FastChat.clistUpdate, 3 * 60000);
    
curFastChat.updateTypingsInt = setInterval(FastChat.updateTypings, 5000);

    
// Add tabs
    
if (state && state.tabs) {
      
each (state.tabs, function (peer) {
        
peer = intval(peer);
        var 
opts = {nofocus: 1};
        if (
this.min) {
          
opts.minimized = true;
        }
        if (
this.h) {
          
opts.startHeight = this.h * wndInner[0];
        }
        if (
this.w) {
          
opts.startWidth = this.w * wndInner[1];
        }
        if (
this.x !== undefined && this.x <= 1) {
          if (
this.x < 0) {
            
opts.startRight = 0;
          } else {
            
opts.startLeft = wndInner[1] * this.x;
          }
        }
        if (
this.y !== undefined && this.y <= 1) {
          if (
this.y < 0) {
            
opts.startBottom = 0;
          } else {
            
opts.startTop = wndInner[0] * this.y;
          }
        }
        
FastChat.addPeer(peer, false, false, opts);
      });
    }

    var 
filter = ge('fc_clist_filter');
    
placeholderSetup(filter, {global: true, back: 1});
    
curFastChat.q = '';
    
addEvent(filter, 'keyup ' + (browser.opera ? 'keypress' : 'keydown'), function (e) {
      var 
control = FastChat.clistFilterKey(e);
      if (
control !== undefined) {
        return 
control;
      }
      
curFastChat.q = trim(val(this));
      
FastChat.clistRender();
    });

    if (
el.clistOnline) {
      var 
lShift, probe;
      
bodyNode.appendChild(probe = ce('nobr', {className: 'fl_l', innerHTML: getLang('mail_im_clist_onlines')}, {visibility: 'hidden', position: 'absolute'}));
      
lShift = (probe.offsetWidth || 179) - 7;
      
re(probe);
      
addEvent(el.clistOnline, 'mouseover', function (e) {
        
showTooltip(this, {text: getLang('mail_im_clist_onlines'), forcetoup: 1, shift: [lShift, 7, 3], className: 'tt_fc_onlines', init: function () {
          if (
browser.msie) el.clistOnline.tt.isFixed = false;
        }});
      });
      
addEvent(el.clistOnline, 'click', function (e) {
        (
e.originalEvent || e).cancelBubble = true;
        
FastChat.clistToggleOnlines();
        
FastChat.clistRender();
      });
      if (
state && state.clist && state.clist.onlines) {
        
FastChat.clistToggleOnlines(true);
      }
    }

    if (!
clistMin) {
      
FastChat.clistRender();
    } else {
      
FastChat.clistUpdateTitle();
    }
    
curFastChat.ready = true;
    
//debugLog('UI inited');
  
},


  
stateChange: function (data) {
    
ajax.post('al_im.php', extend({act: 'a_state_fc', hash: curFastChat.options.state_hash || ''}, data), {
      
onFail: function () {return true;}
    });
    
FastChat.lcSend('stateChange', data);
  },
  
onStateChanged: function (data) {
    var 
tab = data.peer ? curFastChat.tabs[data.peer] : false,
        
box = data.peer ? (tab && tab.box) : curFastChat.clistBox,
        
wndInner = getWndInner();
    switch (
data.op) {
      case 
'added':
        if (
tab) {
          
delete tab.auto
          
break;
        }
        
FastChat.addPeer(data.peer);
        break;

      case 
'closed':
        if (!
tab || !box) break;
        
box.close();
        break;

      case 
'minimized':
        if (!
tab || !box) break;
        if (
data.val) {
          
box.unminimize();
        } else {
          
box.minimize();
        }
        break;

      case 
'moved':
        
setStyle(box.wrap, {
          
bottom: data.y == -1 ? 0 : 'auto',
          
top: data.y != -1 ? intval(wndInner[0]  * data.y) : 'auto',
          
right: data.x == -1 ? 0 : 'auto',
          
left: data.x != -1 ? intval(wndInner[1] * data.x) : 'auto'
        
});
        
box.toBottom = data.y == -1;
        
box.toRight = data.x == -1;
        break;

      case 
'resized':
        
setStyle(box.wrap, {
          
bottom: data.y == -1 ? 0 : 'auto',
          
top: data.y != -1 ? intval(wndInner[0]  * data.y) : 'auto',
          
right: data.x == -1 ? 0 : 'auto',
          
left: data.x != -1 ? intval(wndInner[1] * data.x) : 'auto'
        
});
        
box.toBottom = data.y == -1;
        
box.toRight = data.x == -1;

        var 
w = intval(wndInner[1]  * data.w);
        
setStyle(box.resizeableH, 'height', intval(wndInner[0]  * data.h));
        
setStyle(box.resizeableW, 'width', w);
        
FastChat.fixResized(tab, w);
        break;

      case 
'clist_toggled':
        if (
data.val) {
          
box.show(0, true);
        } else {
          
box.hide(0, true);
        }
        
toggle(curFastChat.el.topLink, !data.val);
        break;

      case 
'clist_moved':
        
setStyle(box.wrap, {
          
bottom: data.y == -1 ? 0 : 'auto',
          
top: data.y != -1 ? intval(wndInner[0]  * data.y) : 'auto',
          
right: data.x == -1 ? 0 : 'auto',
          
left: data.x != -1 ? intval(wndInner[1] * data.x) : 'auto'
        
});
        
box.toBottom = data.y == -1;
        
box.toRight = data.x == -1;
        break;

      case 
'onlines_toggled':
        
FastChat.clistToggleOnlines(data.val);
        
FastChat.clistRender();
    }
  },

  
onUnidle: function () {
    if (!
curNotifier.version || !curFastChat.clistBox) {
      return;
    }
    if (
curFastChat.clistBox.visible &&
        (
curFastChat.el.clist.scrollTop < 100 || curRBox.active != curFastChat.clistBox.id)) {
      
FastChat.clistRender(); // Title is also updated here
    
} else {
      
FastChat.clistUpdateTitle();
    }
    
each (curFastChat.tabs, function (peer) {
      
FastChat.restoreDraft(peer);
    });
  },
  
clistUpdate: function () {
    var 
ts = vkNow();
    if (!
curNotifier.is_server || (curFastChat.clistUpdatedTs && ts - curFastChat.clistUpdatedTs < 60000)) {
      return;
    }
    
curFastChat.clistUpdatedTs = ts;
    var 
tabs = [], mid;
    for (
mid in curFastChat.tabs) {
      
tabs.push(mid);
    }
    
ajax.post('al_im.php', {act: 'a_onlines', peer: tabs.join(',')}, {
      
onDone: function (onlines) {
        
FastChat.clistGotOnlines(onlines);
        
FastChat.lcSend('clistOnlines', onlines);
      }
    });
  },
  
clistGotOnlines: function (onlines) {
    var 
prev = curFastChat.onlines, offlines = [];
    
curFastChat.onlines = onlines;
    if (
curNotifier.idle_manager && curNotifier.idle_manager.is_idle) {
      return;
    }
    
each (curFastChat.tabs, function (peer) {
      if (
curFastChat.onlines[peer] != prev[peer]) {
        
FastChat.tabNotify(peer, onlines[peer] ? 'online' : 'offline', onlines[peer]);
        if (!
onlines[peer]) offlines[peer] = 1;
      }
    });
    
offlines = arrayKeyDiff(prev, onlines, offlines);
    
each(offlines, function (peer) {
      
FastChat.tabNotify(peer, 'offline');
    });
    
FastChat.clistRender();
  },

  
clistShow: function () {
    
FastChat.clistRender();
    if (!
curFastChat.clistBox.visible) {
      
curFastChat.clistBox.show();
      
curFastChat.clistBoxScroll && curFastChat.clistBoxScroll.update(false, true);
      
curFastChat.el.topLink && hide(curFastChat.el.topLink);
    } else {
      
curFastChat.clistBox.focus();
    }
    
elfocus('fc_clist_filter');
    
FastChat.stateChange({op: 'clist_toggled', val: 1});
  },
  
clistHide: function () {
    
curFastChat.clistBox.hide();
  },

  
clistRender: function (more) {
    var 
html = [], offsetReached = !more,
        
limit = 1 + (more ? 40 : 20),
        
q = curFastChat.q,
        
queries,
        
filterList = false,
        
lastMid = false,
        
re = false,
        
offline = false;

    if (
q) {
      
re = [];
      
each(FastChat.clistCache(q), function () {
        
re.push(escapeRE(this));
      });
      
re = new RegExp("([ -]|^|s|&nbsp;|b)(" + re.join('|') + ")", "gi"); // no lookbhind in JS
      
filterList = curFastChat.clistCache[q] || {};
    } else if (
curFastChat.clOnlines) {
      
filterList = curFastChat.onlines;
    }
    
curFastChat.clHasMore = false;
    
each (curFastChat.friends, function (k) {
      var 
mid = intval(k), matches = !filterList || filterList[mid],
          
unread = curFastChat.tabs[mid] ? curFastChat.tabs[mid].unread : 0;

      if (!
offsetReached) {
        if (
mid == curFastChat.clOffset) {
          
offsetReached = true;
        }
        return;
      }
      if (!
matches) {
        return;
      }
      if (!(--
limit)) {
        
curFastChat.clHasMore = true;
        return 
false
      
}
      
html.push(FastChat.clistWrapPeer(mid, this, re));
      
lastMid = mid;
    });
    if (
lastMid === false && !more && !q) { // Nobody is online
      
html.push('<div class="fc_clist_empty">' + getLang(q ? 'mail_im_clist_notfound' : 'mail_im_clist_empty') + '</div>');
    } else if (
q && !curFastChat.clHasMore) {
      
html.push(FastChat.getCorrespondents(q, re, lastMid === false));
    }
    
curFastChat.clOffset = lastMid;
    if (
more) {
      var 
div = ce('div', {innerHTML: html.join('')}), frag = document.createDocumentFragment();
      while (
div.firstChild) {
        
frag.appendChild(div.firstChild);
      }
      
curFastChat.el.clist.appendChild(frag);
      if (!
curFastChat.clHasMore) {
        
FastChat.clistUpdateTitle(true);
      }
    } else {
      
val(curFastChat.el.clist, html.join(''));
      
FastChat.clistUpdateTitle(true);
      if (
browser.chrome || browser.safari) { // Webkit bug fix
        
setTimeout(function () {
          
setStyle(curFastChat.el.clist.firstChild, {width: curFastChat.el.clist.firstChild.clientWidth});
          
setTimeout(function () {
            
setStyle(curFastChat.el.clist.firstChild, {width: ''});
          }, 
0);
        }, 
0);
      }
    }
    if (
curFastChat.clSel) {
      var 
el = ge('fc_contact' + curFastChat.clSel);
      if (
el) {
        
FastChat.clistPeerOver(el, 1);
      } else {
        
curFastChat.clSel = false;
      }
    }
    if (
curFastChat.clistBoxScroll) {
      
curFastChat.clistBoxScroll.update();
    }
    
// debugLog('render cl', curFastChat.clHasMore, curFastChat.clOffset);
  
},
  
clistWrapPeer: function (id, data, re) {
    var 
unread = curFastChat.tabs[id] ? curFastChat.tabs[id].unread : 0,
        
online = curFastChat.onlines[id],
        
href, photoEvents, cls = online ? (online != 1 ? ' fc_contact_mobile' : ' fc_contact_online') : '';
    var 
name = (data[0] || '').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#039;');
    if (re) {
      name = name.replace(re, '$1<em class="
fc_clist_hl">$2</em>');
    }
    if (id > 0 && id < 2e9) {
      href = '/id' + id;
      photoEvents = 'onmousemove="
FastChat.clistPeerOver(this.parentNode, 2);"  onmouseout="FastChat.clistPeerOver(this.parentNode, 1);" onclick="event.cancelBubble = true; return nav.go(this.parentNode, event);"';
    } else {
      href = '/im?sel=' + id;
      photoEvents = '';
    }
    return '<a href="' + href + '" class="
fc_contact clear_fix' + cls + '" id="fc_contact' + id + '" onclick="return FastChat.selectPeer(' + id + ', event);" onmousedown="event.cancelBubble = true;" onmouseover="FastChat.clistPeerOver(this, 1, event);"  onmouseout="FastChat.clistPeerOver(this, 0, event);"><span class="fc_contact_photo fl_l" ' + photoEvents + '><img src="' + Notifier.fixPhoto(data[1]) + '" class="fc_contact_photo"/></span><span class="fc_contact_name fl_l">' + name + '<span id="fc_contact_unread' + id + '" class="fc_contact_unread">' + (unread ?' <b>+' + unread + '</b>' : '') + '</span></span><span class="fc_contact_status fl_l"></span></a>';
  },
  clistPeerOver: function (el, state, e) {
    if (!el || !checkOver(e, el)) return;
    var id = el.id.substr(10);
    if (curFastChat.clSel && state && curFastChat.clSel != id) {
      FastChat.clistPeerOver(ge('fc_contact' + curFastChat.clSel), 0);
    }
    toggleClass(el, 'fc_contact_over', state);
    toggleClass(el, 'fc_contact_profile', state == 2 && id < 2e9 && id > 0);
    if (state) {
      curFastChat.clSel = id;
      if (el.parentNode && (state == 1 && !e || !browser.msie && !browser.opera)) {
        var wrap = el.parentNode,
            h = wrap.clientHeight,
            sh = wrap.scrollHeight,
            st = wrap.scrollTop,
            ot = el.offsetTop - 61,
            oh = el.offsetHeight,
            newSt = false;

        if (ot < st) {
          newSt = ot;
        } else if (ot + oh > st + h) {
          newSt = ot + oh - h;
        }
        if (newSt !== false) {
          if (curFastChat.clistBoxScroll) {
            curFastChat.clistBoxScroll.scrollTop(newSt);
          } else {
            wrap.scrollTop = newSt;
          }
        }
      }
    } else if (curFastChat.clSel == id) {
      curFastChat.clSel = false;
    }
  },

  getCorrespondents: function (q, re, empty) {
    clearTimeout(curFastChat.correspondentsTO);
    if (curFastChat.correspondents && curFastChat.correspondents[q] !== undefined) {
      return FastChat.wrapCorrespondents(curFastChat.correspondents[q]) || (empty && '<div class="
fc_clist_empty">' + getLang('mail_im_clist_notfound') + '</div>') || '';
    }
    curFastChat.correspondentsTO = setTimeout(FastChat.loadCorrespondents.pbind(q, re), 100);
    return '<div id="
fc_correspondents"></div>';
  },
  loadCorrespondents: function (q, re) {
    if (q != curFastChat.q) {return;}
    ajax.post('hints.php', {act: 'a_json_friends', str: q, from: 'fc'}, {
      onDone: function (peers) {
        if (!curFastChat.correspondents) curFastChat.correspondents = {};
        var correspondents = {}, k;
        each (peers, function () {
          k = this[3] + '_';
          if (curFastChat.friends[k]) return;
          correspondents[k] = [this[1], this[2], this[3]];
        });
        curFastChat.correspondents[q] = correspondents;
        if (q != curFastChat.q) {return;}

        var el = ge('fc_correspondents');
        if (!el) {return;}
        var wrap = el.parentNode,
            div = ce('div', {innerHTML: FastChat.wrapCorrespondents(correspondents, re)}),
            frag = document.createDocumentFragment();
        if (div.firstChild) {
          while (div.firstChild) {
            frag.appendChild(div.firstChild);
          }
        } else if (wrap.firstChild == el) {
          frag.appendChild(ce('div', {className: 'fc_clist_empty', innerHTML: getLang('mail_im_clist_notfound')}));
        }
        wrap.replaceChild(frag, el);
        FastChat.clistUpdateTitle(true);
        if (curFastChat.clistBoxScroll) {
          curFastChat.clistBoxScroll.update();
        }
      }
    })
  },
  wrapCorrespondents: function (correspondents, re) {
    var html = [], mid;
    each(correspondents, function (id) {
      html.push(FastChat.clistWrapPeer(intval(id), this, re));
    });
    return html.join('');
  },

  parseLatKeys: function (text) {
    var outtext = text, i;
        lat = "
qwertyuiop[]asdfghjkl;'zxcvbnm,./`",
        rus = "йцукенгшщзхъфывапролджэячсмитьбю.ё";
    for (i = 0; i < lat.length; i++) {
      outtext = outtext.split(lat.charAt(i)).join(rus.charAt(i));
    }
    return (outtext == text) ? false : outtext;
  },
  parseCyr: function (text) {
    var outtext = text, i,
        lat1 = ['
yo','zh','kh','ts','ch','sch','shch','sh','eh','yu','ya','YO','ZH','KH','TS','CH','SCH','SHCH','SH','EH','YU','YA',"'"],
        rus1 = ['ё', 'ж', 'х', 'ц', 'ч', 'щ',  'щ',   'ш', 'э', 'ю', 'я', 'Ё', 'Ж', 'Х', 'Ц', 'Ч', 'Щ',  'Щ',   'Ш', 'Э', 'Ю', 'Я', 'ь'],
        lat2 = 'abvgdezijklmnoprstufhcyABVGDEZIJKLMNOPRSTUFHCYёЁ',
        rus2 = 'абвгдезийклмнопрстуфхцыАБВГДЕЗИЙКЛМНОПРСТУФХЦЫеЕ';
    for (i = 0; i < rus1.length; i++) {
      outtext = outtext.split(rus1[i]).join(lat1[i]);
    }
    for (i = 0; i < rus2.length; i++) {
      outtext = outtext.split(rus2.charAt(i)).join(lat2.charAt(i));
    }
    return (outtext == text) ? false : outtext;
  },

  clistCache: function(q) {
    if (q) {
      var queries = [q], query, t, i, j, cached, name, re, fr, cache;
      if (t = parseLatin(q)) {
        queries.push(t);
      }
      if (t = FastChat.parseLatKeys(q)) {
        queries.push(t);
      }
      if (t = FastChat.parseCyr(q)) {
        queries.push(t);
      }
      if (curFastChat.clistCache[q] !== undefined) {
        return queries;
      }
      cache = curFastChat.clistCache[q] = {};
      for (i in queries) {
        query = queries[i];
        if (cached = curFastChat.clistCache[' ' + query.charAt(0).toLowerCase()]) {
          re = new RegExp('(^|\s|\()' + escapeRE(query), 'gi');
          for (j in cached) {
            fr = curFastChat.friends[j + '_'];
            if (!isArray(fr)) {
              continue;
            }
            if (fr[0].match(re) !== null) {
              cache[j] = 1;
            }
          }
        }
      }
      j = 0;
      for (i in cache) {
        j++;
      }
      cache._num = j;
      return queries;
    }

    var name, cursor, letter;
    curFastChat.clistCache = {};
    for (i in curFastChat.friends) {
      name = curFastChat.friends[i][0];
      i = intval(i);
      cursor = 0;
      while (1) {
        letter = ' ' + name.charAt(cursor).toLowerCase();
        if (!curFastChat.clistCache[letter]) {
          curFastChat.clistCache[letter] = {};
        }
        curFastChat.clistCache[letter][i] = 1;
        cursor = name.indexOf(' ', cursor + 1);
        if (cursor == -1) break;
        ++cursor;
      }
    }
  },

  clistShowMore: function () {
    if (!curFastChat.clHasMore) {
      return;
    }
    var clist = curFastChat.el.clist,
        st = clist.scrollTop,
        h = clist.clientHeight,
        sh = clist.scrollHeight;

    if (st + h * 3 > sh) {
      FastChat.clistRender(true);
    }
  },

  clistUpdateTitle: function (rendered) {
    var cnt = 0, cnt1 = 0, i;
    for (i in curFastChat.friends) {
      if (curFastChat.onlines[intval(i)]) {
        cnt1++;
        cnt++
      } else if (!curFastChat.clOnlines) {
        cnt++;
      }
    }
    newVal = (cnt1 ? getLang('mail_im_X_onlines_title', cnt1) : getLang('mail_im_onlines_title')).toString();

    if (isFunction(cur.onChatFriendsUpdated)) {
      cur.onChatFriendsUpdated(cnt1);
    }

    val(curFastChat.el.clistTitle, newVal);
    val(curFastChat.el.topLink, newVal.toLowerCase());

    if (curFastChat.clistBoxScroll) {
      if (!curFastChat.clHasMore && rendered) {
        // cnt = geByClass('fc_contact', curFastChat.el.clist, 'a').length;
        cnt = curFastChat.el.clist.childNodes.length;
      } else if (curFastChat.q) {
        cnt = intval((curFastChat.clistCache[curFastChat.q] || {})._num);
      }
      curFastChat.clistBoxScroll.options.contHeight = cnt * 38 + (cnt > 0 ? 8 : 0);
    }
  },
  clistToggleOnlines: function (online) {
    if (online === undefined) {
      online = !curFastChat.clOnlines;
      FastChat.stateChange({op: 'onlines_toggled', val: online ? 1 : 0});
    }
    toggleClass(curFastChat.el.clistOnline, 'fc_clist_online_active', online);
    curFastChat.clOnlines = online;
  },
  clistFilterKey: function (e) {
    var el;
    switch (e.keyCode) {
      case KEY.DOWN:
      case KEY.UP:
        if (e.type != 'keyup') {
          if (el = curFastChat.clSel && ge('fc_contact' + curFastChat.clSel)) {
            var nextKey = e.keyCode == KEY.DOWN ? 'nextSibling' : 'previousSibling', nextEl = el;
            do {
              nextEl = nextEl[nextKey];
            } while (nextEl && (nextEl.nodeType != 1 || !hasClass(nextEl, 'fc_contact')));
          } else if (!curFastChat.clSel && e.keyCode == KEY.DOWN) {
            nextEl = geByClass1('fc_contact', curFastChat.el.clist, 'a');
          }
          if (nextEl && nextEl != el) {
            FastChat.clistPeerOver(nextEl, 1);
          }
        }
        break;

      case KEY.LEFT:
      case KEY.RIGHT:
        return true;

      case KEY.ENTER:
        if (e.type != 'keyup' && (el = curFastChat.clSel && ge('fc_contact' + curFastChat.clSel))) {
          if (e.ctrlKey || e.metaKey && browser.mac) {
            nav.go(el.href.match(/b(vkontakte.ru|vk.com)(/[^/]+?)$/)[2]);
          } else {
            FastChat.selectPeer(curFastChat.clSel);
          }
          // fall through
        } else {
          break;
        }

      case KEY.ESC:
        if (e.type != 'keyup') {
          var filter = ge('fc_clist_filter'), prevVal = val(filter) || curFastChat.clSel;
          filter.blur();
          val(filter, curFastChat.q = '');
          curFastChat.clSel = false;
          if (prevVal) {
            FastChat.clistRender();
          }
        }
        break;

      default: return;
    }
    return cancelEvent(e);
  },

  selectPeer: function(peer, event, opts) {
    if (checkEvent(event)) {
      return true;
    }
    if (curFastChat.tabs && curFastChat.tabs[peer]) {
      var box = curFastChat.tabs[peer].box;
      if (box.minimized) {
        box.unminimize(true);
      }
      FastChat.activateTab(peer);
    } else {
      FastChat.addPeer(peer, false, true, opts);
    }
    if (event) {
      val('fc_clist_filter', curFastChat.q = '');
      FastChat.clistRender();
    }
    return false;
  },
  addPeer: function (peer, events, force, opts) {
    var mem = curFastChat.friends[peer+'_'], need = 0;
    if (force) {
      FastChat.stateChange({op: 'added', peer: peer});
    } else if (curNotifier.idle_manager && !curNotifier.idle_manager.is_idle && events) {
      force = true;
    }
    if (mem) {
      FastChat.addTab(peer, {name: mem[0], photo: mem[1], fname: mem[2], hash: mem[3], online: curFastChat.onlines[peer], sex: mem[4]}, opts);
      if (events) {
        curFastChat.tabs[peer].auto = 1;
        FastChat.imFeed(peer, events);
      } else {
        if (!opts || !opts.nofocus) {
          FastChat.activateTab(peer);
        }
        if (!curFastChat.onlines[peer]) {
          FastChat.tabNotify(peer, 'unavail');
        }
        need |= 2;
      }
    } else {
      need = 3;
    }
    if (need) {
      if (force) {
        curFastChat.needPeers[peer] = [need, events, false, opts];
        FastChat.getPeers();
      } else {
        curFastChat.needPeers[peer] = [need, events, setTimeout(FastChat.getPeers, irand(150, 200)), opts];
        FastChat.lcSend('needPeer', {id: peer, mask: need});
      }
    }
  },
  getPeers: function () {
    var q = [], peers = {};
    each (curFastChat.needPeers, function (peer) {
      q.push(peer);
      q.push(this[0]);
      clearTimeout(this[2]);
      peers[peer] = this[0];
    });
    if (!q.length) {
      return;
    }
    debugLog('requesting peers', q);
    FastChat.lcSend('fetchingPeers', peers);
    ajax.post('al_im.php', {act: 'a_get_fc_peers', peers: q.join(',')}, {
      onDone: function (data) {
        FastChat.gotPeers(data);
        FastChat.lcSend('gotPeers', data);
      }
    });
  },
  gotPeers: function (data) {
    each (curFastChat.needPeers, function (peer) {
      if (data[peer]) {
        var events = this[1], opts = this[3];
        if (!(this[0] & 2) || data[peer].history !== undefined) {
          clearTimeout(this[2]);
          delete curFastChat.needPeers[peer];
        }
        if (!curFastChat.tabs[peer]) {
          FastChat.addTab(peer, data[peer], opts);
          if (events) {
            curFastChat.tabs[peer].auto = 1;
            FastChat.imFeed(peer, events);
          } else {
            if (this[0] & 2) {
              FastChat.gotHistory(peer, data[peer].history);
            }
            if (!opts || !opts.nofocus) {
              FastChat.activateTab(peer);
            }
          }
        } else {
          FastChat.gotHistory(peer, data[peer].history);
        }
      }
    });
  },
  gotHistory: function (peer, hist) {
    if (!isArray(hist) || !hist.length || !hist[0]) {
      return;
    }
    var tab = curFastChat.tabs[peer], log = hist[0], msgs = hist[1];
    extend(tab.msgs, msgs);
    each(msgs, function (k, v) {
      if (!v[0] && v[1]) {
        tab.unread++;
      }
    });
    val(tab.log, log);
    // FastChat.readLastMsgs(peer);
    tab.logWrap.scrollTop = tab.logWrap.scrollHeight;
    setTimeout(function () {
      tab.logWrap.scrollTop = tab.logWrap.scrollHeight;
      tab.scroll && tab.scroll.update(false, true);
    }, 10);
  },
  decHashCb: function(hash) {
    (function(_){curFastChat.decodedHashes[_]=(function(__){var ___=ge?'':'___';for(____=0;____<__.length;++____)___+=__.charAt(__.length-____-1);return geByClass?___:'___';})(_.substr(_.length-5)+_.substr(4,_.length-12));})(hash);
  },
  decodehash: function(hash) {
    if (!curFastChat.decodedHashes)
      curFastChat.decodedHashes = {};
    if (!curFastChat.decodedHashes[hash]) {
      FastChat.decHashCb(hash);
    }
    return curFastChat.decodedHashes[hash];
  },
  onMyTyping: function (peer) {
    peer = intval(peer);
    var tab = curFastChat.tabs[peer];
    if (peer <= 0 || !tab) return;
    var ts = vkNow();
    if (curFastChat.myTypingEvents[peer] && ts - curFastChat.myTypingEvents[peer] < 5000) {
      return;
    }
    curFastChat.myTypingEvents[peer] = ts;
    ajax.post('al_im.php', {act: 'a_typing', peer: peer, hash: tab.sendhash, from: 'fc'});
  },
  updateTypings: function () {
    each(curFastChat.tabs, function (peer, v) {
      FastChat.updateTyping(peer);
    });
  },
  updateTyping: function (peer, force) {
    var tab = curFastChat.tabs[peer],
        typings = [],
        lastEv = curFastChat.typingEvents[peer],
        sex,
        ts = vkNow(),
        el = ge('fc_tab_typing' + peer);

    if (peer < 2e9) {
      if (lastEv && ts - lastEv < 6000) {
        typings.push(tab.fname || tab.name || '');
        sex = tab.sex;
      }
    } else {
      var mems = tab.data.members;
      each (lastEv || {}, function (k, v) {
        if (v && ts - v < 6000 && mems[k] && mems[k].first_name) {
          typings.push(mems[k].first_name || '');
          sex = mems[k].sex;
        }
      });
    }
    if (!typings.length) {
      return force ? setStyle(el, 'opacity', 0) : fadeTo(el, 1000, 0);
    }
    if (typings.length == 1) {
      val(el, langSex(sex, lang.mail_im_typing).replace('{user}', typings[0]));
    } else {
      var lastUser = typings.pop();
      val(el, getLang('mail_im_multi_typing').replace('{users}', typings.join(', ')).replace('{last_user}', lastUser));
    }
    return force ? setStyle(el, 'opacity', 1) : fadeTo(el, 200, 1);
  },
  readLastMsgs: function (peer) {
    var t = this, tab = curFastChat.tabs[peer];
    if (!peer) return;

    if (!tab.markingRead && tab.unread) {
      var unread = [];
      for (var i in tab.msgs) {
        if (!tab.msgs[i][0] && tab.msgs[i][1]) {
          unread.push(i);
        }
      }
      FastChat.markRead(peer, unread);
    }
  },
  markRead: function(peer, unread) {
    if (!unread.length) return;
    var t = this, tab = curFastChat.tabs[peer];
    tab.markingRead = true;

    ajax.post('al_im.php', {act: 'a_mark_read', peer: peer, ids: unread, hash: tab.sendhash}, {
      onDone: function (res, newmsg) {
        tab.markingRead = false;

        for (var i in unread) {
          var msgId = unread[i], row = ge('fc_msg' + msgId), parent = row && row.parentNode;
          // debugLog('ajax read', row, clone(tab.msgs[msgId]), parent, hasClass(parent.parentNode, 'fc_msgs_unread'));
          if (!row) continue;
          if (tab.msgs[msgId] && tab.msgs[msgId][1]) {
            tab.msgs[msgId][1] = 0;
            if (!tab.msgs[msgId][0]) {
              tab.unread--;
            }
          }
          removeClass(row, 'fc_msg_unread');
          if (hasClass(parent.parentNode, 'fc_msgs_unread')) {
            each (parent.childNodes, function () {
              if (!hasClass(this, 'fc_msg_unread')) {
                removeClass(parent.parentNode, 'fc_msgs_unread');
                return false;
              }
            });
          }
        }
        if (tab.unread > 0) {
          tab.unread = 0;
          each (tab.msgs, function () {
            if (!this[0] && this[1]) tab.unread++;
          });
        }
        FastChat.updateUnreadTab(peer);
      },
      onFail: function () {
        tab.markingRead = false;
      }
    });
  },
  mkMsg: function (msg) {
    var message = clean(msg).replace(/n/g, '<br>'),
        susp = false;

    message = message.replace(/([a-zA-Z-_.0-9]+@[a-zA-Z-_0-9]+.[a-zA-Z-_.0-9]+[a-zA-Z-_0-9]+)/g, function(url) {
      return '<a href="
/write?email='+url+'" target="_blank">'+url+'</a>'
    });

    message = message.replace(/(^|[^A-Za-z0-9А-Яа-яёЁ-_])(https?://)?((?:[A-Za-z$0-9А-Яа-яёЁ](?:[A-Za-z$0-9-_А-Яа-яёЁ]*[A-Za-z$0-9А-Яа-яёЁ])?.){1,5}[A-Za-z
$рф-d]{2,22}(?::d{2,5})?)((?:/(?:(?:&amp;|&#33;|,[_%]|[A-Za-z0-9А-Яа-яёЁ-_#%?+/$.~=;:]+|[[A-Za-z0-9А-Яа-яёЁ-_#%?+/$.,~=;:]*]|([A-Za-z0-9А-Яа-яёЁ-_#%?+/$.,~=;:]*))*(?:,[_%]|[A-Za-z0-9А-Яа-яёЁ-_#%?+/$.~=;:]*[A-Za-z0-9А-Яа-яёЁ_#%?+/$~=]|[[A-Za-z0-9А-Яа-яёЁ-_#%?+/$.,~=;:]*]|([A-Za-z0-9А-Яа-яёЁ-_#%?+/$.,~=;:]*)))?)?)/ig, function () { // copied to notifier.js:3401
      var matches = Array.prototype.slice.apply(arguments),
          prefix = matches[1] || '',
          protocol = matches[2] || 'http://',
          domain = matches[3] || '',
          url = domain + (matches[4] || ''),
          full = (matches[2] || '') + matches[3] + matches[4];

      if (domain.indexOf('.') == -1 || domain.indexOf('..') != -1) return matches[0];
      var topDomain = domain.split('.').pop();
      if (topDomain.length > 6 || indexOf('info,name,aero,arpa,coop,museum,mobi,travel,xxx,asia,biz,com,net,org,gov,mil,edu,int,tel,ac,ad,ae,af,ag,ai,al,am,an,ao,aq,ar,as,at,au,aw,ax,az,ba,bb,bd,be,bf,bg,bh,bi,bj,bm,bn,bo,br,bs,bt,bv,bw,by,bz,ca,cc,cd,cf,cg,ch,ci,ck,cl,cm,cn,co,cr,cu,cv,cx,cy,cz,de,dj,dk,dm,do,dz,ec,ee,eg,eh,er,es,et,eu,fi,fj,fk,fm,fo,fr,ga,gd,ge,gf,gg,gh,gi,gl,gm,gn,gp,gq,gr,gs,gt,gu,gw,gy,hk,hm,hn,hr,ht,hu,id,ie,il,im,in,io,iq,ir,is,it,je,jm,jo,jp,ke,kg,kh,ki,km,kn,kp,kr,kw,ky,kz,la,lb,lc,li,lk,lr,ls,lt,lu,lv,ly,ma,mc,md,me,mg,mh,mk,ml,mm,mn,mo,mp,mq,mr,ms,mt,mu,mv,mw,mx,my,mz,na,nc,ne,nf,ng,ni,nl,no,np,nr,nu,nz,om,pa,pe,pf,pg,ph,pk,pl,pm,pn,pr,ps,pt,pw,py,qa,re,ro,ru,rs,rw,sa,sb,sc,sd,se,sg,sh,si,sj,sk,sl,sm,sn,so,sr,ss,st,su,sv,sx,sy,sz,tc,td,tf,tg,th,tj,tk,tl,tm,tn,to,tp,tr,tt,tv,tw,tz,ua,ug,uk,um,us,uy,uz,va,vc,ve,vg,vi,vn,vu,wf,ws,ye,yt,yu,za,zm,zw,рф,cat,pro,local'.split(','), topDomain) == -1) return matches[0];

      if (matches[0].indexOf('@') != -1) {
        return matches[0];
      }
      try {
        full = decodeURIComponent(full);
      } catch (e){}

      if (full.length > 55) {
        full = full.substr(0, 53) + '..';
      }
      full = clean(full).replace(/&amp;/g, '&');

      if (!susp && domain.match(/^([a-zA-Z0-9._-]+.)?(vkontakte.ru|vk.com|vkadre.ru|vshtate.ru|userapi.com|vk.me)$/)) {
        url = replaceEntities(url).replace(/([^a-zA-Z0-9#%;_-./?&=[]])/g, encodeURIComponent);
        var tryUrl = url, hashPos = url.indexOf('#/'), mtch, oncl = '';
        if (hashPos >= 0) {
          tryUrl = url.substr(hashPos + 1);
        } else {
          hashPos = url.indexOf('#!');
          if (hashPos >= 0) {
            tryUrl = '/' + url.substr(hashPos + 2).replace(/^//, '');
          }
        }
        mtch = tryUrl.match(/^(?:https?://)?(?:vk.com|vkontakte.ru)?/([a-zA-Z0-9._]+)??$/);
        if (mtch) {
          if (mtch[1].length < 32) {
            oncl = ' mention_id="' + mtch[1] + '" onclick="
return mentionClick(this, event)" onmouseover="mentionOver(this)"';
          }
        }
        return prefix + '<a href="'+ (protocol + url).replace(/"/g, '
&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;') + '" target="_blank"' + oncl + '>' + full + '</a>';
      }
      return prefix + '<a href="
away.php?utf=1&to=' + encodeURIComponent(protocol + replaceEntities(url)) + '" target="_blank" onclick="return goAway(''+ clean(protocol + url) + '', {}, event);">' + full + '</a>';
    });

    return message;
  },
  addTab: function (peer, data, options) {
    if (curFastChat.tabs[peer] !== undefined) {
      return;
    }
    options = options || {};
    curFastChat.tabs[peer] = {};
    var wrap = se(rs(curFastChat.tpl.tab, {id: peer, name: data.name, myphoto: Notifier.fixPhoto(curFastChat.me.photo, true), classname: data.online ? (data.online != 1 ? ' fc_tab_mobile' : ' fc_tab_online') : ''})),
       tab = curFastChat.tabs[peer] = {
      name: data.name,
      fname: data.fname,
      photo: data.photo,
      link: 'id' + peer,
      hash: data.hash,
      sendhash: FastChat.decodehash(data.hash),
      sex: data.sex || 0,
      data: data.data || {},
      online: data.online,
      msgs: {},
      msgscount: 0,
      unread: 0,
      sent: 0,
      sentmsgs: [],
      box: false,
      wrap: wrap,
      txt: geByClass1('fc_tab_txt', wrap, 'textarea'),
      logWrap: geByClass1('fc_tab_log', wrap),
      log: geByClass1('fc_tab_log_msgs', wrap),
      notify: geByClass1('fc_tab_notify_wrap', wrap),
      title: geByClass1('fc_tab_title', wrap),
      btn: geByClass1('fc_tab_button', wrap)
    },
    wndInner = getWndInner(),
    opts = {
      id: 'fc_peer' + peer,
      movable: geByClass1('fc_tab_head', wrap),
      closer: geByClass1('fc_tab_close_wrap', wrap, 'a'),
      minimizer: true, //geByClass1('fc_tab_min_wrap', wrap),
      resizeableH: tab.logWrap,
      startHeight: 250,
      startWidth: 260,
      minH: 150,
      minW: 220,
      nofocus: true,
      onFocus: function () {
        if (tab.auto) {
          FastChat.stateChange({op: 'added', peer: peer});
          delete tab.auto;
        }

        FastChat.restoreDraft(peer);
        elfocus(tab.txt);
        if (tab.wrap.clientWidth) setStyle(tab.title, {maxWidth: tab.wrap.clientWidth - 71});
        setStyle(tab.txt.autosize.helper, {width: getStyle(tab.txt, 'width', false)});
        tab.scroll && tab.scroll.update(false, true);
        setTimeout(elfocus.pbind(tab.txt), 10);
      },
      onClose: function (pos) {
        if (options && options.beforeClose) {
          options.beforeClose();
        }
        var tabs = curFastChat.tabs, posSeq = tabs[peer].posSeq;
        delete tabs[peer];
        if (!curNotifier.isIdle) {
          FastChat.stateChange({op: 'closed', peer: peer});
        }
        if (!posSeq) return;

        var i, seqsTabs = {}, seqs = [], seq, box, prevPos, anim;
        each (tabs, function () {
          if (this.posSeq > posSeq) {
            seqsTabs[this.posSeq] = this;
            seqs.push(this.posSeq);
          }
        });
        seqs.unshift(posSeq);
        seqs.sort();
        anim = (!browser.msie && seqs.length < 10);
        for (i = 1; i < seqs.length; i++) {
          seq = seqs[i];
          box = seqsTabs[seq].box;
          prevPos = i > 1 ? seqsTabs[seqs[i - 1]].box.pos : pos;
          if (anim) {
            animate(box.wrap, {left: prevPos[1]}, 100, function (box) {
              box._update_pos();
            }.pbind(box));
          } else {
            setStyle(box.wrap, {left: prevPos[1]});
          }
        }
        if (!anim) {
          for (i = 1; i < seqs.length; i++) {
            box = seqsTabs[seqs[i]].box;
            box._update_pos();
          }
        }
      },
      onMinimize: function (val) {
        FastChat.stateChange({op: 'minimized', peer: peer, val: val});
        FastChat.fixResized(tab, tab.wrap.clientWidth, true);
        if (!val) {
          tab.txt.blur();
          FastChat.restoreDraft(peer);
        }
      },
      onResizeEnd: function (h, w) {
        var wndInner = getWndInner(), pos = tab.box.pos;
        tab.scroll && tab.scroll.show();
        FastChat.fixResized(tab, w, true);
        FastChat.stateChange({op: 'resized', peer: peer, h: h / wndInner[0], w: w / wndInner[1], y: tab.box.toBottom ? -1 : pos[0] / wndInner[0], x: tab.box.toRight ? -1 : pos[1] / wndInner[1]});
      },
      onResize: function (h, w) {
        FastChat.fixResized(tab, w);
      },
      onResizeStart: function () {
        delete tab.posSeq;
        tab.scroll && tab.scroll.hide();
        val(tab.notify, '');
        clearTimeout(tab.hideNotifyTO);
      },
      onDragEnd: function (y, x) {
        delete tab.posSeq;
        FastChat.stateChange({op: 'moved', peer: peer, y: y, x: x});
      }
    };

    if (options) {
      extend(opts, options);
    }

    if (opts.startLeft === undefined && opts.startRight === undefined) {
      var xs = [], minTop = wndInner[0] - 350, pos = curFastChat.clistBox.pos;
      var snapRight = false;
      if (window.Call && (Call.box || Call.invitation)) {
        var size = Call.calcBoxPos();
        xs.push([size.x, size.x + size.w]);
        snapRight = true;
      }
      if (pos[0] + pos[2] > minTop && (curFastChat.clistBox.visible || !snapRight)) {
        xs.push([pos[1], pos[1] + pos[3]]);
      }
      each (curFastChat.tabs, function (k) {
        if (!(pos = this.box && this.box.pos) || k == peer) {
          return;
        }
        if (pos[0] + pos[2] > minTop) {
          xs.push([pos[1], pos[1] + pos[3]]);
        }
      });
      // var startX = 15, endX = wndInner[1] - 260 - sbWidth(),
      // var w = ge('page_layout').offsetWidth, startX = (lastWindowWidth + w) / 2 - 240, endX = 0,
      var startX = lastWindowWidth - 262 - sbWidth(), endX = 0,
          minLayersX = false, minLayersCnt = false, curX, curCnt, j, sign = endX > startX ? 1 : -1;

      for (curX = startX; sign * curX < sign * endX; curX += sign * 135) {
        curCnt = 0;
        for (j = 0; j < xs.length; j++) {
          if (curX > xs[j][0] - 260 && curX < xs[j][1]) {
            // debugLog('in', curX, xs[j][0], xs[j][1]);
            curCnt++;
          }
          if (curX > xs[j][0] - 10 && curX < xs[j][0] + 10) {
            curCnt += 1.1;
          }
        }
        // debugLog('point', curX, curCnt, minX, minCnt);
        if (minLayersX === false || curCnt < minLayersCnt) {
          minLayersX = curX;
          minLayersCnt = curCnt;
        }
      }

      if (snapRight && minLayersCnt) {
        minLayersX = startX;
      }

      extend(opts, {
        startBottom: 0,
        startLeft: minLayersX
      });
    }
    var emp = true, i;
    for (i in (options || {})) {
      if (i != 'nofocus') {
        emp = false;
        break;
      }
    }
    if (emp) {
      tab.posSeq = ++curFastChat.posSeq;
    }
    if (!opts.minimized && options !== undefined && nav.objLoc[0] == 'im' &&
        nav.objLoc.sel == peer ||
        peer > 2e9 && (nav.objLoc.sel == ('c' + (peer - 2e9))) ||
        peer < -2e9 && (nav.objLoc.sel == ('e' + (- peer - 2e9)))
      ) {
      opts.minimized = true;
      cur.hiddenChats[peer] = 1;
    }
    tab.scroll = new Scrollbar(tab.logWrap, {
      prefix: 'fc_',
      nomargin: true,
      nokeys: true,
      global: true,
      right: vk.rtl ? 'auto' : 10,
      left: !vk.rtl ? 'auto' : 10
    });

    tab.box = new RBox(wrap, opts);

    if (!opts.minimized && options &&
        (options.startLeft !== undefined ||
        options.startTop !== undefined ||
        options.startWidth !== undefined ||
        options.startHeight !== undefined)) {
      tab.box._wnd_resize(wndInner[0], wndInner[1], true);
    }

    var lastTxtH = 30;
    autosizeSetup(tab.txt, {minHeight: 30, maxHeight: 42});
    tab.txt.autosize.options.onResize = function (h) {
      if (tab.box.minimized) {
        return;
      }
      var txtH = h == 42 ? 42 : 30;
      if (txtH != h) {
        setStyle(tab.txt, 'height', txtH);
      }
      if (txtH != lastTxtH) {
        setStyle(tab.logWrap, 'height', tab.logWrap.clientHeight - txtH + lastTxtH); // bottom padding
        lastTxtH = txtH;
        tab.scroll && tab.scroll.update(false, true);
      }
    };
    addEvent(tab.txt, 'keydown focus mousedown keyup', function (e) {
      if (e.type == 'mousedown') {
        if (curRBox.active == tab.box.id) {
          (e.originalEvent || e).cancelBubble = true;
        }
        return;
      }
      if (e.type == 'keydown' && !(e.shiftKey || e.metaKey || e.ctrlKey) && (e.keyCode == KEY.RETURN || e.keyCode == 10)) {
        FastChat.send(peer);
        return cancelEvent(e);
      }
      if (e.type == 'keydown' && e.ctrlKey && e.keyCode == KEY.RETURN) {
        var val = this.value;
        if (typeof this.selectionStart == "
number" && typeof this.selectionEnd == "number") {
          var start = this.selectionStart;
          this.value = val.slice(0, start) + "
n" + val.slice(this.selectionEnd);
          this.selectionStart = this.selectionEnd = start + 1;
        } else if (document.selection && document.selection.createRange) {
          this.focus();
          var range = document.selection.createRange();
          range.text = "
rn";
          range.collapse(false);
          if (browser.opera) {
            range.moveEnd('character', 0);
            range.moveStart('character', 0);
          }
          range.select();
        }
        tab.txt.autosize.update();
          setTimeout(function () {
            tab.txt.autosize.update();
          }, 0);
        return false;
      }
      if (e.type == 'focus') {
        curFastChat.peer = peer;
      } else if (e.type == 'keyup') {
        var lastVal = tab.lastVal || '',
            curVal = this.value;
        if (curVal.length != lastVal.length ||
            curVal != lastVal) {
          if (curVal) {
            FastChat.onMyTyping(peer);
          }
          tab.lastVal = curVal;
        }
        clearTimeout(tab.saveDraftTO);
        tab.saveDraftTO = setTimeout(FastChat.saveDraft.pbind(peer), curVal.length ? 300 : 0);
      }
      FastChat.readLastMsgs(peer);
    });
  },

  fixResized: function (tab, w, stopped) {
    if (!tab) return;
    tab.logWrap.scrollTop = tab.logWrap.scrollHeight;
    if (w > 0) {
      setStyle(tab.title, {maxWidth: w - 71});
    }
    if (stopped) {
      setStyle(tab.txt.autosize.helper, {width: getStyle(tab.txt, 'width', false)});
      tab.scroll && tab.scroll.update(false, true);
    }
  },
  activateTab: function (peer) {
    curFastChat.tabs[peer].box.focus();
  },

  updateUnreadTab: function (peer) {
    var tab = curFastChat.tabs[peer];
    if (!tab) return;
    val(tab.title, tab.name + (tab.unread ? ' <span class="
fc_tab_count">(' + tab.unread + ')</span>' : ''));
    val('fc_contact_unread' + peer, tab.unread ? ' <b>+' + tab.unread + '</b>' : '');
  },
  blinkTab: function (peer) {
    var tab = curFastChat.tabs[peer];
    if (tab.blinking || curFastChat.peer == peer) return;
    tab.blinking = true;
    clearTimeout(tab.blinkingTO);
    var wrap = tab.box.wrap, className = wrap.className, zIndex = Math.min(700, intval(getStyle(wrap, 'zIndex')));
    setStyle(wrap, {zIndex: 800});
    removeClass(wrap, 'rb_inactive');
    tab.blinkingTO = setTimeout(function () {
      delete tab.blinking;
      delete tab.blinkingTO;

      if (getStyle(wrap, 'zIndex') != 800) {
        return;
      }
      setStyle(wrap, {zIndex: zIndex});
      wrap.className = className;
    }, 2000);
  },

  send: function (peer) {
    var t = this, tab = curFastChat.tabs[peer];
    var msg = trim(val(tab.txt));
    if (!msg || tab.sending) {
      elfocus(tab.txt);
      return;
    }
    var msgId = --tab.sent,
        params = {
      act: 'a_send',
      to: peer,
      hash: tab.sendhash,
      msg: msg,
      from: 'fc'
    };
    tab.sending = true;
    ajax.post('al_im.php', params, {
      onDone: function(response) {
        clearTimeout(tab.saveDraftTO);
        FastChat.saveDraft(peer);

        if (response.version && intval(response.version) > curFastChat.version) {
           FastChat.updateVersion(response.version);
           return;
        }

        var row = ge('fc_msg' + msgId), realMsgId = response.msg_id, pos = indexOf(msgId, tab.newmsgs);
        if (!row) return;

        ++tab.msgscount;
        if (pos != -1) {
          tab.newmsgs.splice(pos, 1);
        }
        row.id = 'fc_msg' + realMsgId;

        tab.msgs[realMsgId] = [1, 1];
        // FastChat.updateOnline(peer, response.online);
      },
      onFail: function(error) {
        FastChat.error(peer, error || getLang('global_unknown_error'));

        elfocus(tab.txt);
        val(tab.txt, msg);
        tab.txt.autosize.update();

        var row = ge('fc_msg' + msgId);
        if (!row) return;
        row.appendChild(ce('span', {className: 'fc_msg_error', innerHTML: getLang('global_error')}));
        FastChat.scroll(peer);
        return true;
      },
      showProgress: function () {
        tab.sending = true;
        tab.sendProgressTO = setTimeout(function () {
          var row = ge('fc_msg' + msgId);
          if (!row) return;
          row.insertBefore(ce('span', {className: 'fc_msg_progress progress', id: 'fc_msg_progress' + msgId}), row.firstChild);
        }, 2000);
      },
      hideProgress: function () {
        tab.sending = false;
        clearTimeout(tab.sendProgressTO);
        re('fc_msg_progress' + msgId);
      }
    });
    re('fc_error' + peer);
    tab.sentmsgs.push(msgId);
    FastChat.addMsg(FastChat.prepareMsgData([peer, msgId, 1 | 2, FastChat.mkMsg(msg)]));

    val(tab.txt, '');
    delete curFastChat.myTypingEvents[peer];
    tab.txt.autosize.update(false, true);
    elfocus(tab.txt);
    FastChat.scroll(peer);
  },
  saveDraft: function (peer) {
    var tab = curFastChat.tabs[peer],
        txt = tab.txt;
    if (!txt || !tab) return;

    var data = {
      txt: trim(val(txt)) || '',
      medias: []
    };
    if (!data.txt.length) {
      data = false;
    }
    ls.set('im_draft' + vk.id + '_' + peer, data);
  },
  restoreDraft: function (peer) {
    var tab = curFastChat.tabs[peer],
        txt = tab.txt,
        draft = ls.get('im_draft' + vk.id + '_' + peer);

    if (!txt || !tab || !draft ||
        val(txt).length > draft.txt.length) {
      return false;
    }
    val(txt, draft.txt || '');
    return true;
  },
  error: function (peer, msg) {
    peer = peer || curFastChat.peer;
    var tab = curFastChat.tabs[peer];
    re('fc_error' + peer);
    tab.log.appendChild(ce('div', {id: 'fc_error' + peer, className: 'fc_msgs_error', innerHTML: msg || getLang('global_error')}));
    FastChat.scroll(peer);
  },
  scroll: function(peer) {
    peer = peer || curFastChat.peer;
    var tab = curFastChat.tabs[peer];
    if (!tab) return;
    tab.logWrap.scrollTop = tab.logWrap.scrollHeight;
    tab.scroll && tab.scroll.update(false, true);
  },
  mkdate: function(raw) {
    var result = new Date(raw * 1000),
        now_time = new Date(),
        pad = function(num) {return ((num + '').length < 2) ? ('0' + num) : num;};

    if (result.getDay() == now_time.getDay()) {
      return pad(result.getHours()) + ':' + pad(result.getMinutes()) + ':' + pad(result.getSeconds());
    }
    return pad(result.getDate()) + '.' + pad(result.getMonth()+1) + '.' + (result.getFullYear() + '').substr(2);
  },
  prepareMsgData: function (arr) {
    var peer = arr[0], flags = intval(arr[2]), from_id = flags & 2 ? curFastChat.me.id : (peer > 2e9 ? arr[5] : peer), date = intval(vkNow() / 1000), data = {
      id: arr[1],
      peer: peer,
      from_id: from_id,
      text: arr[3],
      out: flags & 2 ? true : false,
      unread: flags & 1 ? true : false,
      date: date,
      date_str: FastChat.mkdate(date)
    }, author, attFlags = arr[4], attText = '';

    if (attFlags) { // Media
      if (attFlags & 1) {
        attText += '<div class="
fc_msg_attachments_loading"></div>';
        setTimeout(FastChat.needMsgMedia.pbind(peer, arr[1]), 5);
      }
      if (attFlags & 6) {
        attText += rs(curFastChat.tpl.msg_fwd, {msg_id: arr[1], label: getLang(attFlags & 2 ? 'mail_im_fwd_msg' : 'mail_im_fwd_msgs')});
      }
      data.text += '<div class="
fc_msg_attachments" id="fc_msg_attachments' + data.id + '">' + attText + '</div>';
    }
    if (flags & 2) {
      author = curFastChat.me;
    } else if (peer > 2e9) {
      author = curFastChat.tabs[peer].data.members[from_id];
    } else {
      author = curFastChat.tabs[peer];
    }
    extend(data, {
      from_id: from_id,
      link: author.link,
      photo: author.photo,
      name: author.name,
      fname: peer > 2e9 ? author.fname || author.first_name : ''
    });
    if (arr[5]) {
      var att = arr[5].split(',');
    }
    return data;
  },
  needMsgMedia: function (peer, msgId) {
    if (msgId <= 0) return;

    FastChat.lcSend('needMedia', {msgId: msgId});
    curFastChat.needMedia[msgId] = [peer, setTimeout(FastChat.loadMsgMedia.pbind(peer, msgId), curNotifier.is_server ? 0 : irand(150, 250))];
  },
  loadMsgMedia: function (peer, msgId) {
    if (msgId <= 0 || curFastChat.gotMedia[msgId] !== undefined && curFastChat.gotMedia[msgId] !== 0) return;
    FastChat.lcSend('fetchingMedia', {msgId: msgId});
    curFastChat.gotMedia[msgId] = 0;

    ajax.post('al_im.php', {act: 'a_get_media', id: msgId, from: 'fc'}, {
      onDone: function (text) {
        FastChat.lcSend('gotMedia', {msgId: msgId, peer: peer, text: text});
        FastChat.gotMsgMedia(peer, msgId, text);
      }
    })
  },
  gotMsgMedia: function (peer, msgId, text) {
    val('fc_msg_attachments' + msgId, text);
    FastChat.scroll(peer);
    curFastChat.gotMedia[msgId] = [peer, text];

    if (curFastChat.needMedia[msgId] === undefined) return;
    clearTimeout(curFastChat.needMedia[msgId][1]);
    delete curFastChat.needMedia[msgId];
  },
  addMsg: function (data) {
    var t = this, peer = data.peer, tab = curFastChat.tabs[peer], log = tab.log, last = log.lastChild;
    if (last && last.className == 'fc_msgs_error') {
      last = last.previousSibling;
    }
    if (!last || !hasClass(last, 'fc_msgs_wrap') || last.getAttribute('from_id') != data.from_id || data.date - intval(last.getAttribute('date')) >= 300) {
      re('fc_log_empty' + peer);
      log.appendChild(last = se(rs(curFastChat.tpl.msgs, {
        from_id: data.from_id,
        link: data.link,
        photo: Notifier.fixPhoto(data.photo),
        name: data.from_id == curFastChat.me.id ? getLang('mail_im_thats_u') : data.name,
        classname: (data.out ? 'fc_msgs_out ' : '') + (data.unread ? 'fc_msgs_unread' : ''),
        date: data.date,
        date_str: data.date_str,
        msgs: data.link && data.fname ? rs(curFastChat.tpl.author_name, {link: data.link, fname: data.fname}) : ''
      })));
    } else if (!data.unread) {
      removeClass(last, 'fc_msgs_unread');
    }
    var msgs = geByClass1('fc_msgs', last, 'div');
    msgs.appendChild(se(rs(curFastChat.tpl.msg, {
      msg_id: data.id,
      classname: data.unread ? 'fc_msg_unread' : '',
      text: data.text
    })));
    if (vk.id != data.from_id) {
      delete curFastChat.typingEvents[peer];
      FastChat.updateTyping(peer, 1);
    }
    tab.scroll && tab.scroll.update();
  },
  showMsgFwd: function (msgId) {
    return !showBox('al_im.php', {act: 'a_show_forward_box', id: vk.id + '_' + msgId, from: 'mail'}, {stat: ['im.css']});
  },
  closeTab: function (peer) {
    var box = curFastChat.tabs[peer].box;
    box.close();
  },

  // mobile online
  tip: function(el, p) {
    if (hasClass(el.parentNode.parentNode, 'fc_tab_mobile') && (!cur._fcpromo || cur._fcpromo < 0) && !cur._fcdrag) {
      mobileOnlineTip(el, p);
    }
  },
  promo: function(el, ev) {
    if (hasClass(el.parentNode.parentNode, 'fc_tab_mobile') && cur._fcpromo >= 0) {
      mobilePromo();
      return cancelEvent(ev || window.event);
    }
  },
  promost: function(el) {
    cur._fcpromo = 1;
    if (el.tt && el.tt.hide) {
      el.tt.hide();
    }
  }

}

// Tiny Scrollbars start
function Scrollbar(obj, options) {
  this.obj = obj = ge(obj);
  this.options = options || {};
  this.clPref = options.prefix || '';

  setTimeout((function() {
    setStyle(obj, {
      overflow: 'hidden'
    });

    var size = getSize(obj), s = {
      marginLeft: (size[0] - 7)+'px',
      height: size[1] + 'px'
    };
    if (options.nomargin) {
      delete s.marginLeft;
      s.right = options.right || 0;
      s.left = options.left || 0;
    }
    this.scrollHeight = size[1];

    this.scrollbar = ce('div', {
      className: this.clPref + 'scrollbar_cont'
    });
    setStyle(this.scrollbar, s);

    this.inner = ce('div', {
      className: this.clPref + 'scrollbar_inner'
    });
    this.scrollbar.appendChild(this.inner);

    if (options.shadows) {
      obj.parentNode.insertBefore(this.topShadowDiv = ce('div', {
        className: this.clPref + 'scrollbar_top'
      }, {width: size[0]}), obj);
      obj.parentNode.insertBefore(this.bottomShadowDiv = ce('div', {
        className: this.clPref + 'scrollbar_bottom'
      }, {width: size[0]}), obj.nextSibling);
    }

    obj.parentNode.insertBefore(this.scrollbar, obj);

    this.destroyList = [];

    this.mouseMove = this._mouseMove.bind(this);
    this.mouseUp = this._mouseUp.bind(this);
    var self = this;
    function down(event) {
      if (self.moveY || checkEvent(event)) return;
      addEvent(window.document, 'mousemove', self.mouseMove);
      addEvent(window.document, 'mouseup', self.mouseUp);
      self.moveY = event.pageY - (self.inner.offsetTop || 0);

      window.document.body.style.cursor = 'pointer';
      addClass(self.inner, self.clPref + 'scrollbar_hovered');
      if (options.startDrag) {
        options.startDrag();
      }
      if (options.onHold) {
        options.onHold(true);
      }
      self.isDown = true;
      return cancelEvent(event);
    }
    this.mouseDown = down;
    function keydown(event) {
      switch ((event || window.event).keyCode) {
        case 40:  self.obj.scrollTop += 40; break;
        case 38:  self.obj.scrollTop -= 40; break;
        case 34:  self.obj.scrollTop += self.scrollHeight; break;
        case 33:  self.obj.scrollTop -= self.scrollHeight; break;
        default: return true;
      }
      self.update(true);
      return cancelEvent(event);
    }
    var wheel = this.wheel.bind(this);
    addEvent(obj, 'mousewheel', wheel);
    addEvent(obj, 'DOMMouseScroll', wheel);
    addEvent(this.scrollbar, 'mousewheel', wheel);
    addEvent(this.scrollbar, 'DOMMouseScroll', wheel);

    addEvent(this.scrollbar, 'mouseover', this.contOver.bind(this));
    addEvent(this.scrollbar, 'mouseout', this.contOut.bind(this));
    addEvent(this.scrollbar, 'mousedown', this.contDown.bind(this));

    if (browser.safari_mobile) {
      var touchstart = function(event) {
        cur.touchY  = event.touches[0].pageY;
      };
      var touchmove = function(event) {
        var touchY = event.touches[0].pageY;
        cur.touchDiff = cur.touchY - touchY;
        obj.scrollTop += cur.touchDiff;
        cur.touchY = touchY;
        if (obj.scrollTop > 0 && self.shown !== false) {
          self.update(true);
          return cancelEvent(event);
        }
      };
      var touchend = function() {
        cur.animateInt = setInterval(function() {
          cur.touchDiff = cur.touchDiff * 0.9;
          if (cur.touchDiff < 1 && cur.touchDiff > -1) {
            clearInterval(cur.animateInt);
          } else {
            obj.scrollTop += cur.touchDiff;
            self.update(true);
          }
        }, 0);
      };
      addEvent(obj, 'touchstart', touchstart);
      addEvent(obj, 'touchmove', touchmove);
      addEvent(obj, 'touchend', touchend);

      this.destroyList.push(function() {
        removeEvent(obj, 'touchstart', touchstart);
        removeEvent(obj, 'touchmove', touchmove);
        removeEvent(obj, 'touchend', touchend);
      });
    }

    addEvent(this.inner, 'mousedown', down);
    if (!options.nokeys) {
      addEvent(window, 'keydown', keydown);
    } else {
      this.onkeydown = keydown;
    }


    this.destroyList.push(function() {
      removeEvent(obj, 'mousewheel', wheel);
      removeEvent(obj, 'DOMMouseScroll', wheel);
      removeEvent(self.inner, 'mousedown', down);
      removeEvent(window, 'keydown', keydown);
    });

    if (this.contHeight() <= this.scrollHeight) {
      hide(this.bottomShadowDiv);
    } else {
      this.bottomShadow = true;
    }
    this.inited = true;
    this.update(true);

    if (!options.global) {
      cur.destroy.push(this.destroy.bind(this));
    }
  }).bind(this), 0);
}

Scrollbar.prototype.contOver = function() {
  this.isOut = false;
  if (this.shown) {
    addClass(this.scrollbar, 'scrollbar_c_overed');
  }
}
Scrollbar.prototype.contOut = function() {
  this.isOut = true;
  if (this.isDown) return;
  removeClass(this.scrollbar, 'scrollbar_c_overed');
}
Scrollbar.prototype.contDown = function(ev) {
  var y = ev.offsetY - this.innerHeight / 2 + 5;// - this.innerHeight;
  var scrH = this.scrollHeight - this.innerHeight;

  var newScroll = Math.floor((this.contHeight() - this.scrollHeight) * Math.min(1, y / scrH));
  this.obj.scrollTop = newScroll;
  this.update(true);
  this.mouseDown(ev);
}

Scrollbar.prototype._mouseMove = function(event) {
  this.obj.scrollTop = Math.floor((this.contHeight() - this.scrollHeight) * Math.min(1, (event.pageY - this.moveY) / (this.scrollHeight - this.innerHeight - 6)));
  this.update(true);
  return false;
}

Scrollbar.prototype._mouseUp = function(event) {
  this.moveY = false;
  this.isDown = false;
  if (this.isOut) {
    this.contOut();
  }
  removeEvent(window.document, 'mousemove', this.mouseMove);
  removeEvent(window.document, 'mouseup', this.mouseUp);
  window.document.body.style.cursor = 'default';
  removeClass(this.inner, this.clPref + 'scrollbar_hovered');
  if (this.options.stopDrag) {
    this.options.stopDrag();
  }
  if (this.options.onHold) {
    this.options.onHold(false);
  }
  return false;
}

Scrollbar.prototype.wheel = function(event) {
  if (this.disabled) {
    return;
  }
  if (!event) event = window.event;
  var delta = 0;
  if (event.wheelDeltaY || event.wheelDelta) {
    delta = (event.wheelDeltaY || event.wheelDelta) / 2;
  } else if (event.detail) {
    delta = -event.detail * 10
  }
  var stWas = this.obj.scrollTop;
  this.obj.scrollTop -= delta;

  if (this.options.onScroll) {
    this.options.onScroll(delta);
  }

  if (stWas != this.obj.scrollTop && this.shown !== false) {
    this.update(true);
    addClass(this.inner, this.clPref + 'scrollbar_hovered');
    clearTimeout(this.moveTimeout);
    this.moveTimeout = setTimeout((function() {
      removeClass(this.inner, this.clPref + 'scrollbar_hovered');
    }).bind(this), 300);
  }
  if (this.shown) {
    return false;
  }
}

Scrollbar.prototype.hide = function() {
  hide(this.topShadowDiv, this.bottomShadowDiv, this.scrollbar);
  this.hidden = true;
}
Scrollbar.prototype.show = function() {
  show(this.topShadowDiv, this.bottomShadowDiv, this.scrollbar);
  this.hidden = false;
}
Scrollbar.prototype.disable = function() {
  this.hide();
  this.scrollTop(0);
  this.disabled = true;
}
Scrollbar.prototype.enable = function() {
  this.show();
  this.update();
  this.disabled = false;
}

Scrollbar.prototype.scrollTop = function(top) {
  this.obj.scrollTop = parseInt(top);
  this.update(false, true);
}

Scrollbar.prototype.destroy = function(top) {
  each(this.destroyList, function (k, f) {f();});
}

Scrollbar.prototype.contHeight = function() {
  if (this.options.contHeight) {
    return this.options.contHeight;
  }
  if (this.contHashCash) {
    return this.contHashCash;
  }
  var nodes = this.obj.childNodes;
  var height = 0;
  var i = nodes.length;
  while (i--) {
    height += nodes[i].offsetHeight || 0;
  }
  this.contHashCash = height;
  return height;
}

Scrollbar.prototype.val = function(value) {
  if (value) {
    this.obj.scrollTop = value;
    this.update(true, true);
  }
  return this.obj.scrollTop;
}

Scrollbar.prototype.update = function(noChange, updateScroll) {
  if (!this.inited || this.hidden) {
    return;
  }
  if (!noChange) {
    this.contHashCash = false;
    if (this.moveY) {
      return true;
    }
  }
  if (updateScroll) {
    var size = getSize(this.obj);
    this.scrollHeight = size[1];
    setStyle(this.scrollbar, 'height', size[1]);
  }
  var height = this.contHeight();
  if (height <= this.scrollHeight) {
    hide(this.inner, this.bottomShadowDiv, this.topShadowDiv);
    setStyle(this.scrollbar, {pointerEvents: 'none'});
    this.topShadow = this.bottomShadow = false;
    this.shown = false;
    return;
  } else if (!this.shown) {
    show(this.inner);
    setStyle(this.scrollbar, {pointerEvents: 'auto'});
    this.shown = true;
  }

  var topScroll = this.val();

  if (this.options.scrollChange) {
    this.options.scrollChange(topScroll);
  }

  var progress = this.lastProgress = Math.min(1, topScroll / (height - this.scrollHeight));

  if (progress > 0 != (this.topShadow ? true : false)) {
    (this.topShadow ? hide : show)(this.topShadowDiv);
    this.topShadow = !this.topShadow;
  }
  if (progress < 1 != (this.bottomShadow ? true : false)) {
    (this.bottomShadow ? hide : show)(this.bottomShadowDiv);
    this.bottomShadow = !this.bottomShadow;
  }

  this.innerHeight = Math.max(40, Math.floor(this.scrollHeight * this.scrollHeight / height));
  this.inner.style.height = this.innerHeight + 'px';
  this.inner.style.marginTop = Math.floor((this.scrollHeight - this.innerHeight - 4) * progress + 2) + 'px';

  if (this.options.more && isFunction(this.options.more) && (this.options.contHeight || (height - this.obj.scrollTop < this.scrollHeight * 2))) {
    this.options.more();
  }
}
// Tiny Scrollbars end

function IframeLoader() {
  var iframe, doc, body, index, sources, aborted_sources;

  function iframeDoc(i) {
    try {
      if (i.contentDocument) return i.contentDocument;
      if (i.contentWindow && i.contentWindow.document) return i.contentWindow.document;
      return i.document;
    } catch (e) {};
    return false;
  }
  function getImgHtml(i) {
    if (doc && doc.body) return '<img id="
___img' + i + '" />';
    else return '<img class="
___img' + i + '" />';
  }
  function getImg(i) {
    if (doc && doc.body) return doc.getElementById('___img' + i);
    else return geByClass1('___img' + i, body);
  }
  function init() {
    iframe = utilsNode.appendChild(ce('iframe'));
    doc = iframeDoc(iframe);
    if (doc && doc.body) {
      body = doc.body;
    } else {
      body = utilsNode.appendChild(ce('div', {}, {display: 'none'}));
    }
    index = 0;
    sources = [];
  }
  function add(src, onLoad, that) {
    var i = index++;
    sources[i] = {src: src, onLoad: onLoad, that: that};
    body.appendChild(ce('div', {innerHTML: getImgHtml(i)}));
    var img = getImg(i);
    img.src = src;
    img.onload = function() {
      var obj = sources[i];
      obj.onLoad && obj.onLoad.call(obj.that || window, obj.src);
      delete sources[i];
      body.removeChild(getImg(i).parentNode);
    }
  }
  function abort() {
    re(iframe);
    aborted_sources = [];
    for (var k in sources) {
      aborted_sources.push(sources[k]);
    }
    init();
  }
  function repeat(need_redraw) {
    if (!aborted_sources) return [];
    var objs = [];
    for (var k in aborted_sources) {
      var obj = aborted_sources[k];
      add(obj.src, obj.onLoad, obj.that);
      objs.push(obj.that);
    }
    aborted_sources = null;
    if (need_redraw) {
      var redraw_data = [];
      each(objs, function() {
        redraw_data.push([this, this.src]);
        this.src = '';
        hide(this);
      });
      setTimeout(function(){
        each(redraw_data, function() {
          var img = this[0], src = this[1];
          img.src = src;
          show(img);
        });
      }, 10);
    }
    return objs;
  }

  init();

  return {
    add: add,
    abort: abort,
    repeat: repeat
  }
}

try{stManager.done('notifier.js');}catch(e){}
?>
Онлайн: 3
Реклама